@kyzarky/ui 0.0.0 → 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ky Merritt
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,6 +1,172 @@
1
1
  # @kyzarky/ui
2
2
 
3
- This version is a placeholder with nothing in it. It was published so the package exists
4
- and npm trusted publishing could be set up. Use 0.1.0 or later.
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.
5
7
 
6
- Source: https://github.com/KyzarKy/design-system
8
+ ## Use
9
+
10
+ ```bash
11
+ npm install --save-exact @kyzarky/ui
12
+ ```
13
+
14
+ Then import the theme after Tailwind in the app's CSS:
15
+
16
+ ```css
17
+ @import "tailwindcss";
18
+ @import "@kyzarky/ui/theme.css";
19
+ ```
20
+
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
24
+ progress here never changes an app under it. The [changelog](CHANGELOG.md) says what each
25
+ version changes and how to upgrade.
26
+
27
+ ## Tokens
28
+
29
+ The names are shadcn/ui's, plus three status colors. Each is a CSS variable (`--card`) and
30
+ a Tailwind color (`bg-card`, `text-card`, `border-card`, and so on).
31
+
32
+ | Token | Used for |
33
+ |---|---|
34
+ | `background` | The page |
35
+ | `foreground` | Text |
36
+ | `card` | Raised surfaces |
37
+ | `card-foreground` | Text on a card |
38
+ | `muted` | Subtle fills: meter tracks, inset boxes, the unselected segment |
39
+ | `muted-foreground` | Secondary text |
40
+ | `border` | Card edges and dividers. Decorative: too faint for a control's edge |
41
+ | `input` | Edges of form controls (3:1 or more) |
42
+ | `primary` | Brand: filled buttons, meters, links |
43
+ | `primary-foreground` | Text on `primary` |
44
+ | `ring` | Focus rings |
45
+ | `success`, `warning`, `destructive` | Results and notices, as text or as a `/15` badge |
46
+
47
+ The pairs this table implies meet WCAG AA in both modes: text on each surface, status
48
+ colors on their badges, and `input` and `ring` edges. The tests check each one.
49
+
50
+ ## Light and dark
51
+
52
+ Each token holds both values, and CSS's `light-dark()` picks one from the `color-scheme`
53
+ of the element using it. The theme sets `color-scheme: light dark` on `:root`, so the page
54
+ follows the system. To let people choose, set `color-scheme` yourself:
55
+
56
+ ```css
57
+ :root { color-scheme: light; }
58
+ .dark { color-scheme: dark; }
59
+ ```
60
+
61
+ Don't use Tailwind's `dark:` variant: the tokens already change.
62
+
63
+ - **Browsers:** both themes need `light-dark()`: Safari 17.5, Chrome 123, Firefox 120, or
64
+ later. A browser without it gets the light theme, whatever the system is set to
65
+ ([ADR 0003](docs/decisions/0003-light-fallback-and-browser-check.md)).
66
+ - **Reading a token in JavaScript** gives the text `light-dark(#f6f4ef, #15181a)`, not a
67
+ color. Read a computed property instead, such as
68
+ `getComputedStyle(document.body).backgroundColor`.
69
+
70
+ ## Theming
71
+
72
+ Redefine a token after the import. The theme's tokens sit in a cascade layer, so an app's
73
+ own rule wins wherever it is. For example, a green brand color:
74
+
75
+ ```css
76
+ :root {
77
+ --primary: light-dark(#2f6b3f, #5fd08a);
78
+ }
79
+ ```
80
+
81
+ `ring` follows `primary` unless you set it too. Keep the contrast of every pair in the
82
+ table above.
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
+ ## Rules
105
+
106
+ - **Less space inside a group than between groups,** at least 1:2.
107
+ - **Rows are separated by dividers; groups by space and a heading.**
108
+ - **Type roles:** page title `text-2xl` semibold; card title `text-lg` semibold; group
109
+ heading `text-base` semibold; body `text-base`; row labels and help text `text-sm`;
110
+ details `text-sm` in `muted-foreground`, with `tabular-nums` for numbers; counters and
111
+ chips `text-xs`.
112
+ - **Every control is at least 44px tall.**
113
+ - **A value shown as a graphic is also shown as text.**
114
+
115
+ ## Why
116
+
117
+ - One visual language across Ky's portfolio apps.
118
+ - Fluent Prior's "What you know" list is hard to read: it's hard to tell where one row ends
119
+ and the next starts. The fix is rules the app never had, and they belong here, not in
120
+ one app.
121
+ - A token-themed design system is the kind of frontend work Ky's job search targets.
122
+
123
+ ## Decisions
124
+
125
+ - **[ADR 0001](docs/decisions/0001-name-registry-and-contents.md), accepted:** the package
126
+ is `@kyzarky/ui`, published to public npm from this repo, with tokens and React
127
+ components.
128
+ - **[ADR 0002](docs/decisions/0002-first-scope.md), accepted:** the first scope, based on
129
+ a [style audit](docs/audit/README.md) of both apps.
130
+ - **[ADR 0003](docs/decisions/0003-light-fallback-and-browser-check.md), accepted:** a
131
+ light fallback for browsers without `light-dark()`, and a browser check in CI.
132
+
133
+ ## Development
134
+
135
+ | Command | What it does |
136
+ |---|---|
137
+ | `npm run verify` | Typecheck, lint, test, and build |
138
+ | `npm run build` | Compiles `src` to `dist` with `tsc`. `npm pack` and `npm publish` run it first |
139
+ | `npm test` | The contrast, fallback, and component tests, and a Tailwind build of the theme |
140
+ | `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` |
141
+ | `npm run format` | Biome: format and apply safe lint fixes |
142
+
143
+ ## Releasing
144
+
145
+ 1. In a pull request: bump `version` in `package.json` and add a changelog entry.
146
+ 2. Try it in an app before merging: `npm pack`, then in the app
147
+ `npm install --no-save <path to the .tgz>` and its `npm run verify`.
148
+ 3. After merging, tag the merge commit and push the tag:
149
+ `git tag v0.2.0 && git push origin v0.2.0`. The publish workflow checks that the tag
150
+ matches `package.json`, runs `verify`, and publishes with provenance. It skips a
151
+ version that's already on npm, so running it again is safe.
152
+
153
+ **How publishing is set up** ([ADR 0001](docs/decisions/0001-name-registry-and-contents.md)).
154
+ npm adds a trusted publisher only to a package that already exists, and a new one expires
155
+ unless it makes a successful publish within 2 days. So:
156
+
157
+ 1. `0.0.0` is a placeholder with no theme in it, published by hand so the package exists.
158
+ It's deprecated.
159
+ 2. The trusted publisher is added with
160
+ `npm trust github @kyzarky/ui --file publish.yml --repo KyzarKy/design-system --allow-publish`.
161
+ 3. Pushing `v0.1.0` within those 2 days has CI publish the first real version, which
162
+ confirms the setup. A replacement trusted publisher would have the same 2 days.
163
+ 4. Then publishing access is set to require two-factor auth and disallow tokens, in the
164
+ package's settings on npmjs.com.
165
+
166
+ ## How we work
167
+
168
+ Ky decides; Claude proposes and implements. Docs first: decisions go in ADRs before code.
169
+ Small commits, each saying what changed and why, how to verify it, and three questions an
170
+ interviewer might ask, with short answers. Never commit secrets. Never force-push. Commits
171
+ use Ky's GitHub noreply email (set in this repo's git config). In anything written in Ky's
172
+ voice: no em dashes, and use Oxford commas.
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 {};
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ interface MeterProps {
2
+ /** From 0 to 1. Values outside that are clamped, and NaN draws an empty bar. */
3
+ value: number;
4
+ }
5
+ export declare function Meter({ value }: MeterProps): import("react").JSX.Element;
6
+ export {};
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,3 @@
1
+ export { List } from "./List.js";
2
+ export { ListRow } from "./ListRow.js";
3
+ export { Meter } from "./Meter.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // The package's components. The tokens they use come from theme.css.
2
+ export { List } from "./List.js";
3
+ export { ListRow } from "./ListRow.js";
4
+ export { Meter } from "./Meter.js";
package/package.json CHANGED
@@ -1,14 +1,53 @@
1
1
  {
2
2
  "name": "@kyzarky/ui",
3
- "version": "0.0.0",
4
- "description": "Placeholder. The first release of @kyzarky/ui is 0.1.0.",
3
+ "version": "0.2.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": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/KyzarKy/design-system.git"
10
10
  },
