@madecki/ui 1.4.0 → 2.0.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/llm-context.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  This file provides context for AI assistants building apps with the @madecki/ui component library.
4
4
 
5
+ ## Changelog and versioning
6
+
7
+ When upgrading **`@madecki/ui`**, read **`CHANGELOG.md`** for breaking changes, new components, and API adjustments. Release notes are **generated on publish** from [Conventional Commits](https://www.conventionalcommits.org/) on **`main`** only (not hand-edited). The **latest** npm version is always the **topmost** `## [x.y.z]` section and should match **`package.json` `"version"`** in the installed tarball. The same file ships with the package and is addressable as:
8
+
9
+ - Path: `node_modules/@madecki/ui/CHANGELOG.md`
10
+ - Export: `@madecki/ui/CHANGELOG.md`
11
+
12
+ Always align code with the **semver** of the version you install (**major** bumps may include breaking changes).
13
+
5
14
  ## Setup (Required — do this before any UI work)
6
15
 
7
16
  ### globals.css
@@ -36,7 +45,7 @@ When building UI, ALWAYS check if @madecki/ui has a component before creating cu
36
45
 
37
46
  ### Typography
38
47
  - `Heading` - Headings h1-h6. Props: `level?: 1-6`, `size?: "xs"|"sm"|"md"|"lg"|"xl"|"2xl"|"3xl"|"4xl"`, `weight?: "normal"|"medium"|"semibold"|"bold"`, `color?: "default"|"muted"|"primary"|"success"|"warning"|"danger"`
39
- - `Text` - Body text. Props: `as?: "p"|"span"|"div"|"label"`, `size?: "xs"|"sm"|"md"|"lg"`, `weight?: "normal"|"medium"|"semibold"|"bold"`, `color?: "default"|"muted"|"primary"|"success"|"warning"|"danger"`
48
+ - `Text` - Body text. Props: `id?`, `as?: "p"|"span"|"div"|"label"`, `size?: "xs"|"sm"|"md"|"lg"`, `weight?: "normal"|"medium"|"semibold"|"bold"`, `color?: "default"|"muted"|"primary"|"success"|"warning"|"danger"`
40
49
 
41
50
  ### Buttons & tags
42
51
  - `Button` - Primary button. Shares visual styling with `Tag`. Interactive: `cursor-pointer` when enabled, hover affordances in dark mode, `cursor-not-allowed` when `disabled`. Props: `variant: "primary"|"success"|"warning"|"danger"|"info"|"blue"`, `size?: "xs"|"sm"|"md"|"lg"`, `isActive?: boolean` (requires `id` prop; shows × icon; when active, `onClick` is called with no argument — use this to detect deselect), `label?: string`, `id?: string`, `disabled?: boolean`, `type?: "button"|"submit"|"reset"`
@@ -44,11 +53,16 @@ When building UI, ALWAYS check if @madecki/ui has a component before creating cu
44
53
  - `Tag` - Non-interactive label/chip: same `variant` / `size` / look as `Button`, rendered as `<span>`. No `:hover` styles; default cursor (or `cursor-default` when `muted`). Props: `variant`, `size?`, `label?` / `children`, `className?`, `filled?` (filled background like an active button, without the × icon), `muted?` (reduced emphasis, same opacity treatment as a disabled button). Use `Button` when the control must be clickable.
45
54
  - `ButtonTransparent` - Outlined button. Same props as Button.
46
55
  - `GradientButton` - Gradient border button. Props: `size?: "sm"|"md"|"lg"`
47
- - `RadioButtons` - Radio group. Props: `name: string`, `options: {label, value}[]`, `onChange: (value) => void`
56
+ - `RadioButtons` - Segmented control styled as buttons; exposed as a **radiogroup** with `role="radio"` / `aria-checked` on each option. Props: **`label`** (group label, visible by default), **`labelVisibility?: "visible" | "sr-only"`** (default `"visible"`), `items: Omit<ButtonProps, "size"|"onClick"|"isActive"|"role"|"ariaChecked">[]` (each item needs `id` and `variant`, plus `label` or `children`), `onChange: (value: string) => void`, `size?`, `className?`
48
57
 
49
58
  ### Forms
50
- - `Input` - Text input. Props: `name: string`, `label: string`, `placeholder?: string`, `type?: string`, `variant?: "primary"|"secondary"|"tertiary"`, `onChange?: (value) => void`
51
- - `Select` - Combobox styled like `Input`, with trailing chevron. Props: `name`, `label`, `options: { value, label }[]`, `placeholder?`, `variant?` (same as Input), `disabled?`, `className?`, `testId?`. Typing filters the list. **Single (default):** `value?`, `defaultValue?`, `onChange?: (value: string) => void`. **Multi:** `multi={true}`, `value?`, `defaultValue?`, `onChange?: (value: string[]) => void`. The dropdown is absolutely positioned under the field so it does not shift surrounding layout. See below for controlled vs uncontrolled and testing.
59
+ Shared: **`labelVisibility?: "visible" | "sr-only"`** (default **`"visible"`**) on `Input`, `Textarea`, `Select`, and `RadioButtons` use **`"sr-only"`** when the label must stay in the DOM for assistive tech but not be shown visually.
60
+
61
+ - `Input` - Text input. Props: `name`, `label`, `labelVisibility?`, `onChange?: (value: string) => void`, optional **`value`** (controlled) or **`defaultValue`** (uncontrolled initial only), `placeholder?`, **`type?`** (any standard HTML input type from React’s `HTMLInputTypeAttribute`, including `date`, `time`, `datetime-local`, etc.), **`maxLength?`**, `variant?`, `required?`, `pattern?`, `title?`, **`ariaLabel?`** (optional override; omit when the visible `<label>` is enough), `spellCheck?`, `disabled?`, `className?`, `icon?`, **`testId?`** (`data-testid` on the native `<input>` for Playwright). See below for controlled vs uncontrolled and testing.
62
+ - `Textarea` - Multi-line field, same variants and label pattern as `Input`. Props: `name`, `label`, `labelVisibility?`, `onChange?`, `value?`, `defaultValue?`, `placeholder?`, `variant?`, `rows?` (default 4), `maxLength?`, `required?`, `ariaLabel?`, `spellCheck?`, `disabled?`, `className?`, `testId?`.
63
+ - `Select` - Combobox styled like `Input`, with trailing chevron. Props: `name`, `label`, `labelVisibility?`, `options: { value, label }[]`, `placeholder?`, `variant?` (same as Input), `disabled?`, `className?`, `testId?`. Typing filters the list. **Single (default):** `value?`, `defaultValue?`, `onChange?: (value: string) => void`. **Multi:** `multi={true}`, `value?`, `defaultValue?`, `onChange?: (value: string[]) => void`. The dropdown is absolutely positioned under the field so it does not shift surrounding layout. The listbox is named via **`aria-labelledby`** pointing at the field label. See below for controlled vs uncontrolled and testing.
64
+
65
+ Export **`LabelVisibility`** from `@madecki/ui` for typing `labelVisibility` props.
52
66
 
53
67
  ### Navigation
54
68
  - `Tabs` - Tab navigation. Props: `tabs: {label, value}[]`, `onTabClick: (value) => void`
@@ -109,10 +123,11 @@ import { Grid, GridItem } from "@madecki/ui";
109
123
  </Grid>
110
124
 
111
125
  // Form
112
- import { Input, Select, Button, Stack } from "@madecki/ui";
126
+ import { Input, Textarea, Select, Button, Stack } from "@madecki/ui";
113
127
 
114
128
  <Stack gap="4">
115
129
  <Input name="email" label="Email" type="email" onChange={setEmail} />
130
+ <Textarea name="bio" label="Bio" rows={5} onChange={setBio} />
116
131
  <Select
117
132
  name="role"
118
133
  label="Role"
@@ -127,6 +142,20 @@ import { Input, Select, Button, Stack } from "@madecki/ui";
127
142
  </Stack>
128
143
  ```
129
144
 
145
+ ### Input — controlled vs uncontrolled
146
+
147
+ | Mode | What you pass | Who owns the text |
148
+ |------|----------------|-------------------|
149
+ | **Controlled** | `value` (+ `onChange`) | The parent. The field always shows `value`. Update parent state in `onChange` (including for Playwright `fill`) or the UI will not match. |
150
+ | **Uncontrolled** | No `value`; optionally `defaultValue` | The `Input` component. `defaultValue` applies on first mount only; it does not resync if the prop changes later (remount with a `key` if you need that). |
151
+
152
+ If **`value !== undefined`**, the input is controlled (including **`value=""`**).
153
+
154
+ ### Input — testing
155
+
156
+ - Prefer querying by **accessible name** from the label, e.g. **`getByRole("textbox", { name: "Email" })`** or **`getByLabelText("Email")`**. Optional **`testId`** still sets `data-testid` on the native `<input>` for Playwright when you want a stable hook.
157
+ - With **controlled** `value` + `onChange`, `fill()` / typing stay in sync as long as the parent updates state from `onChange` (no `onInputCapture` workaround needed).
158
+
130
159
  ### Select — controlled vs uncontrolled
131
160
 
132
161
  | Mode | What you pass | Who stores the selection |
@@ -141,24 +170,25 @@ import { Input, Select, Button, Stack } from "@madecki/ui";
141
170
 
142
171
  ### Select — testing
143
172
 
173
+ **Prefer label / role:** `getByRole("combobox", { name: "Role" })` matches the combobox’s accessible name from the visible (or sr-only) label.
174
+
144
175
  **Stable hooks:** Set `testId` for predictable selectors (default is `select-{name}`).
145
176
 
146
- - Combobox input: `data-testid={testId}` (also `role="combobox"`, `aria-label` = `label`).
147
- - Open list: `data-testid="{testId}-listbox"` (`role="listbox"`). Open the field first (click or focus) so the list is in the DOM.
177
+ - Combobox input: `data-testid={testId}`, `role="combobox"`.
178
+ - Open list: `data-testid="{testId}-listbox"` (`role="listbox"`, named via `aria-labelledby` → field label). Open the field first (click or focus) so the list is in the DOM.
148
179
  - Each option: `data-testid="{testId}-option-{slug}"` and `data-option-value` with the real value. Slug = option `value` with non-alphanumeric characters replaced by `_`.
149
180
 
150
181
  **Playwright examples:**
151
182
 
152
183
  ```ts
153
- const base = "signup-role";
154
- await page.getByTestId(base).click();
155
- await expect(page.getByTestId(`${base}-listbox`)).toBeVisible();
156
- await page.getByTestId(`${base}-option-admin`).click();
184
+ await page.getByRole("combobox", { name: "Role" }).click();
185
+ await expect(page.getByTestId("signup-role-listbox")).toBeVisible();
186
+ await page.getByTestId("signup-role-option-admin").click();
157
187
  // Or by accessible name (option text is the label):
158
188
  await page.getByRole("option", { name: "Admin" }).click();
159
189
  ```
160
190
 
161
- **Unit / component tests (e.g. Vitest + Testing Library):** Render with a fixed `testId`, `userEvent.click` the combobox, assert the listbox and options with `getByTestId`, or use `getByRole("combobox")` / `getByRole("listbox")` / `getByRole("option", { name: "…" })`.
191
+ **Unit / component tests (e.g. Vitest + Testing Library):** Open the combobox with `getByRole("combobox", { name: "…" })`, then assert the listbox and options with `getByTestId` or `getByRole("option", { name: "…" })`.
162
192
 
163
193
  ## Rules
164
194
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@madecki/ui",
3
- "version": "1.4.0",
3
+ "version": "2.0.0",
4
4
  "description": "Reusable React UI components with Tailwind CSS",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -29,11 +29,13 @@
29
29
  }
30
30
  },
31
31
  "./llm-context": "./llm-context.md",
32
- "./cursor-rule-template": "./cursor-rule-template.md"
32
+ "./cursor-rule-template": "./cursor-rule-template.md",
33
+ "./CHANGELOG.md": "./CHANGELOG.md"
33
34
  },
34
35
  "files": [
35
36
  "dist",
36
37
  "README.md",
38
+ "CHANGELOG.md",
37
39
  "llm-context.md",
38
40
  "cursor-rule-template.md"
39
41
  ],