@hackcove/ui-react 0.0.0-stage → 0.8.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/LICENSE +21 -0
- package/MIGRATING.md +116 -0
- package/README.md +400 -2
- package/dist/AppShell.d.ts +97 -0
- package/dist/AppShell.js +106 -0
- package/dist/Avatar.d.ts +49 -0
- package/dist/Avatar.js +65 -0
- package/dist/AvatarStack.d.ts +29 -0
- package/dist/AvatarStack.js +26 -0
- package/dist/Badge.d.ts +14 -0
- package/dist/Badge.js +11 -0
- package/dist/Breadcrumbs.d.ts +28 -0
- package/dist/Breadcrumbs.js +30 -0
- package/dist/BulletList.d.ts +4 -0
- package/dist/BulletList.js +5 -0
- package/dist/Button.d.ts +78 -0
- package/dist/Button.js +133 -0
- package/dist/ButtonGroup.d.ts +15 -0
- package/dist/ButtonGroup.js +10 -0
- package/dist/Callout.d.ts +24 -0
- package/dist/Callout.js +28 -0
- package/dist/Card.d.ts +95 -0
- package/dist/Card.js +134 -0
- package/dist/CardBadge.d.ts +7 -0
- package/dist/CardBadge.js +12 -0
- package/dist/CardParts.d.ts +25 -0
- package/dist/CardParts.js +25 -0
- package/dist/Checkbox.d.ts +29 -0
- package/dist/Checkbox.js +36 -0
- package/dist/Chevron.d.ts +5 -0
- package/dist/Chevron.js +6 -0
- package/dist/Cluster.d.ts +51 -0
- package/dist/Cluster.js +38 -0
- package/dist/Code.d.ts +4 -0
- package/dist/Code.js +5 -0
- package/dist/CodeBlock.d.ts +27 -0
- package/dist/CodeBlock.js +12 -0
- package/dist/CodeExample.d.ts +17 -0
- package/dist/CodeExample.js +13 -0
- package/dist/CodeText.d.ts +7 -0
- package/dist/CodeText.js +21 -0
- package/dist/ColourSchemeToggle.d.ts +34 -0
- package/dist/ColourSchemeToggle.js +50 -0
- package/dist/Container.d.ts +22 -0
- package/dist/Container.js +13 -0
- package/dist/CopyButton.d.ts +17 -0
- package/dist/CopyButton.js +13 -0
- package/dist/CopyLine.d.ts +9 -0
- package/dist/CopyLine.js +7 -0
- package/dist/DateCalendar.d.ts +10 -0
- package/dist/DateCalendar.js +45 -0
- package/dist/DateField.d.ts +87 -0
- package/dist/DateField.js +100 -0
- package/dist/DateFieldCalendar.d.ts +6 -0
- package/dist/DateFieldCalendar.js +35 -0
- package/dist/Dialog.d.ts +26 -0
- package/dist/Dialog.js +18 -0
- package/dist/Disclosure.d.ts +15 -0
- package/dist/Disclosure.js +12 -0
- package/dist/Divider.d.ts +4 -0
- package/dist/Divider.js +5 -0
- package/dist/Drawer.d.ts +29 -0
- package/dist/Drawer.js +22 -0
- package/dist/EmptyState.d.ts +18 -0
- package/dist/EmptyState.js +14 -0
- package/dist/Field.d.ts +21 -0
- package/dist/Field.js +19 -0
- package/dist/FieldDialog.d.ts +28 -0
- package/dist/FieldDialog.js +34 -0
- package/dist/FileDrop.d.ts +17 -0
- package/dist/FileDrop.js +12 -0
- package/dist/FilterGroup.d.ts +7 -0
- package/dist/FilterGroup.js +6 -0
- package/dist/FormRow.d.ts +42 -0
- package/dist/FormRow.js +33 -0
- package/dist/Icon.d.ts +20 -0
- package/dist/Icon.js +13 -0
- package/dist/IconButton.d.ts +57 -0
- package/dist/IconButton.js +51 -0
- package/dist/Input.d.ts +22 -0
- package/dist/Input.js +22 -0
- package/dist/InputShell.d.ts +16 -0
- package/dist/InputShell.js +26 -0
- package/dist/Kbd.d.ts +6 -0
- package/dist/Kbd.js +5 -0
- package/dist/KeyValueList.d.ts +23 -0
- package/dist/KeyValueList.js +19 -0
- package/dist/Lightbox.d.ts +28 -0
- package/dist/Lightbox.js +44 -0
- package/dist/ListRow.d.ts +109 -0
- package/dist/ListRow.js +92 -0
- package/dist/Markdown.d.ts +9 -0
- package/dist/Markdown.js +43 -0
- package/dist/MetaList.d.ts +23 -0
- package/dist/MetaList.js +14 -0
- package/dist/OutlineRegion.d.ts +43 -0
- package/dist/OutlineRegion.js +27 -0
- package/dist/PageFade.d.ts +26 -0
- package/dist/PageFade.js +39 -0
- package/dist/PageHeader.d.ts +24 -0
- package/dist/PageHeader.js +14 -0
- package/dist/PageLayouts.d.ts +130 -0
- package/dist/PageLayouts.js +86 -0
- package/dist/Pill.d.ts +53 -0
- package/dist/Pill.js +34 -0
- package/dist/ProgressBar.d.ts +32 -0
- package/dist/ProgressBar.js +32 -0
- package/dist/RadioGroup.d.ts +48 -0
- package/dist/RadioGroup.js +39 -0
- package/dist/SearchInput.d.ts +8 -0
- package/dist/SearchInput.js +40 -0
- package/dist/SegmentedControl.d.ts +20 -0
- package/dist/SegmentedControl.js +31 -0
- package/dist/Select.d.ts +5 -0
- package/dist/Select.js +9 -0
- package/dist/SplitBar.d.ts +27 -0
- package/dist/SplitBar.js +15 -0
- package/dist/Stack.d.ts +70 -0
- package/dist/Stack.js +47 -0
- package/dist/StackedBar.d.ts +56 -0
- package/dist/StackedBar.js +35 -0
- package/dist/StatTile.d.ts +21 -0
- package/dist/StatTile.js +22 -0
- package/dist/StatusPages.d.ts +38 -0
- package/dist/StatusPages.js +57 -0
- package/dist/Steps.d.ts +17 -0
- package/dist/Steps.js +14 -0
- package/dist/StickyBar.d.ts +12 -0
- package/dist/StickyBar.js +10 -0
- package/dist/Table.d.ts +142 -0
- package/dist/Table.js +138 -0
- package/dist/Tabs.d.ts +40 -0
- package/dist/Tabs.js +49 -0
- package/dist/TextLink.d.ts +51 -0
- package/dist/TextLink.js +37 -0
- package/dist/Textarea.d.ts +5 -0
- package/dist/Textarea.js +7 -0
- package/dist/Timeline.d.ts +27 -0
- package/dist/Timeline.js +29 -0
- package/dist/Video.d.ts +13 -0
- package/dist/Video.js +9 -0
- package/dist/colourScheme.d.ts +21 -0
- package/dist/colourScheme.js +53 -0
- package/dist/control.d.ts +31 -0
- package/dist/control.js +38 -0
- package/dist/cx.d.ts +10 -0
- package/dist/cx.js +12 -0
- package/dist/highlight.d.ts +16 -0
- package/dist/highlight.js +292 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +66 -0
- package/dist/isoDate.d.ts +5 -0
- package/dist/isoDate.js +16 -0
- package/dist/lift.harness.d.ts +2 -0
- package/dist/lift.harness.js +3 -0
- package/dist/link.d.ts +50 -0
- package/dist/link.js +25 -0
- package/dist/modal.d.ts +11 -0
- package/dist/modal.js +41 -0
- package/dist/props.d.ts +14 -0
- package/dist/props.js +1 -0
- package/dist/shownDate.d.ts +45 -0
- package/dist/shownDate.js +88 -0
- package/dist/size.d.ts +6 -0
- package/dist/size.js +1 -0
- package/dist/switcher.d.ts +88 -0
- package/dist/switcher.js +71 -0
- package/dist/typography.d.ts +123 -0
- package/dist/typography.js +120 -0
- package/dist/ui.d.ts +10 -0
- package/dist/ui.js +27 -0
- package/package.json +91 -4
package/dist/modal.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Opens the `<dialog>` with `showModal()` on mount and closes it on unmount. Closing on unmount
|
|
4
|
+
* (not just on Esc or the backdrop) hands focus back to the opener and drops the inert page behind
|
|
5
|
+
* when the parent unmounts the dialog, and keeps StrictMode's second mount from reopening an open one.
|
|
6
|
+
*
|
|
7
|
+
* Returns the handler for the dialog's `close` event. A closed `<dialog>` fades or slides out with
|
|
8
|
+
* a transition, but only while it is still in the DOM, so the handler calls `onClose` once the exit
|
|
9
|
+
* transition has ended and the parent can then unmount it without cutting the animation short.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useModal(dialog: RefObject<HTMLDialogElement | null>, onClose: () => void): () => void;
|
package/dist/modal.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef } from "react";
|
|
2
|
+
/** Resolves when the transitions running on the dialog (and its backdrop) have ended. */
|
|
3
|
+
async function exitTransitions(dialog) {
|
|
4
|
+
// A browser without the Web Animations API (or jsdom) has nothing to wait for.
|
|
5
|
+
if (typeof dialog.getAnimations !== "function")
|
|
6
|
+
return;
|
|
7
|
+
const running = dialog.getAnimations({ subtree: true }).filter((a) => "transitionProperty" in a);
|
|
8
|
+
await Promise.allSettled(running.map((a) => a.finished));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Opens the `<dialog>` with `showModal()` on mount and closes it on unmount. Closing on unmount
|
|
12
|
+
* (not just on Esc or the backdrop) hands focus back to the opener and drops the inert page behind
|
|
13
|
+
* when the parent unmounts the dialog, and keeps StrictMode's second mount from reopening an open one.
|
|
14
|
+
*
|
|
15
|
+
* Returns the handler for the dialog's `close` event. A closed `<dialog>` fades or slides out with
|
|
16
|
+
* a transition, but only while it is still in the DOM, so the handler calls `onClose` once the exit
|
|
17
|
+
* transition has ended and the parent can then unmount it without cutting the animation short.
|
|
18
|
+
*/
|
|
19
|
+
export function useModal(dialog, onClose) {
|
|
20
|
+
const mounted = useRef(false);
|
|
21
|
+
useLayoutEffect(() => {
|
|
22
|
+
const el = dialog.current;
|
|
23
|
+
mounted.current = true;
|
|
24
|
+
el?.showModal();
|
|
25
|
+
return () => {
|
|
26
|
+
mounted.current = false;
|
|
27
|
+
el?.close();
|
|
28
|
+
};
|
|
29
|
+
}, [dialog]);
|
|
30
|
+
return () => {
|
|
31
|
+
const el = dialog.current;
|
|
32
|
+
if (!el)
|
|
33
|
+
return;
|
|
34
|
+
void exitTransitions(el).then(() => {
|
|
35
|
+
// Skip a close that an unmount caused, and the one StrictMode's remount leaves behind (the
|
|
36
|
+
// dialog is open again by the time its event fires).
|
|
37
|
+
if (mounted.current && !el.open)
|
|
38
|
+
onClose();
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
}
|
package/dist/props.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AriaAttributes } from "react";
|
|
2
|
+
import type { DataAttributes } from "./link.js";
|
|
3
|
+
/**
|
|
4
|
+
* Drops the two props that would let a consumer restyle a component. Components take no
|
|
5
|
+
* `className` or `style`: a look is a prop or a variant, never a class. Wrap any HTML or ARIA prop
|
|
6
|
+
* type a component spreads in this one.
|
|
7
|
+
*/
|
|
8
|
+
export type Unstyled<T> = Omit<T, "className" | "style">;
|
|
9
|
+
/**
|
|
10
|
+
* What a component with no single natural element takes through to its root: `data-*` (an e2e
|
|
11
|
+
* hook, `data-testid`) and `aria-*`. Nothing else, so no handler, `id` or `role` the component
|
|
12
|
+
* would have to merge with its own.
|
|
13
|
+
*/
|
|
14
|
+
export type Passthrough = AriaAttributes & DataAttributes;
|
package/dist/props.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { CalendarDate } from "@internationalized/date";
|
|
2
|
+
import { type FocusEvent, type ReactElement } from "react";
|
|
3
|
+
import type { FieldProps, Read } from "./DateField.js";
|
|
4
|
+
type ValueKeys = "value" | "defaultValue" | "onChange" | "minValue" | "maxValue" | "isRequired";
|
|
5
|
+
/** The value props, each of which may be passed as `undefined`. */
|
|
6
|
+
type ValueProps = {
|
|
7
|
+
[K in ValueKeys]?: FieldProps[K] | undefined;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* The value the field reports is always what it shows: the typed date when it is complete, exists
|
|
11
|
+
* and is within min/max, else `null`. Two parts of React Aria disagree with that, so this owns both:
|
|
12
|
+
*
|
|
13
|
+
* - **The value.** React Aria keeps its last valid date behind an impossible or partly cleared
|
|
14
|
+
* display, and reports an out-of-range one. So the consumer's `onChange` is fired from what the
|
|
15
|
+
* segments read, once per change: a valid date turning invalid reports `null`. React Aria itself
|
|
16
|
+
* is controlled by `inner`, so a parent holding that `null` does not wipe the typed segments; a
|
|
17
|
+
* `value` the parent sets that differs from the last report is passed through, and not reported
|
|
18
|
+
* back. Because React Aria's own value goes stale, `validate` checks `isRequired` against the
|
|
19
|
+
* segments: a partly filled required field blocks the form.
|
|
20
|
+
* - **Keeping an impossible date.** When focus leaves a complete field, React Aria moves an
|
|
21
|
+
* impossible day to the month's last and reports that date (`confirmPlaceholder`); it skips this
|
|
22
|
+
* for a read-only field. So while the typed date is impossible, the field is read-only for exactly
|
|
23
|
+
* the blur that leaves it: `onBlurCapture` on a wrapper of the input turns it on before React
|
|
24
|
+
* Aria's blur handler runs, and the field's own `onBlur`, which runs after it, turns it off again.
|
|
25
|
+
*
|
|
26
|
+
* `shown` (`YYYY-MM-DD` or `""`) is what the hidden `name` input posts.
|
|
27
|
+
*/
|
|
28
|
+
export declare function useShownDate(props: ValueProps): {
|
|
29
|
+
inner: CalendarDate | null;
|
|
30
|
+
setInner: import("react").Dispatch<import("react").SetStateAction<CalendarDate | null>>;
|
|
31
|
+
shown: string;
|
|
32
|
+
impossible: string | null;
|
|
33
|
+
onRead: (next: Read) => void;
|
|
34
|
+
validate: () => "Enter the whole date." | null;
|
|
35
|
+
leaving: boolean;
|
|
36
|
+
onBlurCapture: (event: FocusEvent<HTMLElement>) => void;
|
|
37
|
+
onLeft: () => void;
|
|
38
|
+
};
|
|
39
|
+
/** The hidden input a form posts under `name`: the shown date as `YYYY-MM-DD`, or `""`. */
|
|
40
|
+
export declare function ShownInput({ name, shown, isDisabled, }: {
|
|
41
|
+
name: string | undefined;
|
|
42
|
+
shown: string;
|
|
43
|
+
isDisabled: boolean | undefined;
|
|
44
|
+
}): ReactElement | null;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState, } from "react";
|
|
3
|
+
import { flushSync } from "react-dom";
|
|
4
|
+
import { fromISODate, toISODate } from "./isoDate.js";
|
|
5
|
+
/** `iso` when it is a date within min/max, else `""`. */
|
|
6
|
+
function withinRange(iso, { minValue, maxValue }) {
|
|
7
|
+
const date = fromISODate(iso);
|
|
8
|
+
if (!date)
|
|
9
|
+
return "";
|
|
10
|
+
if (minValue && date.compare(minValue) < 0)
|
|
11
|
+
return "";
|
|
12
|
+
if (maxValue && date.compare(maxValue) > 0)
|
|
13
|
+
return "";
|
|
14
|
+
return iso;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The value the field reports is always what it shows: the typed date when it is complete, exists
|
|
18
|
+
* and is within min/max, else `null`. Two parts of React Aria disagree with that, so this owns both:
|
|
19
|
+
*
|
|
20
|
+
* - **The value.** React Aria keeps its last valid date behind an impossible or partly cleared
|
|
21
|
+
* display, and reports an out-of-range one. So the consumer's `onChange` is fired from what the
|
|
22
|
+
* segments read, once per change: a valid date turning invalid reports `null`. React Aria itself
|
|
23
|
+
* is controlled by `inner`, so a parent holding that `null` does not wipe the typed segments; a
|
|
24
|
+
* `value` the parent sets that differs from the last report is passed through, and not reported
|
|
25
|
+
* back. Because React Aria's own value goes stale, `validate` checks `isRequired` against the
|
|
26
|
+
* segments: a partly filled required field blocks the form.
|
|
27
|
+
* - **Keeping an impossible date.** When focus leaves a complete field, React Aria moves an
|
|
28
|
+
* impossible day to the month's last and reports that date (`confirmPlaceholder`); it skips this
|
|
29
|
+
* for a read-only field. So while the typed date is impossible, the field is read-only for exactly
|
|
30
|
+
* the blur that leaves it: `onBlurCapture` on a wrapper of the input turns it on before React
|
|
31
|
+
* Aria's blur handler runs, and the field's own `onBlur`, which runs after it, turns it off again.
|
|
32
|
+
*
|
|
33
|
+
* `shown` (`YYYY-MM-DD` or `""`) is what the hidden `name` input posts.
|
|
34
|
+
*/
|
|
35
|
+
export function useShownDate(props) {
|
|
36
|
+
const { value, defaultValue, onChange, isRequired } = props;
|
|
37
|
+
const [inner, setInner] = useState(value ?? defaultValue ?? null);
|
|
38
|
+
const [read, setRead] = useState({
|
|
39
|
+
typed: toISODate(inner),
|
|
40
|
+
impossible: null,
|
|
41
|
+
partial: false,
|
|
42
|
+
});
|
|
43
|
+
const [leaving, setLeaving] = useState(false);
|
|
44
|
+
const shown = withinRange(read.typed, props);
|
|
45
|
+
const reported = useRef(shown);
|
|
46
|
+
const report = useRef(onChange);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
report.current = onChange;
|
|
49
|
+
});
|
|
50
|
+
const given = value === undefined ? undefined : toISODate(value);
|
|
51
|
+
const givenShown = withinRange(given ?? "", props);
|
|
52
|
+
const lastGiven = useRef(given);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (given === lastGiven.current)
|
|
55
|
+
return;
|
|
56
|
+
lastGiven.current = given;
|
|
57
|
+
if (given === undefined || given === reported.current)
|
|
58
|
+
return;
|
|
59
|
+
reported.current = givenShown;
|
|
60
|
+
setInner(fromISODate(given));
|
|
61
|
+
}, [given, givenShown]);
|
|
62
|
+
const onRead = useCallback((next) => setRead(next), []);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (shown === reported.current)
|
|
65
|
+
return;
|
|
66
|
+
reported.current = shown;
|
|
67
|
+
report.current?.(fromISODate(shown));
|
|
68
|
+
}, [shown]);
|
|
69
|
+
return {
|
|
70
|
+
inner,
|
|
71
|
+
setInner,
|
|
72
|
+
shown,
|
|
73
|
+
impossible: read.impossible,
|
|
74
|
+
onRead,
|
|
75
|
+
validate: () => (isRequired && read.partial ? "Enter the whole date." : null),
|
|
76
|
+
leaving,
|
|
77
|
+
onBlurCapture: (event) => {
|
|
78
|
+
if (read.impossible && !event.currentTarget.contains(event.relatedTarget)) {
|
|
79
|
+
flushSync(() => setLeaving(true));
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
onLeft: () => setLeaving(false),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** The hidden input a form posts under `name`: the shown date as `YYYY-MM-DD`, or `""`. */
|
|
86
|
+
export function ShownInput({ name, shown, isDisabled, }) {
|
|
87
|
+
return name ? _jsx("input", { type: "hidden", name: name, value: shown, disabled: isDisabled }) : null;
|
|
88
|
+
}
|
package/dist/size.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one size scale of the library: `sm`, `md` (the default everywhere) and `lg`. A component
|
|
3
|
+
* extends it outward only where it has a real use (Avatar's `xl` and `2xl`, the gap scale's `xs`
|
|
4
|
+
* and `xl`), and no scale skips `md`.
|
|
5
|
+
*/
|
|
6
|
+
export type Size = "sm" | "md" | "lg";
|
package/dist/size.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import type { Passthrough } from "./props.js";
|
|
3
|
+
import { type DataAttributes, type LinkRender } from "./link.js";
|
|
4
|
+
type Option = DataAttributes & {
|
|
5
|
+
label: ReactNode;
|
|
6
|
+
/** A small muted count after the label, part of the accessible name ("Shots 12"). */
|
|
7
|
+
count?: number;
|
|
8
|
+
/** A native tooltip; on a disabled option it is the reason. */
|
|
9
|
+
title?: string;
|
|
10
|
+
};
|
|
11
|
+
/** An option that is a button: it calls `onChange(value)`. */
|
|
12
|
+
export type SwitchButton = Option & {
|
|
13
|
+
/** What `value` is set to when this option is current. */
|
|
14
|
+
value: string;
|
|
15
|
+
/**
|
|
16
|
+
* Not chosen by click or arrow key. The button stays in the page as `aria-disabled`, so its
|
|
17
|
+
* `title` still shows on hover and is read out.
|
|
18
|
+
*/
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
/** The id of the element this option controls (a tab's panel). */
|
|
21
|
+
controls?: string;
|
|
22
|
+
href?: undefined;
|
|
23
|
+
render?: undefined;
|
|
24
|
+
external?: undefined;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* An option that is a link: `href`, or `render` to draw the app's own router `<Link>`. The router
|
|
28
|
+
* changes the page; `active` on the item, or a `value` equal to the group's, marks the current one.
|
|
29
|
+
* `external` opens a new tab.
|
|
30
|
+
*/
|
|
31
|
+
export type SwitchLink = Option & {
|
|
32
|
+
/** Marks this link current when it equals the group's `value`. */
|
|
33
|
+
value?: string;
|
|
34
|
+
/** This link is the current page. */
|
|
35
|
+
active?: boolean;
|
|
36
|
+
disabled?: undefined;
|
|
37
|
+
controls?: undefined;
|
|
38
|
+
external?: boolean;
|
|
39
|
+
} & ({
|
|
40
|
+
href: string;
|
|
41
|
+
render?: LinkRender;
|
|
42
|
+
} | {
|
|
43
|
+
href?: string;
|
|
44
|
+
render: LinkRender;
|
|
45
|
+
});
|
|
46
|
+
/** One option of a Tabs or SegmentedControl: a button or a link, never both in one group. */
|
|
47
|
+
export type SwitchItem = SwitchButton | SwitchLink;
|
|
48
|
+
type Shared = {
|
|
49
|
+
/** The accessible name of the group. */
|
|
50
|
+
label: string;
|
|
51
|
+
} & Passthrough;
|
|
52
|
+
export type SwitchProps = Shared & ({
|
|
53
|
+
/** The options in order: `{ value, label }` buttons, or links (`href` or `render`) with `value` marking the current one. Either may carry a `count`; a button may be `disabled`. */
|
|
54
|
+
items: SwitchButton[];
|
|
55
|
+
/** The `value` of the current option. */
|
|
56
|
+
value: string;
|
|
57
|
+
/** Called with the option's `value` when one is chosen, by click or arrow key. */
|
|
58
|
+
onChange: (value: string) => void;
|
|
59
|
+
/** Every option is disabled, e.g. until the page has hydrated. */
|
|
60
|
+
disabled?: boolean;
|
|
61
|
+
} | {
|
|
62
|
+
/** The options in order: links (`href` or `render`), the current one marked by its `active` or by a `value` equal to the group's. A link may carry a `count`. */
|
|
63
|
+
items: SwitchLink[];
|
|
64
|
+
/** The `value` of the current link, as an alternative to `active` on the item. */
|
|
65
|
+
value?: string;
|
|
66
|
+
onChange?: undefined;
|
|
67
|
+
disabled?: undefined;
|
|
68
|
+
});
|
|
69
|
+
type Parts = {
|
|
70
|
+
/** The roles of a group of buttons; a group with links is a `<nav>` instead. */
|
|
71
|
+
group: "tablist" | "radiogroup";
|
|
72
|
+
item: "tab" | "radio";
|
|
73
|
+
/** The vertical arrows move too, as in a radio group. */
|
|
74
|
+
vertical: boolean;
|
|
75
|
+
/** The options stack in a column: a tablist says so with `aria-orientation`. */
|
|
76
|
+
stacked?: boolean;
|
|
77
|
+
root: string;
|
|
78
|
+
option: (active: boolean, disabled: boolean) => string;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* The shared body of Tabs and SegmentedControl: a row of options with one current. Buttons form a
|
|
82
|
+
* `tablist` or `radiogroup` with a single tab stop and arrow-key movement (the arrow focuses and
|
|
83
|
+
* chooses); links form a `<nav>` with `aria-current="page"` on the current one.
|
|
84
|
+
*/
|
|
85
|
+
export declare function Switcher({ items, value, onChange, disabled: groupDisabled, label, parts, ...rest }: SwitchProps & {
|
|
86
|
+
parts: Parts;
|
|
87
|
+
}): ReactElement;
|
|
88
|
+
export {};
|
package/dist/switcher.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createElement as _createElement } from "react";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import {} from "react";
|
|
4
|
+
import { cx } from "./cx.js";
|
|
5
|
+
import { dataAttributes, renderLink } from "./link.js";
|
|
6
|
+
/** The index a key moves to from `at` among `count` enabled options, wrapping; -1 for a key that does not move. */
|
|
7
|
+
function target(key, at, count, vertical) {
|
|
8
|
+
if (key === "ArrowRight" || (vertical && key === "ArrowDown"))
|
|
9
|
+
return (at + 1) % count;
|
|
10
|
+
if (key === "ArrowLeft" || (vertical && key === "ArrowUp"))
|
|
11
|
+
return (at - 1 + count) % count;
|
|
12
|
+
if (key === "Home")
|
|
13
|
+
return 0;
|
|
14
|
+
if (key === "End")
|
|
15
|
+
return count - 1;
|
|
16
|
+
return -1;
|
|
17
|
+
}
|
|
18
|
+
/** The label with its count after it; the plain-space text node keeps the name "Shots 12". */
|
|
19
|
+
function Label({ item }) {
|
|
20
|
+
if (item.count === undefined)
|
|
21
|
+
return _jsx(_Fragment, { children: item.label });
|
|
22
|
+
return (_jsxs(_Fragment, { children: [item.label, " ", _jsx("span", { className: "ml-0.5 text-xs tabular-nums opacity-70", children: item.count })] }));
|
|
23
|
+
}
|
|
24
|
+
const isLinks = (items) => items.some((item) => item.href !== undefined || item.render !== undefined);
|
|
25
|
+
const isCurrent = (item, value) => item.active === true || (item.value !== undefined && item.value === value);
|
|
26
|
+
function SwitchLinkItem({ item, current, parts, }) {
|
|
27
|
+
return renderLink({
|
|
28
|
+
...dataAttributes(item),
|
|
29
|
+
href: item.href,
|
|
30
|
+
render: item.render,
|
|
31
|
+
external: item.external,
|
|
32
|
+
className: parts.option(current, false),
|
|
33
|
+
"aria-current": current ? "page" : undefined,
|
|
34
|
+
...(item.title === undefined ? {} : { title: item.title }),
|
|
35
|
+
children: _jsx(Label, { item: item }),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The shared body of Tabs and SegmentedControl: a row of options with one current. Buttons form a
|
|
40
|
+
* `tablist` or `radiogroup` with a single tab stop and arrow-key movement (the arrow focuses and
|
|
41
|
+
* chooses); links form a `<nav>` with `aria-current="page"` on the current one.
|
|
42
|
+
*/
|
|
43
|
+
export function Switcher({ items, value, onChange, disabled: groupDisabled, label, parts, ...rest }) {
|
|
44
|
+
const root = parts.root;
|
|
45
|
+
if (isLinks(items)) {
|
|
46
|
+
return (_jsx("nav", { ...rest, "aria-label": label, className: root, children: items.map((item, index) => (_jsx(SwitchLinkItem, { item: item, current: isCurrent(item, value), parts: parts }, item.value ?? item.href ?? index))) }));
|
|
47
|
+
}
|
|
48
|
+
const off = (item) => groupDisabled === true || item.disabled === true;
|
|
49
|
+
const enabled = items.filter((item) => !off(item));
|
|
50
|
+
const stop = enabled.some((item) => item.value === value) ? value : enabled[0]?.value;
|
|
51
|
+
function onKeyDown(event) {
|
|
52
|
+
const buttons = [...event.currentTarget.querySelectorAll("button:not([aria-disabled=true])")];
|
|
53
|
+
const at = buttons.findIndex((button) => button === event.target);
|
|
54
|
+
if (at < 0)
|
|
55
|
+
return;
|
|
56
|
+
const to = target(event.key, at, buttons.length, parts.vertical);
|
|
57
|
+
const button = buttons[to];
|
|
58
|
+
if (!(button instanceof HTMLButtonElement))
|
|
59
|
+
return;
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
button.focus();
|
|
62
|
+
button.click();
|
|
63
|
+
}
|
|
64
|
+
return (_jsx("div", { ...rest, role: parts.group, "aria-label": label, ...(parts.stacked ? { "aria-orientation": "vertical" } : {}), ...(groupDisabled ? { "aria-disabled": true } : {}), onKeyDown: onKeyDown, className: root, children: items.map((item) => (_createElement("button", { ...dataAttributes(item), key: item.value, type: "button", role: parts.item, ...(parts.item === "tab"
|
|
65
|
+
? { "aria-selected": item.value === value }
|
|
66
|
+
: { "aria-checked": item.value === value }), ...(item.controls === undefined ? {} : { "aria-controls": item.controls }), tabIndex: item.value === stop ? 0 : -1, ...(off(item) ? { "aria-disabled": true } : {}), ...(item.title === undefined ? {} : { title: item.title }), onClick: () => {
|
|
67
|
+
if (!off(item))
|
|
68
|
+
onChange?.(item.value);
|
|
69
|
+
}, className: cx(parts.option(item.value === value, off(item)), off(item) && "cursor-not-allowed opacity-50") },
|
|
70
|
+
_jsx(Label, { item: item })))) }));
|
|
71
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ElementType, type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import type { Size } from "./size.js";
|
|
3
|
+
export type TypographyColor = "default" | "secondary" | "tertiary" | "muted" | "brand";
|
|
4
|
+
/**
|
|
5
|
+
* The meaning of a run of inline text: the muted tone plus the Callout's, for "Saved." or an error
|
|
6
|
+
* message. Colour alone does not say it, so the words do too. Same names and text colours as a
|
|
7
|
+
* `Table` cell's `tone`.
|
|
8
|
+
*/
|
|
9
|
+
export type TextTone = "muted" | "positive" | "warn" | "danger" | "info";
|
|
10
|
+
/** Tone → text utility. Shared with `Table`'s cell tones, so the two never drift. */
|
|
11
|
+
export declare const TEXT_TONE: Record<TextTone, string>;
|
|
12
|
+
/** Own props shared by every typography component; the rest follow the rendered element. */
|
|
13
|
+
type OwnProps = {
|
|
14
|
+
/** Colour token; each component sets a sensible default. */
|
|
15
|
+
color?: TypographyColor;
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
};
|
|
18
|
+
/** The opt-in props of the inline roles (`Text`, `Caption`, `Overline`). */
|
|
19
|
+
type InlineOwnProps = {
|
|
20
|
+
/** A meaning's colour (see `TextTone`). Wins over `color`. */
|
|
21
|
+
tone?: TextTone | undefined;
|
|
22
|
+
/** Tabular figures, so a column of amounts lines up. */
|
|
23
|
+
numeric?: boolean | undefined;
|
|
24
|
+
/** The monospace face, for an id, a path or a command. */
|
|
25
|
+
mono?: boolean | undefined;
|
|
26
|
+
/** One line, cut with an ellipsis where the container ends; the full text is the `title`. */
|
|
27
|
+
truncate?: boolean | undefined;
|
|
28
|
+
};
|
|
29
|
+
type Own = OwnProps & InlineOwnProps;
|
|
30
|
+
/** Polymorphic props: own props + `as` + the intrinsic props of whichever element `as` selects. */
|
|
31
|
+
export type TypographyProps<E extends ElementType> = OwnProps & {
|
|
32
|
+
/** The element to render (each role has a semantic default); the other props follow it. */
|
|
33
|
+
as?: E;
|
|
34
|
+
} & Omit<ComponentPropsWithoutRef<E>, keyof OwnProps | "as" | "className" | "style">;
|
|
35
|
+
/**
|
|
36
|
+
* The props of an inline role: `TypographyProps` plus `tone`, `numeric`, `mono` and `truncate`.
|
|
37
|
+
* Spelled out like `TypographyProps`, not as an `Omit` of it, so the docs read its real types.
|
|
38
|
+
*/
|
|
39
|
+
export type InlineTypographyProps<E extends ElementType> = Own & {
|
|
40
|
+
/** The element to render (each role has a semantic default); the other props follow it. */
|
|
41
|
+
as?: E;
|
|
42
|
+
} & Omit<ComponentPropsWithoutRef<E>, keyof Own | "as" | "className" | "style">;
|
|
43
|
+
/**
|
|
44
|
+
* Marketing-scale headline — the one line at the top of a public page. `h1`,
|
|
45
|
+
* text-4xl/tight semibold, primary text, growing to text-6xl on wider screens.
|
|
46
|
+
* `PageHeading`'s scale is deliberately dev-tool sized; a brochure
|
|
47
|
+
* hero needs a display scale, so it gets its own pinned role rather than callers
|
|
48
|
+
* overriding PageHeading's size.
|
|
49
|
+
*/
|
|
50
|
+
export declare const Display: {
|
|
51
|
+
<E extends ElementType = "h1">(props: TypographyProps<E>): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
52
|
+
displayName: string;
|
|
53
|
+
};
|
|
54
|
+
/** The supporting sentence under a `Display`. `p`, text-lg/7 normal, secondary. */
|
|
55
|
+
export declare const Lead: {
|
|
56
|
+
<E extends ElementType = "p">(props: TypographyProps<E>): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
57
|
+
displayName: string;
|
|
58
|
+
};
|
|
59
|
+
/** Page title. `h1`, text-2xl/8 semibold, primary text. */
|
|
60
|
+
export declare const PageHeading: {
|
|
61
|
+
<E extends ElementType = "h1">(props: TypographyProps<E>): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
62
|
+
displayName: string;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Top-level section header. `h2`, text-base/6 semibold, primary text. `rule` sets
|
|
66
|
+
* a hairline under it (the `Divider` line colour) with the space either side of
|
|
67
|
+
* the line built in.
|
|
68
|
+
*/
|
|
69
|
+
export declare function SectionHeading<E extends ElementType = "h2">({ rule, ...props }: TypographyProps<E> & {
|
|
70
|
+
/** A hairline under the heading, with the space either side of it built in. */
|
|
71
|
+
rule?: boolean;
|
|
72
|
+
}): ReactElement;
|
|
73
|
+
export type SubsectionHeadingSize = Size;
|
|
74
|
+
/**
|
|
75
|
+
* Nested header. `h3`, text-base/6 semibold, primary text. `size` steps it down for a dense card
|
|
76
|
+
* or up for a drawer or form-group title: `sm` (text-sm), `md` (default) or `lg`. `truncate` keeps it on
|
|
77
|
+
* one line and cuts it with an ellipsis where its container ends, the full text as the `title`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function SubsectionHeading<E extends ElementType = "h3">({ size, ...props }: TypographyProps<E> & {
|
|
80
|
+
/**
|
|
81
|
+
* The heading's type size: `sm` is text-sm/5, `md` text-base/6 and `lg` text-lg/7.
|
|
82
|
+
* @default md
|
|
83
|
+
*/
|
|
84
|
+
size?: SubsectionHeadingSize;
|
|
85
|
+
/** One line, cut with an ellipsis where the container ends; the full text becomes the `title`. */
|
|
86
|
+
truncate?: boolean | undefined;
|
|
87
|
+
}): ReactElement;
|
|
88
|
+
/**
|
|
89
|
+
* Small all-caps label above a group. `span`, text-xs semibold uppercase, tertiary. `tone`,
|
|
90
|
+
* `numeric`, `mono` and `truncate` are opt-in, as on `Text`.
|
|
91
|
+
*/
|
|
92
|
+
export declare const Overline: {
|
|
93
|
+
<E extends ElementType = "span">(props: InlineTypographyProps<E>): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
94
|
+
displayName: string;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Running text. `span` (inline; `as="p"` for a paragraph), text-sm/5 normal, secondary. A block of it
|
|
98
|
+
* stops at a readable line length (`max-w-[72ch]`), wherever it sits. `strong` emphasises it:
|
|
99
|
+
* font-medium in the primary ink, unless `color` or `tone` says otherwise.
|
|
100
|
+
* `preserveNewlines` keeps the line breaks in the text (a user-typed note); runs of spaces still collapse.
|
|
101
|
+
* `tone` colours it by meaning (`positive` for "Saved.", `danger` for an error; it wins over `color`),
|
|
102
|
+
* `numeric` sets tabular figures, `mono` the monospace face, and `truncate` cuts it to one line with an
|
|
103
|
+
* ellipsis (the full text becomes its `title`).
|
|
104
|
+
*/
|
|
105
|
+
export declare function Text<E extends ElementType = "span">({ strong, preserveNewlines, ...props }: InlineTypographyProps<E> & {
|
|
106
|
+
/** Emphasis: font-medium in the primary ink (`color` and `tone` still win). */
|
|
107
|
+
strong?: boolean | undefined;
|
|
108
|
+
/** Keep the line breaks in the text (a user-typed note); runs of spaces still collapse. */
|
|
109
|
+
preserveNewlines?: boolean;
|
|
110
|
+
}): ReactElement;
|
|
111
|
+
/**
|
|
112
|
+
* The supporting line under a `PageHeader` or `Dialog` title: `Text as="p"` without the measure, so
|
|
113
|
+
* it runs the width of its header. Internal; not exported from the package.
|
|
114
|
+
*/
|
|
115
|
+
export declare function HeaderText({ children }: {
|
|
116
|
+
children: ReactNode;
|
|
117
|
+
}): ReactElement;
|
|
118
|
+
/** Fine print / metadata. `span` (inline; `as="p"` for a block), text-xs/4 normal, tertiary. Takes `tone`, `numeric`, `mono` and `truncate` like `Text`. */
|
|
119
|
+
export declare const Caption: {
|
|
120
|
+
<E extends ElementType = "span">(props: InlineTypographyProps<E>): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
121
|
+
displayName: string;
|
|
122
|
+
};
|
|
123
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Children, } from "react";
|
|
3
|
+
import { cx } from "./cx.js";
|
|
4
|
+
/** Colour token → slate/violet utility, which a theme remaps. */
|
|
5
|
+
const COLOR = {
|
|
6
|
+
default: "text-slate-900",
|
|
7
|
+
secondary: "text-slate-600",
|
|
8
|
+
tertiary: "text-slate-500",
|
|
9
|
+
muted: "text-slate-400",
|
|
10
|
+
brand: "text-violet-600",
|
|
11
|
+
};
|
|
12
|
+
/** Tone → text utility. Shared with `Table`'s cell tones, so the two never drift. */
|
|
13
|
+
export const TEXT_TONE = {
|
|
14
|
+
muted: "text-slate-500",
|
|
15
|
+
info: "text-blue-700",
|
|
16
|
+
positive: "text-emerald-700",
|
|
17
|
+
warn: "text-amber-700",
|
|
18
|
+
danger: "text-red-700",
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Build a semantic component that pins `variantClass` (size + weight + tracking)
|
|
22
|
+
* and a default colour. The scale/weight for each role therefore lives in exactly
|
|
23
|
+
* one place (the call below), and callers can only vary the colour and the element —
|
|
24
|
+
* never the pinned size/weight, and never with a class.
|
|
25
|
+
*/
|
|
26
|
+
function typography(displayName, defaultAs, variantClass, defaultColor) {
|
|
27
|
+
function Typography(props) {
|
|
28
|
+
return pinned(defaultAs, variantClass, defaultColor, props);
|
|
29
|
+
}
|
|
30
|
+
Typography.displayName = displayName;
|
|
31
|
+
return Typography;
|
|
32
|
+
}
|
|
33
|
+
/** `typography` for the inline roles, which also take `tone`, `numeric`, `mono` and `truncate`. */
|
|
34
|
+
function inlineRole(displayName, defaultAs, variantClass, defaultColor) {
|
|
35
|
+
function Inline(props) {
|
|
36
|
+
return pinned(defaultAs, variantClass, defaultColor, props);
|
|
37
|
+
}
|
|
38
|
+
Inline.displayName = displayName;
|
|
39
|
+
return Inline;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The one place a pinned role renders. The few roles with an opt-in prop of their own are
|
|
43
|
+
* written by hand, not by `typography`, and call this too.
|
|
44
|
+
*/
|
|
45
|
+
function pinned(defaultAs, variantClass, defaultColor, { as, color, tone, numeric, mono, truncate, children, ...rest }) {
|
|
46
|
+
const Tag = as ?? defaultAs;
|
|
47
|
+
return (_jsx(Tag, { ...rest, ...(truncate ? { title: rest.title ?? plainText(children) } : {}), className: cx(variantClass, tone ? TEXT_TONE[tone] : COLOR[color ?? defaultColor], numeric && "tabular-nums", mono && "font-mono", truncate && "inline-block min-w-0 truncate align-bottom"), children: children }));
|
|
48
|
+
}
|
|
49
|
+
/** The text of `children` when it is all strings and numbers: what a truncated run shows on hover. */
|
|
50
|
+
function plainText(children) {
|
|
51
|
+
const parts = Children.toArray(children);
|
|
52
|
+
return parts.every((part) => typeof part === "string" || typeof part === "number")
|
|
53
|
+
? parts.join("")
|
|
54
|
+
: undefined;
|
|
55
|
+
}
|
|
56
|
+
// --- Structural roles (block by default) -----------------------------------
|
|
57
|
+
/**
|
|
58
|
+
* Marketing-scale headline — the one line at the top of a public page. `h1`,
|
|
59
|
+
* text-4xl/tight semibold, primary text, growing to text-6xl on wider screens.
|
|
60
|
+
* `PageHeading`'s scale is deliberately dev-tool sized; a brochure
|
|
61
|
+
* hero needs a display scale, so it gets its own pinned role rather than callers
|
|
62
|
+
* overriding PageHeading's size.
|
|
63
|
+
*/
|
|
64
|
+
export const Display = typography("Display", "h1", "text-4xl/[1.1] font-semibold tracking-tight sm:text-5xl/[1.05] lg:text-6xl/[1.05]", "default");
|
|
65
|
+
/** The supporting sentence under a `Display`. `p`, text-lg/7 normal, secondary. */
|
|
66
|
+
export const Lead = typography("Lead", "p", "text-lg/7 font-normal sm:text-xl/8", "secondary");
|
|
67
|
+
/** Page title. `h1`, text-2xl/8 semibold, primary text. */
|
|
68
|
+
export const PageHeading = typography("PageHeading", "h1", "text-2xl/8 font-semibold tracking-tight", "default");
|
|
69
|
+
/**
|
|
70
|
+
* Top-level section header. `h2`, text-base/6 semibold, primary text. `rule` sets
|
|
71
|
+
* a hairline under it (the `Divider` line colour) with the space either side of
|
|
72
|
+
* the line built in.
|
|
73
|
+
*/
|
|
74
|
+
export function SectionHeading({ rule, ...props }) {
|
|
75
|
+
const variant = cx(rule && "mb-4 border-b border-slate-200/70 pb-2", "text-base/6 font-semibold");
|
|
76
|
+
return pinned("h2", variant, "default", props);
|
|
77
|
+
}
|
|
78
|
+
const SUBSECTION_SIZE = {
|
|
79
|
+
sm: "text-sm/5 font-semibold",
|
|
80
|
+
md: "text-base/6 font-semibold",
|
|
81
|
+
lg: "text-lg/7 font-semibold",
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Nested header. `h3`, text-base/6 semibold, primary text. `size` steps it down for a dense card
|
|
85
|
+
* or up for a drawer or form-group title: `sm` (text-sm), `md` (default) or `lg`. `truncate` keeps it on
|
|
86
|
+
* one line and cuts it with an ellipsis where its container ends, the full text as the `title`.
|
|
87
|
+
*/
|
|
88
|
+
export function SubsectionHeading({ size = "md", ...props }) {
|
|
89
|
+
return pinned("h3", SUBSECTION_SIZE[size], "default", props);
|
|
90
|
+
}
|
|
91
|
+
// --- Inline roles (inline by default) --------------------------------------
|
|
92
|
+
/**
|
|
93
|
+
* Small all-caps label above a group. `span`, text-xs semibold uppercase, tertiary. `tone`,
|
|
94
|
+
* `numeric`, `mono` and `truncate` are opt-in, as on `Text`.
|
|
95
|
+
*/
|
|
96
|
+
export const Overline = inlineRole("Overline", "span", "text-xs font-semibold uppercase tracking-wide", "tertiary");
|
|
97
|
+
/**
|
|
98
|
+
* Running text. `span` (inline; `as="p"` for a paragraph), text-sm/5 normal, secondary. A block of it
|
|
99
|
+
* stops at a readable line length (`max-w-[72ch]`), wherever it sits. `strong` emphasises it:
|
|
100
|
+
* font-medium in the primary ink, unless `color` or `tone` says otherwise.
|
|
101
|
+
* `preserveNewlines` keeps the line breaks in the text (a user-typed note); runs of spaces still collapse.
|
|
102
|
+
* `tone` colours it by meaning (`positive` for "Saved.", `danger` for an error; it wins over `color`),
|
|
103
|
+
* `numeric` sets tabular figures, `mono` the monospace face, and `truncate` cuts it to one line with an
|
|
104
|
+
* ellipsis (the full text becomes its `title`).
|
|
105
|
+
*/
|
|
106
|
+
export function Text({ strong, preserveNewlines, ...props }) {
|
|
107
|
+
return pinned("span", textVariant(strong, preserveNewlines, true), strong ? "default" : "secondary", props);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The supporting line under a `PageHeader` or `Dialog` title: `Text as="p"` without the measure, so
|
|
111
|
+
* it runs the width of its header. Internal; not exported from the package.
|
|
112
|
+
*/
|
|
113
|
+
export function HeaderText({ children }) {
|
|
114
|
+
return pinned("p", textVariant(false, false, false), "secondary", { children });
|
|
115
|
+
}
|
|
116
|
+
function textVariant(strong, preserveNewlines, measure) {
|
|
117
|
+
return cx(preserveNewlines && "whitespace-pre-line", measure && "max-w-[72ch]", "text-sm/5", strong ? "font-medium" : "font-normal");
|
|
118
|
+
}
|
|
119
|
+
/** Fine print / metadata. `span` (inline; `as="p"` for a block), text-xs/4 normal, tertiary. Takes `tone`, `numeric`, `mono` and `truncate` like `Text`. */
|
|
120
|
+
export const Caption = inlineRole("Caption", "span", "text-xs/4 font-normal", "tertiary");
|
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Passthrough } from "./props.js";
|
|
2
|
+
/** A coloured epic header row: a dot in the epic's colour + its label. */
|
|
3
|
+
export declare function EpicHeader({ color, label, ...rest }: {
|
|
4
|
+
/** The dot's colour, any CSS colour (the epic's own). */
|
|
5
|
+
color: string;
|
|
6
|
+
/** The epic's name. */
|
|
7
|
+
label: string;
|
|
8
|
+
} & Passthrough): import("react").JSX.Element;
|
|
9
|
+
/** Group items by their epic ref, ordered with real epics first (by ref) and "No epic" last. */
|
|
10
|
+
export declare function groupByEpic<T>(items: T[], epicOf: (t: T) => string | null): [string | null, T[]][];
|