11
+ "type": "module",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./theme.css": "./theme.css"
18
+ },
19
+ "files": [
20
+ "theme.css",
21
+ "dist"
22
+ ],
11
23
  "publishConfig": {
12
24
  "access": "public"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.build.json",
28
+ "prepack": "npm run build",
29
+ "typecheck": "tsc --noEmit",
30
+ "lint": "biome check .",
31
+ "format": "biome check --write .",
32
+ "test": "vitest run",
33
+ "test:browsers": "node browser-check.ts",
34
+ "verify": "npm run typecheck && npm run lint && npm run test && npm run build"
35
+ },
36
+ "peerDependencies": {
37
+ "react": "^19.0.0",
38
+ "tailwindcss": "^4.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@biomejs/biome": "^2.5.15",
42
+ "@tailwindcss/node": "^4.3.3",
43
+ "@types/node": "^26.6.4",
44
+ "@types/react": "^19.3.0",
45
+ "@types/react-dom": "^19.3.0",
46
+ "playwright": "^1.64.0",
47
+ "react": "^19.3.0",
48
+ "react-dom": "^19.3.0",
49
+ "tailwindcss": "^4.3.3",
50
+ "typescript": "~6.0.3",
51
+ "vitest": "^5.0.3"
13
52
  }
14
53
  }
