@fungi.computer/caps 0.0.2 → 0.2.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
@@ -222,9 +222,9 @@ keep their shared seam.
222
222
  ### Class merging
223
223
 
224
224
  `@fungi.computer/caps/lib/cn` merges classes with `cn@0.2.6` and Stipe's
225
- [named spacing scale](https://stipe.shiit.app/concepts/#named-spacing). It
226
- accepts conditional strings, arrays and objects and keeps caller-last Tailwind
227
- overrides, so `cn("text-xs px-xs py-sm", "px-md")` gives
225
+ [named spacing scale](https://docs.fungi.computer/stipe/concepts/#named-spacing).
226
+ It accepts conditional strings, arrays and objects and keeps caller-last
227
+ Tailwind overrides, so `cn("text-xs px-xs py-sm", "px-md")` gives
228
228
  `"text-xs py-sm px-md"`. The helper keeps typography and spacing names apart.
229
229
 
230
230
  ### Forms
@@ -550,9 +550,9 @@ marginalia, growth and type tokens, so they look right in `field-guide`,
550
550
  `field-guide-night` and the other editorial themes.
551
551
 
552
552
  - `@fungi.computer/caps/components/field-guide`: `Keycap` (a native keyboard
553
- key), `StatusLed` (a static labelled light), `CartridgeLabel` (a ruled
554
- cartridge face) and `SpecimenLabel` (plate annotation, catalogue metadata,
555
- title and summary).
553
+ key), `CartridgeLabel` (a ruled cartridge face) and `SpecimenLabel` (plate
554
+ annotation, catalogue metadata, title and summary). Status lights are the
555
+ gooey `Status` bead from `@fungi.computer/caps/components/status`.
556
556
  - `@fungi.computer/caps/components/technonomicon`: `CigaretteCard`, `Masthead`,
557
557
  `SectionBar`, `Folio`, `IssueMasthead`, `Spread` and related editorial pieces.
558
558
 
@@ -1006,7 +1006,8 @@ its own control finish.
1006
1006
  ### Editorial pieces
1007
1007
 
1008
1008
  ```tsx
1009
- import { Keycap, StatusLed } from "@fungi.computer/caps/components/field-guide";
1009
+ import { Keycap } from "@fungi.computer/caps/components/field-guide";
1010
+ import { Status } from "@fungi.computer/caps/components/status";
1010
1011
  import {
1011
1012
  Folio,
1012
1013
  Masthead,
@@ -1030,7 +1031,7 @@ export function FrontPage() {
1030
1031
  />
1031
1032
  <p>
1032
1033
  Press <Keycap>J</Keycap> for the next story.{" "}
1033
- <StatusLed active label="Online" />
1034
+ <Status tone="success" label="Online" />
1034
1035
  </p>
1035
1036
  <Folio items={["The Spore Print", "Page A1"]} />
1036
1037
  </>
@@ -37,8 +37,7 @@ export const Button = forwardRef(function Button({ asChild = false, children, cl
37
37
  const mountedResult = useRef(result).current;
38
38
  const node = useRef(null);
39
39
  useImperativeHandle(ref, () => node.current);
40
- // Text labels compare by value; element labels re-measure each render.
41
- useWidthTween(node, [loading, result, children]);
40
+ useWidthTween(node, loading, result);
42
41
  if (asChild) {
43
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 }));
44
43
  }
@@ -47,23 +46,45 @@ export const Button = forwardRef(function Button({ asChild = false, children, cl
47
46
  /**
48
47
  * When a button's label changes (loading, a result, new text) its width eases
49
48
  * to the new size instead of jumping, so neighbours never lurch.
49
+ *
50
+ * What counts as "changed" is what the button renders (its text, loading and
51
+ * result), never the identity of its children: a caller that builds a fresh
52
+ * array or element with the same words each render has not changed the label.
53
+ * Widths are layout (CSS) pixels from `offsetWidth`, the unit the animated
54
+ * `width` is in, so a scaled or zoomed ancestor cannot distort the glide. A
55
+ * button that has no box (an ancestor is `display: none`) records no width, so
56
+ * when it is shown it settles at its size instead of growing from nothing.
50
57
  */
51
- function useWidthTween(node, label) {
58
+ function useWidthTween(node, loading, result) {
52
59
  const width = useRef(null);
60
+ const look = useRef(null);
53
61
  const tween = useRef(null);
62
+ // After every commit: the rendered text is only known once it is in the DOM.
54
63
  useLayoutEffect(() => {
55
64
  const element = node.current;
56
65
  if (!element)
57
66
  return;
67
+ const now = JSON.stringify([loading, result ?? null, element.textContent]);
68
+ const changed = now !== look.current;
69
+ look.current = now;
58
70
  const running = tween.current;
59
- const from = running
60
- ? element.getBoundingClientRect().width
61
- : width.current;
62
- running?.cancel();
63
- tween.current = null;
64
- const to = element.getBoundingClientRect().width;
65
- width.current = to;
66
- if (from === null || Math.abs(from - to) < 1 || !canAnimate(element))
71
+ // Nothing new to say: let a glide in progress finish undisturbed.
72
+ if (!changed && running)
73
+ return;
74
+ const from = running ? element.offsetWidth : width.current;
75
+ if (running) {
76
+ tween.current = null;
77
+ running.cancel();
78
+ element.style.removeProperty("overflow");
79
+ element.style.removeProperty("white-space");
80
+ }
81
+ const to = element.offsetWidth;
82
+ width.current = to > 0 ? to : null;
83
+ if (!changed ||
84
+ from === null ||
85
+ to === 0 ||
86
+ Math.abs(from - to) < 1 ||
87
+ !canAnimate(element))
67
88
  return;
68
89
  const motion = readMotion(element);
69
90
  if (motion.reduced)
@@ -82,7 +103,7 @@ function useWidthTween(node, label) {
82
103
  };
83
104
  animation.onfinish = done;
84
105
  animation.oncancel = done;
85
- }, [node, ...label]);
106
+ });
86
107
  }
87
108
  /** How long the done beat holds before settling: long enough to read. */
88
109
  const resultHoldMs = 1100;
@@ -5,16 +5,6 @@ export type KeycapProps = {
5
5
  } & HTMLAttributes<HTMLElement>;
6
6
  /** The shared field-guide treatment for a keyboard key. */
7
7
  export declare function Keycap({ children, className, ...props }: KeycapProps): import("react/jsx-runtime").JSX.Element;
8
- /** Semantic accent pigments for the static status light. */
9
- export type StatusLedTone = "spore" | "mycelium" | "fruiting";
10
- /** Props accepted by StatusLed. */
11
- export type StatusLedProps = {
12
- active: boolean;
13
- label: string;
14
- tone?: StatusLedTone;
15
- } & Omit<HTMLAttributes<HTMLSpanElement>, "children">;
16
- /** A static, labelled indicator using the current field-guide pigment tokens. */
17
- export declare function StatusLed({ active, className, label, tone, ...props }: StatusLedProps): import("react/jsx-runtime").JSX.Element;
18
8
  /** Props accepted by CartridgeLabel. */
19
9
  export type CartridgeLabelProps = {
20
10
  title: string;
@@ -4,10 +4,6 @@ import { cn } from "../lib/cn.js";
4
4
  export function Keycap({ children, className, ...props }) {
5
5
  return (_jsx("kbd", { ...props, className: cn("caps-keycap", className), children: children }));
6
6
  }
7
- /** A static, labelled indicator using the current field-guide pigment tokens. */
8
- export function StatusLed({ active, className, label, tone = "mycelium", ...props }) {
9
- return (_jsxs("span", { ...props, "aria-label": `${label}: ${active ? "on" : "off"}`, className: cn("caps-led", className), "data-active": active, "data-tone": tone, role: "img", children: [_jsx("span", { "aria-hidden": "true", className: "caps-led-bead" }), _jsx("span", { "aria-hidden": "true", children: label })] }));
10
- }
11
7
  /** A ruled, token-backed printed label for a physical game cartridge. */
12
8
  export function CartridgeLabel({ className, mark, subtitle, title, ...props }) {
13
9
  return (_jsxs("figure", { ...props, "aria-label": `${title} cartridge label`, className: cn("caps-cartridge-label", className), children: [_jsx("span", { "aria-hidden": "true", className: "caps-cartridge-label-rule" }), mark ? (_jsx("span", { "aria-hidden": "true", className: "caps-cartridge-label-mark", children: mark })) : null, _jsx("span", { className: "caps-cartridge-label-title", children: title }), subtitle ? (_jsx("span", { className: "caps-cartridge-label-subtitle", children: subtitle })) : null, _jsx("span", { "aria-hidden": "true", className: "caps-cartridge-label-rule" })] }));
@@ -0,0 +1,48 @@
1
+ import { type StatusTone } from "./status.js";
2
+ /** One place in the content's path; `siblings` makes it a switcher. */
3
+ export type WindowCrumb = {
4
+ id: string;
5
+ label: string;
6
+ /** Other places at this level, offered by a ▾ switcher on the crumb. */
7
+ siblings?: readonly {
8
+ id: string;
9
+ label: string;
10
+ }[];
11
+ };
12
+ /** A command the content offers in the header; the content owns its effect. */
13
+ export type WindowCommand = {
14
+ id: string;
15
+ label: string;
16
+ /** Render a supported glyph alone; label remains the accessible name. */
17
+ icon?: "play";
18
+ /** Which Caps button a primary command wears; default `outline`. */
19
+ variant?: "primary" | "outline" | "ghost";
20
+ /** A menu item that removes or discards something. */
21
+ tone?: "destructive";
22
+ disabled?: boolean;
23
+ };
24
+ /** A quiet fact about the view: muted text led by the Caps status bead. */
25
+ export type WindowStatus = {
26
+ id: string;
27
+ label: string;
28
+ /** Hide only the visible label, retaining it for assistive technology. */
29
+ presentation?: "bead";
30
+ tone?: Extract<StatusTone, "neutral" | "info" | "warning" | "success" | "error">;
31
+ };
32
+ /** The path as a trail; on a phone only `‹ parent › current` remain. */
33
+ export declare function WindowCrumbs({ crumbs, popover, onCrumb, }: {
34
+ crumbs: readonly WindowCrumb[];
35
+ popover?: HTMLElement | null | undefined;
36
+ onCrumb?: ((crumbId: string, siblingId?: string) => void) | undefined;
37
+ }): import("react/jsx-runtime").JSX.Element;
38
+ /** Primary commands as buttons, the rest behind a ⋯ menu. */
39
+ export declare function WindowCommands({ primary, menu, popover, onAction, }: {
40
+ primary?: readonly WindowCommand[] | undefined;
41
+ menu?: readonly WindowCommand[] | undefined;
42
+ popover?: HTMLElement | null | undefined;
43
+ onAction?: ((id: string) => void) | undefined;
44
+ }): import("react/jsx-runtime").JSX.Element | null;
45
+ /** Settled beads reveal their labels on hover/focus; other state stays visible. */
46
+ export declare function WindowStatuses({ status, }: {
47
+ status: readonly WindowStatus[];
48
+ }): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ChevronDown, Ellipsis, Play } from "lucide-react";
3
+ import { Button } from "./button.js";
4
+ import { Status } from "./status.js";
5
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "./dropdown-menu.js";
6
+ import { IconButton } from "./icon-button.js";
7
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
8
+ /** The longest path shown whole; longer paths fold the middle into "…". */
9
+ const CRUMBS_SHOWN = 4;
10
+ function Separator() {
11
+ return (_jsx("span", { className: "caps-window-crumb-separator", "aria-hidden": "true", children: "\u203A" }));
12
+ }
13
+ function Crumb({ crumb, current, popover, onCrumb, }) {
14
+ const label = _jsx("span", { className: "caps-window-crumb-label", children: crumb.label });
15
+ const common = {
16
+ "data-caps-window-crumb": crumb.id,
17
+ "aria-current": current ? "page" : undefined,
18
+ };
19
+ if (crumb.siblings?.length) {
20
+ return (_jsxs(DropdownMenu, { modal: false, children: [_jsxs(DropdownMenuTrigger, { ...common, className: "caps-window-crumb shrink", render: _jsx(Button, { size: "xs", variant: "ghost" }), "aria-label": `${crumb.label}, switch`, children: [label, _jsx(ChevronDown, { "aria-hidden": "true", className: "size-3" })] }), _jsx(DropdownMenuContent, { align: "start", container: popover, children: crumb.siblings.map((sibling) => (_jsx(DropdownMenuItem, { onSelect: () => {
21
+ onCrumb?.(crumb.id, sibling.id);
22
+ }, children: sibling.label }, sibling.id))) })] }));
23
+ }
24
+ if (current || onCrumb === undefined)
25
+ return (_jsx("span", { ...common, className: "caps-window-crumb", children: label }));
26
+ return (_jsx(Button, { ...common, size: "xs", variant: "ghost", className: "caps-window-crumb shrink", onClick: () => {
27
+ onCrumb(crumb.id);
28
+ }, children: label }));
29
+ }
30
+ /** The path as a trail; on a phone only `‹ parent › current` remain. */
31
+ export function WindowCrumbs({ crumbs, popover, onCrumb, }) {
32
+ const folded = crumbs.length > CRUMBS_SHOWN;
33
+ const head = folded ? crumbs.slice(0, 1) : crumbs.slice(0, -1);
34
+ const middle = folded ? crumbs.slice(1, -2) : [];
35
+ const tail = folded ? crumbs.slice(-2) : crumbs.slice(-1);
36
+ const last = crumbs.at(-1);
37
+ const parts = [];
38
+ const add = (crumb) => {
39
+ parts.push(_jsxs("span", { className: "caps-window-crumb-item", "data-caps-crumb-position": crumb.id === last?.id
40
+ ? "current"
41
+ : crumb.id === crumbs.at(-2)?.id
42
+ ? "parent"
43
+ : "ancestor", children: [parts.length > 0 ? _jsx(Separator, {}) : null, _jsx(Crumb, { crumb: crumb, current: crumb.id === last?.id, popover: popover, onCrumb: onCrumb })] }, crumb.id));
44
+ };
45
+ for (const crumb of head)
46
+ add(crumb);
47
+ if (middle.length > 0)
48
+ parts.push(_jsxs("span", { className: "caps-window-crumb-item", "data-caps-crumb-position": "ancestor", children: [_jsx(Separator, {}), _jsxs(DropdownMenu, { modal: false, children: [_jsx(DropdownMenuTrigger, { className: "caps-window-crumb", render: _jsx(Button, { size: "xs", variant: "ghost" }), "aria-label": "More of the path", children: _jsx(Ellipsis, { "aria-hidden": "true", className: "size-3" }) }), _jsx(DropdownMenuContent, { align: "start", container: popover, children: middle.map((crumb) => (_jsx(DropdownMenuItem, { onSelect: () => {
49
+ onCrumb?.(crumb.id);
50
+ }, children: crumb.label }, crumb.id))) })] })] }, "fold"));
51
+ for (const crumb of tail)
52
+ add(crumb);
53
+ return (_jsx("nav", { className: "caps-window-crumbs", "aria-label": "Path", children: parts }));
54
+ }
55
+ /** Primary commands as buttons, the rest behind a ⋯ menu. */
56
+ export function WindowCommands({ primary, menu, popover, onAction, }) {
57
+ if (!primary?.length && !menu?.length)
58
+ return null;
59
+ return (_jsxs("div", { className: "caps-window-commands", role: "group", "aria-label": "Commands", children: [primary?.map((command) => command.icon === "play" ? (_jsx(IconButton, { icon: _jsx(Play, {}), "aria-label": command.label, title: command.label, variant: command.variant ?? "ghost", disabled: command.disabled === true, onClick: () => {
60
+ onAction?.(command.id);
61
+ } }, command.id)) : (_jsx(Button, { size: "xs", variant: command.variant ?? "outline", disabled: command.disabled === true, onClick: () => {
62
+ onAction?.(command.id);
63
+ }, children: command.label }, command.id))), menu?.length ? (_jsxs(DropdownMenu, { modal: false, children: [_jsx(DropdownMenuTrigger, { render: _jsx(IconButton, { icon: _jsx(Ellipsis, {}), "aria-label": "More" }) }), _jsx(DropdownMenuContent, { align: "end", container: popover, children: menu.map((command) => (_jsx(DropdownMenuItem, { disabled: command.disabled === true, variant: command.tone === "destructive" ? "destructive" : "default", onSelect: () => {
64
+ onAction?.(command.id);
65
+ }, children: command.label }, command.id))) })] })) : null] }));
66
+ }
67
+ /** Settled beads reveal their labels on hover/focus; other state stays visible. */
68
+ export function WindowStatuses({ status, }) {
69
+ if (status.length === 0)
70
+ return null;
71
+ return (_jsx("div", { className: "caps-window-statuses", role: "group", "aria-label": "State", children: status.map((item) => {
72
+ const beadOnly = item.presentation === "bead";
73
+ const readout = (_jsx(Status, { glyph: "dot", size: "compact", tone: item.tone ?? "neutral", label: beadOnly ? (_jsx("span", { className: "sr-only", children: item.label })) : (item.label), tabIndex: beadOnly ? 0 : undefined, title: beadOnly ? undefined : item.label, className: "caps-window-status" }, item.id));
74
+ return beadOnly ? (_jsxs(Tooltip, { side: "bottom", children: [_jsx(TooltipTrigger, { asChild: true, children: readout }), _jsx(TooltipContent, { children: item.label })] }, item.id)) : (readout);
75
+ }) }));
76
+ }
@@ -1,4 +1,6 @@
1
1
  import { type ButtonHTMLAttributes, type HTMLAttributes, type ReactNode } from "react";
2
+ import { type WindowStatus, type WindowCommand, type WindowCrumb } from "./window-crumbs.js";
3
+ export type { WindowCommand, WindowCrumb, WindowStatus, } from "./window-crumbs.js";
2
4
  /** Window presentation only. The host owns geometry, focus order and lifecycle. */
3
5
  export type WindowFrameProps = HTMLAttributes<HTMLElement> & {
4
6
  active?: boolean;
@@ -33,12 +35,30 @@ export type WindowHeaderProps = Omit<HTMLAttributes<HTMLElement>, "title" | "chi
33
35
  actions?: ReactNode;
34
36
  /** Present only while there is somewhere to go back to. */
35
37
  back?: WindowBackAction;
38
+ /**
39
+ * The content's path, shown in place of the title and subtitle. The last
40
+ * crumb is the current place; earlier ones are links, and a crumb with
41
+ * siblings is a switcher.
42
+ */
43
+ crumbs?: readonly WindowCrumb[];
44
+ /** Selected crumb, or the sibling chosen from a switcher crumb. */
45
+ onCrumb?: (crumbId: string, siblingId?: string) => void;
46
+ /** Quiet facts about the view, such as "Valid" or "Saved", with beads. */
47
+ status?: readonly WindowStatus[];
48
+ /** The view's own commands, as buttons before the window controls. */
49
+ primary?: readonly WindowCommand[];
50
+ /** Less common commands, behind a ⋯ menu. */
51
+ menu?: readonly WindowCommand[];
52
+ /** A primary or menu command the person used, by its id. */
53
+ onAction?: (id: string) => void;
54
+ /** Where header menus open; the host's popover layer. */
55
+ popover?: HTMLElement | null;
36
56
  /** `compact` is a slim bar for tiled layouts; touch targets keep 44px. */
37
57
  density?: "regular" | "compact";
38
58
  handleProps?: Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children" | "type">;
39
59
  };
40
60
  /** The handle, Back and actions are siblings, so presses never start a drag. */
41
- export declare function WindowHeader({ title, subtitle, icon, actions, back, density, handleProps, className, ...props }: WindowHeaderProps): import("react/jsx-runtime").JSX.Element;
61
+ export declare function WindowHeader({ title, subtitle, icon, actions, back, crumbs, onCrumb, primary, menu, status, onAction, popover, density, handleProps, className, ...props }: WindowHeaderProps): import("react/jsx-runtime").JSX.Element;
42
62
  /** Available callbacks determine available controls; no inert placeholder actions. */
43
63
  export type WindowActionsProps = {
44
64
  title: string;
@@ -3,6 +3,7 @@ import { ArrowLeft, ArrowUpRight, Columns2, Maximize2, Minimize2, Minus, PanelsT
3
3
  import { forwardRef, } from "react";
4
4
  import { cn } from "../lib/cn.js";
5
5
  import { IconButton } from "./icon-button.js";
6
+ import { WindowCommands, WindowCrumbs, WindowStatuses, } from "./window-crumbs.js";
6
7
  /** A clipped, rounded shell for either a tiled or floating application. */
7
8
  export const WindowFrame = forwardRef(function WindowFrame({ active = false, floating = false, fitContent = false, className, ...props }, ref) {
8
9
  return (_jsx("section", { ...props, ref: ref, className: cn("caps-window", className), "data-active": active, "data-floating": floating, "data-fit-content": fitContent }));
@@ -12,10 +13,11 @@ export function WindowSnapPreview({ className, ...props }) {
12
13
  return (_jsx("div", { ...props, "aria-hidden": "true", className: cn("caps-window-snap-preview", className) }));
13
14
  }
14
15
  /** The handle, Back and actions are siblings, so presses never start a drag. */
15
- export function WindowHeader({ title, subtitle, icon, actions, back, density = "regular", handleProps, className, ...props }) {
16
+ export function WindowHeader({ title, subtitle, icon, actions, back, crumbs, onCrumb, primary, menu, status, onAction, popover, density = "regular", handleProps, className, ...props }) {
16
17
  const label = subtitle ? `${title} · ${subtitle}` : title;
17
- const content = (_jsxs(_Fragment, { children: [icon && (_jsx("span", { className: "caps-window-icon", "aria-hidden": "true", children: icon })), _jsx("span", { className: "caps-window-name", children: title }), subtitle && (_jsxs("span", { className: "caps-window-subtitle", children: [_jsx("span", { "aria-hidden": "true", children: "\u00B7" }), " ", subtitle] }))] }));
18
- return (_jsxs("header", { ...props, className: cn("caps-window-header", className), "data-density": density, children: [back && (_jsx(IconButton, { className: "caps-window-back", "aria-label": back.label, title: back.hint ?? back.label, icon: _jsx(ArrowLeft, {}), onClick: back.onBack })), handleProps ? (_jsx("button", { ...handleProps, type: "button", "data-caps-window-handle": true, className: cn("caps-window-title", handleProps.className), title: label, children: content })) : (_jsx("div", { className: "caps-window-title", title: label, children: content })), actions] }));
18
+ const trail = crumbs !== undefined && crumbs.length > 0;
19
+ const content = trail ? null : (_jsxs(_Fragment, { children: [icon && (_jsx("span", { className: "caps-window-icon", "aria-hidden": "true", children: icon })), _jsx("span", { className: "caps-window-name", children: title }), subtitle && (_jsxs("span", { className: "caps-window-subtitle", children: [_jsx("span", { "aria-hidden": "true", children: "\u00B7" }), " ", subtitle] }))] }));
20
+ return (_jsxs("header", { ...props, className: cn("caps-window-header", className), "data-density": density, children: [back && (_jsx(IconButton, { className: "caps-window-back", "aria-label": back.label, title: back.hint ?? back.label, icon: _jsx(ArrowLeft, {}), onClick: back.onBack })), trail ? (_jsxs(_Fragment, { children: [icon && (_jsx("span", { className: "caps-window-icon", "aria-hidden": "true", children: icon })), _jsx(WindowCrumbs, { crumbs: crumbs, popover: popover, onCrumb: onCrumb })] })) : null, handleProps ? (_jsx("button", { ...handleProps, type: "button", "data-caps-window-handle": true, className: cn("caps-window-title", handleProps.className), title: label, ...(trail ? { "aria-label": label } : {}), children: content })) : (_jsx("div", { className: "caps-window-title", title: label, ...(trail ? { "aria-hidden": true } : {}), children: content })), status ? _jsx(WindowStatuses, { status: status }) : null, _jsx(WindowCommands, { primary: primary, menu: menu, popover: popover, onAction: onAction }), actions] }));
19
21
  }
20
22
  /** Consistent icons, ordering and accessible names for host-owned commands. */
21
23
  export function WindowActions({ title, floating = false, maximized = false, onPromote, onToggleFloating, onToggleMaximize, onMinimize, onClose, onSelect, }) {
package/dist/styles.css CHANGED
@@ -5466,9 +5466,6 @@
5466
5466
  .mt-sm {
5467
5467
  margin-top: var(--spacing-sm);
5468
5468
  }
5469
- .mb-xs {
5470
- margin-bottom: var(--spacing-xs);
5471
- }
5472
5469
  .status {
5473
5470
  @layer daisyui.l1.l2.l3 {
5474
5471
  display: inline-block;
@@ -5908,15 +5905,9 @@
5908
5905
  .w-12 {
5909
5906
  width: calc(var(--spacing) * 12);
5910
5907
  }
5911
- .w-20 {
5912
- width: calc(var(--spacing) * 20);
5913
- }
5914
5908
  .w-48 {
5915
5909
  width: calc(var(--spacing) * 48);
5916
5910
  }
5917
- .w-\[calc\(100\%-1rem\)\] {
5918
- width: calc(100% - 1rem);
5919
- }
5920
5911
  .w-auto {
5921
5912
  width: auto;
5922
5913
  }
@@ -5980,12 +5971,12 @@
5980
5971
  .flex-1 {
5981
5972
  flex: 1;
5982
5973
  }
5974
+ .shrink {
5975
+ flex-shrink: 1;
5976
+ }
5983
5977
  .shrink-0 {
5984
5978
  flex-shrink: 0;
5985
5979
  }
5986
- .grow {
5987
- flex-grow: 1;
5988
- }
5989
5980
  .-translate-x-1\/2 {
5990
5981
  --tw-translate-x: calc(calc(1 / 2 * 100%) * -1);
5991
5982
  translate: var(--tw-translate-x) var(--tw-translate-y);
@@ -6175,9 +6166,6 @@
6175
6166
  .self-stretch {
6176
6167
  align-self: stretch;
6177
6168
  }
6178
- .justify-self-end {
6179
- justify-self: flex-end;
6180
- }
6181
6169
  .truncate {
6182
6170
  overflow: hidden;
6183
6171
  text-overflow: ellipsis;
@@ -6394,9 +6382,6 @@
6394
6382
  .p-lg {
6395
6383
  padding: var(--spacing-lg);
6396
6384
  }
6397
- .p-lg\! {
6398
- padding: var(--spacing-lg) !important;
6399
- }
6400
6385
  .p-md {
6401
6386
  padding: var(--spacing-md);
6402
6387
  }
@@ -6409,9 +6394,6 @@
6409
6394
  .p-xs {
6410
6395
  padding: var(--spacing-xs);
6411
6396
  }
6412
- .p-xs\! {
6413
- padding: var(--spacing-xs) !important;
6414
- }
6415
6397
  .caps-stat-compact {
6416
6398
  & > .stat {
6417
6399
  padding-block: var(--spacing-xs);
@@ -6448,15 +6430,6 @@
6448
6430
  .py-xs {
6449
6431
  padding-block: var(--spacing-xs);
6450
6432
  }
6451
- .ps-md {
6452
- padding-inline-start: var(--spacing-md);
6453
- }
6454
- .ps-xs {
6455
- padding-inline-start: var(--spacing-xs);
6456
- }
6457
- .pe-sm {
6458
- padding-inline-end: var(--spacing-sm);
6459
- }
6460
6433
  .pt-3xl {
6461
6434
  padding-top: var(--spacing-3xl);
6462
6435
  }
@@ -6582,9 +6555,6 @@
6582
6555
  .whitespace-nowrap {
6583
6556
  white-space: nowrap;
6584
6557
  }
6585
- .whitespace-pre-wrap {
6586
- white-space: pre-wrap;
6587
- }
6588
6558
  .alert-error {
6589
6559
  @layer daisyui.l1.l2 {
6590
6560
  color: var(--color-error-content);
@@ -6771,9 +6741,6 @@
6771
6741
  .underline {
6772
6742
  text-decoration-line: underline;
6773
6743
  }
6774
- .opacity-50 {
6775
- opacity: 50%;
6776
- }
6777
6744
  .opacity-55 {
6778
6745
  opacity: 55%;
6779
6746
  }
@@ -7172,20 +7139,6 @@
7172
7139
  }
7173
7140
  }
7174
7141
  }
7175
- .hover\:gap-md {
7176
- &:hover {
7177
- @media (hover: hover) {
7178
- gap: var(--spacing-md);
7179
- }
7180
- }
7181
- }
7182
- .hover\:gap-sm {
7183
- &:hover {
7184
- @media (hover: hover) {
7185
- gap: var(--spacing-sm);
7186
- }
7187
- }
7188
- }
7189
7142
  .hover\:bg-base-200 {
7190
7143
  &:hover {
7191
7144
  @media (hover: hover) {
@@ -7214,11 +7167,6 @@
7214
7167
  }
7215
7168
  }
7216
7169
  }
7217
- .focus\:gap-xs {
7218
- &:focus {
7219
- gap: var(--spacing-xs);
7220
- }
7221
- }
7222
7170
  .focus-visible\:scale-100 {
7223
7171
  &:focus-visible {
7224
7172
  --tw-scale-x: 100%;
@@ -7373,16 +7321,6 @@
7373
7321
  }
7374
7322
  }
7375
7323
  }
7376
- .data-\[open\]\:bg-base-200 {
7377
- &[data-open] {
7378
- background-color: var(--color-base-200);
7379
- }
7380
- }
7381
- .data-\[open\]\:bg-base-300 {
7382
- &[data-open] {
7383
- background-color: var(--color-base-300);
7384
- }
7385
- }
7386
7324
  .motion-reduce\:transition-none {
7387
7325
  @media (prefers-reduced-motion: reduce) {
7388
7326
  transition-property: none;
@@ -7448,16 +7386,6 @@
7448
7386
  padding-inline: calc(var(--spacing) * 2);
7449
7387
  }
7450
7388
  }
7451
- .md\:px-lg {
7452
- @media (width >= 48rem) {
7453
- padding-inline: var(--spacing-lg);
7454
- }
7455
- }
7456
- .md\:px-xs {
7457
- @media (width >= 48rem) {
7458
- padding-inline: var(--spacing-xs);
7459
- }
7460
- }
7461
7389
  .md\:btn-xs {
7462
7390
  @media (width >= 48rem) {
7463
7391
  @layer daisyui.l1.l2 {
@@ -9152,37 +9080,6 @@
9152
9080
  font-family: var(--font-mono);
9153
9081
  line-height: 1;
9154
9082
  }
9155
- .caps-led {
9156
- display: inline-flex;
9157
- align-items: center;
9158
- gap: var(--spacing-xs);
9159
- color: var(--marginalia-ink);
9160
- font-size: 0.75rem;
9161
- }
9162
- .caps-led-bead {
9163
- width: 0.625rem;
9164
- height: 0.625rem;
9165
- flex: none;
9166
- border: var(--card-rule-width) solid var(--card-rule);
9167
- border-radius: var(--radius-selector);
9168
- background: var(--caps-led-color);
9169
- box-shadow: 0 0 0 var(--card-rule-width) var(--caps-led-color);
9170
- @supports (color: color-mix(in lab, red, red)) {
9171
- box-shadow: 0 0 0 var(--card-rule-width) color-mix(in srgb, var(--caps-led-color) 36%, transparent);
9172
- }
9173
- }
9174
- .caps-led[data-tone="spore"] {
9175
- --caps-led-color: var(--growth-spore);
9176
- }
9177
- .caps-led[data-tone="mycelium"] {
9178
- --caps-led-color: var(--growth-mycelium);
9179
- }
9180
- .caps-led[data-tone="fruiting"] {
9181
- --caps-led-color: var(--growth-fruiting);
9182
- }
9183
- .caps-led[data-active="false"] {
9184
- --caps-led-color: var(--marginalia-ink);
9185
- }
9186
9083
  .caps-cartridge-label {
9187
9084
  display: grid;
9188
9085
  justify-items: center;
@@ -14075,6 +13972,98 @@
14075
13972
  min-height: 44px;
14076
13973
  }
14077
13974
  }
13975
+ .caps-window-header > .caps-window-icon {
13976
+ margin-inline: var(--spacing-md) calc(var(--spacing-sm) - var(--spacing-2xs));
13977
+ }
13978
+ .caps-window-crumbs {
13979
+ display: flex;
13980
+ align-items: center;
13981
+ min-width: 0;
13982
+ flex-shrink: 1;
13983
+ font-family: var(--caps-label-font, inherit);
13984
+ font-size: 11px;
13985
+ font-weight: 500;
13986
+ }
13987
+ .caps-window-crumb-item[data-caps-crumb-position="current"] {
13988
+ font-weight: 600;
13989
+ }
13990
+ .caps-window-crumb-item {
13991
+ display: flex;
13992
+ align-items: center;
13993
+ min-width: 0;
13994
+ }
13995
+ .caps-window-crumb-separator {
13996
+ padding-inline: var(--spacing-2xs);
13997
+ color: currentColor;
13998
+ @supports (color: color-mix(in lab, red, red)) {
13999
+ color: color-mix(in srgb, currentColor 50%, transparent);
14000
+ }
14001
+ }
14002
+ .caps-window-crumb {
14003
+ min-width: 0;
14004
+ font-size: inherit;
14005
+ font-weight: inherit;
14006
+ gap: var(--spacing-2xs);
14007
+ }
14008
+ .caps-window-crumb-label {
14009
+ overflow: hidden;
14010
+ text-overflow: ellipsis;
14011
+ white-space: nowrap;
14012
+ }
14013
+ .caps-window-crumb-item[data-caps-crumb-position="current"] .caps-window-crumb {
14014
+ padding-inline: var(--spacing-xs);
14015
+ }
14016
+ .caps-window-crumbs:has(+ .caps-window-title) {
14017
+ max-width: 70%;
14018
+ }
14019
+ .caps-window-statuses {
14020
+ display: flex;
14021
+ align-items: center;
14022
+ gap: var(--spacing-sm);
14023
+ flex-shrink: 0;
14024
+ padding-inline-end: var(--spacing-sm);
14025
+ font-size: 11px;
14026
+ color: currentColor;
14027
+ @supports (color: color-mix(in lab, red, red)) {
14028
+ color: color-mix(in srgb, currentColor 65%, transparent);
14029
+ }
14030
+ }
14031
+ .caps-window-commands {
14032
+ display: flex;
14033
+ align-items: center;
14034
+ gap: var(--spacing-2xs);
14035
+ flex-shrink: 0;
14036
+ }
14037
+ @media (max-width: 640px) {
14038
+ .caps-window-header:has(.caps-window-crumbs) {
14039
+ flex-wrap: wrap;
14040
+ }
14041
+ .caps-window-crumbs {
14042
+ min-width: min-content;
14043
+ flex-shrink: 0;
14044
+ }
14045
+ .caps-window-crumb-item {
14046
+ flex-shrink: 0;
14047
+ }
14048
+ .caps-window-statuses {
14049
+ gap: var(--spacing-2xs);
14050
+ padding-inline-end: var(--spacing-2xs);
14051
+ }
14052
+ .caps-window-status > span {
14053
+ position: absolute;
14054
+ width: 1px;
14055
+ height: 1px;
14056
+ overflow: hidden;
14057
+ clip-path: inset(50%);
14058
+ white-space: nowrap;
14059
+ }
14060
+ .caps-window-crumb-item[data-caps-crumb-position="ancestor"] {
14061
+ display: none;
14062
+ }
14063
+ .caps-window-crumb-item[data-caps-crumb-position="parent"] .caps-window-crumb-separator {
14064
+ display: none;
14065
+ }
14066
+ }
14078
14067
  .caps-window-body {
14079
14068
  flex: 1;
14080
14069
  min-height: 0;
@@ -14408,6 +14397,7 @@
14408
14397
  display: block;
14409
14398
  flex: none;
14410
14399
  overflow: visible;
14400
+ pointer-events: none;
14411
14401
  transition: color var(--stipe-motion-normal) ease;
14412
14402
  }
14413
14403
  .caps-status-mark {
@@ -15112,9 +15102,6 @@
15112
15102
  font-size: var(--text-xs);
15113
15103
  white-space: nowrap;
15114
15104
  }
15115
- .caps-status-compact .caps-status-bead {
15116
- transform: scale(0.6);
15117
- }
15118
15105
  .caps-action-row-trailing {
15119
15106
  display: flex;
15120
15107
  width: 6rem;
@@ -118,6 +118,7 @@
118
118
  | [components/undo-action](components/undo-action.md) | - |
119
119
  | [components/undo-toast](components/undo-toast.md) | - |
120
120
  | [components/visually-hidden](components/visually-hidden.md) | - |
121
+ | [components/window-crumbs](components/window-crumbs.md) | - |
121
122
  | [components/window](components/window.md) | - |
122
123
  | [components/wizard](components/wizard.md) | - |
123
124
  | [enhance/cigarette-card](enhance/cigarette-card.md) | - |
@@ -26,35 +26,6 @@ A visible keyboard key from a shortcut or instruction.
26
26
 
27
27
  ---
28
28
 
29
- ### StatusLedTone
30
-
31
- ```ts
32
- type StatusLedTone = "spore" | "mycelium" | "fruiting";
33
- ```
34
-
35
- Semantic accent pigments for the static status light.
36
-
37
- ---
38
-
39
- ### StatusLedProps
40
-
41
- ```ts
42
- type StatusLedProps = object &
43
- Omit<HTMLAttributes<HTMLSpanElement>, "children">;
44
- ```
45
-
46
- Props accepted by StatusLed.
47
-
48
- #### Type Declaration
49
-
50
- | Name | Type |
51
- | -------- | --------------------------------- |
52
- | `active` | `boolean` |
53
- | `label` | `string` |
54
- | `tone?` | [`StatusLedTone`](#statusledtone) |
55
-
56
- ---
57
-
58
29
  ### CartridgeLabelProps
59
30
 
60
31
  ```ts
@@ -115,26 +86,6 @@ The shared field-guide treatment for a keyboard key.
115
86
 
116
87
  ---
117
88
 
118
- ### StatusLed()
119
-
120
- ```ts
121
- function StatusLed(__namedParameters): Element;
122
- ```
123
-
124
- A static, labelled indicator using the current field-guide pigment tokens.
125
-
126
- #### Parameters
127
-
128
- | Parameter | Type |
129
- | ------------------- | ----------------------------------- |
130
- | `__namedParameters` | [`StatusLedProps`](#statusledprops) |
131
-
132
- #### Returns
133
-
134
- `Element`
135
-
136
- ---
137
-
138
89
  ### CartridgeLabel()
139
90
 
140
91
  ```ts
@@ -0,0 +1,201 @@
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/window-crumbs
8
+
9
+ # components/window-crumbs
10
+
11
+ ## Type Aliases
12
+
13
+ ### WindowCrumb
14
+
15
+ ```ts
16
+ type WindowCrumb = object;
17
+ ```
18
+
19
+ One place in the content's path; `siblings` makes it a switcher.
20
+
21
+ #### Properties
22
+
23
+ ##### id
24
+
25
+ ```ts
26
+ id: string;
27
+ ```
28
+
29
+ ##### label
30
+
31
+ ```ts
32
+ label: string;
33
+ ```
34
+
35
+ ##### siblings?
36
+
37
+ ```ts
38
+ optional siblings?: readonly object[];
39
+ ```
40
+
41
+ Other places at this level, offered by a ▾ switcher on the crumb.
42
+
43
+ ---
44
+
45
+ ### WindowCommand
46
+
47
+ ```ts
48
+ type WindowCommand = object;
49
+ ```
50
+
51
+ A command the content offers in the header; the content owns its effect.
52
+
53
+ #### Properties
54
+
55
+ ##### id
56
+
57
+ ```ts
58
+ id: string;
59
+ ```
60
+
61
+ ##### label
62
+
63
+ ```ts
64
+ label: string;
65
+ ```
66
+
67
+ ##### icon?
68
+
69
+ ```ts
70
+ optional icon?: "play";
71
+ ```
72
+
73
+ Render a supported glyph alone; label remains the accessible name.
74
+
75
+ ##### variant?
76
+
77
+ ```ts
78
+ optional variant?: "primary" | "outline" | "ghost";
79
+ ```
80
+
81
+ Which Caps button a primary command wears; default `outline`.
82
+
83
+ ##### tone?
84
+
85
+ ```ts
86
+ optional tone?: "destructive";
87
+ ```
88
+
89
+ A menu item that removes or discards something.
90
+
91
+ ##### disabled?
92
+
93
+ ```ts
94
+ optional disabled?: boolean;
95
+ ```
96
+
97
+ ---
98
+
99
+ ### WindowStatus
100
+
101
+ ```ts
102
+ type WindowStatus = object;
103
+ ```
104
+
105
+ A quiet fact about the view: muted text led by the Caps status bead.
106
+
107
+ #### Properties
108
+
109
+ ##### id
110
+
111
+ ```ts
112
+ id: string;
113
+ ```
114
+
115
+ ##### label
116
+
117
+ ```ts
118
+ label: string;
119
+ ```
120
+
121
+ ##### presentation?
122
+
123
+ ```ts
124
+ optional presentation?: "bead";
125
+ ```
126
+
127
+ Hide only the visible label, retaining it for assistive technology.
128
+
129
+ ##### tone?
130
+
131
+ ```ts
132
+ optional tone?: Extract<StatusTone, "neutral" | "info" | "warning" | "success" | "error">;
133
+ ```
134
+
135
+ ## Functions
136
+
137
+ ### WindowCrumbs()
138
+
139
+ ```ts
140
+ function WindowCrumbs(__namedParameters): Element;
141
+ ```
142
+
143
+ The path as a trail; on a phone only `‹ parent › current` remain.
144
+
145
+ #### Parameters
146
+
147
+ | Parameter | Type |
148
+ | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
149
+ | `__namedParameters` | \{ `crumbs`: readonly [`WindowCrumb`](#windowcrumb)[]; `popover?`: `HTMLElement` \| `null`; `onCrumb?`: (`crumbId`, `siblingId?`) => `void`; \} |
150
+ | `__namedParameters.crumbs` | readonly [`WindowCrumb`](#windowcrumb)[] |
151
+ | `__namedParameters.popover?` | `HTMLElement` \| `null` |
152
+ | `__namedParameters.onCrumb?` | (`crumbId`, `siblingId?`) => `void` |
153
+
154
+ #### Returns
155
+
156
+ `Element`
157
+
158
+ ---
159
+
160
+ ### WindowCommands()
161
+
162
+ ```ts
163
+ function WindowCommands(__namedParameters): Element | null;
164
+ ```
165
+
166
+ Primary commands as buttons, the rest behind a ⋯ menu.
167
+
168
+ #### Parameters
169
+
170
+ | Parameter | Type |
171
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
172
+ | `__namedParameters` | \{ `primary?`: readonly [`WindowCommand`](#windowcommand)[]; `menu?`: readonly [`WindowCommand`](#windowcommand)[]; `popover?`: `HTMLElement` \| `null`; `onAction?`: (`id`) => `void`; \} |
173
+ | `__namedParameters.primary?` | readonly [`WindowCommand`](#windowcommand)[] |
174
+ | `__namedParameters.menu?` | readonly [`WindowCommand`](#windowcommand)[] |
175
+ | `__namedParameters.popover?` | `HTMLElement` \| `null` |
176
+ | `__namedParameters.onAction?` | (`id`) => `void` |
177
+
178
+ #### Returns
179
+
180
+ `Element` \| `null`
181
+
182
+ ---
183
+
184
+ ### WindowStatuses()
185
+
186
+ ```ts
187
+ function WindowStatuses(__namedParameters): Element | null;
188
+ ```
189
+
190
+ Settled beads reveal their labels on hover/focus; other state stays visible.
191
+
192
+ #### Parameters
193
+
194
+ | Parameter | Type |
195
+ | -------------------------- | ----------------------------------------------------------- |
196
+ | `__namedParameters` | \{ `status`: readonly [`WindowStatus`](#windowstatus)[]; \} |
197
+ | `__namedParameters.status` | readonly [`WindowStatus`](#windowstatus)[] |
198
+
199
+ #### Returns
200
+
201
+ `Element` \| `null`
@@ -82,15 +82,22 @@ Supply handleProps only when the title is an interactive layout handle.
82
82
 
83
83
  #### Type Declaration
84
84
 
85
- | Name | Type | Description |
86
- | -------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
87
- | `title` | `string` | - |
88
- | `subtitle?` | `string` | Where the content is now, shown after the title and truncated first. |
89
- | `icon?` | `ReactNode` | - |
90
- | `actions?` | `ReactNode` | - |
91
- | `back?` | [`WindowBackAction`](#windowbackaction) | Present only while there is somewhere to go back to. |
92
- | `density?` | `"regular"` \| `"compact"` | `compact` is a slim bar for tiled layouts; touch targets keep 44px. |
93
- | `handleProps?` | `Omit`\<`ButtonHTMLAttributes`\<`HTMLButtonElement`\>, `"children"` \| `"type"`\> | - |
85
+ | Name | Type | Description |
86
+ | -------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
87
+ | `title` | `string` | - |
88
+ | `subtitle?` | `string` | Where the content is now, shown after the title and truncated first. |
89
+ | `icon?` | `ReactNode` | - |
90
+ | `actions?` | `ReactNode` | - |
91
+ | `back?` | [`WindowBackAction`](#windowbackaction) | Present only while there is somewhere to go back to. |
92
+ | `crumbs?` | readonly [`WindowCrumb`](window-crumbs.md#windowcrumb)[] | The content's path, shown in place of the title and subtitle. The last crumb is the current place; earlier ones are links, and a crumb with siblings is a switcher. |
93
+ | `onCrumb()?` | (`crumbId`, `siblingId?`) => `void` | Selected crumb, or the sibling chosen from a switcher crumb. |
94
+ | `status?` | readonly [`WindowStatus`](window-crumbs.md#windowstatus)[] | Quiet facts about the view, such as "Valid" or "Saved", with beads. |
95
+ | `primary?` | readonly [`WindowCommand`](window-crumbs.md#windowcommand)[] | The view's own commands, as buttons before the window controls. |
96
+ | `menu?` | readonly [`WindowCommand`](window-crumbs.md#windowcommand)[] | Less common commands, behind a ⋯ menu. |
97
+ | `onAction()?` | (`id`) => `void` | A primary or menu command the person used, by its id. |
98
+ | `popover?` | `HTMLElement` \| `null` | Where header menus open; the host's popover layer. |
99
+ | `density?` | `"regular"` \| `"compact"` | `compact` is a slim bar for tiled layouts; touch targets keep 44px. |
100
+ | `handleProps?` | `Omit`\<`ButtonHTMLAttributes`\<`HTMLButtonElement`\>, `"children"` \| `"type"`\> | - |
94
101
 
95
102
  ---
96
103
 
@@ -289,3 +296,21 @@ Consistent icons, ordering and accessible names for host-owned commands.
289
296
  #### Returns
290
297
 
291
298
  `Element`
299
+
300
+ ## References
301
+
302
+ ### WindowCommand
303
+
304
+ Re-exports [WindowCommand](window-crumbs.md#windowcommand)
305
+
306
+ ---
307
+
308
+ ### WindowCrumb
309
+
310
+ Re-exports [WindowCrumb](window-crumbs.md#windowcrumb)
311
+
312
+ ---
313
+
314
+ ### WindowStatus
315
+
316
+ Re-exports [WindowStatus](window-crumbs.md#windowstatus)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fungi.computer/caps",
3
- "version": "0.0.2",
3
+ "version": "0.2.0",
4
4
  "author": "Levi <levi@technomancy.dev>",
5
5
  "description": "Product-neutral React primitives and blocks for Fungi interfaces",
6
6
  "type": "module",
@@ -371,6 +371,7 @@
371
371
  "import": "./dist/blocks/card-grid.js"
372
372
  },
373
373
  "./fonts.css": "./src/fonts.css",
374
+ "./fonts-sans.css": "./src/fonts-sans.css",
374
375
  "./blocks/section-heading": {
375
376
  "types": "./dist/blocks/section-heading.d.ts",
376
377
  "import": "./dist/blocks/section-heading.js"
@@ -458,6 +459,7 @@
458
459
  "docs/api",
459
460
  "README.md",
460
461
  "src/fonts.css",
462
+ "src/fonts-sans.css",
461
463
  "src/status-tray.css",
462
464
  "src/app-status-bar.css"
463
465
  ],
@@ -468,9 +470,9 @@
468
470
  "effect": "4.0.2"
469
471
  },
470
472
  "devDependencies": {
471
- "@fungi.computer/maosaico": "0.0.1",
472
- "@fungi.computer/sprite": "0.0.1",
473
- "@fungi.computer/sprite-zdog": "0.0.1",
473
+ "@fungi.computer/maosaico": "0.1.0",
474
+ "@fungi.computer/sprite": "0.2.0",
475
+ "@fungi.computer/sprite-zdog": "0.2.0",
474
476
  "@ladle/react": "5.1.1",
475
477
  "@tailwindcss/cli": "4.2.1",
476
478
  "@types/react": "18.3.3",
@@ -487,7 +489,7 @@
487
489
  "typescript": "5.9.3",
488
490
  "vite": "8.2.2",
489
491
  "vitest": "4.1.11",
490
- "@fungi.computer/sigils": "0.0.1",
492
+ "@fungi.computer/sigils": "0.2.0",
491
493
  "postcss-value-parser": "4.2.0",
492
494
  "@tanstack/react-form": "1.33.2",
493
495
  "effect": "4.0.2",
@@ -496,7 +498,7 @@
496
498
  },
497
499
  "dependencies": {
498
500
  "@base-ui/react": "1.9.0",
499
- "@fungi.computer/stipe": "0.0.1",
501
+ "@fungi.computer/stipe": "0.1.0",
500
502
  "@radix-ui/react-slot": "1.3.0",
501
503
  "@tailwindcss/typography": "0.5.19",
502
504
  "@tanstack/highlight": "0.0.9",
@@ -0,0 +1 @@
1
+ @import "@fungi.computer/stipe/fonts-sans.css";
package/src/styles.css CHANGED
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  * Caps' single style entry. Stipe owns Daisy's theme engine and all shared
3
- * semantic values; Caps only asks Tailwind to scan its editable source.
3
+ * semantic values; Caps only asks Tailwind to scan its own components.
4
4
  * Caps owns reading rhythm through Tailwind Typography's `prose`.
5
5
  */
6
6
  @import "@fungi.computer/stipe/styles.css";
@@ -9,7 +9,13 @@
9
9
 
10
10
  @plugin "@tailwindcss/typography";
11
11
 
12
+ /* In the workspace Tailwind scans the component source; published, this
13
+ * entry ships without it, and the compiled components in dist carry the same
14
+ * class names. Tests ship in neither, so neither build styles them. */
12
15
  @source "./";
16
+ @source "../dist/**/*.js";
17
+ @source not "./**/*.test.tsx";
18
+ @source not "./test-utils";
13
19
 
14
20
  /* The themed brand fill stays untouched. Link ink and focus rings keep the
15
21
  primary's hue and chroma and only move lightness toward the ink side of
@@ -411,35 +417,6 @@
411
417
  font-family: var(--font-mono);
412
418
  line-height: 1;
413
419
  }
414
- .caps-led {
415
- display: inline-flex;
416
- align-items: center;
417
- gap: var(--spacing-xs);
418
- color: var(--marginalia-ink);
419
- font-size: 0.75rem;
420
- }
421
- .caps-led-bead {
422
- width: 0.625rem;
423
- height: 0.625rem;
424
- flex: none;
425
- border: var(--card-rule-width) solid var(--card-rule);
426
- border-radius: var(--radius-selector);
427
- background: var(--caps-led-color);
428
- box-shadow: 0 0 0 var(--card-rule-width)
429
- color-mix(in srgb, var(--caps-led-color) 36%, transparent);
430
- }
431
- .caps-led[data-tone="spore"] {
432
- --caps-led-color: var(--growth-spore);
433
- }
434
- .caps-led[data-tone="mycelium"] {
435
- --caps-led-color: var(--growth-mycelium);
436
- }
437
- .caps-led[data-tone="fruiting"] {
438
- --caps-led-color: var(--growth-fruiting);
439
- }
440
- .caps-led[data-active="false"] {
441
- --caps-led-color: var(--marginalia-ink);
442
- }
443
420
  .caps-cartridge-label {
444
421
  display: grid;
445
422
  justify-items: center;
@@ -5849,6 +5826,104 @@
5849
5826
  min-height: 44px;
5850
5827
  }
5851
5828
  }
5829
+ /* The path trail: flat text crumbs on the header face, no new colours.
5830
+ It keeps the title's size and the header's 32px height, and its icon
5831
+ sits where a plain title's icon sits, so switching views never shifts it. */
5832
+ .caps-window-header > .caps-window-icon {
5833
+ margin-inline: var(--spacing-md)
5834
+ calc(var(--spacing-sm) - var(--spacing-2xs));
5835
+ }
5836
+ .caps-window-crumbs {
5837
+ display: flex;
5838
+ align-items: center;
5839
+ min-width: 0;
5840
+ flex-shrink: 1;
5841
+ font-family: var(--caps-label-font, inherit);
5842
+ font-size: 11px;
5843
+ font-weight: 500;
5844
+ }
5845
+ .caps-window-crumb-item[data-caps-crumb-position="current"] {
5846
+ font-weight: 600;
5847
+ }
5848
+ .caps-window-crumb-item {
5849
+ display: flex;
5850
+ align-items: center;
5851
+ min-width: 0;
5852
+ }
5853
+ .caps-window-crumb-separator {
5854
+ padding-inline: var(--spacing-2xs);
5855
+ color: color-mix(in srgb, currentColor 50%, transparent);
5856
+ }
5857
+ .caps-window-crumb {
5858
+ min-width: 0;
5859
+ font-size: inherit;
5860
+ font-weight: inherit;
5861
+ gap: var(--spacing-2xs);
5862
+ }
5863
+ .caps-window-crumb-label {
5864
+ overflow: hidden;
5865
+ text-overflow: ellipsis;
5866
+ white-space: nowrap;
5867
+ }
5868
+ .caps-window-crumb-item[data-caps-crumb-position="current"]
5869
+ .caps-window-crumb {
5870
+ padding-inline: var(--spacing-xs);
5871
+ }
5872
+ /* The trail keeps its width beside the empty title handle (a zero basis),
5873
+ and gives way, crumb labels first, only when the row runs out of room. */
5874
+ .caps-window-crumbs:has(+ .caps-window-title) {
5875
+ max-width: 70%;
5876
+ }
5877
+ .caps-window-statuses {
5878
+ display: flex;
5879
+ align-items: center;
5880
+ gap: var(--spacing-sm);
5881
+ flex-shrink: 0;
5882
+ padding-inline-end: var(--spacing-sm);
5883
+ font-size: 11px;
5884
+ color: color-mix(in srgb, currentColor 65%, transparent);
5885
+ }
5886
+ .caps-window-commands {
5887
+ display: flex;
5888
+ align-items: center;
5889
+ gap: var(--spacing-2xs);
5890
+ flex-shrink: 0;
5891
+ }
5892
+ /* A phone keeps `‹ parent › current`; the rest of the trail is a tap away on Back. */
5893
+ @media (max-width: 640px) {
5894
+ /* Touch buttons cannot shrink below 44px. Reserve their path's width
5895
+ instead of shrinking its containers underneath neighbouring targets. */
5896
+ .caps-window-header:has(.caps-window-crumbs) {
5897
+ flex-wrap: wrap;
5898
+ }
5899
+ .caps-window-crumbs {
5900
+ min-width: min-content;
5901
+ flex-shrink: 0;
5902
+ }
5903
+ .caps-window-crumb-item {
5904
+ flex-shrink: 0;
5905
+ }
5906
+ /* State shrinks to its beads; the words stay for screen readers and a long press. */
5907
+ .caps-window-statuses {
5908
+ gap: var(--spacing-2xs);
5909
+ padding-inline-end: var(--spacing-2xs);
5910
+ }
5911
+ .caps-window-status > span {
5912
+ position: absolute;
5913
+ width: 1px;
5914
+ height: 1px;
5915
+ overflow: hidden;
5916
+ clip-path: inset(50%);
5917
+ white-space: nowrap;
5918
+ }
5919
+ .caps-window-crumb-item[data-caps-crumb-position="ancestor"] {
5920
+ display: none;
5921
+ }
5922
+ .caps-window-crumb-item[data-caps-crumb-position="parent"]
5923
+ .caps-window-crumb-separator {
5924
+ display: none;
5925
+ }
5926
+ }
5852
5927
  .caps-window-body {
5853
5928
  flex: 1;
5854
5929
  min-height: 0;
@@ -6260,6 +6335,8 @@
6260
6335
  display: block;
6261
6336
  flex: none;
6262
6337
  overflow: visible;
6338
+ /* Decorative material overflows the bead; only its status wrapper is a target. */
6339
+ pointer-events: none;
6263
6340
  transition: color var(--stipe-motion-normal) ease;
6264
6341
  }
6265
6342
 
@@ -7063,9 +7140,6 @@
7063
7140
  font-size: var(--text-xs);
7064
7141
  white-space: nowrap;
7065
7142
  }
7066
- .caps-status-compact .caps-status-bead {
7067
- transform: scale(0.6);
7068
- }
7069
7143
  .caps-action-row-trailing {
7070
7144
  display: flex;
7071
7145
  width: 6rem;