@parallelworks/ui 0.84.2 → 0.85.0

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 CHANGED
@@ -125,7 +125,7 @@ Each need has one component. Reach for it before styling markup by hand.
125
125
  | A status in words | `StatusBadge`, or `StatusDot` beside text that says it |
126
126
  | A thing's tags or labels | `Tag`; `TagList` for several, wrapped or on one line ending in "+N" |
127
127
  | Keys to press | `Kbd`, with `Mod` for Command or Control; `ariaKeyShortcuts` for the control the keys press |
128
- | A chart's colors, legend and tooltip | `seriesColors`, `ChartLegendList`, `ChartTooltipContent` and `useSeriesHighlight`, around a chart drawn by hand or by a library |
128
+ | A chart's colors, legend and tooltip | `seriesColors` (by each series' place in a stable key list, eight at most) and `foldSeries` (the rest into "Other"), `ChartLegendList`, `ChartTooltipContent` placed beside the crosshair with `ChartTooltipBeside` or `chartTooltipPlacement`, and `useSeriesHighlight`, around a chart drawn by hand or by a library |
129
129
  | A run's, job's or step's status beside its name | `RunStatusIcon`; `StatusBadge` with `runStatusTone` to say it in words |
130
130
  | A copy action | `CopyButton`; `CopyCodeBlock` for a command or snippet, with `secret` for a value shown once; `SelectionCopy` to copy part of a block by selecting it; `useCopy` for a menu row or a control of your own |
131
131
  | A live relative time or duration | `RelativeTime` (with a `fallback`, an `exact` style for its tooltip, or `show="absolute"`), `Elapsed` |
@@ -48,11 +48,16 @@ export interface CopyCodeBlockProps {
48
48
  * surface reads on a panel and a tinted callout alike, and the buttons stay right after the
49
49
  * value, always shown. The copy confirms with its check alone, as the change shows there. */
50
50
  inline?: boolean | undefined;
51
+ /** False also says "Copied" in a toast. By default the copy confirms with the button's
52
+ * check alone, which stays in view until it turns back, as the button is where the reader
53
+ * pressed; a failure toasts either way. */
54
+ quiet?: boolean | undefined;
51
55
  className?: string | undefined;
52
56
  }
53
57
  /** A block of code, such as a command to run, with a button that copies it. The
54
58
  * buttons show while the pointer is over the block or focus is in it, and always
55
- * where there is no hover to reveal them. With `inline` it is a compact value in the line
59
+ * where there is no hover to reveal them. A copy confirms with the button's check,
60
+ * not a toast; a failure still toasts. With `inline` it is a compact value in the line
56
61
  * instead, such as a token shown once, its buttons always beside it. With `secret` it
57
62
  * masks the value until the eye shows it, and copies it either way. */
58
- export declare function CopyCodeBlock({ text, label, children, secret, revealed: revealedProp, defaultRevealed, onRevealedChange, inline, wrap, className, }: CopyCodeBlockProps): import("react").JSX.Element;
63
+ export declare function CopyCodeBlock({ text, label, children, secret, revealed: revealedProp, defaultRevealed, onRevealedChange, inline, wrap, quiet, className, }: CopyCodeBlockProps): import("react").JSX.Element;
@@ -4,7 +4,8 @@ import { CheckIcon, CopyIcon, EyeIcon, EyeOffIcon } from "../icons.js";
4
4
  import { useStrings } from "./Provider.js";
5
5
  import { useCopy } from "./useCopy.js";
6
6
  import { IconButton } from "./IconButton.js";
7
- import { useEffectEvent, useLayoutEffect, useRef, useState } from "react";
7
+ import { fadeMask, useHiddenEnds } from "./scrollFade.js";
8
+ import { useState } from "react";
8
9
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
9
10
  //#region src/components/CopyButton.tsx
