@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.
- package/README.md +58 -0
- package/dist/blocks/app-bar.d.ts +4 -2
- package/dist/blocks/app-bar.js +3 -3
- package/dist/blocks/empty-state.d.ts +31 -4
- package/dist/blocks/empty-state.js +14 -4
- package/dist/blocks/loading-state.d.ts +41 -0
- package/dist/blocks/loading-state.js +18 -3
- package/dist/blocks/permission-grid.js +1 -1
- package/dist/blocks/result-state.d.ts +18 -0
- package/dist/blocks/result-state.js +7 -2
- package/dist/blocks/setup-notice.d.ts +32 -0
- package/dist/blocks/setup-notice.js +23 -0
- package/dist/components/button.js +2 -2
- package/dist/components/confirm-action.d.ts +6 -1
- package/dist/components/confirm-action.js +3 -3
- package/dist/components/dialog.js +1 -1
- package/dist/components/failure-line.d.ts +46 -0
- package/dist/components/failure-line.js +12 -0
- package/dist/components/keycap.d.ts +15 -2
- package/dist/components/keycap.js +3 -3
- package/dist/components/loader-blobs.d.ts +3 -2
- package/dist/components/loader-blobs.js +5 -3
- package/dist/components/loader.d.ts +8 -2
- package/dist/components/loader.js +4 -2
- package/dist/components/toaster.d.ts +5 -1
- package/dist/components/toaster.js +16 -2
- package/dist/styles.css +485 -28
- package/docs/api/README.md +2 -0
- package/docs/api/blocks/app-bar.md +9 -1
- package/docs/api/blocks/empty-state.md +30 -11
- package/docs/api/blocks/loading-state.md +33 -0
- package/docs/api/blocks/result-state.md +30 -6
- package/docs/api/blocks/setup-notice.md +82 -0
- package/docs/api/components/confirm-action.md +8 -6
- package/docs/api/components/data-list.md +33 -0
- package/docs/api/components/failure-line.md +50 -0
- package/docs/api/components/field.md +9 -8
- package/docs/api/components/icon-checkbox.md +55 -0
- package/docs/api/components/icon-radio.md +39 -5
- package/docs/api/components/keycap.md +10 -7
- package/docs/api/components/loader.md +18 -7
- package/docs/api/components/toaster.md +2 -0
- package/package.json +15 -6
- package/src/first-run.css +100 -0
- 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
|
package/dist/blocks/app-bar.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|
package/dist/blocks/app-bar.js
CHANGED
|
@@ -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":
|
|
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":
|
|
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
|
-
/**
|
|
38
|
-
|
|
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
|
-
/**
|
|
55
|
-
|
|
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,
|
|
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
|
|
33
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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, {
|
|
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:
|
|
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:
|
|
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
|
-
: "
|
|
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
|
-
/**
|
|
8
|
-
|
|
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"> & {
|