@fungi.computer/caps 0.8.0 → 0.10.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 CHANGED
@@ -832,6 +832,17 @@ your App owns the words.
832
832
  - `Keycap touch="hide"` drops a keyboard legend on touch-first devices;
833
833
  `KeycapButton` hides its `shortcut` legend that way.
834
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
+
835
846
  Which key and which status light:
836
847
 
837
848
  | Need | Use |
@@ -843,6 +854,14 @@ Which key and which status light:
843
854
 
844
855
  `bang` is for unread beads only. Never put it in a button or chip; use a dot.
845
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
+
846
865
  ### Hairlines
847
866
 
848
867
  `--stipe-hairline` (about 22%) outlines controls: Keycap, Button and chips keep
@@ -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
@@ -0,0 +1,34 @@
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
+ /** The panel's own glyph (an App icon, say) in place of the warning bead; ignored by `banner` and `inline`. */
24
+ visual?: Exclude<ReactNode, string>;
25
+ /** Names what is awaited while pending, after "Waiting for". */
26
+ waitingFor?: string;
27
+ /** `panel` replaces an empty state; `banner` and `inline` sit above or in content. */
28
+ variant: "panel" | "inline" | "banner";
29
+ };
30
+ /**
31
+ * Missing setup, named with its fix. Render it only while the setup fact is
32
+ * missing; when the fact turns ready the host unmounts it (and may toast).
33
+ */
34
+ export declare function SetupNotice({ available, cause, className, fix, need, secondary, variant, visual, 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, visual, 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: 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
+ }
@@ -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,8 +4,11 @@ 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;
@@ -1,4 +1,8 @@
1
1
  import { type ToasterProps } from "sonner";
2
2
  export { toast } from "sonner";
3
- /** Sonner owns gestures, stacking, focus and lifetime; Caps owns the surface. */
3
+ /**
4
+ * Sonner owns gestures, stacking, focus and lifetime; Caps owns the surface.
5
+ * Colors come from the Stipe tokens in the cascade, so a `data-theme` switch
6
+ * restyles open toasts; Sonner's own light and dark palettes are overridden.
7
+ */
4
8
  export declare function Toaster({ className, ...props }: ToasterProps): import("react").ReactPortal | null;
@@ -1,13 +1,27 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Toaster as Sonner } from "sonner";
3
+ import { X } from "lucide-react";
3
4
  import { cn } from "../lib/cn.js";
5
+ import { Loader } from "./loader.js";
6
+ import { Status } from "./status.js";
4
7
  import { createPortal } from "react-dom";
5
8
  import { useOverlayRoot } from "./overlay-root.js";
6
9
  export { toast } from "sonner";
7
- /** Sonner owns gestures, stacking, focus and lifetime; Caps owns the surface. */
10
+ /**
11
+ * Sonner owns gestures, stacking, focus and lifetime; Caps owns the surface.
12
+ * Colors come from the Stipe tokens in the cascade, so a `data-theme` switch
13
+ * restyles open toasts; Sonner's own light and dark palettes are overridden.
14
+ */
8
15
  export function Toaster({ className, ...props }) {
9
16
  const root = useOverlayRoot("toast");
10
17
  if (!root)
11
18
  return null;
12
- return createPortal(_jsx(Sonner, { position: "top-right", swipeDirections: ["right", "top"], offset: 16, gap: 8, className: cn("caps-toaster", className), ...props }), root);
19
+ return createPortal(_jsx(Sonner, { position: "top-right", swipeDirections: ["right", "top"], offset: 16, gap: 8, icons: {
20
+ close: _jsx(X, { "aria-hidden": "true", size: 12 }),
21
+ success: _jsx(Status, { glyph: "dot", tone: "success" }),
22
+ error: _jsx(Status, { glyph: "dot", tone: "error" }),
23
+ warning: _jsx(Status, { glyph: "dot", tone: "warning" }),
24
+ info: _jsx(Status, { glyph: "dot", tone: "info" }),
25
+ loading: (_jsx(Loader, { decorative: true, size: "sm", tone: "current", variant: "wander" })),
26
+ }, className: cn("caps-toaster", className), ...props }), root);
13
27
  }
package/dist/styles.css CHANGED
@@ -5795,6 +5795,10 @@
5795
5795
  width: calc(var(--spacing) * 12);
5796
5796
  height: calc(var(--spacing) * 12);
5797
5797
  }
5798
+ .size-48 {
5799
+ width: calc(var(--spacing) * 48);
5800
+ height: calc(var(--spacing) * 48);
5801
+ }
5798
5802
  .size-\[var\(--stipe-icon-size-lg\)\] {
5799
5803
  width: var(--stipe-icon-size-lg);
5800
5804
  height: var(--stipe-icon-size-lg);
@@ -8787,6 +8791,101 @@
8787
8791
  position: static;
8788
8792
  flex: none;
8789
8793
  }