10
11
  /** The one copy control: a button that copies its text, turns into a check for a
@@ -23,6 +24,7 @@ function CopyButton({ text, label, quiet = false, showLabel = false, variant = s
23
24
  if (showLabel) return /* @__PURE__ */ jsxs("button", {
24
25
  type: "button",
25
26
  ...props,
27
+ "data-copied": copied || void 0,
26
28
  "aria-label": props["aria-label"] ?? name,
27
29
  onClick,
28
30
  className: cx(buttonClasses({
@@ -33,6 +35,7 @@ function CopyButton({ text, label, quiet = false, showLabel = false, variant = s
33
35
  });
34
36
  return /* @__PURE__ */ jsx(IconButton, {
35
37
  ...props,
38
+ "data-copied": copied || void 0,
36
39
  variant,
37
40
  size,
38
41
  label: name,
@@ -40,60 +43,14 @@ function CopyButton({ text, label, quiet = false, showLabel = false, variant = s
40
43
  onClick
41
44
  });
42
45
  }
43
- var NO_ENDS = {
44
- start: false,
45
- end: false
46
- };
47
- function useHiddenEnds(active) {
48
- const ref = useRef(null);
49
- const [ends, setEnds] = useState(NO_ENDS);
50
- const measureNow = () => {
51
- const el = ref.current;
52
- if (!el || !active) {
53
- setEnds(NO_ENDS);
54
- return;
55
- }
56
- const scrolled = Math.abs(el.scrollLeft);
57
- const start = scrolled > 1;
58
- const end = scrolled + el.clientWidth < el.scrollWidth - 1;
59
- setEnds((was) => was.start === start && was.end === end ? was : {
60
- start,
61
- end
62
- });
63
- };
64
- const measure = useEffectEvent(measureNow);
65
- useLayoutEffect(() => {
66
- measure();
67
- });
68
- useLayoutEffect(() => {
69
- const el = ref.current;
70
- if (!el || !active || typeof ResizeObserver === "undefined") return;
71
- const observer = new ResizeObserver(() => measure());
72
- observer.observe(el);
73
- return () => observer.disconnect();
74
- }, [active]);
75
- return {
76
- ref,
77
- ends,
78
- onScroll: measureNow
79
- };
80
- }
81
- var FADE = "1.25rem";
82
- function fadeMask({ start, end }) {
83
- if (!start && !end) return;
84
- const image = `linear-gradient(to var(--fade-end, right), ${start ? `transparent, #000 ${FADE}` : "#000"}, ${end ? `#000 calc(100% - ${FADE}), transparent` : "#000"})`;
85
- return {
86
- maskImage: image,
87
- WebkitMaskImage: image
88
- };
89
- }
90
46
  var MASK = "•".repeat(24);
91
47
  /** A block of code, such as a command to run, with a button that copies it. The
92
48
  * buttons show while the pointer is over the block or focus is in it, and always
93
- * where there is no hover to reveal them. With `inline` it is a compact value in the line
49
+ * where there is no hover to reveal them. A copy confirms with the button's check,
50
+ * not a toast; a failure still toasts. With `inline` it is a compact value in the line
94
51
  * instead, such as a token shown once, its buttons always beside it. With `secret` it
95
52
  * masks the value until the eye shows it, and copies it either way. */
96
- function CopyCodeBlock({ text, label, children, secret = false, revealed: revealedProp, defaultRevealed = false, onRevealedChange, inline = false, wrap = false, className }) {
53
+ function CopyCodeBlock({ text, label, children, secret = false, revealed: revealedProp, defaultRevealed = false, onRevealedChange, inline = false, wrap = false, quiet = true, className }) {
97
54
  const { common: t } = useStrings();
98
55
  const [ownRevealed, setOwnRevealed] = useState(defaultRevealed);
99
56
  const revealed = revealedProp ?? ownRevealed;
@@ -102,6 +59,7 @@ function CopyCodeBlock({ text, label, children, secret = false, revealed: reveal
102
59
  onRevealedChange?.(next);
103
60
  };
104
61
  const reveal = "opacity-0 group-hover/code:opacity-100 group-focus-within/code:opacity-100 pointer-coarse:opacity-100";
62
+ const revealCopy = `${reveal} data-copied:opacity-100`;
105
63
  let shown = children ?? text;
106
64
  if (secret && !revealed && children === void 0) shown = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
107
65
  "aria-hidden": "true",
@@ -140,7 +98,7 @@ function CopyCodeBlock({ text, label, children, secret = false, revealed: reveal
140
98
  text,
141
99
  label,
142
100
  size: "xs",
143
- quiet: true
101
+ quiet
144
102
  }), revealButton]
145
103
  })]
146
104
  });
@@ -154,7 +112,8 @@ function CopyCodeBlock({ text, label, children, secret = false, revealed: reveal
154
112
  children: [revealButton, /* @__PURE__ */ jsx(CopyButton, {
155
113
  text,
156
114
  label,
157
- className: cx("bg-(--theme-muted-panel-bg)", reveal)
115
+ quiet,
116
+ className: cx("bg-(--theme-muted-panel-bg)", revealCopy)
158
117
  })]
159
118
  })]
160
119
  });
