@crewlethq/ui 0.3.0 → 0.4.1

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 (47) hide show
  1. package/dist/Card/Card.d.ts +23 -1
  2. package/dist/Card/index.js +1 -1
  3. package/dist/{Card-AHX3Q2JD.css → Card-RLR254LD.css} +22 -0
  4. package/dist/CodeBlock/CodeBlock.d.ts +68 -9
  5. package/dist/CodeBlock/index.js +3 -3
  6. package/dist/{CodeBlock-U2W6NNVC.css → CodeBlock-GYNJ2HI6.css} +50 -15
  7. package/dist/Copyable/CopyButton.d.ts +11 -1
  8. package/dist/Copyable/Copyable.d.ts +9 -1
  9. package/dist/Copyable/index.js +2 -2
  10. package/dist/DataTable/index.js +2 -2
  11. package/dist/DataView/index.js +3 -3
  12. package/dist/FilterChip/FilterChip.d.ts +37 -5
  13. package/dist/FilterChip/index.d.ts +1 -1
  14. package/dist/FilterChip/index.js +1 -1
  15. package/dist/Meter/Meter.d.ts +79 -2
  16. package/dist/Meter/index.d.ts +2 -2
  17. package/dist/Meter/index.js +1 -1
  18. package/dist/SegmentedControl/SegmentedControl.d.ts +36 -3
  19. package/dist/SegmentedControl/index.d.ts +1 -1
  20. package/dist/SegmentedControl/index.js +2 -2
  21. package/dist/Select/Select.d.ts +16 -1
  22. package/dist/Select/index.js +1 -1
  23. package/dist/Tabs/index.js +1 -1
  24. package/dist/Tabs/roving.d.ts +32 -1
  25. package/dist/ThemeSwitcher/index.js +3 -3
  26. package/dist/TimeWindowPicker/index.js +2 -2
  27. package/dist/{chunk-GUIF6Q6N.js → chunk-2HQR4U4D.js} +1 -1
  28. package/dist/{chunk-GHYY2WPF.js → chunk-34VGEDVV.js} +1 -1
  29. package/dist/{chunk-D3OADIM6.js → chunk-3QRYCPXQ.js} +7 -3
  30. package/dist/{chunk-V663QU4R.js → chunk-5VNUYO76.js} +16 -5
  31. package/dist/chunk-6ZFVFA56.js +108 -0
  32. package/dist/{chunk-HTDFCRJZ.js → chunk-D35VTV2Z.js} +20 -6
  33. package/dist/{chunk-T7FKOJKW.js → chunk-NDCATSW7.js} +11 -3
  34. package/dist/{chunk-5IMACPJS.js → chunk-OFHBK7RI.js} +5 -2
  35. package/dist/{chunk-YMWONFSI.js → chunk-QEUY3A5P.js} +19 -3
  36. package/dist/{chunk-G2QFRTJ2.js → chunk-WOYYD2YR.js} +1 -1
  37. package/dist/{chunk-EHMQQHYR.js → chunk-WQX73TWF.js} +1 -1
  38. package/dist/{chunk-LGCPQIOZ.js → chunk-YODNNSZL.js} +23 -9
  39. package/dist/{chunk-EPOOQZYA.js → chunk-ZFATEERP.js} +32 -6
  40. package/dist/generated/index.js +16 -16
  41. package/dist/index.js +16 -16
  42. package/dist/styles.css +74 -17
  43. package/dist/utils/index.d.ts +1 -1
  44. package/dist/utils/index.js +1 -1
  45. package/dist/utils/useClipboard.d.ts +56 -4
  46. package/package.json +3 -3
  47. package/dist/chunk-ARMRX2YV.js +0 -96
