@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
@@ -7,14 +7,15 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
7
7
  * alone, such as a column head's "Filter by". Nothing here filters rows: `matchesFilter` tests one
8
8
  * value for a list that filters on the client.
9
9
  */
10
- import { CaretDownIcon, CaretLeftIcon, PlusIcon, XIcon } from "@phosphor-icons/react";
10
+ import { CaretDownIcon, CaretLeftIcon, CaretRightIcon, PlusIcon, XIcon, } from "@phosphor-icons/react";
11
11
  import { useState } from "react";
12
12
  import { css, cx } from "#primitives/css";
13
13
  import { Button, IconButton } from "./button.js";
14
14
  import { CheckboxItem } from "./checkbox-item.js";
15
15
  import { useControllableState } from "./controllable-state.js";
16
- import { Divider } from "./data-display.js";
17
- import { icon } from "./icon.js";
16
+ import { glassMenuDivider, glassMenuRow } from "./glass-menu.js";
17
+ import { faceEdge } from "./gradient-edge.js";
18
+ import { directional, icon } from "./icon.js";
18
19
  import { Input, SearchField } from "./input.js";
19
20
  import { useCiridaeLocale } from "./locale.js";
20
21
  import { Menu } from "./menu.js";
@@ -90,6 +91,10 @@ export function describeFilter(field, filter, locale) {
90
91
  : values.join(", "),
91
92
  };
92
93
  }
94
+ /*
95
+ * The slicer's face: the selected tint, hovering to the raised face. The field reads in the
96
+ * secondary grey, the operator a step quieter.
97
+ */
93
98
  const chip = css({
94
99
  display: "inline-flex",
95
100
  alignItems: "center",
@@ -99,7 +104,9 @@ const chip = css({
99
104
  paddingInlineStart: "control.compactInline",
100
105
  paddingInlineEnd: "control.iconGap",
101
106
  borderRadius: "pill",
102
- background: "bg.raised",
107
+ background: "token(gradients.selected)",
108
+ _hover: { background: "token(gradients.raisedHover)" },
109
+ _light: { color: "fg.secondary" },
103
110
  color: "fg.muted",
104
111
  fontSize: "caption",
105
112
  fontWeight: "medium",
@@ -122,6 +129,7 @@ const chipBody = css({
122
129
  _focusVisible: { focusRing: "outer" },
123
130
  "& svg": { flexShrink: "0" },
124
131
  });
132
+ const chipOperator = css({ color: "fg.muted" });
125
133
  const chipValue = css({ color: "fg.primary", overflow: "hidden", textOverflow: "ellipsis" });
126
134
  const chipRemove = css({
127
135
  display: "inline-grid",
@@ -138,10 +146,11 @@ const chipRemove = css({
138
146
  _hover: { color: "fg.primary", background: "bg.hover" },
139
147
  _focusVisible: { focusRing: "outer" },
140
148
  });
149
+ const toolbarEdge = faceEdge("toolbar");
141
150
  /** One set filter: the field and operator muted, its values in full; the text reopens it. */
142
151
  export function FilterChip({ label, icon: fieldIcon, operator, values, onEdit, onRemove, className, ...props }) {
143
152
  const { labels } = useCiridaeLocale();
144
- return (_jsxs("span", { ...props, className: cx(chip, className), "data-ciridae-ui": "", "data-edge": "glass", children: [_jsxs("button", { type: "button", className: chipBody, onClick: onEdit, children: [fieldIcon, label, " ", operator, _jsx("span", { className: chipValue, children: values })] }), _jsx("button", { type: "button", className: chipRemove, "aria-label": labels.filters.remove(label), onClick: onRemove, children: _jsx(XIcon, { className: icon({ size: "small" }) }) })] }));
153
+ return (_jsxs("span", { ...props, className: cx(chip, toolbarEdge.className, className), "data-ciridae-ui": "", "data-edge": toolbarEdge["data-edge"], children: [_jsxs("button", { type: "button", className: chipBody, onClick: onEdit, children: [fieldIcon, label, " ", _jsx("span", { className: chipOperator, children: operator }), _jsx("span", { className: chipValue, children: values })] }), _jsx("button", { type: "button", className: chipRemove, "aria-label": labels.filters.remove(label), onClick: onRemove, children: _jsx(XIcon, { className: icon({ size: "small" }) }) })] }));
145
154
  }
146
155
  const bar = css({
147
156
  display: "flex",
@@ -170,28 +179,29 @@ const tools = css({
170
179
  "& > button": { flexShrink: "0" },
171
180
  "& > :not(button)": { minWidth: "0" },
172
181
  });
173
- const builder = css({ display: "flex", flexDirection: "column", width: "300px", maxWidth: "100%" });
174
- const fieldItem = css({
182
+ const builder = css({
175
183
  display: "flex",
176
- alignItems: "center",
177
- gap: "layout.controlGap",
184
+ flexDirection: "column",
185
+ width: "overlay.popover",
186
+ maxWidth: "100%",
187
+ });
188
+ // A menu row: the field's icon, its name, and an arrow on to its operator and values.
189
+ const fieldItem = css(glassMenuRow, {
178
190
  width: "100%",
179
- minHeight: "control.compact",
180
- paddingInline: "menu.itemInline",
181
191
  border: "0",
182
- borderRadius: "control",
183
192
  background: "transparent",
184
- color: "fg.primary",
185
- font: "inherit",
186
- fontSize: "body",
187
- fontWeight: "medium",
188
- letterSpacing: "compact",
193
+ fontFamily: "inherit",
189
194
  textAlign: "start",
190
- cursor: "pointer",
191
- _hover: { background: "bg.hover" },
192
- _focusVisible: { background: "bg.hover", outline: "none" },
193
- "& svg": { color: "fg.muted", width: "icon.small", height: "icon.small" },
195
+ "& > span": { flex: "1", minWidth: "0" },
196
+ "& svg": {
197
+ flexShrink: "0",
198
+ color: "fg.muted",
199
+ fontSize: "icon.small",
200
+ width: "1em",
201
+ height: "1em",
202
+ },
194
203
  });
204
+ const menuDivider = css(glassMenuDivider);
195
205
  const groupLabel = css({
196
206
  paddingBlockStart: "control.block",
197
207
  paddingBlockEnd: "layout.tightGap",
@@ -201,7 +211,8 @@ const groupLabel = css({
201
211
  fontWeight: "medium",
202
212
  letterSpacing: "compact",
203
213
  });
204
- const searchWrap = css({ padding: "layout.tightGap" });
214
+ // The quiet search's hairline is inset as the dividers are.
215
+ const searchWrap = css({ paddingInline: "menu.itemInline", paddingBlockEnd: "layout.tightGap" });
205
216
  const noMatches = css({
206
217
  paddingBlock: "control.block",
207
218
  paddingInline: "menu.itemInline",
@@ -235,7 +246,7 @@ export function FilterBar({ fields, value, defaultValue = [], onValueChange, ope
235
246
  setEditing(field.key);
236
247
  setOpen(true);
237
248
  }, onRemove: () => setFilters(filters.filter((candidate) => candidate.field !== filter.field)) }, filter.field));
238
- }) })), _jsxs("div", { className: tools, children: [filters.length > 0 && (_jsx(Button, { variant: "ghost", onClick: () => setFilters([]), children: labels.filters.clearAll })), _jsx(Popover, { open: open, onOpenChange: changeOpen, density: "compact", trigger: _jsx(Button, { variant: "ghost", icon: _jsx(PlusIcon, { className: icon({ size: "small" }) }), onClick: () => setEditing(null), children: labels.filters.addFilter }), children: open && (_jsx(FilterBuilder, { fields: fields, startField: editing, filters: filters, onChange: (filter) => setFilters(replaceFilter(filters, filter)) }, editing ?? "")) }), children] })] }));
249
+ }) })), _jsxs("div", { className: tools, children: [filters.length > 0 && (_jsx(Button, { variant: "ghost", onClick: () => setFilters([]), children: labels.filters.clearAll })), _jsx(Popover, { open: open, onOpenChange: changeOpen, density: "compact", trigger: _jsx(Button, { variant: "secondary", size: "medium", icon: _jsx(PlusIcon, { className: icon({ size: "small" }) }), onClick: () => setEditing(null), children: labels.filters.addFilter }), children: open && (_jsx(FilterBuilder, { fields: fields, startField: editing, filters: filters, onChange: (filter) => setFilters(replaceFilter(filters, filter)) }, editing ?? "")) }), children] })] }));
239
250
  }
