@parallelworks/ui 0.84.2 → 0.86.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.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/dist/ai/components/ChatMessage.js +6 -0
  3. package/dist/ai/core/ChatProvider.js +16 -5
  4. package/dist/components/CopyButton.d.ts +7 -2
  5. package/dist/components/CopyButton.js +11 -52
  6. package/dist/components/PageHeaderAction.d.ts +12 -5
  7. package/dist/components/PageHeaderAction.js +10 -9
  8. package/dist/components/Pagination.d.ts +4 -2
  9. package/dist/components/Pagination.js +54 -17
  10. package/dist/components/Provider.d.ts +4 -0
  11. package/dist/components/Provider.js +5 -1
  12. package/dist/components/RunStatusIcon.js +14 -17
  13. package/dist/components/StatusBadge.d.ts +10 -1
  14. package/dist/components/StatusBadge.js +27 -15
  15. package/dist/components/StatusIcons.d.ts +2 -0
  16. package/dist/components/StatusIcons.js +5 -3
  17. package/dist/components/Tabs.js +31 -2
  18. package/dist/components/Tag.d.ts +2 -1
  19. package/dist/components/Tag.js +5 -4
  20. package/dist/components/chart.d.ts +57 -10
  21. package/dist/components/chart.js +117 -22
  22. package/dist/components/scrollFade.d.ts +13 -0
  23. package/dist/components/scrollFade.js +51 -0
  24. package/dist/file-explorer/FileExplorer.js +8 -0
  25. package/dist/icons.d.ts +14 -1
  26. package/dist/icons.js +104 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +2 -2
  29. package/dist/list/ListPage.d.ts +4 -1
  30. package/dist/list/ListPage.js +96 -14
  31. package/dist/list/ListTable.d.ts +1 -0
  32. package/dist/list/ListTable.js +1 -1
  33. package/dist/list/ListViewControls.d.ts +1 -1
  34. package/dist/list/ListViewControls.js +2 -1
  35. package/dist/list/index.js +2 -2
  36. package/dist/list/listView.d.ts +15 -3
  37. package/dist/list/listView.js +11 -4
  38. package/dist/styles.css +1 -1
  39. package/package.json +1 -1
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` |
@@ -121,6 +121,12 @@ function ChatMessage({ message, allMessages, isStreaming = false, latest = false
121
121
  isStreaming,
122
122
  onApprovalAnswer: approvalAnswer
123
123
  }),
124
+ hasError && /* @__PURE__ */ jsx(Callout, {
125
+ tone: "error",
126
+ size: "sm",
127
+ className: "mt-2",
128
+ children: message.error
129
+ }),
124
130
  isStreaming && !isUser && /* @__PURE__ */ jsx("div", {
125
131
  "aria-hidden": true,
126
132
  className: "mt-2 h-7"
@@ -312,7 +312,7 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
312
312
  state.currentConversation,
313
313
  navigation
314
314
  ]);
315
- const streamReply = useCallback(async (targetConversationId, request, parentId, onFailure, routePart) => {
315
+ const streamReply = useCallback(async (targetConversationId, request, parentId, onFailure, routePart, onAccepted) => {
316
316
  backgroundStreamsRef.current.set(targetConversationId, {
317
317
  content: "",
318
318
  reasoning: "",
@@ -335,7 +335,10 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
335
335
  let thinkingStart = null;
336
336
  let thinkingEnd = null;
337
337
  const markStreamStart = () => {
338
- if (thinkingStart === null) thinkingStart = Date.now();
338
+ if (thinkingStart === null) {
339
+ thinkingStart = Date.now();
340
+ onAccepted?.();
341
+ }
339
342
  };
340
343
  let liveParts = [];
341
344
  let liveText = "";
@@ -637,7 +640,8 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
637
640
  isSendingRef.current = true;
638
641
  const targetConversationId = conversation.id;
639
642
  const t = resolvedConfig.strings.agentTranscript;
640
- let pausedParts = paused.parts ?? [];
643
+ const askedParts = paused.parts ?? [];
644
+ let pausedParts = askedParts;
641
645
  const updatePaused = (updates) => dispatch({
642
646
  type: "UPDATE_MESSAGE",
643
647
  messageId: paused.id,
@@ -648,6 +652,7 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
648
652
  pausedParts = applyPartDelta(pausedParts, delta);
649
653
  updatePaused({ parts: pausedParts });
650
654
  };
655
+ if (paused.error) updatePaused({ error: null });
651
656
  applyToPaused({
652
657
  type: "approval_resolved",
653
658
  id: approvalId,
@@ -677,8 +682,9 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
677
682
  }
678
683
  streamingToConversationRef.current = targetConversationId;
679
684
  startStreaming(targetConversationId);
685
+ let accepted = false;
680
686
  try {
681
- await streamReply(targetConversationId, {
687
+ if (await streamReply(targetConversationId, {
682
688
  model: modelString,
683
689
  messages: historyMessages(conversation.messages.slice(0, pausedAt + 1), state.selectedProvider),
684
690
  conversationId: targetConversationId,
@@ -698,7 +704,12 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
698
704
  if (delta.type !== "tool_end" || delta.id !== approvalId) return false;
699
705
  applyToPaused(delta);
700
706
  return true;
701
- });
707
+ }, () => {
708
+ accepted = true;
709
+ }) === "failed" && !accepted) {
710
+ pausedParts = askedParts;
711
+ updatePaused({ parts: askedParts });
712
+ }
702
713
  } finally {
703
714
  isSendingRef.current = false;
704
715
  }
@@ -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: {
@@ -1,5 +1,5 @@
1
1
  import { cx } from "../cx.js";
2
- import { CancelIcon, FailIcon, NotRunningIcon, PendingIcon, RunningIcon, SkipIcon, SuccessIcon, WarningIcon } from "../icons.js";
2
+ import { CancelFilledIcon, FailFilledIcon, NotRunningIcon, PendingRingIcon, RunningRingIcon, SkipIcon, SuccessFilledIcon, WarningFilledIcon } from "../icons.js";
3
3
  import { toneIcon } from "./tone.js";
4
4
  import { Tooltip } from "./Tooltip.js";
5
5
  import { useStrings } from "./Provider.js";
@@ -8,33 +8,31 @@ import { jsx } from "react/jsx-runtime";
8
8
  var looks = {
9
9
  pending: {
10
10
  tone: "muted",
11
- icon: PendingIcon
11
+ icon: PendingRingIcon
12
12
  },
13
13
  started: {
14
14
  tone: "info",
15
- icon: RunningIcon,
16
- spin: true
15
+ icon: RunningRingIcon
17
16
  },
18
17
  running: {
19
18
  tone: "info",
20
- icon: RunningIcon,
21
- spin: true
19
+ icon: RunningRingIcon
22
20
  },
23
21
  completed: {
24
22
  tone: "success",
25
- icon: SuccessIcon
23
+ icon: SuccessFilledIcon
26
24
  },
27
25
  warning: {
28
26
  tone: "warning",
29
- icon: WarningIcon
27
+ icon: WarningFilledIcon
30
28
  },
31
29
  failed: {
32
30
  tone: "error",
33
- icon: FailIcon
31
+ icon: FailFilledIcon
34
32
  },
35
33
  error: {
36
34
  tone: "error",
37
- icon: FailIcon
35
+ icon: FailFilledIcon
38
36
  },
39
37
  skipped: {
40
38
  tone: "muted",
@@ -46,21 +44,20 @@ var looks = {
46
44
  },
47
45
  canceled: {
48
46
  tone: "info",
49
- icon: CancelIcon
47
+ icon: CancelFilledIcon
50
48
  },
51
49
  faulted: {
52
50
  tone: "error",
53
- icon: RunningIcon,
54
- spin: true
51
+ icon: RunningRingIcon
55
52
  },
56
53
  canceling: {
57
54
  tone: "info",
58
- icon: RunningIcon,
59
- spin: true
55
+ icon: RunningRingIcon
60
56
  },
61
57
  "": {
62
58
  tone: "muted",
63
- icon: NotRunningIcon
59
+ icon: NotRunningIcon,
60
+ faint: true
64
61
  }
65
62
  };
66
63
  /** The tone a run status reads in, so a badge that says the status in words matches the
@@ -86,7 +83,7 @@ function RunStatusIcon({ status, label: ownLabel, tooltip = false, className, ..
86
83
  className: cx("flex h-2 items-center", toneIcon[look.tone], className),
87
84
  children: /* @__PURE__ */ jsx(Icon, {
88
85
  "aria-hidden": "true",
89
- className: cx(look.spin && "animate-spin rounded-full")
86
+ className: cx(look.faint && "opacity-55")
90
87
  })
91
88
  });
92
89
  return tooltip && label ? /* @__PURE__ */ jsx(Tooltip, {
@@ -72,10 +72,19 @@ export declare function CountBadge({ count, max, variant, emphasis, size, label,
72
72
  /** Any other span attribute and the ref reach its element, so it can be a Tooltip's or a popover's
73
73
  * direct child. */
74
74
  export interface StatusDotProps extends Omit<ComponentPropsWithRef<'span'>, 'children' | 'className'> {
75
+ /** The status's tone. */
75
76
  variant?: BadgeVariant | undefined;
77
+ /** A ping that ripples out from the dot, for something under way; still for reduced motion. */
76
78
  pulse?: boolean | undefined;
79
+ /** The dot's width and height in pixels, without its halo; 8 by default. */
77
80
  size?: number | undefined;
81
+ /** Takes the text's color with no halo, for a dot on a solid fill, where the tone would
82
+ * vanish into it. */
83
+ current?: boolean | undefined;
78
84
  className?: string | undefined;
79
85
  }
80
- export declare function StatusDot({ variant, pulse, size, className, style, ...rest }: StatusDotProps): import("react").JSX.Element;
86
+ /** A status's dot in its tone with a soft halo, for a status that stands beside its words
87
+ * without a badge, such as "running" in a list's cell. A badge's own dot is this one. Name
88
+ * the status in words beside it; the dot alone is only seen. */
89
+ export declare function StatusDot({ variant, pulse, size, current, className, style, ...rest }: StatusDotProps): import("react").JSX.Element;
81
90
  export {};
@@ -33,6 +33,15 @@ var dotFill = {
33
33
  ...toneFill,
34
34
  accent: "bg-(--theme-accent)"
35
35
  };
36
+ var dotHalo = {
37
+ default: "ring-2 ring-(--theme-app)/22",
38
+ success: "ring-2 ring-(--theme-success)/22",
39
+ warning: "ring-2 ring-(--theme-warning)/22",
40
+ error: "ring-2 ring-(--theme-error)/22",
41
+ info: "ring-2 ring-(--theme-info)/22",
42
+ muted: "ring-2 ring-(--theme-muted-text-color)/22",
43
+ accent: "ring-2 ring-(--theme-accent)/22"
44
+ };
36
45
  var sizeClasses = {
37
46
  sm: "gap-1 py-px text-[10px] leading-3.5",
38
47
  md: "gap-1.5 py-0.5 text-[11px] leading-4"
@@ -58,23 +67,21 @@ function statusTextClass(variant) {
58
67
  }
59
68
  /** A short status or label in a tone, such as "Running" or "Preview". */
60
69
  function StatusBadge({ children, variant = "default", emphasis = "soft", shape = "pill", caps = false, truncate = false, pulse = false, dot = false, size = "sm", render, className, ref, ...rest }) {
61
- const indicator = emphasis === "solid" ? "bg-current" : dotFill[variant];
62
70
  return useRender({
63
71
  render: render ?? /* @__PURE__ */ jsx("span", {}),
64
72
  ref,
65
73
  props: mergeProps(rest, {
66
74
  className: cx("inline-flex items-center border font-medium no-underline", "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--theme-focus)", sizeClasses[size], padClasses[shape][size], shapeClasses[shape], caps && "uppercase tracking-wider", truncate && "min-w-0 max-w-full", emphasisClasses[emphasis][variant], className),
67
- children: /* @__PURE__ */ jsxs(Fragment, { children: [
68
- pulse && /* @__PURE__ */ jsxs("span", {
69
- className: "relative flex h-2 w-2 shrink-0",
70
- children: [/* @__PURE__ */ jsx("span", { className: cx("absolute inline-flex h-full w-full animate-ping rounded-full opacity-75 motion-reduce:hidden", indicator) }), /* @__PURE__ */ jsx("span", { className: cx("relative inline-flex h-2 w-2 rounded-full", indicator) })]
71
- }),
72
- dot && !pulse && /* @__PURE__ */ jsx("span", { className: cx("inline-flex h-1.5 w-1.5 shrink-0 rounded-full", indicator) }),
73
- truncate ? /* @__PURE__ */ jsx("span", {
74
- className: "min-w-0 truncate",
75
- children
76
- }) : children
77
- ] })
75
+ children: /* @__PURE__ */ jsxs(Fragment, { children: [(pulse || dot) && /* @__PURE__ */ jsx(StatusDot, {
76
+ variant,
77
+ pulse,
78
+ size: pulse ? 8 : 6,
79
+ current: emphasis === "solid",
80
+ className: "shrink-0"
81
+ }), truncate ? /* @__PURE__ */ jsx("span", {
82
+ className: "min-w-0 truncate",
83
+ children
84
+ }) : children] })
78
85
  })
79
86
  });
80
87
  }
@@ -99,8 +106,13 @@ function CountBadge({ count, max = 99, variant = "accent", emphasis = "solid", s
99
106
  })]
100
107
  });
101
108
  }
102
- function StatusDot({ variant = "default", pulse = false, size = 8, className, style, ...rest }) {
109
+ /** A status's dot in its tone with a soft halo, for a status that stands beside its words
110
+ * without a badge, such as "running" in a list's cell. A badge's own dot is this one. Name
111
+ * the status in words beside it; the dot alone is only seen. */
112
+ function StatusDot({ variant = "default", pulse = false, size = 8, current = false, className, style, ...rest }) {
113
+ const fill = current ? "bg-current" : dotFill[variant];
103
114
  return /* @__PURE__ */ jsxs("span", {
115
+ "aria-hidden": "true",
104
116
  ...rest,
105
117
  className: cx("relative inline-flex", className),
106
118
  style: {
@@ -108,8 +120,8 @@ function StatusDot({ variant = "default", pulse = false, size = 8, className, st
108
120
  width: size,
109
121
  height: size
110
122
  },
111
- children: [pulse && /* @__PURE__ */ jsx("span", { className: cx("absolute inline-flex h-full w-full animate-ping rounded-full opacity-75 motion-reduce:hidden", dotFill[variant]) }), /* @__PURE__ */ jsx("span", {
112
- className: cx("relative inline-flex rounded-full", dotFill[variant]),
123
+ children: [pulse && /* @__PURE__ */ jsx("span", { className: cx("absolute inline-flex h-full w-full animate-ping rounded-full opacity-75 motion-reduce:hidden", fill) }), /* @__PURE__ */ jsx("span", {
124
+ className: cx("relative inline-flex rounded-full", fill, !current && dotHalo[variant]),
113
125
  style: {
114
126
  width: size,
115
127
  height: size
@@ -2,6 +2,8 @@ interface IBaseCheckmarkProps {
2
2
  className?: string;
3
3
  colored?: boolean;
4
4
  }
5
+ /** Something done: a solid disc in the success tone with a check cut out of it. */
5
6
  export declare function SuccessCheckmark({ className, colored }: IBaseCheckmarkProps): import("react").JSX.Element;
7
+ /** Something failed: a solid disc in the error tone with an x cut out of it. */
6
8
  export declare function ErrorCheckmark({ className, colored }: IBaseCheckmarkProps): import("react").JSX.Element;
7
9
  export {};