@fungi.computer/caps 0.3.1 → 0.5.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 +9 -6
- package/dist/blocks/composer.d.ts +18 -13
- package/dist/blocks/composer.js +10 -11
- package/dist/blocks/faq-chat.js +4 -4
- package/dist/blocks/status-tray.d.ts +8 -2
- package/dist/blocks/status-tray.js +26 -10
- package/dist/components/dropdown-menu.js +4 -4
- package/dist/components/field.d.ts +20 -0
- package/dist/components/field.js +8 -8
- package/dist/components/send-action.d.ts +9 -8
- package/dist/components/send-action.js +10 -8
- package/dist/form-fields.d.ts +3 -1
- package/dist/form-fields.js +25 -18
- package/dist/lib/format-money.d.ts +8 -2
- package/dist/lib/format-money.js +10 -7
- package/dist/schema-form.d.ts +13 -1
- package/dist/schema-form.js +46 -13
- package/dist/styles.css +353 -78
- package/docs/api/blocks/composer.md +10 -7
- package/docs/api/blocks/release-tag.md +7 -7
- package/docs/api/blocks/status-tray.md +11 -10
- package/docs/api/components/field.md +12 -9
- package/docs/api/components/send-action.md +25 -6
- package/docs/api/form.md +48 -8
- package/docs/api/lib/format-money.md +8 -6
- package/package.json +3 -3
- package/src/app-status-bar.css +21 -0
- package/src/styles.css +451 -114
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ export function NoteComposer() {
|
|
|
72
72
|
<Composer
|
|
73
73
|
input={
|
|
74
74
|
<input
|
|
75
|
-
className="input
|
|
75
|
+
className="input w-full"
|
|
76
76
|
value={draft}
|
|
77
77
|
onChange={(event) => setDraft(event.target.value)}
|
|
78
78
|
aria-label="Note"
|
|
@@ -663,9 +663,12 @@ compose, so color belongs in `variant` or an extra daisyUI class.
|
|
|
663
663
|
|
|
664
664
|
### Composer with an async submit
|
|
665
665
|
|
|
666
|
-
`Composer` is a layout block
|
|
667
|
-
|
|
668
|
-
|
|
666
|
+
`Composer` is a layout block: one surface with optional `attachments` on top,
|
|
667
|
+
your `input` typed straight onto the host region, and a bottom row of `toolbar`
|
|
668
|
+
controls with the `action` at its end. It draws no box of its own; the host
|
|
669
|
+
region is the field. Every slot is your control, and so are its labels, disabled
|
|
670
|
+
state, keyboard behaviour and effects. A composer's send key is `SendAction`,
|
|
671
|
+
which turns into a stop key while work runs.
|
|
669
672
|
|
|
670
673
|
```tsx
|
|
671
674
|
import { useState, type FormEvent } from "react";
|
|
@@ -697,7 +700,7 @@ export function PromptComposer({
|
|
|
697
700
|
<Composer
|
|
698
701
|
input={
|
|
699
702
|
<input
|
|
700
|
-
className="input
|
|
703
|
+
className="input w-full"
|
|
701
704
|
value={value}
|
|
702
705
|
onChange={(event) => setValue(event.target.value)}
|
|
703
706
|
aria-label="Prompt"
|
|
@@ -720,7 +723,7 @@ export function PromptComposer({
|
|
|
720
723
|
```
|
|
721
724
|
|
|
722
725
|
`submit` is your effect. It can call an API, enqueue work or do something local.
|
|
723
|
-
Caps only
|
|
726
|
+
Caps only lays out the controls.
|
|
724
727
|
|
|
725
728
|
### Window
|
|
726
729
|
|
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
import { type HTMLAttributes, type ReactNode } from "react";
|
|
2
2
|
/** Props accepted by Composer. */
|
|
3
3
|
export type ComposerProps = HTMLAttributes<HTMLDivElement> & {
|
|
4
|
-
/** The caller-owned
|
|
4
|
+
/** The caller-owned field, typed into directly: the surface is the field. */
|
|
5
5
|
input: ReactNode;
|
|
6
|
-
/** The caller-owned
|
|
6
|
+
/** The caller-owned key at the end of the bottom row, such as a SendAction. */
|
|
7
7
|
action?: ReactNode;
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
|
|
8
|
+
/** Caller-owned controls and readouts along the bottom edge, before the action. */
|
|
9
|
+
toolbar?: ReactNode;
|
|
10
|
+
/** Caller-owned items riding with the draft, shown above the text. */
|
|
11
|
+
attachments?: ReactNode;
|
|
12
12
|
};
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* One composer surface: attachments, then the text, then a slim row along the
|
|
15
|
+
* bottom edge holding the toolbar and, at its end, the action. It draws no box
|
|
16
|
+
* of its own: the host region is the field. Layout and slots only; it never
|
|
17
|
+
* assigns action-control semantics.
|
|
18
|
+
*/
|
|
14
19
|
export declare const Composer: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & {
|
|
15
|
-
/** The caller-owned
|
|
20
|
+
/** The caller-owned field, typed into directly: the surface is the field. */
|
|
16
21
|
input: ReactNode;
|
|
17
|
-
/** The caller-owned
|
|
22
|
+
/** The caller-owned key at the end of the bottom row, such as a SendAction. */
|
|
18
23
|
action?: ReactNode;
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
|
|
24
|
+
/** Caller-owned controls and readouts along the bottom edge, before the action. */
|
|
25
|
+
toolbar?: ReactNode;
|
|
26
|
+
/** Caller-owned items riding with the draft, shown above the text. */
|
|
27
|
+
attachments?: ReactNode;
|
|
23
28
|
} & import("react").RefAttributes<HTMLDivElement>>;
|
package/dist/blocks/composer.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef } from "react";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
.join(" "), children: [leading, input, action] }) }));
|
|
3
|
+
import { cn } from "../lib/cn.js";
|
|
4
|
+
/**
|
|
5
|
+
* One composer surface: attachments, then the text, then a slim row along the
|
|
6
|
+
* bottom edge holding the toolbar and, at its end, the action. It draws no box
|
|
7
|
+
* of its own: the host region is the field. Layout and slots only; it never
|
|
8
|
+
* assigns action-control semantics.
|
|
9
|
+
*/
|
|
10
|
+
export const Composer = forwardRef(function Composer({ action, attachments, className, input, toolbar, ...rootProps }, ref) {
|
|
11
|
+
return (_jsxs("div", { ...rootProps, ref: ref, className: cn("caps-composer", className), children: [attachments === undefined ? null : (_jsx("div", { className: "caps-composer-attachments", children: attachments })), _jsx("div", { className: "caps-composer-input", children: input }), toolbar === undefined && action === undefined ? null : (_jsxs("div", { className: "caps-composer-bar", children: [_jsx("div", { className: "caps-composer-toolbar", children: toolbar }), action] }))] }));
|
|
13
12
|
});
|
package/dist/blocks/faq-chat.js
CHANGED
|
@@ -2,8 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { forwardRef, useEffect, useId, useRef, useState, } from "react";
|
|
3
3
|
import { Button } from "../components/button.js";
|
|
4
4
|
import { Input } from "../components/input.js";
|
|
5
|
+
import { InputGroup } from "../components/input-group.js";
|
|
5
6
|
import { cn } from "../lib/cn.js";
|
|
6
|
-
import { Composer } from "./composer.js";
|
|
7
7
|
function words(text) {
|
|
8
8
|
return text.toLowerCase().match(/[a-z0-9$]+/g) ?? [];
|
|
9
9
|
}
|
|
@@ -71,7 +71,7 @@ export const FaqChat = forwardRef(function FaqChat({ avatar, className, entries,
|
|
|
71
71
|
}
|
|
72
72
|
return (_jsxs("section", { ...rootProps, "aria-labelledby": titleId, className: cn("caps-faq-chat", className), ref: ref, children: [_jsxs("header", { className: "caps-faq-chat-header", children: [avatar, _jsxs("div", { className: "caps-faq-chat-heading", children: [_jsx("p", { className: "caps-faq-chat-title", id: titleId, children: title }), note ? _jsx("p", { className: "caps-faq-chat-note", children: note }) : null] })] }), _jsxs("div", { "aria-live": "polite", className: "caps-faq-chat-log", ref: logRef, role: "log", children: [_jsx("div", { className: "caps-faq-chat-agent", children: greeting }), turns.map((turn) => turn.speaker === "user" ? (_jsx("p", { className: "caps-faq-chat-user", children: turn.text }, turn.key)) : (_jsxs("div", { className: "caps-faq-chat-agent", children: [turn.answer, turn.actions ? (_jsx("div", { className: "caps-faq-chat-actions", children: turn.actions })) : null] }, turn.key)))] }), _jsx("ul", { "aria-label": "Suggested questions", className: "caps-faq-chat-suggestions", children: entries.map((entry) => (_jsx("li", { children: _jsx(Button, { onClick: () => {
|
|
73
73
|
ask(entry.question, entry);
|
|
74
|
-
}, size: "xs", type: "button", variant: "outline", children: entry.question }) }, entry.id))) }), _jsx("form", { className: "caps-faq-chat-form", onSubmit: submit, children:
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
}, size: "xs", type: "button", variant: "outline", children: entry.question }) }, entry.id))) }), _jsx("form", { className: "caps-faq-chat-form", onSubmit: submit, children: _jsxs(InputGroup, { children: [_jsx(Input, { "aria-label": inputLabel, onChange: (event) => {
|
|
75
|
+
setDraft(event.target.value);
|
|
76
|
+
}, placeholder: placeholder, value: draft }), _jsx(Button, { type: "submit", children: submitLabel })] }) })] }));
|
|
77
77
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type HTMLAttributes, type ReactNode } from "react";
|
|
1
|
+
import { type ComponentPropsWithRef, type HTMLAttributes, type ReactNode } from "react";
|
|
2
2
|
import { type StatusGlyph, type StatusMotion, type StatusTone } from "../components/status.js";
|
|
3
3
|
/**
|
|
4
4
|
* One footer readout and the small tray it opens. Semantic data only: the
|
|
@@ -43,6 +43,12 @@ export type StatusTrayProps = Omit<HTMLAttributes<HTMLDivElement>, "title"> & {
|
|
|
43
43
|
leading?: ReactNode;
|
|
44
44
|
/** Trailing controls after the chips (Send, Expand, …). */
|
|
45
45
|
trailing?: ReactNode;
|
|
46
|
+
/**
|
|
47
|
+
* Compose a native button trigger, forwarding these attributes, children
|
|
48
|
+
* and ref. StatusTray still owns disclosure, dismissal and return focus.
|
|
49
|
+
* Omit to retain the default chip and its theme-owned finish.
|
|
50
|
+
*/
|
|
51
|
+
renderTrigger?: ((props: ComponentPropsWithRef<"button">) => ReactNode) | undefined;
|
|
46
52
|
/**
|
|
47
53
|
* Structural seat. `housing` sits on the plastic below a screen; `screen`
|
|
48
54
|
* sits inside a Display as flat, screen-native text (the wiki's screen
|
|
@@ -59,7 +65,7 @@ export type StatusTrayProps = Omit<HTMLAttributes<HTMLDivElement>, "title"> & {
|
|
|
59
65
|
hostInset?: number;
|
|
60
66
|
};
|
|
61
67
|
/** A footer row of readout chips; one opens its tray above the row at a time. */
|
|
62
|
-
export declare function StatusTray({ items, openId, onOpenChange, leading, trailing, placement, align, hostInset, className, ...props }: StatusTrayProps): import("react/jsx-runtime").JSX.Element;
|
|
68
|
+
export declare function StatusTray({ items, openId, onOpenChange, leading, trailing, renderTrigger, placement, align, hostInset, className, ...props }: StatusTrayProps): import("react/jsx-runtime").JSX.Element;
|
|
63
69
|
/** A group of tray rows. Precede it with a DataListHeading to label it. */
|
|
64
70
|
export declare function StatusTrayList({ children, className, ...props }: HTMLAttributes<HTMLDivElement>): import("react/jsx-runtime").JSX.Element;
|
|
65
71
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { X } from "lucide-react";
|
|
3
3
|
import { Fragment, useEffect, useId, useRef, } from "react";
|
|
4
4
|
import { IconButton } from "../components/icon-button.js";
|
|
@@ -6,7 +6,7 @@ import { Popover, PopoverAnchor } from "../components/popover.js";
|
|
|
6
6
|
import { Status, } from "../components/status.js";
|
|
7
7
|
import { cn } from "../lib/cn.js";
|
|
8
8
|
/** A footer row of readout chips; one opens its tray above the row at a time. */
|
|
9
|
-
export function StatusTray({ items, openId, onOpenChange, leading, trailing, placement = "housing", align = "end", hostInset, className, ...props }) {
|
|
9
|
+
export function StatusTray({ items, openId, onOpenChange, leading, trailing, renderTrigger, placement = "housing", align = "end", hostInset, className, ...props }) {
|
|
10
10
|
const scope = useId();
|
|
11
11
|
const root = useRef(null);
|
|
12
12
|
const panel = useRef(null);
|
|
@@ -50,14 +50,30 @@ export function StatusTray({ items, openId, onOpenChange, leading, trailing, pla
|
|
|
50
50
|
document.removeEventListener("pointerdown", onPointer);
|
|
51
51
|
};
|
|
52
52
|
}, [activeId, onOpenChange]);
|
|
53
|
-
return (_jsxs(PopoverAnchor, { ...props, ref: root, className: cn("caps-status-tray", className), "data-placement": placement, "data-align": align, children: [open ? (_jsxs(Popover, { open: true, ref: panel, tabIndex: -1, side: "top", id: `${scope}-${open.id}`, role: "dialog", "aria-labelledby": `${scope}-title`, className: "caps-status-tray-panel", "data-status-tray-panel": open.id, children: [_jsxs("div", { className: "caps-status-tray-head", children: [open.tone ? (_jsx(Status, { size: "compact", tone: open.tone, glyph: open.glyph ?? "dot" })) : null, _jsx("span", { id: `${scope}-title`, className: "caps-status-tray-title", children: open.title ?? open.label }), open.actions, _jsx(IconButton, { "aria-label": "Close tray", icon: _jsx(X, { "aria-hidden": "true" }), onClick: close })] }), _jsx("div", { className: "caps-status-tray-body", children: open.content })] })) : null, placement === "host" ? null : (_jsxs("div", { className: "caps-status-tray-row", children: [leading, _jsx("div", { className: "caps-status-tray-chips", children: items.map((item) =>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
return (_jsxs(PopoverAnchor, { ...props, ref: root, className: cn("caps-status-tray", className), "data-placement": placement, "data-align": align, children: [open ? (_jsxs(Popover, { open: true, ref: panel, tabIndex: -1, side: "top", id: `${scope}-${open.id}`, role: "dialog", "aria-labelledby": `${scope}-title`, className: "caps-status-tray-panel", "data-status-tray-panel": open.id, children: [_jsxs("div", { className: "caps-status-tray-head", children: [open.tone ? (_jsx(Status, { size: "compact", tone: open.tone, glyph: open.glyph ?? "dot" })) : null, _jsx("span", { id: `${scope}-title`, className: "caps-status-tray-title", children: open.title ?? open.label }), open.actions, _jsx(IconButton, { "aria-label": "Close tray", icon: _jsx(X, { "aria-hidden": "true" }), onClick: close })] }), _jsx("div", { className: "caps-status-tray-body", children: open.content })] })) : null, placement === "host" ? null : (_jsxs("div", { className: "caps-status-tray-row", children: [leading, _jsx("div", { className: "caps-status-tray-chips", children: items.map((item) => {
|
|
54
|
+
const trigger = {
|
|
55
|
+
ref: (node) => {
|
|
56
|
+
if (node)
|
|
57
|
+
triggers.current.set(item.id, node);
|
|
58
|
+
else
|
|
59
|
+
triggers.current.delete(item.id);
|
|
60
|
+
},
|
|
61
|
+
type: "button",
|
|
62
|
+
className: renderTrigger ? undefined : "caps-status-tray-chip",
|
|
63
|
+
"aria-expanded": activeId === item.id,
|
|
64
|
+
"aria-controls": activeId === item.id ? `${scope}-${item.id}` : undefined,
|
|
65
|
+
"aria-label": item.accessibleLabel,
|
|
66
|
+
disabled: item.disabled,
|
|
67
|
+
title: item.disabled ? item.accessibleLabel : undefined,
|
|
68
|
+
"aria-haspopup": "dialog",
|
|
69
|
+
"data-status-tray-chip": item.id,
|
|
70
|
+
onClick: () => {
|
|
71
|
+
onOpenChange(openId === item.id ? null : item.id);
|
|
72
|
+
},
|
|
73
|
+
children: (_jsxs(_Fragment, { children: [item.tone ? (_jsx(Status, { size: "compact", glyph: item.glyph ?? "dot", motion: item.motion ?? "none", tone: item.tone })) : null, _jsx("span", { className: "caps-status-tray-label", children: item.label }), item.count !== undefined && item.count > 0 ? (_jsx("span", { className: "caps-status-tray-count", children: item.count })) : null] })),
|
|
74
|
+
};
|
|
75
|
+
return (_jsxs(Fragment, { children: [renderTrigger ? (renderTrigger(trigger)) : (_jsx("button", { ...trigger })), item.after] }, item.id));
|
|
76
|
+
}) }), trailing] }))] }));
|
|
61
77
|
}
|
|
62
78
|
/** A group of tray rows. Precede it with a DataListHeading to label it. */
|
|
63
79
|
export function StatusTrayList({ children, className, ...props }) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Menu as BaseMenu } from "@base-ui/react/menu";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { createContext, forwardRef, useContext, useCallback, useImperativeHandle, useRef, } from "react";
|
|
@@ -79,12 +79,12 @@ export const DropdownMenuItem = forwardRef(function DropdownMenuItem({ className
|
|
|
79
79
|
}, ref: ref }));
|
|
80
80
|
});
|
|
81
81
|
/** A checkable item in a DropdownMenu. */
|
|
82
|
-
export const DropdownMenuCheckboxItem = forwardRef(function DropdownMenuCheckboxItem({ className, onClick, onSelect, ...props }, ref) {
|
|
83
|
-
return (
|
|
82
|
+
export const DropdownMenuCheckboxItem = forwardRef(function DropdownMenuCheckboxItem({ children, className, onClick, onSelect, ...props }, ref) {
|
|
83
|
+
return (_jsxs(BaseMenu.CheckboxItem, { ...props, className: (state) => cn(itemClasses, resolveClassName(className, state)), onClick: (event) => {
|
|
84
84
|
onClick?.(event);
|
|
85
85
|
if (!event.defaultPrevented)
|
|
86
86
|
onSelect?.(event);
|
|
87
|
-
}, ref: ref }));
|
|
87
|
+
}, ref: ref, children: [_jsx("span", { className: "min-w-0 flex-1", children: children }), _jsx(BaseMenu.CheckboxItemIndicator, { "aria-hidden": "true", className: "shrink-0", children: "\u2713" })] }));
|
|
88
88
|
});
|
|
89
89
|
/** A visual separator in a DropdownMenu. */
|
|
90
90
|
export const DropdownMenuSeparator = forwardRef(function DropdownMenuSeparator({ className, ...props }, ref) {
|
|
@@ -21,6 +21,8 @@ export declare function FormFooter({ children, divider, error, layout, status, .
|
|
|
21
21
|
export type FormProps = {
|
|
22
22
|
/** Disables every control while a submission is in flight. */
|
|
23
23
|
busy?: boolean;
|
|
24
|
+
/** Opt-in label/control grid; the default field rhythm is unchanged. */
|
|
25
|
+
layout?: "columns" | undefined;
|
|
24
26
|
} & FormHTMLAttributes<HTMLFormElement>;
|
|
25
27
|
/**
|
|
26
28
|
* A form with Caps' field rhythm. While `busy`, its fieldset disables every
|
|
@@ -29,11 +31,15 @@ export type FormProps = {
|
|
|
29
31
|
export declare const Form: import("react").ForwardRefExoticComponent<{
|
|
30
32
|
/** Disables every control while a submission is in flight. */
|
|
31
33
|
busy?: boolean;
|
|
34
|
+
/** Opt-in label/control grid; the default field rhythm is unchanged. */
|
|
35
|
+
layout?: "columns" | undefined;
|
|
32
36
|
} & FormHTMLAttributes<HTMLFormElement> & import("react").RefAttributes<HTMLFormElement>>;
|
|
33
37
|
/** Props accepted by FormSection. */
|
|
34
38
|
export type FormSectionProps = {
|
|
35
39
|
title: ReactNode;
|
|
36
40
|
description?: ReactNode;
|
|
41
|
+
/** Opt-in label/control grid when used without a Form parent. */
|
|
42
|
+
layout?: "columns" | undefined;
|
|
37
43
|
/** Heading level inside the page outline. */
|
|
38
44
|
level?: 2 | 3 | 4;
|
|
39
45
|
children: ReactNode;
|
|
@@ -42,6 +48,8 @@ export type FormSectionProps = {
|
|
|
42
48
|
export declare const FormSection: import("react").ForwardRefExoticComponent<{
|
|
43
49
|
title: ReactNode;
|
|
44
50
|
description?: ReactNode;
|
|
51
|
+
/** Opt-in label/control grid when used without a Form parent. */
|
|
52
|
+
layout?: "columns" | undefined;
|
|
45
53
|
/** Heading level inside the page outline. */
|
|
46
54
|
level?: 2 | 3 | 4;
|
|
47
55
|
children: ReactNode;
|
|
@@ -49,7 +57,11 @@ export declare const FormSection: import("react").ForwardRefExoticComponent<{
|
|
|
49
57
|
/** Props accepted by FieldSet. */
|
|
50
58
|
export type FieldSetProps = {
|
|
51
59
|
legend: ReactNode;
|
|
60
|
+
/** Full legend on hover when a columns form clips it to one line. */
|
|
61
|
+
legendTitle?: string;
|
|
52
62
|
description?: ReactNode;
|
|
63
|
+
/** Opt-in label/control grid when used without a Form parent. */
|
|
64
|
+
layout?: "columns" | undefined;
|
|
53
65
|
/** The group's announced error, such as "Choose at least one right". */
|
|
54
66
|
error?: ReactNode;
|
|
55
67
|
children: ReactNode;
|
|
@@ -60,7 +72,11 @@ export type FieldSetProps = {
|
|
|
60
72
|
*/
|
|
61
73
|
export declare const FieldSet: import("react").ForwardRefExoticComponent<{
|
|
62
74
|
legend: ReactNode;
|
|
75
|
+
/** Full legend on hover when a columns form clips it to one line. */
|
|
76
|
+
legendTitle?: string;
|
|
63
77
|
description?: ReactNode;
|
|
78
|
+
/** Opt-in label/control grid when used without a Form parent. */
|
|
79
|
+
layout?: "columns" | undefined;
|
|
64
80
|
/** The group's announced error, such as "Choose at least one right". */
|
|
65
81
|
error?: ReactNode;
|
|
66
82
|
children: ReactNode;
|
|
@@ -76,6 +92,8 @@ export type FieldProps = {
|
|
|
76
92
|
*/
|
|
77
93
|
icon?: ReactNode;
|
|
78
94
|
label?: ReactNode;
|
|
95
|
+
/** Full label on hover when a columns form clips it to one line. */
|
|
96
|
+
labelTitle?: string;
|
|
79
97
|
/**
|
|
80
98
|
* `vertical` stacks label, control and error; `horizontal` puts the control
|
|
81
99
|
* opposite its copy (settings rows); `inline` puts a checkbox or switch
|
|
@@ -102,6 +120,8 @@ export declare const Field: import("react").ForwardRefExoticComponent<{
|
|
|
102
120
|
*/
|
|
103
121
|
icon?: ReactNode;
|
|
104
122
|
label?: ReactNode;
|
|
123
|
+
/** Full label on hover when a columns form clips it to one line. */
|
|
124
|
+
labelTitle?: string;
|
|
105
125
|
/**
|
|
106
126
|
* `vertical` stacks label, control and error; `horizontal` puts the control
|
|
107
127
|
* opposite its copy (settings rows); `inline` puts a checkbox or switch
|
package/dist/components/field.js
CHANGED
|
@@ -25,21 +25,21 @@ export function FormFooter({ children, divider = true, error, layout = "end", st
|
|
|
25
25
|
* A form with Caps' field rhythm. While `busy`, its fieldset disables every
|
|
26
26
|
* control and the form reports `aria-busy`; validation stays with the caller.
|
|
27
27
|
*/
|
|
28
|
-
export const Form = forwardRef(function Form({ busy = false, children, className, ...props }, ref) {
|
|
29
|
-
return (_jsx("form", { ...props, "aria-busy": busy || undefined, className: cn("caps-form", className), ref: ref, children: _jsx("fieldset", { className: "caps-form-body", disabled: busy, children: children }) }));
|
|
28
|
+
export const Form = forwardRef(function Form({ busy = false, children, className, layout, ...props }, ref) {
|
|
29
|
+
return (_jsx("form", { ...props, "aria-busy": busy || undefined, className: cn("caps-form", className), "data-layout": layout, ref: ref, children: _jsx("fieldset", { className: "caps-form-body", disabled: busy, children: children }) }));
|
|
30
30
|
});
|
|
31
31
|
const sectionHeadings = { 2: "h2", 3: "h3", 4: "h4" };
|
|
32
32
|
/** A titled group of fields; consecutive sections share one quiet rule. */
|
|
33
|
-
export const FormSection = forwardRef(function FormSection({ children, className, description, level = 3, title, ...props }, ref) {
|
|
33
|
+
export const FormSection = forwardRef(function FormSection({ children, className, description, layout, level = 3, title, ...props }, ref) {
|
|
34
34
|
const id = useId();
|
|
35
35
|
const Heading = sectionHeadings[level];
|
|
36
|
-
return (_jsxs("section", { ...props, "aria-labelledby": `${id}-title`, className: cn("caps-form-section", className), ref: ref, children: [_jsxs("header", { className: "caps-form-section-copy", children: [_jsx(Heading, { className: "caps-form-section-title", id: `${id}-title`, children: title }), description ? (_jsx("p", { className: "caps-form-section-description", children: description })) : null] }), _jsx("div", { className: "caps-form-section-fields", children: children })] }));
|
|
36
|
+
return (_jsxs("section", { ...props, "aria-labelledby": `${id}-title`, className: cn("caps-form-section", className), "data-layout": layout, ref: ref, children: [_jsxs("header", { className: "caps-form-section-copy", children: [_jsx(Heading, { className: "caps-form-section-title", id: `${id}-title`, children: title }), description ? (_jsx("p", { className: "caps-form-section-description", children: description })) : null] }), _jsx("div", { className: "caps-form-section-fields", children: children })] }));
|
|
37
37
|
});
|
|
38
38
|
/**
|
|
39
39
|
* A legend-named group of related choices, such as a list of inline
|
|
40
40
|
* checkbox Fields. `disabled` disables every control inside.
|
|
41
41
|
*/
|
|
42
|
-
export const FieldSet = forwardRef(function FieldSet({ children, className, description, error, legend, ...props }, ref) {
|
|
42
|
+
export const FieldSet = forwardRef(function FieldSet({ children, className, description, error, layout, legend, legendTitle, ...props }, ref) {
|
|
43
43
|
const id = useId();
|
|
44
44
|
const describedBy = [
|
|
45
45
|
description ? `${id}-description` : props["aria-describedby"],
|
|
@@ -47,7 +47,7 @@ export const FieldSet = forwardRef(function FieldSet({ children, className, desc
|
|
|
47
47
|
]
|
|
48
48
|
.filter(Boolean)
|
|
49
49
|
.join(" ") || undefined;
|
|
50
|
-
return (_jsxs("fieldset", { ...props, "aria-describedby": describedBy, "aria-invalid": error ? true : props["aria-invalid"], className: cn("caps-fieldset", className), ref: ref, children: [_jsx("legend", { className: "caps-fieldset-legend", children: legend }), description ? (_jsx("p", { className: "caps-fieldset-description", id: `${id}-description`, children: description })) : null, _jsx("div", { className: "caps-fieldset-fields", children: children }), error === undefined ? null : (_jsx(FieldError, { id: `${id}-error`, children: error }))] }));
|
|
50
|
+
return (_jsxs("fieldset", { ...props, "aria-describedby": describedBy, "aria-invalid": error ? true : props["aria-invalid"], className: cn("caps-fieldset", className), "data-layout": layout, ref: ref, children: [_jsx("legend", { className: "caps-fieldset-legend", title: legendTitle, children: legend }), description ? (_jsx("p", { className: "caps-fieldset-description", id: `${id}-description`, children: description })) : null, _jsx("div", { className: "caps-fieldset-fields", children: children }), error === undefined ? null : (_jsx(FieldError, { id: `${id}-error`, children: error }))] }));
|
|
51
51
|
});
|
|
52
52
|
/** The labelled control inside an InputGroup: its first non-addon element. */
|
|
53
53
|
function groupControl(group) {
|
|
@@ -60,7 +60,7 @@ function groupControl(group) {
|
|
|
60
60
|
return controls[0] ?? null;
|
|
61
61
|
}
|
|
62
62
|
/** A labelled form-field layout that renders caller-provided control and supporting content. */
|
|
63
|
-
export const Field = forwardRef(function Field({ children, className, description, error, icon, label, optional, orientation = "vertical", readout = false, required, ...props }, ref) {
|
|
63
|
+
export const Field = forwardRef(function Field({ children, className, description, error, icon, label, labelTitle, optional, orientation = "vertical", readout = false, required, ...props }, ref) {
|
|
64
64
|
const generatedId = useId();
|
|
65
65
|
const child = isValidElement(children) ? children : null;
|
|
66
66
|
// An InputGroup is layout: the labelled control is the input inside it.
|
|
@@ -97,7 +97,7 @@ export const Field = forwardRef(function Field({ children, className, descriptio
|
|
|
97
97
|
: part))
|
|
98
98
|
: cloneElement(child, clonedProps)
|
|
99
99
|
: children;
|
|
100
|
-
const labelContent = label ? (_jsx(Label, { htmlFor: readout ? undefined : controlId,
|
|
100
|
+
const labelContent = label ? (_jsx(Label, { htmlFor: readout ? undefined : controlId, title: labelTitle,
|
|
101
101
|
// A choice's own label reads as body text; the group's legend carries weight.
|
|
102
102
|
className: orientation === "inline" ? "font-normal" : undefined, ...(required === undefined ? {} : { required }), ...(optional === undefined ? {} : { optional }), children: label })) : null;
|
|
103
103
|
const copy = label || description ? (_jsxs("div", { className: "caps-field-copy", children: [labelContent, description ? (_jsx("p", { className: "caps-field-description", id: descriptionId, children: description })) : null] })) : null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type KeycapButtonProps } from "./keycap.js";
|
|
2
2
|
/** Where a composer's send action is: nothing to send, ready, or working. */
|
|
3
3
|
export type SendActionState = "empty" | "ready" | "busy";
|
|
4
4
|
/** Props accepted by SendAction. */
|
|
@@ -12,13 +12,14 @@ export type SendActionProps = {
|
|
|
12
12
|
stopLabel?: string;
|
|
13
13
|
/** Glyph size in pixels. */
|
|
14
14
|
iconSize?: number;
|
|
15
|
-
} & Omit<
|
|
15
|
+
} & Omit<KeycapButtonProps, "label" | "icon" | "readout" | "shortcut" | "pressed" | "expanded" | "aria-label">;
|
|
16
16
|
/**
|
|
17
|
-
* A composer's send key. Empty, it rests disabled;
|
|
18
|
-
* wakes with a small pop; sending, its arrow
|
|
19
|
-
* indicator (or, with `onStop`, a stop square)
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* A composer's one send key, on the Caps keycap. Empty, it rests disabled;
|
|
18
|
+
* given something to send it wakes with a small pop; sending, its arrow
|
|
19
|
+
* launches away and a working indicator (or, with `onStop`, a stop square)
|
|
20
|
+
* takes its place in the same key; when the work ends the arrow drops back
|
|
21
|
+
* in. It never grows a second button. All timing comes from the theme's
|
|
22
|
+
* motion tokens, and reduced motion keeps short fades only.
|
|
22
23
|
*/
|
|
23
24
|
export declare const SendAction: import("react").ForwardRefExoticComponent<{
|
|
24
25
|
state: SendActionState;
|
|
@@ -30,4 +31,4 @@ export declare const SendAction: import("react").ForwardRefExoticComponent<{
|
|
|
30
31
|
stopLabel?: string;
|
|
31
32
|
/** Glyph size in pixels. */
|
|
32
33
|
iconSize?: number;
|
|
33
|
-
} & Omit<
|
|
34
|
+
} & Omit<KeycapButtonProps, "aria-label" | "label" | "icon" | "readout" | "expanded" | "pressed" | "shortcut"> & import("react").RefAttributes<HTMLButtonElement>>;
|
|
@@ -2,8 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { ArrowUp, Square } from "lucide-react";
|
|
3
3
|
import { forwardRef, useImperativeHandle, useLayoutEffect, useRef, } from "react";
|
|
4
4
|
import { canAnimate, motionEasing, readMotion, } from "../lib/motion.js";
|
|
5
|
-
import {
|
|
5
|
+
import { KeycapButton } from "./keycap.js";
|
|
6
6
|
import { Loader } from "./loader.js";
|
|
7
|
+
import { cn } from "../lib/cn.js";
|
|
7
8
|
function lift(face, motion) {
|
|
8
9
|
// The arrow launches upward and away: the prompt has gone.
|
|
9
10
|
return face.animate(motion.reduced
|
|
@@ -57,13 +58,14 @@ function charge(button, motion) {
|
|
|
57
58
|
});
|
|
58
59
|
}
|
|
59
60
|
/**
|
|
60
|
-
* A composer's send key. Empty, it rests disabled;
|
|
61
|
-
* wakes with a small pop; sending, its arrow
|
|
62
|
-
* indicator (or, with `onStop`, a stop square)
|
|
63
|
-
*
|
|
64
|
-
*
|
|
61
|
+
* A composer's one send key, on the Caps keycap. Empty, it rests disabled;
|
|
62
|
+
* given something to send it wakes with a small pop; sending, its arrow
|
|
63
|
+
* launches away and a working indicator (or, with `onStop`, a stop square)
|
|
64
|
+
* takes its place in the same key; when the work ends the arrow drops back
|
|
65
|
+
* in. It never grows a second button. All timing comes from the theme's
|
|
66
|
+
* motion tokens, and reduced motion keeps short fades only.
|
|
65
67
|
*/
|
|
66
|
-
export const SendAction = forwardRef(function SendAction({ state, onStop, stopLabel = "Stop", iconSize = 20, onClick, disabled, "aria-label": label, ...props }, ref) {
|
|
68
|
+
export const SendAction = forwardRef(function SendAction({ state, onStop, stopLabel = "Stop", iconSize = 20, onClick, disabled, className, tone = "primary", "aria-label": label, ...props }, ref) {
|
|
67
69
|
const node = useRef(null);
|
|
68
70
|
useImperativeHandle(ref, () => node.current);
|
|
69
71
|
const previous = useRef(state);
|
|
@@ -92,5 +94,5 @@ export const SendAction = forwardRef(function SendAction({ state, onStop, stopLa
|
|
|
92
94
|
}, [state]);
|
|
93
95
|
const busy = state === "busy";
|
|
94
96
|
const stoppable = busy && onStop !== undefined;
|
|
95
|
-
return (_jsx(
|
|
97
|
+
return (_jsx(KeycapButton, { ...props, ref: node, className: cn("caps-send-action", className), tone: tone, "aria-busy": busy ? true : undefined, "aria-label": stoppable ? stopLabel : label, "data-send-state": state, disabled: disabled === true || state === "empty" || (busy && !stoppable), onClick: stoppable ? onStop : onClick, label: null, icon: _jsxs("span", { className: "caps-send-glyph", "data-state": state, children: [_jsx("span", { "data-glyph": "arrow", children: _jsx(ArrowUp, { "aria-hidden": "true", size: iconSize }) }), _jsx("span", { "data-glyph": "busy", children: onStop === undefined ? (_jsx(Loader, { decorative: true, size: "sm" })) : (_jsx(Square, { "aria-hidden": "true", fill: "currentColor", size: Math.round(iconSize * 0.7) })) })] }) }));
|
|
96
98
|
});
|
package/dist/form-fields.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { SchemaFormModel } from "./schema-form.js";
|
|
|
3
3
|
/** Props accepted by FormFields. */
|
|
4
4
|
export type FormFieldsProps<V extends FormValues> = {
|
|
5
5
|
model: SchemaFormModel<V>;
|
|
6
|
+
/** Opt-in label/control grid for generated sections and groups. */
|
|
7
|
+
layout?: "columns" | undefined;
|
|
6
8
|
/** Render only these fields, in this order, without section headings. */
|
|
7
9
|
names?: readonly (keyof V & string)[];
|
|
8
10
|
/** Disable every field, such as while an uncertain request is retried. */
|
|
@@ -12,4 +14,4 @@ export type FormFieldsProps<V extends FormValues> = {
|
|
|
12
14
|
* Render a schema form's fields from its schema: every section in order, or
|
|
13
15
|
* only `names` (for placing fields in Screen cells or custom layouts).
|
|
14
16
|
*/
|
|
15
|
-
export declare function FormFields<V extends FormValues>({ model, names, disabled, }: FormFieldsProps<V>): (import("react/jsx-runtime").JSX.Element | null)[];
|
|
17
|
+
export declare function FormFields<V extends FormValues>({ model, names, disabled, layout, }: FormFieldsProps<V>): (import("react/jsx-runtime").JSX.Element | null)[];
|
package/dist/form-fields.js
CHANGED
|
@@ -159,7 +159,7 @@ const groupKinds = new Set([
|
|
|
159
159
|
"icon-checkboxes",
|
|
160
160
|
]);
|
|
161
161
|
/** Choice groups: a legend names the group; each option names its input. */
|
|
162
|
-
function SpecGroup({ spec, field, locked, error, }) {
|
|
162
|
+
function SpecGroup({ spec, field, locked, error, layout, }) {
|
|
163
163
|
const disabled = locked || spec.readOnly === true;
|
|
164
164
|
const legend = spec.label;
|
|
165
165
|
if (spec.kind === "checkboxes" || spec.kind === "icon-checkboxes") {
|
|
@@ -169,7 +169,7 @@ function SpecGroup({ spec, field, locked, error, }) {
|
|
|
169
169
|
? [...selected, value]
|
|
170
170
|
: selected.filter((current) => current !== value));
|
|
171
171
|
};
|
|
172
|
-
return (_jsx(FieldSet, { legend: legend, description: spec.help, disabled: disabled, error: error ?? null, children: spec.kind === "icon-checkboxes" ? (_jsx("div", { className: "caps-field-icon-choices", children: spec.options.map((option) => (_jsx(IconCheckbox, { name: spec.name, value: option.value, label: option.label, ...(option.caption === undefined
|
|
172
|
+
return (_jsx(FieldSet, { legend: legend, layout: layout, ...(layout === "columns" ? { legendTitle: legend } : {}), description: spec.help, disabled: disabled, error: error ?? null, children: spec.kind === "icon-checkboxes" ? (_jsx("div", { className: "caps-field-icon-choices", children: spec.options.map((option) => (_jsx(IconCheckbox, { name: spec.name, value: option.value, label: option.label, ...(option.caption === undefined
|
|
173
173
|
? {}
|
|
174
174
|
: { caption: option.caption }), icon: option.icon, disabled: option.disabled, checked: selected.includes(option.value), onBlur: field.handleBlur, onChange: (event) => {
|
|
175
175
|
toggle(option.value, event.currentTarget.checked);
|
|
@@ -178,7 +178,7 @@ function SpecGroup({ spec, field, locked, error, }) {
|
|
|
178
178
|
} }) }, option.value)))) }));
|
|
179
179
|
}
|
|
180
180
|
const cards = spec.options.some((option) => option.description !== undefined || option.meta !== undefined);
|
|
181
|
-
return (_jsx(FieldSet, { legend: legend, description: spec.help, disabled: disabled, error: error ?? null, children: _jsx(RadioGroup, { name: spec.name, "aria-label": legend, value: text(field), onValueChange: (next) => {
|
|
181
|
+
return (_jsx(FieldSet, { legend: legend, layout: layout, ...(layout === "columns" ? { legendTitle: legend } : {}), description: spec.help, disabled: disabled, error: error ?? null, children: _jsx(RadioGroup, { name: spec.name, "aria-label": legend, value: text(field), onValueChange: (next) => {
|
|
182
182
|
field.handleChange(next);
|
|
183
183
|
field.handleBlur();
|
|
184
184
|
},
|
|
@@ -206,45 +206,52 @@ function RenderedControl({ spec, field, locked, error, render, ...control }) {
|
|
|
206
206
|
"aria-invalid": error !== undefined,
|
|
207
207
|
});
|
|
208
208
|
}
|
|
209
|
-
function SchemaField({ form, spec, locked, }) {
|
|
209
|
+
function SchemaField({ form, spec, locked, layout, }) {
|
|
210
210
|
const field = useField({ form, name: spec.name });
|
|
211
211
|
const submitted = useSelector(form.store, (state) => state.submissionAttempts > 0);
|
|
212
212
|
const error = field.state.meta.isBlurred || submitted
|
|
213
213
|
? fieldError(field.state.meta)
|
|
214
214
|
: undefined;
|
|
215
215
|
if (spec.render === undefined && groupKinds.has(spec.kind))
|
|
216
|
-
return (_jsx(SpecGroup, { spec: spec, field: field, locked: locked, error: error }));
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
216
|
+
return (_jsx(SpecGroup, { spec: spec, field: field, locked: locked, error: error, layout: layout }));
|
|
217
|
+
// A columns form chooses one rhythm; per-field layout hints only govern
|
|
218
|
+
// the unchanged default presentation.
|
|
219
|
+
const orientation = layout === "columns"
|
|
220
|
+
? spec.kind === "switch" || spec.kind === "checkbox"
|
|
220
221
|
? "horizontal"
|
|
221
|
-
: "vertical"
|
|
222
|
-
|
|
222
|
+
: "vertical"
|
|
223
|
+
: spec.kind === "checkbox"
|
|
224
|
+
? "inline"
|
|
225
|
+
: (spec.layout ?? (spec.kind === "switch" ? "row" : "stacked")) ===
|
|
226
|
+
"row"
|
|
227
|
+
? "horizontal"
|
|
228
|
+
: "vertical";
|
|
229
|
+
return (_jsx(Field, { label: spec.label, ...(layout === "columns" ? { labelTitle: spec.label } : {}), description: spec.help, icon: spec.icon, orientation: orientation, error: error, ...(spec.required === undefined ? {} : { required: spec.required }), ...(spec.optional === undefined ? {} : { optional: spec.optional }), children: spec.render === undefined ? (_jsx(SpecControl, { spec: spec, field: field, locked: locked })) : (_jsx(RenderedControl, { spec: spec, field: field, locked: locked, error: error, render: spec.render })) }));
|
|
223
230
|
}
|
|
224
231
|
const readValues = Schema.decodeUnknownOption(Schema.Record(Schema.String, inputSchema));
|
|
225
|
-
function Visible({ form, spec, locked, }) {
|
|
232
|
+
function Visible({ form, spec, locked, layout, }) {
|
|
226
233
|
const when = spec.when;
|
|
227
234
|
const shown = useSelector(form.store, (state) => when === undefined
|
|
228
235
|
? true
|
|
229
236
|
: when(Option.getOrElse(readValues(state.values), () => ({}))));
|
|
230
237
|
if (!shown)
|
|
231
238
|
return null;
|
|
232
|
-
return _jsx(SchemaField, { form: form, spec: spec, locked: locked });
|
|
239
|
+
return (_jsx(SchemaField, { form: form, spec: spec, locked: locked, layout: layout }));
|
|
233
240
|
}
|
|
234
|
-
function SectionFields({ form, section, locked, }) {
|
|
235
|
-
const fields = section.fields.map((spec) => (_jsx(Visible, { form: form, spec: spec, locked: locked }, spec.name)));
|
|
236
|
-
return section.title === undefined ? (fields) : (_jsx(FormSection, { title: section.title, description: section.description, children: fields }));
|
|
241
|
+
function SectionFields({ form, section, locked, layout, }) {
|
|
242
|
+
const fields = section.fields.map((spec) => (_jsx(Visible, { form: form, spec: spec, locked: locked, layout: layout }, spec.name)));
|
|
243
|
+
return section.title === undefined ? (fields) : (_jsx(FormSection, { title: section.title, description: section.description, layout: layout, children: fields }));
|
|
237
244
|
}
|
|
238
245
|
/**
|
|
239
246
|
* Render a schema form's fields from its schema: every section in order, or
|
|
240
247
|
* only `names` (for placing fields in Screen cells or custom layouts).
|
|
241
248
|
*/
|
|
242
|
-
export function FormFields({ model, names, disabled = false, }) {
|
|
249
|
+
export function FormFields({ model, names, disabled = false, layout, }) {
|
|
243
250
|
const form = model.form;
|
|
244
251
|
if (names !== undefined)
|
|
245
252
|
return names.map((name) => {
|
|
246
253
|
const spec = model.description.fields.find((field) => field.name === name);
|
|
247
|
-
return spec === undefined ? null : (_jsx(Visible, { form: form, spec: spec, locked: disabled }, name));
|
|
254
|
+
return spec === undefined ? null : (_jsx(Visible, { form: form, spec: spec, locked: disabled, layout: layout }, name));
|
|
248
255
|
});
|
|
249
|
-
return model.description.sections.map((section) => (_jsx(SectionFields, { form: form, section: section, locked: disabled }, section.id ?? "")));
|
|
256
|
+
return model.description.sections.map((section) => (_jsx(SectionFields, { form: form, section: section, locked: disabled, layout: layout }, section.id ?? "")));
|
|
250
257
|
}
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Format USD to cents, marking non-zero sub-cent amounts rather than rounding
|
|
3
|
+
* them to zero. Callers needing the supported five-decimal precision opt in
|
|
4
|
+
* with `precision: "exact"`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function formatMoney(amount: number, options?: Readonly<{
|
|
7
|
+
precision: "exact";
|
|
8
|
+
}>): string;
|
package/dist/lib/format-money.js
CHANGED
|
@@ -4,19 +4,22 @@ const regularUsd = new Intl.NumberFormat("en-US", {
|
|
|
4
4
|
minimumFractionDigits: 2,
|
|
5
5
|
maximumFractionDigits: 2,
|
|
6
6
|
});
|
|
7
|
-
const
|
|
7
|
+
const exactUsd = new Intl.NumberFormat("en-US", {
|
|
8
8
|
style: "currency",
|
|
9
9
|
currency: "USD",
|
|
10
10
|
minimumFractionDigits: 2,
|
|
11
11
|
maximumFractionDigits: 5,
|
|
12
12
|
});
|
|
13
|
-
/**
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Format USD to cents, marking non-zero sub-cent amounts rather than rounding
|
|
15
|
+
* them to zero. Callers needing the supported five-decimal precision opt in
|
|
16
|
+
* with `precision: "exact"`.
|
|
17
|
+
*/
|
|
18
|
+
export function formatMoney(amount, options) {
|
|
15
19
|
if (!Number.isFinite(amount))
|
|
16
20
|
throw new RangeError("Money amount must be finite");
|
|
17
|
-
|
|
21
|
+
const subcent = amount !== 0 && Math.abs(amount) < 0.01;
|
|
22
|
+
if (subcent && (options?.precision !== "exact" || Math.abs(amount) < 0.00001))
|
|
18
23
|
return amount < 0 ? ">-$0.01" : "<$0.01";
|
|
19
|
-
return
|
|
20
|
-
? subcentUsd.format(amount)
|
|
21
|
-
: regularUsd.format(amount);
|
|
24
|
+
return subcent ? exactUsd.format(amount) : regularUsd.format(amount);
|
|
22
25
|
}
|