@fungi.computer/caps 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +58 -0
  2. package/dist/blocks/app-bar.d.ts +4 -2
  3. package/dist/blocks/app-bar.js +3 -3
  4. package/dist/blocks/empty-state.d.ts +31 -4
  5. package/dist/blocks/empty-state.js +14 -4
  6. package/dist/blocks/loading-state.d.ts +41 -0
  7. package/dist/blocks/loading-state.js +18 -3
  8. package/dist/blocks/permission-grid.js +1 -1
  9. package/dist/blocks/result-state.d.ts +18 -0
  10. package/dist/blocks/result-state.js +7 -2
  11. package/dist/blocks/setup-notice.d.ts +32 -0
  12. package/dist/blocks/setup-notice.js +23 -0
  13. package/dist/components/button.js +2 -2
  14. package/dist/components/confirm-action.d.ts +6 -1
  15. package/dist/components/confirm-action.js +3 -3
  16. package/dist/components/dialog.js +1 -1
  17. package/dist/components/failure-line.d.ts +46 -0
  18. package/dist/components/failure-line.js +12 -0
  19. package/dist/components/keycap.d.ts +15 -2
  20. package/dist/components/keycap.js +3 -3
  21. package/dist/components/loader-blobs.d.ts +3 -2
  22. package/dist/components/loader-blobs.js +5 -3
  23. package/dist/components/loader.d.ts +8 -2
  24. package/dist/components/loader.js +4 -2
  25. package/dist/components/toaster.d.ts +5 -1
  26. package/dist/components/toaster.js +16 -2
  27. package/dist/styles.css +485 -28
  28. package/docs/api/README.md +2 -0
  29. package/docs/api/blocks/app-bar.md +9 -1
  30. package/docs/api/blocks/empty-state.md +30 -11
  31. package/docs/api/blocks/loading-state.md +33 -0
  32. package/docs/api/blocks/result-state.md +30 -6
  33. package/docs/api/blocks/setup-notice.md +82 -0
  34. package/docs/api/components/confirm-action.md +8 -6
  35. package/docs/api/components/data-list.md +33 -0
  36. package/docs/api/components/failure-line.md +50 -0
  37. package/docs/api/components/field.md +9 -8
  38. package/docs/api/components/icon-checkbox.md +55 -0
  39. package/docs/api/components/icon-radio.md +39 -5
  40. package/docs/api/components/keycap.md +10 -7
  41. package/docs/api/components/loader.md +18 -7
  42. package/docs/api/components/toaster.md +2 -0
  43. package/package.json +15 -6
  44. package/src/first-run.css +100 -0
  45. package/src/styles.css +394 -21
package/README.md CHANGED
@@ -812,6 +812,64 @@ closes the disclosure and returns focus to the launcher. Reduced motion settles
812
812
  immediately. Available-app and primary-action buttons do not claim toggle state.
813
813
  `alwaysExpanded` keeps the disclosure open for dock layouts.
814
814
 
815
+ ### First-run kit
816
+
817
+ Compose these for an App's first ten seconds; Caps owns the presentation and
818
+ your App owns the words.
819
+
820
+ - `EmptyState` takes `primaryAction` (`label`, `onSelect`, `pending`,
821
+ `disabled`, `disabledReason`): at most one primary per state. Put secondary
822
+ buttons in `actions`. `suggestions` chips accept `pending` and `disabled`.
823
+ `visual="blobs"` is the living idle Loader and is what a `panel` shows when
824
+ you pass no visual; `speech` keeps the mascot.
825
+ - `ResultState` is the failure card: `tone` (`failed`, `done`, `neutral`), up to
826
+ three numbered `steps`, `visual` art, and `headingLevel`.
827
+ - `LoadingState` takes `size`, `variant`, `detail`, and `slowAfterMs`
828
+ (default 8000) with `onRetry`; after that it adds "Still working…" and a
829
+ Retry. Formations by moment: `line` opening a window, `wander` connecting,
830
+ `square` installing, `bloom` a run in progress. `Loader pace="idle"` slows a
831
+ formation for decorative art. Reduced motion shows the static silhouette.
832
+ - `Keycap touch="hide"` drops a keyboard legend on touch-first devices;
833
+ `KeycapButton` hides its `shortcut` legend that way.
834
+
835
+ - `SetupNotice` (`blocks/setup-notice`) names a missing setup with its one fix:
836
+ `need` (title), `cause` (one sentence), required `fix`, optional `secondary`
837
+ and `available` (what still works), and `variant` `panel` (an EmptyState
838
+ panel), `banner` or `inline` (a warning Alert). While `fix.pending` the button
839
+ shows the Loader and reads "Waiting for {waitingFor}…". Render it only while
840
+ the setup is missing; the host unmounts it when the fact turns ready. One
841
+ notice per window, and the notice is the call to action.
842
+ - `FailureLine` (`components/failure-line`) is the item failure: an error dot,
843
+ one plain line, labelled `Retry` / `Dismiss`, and "Failed {n} times" once when
844
+ `count` is above 1. `density` is `row` or `tile`. Never use the `bang` glyph.
845
+
846
+ Which key and which status light:
847
+
848
+ | Need | Use |
849
+ | ------------------------------------------------------------------- | ----------------------- |
850
+ | An action button in an App, game dock or toy panel | `KeycapButton` |
851
+ | A keyboard legend beside copy or inside a key | `Keycap` (native `kbd`) |
852
+ | State of an App row, chip or toolbar | `Status` (gooey bead) |
853
+ | An editorial or field-guide progress mark (ring, arc, check, cross) | `StatusMark` |
854
+
855
+ `bang` is for unread beads only. Never put it in a button or chip; use a dot.
856
+
857
+ ### Toaster CSS placement
858
+
859
+ `Toaster` wraps Sonner, which injects its own unlayered stylesheet at runtime.
860
+ Anything inside a cascade layer loses to it, so the toast rules (Stipe token
861
+ variables, font, close button, icons) live unlayered near the top of
862
+ `src/styles.css`, with raised specificity. Keep new toast rules there, not in
863
+ `first-run.css` or a layer.
864
+
865
+ ### Hairlines
866
+
867
+ `--stipe-hairline` (about 22%) outlines controls: Keycap, Button and chips keep
868
+ it, and the plastic look stays. `--stipe-hairline-quiet` (about 10%, an additive
869
+ Stipe token) draws separators inside a surface: table row lines, list and panel
870
+ dividers, prose rules. Use fewer frames, not lighter ones: never put a framed
871
+ box inside a framed box, and never change a control outline to the quiet token.
872
+
815
873
  ### Status beads and dashboard controls
816
874
 
817
875
  `Status` is a gooey SVG bead. Set `glyph` (`dot`, `pill` or `bang`), `tone`, and