240
251
  function FilterBuilder({ fields, startField, filters, onChange, }) {
241
252
  const { labels } = useCiridaeLocale();
@@ -254,10 +265,10 @@ function FilterBuilder({ fields, startField, filters, onChange, }) {
254
265
  else
255
266
  groups.push({ label: candidate.group, fields: [candidate] });
256
267
  }
257
- return (_jsxs("div", { className: builder, children: [_jsx("div", { className: searchWrap, children: _jsx(SearchField, { size: "compact", "aria-label": labels.filters.filterBy, placeholder: labels.filters.filterBy, value: query, onChange: (event) => setQuery(event.target.value) }) }), groups.map((group, index) => (_jsxs("div", { className: css({ display: "grid" }), children: [index > 0 && _jsx(Divider, { inset: "menu" }), group.label && _jsx("span", { className: groupLabel, children: group.label }), group.fields.map((candidate) => (_jsxs("button", { type: "button", className: fieldItem, onClick: () => {
268
+ return (_jsxs("div", { className: builder, children: [_jsx("div", { className: searchWrap, children: _jsx(SearchField, { size: "compact", quiet: true, "aria-label": labels.filters.filterBy, placeholder: labels.filters.filterBy, value: query, onChange: (event) => setQuery(event.target.value) }) }), groups.map((group, index) => (_jsxs("div", { className: css({ display: "grid" }), children: [index > 0 && _jsx("div", { className: menuDivider, role: "separator" }), group.label && _jsx("span", { className: groupLabel, children: group.label }), group.fields.map((candidate) => (_jsxs("button", { type: "button", className: fieldItem, onClick: () => {
258
269
  setFieldKey(candidate.key);
259
270
  setQuery("");
260
- }, children: [candidate.icon, candidate.label] }, candidate.key)))] }, group.label ?? ""))), matching.length === 0 && _jsx("span", { className: noMatches, children: labels.filters.noFields })] }));
271
+ }, children: [candidate.icon, _jsx("span", { children: candidate.label }), _jsx(CaretRightIcon, { className: directional, "aria-hidden": "true" })] }, candidate.key)))] }, group.label ?? ""))), matching.length === 0 && _jsx("span", { className: noMatches, children: labels.filters.noFields })] }));
261
272
  }
