@siyahi/ui 0.1.9-next.3 → 0.1.9-next.30
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 +170 -0
- package/README.md +94 -177
- package/THIRD_PARTY_NOTICES.md +11 -11
- package/agents/examples/accordion.tsx +39 -0
- package/agents/examples/alert.tsx +24 -0
- package/agents/examples/app-sidebar.tsx +40 -0
- package/agents/examples/avatar.tsx +24 -0
- package/agents/examples/badge.tsx +26 -0
- package/agents/examples/bar-list-labels.tsx +22 -0
- package/agents/examples/bar-list-sides.tsx +40 -0
- package/agents/examples/bar-list.tsx +24 -0
- package/agents/examples/breadcrumb.tsx +28 -0
- package/agents/examples/button-states.tsx +23 -0
- package/agents/examples/button.tsx +21 -0
- package/agents/examples/calendar.tsx +11 -0
- package/agents/examples/callout.tsx +33 -0
- package/agents/examples/card.tsx +36 -0
- package/agents/examples/change-bar.tsx +104 -0
- package/agents/examples/change-grid-money.tsx +36 -0
- package/agents/examples/change-grid.tsx +42 -0
- package/agents/examples/chart-area-log.tsx +45 -0
- package/agents/examples/chart-area.tsx +39 -0
- package/agents/examples/chart-bar-flows.tsx +32 -0
- package/agents/examples/chart-bar-hourly.tsx +28 -0
- package/agents/examples/chart-bar-losses.tsx +29 -0
- package/agents/examples/chart-bar-teams.tsx +72 -0
- package/agents/examples/chart-bar-years.tsx +40 -0
- package/agents/examples/chart-bar.tsx +56 -0
- package/agents/examples/chart-category-ticks.tsx +50 -0
- package/agents/examples/chart-line-events.tsx +47 -0
- package/agents/examples/chart-line-forecast.tsx +36 -0
- package/agents/examples/chart-line-log.tsx +39 -0
- package/agents/examples/chart-line-quiet.tsx +114 -0
- package/agents/examples/chart-line-timeline.tsx +41 -0
- package/agents/examples/chart-line.tsx +57 -0
- package/agents/examples/chart-panels.tsx +94 -0
- package/agents/examples/code-block.tsx +40 -0
- package/agents/examples/collapsible.tsx +18 -0
- package/agents/examples/command.tsx +29 -0
- package/agents/examples/context-menu.tsx +17 -0
- package/agents/examples/copy-button.tsx +8 -0
- package/agents/examples/data-bar.tsx +108 -0
- package/agents/examples/data-table-compact.tsx +96 -0
- package/agents/examples/data-table-filters.tsx +103 -0
- package/agents/examples/data-table.tsx +99 -0
- package/agents/examples/delta.tsx +18 -0
- package/agents/examples/dialog.tsx +28 -0
- package/agents/examples/distribution-bar-budget.tsx +30 -0
- package/agents/examples/distribution-bar-tug.tsx +74 -0
- package/agents/examples/drawdown-chart.tsx +35 -0
- package/agents/examples/dropdown-menu.tsx +31 -0
- package/agents/examples/field.tsx +108 -0
- package/agents/examples/formatters.tsx +42 -0
- package/agents/examples/heatmap-calendar-signed.tsx +67 -0
- package/agents/examples/heatmap-calendar.tsx +62 -0
- package/agents/examples/heatmap-status.tsx +61 -0
- package/agents/examples/heatmap-weekday.tsx +72 -0
- package/agents/examples/histogram-chart.tsx +28 -0
- package/agents/examples/input-states.tsx +23 -0
- package/agents/examples/input.tsx +17 -0
- package/agents/examples/local-time.tsx +14 -0
- package/agents/examples/metric-strip.tsx +46 -0
- package/agents/examples/overview-dashboard.tsx +180 -0
- package/agents/examples/page-hero.tsx +38 -0
- package/agents/examples/page-section.tsx +52 -0
- package/agents/examples/popover.tsx +23 -0
- package/agents/examples/price-chart.tsx +32 -0
- package/agents/examples/progress-tone.tsx +29 -0
- package/agents/examples/progress.tsx +10 -0
- package/agents/examples/range-position-table.tsx +101 -0
- package/agents/examples/range-position.tsx +55 -0
- package/agents/examples/scatter-chart-quadrants.tsx +63 -0
- package/agents/examples/scatter-chart-scales.tsx +46 -0
- package/agents/examples/scatter-chart.tsx +78 -0
- package/agents/examples/scroll-area.tsx +22 -0
- package/agents/examples/section-cards-select.tsx +28 -0
- package/agents/examples/section-cards-trend.tsx +32 -0
- package/agents/examples/section-cards.tsx +48 -0
- package/agents/examples/select.tsx +27 -0
- package/agents/examples/separator.tsx +13 -0
- package/agents/examples/sheet.tsx +15 -0
- package/agents/examples/skeleton.tsx +11 -0
- package/agents/examples/slider.tsx +5 -0
- package/agents/examples/slope-chart-market.tsx +33 -0
- package/agents/examples/slope-chart.tsx +26 -0
- package/agents/examples/stale-blocks.tsx +56 -0
- package/agents/examples/surfaces.tsx +116 -0
- package/agents/examples/table.tsx +60 -0
- package/agents/examples/tabs.tsx +16 -0
- package/agents/examples/target-rows.tsx +74 -0
- package/agents/examples/toggle-group.tsx +12 -0
- package/agents/examples/toggle.tsx +11 -0
- package/agents/examples/tooltip.tsx +13 -0
- package/agents/examples/treemap.tsx +93 -0
- package/agents/examples/waterfall-chart-stores.tsx +27 -0
- package/agents/examples/waterfall-chart.tsx +37 -0
- package/agents/manifest.json +1391 -0
- package/agents/report-rules.json +7 -0
- package/dist/components/registry/charts/area-chart-card.d.ts +7 -3
- package/dist/components/registry/charts/area-chart-card.js +56 -10
- package/dist/components/registry/charts/bar-chart-card.d.ts +1 -1
- package/dist/components/registry/charts/bar-chart-card.js +20 -22
- package/dist/components/registry/charts/line-chart-card.d.ts +3 -3
- package/dist/components/registry/charts/line-chart-card.js +44 -9
- package/dist/components/registry/dashboard-01/section-cards.d.ts +6 -0
- package/dist/components/registry/dashboard-01/section-cards.js +51 -35
- package/dist/components/registry/dashboardcn/bar-list.js +17 -10
- package/dist/components/registry/dashboardcn/distribution-bar.js +6 -4
- package/dist/components/registry/dashboardcn/kpi-row-card.d.ts +29 -0
- package/dist/components/registry/dashboardcn/kpi-row-card.js +81 -0
- package/dist/components/registry/dashboardcn/sparkline.d.ts +22 -0
- package/dist/components/registry/dashboardcn/sparkline.js +117 -0
- package/dist/components/registry/heatmap/calendar-heatmap.d.ts +11 -3
- package/dist/components/registry/heatmap/calendar-heatmap.js +164 -45
- package/dist/components/registry/heatmap/status-heatmap.d.ts +5 -3
- package/dist/components/registry/heatmap/status-heatmap.js +115 -54
- package/dist/components/registry/heatmap/weekday-heatmap.d.ts +9 -3
- package/dist/components/registry/heatmap/weekday-heatmap.js +83 -53
- package/dist/components/registry/niko-table/core/data-table-context.d.ts +18 -1
- package/dist/components/registry/niko-table/core/data-table-context.js +7 -1
- package/dist/components/registry/niko-table/core/data-table-grouped-row.js +3 -1
- package/dist/components/registry/niko-table/core/data-table-root.js +39 -4
- package/dist/components/registry/niko-table/core/data-table-structure.js +17 -12
- package/dist/components/registry/niko-table/core/data-table.d.ts +2 -2
- package/dist/components/registry/niko-table/core/data-table.js +5 -1
- package/dist/components/registry/niko-table/filters/table-faceted-filter.js +5 -5
- package/dist/components/registry/niko-table/filters/table-search-filter.js +1 -1
- package/dist/components/registry/niko-table/lib/flex-columns.d.ts +1 -1
- package/dist/components/registry/niko-table/lib/flex-columns.js +3 -3
- package/dist/components/registry/niko-table/lib/styles.d.ts +1 -1
- package/dist/components/registry/niko-table/lib/styles.js +3 -3
- package/dist/components/registry/niko-table/types/index.d.ts +9 -0
- package/dist/components/registry/niko-table/ui/table.js +2 -2
- package/dist/components/registry/sidebar-07/app-sidebar.d.ts +1 -1
- package/dist/components/registry/sidebar-07/app-sidebar.js +3 -1
- package/dist/components/registry/sidebar-07/nav-main.d.ts +2 -1
- package/dist/components/registry/sidebar-07/nav-main.js +16 -3
- package/dist/components/siyahi/callout.d.ts +17 -0
- package/dist/components/siyahi/callout.js +34 -0
- package/dist/components/siyahi/change-bar.d.ts +21 -0
- package/dist/components/siyahi/change-bar.js +35 -0
- package/dist/components/siyahi/change-grid.d.ts +31 -0
- package/dist/components/siyahi/change-grid.js +74 -0
- package/dist/components/siyahi/data-bar.d.ts +19 -0
- package/dist/components/siyahi/data-bar.js +29 -0
- package/dist/components/siyahi/data-table-compact-columns-toggle.d.ts +4 -0
- package/dist/components/siyahi/data-table-compact-columns-toggle.js +19 -0
- package/dist/components/siyahi/data-table-footnote.d.ts +12 -0
- package/dist/components/siyahi/data-table-footnote.js +35 -0
- package/dist/components/siyahi/default-sidebar-open.d.ts +8 -0
- package/dist/components/siyahi/default-sidebar-open.js +13 -0
- package/dist/components/siyahi/delta.d.ts +5 -1
- package/dist/components/siyahi/delta.js +14 -9
- package/dist/components/siyahi/drawdown-chart.d.ts +34 -0
- package/dist/components/siyahi/drawdown-chart.js +102 -0
- package/dist/components/siyahi/histogram-chart.d.ts +57 -0
- package/dist/components/siyahi/histogram-chart.js +134 -0
- package/dist/components/siyahi/page-hero.d.ts +23 -0
- package/dist/components/siyahi/page-hero.js +43 -0
- package/dist/components/siyahi/page-section.d.ts +4 -1
- package/dist/components/siyahi/page-section.js +6 -3
- package/dist/components/siyahi/page-toc.d.ts +20 -0
- package/dist/components/siyahi/page-toc.js +76 -0
- package/dist/components/siyahi/panel-grid.d.ts +24 -0
- package/dist/components/siyahi/panel-grid.js +37 -0
- package/dist/components/siyahi/quote.d.ts +12 -0
- package/dist/components/siyahi/quote.js +23 -0
- package/dist/components/siyahi/range-position.d.ts +24 -0
- package/dist/components/siyahi/range-position.js +105 -0
- package/dist/components/siyahi/scale-toggle.d.ts +32 -0
- package/dist/components/siyahi/scale-toggle.js +61 -0
- package/dist/components/siyahi/scatter-chart.d.ts +69 -0
- package/dist/components/siyahi/scatter-chart.js +395 -0
- package/dist/components/siyahi/siyahi-provider.d.ts +15 -2
- package/dist/components/siyahi/siyahi-provider.js +17 -4
- package/dist/components/siyahi/slope-chart.d.ts +35 -0
- package/dist/components/siyahi/slope-chart.js +159 -0
- package/dist/components/siyahi/stale-note.d.ts +9 -0
- package/dist/components/siyahi/stale-note.js +12 -0
- package/dist/components/siyahi/status-alert.d.ts +15 -0
- package/dist/components/siyahi/status-alert.js +29 -0
- package/dist/components/siyahi/status-badge.d.ts +15 -0
- package/dist/components/siyahi/status-badge.js +24 -0
- package/dist/components/siyahi/target-rows.d.ts +34 -0
- package/dist/components/siyahi/target-rows.js +97 -0
- package/dist/components/siyahi/tone-progress.d.ts +1 -1
- package/dist/components/siyahi/tone-progress.js +1 -1
- package/dist/components/siyahi/treemap.d.ts +8 -5
- package/dist/components/siyahi/treemap.js +80 -34
- package/dist/components/siyahi/waterfall-chart.d.ts +51 -0
- package/dist/components/siyahi/waterfall-chart.js +147 -0
- package/dist/components/ui/accordion.js +5 -5
- package/dist/components/ui/alert.js +2 -2
- package/dist/components/ui/badge.js +1 -1
- package/dist/components/ui/breadcrumb.js +2 -2
- package/dist/components/ui/button.js +10 -10
- package/dist/components/ui/calendar.js +1 -1
- package/dist/components/ui/card.js +4 -4
- package/dist/components/ui/chart.js +1 -1
- package/dist/components/ui/code-block.js +13 -12
- package/dist/components/ui/command.js +7 -6
- package/dist/components/ui/context-menu.js +7 -7
- package/dist/components/ui/dialog.js +5 -5
- package/dist/components/ui/dropdown-menu.js +8 -8
- package/dist/components/ui/field.d.ts +26 -0
- package/dist/components/ui/field.js +109 -0
- package/dist/components/ui/input-group.js +11 -11
- package/dist/components/ui/input.js +1 -1
- package/dist/components/ui/label.d.ts +5 -0
- package/dist/components/ui/label.js +13 -0
- package/dist/components/ui/popover.js +3 -3
- package/dist/components/ui/progress.js +1 -1
- package/dist/components/ui/select.js +6 -6
- package/dist/components/ui/sheet.js +5 -5
- package/dist/components/ui/sidebar.js +17 -18
- package/dist/components/ui/skeleton.js +1 -1
- package/dist/components/ui/slider.js +2 -2
- package/dist/components/ui/table.js +2 -2
- package/dist/components/ui/tabs.js +2 -2
- package/dist/components/ui/textarea.js +1 -1
- package/dist/components/ui/toggle-group.js +2 -2
- package/dist/components/ui/toggle.js +4 -4
- package/dist/components/ui/tooltip.js +2 -2
- package/dist/index.d.ts +35 -8
- package/dist/index.js +30 -4
- package/dist/lib/chart-frame.d.ts +51 -7
- package/dist/lib/chart-frame.js +390 -133
- package/dist/lib/chart-marks.d.ts +83 -0
- package/dist/lib/chart-marks.js +216 -0
- package/dist/lib/chart-series.d.ts +42 -10
- package/dist/lib/compact-columns.d.ts +19 -0
- package/dist/lib/compact-columns.js +55 -0
- package/dist/lib/fit-text.d.ts +6 -0
- package/dist/lib/fit-text.js +11 -0
- package/dist/lib/format.d.ts +8 -3
- package/dist/lib/format.js +55 -17
- package/dist/lib/heat.d.ts +7 -0
- package/dist/lib/heat.js +14 -0
- package/dist/lib/heatmap-navigation.d.ts +8 -0
- package/dist/lib/heatmap-navigation.js +49 -0
- package/dist/lib/scatter.d.ts +88 -0
- package/dist/lib/scatter.js +179 -0
- package/dist/lib/ticks.d.ts +27 -0
- package/dist/lib/ticks.js +67 -0
- package/dist/lib/tone.d.ts +36 -0
- package/dist/lib/tone.js +35 -0
- package/dist/lib/treemap.d.ts +0 -5
- package/dist/lib/treemap.js +1 -11
- package/dist/lib/use-element-width.d.ts +5 -0
- package/dist/lib/use-element-width.js +15 -0
- package/package.json +5 -1
- package/registry.lock.json +16 -0
- package/report.css +63 -47
- package/styles/siyahi.css +708 -55
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
import { useFormat } from "../../lib/format.js";
|
|
2
2
|
import { cn } from "cn";
|
|
3
|
+
import { MinusIcon, TrendingDownIcon, TrendingUpIcon } from "lucide-react";
|
|
3
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
5
|
//#region src/components/siyahi/delta.tsx
|
|
5
|
-
|
|
6
|
+
const MINUS = "−";
|
|
7
|
+
/**
|
|
8
|
+
* A signed change: the figure in the surrounding text colour with its sign, and a trend icon in --positive or
|
|
9
|
+
* --negative. The sign and the icon's shape carry the direction, so colour is never the only cue, and a column of
|
|
10
|
+
* changes reads as figures rather than a wall of red and green.
|
|
11
|
+
*/
|
|
6
12
|
function Delta({ value, format = "pct", className }) {
|
|
7
13
|
const f = useFormat();
|
|
8
14
|
const size = format === "money" ? f.money(Math.abs(value)) : f.pct(Math.abs(value));
|
|
9
|
-
const direction = value > 0 ? "up" : value < 0 ? "down" :
|
|
15
|
+
const direction = value > 0 ? "up" : value < 0 ? "down" : "none";
|
|
16
|
+
const Icon = direction === "up" ? TrendingUpIcon : direction === "down" ? TrendingDownIcon : MinusIcon;
|
|
10
17
|
return /* @__PURE__ */ jsxs("span", {
|
|
11
18
|
"data-slot": "delta",
|
|
12
|
-
|
|
19
|
+
"data-direction": direction,
|
|
20
|
+
className: cn("inline-flex items-baseline gap-1 whitespace-nowrap", className),
|
|
13
21
|
children: [
|
|
14
|
-
|
|
22
|
+
/* @__PURE__ */ jsx(Icon, {
|
|
15
23
|
"aria-hidden": "true",
|
|
16
|
-
|
|
17
|
-
}),
|
|
18
|
-
direction && /* @__PURE__ */ jsx("span", {
|
|
19
|
-
className: "sr-only",
|
|
20
|
-
children: direction
|
|
24
|
+
className: cn("size-3.5 shrink-0 self-center", 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
|
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NumericKey, SeriesChartProps } from "../../lib/chart-series.js";
|
|
2
|
+
//#region src/components/siyahi/drawdown-chart.d.ts
|
|
3
|
+
/** One point as drawn: how far the level sits below the highest it had reached, and that high. */
|
|
4
|
+
export interface DrawdownPoint<X> {
|
|
5
|
+
x: X;
|
|
6
|
+
/** The fall from the peak as a fraction, 0 at a new high and −0.25 a quarter below it; null when the level is unknown. */
|
|
7
|
+
drawdown: number | null;
|
|
8
|
+
/** The highest level so far. */
|
|
9
|
+
peak: number;
|
|
10
|
+
level: number | null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Walks the levels in order, keeping the highest so far. A level that is not a finite number has no drawdown and
|
|
14
|
+
* leaves the peak as it was; the first level is the first peak.
|
|
15
|
+
*/
|
|
16
|
+
export declare function drawdowns<X>(levels: readonly {
|
|
17
|
+
x: X;
|
|
18
|
+
level: number | null;
|
|
19
|
+
}[]): DrawdownPoint<X>[];
|
|
20
|
+
export interface DrawdownChartCardProps<T extends object, K extends keyof T> extends Pick<SeriesChartProps<T, K>, "title" | "description" | "action" | "data" | "xKey" | "height" | "xFormatter" | "xTooltipFormatter" | "xTicks" | "placeholder" | "footer"> {
|
|
21
|
+
/** The field holding the level, such as the day's cash balance or a fund's value. */
|
|
22
|
+
levelKey: NumericKey<T>;
|
|
23
|
+
/** Writes the level and the peak in the tooltip. Defaults to useFormat().money. */
|
|
24
|
+
levelFormatter?: (value: number) => string;
|
|
25
|
+
/** Names the fall in the legend and tooltip. */
|
|
26
|
+
label?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* How far a level sits below the highest it had reached, over time: a fund's or a cash balance's fall from its
|
|
30
|
+
* peak. The area hangs from zero in the loss colour, so the deepest and longest falls read at once, and a dashed
|
|
31
|
+
* line marks the deepest one. Levels must be above zero: a fall is a share of the peak.
|
|
32
|
+
*/
|
|
33
|
+
export declare function DrawdownChartCard<T extends object, K extends keyof T>({ data, xKey, levelKey, levelFormatter, label, xFormatter, xTooltipFormatter, xTicks, ...props }: DrawdownChartCardProps<T, K>): import("react").JSX.Element;
|
|
34
|
+
//#endregion
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { linearTicks } from "../../lib/ticks.js";
|
|
2
|
+
import { ChartFrame } from "../../lib/chart-frame.js";
|
|
3
|
+
import { useFormat } from "../../lib/format.js";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { useMemo } from "react";
|
|
6
|
+
import { Area, AreaChart } from "recharts";
|
|
7
|
+
//#region src/components/siyahi/drawdown-chart.tsx
|
|
8
|
+
/**
|
|
9
|
+
* Walks the levels in order, keeping the highest so far. A level that is not a finite number has no drawdown and
|
|
10
|
+
* leaves the peak as it was; the first level is the first peak.
|
|
11
|
+
*/
|
|
12
|
+
function drawdowns(levels) {
|
|
13
|
+
let peak = Number.NEGATIVE_INFINITY;
|
|
14
|
+
return levels.map(({ x, level }) => {
|
|
15
|
+
if (level === null || !Number.isFinite(level)) return {
|
|
16
|
+
x,
|
|
17
|
+
level: null,
|
|
18
|
+
drawdown: null,
|
|
19
|
+
peak
|
|
20
|
+
};
|
|
21
|
+
peak = Math.max(peak, level);
|
|
22
|
+
return {
|
|
23
|
+
x,
|
|
24
|
+
level,
|
|
25
|
+
peak,
|
|
26
|
+
drawdown: peak > 0 ? level / peak - 1 : 0
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* How far a level sits below the highest it had reached, over time: a fund's or a cash balance's fall from its
|
|
32
|
+
* peak. The area hangs from zero in the loss colour, so the deepest and longest falls read at once, and a dashed
|
|
33
|
+
* line marks the deepest one. Levels must be above zero: a fall is a share of the peak.
|
|
34
|
+
*/
|
|
35
|
+
function DrawdownChartCard({ data, xKey, levelKey, levelFormatter, label = "Below peak", xFormatter = String, xTooltipFormatter, xTicks, ...props }) {
|
|
36
|
+
const f = useFormat();
|
|
37
|
+
const formatLevel = levelFormatter ?? ((value) => f.money(value));
|
|
38
|
+
const points = useMemo(() => drawdowns(data.map((row) => ({
|
|
39
|
+
x: row[xKey],
|
|
40
|
+
level: row[levelKey]
|
|
41
|
+
}))), [
|
|
42
|
+
data,
|
|
43
|
+
xKey,
|
|
44
|
+
levelKey
|
|
45
|
+
]);
|
|
46
|
+
const fall = (value) => value < 0 ? `−${f.pct(-value)}` : f.pct(0);
|
|
47
|
+
const deepest = Math.min(0, ...points.flatMap(({ drawdown }) => drawdown ?? []));
|
|
48
|
+
const ticks = linearTicks(Math.min(deepest, -.01), 0);
|
|
49
|
+
return /* @__PURE__ */ jsx(ChartFrame, {
|
|
50
|
+
chart: AreaChart,
|
|
51
|
+
...props,
|
|
52
|
+
data: points,
|
|
53
|
+
xKey: "x",
|
|
54
|
+
xFormatter: (x) => xFormatter(x),
|
|
55
|
+
xTooltipFormatter: (x) => (xTooltipFormatter ?? xFormatter)(x),
|
|
56
|
+
xTicks,
|
|
57
|
+
series: [{
|
|
58
|
+
key: "drawdown",
|
|
59
|
+
label,
|
|
60
|
+
color: "var(--negative)"
|
|
61
|
+
}],
|
|
62
|
+
yDomain: [Math.min(deepest, ticks[0] ?? deepest), 0],
|
|
63
|
+
yTicks: ticks,
|
|
64
|
+
yTickFormatter: (value) => fall(value),
|
|
65
|
+
referenceLines: deepest < 0 ? [{
|
|
66
|
+
y: deepest,
|
|
67
|
+
label: `Deepest ${fall(deepest)}`,
|
|
68
|
+
labelPosition: "insideBottomRight"
|
|
69
|
+
}] : [],
|
|
70
|
+
tooltipRow: (_value, _name, item) => {
|
|
71
|
+
const point = item.payload;
|
|
72
|
+
return (point.drawdown === null ? [[label, "—"]] : [
|
|
73
|
+
[label, fall(point.drawdown)],
|
|
74
|
+
["Level", formatLevel(point.level ?? 0)],
|
|
75
|
+
["Peak so far", formatLevel(point.peak)]
|
|
76
|
+
]).map(([name, figure]) => /* @__PURE__ */ jsxs("div", {
|
|
77
|
+
className: "flex w-full items-center gap-2",
|
|
78
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
79
|
+
className: "text-muted-foreground",
|
|
80
|
+
children: name
|
|
81
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
82
|
+
className: "ml-auto font-mono font-medium text-foreground tabular-nums",
|
|
83
|
+
children: figure
|
|
84
|
+
})]
|
|
85
|
+
}, name));
|
|
86
|
+
},
|
|
87
|
+
children: /* @__PURE__ */ jsx(Area, {
|
|
88
|
+
dataKey: "drawdown",
|
|
89
|
+
name: label,
|
|
90
|
+
type: "linear",
|
|
91
|
+
baseValue: 0,
|
|
92
|
+
stroke: "var(--negative)",
|
|
93
|
+
strokeWidth: 2,
|
|
94
|
+
fill: "color-mix(in oklab, var(--negative) 25%, var(--card))",
|
|
95
|
+
fillOpacity: 1,
|
|
96
|
+
dot: false,
|
|
97
|
+
connectNulls: false
|
|
98
|
+
})
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
export { DrawdownChartCard, drawdowns };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ChartReferenceLine, SeriesChartProps } from "../../lib/chart-series.js";
|
|
2
|
+
//#region src/components/siyahi/histogram-chart.d.ts
|
|
3
|
+
/** One bucket: the values from `from` up to, not including, `to`; the last bucket takes its upper edge too. */
|
|
4
|
+
export interface HistogramBin {
|
|
5
|
+
key: string;
|
|
6
|
+
from: number;
|
|
7
|
+
to: number;
|
|
8
|
+
count: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Buckets of one round width (1, 2, 2.5 or 5 × 10ⁿ) covering the values, about `target` of them. Values that are not
|
|
12
|
+
* finite numbers are left out.
|
|
13
|
+
*/
|
|
14
|
+
export declare function histogram(values: readonly number[], target?: number): HistogramBin[];
|
|
15
|
+
/**
|
|
16
|
+
* The bucket a value falls in, read against the buckets' own edges: dividing by the width instead put 0.3 in the
|
|
17
|
+
* 0.2–0.3 bucket, as 0.3 / 0.1 is 2.9999999999999996. A value past the last edge belongs to the last bucket.
|
|
18
|
+
*/
|
|
19
|
+
export declare function bucketOf(bins: readonly HistogramBin[], value: number): number;
|
|
20
|
+
/** The value below which `share` of the values fall, interpolating between the two nearest, as spreadsheets do. */
|
|
21
|
+
export declare function percentile(values: readonly number[], share: number): number | null;
|
|
22
|
+
declare const MARKS: {
|
|
23
|
+
readonly median: 0.5;
|
|
24
|
+
readonly p90: 0.9;
|
|
25
|
+
readonly p95: 0.95;
|
|
26
|
+
readonly p99: 0.99;
|
|
27
|
+
};
|
|
28
|
+
export type HistogramMark = keyof typeof MARKS;
|
|
29
|
+
/**
|
|
30
|
+
* One line per bucket that holds a mark, named by its marks: marks that share a bucket share a line and a name,
|
|
31
|
+
* "median, p90", rather than drawing over each other.
|
|
32
|
+
*/
|
|
33
|
+
export declare function markLines(bins: readonly HistogramBin[], marked: readonly {
|
|
34
|
+
mark: HistogramMark;
|
|
35
|
+
value: number;
|
|
36
|
+
}[]): ChartReferenceLine[];
|
|
37
|
+
export interface HistogramChartCardProps extends Pick<SeriesChartProps<HistogramBin, "key">, "title" | "description" | "action" | "height" | "xLabel" | "yLabel" | "placeholder" | "footer"> {
|
|
38
|
+
/** The raw values, one per item, such as each request's response time. */
|
|
39
|
+
values: readonly number[];
|
|
40
|
+
/** About how many buckets; the width is rounded, so the count varies a little. */
|
|
41
|
+
bins?: number;
|
|
42
|
+
/** Writes the bucket edges and the marks, such as (v) => `${v} ms`. Defaults to the locale's numbers. */
|
|
43
|
+
valueFormatter?: (value: number) => string;
|
|
44
|
+
/** The bucket edges under the axis, when they should be shorter than valueFormatter: 1,000 against 1,000 ms. */
|
|
45
|
+
tickFormatter?: (value: number) => string;
|
|
46
|
+
/** What is counted, in the legend and tooltip, such as "Requests". */
|
|
47
|
+
countLabel?: string;
|
|
48
|
+
/** Percentiles to mark with a line at their bucket; their exact values are written under the chart. */
|
|
49
|
+
marks?: readonly HistogramMark[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* How values spread: counts in buckets of one round width, such as response times by 50 ms, with the median or a
|
|
53
|
+
* high percentile marked. The page gives the raw values; the card buckets them.
|
|
54
|
+
*/
|
|
55
|
+
export declare function HistogramChartCard({ values, bins: target, valueFormatter, tickFormatter, countLabel, marks, ...props }: HistogramChartCardProps): import("react").JSX.Element;
|
|
56
|
+
//#endregion
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { useLocale } from "./siyahi-provider.js";
|
|
2
|
+
import { linearTicks } from "../../lib/ticks.js";
|
|
3
|
+
import { ChartFrame, barFill } from "../../lib/chart-frame.js";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { useMemo } from "react";
|
|
6
|
+
import { Bar, BarChart, Cell } from "recharts";
|
|
7
|
+
//#region src/components/siyahi/histogram-chart.tsx
|
|
8
|
+
/**
|
|
9
|
+
* Buckets of one round width (1, 2, 2.5 or 5 × 10ⁿ) covering the values, about `target` of them. Values that are not
|
|
10
|
+
* finite numbers are left out.
|
|
11
|
+
*/
|
|
12
|
+
function histogram(values, target = 12) {
|
|
13
|
+
const known = values.filter(Number.isFinite);
|
|
14
|
+
if (known.length === 0) return [];
|
|
15
|
+
let low = known[0];
|
|
16
|
+
let high = low;
|
|
17
|
+
for (const value of known) {
|
|
18
|
+
low = Math.min(low, value);
|
|
19
|
+
high = Math.max(high, value);
|
|
20
|
+
}
|
|
21
|
+
const edges = linearTicks(low, high, target + 1);
|
|
22
|
+
if (edges.length < 2) return [{
|
|
23
|
+
key: "0",
|
|
24
|
+
from: low,
|
|
25
|
+
to: high,
|
|
26
|
+
count: known.length
|
|
27
|
+
}];
|
|
28
|
+
const bins = edges.slice(0, -1).map((from, i) => ({
|
|
29
|
+
key: String(i),
|
|
30
|
+
from,
|
|
31
|
+
to: edges[i + 1],
|
|
32
|
+
count: 0
|
|
33
|
+
}));
|
|
34
|
+
for (const value of known) bins[bucketOf(bins, value)].count += 1;
|
|
35
|
+
return bins;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The bucket a value falls in, read against the buckets' own edges: dividing by the width instead put 0.3 in the
|
|
39
|
+
* 0.2–0.3 bucket, as 0.3 / 0.1 is 2.9999999999999996. A value past the last edge belongs to the last bucket.
|
|
40
|
+
*/
|
|
41
|
+
function bucketOf(bins, value) {
|
|
42
|
+
const index = bins.findIndex((bin) => value < bin.to);
|
|
43
|
+
return index === -1 ? bins.length - 1 : index;
|
|
44
|
+
}
|
|
45
|
+
/** The value below which `share` of the values fall, interpolating between the two nearest, as spreadsheets do. */
|
|
46
|
+
function percentile(values, share) {
|
|
47
|
+
const sorted = values.filter(Number.isFinite).toSorted((a, b) => a - b);
|
|
48
|
+
if (sorted.length === 0) return null;
|
|
49
|
+
const at = (sorted.length - 1) * share;
|
|
50
|
+
const low = Math.floor(at);
|
|
51
|
+
return sorted[low] + (sorted[Math.ceil(at)] - sorted[low]) * (at - low);
|
|
52
|
+
}
|
|
53
|
+
const MARKS = {
|
|
54
|
+
median: .5,
|
|
55
|
+
p90: .9,
|
|
56
|
+
p95: .95,
|
|
57
|
+
p99: .99
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* One line per bucket that holds a mark, named by its marks: marks that share a bucket share a line and a name,
|
|
61
|
+
* "median, p90", rather than drawing over each other.
|
|
62
|
+
*/
|
|
63
|
+
function markLines(bins, marked) {
|
|
64
|
+
return [...Map.groupBy(marked, ({ value }) => bucketOf(bins, value))].flatMap(([index, group]) => {
|
|
65
|
+
const bin = bins[index];
|
|
66
|
+
if (!bin) return [];
|
|
67
|
+
const right = index >= bins.length / 2;
|
|
68
|
+
const label = group.map(({ mark }) => mark).join(", ");
|
|
69
|
+
return [{
|
|
70
|
+
x: bin.key,
|
|
71
|
+
label,
|
|
72
|
+
emphasis: "strong",
|
|
73
|
+
labelPosition: right ? "insideTopRight" : "insideTopLeft"
|
|
74
|
+
}];
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const NO_MARKS = [];
|
|
78
|
+
/**
|
|
79
|
+
* How values spread: counts in buckets of one round width, such as response times by 50 ms, with the median or a
|
|
80
|
+
* high percentile marked. The page gives the raw values; the card buckets them.
|
|
81
|
+
*/
|
|
82
|
+
function HistogramChartCard({ values, bins: target = 12, valueFormatter, tickFormatter, countLabel = "Count", marks = NO_MARKS, ...props }) {
|
|
83
|
+
const { numberLocale } = useLocale();
|
|
84
|
+
const format = valueFormatter ?? new Intl.NumberFormat(numberLocale).format;
|
|
85
|
+
const bins = useMemo(() => histogram(values, target), [values, target]);
|
|
86
|
+
const range = (bin) => `${format(bin.from)}–${format(bin.to)}`;
|
|
87
|
+
const names = new Map(bins.map((bin) => [bin.key, bin]));
|
|
88
|
+
const marked = marks.flatMap((mark) => {
|
|
89
|
+
const value = percentile(values, MARKS[mark]);
|
|
90
|
+
return value === null ? [] : [{
|
|
91
|
+
mark,
|
|
92
|
+
value
|
|
93
|
+
}];
|
|
94
|
+
});
|
|
95
|
+
const lines = markLines(bins, marked);
|
|
96
|
+
return /* @__PURE__ */ jsx(ChartFrame, {
|
|
97
|
+
chart: BarChart,
|
|
98
|
+
...props,
|
|
99
|
+
data: bins,
|
|
100
|
+
xKey: "key",
|
|
101
|
+
xFormatter: (key) => {
|
|
102
|
+
const bin = names.get(key);
|
|
103
|
+
return bin ? (tickFormatter ?? format)(bin.from) : key;
|
|
104
|
+
},
|
|
105
|
+
xTooltipFormatter: (key) => {
|
|
106
|
+
const bin = names.get(key);
|
|
107
|
+
return bin ? range(bin) : key;
|
|
108
|
+
},
|
|
109
|
+
series: [{
|
|
110
|
+
key: "count",
|
|
111
|
+
label: countLabel,
|
|
112
|
+
color: "var(--chart-1)"
|
|
113
|
+
}],
|
|
114
|
+
referenceLines: lines,
|
|
115
|
+
footer: marked.length > 0 || props.footer ? /* @__PURE__ */ jsxs(Fragment, { children: [marked.length > 0 && /* @__PURE__ */ jsx("p", {
|
|
116
|
+
"data-slot": "histogram-marks",
|
|
117
|
+
className: "tabular-nums",
|
|
118
|
+
children: marked.map(({ mark, value }) => `${mark} ${format(value)}`).join(" · ")
|
|
119
|
+
}), props.footer] }) : void 0,
|
|
120
|
+
children: /* @__PURE__ */ jsx(Bar, {
|
|
121
|
+
dataKey: "count",
|
|
122
|
+
name: countLabel,
|
|
123
|
+
fill: "var(--chart-1)",
|
|
124
|
+
isAnimationActive: false,
|
|
125
|
+
children: bins.map((bin) => /* @__PURE__ */ jsx(Cell, {
|
|
126
|
+
fill: barFill("var(--chart-1)"),
|
|
127
|
+
stroke: "var(--chart-1)",
|
|
128
|
+
strokeWidth: 1
|
|
129
|
+
}, bin.key))
|
|
130
|
+
})
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
export { HistogramChartCard, bucketOf, histogram, markLines, percentile };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/components/siyahi/page-hero.d.ts
|
|
3
|
+
export interface PageHeroProps {
|
|
4
|
+
/** A short line above the title in the mono face and the link colour, such as the app or a section name. */
|
|
5
|
+
eyebrow?: ReactNode;
|
|
6
|
+
/** The page's h1. */
|
|
7
|
+
title: ReactNode;
|
|
8
|
+
/** What the page is, in a sentence, in the muted colour. */
|
|
9
|
+
lead?: ReactNode;
|
|
10
|
+
/** Scope, sources or what is left out, run on after the lead in the same muted line. */
|
|
11
|
+
detail?: ReactNode;
|
|
12
|
+
/** The one or two places a reader goes next, usually a primary Button and an outline one, at the right. */
|
|
13
|
+
actions?: ReactNode;
|
|
14
|
+
/** A small mark before the eyebrow, such as the product's logo as an SVG that paints with currentColor. */
|
|
15
|
+
mark?: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The masthead at the top of a page: a small mark and eyebrow, the title, one muted line saying what the page is,
|
|
19
|
+
* the actions at the right, and a hairline under it all. One per page, above everything else; a section uses
|
|
20
|
+
* PageSection.
|
|
21
|
+
*/
|
|
22
|
+
export declare function PageHero({ eyebrow, title, lead, detail, actions, mark }: PageHeroProps): import("react").JSX.Element;
|
|
23
|
+
//#endregion
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
//#region src/components/siyahi/page-hero.tsx
|
|
3
|
+
/**
|
|
4
|
+
* The masthead at the top of a page: a small mark and eyebrow, the title, one muted line saying what the page is,
|
|
5
|
+
* the actions at the right, and a hairline under it all. One per page, above everything else; a section uses
|
|
6
|
+
* PageSection.
|
|
7
|
+
*/
|
|
8
|
+
function PageHero({ eyebrow, title, lead, detail, actions, mark }) {
|
|
9
|
+
return /* @__PURE__ */ jsxs("header", {
|
|
10
|
+
"data-slot": "page-hero",
|
|
11
|
+
className: "flex flex-wrap items-end justify-between gap-x-6 gap-y-3 border-b pb-4",
|
|
12
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
13
|
+
className: "flex min-w-0 flex-col gap-1",
|
|
14
|
+
children: [
|
|
15
|
+
(mark || eyebrow) && /* @__PURE__ */ jsxs("p", {
|
|
16
|
+
className: "flex items-center gap-1.5 font-mono text-xs text-link",
|
|
17
|
+
children: [mark && /* @__PURE__ */ jsx("span", {
|
|
18
|
+
"aria-hidden": true,
|
|
19
|
+
className: "size-3.5 shrink-0 [&>svg]:size-full",
|
|
20
|
+
children: mark
|
|
21
|
+
}), eyebrow]
|
|
22
|
+
}),
|
|
23
|
+
/* @__PURE__ */ jsx("h1", {
|
|
24
|
+
className: "text-3xl font-semibold tracking-tight text-balance",
|
|
25
|
+
children: title
|
|
26
|
+
}),
|
|
27
|
+
(lead || detail) && /* @__PURE__ */ jsxs("div", {
|
|
28
|
+
className: "text-sm text-pretty text-muted-foreground",
|
|
29
|
+
children: [
|
|
30
|
+
lead,
|
|
31
|
+
lead && detail && " ",
|
|
32
|
+
detail
|
|
33
|
+
]
|
|
34
|
+
})
|
|
35
|
+
]
|
|
36
|
+
}), actions && /* @__PURE__ */ jsx("div", {
|
|
37
|
+
className: "flex flex-wrap gap-2",
|
|
38
|
+
children: actions
|
|
39
|
+
})]
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { PageHero };
|
|
@@ -12,7 +12,10 @@ export interface PageSectionProps {
|
|
|
12
12
|
action?: ReactNode;
|
|
13
13
|
children?: ReactNode;
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* One part of a long page: a rule above, then the heading and lead on the canvas, then its content. Spaced for
|
|
17
|
+
* density, 24px under the rule and 16px to the content, so sections stacked 24px apart sit 24px either side of it.
|
|
18
|
+
*/
|
|
16
19
|
export declare function PageSection({ id, eyebrow, title, lead, action, children }: PageSectionProps): import("react").JSX.Element;
|
|
17
20
|
/** A heading inside a PageSection, one level under its title. */
|
|
18
21
|
export declare function PageSubheading({ children }: {
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
//#region src/components/siyahi/page-section.tsx
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* One part of a long page: a rule above, then the heading and lead on the canvas, then its content. Spaced for
|
|
5
|
+
* density, 24px under the rule and 16px to the content, so sections stacked 24px apart sit 24px either side of it.
|
|
6
|
+
*/
|
|
4
7
|
function PageSection({ id, eyebrow, title, lead, action, children }) {
|
|
5
8
|
return /* @__PURE__ */ jsxs("section", {
|
|
6
9
|
id,
|
|
7
10
|
"data-slot": "page-section",
|
|
8
|
-
className: "flex scroll-mt-6 flex-col gap-
|
|
11
|
+
className: "flex scroll-mt-6 flex-col gap-4 border-t pt-6",
|
|
9
12
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
10
13
|
className: "flex flex-wrap items-end justify-between gap-4",
|
|
11
14
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
@@ -16,7 +19,7 @@ function PageSection({ id, eyebrow, title, lead, action, children }) {
|
|
|
16
19
|
children: eyebrow
|
|
17
20
|
}),
|
|
18
21
|
/* @__PURE__ */ jsx("h2", {
|
|
19
|
-
className: "text-
|
|
22
|
+
className: "text-xl font-semibold tracking-tight",
|
|
20
23
|
children: title
|
|
21
24
|
}),
|
|
22
25
|
lead && /* @__PURE__ */ jsx("div", {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/components/siyahi/page-toc.d.ts
|
|
2
|
+
export interface PageTocItem {
|
|
3
|
+
/** The `id` of the PageSection it jumps to. */
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PageTocProps {
|
|
8
|
+
items: PageTocItem[];
|
|
9
|
+
/** Names the navigation for screen readers. */
|
|
10
|
+
label?: string;
|
|
11
|
+
/** Layout only, such as `top-14` under a sticky page header. */
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A long page's sections as one sticky "On this page" index. It stays at the top while the reader scrolls,
|
|
16
|
+
* underlines the section in view in the link colour, and scrolls sideways on a phone with the current link kept
|
|
17
|
+
* in sight.
|
|
18
|
+
*/
|
|
19
|
+
export declare function PageToc({ items, label, className }: PageTocProps): import("react").JSX.Element;
|
|
20
|
+
//#endregion
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { cn } from "cn";
|
|
2
|
+
import { ChevronRightIcon } from "lucide-react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
//#region src/components/siyahi/page-toc.tsx
|
|
6
|
+
/**
|
|
7
|
+
* A long page's sections as one sticky "On this page" index. It stays at the top while the reader scrolls,
|
|
8
|
+
* underlines the section in view in the link colour, and scrolls sideways on a phone with the current link kept
|
|
9
|
+
* in sight.
|
|
10
|
+
*/
|
|
11
|
+
function PageToc({ items, label = "On this page", className }) {
|
|
12
|
+
const nav = useRef(null);
|
|
13
|
+
const active = useActiveSection(items, nav);
|
|
14
|
+
const strip = useRef(null);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const link = strip.current?.querySelector(`a[href="#${CSS.escape(active ?? "")}"]`);
|
|
17
|
+
const box = strip.current;
|
|
18
|
+
if (!link || !box) return;
|
|
19
|
+
const left = link.offsetLeft - box.clientWidth / 2 + link.offsetWidth / 2;
|
|
20
|
+
box.scrollTo({
|
|
21
|
+
left,
|
|
22
|
+
behavior: "smooth"
|
|
23
|
+
});
|
|
24
|
+
}, [active]);
|
|
25
|
+
return /* @__PURE__ */ jsx("nav", {
|
|
26
|
+
ref: nav,
|
|
27
|
+
"aria-label": label,
|
|
28
|
+
"data-slot": "page-toc",
|
|
29
|
+
className: cn("sticky top-0 z-10 -mx-4 border-b bg-background px-4 sm:mx-0 sm:px-0", className),
|
|
30
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
31
|
+
ref: strip,
|
|
32
|
+
className: "flex items-baseline gap-x-4 overflow-x-auto py-2.5 mask-r-from-90% [scrollbar-width:none] sm:mask-none",
|
|
33
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
34
|
+
"aria-hidden": true,
|
|
35
|
+
className: "flex shrink-0 cursor-default items-center gap-1 self-center text-[0.6875rem] font-medium tracking-wider text-muted-foreground uppercase select-none",
|
|
36
|
+
children: [label, /* @__PURE__ */ jsx(ChevronRightIcon, { className: "size-3.5" })]
|
|
37
|
+
}), items.map((item) => /* @__PURE__ */ jsx("a", {
|
|
38
|
+
href: `#${item.id}`,
|
|
39
|
+
"aria-current": item.id === active ? "location" : void 0,
|
|
40
|
+
onClick: (event) => {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
document.getElementById(item.id)?.scrollIntoView({ behavior: "smooth" });
|
|
43
|
+
},
|
|
44
|
+
className: "shrink-0 text-sm whitespace-nowrap text-muted-foreground decoration-2 underline-offset-[6px] transition-colors hover:text-foreground hover:underline hover:decoration-border aria-[current=location]:font-medium aria-[current=location]:text-link aria-[current=location]:underline aria-[current=location]:decoration-current",
|
|
45
|
+
children: item.title
|
|
46
|
+
}, item.id))]
|
|
47
|
+
})
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function useActiveSection(items, strip) {
|
|
51
|
+
const [active, setActive] = useState(items[0]?.id);
|
|
52
|
+
const ids = items.map((item) => item.id).join(" ");
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const sections = ids.split(" ").flatMap((id) => document.getElementById(id) ?? []);
|
|
55
|
+
if (sections.length === 0) return;
|
|
56
|
+
const update = () => {
|
|
57
|
+
const stripBottom = strip.current?.getBoundingClientRect().bottom ?? 0;
|
|
58
|
+
const line = (section) => Math.max(stripBottom, Number.parseFloat(getComputedStyle(section).scrollMarginTop) || 0, innerHeight * .3);
|
|
59
|
+
const reached = sections.filter((section) => section.getBoundingClientRect().top <= line(section) + 1);
|
|
60
|
+
setActive((reached.at(-1) ?? sections[0]).id);
|
|
61
|
+
};
|
|
62
|
+
update();
|
|
63
|
+
addEventListener("scroll", update, {
|
|
64
|
+
passive: true,
|
|
65
|
+
capture: true
|
|
66
|
+
});
|
|
67
|
+
addEventListener("resize", update);
|
|
68
|
+
return () => {
|
|
69
|
+
removeEventListener("scroll", update, { capture: true });
|
|
70
|
+
removeEventListener("resize", update);
|
|
71
|
+
};
|
|
72
|
+
}, [ids, strip]);
|
|
73
|
+
return active;
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { PageToc };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/components/siyahi/panel-grid.d.ts
|
|
3
|
+
declare const COLUMNS: {
|
|
4
|
+
readonly 1: "";
|
|
5
|
+
readonly 2: "@2xl/panels:grid-cols-2";
|
|
6
|
+
readonly 3: "@2xl/panels:grid-cols-2 @5xl/panels:grid-cols-3";
|
|
7
|
+
readonly 4: "@2xl/panels:grid-cols-2 @5xl/panels:grid-cols-4";
|
|
8
|
+
};
|
|
9
|
+
export interface PanelGridProps {
|
|
10
|
+
/** Blocks across on a wide page; two on a medium one, and one on a phone. */
|
|
11
|
+
columns?: keyof typeof COLUMNS;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Data blocks side by side: chart cards, tables, a MetricStrip. On a ruled surface, the default, the grid is filled
|
|
17
|
+
* with the card colour and a hairline parts the blocks across and down, with a rule above and below the grid and none
|
|
18
|
+
* on its sides; open leaves whitespace between them, and card an 8px gap between boxes. Each block's bottom and end
|
|
19
|
+
* edges carry its rules and the grid clips the ones on its own edges, so an empty cell at the end of a short last row
|
|
20
|
+
* is a blank stretch of the sheet, ruled off like the rest. The theme numbers each block's card title 01, 02.
|
|
21
|
+
*/
|
|
22
|
+
export declare function PanelGrid({ columns, children, className }: PanelGridProps): import("react").JSX.Element;
|
|
23
|
+
//#endregion
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useSurface } from "./siyahi-provider.js";
|
|
2
|
+
import { cn } from "cn";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
import { Children } from "react";
|
|
5
|
+
//#region src/components/siyahi/panel-grid.tsx
|
|
6
|
+
const COLUMNS = {
|
|
7
|
+
1: "",
|
|
8
|
+
2: "@2xl/panels:grid-cols-2",
|
|
9
|
+
3: "@2xl/panels:grid-cols-2 @5xl/panels:grid-cols-3",
|
|
10
|
+
4: "@2xl/panels:grid-cols-2 @5xl/panels:grid-cols-4"
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Data blocks side by side: chart cards, tables, a MetricStrip. On a ruled surface, the default, the grid is filled
|
|
14
|
+
* with the card colour and a hairline parts the blocks across and down, with a rule above and below the grid and none
|
|
15
|
+
* on its sides; open leaves whitespace between them, and card an 8px gap between boxes. Each block's bottom and end
|
|
16
|
+
* edges carry its rules and the grid clips the ones on its own edges, so an empty cell at the end of a short last row
|
|
17
|
+
* is a blank stretch of the sheet, ruled off like the rest. The theme numbers each block's card title 01, 02.
|
|
18
|
+
*/
|
|
19
|
+
function PanelGrid({ columns = 2, children, className }) {
|
|
20
|
+
const surface = useSurface();
|
|
21
|
+
const ruled = surface === "ruled";
|
|
22
|
+
return /* @__PURE__ */ jsx("div", {
|
|
23
|
+
"data-slot": "panel-grid",
|
|
24
|
+
"data-surface": surface,
|
|
25
|
+
className: cn("@container/panels", ruled && "overflow-clip border-y", className),
|
|
26
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
27
|
+
className: cn("grid grid-cols-1", COLUMNS[columns], ruled ? "-me-px -mb-px bg-card" : surface === "open" ? "gap-x-8 gap-y-6" : "gap-2"),
|
|
28
|
+
children: Children.map(children, (child) => /* @__PURE__ */ jsx("div", {
|
|
29
|
+
"data-slot": "panel",
|
|
30
|
+
className: cn("min-w-0", ruled && "border-e border-b"),
|
|
31
|
+
children: child
|
|
32
|
+
}))
|
|
33
|
+
})
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { PanelGrid };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ComponentProps, ReactNode } from "react";
|
|
2
|
+
//#region src/components/siyahi/quote.d.ts
|
|
3
|
+
export interface QuoteProps extends ComponentProps<"blockquote"> {
|
|
4
|
+
/** Who said it, or where it is from, set under the quote. */
|
|
5
|
+
source?: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A quotation set apart from the prose on a muted panel, with its source under it. The panel is the whole box, not a
|
|
9
|
+
* stripe down one side.
|
|
10
|
+
*/
|
|
11
|
+
export declare function Quote({ source, className, children, ...props }: QuoteProps): import("react").JSX.Element;
|
|
12
|
+
//#endregion
|