@@ -15,8 +15,10 @@ export type AppBarProps = {
15
15
  /** Portal container for the App dial; defaults to the document body. */
16
16
  container?: HTMLElement | null;
17
17
  windows: readonly AppBarItem[];
18
- /** Team Apps available to launch from the speed dial. */
18
+ /** Apps available to launch from the speed dial. */
19
19
  launchers: readonly AppBarItem[];
20
+ /** The Apps section heading and its launcher name; the host owns the wording. */
21
+ appsLabel?: string;
20
22
  /** Recent Sessions supplied by the shell's session owner. */
21
23
  sessions?: readonly AppBarItem[];
22
24
  /** Keep long launcher names readable instead of truncating them. */
@@ -40,4 +42,4 @@ export type AppBarProps = {
40
42
  };
41
43
  };
42
44
  /** An anchored launcher between persistent running apps and a spring-out app tray. */
43
- export declare function AppBar({ container, windows, launchers, sessions, launcherLabels, alwaysExpanded, isOpen, onOpenChange, emptyLabel, launcher, primaryAction, }: AppBarProps): import("react/jsx-runtime").JSX.Element;
45
+ export declare function AppBar({ container, windows, launchers, sessions, appsLabel, launcherLabels, alwaysExpanded, isOpen, onOpenChange, emptyLabel, launcher, primaryAction, }: AppBarProps): import("react/jsx-runtime").JSX.Element;
@@ -42,7 +42,7 @@ function AppBarItemView({ item, available, expanded, onSelect, onClose, session
42
42
  }, children: [_jsx("span", { ref: icon, className: "caps-app-bar-icon", "aria-hidden": "true", children: item.icon ?? _jsx(PanelsTopLeft, {}) }), available ? (_jsx("span", { className: "caps-app-bar-item-label", children: item.label })) : null] }), item.onClose ? (_jsx("button", { type: "button", className: "caps-app-bar-close", "aria-label": `Close ${item.label}`, tabIndex: tabIndex, onClick: onClose, children: _jsx(X, { "aria-hidden": "true" }) })) : null] }));
43
43
  }
44
44
  /** An anchored launcher between persistent running apps and a spring-out app tray. */