262
273
  const editorHeader = css({
263
274
  display: "flex",
@@ -329,13 +340,13 @@ export function FilterValueEditor({ field, value, onChange, onBack, }) {
329
340
  const keepsValues = multiValueOperators.includes(candidate) === multiple && candidate !== "between";
330
341
  report(candidate, keepsValues ? values : values.slice(0, 1));
331
342
  },
332
- })) })) : (_jsx("span", { className: fixedOperator, children: operatorLabel }))] }), options ? (_jsxs(_Fragment, { children: [field.options && field.options.length > 6 && (_jsx("div", { className: searchWrap, children: _jsx(SearchField, { size: "compact", "aria-label": labels.filters.searchValues(field.label), placeholder: labels.filters.searchValues(field.label), value: query, onChange: (event) => setQuery(event.target.value) }) })), _jsxs("div", { className: valueList, children: [options.map((option) => (_jsxs(CheckboxItem, { checked: values.includes(option.value), onCheckedChange: (checked) => report(operator, multiple
343
+ })) })) : (_jsx("span", { className: fixedOperator, children: operatorLabel }))] }), options ? (_jsxs(_Fragment, { children: [field.options && field.options.length > 6 && (_jsx("div", { className: searchWrap, children: _jsx(SearchField, { size: "compact", quiet: true, "aria-label": labels.filters.searchValues(field.label), placeholder: labels.filters.searchValues(field.label), value: query, onChange: (event) => setQuery(event.target.value) }) })), _jsxs("div", { className: valueList, children: [options.map((option) => (_jsxs(CheckboxItem, { checked: values.includes(option.value), onCheckedChange: (checked) => report(operator, multiple
333
344
  ? checked
334
345
  ? [...values, option.value]
335
346
  : values.filter((candidate) => candidate !== option.value)
336
347
  : checked
337
348
  ? [option.value]
338
- : []), meta: option.count, children: [option.icon, option.label] }, option.value))), options.length === 0 && _jsx("span", { className: noMatches, children: labels.filters.noValues })] })] })) : (_jsx("div", { className: inputs, children: (operator === "between" ? [0, 1] : [0]).map((index) => (_jsx(Input, { type: field.kind === "date" ? "date" : field.kind === "number" ? "number" : "text", "aria-label": operator === "between"
349
+ : []), meta: option.count, children: [option.icon, option.label] }, option.value))), options.length === 0 && _jsx("span", { className: noMatches, children: labels.filters.noValues })] })] })) : (_jsx("div", { className: inputs, children: (operator === "between" ? [0, 1] : [0]).map((index) => (_jsx(Input, { size: "small", type: field.kind === "date" ? "date" : field.kind === "number" ? "number" : "text", "aria-label": operator === "between"
339
350
  ? index === 0
340
351
  ? labels.filters.from
341
352
  : labels.filters.to
@@ -347,7 +358,7 @@ export function FilterValueEditor({ field, value, onChange, onBack, }) {
347
358
  const next = [...values];
348
359
  next[index] = event.target.value;
349
360
  report(operator, next);
350
- } }, index))) })), _jsx(Divider, { inset: "menu" }), _jsxs("div", { className: editorFooter, children: [options
361
+ } }, index))) })), _jsx("div", { className: menuDivider, role: "separator" }), _jsxs("div", { className: editorFooter, children: [options
351
362
  ? `${labels.filters.selectedCount(picked)} · ${labels.filters.updatesAsYouPick}`
352
363
  : labels.filters.updatesAsYouPick, _jsx(Button, { variant: "ghost", onClick: () => report(operator, []), children: labels.common.clear })] })] }));
353
364
  }
@@ -23,7 +23,16 @@ export declare const checkMark: import("../styled-system/types/index.js").Recipe
23
23
  };
24
24
  };
25
25
  }>;
26
+ /** What a check mark's element carries, so its filled face can draw the gradient edge. */
27
+ export declare const checkMarkEdge: {
28
+ readonly "data-ciridae-ui": "";
29
+ readonly "data-edge": "gradient";
30
+ };
26
31
  export declare const checkIndicator: string;
