@ciridae/ui 1.0.3 → 1.0.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.
Files changed (124) hide show
  1. package/dist/chat/conversation-source.d.ts +15 -1
  2. package/dist/chat/describe-tool-call.d.ts +3 -3
  3. package/dist/chat/describe-tool-call.js +3 -3
  4. package/dist/chat/message-text.js +2 -2
  5. package/dist/ciridae.css +16 -4
  6. package/dist/components/activity-feed.d.ts +1 -0
  7. package/dist/components/activity-feed.js +1 -0
  8. package/dist/components/app-frame.d.ts +13 -1
  9. package/dist/components/app-frame.js +45 -3
  10. package/dist/components/app-navigation.d.ts +28 -1
  11. package/dist/components/app-navigation.js +110 -14
  12. package/dist/components/badge.d.ts +29 -4
  13. package/dist/components/badge.js +134 -15
  14. package/dist/components/bulk-action-bar.js +3 -3
  15. package/dist/components/button.d.ts +10 -3
  16. package/dist/components/button.js +77 -44
  17. package/dist/components/calendar.js +44 -17
  18. package/dist/components/centered-page.js +2 -2
  19. package/dist/components/chart.d.ts +1 -1
  20. package/dist/components/chart.js +16 -11
  21. package/dist/components/chat-client-source.js +47 -6
  22. package/dist/components/chat-sheet-controller.d.ts +1 -0
  23. package/dist/components/chat-sheet-controller.js +1 -0
  24. package/dist/components/chat-sheet.d.ts +10 -2
  25. package/dist/components/chat-sheet.js +74 -37
  26. package/dist/components/chat.d.ts +90 -14
  27. package/dist/components/chat.js +401 -101
  28. package/dist/components/checkbox-item.d.ts +5 -2
  29. package/dist/components/checkbox-item.js +9 -5
  30. package/dist/components/chip.d.ts +5 -0
  31. package/dist/components/chip.js +27 -1
  32. package/dist/components/column-settings.js +1 -1
  33. package/dist/components/command-menu.d.ts +5 -1
  34. package/dist/components/command-menu.js +98 -32
  35. package/dist/components/command-palette.d.ts +6 -2
  36. package/dist/components/command-palette.js +20 -10
  37. package/dist/components/data-display.d.ts +35 -4
  38. package/dist/components/data-display.js +138 -19
  39. package/dist/components/data-grid.d.ts +14 -2
  40. package/dist/components/data-grid.js +61 -26
  41. package/dist/components/date-range-picker.js +9 -20
  42. package/dist/components/drawer.d.ts +5 -0
  43. package/dist/components/drawer.js +91 -54
  44. package/dist/components/editable-field.d.ts +17 -3
  45. package/dist/components/editable-field.js +57 -27
  46. package/dist/components/email-input.js +6 -5
  47. package/dist/components/environment-frame.d.ts +1 -1
  48. package/dist/components/environment-frame.js +37 -6
  49. package/dist/components/filter-bar.js +39 -28
  50. package/dist/components/form-controls.d.ts +9 -0
  51. package/dist/components/form-controls.js +46 -22
  52. package/dist/components/form-layout.d.ts +7 -3
  53. package/dist/components/form-layout.js +68 -23
  54. package/dist/components/glass-menu.d.ts +3 -0
  55. package/dist/components/glass-menu.js +63 -0
  56. package/dist/components/gradient-edge.d.ts +9 -0
  57. package/dist/components/gradient-edge.js +31 -0
  58. package/dist/components/input.d.ts +4 -2
  59. package/dist/components/input.js +26 -15
  60. package/dist/components/keycap.d.ts +9 -5
  61. package/dist/components/keycap.js +57 -19
  62. package/dist/components/lightbox.js +2 -1
  63. package/dist/components/locale.d.ts +10 -0
  64. package/dist/components/locale.js +18 -2
  65. package/dist/components/log-entry.d.ts +40 -0
  66. package/dist/components/log-entry.js +146 -0
  67. package/dist/components/mention-input.js +4 -3
  68. package/dist/components/menu.d.ts +6 -2
  69. package/dist/components/menu.js +34 -37
  70. package/dist/components/meter.d.ts +9 -1
  71. package/dist/components/meter.js +64 -6
  72. package/dist/components/navigation.d.ts +21 -4
  73. package/dist/components/navigation.js +84 -19
  74. package/dist/components/notification-tray.js +5 -4
  75. package/dist/components/overlays.d.ts +3 -1
  76. package/dist/components/overlays.js +69 -40
  77. package/dist/components/page-frame.js +1 -1
  78. package/dist/components/panel.d.ts +1 -1
  79. package/dist/components/panel.js +18 -12
  80. package/dist/components/radio-card.js +10 -9
  81. package/dist/components/record.d.ts +25 -1
  82. package/dist/components/record.js +147 -18
  83. package/dist/components/segmented-bar.d.ts +3 -1
  84. package/dist/components/segmented-bar.js +16 -6
  85. package/dist/components/select.d.ts +5 -2
  86. package/dist/components/select.js +103 -127
  87. package/dist/components/shell-skeleton.js +1 -1
  88. package/dist/components/shell.d.ts +2 -2
  89. package/dist/components/shell.js +43 -45
  90. package/dist/components/sidebar.d.ts +7 -2
  91. package/dist/components/sidebar.js +55 -27
  92. package/dist/components/single-line-text.d.ts +2 -0
  93. package/dist/components/single-line-text.js +16 -0
  94. package/dist/components/site-banner.d.ts +5 -2
  95. package/dist/components/site-banner.js +17 -6
  96. package/dist/components/skeleton.d.ts +7 -6
  97. package/dist/components/skeleton.js +28 -16
  98. package/dist/components/skeletons.d.ts +12 -0
  99. package/dist/components/skeletons.js +71 -8
  100. package/dist/components/stages.d.ts +78 -6
  101. package/dist/components/stages.js +512 -156
  102. package/dist/components/stat-card.d.ts +10 -2
  103. package/dist/components/stat-card.js +92 -37
  104. package/dist/components/status-banner.js +1 -1
  105. package/dist/components/tabs.js +26 -13
  106. package/dist/components/theme-toggle.d.ts +4 -0
  107. package/dist/components/theme-toggle.js +67 -0
  108. package/dist/components/toast.d.ts +9 -2
  109. package/dist/components/toast.js +78 -29
  110. package/dist/preset.js +291 -33
  111. package/dist/styled-system/styles.css +3107 -1039
  112. package/dist/styled-system/tokens/index.mjs +602 -22
  113. package/dist/styled-system/tokens/tokens.d.ts +11 -11
  114. package/dist/styled-system/types/prop-type.d.ts +2 -2
  115. package/dist/styles.css +16 -4
  116. package/dist/theme/color.d.ts +3 -0
  117. package/dist/theme/color.js +5 -0
  118. package/dist/theme/derive.d.ts +9 -2
  119. package/dist/theme/derive.js +72 -12
  120. package/dist/theme/palettes.d.ts +49 -6
  121. package/dist/theme/palettes.js +84 -15
  122. package/dist/theme/tokens.d.ts +3 -3
  123. package/dist/theme/tokens.js +108 -32
  124. package/package.json +9 -1