45
- export function AppBar({ container, windows, launchers, sessions = [], launcherLabels = "truncate", alwaysExpanded = false, isOpen, onOpenChange, emptyLabel, launcher, primaryAction, }) {
45
+ export function AppBar({ container, windows, launchers, sessions = [], appsLabel = "Apps", launcherLabels = "truncate", alwaysExpanded = false, isOpen, onOpenChange, emptyLabel, launcher, primaryAction, }) {
46
46
  const popoverRoot = useOverlayRoot("popover");
47
47
  const portalContainer = container ?? popoverRoot;
48
48
  const [internalOpen, setInternalOpen] = useState(false);
@@ -232,9 +232,9 @@ export function AppBar({ container, windows, launchers, sessions = [], launcherL
232
232
  else
233
233
  setOpen(next);
234
234
  };
235
- return (_jsx(BasePopover.Root, { open: expanded, onOpenChange: changeOpen, modal: false, children: _jsxs("nav", { ref: root, style: appBarStyle, className: "caps-app-bar", "aria-label": "Team Apps and Sessions launcher", "data-open": expanded ? "true" : "false", children: [_jsx("div", { ref: runningRef, className: "caps-app-bar-region caps-app-bar-items", role: "group", "aria-label": "Running apps", "aria-hidden": "false", children: windows.map((item) => renderItem(item, false)) }), _jsx("div", { className: "caps-app-bar-launcher", children: _jsx("button", { ref: launcherRef, type: "button", className: "caps-app-bar-launcher-button", "aria-label": launcher.label, title: launcher.label, "aria-expanded": expanded, "aria-controls": primaryAction ? actionId : availableId, onClick: () => {
235
+ return (_jsx(BasePopover.Root, { open: expanded, onOpenChange: changeOpen, modal: false, children: _jsxs("nav", { ref: root, style: appBarStyle, className: "caps-app-bar", "aria-label": `${appsLabel} and Sessions launcher`, "data-open": expanded ? "true" : "false", children: [_jsx("div", { ref: runningRef, className: "caps-app-bar-region caps-app-bar-items", role: "group", "aria-label": "Running apps", "aria-hidden": "false", children: windows.map((item) => renderItem(item, false)) }), _jsx("div", { className: "caps-app-bar-launcher", children: _jsx("button", { ref: launcherRef, type: "button", className: "caps-app-bar-launcher-button", "aria-label": launcher.label, title: launcher.label, "aria-expanded": expanded, "aria-controls": primaryAction ? actionId : availableId, onClick: () => {
236
236
  setOpen(!open);
237
- }, children: _jsx("span", { className: "caps-app-bar-mascot", "aria-hidden": "true", children: launcher.icon ?? _jsx(PanelsTopLeft, {}) }) }) }), portalContainer ? (_jsx(BasePopover.Portal, { container: portalContainer, keepMounted: true, children: _jsx(BasePopover.Positioner, { anchor: root, side: "bottom", align: "start", sideOffset: 4, collisionPadding: 8, collisionBoundary: portalContainer === document.body ? undefined : portalContainer, className: "caps-app-bar-positioner", children: _jsxs(BasePopover.Popup, { initialFocus: false, finalFocus: false, style: appBarStyle, ref: availableRef, id: availableId, className: "caps-app-bar-region caps-app-bar-launchers", role: "group", "aria-label": "Speed dial", "aria-hidden": !expanded, children: [launchers.length > 0 ? (_jsxs("div", { className: "caps-app-bar-section", role: "group", "aria-label": "Team Apps", children: [_jsx("span", { className: "caps-app-bar-section-label", children: "Team Apps" }), _jsx("div", { className: "caps-app-bar-app-list", children: launchers.map((item) => renderItem(item, true)) })] })) : null, sessions.length > 0 ? (_jsxs("div", { className: "caps-app-bar-section caps-app-bar-sessions", role: "group", "aria-label": "Recent Sessions", children: [_jsx("span", { className: "caps-app-bar-section-label", children: "Recent Sessions" }), _jsx("div", { className: "caps-app-bar-session-list", children: sessions.map((item) => renderItem(item, true, true)) })] })) : null, launchers.length === 0 &&
237
+ }, children: _jsx("span", { className: "caps-app-bar-mascot", "aria-hidden": "true", children: launcher.icon ?? _jsx(PanelsTopLeft, {}) }) }) }), portalContainer ? (_jsx(BasePopover.Portal, { container: portalContainer, keepMounted: true, children: _jsx(BasePopover.Positioner, { anchor: root, side: "bottom", align: "start", sideOffset: 4, collisionPadding: 8, collisionBoundary: portalContainer === document.body ? undefined : portalContainer, className: "caps-app-bar-positioner", children: _jsxs(BasePopover.Popup, { initialFocus: false, finalFocus: false, style: appBarStyle, ref: availableRef, id: availableId, className: "caps-app-bar-region caps-app-bar-launchers", role: "group", "aria-label": "Speed dial", "aria-hidden": !expanded, children: [launchers.length > 0 ? (_jsxs("div", { className: "caps-app-bar-section", role: "group", "aria-label": appsLabel, children: [_jsx("span", { className: "caps-app-bar-section-label", children: appsLabel }), _jsx("div", { className: "caps-app-bar-app-list", children: launchers.map((item) => renderItem(item, true)) })] })) : null, sessions.length > 0 ? (_jsxs("div", { className: "caps-app-bar-section caps-app-bar-sessions", role: "group", "aria-label": "Recent Sessions", children: [_jsx("span", { className: "caps-app-bar-section-label", children: "Recent Sessions" }), _jsx("div", { className: "caps-app-bar-session-list", children: sessions.map((item) => renderItem(item, true, true)) })] })) : null, launchers.length === 0 &&
238
238
  sessions.length === 0 &&
239
239
  emptyLabel ? (_jsx("span", { className: "caps-app-bar-empty", role: "status", children: emptyLabel })) : null] }) }) })) : null, primaryAction && portalContainer ? (_jsx(BasePopover.Root, { open: open, onOpenChange: changeOpen, modal: false, children: _jsx(BasePopover.Portal, { container: portalContainer, keepMounted: true, children: _jsx(BasePopover.Positioner, { anchor: launcherRef, side: "bottom", align: "end", sideOffset: 8, collisionPadding: 8, collisionBoundary: portalContainer === document.body
240
240
  ? undefined
@@ -22,10 +22,28 @@ export type EmptyStateSuggestion = Readonly<{
22
22
  label: string;
23
23
  /** Called when the chip is chosen. */
24
24
  onSelect: () => void;
25
+ /** Running what this chip starts; shows a loader in the chip. */
26
+ pending?: boolean;
27
+ disabled?: boolean;
28
+ }>;
29
+ /** The one main step a first-run state invites; at most one per state. */
30
+ export type EmptyStatePrimaryAction = Readonly<{
31
+ /** A verb plus an object, such as "New repository". */
32
+ label: string;
33
+ /** Called when the action is chosen. */
34
+ onSelect: () => void;
35
+ /** Running the action; shows a loader in the button and blocks a repeat. */
36
+ pending?: boolean;
37
+ disabled?: boolean;
38
+ /** Why the action is unavailable; shown under the button when disabled. */
39
+ disabledReason?: string;
25
40
  }>;
26
41
  /** Props accepted by EmptyState. */
27
42
  export type EmptyStateProps = Omit<HTMLAttributes<HTMLElement>, "title"> & {
43
+ /** Secondary actions only; the main step goes in `primaryAction`. */
28
44
  actions?: ReactNode;
45
+ /** The one main step, rendered as a primary Button before the chips. */
46
+ primaryAction?: EmptyStatePrimaryAction;
29
47
  description: ReactNode;
30
48
  /** Chips that offer a starting point, such as a prompt to try. */
31
49
  suggestions?: readonly EmptyStateSuggestion[];
@@ -34,15 +52,21 @@ export type EmptyStateProps = Omit<HTMLAttributes<HTMLElement>, "title"> & {
34
52
  title: ReactNode;
35
53
  /** Heading level for the title; use one below the heading it sits under. */
36
54
  headingLevel?: EmptyStateHeadingLevel;
37
- /** Static art. */
38
- visual?: ReactNode;
55
+ /**
56
+ * Static art. `"blobs"` is the living idle Loader; a `panel` with no visual
57
+ * shows it too.
58
+ */
59
+ visual?: Exclude<ReactNode, string> | "blobs";
39
60
  /** Render a visual from the current speaking and thinking state. */
40
61
  renderVisual?: (state: EmptyStateVisualState) => ReactNode;
41
62
  variant?: EmptyStateVariant;
42
63
  };
43
64
  /** Product-neutral absence presentation; hosts supply copy, art, and actions. */
44
65
  export declare const EmptyState: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLElement>, "title"> & {
66
+ /** Secondary actions only; the main step goes in `primaryAction`. */
45
67
  actions?: ReactNode;
68
+ /** The one main step, rendered as a primary Button before the chips. */
69
+ primaryAction?: EmptyStatePrimaryAction;
46
70
  description: ReactNode;
47
71
  /** Chips that offer a starting point, such as a prompt to try. */
48
72
  suggestions?: readonly EmptyStateSuggestion[];
@@ -51,8 +75,11 @@ export declare const EmptyState: import("react").ForwardRefExoticComponent<Omit<
51
75
  title: ReactNode;
52
76
  /** Heading level for the title; use one below the heading it sits under. */
53
77
  headingLevel?: EmptyStateHeadingLevel;
54
- /** Static art. */
55
- visual?: ReactNode;
78
+ /**
79
+ * Static art. `"blobs"` is the living idle Loader; a `panel` with no visual
80
+ * shows it too.
81
+ */
82
+ visual?: Exclude<ReactNode, string> | "blobs";
56
83
  /** Render a visual from the current speaking and thinking state. */
57
84
  renderVisual?: (state: EmptyStateVisualState) => ReactNode;
58
85
  variant?: EmptyStateVariant;
@@ -1,6 +1,7 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { forwardRef, useEffect, useId, useState, useRef, } from "react";
3
3
  import { Button } from "../components/button.js";
4
+ import { Loader } from "../components/loader.js";
4
5
  import { useMotion, motionTime } from "../lib/motion.js";
5
6
  import { cn } from "../lib/cn.js";
6
7
  const headingTags = { 2: "h2", 3: "h3", 4: "h4" };
@@ -22,14 +23,23 @@ function useSpeaking(enabled, timing) {
22
23
  }, [active, duration]);
23
24
  return speaking;
24
25
  }
26
+ const blobs = (_jsx(Loader, { decorative: true, pace: "idle", size: "md", tone: "primary", variant: "wander" }));
25
27
  /** Product-neutral absence presentation; hosts supply copy, art, and actions. */
26
- export const EmptyState = forwardRef(function EmptyState({ actions, className, description, headingLevel = 2, suggestions, thinking = false, title, visual, renderVisual, variant = "plain", ...props }, ref) {
28
+ export const EmptyState = forwardRef(function EmptyState({ actions, className, description, headingLevel = 2, primaryAction, suggestions, thinking = false, title, visual, renderVisual, variant = "plain", ...props }, ref) {
27
29
  const titleId = useId();
28
30
  const heading = useRef(null);
29
31
  const timing = useMotion(heading);
30
32
  const speaking = useSpeaking(variant === "speech", timing) && !thinking;
31
33
  const Heading = headingTags[headingLevel];
32
- const art = renderVisual ? renderVisual({ speaking, thinking }) : visual;
33
- const copy = (_jsxs(_Fragment, { children: [_jsx(Heading, { ref: heading, className: "empty-state-title", id: props["aria-labelledby"] ? undefined : titleId, children: title }), _jsx("p", { children: description }), suggestions && suggestions.length > 0 ? (_jsx("ul", { "aria-label": "Try one of these", className: "empty-state-suggestions", children: suggestions.map((suggestion) => (_jsx("li", { children: _jsx(Button, { size: "sm", variant: "outline", onClick: suggestion.onSelect, children: suggestion.label }) }, suggestion.label))) })) : null, actions ? _jsx("div", { className: "empty-state-actions", children: actions }) : null] }));
34
+ const chosen = visual ?? (variant === "panel" ? "blobs" : undefined);
35
+ const art = renderVisual
36
+ ? renderVisual({ speaking, thinking })
37
+ : chosen === "blobs"
38
+ ? blobs
39
+ : chosen;
40
+ const disabledReasonId = `${titleId}-reason`;
41
+ const copy = (_jsxs(_Fragment, { children: [_jsx(Heading, { ref: heading, className: "empty-state-title", id: props["aria-labelledby"] ? undefined : titleId, children: title }), _jsx("p", { children: description }), primaryAction ? (_jsxs("div", { className: "empty-state-primary", children: [_jsx(Button, { "aria-describedby": primaryAction.disabled && primaryAction.disabledReason
42
+ ? disabledReasonId
43
+ : undefined, disabled: primaryAction.disabled, loading: primaryAction.pending === true, onClick: primaryAction.onSelect, variant: "primary", children: primaryAction.label }), primaryAction.disabled && primaryAction.disabledReason ? (_jsx("span", { className: "empty-state-reason", id: disabledReasonId, children: primaryAction.disabledReason })) : null] })) : null, suggestions && suggestions.length > 0 ? (_jsx("ul", { "aria-label": "Try one of these", className: "empty-state-suggestions", children: suggestions.map((suggestion, index) => (_jsx("li", { children: _jsx(Button, { disabled: suggestion.disabled, loading: suggestion.pending === true, size: "sm", variant: "outline", onClick: suggestion.onSelect, children: suggestion.label }) }, `${String(index)}:${suggestion.label}`))) })) : null, actions ? _jsx("div", { className: "empty-state-actions", children: actions }) : null] }));
34
44
  return (_jsxs("section", { ...props, "aria-labelledby": props["aria-labelledby"] ?? titleId, className: cn("empty-state", variant !== "plain" && `empty-state-${variant}`, className), ref: ref, children: [art ? (_jsx("div", { "aria-hidden": "true", className: "empty-state-visual", children: art })) : null, variant === "speech" ? (_jsx("div", { className: "empty-state-bubble", children: copy })) : (copy)] }));
35
45
  });
@@ -1,5 +1,46 @@
1
1
  import { type HTMLAttributes, type ReactNode } from "react";
2
+ import { type LoaderVariant } from "../components/loader.js";
3
+ /** How long a wait runs before LoadingState admits it is slow. */
4
+ export declare const LOADING_SLOW_AFTER_MS = 8000;
5
+ /** Props accepted by LoadingState. */
6
+ export type LoadingStateProps = HTMLAttributes<HTMLDivElement> & {
7
+ message: ReactNode;
8
+ /** `false` drops the frame (border, plate, padding) for a window-opening state that fills its pane. */
9
+ framed?: boolean;
10
+ /** An App icon shown above the loader, for an App opening state. */
11
+ icon?: ReactNode;
12
+ /** A second, quieter line under the message. */
13
+ detail?: ReactNode;
14
+ /** Blob size; `md` is the default for a window or pane. */
15
+ size?: "sm" | "md" | "lg";
16
+ /** Which formation moves; pick one per moment (see the Caps README). */
17
+ variant?: LoaderVariant;
18
+ /**
19
+ * Milliseconds before the wait is called slow: "Still working…" is added
20
+ * and `onRetry`, if given, offers a Retry. Infinity never says so.
21
+ */
22
+ slowAfterMs?: number;
23
+ /** Called when Retry is chosen; Retry only appears once the wait is slow. */
24
+ onRetry?: () => void;
25
+ };
2
26
  /** A bounded, labelled loading region; the caller owns the message. */
3
27
  export declare const LoadingState: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & {
4
28
  message: ReactNode;
29
+ /** `false` drops the frame (border, plate, padding) for a window-opening state that fills its pane. */
30
+ framed?: boolean;
31
+ /** An App icon shown above the loader, for an App opening state. */
32
+ icon?: ReactNode;
33
+ /** A second, quieter line under the message. */
34
+ detail?: ReactNode;
35
+ /** Blob size; `md` is the default for a window or pane. */
36
+ size?: "sm" | "md" | "lg";
37
+ /** Which formation moves; pick one per moment (see the Caps README). */
38
+ variant?: LoaderVariant;
39
+ /**
40
+ * Milliseconds before the wait is called slow: "Still working…" is added
41
+ * and `onRetry`, if given, offers a Retry. Infinity never says so.
42
+ */
43
+ slowAfterMs?: number;
44
+ /** Called when Retry is chosen; Retry only appears once the wait is slow. */
45
+ onRetry?: () => void;
5
46
  } & import("react").RefAttributes<HTMLDivElement>>;
@@ -1,8 +1,23 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { forwardRef } from "react";
2
+ import { forwardRef, useEffect, useState, } from "react";
3
+ import { Button } from "../components/button.js";
3
4
  import { Loader } from "../components/loader.js";
4
5
  import { cn } from "../lib/cn.js";
6
+ /** How long a wait runs before LoadingState admits it is slow. */
7
+ export const LOADING_SLOW_AFTER_MS = 8000;
5
8
  /** A bounded, labelled loading region; the caller owns the message. */
6
- export const LoadingState = forwardRef(function LoadingState({ className, message, ...props }, ref) {
7
- return (_jsxs("div", { ...props, className: cn("caps-loading-state", className), ref: ref, role: "status", children: [_jsx(Loader, { decorative: true, size: "sm" }), _jsx("span", { children: message })] }));
9
+ export const LoadingState = forwardRef(function LoadingState({ className, detail, framed = true, icon, message, onRetry, size = "md", slowAfterMs = LOADING_SLOW_AFTER_MS, variant = "line", ...props }, ref) {
10
+ const [slow, setSlow] = useState(false);
11
+ useEffect(() => {
12
+ setSlow(false);
13
+ if (!Number.isFinite(slowAfterMs))
14
+ return undefined;
15
+ const timer = globalThis.setTimeout(() => {
16
+ setSlow(true);
17
+ }, slowAfterMs);
18
+ return () => {
19
+ globalThis.clearTimeout(timer);
20
+ };
21
+ }, [slowAfterMs, message]);
22
+ return (_jsxs("div", { ...props, className: cn("caps-loading-state", className), "data-framed": framed ? undefined : "false", "data-icon": icon ? "" : undefined, "data-size": size, "data-slow": slow || undefined, ref: ref, role: "status", children: [icon ? (_jsx("span", { "aria-hidden": "true", className: "caps-loading-state-icon", children: icon })) : null, _jsx(Loader, { decorative: true, size: size, variant: variant }), _jsxs("span", { className: "caps-loading-state-copy", children: [_jsx("span", { children: message }), detail ? (_jsx("span", { className: "caps-loading-state-detail", children: detail })) : null, slow ? (_jsx("span", { className: "caps-loading-state-detail", children: "Still working\u2026" })) : null] }), slow && onRetry ? (_jsx(Button, { className: "caps-loading-state-retry", size: "sm", variant: "outline", onClick: onRetry, children: "Retry" })) : null] }));
8
23
  });
@@ -10,5 +10,5 @@ function index(position) {
10
10
  /** Product-neutral, dense capability sheet: one row per capability. */
11
11
  export function PermissionGrid({ caption, summary, rows, headings = DEFAULT_HEADINGS, className, }) {
12
12
  const hasControls = rows.some((row) => row.control !== undefined);
13
- return (_jsxs("figure", { className: `caps-permission-grid ${className ?? ""}`, children: [_jsxs("figcaption", { className: "caps-permission-grid-caption", children: [_jsx("span", { className: "caps-permission-grid-title", children: caption }), summary === undefined ? null : (_jsx("span", { className: "caps-permission-grid-summary", children: summary }))] }), _jsxs("table", { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", className: "caps-permission-grid-index", children: "#" }), _jsx("th", { scope: "col", children: headings.capability }), _jsx("th", { scope: "col", children: headings.state }), _jsx("th", { scope: "col", children: headings.scope }), hasControls ? (_jsx("th", { scope: "col", children: headings.control ?? _jsx("span", { className: "sr-only", children: "Change" }) })) : null] }) }), _jsx("tbody", { children: rows.map((row, position) => (_jsxs("tr", { "data-state": row.state, children: [_jsxs("td", { className: "caps-permission-grid-index", children: [_jsx("span", { children: index(position) }), row.group === undefined ? null : (_jsx("span", { className: "caps-permission-grid-group", children: row.group }))] }), _jsx("th", { scope: "row", children: _jsxs("span", { className: "caps-permission-grid-capability", children: [_jsx("span", { className: "caps-permission-grid-mark", "aria-hidden": "true", children: row.mark }), _jsx("span", { children: row.label })] }) }), _jsx("td", { children: _jsx("span", { className: "caps-permission-grid-state", "data-state": row.state, children: row.stateLabel }) }), _jsx("td", { className: "caps-permission-grid-scope", children: row.scope }), hasControls ? _jsx("td", { children: row.control ?? null }) : null] }, row.id))) })] })] }));
13
+ return (_jsxs("figure", { className: `caps-permission-grid ${className ?? ""}`, children: [_jsxs("figcaption", { className: "caps-permission-grid-caption", children: [_jsx("span", { className: "caps-permission-grid-title", children: caption }), summary === undefined ? null : (_jsx("span", { className: "caps-permission-grid-summary", children: summary }))] }), _jsxs("table", { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", className: "caps-permission-grid-index", children: "#" }), _jsx("th", { scope: "col", children: headings.capability }), _jsx("th", { scope: "col", className: "caps-permission-grid-col-state", children: headings.state }), _jsx("th", { scope: "col", className: "caps-permission-grid-col-scope", children: headings.scope }), hasControls ? (_jsx("th", { scope: "col", className: "caps-permission-grid-col-control", children: headings.control ?? _jsx("span", { className: "sr-only", children: "Change" }) })) : null] }) }), _jsx("tbody", { children: rows.map((row, position) => (_jsxs("tr", { "data-state": row.state, children: [_jsxs("td", { className: "caps-permission-grid-index", children: [_jsx("span", { children: index(position) }), row.group === undefined ? null : (_jsx("span", { className: "caps-permission-grid-group", title: row.group, children: row.group }))] }), _jsx("th", { scope: "row", children: _jsxs("span", { className: "caps-permission-grid-capability", children: [_jsx("span", { className: "caps-permission-grid-mark", "aria-hidden": "true", children: row.mark }), _jsx("span", { children: row.label })] }) }), _jsx("td", { children: _jsx("span", { className: "caps-permission-grid-state", "data-state": row.state, children: row.stateLabel }) }), _jsx("td", { className: "caps-permission-grid-scope", children: row.scope }), hasControls ? _jsx("td", { children: row.control ?? null }) : null] }, row.id))) })] })] }));
14
14
  }
@@ -1,15 +1,33 @@
1
1
  import { type HTMLAttributes, type ReactNode } from "react";
2
+ /** The document heading level of a ResultState title. */
3
+ export type ResultStateHeadingLevel = 2 | 3 | 4;
4
+ /** What happened: `failed` is the failure card, `done` a finished result. */
5
+ export type ResultStateTone = "failed" | "done" | "neutral";
2
6
  /** Props accepted by ResultState. */
3
7
  export type ResultStateProps = Omit<HTMLAttributes<HTMLElement>, "title"> & {
8
+ /** One primary action, then secondary ones. */
4
9
  actions?: ReactNode;
5
10
  description: ReactNode;
11
+ headingLevel?: ResultStateHeadingLevel;
6
12
  icon?: ReactNode;
13
+ /** Up to three numbered next steps; extra steps are not shown. */
14
+ steps?: readonly ReactNode[];
7
15
  title: ReactNode;
16
+ tone?: ResultStateTone;
17
+ /** Larger decorative art above the title, such as an illustration. */
18
+ visual?: ReactNode;
8
19
  };
9
20
  /** A labelled result presentation; hosts own its meaning and next actions. */
10
21
  export declare const ResultState: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLElement>, "title"> & {
22
+ /** One primary action, then secondary ones. */
11
23
  actions?: ReactNode;
12
24
  description: ReactNode;
25
+ headingLevel?: ResultStateHeadingLevel;
13
26
  icon?: ReactNode;
27
+ /** Up to three numbered next steps; extra steps are not shown. */
28
+ steps?: readonly ReactNode[];
14
29
  title: ReactNode;
30
+ tone?: ResultStateTone;
31
+ /** Larger decorative art above the title, such as an illustration. */
32
+ visual?: ReactNode;
15
33
  } & import("react").RefAttributes<HTMLElement>>;
@@ -1,8 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef, useId } from "react";
3
+ import { Status } from "../components/status.js";
3
4
  import { cn } from "../lib/cn.js";
5
+ const headingTags = { 2: "h2", 3: "h3", 4: "h4" };
6
+ const MAX_STEPS = 3;
4
7
  /** A labelled result presentation; hosts own its meaning and next actions. */
5
- export const ResultState = forwardRef(function ResultState({ actions, className, description, icon, title, ...props }, ref) {
8
+ export const ResultState = forwardRef(function ResultState({ actions, className, description, headingLevel = 2, icon, steps, title, tone = "neutral", visual, ...props }, ref) {
6
9
  const titleId = useId();
7
- return (_jsxs("section", { ...props, "aria-labelledby": props["aria-labelledby"] ?? titleId, className: cn("result-state", className), ref: ref, children: [icon ? _jsx("div", { "aria-hidden": "true", children: icon }) : null, _jsx("h2", { id: props["aria-labelledby"] ? undefined : titleId, children: title }), _jsx("p", { children: description }), actions ? _jsx("div", { className: "result-state-actions", children: actions }) : null] }));
10
+ const Heading = headingTags[headingLevel];
11
+ const shown = steps?.slice(0, MAX_STEPS) ?? [];
12
+ return (_jsxs("section", { ...props, "aria-labelledby": props["aria-labelledby"] ?? titleId, className: cn("result-state", className), "data-tone": tone === "neutral" ? undefined : tone, ref: ref, children: [visual ? (_jsx("div", { "aria-hidden": "true", className: "result-state-visual", children: visual })) : null, icon ? _jsx("div", { "aria-hidden": "true", children: icon }) : null, _jsxs(Heading, { id: props["aria-labelledby"] ? undefined : titleId, children: [tone === "neutral" ? null : (_jsx(Status, { glyph: "dot", tone: tone === "failed" ? "error" : "success" })), _jsx("span", { children: title })] }), _jsx("p", { children: description }), shown.length > 0 ? (_jsx("ol", { className: "result-state-steps", children: shown.map((step, index) => (_jsx("li", { children: step }, index))) })) : null, actions ? _jsx("div", { className: "result-state-actions", children: actions }) : null] }));
8
13
  });
@@ -0,0 +1,32 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ /** A step the person takes to finish setup. */
3
+ export type SetupNoticeAction = Readonly<{
4
+ label: string;
5
+ /** Called when the step is chosen. */
6
+ onSelect: () => void;
7
+ }>;
8
+ /** The required fix; `pending` means it opened Settings and is waiting. */
9
+ export type SetupNoticeFix = SetupNoticeAction & Readonly<{
10
+ pending?: boolean;
11
+ }>;
12
+ /** Props accepted by SetupNotice. */
13
+ export type SetupNoticeProps = Omit<HTMLAttributes<HTMLElement>, "title" | "children"> & {
14
+ /** The title, such as "Connect a model to start chatting". */
15
+ need: ReactNode;
16
+ /** One sentence naming the real cause. */
17
+ cause: ReactNode;
18
+ /** The one primary fix; never a dead CTA. */
19
+ fix: SetupNoticeFix;
20
+ secondary?: SetupNoticeAction;
21
+ /** What still works meanwhile, such as "Uploads work now". */
22
+ available?: ReactNode;
23
+ /** Names what is awaited while pending, after "Waiting for". */
24
+ waitingFor?: string;
25
+ /** `panel` replaces an empty state; `banner` and `inline` sit above or in content. */
26
+ variant: "panel" | "inline" | "banner";
27
+ };
28
+ /**
29
+ * Missing setup, named with its fix. Render it only while the setup fact is
30
+ * missing; when the fact turns ready the host unmounts it (and may toast).
31
+ */
32
+ export declare function SetupNotice({ available, cause, className, fix, need, secondary, variant, waitingFor, ...props }: SetupNoticeProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Alert } from "../components/alert.js";
3
+ import { Button } from "../components/button.js";
4
+ import { Status } from "../components/status.js";
5
+ import { cn } from "../lib/cn.js";
6
+ import { EmptyState } from "./empty-state.js";
7
+ /**
8
+ * Missing setup, named with its fix. Render it only while the setup fact is
9
+ * missing; when the fact turns ready the host unmounts it (and may toast).
10
+ */
11
+ export function SetupNotice({ available, cause, className, fix, need, secondary, variant, waitingFor = "setup", ...props }) {
12
+ const pending = fix.pending === true;
13
+ const label = pending ? `Waiting for ${waitingFor}…` : fix.label;
14
+ const detail = available ? (_jsxs(_Fragment, { children: [cause, " ", _jsx("span", { className: "caps-setup-notice-available", children: available })] })) : (cause);
15
+ if (variant === "panel") {
16
+ return (_jsx(EmptyState, { ...props, actions: secondary ? (_jsx(Button, { size: "sm", variant: "ghost", onClick: secondary.onSelect, children: secondary.label })) : undefined, className: cn("caps-setup-notice", className), "data-variant": "panel", description: detail, primaryAction: {
17
+ label,
18
+ onSelect: fix.onSelect,
19
+ pending,
20
+ }, title: need, variant: "panel", visual: _jsx(Status, { className: "caps-setup-notice-dot", glyph: "dot", tone: "warning" }) }));
21
+ }
22
+ return (_jsx("div", { className: "caps-setup-notice-frame", children: _jsx(Alert, { ...props, action: _jsxs("span", { className: "caps-setup-notice-actions", children: [secondary ? (_jsx(Button, { size: "sm", variant: "ghost", onClick: secondary.onSelect, children: secondary.label })) : null, _jsx(Button, { loading: pending, size: "sm", variant: "outline", onClick: fix.onSelect, children: label })] }), className: cn("caps-setup-notice", className), "data-variant": variant, description: detail, role: "status", title: _jsxs("span", { className: "caps-setup-notice-title", children: [_jsx(Status, { glyph: "dot", tone: "warning" }), need] }), tone: "warning" }) }));
23
+ }
@@ -41,7 +41,7 @@ export const Button = forwardRef(function Button({ asChild = false, children, cl
41
41
  if (asChild) {
42
42
  return (_jsx(Slot, { ...props, "aria-busy": loading ? true : props["aria-busy"], "aria-disabled": disabled === true || loading ? true : undefined, className: classes, ref: node, children: children }));
43
43
  }
44
- return (_jsx("button", { ...props, "aria-busy": loading ? true : props["aria-busy"], className: classes, disabled: disabled === true || loading, ref: node, type: props.type ?? "button", children: result === undefined && resultLabel === undefined ? (_jsxs(_Fragment, { children: [loading ? (_jsx(Loader, { className: "size-3", decorative: true, size: "sm" })) : null, children] })) : (_jsx(ResultFaces, { label: resultLabel, loading: loading, onResultEnd: onResultEnd, result: result, settled: mountedResult, children: children })) }));
44
+ return (_jsx("button", { ...props, "aria-busy": loading ? true : props["aria-busy"], className: classes, disabled: disabled === true || loading, ref: node, type: props.type ?? "button", children: result === undefined && resultLabel === undefined ? (_jsxs(_Fragment, { children: [loading ? (_jsx(Loader, { decorative: true, size: "sm", tone: "current", variant: "wander" })) : null, children] })) : (_jsx(ResultFaces, { label: resultLabel, loading: loading, onResultEnd: onResultEnd, result: result, settled: mountedResult, children: children })) }));
45
45
  });
46
46
  /**
47
47
  * When a button's label changes (loading, a result, new text) its width eases
@@ -247,5 +247,5 @@ function ResultFaces({ children, label, loading, onResultEnd, result, settled, }
247
247
  animation.cancel();
248
248
  };
249
249
  }, [showing, reduced]);
250
- return (_jsxs("span", { ref: faces, className: "caps-button-faces", "data-showing": showing ? "result" : "label", children: [_jsxs("span", { className: "caps-button-face", "data-face": "label", "aria-hidden": showing ? true : undefined, children: [loading || showing ? (_jsx(Loader, { className: "size-3", decorative: true, size: "sm" })) : null, children] }), resultMounted ? (_jsxs("span", { className: "caps-button-face", "data-face": "result", "aria-hidden": showing ? undefined : true, children: [_jsx("svg", { className: "caps-button-check", viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "M4.5 12.5 9.5 17.5 19.5 6.5", pathLength: 1 }) }), label === undefined ? children : label] })) : null] }));
250
+ return (_jsxs("span", { ref: faces, className: "caps-button-faces", "data-showing": showing ? "result" : "label", children: [_jsxs("span", { className: "caps-button-face", "data-face": "label", "aria-hidden": showing ? true : undefined, children: [loading || showing ? (_jsx(Loader, { decorative: true, size: "sm", tone: "current", variant: "wander" })) : null, children] }), resultMounted ? (_jsxs("span", { className: "caps-button-face", "data-face": "result", "aria-hidden": showing ? undefined : true, children: [_jsx("svg", { className: "caps-button-check", viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "M4.5 12.5 9.5 17.5 19.5 6.5", pathLength: 1 }) }), label === undefined ? children : label] })) : null] }));
251
251
  }
@@ -1,3 +1,4 @@
1
+ import { type ButtonSize, type ButtonVariant } from "./button.js";
1
2
  /** A deliberate hold, or an exact phrase before a destructive action. */
2
3
  export type ConfirmActionProps = {
3
4
  /** The action's trigger label and dialog title. */
@@ -8,6 +9,10 @@ export type ConfirmActionProps = {
8
9
  onConfirm: () => Promise<void>;
9
10
  /** Caller-owned, safe-to-display failure copy; rejection details are never displayed. */
10
11
  error?: string;
12
+ /** The trigger key's look; destructive by default, quiet where a toolbar needs it. */
13
+ variant?: ButtonVariant;
14
+ /** The trigger key's size. */
15
+ size?: ButtonSize;
11
16
  } & ({
12
17
  mode: "hold";
13
18
  phrase?: never;
@@ -20,4 +25,4 @@ export type ConfirmActionProps = {
20
25
  * confirmations available for retry. Type mode closes on success and restores
21
26
  * focus through Dialog. A pending action cannot be dismissed or cancelled.
22
27
  */
23
- export declare function ConfirmAction({ label, confirmLabel, onConfirm, error, mode, phrase, }: ConfirmActionProps): import("react/jsx-runtime").JSX.Element;
28
+ export declare function ConfirmAction({ label, confirmLabel, onConfirm, error, variant, size, mode, phrase, }: ConfirmActionProps): import("react/jsx-runtime").JSX.Element;
@@ -10,7 +10,7 @@ import { Input } from "./input.js";
10
10
  * confirmations available for retry. Type mode closes on success and restores
11
11
  * focus through Dialog. A pending action cannot be dismissed or cancelled.
12
12
  */
13
- export function ConfirmAction({ label, confirmLabel, onConfirm, error, mode, phrase, }) {
13
+ export function ConfirmAction({ label, confirmLabel, onConfirm, error, variant = "destructive", size, mode, phrase, }) {
14
14
  const [open, setOpen] = useState(false);
15
15
  const [typed, setTyped] = useState("");
16
16
  const [state, setState] = useState("idle");
@@ -45,7 +45,7 @@ export function ConfirmAction({ label, confirmLabel, onConfirm, error, mode, phr
45
45
  }
46
46
  }
47
47
  if (mode === "hold") {
48
- return (_jsxs("div", { className: "grid gap-sm", children: [_jsx(HoldButton, { variant: "destructive", loading: busy, result: result, holdHint: confirmLabel, onConfirm: () => void confirm(), children: label }), _jsx(FieldError, { children: message })] }));
48
+ return (_jsxs("div", { className: "grid gap-sm", children: [_jsx(HoldButton, { variant: variant, ...(size === undefined ? {} : { size }), loading: busy, result: result, holdHint: confirmLabel, onConfirm: () => void confirm(), children: label }), _jsx(FieldError, { children: message })] }));
49
49
  }
50
50
  return (_jsxs(Dialog, { open: open, onOpenChange: (next, details) => {
51
51
  if (pending.current) {
@@ -55,7 +55,7 @@ export function ConfirmAction({ label, confirmLabel, onConfirm, error, mode, phr
55
55
  setOpen(next);
56
56
  setTyped("");
57
57
  setState("idle");
58
- }, children: [_jsx(DialogTrigger, { render: _jsx(Button, { variant: "destructive" }), children: label }), _jsx(DialogContent, { initialFocus: input, children: _jsxs(Form, { busy: busy, onSubmit: (event) => {
58
+ }, children: [_jsx(DialogTrigger, { render: _jsx(Button, { variant: variant, ...(size === undefined ? {} : { size }) }), children: label }), _jsx(DialogContent, { initialFocus: input, children: _jsxs(Form, { busy: busy, onSubmit: (event) => {
59
59
  event.preventDefault();
60
60
  void confirm();
61
61
  }, children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: label }), _jsx(DialogDescription, { children: "Type the phrase exactly to confirm." })] }), _jsx(Field, { label: `Type ${phrase} to confirm`, children: _jsx(Input, { ref: input, autoComplete: "off", autoCapitalize: "none", spellCheck: false, value: typed, onChange: (event) => {
@@ -43,7 +43,7 @@ export const DialogPopup = forwardRef(function DialogPopup({ className, finalFoc
43
43
  const dialog = useDialogContext();
44
44
  return (_jsx(BaseDialog.Popup, { ...props, "data-size": size, className: (state) => cn("caps-dialog-popup overflow-y-auto overscroll-contain rounded-box bg-base-100 p-lg text-base-content shadow-xl", size === "fill"
45
45
  ? "fixed inset-0 h-full w-full max-w-none"
46
- : "max-h-screen w-11/12 max-w-(--container-lg)", resolveClassName(className, state)), finalFocus: dialog.interactionOwner === "host" ? false : (finalFocus ?? true), ref: ref }));
46
+ : "caps-dialog-popup-default", resolveClassName(className, state)), finalFocus: dialog.interactionOwner === "host" ? false : (finalFocus ?? true), ref: ref }));
47
47
  });
48
48
  /**
49
49
  * The default portal, backdrop and popup composition for a Dialog.
@@ -0,0 +1,46 @@
1
+ import { type HTMLAttributes, type ReactNode } from "react";
2
+ /** Props accepted by FailureLine. */
3
+ export type FailureLineProps = Omit<HTMLAttributes<HTMLDivElement>, "children"> & {
4
+ /** One plain line naming the real cause; never a raw code. */
5
+ message: ReactNode;
6
+ /** Called when Retry is chosen; without it no Retry button shows. */
7
+ onRetry?: () => void;
8
+ /** Called when Dismiss is chosen; without it no Dismiss button shows. */
9
+ onDismiss?: () => void;
10
+ retryLabel?: string;
11
+ dismissLabel?: string;
12
+ /** A more specific accessible name for Retry, such as "Retry prompt". */
13
+ retryAriaLabel?: string;
14
+ /** A more specific accessible name for Dismiss. */
15
+ dismissAriaLabel?: string;
16
+ /** How many times it failed; above 1 the line adds a "Failed" line with the count once. */
17
+ count?: number;
18
+ /** `row` sits in a list or under a control; `tile` fills a media tile. */
19
+ density?: "row" | "tile";
20
+ /** `status` is polite; use `alert` only for a single, urgent failure. */
21
+ role?: "status" | "alert";
22
+ };
23
+ /**
24
+ * One item failure: an error dot, one line, and labelled Retry and Dismiss.
25
+ * It never uses the bang glyph; the dot is the whole signal.
26
+ */
27
+ export declare const FailureLine: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLDivElement>, "children"> & {
28
+ /** One plain line naming the real cause; never a raw code. */
29
+ message: ReactNode;
30
+ /** Called when Retry is chosen; without it no Retry button shows. */
31
+ onRetry?: () => void;
32
+ /** Called when Dismiss is chosen; without it no Dismiss button shows. */
33
+ onDismiss?: () => void;
34
+ retryLabel?: string;
35
+ dismissLabel?: string;
36
+ /** A more specific accessible name for Retry, such as "Retry prompt". */
37
+ retryAriaLabel?: string;
38
+ /** A more specific accessible name for Dismiss. */
39
+ dismissAriaLabel?: string;
40
+ /** How many times it failed; above 1 the line adds a "Failed" line with the count once. */
41
+ count?: number;
42
+ /** `row` sits in a list or under a control; `tile` fills a media tile. */
43
+ density?: "row" | "tile";
44
+ /** `status` is polite; use `alert` only for a single, urgent failure. */
45
+ role?: "status" | "alert";
46
+ } & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import { Button } from "./button.js";
4
+ import { Status } from "./status.js";
5
+ import { cn } from "../lib/cn.js";
6
+ /**
7
+ * One item failure: an error dot, one line, and labelled Retry and Dismiss.
8
+ * It never uses the bang glyph; the dot is the whole signal.
9
+ */
10
+ export const FailureLine = forwardRef(function FailureLine({ className, count, density = "row", dismissAriaLabel, dismissLabel = "Dismiss", message, onDismiss, onRetry, retryAriaLabel, retryLabel = "Retry", role = "status", ...props }, ref) {
11
+ return (_jsxs("div", { ...props, className: cn("caps-failure-line", className), "data-density": density, ref: ref, role: role, children: [_jsx(Status, { glyph: "dot", tone: "error" }), _jsxs("span", { className: "caps-failure-line-copy", children: [_jsx("span", { children: message }), count !== undefined && count > 1 ? (_jsxs("span", { className: "caps-failure-line-count", children: ["Failed ", count, " times"] })) : null] }), onRetry || onDismiss ? (_jsxs("span", { className: "caps-failure-line-actions", children: [onRetry ? (_jsx(Button, { "aria-label": retryAriaLabel, size: "sm", variant: "outline", onClick: onRetry, children: retryLabel })) : null, onDismiss ? (_jsx(Button, { "aria-label": dismissAriaLabel, size: "sm", variant: "ghost", onClick: onDismiss, children: dismissLabel })) : null] })) : null] }));
12
+ });
@@ -4,14 +4,22 @@ import { type ButtonHTMLAttributes, type HTMLAttributes, type ReactNode } from "
4
4
  * hovered or focused; the rest follow the theme.
5
5
  */
6
6
  export type KeycapTone = "neutral" | "primary" | "accent" | "inverse" | "destructive";
7
- /** Keycap sizes: a legend chip, a dock key and a big face key. */
8
- export type KeycapSize = "sm" | "md" | "lg";
7
+ /**
8
+ * Keycap sizes: `hint` is a muted inline shortcut caption (caption height, no
9
+ * plastic), then a legend chip, a dock key and a big face key.
10
+ */
11
+ export type KeycapSize = "hint" | "sm" | "md" | "lg";
9
12
  /** Props accepted by Keycap. */
10
13
  export type KeycapProps = HTMLAttributes<HTMLElement> & {
11
14
  tone?: KeycapTone;
12
15
  size?: KeycapSize;
13
16
  /** Show the key held down. Presentation only; the host owns input. */
14
17
  pressed?: boolean;
18
+ /**
19
+ * `hide` drops a keyboard legend on touch-first devices (`pointer: coarse`),
20
+ * where no one presses it.
21
+ */
22
+ touch?: "hide";
15
23
  };
16
24
  /**
17
25
  * One molded key legend, rendered as native `<kbd>`: the key a player presses.
@@ -22,6 +30,11 @@ export declare const Keycap: import("react").ForwardRefExoticComponent<HTMLAttri
22
30
  size?: KeycapSize;
23
31
  /** Show the key held down. Presentation only; the host owns input. */
24
32
  pressed?: boolean;
33
+ /**
34
+ * `hide` drops a keyboard legend on touch-first devices (`pointer: coarse`),
35
+ * where no one presses it.
36
+ */
37
+ touch?: "hide";
25
38
  } & import("react").RefAttributes<HTMLElement>>;
26
39
  /** Props accepted by KeycapButton. */
27
40
  export type KeycapButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> & {