8794
+ @layer utilities {
8795
+ .caps-setup-notice-frame {
8796
+ container-type: inline-size;
8797
+ }
8798
+ .caps-setup-notice-title {
8799
+ display: flex;
8800
+ align-items: center;
8801
+ gap: var(--spacing-sm);
8802
+ }
8803
+ .caps-setup-notice[data-variant] {
8804
+ border-color: var(--color-warning);
8805
+ @supports (color: color-mix(in lab, red, red)) {
8806
+ border-color: color-mix(in oklab, var(--color-warning) 45%, transparent);
8807
+ }
8808
+ background: var(--color-warning);
8809
+ @supports (color: color-mix(in lab, red, red)) {
8810
+ background: color-mix( in oklab, var(--color-warning) 12%, var(--stipe-surface-raised) );
8811
+ }
8812
+ color: var(--color-base-content);
8813
+ }
8814
+ .caps-setup-notice-actions {
8815
+ display: flex;
8816
+ flex-wrap: wrap;
8817
+ align-items: center;
8818
+ justify-content: flex-end;
8819
+ gap: var(--spacing-xs);
8820
+ }
8821
+ .caps-setup-notice-dot {
8822
+ margin-block: var(--spacing-md);
8823
+ scale: 2.5;
8824
+ }
8825
+ .caps-setup-notice-available {
8826
+ color: var(--stipe-fg-muted);
8827
+ }
8828
+ .caps-setup-notice[data-variant="inline"] {
8829
+ padding: var(--spacing-xs) var(--spacing-sm);
8830
+ font-size: var(--text-sm);
8831
+ }
8832
+ .caps-setup-notice[data-variant="banner"] {
8833
+ border-radius: 0;
8834
+ border-inline: 0;
8835
+ }
8836
+ @container (width <= 36rem) {
8837
+ .caps-setup-notice[data-variant] {
8838
+ flex-direction: column;
8839
+ align-items: stretch;
8840
+ }
8841
+ .caps-setup-notice[data-variant] > * {
8842
+ flex: none;
8843
+ width: 100%;
8844
+ }
8845
+ .caps-setup-notice-actions {
8846
+ justify-content: flex-start;
8847
+ }
8848
+ }
8849
+ .caps-failure-line {
8850
+ display: flex;
8851
+ align-items: flex-start;
8852
+ gap: var(--spacing-sm);
8853
+ min-width: 0;
8854
+ font-size: var(--text-sm);
8855
+ }
8856
+ .caps-failure-line-copy {
8857
+ display: grid;
8858
+ flex: 1;
8859
+ min-width: 0;
8860
+ gap: var(--spacing-2xs);
8861
+ }
8862
+ .caps-failure-line-count {
8863
+ color: var(--stipe-fg-muted);
8864
+ font-size: var(--text-xs);
8865
+ }
8866
+ .caps-failure-line > .caps-status {
8867
+ margin-block-start: var(--spacing-2xs);
8868
+ }
8869
+ .caps-failure-line-actions {
8870
+ display: flex;
8871
+ flex: none;
8872
+ gap: var(--spacing-2xs);
8873
+ }
8874
+ .caps-failure-line[data-density="tile"] {
8875
+ flex-direction: column;
8876
+ align-items: center;
8877
+ justify-content: center;
8878
+ height: 100%;
8879
+ padding: var(--spacing-md);
8880
+ text-align: center;
8881
+ }
8882
+ .caps-failure-line[data-density="tile"] > .caps-status {
8883
+ margin-block-start: 0;
8884
+ }
8885
+ .caps-failure-line[data-density="tile"] .caps-failure-line-copy {
8886
+ flex: none;
8887
+ }
8888
+ }
8790
8889
  :root, [data-theme] {
8791
8890
  --caps-primary-text: light-dark(
8792
8891
  oklch(from var(--color-primary) clamp(0.2, l, 0.4) c h),
@@ -8806,6 +8905,130 @@
8806
8905
  .caps-toaster[data-sonner-toaster] {
8807
8906
  z-index: var(--stipe-layer-toast);
8808
8907
  }
8908
+ .caps-toaster[data-sonner-toaster][data-sonner-theme] {
8909
+ font-family: inherit;
8910
+ --border-radius: var(--radius-box);
8911
+ --normal-bg: var(--stipe-surface-raised);
8912
+ --normal-bg-hover: var(--color-base-200);
8913
+ --normal-border: var(--stipe-hairline-quiet);
8914
+ --normal-border-hover: var(--stipe-hairline);
8915
+ --normal-text: var(--color-base-content);
8916
+ --success-bg: var(--color-success);
8917
+ @supports (color: color-mix(in lab, red, red)) {
8918
+ --success-bg: color-mix(
8919
+ in oklab,
8920
+ var(--color-success) 10%,
8921
+ var(--stipe-surface-raised)
8922
+ );
8923
+ }
8924
+ --success-border: var(--color-success);
8925
+ @supports (color: color-mix(in lab, red, red)) {
8926
+ --success-border: color-mix(in oklab, var(--color-success) 35%, transparent);
8927
+ }
8928
+ --success-text: var(--color-base-content);
8929
+ --info-bg: var(--color-info);
8930
+ @supports (color: color-mix(in lab, red, red)) {
8931
+ --info-bg: color-mix(
8932
+ in oklab,
8933
+ var(--color-info) 10%,
8934
+ var(--stipe-surface-raised)
8935
+ );
8936
+ }
8937
+ --info-border: var(--color-info);
8938
+ @supports (color: color-mix(in lab, red, red)) {
8939
+ --info-border: color-mix(in oklab, var(--color-info) 35%, transparent);
8940
+ }
8941
+ --info-text: var(--color-base-content);
8942
+ --warning-bg: var(--color-warning);
8943
+ @supports (color: color-mix(in lab, red, red)) {
8944
+ --warning-bg: color-mix(
8945
+ in oklab,
8946
+ var(--color-warning) 12%,
8947
+ var(--stipe-surface-raised)
8948
+ );
8949
+ }
8950
+ --warning-border: var(--color-warning);
8951
+ @supports (color: color-mix(in lab, red, red)) {
8952
+ --warning-border: color-mix(in oklab, var(--color-warning) 40%, transparent);
8953
+ }
8954
+ --warning-text: var(--color-base-content);
8955
+ --error-bg: var(--color-error);
8956
+ @supports (color: color-mix(in lab, red, red)) {
8957
+ --error-bg: color-mix(
8958
+ in oklab,
8959
+ var(--color-error) 10%,
8960
+ var(--stipe-surface-raised)
8961
+ );
8962
+ }
8963
+ --error-border: var(--color-error);
8964
+ @supports (color: color-mix(in lab, red, red)) {
8965
+ --error-border: color-mix(in oklab, var(--color-error) 40%, transparent);
8966
+ }
8967
+ --error-text: var(--color-base-content);
8968
+ }
8969
+ .caps-toaster[data-sonner-toaster] [data-sonner-toast][data-styled="true"] {
8970
+ font-size: var(--text-sm);
8971
+ line-height: 1.35;
8972
+ box-shadow: 0 0.25rem 0.75rem var(--color-base-content);
8973
+ @supports (color: color-mix(in lab, red, red)) {
8974
+ box-shadow: 0 0.25rem 0.75rem color-mix( in oklab, var(--color-base-content) calc(var(--depth) * 14%), transparent );
8975
+ }
8976
+ }
8977
+ .caps-toaster[data-sonner-toaster] [data-sonner-toast][data-styled] {
8978
+ align-items: flex-start;
8979
+ font-family: inherit;
8980
+ }
8981
+ .caps-toaster[data-sonner-toaster] [data-sonner-toast][data-styled] [data-icon] {
8982
+ align-items: center;
8983
+ height: calc(var(--text-sm) * 1.35);
8984
+ margin: 0;
8985
+ }
8986
+ .caps-toaster[data-sonner-toaster] [data-sonner-toast][data-styled] [data-description] {
8987
+ color: var(--color-base-content);
8988
+ @supports (color: color-mix(in lab, red, red)) {
8989
+ color: color-mix(in oklab, var(--color-base-content) 78%, transparent);
8990
+ }
8991
+ }
8992
+ .caps-toaster[data-sonner-toaster] [data-sonner-toast][data-styled] [data-close-button] {
8993
+ position: static;
8994
+ order: 99;
8995
+ align-self: flex-start;
8996
+ margin-inline-start: auto;
8997
+ transform: none;
8998
+ flex: none;
8999
+ }
9000
+ .caps-toaster[data-sonner-toaster] [data-sonner-toast][data-styled] [data-icon] {
9001
+ color: var(--color-base-content);
9002
+ }
9003
+ .caps-toaster[data-sonner-toaster] [data-sonner-toast][data-styled][data-type="success"] [data-icon] {
9004
+ color: var(--color-success);
9005
+ }
9006
+ .caps-toaster[data-sonner-toaster] [data-sonner-toast][data-styled][data-type="error"] [data-icon] {
9007
+ color: var(--color-error);
9008
+ }
9009
+ .caps-toaster[data-sonner-toaster] [data-sonner-toast][data-styled][data-type="warning"] [data-icon] {
9010
+ color: var(--color-warning);
9011
+ }
9012
+ .caps-toaster[data-sonner-toaster] [data-sonner-toast][data-styled][data-type="info"] [data-icon] {
9013
+ color: var(--color-info);
9014
+ }
9015
+ .caps-toaster[data-sonner-toaster] [data-sonner-toast][data-styled] [data-close-button] {
9016
+ width: 1.25rem;
9017
+ height: 1.25rem;
9018
+ color: var(--color-base-content);
9019
+ background: var(--stipe-surface-raised);
9020
+ border: var(--border) solid var(--stipe-hairline);
9021
+ transition-duration: var(--stipe-motion-fast);
9022
+ }
9023
+ .caps-toaster[data-sonner-toaster] [data-sonner-toast][data-styled] [data-close-button]:hover {
9024
+ background: var(--color-base-200);
9025
+ border-color: var(--stipe-hairline);
9026
+ }
9027
+ .caps-toaster[data-sonner-toaster] [data-sonner-toast][data-styled] [data-close-button]:focus-visible {
9028
+ outline: 2px solid var(--caps-focus-ring);
9029
+ outline-offset: 1px;
9030
+ box-shadow: none;
9031
+ }
8809
9032
  @layer caps-dropdown {
8810
9033
  .caps-dropdown-popup {
8811
9034
  max-height: var(--available-height);
@@ -10201,6 +10424,11 @@
10201
10424
  .caps-shortcuts dt, .caps-shortcuts dd {
10202
10425
  margin: 0;
10203
10426
  }
10427
+ .caps-dialog-popup-default {
10428
+ width: min( var(--container-lg), calc( 100% - max(1rem, env(safe-area-inset-left)) - max(1rem, env(safe-area-inset-right)) ) );
10429
+ max-width: none;
10430
+ max-height: calc( 100dvh - max(1rem, env(safe-area-inset-top)) - max(1rem, env(safe-area-inset-bottom)) );
10431
+ }
10204
10432
  dialog.caps-surface {
10205
10433
  color: inherit;
10206
10434
  }
@@ -16759,6 +16987,23 @@
16759
16987
  min-width: var(--caps-keycap-label-min-width, auto);
16760
16988
  line-height: var(--caps-keycap-label-line-height, inherit);
16761
16989
  }
16990
+ .caps-keycap[data-size="hint"] {
16991
+ display: inline-block;
16992
+ min-width: 0;
16993
+ min-height: 0;
16994
+ margin-inline: var(--spacing-2xs);
16995
+ padding: 0 var(--spacing-2xs);
16996
+ border-radius: var(--radius-selector);
16997
+ border-color: var(--stipe-hairline-quiet);
16998
+ background: transparent;
16999
+ box-shadow: none;
17000
+ color: var(--color-base-content);
17001
+ @supports (color: color-mix(in lab, red, red)) {
17002
+ color: color-mix(in oklab, var(--color-base-content) 65%, transparent);
17003
+ }
17004
+ font-size: var(--text-xs);
17005
+ line-height: 1.25;
17006
+ }
16762
17007
  .caps-keycap-button .caps-keycap {
16763
17008
  flex: var(--caps-keycap-shortcut-flex, 0 1 auto);
16764
17009
  min-width: var( --caps-keycap-shortcut-size, var(--caps-keycap-legend-min-size, 2rem) );
@@ -30,6 +30,7 @@
30
30
  | [blocks/result-state](blocks/result-state.md) | - |
31
31
  | [blocks/section-card](blocks/section-card.md) | - |
32
32
  | [blocks/section-heading](blocks/section-heading.md) | - |
33
+ | [blocks/setup-notice](blocks/setup-notice.md) | - |
33
34
  | [blocks/spec-sheet](blocks/spec-sheet.md) | - |
34
35
  | [blocks/status-tray](blocks/status-tray.md) | - |
35
36
  | [blocks/upload-queue](blocks/upload-queue.md) | - |
@@ -53,6 +54,7 @@
53
54
  | [components/diff-view](components/diff-view.md) | - |
54
55
  | [components/disclosure](components/disclosure.md) | - |
55
56
  | [components/dropdown-menu](components/dropdown-menu.md) | - |
57
+ | [components/failure-line](components/failure-line.md) | - |
56
58
  | [components/field-guide](components/field-guide.md) | - |
57
59
  | [components/field](components/field.md) | - |
58
60
  | [components/game-feedback](components/game-feedback.md) | - |
@@ -100,7 +100,15 @@ windows: readonly AppBarItem[];
100
100
  launchers: readonly AppBarItem[];
101
101
  ```
102
102
 
103
- Team Apps available to launch from the speed dial.
103
+ Apps available to launch from the speed dial.
104
+
105
+ ##### appsLabel?
106
+
107
+ ```ts
108
+ optional appsLabel?: string;
109
+ ```
110
+
111
+ The Apps section heading and its launcher name; the host owns the wording.
104
112
 
105
113
  ##### sessions?
106
114
 
@@ -0,0 +1,83 @@
1
+ <!-- Generated by Botanical API docs (TypeDoc + typedoc-plugin-markdown). Do not edit. -->
2
+
3
+ [**@fungi.computer/caps**](../README.md)
4
+
5
+ ---
6
+
7
+ [@fungi.computer/caps](../README.md) / blocks/setup-notice
8
+
9
+ # blocks/setup-notice
10
+
11
+ ## Type Aliases
12
+
13
+ ### SetupNoticeAction
14
+
15
+ ```ts
16
+ type SetupNoticeAction = Readonly<{
17
+ label: string;
18
+ onSelect: () => void;
19
+ }>;
20
+ ```
21
+
22
+ A step the person takes to finish setup.
23
+
24
+ ---
25
+
26
+ ### SetupNoticeFix
27
+
28
+ ```ts
29
+ type SetupNoticeFix = SetupNoticeAction &
30
+ Readonly<{
31
+ pending?: boolean;
32
+ }>;
33
+ ```
34
+
35
+ The required fix; `pending` means it opened Settings and is waiting.
36
+
37
+ ---
38
+
39
+ ### SetupNoticeProps
40
+
41
+ ```ts
42
+ type SetupNoticeProps = Omit<
43
+ HTMLAttributes<HTMLElement>,
44
+ "title" | "children"
45
+ > &
46
+ object;
47
+ ```
48
+
49
+ Props accepted by SetupNotice.
50
+
51
+ #### Type Declaration
52
+
53
+ | Name | Type | Description |
54
+ | ------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------- |
55
+ | `need` | `ReactNode` | The title, such as "Connect a model to start chatting". |
56
+ | `cause` | `ReactNode` | One sentence naming the real cause. |
57
+ | `fix` | [`SetupNoticeFix`](#setupnoticefix) | The one primary fix; never a dead CTA. |
58
+ | `secondary?` | [`SetupNoticeAction`](#setupnoticeaction) | - |
59
+ | `available?` | `ReactNode` | What still works meanwhile, such as "Uploads work now". |
60
+ | `visual?` | `Exclude`\<`ReactNode`, `string`\> | The panel's own glyph (an App icon, say) in place of the warning bead; ignored by `banner` and `inline`. |
61
+ | `waitingFor?` | `string` | Names what is awaited while pending, after "Waiting for". |
62
+ | `variant` | `"panel"` \| `"inline"` \| `"banner"` | `panel` replaces an empty state; `banner` and `inline` sit above or in content. |
63
+
64
+ ## Functions
65
+
66
+ ### SetupNotice()
67
+
68
+ ```ts
69
+ function SetupNotice(__namedParameters): Element;
70
+ ```
71
+
72
+ Missing setup, named with its fix. Render it only while the setup fact is
73
+ missing; when the fact turns ready the host unmounts it (and may toast).
74
+
75
+ #### Parameters
76
+
77
+ | Parameter | Type |
78
+ | ------------------- | --------------------------------------- |
79
+ | `__namedParameters` | [`SetupNoticeProps`](#setupnoticeprops) |
80
+
81
+ #### Returns
82
+
83
+ `Element`
@@ -28,12 +28,14 @@ A deliberate hold, or an exact phrase before a destructive action.
28
28
 
29
29
  #### Type Declaration
30
30
 
31
- | Name | Type | Description |
32
- | -------------- | ------------------------- | ---------------------------------------------------------------------------------- |
33
- | `label` | `string` | The action's trigger label and dialog title. |
34
- | `confirmLabel` | `string` | The destructive key's label (and the hold gesture's instruction). |
35
- | `onConfirm()` | () => `Promise`\<`void`\> | Resolve on success; reject on failure. The component prevents concurrent calls. |
36
- | `error?` | `string` | Caller-owned, safe-to-display failure copy; rejection details are never displayed. |
31
+ | Name | Type | Description |
32
+ | -------------- | ------------------------------------------ | ---------------------------------------------------------------------------------- |
33
+ | `label` | `string` | The action's trigger label and dialog title. |
34
+ | `confirmLabel` | `string` | The destructive key's label (and the hold gesture's instruction). |
35
+ | `onConfirm()` | () => `Promise`\<`void`\> | Resolve on success; reject on failure. The component prevents concurrent calls. |
36
+ | `error?` | `string` | Caller-owned, safe-to-display failure copy; rejection details are never displayed. |
37
+ | `variant?` | [`ButtonVariant`](button.md#buttonvariant) | The trigger key's look; destructive by default, quiet where a toolbar needs it. |
38
+ | `size?` | [`ButtonSize`](button.md#buttonsize) | The trigger key's size. |
37
39
 
38
40
  ## Functions
39
41
 
@@ -0,0 +1,50 @@
1
+ <!-- Generated by Botanical API docs (TypeDoc + typedoc-plugin-markdown). Do not edit. -->
2
+
3
+ [**@fungi.computer/caps**](../README.md)
4
+
5
+ ---
6
+
7
+ [@fungi.computer/caps](../README.md) / components/failure-line
8
+
9
+ # components/failure-line
10
+
11
+ ## Type Aliases
12
+
13
+ ### FailureLineProps
14
+
15
+ ```ts
16
+ type FailureLineProps = Omit<HTMLAttributes<HTMLDivElement>, "children"> &
17
+ object;
18
+ ```
19
+
20
+ Props accepted by FailureLine.
21
+
22
+ #### Type Declaration
23
+
24
+ | Name | Type | Description |
25
+ | ------------------- | ----------------------- | ------------------------------------------------------------------------------------ |
26
+ | `message` | `ReactNode` | One plain line naming the real cause; never a raw code. |
27
+ | `onRetry()?` | () => `void` | Called when Retry is chosen; without it no Retry button shows. |
28
+ | `onDismiss()?` | () => `void` | Called when Dismiss is chosen; without it no Dismiss button shows. |
29
+ | `retryLabel?` | `string` | - |
30
+ | `dismissLabel?` | `string` | - |
31
+ | `retryAriaLabel?` | `string` | A more specific accessible name for Retry, such as "Retry prompt". |
32
+ | `dismissAriaLabel?` | `string` | A more specific accessible name for Dismiss. |
33
+ | `count?` | `number` | How many times it failed; above 1 the line adds a "Failed" line with the count once. |
34
+ | `density?` | `"row"` \| `"tile"` | `row` sits in a list or under a control; `tile` fills a media tile. |
35
+ | `role?` | `"status"` \| `"alert"` | `status` is polite; use `alert` only for a single, urgent failure. |
36
+
37
+ ## Variables
38
+
39
+ ### FailureLine
40
+
41
+ ```ts
42
+ const FailureLine: ForwardRefExoticComponent<
43
+ Omit<HTMLAttributes<HTMLDivElement>, "children"> &
44
+ object &
45
+ RefAttributes<HTMLDivElement>
46
+ >;
47
+ ```
48
+
49
+ One item failure: an error dot, one line, and labelled Retry and Dismiss. It
50
+ never uses the bang glyph; the dot is the whole signal.
@@ -24,10 +24,11 @@ until hovered or focused; the rest follow the theme.
24
24
  ### KeycapSize
25
25
 
26
26
  ```ts
27
- type KeycapSize = "sm" | "md" | "lg";
27
+ type KeycapSize = "hint" | "sm" | "md" | "lg";
28
28
  ```
29
29
 
30
- Keycap sizes: a legend chip, a dock key and a big face key.
30
+ Keycap sizes: `hint` is a muted inline shortcut caption (caption height, no
31
+ plastic), then a legend chip, a dock key and a big face key.
31
32
 
32
33
  ---
33
34
 
@@ -17,6 +17,8 @@ function Toaster(__namedParameters): ReactPortal | null;
17
17
  ```
18
18
 
19
19
  Sonner owns gestures, stacking, focus and lifetime; Caps owns the surface.
20
+ Colors come from the Stipe tokens in the cascade, so a `data-theme` switch
21
+ restyles open toasts; Sonner's own light and dark palettes are overridden.
20
22
 
21
23
  #### Parameters
22
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fungi.computer/caps",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "author": "Levi <levi@technomancy.dev>",
5
5
  "description": "Product-neutral React primitives and blocks for Fungi interfaces",
6
6
  "type": "module",
@@ -242,6 +242,10 @@
242
242
  "types": "./dist/components/alert.d.ts",
243
243
  "import": "./dist/components/alert.js"
244
244
  },
245
+ "./components/failure-line": {
246
+ "types": "./dist/components/failure-line.d.ts",
247
+ "import": "./dist/components/failure-line.js"
248
+ },
245
249
  "./components/status": {
246
250
  "types": "./dist/components/status.d.ts",
247
251
  "import": "./dist/components/status.js"
@@ -338,6 +342,10 @@
338
342
  "types": "./dist/blocks/async-state.d.ts",
339
343
  "import": "./dist/blocks/async-state.js"
340
344
  },
345
+ "./blocks/setup-notice": {
346
+ "types": "./dist/blocks/setup-notice.d.ts",
347
+ "import": "./dist/blocks/setup-notice.js"
348
+ },
341
349
  "./blocks/loading-state": {
342
350
  "types": "./dist/blocks/loading-state.d.ts",
343
351
  "import": "./dist/blocks/loading-state.js"
@@ -465,7 +473,8 @@
465
473
  "src/fonts-display.css",
466
474
  "src/fonts-mono.css",
467
475
  "src/status-tray.css",
468
- "src/app-status-bar.css"
476
+ "src/app-status-bar.css",
477
+ "src/first-run.css"
469
478
  ],
470
479
  "peerDependencies": {
471
480
  "react": "^18.2.0 || ^19.0.0",
@@ -475,8 +484,8 @@
475
484
  },
476
485
  "devDependencies": {
477
486
  "@fungi.computer/maosaico": "0.1.1",
478
- "@fungi.computer/sprite": "0.2.1",
479
- "@fungi.computer/sprite-zdog": "0.3.1",
487
+ "@fungi.computer/sprite": "0.2.2",
488
+ "@fungi.computer/sprite-zdog": "0.3.2",
480
489
  "@ladle/react": "5.1.1",
481
490
  "@tailwindcss/cli": "4.2.1",
482
491
  "@types/react": "18.3.3",
@@ -0,0 +1,100 @@
1
+ /*
2
+ * First-run kit: SetupNotice (alert variants) and FailureLine. The panel
3
+ * variant is an ordinary EmptyState panel and needs no rules here.
4
+ */
5
+ @layer utilities {
6
+ .caps-setup-notice-frame {
7
+ container-type: inline-size;
8
+ }
9
+ .caps-setup-notice-title {
10
+ display: flex;
11
+ align-items: center;
12
+ gap: var(--spacing-sm);
13
+ }
14
+ .caps-setup-notice[data-variant] {
15
+ border-color: color-mix(in oklab, var(--color-warning) 45%, transparent);
16
+ background: color-mix(
17
+ in oklab,
18
+ var(--color-warning) 12%,
19
+ var(--stipe-surface-raised)
20
+ );
21
+ color: var(--color-base-content);
22
+ }
23
+ .caps-setup-notice-actions {
24
+ display: flex;
25
+ flex-wrap: wrap;
26
+ align-items: center;
27
+ justify-content: flex-end;
28
+ gap: var(--spacing-xs);
29
+ }
30
+ /* The panel's art: a warning bead at a size that holds the title. */
31
+ .caps-setup-notice-dot {
32
+ margin-block: var(--spacing-md);
33
+ scale: 2.5;
34
+ }
35
+ .caps-setup-notice-available {
36
+ color: var(--stipe-fg-muted);
37
+ }
38
+ .caps-setup-notice[data-variant="inline"] {
39
+ padding: var(--spacing-xs) var(--spacing-sm);
40
+ font-size: var(--text-sm);
41
+ }
42
+ .caps-setup-notice[data-variant="banner"] {
43
+ border-radius: 0;
44
+ border-inline: 0;
45
+ }
46
+ /* Narrow frame (a phone, or a narrow window): actions go under the copy. */
47
+ @container (width <= 36rem) {
48
+ .caps-setup-notice[data-variant] {
49
+ flex-direction: column;
50
+ align-items: stretch;
51
+ }
52
+ .caps-setup-notice[data-variant] > * {
53
+ flex: none;
54
+ width: 100%;
55
+ }
56
+ .caps-setup-notice-actions {
57
+ justify-content: flex-start;
58
+ }
59
+ }
60
+
61
+ .caps-failure-line {
62
+ display: flex;
63
+ align-items: flex-start;
64
+ gap: var(--spacing-sm);
65
+ min-width: 0;
66
+ font-size: var(--text-sm);
67
+ }
68
+ .caps-failure-line-copy {
69
+ display: grid;
70
+ flex: 1;
71
+ min-width: 0;
72
+ gap: var(--spacing-2xs);
73
+ }
74
+ .caps-failure-line-count {
75
+ color: var(--stipe-fg-muted);
76
+ font-size: var(--text-xs);
77
+ }
78
+ .caps-failure-line > .caps-status {
79
+ margin-block-start: var(--spacing-2xs);
80
+ }
81
+ .caps-failure-line-actions {
82
+ display: flex;
83
+ flex: none;
84
+ gap: var(--spacing-2xs);
85
+ }
86
+ .caps-failure-line[data-density="tile"] {
87
+ flex-direction: column;
88
+ align-items: center;
89
+ justify-content: center;
90
+ height: 100%;
91
+ padding: var(--spacing-md);
92
+ text-align: center;
93
+ }
94
+ .caps-failure-line[data-density="tile"] > .caps-status {
95
+ margin-block-start: 0;
96
+ }
97
+ .caps-failure-line[data-density="tile"] .caps-failure-line-copy {
98
+ flex: none;
99
+ }
100
+ }
package/src/styles.css CHANGED
@@ -6,6 +6,7 @@
6
6
  @import "@fungi.computer/stipe/styles.css";
7
7
  @import "./status-tray.css";
8
8
  @import "./app-status-bar.css";
9
+ @import "./first-run.css";
9
10
 
10
11
  @plugin "@tailwindcss/typography";
11
12
 
@@ -42,6 +43,134 @@
42
43
  z-index: var(--stipe-layer-toast);
43
44
  }
44
45
 
46
+ /* Unlayered on purpose: Sonner injects an unlayered sheet at runtime, which
47
+ would outrank anything inside a cascade layer. */
48
+ /* Sonner paints from --normal-* and per-type variables; point them at the
49
+ Stipe tokens so latte, mocha and any data-theme switch are followed. */
50
+ .caps-toaster[data-sonner-toaster][data-sonner-theme] {
51
+ font-family: inherit;
52
+ --border-radius: var(--radius-box);
53
+ --normal-bg: var(--stipe-surface-raised);
54
+ --normal-bg-hover: var(--color-base-200);
55
+ --normal-border: var(--stipe-hairline-quiet);
56
+ --normal-border-hover: var(--stipe-hairline);
57
+ --normal-text: var(--color-base-content);
58
+ --success-bg: color-mix(
59
+ in oklab,
60
+ var(--color-success) 10%,
61
+ var(--stipe-surface-raised)
62
+ );
63
+ --success-border: color-mix(in oklab, var(--color-success) 35%, transparent);
64
+ --success-text: var(--color-base-content);
65
+ --info-bg: color-mix(
66
+ in oklab,
67
+ var(--color-info) 10%,
68
+ var(--stipe-surface-raised)
69
+ );
70
+ --info-border: color-mix(in oklab, var(--color-info) 35%, transparent);
71
+ --info-text: var(--color-base-content);
72
+ --warning-bg: color-mix(
73
+ in oklab,
74
+ var(--color-warning) 12%,
75
+ var(--stipe-surface-raised)
76
+ );
77
+ --warning-border: color-mix(in oklab, var(--color-warning) 40%, transparent);
78
+ --warning-text: var(--color-base-content);
79
+ --error-bg: color-mix(
80
+ in oklab,
81
+ var(--color-error) 10%,
82
+ var(--stipe-surface-raised)
83
+ );
84
+ --error-border: color-mix(in oklab, var(--color-error) 40%, transparent);
85
+ --error-text: var(--color-base-content);
86
+ }
87
+ .caps-toaster[data-sonner-toaster] [data-sonner-toast][data-styled="true"] {
88
+ font-size: var(--text-sm);
89
+ line-height: 1.35;
90
+ box-shadow: 0 0.25rem 0.75rem
91
+ color-mix(
92
+ in oklab,
93
+ var(--color-base-content) calc(var(--depth) * 14%),
94
+ transparent
95
+ );
96
+ }
97
+ .caps-toaster[data-sonner-toaster] [data-sonner-toast][data-styled] {
98
+ align-items: flex-start;
99
+ font-family: inherit;
100
+ }
101
+ /* The icon sits on the title line, not centred on a two-line block. */
102
+ .caps-toaster[data-sonner-toaster]
103
+ [data-sonner-toast][data-styled]
104
+ [data-icon] {
105
+ align-items: center;
106
+ height: calc(var(--text-sm) * 1.35);
107
+ margin: 0;
108
+ }
109
+ .caps-toaster[data-sonner-toaster]
110
+ [data-sonner-toast][data-styled]
111
+ [data-description] {
112
+ color: color-mix(in oklab, var(--color-base-content) 78%, transparent);
113
+ }
114
+ /* One close position: inline at the end, top-aligned with the first line. */
115
+ .caps-toaster[data-sonner-toaster]
116
+ [data-sonner-toast][data-styled]
117
+ [data-close-button] {
118
+ position: static;
119
+ order: 99;
120
+ align-self: flex-start;
121
+ margin-inline-start: auto;
122
+ transform: none;
123
+ flex: none;
124
+ }
125
+ .caps-toaster[data-sonner-toaster]
126
+ [data-sonner-toast][data-styled]
127
+ [data-icon] {
128
+ color: var(--color-base-content);
129
+ }
130
+ .caps-toaster[data-sonner-toaster]
131
+ [data-sonner-toast][data-styled][data-type="success"]
132
+ [data-icon] {
133
+ color: var(--color-success);
134
+ }
135
+ .caps-toaster[data-sonner-toaster]
136
+ [data-sonner-toast][data-styled][data-type="error"]
137
+ [data-icon] {
138
+ color: var(--color-error);
139
+ }
140
+ .caps-toaster[data-sonner-toaster]
141
+ [data-sonner-toast][data-styled][data-type="warning"]
142
+ [data-icon] {
143
+ color: var(--color-warning);
144
+ }
145
+ .caps-toaster[data-sonner-toaster]
146
+ [data-sonner-toast][data-styled][data-type="info"]
147
+ [data-icon] {
148
+ color: var(--color-info);
149
+ }
150
+ .caps-toaster[data-sonner-toaster]
151
+ [data-sonner-toast][data-styled]
152
+ [data-close-button] {
153
+ width: 1.25rem;
154
+ height: 1.25rem;
155
+ color: var(--color-base-content);
156
+ background: var(--stipe-surface-raised);
157
+ border: var(--border) solid var(--stipe-hairline);
158
+ transition-duration: var(--stipe-motion-fast);
159
+ }
160
+ .caps-toaster[data-sonner-toaster]
161
+ [data-sonner-toast][data-styled]
162
+ [data-close-button]:hover {
163
+ background: var(--color-base-200);
164
+ border-color: var(--stipe-hairline);
165
+ }
166
+ .caps-toaster[data-sonner-toaster]
167
+ [data-sonner-toast][data-styled]
168
+ [data-close-button]:focus-visible {
169
+ outline: 2px solid var(--caps-focus-ring);
170
+ outline-offset: 1px;
171
+ box-shadow: none;
172
+ }
173
+
45
174
  @layer caps-dropdown {
46
175
  .caps-dropdown-popup {
47
176
  max-height: var(--available-height);
@@ -1568,6 +1697,22 @@
1568
1697
  .caps-shortcuts dd {
1569
1698
  margin: 0;
1570
1699
  }
1700
+ /* A default dialog keeps a 16px gutter on each side (more where the device
1701
+ has a safe-area inset) and never outgrows the screen vertically. */
1702
+ .caps-dialog-popup-default {
1703
+ width: min(
1704
+ var(--container-lg),
1705
+ calc(
1706
+ 100% - max(1rem, env(safe-area-inset-left)) -
1707
+ max(1rem, env(safe-area-inset-right))
1708
+ )
1709
+ );
1710
+ max-width: none;
1711
+ max-height: calc(
1712
+ 100dvh - max(1rem, env(safe-area-inset-top)) -
1713
+ max(1rem, env(safe-area-inset-bottom))
1714
+ );
1715
+ }
1571
1716
  dialog.caps-surface {
1572
1717
  color: inherit;
1573
1718
  }
@@ -9052,6 +9197,21 @@
9052
9197
  min-width: var(--caps-keycap-label-min-width, auto);
9053
9198
  line-height: var(--caps-keycap-label-line-height, inherit);
9054
9199
  }
9200
+ /* An inline shortcut caption: reads as text, never as another button. */
9201
+ .caps-keycap[data-size="hint"] {
9202
+ display: inline-block;
9203
+ min-width: 0;
9204
+ min-height: 0;
9205
+ margin-inline: var(--spacing-2xs);
9206
+ padding: 0 var(--spacing-2xs);
9207
+ border-radius: var(--radius-selector);
9208
+ border-color: var(--stipe-hairline-quiet);
9209
+ background: transparent;
9210
+ box-shadow: none;
9211
+ color: color-mix(in oklab, var(--color-base-content) 65%, transparent);
9212
+ font-size: var(--text-xs);
9213
+ line-height: 1.25;
9214
+ }
9055
9215
  .caps-keycap-button .caps-keycap {
9056
9216
  flex: var(--caps-keycap-shortcut-flex, 0 1 auto);
9057
9217
  min-width: var(