@@ -17,7 +17,10 @@ export type SparklineProps = Omit<ComponentProps<"div">, "children"> & {
17
17
  strokeWidth?: number;
18
18
  endPointSize?: number;
19
19
  };
20
- /** A run of values as a line filling its box; decorative, as the figure beside it says what it shows. */
20
+ /**
21
+ * A run of values as a line filling its box; decorative, as the figure beside it says what it
22
+ * shows. Under two values there is no line, and a dashed rule stands in for it.
23
+ */
21
24
  export declare function Sparkline({ values, low: ownLow, high: ownHigh, strokeWidth, endPointSize, className, ...props }: SparklineProps): import("react").JSX.Element;
22
25
  export type MetricCardProps = Omit<useRender.ComponentProps<"div">, "children" | "onSelect"> & {
23
26
  label: ReactNode;
@@ -30,5 +33,10 @@ export type MetricCardProps = Omit<useRender.ComponentProps<"div">, "children" |
30
33
  onSelect?: () => void;
31
34
  /** Makes the card a link; a router's link as `render` routes it. */
32
35
  href?: string;
36
+ /**
37
+ * `card` (the default) frames it on its own; in a row of tiles, `left` rules its leading edge
38
+ * and `none` leaves it bare.
39
+ */
40
+ divider?: "card" | "left" | "none";
33
41
  };
