@ciridae/ui 1.0.3 → 1.0.4

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 +105 -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 +76 -39
  26. package/dist/components/chat.d.ts +90 -14
  27. package/dist/components/chat.js +402 -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 +12 -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 +289 -33
  111. package/dist/styled-system/styles.css +3115 -1044
  112. package/dist/styled-system/tokens/index.mjs +598 -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 +107 -32
  124. package/package.json +9 -1
@@ -1,12 +1,21 @@
1
- import { Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  /*
3
3
  * A badge: a short label, usually with an icon, that says what state something is in. Its
4
4
  * `tone` carries the meaning: `warning`, `danger`, `success` and `info` tint the glass toward
5
- * their hue, `neutral` stays grey. A `pill` carries a judgment, such as a verdict or a run mode;
6
- * a `square` badge carries a fact, such as where a value came from, in a field's flat well.
5
+ * their hue, `neutral` stays grey, and `ink` is the solid face of what a run actually did. A
6
+ * `pill` carries a judgment, such as a verdict or a run mode; a `square` badge carries a fact,
7
+ * such as where a value came from, in a field's flat well. `LifecycleBadge` is the pill for where
8
+ * a record sits in its stages, and `BadgeGlyph` and `AssuranceGlyph` the small marks badges and
9
+ * fields put in their icon slot.
7
10
  */
8
11
  import { useRender } from "@base-ui/react/use-render";
9
- import { cva, cx } from "#primitives/css";
12
+ import { CheckIcon, ClockIcon, HourglassIcon, SparkleIcon, StopIcon } from "@phosphor-icons/react";
13
+ import { css, cva, cx } from "#primitives/css";
14
+ /*
15
+ * A tinted pill is its hue's glass, falling from light to its hue, inside that hue's gradient
16
+ * hairline, which the `[data-edge="gradient"]` ring draws from `--edge`. Neutral is the raised
17
+ * glass, warning the tinted button's warm glass, and ink the primary face.
18
+ */
10
19
  const badge = cva({
11
20
  base: {
12
21
  display: "inline-flex",
@@ -29,16 +38,23 @@ const badge = cva({
29
38
  variants: {
30
39
  shape: {
31
40
  pill: { borderRadius: "pill" },
41
+ // The well of a form field in dark, the card's face in light, lettered in the label grey.
32
42
  square: {
33
43
  borderRadius: "control",
34
44
  borderColor: "border.control",
35
- background: "bg.field",
36
- _light: { borderColor: "border.mark", background: "bg.card" },
45
+ background: "bg.formField",
46
+ color: "fg.label",
47
+ _light: { background: "bg.card" },
37
48
  },
38
49
  },
39
- // A fact told in passing: the icon and the words, muted, with no box; a tone tints the icon.
50
+ /*
51
+ * `filled` is the tone's glass; `outline` the card's face inside the tone's edge, as an
52
+ * outcome that changed nothing; `bare` a fact told in passing: the icon and the words, muted,
53
+ * with no box, the tone on the icon alone.
54
+ */
40
55
  appearance: {
41
56
  filled: {},
57
+ outline: {},
42
58
  bare: {
43
59
  height: "auto",
44
60
  gap: "layout.tightGap",
@@ -54,21 +70,80 @@ const badge = cva({
54
70
  neutral: {},
55
71
  info: { color: "accent.blue" },
56
72
  success: { color: "accent.green" },
57
- warning: { color: "accent.yellow" },
73
+ warning: { color: "accent.yellow", "& svg": { color: "accent.yellowIcon" } },
58
74
  danger: { color: "accent.red" },
75
+ ink: { color: "fg.primary" },
59
76
  },
60
77
  },
61
78
  compoundVariants: [
62
- { appearance: "filled", shape: "pill", tone: "neutral", css: { background: "bg.raised" } },
63
- { appearance: "filled", shape: "pill", tone: "info", css: { background: "bg.infoSubtle" } },
79
+ {
80
+ appearance: "filled",
81
+ shape: "pill",
82
+ tone: "neutral",
83
+ css: {
84
+ background: "token(gradients.raised)",
85
+ color: "fg.label",
86
+ "--edge": "token(gradients.border.raised)",
87
+ },
88
+ },
89
+ {
90
+ appearance: "filled",
91
+ shape: "pill",
92
+ tone: "info",
93
+ css: {
94
+ background: "token(gradients.infoGlass)",
95
+ "--edge": "token(gradients.border.infoGlass)",
96
+ },
97
+ },
64
98
  {
65
99
  appearance: "filled",
66
100
  shape: "pill",
67
101
  tone: "success",
68
- css: { background: "bg.successSubtle" },
102
+ css: {
103
+ background: "token(gradients.successGlass)",
104
+ "--edge": "token(gradients.border.successGlass)",
105
+ },
106
+ },
107
+ {
108
+ appearance: "filled",
109
+ shape: "pill",
110
+ tone: "warning",
111
+ css: {
112
+ background: "token(gradients.tinted)",
113
+ "--edge": "token(gradients.border.raised)",
114
+ _light: { "--edge": "token(gradients.border.tinted)" },
115
+ },
116
+ },
117
+ {
118
+ appearance: "filled",
119
+ shape: "pill",
120
+ tone: "danger",
121
+ css: {
122
+ background: "token(gradients.dangerGlass)",
123
+ "--edge": "token(gradients.border.dangerGlass)",
124
+ },
125
+ },
126
+ {
127
+ appearance: "filled",
128
+ shape: "pill",
129
+ tone: "ink",
130
+ css: {
131
+ background: "token(gradients.primary)",
132
+ color: "bg.canvas",
133
+ "--edge": "token(gradients.border.primary)",
134
+ _light: { color: "fg.onAccent" },
135
+ },
136
+ },
137
+ {
138
+ appearance: "outline",
139
+ shape: "pill",
140
+ css: {
141
+ background: "token(gradients.raised)",
142
+ color: "fg.primary",
143
+ "--edge": "token(gradients.border.raised)",
144
+ _light: { background: "bg.card", "--edge": "token(gradients.border.primary)" },
145
+ },
69
146
  },
70
- { appearance: "filled", shape: "pill", tone: "warning", css: { background: "bg.tinted" } },
71
- { appearance: "filled", shape: "pill", tone: "danger", css: { background: "bg.errorSubtle" } },
72
147
  {
73
148
  appearance: "bare",
74
149
  tone: "info",
@@ -82,7 +157,7 @@ const badge = cva({
82
157
  {
83
158
  appearance: "bare",
84
159
  tone: "warning",
85
- css: { color: "fg.muted", "& svg": { color: "accent.yellow" } },
160
+ css: { color: "fg.muted", "& svg": { color: "accent.yellowIcon" } },
86
161
  },
87
162
  {
88
163
  appearance: "bare",
@@ -100,8 +175,52 @@ export function Badge({ tone = "neutral", shape = "pill", appearance = "filled",
100
175
  ...props,
101
176
  className: cx(badge({ tone, shape, appearance }), className),
102
177
  "data-ciridae-ui": "",
103
- "data-edge": shape === "pill" && appearance === "filled" ? "glass" : undefined,
178
+ "data-edge": shape === "pill" && appearance !== "bare" ? "gradient" : undefined,
104
179
  children: (_jsxs(_Fragment, { children: [icon, children] })),
105
180
  },
106
181
  });
107
182
  }
183
+ /*
184
+ * The glyphs the kit draws itself, in a 14px box (12px for the progress step's check and stop),
185
+ * in the color of the badge's or field's label.
186
+ */
187
+ const glyph = css({ flexShrink: "0", width: "1em", height: "1em", fontSize: "icon.small" });
188
+ const smallGlyph = css({ flexShrink: "0", width: "1em", height: "1em", fontSize: "icon.compact" });
189
+ /** A badge's drawn glyph: a lifecycle position, a contested verdict, an unchanged outcome. */
190
+ export function BadgeGlyph({ kind, className, ...props }) {
191
+ const small = kind === "check" || kind === "stop";
192
+ return (_jsxs("svg", { "aria-hidden": "true", viewBox: small ? "0 0 12 12" : "0 0 14 14", fill: "none", ...props, className: cx(small ? smallGlyph : glyph, className), children: [kind === "notReached" && (_jsx("circle", { cx: "7", cy: "7", r: "3.875", stroke: "currentColor", strokeWidth: "1.25", strokeDasharray: "2 1.6" })), kind === "hereNow" && _jsx("circle", { cx: "7", cy: "7", r: "3", fill: "currentColor" }), kind === "terminal" && (_jsx("rect", { x: "3.5", y: "3.5", width: "7", height: "7", rx: "1.5", fill: "currentColor" })), kind === "contested" && (_jsx("circle", { cx: "7", cy: "7", r: "3.875", fill: "currentColor", stroke: "currentColor", strokeWidth: "1.25" })), kind === "noChange" && (_jsx("circle", { cx: "7", cy: "7", r: "3.375", stroke: "currentColor", strokeWidth: "1.25" })), kind === "check" && (_jsx("path", { d: "M2.4 6.2 4.6 8.5 9.7 3.4", stroke: "currentColor", strokeWidth: "0.857", strokeLinecap: "round", strokeLinejoin: "round" })), kind === "stop" && (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "6", cy: "6", r: "3.95", stroke: "currentColor", strokeWidth: "0.857" }), _jsx("path", { d: "M4.1 6h3.8", stroke: "currentColor", strokeWidth: "0.857", strokeLinecap: "round" })] }))] }));
193
+ }
194
+ const ASSURANCE_BARS = [
195
+ { x: 2, y: 7.5, height: 5 },
196
+ { x: 5.8, y: 4.5, height: 8 },
197
+ { x: 9.5, y: 1.5, height: 11 },
198
+ ];
199
+ /** A three-bar meter, lit to the level, so a value's assurance reads without color. */
200
+ export function AssuranceGlyph({ level, className, ...props }) {
201
+ return (_jsx("svg", { "aria-hidden": "true", viewBox: "0 0 14 14", fill: "currentColor", ...props, className: cx(glyph, className), children: ASSURANCE_BARS.map((bar, index) => (_jsx("rect", { x: bar.x, y: bar.y, width: "2.5", height: bar.height, rx: "0.75", opacity: index < level ? 1 : 0.28 }, bar.x))) }));
202
+ }
203
+ const LIFECYCLE_TONES = {
204
+ notReached: "neutral",
205
+ hereNow: "info",
206
+ done: "success",
207
+ blocked: "warning",
208
+ agentWaiting: "info",
209
+ expired: "neutral",
210
+ terminal: "neutral",
211
+ awaiting: "warning",
212
+ };
213
+ const LIFECYCLE_GLYPHS = {
214
+ notReached: _jsx(BadgeGlyph, { kind: "notReached" }),
215
+ hereNow: _jsx(BadgeGlyph, { kind: "hereNow" }),
216
+ done: _jsx(CheckIcon, { weight: "bold" }),
217
+ blocked: _jsx(StopIcon, { weight: "bold" }),
218
+ agentWaiting: _jsx(SparkleIcon, { weight: "fill" }),
219
+ expired: _jsx(ClockIcon, { weight: "bold" }),
220
+ terminal: _jsx(BadgeGlyph, { kind: "terminal" }),
221
+ awaiting: _jsx(HourglassIcon, { weight: "bold" }),
222
+ };
223
+ /** A lifecycle pill: the state picks the glyph and the hue; the words are the stage's name. */
224
+ export function LifecycleBadge({ state, ...props }) {
225
+ return _jsx(Badge, { ...props, tone: LIFECYCLE_TONES[state], icon: LIFECYCLE_GLYPHS[state] });
226
+ }
@@ -14,13 +14,13 @@ const bar = css({
14
14
  paddingBlock: "panel.compact",
15
15
  paddingInlineStart: "panel.comfortable",
16
16
  paddingInlineEnd: "panel.compact",
17
- border: "1px solid token(colors.border.panel)",
17
+ border: "1px solid token(colors.border.divider)",
18
18
  borderRadius: "action",
19
- background: "bg.hover",
19
+ background: "token(gradients.card)",
20
20
  fontSize: "body",
21
21
  fontWeight: "medium",
22
22
  letterSpacing: "compact",
23
- _light: { background: "bg.card" },
23
+ lineHeight: "tight",
24
24
  });
25
25
  const leading = css({
26
26
  display: "flex",
@@ -13,7 +13,12 @@ export type ButtonProps = Omit<BaseButtonProps, "className"> & {
13
13
  /** The hotkey that does what the button does, shown after its label; the host binds it. */
14
14
  shortcut?: Hotkey;
15
15
  };
16
- export declare function Button({ className, variant, size, fullWidth, loading, icon, shortcut, children, disabled, ...props }: ButtonProps): import("react").JSX.Element;
16
+ /**
17
+ * The size a `Button` takes where it does not set its own, so the container decides: a dialog's
18
+ * footer holds the default 38px buttons, where everywhere else the small one is the everyday.
19
+ */
20
+ export declare const ButtonSizeContext: import("react").Context<NonNullable<"medium" | "large" | "small" | undefined>>;
21
+ export declare function Button({ className, variant, size: ownSize, fullWidth, loading, icon, shortcut, children, disabled, ...props }: ButtonProps): import("react").JSX.Element;
17
22
  export type DataButtonState = "idle" | "pending" | "error" | "success";
18
23
  export type DataButtonProps = Omit<ButtonProps, "children" | "icon" | "loading"> & {
19
24
  state: DataButtonState;
@@ -25,7 +30,7 @@ export type DataButtonProps = Omit<ButtonProps, "children" | "icon" | "loading">
25
30
  };
26
31
  /** Async action button with consistent pending, retry, and completion feedback. */
27
32
  export declare function DataButton({ state, idleLabel, pendingLabel, errorLabel, successLabel, variant, icon, disabled, ...props }: DataButtonProps): import("react").JSX.Element;
28
- type IconButtonSize = "compact" | "small" | "medium" | "large";
33
+ type IconButtonSize = "compact" | "send" | "small" | "medium" | "large";
29
34
  /**
30
35
  * The size an `IconButton` takes where it does not set its own, so the container decides: the
31
36
  * sidebar rail's icons are large, and the same icon shown in the phone header matches the header's
@@ -37,6 +42,8 @@ export type IconButtonProps = Omit<BaseButtonProps, "className" | "aria-label">
37
42
  "aria-label": string;
38
43
  size?: IconButtonSize;
39
44
  variant?: "raised" | "ghost" | "primary";
45
+ /** `square` is the 8-round square that leads a page title; buttons are otherwise round. */
46
+ shape?: "round" | "square";
40
47
  /**
41
48
  * `true` marks the button with a dot; a number shows that count, nothing at zero, and joins the
42
49
  * button's name so assistive tech hears it too.
@@ -48,5 +55,5 @@ export type IconButtonProps = Omit<BaseButtonProps, "className" | "aria-label">
48
55
  */
49
56
  shortcut?: Hotkey;
50
57
  };
51
- export declare function IconButton({ className, size, variant, badge, shortcut, children, "aria-label": label, ...props }: IconButtonProps): import("react").JSX.Element;
58
+ export declare function IconButton({ className, size, variant, shape, badge, shortcut, children, "aria-label": label, ...props }: IconButtonProps): import("react").JSX.Element;
52
59
  export {};
@@ -3,10 +3,14 @@ import { Button as BaseButton } from "@base-ui/react/button";
3
3
  import { CheckCircleIcon, WarningCircleIcon } from "@phosphor-icons/react";
4
4
  import { createContext, useContext } from "react";
5
5
  import { css, cva, cx } from "#primitives/css";
6
+ import { faceEdge } from "./gradient-edge.js";
6
7
  import { formatForDisplay } from "./hotkeys.js";
7
8
  import { icon as iconStyle, Spinner } from "./icon.js";
8
9
  import { ShortcutHint } from "./keycap.js";
9
10
  import { useCiridaeLocale } from "./locale.js";
11
+ /* The hover wash laid over a gradient face, as the design's state layer sits over it. */
12
+ const raisedHoverLight = "linear-gradient(token(colors.bg.hover), token(colors.bg.hover)), token(gradients.raised)";
13
+ const tintedHover = "linear-gradient(token(colors.bg.hover), token(colors.bg.hover)), token(gradients.tinted)";
10
14
  const action = cva({
11
15
  base: {
12
16
  display: "inline-flex",
@@ -14,8 +18,10 @@ const action = cva({
14
18
  alignItems: "center",
15
19
  justifyContent: "center",
16
20
  gap: "control.iconGap",
17
- border: "1px solid",
18
- borderRadius: "action",
21
+ // The design lays a button's edge over its padding rather than inside it, so no border.
22
+ border: "0",
23
+ // Buttons are pills at both heights.
24
+ borderRadius: "pill",
19
25
  height: "control.default",
20
26
  paddingInline: "control.inline",
21
27
  fontFamily: "sans",
@@ -36,67 +42,67 @@ const action = cva({
36
42
  },
37
43
  variants: {
38
44
  variant: {
39
- // Round 2 buttons cast no shadow: each face is told apart by its fill and its edge alone.
45
+ /*
46
+ * Buttons cast no shadow: each face is told apart by its gradient fill and its gradient
47
+ * edge alone. Light has no hover on the primary face; dark lets a little of the surface
48
+ * through. A raised face takes the hover wash over its gradient in light, and thins in dark.
49
+ */
40
50
  primary: {
41
- background: "bg.primary",
51
+ background: "token(gradients.primary)",
42
52
  color: "bg.canvas",
43
53
  _light: { color: "fg.onAccent" },
44
- borderColor: "transparent",
45
- _hover: { background: "bg.primaryHover" },
54
+ _hover: {
55
+ background: "bg.primaryHover",
56
+ _light: { background: "token(gradients.primary)" },
57
+ },
46
58
  _active: { translate: "0 1px" },
47
59
  },
48
60
  secondary: {
49
- background: "bg.raised",
61
+ background: "token(gradients.raised)",
50
62
  color: "fg.primary",
51
- borderColor: "transparent",
52
- _hover: { background: "bg.raisedHover" },
63
+ _hover: { background: "bg.raisedHover", _light: { background: raisedHoverLight } },
53
64
  _active: { translate: "0 1px" },
54
65
  },
55
66
  subtle: {
56
67
  background: "bg.fill",
57
68
  color: "fg.muted",
58
- borderColor: "transparent",
59
69
  _hover: { color: "fg.primary" },
60
70
  _active: { translate: "0 1px" },
61
71
  },
62
- // White in dark, muted in light: the one face whose text follows the theme differently.
72
+ // White in dark, the secondary grey in light: the one face whose text follows the theme
73
+ // differently.
63
74
  ghost: {
64
75
  background: "transparent",
65
76
  color: "fg.primary",
66
- _light: { color: "fg.muted" },
67
- borderColor: "transparent",
77
+ _light: { color: "fg.secondary" },
68
78
  _hover: { color: "fg.primary", background: "bg.hover" },
69
79
  },
70
80
  // Warm glass with an amber edge and the warm accent for its text.
71
81
  tinted: {
72
- background: "bg.tinted",
82
+ background: "token(gradients.tinted)",
73
83
  color: "accent.yellow",
74
84
  _light: { color: "accent.bronze" },
75
- borderColor: "transparent",
76
- _hover: { background: "bg.raisedHover" },
85
+ _hover: { background: tintedHover },
77
86
  _active: { translate: "0 1px" },
78
87
  },
79
88
  // The secondary face with red text, for a destructive action that asks for confirmation:
80
89
  // it names the risk without being the loudest thing on the page. The confirmation's own
81
90
  // button is `danger`.
82
91
  secondaryDanger: {
83
- background: "bg.raised",
92
+ background: "token(gradients.raised)",
84
93
  color: "accent.red",
85
- borderColor: "transparent",
86
- _hover: { background: "bg.raisedHover" },
94
+ _hover: { background: "bg.raisedHover", _light: { background: raisedHoverLight } },
87
95
  _active: { translate: "0 1px" },
88
96
  },
89
97
  danger: {
90
- background: "bg.danger",
98
+ background: "token(gradients.danger)",
91
99
  color: "fg.onAccent",
92
- borderColor: "transparent",
93
100
  _hover: { filter: "brightness(1.05)" },
94
101
  _active: { translate: "0 1px" },
95
102
  },
96
103
  success: {
97
104
  background: "accent.green",
98
105
  color: "bg.canvas",
99
- borderColor: "transparent",
100
106
  _hover: { filter: "brightness(1.05)" },
101
107
  _active: { translate: "0 1px" },
102
108
  },
@@ -104,8 +110,6 @@ const action = cva({
104
110
  link: {
105
111
  background: "transparent",
106
112
  color: "fg.primary",
107
- borderColor: "transparent",
108
- borderWidth: "0",
109
113
  fontFamily: "inherit",
110
114
  fontWeight: "inherit",
111
115
  letterSpacing: "inherit",
@@ -134,22 +138,30 @@ const action = cva({
134
138
  // Small is the everyday button; medium and large are for a page's one main action.
135
139
  defaultVariants: { variant: "primary", size: "small" },
136
140
  });
137
- // The glass faces draw a hairline, which is their outline; a filled face draws none.
141
+ // Each gradient face draws its own gradient edge; the flat faces draw a hairline or none.
138
142
  const buttonEdges = {
139
- primary: undefined,
140
- secondary: "glass",
143
+ primary: "primary",
144
+ secondary: "raised",
141
145
  subtle: "glass",
142
146
  ghost: undefined,
143
- tinted: undefined,
144
- secondaryDanger: "glass",
145
- danger: undefined,
147
+ tinted: "tinted",
148
+ secondaryDanger: "raised",
149
+ danger: "danger",
146
150
  success: undefined,
147
151
  link: undefined,
148
152
  };
149
153
  // In the button's own color, quieter than its label.
150
154
  const buttonHint = css({ color: "inherit", opacity: "0.6" });
151
- export function Button({ className, variant = "primary", size = "small", fullWidth, loading, icon, shortcut, children, disabled, ...props }) {
152
- return (_jsxs(BaseButton, { ...props, disabled: disabled || loading, "aria-busy": loading || undefined, className: cx(action({ variant, size, fullWidth }), className), "data-ciridae-ui": "", "data-edge": buttonEdges[variant], children: [loading ? _jsx(Spinner, { "aria-hidden": "true" }) : icon, children, shortcut && _jsx(ShortcutHint, { hotkey: shortcut, className: buttonHint })] }));
155
+ /**
156
+ * The size a `Button` takes where it does not set its own, so the container decides: a dialog's
157
+ * footer holds the default 38px buttons, where everywhere else the small one is the everyday.
158
+ */
159
+ export const ButtonSizeContext = createContext("small");
160
+ export function Button({ className, variant = "primary", size: ownSize, fullWidth, loading, icon, shortcut, children, disabled, ...props }) {
161
+ const containerSize = useContext(ButtonSizeContext);
162
+ const size = ownSize ?? containerSize;
163
+ const edge = faceEdge(buttonEdges[variant]);
164
+ return (_jsxs(BaseButton, { ...props, disabled: disabled || loading, "aria-busy": loading || undefined, className: cx(action({ variant, size, fullWidth }), edge.className, className), "data-ciridae-ui": "", "data-edge": edge["data-edge"], children: [loading ? _jsx(Spinner, { "aria-hidden": "true" }) : icon, children, shortcut && _jsx(ShortcutHint, { hotkey: shortcut, className: buttonHint })] }));
153
165
  }
154
166
  /** Async action button with consistent pending, retry, and completion feedback. */
155
167
  export function DataButton({ state, idleLabel, pendingLabel, errorLabel, successLabel, variant = "primary", icon, disabled, ...props }) {
@@ -183,19 +195,22 @@ const round = cva({
183
195
  alignItems: "center",
184
196
  justifyContent: "center",
185
197
  padding: "none",
198
+ // The edge's own 1px, as the design counts it; the bare button has neither.
186
199
  border: "1px solid transparent",
187
200
  borderRadius: "pill",
188
201
  color: "fg.muted",
189
- background: "bg.raised",
202
+ background: "token(gradients.raised)",
190
203
  cursor: "pointer",
191
204
  flexShrink: "0",
192
205
  transitionProperty: "translate, background, color",
193
206
  transitionDuration: "fast",
194
- _hover: { color: "fg.primary", background: "bg.raisedHover" },
207
+ // Hover keeps the face in light and thins it in dark; the icon takes the primary ink.
208
+ _hover: { color: "fg.primary", background: "token(gradients.raisedHover)" },
195
209
  _active: { translate: "0 1px" },
196
210
  _focusVisible: { focusRing: "outer" },
211
+ // A round face fades further than other controls when it cannot be pressed.
197
212
  "&:disabled, &[data-disabled]": {
198
- opacity: "disabled",
213
+ opacity: "disabledRound",
199
214
  cursor: "not-allowed",
200
215
  translate: "none",
201
216
  },
@@ -203,27 +218,44 @@ const round = cva({
203
218
  variants: {
204
219
  variant: {
205
220
  raised: {},
221
+ // The bare icon button: no face until hovered, and disabled it fades as other controls do.
206
222
  ghost: {
223
+ border: "0",
207
224
  background: "transparent",
208
- _hover: { background: "bg.hover" },
225
+ _hover: { background: "bg.hover", _light: { background: "bg.hover" } },
209
226
  _active: { translate: "none" },
227
+ "&:disabled, &[data-disabled]": { opacity: "disabled" },
210
228
  },
211
229
  // The primary face as a round button: a composer's send.
212
230
  primary: {
213
- background: "bg.primary",
231
+ background: "token(gradients.primary)",
214
232
  color: "bg.canvas",
215
233
  _light: { color: "fg.onAccent" },
216
- _hover: { color: "bg.canvas", background: "bg.primaryHover" },
234
+ _hover: {
235
+ color: "bg.canvas",
236
+ background: "bg.primaryHover",
237
+ _light: { color: "fg.onAccent", background: "token(gradients.primary)" },
238
+ },
217
239
  },
218
240
  },
241
+ // 24 is the bare button's in a column or group header; 26 the composer's send on the
242
+ // primary face; the other round faces are 28, 34 and 38.
219
243
  size: {
220
244
  compact: { width: "control.shortcut", height: "control.shortcut" },
221
- small: { width: "control.small", height: "control.small" },
222
- medium: { width: "control.compact", height: "control.compact" },
245
+ send: { width: "control.send", height: "control.send" },
246
+ small: { width: "control.roundSmall", height: "control.roundSmall" },
247
+ medium: { width: "control.round", height: "control.round" },
223
248
  large: { width: "control.default", height: "control.default" },
224
249
  },
250
+ // A page's leading icon button is a rounded square rather than a circle.
251
+ shape: { round: {}, square: { borderRadius: "action" } },
225
252
  },
226
- defaultVariants: { size: "medium", variant: "raised" },
253
+ compoundVariants: [
254
+ // Bare, the button is a rounded square too: 6 round at 24 and 8 round at 28.
255
+ { variant: "ghost", size: "compact", css: { borderRadius: "keycap" } },
256
+ { variant: "ghost", size: "small", css: { borderRadius: "action" } },
257
+ ],
258
+ defaultVariants: { size: "medium", variant: "raised", shape: "round" },
227
259
  });
228
260
  const badgeDot = css({
229
261
  position: "absolute",
@@ -263,10 +295,11 @@ const badgeCount = css({
263
295
  * small buttons.
264
296
  */
265
297
  export const IconButtonSizeContext = createContext("medium");
266
- const iconButtonEdges = { raised: "glass", ghost: undefined, primary: undefined };
267
- export function IconButton({ className, size, variant = "raised", badge, shortcut, children, "aria-label": label, ...props }) {
298
+ const iconButtonEdges = { raised: "raised", ghost: undefined, primary: "primary" };
299
+ export function IconButton({ className, size, variant = "raised", shape = "round", badge, shortcut, children, "aria-label": label, ...props }) {
268
300
  const containerSize = useContext(IconButtonSizeContext);
269
301
  const { labels } = useCiridaeLocale();
270
302
  const count = typeof badge === "number" && badge > 0 ? badge : 0;
271
- return (_jsxs(BaseButton, { ...props, "aria-label": count > 0 ? labels.common.withCount(label, count) : label, "aria-keyshortcuts": shortcut && formatForDisplay(shortcut, { useSymbols: false }), className: cx(round({ size: size ?? containerSize, variant }), className), "data-ciridae-ui": "", "data-edge": iconButtonEdges[variant], children: [children, badge === true && _jsx("span", { className: badgeDot, "aria-hidden": "true" }), count > 0 && (_jsx("span", { className: badgeCount, "aria-hidden": "true", children: count > 99 ? "99+" : count }))] }));
303
+ const edge = faceEdge(iconButtonEdges[variant]);
304
+ return (_jsxs(BaseButton, { ...props, "aria-label": count > 0 ? labels.common.withCount(label, count) : label, "aria-keyshortcuts": shortcut && formatForDisplay(shortcut, { useSymbols: false }), className: cx(round({ size: size ?? containerSize, variant, shape }), edge.className, className), "data-ciridae-ui": "", "data-edge": edge["data-edge"], children: [children, badge === true && _jsx("span", { className: badgeDot, "aria-hidden": "true" }), count > 0 && (_jsx("span", { className: badgeCount, "aria-hidden": "true", children: count > 99 ? "99+" : count }))] }));
272
305
  }
@@ -5,11 +5,39 @@ import { css, cx } from "#primitives/css";
5
5
  import { IconButton } from "./button.js";
6
6
  import { directional, icon } from "./icon.js";
7
7
  import { useCiridaeLocale } from "./locale.js";
8
+ /*
9
+ * The month alone, no face of its own: it sits on the popover or picker that opens it. Seven 32px
10
+ * day columns inside an 8px inset, which with the glass sheet's 4px makes the design's 12px.
11
+ */
12
+ const calendar = css({
13
+ display: "grid",
14
+ gap: "layout.controlGap",
15
+ width: "calc(7 * token(sizes.control.compact) + 2 * token(spacing.panel.compact))",
16
+ padding: "panel.compact",
17
+ });
18
+ const header = css({
19
+ display: "flex",
20
+ alignItems: "center",
21
+ justifyContent: "space-between",
22
+ fontSize: "caption",
23
+ fontWeight: "medium",
24
+ letterSpacing: "compact",
25
+ });
8
26
  const grid = css({
9
27
  display: "grid",
10
28
  gridTemplateColumns: "repeat(7, minmax(0, 1fr))",
11
29
  justifyItems: "center",
12
- gap: "layout.tightGap",
30
+ rowGap: "control.trackInset",
31
+ });
32
+ // The weekdays sit as far above the days as the month sits above them.
33
+ const weekDay = css({
34
+ paddingBlock: "layout.tightGap",
35
+ marginBottom: "control.iconGap",
36
+ textAlign: "center",
37
+ color: "fg.muted",
38
+ fontSize: "code",
39
+ fontWeight: "medium",
40
+ lineHeight: "tight",
13
41
  });
14
42
  const row = css({
15
43
  display: "grid",
@@ -18,20 +46,29 @@ const row = css({
18
46
  width: "100%",
19
47
  });
20
48
  const previousMonthIcon = css({ transform: "rotate(180deg)" });
49
+ // Today wears a hairline ring; the chosen day the primary button's face.
21
50
  const day = css({
22
51
  width: "control.compact",
23
52
  height: "control.compact",
24
- border: "0",
53
+ border: "1px solid transparent",
25
54
  borderRadius: "round",
26
55
  background: "transparent",
27
56
  color: "fg.primary",
57
+ fontSize: "caption",
58
+ fontWeight: "medium",
28
59
  cursor: "pointer",
29
60
  transitionProperty: "background, color",
30
61
  transitionDuration: "fast",
62
+ "&[aria-current=date]": { borderColor: "border.control" },
31
63
  "&:hover:not(:disabled):not([aria-selected=true])": {
32
- background: "bg.fill",
64
+ background: "bg.hover",
65
+ },
66
+ "&[aria-selected=true]": {
67
+ borderColor: "transparent",
68
+ background: "token(gradients.primary)",
69
+ color: "bg.canvas",
70
+ _light: { color: "fg.onAccent" },
33
71
  },
34
- "&[aria-selected=true]": { background: "fg.primary", color: "bg.canvas" },
35
72
  _disabled: { color: "fg.muted", opacity: "disabled", cursor: "default" },
36
73
  _focusVisible: { focusRing: "outer" },
37
74
  });
@@ -135,20 +172,10 @@ export function Calendar({ month, selected, onMonthChange, onSelect, locale: own
135
172
  });
136
173
  const weekDayFormatter = new Intl.DateTimeFormat(locale, { weekday: "narrow" });
137
174
  const fullWeekDayFormatter = new Intl.DateTimeFormat(locale, { weekday: "long" });
138
- return (_jsxs("section", { className: css({
139
- width: "calendar.width",
140
- padding: "panel.compact",
141
- border: "1px solid token(colors.border.control)",
142
- borderRadius: "inset",
143
- background: "bg.card",
144
- }), "data-ciridae-ui": "", children: [_jsxs("header", { className: css({
145
- display: "flex",
146
- alignItems: "center",
147
- justifyContent: "space-between",
148
- marginBottom: "panel.compact",
149
- }), children: [_jsx(IconButton, { "aria-label": labels.previousMonth, size: "compact", type: "button", onClick: () => onMonthChange(new Date(month.getFullYear(), month.getMonth() - 1, 1)), children: _jsx(ArrowRightIcon, { className: cx(icon(), directional, previousMonthIcon) }) }), _jsx("strong", { id: monthLabelId, children: monthFormatter.format(month) }), _jsx(IconButton, { "aria-label": labels.nextMonth, size: "compact", type: "button", onClick: () => onMonthChange(new Date(month.getFullYear(), month.getMonth() + 1, 1)), children: _jsx(ArrowRightIcon, { className: cx(icon(), directional) }) })] }), _jsxs("div", { className: grid, role: "grid", "aria-labelledby": monthLabelId, children: [_jsx("div", { className: row, role: "row", children: weekDays.map((date) => (_jsx("span", { "aria-label": fullWeekDayFormatter.format(date), className: css({ textAlign: "center", color: "fg.muted", fontSize: "caption" }), role: "columnheader", children: weekDayFormatter.format(date) }, date.getDay()))) }), Array.from({ length: 6 }, (_, weekIndex) => (_jsx("div", { className: row, role: "row", children: days.slice(weekIndex * 7, weekIndex * 7 + 7).map((date) => {
175
+ const today = new Date();
176
+ return (_jsxs("section", { className: calendar, "data-ciridae-ui": "", children: [_jsxs("header", { className: header, children: [_jsx(IconButton, { "aria-label": labels.previousMonth, size: "compact", variant: "ghost", type: "button", onClick: () => onMonthChange(new Date(month.getFullYear(), month.getMonth() - 1, 1)), children: _jsx(ArrowRightIcon, { className: cx(icon({ size: "small" }), directional, previousMonthIcon) }) }), _jsx("span", { id: monthLabelId, children: monthFormatter.format(month) }), _jsx(IconButton, { "aria-label": labels.nextMonth, size: "compact", variant: "ghost", type: "button", onClick: () => onMonthChange(new Date(month.getFullYear(), month.getMonth() + 1, 1)), children: _jsx(ArrowRightIcon, { className: cx(icon({ size: "small" }), directional) }) })] }), _jsxs("div", { className: grid, role: "grid", "aria-labelledby": monthLabelId, children: [_jsx("div", { className: row, role: "row", children: weekDays.map((date) => (_jsx("span", { "aria-label": fullWeekDayFormatter.format(date), className: weekDay, role: "columnheader", children: weekDayFormatter.format(date) }, date.getDay()))) }), Array.from({ length: 6 }, (_, weekIndex) => (_jsx("div", { className: row, role: "row", children: days.slice(weekIndex * 7, weekIndex * 7 + 7).map((date) => {
150
177
  const inCurrentMonth = isSameMonth(date, currentMonth);
151
- return (_jsx("button", { type: "button", "aria-label": dayFormatter.format(date), "aria-selected": inCurrentMonth && isSameDay(date, selected), className: day, disabled: !inCurrentMonth, onClick: () => onSelect(date), onFocus: () => setFocusedDate(date), onKeyDown: (event) => handleDayKeyDown(event, date), ref: (element) => {
178
+ return (_jsx("button", { type: "button", "aria-label": dayFormatter.format(date), "aria-selected": inCurrentMonth && isSameDay(date, selected), "aria-current": isSameDay(date, today) ? "date" : undefined, className: day, disabled: !inCurrentMonth, onClick: () => onSelect(date), onFocus: () => setFocusedDate(date), onKeyDown: (event) => handleDayKeyDown(event, date), ref: (element) => {
152
179
  if (element)
153
180
  dayButtons.current.set(dateKey(date), element);
154
181
  else
@@ -10,7 +10,7 @@ const page = css({
10
10
  // A phone keeps a thin margin around the card rather than giving its width away.
11
11
  padding: "panel.compact",
12
12
  "@media(min-width: 640px)": { padding: "page.inline" },
13
- background: "bg.canvas",
13
+ background: "token(gradients.canvas)",
14
14
  color: "fg.primary",
15
15
  });
16
16
  const column = css({
@@ -31,7 +31,7 @@ const headerRow = css({
31
31
  paddingInline: "panel.roomy",
32
32
  });
33
33
  const brandMark = css({ width: "brand.logo", height: "brand.logo", color: "fg.primary" });
34
- const content = css({ display: "flex", flexDirection: "column", gap: "layout.sectionGap" });
34
+ const content = css({ display: "flex", flexDirection: "column", gap: "layout.centeredCardGap" });
35
35
  const titleGroup = css({ display: "grid", gap: "layout.tightGap" });
36
36
  const titleText = css({ margin: "none", fontSize: "heading.section", fontWeight: "medium" });
37
37
  const descriptionText = css({ margin: "none", color: "fg.muted", fontSize: "caption" });