@hackcove/ui 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/Breadcrumbs.d.ts +8 -0
- package/dist/Breadcrumbs.js +9 -0
- package/dist/Button.d.ts +15 -3
- package/dist/Button.js +31 -8
- package/dist/CopyButton.d.ts +7 -0
- package/dist/CopyButton.js +13 -0
- package/dist/CopyLine.d.ts +6 -0
- package/dist/CopyLine.js +8 -0
- package/dist/Dialog.d.ts +25 -0
- package/dist/Dialog.js +19 -0
- package/dist/FieldDialog.d.ts +20 -0
- package/dist/FieldDialog.js +32 -0
- package/dist/Kbd.d.ts +5 -0
- package/dist/Kbd.js +6 -0
- package/dist/KeyValueList.d.ts +10 -0
- package/dist/KeyValueList.js +10 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ export const Example = () => (
|
|
|
40
40
|
- Typography roles (each pins size + weight, takes a `color` and an `as`): `Display`, `Lead`,
|
|
41
41
|
`PageHeading`, `SectionHeading`, `SubsectionHeading`, `ModalHeading`, `Subtitle`, `Overline`,
|
|
42
42
|
`Body`, `BodyStrong`, `Caption`.
|
|
43
|
-
- Controls: `Button`, `buttonClass` (the classes of a `Button`, for a router's own `<Link>`), `IconButton` (a square button holding one icon; `label` is required and becomes its `aria-label` and `title`), `Input` (a text input; `icon` adds a leading icon inside the field), `Textarea` (a multi-line text input, `rows` defaults to 3), `Select` (a native `<select>` in the control look; takes `<option>` children and keeps the browser's arrow). `Checkbox` (a native checkbox with its label; `className` goes on the label, other props on the input). `Field` (a label above a control, `htmlFor` names the control's id; a `hint` sits under it, and an `error` replaces the hint with a `role="alert"` line). `Callout` (a tinted notice box; `tone` is `neutral`, `info` (default), `positive`, `warn` or `danger`; an optional `title` sits in bold above the body). `NavTabs` (a row of pill links in a `<nav>`; `items` are `{ label, href, active? }`, `label` names the landmark, the active one is filled and gets `aria-current="page"`). `navTabClass` (the classes of one tab, for a router's own `<Link>`). `ToggleChip` (an on/off filter pill; `pressed` sets `aria-pressed` and the filled look, `dot` adds a leading colour dot, and the caller toggles it in `onClick`). `FilterGroup` (a `<fieldset>` with a small uppercase `legend` over a wrapped row of its children, e.g. `ToggleChip`s; `className` goes on the fieldset).
|
|
43
|
+
- Controls: `Button`, `buttonClass` (the classes of a `Button`, for a router's own `<Link>`), `IconButton` (a square button holding one icon; `label` is required and becomes its `aria-label` and `title`), `Input` (a text input; `icon` adds a leading icon inside the field), `Textarea` (a multi-line text input, `rows` defaults to 3), `Select` (a native `<select>` in the control look; takes `<option>` children and keeps the browser's arrow). `Checkbox` (a native checkbox with its label; `className` goes on the label, other props on the input). `CopyButton` (a small text button that writes `text` to the clipboard and swaps `label` ("copy") for `copiedLabel` ("copied"); every other button prop, `disabled` included, goes on the `<button>`). `CopyLine` (one line of mono text in a box with a `CopyButton` beside it; other props go on the root `<div>`). `Field` (a label above a control, `htmlFor` names the control's id; a `hint` sits under it, and an `error` replaces the hint with a `role="alert"` line). `Callout` (a tinted notice box; `tone` is `neutral`, `info` (default), `positive`, `warn` or `danger`; an optional `title` sits in bold above the body). `NavTabs` (a row of pill links in a `<nav>`; `items` are `{ label, href, active? }`, `label` names the landmark, the active one is filled and gets `aria-current="page"`). `navTabClass` (the classes of one tab, for a router's own `<Link>`). `Breadcrumbs` (a `<nav aria-label="Breadcrumb">` over an ordered list; `crumbs` are nodes, the last is the current page and gets `aria-current="page"`, and each crumb truncates). `crumbLinkClass` (the classes of a crumb that links, for a router's own `<Link>`). `KeyValueList` (a `<dl>` of label/value rows; each `KeyValue` takes a `label` and its value as children, the value right-aligned in tabular figures). `Kbd` (a keyboard key in mono; its border takes the surrounding text colour, so it reads on a filled button too). `ToggleChip` (an on/off filter pill; `pressed` sets `aria-pressed` and the filled look, `dot` adds a leading colour dot, and the caller toggles it in `onClick`). `FilterGroup` (a `<fieldset>` with a small uppercase `legend` over a wrapped row of its children, e.g. `ToggleChip`s; `className` goes on the fieldset).
|
|
44
44
|
|
|
45
45
|
## Palette
|
|
46
46
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
/** The classes of a crumb that links somewhere, for a router's own <Link>. The current page is plain text, not a link. */
|
|
3
|
+
export declare const crumbLinkClass: string;
|
|
4
|
+
/** `org / project / build / snapshot`: each crumb truncates, and the last is the current page. */
|
|
5
|
+
export declare function Breadcrumbs({ crumbs, className, }: {
|
|
6
|
+
crumbs: ReactNode[];
|
|
7
|
+
className?: string;
|
|
8
|
+
}): ReactElement;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx, focusRing } from "./cx.js";
|
|
3
|
+
/** The classes of a crumb that links somewhere, for a router's own <Link>. The current page is plain text, not a link. */
|
|
4
|
+
export const crumbLinkClass = cx("rounded text-slate-500 hover:underline", focusRing);
|
|
5
|
+
/** `org / project / build / snapshot`: each crumb truncates, and the last is the current page. */
|
|
6
|
+
export function Breadcrumbs({ crumbs, className, }) {
|
|
7
|
+
const last = crumbs.length - 1;
|
|
8
|
+
return (_jsx("nav", { "aria-label": "Breadcrumb", className: cx("min-w-0", className), children: _jsx("ol", { className: "flex min-w-0 items-center gap-2 text-sm", children: crumbs.map((crumb, index) => (_jsxs("li", { "aria-current": index === last ? "page" : undefined, className: "flex min-w-0 items-center gap-2", children: [index > 0 && (_jsx("span", { "aria-hidden": "true", className: "text-slate-400", children: "/" })), _jsx("span", { className: cx("min-w-0 truncate", index === last ? "font-medium text-slate-900" : "text-slate-500"), children: crumb })] }, index))) }) }));
|
|
9
|
+
}
|
package/dist/Button.d.ts
CHANGED
|
@@ -2,7 +2,19 @@ import type { ComponentPropsWithoutRef, ReactElement } from "react";
|
|
|
2
2
|
export type ButtonVariant = "primary" | "outline" | "ghost";
|
|
3
3
|
/** The classes of a Button, for a router's own <Link> that should look like one. */
|
|
4
4
|
export declare function buttonClass(variant?: ButtonVariant): string;
|
|
5
|
-
|
|
6
|
-
export declare function Button({ variant, className, ...rest }: {
|
|
5
|
+
type ButtonProps = {
|
|
7
6
|
variant?: ButtonVariant;
|
|
8
|
-
|
|
7
|
+
/** Why the button cannot be pressed: disables it, sets it as the `title`, and greys it flat. */
|
|
8
|
+
disabledReason?: string | undefined;
|
|
9
|
+
/** The work is in flight: disabled, `aria-busy`, keeps its colour. Wins over `disabledReason`. */
|
|
10
|
+
busy?: boolean | undefined;
|
|
11
|
+
/** The label while `busy`, in place of the children. */
|
|
12
|
+
busyLabel?: string | undefined;
|
|
13
|
+
} & ComponentPropsWithoutRef<"button">;
|
|
14
|
+
/**
|
|
15
|
+
* A button in the primary, outline or ghost style. Defaults to `type="button"`. `disabledReason`
|
|
16
|
+
* and `busy` are the two disabled states that say more than a plain `disabled`, which keeps its
|
|
17
|
+
* half-opacity look.
|
|
18
|
+
*/
|
|
19
|
+
export declare function Button({ variant, disabledReason, busy, busyLabel, className, children, title, disabled, ...rest }: ButtonProps): ReactElement;
|
|
20
|
+
export {};
|
package/dist/Button.js
CHANGED
|
@@ -1,16 +1,39 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx, focusRing } from "./cx.js";
|
|
3
|
-
const
|
|
3
|
+
const SHAPE = "inline-flex items-center justify-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition";
|
|
4
|
+
// A plain `disabled` washes out and ignores the pointer. Blocked and busy buttons are disabled too,
|
|
5
|
+
// but they keep pointer events (the title shows, the cursor says why) and do not take this.
|
|
6
|
+
const DISABLED_PLAIN = "disabled:pointer-events-none disabled:opacity-50";
|
|
4
7
|
const VARIANT = {
|
|
5
|
-
primary:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
primary: {
|
|
9
|
+
color: "border-violet-600 bg-violet-600 text-violet-50",
|
|
10
|
+
hover: "hover:bg-violet-700",
|
|
11
|
+
},
|
|
12
|
+
outline: { color: "border-violet-600 text-violet-600", hover: "hover:bg-violet-50" },
|
|
13
|
+
ghost: { color: "border-transparent text-slate-600", hover: "hover:bg-slate-100" },
|
|
8
14
|
};
|
|
15
|
+
// Blocked replaces the variant's colour and hover rather than overriding them, so no `!` is needed.
|
|
16
|
+
const BLOCKED = "cursor-not-allowed border-slate-200 bg-slate-100 text-slate-400";
|
|
17
|
+
const BUSY = "cursor-progress opacity-85";
|
|
9
18
|
/** The classes of a Button, for a router's own <Link> that should look like one. */
|
|
10
19
|
export function buttonClass(variant = "primary") {
|
|
11
|
-
return cx(
|
|
20
|
+
return cx(SHAPE, DISABLED_PLAIN, focusRing, VARIANT[variant].color, VARIANT[variant].hover);
|
|
12
21
|
}
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
22
|
+
/** The classes of a Button in a given state: blocked swaps the colour out, busy keeps it. */
|
|
23
|
+
function stateClass(variant, blocked, busy) {
|
|
24
|
+
const { color } = VARIANT[variant];
|
|
25
|
+
if (blocked)
|
|
26
|
+
return cx(SHAPE, focusRing, BLOCKED);
|
|
27
|
+
if (busy)
|
|
28
|
+
return cx(SHAPE, focusRing, color, BUSY);
|
|
29
|
+
return buttonClass(variant);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A button in the primary, outline or ghost style. Defaults to `type="button"`. `disabledReason`
|
|
33
|
+
* and `busy` are the two disabled states that say more than a plain `disabled`, which keeps its
|
|
34
|
+
* half-opacity look.
|
|
35
|
+
*/
|
|
36
|
+
export function Button({ variant = "primary", disabledReason, busy = false, busyLabel, className, children, title, disabled, ...rest }) {
|
|
37
|
+
const blocked = disabledReason !== undefined && !busy;
|
|
38
|
+
return (_jsx("button", { type: "button", className: cx(stateClass(variant, blocked, busy), className), disabled: disabled || busy || blocked, "aria-busy": busy || undefined, title: blocked ? disabledReason : title, ...rest, children: busy && busyLabel ? busyLabel : children }));
|
|
16
39
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ReactElement } from "react";
|
|
2
|
+
/** Writes `text` to the clipboard and swaps the label to `copiedLabel`. Defaults to `type="button"`. A refused or missing clipboard leaves the label on `label`. Every other button prop, `disabled` included, goes on the `<button>`. */
|
|
3
|
+
export declare function CopyButton({ text, label, copiedLabel, onClick, className, ...rest }: {
|
|
4
|
+
text: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
copiedLabel?: string;
|
|
7
|
+
} & Omit<ComponentPropsWithoutRef<"button">, "children">): ReactElement;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { cx, focusRing } from "./cx.js";
|
|
4
|
+
const BASE = `shrink-0 rounded px-1.5 py-0.5 font-sans text-xs text-slate-400 hover:bg-slate-100 disabled:pointer-events-none disabled:opacity-50 ${focusRing}`;
|
|
5
|
+
/** Writes `text` to the clipboard and swaps the label to `copiedLabel`. Defaults to `type="button"`. A refused or missing clipboard leaves the label on `label`. Every other button prop, `disabled` included, goes on the `<button>`. */
|
|
6
|
+
export function CopyButton({ text, label = "copy", copiedLabel = "copied", onClick, className, ...rest }) {
|
|
7
|
+
const [copied, setCopied] = useState(false);
|
|
8
|
+
return (_jsx("button", { type: "button", ...rest, className: cx(BASE, className), onClick: (event) => {
|
|
9
|
+
onClick?.(event);
|
|
10
|
+
// The rejection is swallowed on purpose: a denied write is not an error the page can act on.
|
|
11
|
+
navigator.clipboard?.writeText(text).then(() => setCopied(true), () => { });
|
|
12
|
+
}, children: copied ? copiedLabel : label }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactElement } from "react";
|
|
2
|
+
/** One line of mono text in a box, with a `CopyButton`: a command or a token to paste elsewhere. `className` and every other div prop go on the root `<div>`, so `data-testid` lands there; the `<code>` holds only `text`. */
|
|
3
|
+
export declare function CopyLine({ text, className, ...rest }: {
|
|
4
|
+
text: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
} & Omit<ComponentPropsWithoutRef<"div">, "children">): ReactElement;
|
package/dist/CopyLine.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CopyButton } from "./CopyButton.js";
|
|
3
|
+
import { cx } from "./cx.js";
|
|
4
|
+
const BOX = "flex items-center justify-between gap-3 rounded-lg border border-slate-200 bg-white px-2.5 py-2 font-mono text-[12.5px] text-slate-900";
|
|
5
|
+
/** One line of mono text in a box, with a `CopyButton`: a command or a token to paste elsewhere. `className` and every other div prop go on the root `<div>`, so `data-testid` lands there; the `<code>` holds only `text`. */
|
|
6
|
+
export function CopyLine({ text, className, ...rest }) {
|
|
7
|
+
return (_jsxs("div", { ...rest, className: cx(BOX, className), children: [_jsx("code", { className: "min-w-0 break-words", children: text }), _jsx(CopyButton, { text: text })] }));
|
|
8
|
+
}
|
package/dist/Dialog.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
+
declare const SIZE: {
|
|
3
|
+
readonly sm: "w-80";
|
|
4
|
+
readonly md: "w-110";
|
|
5
|
+
};
|
|
6
|
+
export type DialogProps = {
|
|
7
|
+
/** The dialog's accessible name, shown as its heading. */
|
|
8
|
+
title: ReactNode;
|
|
9
|
+
description?: ReactNode;
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
/** Actions under the body, right-aligned. A submit button here can reach a form in the body with its `form` attribute. */
|
|
12
|
+
footer?: ReactNode;
|
|
13
|
+
/** `sm` is a list; `md` (the default) holds a form. */
|
|
14
|
+
size?: keyof typeof SIZE;
|
|
15
|
+
/** Called when the dialog closes: Esc, a click on the backdrop, or a `<form method="dialog">` submit. */
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
className?: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A modal on the browser's own `<dialog>`: it is opened with `showModal()` on mount, so render it
|
|
21
|
+
* only while it is open. The page behind goes inert, Esc closes it and focus stays inside; a click
|
|
22
|
+
* on the backdrop (the `<dialog>` element itself, not its contents) closes it too.
|
|
23
|
+
*/
|
|
24
|
+
export declare function Dialog({ title, description, children, footer, size, onClose, className, }: DialogProps): ReactElement;
|
|
25
|
+
export {};
|
package/dist/Dialog.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useRef } from "react";
|
|
3
|
+
import { cx } from "./cx.js";
|
|
4
|
+
import { Subtitle, ModalHeading } from "./typography.js";
|
|
5
|
+
const PANEL = "mx-auto mt-28 max-w-[calc(100vw-2rem)] rounded-2xl border border-slate-200 bg-white p-0 text-slate-900 shadow-2xl backdrop:bg-slate-900/35";
|
|
6
|
+
const SIZE = { sm: "w-80", md: "w-110" };
|
|
7
|
+
/**
|
|
8
|
+
* A modal on the browser's own `<dialog>`: it is opened with `showModal()` on mount, so render it
|
|
9
|
+
* only while it is open. The page behind goes inert, Esc closes it and focus stays inside; a click
|
|
10
|
+
* on the backdrop (the `<dialog>` element itself, not its contents) closes it too.
|
|
11
|
+
*/
|
|
12
|
+
export function Dialog({ title, description, children, footer, size = "md", onClose, className, }) {
|
|
13
|
+
const dialog = useRef(null);
|
|
14
|
+
const titleId = useId();
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
dialog.current?.showModal();
|
|
17
|
+
}, []);
|
|
18
|
+
return (_jsx("dialog", { ref: dialog, "aria-labelledby": titleId, onClose: onClose, onClick: (event) => event.target === dialog.current && dialog.current?.close(), className: cx(PANEL, SIZE[size], className), children: _jsxs("div", { className: "p-5.5", children: [_jsx(ModalHeading, { id: titleId, children: title }), description && _jsx(Subtitle, { className: "mt-1", children: description }), children && _jsx("div", { className: "mt-4", children: children }), footer && _jsx("div", { className: "mt-5 flex justify-end gap-2", children: footer })] }) }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
|
+
export type FieldDialogProps = {
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
/** The visible label above the field. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** The field's accessible name, when it says more than the visible label. */
|
|
8
|
+
inputLabel: string;
|
|
9
|
+
placeholder: string;
|
|
10
|
+
hint: string;
|
|
11
|
+
/** Said when the field is empty and the submit button is blocked. */
|
|
12
|
+
emptyReason: string;
|
|
13
|
+
submit: string;
|
|
14
|
+
busyLabel: string;
|
|
15
|
+
/** Gets the trimmed value. Resolving closes the dialog; a rejection shows its message in it. */
|
|
16
|
+
onSubmit: (value: string) => Promise<unknown>;
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
};
|
|
19
|
+
/** A `Dialog` with one text field and Cancel / submit buttons. Render it only while open. */
|
|
20
|
+
export declare function FieldDialog({ title, description, label, inputLabel, placeholder, hint, emptyReason, submit, busyLabel, onSubmit, onClose, }: FieldDialogProps): ReactElement;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId, useState } from "react";
|
|
3
|
+
import { Button } from "./Button.js";
|
|
4
|
+
import { Dialog } from "./Dialog.js";
|
|
5
|
+
import { Field } from "./Field.js";
|
|
6
|
+
import { Input } from "./Input.js";
|
|
7
|
+
/** The field's value and its in-flight state; `send` is the form's submit handler. */
|
|
8
|
+
function useFieldSubmit(onSubmit, onClose) {
|
|
9
|
+
const [value, setValue] = useState("");
|
|
10
|
+
const [busy, setBusy] = useState(false);
|
|
11
|
+
const [error, setError] = useState(null);
|
|
12
|
+
const empty = value.trim().length === 0;
|
|
13
|
+
const send = (event) => {
|
|
14
|
+
event.preventDefault();
|
|
15
|
+
if (empty || busy)
|
|
16
|
+
return;
|
|
17
|
+
setError(null);
|
|
18
|
+
setBusy(true);
|
|
19
|
+
void onSubmit(value.trim()).then(onClose, (e) => {
|
|
20
|
+
setBusy(false);
|
|
21
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
return { value, setValue, busy, error, empty, send };
|
|
25
|
+
}
|
|
26
|
+
/** A `Dialog` with one text field and Cancel / submit buttons. Render it only while open. */
|
|
27
|
+
export function FieldDialog({ title, description, label, inputLabel, placeholder, hint, emptyReason, submit, busyLabel, onSubmit, onClose, }) {
|
|
28
|
+
const id = useId();
|
|
29
|
+
const formId = `${id}-form`;
|
|
30
|
+
const { value, setValue, busy, error, empty, send } = useFieldSubmit(onSubmit, onClose);
|
|
31
|
+
return (_jsx(Dialog, { title: title, description: description, onClose: onClose, footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", onClick: (event) => event.currentTarget.closest("dialog")?.close(), children: "Cancel" }), _jsx(Button, { type: "submit", form: formId, disabledReason: empty ? emptyReason : undefined, busy: busy, busyLabel: busyLabel, children: submit })] }), children: _jsx("form", { id: formId, onSubmit: send, children: _jsx(Field, { label: label, htmlFor: id, hint: empty ? emptyReason : hint, error: error ?? undefined, children: _jsx(Input, { autoFocus: true, id: id, "aria-label": inputLabel, placeholder: placeholder, value: value, onChange: (e) => setValue(e.target.value), autoComplete: "off" }) }) }) }));
|
|
32
|
+
}
|
package/dist/Kbd.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactElement, ReactNode } from "react";
|
|
2
|
+
/** A keyboard key. Its border takes the surrounding text colour, so it reads on a filled button and on plain text. */
|
|
3
|
+
export declare function Kbd({ children, className, ...rest }: {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
} & HTMLAttributes<HTMLElement>): ReactElement;
|
package/dist/Kbd.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/** A keyboard key. Its border takes the surrounding text colour, so it reads on a filled button and on plain text. */
|
|
4
|
+
export function Kbd({ children, className, ...rest }) {
|
|
5
|
+
return (_jsx("kbd", { className: cx("rounded border border-current/30 px-1 font-mono text-xs leading-4", className), ...rest, children: children }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactElement, ReactNode } from "react";
|
|
2
|
+
/** A definition list of label/value rows: each label on the left, its value right-aligned. */
|
|
3
|
+
export declare function KeyValueList({ className, children, ...rest }: {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
} & HTMLAttributes<HTMLDListElement>): ReactElement;
|
|
6
|
+
/** One row of a KeyValueList: a label on the left, a right-aligned tabular value on the right. */
|
|
7
|
+
export declare function KeyValue({ label, children, className, ...rest }: {
|
|
8
|
+
label: ReactNode;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
} & HTMLAttributes<HTMLDivElement>): ReactElement;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/** A definition list of label/value rows: each label on the left, its value right-aligned. */
|
|
4
|
+
export function KeyValueList({ className, children, ...rest }) {
|
|
5
|
+
return (_jsx("dl", { className: cx("flex flex-col gap-1.5 text-sm", className), ...rest, children: children }));
|
|
6
|
+
}
|
|
7
|
+
/** One row of a KeyValueList: a label on the left, a right-aligned tabular value on the right. */
|
|
8
|
+
export function KeyValue({ label, children, className, ...rest }) {
|
|
9
|
+
return (_jsxs("div", { className: cx("flex justify-between gap-3", className), ...rest, children: [_jsx("dt", { className: "text-slate-600", children: label }), _jsx("dd", { className: "text-right tabular-nums", children: children })] }));
|
|
10
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
export { Card, EpicHeader, Empty, PageTitle, Pill, groupByEpic } from "./ui.js";
|
|
2
2
|
export { Markdown } from "./Markdown.js";
|
|
3
3
|
export { Button, buttonClass, type ButtonVariant } from "./Button.js";
|
|
4
|
+
export { Breadcrumbs, crumbLinkClass } from "./Breadcrumbs.js";
|
|
4
5
|
export { Callout, type CalloutTone } from "./Callout.js";
|
|
6
|
+
export { Dialog, type DialogProps } from "./Dialog.js";
|
|
5
7
|
export { IconButton } from "./IconButton.js";
|
|
6
8
|
export { NavTabs, navTabClass, type NavTab } from "./NavTabs.js";
|
|
7
9
|
export { ToggleChip } from "./ToggleChip.js";
|
|
8
10
|
export { FilterGroup } from "./FilterGroup.js";
|
|
9
11
|
export { Checkbox } from "./Checkbox.js";
|
|
12
|
+
export { CopyButton } from "./CopyButton.js";
|
|
13
|
+
export { CopyLine } from "./CopyLine.js";
|
|
10
14
|
export { Field } from "./Field.js";
|
|
15
|
+
export { FieldDialog, type FieldDialogProps } from "./FieldDialog.js";
|
|
11
16
|
export { Input } from "./Input.js";
|
|
17
|
+
export { Kbd } from "./Kbd.js";
|
|
18
|
+
export { KeyValue, KeyValueList } from "./KeyValueList.js";
|
|
12
19
|
export { Select } from "./Select.js";
|
|
13
20
|
export { Textarea } from "./Textarea.js";
|
|
14
21
|
export { Body, BodyStrong, Caption, Display, Lead, ModalHeading, Overline, PageHeading, SectionHeading, SubsectionHeading, Subtitle, type TypographyColor, type TypographyProps, } from "./typography.js";
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
export { Card, EpicHeader, Empty, PageTitle, Pill, groupByEpic } from "./ui.js";
|
|
2
2
|
export { Markdown } from "./Markdown.js";
|
|
3
3
|
export { Button, buttonClass } from "./Button.js";
|
|
4
|
+
export { Breadcrumbs, crumbLinkClass } from "./Breadcrumbs.js";
|
|
4
5
|
export { Callout } from "./Callout.js";
|
|
6
|
+
export { Dialog } from "./Dialog.js";
|
|
5
7
|
export { IconButton } from "./IconButton.js";
|
|
6
8
|
export { NavTabs, navTabClass } from "./NavTabs.js";
|
|
7
9
|
export { ToggleChip } from "./ToggleChip.js";
|
|
8
10
|
export { FilterGroup } from "./FilterGroup.js";
|
|
9
11
|
export { Checkbox } from "./Checkbox.js";
|
|
12
|
+
export { CopyButton } from "./CopyButton.js";
|
|
13
|
+
export { CopyLine } from "./CopyLine.js";
|
|
10
14
|
export { Field } from "./Field.js";
|
|
15
|
+
export { FieldDialog } from "./FieldDialog.js";
|
|
11
16
|
export { Input } from "./Input.js";
|
|
17
|
+
export { Kbd } from "./Kbd.js";
|
|
18
|
+
export { KeyValue, KeyValueList } from "./KeyValueList.js";
|
|
12
19
|
export { Select } from "./Select.js";
|
|
13
20
|
export { Textarea } from "./Textarea.js";
|
|
14
21
|
export { Body, BodyStrong, Caption, Display, Lead, ModalHeading, Overline, PageHeading, SectionHeading, SubsectionHeading, Subtitle, } from "./typography.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hackcove/ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Hack Cove React components: primitives, typography roles and a safe Markdown renderer, written against Tailwind v4 utilities. Pair with a @hackcove/theme-* package.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"@types/bun": "1.4.3",
|
|
34
34
|
"@types/react": "^19.3.0",
|
|
35
35
|
"@types/react-dom": "^19.3.0",
|
|
36
|
+
"jsdom": "^30.1.3",
|
|
36
37
|
"react": "^19.3.1",
|
|
37
38
|
"react-dom": "^19.3.1",
|
|
38
39
|
"tailwindcss": "^4.3.3",
|