@nibapp/ui 0.0.0-stage → 0.1.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/CHANGELOG.md +35 -0
- package/LICENSE +21 -0
- package/README.md +124 -2
- package/TRADEMARKS.md +22 -0
- package/dist/_bundled/money/index.d.ts +98 -0
- package/dist/_bundled/money/index.js +152 -0
- package/dist/_bundled/tokens/index.d.ts +2 -0
- package/dist/_bundled/tokens/index.js +2 -0
- package/dist/_bundled/tokens/palette.gen.d.ts +51 -0
- package/dist/_bundled/tokens/palette.gen.js +206 -0
- package/dist/_bundled/tokens/spec.d.ts +291 -0
- package/dist/_bundled/tokens/spec.js +221 -0
- package/dist/components/accordion.d.ts +27 -0
- package/dist/components/accordion.js +66 -0
- package/dist/components/alert-dialog.d.ts +59 -0
- package/dist/components/alert-dialog.js +82 -0
- package/dist/components/alert.d.ts +141 -0
- package/dist/components/alert.js +55 -0
- package/dist/components/appear.d.ts +21 -0
- package/dist/components/appear.js +42 -0
- package/dist/components/avatar.d.ts +196 -0
- package/dist/components/avatar.js +135 -0
- package/dist/components/badge.d.ts +183 -0
- package/dist/components/badge.js +68 -0
- package/dist/components/block-stack.d.ts +33 -0
- package/dist/components/block-stack.js +197 -0
- package/dist/components/button.d.ts +327 -0
- package/dist/components/button.js +101 -0
- package/dist/components/card.d.ts +131 -0
- package/dist/components/card.js +100 -0
- package/dist/components/checkbox.d.ts +34 -0
- package/dist/components/checkbox.js +37 -0
- package/dist/components/choice-row.d.ts +104 -0
- package/dist/components/choice-row.js +56 -0
- package/dist/components/clamped-text.d.ts +17 -0
- package/dist/components/clamped-text.js +47 -0
- package/dist/components/code-block.d.ts +48 -0
- package/dist/components/code-block.js +124 -0
- package/dist/components/code-input.d.ts +49 -0
- package/dist/components/code-input.js +59 -0
- package/dist/components/combobox.d.ts +194 -0
- package/dist/components/combobox.js +176 -0
- package/dist/components/command-palette.d.ts +57 -0
- package/dist/components/command-palette.js +83 -0
- package/dist/components/context-menu.d.ts +24 -0
- package/dist/components/context-menu.js +30 -0
- package/dist/components/copy-button.d.ts +9 -0
- package/dist/components/copy-button.js +20 -0
- package/dist/components/copy-command.d.ts +18 -0
- package/dist/components/copy-command.js +19 -0
- package/dist/components/data-table.d.ts +240 -0
- package/dist/components/data-table.js +63 -0
- package/dist/components/dialog.d.ts +80 -0
- package/dist/components/dialog.js +85 -0
- package/dist/components/diff-lines.d.ts +20 -0
- package/dist/components/diff-lines.js +10 -0
- package/dist/components/diff-view.d.ts +128 -0
- package/dist/components/diff-view.js +675 -0
- package/dist/components/disclosure.d.ts +33 -0
- package/dist/components/disclosure.js +51 -0
- package/dist/components/drawn-check.d.ts +8 -0
- package/dist/components/drawn-check.js +16 -0
- package/dist/components/edge-resizer.d.ts +26 -0
- package/dist/components/edge-resizer.js +53 -0
- package/dist/components/empty-state.d.ts +112 -0
- package/dist/components/empty-state.js +56 -0
- package/dist/components/expand-toggle.d.ts +19 -0
- package/dist/components/expand-toggle.js +10 -0
- package/dist/components/field.d.ts +48 -0
- package/dist/components/field.js +60 -0
- package/dist/components/filter-chip.d.ts +47 -0
- package/dist/components/filter-chip.js +33 -0
- package/dist/components/group.d.ts +10 -0
- package/dist/components/group.js +9 -0
- package/dist/components/icon-text.d.ts +11 -0
- package/dist/components/icon-text.js +10 -0
- package/dist/components/image-sources.d.ts +57 -0
- package/dist/components/image-sources.js +58 -0
- package/dist/components/image-thumbnail.d.ts +34 -0
- package/dist/components/image-thumbnail.js +58 -0
- package/dist/components/image-viewer.d.ts +60 -0
- package/dist/components/image-viewer.js +411 -0
- package/dist/components/inline-edit.d.ts +22 -0
- package/dist/components/inline-edit.js +51 -0
- package/dist/components/input.d.ts +155 -0
- package/dist/components/input.js +109 -0
- package/dist/components/item-label.d.ts +11 -0
- package/dist/components/item-label.js +5 -0
- package/dist/components/item-row.d.ts +23 -0
- package/dist/components/item-row.js +19 -0
- package/dist/components/kbd.d.ts +55 -0
- package/dist/components/kbd.js +86 -0
- package/dist/components/link-policy.d.ts +24 -0
- package/dist/components/link-policy.js +32 -0
- package/dist/components/linked-text.d.ts +15 -0
- package/dist/components/linked-text.js +48 -0
- package/dist/components/loading-region.d.ts +18 -0
- package/dist/components/loading-region.js +17 -0
- package/dist/components/markdown.d.ts +34 -0
- package/dist/components/markdown.js +307 -0
- package/dist/components/menu.d.ts +132 -0
- package/dist/components/menu.js +102 -0
- package/dist/components/message-bubble.d.ts +57 -0
- package/dist/components/message-bubble.js +28 -0
- package/dist/components/meta-line.d.ts +28 -0
- package/dist/components/meta-line.js +36 -0
- package/dist/components/money-input.d.ts +23 -0
- package/dist/components/money-input.js +34 -0
- package/dist/components/native-select.d.ts +120 -0
- package/dist/components/native-select.js +85 -0
- package/dist/components/page-trail.d.ts +30 -0
- package/dist/components/page-trail.js +43 -0
- package/dist/components/page.d.ts +80 -0
- package/dist/components/page.js +60 -0
- package/dist/components/path-text.d.ts +8 -0
- package/dist/components/path-text.js +11 -0
- package/dist/components/percent-input.d.ts +20 -0
- package/dist/components/percent-input.js +32 -0
- package/dist/components/popover.d.ts +52 -0
- package/dist/components/popover.js +36 -0
- package/dist/components/progress.d.ts +389 -0
- package/dist/components/progress.js +142 -0
- package/dist/components/radio.d.ts +65 -0
- package/dist/components/radio.js +54 -0
- package/dist/components/remote-image-gate.d.ts +34 -0
- package/dist/components/remote-image-gate.js +60 -0
- package/dist/components/resizable.d.ts +23 -0
- package/dist/components/resizable.js +30 -0
- package/dist/components/rolling-number.d.ts +13 -0
- package/dist/components/rolling-number.js +69 -0
- package/dist/components/scroll-area.d.ts +17 -0
- package/dist/components/scroll-area.js +25 -0
- package/dist/components/section-label.d.ts +10 -0
- package/dist/components/section-label.js +21 -0
- package/dist/components/section-nav.d.ts +22 -0
- package/dist/components/section-nav.js +80 -0
- package/dist/components/segmented-control.d.ts +125 -0
- package/dist/components/segmented-control.js +101 -0
- package/dist/components/select.d.ts +120 -0
- package/dist/components/select.js +89 -0
- package/dist/components/separator.d.ts +10 -0
- package/dist/components/separator.js +12 -0
- package/dist/components/sheet.d.ts +101 -0
- package/dist/components/sheet.js +129 -0
- package/dist/components/shortcut-recorder.d.ts +31 -0
- package/dist/components/shortcut-recorder.js +67 -0
- package/dist/components/skeleton.d.ts +31 -0
- package/dist/components/skeleton.js +33 -0
- package/dist/components/slider.d.ts +138 -0
- package/dist/components/slider.js +98 -0
- package/dist/components/spinner.d.ts +37 -0
- package/dist/components/spinner.js +16 -0
- package/dist/components/switch.d.ts +59 -0
- package/dist/components/switch.js +56 -0
- package/dist/components/tab-strip.d.ts +29 -0
- package/dist/components/tab-strip.js +42 -0
- package/dist/components/tabs.d.ts +13 -0
- package/dist/components/tabs.js +68 -0
- package/dist/components/text-link.d.ts +54 -0
- package/dist/components/text-link.js +38 -0
- package/dist/components/textarea.d.ts +75 -0
- package/dist/components/textarea.js +90 -0
- package/dist/components/toast.d.ts +74 -0
- package/dist/components/toast.js +128 -0
- package/dist/components/tooltip.d.ts +50 -0
- package/dist/components/tooltip.js +46 -0
- package/dist/components/truncated-text.d.ts +15 -0
- package/dist/components/truncated-text.js +19 -0
- package/dist/i18n/defaults.d.ts +7 -0
- package/dist/i18n/defaults.js +7 -0
- package/dist/i18n/format.d.ts +84 -0
- package/dist/i18n/format.js +169 -0
- package/dist/i18n/index.d.ts +6 -0
- package/dist/i18n/index.js +8 -0
- package/dist/i18n/locales/ru-formatters.d.ts +3 -0
- package/dist/i18n/locales/ru-formatters.js +22 -0
- package/dist/i18n/locales/ru.d.ts +5 -0
- package/dist/i18n/locales/ru.js +185 -0
- package/dist/i18n/plural.d.ts +20 -0
- package/dist/i18n/plural.js +26 -0
- package/dist/i18n/provider.d.ts +36 -0
- package/dist/i18n/provider.js +39 -0
- package/dist/i18n/rich.d.ts +14 -0
- package/dist/i18n/rich.js +44 -0
- package/dist/i18n/ru-plural.d.ts +3 -0
- package/dist/i18n/ru-plural.js +4 -0
- package/dist/i18n/strings/alert-dialog.d.ts +5 -0
- package/dist/i18n/strings/alert-dialog.js +5 -0
- package/dist/i18n/strings/block-stack.d.ts +17 -0
- package/dist/i18n/strings/block-stack.js +11 -0
- package/dist/i18n/strings/code-block.d.ts +18 -0
- package/dist/i18n/strings/code-block.js +13 -0
- package/dist/i18n/strings/combobox.d.ts +12 -0
- package/dist/i18n/strings/combobox.js +10 -0
- package/dist/i18n/strings/command-palette.d.ts +17 -0
- package/dist/i18n/strings/command-palette.js +11 -0
- package/dist/i18n/strings/diff-view.d.ts +92 -0
- package/dist/i18n/strings/diff-view.js +68 -0
- package/dist/i18n/strings/image-viewer.d.ts +43 -0
- package/dist/i18n/strings/image-viewer.js +32 -0
- package/dist/i18n/strings/index.d.ts +314 -0
- package/dist/i18n/strings/index.js +43 -0
- package/dist/i18n/strings/kbd.d.ts +42 -0
- package/dist/i18n/strings/kbd.js +39 -0
- package/dist/i18n/strings/markdown.d.ts +23 -0
- package/dist/i18n/strings/markdown.js +18 -0
- package/dist/i18n/strings/money.d.ts +7 -0
- package/dist/i18n/strings/money.js +5 -0
- package/dist/i18n/strings/primitives.d.ts +25 -0
- package/dist/i18n/strings/primitives.js +18 -0
- package/dist/i18n/strings/progress.d.ts +4 -0
- package/dist/i18n/strings/progress.js +4 -0
- package/dist/i18n/strings/shortcut-recorder.d.ts +15 -0
- package/dist/i18n/strings/shortcut-recorder.js +11 -0
- package/dist/i18n/strings/time.d.ts +41 -0
- package/dist/i18n/strings/time.js +20 -0
- package/dist/i18n/strings/toast.d.ts +6 -0
- package/dist/i18n/strings/toast.js +6 -0
- package/dist/i18n/time-zones.d.ts +20 -0
- package/dist/i18n/time-zones.js +64 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +88 -0
- package/dist/lib/ansi.d.ts +55 -0
- package/dist/lib/ansi.js +438 -0
- package/dist/lib/clock.d.ts +32 -0
- package/dist/lib/clock.js +106 -0
- package/dist/lib/cn.d.ts +12 -0
- package/dist/lib/cn.js +28 -0
- package/dist/lib/control-props.d.ts +21 -0
- package/dist/lib/control-props.js +14 -0
- package/dist/lib/direction.d.ts +13 -0
- package/dist/lib/direction.js +25 -0
- package/dist/lib/image-size.d.ts +12 -0
- package/dist/lib/image-size.js +66 -0
- package/dist/lib/keycaps.d.ts +21 -0
- package/dist/lib/keycaps.js +62 -0
- package/dist/lib/keyed.d.ts +9 -0
- package/dist/lib/keyed.js +14 -0
- package/dist/lib/keys.d.ts +49 -0
- package/dist/lib/keys.js +159 -0
- package/dist/lib/kit-motion.d.ts +9 -0
- package/dist/lib/kit-motion.js +12 -0
- package/dist/lib/link-check-dialog.d.ts +11 -0
- package/dist/lib/link-check-dialog.js +18 -0
- package/dist/lib/motion-features.d.ts +1 -0
- package/dist/lib/motion-features.js +4 -0
- package/dist/lib/motion-provider.d.ts +5 -0
- package/dist/lib/motion-provider.js +8 -0
- package/dist/lib/motion.d.ts +39 -0
- package/dist/lib/motion.js +49 -0
- package/dist/lib/number-format.d.ts +114 -0
- package/dist/lib/number-format.js +313 -0
- package/dist/lib/patch.d.ts +72 -0
- package/dist/lib/patch.js +344 -0
- package/dist/lib/path.d.ts +34 -0
- package/dist/lib/path.js +85 -0
- package/dist/lib/search.d.ts +10 -0
- package/dist/lib/search.js +20 -0
- package/dist/lib/shiki-themes.d.ts +7 -0
- package/dist/lib/shiki-themes.js +115 -0
- package/dist/lib/shiki.d.ts +101 -0
- package/dist/lib/shiki.js +353 -0
- package/dist/lib/shortcut-context.d.ts +23 -0
- package/dist/lib/shortcut-context.js +29 -0
- package/dist/lib/text.d.ts +56 -0
- package/dist/lib/text.js +149 -0
- package/dist/lib/theme-scope.d.ts +18 -0
- package/dist/lib/theme-scope.js +24 -0
- package/dist/lib/theme.d.ts +40 -0
- package/dist/lib/theme.js +96 -0
- package/dist/lib/tree-keys.d.ts +72 -0
- package/dist/lib/tree-keys.js +126 -0
- package/dist/lib/url.d.ts +17 -0
- package/dist/lib/url.js +38 -0
- package/dist/lib/use-copy-to-clipboard.d.ts +13 -0
- package/dist/lib/use-copy-to-clipboard.js +40 -0
- package/dist/lib/use-delayed-loading.d.ts +11 -0
- package/dist/lib/use-delayed-loading.js +31 -0
- package/dist/lib/use-file-picker.d.ts +26 -0
- package/dist/lib/use-file-picker.js +43 -0
- package/dist/lib/use-hydrated.d.ts +6 -0
- package/dist/lib/use-hydrated.js +11 -0
- package/dist/lib/use-latest.d.ts +12 -0
- package/dist/lib/use-latest.js +22 -0
- package/dist/lib/use-resolved-theme.d.ts +12 -0
- package/dist/lib/use-resolved-theme.js +25 -0
- package/dist/styles/base.css +393 -0
- package/dist/styles/code.css +35 -0
- package/dist/styles/fonts.css +12 -0
- package/dist/styles/index.css +13 -0
- package/dist/styles/tokens.gen.css +494 -0
- package/package.json +208 -4
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode } from "react";
|
|
2
|
+
/** What a whole line does when pressed (the turn's line opens its receipt). */
|
|
3
|
+
export interface MetaLineAction {
|
|
4
|
+
/** What pressing it does ("Open the receipt"): the tooltip's last line and the button's description. */
|
|
5
|
+
readonly label: string;
|
|
6
|
+
readonly onClick: () => void;
|
|
7
|
+
}
|
|
8
|
+
export interface MetaLineProps extends Omit<ComponentProps<"span">, "children"> {
|
|
9
|
+
/** The facts, most important first: each child is one whole item. */
|
|
10
|
+
readonly children?: ReactNode;
|
|
11
|
+
/** The "·" between items; without it the caller spaces them (a `gap-x-*`), e.g. items with their own marks. */
|
|
12
|
+
readonly separated?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* The line is a button: pressing it does `action`, and its tooltip lists every item, the ones a
|
|
15
|
+
* narrow line drops too, then the action. Its items are text then (no controls inside a button).
|
|
16
|
+
*/
|
|
17
|
+
readonly action?: MetaLineAction | undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A line of facts ("Converged in 42s · 6 tools · $0.42 · ↓ 3.1k tokens") that never breaks inside a
|
|
21
|
+
* fact. Each child is one whole item, kept on one line, with a quiet "·" before every item but the
|
|
22
|
+
* first. When the line is too narrow, the items at its end wrap onto a second line that is clipped,
|
|
23
|
+
* so the line keeps its first facts whole and drops the last: put children in priority order. The
|
|
24
|
+
* first item truncates only when even it doesn't fit. CSS only (a one-line-high wrapping row with
|
|
25
|
+
* its overflow hidden): no measuring, no layout motion. Dropped items stay in the DOM, so a screen
|
|
26
|
+
* reader still reads the whole line; with an `action`, the tooltip shows them to the pointer too.
|
|
27
|
+
*/
|
|
28
|
+
export declare function MetaLine({ separated, action, className, children, ...props }: MetaLineProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Children, Fragment, isValidElement, useId } from "react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { Tooltip } from "./tooltip.js";
|
|
6
|
+
/**
|
|
7
|
+
* A line of facts ("Converged in 42s · 6 tools · $0.42 · ↓ 3.1k tokens") that never breaks inside a
|
|
8
|
+
* fact. Each child is one whole item, kept on one line, with a quiet "·" before every item but the
|
|
9
|
+
* first. When the line is too narrow, the items at its end wrap onto a second line that is clipped,
|
|
10
|
+
* so the line keeps its first facts whole and drops the last: put children in priority order. The
|
|
11
|
+
* first item truncates only when even it doesn't fit. CSS only (a one-line-high wrapping row with
|
|
12
|
+
* its overflow hidden): no measuring, no layout motion. Dropped items stay in the DOM, so a screen
|
|
13
|
+
* reader still reads the whole line; with an `action`, the tooltip shows them to the pointer too.
|
|
14
|
+
*/
|
|
15
|
+
export function MetaLine({ separated = true, action, className, children, ...props }) {
|
|
16
|
+
const items = Children.toArray(children);
|
|
17
|
+
const classes = cn("flex h-lh min-w-0 flex-wrap items-center overflow-hidden", "*:shrink-0 *:whitespace-nowrap",
|
|
18
|
+
// Alone on its line, the first item shrinks to it (no max-width here: a caller's own cap holds).
|
|
19
|
+
"[&>*:first-child]:min-w-0 [&>*:first-child]:shrink [&>*:first-child]:truncate", className);
|
|
20
|
+
const line = separated
|
|
21
|
+
? items.map((item, index) => index === 0 ? (item) : (_jsxs("span", { children: [_jsx("span", { "aria-hidden": true, className: "whitespace-pre", children: " · " }), item] }, keyOf(item))))
|
|
22
|
+
: children;
|
|
23
|
+
if (action === undefined) {
|
|
24
|
+
return (_jsx("span", { "data-slot": "meta-line", className: classes, ...props, children: line }));
|
|
25
|
+
}
|
|
26
|
+
return _jsx(MetaLineButton, { action: action, items: items, className: classes, line: line });
|
|
27
|
+
}
|
|
28
|
+
function MetaLineButton({ action, items, className, line, }) {
|
|
29
|
+
const descriptionId = useId();
|
|
30
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tooltip, { content: _jsxs("span", { className: "flex flex-col gap-0.5", children: [_jsx("span", { children: items.map((item, index) => (_jsxs(Fragment, { children: [index > 0 && " · ", item] }, keyOf(item)))) }), _jsx("span", { className: "opacity-70", children: action.label })] }), children: _jsx("button", { type: "button", "data-slot": "meta-line", "aria-describedby": descriptionId, onClick: action.onClick, className: cn(className, "rounded-xs text-start outline-none hover:text-fg focus-visible:ring-focus"), children: line }) }), _jsx("span", { id: descriptionId, hidden: true, children: action.label })] }));
|
|
31
|
+
}
|
|
32
|
+
function keyOf(item) {
|
|
33
|
+
if (isValidElement(item))
|
|
34
|
+
return item.key;
|
|
35
|
+
return typeof item === "string" ? item : null;
|
|
36
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type AmountBounds } from "../lib/number-format.js";
|
|
2
|
+
import { type InputProps } from "./input.js";
|
|
3
|
+
export interface MoneyInputProps extends Omit<InputProps, "leading" | "trailing" | "type" | "min" | "max" | "step" | "value" | "defaultValue">, AmountBounds {
|
|
4
|
+
/** The text, when the caller keeps it (a slider writes into the field); otherwise the field keeps its own. */
|
|
5
|
+
readonly value?: string | undefined;
|
|
6
|
+
/** The amount the field starts with, written in the person's locale ("12.5", "12,5"); empty for `null`. */
|
|
7
|
+
readonly defaultAmount?: number | null | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* On every edit, the amount typed in the person's locale within the bounds: the number, `null` for an
|
|
10
|
+
* empty field that `allowEmpty`, `undefined` for anything else.
|
|
11
|
+
*/
|
|
12
|
+
readonly onAmountChange?: ((amount: number | null | undefined) => void) | undefined;
|
|
13
|
+
/** The ISO 4217 currency the amount is in ("EUR"); the provider's when left out. */
|
|
14
|
+
readonly currency?: string | undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* An amount field: its currency's symbol (`currency`, else the provider's) on the side the person's
|
|
18
|
+
* locale writes it ("$ 12.50", "12,50 €"), and the amount read in that locale (`parseAmount`: its digits, group and decimal
|
|
19
|
+
* marks) within `min`, `max`, `step` (1 for whole units) and `nonZero`, reported by `onAmountChange`.
|
|
20
|
+
* Text that isn't such an amount marks the field invalid unless `invalid` says otherwise. Whole
|
|
21
|
+
* non-negative steps get the numeric keyboard, everything else the decimal one.
|
|
22
|
+
*/
|
|
23
|
+
export declare function MoneyInput({ min, max, step, nonZero, allowEmpty, defaultAmount, onAmountChange, value, onValueChange, invalid, inputMode, currency, ...props }: MoneyInputProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useFormatters } from "../i18n/provider.js";
|
|
5
|
+
import { readAmount } from "../lib/number-format.js";
|
|
6
|
+
import { Input } from "./input.js";
|
|
7
|
+
/** An amount as the field shows it: the locale's digits and decimal mark, no grouping, nothing for none. */
|
|
8
|
+
function amountText(amount, formatters) {
|
|
9
|
+
return amount === null || amount === undefined || !Number.isFinite(amount)
|
|
10
|
+
? ""
|
|
11
|
+
: formatters.number(amount, { useGrouping: false, maximumFractionDigits: 20 });
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* An amount field: its currency's symbol (`currency`, else the provider's) on the side the person's
|
|
15
|
+
* locale writes it ("$ 12.50", "12,50 €"), and the amount read in that locale (`parseAmount`: its digits, group and decimal
|
|
16
|
+
* marks) within `min`, `max`, `step` (1 for whole units) and `nonZero`, reported by `onAmountChange`.
|
|
17
|
+
* Text that isn't such an amount marks the field invalid unless `invalid` says otherwise. Whole
|
|
18
|
+
* non-negative steps get the numeric keyboard, everything else the decimal one.
|
|
19
|
+
*/
|
|
20
|
+
export function MoneyInput({ min, max, step, nonZero, allowEmpty, defaultAmount, onAmountChange, value, onValueChange, invalid, inputMode, currency, ...props }) {
|
|
21
|
+
const formatters = useFormatters();
|
|
22
|
+
const [own, setOwn] = useState(() => amountText(defaultAmount, formatters));
|
|
23
|
+
const text = value ?? own;
|
|
24
|
+
const bounds = { min, max, step, nonZero, allowEmpty };
|
|
25
|
+
const { symbol, leading } = formatters.currencySymbol(currency);
|
|
26
|
+
const affix = (_jsx("span", { "aria-hidden": true, className: "text-fg-subtle", children: symbol }));
|
|
27
|
+
const whole = step !== undefined && Number.isInteger(step) && min !== undefined && min >= 0;
|
|
28
|
+
return (_jsx(Input, { inputMode: inputMode ?? (whole ? "numeric" : "decimal"), value: text, invalid: invalid ?? (text.trim() !== "" && readAmount(text, formatters.locale, bounds) === undefined), onValueChange: (next, details) => {
|
|
29
|
+
if (value === undefined)
|
|
30
|
+
setOwn(next);
|
|
31
|
+
onValueChange?.(next, details);
|
|
32
|
+
onAmountChange?.(readAmount(next, formatters.locale, bounds));
|
|
33
|
+
}, ...props, ...(leading ? { leading: affix } : { trailing: affix }) }));
|
|
34
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode, type Ref } from "react";
|
|
2
|
+
import { type VariantProps } from "../lib/cn.js";
|
|
3
|
+
declare const nativeSelectStyles: import("tailwind-variants").TVReturnType<{
|
|
4
|
+
size: {
|
|
5
|
+
sm: {
|
|
6
|
+
root: string;
|
|
7
|
+
select: string;
|
|
8
|
+
adornment: string;
|
|
9
|
+
};
|
|
10
|
+
md: {
|
|
11
|
+
root: string;
|
|
12
|
+
select: string;
|
|
13
|
+
adornment: string;
|
|
14
|
+
};
|
|
15
|
+
lg: {
|
|
16
|
+
root: string;
|
|
17
|
+
select: string;
|
|
18
|
+
adornment: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
block: {
|
|
22
|
+
true: {
|
|
23
|
+
root: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
leading: {
|
|
27
|
+
true: {};
|
|
28
|
+
false: {};
|
|
29
|
+
};
|
|
30
|
+
}, {
|
|
31
|
+
root: string[];
|
|
32
|
+
select: string[];
|
|
33
|
+
adornment: string;
|
|
34
|
+
chevron: string[];
|
|
35
|
+
}, undefined, {
|
|
36
|
+
size: {
|
|
37
|
+
sm: {
|
|
38
|
+
root: string;
|
|
39
|
+
select: string;
|
|
40
|
+
adornment: string;
|
|
41
|
+
};
|
|
42
|
+
md: {
|
|
43
|
+
root: string;
|
|
44
|
+
select: string;
|
|
45
|
+
adornment: string;
|
|
46
|
+
};
|
|
47
|
+
lg: {
|
|
48
|
+
root: string;
|
|
49
|
+
select: string;
|
|
50
|
+
adornment: string;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
block: {
|
|
54
|
+
true: {
|
|
55
|
+
root: string;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
leading: {
|
|
59
|
+
true: {};
|
|
60
|
+
false: {};
|
|
61
|
+
};
|
|
62
|
+
}, {
|
|
63
|
+
root: string[];
|
|
64
|
+
select: string[];
|
|
65
|
+
adornment: string;
|
|
66
|
+
chevron: string[];
|
|
67
|
+
}, import("tailwind-variants").TVReturnTypeLike<{
|
|
68
|
+
size: {
|
|
69
|
+
sm: {
|
|
70
|
+
root: string;
|
|
71
|
+
select: string;
|
|
72
|
+
adornment: string;
|
|
73
|
+
};
|
|
74
|
+
md: {
|
|
75
|
+
root: string;
|
|
76
|
+
select: string;
|
|
77
|
+
adornment: string;
|
|
78
|
+
};
|
|
79
|
+
lg: {
|
|
80
|
+
root: string;
|
|
81
|
+
select: string;
|
|
82
|
+
adornment: string;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
block: {
|
|
86
|
+
true: {
|
|
87
|
+
root: string;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
leading: {
|
|
91
|
+
true: {};
|
|
92
|
+
false: {};
|
|
93
|
+
};
|
|
94
|
+
}, {
|
|
95
|
+
root: string[];
|
|
96
|
+
select: string[];
|
|
97
|
+
adornment: string;
|
|
98
|
+
chevron: string[];
|
|
99
|
+
}>>;
|
|
100
|
+
export interface NativeSelectProps extends Omit<ComponentProps<"select">, "ref" | "size" | "multiple" | "value" | "defaultValue" | "className">, Omit<VariantProps<typeof nativeSelectStyles>, "leading"> {
|
|
101
|
+
readonly ref?: Ref<HTMLSelectElement>;
|
|
102
|
+
readonly value?: string;
|
|
103
|
+
readonly defaultValue?: string;
|
|
104
|
+
/** Fires with the chosen option's value. */
|
|
105
|
+
readonly onValueChange?: (value: string) => void;
|
|
106
|
+
/** Icon before the value (a globe for a language). */
|
|
107
|
+
readonly leading?: ReactNode;
|
|
108
|
+
/** Marks the field invalid outside of a <Field> (inside one, Field drives it). */
|
|
109
|
+
readonly invalid?: boolean;
|
|
110
|
+
/** Classes for the frame (width, margins…). */
|
|
111
|
+
readonly className?: string | undefined;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A select that is the platform's own: `<option>` children, the native keyboard and the phone's own
|
|
115
|
+
* picker, in the kit's field look (the Select trigger's chrome, chevron and sizes). Use it where the
|
|
116
|
+
* native list matters more than a styled one: a language switcher, a long list on the web for phones.
|
|
117
|
+
* Works inside <Field> for its label, description and validation.
|
|
118
|
+
*/
|
|
119
|
+
export declare function NativeSelect({ ref, size, block, leading, invalid, value, defaultValue, onValueChange, disabled, id, name, autoFocus, className, children, ...rest }: NativeSelectProps): import("react").JSX.Element;
|
|
120
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Field as BaseField } from "@base-ui/react/field";
|
|
4
|
+
import { ChevronsUpDown } from "lucide-react";
|
|
5
|
+
import { useCallback } from "react";
|
|
6
|
+
import { tv } from "../lib/cn.js";
|
|
7
|
+
import { givenControlProps } from "../lib/control-props.js";
|
|
8
|
+
import { fieldChrome } from "./input.js";
|
|
9
|
+
const nativeSelectStyles = tv({
|
|
10
|
+
slots: {
|
|
11
|
+
// The frame wears the field's chrome; the select fills it, so a press anywhere on it opens the list.
|
|
12
|
+
root: [
|
|
13
|
+
"group/native-select relative inline-flex min-w-0 shrink-0 items-center",
|
|
14
|
+
fieldChrome,
|
|
15
|
+
"has-[select:focus]:border-border-strong has-[select:focus-visible]:ring-3",
|
|
16
|
+
"has-[select:disabled]:pointer-events-none has-[select:disabled]:opacity-45",
|
|
17
|
+
],
|
|
18
|
+
select: [
|
|
19
|
+
"h-full w-full min-w-0 cursor-default appearance-none truncate bg-transparent text-fg outline-none",
|
|
20
|
+
// iOS zooms into a field whose text is under 16 px, and a finger needs 44 px: the select grows
|
|
21
|
+
// past the frame on touch screens, centred, so its picker opens from the whole hit area.
|
|
22
|
+
"pointer-coarse:h-11 pointer-coarse:text-[1rem]",
|
|
23
|
+
],
|
|
24
|
+
adornment: "pointer-events-none absolute flex items-center text-fg-subtle [&_svg]:shrink-0",
|
|
25
|
+
chevron: [
|
|
26
|
+
"size-3.5 text-fg-subtle transition-colors duration-(--duration-fast)",
|
|
27
|
+
"group-hover/native-select:text-fg-muted group-has-[select:focus]/native-select:text-fg-muted",
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
variants: {
|
|
31
|
+
size: {
|
|
32
|
+
sm: {
|
|
33
|
+
root: "h-7 rounded-md text-sm",
|
|
34
|
+
select: "ps-2 pe-6.5",
|
|
35
|
+
adornment: "[&_svg:not([class*='size-'])]:size-3.5",
|
|
36
|
+
},
|
|
37
|
+
md: {
|
|
38
|
+
root: "h-8 rounded-lg text-base",
|
|
39
|
+
select: "ps-2.5 pe-7.5",
|
|
40
|
+
adornment: "[&_svg:not([class*='size-'])]:size-4",
|
|
41
|
+
},
|
|
42
|
+
lg: {
|
|
43
|
+
root: "h-10 rounded-xl text-md",
|
|
44
|
+
select: "ps-3 pe-8.5",
|
|
45
|
+
adornment: "[&_svg:not([class*='size-'])]:size-4",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
block: { true: { root: "flex w-full" } },
|
|
49
|
+
leading: { true: {}, false: {} },
|
|
50
|
+
},
|
|
51
|
+
compoundVariants: [
|
|
52
|
+
{ size: "sm", leading: true, class: { select: "ps-7" } },
|
|
53
|
+
{ size: "md", leading: true, class: { select: "ps-8.5" } },
|
|
54
|
+
{ size: "lg", leading: true, class: { select: "ps-9.5" } },
|
|
55
|
+
],
|
|
56
|
+
defaultVariants: { size: "md", leading: false },
|
|
57
|
+
});
|
|
58
|
+
const SIDE = {
|
|
59
|
+
sm: { start: "start-2", end: "end-1.5" },
|
|
60
|
+
md: { start: "start-2.5", end: "end-2" },
|
|
61
|
+
lg: { start: "start-3", end: "end-2.5" },
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* A select that is the platform's own: `<option>` children, the native keyboard and the phone's own
|
|
65
|
+
* picker, in the kit's field look (the Select trigger's chrome, chevron and sizes). Use it where the
|
|
66
|
+
* native list matters more than a styled one: a language switcher, a long list on the web for phones.
|
|
67
|
+
* Works inside <Field> for its label, description and validation.
|
|
68
|
+
*/
|
|
69
|
+
export function NativeSelect({ ref, size, block, leading, invalid, value, defaultValue, onValueChange, disabled, id, name, autoFocus, className, children, ...rest }) {
|
|
70
|
+
const resolvedSize = size ?? "md";
|
|
71
|
+
const styles = nativeSelectStyles({ size: resolvedSize, block, leading: leading !== undefined });
|
|
72
|
+
const side = SIDE[resolvedSize];
|
|
73
|
+
const setRef = useCallback((node) => {
|
|
74
|
+
const select = node instanceof HTMLSelectElement ? node : null;
|
|
75
|
+
if (typeof ref === "function")
|
|
76
|
+
ref(select);
|
|
77
|
+
else if (ref)
|
|
78
|
+
ref.current = select;
|
|
79
|
+
}, [ref]);
|
|
80
|
+
return (_jsxs("span", { "data-slot": "native-select", className: styles.root({ className }), children: [leading !== undefined && (_jsx("span", { "aria-hidden": true, className: styles.adornment({ className: side.start }), children: leading })), _jsx(BaseField.Control, { ref: setRef, ...givenControlProps({ value, defaultValue, disabled, id, name, autoFocus }), ...(onValueChange === undefined ? {} : { onValueChange: (next) => onValueChange(next) }),
|
|
81
|
+
// Element form, as Textarea: Base UI merges its control props into the select, and the
|
|
82
|
+
// element's own props win, so `aria-invalid` is set only when given: an undefined one would
|
|
83
|
+
// hide a <Field>'s.
|
|
84
|
+
render: _jsx("select", { ...rest, ...(invalid === true ? { "aria-invalid": true } : {}), "data-slot": "native-select-control", className: styles.select(), children: children }) }), _jsx("span", { "aria-hidden": true, className: styles.adornment({ className: side.end }), children: _jsx(ChevronsUpDown, { className: styles.chevron() }) })] }));
|
|
85
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactElement, type ReactNode } from "react";
|
|
2
|
+
/** A place above the page: its name, and the link back to it. */
|
|
3
|
+
export interface PageTrailPlace {
|
|
4
|
+
readonly label: string;
|
|
5
|
+
/** The link back as the host's router renders it (`<Link to="/projects" />`); it takes the label. */
|
|
6
|
+
readonly render?: ReactElement;
|
|
7
|
+
/** A plain link back, where there is no router. */
|
|
8
|
+
readonly href?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface PageTrailProps extends Omit<ComponentProps<"nav">, "children"> {
|
|
11
|
+
/** The places this page sits in, outermost first; each is a link back. */
|
|
12
|
+
readonly places: readonly PageTrailPlace[];
|
|
13
|
+
/**
|
|
14
|
+
* This page: its name (the page's heading), or a control that names it (a team switch). Absent
|
|
15
|
+
* when the page's own content names it (an artifact's toolbar): the trail is then the way back
|
|
16
|
+
* alone ("‹ Artifacts").
|
|
17
|
+
*/
|
|
18
|
+
readonly children?: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Where a page sits, in the title bar of a page inside a place ("Races › Fix the login bug"): the
|
|
22
|
+
* places above it as links back, then the page's own name as its heading. The way back is in sight
|
|
23
|
+
* and says where it goes, so nothing depends on Back or a gesture.
|
|
24
|
+
*
|
|
25
|
+
* It is a size container. In a narrow title bar the page's name matters most: the places fold first,
|
|
26
|
+
* into one back arrow to the nearest place (named by its tooltip and accessible name), and only then
|
|
27
|
+
* does the name truncate (its whole text in a tooltip). A place's name is never cut, and no "›" is
|
|
28
|
+
* left bare.
|
|
29
|
+
*/
|
|
30
|
+
export declare function PageTrail({ places, children, className, ...props }: PageTrailProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
4
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
5
|
+
import { Fragment } from "react";
|
|
6
|
+
import { useUiStrings } from "../i18n/provider.js";
|
|
7
|
+
import { cn } from "../lib/cn.js";
|
|
8
|
+
import { buttonStyles } from "./button.js";
|
|
9
|
+
import { Tooltip } from "./tooltip.js";
|
|
10
|
+
/**
|
|
11
|
+
* Where a page sits, in the title bar of a page inside a place ("Races › Fix the login bug"): the
|
|
12
|
+
* places above it as links back, then the page's own name as its heading. The way back is in sight
|
|
13
|
+
* and says where it goes, so nothing depends on Back or a gesture.
|
|
14
|
+
*
|
|
15
|
+
* It is a size container. In a narrow title bar the page's name matters most: the places fold first,
|
|
16
|
+
* into one back arrow to the nearest place (named by its tooltip and accessible name), and only then
|
|
17
|
+
* does the name truncate (its whole text in a tooltip). A place's name is never cut, and no "›" is
|
|
18
|
+
* left bare.
|
|
19
|
+
*/
|
|
20
|
+
export function PageTrail({ places, children, className, ...props }) {
|
|
21
|
+
const t = useUiStrings().primitives;
|
|
22
|
+
const named = children !== undefined && children !== null;
|
|
23
|
+
const nearest = places.length - 1;
|
|
24
|
+
return (_jsx("nav", { "data-slot": "page-trail", "aria-label": t.trail, className: cn("@container/trail min-w-0 flex-1", className), ...props, children: _jsxs("ol", { className: "flex min-w-0 items-center gap-1 text-sm", children: [places.map((place, index) => (_jsxs(Fragment, { children: [_jsx("li", { className: index === nearest ? "flex shrink-0" : "hidden shrink-0 @[24rem]/trail:flex", children: _jsx(TrailLink, { place: place, arrow: index === nearest ? (named ? "narrow" : "always") : "never" }) }), (index < nearest || named) && (_jsx("li", { "aria-hidden": true, className: "hidden shrink-0 @[24rem]/trail:flex", children: _jsx(ChevronRight, { className: "size-3 text-fg-faint rtl:-scale-x-100" }) }))] }, place.label))), named && (_jsx("li", { "aria-current": "page", className: "flex min-w-0", children: typeof children === "string" ? (_jsx(Tooltip, { content: children, children: _jsx("h1", { className: "truncate font-medium text-fg", children: children }) })) : (children) }))] }) }));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A place's link. With a page name after it, it reads as the place's name and folds to a back arrow in a
|
|
28
|
+
* narrow title bar; alone, it is the back arrow and the name ("‹ Artifacts").
|
|
29
|
+
*/
|
|
30
|
+
function TrailLink({ place, arrow, }) {
|
|
31
|
+
const link = useRender({
|
|
32
|
+
defaultTagName: "a",
|
|
33
|
+
render: place.render,
|
|
34
|
+
props: {
|
|
35
|
+
...(place.href === undefined ? {} : { href: place.href }),
|
|
36
|
+
"data-slot": "page-trail-link",
|
|
37
|
+
"aria-label": place.label,
|
|
38
|
+
className: buttonStyles({ variant: "quiet", size: "xs" }),
|
|
39
|
+
children: (_jsxs(_Fragment, { children: [arrow !== "never" && (_jsx(ChevronLeft, { "aria-hidden": true, className: arrow === "narrow" ? "rtl:-scale-x-100 @[24rem]/trail:hidden" : "rtl:-scale-x-100" })), _jsx("span", { className: arrow === "narrow" ? "hidden @[24rem]/trail:inline" : undefined, children: place.label })] })),
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
return _jsx(Tooltip, { content: place.label, children: link });
|
|
43
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
+
import { type VariantProps } from "../lib/cn.js";
|
|
3
|
+
/**
|
|
4
|
+
* Page layout. A page picks one width; the gutter follows the
|
|
5
|
+
* page's own width (a container query), not the window's, because the sidebar and panes change the
|
|
6
|
+
* room a page has. Screens never set their own `max-w-*`, `mx-auto` or page padding.
|
|
7
|
+
*/
|
|
8
|
+
declare const pageColumnStyles: import("tailwind-variants").TVReturnType<{
|
|
9
|
+
width: {
|
|
10
|
+
reading: "max-w-reading";
|
|
11
|
+
form: "max-w-form";
|
|
12
|
+
page: "max-w-page";
|
|
13
|
+
wide: "max-w-wide";
|
|
14
|
+
full: "";
|
|
15
|
+
};
|
|
16
|
+
}, undefined, string[], {
|
|
17
|
+
width: {
|
|
18
|
+
reading: "max-w-reading";
|
|
19
|
+
form: "max-w-form";
|
|
20
|
+
page: "max-w-page";
|
|
21
|
+
wide: "max-w-wide";
|
|
22
|
+
full: "";
|
|
23
|
+
};
|
|
24
|
+
}, undefined, import("tailwind-variants").TVReturnTypeLike<{
|
|
25
|
+
width: {
|
|
26
|
+
reading: "max-w-reading";
|
|
27
|
+
form: "max-w-form";
|
|
28
|
+
page: "max-w-page";
|
|
29
|
+
wide: "max-w-wide";
|
|
30
|
+
full: "";
|
|
31
|
+
};
|
|
32
|
+
}, undefined>>;
|
|
33
|
+
export type PageWidth = NonNullable<VariantProps<typeof pageColumnStyles>["width"]>;
|
|
34
|
+
/** The column's classes, for pages whose column is split (a sticky toolbar over a scrolling grid). */
|
|
35
|
+
export declare function pageColumn(width?: PageWidth, className?: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* A full-bleed strip inside a page column (a horizontal shelf): it reaches the page's edges and pads
|
|
38
|
+
* its content back to the gutter, so the first card lines up with the text above it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function pageBleed(className?: string): string;
|
|
41
|
+
/** A column with the page rhythm — `Page`'s own column, for pages that draw their own region or animate it. */
|
|
42
|
+
export declare function pageBody(width?: PageWidth, className?: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* The page region: the `page` size container (so the gutter can follow it) and, by default, the
|
|
45
|
+
* scroll container. Put `pageColumn()` columns inside it when the page splits into bands.
|
|
46
|
+
*/
|
|
47
|
+
export declare function PageRegion({ scroll, className, ...props }: ComponentProps<"div"> & {
|
|
48
|
+
/** Scroll the page's content (default). Off when a child owns scrolling. */
|
|
49
|
+
readonly scroll?: boolean;
|
|
50
|
+
}): import("react").JSX.Element;
|
|
51
|
+
export interface PageProps extends ComponentProps<"div"> {
|
|
52
|
+
/** Content width (§1.3). */
|
|
53
|
+
readonly width?: PageWidth;
|
|
54
|
+
/** Scroll the page (default). */
|
|
55
|
+
readonly scroll?: boolean;
|
|
56
|
+
/** Classes for the inner column (gap, extra padding). */
|
|
57
|
+
readonly columnClassName?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A whole page: the region plus one centred column with the standard rhythm — 16/24/32 px top padding
|
|
61
|
+
* by page width, 32 px between the heading and the first section, 64 px of run-out at the bottom.
|
|
62
|
+
*/
|
|
63
|
+
export declare function Page({ width, scroll, className, columnClassName, children, ...props }: PageProps): import("react").JSX.Element;
|
|
64
|
+
export interface PageHeadingProps extends Omit<ComponentProps<"div">, "title"> {
|
|
65
|
+
readonly title: ReactNode;
|
|
66
|
+
readonly description?: ReactNode;
|
|
67
|
+
/** Right-aligned actions; they wrap under the title when the page is narrow. */
|
|
68
|
+
readonly actions?: ReactNode;
|
|
69
|
+
}
|
|
70
|
+
/** Page title block: a 20 px semibold title, a muted description, actions on the right. */
|
|
71
|
+
export declare function PageHeading({ title, description, actions, className, ...props }: PageHeadingProps): import("react").JSX.Element;
|
|
72
|
+
export interface PageSectionProps extends Omit<ComponentProps<"section">, "title"> {
|
|
73
|
+
readonly title?: ReactNode;
|
|
74
|
+
readonly description?: ReactNode;
|
|
75
|
+
/** A trailing control on the title row (a link, a small button). */
|
|
76
|
+
readonly action?: ReactNode;
|
|
77
|
+
}
|
|
78
|
+
/** A titled group of content. Sections sit 32 px apart in a `Page`; title to content is 10 px. */
|
|
79
|
+
export declare function PageSection({ title, description, action, className, children, ...props }: PageSectionProps): import("react").JSX.Element;
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn, tv } from "../lib/cn.js";
|
|
3
|
+
/**
|
|
4
|
+
* Page layout. A page picks one width; the gutter follows the
|
|
5
|
+
* page's own width (a container query), not the window's, because the sidebar and panes change the
|
|
6
|
+
* room a page has. Screens never set their own `max-w-*`, `mx-auto` or page padding.
|
|
7
|
+
*/
|
|
8
|
+
const pageColumnStyles = tv({
|
|
9
|
+
base: ["mx-auto w-full min-w-0", "px-4 @page-md/page:px-6 @page-lg/page:px-8"],
|
|
10
|
+
variants: {
|
|
11
|
+
width: {
|
|
12
|
+
reading: "max-w-reading",
|
|
13
|
+
form: "max-w-form",
|
|
14
|
+
page: "max-w-page",
|
|
15
|
+
wide: "max-w-wide",
|
|
16
|
+
full: "",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
defaultVariants: { width: "page" },
|
|
20
|
+
});
|
|
21
|
+
/** The column's classes, for pages whose column is split (a sticky toolbar over a scrolling grid). */
|
|
22
|
+
export function pageColumn(width = "page", className) {
|
|
23
|
+
return pageColumnStyles({ width, className });
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A full-bleed strip inside a page column (a horizontal shelf): it reaches the page's edges and pads
|
|
27
|
+
* its content back to the gutter, so the first card lines up with the text above it.
|
|
28
|
+
*/
|
|
29
|
+
export function pageBleed(className) {
|
|
30
|
+
return cn("-mx-4 px-4 scroll-px-4", "@page-md/page:-mx-6 @page-md/page:px-6 @page-md/page:scroll-px-6", "@page-lg/page:-mx-8 @page-lg/page:px-8 @page-lg/page:scroll-px-8", className);
|
|
31
|
+
}
|
|
32
|
+
/** The standard page rhythm: 16/24/32 px top padding by page width, 32 px between blocks, 64 px run-out. */
|
|
33
|
+
const RHYTHM = "flex flex-col gap-8 pt-4 pb-16 @page-md/page:pt-6 @page-lg/page:pt-8";
|
|
34
|
+
/** A column with the page rhythm — `Page`'s own column, for pages that draw their own region or animate it. */
|
|
35
|
+
export function pageBody(width = "page", className) {
|
|
36
|
+
return pageColumnStyles({ width, className: cn(RHYTHM, className) });
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The page region: the `page` size container (so the gutter can follow it) and, by default, the
|
|
40
|
+
* scroll container. Put `pageColumn()` columns inside it when the page splits into bands.
|
|
41
|
+
*/
|
|
42
|
+
export function PageRegion({ scroll = true, className, ...props }) {
|
|
43
|
+
return (_jsx("div", { "data-slot": "page", className: cn("@container/page flex min-h-0 w-full min-w-0 flex-1 flex-col", scroll && "overflow-y-auto overscroll-contain", className), ...props }));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A whole page: the region plus one centred column with the standard rhythm — 16/24/32 px top padding
|
|
47
|
+
* by page width, 32 px between the heading and the first section, 64 px of run-out at the bottom.
|
|
48
|
+
*/
|
|
49
|
+
export function Page({ width = "page", scroll = true, className, columnClassName, children, ...props }) {
|
|
50
|
+
return (_jsx(PageRegion, { scroll: scroll, className: className, ...props, children: _jsx("div", { "data-slot": "page-column", className: pageBody(width, columnClassName), children: children }) }));
|
|
51
|
+
}
|
|
52
|
+
/** Page title block: a 20 px semibold title, a muted description, actions on the right. */
|
|
53
|
+
export function PageHeading({ title, description, actions, className, ...props }) {
|
|
54
|
+
return (_jsxs("div", { "data-slot": "page-heading", className: cn("flex flex-wrap items-end justify-between gap-x-6 gap-y-3", className), ...props, children: [_jsxs("div", { className: "flex min-w-0 max-w-form flex-1 basis-80 flex-col gap-1", children: [_jsx("h1", { className: "text-xl font-semibold tracking-tight text-balance text-fg", children: title }), description !== undefined && _jsx("p", { className: "text-base text-pretty text-fg-muted", children: description })] }), actions !== undefined && (_jsx("div", { "data-slot": "page-actions", className: "flex shrink-0 flex-wrap items-center gap-2", children: actions }))] }));
|
|
55
|
+
}
|
|
56
|
+
/** A titled group of content. Sections sit 32 px apart in a `Page`; title to content is 10 px. */
|
|
57
|
+
export function PageSection({ title, description, action, className, children, ...props }) {
|
|
58
|
+
const head = title !== undefined || description !== undefined || action !== undefined;
|
|
59
|
+
return (_jsxs("section", { "data-slot": "page-section", className: cn("flex min-w-0 flex-col gap-2.5", className), ...props, children: [head && (_jsxs("div", { className: "flex items-end justify-between gap-4", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-0.5", children: [title !== undefined && _jsx("h2", { className: "text-sm font-medium text-fg", children: title }), description !== undefined && _jsx("p", { className: "text-sm text-pretty text-fg-subtle", children: description })] }), action !== undefined && _jsx("div", { className: "flex shrink-0 items-center gap-2", children: action })] })), children] }));
|
|
60
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `src/lib/` dim + `app.ts` bright; the directory gives way first when space runs out. A path reads
|
|
3
|
+
* left to right in every language, so it is laid out left to right (the directory before the name).
|
|
4
|
+
*/
|
|
5
|
+
export declare function PathText({ path, className }: {
|
|
6
|
+
path: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { splitPath } from "../lib/path.js";
|
|
4
|
+
/**
|
|
5
|
+
* `src/lib/` dim + `app.ts` bright; the directory gives way first when space runs out. A path reads
|
|
6
|
+
* left to right in every language, so it is laid out left to right (the directory before the name).
|
|
7
|
+
*/
|
|
8
|
+
export function PathText({ path, className }) {
|
|
9
|
+
const [dir, name] = splitPath(path);
|
|
10
|
+
return (_jsxs("span", { dir: "ltr", className: cn("flex min-w-0 items-baseline font-mono text-xs", className), title: path, children: [dir !== "" && _jsx("span", { className: "min-w-0 truncate text-fg-subtle", children: dir }), _jsx("span", { className: "max-w-full shrink-0 truncate text-fg", children: name })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type InputProps } from "./input.js";
|
|
2
|
+
export interface PercentInputProps extends Omit<InputProps, "leading" | "trailing" | "type" | "min" | "max" | "step" | "value" | "defaultValue" | "inputMode"> {
|
|
3
|
+
/** The whole percent the field starts with, 0–100 ("12" for 12%); empty for `null`. */
|
|
4
|
+
readonly defaultPercent?: number | null | undefined;
|
|
5
|
+
/** Zero isn't a percent here: only 1–100 are. */
|
|
6
|
+
readonly nonZero?: boolean | undefined;
|
|
7
|
+
/** An empty field is `null` ("no limit", "the default"); otherwise it isn't a percent. */
|
|
8
|
+
readonly allowEmpty?: boolean | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* On every edit, the whole percent typed in the person's locale, 0–100: the number, `null` for an
|
|
11
|
+
* empty field that `allowEmpty`, `undefined` for anything else.
|
|
12
|
+
*/
|
|
13
|
+
readonly onPercentChange?: ((percent: number | null | undefined) => void) | undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A whole-percent field (0–100): the number read in the person's locale (its digits), the locale's
|
|
17
|
+
* percent sign on the side it writes it, and `onPercentChange` with what was typed. Text that isn't
|
|
18
|
+
* such a percent marks the field invalid unless `invalid` says otherwise.
|
|
19
|
+
*/
|
|
20
|
+
export declare function PercentInput({ defaultPercent, nonZero, allowEmpty, onPercentChange, invalid, onValueChange, ...props }: PercentInputProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useFormatters } from "../i18n/provider.js";
|
|
5
|
+
import { readAmount } from "../lib/number-format.js";
|
|
6
|
+
import { Input } from "./input.js";
|
|
7
|
+
/** The locale's percent sign and whether it comes before the number ("%12") or after it ("12 %"). */
|
|
8
|
+
function percentSign(locale) {
|
|
9
|
+
const parts = new Intl.NumberFormat(locale, { style: "percent" }).formatToParts(0.5);
|
|
10
|
+
const sign = parts.findIndex((part) => part.type === "percentSign");
|
|
11
|
+
const integer = parts.findIndex((part) => part.type === "integer");
|
|
12
|
+
return { sign: parts[sign]?.value ?? "%", leading: sign !== -1 && sign < integer };
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A whole-percent field (0–100): the number read in the person's locale (its digits), the locale's
|
|
16
|
+
* percent sign on the side it writes it, and `onPercentChange` with what was typed. Text that isn't
|
|
17
|
+
* such a percent marks the field invalid unless `invalid` says otherwise.
|
|
18
|
+
*/
|
|
19
|
+
export function PercentInput({ defaultPercent, nonZero, allowEmpty, onPercentChange, invalid, onValueChange, ...props }) {
|
|
20
|
+
const formatters = useFormatters();
|
|
21
|
+
const [text, setText] = useState(() => defaultPercent === null || defaultPercent === undefined
|
|
22
|
+
? ""
|
|
23
|
+
: formatters.number(defaultPercent, { useGrouping: false, maximumFractionDigits: 0 }));
|
|
24
|
+
const bounds = { min: 0, max: 100, step: 1, nonZero, allowEmpty };
|
|
25
|
+
const { sign, leading } = percentSign(formatters.locale);
|
|
26
|
+
const affix = (_jsx("span", { "aria-hidden": true, className: "text-fg-subtle", children: sign }));
|
|
27
|
+
return (_jsx(Input, { inputMode: "numeric", value: text, invalid: invalid ?? (text.trim() !== "" && readAmount(text, formatters.locale, bounds) === undefined), onValueChange: (next, details) => {
|
|
28
|
+
setText(next);
|
|
29
|
+
onValueChange?.(next, details);
|
|
30
|
+
onPercentChange?.(readAmount(next, formatters.locale, bounds));
|
|
31
|
+
}, ...props, ...(leading ? { leading: affix } : { trailing: affix }) }));
|
|
32
|
+
}
|