@hackcove/ui 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +68 -2
- package/dist/Button.d.ts +8 -0
- package/dist/Button.js +16 -0
- package/dist/Callout.d.ts +10 -0
- package/dist/Callout.js +13 -0
- package/dist/Checkbox.d.ts +5 -0
- package/dist/Checkbox.js +6 -0
- package/dist/Field.d.ts +12 -0
- package/dist/Field.js +13 -0
- package/dist/FilterGroup.d.ts +5 -0
- package/dist/FilterGroup.js +6 -0
- package/dist/IconButton.d.ts +6 -0
- package/dist/IconButton.js +7 -0
- package/dist/Input.d.ts +5 -0
- package/dist/Input.js +10 -0
- package/dist/Markdown.d.ts +3 -0
- package/dist/Markdown.js +39 -0
- package/dist/NavTabs.d.ts +16 -0
- package/dist/NavTabs.js +11 -0
- package/dist/Select.d.ts +3 -0
- package/dist/Select.js +7 -0
- package/dist/Textarea.d.ts +3 -0
- package/dist/Textarea.js +7 -0
- package/dist/ToggleChip.d.ts +7 -0
- package/dist/ToggleChip.js +9 -0
- package/dist/control.d.ts +2 -0
- package/dist/control.js +3 -0
- package/dist/cx.d.ts +4 -0
- package/dist/cx.js +6 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/typography.d.ts +75 -0
- package/dist/typography.js +54 -0
- package/dist/ui.d.ts +29 -0
- package/dist/ui.js +49 -0
- package/package.json +56 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jacobus Langner
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,69 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @hackcove/ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Hack Cove React components, written against Tailwind v4 utilities. They carry no theme: pair them
|
|
4
|
+
with a `@hackcove/theme-*` package such as `@hackcove/theme-catppuccin-mocha`, or use them on their
|
|
5
|
+
own for the default light look.
|
|
6
|
+
|
|
7
|
+
Status: `0.x`, published to npm. License: MIT.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
bun add @hackcove/ui
|
|
13
|
+
# peers: react >= 18, tailwindcss ^4
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Tailwind skips `node_modules`, so tell it to scan the build for the utilities the components use,
|
|
17
|
+
in the stylesheet that imports Tailwind (the path is relative to that file):
|
|
18
|
+
|
|
19
|
+
```css
|
|
20
|
+
@import "tailwindcss";
|
|
21
|
+
@source "../node_modules/@hackcove/ui/dist";
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Use
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { Body, Card, PageHeading, Pill } from "@hackcove/ui";
|
|
28
|
+
|
|
29
|
+
export const Example = () => (
|
|
30
|
+
<Card title="Status" count={2}>
|
|
31
|
+
<PageHeading>All good</PageHeading>
|
|
32
|
+
<Body>Two things shipped.</Body> <Pill tone="emerald">ok</Pill>
|
|
33
|
+
</Card>
|
|
34
|
+
);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## What is exported
|
|
38
|
+
|
|
39
|
+
- Primitives: `Card`, `EpicHeader`, `Empty`, `PageTitle`, `Pill`, `groupByEpic`, `Markdown`.
|
|
40
|
+
- Typography roles (each pins size + weight, takes a `color` and an `as`): `Display`, `Lead`,
|
|
41
|
+
`PageHeading`, `SectionHeading`, `SubsectionHeading`, `ModalHeading`, `Subtitle`, `Overline`,
|
|
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).
|
|
44
|
+
|
|
45
|
+
## Palette
|
|
46
|
+
|
|
47
|
+
`palette.json` (in the repo, not in the package) lists the colour utilities the components may use,
|
|
48
|
+
by role.
|
|
49
|
+
|
|
50
|
+
| Role | For |
|
|
51
|
+
| ---------- | --------------------------------------------------------------------- |
|
|
52
|
+
| `surface` | Cards and panels: the main background. |
|
|
53
|
+
| `subtle` | A step off the surface: table stripes, hover rows. |
|
|
54
|
+
| `inset` | A step deeper: wells, chips, pressed fills. |
|
|
55
|
+
| `line` | Borders and dividers. |
|
|
56
|
+
| `ink` | Text, from primary down to muted, plus input placeholders. |
|
|
57
|
+
| `accent` | Primary actions, links, focus rings and controls; text on the accent. |
|
|
58
|
+
| `positive` | Success: fill, text and border. |
|
|
59
|
+
| `warn` | Caution: fill, text and border. |
|
|
60
|
+
| `danger` | Errors and destructive actions: fill, text and border. |
|
|
61
|
+
| `info` | Neutral notes: fill, text and border. |
|
|
62
|
+
|
|
63
|
+
A theme remaps every utility in `palette.json`; a component uses no colour utility outside it.
|
|
64
|
+
|
|
65
|
+
## Develop
|
|
66
|
+
|
|
67
|
+
From the workspace root: `bunx nx run-many -t typecheck lint test build -p ui` and
|
|
68
|
+
`bunx nx pack-check ui` (the tarball is `dist/` plus `package.json`, `README.md`, `LICENSE`).
|
|
69
|
+
The stories live in `apps/storybook`.
|
package/dist/Button.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactElement } from "react";
|
|
2
|
+
export type ButtonVariant = "primary" | "outline" | "ghost";
|
|
3
|
+
/** The classes of a Button, for a router's own <Link> that should look like one. */
|
|
4
|
+
export declare function buttonClass(variant?: ButtonVariant): string;
|
|
5
|
+
/** A button in the primary, outline or ghost style. Defaults to `type="button"`. */
|
|
6
|
+
export declare function Button({ variant, className, ...rest }: {
|
|
7
|
+
variant?: ButtonVariant;
|
|
8
|
+
} & ComponentPropsWithoutRef<"button">): ReactElement;
|
package/dist/Button.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx, focusRing } from "./cx.js";
|
|
3
|
+
const BASE = `inline-flex items-center justify-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition disabled:pointer-events-none disabled:opacity-50 ${focusRing}`;
|
|
4
|
+
const VARIANT = {
|
|
5
|
+
primary: "border-violet-600 bg-violet-600 text-violet-50 hover:bg-violet-700",
|
|
6
|
+
outline: "border-violet-600 text-violet-600 hover:bg-violet-50",
|
|
7
|
+
ghost: "border-transparent text-slate-600 hover:bg-slate-100",
|
|
8
|
+
};
|
|
9
|
+
/** The classes of a Button, for a router's own <Link> that should look like one. */
|
|
10
|
+
export function buttonClass(variant = "primary") {
|
|
11
|
+
return cx(BASE, VARIANT[variant]);
|
|
12
|
+
}
|
|
13
|
+
/** A button in the primary, outline or ghost style. Defaults to `type="button"`. */
|
|
14
|
+
export function Button({ variant = "primary", className, ...rest }) {
|
|
15
|
+
return _jsx("button", { type: "button", className: cx(buttonClass(variant), className), ...rest });
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactElement, ReactNode } from "react";
|
|
2
|
+
/** The colour family of a callout. */
|
|
3
|
+
export type CalloutTone = "neutral" | "info" | "positive" | "warn" | "danger";
|
|
4
|
+
/** A tinted notice box, with an optional bold title above its body. */
|
|
5
|
+
export declare function Callout({ tone, title, children, className, ...rest }: {
|
|
6
|
+
/** Defaults to "info". */
|
|
7
|
+
tone?: CalloutTone;
|
|
8
|
+
title?: ReactNode;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
} & Omit<HTMLAttributes<HTMLDivElement>, "title">): ReactElement;
|
package/dist/Callout.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
const TONES = {
|
|
4
|
+
neutral: "border-slate-200 bg-slate-50 text-slate-700",
|
|
5
|
+
info: "border-blue-200 bg-blue-50 text-blue-800",
|
|
6
|
+
positive: "border-emerald-200 bg-emerald-50 text-emerald-800",
|
|
7
|
+
warn: "border-amber-200 bg-amber-50 text-amber-800",
|
|
8
|
+
danger: "border-red-200 bg-red-50 text-red-800",
|
|
9
|
+
};
|
|
10
|
+
/** A tinted notice box, with an optional bold title above its body. */
|
|
11
|
+
export function Callout({ tone = "info", title, children, className, ...rest }) {
|
|
12
|
+
return (_jsx("div", { className: cx("rounded-xl border p-4 text-sm", TONES[tone], className), ...rest, children: title ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "font-semibold", children: title }), _jsx("div", { className: "mt-1", children: children })] })) : (children) }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
|
2
|
+
/** A native checkbox with its label. `className` goes on the label; every other prop goes on the input. */
|
|
3
|
+
export declare function Checkbox({ label, className, ...rest }: {
|
|
4
|
+
label: ReactNode;
|
|
5
|
+
} & Omit<ComponentPropsWithoutRef<"input">, "type">): ReactElement;
|
package/dist/Checkbox.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx, focusRing } from "./cx.js";
|
|
3
|
+
/** A native checkbox with its label. `className` goes on the label; every other prop goes on the input. */
|
|
4
|
+
export function Checkbox({ label, className, ...rest }) {
|
|
5
|
+
return (_jsxs("label", { className: cx("inline-flex cursor-pointer items-center gap-2 text-sm text-slate-700", className), children: [_jsx("input", { type: "checkbox", className: cx("h-4 w-4 accent-violet-600", focusRing), ...rest }), label] }));
|
|
6
|
+
}
|
package/dist/Field.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
/** A label above a control, with a hint or an error line under it. An error replaces the hint. */
|
|
3
|
+
export declare function Field({ label, htmlFor, hint, error, children, className, }: {
|
|
4
|
+
label: ReactNode;
|
|
5
|
+
/** The id of the control inside; the label's htmlFor. */
|
|
6
|
+
htmlFor: string;
|
|
7
|
+
hint?: ReactNode;
|
|
8
|
+
/** Replaces the hint when set. */
|
|
9
|
+
error?: ReactNode;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
}): ReactElement;
|
package/dist/Field.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/** A label above a control, with a hint or an error line under it. An error replaces the hint. */
|
|
4
|
+
export function Field({ label, htmlFor, hint, error, children, className, }) {
|
|
5
|
+
let note = null;
|
|
6
|
+
if (error) {
|
|
7
|
+
note = (_jsx("p", { role: "alert", className: "text-xs text-red-700", children: error }));
|
|
8
|
+
}
|
|
9
|
+
else if (hint) {
|
|
10
|
+
note = _jsx("p", { className: "text-xs text-slate-500", children: hint });
|
|
11
|
+
}
|
|
12
|
+
return (_jsxs("div", { className: cx("flex flex-col gap-1.5", className), children: [_jsx("label", { htmlFor: htmlFor, className: "text-sm font-medium text-slate-700", children: label }), children, note] }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
|
2
|
+
/** A labelled filter section: a small uppercase legend over a wrapped row of chips (e.g. ToggleChip). Other props go on the fieldset. */
|
|
3
|
+
export declare function FilterGroup({ legend, children, className, ...rest }: {
|
|
4
|
+
legend: ReactNode;
|
|
5
|
+
} & ComponentPropsWithoutRef<"fieldset">): ReactElement;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/** A labelled filter section: a small uppercase legend over a wrapped row of chips (e.g. ToggleChip). Other props go on the fieldset. */
|
|
4
|
+
export function FilterGroup({ legend, children, className, ...rest }) {
|
|
5
|
+
return (_jsxs("fieldset", { className: cx("min-w-0", className), ...rest, children: [_jsx("legend", { className: "mb-2 text-xs font-medium tracking-wide text-slate-500 uppercase", children: legend }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: children })] }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
|
2
|
+
/** A square bordered button holding one icon. `label` is its accessible name and tooltip. Defaults to `type="button"`. */
|
|
3
|
+
export declare function IconButton({ label, className, children, ...rest }: {
|
|
4
|
+
label: string;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
} & Omit<ComponentPropsWithoutRef<"button">, "children">): ReactElement;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx, focusRing } from "./cx.js";
|
|
3
|
+
const BASE = `inline-flex h-9 w-9 items-center justify-center rounded-lg border border-slate-200 text-slate-500 transition hover:bg-slate-50 disabled:pointer-events-none disabled:opacity-50 ${focusRing}`;
|
|
4
|
+
/** A square bordered button holding one icon. `label` is its accessible name and tooltip. Defaults to `type="button"`. */
|
|
5
|
+
export function IconButton({ label, className, children, ...rest }) {
|
|
6
|
+
return (_jsx("button", { type: "button", ...rest, "aria-label": label, title: label, className: cx(BASE, className), children: children }));
|
|
7
|
+
}
|
package/dist/Input.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
|
2
|
+
/** A text input. `icon` puts a leading icon inside the field (e.g. a search glass). */
|
|
3
|
+
export declare function Input({ icon, className, ...rest }: {
|
|
4
|
+
icon?: ReactNode;
|
|
5
|
+
} & ComponentPropsWithoutRef<"input">): ReactElement;
|
package/dist/Input.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { controlClass } from "./control.js";
|
|
3
|
+
import { cx } from "./cx.js";
|
|
4
|
+
/** A text input. `icon` puts a leading icon inside the field (e.g. a search glass). */
|
|
5
|
+
export function Input({ icon, className, ...rest }) {
|
|
6
|
+
if (icon === undefined) {
|
|
7
|
+
return _jsx("input", { ...rest, className: cx(controlClass, "px-3", className) });
|
|
8
|
+
}
|
|
9
|
+
return (_jsxs("div", { className: "relative", children: [_jsx("span", { "aria-hidden": "true", className: "pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-slate-400 [&>svg]:h-4 [&>svg]:w-4", children: icon }), _jsx("input", { ...rest, className: cx(controlClass, "pr-3 pl-9", className) })] }));
|
|
10
|
+
}
|
package/dist/Markdown.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// A minimal, SAFE markdown renderer for the standup (headings / bullets / bold). No
|
|
3
|
+
// dangerouslySetInnerHTML — text is rendered as React nodes, so it can't inject HTML.
|
|
4
|
+
function inline(text) {
|
|
5
|
+
return text.split(/(\*\*[^*]+\*\*)/g).map((p, i) => p.startsWith("**") && p.endsWith("**") ? (_jsx("strong", { className: "font-semibold text-slate-900", children: p.slice(2, -2) }, i)) : (_jsx("span", { children: p }, i)));
|
|
6
|
+
}
|
|
7
|
+
export function Markdown({ md }) {
|
|
8
|
+
const out = [];
|
|
9
|
+
let bullets = [];
|
|
10
|
+
const flush = () => {
|
|
11
|
+
if (bullets.length) {
|
|
12
|
+
out.push(_jsx("ul", { className: "ml-4 list-disc space-y-1", children: bullets }, `u${out.length}`));
|
|
13
|
+
bullets = [];
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
md.split("\n").forEach((ln, i) => {
|
|
17
|
+
const t = ln.trim();
|
|
18
|
+
if (t.startsWith("## ")) {
|
|
19
|
+
flush();
|
|
20
|
+
out.push(_jsx("h2", { className: "mt-4 text-xs font-semibold uppercase tracking-wide text-slate-400 first:mt-0", children: t.slice(3) }, i));
|
|
21
|
+
}
|
|
22
|
+
else if (t.startsWith("### ")) {
|
|
23
|
+
flush();
|
|
24
|
+
out.push(_jsx("h3", { className: "mt-3 text-sm font-semibold text-slate-700", children: t.slice(4) }, i));
|
|
25
|
+
}
|
|
26
|
+
else if (t.startsWith("- ") || t.startsWith("* ")) {
|
|
27
|
+
bullets.push(_jsx("li", { className: "text-sm text-slate-700", children: inline(t.slice(2)) }, i));
|
|
28
|
+
}
|
|
29
|
+
else if (t === "") {
|
|
30
|
+
flush();
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
flush();
|
|
34
|
+
out.push(_jsx("p", { className: "text-sm text-slate-700", children: inline(t) }, i));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
flush();
|
|
38
|
+
return _jsx("div", { className: "space-y-1", children: out });
|
|
39
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
/** One pill link in a NavTabs row. */
|
|
3
|
+
export type NavTab = {
|
|
4
|
+
label: ReactNode;
|
|
5
|
+
href: string;
|
|
6
|
+
active?: boolean;
|
|
7
|
+
};
|
|
8
|
+
/** The classes of one tab, for a router's own <Link>. */
|
|
9
|
+
export declare function navTabClass(active: boolean): string;
|
|
10
|
+
/** A row of pill links where the active one is filled. Renders plain <a>; use navTabClass with a router's Link. */
|
|
11
|
+
export declare function NavTabs({ items, label, className, }: {
|
|
12
|
+
items: NavTab[];
|
|
13
|
+
/** The accessible name of the nav landmark. */
|
|
14
|
+
label: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
}): ReactElement;
|
package/dist/NavTabs.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx, focusRing } from "./cx.js";
|
|
3
|
+
const BASE = `rounded-lg px-3 py-1.5 transition ${focusRing}`;
|
|
4
|
+
/** The classes of one tab, for a router's own <Link>. */
|
|
5
|
+
export function navTabClass(active) {
|
|
6
|
+
return cx(BASE, active ? "bg-slate-100 font-medium text-slate-900" : "text-slate-600 hover:bg-slate-100");
|
|
7
|
+
}
|
|
8
|
+
/** A row of pill links where the active one is filled. Renders plain <a>; use navTabClass with a router's Link. */
|
|
9
|
+
export function NavTabs({ items, label, className, }) {
|
|
10
|
+
return (_jsx("nav", { "aria-label": label, className: cx("flex flex-wrap gap-1 text-sm", className), children: items.map((item) => (_jsx("a", { href: item.href, className: navTabClass(!!item.active), "aria-current": item.active ? "page" : undefined, children: item.label }, item.href))) }));
|
|
11
|
+
}
|
package/dist/Select.d.ts
ADDED
package/dist/Select.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { controlClass } from "./control.js";
|
|
3
|
+
import { cx } from "./cx.js";
|
|
4
|
+
/** A native `<select>` styled like `Input`, keeping the browser's own arrow. */
|
|
5
|
+
export function Select({ className, children, ...rest }) {
|
|
6
|
+
return (_jsx("select", { ...rest, className: cx(controlClass, "px-3", className), children: children }));
|
|
7
|
+
}
|
package/dist/Textarea.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { controlClass } from "./control.js";
|
|
3
|
+
import { cx } from "./cx.js";
|
|
4
|
+
/** A multi-line text input, styled like `Input`. `rows` defaults to 3. */
|
|
5
|
+
export function Textarea({ className, ...rest }) {
|
|
6
|
+
return _jsx("textarea", { rows: 3, ...rest, className: cx(controlClass, "px-3", className) });
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactElement } from "react";
|
|
2
|
+
/** An on/off pill button for filters. `pressed` sets its state; `dot` adds a leading colour dot. The caller toggles it in onClick. */
|
|
3
|
+
export declare function ToggleChip({ pressed, dot, className, children, ...rest }: {
|
|
4
|
+
pressed: boolean;
|
|
5
|
+
/** A CSS colour for a leading dot (e.g. a category colour); no dot when omitted. */
|
|
6
|
+
dot?: string;
|
|
7
|
+
} & ComponentPropsWithoutRef<"button">): 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
|
+
const BASE = `inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium transition ${focusRing}`;
|
|
4
|
+
const OFF = "border-slate-200 bg-white text-slate-600 hover:border-violet-400";
|
|
5
|
+
const ON = "border-violet-600 bg-violet-600 text-violet-50";
|
|
6
|
+
/** An on/off pill button for filters. `pressed` sets its state; `dot` adds a leading colour dot. The caller toggles it in onClick. */
|
|
7
|
+
export function ToggleChip({ pressed, dot, className, children, ...rest }) {
|
|
8
|
+
return (_jsxs("button", { type: "button", "aria-pressed": pressed, className: cx(BASE, pressed ? ON : OFF, className), ...rest, children: [dot && (_jsx("span", { "aria-hidden": "true", className: cx("h-1.5 w-1.5 rounded-full", pressed && "ring-1 ring-current"), style: { backgroundColor: dot } })), children] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Shared look of text controls; each control adds its own horizontal padding. */
|
|
2
|
+
export declare const controlClass = "w-full rounded-lg border border-slate-300 bg-white py-2 text-sm text-slate-900 placeholder:text-slate-400 transition hover:border-violet-400 disabled:opacity-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600";
|
package/dist/control.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { focusRing } from "./cx.js";
|
|
2
|
+
/** Shared look of text controls; each control adds its own horizontal padding. */
|
|
3
|
+
export const controlClass = `w-full rounded-lg border border-slate-300 bg-white py-2 text-sm text-slate-900 placeholder:text-slate-400 transition hover:border-violet-400 disabled:opacity-50 ${focusRing}`;
|
package/dist/cx.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Join class names, dropping falsy parts. */
|
|
2
|
+
export declare function cx(...parts: Array<string | false | null | undefined>): string;
|
|
3
|
+
/** The keyboard focus ring every interactive control shares. */
|
|
4
|
+
export declare const focusRing = "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600";
|
package/dist/cx.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Join class names, dropping falsy parts. */
|
|
2
|
+
export function cx(...parts) {
|
|
3
|
+
return parts.filter(Boolean).join(" ");
|
|
4
|
+
}
|
|
5
|
+
/** The keyboard focus ring every interactive control shares. */
|
|
6
|
+
export const focusRing = "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600";
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { Card, EpicHeader, Empty, PageTitle, Pill, groupByEpic } from "./ui.js";
|
|
2
|
+
export { Markdown } from "./Markdown.js";
|
|
3
|
+
export { Button, buttonClass, type ButtonVariant } from "./Button.js";
|
|
4
|
+
export { Callout, type CalloutTone } from "./Callout.js";
|
|
5
|
+
export { IconButton } from "./IconButton.js";
|
|
6
|
+
export { NavTabs, navTabClass, type NavTab } from "./NavTabs.js";
|
|
7
|
+
export { ToggleChip } from "./ToggleChip.js";
|
|
8
|
+
export { FilterGroup } from "./FilterGroup.js";
|
|
9
|
+
export { Checkbox } from "./Checkbox.js";
|
|
10
|
+
export { Field } from "./Field.js";
|
|
11
|
+
export { Input } from "./Input.js";
|
|
12
|
+
export { Select } from "./Select.js";
|
|
13
|
+
export { Textarea } from "./Textarea.js";
|
|
14
|
+
export { Body, BodyStrong, Caption, Display, Lead, ModalHeading, Overline, PageHeading, SectionHeading, SubsectionHeading, Subtitle, type TypographyColor, type TypographyProps, } from "./typography.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { Card, EpicHeader, Empty, PageTitle, Pill, groupByEpic } from "./ui.js";
|
|
2
|
+
export { Markdown } from "./Markdown.js";
|
|
3
|
+
export { Button, buttonClass } from "./Button.js";
|
|
4
|
+
export { Callout } from "./Callout.js";
|
|
5
|
+
export { IconButton } from "./IconButton.js";
|
|
6
|
+
export { NavTabs, navTabClass } from "./NavTabs.js";
|
|
7
|
+
export { ToggleChip } from "./ToggleChip.js";
|
|
8
|
+
export { FilterGroup } from "./FilterGroup.js";
|
|
9
|
+
export { Checkbox } from "./Checkbox.js";
|
|
10
|
+
export { Field } from "./Field.js";
|
|
11
|
+
export { Input } from "./Input.js";
|
|
12
|
+
export { Select } from "./Select.js";
|
|
13
|
+
export { Textarea } from "./Textarea.js";
|
|
14
|
+
export { Body, BodyStrong, Caption, Display, Lead, ModalHeading, Overline, PageHeading, SectionHeading, SubsectionHeading, Subtitle, } from "./typography.js";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ElementType, ReactNode } from "react";
|
|
2
|
+
export type TypographyColor = "default" | "secondary" | "tertiary" | "muted" | "brand";
|
|
3
|
+
/** Own props shared by every typography component; the rest follow the rendered element. */
|
|
4
|
+
type OwnProps = {
|
|
5
|
+
/** Colour token; each component sets a sensible default. */
|
|
6
|
+
color?: TypographyColor;
|
|
7
|
+
className?: string;
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
};
|
|
10
|
+
/** Polymorphic props: own props + `as` + the intrinsic props of whichever element `as` selects. */
|
|
11
|
+
export type TypographyProps<E extends ElementType> = OwnProps & {
|
|
12
|
+
as?: E;
|
|
13
|
+
} & Omit<ComponentPropsWithoutRef<E>, keyof OwnProps | "as">;
|
|
14
|
+
/**
|
|
15
|
+
* Marketing-scale headline — the one line at the top of a public page. `h1`,
|
|
16
|
+
* text-4xl/tight semibold, primary text, growing to text-6xl on wider screens.
|
|
17
|
+
* `PageHeading`'s scale is deliberately dev-tool sized; a brochure
|
|
18
|
+
* hero needs a display scale, so it gets its own pinned role rather than callers
|
|
19
|
+
* overriding PageHeading's size.
|
|
20
|
+
*/
|
|
21
|
+
export declare const Display: {
|
|
22
|
+
<E extends ElementType = "h1">({ as, color, className, children, ...rest }: TypographyProps<E>): import("react").JSX.Element;
|
|
23
|
+
displayName: string;
|
|
24
|
+
};
|
|
25
|
+
/** The supporting sentence under a `Display`. `p`, text-lg/7 normal, secondary. */
|
|
26
|
+
export declare const Lead: {
|
|
27
|
+
<E extends ElementType = "p">({ as, color, className, children, ...rest }: TypographyProps<E>): import("react").JSX.Element;
|
|
28
|
+
displayName: string;
|
|
29
|
+
};
|
|
30
|
+
/** Page title. `h1`, text-2xl/8 semibold, primary text. */
|
|
31
|
+
export declare const PageHeading: {
|
|
32
|
+
<E extends ElementType = "h1">({ as, color, className, children, ...rest }: TypographyProps<E>): import("react").JSX.Element;
|
|
33
|
+
displayName: string;
|
|
34
|
+
};
|
|
35
|
+
/** Top-level section header. `h2`, text-base/6 semibold, primary text. */
|
|
36
|
+
export declare const SectionHeading: {
|
|
37
|
+
<E extends ElementType = "h2">({ as, color, className, children, ...rest }: TypographyProps<E>): import("react").JSX.Element;
|
|
38
|
+
displayName: string;
|
|
39
|
+
};
|
|
40
|
+
/** Nested header. `h3`, text-sm/5 semibold, primary text. */
|
|
41
|
+
export declare const SubsectionHeading: {
|
|
42
|
+
<E extends ElementType = "h3">({ as, color, className, children, ...rest }: TypographyProps<E>): import("react").JSX.Element;
|
|
43
|
+
displayName: string;
|
|
44
|
+
};
|
|
45
|
+
/** Dialog/modal title. `h2`, text-base/6 semibold, primary text. */
|
|
46
|
+
export declare const ModalHeading: {
|
|
47
|
+
<E extends ElementType = "h2">({ as, color, className, children, ...rest }: TypographyProps<E>): import("react").JSX.Element;
|
|
48
|
+
displayName: string;
|
|
49
|
+
};
|
|
50
|
+
/** Supporting line under a heading. `p`, text-sm/5 normal, secondary. */
|
|
51
|
+
export declare const Subtitle: {
|
|
52
|
+
<E extends ElementType = "p">({ as, color, className, children, ...rest }: TypographyProps<E>): import("react").JSX.Element;
|
|
53
|
+
displayName: string;
|
|
54
|
+
};
|
|
55
|
+
/** Small all-caps label above a group. `span`, text-xs semibold uppercase, tertiary. */
|
|
56
|
+
export declare const Overline: {
|
|
57
|
+
<E extends ElementType = "span">({ as, color, className, children, ...rest }: TypographyProps<E>): import("react").JSX.Element;
|
|
58
|
+
displayName: string;
|
|
59
|
+
};
|
|
60
|
+
/** Default running text. `span` (inline; `as="p"` for a paragraph), text-sm/5 normal, secondary. */
|
|
61
|
+
export declare const Body: {
|
|
62
|
+
<E extends ElementType = "span">({ as, color, className, children, ...rest }: TypographyProps<E>): import("react").JSX.Element;
|
|
63
|
+
displayName: string;
|
|
64
|
+
};
|
|
65
|
+
/** Emphasised run of body text. `span`, text-sm/5 medium, primary text. */
|
|
66
|
+
export declare const BodyStrong: {
|
|
67
|
+
<E extends ElementType = "span">({ as, color, className, children, ...rest }: TypographyProps<E>): import("react").JSX.Element;
|
|
68
|
+
displayName: string;
|
|
69
|
+
};
|
|
70
|
+
/** Fine print / metadata. `span` (inline; `as="p"` for a block), text-xs/4 normal, tertiary. */
|
|
71
|
+
export declare const Caption: {
|
|
72
|
+
<E extends ElementType = "span">({ as, color, className, children, ...rest }: TypographyProps<E>): import("react").JSX.Element;
|
|
73
|
+
displayName: string;
|
|
74
|
+
};
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/** Colour token → slate/violet utility (remapped to Catppuccin Mocha in dark mode by theme.css). */
|
|
4
|
+
const COLOR = {
|
|
5
|
+
default: "text-slate-900", // Catppuccin text #cdd6f4
|
|
6
|
+
secondary: "text-slate-600", // Catppuccin subtext1 #bac2de
|
|
7
|
+
tertiary: "text-slate-500", // Catppuccin subtext0 #a6adc8
|
|
8
|
+
muted: "text-slate-400", // Catppuccin overlay2 #9399b2
|
|
9
|
+
brand: "text-violet-600", // Catppuccin mauve #cba6f7
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Build a semantic component that pins `variantClass` (size + weight + tracking)
|
|
13
|
+
* and a default colour. The scale/weight for each role therefore lives in exactly
|
|
14
|
+
* one place (the call below), and callers can only vary the colour, element, and
|
|
15
|
+
* layout classes — never the pinned size/weight.
|
|
16
|
+
*/
|
|
17
|
+
function typography(displayName, defaultAs, variantClass, defaultColor) {
|
|
18
|
+
function Typography({ as, color, className, children, ...rest }) {
|
|
19
|
+
const Tag = as ?? defaultAs;
|
|
20
|
+
return (_jsx(Tag, { className: cx(variantClass, COLOR[color ?? defaultColor], className), ...rest, children: children }));
|
|
21
|
+
}
|
|
22
|
+
Typography.displayName = displayName;
|
|
23
|
+
return Typography;
|
|
24
|
+
}
|
|
25
|
+
// --- Structural roles (block by default) -----------------------------------
|
|
26
|
+
/**
|
|
27
|
+
* Marketing-scale headline — the one line at the top of a public page. `h1`,
|
|
28
|
+
* text-4xl/tight semibold, primary text, growing to text-6xl on wider screens.
|
|
29
|
+
* `PageHeading`'s scale is deliberately dev-tool sized; a brochure
|
|
30
|
+
* hero needs a display scale, so it gets its own pinned role rather than callers
|
|
31
|
+
* overriding PageHeading's size.
|
|
32
|
+
*/
|
|
33
|
+
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");
|
|
34
|
+
/** The supporting sentence under a `Display`. `p`, text-lg/7 normal, secondary. */
|
|
35
|
+
export const Lead = typography("Lead", "p", "text-lg/7 font-normal sm:text-xl/8", "secondary");
|
|
36
|
+
/** Page title. `h1`, text-2xl/8 semibold, primary text. */
|
|
37
|
+
export const PageHeading = typography("PageHeading", "h1", "text-2xl/8 font-semibold tracking-tight", "default");
|
|
38
|
+
/** Top-level section header. `h2`, text-base/6 semibold, primary text. */
|
|
39
|
+
export const SectionHeading = typography("SectionHeading", "h2", "text-base/6 font-semibold", "default");
|
|
40
|
+
/** Nested header. `h3`, text-sm/5 semibold, primary text. */
|
|
41
|
+
export const SubsectionHeading = typography("SubsectionHeading", "h3", "text-sm/5 font-semibold", "default");
|
|
42
|
+
/** Dialog/modal title. `h2`, text-base/6 semibold, primary text. */
|
|
43
|
+
export const ModalHeading = typography("ModalHeading", "h2", "text-base/6 font-semibold", "default");
|
|
44
|
+
/** Supporting line under a heading. `p`, text-sm/5 normal, secondary. */
|
|
45
|
+
export const Subtitle = typography("Subtitle", "p", "text-sm/5 font-normal", "secondary");
|
|
46
|
+
// --- Inline roles (inline by default) --------------------------------------
|
|
47
|
+
/** Small all-caps label above a group. `span`, text-xs semibold uppercase, tertiary. */
|
|
48
|
+
export const Overline = typography("Overline", "span", "text-xs font-semibold uppercase tracking-wide", "tertiary");
|
|
49
|
+
/** Default running text. `span` (inline; `as="p"` for a paragraph), text-sm/5 normal, secondary. */
|
|
50
|
+
export const Body = typography("Body", "span", "text-sm/5 font-normal", "secondary");
|
|
51
|
+
/** Emphasised run of body text. `span`, text-sm/5 medium, primary text. */
|
|
52
|
+
export const BodyStrong = typography("BodyStrong", "span", "text-sm/5 font-medium", "default");
|
|
53
|
+
/** Fine print / metadata. `span` (inline; `as="p"` for a block), text-xs/4 normal, tertiary. */
|
|
54
|
+
export const Caption = typography("Caption", "span", "text-xs/4 font-normal", "tertiary");
|
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export declare function PageTitle({ title, subtitle }: {
|
|
3
|
+
title: string;
|
|
4
|
+
subtitle?: string;
|
|
5
|
+
}): import("react").JSX.Element;
|
|
6
|
+
/** A bordered panel with an optional title and count; `as` picks the element (default `section`). */
|
|
7
|
+
export declare function Card({ title, count, as: Tag, className, children, }: {
|
|
8
|
+
title?: string;
|
|
9
|
+
count?: number;
|
|
10
|
+
as?: "section" | "article" | "div";
|
|
11
|
+
className?: string;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
14
|
+
/** A coloured epic header row: a dot in the epic's colour + its label. */
|
|
15
|
+
export declare function EpicHeader({ color, label }: {
|
|
16
|
+
color: string;
|
|
17
|
+
label: string;
|
|
18
|
+
}): import("react").JSX.Element;
|
|
19
|
+
/** A small tinted label; `dot` adds a leading dot in the pill's own text colour. */
|
|
20
|
+
export declare function Pill({ children, tone, dot, }: {
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
tone?: "slate" | "amber" | "emerald" | "blue" | "red";
|
|
23
|
+
dot?: boolean;
|
|
24
|
+
}): import("react").JSX.Element;
|
|
25
|
+
export declare function Empty({ children }: {
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
}): import("react").JSX.Element;
|
|
28
|
+
/** Group items by their epic ref, ordered with real epics first (by ref) and "No epic" last. */
|
|
29
|
+
export declare function groupByEpic<T>(items: T[], epicOf: (t: T) => string | null): [string | null, T[]][];
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
// Small presentational primitives written against Tailwind's slate-based class vocabulary
|
|
4
|
+
// (remapped to Catppuccin Mocha in dark mode by theme.css).
|
|
5
|
+
export function PageTitle({ title, subtitle }) {
|
|
6
|
+
return (_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight text-slate-900", children: title }), subtitle && _jsx("p", { className: "mt-1 text-sm text-slate-500", children: subtitle })] }));
|
|
7
|
+
}
|
|
8
|
+
/** A bordered panel with an optional title and count; `as` picks the element (default `section`). */
|
|
9
|
+
export function Card({ title, count, as: Tag = "section", className, children, }) {
|
|
10
|
+
return (_jsxs(Tag, { className: cx("rounded-xl border border-slate-200/70 bg-white p-5 shadow-sm", className), children: [title && (_jsxs("div", { className: "mb-3 flex items-baseline gap-2", children: [_jsx("h2", { className: "text-xs font-semibold uppercase tracking-wide text-slate-400", children: title }), count !== undefined && (_jsx("span", { className: "text-xs font-medium text-slate-400", children: count }))] })), children] }));
|
|
11
|
+
}
|
|
12
|
+
/** A coloured epic header row: a dot in the epic's colour + its label. */
|
|
13
|
+
export function EpicHeader({ color, label }) {
|
|
14
|
+
return (_jsxs("div", { className: "mb-1.5 mt-3 flex items-center gap-2 first:mt-0", children: [_jsx("span", { className: "inline-block h-2.5 w-2.5 rounded-full", style: { backgroundColor: color } }), _jsx("span", { className: "text-sm font-semibold text-slate-700", children: label })] }));
|
|
15
|
+
}
|
|
16
|
+
/** A small tinted label; `dot` adds a leading dot in the pill's own text colour. */
|
|
17
|
+
export function Pill({ children, tone = "slate", dot = false, }) {
|
|
18
|
+
const tones = {
|
|
19
|
+
slate: "bg-slate-100 text-slate-600",
|
|
20
|
+
amber: "bg-amber-50 text-amber-700",
|
|
21
|
+
emerald: "bg-emerald-50 text-emerald-700",
|
|
22
|
+
blue: "bg-blue-50 text-blue-700",
|
|
23
|
+
red: "bg-red-50 text-red-900",
|
|
24
|
+
};
|
|
25
|
+
return (_jsxs("span", { className: cx("rounded-md px-1.5 py-0.5 text-xs font-medium", tones[tone], dot && "inline-flex items-center gap-1"), children: [dot && _jsx("span", { "aria-hidden": "true", className: "h-1.5 w-1.5 rounded-full bg-current" }), children] }));
|
|
26
|
+
}
|
|
27
|
+
export function Empty({ children }) {
|
|
28
|
+
return _jsx("p", { className: "text-sm text-slate-400", children: children });
|
|
29
|
+
}
|
|
30
|
+
function compareEpics(a, b) {
|
|
31
|
+
if (a === null)
|
|
32
|
+
return 1;
|
|
33
|
+
if (b === null)
|
|
34
|
+
return -1;
|
|
35
|
+
return a < b ? -1 : 1;
|
|
36
|
+
}
|
|
37
|
+
/** Group items by their epic ref, ordered with real epics first (by ref) and "No epic" last. */
|
|
38
|
+
export function groupByEpic(items, epicOf) {
|
|
39
|
+
const groups = new Map();
|
|
40
|
+
for (const item of items) {
|
|
41
|
+
const key = epicOf(item) ?? null;
|
|
42
|
+
const group = groups.get(key);
|
|
43
|
+
if (group)
|
|
44
|
+
group.push(item);
|
|
45
|
+
else
|
|
46
|
+
groups.set(key, [item]);
|
|
47
|
+
}
|
|
48
|
+
return [...groups].toSorted(([a], [b]) => compareEpics(a, b));
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hackcove/ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/madangryscientist/hackcove.git",
|
|
9
|
+
"directory": "packages/ui"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"lint": "oxlint -c ../../.oxlintrc.json --type-aware --deny-warnings && bun ../../scripts/check-slop.ts",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"pack-check": "npm pack --dry-run",
|
|
30
|
+
"prepack": "bun run build"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/bun": "1.4.3",
|
|
34
|
+
"@types/react": "^19.3.0",
|
|
35
|
+
"@types/react-dom": "^19.3.0",
|
|
36
|
+
"react": "^19.3.1",
|
|
37
|
+
"react-dom": "^19.3.1",
|
|
38
|
+
"tailwindcss": "^4.3.3",
|
|
39
|
+
"vitest": "5.0.5"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"react": ">=18",
|
|
43
|
+
"tailwindcss": "^4"
|
|
44
|
+
},
|
|
45
|
+
"nx": {
|
|
46
|
+
"name": "ui",
|
|
47
|
+
"tags": [
|
|
48
|
+
"kind:package"
|
|
49
|
+
],
|
|
50
|
+
"targets": {
|
|
51
|
+
"build": {
|
|
52
|
+
"outputs": [
|
|
53
|
+
"{projectRoot}/dist"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|