package/theme.css ADDED
@@ -0,0 +1,86 @@
1
+ /* The design tokens and the Tailwind utilities that use them (ADRs 0002 and 0003). An app
2
+ imports this after Tailwind:
3
+
4
+ @import "tailwindcss";
5
+ @import "@kyzarky/ui/theme.css";
6
+
7
+ Each token holds a light and a dark value. light-dark() picks one from the color-scheme
8
+ of the element using it, so the page follows the system unless the app sets
9
+ color-scheme itself. Components use the utilities (bg-card, text-muted-foreground) and
10
+ never Tailwind's dark: variant. */
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
+
16
+ /* In the base layer, so an app's own unlayered rules win wherever it puts them. That's
17
+ how an app themes the package: by redefining a token. */
18
+ @layer base {
19
+ /* The light theme, for browsers without light-dark() (Safari before 17.5). Without
20
+ these, every token is invalid there and each fill it colors disappears (ADR 0003).
21
+ theme.test.ts checks that each value equals the light value in the block below. */
22
+ :root {
23
+ color-scheme: light;
24
+
25
+ --background: #f6f4ef;
26
+ --foreground: #1f2326;
27
+ --card: #ffffff;
28
+ --card-foreground: var(--foreground);
29
+ --muted: #efece4;
30
+ --muted-foreground: #5b6166;
31
+ --border: #dedad0;
32
+ --input: #858279;
33
+ --primary: #1d6b73;
34
+ --primary-foreground: #ffffff;
35
+ --ring: var(--primary);
36
+ --success: #2a7548;
37
+ --warning: #915d00;
38
+ --destructive: #b1352b;
39
+ }
40
+
41
+ @supports (color: light-dark(#000, #fff)) {
42
+ :root {
43
+ color-scheme: light dark;
44
+
45
+ --background: light-dark(#f6f4ef, #15181a);
46
+ --foreground: light-dark(#1f2326, #eceae4);
47
+ --card: light-dark(#ffffff, #1e2225);
48
+ /* Subtle fills: meter tracks, inset boxes, the unselected segment. */
49
+ --muted: light-dark(#efece4, #272c30);
50
+ --muted-foreground: light-dark(#5b6166, #a3a9ad);
51
+ /* Decorative edges: cards and dividers. Too faint for a control's edge. */
52
+ --border: light-dark(#dedad0, #353b40);
53
+ /* Edges of form controls: at least 3:1 on card and background (WCAG 1.4.11). */
54
+ --input: light-dark(#858279, #737779);
55
+ --primary: light-dark(#1d6b73, #5fb3ba);
56
+ --primary-foreground: light-dark(#ffffff, #0e1a1c);
57
+ --success: light-dark(#2a7548, #6cc38f);
58
+ --warning: light-dark(#915d00, #e0a846);
59
+ --destructive: light-dark(#b1352b, #ef8a7f);
60
+ }
61
+ }
62
+
63
+ body {
64
+ background-color: var(--background);
65
+ color: var(--foreground);
66
+ }
67
+ }
68
+
69
+ /* inline: each utility reads the token where it's used, not a copy resolved at :root, so
70
+ an element that redefines a token gets its own value. */
71
+ @theme inline {
72
+ --color-background: var(--background);
73
+ --color-foreground: var(--foreground);
74
+ --color-card: var(--card);
75
+ --color-card-foreground: var(--card-foreground);
76
+ --color-muted: var(--muted);
77
+ --color-muted-foreground: var(--muted-foreground);
78
+ --color-border: var(--border);
79
+ --color-input: var(--input);
80
+ --color-primary: var(--primary);
81
+ --color-primary-foreground: var(--primary-foreground);
82
+ --color-ring: var(--ring);
83
+ --color-success: var(--success);
84
+ --color-warning: var(--warning);
85
+ --color-destructive: var(--destructive);
86
+ }