@@ -59,8 +59,23 @@ export interface SelectOption {
59
59
  * `HTMLSelectElement`, because the element a caller's handler lands on depends
60
60
  * on the mode: the listbox's trigger is a button and the native mode's is a
61
61
  * `select`. A handler written for the wider type works in both.
62
+ *
63
+ * `required` AND `aria-required` ARE BOTH HERE, and they are not the same
64
+ * fact. `required` is the form constraint: in native mode it is the attribute
65
+ * the browser validates against and refuses a submission on, and it implies
66
+ * the ARIA fact. `aria-required` states that fact ALONE — which is what a form
67
+ * needs where requiredness is the unmarked default and the visible mark beside
68
+ * the label is welded to the constraint flag, so asking for the constraint
69
+ * would mark every field on the screen. Where both are given the explicit one
70
+ * wins, because it is the more specific of the two statements.
71
+ *
72
+ * The rest of the `aria-*` surface is deliberately NOT picked. The listbox
73
+ * mode draws a button, a search box and a portalled panel rather than one
74
+ * element, so each attribute has to be routed to whichever of them is the
75
+ * combobox at the time; widened wholesale, every name nothing routes would be
76
+ * a prop the component accepts and silently ignores.
62
77
  */
63
- type NativePassthrough = Pick<SelectHTMLAttributes<HTMLSelectElement>, 'autoFocus' | 'name' | 'required' | 'aria-describedby'> & {
78
+ type NativePassthrough = Pick<SelectHTMLAttributes<HTMLSelectElement>, 'autoFocus' | 'name' | 'required' | 'aria-describedby' | 'aria-required'> & {
64
79
  onBlur?: FocusEventHandler<HTMLElement> | undefined;
65
80
  onFocus?: FocusEventHandler<HTMLElement> | undefined;
66
81
  };
@@ -1,4 +1,4 @@
1
- export { Select } from '../chunk-5IMACPJS.js';
1
+ export { Select } from '../chunk-OFHBK7RI.js';
2
2
  import '../chunk-GTOUGQQK.js';
3
3
  import '../chunk-7RPGY3JE.js';
4
4
  import '../chunk-ITHXJHBB.js';
@@ -1,4 +1,4 @@
1
- export { TabPanel, Tabs, tabId } from '../chunk-YMWONFSI.js';
1
+ export { TabPanel, Tabs, tabId } from '../chunk-QEUY3A5P.js';
2
2
  import '../chunk-BHHIIK3M.js';
3
3
  import '../chunk-BZFWDIV6.js';
4
4
  import '../chunk-ODEQTPFM.js';
@@ -8,10 +8,35 @@ import { type KeyboardEvent } from 'react';
8
8
  * an element Tab skips, and the keyboard could not enter the row at all. It
9
9
  * falls back to the first option that can take focus, and to none where every
10
10
  * option is disabled, which is a row with nothing to reach.
11
+ *
12
+ * `selected` is where the stop BELONGS rather than what is chosen: under
13
+ * manual activation that is where focus last was, which is not the chosen
14
+ * option (see [RovingOptions.followFocus]).
11
15
  */
12
16
  export declare function tabStop(items: readonly {
13
17
  disabled?: boolean | undefined;
14
18
  }[], selected: number): number;
19
+ /**
20
+ * How a row places its ONE TAB STOP while focus moves inside it.
21
+ */
22
+ export interface RovingOptions {
23
+ /**
24
+ * Whether the stop FOLLOWS FOCUS rather than staying on the selection.
25
+ *
26
+ * Off by default, which is the only honest answer for a row that SELECTS AS
27
+ * FOCUS MOVES: there the two are the same element, so the stop needs no
28
+ * state of its own and an arrow key costs the render it was going to cost
29
+ * anyway.
30
+ *
31
+ * On is what a row whose arrows do NOT select has to have. With the stop
32
+ * left on the selection, a reader who arrows three options along without
33
+ * committing, Tabs out of the row and Tabs back is dropped on the option
34
+ * they started from rather than the one they left, and every move they made
35
+ * is gone with nothing said about it. It is half of manual activation, not
36
+ * a refinement of it.
37
+ */
38
+ followFocus?: boolean | undefined;
39
+ }
15
40
  /**
16
41
  * Roving focus over a row of buttons: ONE TAB STOP for the row, the arrows to
17
42
  * move inside it.
@@ -26,11 +51,17 @@ export declare function tabStop(items: readonly {
26
51
  * tab is a section that pushes a history entry, and a row that selected on
27
52
  * every arrow press left one entry per keystroke for Back to walk through.
28
53
  *
54
+ * THE TWO ARGUMENTS GO TOGETHER. A caller that passes no `select` is a row
55
+ * under manual activation and wants `followFocus`, because the stop and the
56
+ * selection have come apart; a caller that passes one wants neither.
57
+ *
29
58
  * A DISABLED BUTTON IS STEPPED OVER rather than focused: a browser refuses it
30
59
  * focus, so a move that lands on one drops focus on the page body and the
31
60
  * next arrow press does nothing at all.
32
61
  */
33
- export declare function useRoving(count: number, select: ((index: number) => void) | null): {
62
+ export declare function useRoving(count: number, select: ((index: number) => void) | null, { followFocus }?: RovingOptions): {
34
63
  buttons: import("react").RefObject<(HTMLButtonElement | null)[]>;
35
64
  onKeyDown: (event: KeyboardEvent<HTMLElement>, vertical: boolean) => void;
65
+ onFocusAt: (index: number) => void;
66
+ focused: number | null;
36
67
  };
@@ -1,6 +1,6 @@
1
- export { DensitySwitcher, ThemeSwitcher, applyDensity, applyStoredPreferences, applyTheme, useDensityPreference, useThemePreference } from '../chunk-GUIF6Q6N.js';
2
- import '../chunk-ARMRX2YV.js';
3
- import '../chunk-YMWONFSI.js';
1
+ export { DensitySwitcher, ThemeSwitcher, applyDensity, applyStoredPreferences, applyTheme, useDensityPreference, useThemePreference } from '../chunk-2HQR4U4D.js';
2
+ import '../chunk-6ZFVFA56.js';
3
+ import '../chunk-QEUY3A5P.js';
4
4
  import '../chunk-BHHIIK3M.js';
5
5
  import '../chunk-BZFWDIV6.js';
6
6
  import '../chunk-ODEQTPFM.js';
@@ -1,5 +1,5 @@
1
- export { DEFAULT_TIME_WINDOW_LABELS, TIME_WINDOW_SPANS, TimeWindowPicker, describeTimeWindow, isTimeWindowSet, parseTimeWindowDuration, resolveTimeWindow, timeWindowMatchesPreset } from '../chunk-G2QFRTJ2.js';
2
- import '../chunk-5IMACPJS.js';
1
+ export { DEFAULT_TIME_WINDOW_LABELS, TIME_WINDOW_SPANS, TimeWindowPicker, describeTimeWindow, isTimeWindowSet, parseTimeWindowDuration, resolveTimeWindow, timeWindowMatchesPreset } from '../chunk-WOYYD2YR.js';
2
+ import '../chunk-OFHBK7RI.js';
3
3
  import '../chunk-DD5BBGBE.js';
4
4
  import '../chunk-GXQ3B46I.js';
5
5
  import '../chunk-GTOUGQQK.js';
@@ -1,4 +1,4 @@
1
- import { SegmentedControl } from './chunk-ARMRX2YV.js';
1
+ import { SegmentedControl } from './chunk-6ZFVFA56.js';
2
2
  import { LightModeGlyph, ComputerGlyph, DarkModeGlyph } from '@crewlethq/icons/glyphs';
3
3
  import { useState, useEffect, useCallback } from 'react';
4
4
  import { jsx } from 'react/jsx-runtime';
@@ -2,7 +2,7 @@ import { Toolbar } from './chunk-5EKIWVOR.js';
2
2
  import { PageHeader } from './chunk-HOXGBLEY.js';
3
3
  import { RelativeTime } from './chunk-3CZ7XAVY.js';
4
4
  import { Kbd } from './chunk-SFZOKW2G.js';
5
- import { DataTable } from './chunk-EHMQQHYR.js';
5
+ import { DataTable } from './chunk-WQX73TWF.js';
6
6
  import { Menu } from './chunk-FBZAKNSX.js';
7
7
  import { DateTimePicker } from './chunk-UG46QBLT.js';
8
8
  import { Popover } from './chunk-DD5BBGBE.js';
@@ -1,4 +1,4 @@
1
- import { useClipboard } from './chunk-V663QU4R.js';
1
+ import { useClipboard } from './chunk-5VNUYO76.js';
2
2
  import { IconButton } from './chunk-F27EPQ7D.js';
3
3
  import { Button } from './chunk-BZYDZBAN.js';
4
4
  import { VisuallyHidden } from './chunk-BZFWDIV6.js';
@@ -26,9 +26,11 @@ function CopyButton({
26
26
  size = "small",
27
27
  variant = "tertiary",
28
28
  title,
29
+ resetMs,
30
+ failedResetMs,
29
31
  className
30
32
  }) {
31
- const { state, copy } = useClipboard();
33
+ const { state, copy } = useClipboard({ resetMs, failedResetMs });
32
34
  const onClick = useCallback(() => {
33
35
  void copy(text);
34
36
  }, [copy, text]);
@@ -67,10 +69,12 @@ var Copyable = ({
67
69
  title = "Copy",
68
70
  copiedMessage = "copied to the clipboard",
69
71
  failedMessage = "the browser refused the clipboard",
72
+ resetMs,
73
+ failedResetMs,
70
74
  className
71
75
  }) => {
72
76
  const text = value === void 0 || value === null ? "" : String(value);
73
- const { state, copy } = useClipboard();
77
+ const { state, copy } = useClipboard({ resetMs, failedResetMs });
74
78
  const onCopy = useCallback(() => {
75
79
  void copy(text);
76
80
  }, [copy, text]);
@@ -1,19 +1,30 @@
1
1
  import { useState, useRef, useEffect, useCallback } from 'react';
2
2
 
3
3
  // src/utils/useClipboard.ts
4
- function useClipboard({ resetMs = 2e3 } = {}) {
4
+ function useClipboard({ resetMs = 2e3, failedResetMs } = {}) {
5
5
  const [state, setState] = useState("idle");
6
6
  const timer = useRef(void 0);
7
- useEffect(() => () => clearTimeout(timer.current), []);
7
+ const live = useRef(true);
8
+ useEffect(() => {
9
+ live.current = true;
10
+ return () => {
11
+ live.current = false;
12
+ clearTimeout(timer.current);
13
+ timer.current = void 0;
14
+ };
15
+ }, []);
8
16
  const copy = useCallback(
9
17
  async (text) => {
10
18
  const ok = await writeClipboard(typeof text === "function" ? text() : text);
19
+ if (!live.current) return ok;
11
20
  setState(ok ? "copied" : "failed");
12
21
  clearTimeout(timer.current);
13
- timer.current = setTimeout(() => setState("idle"), resetMs);
22
+ timer.current = void 0;
23
+ const after = ok ? resetMs : failedResetMs === void 0 ? resetMs : failedResetMs;
24
+ if (after !== null) timer.current = setTimeout(() => setState("idle"), after);
14
25
  return ok;
15
26
  },
16
- [resetMs]
27
+ [resetMs, failedResetMs]
17
28
  );
18
29
  return { state, copy };
19
30
  }
@@ -48,4 +59,4 @@ function execCommandCopy(text) {
48
59
  }
49
60
  }
50
61
 
51
- export { useClipboard };
62
+ export { useClipboard, writeClipboard };
@@ -0,0 +1,108 @@
1
+ import { useRoving, tabStop, tabId } from './chunk-QEUY3A5P.js';
2
+ import { Count } from './chunk-BHHIIK3M.js';
3
+ import { VisuallyHidden } from './chunk-BZFWDIV6.js';
4
+ import { cx } from './chunk-ODEQTPFM.js';
5
+ import './Tabs-VLICJFVB.css';
6
+ import './SegmentedControl-X3BI3XU4.css';
7
+ import { useId } from 'react';
8
+ import { jsxs, jsx } from 'react/jsx-runtime';
9
+
10
+ var ACTIVATE_HINT = "Press Enter or Space to choose the option you land on.";
11
+ function SegmentedControl({
12
+ label,
13
+ options,
14
+ value,
15
+ onValueChange,
16
+ size = "md",
17
+ className = "",
18
+ semantics,
19
+ panelId,
20
+ activate = "automatic",
21
+ activateHint
22
+ }) {
23
+ const radio = semantics === "radio";
24
+ const manual = radio && activate === "manual";
25
+ const cards = options.some((option) => option.description !== void 0);
26
+ const { buttons, onKeyDown, onFocusAt, focused } = useRoving(
27
+ options.length,
28
+ radio && !manual ? (index) => onValueChange(options[index].value) : null,
29
+ { followFocus: manual }
30
+ );
31
+ const chosenIndex = Math.max(
32
+ 0,
33
+ options.findIndex((option) => option.value === value)
34
+ );
35
+ const stop = tabStop(options, focused ?? chosenIndex);
36
+ const hintId = useId();
37
+ const hint = manual ? activateHint === void 0 ? ACTIVATE_HINT : activateHint : null;
38
+ return /* @__PURE__ */ jsxs(
39
+ "div",
40
+ {
41
+ className: cx(
42
+ "crewlet-tabs",
43
+ /*
44
+ * THE PILL CHROME IS NOT WORN IN CARDS MODE. A card row is not a chip
45
+ * in a tinted container: the pill's own rules set the container's
46
+ * background, border, padding and `width: fit-content`, and each
47
+ * option's padding, radius and active chip. Left on, every one of
48
+ * them still applied (the two stylesheets are single-class rules and
49
+ * Tabs.css is bundled last), so the cards were drawn inside a pill
50
+ * bar sized to its content, which is what the mode exists to avoid.
51
+ */
52
+ !cards && "crewlet-tabs--pill",
53
+ `crewlet-tabs--${size}`,
54
+ "crewlet-segmented",
55
+ cards && "crewlet-segmented--cards",
56
+ className
57
+ ),
58
+ role: radio ? "radiogroup" : "tablist",
59
+ "aria-label": label,
60
+ "aria-describedby": hint ? hintId : void 0,
61
+ children: [
62
+ options.map((option, index) => {
63
+ const chosen = option.value === value;
64
+ const named = option.srLabel ?? (option.label ? void 0 : option.title);
65
+ return /* @__PURE__ */ jsxs(
66
+ "button",
67
+ {
68
+ ref: (el) => {
69
+ buttons.current[index] = el;
70
+ },
71
+ type: "button",
72
+ role: radio ? "radio" : "tab",
73
+ id: radio || !panelId ? void 0 : tabId(panelId, option.value),
74
+ "aria-checked": radio ? chosen : void 0,
75
+ "aria-selected": radio ? void 0 : chosen,
76
+ "aria-controls": radio ? void 0 : panelId,
77
+ "aria-label": named,
78
+ tabIndex: index === stop ? 0 : -1,
79
+ title: option.title,
80
+ disabled: option.disabled,
81
+ className: cx(
82
+ "crewlet-tabs__tab",
83
+ "crewlet-segmented__option",
84
+ chosen && "is-active",
85
+ option.disabled && "is-disabled"
86
+ ),
87
+ onClick: () => onValueChange(option.value),
88
+ onFocus: () => onFocusAt(index),
89
+ onKeyDown: (event) => onKeyDown(event, radio),
90
+ children: [
91
+ option.icon ? /* @__PURE__ */ jsx("span", { className: "crewlet-tabs__icon", children: option.icon }) : null,
92
+ option.label || option.description ? /* @__PURE__ */ jsxs("span", { className: "crewlet-segmented__text", children: [
93
+ /* @__PURE__ */ jsx("span", { className: "crewlet-tabs__label", children: option.label }),
94
+ option.description ? /* @__PURE__ */ jsx("span", { className: "crewlet-segmented__description", children: option.description }) : null
95
+ ] }) : null,
96
+ option.count != null ? /* @__PURE__ */ jsx(Count, { value: option.count }) : null
97
+ ]
98
+ },
99
+ option.value
100
+ );
101
+ }),
102
+ hint ? /* @__PURE__ */ jsx(VisuallyHidden, { id: hintId, children: hint }) : null
103
+ ]
104
+ }
105
+ );
106
+ }
107
+
108
+ export { SegmentedControl };
@@ -1,7 +1,7 @@
1
- import { CopyButton } from './chunk-D3OADIM6.js';
1
+ import { CopyButton } from './chunk-3QRYCPXQ.js';
2
2
  import { cx } from './chunk-ODEQTPFM.js';
3
- import './CodeBlock-U2W6NNVC.css';
4
- import { forwardRef, useRef } from 'react';
3
+ import './CodeBlock-GYNJ2HI6.css';
4
+ import { forwardRef, useRef, useState, useLayoutEffect } from 'react';
5
5
  import { jsxs, jsx } from 'react/jsx-runtime';
6
6
 
7
7
  var CodeBlock = forwardRef(function CodeBlock2({
@@ -16,6 +16,7 @@ var CodeBlock = forwardRef(function CodeBlock2({
16
16
  maxHeight,
17
17
  plain = false,
18
18
  selectable = false,
19
+ focusWhenScrollable = false,
19
20
  label,
20
21
  className,
21
22
  style,
@@ -25,6 +26,19 @@ var CodeBlock = forwardRef(function CodeBlock2({
25
26
  const body = code.replace(/\n+$/, "");
26
27
  const showHeader = !plain && (filename !== void 0 || language !== void 0 || copyable);
27
28
  const perLine = showLineNumbers || prompt !== void 0;
29
+ const [overflows, setOverflows] = useState(false);
30
+ useLayoutEffect(() => {
31
+ if (!focusWhenScrollable) return;
32
+ const node = box.current;
33
+ if (!node) return;
34
+ const measure = () => setOverflows(node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth);
35
+ measure();
36
+ if (typeof ResizeObserver !== "function") return;
37
+ const watch = new ResizeObserver(measure);
38
+ watch.observe(node);
39
+ return () => watch.disconnect();
40
+ }, [focusWhenScrollable, body, wrap, showLineNumbers, prompt, maxHeight]);
41
+ const focusable = selectable || focusWhenScrollable && overflows;
28
42
  const onKeyDown = (event) => {
29
43
  const isA = event.code === "KeyA" || !event.code && event.key.toLowerCase() === "a";
30
44
  if (!isA || event.altKey || event.shiftKey || !(event.metaKey || event.ctrlKey)) return;
@@ -77,9 +91,9 @@ var CodeBlock = forwardRef(function CodeBlock2({
77
91
  ref: box,
78
92
  className: "crewlet-codeblock__pre",
79
93
  "data-language": language,
80
- tabIndex: selectable ? 0 : void 0,
81
- role: selectable ? "region" : void 0,
82
- "aria-label": selectable ? label : void 0,
94
+ tabIndex: focusable ? 0 : void 0,
95
+ role: focusable ? "region" : void 0,
96
+ "aria-label": focusable ? label : void 0,
83
97
  onKeyDown: selectable ? onKeyDown : void 0,
84
98
  children: /* @__PURE__ */ jsx("code", { className: "crewlet-codeblock__code", children: content })
85
99
  }
@@ -2,7 +2,7 @@ import { CardHeaderSlotContext } from './chunk-AV2DXAEL.js';
2
2
  import { Count } from './chunk-BHHIIK3M.js';
3
3
  import { useHeadingLevel, HeadingLevelProvider, nextHeadingLevel, headingTag } from './chunk-PQADV6P6.js';
4
4
  import { cx } from './chunk-ODEQTPFM.js';
5
- import './Card-AHX3Q2JD.css';
5
+ import './Card-RLR254LD.css';
6
6
  import { createContext, useId, useState, isValidElement, useRef, useCallback, useMemo, Children, cloneElement, useContext, useEffect, Fragment } from 'react';
7
7
  import { jsx, jsxs } from 'react/jsx-runtime';
8
8
 
@@ -198,7 +198,7 @@ var CardFooter = ({ variant = "actions", padding = "sm", className, children, ..
198
198
  children
199
199
  }
200
200
  );
201
- var CardTitle = ({ as, className, children, ...rest }) => {
201
+ var CardTitle = ({ as, truncate = false, className, children, ...rest }) => {
202
202
  const card = useContext(CardContext);
203
203
  const fallback = useHeadingLevel();
204
204
  const Tag = as ?? headingTag(card?.titleLevel ?? fallback);
@@ -206,7 +206,15 @@ var CardTitle = ({ as, className, children, ...rest }) => {
206
206
  useEffect(() => {
207
207
  register?.();
208
208
  }, [register]);
209
- return /* @__PURE__ */ jsx(Tag, { ...rest, id: rest.id ?? card?.titleId, className: cx("crewlet-card__title", className), children });
209
+ return /* @__PURE__ */ jsx(
210
+ Tag,
211
+ {
212
+ ...rest,
213
+ id: rest.id ?? card?.titleId,
214
+ className: cx("crewlet-card__title", truncate && "crewlet-card__title--truncate", className),
215
+ children
216
+ }
217
+ );
210
218
  };
211
219
  var CardDescription = ({ className, children, ...rest }) => /* @__PURE__ */ jsx("p", { ...rest, className: cx("crewlet-card__description", className), children });
212
220
  var SLOT_TYPES = /* @__PURE__ */ new Set([CardHeader, CardBody, CardFooter]);
@@ -90,6 +90,7 @@ var Select = forwardRef(function Select2({
90
90
  emptyMessage,
91
91
  describedBy: native["aria-describedby"],
92
92
  required: native.required,
93
+ ariaRequired: native["aria-required"],
93
94
  focusOnMount: native.autoFocus,
94
95
  name: native.name,
95
96
  onBlur: native.onBlur,
@@ -165,6 +166,7 @@ var ListboxSelect = forwardRef(function ListboxSelect2({
165
166
  emptyMessage,
166
167
  describedBy,
167
168
  required,
169
+ ariaRequired,
168
170
  focusOnMount,
169
171
  name,
170
172
  onBlur,
@@ -339,6 +341,7 @@ var ListboxSelect = forwardRef(function ListboxSelect2({
339
341
  const listId = listbox.listId;
340
342
  const activeId = open && flat.length > 0 ? listbox.optionId(listbox.active) : void 0;
341
343
  const comboOnTrigger = !searchable;
344
+ const requiredForAria = ariaRequired ?? required;
342
345
  let index = -1;
343
346
  const rows = groups.map((group, position2) => {
344
347
  const items = group.items.map((option) => {
@@ -414,7 +417,7 @@ var ListboxSelect = forwardRef(function ListboxSelect2({
414
417
  "aria-controls": listId,
415
418
  "aria-activedescendant": activeId,
416
419
  "aria-label": searchPlaceholder,
417
- "aria-required": required,
420
+ "aria-required": requiredForAria,
418
421
  placeholder: searchPlaceholder,
419
422
  value: query,
420
423
  onChange: (event) => setQuery(event.target.value),
@@ -474,7 +477,7 @@ var ListboxSelect = forwardRef(function ListboxSelect2({
474
477
  "aria-label": ariaLabel,
475
478
  "aria-describedby": describedBy,
476
479
  "aria-invalid": error || void 0,
477
- "aria-required": comboOnTrigger ? required : void 0,
480
+ "aria-required": comboOnTrigger ? requiredForAria : void 0,
478
481
  disabled,
479
482
  onBlur,
480
483
  onFocus,
@@ -1,15 +1,16 @@
1
1
  import { Count } from './chunk-BHHIIK3M.js';
2
2
  import { cx } from './chunk-ODEQTPFM.js';
3
3
  import './Tabs-VLICJFVB.css';
4
- import React, { useRef, useLayoutEffect } from 'react';
4
+ import React, { useRef, useState, useLayoutEffect } from 'react';
5
5
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
6
6
 
7
7
  function tabStop(items, selected) {
8
8
  if (selected >= 0 && !items[selected]?.disabled) return selected;
9
9
  return items.findIndex((item) => !item.disabled);
10
10
  }
11
- function useRoving(count, select) {
11
+ function useRoving(count, select, { followFocus = false } = {}) {
12
12
  const buttons = useRef([]);
13
+ const [landed, setLanded] = useState(null);
13
14
  function step(from, by) {
14
15
  for (let i = 0; i < count; i += 1) {
15
16
  const at = ((from + by * i) % count + count) % count;
@@ -37,6 +38,9 @@ function useRoving(count, select) {
37
38
  return null;
38
39
  }
39
40
  }
41
+ const onFocusAt = (index) => {
42
+ if (followFocus) setLanded(index);
43
+ };
40
44
  const onKeyDown = (event, vertical) => {
41
45
  const at = buttons.current.findIndex((button) => button === document.activeElement);
42
46
  if (at < 0) return;
@@ -44,9 +48,21 @@ function useRoving(count, select) {
44
48
  if (next === null) return;
45
49
  event.preventDefault();
46
50
  buttons.current[next]?.focus();
51
+ onFocusAt(next);
47
52
  select?.(next);
48
53
  };
49
- return { buttons, onKeyDown };
54
+ return {
55
+ buttons,
56
+ onKeyDown,
57
+ onFocusAt,
58
+ /*
59
+ * Where the stop belongs, or null for "on the caller's selection, as
60
+ * ever". Bounded by the CURRENT count, because a row whose options
61
+ * changed under a remembered index would otherwise put its only stop past
62
+ * its own end, where Tab finds nothing at all.
63
+ */
64
+ focused: followFocus && landed !== null && landed < count ? landed : null
65
+ };
50
66
  }
51
67
  function tabId(panelId, value) {
52
68
  return `${panelId}-tab-${value}`;
@@ -1,4 +1,4 @@
1
- import { Select } from './chunk-5IMACPJS.js';
1
+ import { Select } from './chunk-OFHBK7RI.js';
2
2
  import { Popover } from './chunk-DD5BBGBE.js';
3
3
  import { today, shiftMonth, CalendarGrid, compareDays } from './chunk-GXQ3B46I.js';
4
4
  import { Input } from './chunk-O6232G37.js';
@@ -3,7 +3,7 @@ import { EmptyValue } from './chunk-EUCC37WY.js';
3
3
  import { Modal } from './chunk-2EOKFKHX.js';
4
4
  import { useCardHeaderSlot } from './chunk-AV2DXAEL.js';
5
5
  import { Checkbox } from './chunk-2PRH365R.js';
6
- import { useClipboard } from './chunk-V663QU4R.js';
6
+ import { useClipboard } from './chunk-5VNUYO76.js';
7
7
  import { IconButton } from './chunk-F27EPQ7D.js';
8
8
  import { Button } from './chunk-BZYDZBAN.js';
9
9
  import { VisuallyHidden } from './chunk-BZFWDIV6.js';
@@ -9,6 +9,13 @@ function meterTone(percent) {
9
9
  if (percent >= 75) return "warning";
10
10
  return "brand";
11
11
  }
12
+ function progressTone(percent) {
13
+ return percent >= 100 ? "success" : "brand";
14
+ }
15
+ var RAMPS = {
16
+ spent: meterTone,
17
+ progress: progressTone
18
+ };
12
19
  function Meter({
13
20
  value,
14
21
  max,
@@ -16,34 +23,41 @@ function Meter({
16
23
  valueText,
17
24
  hint,
18
25
  tone,
26
+ polarity = "spent",
19
27
  size = "default",
20
28
  hideLabel = false,
21
29
  className,
22
30
  ...rest
23
31
  }) {
24
32
  const labelId = useId();
25
- const percent = max > 0 ? Math.min(100, Math.max(0, value / max * 100)) : 0;
26
- const drawn = tone ?? meterTone(percent);
33
+ const bounded = max > 0;
34
+ const percent = bounded ? Math.min(100, Math.max(0, value / max * 100)) : 0;
35
+ const drawn = tone ?? RAMPS[polarity](percent);
27
36
  const trailing = hint ?? valueText;
37
+ const reported = bounded ? Math.min(max, Math.max(0, value)) : value;
38
+ const spoken = bounded ? valueText ?? (reported === value ? void 0 : `${value} of ${max}`) : void 0;
39
+ const unspoken = !bounded && valueText !== void 0 && (hideLabel || hint !== void 0) ? valueText : void 0;
28
40
  return /* @__PURE__ */ jsxs("div", { ...rest, className: cx("crewlet-meter", `crewlet-meter--${size}`, className), children: [
29
41
  hideLabel ? /* @__PURE__ */ jsx(VisuallyHidden, { id: labelId, children: label }) : /* @__PURE__ */ jsxs("div", { className: "crewlet-meter__legend", children: [
30
42
  /* @__PURE__ */ jsx("span", { id: labelId, className: "crewlet-meter__label", children: label }),
31
43
  trailing === void 0 ? null : /* @__PURE__ */ jsx("span", { className: "crewlet-meter__hint", children: trailing })
32
44
  ] }),
45
+ unspoken === void 0 ? null : /* @__PURE__ */ jsx(VisuallyHidden, { children: unspoken }),
33
46
  /* @__PURE__ */ jsx(
34
47
  "div",
35
48
  {
36
49
  className: "crewlet-meter__track",
37
- role: "meter",
38
- "aria-labelledby": labelId,
39
- "aria-valuenow": value,
40
- "aria-valuemin": 0,
41
- "aria-valuemax": max,
42
- "aria-valuetext": valueText,
50
+ role: bounded ? "meter" : void 0,
51
+ "aria-labelledby": bounded ? labelId : void 0,
52
+ "aria-valuenow": bounded ? reported : void 0,
53
+ "aria-valuemin": bounded ? 0 : void 0,
54
+ "aria-valuemax": bounded ? max : void 0,
55
+ "aria-valuetext": spoken,
56
+ "aria-hidden": bounded ? void 0 : true,
43
57
  children: /* @__PURE__ */ jsx("div", { className: "crewlet-meter__fill", "data-tone": drawn, style: { width: `${percent}%` } })
44
58
  }
45
59
  )
46
60
  ] });
47
61
  }
48
62
 
49
- export { Meter, meterTone };
63
+ export { Meter, meterTone, progressTone };