@hackcove/ui 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,48 @@
1
- # Temporary Holding Version
1
+ # @hackcove/ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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`, not yet 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
+
44
+ ## Develop
45
+
46
+ From the workspace root: `bunx nx run-many -t typecheck lint test build -p ui` and
47
+ `bunx nx pack-check ui` (the tarball is `dist/` plus `package.json`, `README.md`, `LICENSE`).
48
+ The stories live in `apps/storybook`.
@@ -0,0 +1,3 @@
1
+ export declare function Markdown({ md }: {
2
+ md: string;
3
+ }): import("react").JSX.Element;
@@ -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,3 @@
1
+ export { Card, EpicHeader, Empty, PageTitle, Pill, groupByEpic } from "./ui.js";
2
+ export { Markdown } from "./Markdown.js";
3
+ 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,3 @@
1
+ export { Card, EpicHeader, Empty, PageTitle, Pill, groupByEpic } from "./ui.js";
2
+ export { Markdown } from "./Markdown.js";
3
+ 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,56 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /** Colour token → slate/violet utility (remapped to Catppuccin Mocha in dark mode by theme.css). */
3
+ const COLOR = {
4
+ default: "text-slate-900", // Catppuccin text #cdd6f4
5
+ secondary: "text-slate-600", // Catppuccin subtext1 #bac2de
6
+ tertiary: "text-slate-500", // Catppuccin subtext0 #a6adc8
7
+ muted: "text-slate-400", // Catppuccin overlay2 #9399b2
8
+ brand: "text-violet-600", // Catppuccin mauve #cba6f7
9
+ };
10
+ function cx(...parts) {
11
+ return parts.filter(Boolean).join(" ");
12
+ }
13
+ /**
14
+ * Build a semantic component that pins `variantClass` (size + weight + tracking)
15
+ * and a default colour. The scale/weight for each role therefore lives in exactly
16
+ * one place (the call below), and callers can only vary the colour, element, and
17
+ * layout classes — never the pinned size/weight.
18
+ */
19
+ function typography(displayName, defaultAs, variantClass, defaultColor) {
20
+ function Typography({ as, color, className, children, ...rest }) {
21
+ const Tag = as ?? defaultAs;
22
+ return (_jsx(Tag, { className: cx(variantClass, COLOR[color ?? defaultColor], className), ...rest, children: children }));
23
+ }
24
+ Typography.displayName = displayName;
25
+ return Typography;
26
+ }
27
+ // --- Structural roles (block by default) -----------------------------------
28
+ /**
29
+ * Marketing-scale headline — the one line at the top of a public page. `h1`,
30
+ * text-4xl/tight semibold, primary text, growing to text-6xl on wider screens.
31
+ * `PageHeading`'s scale is deliberately dev-tool sized; a brochure
32
+ * hero needs a display scale, so it gets its own pinned role rather than callers
33
+ * overriding PageHeading's size.
34
+ */
35
+ 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");
36
+ /** The supporting sentence under a `Display`. `p`, text-lg/7 normal, secondary. */
37
+ export const Lead = typography("Lead", "p", "text-lg/7 font-normal sm:text-xl/8", "secondary");
38
+ /** Page title. `h1`, text-2xl/8 semibold, primary text. */
39
+ export const PageHeading = typography("PageHeading", "h1", "text-2xl/8 font-semibold tracking-tight", "default");
40
+ /** Top-level section header. `h2`, text-base/6 semibold, primary text. */
41
+ export const SectionHeading = typography("SectionHeading", "h2", "text-base/6 font-semibold", "default");
42
+ /** Nested header. `h3`, text-sm/5 semibold, primary text. */
43
+ export const SubsectionHeading = typography("SubsectionHeading", "h3", "text-sm/5 font-semibold", "default");
44
+ /** Dialog/modal title. `h2`, text-base/6 semibold, primary text. */
45
+ export const ModalHeading = typography("ModalHeading", "h2", "text-base/6 font-semibold", "default");
46
+ /** Supporting line under a heading. `p`, text-sm/5 normal, secondary. */
47
+ export const Subtitle = typography("Subtitle", "p", "text-sm/5 font-normal", "secondary");
48
+ // --- Inline roles (inline by default) --------------------------------------
49
+ /** Small all-caps label above a group. `span`, text-xs semibold uppercase, tertiary. */
50
+ export const Overline = typography("Overline", "span", "text-xs font-semibold uppercase tracking-wide", "tertiary");
51
+ /** Default running text. `span` (inline; `as="p"` for a paragraph), text-sm/5 normal, secondary. */
52
+ export const Body = typography("Body", "span", "text-sm/5 font-normal", "secondary");
53
+ /** Emphasised run of body text. `span`, text-sm/5 medium, primary text. */
54
+ export const BodyStrong = typography("BodyStrong", "span", "text-sm/5 font-medium", "default");
55
+ /** Fine print / metadata. `span` (inline; `as="p"` for a block), text-xs/4 normal, tertiary. */
56
+ export const Caption = typography("Caption", "span", "text-xs/4 font-normal", "tertiary");
package/dist/ui.d.ts ADDED
@@ -0,0 +1,24 @@
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
+ export declare function Card({ title, count, children, }: {
7
+ title?: string;
8
+ count?: number;
9
+ children: ReactNode;
10
+ }): import("react").JSX.Element;
11
+ /** A coloured epic header row: a dot in the epic's colour + its label. */
12
+ export declare function EpicHeader({ color, label }: {
13
+ color: string;
14
+ label: string;
15
+ }): import("react").JSX.Element;
16
+ export declare function Pill({ children, tone, }: {
17
+ children: ReactNode;
18
+ tone?: "slate" | "amber" | "emerald" | "blue" | "red";
19
+ }): import("react").JSX.Element;
20
+ export declare function Empty({ children }: {
21
+ children: ReactNode;
22
+ }): import("react").JSX.Element;
23
+ /** Group items by their epic ref, ordered with real epics first (by ref) and "No epic" last. */
24
+ export declare function groupByEpic<T>(items: T[], epicOf: (t: T) => string | null): [string | null, T[]][];
package/dist/ui.js ADDED
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Small presentational primitives written against Tailwind's slate-based class vocabulary
3
+ // (remapped to Catppuccin Mocha in dark mode by theme.css).
4
+ export function PageTitle({ title, subtitle }) {
5
+ 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 })] }));
6
+ }
7
+ export function Card({ title, count, children, }) {
8
+ return (_jsxs("section", { className: "rounded-xl border border-slate-200/70 bg-white p-5 shadow-sm", 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] }));
9
+ }
10
+ /** A coloured epic header row: a dot in the epic's colour + its label. */
11
+ export function EpicHeader({ color, label }) {
12
+ 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 })] }));
13
+ }
14
+ export function Pill({ children, tone = "slate", }) {
15
+ const tones = {
16
+ slate: "bg-slate-100 text-slate-600",
17
+ amber: "bg-amber-50 text-amber-700",
18
+ emerald: "bg-emerald-50 text-emerald-700",
19
+ blue: "bg-blue-50 text-blue-700",
20
+ red: "bg-red-50 text-red-900",
21
+ };
22
+ return (_jsx("span", { className: `rounded-md px-1.5 py-0.5 text-xs font-medium ${tones[tone]}`, children: children }));
23
+ }
24
+ export function Empty({ children }) {
25
+ return _jsx("p", { className: "text-sm text-slate-400", children: children });
26
+ }
27
+ function compareEpics(a, b) {
28
+ if (a === null)
29
+ return 1;
30
+ if (b === null)
31
+ return -1;
32
+ return a < b ? -1 : 1;
33
+ }
34
+ /** Group items by their epic ref, ordered with real epics first (by ref) and "No epic" last. */
35
+ export function groupByEpic(items, epicOf) {
36
+ const groups = new Map();
37
+ for (const item of items) {
38
+ const key = epicOf(item) ?? null;
39
+ const group = groups.get(key);
40
+ if (group)
41
+ group.push(item);
42
+ else
43
+ groups.set(key, [item]);
44
+ }
45
+ return [...groups].toSorted(([a], [b]) => compareEpics(a, b));
46
+ }
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "@hackcove/ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.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
+ }