cleanplate 0.2.2 → 0.2.4

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.
@@ -10,10 +10,10 @@ FormControls is a set of form primitives exported as a namespace: `FormControls.
10
10
  | TextArea | Multi-line text | placeholder, value, onChange(e) |
11
11
  | Select | Single or multi select dropdown | options ({ label, value }[]), value, onChange(option \| option[]), isMulti, placeholder |
12
12
  | Date | Day/month/year picker (DD-MMM-YYYY) | defaultValue, onChange(dateValue: string) |
13
- | Checkbox | Single checkbox | value (boolean), onChange(checked: boolean) |
14
- | Radio | Radio input | name, value, onChange(e) |
15
- | File | File input | onChange(e) |
16
- | Toggle | Toggle input | name, value, onChange(e) |
13
+ | Checkbox | Checkbox group (array-based, multi-select) | name, label, options, value (CheckboxValue[]), defaultValue, onChange(values, e), orientation, variant |
14
+ | Radio | Radio group (array-based) | name, label, options, value, defaultValue, onChange(value, e), orientation, variant |
15
+ | File | File picker with `button` / `card` variants, drag-and-drop, and a removable file list | name, label, variant, multiple, accept, value (File[]), onChange(files, e), buttonLabel, dropZoneText |
16
+ | Toggle | On/off switch | checked, defaultChecked, onChange(checked: boolean) |
17
17
  | Stepper | Text input for step flows | placeholder, value, onChange(e) |
18
18
 
19
19
  ## Types
@@ -53,6 +53,7 @@ interface InputProps {
53
53
  id?: string;
54
54
  onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
55
55
  onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
56
+ onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
56
57
  value?: string;
57
58
  defaultValue?: string;
58
59
  label?: string;
@@ -63,14 +64,36 @@ interface InputProps {
63
64
  isFluid?: boolean;
64
65
  className?: string;
65
66
  error?: string;
67
+ dataTestId?: string;
68
+ /** Native `autocomplete` attribute. */
69
+ autoComplete?: string;
70
+ /** Hard cap on the number of characters the user can type. */
71
+ maxLength?: number;
72
+ /** Numeric lower bound (clamped on blur for `type="number"`). */
73
+ min?: number | string;
74
+ /** Numeric upper bound (clamped on blur for `type="number"`). */
75
+ max?: number | string;
76
+ /** Inline leading affix (currency, country code, …). Soft-capped at 4 chars. */
77
+ prefix?: string;
78
+ /** Inline trailing affix (unit, TLD, …). Soft-capped at 4 chars. */
79
+ suffix?: string;
80
+ /** Spoken label for screen readers when `prefix` is a symbol/abbreviation. */
81
+ prefixA11yLabel?: string;
82
+ /** Spoken label for screen readers when `suffix` is a symbol/abbreviation. */
83
+ suffixA11yLabel?: string;
66
84
  }
67
85
  ```
68
86
 
69
87
  ### Other control types
