@hackcove/ui-react 0.10.3 → 0.10.5
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/README.md +23 -5
- package/dist/CodeBlock.d.ts +2 -2
- package/dist/CodeBlock.js +2 -2
- package/dist/CodeBody.d.ts +2 -1
- package/dist/CodeBody.js +3 -2
- package/dist/CodeExample.d.ts +4 -3
- package/dist/CodeExample.js +4 -2
- package/dist/CopyButton.d.ts +3 -3
- package/dist/CopyButton.js +34 -14
- package/dist/CopyLine.js +1 -1
- package/dist/Dialog.js +1 -1
- package/dist/Hero.d.ts +23 -0
- package/dist/Hero.js +28 -0
- package/dist/Icon.d.ts +2 -1
- package/dist/Icon.js +2 -1
- package/dist/NavigationProgress.d.ts +20 -0
- package/dist/NavigationProgress.js +26 -0
- package/dist/SidebarLayout.d.ts +5 -2
- package/dist/SidebarLayout.js +51 -3
- package/dist/SiteSearch.d.ts +32 -0
- package/dist/SiteSearch.js +150 -0
- package/dist/Stack.d.ts +6 -1
- package/dist/Stack.js +7 -2
- package/dist/StackedBar.d.ts +2 -0
- package/dist/StackedBar.js +1 -1
- package/dist/StatTile.d.ts +8 -2
- package/dist/StatTile.js +14 -3
- package/dist/TrendChart.d.ts +34 -0
- package/dist/TrendChart.js +51 -0
- package/dist/copyScript.d.ts +13 -0
- package/dist/copyScript.js +13 -0
- package/dist/icons.d.ts +2 -2
- package/dist/icons.js +30 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/stickyColumn.d.ts +8 -0
- package/dist/stickyColumn.js +8 -0
- package/dist/trend.d.ts +26 -0
- package/dist/trend.js +64 -0
- package/dist/typography.js +6 -2
- package/package.json +2 -12
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
|
+
import type { Passthrough } from "./props.js";
|
|
3
|
+
/** One page the search can find. */
|
|
4
|
+
export type SiteSearchItem = {
|
|
5
|
+
/** The page's name: the row's label and the first thing matched. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** A muted line under the label, matched last. */
|
|
8
|
+
description?: string | undefined;
|
|
9
|
+
/** Where the row goes. */
|
|
10
|
+
href: string;
|
|
11
|
+
/** Other names the page answers to (the exports it documents), matched like the label but not shown. */
|
|
12
|
+
keywords?: string[] | undefined;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The items that match `text`, best first: the label starts with it, then the label contains it,
|
|
16
|
+
* then a keyword contains it, then the description does. Case is ignored, and items of the same
|
|
17
|
+
* rank keep their order in `items`. Empty text matches nothing.
|
|
18
|
+
*/
|
|
19
|
+
export declare function rankSiteSearch(items: readonly SiteSearchItem[], text: string): SiteSearchItem[];
|
|
20
|
+
/**
|
|
21
|
+
* The site's search: a header button that opens a dialog with a search field over a list of links
|
|
22
|
+
* to the pages that match. Put it in `AppShell`'s `controls` and pass every page as `items`. Ctrl+K
|
|
23
|
+
* (⌘ K on a Mac) opens it from anywhere on the page, and the button shows that key. Typing filters
|
|
24
|
+
* by name, then by `keywords`, then by description; Enter opens the first result, the arrow keys
|
|
25
|
+
* move through them, and Esc closes it and puts focus back where it was. Render it once per page.
|
|
26
|
+
*/
|
|
27
|
+
export declare function SiteSearch({ items, label, ...rest }: {
|
|
28
|
+
/** Every page to search, in the order ties are listed (the rail's order). */
|
|
29
|
+
items: readonly SiteSearchItem[];
|
|
30
|
+
/** The dialog's title and the button's accessible name: "Search components". */
|
|
31
|
+
label: string;
|
|
32
|
+
} & Passthrough): ReactElement;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useLayoutEffect, useRef, useState, useSyncExternalStore, } from "react";
|
|
3
|
+
import { Button } from "./Button.js";
|
|
4
|
+
import { Dialog } from "./Dialog.js";
|
|
5
|
+
import { Icon } from "./Icon.js";
|
|
6
|
+
import { IconButton } from "./IconButton.js";
|
|
7
|
+
import { Input } from "./Input.js";
|
|
8
|
+
import { Kbd } from "./Kbd.js";
|
|
9
|
+
import { ListRow } from "./ListRow.js";
|
|
10
|
+
import { Text } from "./typography.js";
|
|
11
|
+
/**
|
|
12
|
+
* The items that match `text`, best first: the label starts with it, then the label contains it,
|
|
13
|
+
* then a keyword contains it, then the description does. Case is ignored, and items of the same
|
|
14
|
+
* rank keep their order in `items`. Empty text matches nothing.
|
|
15
|
+
*/
|
|
16
|
+
export function rankSiteSearch(items, text) {
|
|
17
|
+
const query = text.trim().toLowerCase();
|
|
18
|
+
if (query === "")
|
|
19
|
+
return [];
|
|
20
|
+
const rankOf = (item) => {
|
|
21
|
+
const label = item.label.toLowerCase();
|
|
22
|
+
if (label.startsWith(query))
|
|
23
|
+
return 0;
|
|
24
|
+
if (label.includes(query))
|
|
25
|
+
return 1;
|
|
26
|
+
if (item.keywords?.some((keyword) => keyword.toLowerCase().includes(query)))
|
|
27
|
+
return 2;
|
|
28
|
+
if (item.description?.toLowerCase().includes(query))
|
|
29
|
+
return 3;
|
|
30
|
+
return -1;
|
|
31
|
+
};
|
|
32
|
+
return items
|
|
33
|
+
.map((item, index) => ({ item, index, rank: rankOf(item) }))
|
|
34
|
+
.filter((entry) => entry.rank >= 0)
|
|
35
|
+
.toSorted((a, b) => a.rank - b.rank || a.index - b.index)
|
|
36
|
+
.map((entry) => entry.item);
|
|
37
|
+
}
|
|
38
|
+
/** What the live region says for a result count. */
|
|
39
|
+
function countText(query, count) {
|
|
40
|
+
if (query.trim() === "")
|
|
41
|
+
return "";
|
|
42
|
+
if (count === 0)
|
|
43
|
+
return "No results";
|
|
44
|
+
return count === 1 ? "1 result" : `${count} results`;
|
|
45
|
+
}
|
|
46
|
+
// The shortcut's key reads ⌘ K on Apple platforms. The server cannot know, so it renders Ctrl K and
|
|
47
|
+
// the client swaps after hydration.
|
|
48
|
+
const noSubscribe = () => () => { };
|
|
49
|
+
const isApple = () => /mac|iphone|ipad|ipod/i.test(typeof navigator === "undefined" ? "" : navigator.userAgent);
|
|
50
|
+
/** Ctrl+K or Cmd+K with no other modifier, the one shortcut that opens the search. */
|
|
51
|
+
function isShortcut(event) {
|
|
52
|
+
return ((event.ctrlKey || event.metaKey) &&
|
|
53
|
+
!event.altKey &&
|
|
54
|
+
!event.shiftKey &&
|
|
55
|
+
event.key.toLowerCase() === "k");
|
|
56
|
+
}
|
|
57
|
+
// A link that stays on the page (a hash, an app's router) still closes the search; one opened in a
|
|
58
|
+
// new tab or window leaves it open.
|
|
59
|
+
function onListClick(event) {
|
|
60
|
+
if (event.ctrlKey || event.metaKey || event.shiftKey)
|
|
61
|
+
return;
|
|
62
|
+
if (event.target instanceof Element && event.target.closest("a[href]")) {
|
|
63
|
+
event.currentTarget.closest("dialog")?.close();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** The result links in the list, in order. */
|
|
67
|
+
function linksIn(list) {
|
|
68
|
+
return list ? [...list.querySelectorAll("a[href]")] : [];
|
|
69
|
+
}
|
|
70
|
+
function SearchBody({ items, label }) {
|
|
71
|
+
const [query, setQuery] = useState("");
|
|
72
|
+
const box = useRef(null);
|
|
73
|
+
const list = useRef(null);
|
|
74
|
+
const results = rankSiteSearch(items, query);
|
|
75
|
+
const typed = query.trim() !== "";
|
|
76
|
+
const onFieldKey = (event) => {
|
|
77
|
+
if (event.key === "Escape") {
|
|
78
|
+
// Close here, so no engine spends the first Esc clearing the search field.
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
event.currentTarget.closest("dialog")?.close();
|
|
81
|
+
}
|
|
82
|
+
else if (event.key === "Enter") {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
linksIn(list.current)[0]?.click();
|
|
85
|
+
}
|
|
86
|
+
else if (event.key === "ArrowDown") {
|
|
87
|
+
const first = linksIn(list.current)[0];
|
|
88
|
+
if (first) {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
first.focus();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const onListKey = (event) => {
|
|
95
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowUp")
|
|
96
|
+
return;
|
|
97
|
+
const links = linksIn(list.current);
|
|
98
|
+
const at = links.findIndex((link) => link === document.activeElement);
|
|
99
|
+
if (at < 0)
|
|
100
|
+
return;
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
const next = at + (event.key === "ArrowDown" ? 1 : -1);
|
|
103
|
+
if (next < 0)
|
|
104
|
+
box.current?.querySelector("input")?.focus();
|
|
105
|
+
else
|
|
106
|
+
links[Math.min(next, links.length - 1)]?.focus();
|
|
107
|
+
};
|
|
108
|
+
return (_jsxs("div", { ref: box, className: "flex flex-col gap-3", children: [_jsx(Input, { type: "search", icon: _jsx(Icon, { name: "search" }), "aria-label": label, placeholder: "Search", autoComplete: "off", spellCheck: false, value: query, onChange: (event) => setQuery(event.target.value), onKeyDown: onFieldKey }), _jsx("div", { role: "status", "aria-live": "polite", className: "sr-only", children: countText(query, results.length) }), !typed && (_jsx(Text, { as: "p", tone: "muted", children: "Type a name" })), typed && results.length === 0 && (_jsxs(Text, { as: "p", tone: "muted", children: ["No results for ", query.trim()] })), results.length > 0 && (_jsx("ul", { ref: list, "aria-label": "Results", onKeyDown: onListKey, onClick: onListClick, children: results.map((item, index) => (_jsx("li", { children: _jsx(ListRow, { href: item.href, size: "sm", divided: index > 0, secondary: item.description, children: item.label }) }, item.href))) }))] }));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The site's search: a header button that opens a dialog with a search field over a list of links
|
|
112
|
+
* to the pages that match. Put it in `AppShell`'s `controls` and pass every page as `items`. Ctrl+K
|
|
113
|
+
* (⌘ K on a Mac) opens it from anywhere on the page, and the button shows that key. Typing filters
|
|
114
|
+
* by name, then by `keywords`, then by description; Enter opens the first result, the arrow keys
|
|
115
|
+
* move through them, and Esc closes it and puts focus back where it was. Render it once per page.
|
|
116
|
+
*/
|
|
117
|
+
export function SiteSearch({ items, label, ...rest }) {
|
|
118
|
+
const [open, setOpen] = useState(false);
|
|
119
|
+
const opener = useRef(null);
|
|
120
|
+
const apple = useSyncExternalStore(noSubscribe, isApple, () => false);
|
|
121
|
+
const keys = apple ? "⌘ K" : "Ctrl K";
|
|
122
|
+
// Before paint, so a client-rendered trigger that is on screen already answers the shortcut.
|
|
123
|
+
useLayoutEffect(() => {
|
|
124
|
+
const onKey = (event) => {
|
|
125
|
+
if (event.defaultPrevented || event.repeat || !isShortcut(event))
|
|
126
|
+
return;
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
// Only the press that opens it records the opener; `close` clears it.
|
|
129
|
+
if (!opener.current && document.activeElement instanceof HTMLElement) {
|
|
130
|
+
opener.current = document.activeElement;
|
|
131
|
+
}
|
|
132
|
+
setOpen(true);
|
|
133
|
+
};
|
|
134
|
+
window.addEventListener("keydown", onKey);
|
|
135
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
136
|
+
}, []);
|
|
137
|
+
// A pointer click does not focus a button in every engine, so the trigger is remembered by hand.
|
|
138
|
+
const openFrom = (event) => {
|
|
139
|
+
opener.current = event.currentTarget;
|
|
140
|
+
setOpen(true);
|
|
141
|
+
};
|
|
142
|
+
const close = () => {
|
|
143
|
+
setOpen(false);
|
|
144
|
+
const back = opener.current;
|
|
145
|
+
opener.current = null;
|
|
146
|
+
if (back?.isConnected && back !== document.body)
|
|
147
|
+
back.focus();
|
|
148
|
+
};
|
|
149
|
+
return (_jsxs("span", { ...rest, className: "inline-flex", children: [_jsx("span", { className: "hidden sm:inline-flex", children: _jsxs(Button, { variant: "outline", size: "sm", "aria-label": label, "aria-haspopup": "dialog", "aria-keyshortcuts": "Control+K Meta+K", onClick: openFrom, children: [_jsx(Icon, { name: "search", size: "sm" }), "Search", _jsx(Kbd, { children: keys })] }) }), _jsx("span", { className: "inline-flex sm:hidden", children: _jsx(IconButton, { label: label, size: "sm", "aria-haspopup": "dialog", "aria-keyshortcuts": "Control+K Meta+K", onClick: openFrom, children: _jsx(Icon, { name: "search", size: "sm" }) }) }), open && (_jsx(Dialog, { title: label, size: "md", onClose: close, children: _jsx(SearchBody, { items: items, label: label }) }))] }));
|
|
150
|
+
}
|
package/dist/Stack.d.ts
CHANGED
|
@@ -66,7 +66,12 @@ type StackProps = StackBase & (({
|
|
|
66
66
|
* title row, `actions` included, as `SectionHeading`'s `rule` does for a bare heading. A line
|
|
67
67
|
* between two sections is a `Divider` in the `Stack` that holds them.
|
|
68
68
|
*/
|
|
69
|
-
export declare function Section({ title, actions, rule, gap, width, children, ...rest }: {
|
|
69
|
+
export declare function Section({ id, title, actions, rule, gap, width, children, ...rest }: {
|
|
70
|
+
/**
|
|
71
|
+
* Makes the section a link target: `#<id>` scrolls to its heading, which stops below the sticky
|
|
72
|
+
* AppShell header. Needs a `title`: with no title the heading that carries it is not rendered.
|
|
73
|
+
*/
|
|
74
|
+
id?: string | undefined;
|
|
70
75
|
/** A `SectionHeading` at the top, which names the section. */
|
|
71
76
|
title?: ReactNode;
|
|
72
77
|
/** Buttons or links at the end of the title row. */
|
package/dist/Stack.js
CHANGED
|
@@ -47,13 +47,18 @@ export function Stack({ gap = "md", width = "default", rowFrom, as = "div", chil
|
|
|
47
47
|
function Box({ tag: Tag, ...props }) {
|
|
48
48
|
return _jsx(Tag, { ...props });
|
|
49
49
|
}
|
|
50
|
+
/** The id a `Section`'s heading takes: the caller's, or one of React's. */
|
|
51
|
+
function useHeadingId(id) {
|
|
52
|
+
const generated = useId();
|
|
53
|
+
return id ?? generated;
|
|
54
|
+
}
|
|
50
55
|
/**
|
|
51
56
|
* A `<section>` Stack with an optional `SectionHeading` title (which names it) and `actions` beside
|
|
52
57
|
* it. Its blocks are `gap` apart, `md` by default as in a `Stack`. `rule` draws a hairline under the
|
|
53
58
|
* title row, `actions` included, as `SectionHeading`'s `rule` does for a bare heading. A line
|
|
54
59
|
* between two sections is a `Divider` in the `Stack` that holds them.
|
|
55
60
|
*/
|
|
56
|
-
export function Section({ title, actions, rule = false, gap = "md", width = "default", children, ...rest }) {
|
|
57
|
-
const titleId =
|
|
61
|
+
export function Section({ id, title, actions, rule = false, gap = "md", width = "default", children, ...rest }) {
|
|
62
|
+
const titleId = useHeadingId(id);
|
|
58
63
|
return (_jsxs(Stack, { as: "section", ...rest, gap: gap, width: width, "aria-labelledby": title ? titleId : rest["aria-labelledby"], children: [(title || actions) && (_jsxs("div", { className: cx("flex items-center justify-between gap-4", rule && "border-b border-slate-200/70 pb-2"), children: [title && _jsx(SectionHeading, { id: titleId, children: title }), actions && _jsx("div", { className: "ml-auto flex items-center gap-2", children: actions })] })), children] }));
|
|
59
64
|
}
|
package/dist/StackedBar.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { type TextTone } from "./typography.js";
|
|
|
9
9
|
* five `TextTone` names; a chart with more than five parts is why these two live here.
|
|
10
10
|
*/
|
|
11
11
|
export type VizTone = TextTone | "neutral" | "violet";
|
|
12
|
+
/** Tone → text utility, painted as a fill. The five `TextTone` colours are shared; the two extra ones are remapped by every theme. */
|
|
13
|
+
export declare const VIZ_TONE: Record<VizTone, string>;
|
|
12
14
|
/** One part of a `StackedBar`: its `label`, its `value` (its share is `value` over the sum) and its colour. */
|
|
13
15
|
export type StackedSegment = {
|
|
14
16
|
label: string;
|
package/dist/StackedBar.js
CHANGED
|
@@ -3,7 +3,7 @@ import { cx } from "./cx.js";
|
|
|
3
3
|
import { announcedPercent, BAR_TRACK, sharePercent } from "./ProgressBar.js";
|
|
4
4
|
import { TEXT_TONE } from "./typography.js";
|
|
5
5
|
/** Tone → text utility, painted as a fill. The five `TextTone` colours are shared; the two extra ones are remapped by every theme. */
|
|
6
|
-
const VIZ_TONE = {
|
|
6
|
+
export const VIZ_TONE = {
|
|
7
7
|
...TEXT_TONE,
|
|
8
8
|
neutral: "text-neutral-500",
|
|
9
9
|
violet: "text-purple-700",
|
package/dist/StatTile.d.ts
CHANGED
|
@@ -5,9 +5,10 @@ export type StatTileTone = "neutral" | "positive" | "warn" | "negative";
|
|
|
5
5
|
/**
|
|
6
6
|
* One headline figure: an uppercase caption over a large tabular-nums value, with an optional
|
|
7
7
|
* hint line beneath. `tone` colours the value; `accent` adds a bar across the top edge in the same
|
|
8
|
-
* tone. Pass the value already
|
|
8
|
+
* tone. `trend` draws a small line under the value in the same colour. Pass the value already
|
|
9
|
+
* formatted.
|
|
9
10
|
*/
|
|
10
|
-
export declare function StatTile({ label, value, hint, tone, accent, ...rest }: {
|
|
11
|
+
export declare function StatTile({ label, value, hint, tone, accent, trend, ...rest }: {
|
|
11
12
|
/** The small uppercase caption over the value. */
|
|
12
13
|
label: ReactNode;
|
|
13
14
|
/** The figure, already formatted, large and in tabular figures. */
|
|
@@ -18,4 +19,9 @@ export declare function StatTile({ label, value, hint, tone, accent, ...rest }:
|
|
|
18
19
|
tone?: StatTileTone;
|
|
19
20
|
/** A bar across the top edge in the tone's colour. */
|
|
20
21
|
accent?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The figure over time, oldest first: a sparkline under the value, in its colour, on a scale
|
|
24
|
+
* from 0. It is decoration, so say the trend in `hint` as well.
|
|
25
|
+
*/
|
|
26
|
+
trend?: readonly number[];
|
|
21
27
|
} & Unstyled<Omit<HTMLAttributes<HTMLDivElement>, "children">>): ReactElement;
|
package/dist/StatTile.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from "./cx.js";
|
|
3
3
|
import { BAR_FILL } from "./ProgressBar.js";
|
|
4
|
+
import { drawn, largest, plotY, runs, scaleMax } from "./trend.js";
|
|
4
5
|
import { Overline } from "./typography.js";
|
|
5
6
|
const VALUE = {
|
|
6
7
|
neutral: "text-slate-900",
|
|
@@ -17,8 +18,18 @@ const BAR = {
|
|
|
17
18
|
/**
|
|
18
19
|
* One headline figure: an uppercase caption over a large tabular-nums value, with an optional
|
|
19
20
|
* hint line beneath. `tone` colours the value; `accent` adds a bar across the top edge in the same
|
|
20
|
-
* tone. Pass the value already
|
|
21
|
+
* tone. `trend` draws a small line under the value in the same colour. Pass the value already
|
|
22
|
+
* formatted.
|
|
21
23
|
*/
|
|
22
|
-
export function StatTile({ label, value, hint, tone = "neutral", accent = false, ...rest }) {
|
|
23
|
-
return (_jsxs("div", { ...rest, className: "relative overflow-hidden rounded-xl border border-slate-200/70 bg-white p-5 shadow-sm", children: [accent && (_jsx("span", { "aria-hidden": "true", className: cx("absolute inset-x-0 top-0 h-1", BAR[tone]) })), _jsx(Overline, { as: "p", children: label }), _jsx("p", { className: cx("mt-2 text-3xl font-bold tabular-nums", VALUE[tone]), children: value }), hint != null && _jsx("p", { className: "mt-1 text-xs text-slate-500", children: hint })] }));
|
|
24
|
+
export function StatTile({ label, value, hint, tone = "neutral", accent = false, trend, ...rest }) {
|
|
25
|
+
return (_jsxs("div", { ...rest, className: "relative overflow-hidden rounded-xl border border-slate-200/70 bg-white p-5 shadow-sm", children: [accent && (_jsx("span", { "aria-hidden": "true", className: cx("absolute inset-x-0 top-0 h-1", BAR[tone]) })), _jsx(Overline, { as: "p", children: label }), _jsx("p", { className: cx("mt-2 text-3xl font-bold tabular-nums", VALUE[tone]), children: value }), trend !== undefined && trend.length > 0 && _jsx(Sparkline, { values: trend, tone: tone }), hint != null && _jsx("p", { className: "mt-1 text-xs text-slate-500", children: hint })] }));
|
|
26
|
+
}
|
|
27
|
+
/** The tile's trend line: edge to edge, `h-8`, in the value's colour. Hidden from assistive tech. */
|
|
28
|
+
function Sparkline({ values, tone, }) {
|
|
29
|
+
const max = scaleMax(largest([values]));
|
|
30
|
+
const step = values.length > 1 ? 100 / (values.length - 1) : 0;
|
|
31
|
+
return (_jsx("svg", { "aria-hidden": "true", viewBox: "0 0 100 100", preserveAspectRatio: "none", className: cx("mt-3 block h-8 w-full overflow-visible", VALUE[tone]), children: runs(values.map((value, i) => {
|
|
32
|
+
const y = drawn(value);
|
|
33
|
+
return y === null ? null : { x: values.length > 1 ? step * i : 50, y: plotY(y, max) };
|
|
34
|
+
})).map((points) => (_jsx("polyline", { points: points, fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", vectorEffect: "non-scaling-stroke" }, points))) }));
|
|
24
35
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import type { Passthrough } from "./props.js";
|
|
3
|
+
import { type VizTone } from "./StackedBar.js";
|
|
4
|
+
/**
|
|
5
|
+
* One series of a `TrendChart`: its `label` (the table's column, the Legend's entry), its colour,
|
|
6
|
+
* and one value per bucket in the buckets' order. A `null`, or a list shorter than the buckets, is
|
|
7
|
+
* a missing value.
|
|
8
|
+
*/
|
|
9
|
+
export type TrendSeries = {
|
|
10
|
+
label: string;
|
|
11
|
+
tone: VizTone;
|
|
12
|
+
values: readonly (number | null)[];
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A chart of one or two series over ordered buckets (weeks, months), as bars or a line, on a
|
|
16
|
+
* y-axis from 0. The y-axis is labelled 0, half and a rounded maximum; the x-axis names the
|
|
17
|
+
* buckets. A screen reader gets the same figures as a table.
|
|
18
|
+
*/
|
|
19
|
+
export declare function TrendChart({ label, buckets, series, kind, ...rest }: {
|
|
20
|
+
/** What the chart shows: its accessible name and the caption of its table. */
|
|
21
|
+
label: string;
|
|
22
|
+
/** The x-axis labels, in order (`"W12"`, `"Mar"`). */
|
|
23
|
+
buckets: readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* One or two series, each `{ label, tone, values }`. A `null` value, or a `values` list shorter
|
|
26
|
+
* than `buckets`, is missing. Two series draw a Legend under the chart.
|
|
27
|
+
*/
|
|
28
|
+
series: readonly [TrendSeries] | readonly [TrendSeries, TrendSeries];
|
|
29
|
+
/**
|
|
30
|
+
* `bar` for a count in each bucket, `line` for a level over time.
|
|
31
|
+
* @default bar
|
|
32
|
+
*/
|
|
33
|
+
kind?: "bar" | "line";
|
|
34
|
+
} & Passthrough): ReactElement;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Legend, VIZ_TONE } from "./StackedBar.js";
|
|
3
|
+
import { drawn, largest, plotY, round, runs, scaleMax } from "./trend.js";
|
|
4
|
+
/** The most x labels drawn; past it, every k-th label is, starting with the first. */
|
|
5
|
+
const MAX_X_LABELS = 8;
|
|
6
|
+
/** Share of a bucket's width the bars of one bucket take, centred; the rest is the gap between buckets. */
|
|
7
|
+
const BAR_SPAN = 0.7;
|
|
8
|
+
const FORMAT = new Intl.NumberFormat("en-US");
|
|
9
|
+
/** A value as the table says it: as given, or blank when missing. */
|
|
10
|
+
const spoken = (value) => value == null || !Number.isFinite(value) ? null : FORMAT.format(value);
|
|
11
|
+
/**
|
|
12
|
+
* A chart of one or two series over ordered buckets (weeks, months), as bars or a line, on a
|
|
13
|
+
* y-axis from 0. The y-axis is labelled 0, half and a rounded maximum; the x-axis names the
|
|
14
|
+
* buckets. A screen reader gets the same figures as a table.
|
|
15
|
+
*/
|
|
16
|
+
export function TrendChart({ label, buckets, series, kind = "bar", ...rest }) {
|
|
17
|
+
const values = series.map((s) => buckets.map((_, i) => drawn(s.values[i])));
|
|
18
|
+
const max = scaleMax(largest(values));
|
|
19
|
+
const ticks = [max, max / 2, 0];
|
|
20
|
+
return (_jsxs("figure", { ...rest, "aria-label": label, className: "flex flex-col gap-3", children: [_jsxs("div", { "aria-hidden": "true", className: "flex gap-2 pt-2 text-xs tabular-nums text-slate-500", children: [_jsx(YAxis, { ticks: ticks, max: max }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [_jsxs("div", { className: "relative h-48", children: [ticks.map((tick) => (_jsx("span", { className: "absolute inset-x-0 border-t border-slate-200", style: { top: `${plotY(tick, max)}%` } }, tick))), _jsx("svg", { viewBox: "0 0 100 100", preserveAspectRatio: "none", className: "absolute inset-0 size-full overflow-visible", children: kind === "bar" ? (_jsx(Bars, { series: series, values: values, max: max })) : (_jsx(Lines, { series: series, values: values, max: max })) })] }), _jsx(XAxis, { buckets: buckets })] })] }), series.length === 2 && (_jsx(Legend, { items: series.map((s) => ({ label: s.label, tone: s.tone })) })), _jsx(FigureTable, { label: label, buckets: buckets, series: series })] }));
|
|
21
|
+
}
|
|
22
|
+
/** The y labels, each centred on its gridline; the widest (the top) sets the column's width. */
|
|
23
|
+
function YAxis({ ticks, max }) {
|
|
24
|
+
return (_jsxs("div", { className: "relative h-48 shrink-0 text-right", children: [_jsx("span", { className: "invisible block", children: FORMAT.format(max) }), ticks.map((tick) => (_jsx("span", { className: "absolute right-0 -translate-y-1/2", style: { top: `${plotY(tick, max)}%` }, children: FORMAT.format(tick) }, tick)))] }));
|
|
25
|
+
}
|
|
26
|
+
/** One equal column per bucket; past 8 buckets, every k-th label from the first. */
|
|
27
|
+
function XAxis({ buckets }) {
|
|
28
|
+
const every = Math.max(1, Math.ceil(buckets.length / MAX_X_LABELS));
|
|
29
|
+
return (_jsx("div", { className: "grid text-center", style: { gridTemplateColumns: `repeat(${buckets.length}, minmax(0, 1fr))` }, children: buckets.map((bucket, i) => (_jsx("span", { className: "whitespace-nowrap", children: i % every === 0 ? bucket : null }, `${bucket}-${i}`))) }));
|
|
30
|
+
}
|
|
31
|
+
/** A rect per present value, the series side by side in each bucket. */
|
|
32
|
+
function Bars({ series, values, max }) {
|
|
33
|
+
const width = 100 / Math.max(1, values[0]?.length ?? 1);
|
|
34
|
+
const bar = (width * BAR_SPAN) / series.length;
|
|
35
|
+
const inset = (width * (1 - BAR_SPAN)) / 2;
|
|
36
|
+
return (_jsx(_Fragment, { children: series.map((s, si) => values[si]?.map((value, i) => {
|
|
37
|
+
if (value === null)
|
|
38
|
+
return null;
|
|
39
|
+
const y = plotY(value, max);
|
|
40
|
+
return (_jsx("rect", { "data-bucket": i, className: VIZ_TONE[s.tone], fill: "currentColor", x: round(width * i + inset + bar * si), y: round(y), width: round(bar), height: round(100 - y) }, `${si}-${i}`));
|
|
41
|
+
})) }));
|
|
42
|
+
}
|
|
43
|
+
/** A polyline per run of present values, through the middle of each bucket. */
|
|
44
|
+
function Lines({ series, values, max }) {
|
|
45
|
+
const width = 100 / Math.max(1, values[0]?.length ?? 1);
|
|
46
|
+
return (_jsx(_Fragment, { children: series.map((s, si) => runs((values[si] ?? []).map((value, i) => value === null ? null : { x: width * (i + 0.5), y: plotY(value, max) })).map((points) => (_jsx("polyline", { className: VIZ_TONE[s.tone], points: points, fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", vectorEffect: "non-scaling-stroke" }, `${si}-${points}`)))) }));
|
|
47
|
+
}
|
|
48
|
+
/** The chart's figures for a screen reader: a row per bucket, a column per series. */
|
|
49
|
+
function FigureTable({ label, buckets, series, }) {
|
|
50
|
+
return (_jsxs("table", { className: "sr-only", children: [_jsx("caption", { children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("td", {}), series.map((s, si) => (_jsx("th", { scope: "col", children: s.label }, si)))] }) }), _jsx("tbody", { children: buckets.map((bucket, i) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: bucket }), series.map((s, si) => (_jsx("td", { children: spoken(s.values[i]) }, si)))] }, `${bucket}-${i}`))) })] }));
|
|
51
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JavaScript to inline once on a page that is rendered to static HTML and never hydrated, so every
|
|
3
|
+
* `CopyButton` (and the `CopyLine`, `CodeBlock` and `CodeExample` copy buttons) works there too.
|
|
4
|
+
* One click listener on `document` reads what to copy from the button's data attributes, writes the
|
|
5
|
+
* clipboard, then does what `CopyButton` does: sets `data-copied` again after a reflow, so the CSS
|
|
6
|
+
* that shows the copied label for two seconds starts over, and puts a new node holding the copied
|
|
7
|
+
* label in the status region beside the button, so a screen reader hears each copy. No timer: the
|
|
8
|
+
* label goes back when the animation ends, and its end clears `data-copied` (a second listener), so a
|
|
9
|
+
* button hidden and shown again does not replay it. A refused or missing clipboard leaves it alone.
|
|
10
|
+
* Whichever of the script and a hydrated button's own handler hears a click first copies and flags
|
|
11
|
+
* the event (`copyHandled`), and the other skips it: one copy per click.
|
|
12
|
+
*/
|
|
13
|
+
export declare const copyScript: string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JavaScript to inline once on a page that is rendered to static HTML and never hydrated, so every
|
|
3
|
+
* `CopyButton` (and the `CopyLine`, `CodeBlock` and `CodeExample` copy buttons) works there too.
|
|
4
|
+
* One click listener on `document` reads what to copy from the button's data attributes, writes the
|
|
5
|
+
* clipboard, then does what `CopyButton` does: sets `data-copied` again after a reflow, so the CSS
|
|
6
|
+
* that shows the copied label for two seconds starts over, and puts a new node holding the copied
|
|
7
|
+
* label in the status region beside the button, so a screen reader hears each copy. No timer: the
|
|
8
|
+
* label goes back when the animation ends, and its end clears `data-copied` (a second listener), so a
|
|
9
|
+
* button hidden and shown again does not replay it. A refused or missing clipboard leaves it alone.
|
|
10
|
+
* Whichever of the script and a hydrated button's own handler hears a click first copies and flags
|
|
11
|
+
* the event (`copyHandled`), and the other skips it: one copy per click.
|
|
12
|
+
*/
|
|
13
|
+
export const copyScript = `document.addEventListener("click",function(e){var b=e.target.closest&&e.target.closest("[data-copy-text]"),c=navigator.clipboard;if(!b||!c||e.copyHandled||b.disabled)return;e.copyHandled=1;c.writeText(b.dataset.copyText).then(function(){var r=b.nextElementSibling;b.removeAttribute("data-copied");void b.offsetWidth;b.setAttribute("data-copied","");if(r&&r.matches("[data-copy-status]"))r.replaceChildren(b.dataset.copyCopied)},function(){})});document.addEventListener("animationend",function(e){var b=e.target.closest&&e.target.closest("[data-copied]");if(b)b.removeAttribute("data-copied")})`;
|
package/dist/icons.d.ts
CHANGED
|
@@ -6,8 +6,8 @@ import type { ReactElement } from "react";
|
|
|
6
6
|
* icon's own shapes carry no `fill`, `stroke` or `stroke-width` (`Icon.test.tsx` holds them to
|
|
7
7
|
* that). A name is the idea or action the icon stands for, not a product.
|
|
8
8
|
*/
|
|
9
|
-
declare const NAMES: readonly ["diff", "baseline", "review", "ci", "self-host", "privacy", "capture", "branch", "theme", "speed", "search", "chevron-right", "chevron-down", "close", "external-link", "settings", "bell", "check", "plus"];
|
|
10
|
-
/** The names of the icon set: feature ideas, then UI glyphs. */
|
|
9
|
+
declare const NAMES: readonly ["diff", "baseline", "review", "ci", "self-host", "privacy", "capture", "branch", "theme", "speed", "search", "chevron-right", "chevron-down", "close", "external-link", "settings", "bell", "check", "plus", "grid", "window", "type", "card", "pointer", "field", "table", "compass", "page", "briefcase"];
|
|
10
|
+
/** The names of the icon set: feature ideas, then UI glyphs, then the ideas the docs sections stand for. */
|
|
11
11
|
export type IconName = (typeof NAMES)[number];
|
|
12
12
|
/** The icon `name` as an `<svg>`: the set's one grid, stroke, caps and joins, drawn in `currentColor`. */
|
|
13
13
|
export declare function iconGlyph(name: IconName): ReactElement;
|
package/dist/icons.js
CHANGED
|
@@ -26,6 +26,16 @@ const NAMES = [
|
|
|
26
26
|
"bell",
|
|
27
27
|
"check",
|
|
28
28
|
"plus",
|
|
29
|
+
"grid",
|
|
30
|
+
"window",
|
|
31
|
+
"type",
|
|
32
|
+
"card",
|
|
33
|
+
"pointer",
|
|
34
|
+
"field",
|
|
35
|
+
"table",
|
|
36
|
+
"compass",
|
|
37
|
+
"page",
|
|
38
|
+
"briefcase",
|
|
29
39
|
];
|
|
30
40
|
const GLYPHS = {
|
|
31
41
|
/** Before and after, side by side: a minus on the left, a plus on the right. */
|
|
@@ -66,6 +76,26 @@ const GLYPHS = {
|
|
|
66
76
|
check: _jsx("path", { d: "m4.5 12.5 5 5L19.5 7" }),
|
|
67
77
|
/** Add or create: two strokes crossing, upright. */
|
|
68
78
|
plus: _jsx("path", { d: "M12 5v14M5 12h14" }),
|
|
79
|
+
/** Things laid out in a grid: four tiles. */
|
|
80
|
+
grid: (_jsxs(_Fragment, { children: [_jsx("rect", { x: "3.5", y: "3.5", width: "7", height: "7", rx: "1.5" }), _jsx("rect", { x: "13.5", y: "3.5", width: "7", height: "7", rx: "1.5" }), _jsx("rect", { x: "3.5", y: "13.5", width: "7", height: "7", rx: "1.5" }), _jsx("rect", { x: "13.5", y: "13.5", width: "7", height: "7", rx: "1.5" })] })),
|
|
81
|
+
/** A whole page's frame: a window with a title bar and a panel down the side. */
|
|
82
|
+
window: (_jsxs(_Fragment, { children: [_jsx("rect", { x: "3", y: "4.5", width: "18", height: "15", rx: "2.5" }), _jsx("path", { d: "M3 9h18M9.5 9v10.5M6.5 6.75h.01M9 6.75h.01" })] })),
|
|
83
|
+
/** Text and its styles: a capital T. */
|
|
84
|
+
type: _jsx("path", { d: "M5.5 7V5h13v2M12 5v14M9 19h6" }),
|
|
85
|
+
/** A unit of content: a card with a title line and lines of body. */
|
|
86
|
+
card: (_jsxs(_Fragment, { children: [_jsx("rect", { x: "3", y: "4.5", width: "18", height: "15", rx: "2.5" }), _jsx("path", { d: "M7 9h5M7 12.5h10M7 15.75h7" })] })),
|
|
87
|
+
/** Something to press: a pointer arrow. */
|
|
88
|
+
pointer: _jsx("path", { d: "M7 4.5v13.2l3.3-3.1 2.4 5 2.2-1-2.3-4.9h4.6Z" }),
|
|
89
|
+
/** Something to type in: a text field with its text and caret. */
|
|
90
|
+
field: (_jsxs(_Fragment, { children: [_jsx("rect", { x: "3", y: "7.5", width: "18", height: "9", rx: "2.5" }), _jsx("path", { d: "M7 12h4M13.5 9.5v5" })] })),
|
|
91
|
+
/** Rows and columns of data: a table with a header row and a first column. */
|
|
92
|
+
table: (_jsxs(_Fragment, { children: [_jsx("rect", { x: "3", y: "4.5", width: "18", height: "15", rx: "2.5" }), _jsx("path", { d: "M3 9.5h18M3 14.5h18M9.5 9.5v10" })] })),
|
|
93
|
+
/** Finding your way around: a compass with its needle. */
|
|
94
|
+
compass: (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "m15.5 8.5-2 5-5 2 2-5Z" })] })),
|
|
95
|
+
/** A whole page of its own: a sheet with a folded corner and lines of text. */
|
|
96
|
+
page: (_jsxs(_Fragment, { children: [_jsx("path", { d: "M7.5 3H14l5 5v10.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 5 18.5v-13A2.5 2.5 0 0 1 7.5 3Z" }), _jsx("path", { d: "M14 3v3.5A1.5 1.5 0 0 0 15.5 8H19M9 13h6M9 16.5h6" })] })),
|
|
97
|
+
/** Work for hire: a briefcase with its handle and clasp. */
|
|
98
|
+
briefcase: (_jsxs(_Fragment, { children: [_jsx("rect", { x: "3", y: "7.5", width: "18", height: "12", rx: "2.5" }), _jsx("path", { d: "M8.5 7.5V6a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v1.5M3 13h18M12 12v2.5" })] })),
|
|
69
99
|
};
|
|
70
100
|
/** The icon `name` as an `<svg>`: the set's one grid, stroke, caps and joins, drawn in `currentColor`. */
|
|
71
101
|
export function iconGlyph(name) {
|
package/dist/index.d.ts
CHANGED
|
@@ -6,10 +6,12 @@ export { StatTile, type StatTileTone } from "./StatTile.js";
|
|
|
6
6
|
export { ProgressBar, type BarSize, type BarTone } from "./ProgressBar.js";
|
|
7
7
|
export { SplitBar } from "./SplitBar.js";
|
|
8
8
|
export { Legend, StackedBar, type LegendItem, type StackedSegment, type VizTone, } from "./StackedBar.js";
|
|
9
|
+
export { TrendChart, type TrendSeries } from "./TrendChart.js";
|
|
9
10
|
export { Markdown } from "./Markdown.js";
|
|
10
11
|
export { AppShell, AppShellBrand, type AppShellBreakpoint, AppShellHideBelow, appShellRootClassFor, } from "./AppShell.js";
|
|
11
12
|
export { Container } from "./Container.js";
|
|
12
13
|
export { PageHeader, type PageHeaderProps } from "./PageHeader.js";
|
|
14
|
+
export { Hero, type HeroProps } from "./Hero.js";
|
|
13
15
|
export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, ProseLayout, } from "./PageLayouts.js";
|
|
14
16
|
export { SidebarLayout } from "./SidebarLayout.js";
|
|
15
17
|
export { SideColumn, type SideColumnWidth } from "./SideColumn.js";
|
|
@@ -43,7 +45,9 @@ export { CodeExample } from "./CodeExample.js";
|
|
|
43
45
|
export { Disclosure } from "./Disclosure.js";
|
|
44
46
|
export { Divider } from "./Divider.js";
|
|
45
47
|
export { PageFade, startPageTransition } from "./PageFade.js";
|
|
48
|
+
export { NavigationProgress } from "./NavigationProgress.js";
|
|
46
49
|
export { Dialog, type DialogProps } from "./Dialog.js";
|
|
50
|
+
export { SiteSearch, type SiteSearchItem } from "./SiteSearch.js";
|
|
47
51
|
export { Drawer, type DrawerProps } from "./Drawer.js";
|
|
48
52
|
export { Lightbox, type LightboxProps } from "./Lightbox.js";
|
|
49
53
|
export { Icon } from "./Icon.js";
|
|
@@ -59,6 +63,7 @@ export { Switch } from "./Switch.js";
|
|
|
59
63
|
export { Radio, RadioGroup } from "./RadioGroup.js";
|
|
60
64
|
export { FileDrop } from "./FileDrop.js";
|
|
61
65
|
export { CopyButton } from "./CopyButton.js";
|
|
66
|
+
export { copyScript } from "./copyScript.js";
|
|
62
67
|
export { CopyLine } from "./CopyLine.js";
|
|
63
68
|
export { Field } from "./Field.js";
|
|
64
69
|
export { FormGrid, FormRow, FormRowField, type FormRowSpan } from "./FormRow.js";
|
package/dist/index.js
CHANGED
|
@@ -6,10 +6,12 @@ export { StatTile } from "./StatTile.js";
|
|
|
6
6
|
export { ProgressBar } from "./ProgressBar.js";
|
|
7
7
|
export { SplitBar } from "./SplitBar.js";
|
|
8
8
|
export { Legend, StackedBar, } from "./StackedBar.js";
|
|
9
|
+
export { TrendChart } from "./TrendChart.js";
|
|
9
10
|
export { Markdown } from "./Markdown.js";
|
|
10
11
|
export { AppShell, AppShellBrand, AppShellHideBelow, appShellRootClassFor, } from "./AppShell.js";
|
|
11
12
|
export { Container } from "./Container.js";
|
|
12
13
|
export { PageHeader } from "./PageHeader.js";
|
|
14
|
+
export { Hero } from "./Hero.js";
|
|
13
15
|
export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, ProseLayout, } from "./PageLayouts.js";
|
|
14
16
|
export { SidebarLayout } from "./SidebarLayout.js";
|
|
15
17
|
export { SideColumn } from "./SideColumn.js";
|
|
@@ -42,7 +44,9 @@ export { CodeExample } from "./CodeExample.js";
|
|
|
42
44
|
export { Disclosure } from "./Disclosure.js";
|
|
43
45
|
export { Divider } from "./Divider.js";
|
|
44
46
|
export { PageFade, startPageTransition } from "./PageFade.js";
|
|
47
|
+
export { NavigationProgress } from "./NavigationProgress.js";
|
|
45
48
|
export { Dialog } from "./Dialog.js";
|
|
49
|
+
export { SiteSearch } from "./SiteSearch.js";
|
|
46
50
|
export { Drawer } from "./Drawer.js";
|
|
47
51
|
export { Lightbox } from "./Lightbox.js";
|
|
48
52
|
export { Icon } from "./Icon.js";
|
|
@@ -57,6 +61,7 @@ export { Switch } from "./Switch.js";
|
|
|
57
61
|
export { Radio, RadioGroup } from "./RadioGroup.js";
|
|
58
62
|
export { FileDrop } from "./FileDrop.js";
|
|
59
63
|
export { CopyButton } from "./CopyButton.js";
|
|
64
|
+
export { copyScript } from "./copyScript.js";
|
|
60
65
|
export { CopyLine } from "./CopyLine.js";
|
|
61
66
|
export { Field } from "./Field.js";
|
|
62
67
|
export { FormGrid, FormRow, FormRowField } from "./FormRow.js";
|
package/dist/stickyColumn.d.ts
CHANGED
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export declare const STICKY_ASIDE_MD = "md:sticky md:top-[var(--app-header,0px)] md:self-start";
|
|
15
15
|
export declare const STICKY_SCROLL_MD = "md:-m-1 md:max-h-[calc(100dvh-var(--app-header,0px)-2rem)] md:overflow-y-auto md:p-1";
|
|
16
|
+
/**
|
|
17
|
+
* The thin themed scrollbar (`themedScrollbar` in `cx.ts`) on a rail's scroller from `md`, where it
|
|
18
|
+
* scrolls, instead of the heavy native one with arrow buttons. Its thumb is the scroller's own text
|
|
19
|
+
* colour, so the rail's real ink goes back on an inner box (`STICKY_INK_MD`), the colour the shell
|
|
20
|
+
* gives every page.
|
|
21
|
+
*/
|
|
22
|
+
export declare const STICKY_BAR_MD = "md:text-slate-400 md:[scrollbar-width:thin] md:[scrollbar-color:currentColor_transparent]";
|
|
23
|
+
export declare const STICKY_INK_MD = "md:text-slate-900";
|
|
16
24
|
export declare const STICKY_ASIDE_LG = "lg:sticky lg:top-[var(--app-header,0px)] lg:self-start";
|
|
17
25
|
export declare const STICKY_SCROLL_LG = "lg:-m-1 lg:max-h-[calc(100dvh-var(--app-header,0px)-2rem)] lg:overflow-y-auto lg:p-1";
|
|
18
26
|
/**
|
package/dist/stickyColumn.js
CHANGED
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export const STICKY_ASIDE_MD = "md:sticky md:top-[var(--app-header,0px)] md:self-start";
|
|
15
15
|
export const STICKY_SCROLL_MD = "md:-m-1 md:max-h-[calc(100dvh-var(--app-header,0px)-2rem)] md:overflow-y-auto md:p-1";
|
|
16
|
+
/**
|
|
17
|
+
* The thin themed scrollbar (`themedScrollbar` in `cx.ts`) on a rail's scroller from `md`, where it
|
|
18
|
+
* scrolls, instead of the heavy native one with arrow buttons. Its thumb is the scroller's own text
|
|
19
|
+
* colour, so the rail's real ink goes back on an inner box (`STICKY_INK_MD`), the colour the shell
|
|
20
|
+
* gives every page.
|
|
21
|
+
*/
|
|
22
|
+
export const STICKY_BAR_MD = "md:text-slate-400 md:[scrollbar-width:thin] md:[scrollbar-color:currentColor_transparent]";
|
|
23
|
+
export const STICKY_INK_MD = "md:text-slate-900";
|
|
16
24
|
export const STICKY_ASIDE_LG = "lg:sticky lg:top-[var(--app-header,0px)] lg:self-start";
|
|
17
25
|
export const STICKY_SCROLL_LG = "lg:-m-1 lg:max-h-[calc(100dvh-var(--app-header,0px)-2rem)] lg:overflow-y-auto lg:p-1";
|
|
18
26
|
/**
|
package/dist/trend.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scale shared by `TrendChart` and the `StatTile` sparkline. Internal: not exported from
|
|
3
|
+
* `index.ts`.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The top of a y-axis that starts at 0: the smallest of 2, 4 or 10 times a whole power of ten
|
|
7
|
+
* (1, 10, 100, …) that is at least `top`. Its half is then a whole number: 1 gives 2, 3 gives
|
|
8
|
+
* 4, 5 gives 10, 13 gives 20. Nothing above 0 gives 2.
|
|
9
|
+
*/
|
|
10
|
+
export declare function scaleMax(top: number): number;
|
|
11
|
+
/** A value as drawn: a negative one is 0, and a missing or non-finite one is `null`. */
|
|
12
|
+
export declare function drawn(value: number | null | undefined): number | null;
|
|
13
|
+
/** The largest drawn value of every list, or 0. */
|
|
14
|
+
export declare function largest(lists: readonly (readonly (number | null)[])[]): number;
|
|
15
|
+
/** `value` as a height on a 0 to 100 plot, measured down from the top (SVG's y). */
|
|
16
|
+
export declare function plotY(value: number, max: number): number;
|
|
17
|
+
/**
|
|
18
|
+
* The polylines of a line through `points`, broken at every missing one: one `points` string per
|
|
19
|
+
* run of present values. A run of one point repeats it, so a round cap draws it as a dot.
|
|
20
|
+
*/
|
|
21
|
+
export declare function runs(points: readonly ({
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
} | null)[]): string[];
|
|
25
|
+
/** Four decimals, so the markup stays short and stable. */
|
|
26
|
+
export declare function round(n: number): number;
|