32
+ /** The tick, or the dash of a mixed checkbox. */
33
+ export declare function CheckGlyph({ mixed }: {
34
+ mixed: boolean | undefined;
35
+ }): import("react").JSX.Element;
27
36
  export declare function Checkbox({ children, className, wrapperClassName, ...props }: Omit<ComponentProps<typeof BaseCheckbox.Root>, "children" | "className"> & {
28
37
  children?: ReactNode;
29
38
  className?: string;
@@ -2,13 +2,16 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Checkbox as BaseCheckbox } from "@base-ui/react/checkbox";
3
3
  import { Radio as BaseRadio } from "@base-ui/react/radio";
4
4
  import { RadioGroup as BaseRadioGroup } from "@base-ui/react/radio-group";
5
- import { CheckIcon, MinusIcon } from "@phosphor-icons/react";
5
+ import { CheckIcon } from "@phosphor-icons/react";
6
6
  import { css, cva, cx } from "#primitives/css";
7
7
  import { useFieldControl } from "./field-context.js";
8
8
  /*
9
- * The one mark for a checkbox and a radio: a glass well in dark, a near-white one in light, that
10
- * fills with the primary face once on. Hover darkens the edge of an empty mark and the face of a
11
- * filled one. The ring is a variant, because a row that holds the mark draws its own ring instead.
9
+ * The one mark for a checkbox and a radio: the selected tint in dark, a near-white well in light,
10
+ * that fills with the primary button's face and gradient edge once on. The edge is the `[data-edge]`
11
+ * ring the element carries through `checkMarkEdge`: the selected face's gradient in dark, the
12
+ * control hairline in light. Hover darkens the edge of an empty mark, washes it in light, and fades
13
+ * a filled one in dark. The ring is a variant, because a row that holds the mark draws its own
14
+ * ring instead.
12
15
  */
13
16
  export const checkMark = cva({
14
17
  base: {
@@ -18,25 +21,31 @@ export const checkMark = cva({
18
21
  display: "inline-grid",
19
22
  placeItems: "center",
20
23
  padding: "none",
21
- border: "1px solid token(colors.border.mark)",
22
- background: "bg.check",
24
+ border: "1px solid transparent",
25
+ background: "token(gradients.selected)",
23
26
  color: "bg.canvas",
24
- _light: { color: "fg.onAccent" },
27
+ _light: { color: "fg.onAccent", background: "bg.check" },
25
28
  cursor: "pointer",
26
- transitionProperty: "background, border-color",
29
+ transitionProperty: "background, opacity",
27
30
  transitionDuration: "fast",
28
- // Pointing anywhere at a row's label marks the box it would tick.
29
- "&:is(label:hover > *, label:hover *, :hover):not([data-disabled])": {
30
- borderColor: "fg.muted",
31
+ _before: {
32
+ background: "token(gradients.border.selected)",
33
+ _light: { background: "border.control" },
31
34
  },
32
- "&[data-checked], &[data-indeterminate]": {
33
- background: "bg.primary",
34
- borderColor: "transparent",
35
+ // Pointing anywhere at a row's label marks the box it would tick, while it is empty: a filled
36
+ // mark, or a radio card's once chosen, keeps its face.
37
+ "&:not([data-checked], [data-indeterminate], [data-checked] > *):is(label:hover > *, label:hover *, :hover):not([data-disabled])": {
38
+ _before: { background: "fg.muted", _light: { background: "fg.secondary" } },
39
+ _light: {
40
+ background: "linear-gradient(token(colors.bg.controlHover), token(colors.bg.controlHover)), token(colors.bg.check)",
41
+ },
35
42
  },
36
- "&:is([data-checked], [data-indeterminate]):is(label:hover > *, label:hover *, :hover):not([data-disabled])": {
37
- background: "bg.primaryHover",
38
- borderColor: "transparent",
43
+ "&[data-checked], &[data-indeterminate]": {
44
+ background: "token(gradients.primary)",
45
+ _light: { background: "token(gradients.primary)" },
46
+ _before: { background: "token(gradients.border.primary)" },
39
47
  },
48
+ "&:is([data-checked], [data-indeterminate]):is(label:hover > *, label:hover *, :hover):not([data-disabled])": { opacity: "checkedHover", _light: { opacity: "1" } },
40
49
  "&[data-disabled]": { opacity: "disabled", cursor: "not-allowed" },
41
50
  },
42
51
  variants: {
@@ -58,13 +67,25 @@ const label = css({
58
67
  fontWeight: "normal",
59
68
  cursor: "pointer",
60
69
  });
70
+ /** What a check mark's element carries, so its filled face can draw the gradient edge. */
71
+ export const checkMarkEdge = { "data-ciridae-ui": "", "data-edge": "gradient" };
61
72
  export const checkIndicator = css({
62
73
  display: "inline-flex",
63
74
  lineHeight: "1",
64
- fontSize: "icon.compact",
75
+ fontSize: "icon.small",
76
+ });
77
+ // A mixed checkbox's mark: a short dash rather than a minus glyph.
78
+ const mixedDash = css({
79
+ width: "indicator.swatch",
80
+ height: "border.hairline",
81
+ background: "currentColor",
65
82
  });
83
+ /** The tick, or the dash of a mixed checkbox. */
84
+ export function CheckGlyph({ mixed }) {
85
+ return mixed ? _jsx("span", { className: mixedDash }) : _jsx(CheckIcon, { weight: "bold" });
86
+ }
66
87
  export function Checkbox({ children, className, wrapperClassName, ...props }) {
67
- return (_jsxs("label", { className: cx(label, wrapperClassName), "data-ciridae-ui": "", children: [_jsx(BaseCheckbox.Root, { ...props, className: cx(checkMark(), className), children: _jsx(BaseCheckbox.Indicator, { className: checkIndicator, children: props.indeterminate ? _jsx(MinusIcon, { weight: "bold" }) : _jsx(CheckIcon, { weight: "bold" }) }) }), children] }));
88
+ return (_jsxs("label", { className: cx(label, wrapperClassName), "data-ciridae-ui": "", children: [_jsx(BaseCheckbox.Root, { ...props, ...checkMarkEdge, className: cx(checkMark(), className), children: _jsx(BaseCheckbox.Indicator, { className: checkIndicator, children: _jsx(CheckGlyph, { mixed: props.indeterminate }) }) }), children] }));
68
89
  }
69
90
  const textarea = css({
70
91
  width: "100%",
@@ -74,7 +95,7 @@ const textarea = css({
74
95
  paddingInline: "control.compactInline",
75
96
  border: "1px solid token(colors.border.control)",
76
97
  borderRadius: "control",
77
- background: "bg.field",
98
+ background: "bg.formField",
78
99
  color: "fg.primary",
79
100
  fontFamily: "sans",
80
101
  fontSize: "body",
@@ -108,7 +129,10 @@ const menuRadio = css({
108
129
  fontWeight: "medium",
109
130
  letterSpacing: "compact",
110
131
  cursor: "pointer",
111
- _hover: { background: "bg.hover" },
132
+ _hover: {
133
+ background: "bg.hover",
134
+ boxShadow: "inset 0 0 0 1px token(colors.border.menuHover)",
135
+ },
112
136
  "&:has([data-disabled])": { opacity: "disabled", cursor: "not-allowed" },
113
137
  });
114
138
  const menuRadioMeta = css({ marginInlineStart: "auto", color: "fg.muted" });
@@ -119,5 +143,5 @@ export const radioDot = css({
119
143
  background: "currentColor",
120
144
  });
121
145
  export function RadioGroup({ name, options, label: groupLabel, defaultValue, value, onValueChange, layout = "inline", className, ...props }) {
122
- return (_jsx(BaseRadioGroup, { ...props, className: cx(layout === "menu" ? menuRadios : radios, className), "aria-label": props["aria-label"] ?? groupLabel, "data-ciridae-ui": "", defaultValue: defaultValue, name: name, onValueChange: (nextValue) => onValueChange?.(String(nextValue)), value: value, children: options.map((option) => (_jsxs("label", { className: layout === "menu" ? menuRadio : label, children: [_jsx(BaseRadio.Root, { className: checkMark({ shape: "round" }), disabled: option.disabled, value: option.value, children: _jsx(BaseRadio.Indicator, { className: radioDot }) }), option.label, option.meta !== undefined && _jsx("span", { className: menuRadioMeta, children: option.meta })] }, option.value))) }));
146
+ return (_jsx(BaseRadioGroup, { ...props, className: cx(layout === "menu" ? menuRadios : radios, className), "aria-label": props["aria-label"] ?? groupLabel, "data-ciridae-ui": "", defaultValue: defaultValue, name: name, onValueChange: (nextValue) => onValueChange?.(String(nextValue)), value: value, children: options.map((option) => (_jsxs("label", { className: layout === "menu" ? menuRadio : label, children: [_jsx(BaseRadio.Root, { ...checkMarkEdge, className: checkMark({ shape: "round" }), disabled: option.disabled, value: option.value, children: _jsx(BaseRadio.Indicator, { className: radioDot }) }), option.label, option.meta !== undefined && _jsx("span", { className: menuRadioMeta, children: option.meta })] }, option.value))) }));
123
147
  }
@@ -6,13 +6,17 @@ import type { ComponentProps, ReactNode } from "react";
6
6
  */
7
7
  export declare function Form({ className, ...props }: ComponentProps<"form">): import("react").JSX.Element;
8
8
  export declare function FormActions({ className, ...props }: ComponentProps<"div">): import("react").JSX.Element;
9
+ /** Fields in one row of a `Form`, sharing its width equally. */
10
+ export declare function FieldRow({ className, ...props }: ComponentProps<"div">): import("react").JSX.Element;
9
11
  /**
10
12
  * A label round a `FieldLabel`, its control and any `FieldError`: the label names the control it
11
13
  * holds, and while `invalid` the control is marked and described by the error, so `Input`,
12
- * `Textarea`, `Select` and `Combobox` inside need no ids of their own.
14
+ * `Textarea`, `Select` and `Combobox` inside need no ids of their own. A `hint` sits under the
15
+ * control and describes it until the error takes its place.
13
16
  */
14
- export declare function Field({ className, htmlFor, invalid, ...props }: ComponentProps<"label"> & {
17
+ export declare function Field({ className, htmlFor, invalid, hint, children, ...props }: ComponentProps<"label"> & {
15
18
  invalid?: boolean;
19
+ hint?: ReactNode;
16
20
  }): import("react").JSX.Element;
17
21
  export declare function FieldLabel({ children }: {
18
22
  children: ReactNode;
@@ -28,5 +32,5 @@ export type SegmentedOptionProps = Omit<ButtonProps, "variant" | "type" | "aria-
28
32
  /** Shown muted after the label, such as how many items the option lists. */
29
33
  count?: number;
30
34
  };
31
- /** One option of a `SegmentedControl`. */
35
+ /** One option of a `SegmentedControl`: the selected one is raised, the others bare. */
32
36
  export declare function SegmentedOption({ selected, count, children, ...props }: SegmentedOptionProps): import("react").JSX.Element;
@@ -3,31 +3,65 @@ import { useContext, useId, useMemo } from "react";
3
3
  import { css, cx } from "#primitives/css";
4
4
  import { Button } from "./button.js";
5
5
  import { FieldControlContext } from "./field-context.js";
6
+ import { faceEdge } from "./gradient-edge.js";
6
7
  import { useInToolbar } from "./layout.js";
7
8
  const field = css({ display: "grid", gap: "control.iconGap", minWidth: "0", fontSize: "caption" });
8
- const labelText = css({ color: "fg.muted" });
9
- const errorText = css({ margin: "none", color: "accent.red", fontSize: "caption" });
9
+ // A single line, its box the type's own 12px, so the gap to the control is the field's 6px alone.
10
+ const labelText = css({
11
+ color: "fg.primary",
12
+ fontSize: "code",
13
+ fontWeight: "medium",
14
+ lineHeight: "tight",
15
+ letterSpacing: "compact",
16
+ });
17
+ // Help and error lines under the control, which can wrap.
18
+ const noteText = css({
19
+ margin: "none",
20
+ fontSize: "code",
21
+ fontWeight: "medium",
22
+ lineHeight: "body",
23
+ letterSpacing: "compact",
24
+ });
25
+ const hintText = css({ color: "fg.muted" });
26
+ const errorText = css({ color: "accent.red" });
27
+ /*
28
+ * A grey well holding its options, the selected one raised on the secondary button's face. The
29
+ * track is a default button's height, or in a toolbar a small one's on a thinner inset, and each
30
+ * option clears it by the inset and a hairline.
31
+ */
10
32
  const segmented = css({
33
+ "--track-inset": "token(spacing.layout.tightGap)",
34
+ "--track-height": "token(sizes.control.default)",
11
35
  display: "inline-flex",
12
36
  alignSelf: "flex-start",
13
- padding: "control.trackInset",
14
- border: "1px solid token(colors.border.control)",
15
- borderRadius: "pill",
16
- // Children follow the track's own shape, so the selected pill nests inside it. Ghost text is
17
- // white in dark, so the track mutes every option and lets only the selected one, on the accent
18
- // wash, read fully.
19
- "& > *": { borderColor: "transparent!", borderRadius: "pill", color: "fg.muted!" },
20
- "& > [aria-pressed=true]": {
21
- background: "bg.selected",
22
- color: "fg.primary!",
37
+ alignItems: "center",
38
+ gap: "control.trackInset",
39
+ height: "var(--track-height)",
40
+ padding: "var(--track-inset)",
41
+ borderRadius: "inset",
42
+ // The well grey in light; in dark the panel's, where the field's white wash would read as glass.
43
+ background: "bg.panel",
44
+ _light: { background: "bg.field" },
45
+ "&[data-density=compact]": {
46
+ "--track-inset": "token(spacing.control.trackInset)",
47
+ "--track-height": "token(sizes.control.compact)",
23
48
  },
24
- // In a toolbar the whole track, not each option, is a small button's height.
25
- "&[data-density=compact] > *": {
26
- height: "calc(token(sizes.control.compact) - 2 * (token(spacing.control.trackInset) + token(sizes.border.hairline)))",
49
+ "& > *": {
50
+ height: "calc(var(--track-height) - 2 * (var(--track-inset) + token(sizes.border.hairline)))!",
51
+ borderRadius: "action!",
27
52
  },
53
+ "& > [aria-pressed=false]:not(:hover)": { color: "fg.muted" },
54
+ "& > [aria-pressed=true]": { boxShadow: "soft" },
28
55
  });
29
56
  const countText = css({ color: "fg.muted", fontVariantNumeric: "tabular-nums" });
30
57
  const formStack = css({ display: "grid", gap: "layout.clusterGap", minWidth: "0" });
58
+ // Fields side by side sit closer than stacked ones.
59
+ const fieldRow = css({
60
+ display: "grid",
61
+ gridAutoFlow: "column",
62
+ gridAutoColumns: "minmax(0, 1fr)",
63
+ gap: "layout.contentGap",
64
+ });
31
65
  const formActions = css({
32
66
  display: "flex",
33
67
  justifyContent: "flex-end",
@@ -43,16 +77,26 @@ export function Form({ className, ...props }) {
43
77
  export function FormActions({ className, ...props }) {
44
78
  return _jsx("div", { ...props, className: cx(formActions, className), "data-ciridae-ui": "" });
45
79
  }
80
+ /** Fields in one row of a `Form`, sharing its width equally. */
81
+ export function FieldRow({ className, ...props }) {
82
+ return _jsx("div", { ...props, className: cx(fieldRow, className), "data-ciridae-ui": "" });
83
+ }
46
84
  /**
47
85
  * A label round a `FieldLabel`, its control and any `FieldError`: the label names the control it
48
86
  * holds, and while `invalid` the control is marked and described by the error, so `Input`,
49
- * `Textarea`, `Select` and `Combobox` inside need no ids of their own.
87
+ * `Textarea`, `Select` and `Combobox` inside need no ids of their own. A `hint` sits under the
88
+ * control and describes it until the error takes its place.
50
89
  */
51
- export function Field({ className, htmlFor, invalid = false, ...props }) {
90
+ export function Field({ className, htmlFor, invalid = false, hint, children, ...props }) {
52
91
  const generatedId = useId();
53
92
  const controlId = htmlFor ?? `${generatedId}-control`;
54
- const fieldControl = useMemo(() => ({ controlId, descriptionId: invalid ? `${controlId}-error` : undefined, invalid }), [controlId, invalid]);
55
- return (_jsx(FieldControlContext, { value: fieldControl, children: _jsx("label", { ...props, htmlFor: htmlFor, className: cx(field, className), "data-ciridae-ui": "" }) }));
93
+ const showsHint = hint !== undefined && !invalid;
94
+ const fieldControl = useMemo(() => ({
95
+ controlId,
96
+ descriptionId: invalid ? `${controlId}-error` : showsHint ? `${controlId}-hint` : undefined,
97
+ invalid,
98
+ }), [controlId, invalid, showsHint]);
99
+ return (_jsx(FieldControlContext, { value: fieldControl, children: _jsxs("label", { ...props, htmlFor: htmlFor, className: cx(field, className), "data-ciridae-ui": "", children: [children, showsHint && (_jsx("span", { id: `${controlId}-hint`, className: cx(noteText, hintText), children: hint }))] }) }));
56
100
  }
57
101
  export function FieldLabel({ children }) {
58
102
  return _jsx("span", { className: labelText, children: children });
@@ -60,13 +104,14 @@ export function FieldLabel({ children }) {
60
104
  /** The field's error; its id is the one the field's control is described by. */
61
105
  export function FieldError({ id, children }) {
62
106
  const field = useContext(FieldControlContext);
63
- return (_jsx("p", { id: id ?? (field ? `${field.controlId}-error` : undefined), role: "alert", className: errorText, children: children }));
107
+ return (_jsx("p", { id: id ?? (field ? `${field.controlId}-error` : undefined), role: "alert", className: cx(noteText, errorText), children: children }));
64
108
  }
65
109
  export function SegmentedControl({ className, ...props }) {
66
110
  const inToolbar = useInToolbar();
67
- return (_jsx("div", { ...props, className: cx(segmented, className), "data-density": inToolbar ? "compact" : undefined, "data-ciridae-ui": "" }));
111
+ const edge = faceEdge("raised");
112
+ return (_jsx("div", { ...props, className: cx(segmented, edge.className, className), "data-density": inToolbar ? "compact" : undefined, "data-ciridae-ui": "", "data-edge": edge["data-edge"] }));
68
113
  }
69
- /** One option of a `SegmentedControl`. */
114
+ /** One option of a `SegmentedControl`: the selected one is raised, the others bare. */
70
115
  export function SegmentedOption({ selected, count, children, ...props }) {
71
- return (_jsxs(Button, { ...props, type: "button", variant: "ghost", "aria-pressed": selected, children: [children, count !== undefined && _jsx("span", { className: countText, children: count })] }));
116
+ return (_jsxs(Button, { ...props, type: "button", variant: selected ? "secondary" : "ghost", "aria-pressed": selected, children: [children, count !== undefined && _jsx("span", { className: countText, children: count })] }));
72
117
  }
@@ -0,0 +1,3 @@
1
+ export declare const glassMenuSurface: import("../styled-system/types/index.js").SystemStyleObject;
2
+ export declare const glassMenuRow: import("../styled-system/types/index.js").SystemStyleObject;
3
+ export declare const glassMenuDivider: import("../styled-system/types/index.js").SystemStyleObject;
@@ -0,0 +1,63 @@
1
+ import { css } from "#primitives/css";
2
+ /*
3
+ * The glass sheet every menu, popover and select list floats on, and the rows inside it, as raw
4
+ * styles for `css()` to merge with each popup's own: an 8-round sheet padded 4px all round that
5
+ * blurs what lies under it, holding 4-round rows 32px tall.
6
+ */
7
+ export const glassMenuSurface = css.raw({
8
+ padding: "menu.inset",
9
+ border: "1px solid token(colors.border.control)",
10
+ borderRadius: "action",
11
+ background: "bg.popover",
12
+ backdropFilter: "blur(token(blurs.popover))",
13
+ boxShadow: "soft",
14
+ color: "fg.primary",
15
+ textStyle: "body",
16
+ // The popup takes focus as it opens; it is a container, not a control, so it draws no ring.
17
+ outline: "none",
18
+ });
19
+ /*
20
+ * A row: hovered, it takes a wash inside the menu hover hairline, the raised glass in dark and the
21
+ * row's hover wash in light. A row that is a plain button has no highlight state, so it takes the
22
+ * wash on hover. Reached by the keyboard, away from the pointer, it draws the focus ring inside its
23
+ * own edge in place of the wash and the hairline.
24
+ */
25
+ export const glassMenuRow = css.raw({
26
+ display: "flex",
27
+ alignItems: "center",
28
+ gap: "layout.controlGap",
29
+ minHeight: "control.compact",
30
+ paddingInline: "menu.itemInline",
31
+ borderRadius: "control",
32
+ color: "fg.primary",
33
+ fontSize: "body",
34
+ fontWeight: "medium",
35
+ lineHeight: "tight",
36
+ letterSpacing: "compact",
37
+ cursor: "pointer",
38
+ outline: "none",
39
+ "&[data-highlighted], &:is(button):hover": {
40
+ background: "token(gradients.raised)",
41
+ boxShadow: "inset 0 0 0 1px token(colors.border.menuHover)",
42
+ _light: { background: "bg.rowHover" },
43
+ },
44
+ // Under the pointer the row keeps the wash, so the ring marks only the row the keys reached.
45
+ "&:focus-visible:not(:hover), &[data-highlighted]:focus-visible:not(:hover)": {
46
+ focusRing: "inset",
47
+ background: "transparent",
48
+ boxShadow: "none",
49
+ _light: { background: "transparent" },
50
+ },
51
+ "&[data-disabled]": { opacity: "disabled", cursor: "not-allowed" },
52
+ });
53
+ /*
54
+ * The rule between groups of rows, inset 8px from the sheet's sides with 4px above and below: a
55
+ * hairline along the top of a 2px band, as the design draws it.
56
+ */
57
+ export const glassMenuDivider = css.raw({
58
+ flexShrink: "0",
59
+ height: "calc(token(sizes.border.hairline) * 2)",
60
+ marginBlock: "layout.tightGap",
61
+ marginInline: "menu.itemInline",
62
+ borderTop: "1px solid token(colors.border.divider)",
63
+ });
@@ -0,0 +1,9 @@
1
+ type GradientEdge = "raised" | "primary" | "danger" | "tinted" | "toolbar";
2
+ /** A face's edge: a flat hairline ciridae.css paints by name, or one of the gradient edges. */
3
+ export type FaceEdge = "glass" | "control" | GradientEdge;
4
+ /** The class and `data-edge` that draw `edge`; nothing for a face with no edge. */
5
+ export declare function faceEdge(edge: FaceEdge | undefined): {
6
+ className: string | undefined;
7
+ "data-edge": string | undefined;
8
+ };
9
+ export {};
@@ -0,0 +1,31 @@
1
+ import { cva } from "#primitives/css";
2
+ /*
3
+ * The 1px edge a face draws as a gradient. A gradient cannot be a border color, so it paints the
4
+ * `[data-edge]` ring ciridae.css draws inside the element's box, which also keeps its corners:
5
+ * `faceEdge` gives the class that paints the ring and the `data-edge` that turns it on. Dark draws
6
+ * no edge on the primary or destructive face. A toolbar control's edge is the selected face's: the
7
+ * raised gradient in dark and a paler grey gradient in light.
8
+ */
9
+ const gradientEdge = cva({
10
+ base: {},
11
+ variants: {
12
+ edge: {
13
+ raised: { _before: { background: "token(gradients.border.raised)" } },
14
+ primary: { _before: { background: "token(gradients.border.primary)" } },
15
+ danger: { _before: { background: "token(gradients.border.danger)" } },
16
+ tinted: { _before: { background: "token(gradients.border.tinted)" } },
17
+ toolbar: { _before: { background: "token(gradients.border.selected)" } },
18
+ },
19
+ },
20
+ });
21
+ function isGradientEdge(edge) {
22
+ return edge !== "glass" && edge !== "control";
23
+ }
24
+ /** The class and `data-edge` that draw `edge`; nothing for a face with no edge. */
25
+ export function faceEdge(edge) {
26
+ if (edge === undefined)
27
+ return { className: undefined, "data-edge": undefined };
28
+ if (isGradientEdge(edge))
29
+ return { className: gradientEdge({ edge }), "data-edge": "gradient" };
30
+ return { className: undefined, "data-edge": edge };
31
+ }
@@ -1,7 +1,7 @@
1
1
  import { type ComponentProps, type MouseEvent } from "react";
2
2
  import type { Hotkey } from "./hotkeys.js";
3
3
  export type InputProps = Omit<ComponentProps<"input">, "size"> & {
4
- size?: "small" | "medium" | "large";
4
+ size?: "inline" | "small" | "medium" | "large";
5
5
  };
6
6
  export declare function Input({ className, size, ...props }: InputProps): import("react").JSX.Element;
7
7
  export type SearchInputProps = Omit<ComponentProps<"input">, "size" | "value" | "defaultValue"> & {
@@ -31,6 +31,8 @@ export type SearchFieldProps = Omit<ComponentProps<"input">, "size" | "type"> &
31
31
  onClear?: (event: MouseEvent<HTMLButtonElement>) => void;
32
32
  /** Shows this hotkey's hint while the field is empty, such as "/"; the host binds it. */
33
33
  shortcut?: Hotkey;
34
+ /** In a menu or popover: the hairline alone, as the sheet's divider, with no hover or ring. */
35
+ quiet?: boolean;
34
36
  wrapperClassName?: string;
35
37
  };
36
- export declare function SearchField({ size: ownSize, onClear, shortcut, wrapperClassName, className, disabled, ...props }: SearchFieldProps): import("react").JSX.Element;
38
+ export declare function SearchField({ size: ownSize, onClear, quiet, shortcut, wrapperClassName, className, disabled, ...props }: SearchFieldProps): import("react").JSX.Element;