70
- - **TextAreaProps**, **FileProps**, **RadioProps**, **ToggleProps**: label, value/defaultValue, onChange, isDisabled, isRequired, isFluid, className, error (where applicable).
71
- - **CheckboxProps**: value (boolean), onChange(checked: boolean).
72
- - **DateProps**: defaultValue (string "dd-mm-yyyy"), onChange(dateValue: string).
73
- - **FormControlsStepperProps**: label, placeholder, value, onChange(e).
88
+ - **TextAreaProps**: label, value/defaultValue, onChange, isDisabled, isRequired, isFluid, className, error, dataTestId.
89
+ - **FileProps**: `name`, `label`, `variant` (`"button" | "card"`, default `"button"`), `multiple`, `accept`, `value: File[]` (controlled), `defaultValue: File[]` (uncontrolled initial visual list), `onChange(files: File[], e?)`, `buttonLabel` (default `"Browse file"`), `dropZoneText` (default `"Drag files to upload"`, card variant only), plus the common `isDisabled`, `isRequired`, `isFluid`, `className`, `error`, `dataTestId`. The card variant supports drag-and-drop. **FileVariant** = `"button" | "card"`.
90
+ - **RadioProps**: `options` (non-empty `RadioOption[]`), `name`, `label` (group `<legend>`), optional `id`, `value`, `defaultValue`, `onChange(value, e)`, `orientation` (`"vertical" | "horizontal"`), `variant` (`"default" | "card"`), `isDisabled`, `isRequired`, `isFluid`, `className`, `error`, `dataTestId`.
91
+ - **RadioOption**: `{ label, value, isDisabled?, description?, icon?, dataTestId?, id? }`. `description` is rendered under the option label as muted secondary text and linked via `aria-describedby`. `icon` accepts any `ReactNode` (e.g. `<Icon />`, `<img />`, custom SVG) and renders to the left of the label/description.
92
+ - **ToggleProps**: checked, defaultChecked, onChange(checked: boolean), label, isDisabled, isRequired, isFluid, className, error, dataTestId.
93
+ - **CheckboxProps**: `options` (non-empty `CheckboxOption[]`), `name`, `label` (group `<legend>`), optional `id`, `value` (`CheckboxValue[]`), `defaultValue` (`CheckboxValue[]`), `onChange(values, e)`, `orientation` (`"vertical" | "horizontal"`), `variant` (`"default" | "card"`), `isDisabled`, `isRequired`, `isFluid`, `className`, `error`, `dataTestId`.
94
+ - **CheckboxOption**: `{ label, value, isDisabled?, description?, icon?, dataTestId?, id? }`. `description` is rendered under the option label as muted secondary text and linked via `aria-describedby`. `icon` accepts any `ReactNode` (e.g. `<Icon />`, `<img />`, custom SVG) and renders to the left of the label/description. `CheckboxValue = string | number`.
95
+ - **DateProps**: optional `id`, `label`, `defaultValue` (`"dd-mm-yyyy"`), `onChange(dateValue: string)`, plus `isDisabled`, `isRequired`, `isFluid`, `className`, `error`, `dataTestId`.
96
+ - **FormControlsStepperProps**: label, placeholder, value/defaultValue, onChange(e), type, isDisabled, isRequired, isFluid, className, error, dataTestId.
74
97
 
75
98
  ## Usage Examples
76
99
 
@@ -88,25 +111,221 @@ import { FormControls } from "cleanplate";
88
111
  />
89
112
  ```
90
113
 
91
- ### TextArea, Checkbox, Date
114
+ ### Input with prefix / suffix
115
+
116
+ ```jsx
117
+ <FormControls.Input label="Amount" type="number" prefix="$" suffix="USD" placeholder="0.00" />
118
+ <FormControls.Input label="Phone" type="tel" prefix="+91" placeholder="98765 43210" />
119
+ <FormControls.Input label="Weight" type="number" suffix="kg" placeholder="0" />
120
+ <FormControls.Input label="Discount" type="number" suffix="%" placeholder="0" />
121
+ <FormControls.Input label="Website" type="url" suffix=".com" placeholder="acme" />
122
+ ```
123
+
124
+ ### TextArea and Date
92
125
 
93
126
  ```jsx
94
127
  <FormControls.TextArea label="Message" placeholder="Hello" />
95
- <FormControls.Checkbox label="Accept terms?" value={checked} onChange={setChecked} />
96
128
  <FormControls.Date label="DOB" defaultValue="31-05-1992" onChange={(v) => {}} />
