@kyzarky/ui 0.1.0 → 0.3.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/README.md +60 -6
- package/dist/Button.d.ts +20 -0
- package/dist/Button.js +20 -0
- package/dist/Card.d.ts +2 -0
- package/dist/Card.js +5 -0
- package/dist/List.d.ts +15 -0
- package/dist/List.js +4 -0
- package/dist/ListRow.d.ts +10 -0
- package/dist/ListRow.js +4 -0
- package/dist/Meter.d.ts +6 -0
- package/dist/Meter.js +6 -0
- package/dist/SegmentedControl.d.ts +13 -0
- package/dist/SegmentedControl.js +10 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/joinClasses.d.ts +1 -0
- package/dist/joinClasses.js +6 -0
- package/package.json +17 -5
- package/theme.css +4 -0
package/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# @kyzarky/ui
|
|
2
2
|
|
|
3
|
-
Design tokens for Ky Merritt's apps: a Tailwind CSS 4 theme with
|
|
4
|
-
|
|
5
|
-
consumer: Fluent Prior. Next:
|
|
3
|
+
Design tokens and React components for Ky Merritt's apps: a Tailwind CSS 4 theme with
|
|
4
|
+
light and dark values, and the components built on it
|
|
5
|
+
([ADR 0002](docs/decisions/0002-first-scope.md)). First consumer: Fluent Prior. Next:
|
|
6
|
+
Merritt's Mini Mind.
|
|
6
7
|
|
|
7
8
|
## Use
|
|
8
9
|
|
|
@@ -17,7 +18,9 @@ Then import the theme after Tailwind in the app's CSS:
|
|
|
17
18
|
@import "@kyzarky/ui/theme.css";
|
|
18
19
|
```
|
|
19
20
|
|
|
20
|
-
It needs Tailwind CSS 4
|
|
21
|
+
It needs Tailwind CSS 4, and React 19 for the components. The import also has the app's
|
|
22
|
+
Tailwind scan the package's components, so their classes are generated with the app's own.
|
|
23
|
+
**Pin an exact version and upgrade on purpose,** so work in
|
|
21
24
|
progress here never changes an app under it. The [changelog](CHANGELOG.md) says what each
|
|
22
25
|
version changes and how to upgrade.
|
|
23
26
|
|
|
@@ -78,6 +81,56 @@ own rule wins wherever it is. For example, a green brand color:
|
|
|
78
81
|
`ring` follows `primary` unless you set it too. Keep the contrast of every pair in the
|
|
79
82
|
table above.
|
|
80
83
|
|
|
84
|
+
## Components
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
import { List, ListRow, Meter } from "@kyzarky/ui";
|
|
88
|
+
|
|
89
|
+
<List heading={<h3>Grammar</h3>} description="Optional help text.">
|
|
90
|
+
<ListRow label="Question word order" detail="99% · 9 answers">
|
|
91
|
+
<Meter value={0.99} />
|
|
92
|
+
</ListRow>
|
|
93
|
+
</List>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- **`List`** is a group of rows with a divider between them. Pass the `heading` as an
|
|
97
|
+
element, at whatever level the page needs; `List` sets its look. A `List` that follows
|
|
98
|
+
something sits 32px below it.
|
|
99
|
+
- **`ListRow`** is one row: a `label`, a `detail` on the right, and optional children
|
|
100
|
+
below for a graphic.
|
|
101
|
+
- **`Meter`** is a bar for a `value` from 0 to 1. It's hidden from screen readers, so
|
|
102
|
+
state the value in the row's `detail`.
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import { Button, Card, SegmentedControl } from "@kyzarky/ui";
|
|
106
|
+
|
|
107
|
+
<Card aria-labelledby="progress-heading" className="flex flex-col gap-4">
|
|
108
|
+
<SegmentedControl
|
|
109
|
+
label="Show"
|
|
110
|
+
value={view}
|
|
111
|
+
onChange={setView}
|
|
112
|
+
options={[
|
|
113
|
+
{ value: "now", label: "Now" },
|
|
114
|
+
{ value: "over-time", label: "Over time" },
|
|
115
|
+
]}
|
|
116
|
+
/>
|
|
117
|
+
<Button type="submit">Check my answer</Button>
|
|
118
|
+
<Button variant="outline" onClick={next}>Next situation</Button>
|
|
119
|
+
</Card>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- **`Card`** is a raised surface, rendered as a `<section>`. Name it with
|
|
123
|
+
`aria-labelledby` or `aria-label`.
|
|
124
|
+
- **`Button`** takes shadcn/ui's props: `variant` is `default` (filled), `outline`, or
|
|
125
|
+
`ghost`, and `size` is `default` or `icon`. Unlike a bare `<button>`, its `type` is
|
|
126
|
+
`button` unless you pass `submit`. It also takes every native button prop.
|
|
127
|
+
- **`SegmentedControl`** picks one option from a few. It's a group of radio buttons
|
|
128
|
+
underneath, so arrow keys move between options. `label` names the group for screen
|
|
129
|
+
readers and isn't shown.
|
|
130
|
+
|
|
131
|
+
`Card` and `Button` take a `className` for layout only (`flex-1`, `w-full`, a `gap`). It's
|
|
132
|
+
added after the component's own classes, not merged with them, so don't use it to restyle.
|
|
133
|
+
|
|
81
134
|
## Rules
|
|
82
135
|
|
|
83
136
|
- **Less space inside a group than between groups,** at least 1:2.
|
|
@@ -111,8 +164,9 @@ table above.
|
|
|
111
164
|
|
|
112
165
|
| Command | What it does |
|
|
113
166
|
|---|---|
|
|
114
|
-
| `npm run verify` | Typecheck, lint, and
|
|
115
|
-
| `npm
|
|
167
|
+
| `npm run verify` | Typecheck, lint, test, and build |
|
|
168
|
+
| `npm run build` | Compiles `src` to `dist` with `tsc`. `npm pack` and `npm publish` run it first |
|
|
169
|
+
| `npm test` | The contrast, fallback, and component tests, and a Tailwind build of the theme |
|
|
116
170
|
| `npm run test:browsers` | Opens `browser-check.html` in Chromium, Firefox, and WebKit, in light and dark. Needs `npx playwright install` once. Add engine names to run only those: `npm run test:browsers -- webkit` |
|
|
117
171
|
| `npm run format` | Biome: format and apply safe lint fixes |
|
|
118
172
|
|
package/dist/Button.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
declare const VARIANTS: {
|
|
3
|
+
/** Filled with the brand color: the main action. */
|
|
4
|
+
default: string;
|
|
5
|
+
/** An edge and the card color: a second action beside a filled one. */
|
|
6
|
+
outline: string;
|
|
7
|
+
/** Nothing until hovered: a quiet action, such as an icon in a toolbar. */
|
|
8
|
+
ghost: string;
|
|
9
|
+
};
|
|
10
|
+
declare const SIZES: {
|
|
11
|
+
default: string;
|
|
12
|
+
/** Square, for a single icon. Give the button an aria-label. */
|
|
13
|
+
icon: string;
|
|
14
|
+
};
|
|
15
|
+
interface ButtonProps extends ComponentProps<"button"> {
|
|
16
|
+
variant?: keyof typeof VARIANTS;
|
|
17
|
+
size?: keyof typeof SIZES;
|
|
18
|
+
}
|
|
19
|
+
export declare function Button({ variant, size, type, className, ...props }: ButtonProps): import("react").JSX.Element;
|
|
20
|
+
export {};
|
package/dist/Button.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { joinClasses } from "./joinClasses.js";
|
|
3
|
+
const VARIANTS = {
|
|
4
|
+
/** Filled with the brand color: the main action. */
|
|
5
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80",
|
|
6
|
+
/** An edge and the card color: a second action beside a filled one. */
|
|
7
|
+
outline: "border border-input bg-card hover:bg-muted active:bg-muted",
|
|
8
|
+
/** Nothing until hovered: a quiet action, such as an icon in a toolbar. */
|
|
9
|
+
ghost: "hover:bg-muted active:bg-muted",
|
|
10
|
+
};
|
|
11
|
+
const SIZES = {
|
|
12
|
+
default: "px-5",
|
|
13
|
+
/** Square, for a single icon. Give the button an aria-label. */
|
|
14
|
+
icon: "w-11",
|
|
15
|
+
};
|
|
16
|
+
export function Button({ variant = "default", size = "default",
|
|
17
|
+
// A bare <button> inside a form submits it. Submitting should be asked for.
|
|
18
|
+
type = "button", className, ...props }) {
|
|
19
|
+
return (_jsx("button", { type: type, className: joinClasses("inline-flex min-h-11 items-center justify-center gap-2 rounded-xl font-semibold transition-colors focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2 disabled:pointer-events-none disabled:opacity-50", VARIANTS[variant], SIZES[size], className), ...props }));
|
|
20
|
+
}
|
package/dist/Card.d.ts
ADDED
package/dist/Card.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { joinClasses } from "./joinClasses.js";
|
|
3
|
+
export function Card({ className, ...props }) {
|
|
4
|
+
return (_jsx("section", { className: joinClasses("rounded-2xl border border-border bg-card p-5 text-card-foreground shadow-sm", className), ...props }));
|
|
5
|
+
}
|
package/dist/List.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
interface ListProps {
|
|
3
|
+
/**
|
|
4
|
+
* The heading element, at whatever level the page needs: `<h3>Grammar</h3>`. The level
|
|
5
|
+
* is the page's to choose; the look is set here. Tailwind's preflight makes headings
|
|
6
|
+
* inherit size and weight, so the wrapper's classes apply to it.
|
|
7
|
+
*/
|
|
8
|
+
heading?: ReactNode;
|
|
9
|
+
/** A line of help text between the heading and the rows. */
|
|
10
|
+
description?: ReactNode;
|
|
11
|
+
/** `ListRow`s. */
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
export declare function List({ heading, description, children }: ListProps): import("react").JSX.Element;
|
|
15
|
+
export {};
|
package/dist/List.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function List({ heading, description, children }) {
|
|
3
|
+
return (_jsxs("div", { className: "mt-8 first:mt-0", children: [heading && _jsx("div", { className: "font-semibold text-base", children: heading }), description && _jsx("p", { className: "mt-1 text-muted-foreground text-sm", children: description }), _jsx("ul", { className: "mt-2 divide-y divide-border", children: children })] }));
|
|
4
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
interface ListRowProps {
|
|
3
|
+
label: ReactNode;
|
|
4
|
+
/** Secondary text on the right. If the row has a graphic, this states its value. */
|
|
5
|
+
detail?: ReactNode;
|
|
6
|
+
/** A graphic for the row, such as a `Meter`. */
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
export declare function ListRow({ label, detail, children }: ListRowProps): import("react").JSX.Element;
|
|
10
|
+
export {};
|
package/dist/ListRow.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function ListRow({ label, detail, children }) {
|
|
3
|
+
return (_jsxs("li", { className: "py-3", children: [_jsxs("div", { className: "flex items-baseline justify-between gap-3 text-sm", children: [_jsx("span", { children: label }), detail && _jsx("span", { className: "shrink-0 text-muted-foreground tabular-nums", children: detail })] }), children && _jsx("div", { className: "mt-2", children: children })] }));
|
|
4
|
+
}
|
package/dist/Meter.d.ts
ADDED
package/dist/Meter.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export function Meter({ value }) {
|
|
3
|
+
// `|| 0` turns NaN into 0. An invalid width would be ignored, leaving the bar full.
|
|
4
|
+
const percent = Math.min(1, Math.max(0, value || 0)) * 100;
|
|
5
|
+
return (_jsx("div", { "aria-hidden": "true", className: "h-2 overflow-hidden rounded-full bg-muted", children: _jsx("div", { className: "h-full rounded-full bg-primary", style: { width: `${percent}%` } }) }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
interface SegmentedControlProps<Value extends string> {
|
|
3
|
+
/** What the choice is, for screen readers: "Show", "Sort by". Not displayed. */
|
|
4
|
+
label: string;
|
|
5
|
+
options: readonly {
|
|
6
|
+
value: Value;
|
|
7
|
+
label: ReactNode;
|
|
8
|
+
}[];
|
|
9
|
+
value: Value;
|
|
10
|
+
onChange: (value: Value) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function SegmentedControl<Value extends string>({ label, options, value, onChange, }: SegmentedControlProps<Value>): import("react").JSX.Element;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// A choice of one option from a few, shown side by side. Underneath it's a group of radio
|
|
3
|
+
// buttons, so arrow keys move between options and a screen reader says how many there
|
|
4
|
+
// are, with no key handling written here. The selected look comes from CSS :has(:checked).
|
|
5
|
+
import { useId } from "react";
|
|
6
|
+
export function SegmentedControl({ label, options, value, onChange, }) {
|
|
7
|
+
// Radio buttons form one group by sharing a name.
|
|
8
|
+
const name = useId();
|
|
9
|
+
return (_jsxs("fieldset", { className: "flex rounded-full bg-muted p-1 text-sm", children: [_jsx("legend", { className: "sr-only", children: label }), options.map((option) => (_jsxs("label", { className: "flex min-h-11 cursor-pointer items-center rounded-full px-4 text-muted-foreground has-checked:bg-card has-checked:font-medium has-checked:text-foreground has-checked:shadow-sm has-focus-visible:outline-2 has-focus-visible:outline-ring has-focus-visible:outline-offset-2", children: [_jsx("input", { type: "radio", className: "sr-only", name: name, value: option.value, checked: option.value === value, onChange: () => onChange(option.value) }), option.label] }, option.value)))] }));
|
|
10
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The package's components. The tokens they use come from theme.css.
|
|
2
|
+
export { Button } from "./Button.js";
|
|
3
|
+
export { Card } from "./Card.js";
|
|
4
|
+
export { List } from "./List.js";
|
|
5
|
+
export { ListRow } from "./ListRow.js";
|
|
6
|
+
export { Meter } from "./Meter.js";
|
|
7
|
+
export { SegmentedControl } from "./SegmentedControl.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function joinClasses(...names: (string | undefined)[]): string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Joins class names, skipping any that aren't set. It doesn't resolve conflicts: a
|
|
2
|
+
// component's `className` prop is for layout (flex-1, w-full, a margin), which the
|
|
3
|
+
// component's own classes never set.
|
|
4
|
+
export function joinClasses(...names) {
|
|
5
|
+
return names.filter(Boolean).join(" ");
|
|
6
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kyzarky/ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Design tokens for Ky Merritt's apps,
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Design tokens and React components for Ky Merritt's apps, built on Tailwind CSS 4",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ky Merritt",
|
|
7
7
|
"repository": {
|
|
@@ -10,30 +10,42 @@
|
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
12
|
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
13
17
|
"./theme.css": "./theme.css"
|
|
14
18
|
},
|
|
15
19
|
"files": [
|
|
16
|
-
"theme.css"
|
|
20
|
+
"theme.css",
|
|
21
|
+
"dist"
|
|
17
22
|
],
|
|
18
23
|
"publishConfig": {
|
|
19
24
|
"access": "public"
|
|
20
25
|
},
|
|
21
26
|
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.build.json",
|
|
28
|
+
"prepack": "npm run build",
|
|
22
29
|
"typecheck": "tsc --noEmit",
|
|
23
30
|
"lint": "biome check .",
|
|
24
31
|
"format": "biome check --write .",
|
|
25
32
|
"test": "vitest run",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
33
|
+
"test:browsers": "node browser-check.ts",
|
|
34
|
+
"verify": "npm run typecheck && npm run lint && npm run test && npm run build"
|
|
28
35
|
},
|
|
29
36
|
"peerDependencies": {
|
|
37
|
+
"react": "^19.0.0",
|
|
30
38
|
"tailwindcss": "^4.0.0"
|
|
31
39
|
},
|
|
32
40
|
"devDependencies": {
|
|
33
41
|
"@biomejs/biome": "^2.5.15",
|
|
34
42
|
"@tailwindcss/node": "^4.3.3",
|
|
35
43
|
"@types/node": "^26.6.4",
|
|
44
|
+
"@types/react": "^19.3.0",
|
|
45
|
+
"@types/react-dom": "^19.3.0",
|
|
36
46
|
"playwright": "^1.64.0",
|
|
47
|
+
"react": "^19.3.0",
|
|
48
|
+
"react-dom": "^19.3.0",
|
|
37
49
|
"tailwindcss": "^4.3.3",
|
|
38
50
|
"typescript": "~6.0.3",
|
|
39
51
|
"vitest": "^5.0.3"
|
package/theme.css
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
color-scheme itself. Components use the utilities (bg-card, text-muted-foreground) and
|
|
10
10
|
never Tailwind's dark: variant. */
|
|
11
11
|
|
|
12
|
+
/* The components are published as compiled files full of Tailwind class names. This has
|
|
13
|
+
the app's Tailwind scan them, so it generates those classes along with the app's own. */
|
|
14
|
+
@source "./dist";
|
|
15
|
+
|
|
12
16
|
/* In the base layer, so an app's own unlayered rules win wherever it puts them. That's
|
|
13
17
|
how an app themes the package: by redefining a token. */
|
|
14
18
|
@layer base {
|