@@ -35,8 +35,15 @@ export type PageHeaderActionProps = Target & {
35
35
  disabledReason?: string | undefined;
36
36
  /** A hint shown on hover and focus while it can be pressed. */
37
37
  tooltip?: string | undefined;
38
- /** In the error color, as a destructive menu entry is, for an action that
39
- * removes or breaks something. */
38
+ /** How a labeled action reads in the bar: `outline`, the page's small
39
+ * secondary button, as "+ Add" is; or `link`, text in the link color, for a
40
+ * page that keeps the older look. On a narrow bar the label folds into the
41
+ * tooltip and the outline stays, so the action is an outlined icon. An
42
+ * `iconOnly` action, such as a pencil beside the title, is always bare. */
43
+ variant?: 'outline' | 'link' | undefined;
44
+ /** For an action that removes or breaks something: in the error color where
45
+ * it is a menu entry, in the header's "…" menu. In the bar it reads as the
46
+ * others do; a page's destructive actions belong in its menu. */
40
47
  destructive?: boolean | undefined;
41
48
  /** A toggle's state, such as a favorite: set, it reads as pressed. Buttons only. */
42
49
  pressed?: boolean | undefined;
@@ -47,10 +54,10 @@ export type PageHeaderActionProps = Target & {
47
54
  /**
48
55
  * An action in the page header: a link (`href`, `external` for a new tab,
49
56
  * `reloadDocument` for a path the router doesn't serve), a
50
- * button (`onClick`) or a menu (`menu`). Labeled actions read in the link
51
- * color, so the page's commands stand apart from the trail.
57
+ * button (`onClick`) or a menu (`menu`). A labeled action is a small outline
58
+ * button, as "+ Add" is, so the page's commands stand apart from the trail.
52
59
  */
53
- export declare function PageHeaderAction({ icon: Icon, label, ariaLabel, iconOnly, disabled, disabledReason, tooltip, destructive, pressed, loading, ...target }: PageHeaderActionProps): import("react").JSX.Element;
60
+ export declare function PageHeaderAction({ icon: Icon, label, ariaLabel, iconOnly, variant, disabled, disabledReason, tooltip, destructive, pressed, loading, ...target }: PageHeaderActionProps): import("react").JSX.Element;
54
61
  export type PageHeaderAddButtonProps = Target & {
55
62
  /** The full action, such as "New cluster": the tooltip and accessible name. */
56
63
  label: string;
@@ -55,11 +55,16 @@ function HeaderControl({ name, tooltip, disabled, disabledReason, loading = fals
55
55
  /**
56
56
  * An action in the page header: a link (`href`, `external` for a new tab,
57
57
  * `reloadDocument` for a path the router doesn't serve), a
58
- * button (`onClick`) or a menu (`menu`). Labeled actions read in the link
59
- * color, so the page's commands stand apart from the trail.
58
+ * button (`onClick`) or a menu (`menu`). A labeled action is a small outline
59
+ * button, as "+ Add" is, so the page's commands stand apart from the trail.
60
60
  */
61
- function PageHeaderAction({ icon: Icon, label, ariaLabel, iconOnly = false, disabled = false, disabledReason, tooltip, destructive = false, pressed, loading = false, ...target }) {
62
- const tone = destructive ? "*:text-(--theme-error-text)" : iconOnly ? void 0 : "*:text-(--theme-link)";
61
+ function PageHeaderAction({ icon: Icon, label, ariaLabel, iconOnly = false, variant = "outline", disabled = false, disabledReason, tooltip, destructive = false, pressed, loading = false, ...target }) {
62
+ const link = variant === "link" && !iconOnly;
63
+ const className = cx(buttonClasses({
64
+ variant: link || iconOnly ? "ghost" : "outline",
65
+ size: "xs",
66
+ iconOnly
67
+ }), link && "*:text-(--theme-link)");
63
68
  const icon = loading ? /* @__PURE__ */ jsx(Loader, {
64
69
  inline: true,
65
70
  decorative: true,
@@ -83,11 +88,7 @@ function PageHeaderAction({ icon: Icon, label, ariaLabel, iconOnly = false, disa
83
88
  disabledReason,
84
89
  loading,
85
90
  pressed,
86
- className: cx(buttonClasses({
87
- variant: "ghost",
88
- size: "xs",
89
- iconOnly
90
- }), tone),
91
+ className,
91
92
  children: [icon, !iconOnly && /* @__PURE__ */ jsx("span", {
92
93
  className: "@max-md:sr-only",
93
94
  children: label
@@ -23,8 +23,10 @@ export interface PaginationProps {
23
23
  /** Numbers the pages between previous and next, for a list a reader jumps
24
24
  * around in, such as a catalog: the first and last, and a window around
25
25
  * the current one, with gaps between. `max` is how many steps show at
26
- * most, gaps included (7 by default, 5 at least). Needs a `total`; hidden
27
- * on a phone-width screen, where previous and next remain. */
26
+ * most, gaps included (7 by default, 5 at least). Needs a `total`. Where
27
+ * the bar is narrow, as on a phone, the window narrows to the first, the
28
+ * current and the last page (5 steps), and where even those won't fit, to
29
+ * the current page and the count: "5 / 12". */
28
30
  pageNumbers?: boolean | {
29
31
  max?: number | undefined;
30
32
  } | undefined;
@@ -6,7 +6,7 @@ import { AppLink } from "./AppLink.js";
6
6
  import { IconButton } from "./IconButton.js";
7
7
  import { formatInteger } from "../format.js";
8
8
  import { Select } from "./Select.js";
9
- import { jsx, jsxs } from "react/jsx-runtime";
9
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
10
  //#region src/components/Pagination.tsx
11
11
  /** Page sizes a list offers; the first is the default. */
12
12
  var PAGE_SIZES = [
@@ -45,6 +45,7 @@ function pageSteps(page, count, max = 7) {
45
45
  * nothing when everything fits on one page. */
46
46
  function Pagination({ page, pageSize, total, hasNext, onPageChange, pageHref, pageSizes = PAGE_SIZES, onPageSizeChange, summary, pageNumbers = false, className }) {
47
47
  const t = useStrings().list;
48
+ const locale = useLocale();
48
49
  const start = page * pageSize + 1;
49
50
  const pageEnd = (page + 1) * pageSize;
50
51
  const known = total !== void 0;
@@ -59,7 +60,7 @@ function Pagination({ page, pageSize, total, hasNext, onPageChange, pageHref, pa
59
60
  const position = summary ?? (known ? t.pager(start, end, pastCap ? end : total, pastCap) : null);
60
61
  return /* @__PURE__ */ jsxs("nav", {
61
62
  "aria-label": t.pagination,
62
- className: cx("flex h-8 shrink-0 items-center justify-between gap-3 px-2.5", className),
63
+ className: cx("@container/pager flex h-8 shrink-0 items-center justify-between gap-3 px-2.5", className),
63
64
  children: [/* @__PURE__ */ jsxs("div", {
64
65
  className: "flex min-w-0 flex-1 items-center gap-3 text-xs text-(--theme-muted-text-color)",
65
66
  children: [position && /* @__PURE__ */ jsx("span", {
@@ -87,22 +88,39 @@ function Pagination({ page, pageSize, total, hasNext, onPageChange, pageHref, pa
87
88
  onPageChange,
88
89
  pageHref
89
90
  }),
90
- pageNumbers && pageCount !== void 0 && pageCount > 1 && /* @__PURE__ */ jsx("ol", {
91
- className: "m-0 hidden list-none items-center gap-1 p-0 sm:flex",
92
- children: pageSteps(page, pageCount, typeof pageNumbers === "object" ? pageNumbers.max : void 0).map((step, i) => /* @__PURE__ */ jsx("li", {
93
- className: "flex",
94
- children: step === null ? /* @__PURE__ */ jsx(MoreIcon, {
91
+ pageNumbers && pageCount !== void 0 && pageCount > 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
92
+ /* @__PURE__ */ jsx(PageNumbers, {
93
+ steps: pageSteps(page, pageCount, typeof pageNumbers === "object" ? pageNumbers.max : void 0),
94
+ page,
95
+ className: "hidden @min-[34rem]/pager:flex",
96
+ onPageChange,
97
+ pageHref
98
+ }),
99
+ /* @__PURE__ */ jsx(PageNumbers, {
100
+ steps: pageSteps(page, pageCount, 5),
101
+ page,
102
+ className: "hidden @min-[19rem]/pager:flex @min-[34rem]/pager:hidden",
103
+ onPageChange,
104
+ pageHref
105
+ }),
106
+ /* @__PURE__ */ jsxs("span", {
107
+ className: "flex items-center px-1 text-xs whitespace-nowrap text-(--theme-muted-text-color) tabular-nums @min-[19rem]/pager:hidden",
108
+ children: [/* @__PURE__ */ jsx("span", {
109
+ className: "sr-only",
110
+ children: t.pageOfTotal(page + 1, pageCount)
111
+ }), /* @__PURE__ */ jsxs("span", {
95
112
  "aria-hidden": "true",
96
- className: "size-3.5 w-5 text-(--theme-muted-text-color)"
97
- }) : /* @__PURE__ */ jsx(PageNumber, {
98
- to: step,
99
- current: step === page,
100
- label: t.page(step + 1),
101
- onPageChange,
102
- pageHref
103
- })
104
- }, step ?? `gap-${i}`))
105
- }),
113
+ children: [
114
+ /* @__PURE__ */ jsx("span", {
115
+ className: "text-(--theme-app)",
116
+ children: formatInteger(page + 1, locale)
117
+ }),
118
+ " / ",
119
+ formatInteger(pageCount, locale)
120
+ ]
121
+ })]
122
+ })
123
+ ] }),
106
124
  /* @__PURE__ */ jsx(PageStep, {
107
125
  to: page + 1,
108
126
  enabled: hasMore,
@@ -115,6 +133,25 @@ function Pagination({ page, pageSize, total, hasNext, onPageChange, pageHref, pa
115
133
  })]
116
134
  });
117
135
  }
136
+ function PageNumbers({ steps, page, className, onPageChange, pageHref }) {
137
+ const t = useStrings().list;
138
+ return /* @__PURE__ */ jsx("ol", {
139
+ className: cx("m-0 list-none items-center gap-1 p-0", className),
140
+ children: steps.map((step, i) => /* @__PURE__ */ jsx("li", {
141
+ className: "flex",
142
+ children: step === null ? /* @__PURE__ */ jsx(MoreIcon, {
143
+ "aria-hidden": "true",
144
+ className: "size-3.5 w-4 text-(--theme-muted-text-color)"
145
+ }) : /* @__PURE__ */ jsx(PageNumber, {
146
+ to: step,
147
+ current: step === page,
148
+ label: t.page(step + 1),
149
+ onPageChange,
150
+ pageHref
151
+ })
152
+ }, step ?? `gap-${i}`))
153
+ });
154
+ }
118
155
  function PageStep({ to, enabled, label, icon, onPageChange, pageHref }) {
119
156
  if (pageHref && enabled) return /* @__PURE__ */ jsx(IconButton, {
120
157
  variant: "outline",
@@ -588,6 +588,8 @@ export interface UIStrings {
588
588
  chart: {
589
589
  /** The row under a chart's tooltip that sums its series. */
590
590
  total: string;
591
+ /** The series that sums a chart's series past the eighth. */
592
+ other: string;
591
593
  };
592
594
  list: {
593
595
  moreActions: string;
@@ -635,6 +637,8 @@ export interface UIStrings {
635
637
  pagination: string;
636
638
  /** A numbered page's name in `Pagination`, counting from 1. */
637
639
  page: (page: number) => string;
640
+ /** Where the page sits, for `Pagination` too narrow for page numbers: it shows "5 / 12". */
641
+ pageOfTotal: (page: number, total: number) => string;
638
642
  removeSearchTerm: (term: string) => string;
639
643
  };
640
644
  /** The chip row of a list's active filters, its Filter menu and its pickers. Field and option
@@ -580,7 +580,10 @@ var DEFAULTS = {
580
580
  backspace: "Backspace",
581
581
  delete: "Delete"
582
582
  },
583
- chart: { total: "Total" },
583
+ chart: {
584
+ total: "Total",
585
+ other: "Other"
586
+ },
584
587
  list: {
585
588
  moreActions: "More actions",
586
589
  labelName: "name",
@@ -619,6 +622,7 @@ var DEFAULTS = {
619
622
  paginationNext: "Next",
620
623
  pagination: "Pagination",
621
624
  page: (page) => `Page ${page}`,
625
+ pageOfTotal: (page, total) => `Page ${page} of ${total}`,
622
626
  removeSearchTerm: (term) => `Remove filter ${term}`
623
627
  },
624
628
  filters: {
@@ -2,8 +2,9 @@ import { cx } from "../cx.js";
2
2
  import { useStickyTop } from "./stickyTop.js";
3
3
  import { Tooltip } from "./Tooltip.js";
4
4
  import { AppLink } from "./AppLink.js";
5
+ import { fadeMask, useHiddenEnds } from "./scrollFade.js";
5
6
  import { SegmentContent, segmentClassName, segmentGroupClassName } from "./segmented.js";
6
- import { useState } from "react";
7
+ import { useLayoutEffect, useRef, useState } from "react";
7
8
  import { jsx, jsxs } from "react/jsx-runtime";
8
9
  import { Tabs } from "@base-ui/react/tabs";
9
10
  //#region src/components/Tabs.tsx
@@ -26,6 +27,31 @@ function Tabs$1({ label, items, value, defaultValue, onValueChange, variant = "l
26
27
  const showRow = items.length > 1 || !!end;
27
28
  const [row, setRow] = useState(null);
28
29
  useStickyTop(row, !!sticky);
30
+ const scroller = useHiddenEnds(!segmented && showRow);
31
+ const scrolled = useRef(false);
32
+ useLayoutEffect(() => {
33
+ const list = scroller.ref.current;
34
+ const tab = list?.querySelector("[aria-selected=\"true\"]");
35
+ if (!list || !tab || segmented) return;
36
+ const margin = 24;
37
+ const left = tab.offsetLeft;
38
+ const right = left + tab.offsetWidth;
39
+ let to;
40
+ if (left - margin < list.scrollLeft) to = Math.max(0, left - margin);
41
+ else if (right + margin > list.scrollLeft + list.clientWidth) to = right + margin - list.clientWidth;
42
+ if (to !== void 0) {
43
+ const smooth = scrolled.current && !globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
44
+ list.scrollTo?.({
45
+ left: to,
46
+ behavior: smooth ? "smooth" : "instant"
47
+ });
48
+ }
49
+ scrolled.current = true;
50
+ }, [
51
+ current,
52
+ segmented,
53
+ scroller.ref
54
+ ]);
29
55
  const tabs = items.map((item) => {
30
56
  const tab = /* @__PURE__ */ jsx(Tabs.Tab, {
31
57
  value: item.value,
@@ -61,7 +87,10 @@ function Tabs$1({ label, items, value, defaultValue, onValueChange, variant = "l
61
87
  className: cx("flex items-center justify-between gap-2", !segmented && "border-b border-(--theme-border)", sticky && "sticky top-0 z-(--layer-sticky) bg-(--theme-app-bg)", className),
62
88
  children: [/* @__PURE__ */ jsxs(Tabs.List, {
63
89
  "aria-label": label,
64
- className: cx(segmented ? segmentGroupClassName(layout) : "relative flex min-w-0 overflow-x-auto"),
90
+ ref: segmented ? void 0 : scroller.ref,
91
+ onScroll: segmented ? void 0 : scroller.onScroll,
92
+ style: segmented ? void 0 : fadeMask(scroller.ends),
93
+ className: cx(segmented ? segmentGroupClassName(layout) : "relative flex min-w-0 overflow-x-auto rtl:[--fade-end:left]"),
65
94
  children: [tabs, !segmented && /* @__PURE__ */ jsx(Tabs.Indicator, { className: "absolute bottom-0 left-0 h-0.5 w-(--active-tab-width) translate-x-(--active-tab-left) bg-(--theme-link) transition-[translate,width] duration-200 ease-in-out motion-reduce:transition-none" })]
66
95
  }), end]
67
96
  }), children]
@@ -6,7 +6,8 @@ export interface TagStyle {
6
6
  caps?: boolean | undefined;
7
7
  }
8
8
  /** The one tag look: a pill with a hairline border and 11px text. Another component's chip,
9
- * such as a picker's chosen value, uses it to look the same. */
9
+ * such as a picker's chosen value, uses it to look the same. It has the panel's fill, so its
10
+ * edge shows on a hovered or selected row too, whose fill can match the border. */
10
11
  export declare function tagClasses({ mono, caps }?: TagStyle): string;
11
12
  /** Any other attribute and the ref reach the tag's element, its link with `href` and no `onRemove`,
12
13
  * so it can be a Tooltip's or a popover's direct child. */
@@ -7,9 +7,10 @@ import { useLayoutEffect, useRef, useState } from "react";
7
7
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/components/Tag.tsx
9
9
  /** The one tag look: a pill with a hairline border and 11px text. Another component's chip,
10
- * such as a picker's chosen value, uses it to look the same. */
10
+ * such as a picker's chosen value, uses it to look the same. It has the panel's fill, so its
11
+ * edge shows on a hovered or selected row too, whose fill can match the border. */
11
12
  function tagClasses({ mono = false, caps = false } = {}) {
12
- return cx("inline-flex h-5 max-w-full min-w-0 shrink-0 items-center gap-1 rounded-full border border-(--theme-border) px-2 text-[11px] leading-none whitespace-nowrap text-(--theme-app)", mono ? "font-mono" : "font-sans", caps && "text-[10px] font-medium tracking-wide uppercase");
13
+ return cx("inline-flex h-5 max-w-full min-w-0 shrink-0 items-center gap-1 rounded-full border border-(--theme-border) bg-(--theme-panel-bg) px-2 text-[11px] leading-none whitespace-nowrap text-(--theme-app)", mono ? "font-mono" : "font-sans", caps && "text-[10px] font-medium tracking-wide uppercase");
13
14
  }
14
15
  /** A tag or label on a thing: a pill with an optional color dot, a link with `href`, and an ×
15
16
  * with `onRemove`. */
@@ -27,7 +28,7 @@ function Tag({ children, color, href, onRemove, removeLabel, title, mono, caps,
27
28
  mono,
28
29
  caps
29
30
  }), className);
30
- const hover = "transition-colors hover:bg-(--theme-hover)";
31
+ const hover = "transition-colors hover:bg-(--theme-muted-panel-bg)";
31
32
  if (!onRemove) return href ? /* @__PURE__ */ jsx(AppLink, {
32
33
  ...rest,
33
34
  ref,
@@ -173,7 +174,7 @@ function MoreTags({ tags, ...style }) {
173
174
  trigger: /* @__PURE__ */ jsxs("button", {
174
175
  type: "button",
175
176
  "aria-label": `${common.moreCount(tags.length)}: ${names.join(", ")}`,
176
- className: cx(tagClasses(style), "cursor-pointer text-(--theme-muted-text-color) tabular-nums hover:bg-(--theme-hover) hover:text-(--theme-app) focus-visible:outline-2 focus-visible:outline-(--theme-focus) data-popup-open:bg-(--theme-hover)"),
177
+ className: cx(tagClasses(style), "cursor-pointer text-(--theme-muted-text-color) tabular-nums hover:bg-(--theme-muted-panel-bg) hover:text-(--theme-app) focus-visible:outline-2 focus-visible:outline-(--theme-focus) data-popup-open:bg-(--theme-muted-panel-bg)"),
177
178
  children: ["+", count]
178
179
  }),
179
180
  children: /* @__PURE__ */ jsx("ul", {
@@ -1,14 +1,28 @@
1
1
  import { type CSSProperties, type ReactNode } from 'react';
2
- /** A series' mark color for palette slot 1–8: the theme's category hue, between the slot's
3
- * soft fill and its text and twice as saturated, so a bar or a line stands out from the page
4
- * in light and dark alike and keeps the hue the slot has everywhere else. */
5
- export declare function seriesSlotColor(slot: number): string;
6
- /** A series' color by its key, any CSS color for a fill, a stroke or a style: the same key
7
- * gets the same color on every chart, as `categoryOf` gives a name its avatar color. */
8
- export declare function seriesColor(key: string): string;
9
- /** Colors for a chart's series, each key its `seriesColor` unless another key took that slot
10
- * first, when it moves to the next free one: up to eight series never share a color. */
2
+ /** How many series a chart colors: a series past the eighth folds into "Other" (`foldSeries`)
3
+ * rather than taking a hue made up for it. */
4
+ export declare const SERIES_LIMIT: 8;
5
+ /** The key `foldSeries` gives the series that sums those past the limit. */
6
+ export declare const OTHER_SERIES_KEY = "__other__";
7
+ /** The mark color of the "Other" series: a neutral gray, so it never reads as one more series. */
8
+ export declare const OTHER_SERIES_COLOR = "color-mix(in oklch, var(--theme-muted-text-color) 75%, var(--theme-panel-bg))";
9
+ /** The color of a chart's nth series, 1–8: the theme's category hue, between the slot's soft
10
+ * fill and its text and twice as saturated, so a bar or a line stands out from the page in
11
+ * light and dark alike. Past the eighth it is the "Other" gray; a hue is never repeated. */
12
+ export declare function seriesSlotColor(position: number): string;
13
+ /** Colors for a chart's series by their place in `keys`: the first takes the first color, the
14
+ * second the next, so the first few are always the most distinct. Pass every key the chart can
15
+ * show, in an order that doesn't depend on the data shown, such as by name or as configured:
16
+ * a filter that hides a series then leaves the others their colors. `OTHER_SERIES_KEY`, and any
17
+ * key past the eighth, is the "Other" gray; fold them first with `foldSeries`. */
11
18
  export declare function seriesColors(keys: readonly string[]): Record<string, string>;
19
+ /** Splits a chart's series at the limit: up to eight show as they are, and more keep the first
20
+ * seven and fold the rest into one "Other" series, `OTHER_SERIES_KEY`, whose value at a point
21
+ * is the sum of `folded`. Name it with the `chart.other` string. */
22
+ export declare function foldSeries(keys: readonly string[], limit?: number): {
23
+ keys: string[];
24
+ folded: string[];
25
+ };
12
26
  export type SeriesSwatchShape = 'square' | 'dot' | 'line';
13
27
  /** A series' color as a mark, for a legend or a tooltip's row. Only seen: the series' name
14
28
  * beside it says which it is. */
@@ -64,6 +78,39 @@ export declare function ChartTooltipContent({ label, items, formatValue, total,
64
78
  /** Lists the largest value first; on by default. */
65
79
  sort?: boolean | undefined;
66
80
  className?: string | undefined;
67
- /** Places it, such as beside the point it describes. */
81
+ /** Places it; `ChartTooltipBeside` places it beside a crosshair. */
68
82
  style?: CSSProperties | undefined;
69
83
  }): import("react").JSX.Element;
84
+ export interface ChartTooltipPlacement {
85
+ /** The tooltip's left edge. */
86
+ left: number;
87
+ side: 'left' | 'right';
88
+ /** A width to hold it to when neither side has room for all of it, its names truncating. */
89
+ maxWidth: number | null;
90
+ }
91
+ /** Where a chart's tooltip goes beside a crosshair at `x`, so it never covers the column under
92
+ * the pointer: `gap` to the `prefer` side, or the other when it doesn't fit between `x` and
93
+ * that edge, or held to the wider side when neither fits. `start` and `end` bound it, in the
94
+ * same coordinates as `x`, such as the chart's box or the viewport. A charting library's
95
+ * tooltip takes `left` as its position. */
96
+ export declare function chartTooltipPlacement({ x, width, start, end, gap, prefer, }: {
97
+ x: number;
98
+ width: number;
99
+ start: number;
100
+ end: number;
101
+ gap?: number | undefined;
102
+ prefer?: 'left' | 'right' | undefined;
103
+ }): ChartTooltipPlacement;
104
+ /** Holds a chart's tooltip beside its crosshair, inside a positioned box around the plot: `x`
105
+ * is the crosshair in that box, and the tooltip sits `top` from its top, to the crosshair's
106
+ * inline end, or its start where the box has no room. When neither side of the box has room,
107
+ * as on a phone, it may reach past the box to the viewport's edge, and is held narrower when
108
+ * even that is too small. It never covers the hovered column or the pointer on it. Nothing
109
+ * shows while `x` is null. */
110
+ export declare function ChartTooltipBeside({ x, top, gap, className, children, }: {
111
+ x: number | null;
112
+ top?: number | undefined;
113
+ gap?: number | undefined;
114
+ className?: string | undefined;
115
+ children: ReactNode;
116
+ }): import("react").JSX.Element | null;