97
129
  ```
98
130
 
131
+ ### Checkbox group
132
+
133
+ Pass an `options` array. The component renders the entire group inside a `<fieldset>` + `<legend>` and emits `onChange(values, event)` with the next array of selected values. The required `*` is rendered on the group label, not on individual options.
134
+
135
+ ```jsx
136
+ const [interests, setInterests] = useState(["product"]);
137
+ <FormControls.Checkbox
138
+ label="Email me about"
139
+ name="interests"
140
+ value={interests}
141
+ onChange={(v) => setInterests(v)}
142
+ options={[
143
+ { label: "Newsletters", value: "newsletter", description: "Weekly digest" },
144
+ { label: "Product updates", value: "product", description: "Release notes for features you use" },
145
+ { label: "Promotions", value: "promo", isDisabled: true },
146
+ ]}
147
+ />
148
+ ```
149
+
150
+ For a single checkbox (consent / opt-in), pass an array with one entry — `value` is still an array; an empty array means unchecked:
151
+
152
+ ```jsx
153
+ const [accepted, setAccepted] = useState([]);
154
+ <FormControls.Checkbox
155
+ label="Terms and conditions"
156
+ name="accept"
157
+ isRequired
158
+ value={accepted}
159
+ onChange={(v) => setAccepted(v)}
160
+ options={[{ label: "I accept the terms and conditions", value: "yes" }]}
161
+ />
162
+ ```
163
+
164
+ ### Checkbox (card variant with icons)
165
+
166
+ ```jsx
167
+ import { FormControls, Icon } from "cleanplate";
168
+
169
+ <FormControls.Checkbox
170
+ label="Add-ons"
171
+ name="addons"
172
+ variant="card"
173
+ orientation="horizontal"
174
+ isFluid
175
+ value={addons}
176
+ onChange={(v) => setAddons(v)}
177
+ options={[
178
+ { label: "Analytics", value: "analytics", description: "Real-time dashboards", icon: <Icon name="bar_chart" /> },
179
+ { label: "Automation", value: "automation", description: "Trigger on events", icon: <Icon name="bolt" /> },
180
+ { label: "Collaboration", value: "collab", description: "Roles and comments", icon: <Icon name="groups" /> },
181
+ ]}
182
+ />
183
+ ```
184
+
185
+ ### Radio group
186
+
187
+ Pass an `options` array. The component renders the entire group inside a `<fieldset>` + `<legend>` and emits `onChange(value, event)`. The required `*` is rendered on the group label, not on individual options.
188
+
189
+ ```jsx
190
+ const [plan, setPlan] = useState("std");
191
+ <FormControls.Radio
192
+ label="Shipping"
193
+ name="ship"
194
+ value={plan}
195
+ onChange={(v) => setPlan(String(v))}
196
+ isRequired
197
+ options={[
198
+ { label: "Standard", value: "std" },
199
+ { label: "Express", value: "exp", description: "1–2 business days" },
200
+ { label: "Overnight", value: "ovn", isDisabled: true },
201
+ ]}
202
+ />
203
+ ```
204
+
205
+ For a single radio, just pass an array with one entry:
206
+
207
+ ```jsx
208
+ <FormControls.Radio
209
+ label="Subscription"
210
+ name="subscribe"
211
+ value={subscribed ? "yes" : ""}
212
+ onChange={(v) => setSubscribed(v === "yes")}
213
+ options={[{ label: "Subscribe to weekly digest", value: "yes" }]}
214
+ />
215
+ ```
216
+
217
+ ### Radio (card variant with icons)
218
+
219
+ ```jsx
220
+ import { FormControls, Icon } from "cleanplate";
221
+
222
+ <FormControls.Radio
223
+ label="Delivery method"
224
+ name="delivery"
225
+ variant="card"
226
+ orientation="horizontal"
227
+ isFluid
228
+ value={method}
229
+ onChange={(v) => setMethod(String(v))}
230
+ options={[
231
+ { label: "Standard", value: "std", description: "4–10 business days · $5.00", icon: <Icon name="local_shipping" /> },
232
+ { label: "Express", value: "exp", description: "2–5 business days · $16.00", icon: <Icon name="bolt" /> },
233
+ { label: "Super Fast", value: "fast", description: "1 business day · $25.00", icon: <Icon name="rocket_launch" /> },
234
+ ]}
235
+ />
236
+ ```
237
+
238
+ ### File (button variant)
239
+
240
+ Compact trigger that looks like a primary button. Selected files render below the trigger as small cards with a type-specific thumbnail icon, name, size, and a remove button.
241
+
242
+ ```jsx
243
+ const [files, setFiles] = useState([]);
244
+ <FormControls.File
245
+ label="Upload file"
246
+ name="upload"
247
+ value={files}
248
+ onChange={(next) => setFiles(next)}
249
+ />
250
+ ```
251
+
252
+ ### File (card / drop-zone variant)
253
+
254
+ Drop-zone with dashed border, helper text, and a `Browse file` CTA. Click anywhere in the zone to open the picker, or drag files in. Combine with `multiple` and `accept` to constrain selection.
255
+
256
+ ```jsx
257
+ const [files, setFiles] = useState([]);
258
+ <FormControls.File
259
+ label="File Upload"
260
+ name="upload"
261
+ variant="card"
262
+ multiple
263
+ accept="image/*,application/pdf"
264
+ value={files}
265
+ onChange={(next) => setFiles(next)}
266
+ />
267
+ ```
268
+
99
269
  ### Used by other components
100
270
 
101
271
  Pagination uses `FormControls.Select` for rows-per-page. Pills uses `FormControls.Input` in edit mode.
102
272
 
103
273
  ## Behavior Notes
104
274
 
275
+ - **Input (`type="number"`):** Renders as `<input type="text" inputmode="numeric" pattern="[0-9]*">` so the field shows the numeric keypad on mobile, validates digit-only input via HTML5 pattern, and avoids the well-known UX issues of native `type="number"` (scroll-wheel mutates value, spinner buttons, accepts `e`/`+`/`-`). Consumers still pass `type="number"` at the API boundary; for decimals or signed numbers, use `type="text"` and add a custom `inputMode`/validation.
276
+ - **Input (`type="search"`):** Keeps `type="search"` semantics (mobile search keyboard, autosuggest history) but hides the browser's native cancel button and renders a leading `search` icon plus a custom `close` clear button from the icon library. The clear button shows only when the input has content, focuses the input on click, and emits a synthetic `onChange` with an empty value so both controlled (`value`/`onChange`) and uncontrolled (`defaultValue`) usage stay in sync.
277
+ - **Input (`prefix` / `suffix`):** Inline leading/trailing text affix for currency (`$`), country code (`+91`), unit (`kg`, `%`), TLD (`.com`), etc. Soft-capped at 4 characters so the layout stays predictable; longer strings are truncated. When set, the field's outer wrapper takes over the visible border / padding / focus ring so the affixes read as part of the same input. Affixes are linked to the input via `aria-describedby`, so screen readers announce e.g. "Amount, dollars, $500" when the visible affix is `$`. For symbols/abbreviations that don't read well, pass `prefixA11yLabel` / `suffixA11yLabel` (e.g. `prefix="$"`, `prefixA11yLabel="dollars"`). Ignored when `type="search"` (search already uses both edges) — for any other `type`, including `number`, affixes work as expected.
278
+ - **Input (validation / constraints):** `maxLength` is passed straight to the native attribute (works for any `type`). `min` / `max` are passed to the native attribute (HTML5 form-validation hints) and, for `type="number"` only, also clamped on `blur` — the user can finish typing freely and the value snaps to the bound when they leave the field.
279
+ - **Input (`autoComplete` / `onBlur`):** `autoComplete` maps to the native attribute (`"email"`, `"current-password"`, `"off"`, …). `onBlur` runs after any internal numeric clamp so consumers see the final value.
105
280
  - **Select:** options are `{ label, value }`; single select passes one option to onChange, multi passes an array. value can be option or array for multi.
106
281
  - **Date:** Returns string "dd-mm-yyyy" to onChange; uses internal day/month/year Selects.
107
- - **Error:** When `error` is set, the field shows error styling and message below.
282
+ - **Radio:** Group-first API — pass `options: RadioOption[]`. Renders `<fieldset>` + `<legend>` with a single `value` and `onChange(value, e)`. `isRequired` puts `*` on the legend and adds `required`/`aria-required` to the first enabled option (HTML5 only requires one input in the group to carry it). Custom ring/dot follows the native `:checked` state so uncontrolled groups stay visually correct. Pass `variant="card"` for tile-style options (ring in top-right, optional `icon` on the left, primary-brand border + tint when selected).
283
+ - **Checkbox:** Group-first API — pass `options: CheckboxOption[]`. Renders `<fieldset>` + `<legend>` with a `value: CheckboxValue[]` and `onChange(values, e)`. `isRequired` puts `*` on the legend and sets `aria-required` on the group; native HTML5 doesn't enforce "at least one" for checkbox groups, so add custom validation at the form layer. Custom box/tick follows the native `:checked` state. Pass `variant="card"` for tile-style options (box in top-right, optional `icon` on the left, primary-brand border + tint when checked). For a single checkbox, pass a one-element `options` array — `value=[]` is unchecked, `value=[opt.value]` is checked.
284
+ - **File:** Native `<input type="file">` is visually hidden but stays in the a11y tree. Manages a `File[]` selection internally; `onChange(files, e)` fires for picker selections, drops, and removals (the underlying event is `undefined` for non-picker triggers). With `multiple`, subsequent picks/drops append; without, the new selection replaces the old. The card variant supports drag-and-drop and tints primary-brand on hover. Removing a file resets the native input so re-selecting the same file still emits a change. `defaultValue` seeds the visual list only — browsers don't allow programmatic pre-population of file inputs.
108
285
  - **isFluid:** Full-width field wrapper.
109
286
 
287
+ ## Theming
288
+
289
+ CleanPlate exposes a thin layer of CSS custom properties on `:root` so consumer apps can retheme the form-control surface without forking styles or wrestling specificity. Override these in your own stylesheet **after** the `cleanplate/dist/index.css` import.
290
+
291
+ | Token | Default | What it controls |
292
+ | --- | --- | --- |
293
+ | `--cp-form-control-radius` | `var(--radius-large)` (12px) | Corner radius for `Input`, `TextArea`, `Stepper`, `Select` trigger + open dropdown corners, `Date` day/month/year segments, `File` (outline trigger, drop zone, in-card CTA, file list rows), and `Radio` / `Checkbox` `variant="card"` option tiles. |
294
+
295
+ ### Recipes
296
+
297
+ ```css
298
+ /* Square-ish form fields across the whole app, while leaving badges, cards, */
299
+ /* and other --radius-large surfaces alone. */
300
+ :root {
301
+ --cp-form-control-radius: 4px;
302
+ }
303
+ ```
304
+
305
+ ```css
306
+ /* Scope the override to one section — CSS custom properties cascade, so any */
307
+ /* wrapper works as the boundary. */
308
+ .checkout-form {
309
+ --cp-form-control-radius: 0;
310
+ }
311
+ ```
312
+
313
+ ```jsx
314
+ // Per-instance override — same token, scoped to one element via the style prop.
315
+ <FormControls.Input
316
+ name="zip"
317
+ label="ZIP"
318
+ style={{ "--cp-form-control-radius": "20px" }}
319
+ />
320
+ ```
321
+
322
+ ### When to override what
323
+
324
+ - **`--cp-form-control-radius`** when you want to retheme just the form-field family (Input, Select trigger, Date, Stepper, TextArea, File triggers and card CTA, file list rows, Radio/Checkbox card tiles). Recommended path.
325
+ - **`--radius-large`** (the underlying design token) when you want every "large radius" surface in CleanPlate — form fields *and* anything else that opts into the same scale — to move together. Coarser, but useful for whole-product rebrands.
326
+
327
+ The component-level token is the public, supported override. Underlying design tokens (`--radius-small`, `--radius-medium`, `--radius-large`, …) are exposed but treated as the lower tier — overriding them is allowed, but expect broader visual impact.
328
+
110
329
  ## Related Components / Links
111
330
 
112
331
  - Pills (uses FormControls.Input in edit mode)
package/llms.txt CHANGED
@@ -177,8 +177,9 @@ All component documentation is located in the `docs/` folder. The following docu
177
177
  ### FormControls
178
178
  - File: `docs/FormControls.md`
179
179
  - Purpose: Set of form primitives: Input, TextArea, Select, Date, Checkbox, Radio, File, Toggle, Stepper. Access via FormControls.Input, FormControls.Select, etc.
180
- - Key Features: Input (text), TextArea, Select (single/multi, options { label, value }), Date (dd-mm-yyyy), Checkbox (onChange(boolean)), Radio, File, Toggle, Stepper; common props label, isRequired, isFluid, error
181
- - Types: InputProps, SelectProps, SelectOption, TextAreaProps, CheckboxProps, FileProps, RadioProps, ToggleProps, DateProps, FormControlsStepperProps
180
+ - Key Features: Input (supports text/search/number + prefix/suffix; number maps to numeric text input and clamps via `min`/`max` on blur; search adds icon + clear button), TextArea, Select (single/multi, options { label, value }, supports `name`/`id`, ARIA wiring, and hidden input for native form submission), Date (dd-mm-yyyy), Checkbox (group-first: options[], CheckboxValue[] for multi-select, onChange(values, e); each option supports `description` and `icon`; `variant="card"` for tile-style options), Radio (group-first: options[], single value, onChange(value, e); each option supports `description` and `icon`; `variant="card"` for tile-style options), File (`variant="button" | "card"`; card variant has dashed drop zone with drag-and-drop; `value: File[]` + `onChange(files, e)`; renders a removable file list with type-aware thumbnail, name, and size), Toggle (switch semantics via checkbox + role="switch"), Stepper; common props label, isRequired, isFluid, error
181
+ - Types: InputProps, SelectProps, SelectOption, TextAreaProps, CheckboxProps, CheckboxOption, CheckboxValue, FileProps, FileVariant, RadioProps, RadioOption, RadioValue, ToggleProps, DateProps, FormControlsStepperProps
182
+ - Theming: Public CSS custom property `--cp-form-control-radius` (default `var(--radius-large)` = 12px) controls corner radius for Input, TextArea, Stepper, Select trigger + open dropdown corners, Date day/month/year segments, File (outline trigger, drop zone, in-card CTA span, file list rows), and Radio/Checkbox `variant="card"` tiles. Override on `:root` (or any wrapper / inline `style`) after importing `cleanplate/dist/index.css` to retheme just form fields. Prefer this over overriding the underlying `--radius-large` design token, which affects every "large radius" surface in the framework.
182
183
  - Related Components: Pills (Input), Pagination (Select), Container, Button
183
184
 
184
185
  ### Toast Component
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cleanplate",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "CleanPlate - A Headless React UI Framework",
5
5
  "files": [
6
6
  "dist",