34
- export declare function MetricCard({ label, value, change, sparkline, onSelect, href, render, className, ...props }: MetricCardProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
42
+ export declare function MetricCard({ label, value, change, sparkline, onSelect, href, divider, render, className, ...props }: MetricCardProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
@@ -2,12 +2,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  /*
3
3
  * Figures for a dashboard. `MetricCard` is one figure: its label, its value, how it changed
4
4
  * (`Trend`) and its recent run (`Sparkline`); it can open what it counts. `Sparkline` is a blue
5
- * line over a flat tinted area that stretches to its box: the stroke keeps its width and the end
6
- * point is HTML, so neither distorts.
5
+ * line over an area that fades to its foot, stretched to its box: the stroke keeps its width, and
6
+ * the area and end point are HTML, so none of them distorts.
7
7
  */
8
8
  import { useRender } from "@base-ui/react/use-render";
9
9
  import { ArrowDownIcon, ArrowRightIcon, ArrowUpIcon } from "@phosphor-icons/react";
10
- import { css, cx } from "#primitives/css";
10
+ import { css, cva, cx } from "#primitives/css";
11
11
  import { icon } from "./icon.js";
12
12
  import { cardSurface } from "./panel.js";
13
13
  /* A change beside a figure: which way it moved, and separately whether that is good. */
@@ -20,17 +20,28 @@ const trend = css({
20
20
  fontWeight: "medium",
21
21
  "&[data-tone=good]": { color: "accent.green" },
22
22
  "&[data-tone=bad]": { color: "accent.red" },
23
- "&[data-tone=neutral]": { color: "fg.muted" },
23
+ "&[data-tone=neutral]": { color: "fg.label" },
24
24
  "& > svg": { flexShrink: "0" },
25
25
  });
26
26
  const trendText = css({ minWidth: "0", overflow: "hidden", textOverflow: "ellipsis" });
27
27
  const TREND_ARROWS = { up: ArrowUpIcon, down: ArrowDownIcon, flat: ArrowRightIcon };
28
28
  export function Trend({ direction, tone = "neutral", arrow = true, children }) {
29
29
  const Arrow = TREND_ARROWS[direction];
30
- return (_jsxs("span", { className: trend, "data-tone": tone, "data-ciridae-ui": "", children: [arrow && _jsx(Arrow, { className: icon({ size: "compact" }), weight: "bold", "aria-hidden": "true" }), _jsx("span", { className: trendText, children: children })] }));
30
+ return (_jsxs("span", { className: trend, "data-tone": tone, "data-ciridae-ui": "", children: [arrow && _jsx(Arrow, { className: icon({ size: "compact" }), weight: "bold", "aria-hidden": "true" }), _jsx("span", { className: trendText, "data-trend-text": "", children: children })] }));
31
31
  }
32
- /* Sparkline: drawn into a 100x100 box that stretches to its container. */
33
- const plot = css({ position: "relative", color: "accent.blue" });
32
+ /*
33
+ * Sparkline: drawn into a 100x100 box that stretches to its plot. The plot fills the box unless
34
+ * `--sparkline-plot-width` and `--sparkline-plot-height` size it, at the box's foot and start, so
35
+ * the end point's ring has room above and after the line.
36
+ */
37
+ const plot = css({ position: "relative", color: "chart.trend" });
38
+ const plotArea = css({
39
+ position: "absolute",
40
+ insetInlineStart: "0",
41
+ bottom: "0",
42
+ width: "var(--sparkline-plot-width, 100%)",
43
+ height: "var(--sparkline-plot-height, 100%)",
44
+ });
34
45
  const svg = css({
35
46
  position: "absolute",
36
47
  inset: "0",
@@ -38,16 +49,31 @@ const svg = css({
38
49
  height: "100%",
39
50
  overflow: "visible",
40
51
  });
52
+ // The area under the line, clipped to it, so its gradient runs top to foot whatever the box.
53
+ const area = css({ position: "absolute", inset: "0", background: "token(gradients.chart.area)" });
54
+ // The ring is the tile's face, so the point reads as cut out of the line.
41
55
  const endPoint = css({
42
56
  position: "absolute",
43
57
  borderRadius: "round",
44
- background: "accent.blue",
58
+ background: "chart.trend",
45
59
  borderStyle: "solid",
46
- borderColor: "bg.card",
60
+ borderColor: "bg.surface",
47
61
  translate: "-50% -50%",
48
62
  });
49
- /** A run of values as a line filling its box; decorative, as the figure beside it says what it shows. */
63
+ // No run to draw: a dashed rule across the middle holds the line's place.
64
+ const noRun = css({
65
+ position: "absolute",
66
+ insetInline: "0",
67
+ top: "50%",
68
+ borderTop: "2px dashed token(colors.border.divider)",
69
+ });
70
+ /**
71
+ * A run of values as a line filling its box; decorative, as the figure beside it says what it
72
+ * shows. Under two values there is no line, and a dashed rule stands in for it.
73
+ */
50
74
  export function Sparkline({ values, low: ownLow, high: ownHigh, strokeWidth = 1.5, endPointSize = 6, className, ...props }) {
75
+ if (values.length < 2)
76
+ return (_jsx("div", { "aria-hidden": "true", ...props, className: cx(plot, className), "data-ciridae-ui": "", children: _jsx("span", { className: noRun }) }));
51
77
  const lowest = Math.min(...values);
52
78
  const high = ownHigh ?? Math.max(...values);
53
79
  const low = ownLow ?? lowest - (high - lowest) * 0.4;
@@ -56,31 +82,60 @@ export function Sparkline({ values, low: ownLow, high: ownHigh, strokeWidth = 1.
56
82
  const step = 100 / Math.max(values.length - 1, 1);
57
83
  const points = tops.map((top, index) => `${index * step},${top}`).join(" ");
58
84
  const lastTop = tops[tops.length - 1] ?? 100;
59
- return (_jsxs("div", { "aria-hidden": "true", ...props, className: cx(plot, className), "data-ciridae-ui": "", children: [_jsxs("svg", { className: svg, viewBox: "0 0 100 100", preserveAspectRatio: "none", children: [_jsx("polygon", { points: `0,100 ${points} 100,100`, fill: "currentColor", fillOpacity: 0.08 }), _jsx("polyline", { points: points, fill: "none", stroke: "currentColor", strokeWidth: strokeWidth, strokeLinejoin: "round", vectorEffect: "non-scaling-stroke" })] }), _jsx("span", { className: endPoint, style: {
60
- insetInlineStart: "100%",
61
- top: `${lastTop}%`,
62
- width: endPointSize,
63
- height: endPointSize,
64
- borderWidth: strokeWidth,
65
- } })] }));
85
+ const areaOutline = tops.map((top, index) => `${index * step}% ${top}%`).join(", ");
86
+ return (_jsx("div", { "aria-hidden": "true", ...props, className: cx(plot, className), "data-ciridae-ui": "", children: _jsxs("span", { className: plotArea, children: [_jsx("span", { className: area, style: { clipPath: `polygon(0% 100%, ${areaOutline}, 100% 100%)` } }), _jsx("svg", { className: svg, viewBox: "0 0 100 100", preserveAspectRatio: "none", children: _jsx("polyline", { points: points, fill: "none", stroke: "currentColor", strokeWidth: strokeWidth, strokeLinejoin: "round", vectorEffect: "non-scaling-stroke" }) }), _jsx("span", { className: endPoint, style: {
87
+ insetInlineStart: "100%",
88
+ top: `${lastTop}%`,
89
+ width: endPointSize,
90
+ height: endPointSize,
91
+ borderWidth: strokeWidth,
92
+ } })] }) }));
66
93
  }
67
- /* Metric card: a figure with its change, and its run beside it. */
68
- const metricCard = css(cardSurface, {
69
- display: "flex",
70
- gap: "layout.contentGap",
71
- padding: "panel.comfortable",
72
- color: "fg.primary",
73
- font: "inherit",
74
- textAlign: "start",
75
- textDecoration: "none",
76
- lineHeight: "tight",
77
- "&:is(button, a)": { cursor: "pointer", _hover: { borderColor: "border.glass" } },
78
- _focusVisible: { focusRing: "outer" },
79
- "& > [data-sparkline]": {
80
- width: "84px",
81
- height: "25px",
82
- flexShrink: "0",
83
- alignSelf: "flex-end",
94
+ /*
95
+ * Metric card: a figure with its change, and its run beside it. A tile in a row of them drops the
96
+ * card for a rule on its leading edge, or for nothing, and its label steps back to the label grey.
97
+ * The change's words are the label grey too, the kit's Stat tile: its arrow carries the tone.
98
+ * The run sits in the kit's 88x32 chart slot, its line in the slot's lower 84x25.
99
+ */
100
+ const metricCard = cva({
101
+ base: {
102
+ display: "flex",
103
+ gap: "layout.contentGap",
104
+ padding: "panel.comfortable",
105
+ color: "fg.primary",
106
+ font: "inherit",
107
+ textAlign: "start",
108
+ textDecoration: "none",
109
+ lineHeight: "tight",
110
+ "&:is(button, a)": { cursor: "pointer" },
111
+ _focusVisible: { focusRing: "outer" },
112
+ "& [data-trend-text]": { color: "fg.label" },
113
+ "& > [data-sparkline]": {
114
+ "--sparkline-plot-width": "token(sizes.sparkline.plotWidth)",
115
+ "--sparkline-plot-height": "token(sizes.sparkline.plotHeight)",
116
+ width: "sparkline.width",
117
+ height: "sparkline.height",
118
+ flexShrink: "0",
119
+ alignSelf: "flex-end",
120
+ },
121
+ },
122
+ variants: {
123
+ divider: {
124
+ card: { ...cardSurface, "&:is(button, a)": { _hover: { borderColor: "border.glass" } } },
125
+ left: {
126
+ minWidth: "0",
127
+ border: "0",
128
+ borderInlineStart: "1px solid token(colors.border.panel)",
129
+ background: "transparent",
130
+ "& [data-metric-label]": { color: "fg.label" },
131
+ },
132
+ none: {
133
+ minWidth: "0",
134
+ border: "0",
135
+ background: "transparent",
136
+ "& [data-metric-label]": { color: "fg.label" },
137
+ },
138
+ },
84
139
  },
85
140
  });
86
141
  const metricCopy = css({
@@ -97,7 +152,7 @@ const metricValue = css({
97
152
  lineHeight: "tight",
98
153
  fontVariantNumeric: "tabular-nums",
99
154
  });
100
- export function MetricCard({ label, value, change, sparkline, onSelect, href, render, className, ...props }) {
155
+ export function MetricCard({ label, value, change, sparkline, onSelect, href, divider = "card", render, className, ...props }) {
101
156
  return useRender({
102
157
  defaultTagName: href !== undefined ? "a" : onSelect !== undefined ? "button" : "div",
103
158
  render,
@@ -109,9 +164,9 @@ export function MetricCard({ label, value, change, sparkline, onSelect, href, re
109
164
  : {}),
110
165
  ...props,
111
166
  onClick: onSelect,
112
- className: cx(metricCard, className),
167
+ className: cx(metricCard({ divider }), className),
113
168
  "data-ciridae-ui": "",
114
- children: (_jsxs(_Fragment, { children: [_jsxs("span", { className: metricCopy, children: [_jsx("span", { className: metricLabel, children: label }), _jsx("span", { className: metricValue, children: value }), change && _jsx(Trend, { ...change })] }), sparkline && _jsx(Sparkline, { values: sparkline, "data-sparkline": "" })] })),
169
+ children: (_jsxs(_Fragment, { children: [_jsxs("span", { className: metricCopy, children: [_jsx("span", { className: metricLabel, "data-metric-label": "", children: label }), _jsx("span", { className: metricValue, children: value }), change && _jsx(Trend, { ...change })] }), sparkline && _jsx(Sparkline, { values: sparkline, "data-sparkline": "" })] })),
115
170
  },
116
171
  });
117
172
  }
@@ -70,7 +70,7 @@ const bannerIcon = cva({
70
70
  neutral: { color: "fg.muted" },
71
71
  info: { color: "accent.blue" },
72
72
  success: { color: "accent.green" },
73
- warning: { color: "accent.yellow" },
73
+ warning: { color: "accent.yellowIcon" },
74
74
  danger: { color: "accent.red" },
75
75
  },
76
76
  },
@@ -22,14 +22,15 @@ const list = css({
22
22
  overflowX: "auto",
23
23
  scrollbarWidth: "none",
24
24
  });
25
- /** Finalization "Menu Tabs": tabs sit inside a bordered pill with a hairline inset. */
25
+ /** Finalization "View tabs": tabs sit inside a grey pill well with a hairline inset. */
26
26
  const containedList = css({
27
27
  width: "max-content",
28
28
  padding: "control.trackInset",
29
29
  border: "1px solid token(colors.border.control)",
30
30
  borderRadius: "pill",
31
- background: "bg.control",
32
- gap: "control.trackInset",
31
+ background: "bg.field",
32
+ // Raised over the base list's own gap, as the text list's is.
33
+ "&[data-variant=contained]": { gap: "control.trackInset" },
33
34
  });
34
35
  /* Text tabs: words only, the open one in the primary text, for a tray's or a sheet's filters. */
35
36
  const textList = css({ "&[data-variant=text]": { gap: "layout.contentGap" } });
@@ -48,8 +49,9 @@ const tab = css({
48
49
  alignItems: "center",
49
50
  justifyContent: "center",
50
51
  gap: "control.iconGap",
51
- height: "control.small",
52
+ height: "control.restingTab",
52
53
  paddingInline: "control.tabInline",
54
+ // The open tab's edge's own 1px, kept at rest too so a tab keeps its width as it opens.
53
55
  border: "1px solid transparent",
54
56
  borderRadius: "pill",
55
57
  background: "transparent",
@@ -62,19 +64,26 @@ const tab = css({
62
64
  cursor: "pointer",
63
65
  flexShrink: "0",
64
66
  "&[data-icon-tone=bronze] svg": { color: "accent.bronze" },
65
- _before: { display: "none" },
67
+ // The open tab is raised on the secondary button's face, with its gradient edge and soft shadow.
68
+ _before: { display: "none", background: "token(gradients.border.raised)" },
66
69
  _hover: {
67
- background: "bg.hover",
70
+ background: "bg.controlHover",
68
71
  color: "fg.primary",
69
72
  "& svg": { color: "inherit" },
70
- _before: { display: "block" },
71
73
  },
72
74
  "&[data-active]": {
73
- background: "bg.selected",
75
+ background: "token(gradients.raised)",
76
+ boxShadow: "soft",
74
77
  color: "fg.primary",
75
78
  "& svg": { color: "inherit" },
76
79
  _before: { display: "block" },
77
80
  },
81
+ // On a face, the words read in the primary ink in light, where only the open tab is raised, and
82
+ // the open tab stands 2px taller than the rest.
83
+ "[data-variant=plain] > &, [data-variant=contained] > &": {
84
+ _light: { color: "fg.primary" },
85
+ "&[data-active]": { height: "control.small", _phone: { height: "control.compact" } },
86
+ },
78
87
  // Words only: no face, no inset, and focus underlines the word rather than boxing it.
79
88
  "[data-variant=text] > &": {
80
89
  height: "auto",
@@ -83,8 +92,8 @@ const tab = css({
83
92
  border: "0",
84
93
  background: "transparent",
85
94
  _hover: { background: "transparent" },
86
- "&[data-active]": { background: "transparent" },
87
- // No glass edge either: the open word is the only mark.
95
+ "&[data-active]": { background: "transparent", boxShadow: "none" },
96
+ // No edge either: the open word is the only mark.
88
97
  "&::before, &:hover::before, &[data-active]::before": { display: "none" },
89
98
  _focusVisible: {
90
99
  boxShadow: "none",
@@ -103,7 +112,11 @@ const tab = css({
103
112
  marginBottom: "-1px",
104
113
  background: "transparent",
105
114
  _hover: { background: "transparent" },
106
- "&[data-active]": { background: "transparent", borderBottomColor: "fg.primary" },
115
+ "&[data-active]": {
116
+ background: "transparent",
117
+ boxShadow: "none",
118
+ borderBottomColor: "fg.primary",
119
+ },
107
120
  "&::before, &:hover::before, &[data-active]::before": { display: "none" },
108
121
  _phone: { height: "control.search", paddingInline: "none" },
109
122
  },
@@ -190,7 +203,7 @@ export function TabsList({ className, variant = "plain", overflow: ownOverflow,
190
203
  const overflow = ownOverflow ?? (inBar ? "menu" : "scroll");
191
204
  const { labels } = useCiridaeLocale();
192
205
  const { listRef, moreRef, overflowed, activeHidden, activate } = useOverflowedTabs(overflow === "menu", children);
193
- return (_jsxs(BaseTabs.List, { ...props, ref: listRef, className: cx(list, variant === "contained" && containedList, variant === "text" && textList, variant === "underline" && underlineList, className), "data-ciridae-ui": "", "data-variant": variant, children: [children, overflow === "menu" && overflowed.length > 0 && (_jsx(Menu, { align: "end", trigger: _jsx("button", { type: "button", ref: moreRef, className: tab, "aria-label": labels.common.moreTabs, "data-ciridae-ui": "", "data-edge": "glass", "data-active": activeHidden || undefined, children: _jsx(DotsThreeIcon, { className: icon({ size: "small" }), weight: "bold" }) }), items: overflowed.map((item) => ({
206
+ return (_jsxs(BaseTabs.List, { ...props, ref: listRef, className: cx(list, variant === "contained" && containedList, variant === "text" && textList, variant === "underline" && underlineList, className), "data-ciridae-ui": "", "data-variant": variant, children: [children, overflow === "menu" && overflowed.length > 0 && (_jsx(Menu, { align: "end", trigger: _jsx("button", { type: "button", ref: moreRef, className: tab, "aria-label": labels.common.moreTabs, "data-ciridae-ui": "", "data-edge": "gradient", "data-active": activeHidden || undefined, children: _jsx(DotsThreeIcon, { className: icon({ size: "small" }), weight: "bold" }) }), items: overflowed.map((item) => ({
194
207
  label: item.label,
195
208
  meta: item.count,
196
209
  onSelect: () => activate(item.index),
@@ -198,7 +211,7 @@ export function TabsList({ className, variant = "plain", overflow: ownOverflow,
198
211
  }
199
212
  export function TabsTab({ className, iconTone, href, render, count: tabCount, children, ...props }) {
200
213
  const link = href !== undefined || render !== undefined;
201
- return (_jsxs(BaseTabs.Tab, { ...props, render: render ?? (href !== undefined ? _jsx("a", { href: href }) : undefined), nativeButton: link ? false : props.nativeButton, className: cx(tab, className), "data-ciridae-ui": "", "data-edge": "glass", "data-icon-tone": iconTone, children: [children, tabCount !== undefined && tabCount !== null && (_jsx("span", { className: count, "data-tab-count": "", children: tabCount }))] }));
214
+ return (_jsxs(BaseTabs.Tab, { ...props, render: render ?? (href !== undefined ? _jsx("a", { href: href }) : undefined), nativeButton: link ? false : props.nativeButton, className: cx(tab, className), "data-ciridae-ui": "", "data-edge": "gradient", "data-icon-tone": iconTone, children: [children, tabCount !== undefined && tabCount !== null && (_jsx("span", { className: count, "data-tab-count": "", children: tabCount }))] }));
202
215
  }
203
216
  /*
204
217
  * A page's tabs share one row with the open section's controls: tabs that do not fit go into the
@@ -0,0 +1,4 @@
1
+ /** Sets the color theme the `ThemeProvider` keeps: light, dark, or the operating system's. */
2
+ export declare function ThemeToggle({ className }: {
3
+ className?: string;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,67 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { CircleHalfIcon, MoonIcon, SunIcon } from "@phosphor-icons/react";
3
+ import { css, cx } from "#primitives/css";
4
+ import { useColorTheme } from "./color-theme.js";
5
+ import { faceEdge } from "./gradient-edge.js";
6
+ import { icon } from "./icon.js";
7
+ import { useCiridaeLocale } from "./locale.js";
8
+ /*
9
+ * Light, dark or the system's theme, as three icon segments in a pale track: the chosen one is
10
+ * raised on the secondary button's face, its icon in the primary ink, the others a step quieter.
11
+ */
12
+ const track = css({
13
+ display: "inline-flex",
14
+ alignItems: "center",
15
+ gap: "control.trackInset",
16
+ padding: "control.trackInset",
17
+ // The design lays the track's edge over its inset, and rounds it to its options' corners.
18
+ border: "0",
19
+ borderRadius: "calc(token(radii.inset) + token(spacing.control.trackInset))",
20
+ background: "token(gradients.panel)",
21
+ });
22
+ const option = css({
23
+ display: "inline-grid",
24
+ placeItems: "center",
25
+ width: "control.small",
26
+ height: "calc(token(sizes.control.small) - 2 * token(spacing.control.trackInset))",
27
+ padding: "none",
28
+ border: "0",
29
+ borderRadius: "inset",
30
+ background: "transparent",
31
+ color: "fg.secondary",
32
+ cursor: "pointer",
33
+ transitionProperty: "background, color",
34
+ transitionDuration: "fast",
35
+ _hover: { color: "fg.primary" },
36
+ _focusVisible: { focusRing: "outer" },
37
+ _before: { display: "none", background: "token(gradients.border.raised)" },
38
+ "&[aria-pressed=true]": {
39
+ background: "token(gradients.raised)",
40
+ color: "fg.primary",
41
+ _before: { display: "block" },
42
+ },
43
+ });
44
+ /** Sets the color theme the `ThemeProvider` keeps: light, dark, or the operating system's. */
45
+ export function ThemeToggle({ className }) {
46
+ const { preference, setPreference } = useColorTheme();
47
+ const { labels } = useCiridaeLocale();
48
+ const trackEdge = faceEdge("raised");
49
+ const choices = [
50
+ {
51
+ value: "light",
52
+ label: labels.themeCustomizer.light,
53
+ glyph: _jsx(SunIcon, { className: icon({ size: "small" }) }),
54
+ },
55
+ {
56
+ value: "dark",
57
+ label: labels.themeCustomizer.dark,
58
+ glyph: _jsx(MoonIcon, { className: icon({ size: "small" }) }),
59
+ },
60
+ {
61
+ value: "system",
62
+ label: labels.themeCustomizer.system,
63
+ glyph: _jsx(CircleHalfIcon, { className: icon({ size: "small" }) }),
64
+ },
65
+ ];
66
+ return (_jsx("div", { role: "group", "aria-label": labels.themeCustomizer.colorTheme, className: cx(track, trackEdge.className, className), "data-ciridae-ui": "", "data-edge": trackEdge["data-edge"], children: choices.map((choice) => (_jsx("button", { type: "button", "aria-label": choice.label, "aria-pressed": preference === choice.value, className: option, "data-ciridae-ui": "", "data-edge": "gradient", onClick: () => setPreference(choice.value), children: choice.glyph }, choice.value))) }));
67
+ }
@@ -11,11 +11,18 @@ export type ToastOptions = {
11
11
  variant?: ToastVariant;
12
12
  /** Milliseconds it stays; `0` keeps it until dismissed. An error stays by default. */
13
13
  timeout?: number;
14
- /** One text button, such as Undo; its children are the label. */
14
+ /**
15
+ * One small secondary button, such as Undo; its children are the label. It closes the toast, and
16
+ * takes the close button's place.
17
+ */
15
18
  actionProps?: Pick<ComponentProps<"button">, "children" | "onClick">;
19
+ /** Replaces the toast already shown with this id, such as a progress toast moving along. */
20
+ id?: string | number;
21
+ /** How far a `progress` toast is, 0 to 100, shown as a track and a percentage after its message. */
22
+ percent?: number;
16
23
  };
17
24
  /** Raises a toast with the kit's options, returning its id. */
18
- declare function addToast({ title: message, description: detail, variant, timeout, actionProps, }: ToastOptions): string | number;
25
+ declare function addToast({ title: ownMessage, description: detail, variant, timeout, actionProps, id, percent, }: ToastOptions): string | number;
19
26
  /** The kit's toast calls: `add` raises one and returns its id, `close` dismisses it. */
20
27
  export declare function useToast(): {
21
28
  add: typeof addToast;
@@ -1,30 +1,37 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /*
3
- * Toasts are sonner's, in the kit's look: a compact card with a tone icon, the message, an
4
- * optional muted line under it, an optional action and a close button. Mount one `Toaster` at the
5
- * app's root; raise toasts with `toast` from anywhere, or `useToast().add` with the kit's options.
3
+ * Toasts are sonner's, in the kit's look: a solid pill with a tone icon, the message, an optional
4
+ * muted line under it, and at its end either an action as a small secondary button or a close
5
+ * button. Mount
6
+ * one `Toaster` at the app's root; raise toasts with `toast` from anywhere, or `useToast().add`
7
+ * with the kit's options.
6
8
  */
7
9
  import { CheckCircleIcon, InfoIcon, SpinnerGapIcon, WarningCircleIcon, WarningIcon, XIcon, } from "@phosphor-icons/react";
8
10
  import { Toaster as SonnerToaster, toast } from "sonner";
9
11
  import { css, cx } from "#primitives/css";
12
+ import { Button } from "./button.js";
10
13
  import { icon, spinning } from "./icon.js";
11
14
  import { useCiridaeLocale } from "./locale.js";
12
15
  export { toast };
13
16
  const card = css({
14
17
  display: "flex",
15
- alignItems: "flex-start",
16
- gap: "layout.controlGap",
17
- width: "max-content",
18
- maxWidth: "min(360px, calc(100vw - token(spacing.layout.pageGap) * 2))",
18
+ alignItems: "center",
19
+ gap: "layout.contentGap",
20
+ width: "100%",
21
+ maxWidth: "calc(100vw - token(spacing.layout.pageGap) * 2)",
19
22
  paddingBlock: "control.block",
20
- paddingInline: "control.iconInline",
21
- border: "1px solid token(colors.border.control)",
22
- borderRadius: "action",
23
- background: "bg.card",
23
+ paddingInlineStart: "control.inline",
24
+ paddingInlineEnd: "control.block",
25
+ border: "1px solid token(colors.border.divider)",
26
+ borderRadius: "card",
27
+ // Solid in both themes: the panel's glass laid on the canvas in dark.
28
+ background: "linear-gradient(token(colors.bg.panel), token(colors.bg.panel)), token(colors.bg.canvas)",
29
+ _light: { background: "bg.surface" },
24
30
  color: "fg.primary",
25
- boxShadow: "toast",
31
+ boxShadow: "soft",
26
32
  textStyle: "body",
27
33
  fontFamily: "sans",
34
+ fontWeight: "medium",
28
35
  lineHeight: "compact",
29
36
  pointerEvents: "auto",
30
37
  });
@@ -43,18 +50,28 @@ const iconSlot = css({
43
50
  alignItems: "center",
44
51
  flexShrink: "0",
45
52
  });
53
+ /*
54
+ * An action raised through sonner's own `toast` with `{ label, onClick }`: the small secondary
55
+ * button's shape and face, with a flat hairline, since sonner draws the button. `useToast` draws a
56
+ * kit `Button` instead.
57
+ */
46
58
  const actionButton = css({
47
59
  flexShrink: "0",
48
- padding: "none",
49
- border: "0",
50
- background: "transparent",
51
- color: "accent.brand",
52
- font: "inherit",
60
+ height: "control.small",
61
+ paddingInline: "control.compactInline",
62
+ border: "1px solid token(colors.border.glass)",
63
+ borderRadius: "pill",
64
+ background: "token(gradients.raised)",
65
+ color: "fg.primary",
66
+ fontFamily: "inherit",
67
+ fontSize: "caption",
53
68
  fontWeight: "medium",
69
+ letterSpacing: "compact",
54
70
  cursor: "pointer",
55
- _hover: { textDecoration: "underline" },
71
+ _hover: { background: "bg.raisedHover" },
56
72
  _focusVisible: { focusRing: "outer" },
57
73
  });
74
+ const actionSlot = css({ flexShrink: "0" });
58
75
  // Sonner places its close button in the card's corner; the kit's sits at the end of the row.
59
76
  const closeButton = css({
60
77
  position: "static!",
@@ -62,8 +79,8 @@ const closeButton = css({
62
79
  display: "grid",
63
80
  placeItems: "center",
64
81
  flexShrink: "0",
65
- width: "control.mark",
66
- height: "control.mark",
82
+ width: "control.compact",
83
+ height: "control.compact",
67
84
  padding: "none",
68
85
  border: "0",
69
86
  borderRadius: "round",
@@ -74,12 +91,40 @@ const closeButton = css({
74
91
  _hover: { color: "fg.primary", background: "bg.hover" },
75
92
  _focusVisible: { focusRing: "outer" },
76
93
  });
94
+ /* A progress toast's track and how far along it is, after its message. */
95
+ const progressMessage = css({ display: "flex", alignItems: "center", gap: "layout.contentGap" });
96
+ const progressMessageText = css({ flex: "1", minWidth: "0" });
97
+ const progress = css({
98
+ display: "inline-flex",
99
+ alignItems: "center",
100
+ gap: "layout.controlGap",
101
+ flexShrink: "0",
102
+ });
103
+ const progressTrack = css({
104
+ width: "toast.progressWidth",
105
+ height: "toast.progressHeight",
106
+ overflow: "hidden",
107
+ borderRadius: "round",
108
+ background: "border.control",
109
+ _light: { background: "token(gradients.canvas)" },
110
+ });
111
+ const progressFill = css({ height: "100%", borderRadius: "round", background: "fg.primary" });
112
+ const progressPercent = css({
113
+ color: "fg.muted",
114
+ _light: { color: "fg.secondary" },
115
+ fontSize: "code",
116
+ fontVariantNumeric: "tabular-nums",
117
+ });
77
118
  const toneIcon = {
78
119
  success: css({ color: "accent.green" }),
79
120
  info: css({ color: "accent.blue" }),
80
- warning: css({ color: "accent.yellow" }),
121
+ warning: css({ color: "accent.yellowIcon" }),
81
122
  danger: css({ color: "accent.red" }),
82
123
  };
124
+ // Sonner sizes its stack by `--width`; the kit's toast is the design's width.
125
+ const toasterStyle = {
126
+ "--width": "var(--sizes-toast\\.width)",
127
+ };
83
128
  /** The toast stack, in the kit's look: mount one at the app's root. */
84
129
  export function Toaster(props) {
85
130
  const { labels, dir } = useCiridaeLocale();
@@ -92,8 +137,8 @@ export function Toaster(props) {
92
137
  warning: _jsx(WarningCircleIcon, { className: cx(small, toneIcon.warning), weight: "fill" }),
93
138
  error: _jsx(WarningIcon, { className: cx(small, toneIcon.danger), weight: "fill" }),
94
139
  loading: _jsx(SpinnerGapIcon, { className: cx(small, toneIcon.info, spinning) }),
95
- close: _jsx(XIcon, { className: icon({ size: "compact" }) }),
96
- }, containerAriaLabel: labels.notifications.title, ...props, toastOptions: {
140
+ close: _jsx(XIcon, { className: small }),
141
+ }, containerAriaLabel: labels.notifications.title, ...props, style: { ...toasterStyle, ...props.style }, toastOptions: {
97
142
  unstyled: true,
98
143
  ...props.toastOptions,
99
144
  classNames: {
@@ -108,16 +153,20 @@ export function Toaster(props) {
108
153
  },
109
154
  } }));
110
155
  }
156
+ let lastToastId = 0;
111
157
  /** Raises a toast with the kit's options, returning its id. */
112
- function addToast({ title: message, description: detail, variant, timeout, actionProps, }) {
158
+ function addToast({ title: ownMessage, description: detail, variant, timeout, actionProps, id = `ciridae-toast-${++lastToastId}`, percent, }) {
113
159
  const duration = timeout ?? (variant === "error" ? 0 : undefined);
160
+ const message = percent === undefined ? (ownMessage) : (_jsxs("span", { className: progressMessage, children: [_jsx("span", { className: progressMessageText, children: ownMessage }), _jsxs("span", { className: progress, children: [_jsx("span", { className: progressTrack, children: _jsx("span", { className: progressFill, style: { width: `${percent}%` } }) }), _jsxs("span", { className: progressPercent, children: [Math.round(percent), "%"] })] })] }));
114
161
  const options = {
162
+ id,
115
163
  description: detail,
116
164
  duration: duration === 0 ? Infinity : duration,
117
- action: actionProps && {
118
- label: actionProps.children,
119
- onClick: (event) => actionProps.onClick?.(event),
120
- },
165
+ closeButton: actionProps === undefined,
166
+ action: actionProps && (_jsx("span", { className: actionSlot, children: _jsx(Button, { variant: "secondary", size: "small", onClick: (event) => {
167
+ actionProps.onClick?.(event);
168
+ toast.dismiss(id);
169
+ }, children: actionProps.children }) })),
121
170
  };
122
171
  switch (variant) {
123
172
  case "success":