@alexandregme/zljs 1.11.0 → 1.13.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/CHANGELOG.md CHANGED
@@ -8,6 +8,62 @@ A change that needs action in an app has a **Migration** note. When updating, re
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.13.0] - 2026-11-02
12
+
13
+ ### Added
14
+
15
+ - `Radio` takes `aria-label`, `required`, `onBlur`, `onKeyDown` and `ref`, as `Input` does, so it works with `register` from `react-hook-form`
16
+
17
+ ### Changed
18
+
19
+ - `Radio` requires `label` or `aria-label` in its type, instead of always `label`
20
+ - `Radio` input now sits inside its `<label>`, as on `Checkbox`, so the gap between the circle and the text is clickable too
21
+ - `Radio` draws a real `focus-visible` outline in the theme's `primary` color instead of a `focus:ring`, so focus stays visible in forced colors mode
22
+
23
+ ### Fixed
24
+
25
+ - A checked `Radio` uses the theme's `primary` color. It had `text-blue-600` and `border-gray-300`, which do nothing on a native radio, so it kept the browser's own color
26
+ - The label of a disabled `Radio` shows the `not-allowed` cursor instead of `pointer`
27
+
28
+ #### Migration
29
+
30
+ Only code that extends `RadioProps`, and tests or selectors that rely on the old structure or classes, need a change. To find them, run the commands below from the app's root. The second also finds other uses of these classes, and those stay as they are:
31
+
32
+ ```sh
33
+ grep -rnE "\bRadioProps\b" . --include='*.ts' --include='*.tsx' --exclude-dir=node_modules --exclude-dir=.next
34
+ grep -rnE "text-blue-600|focus:ring-blue-500|nextSibling|previousSibling" . --include='*.test.*' --exclude-dir=node_modules --exclude-dir=.next
35
+ ```
36
+
37
+ Replace `interface X extends RadioProps` with `type X = RadioProps & { ... }`, because `RadioProps` is now a union. In tests, replace those classes with `accent-zl-primary` and `focus-visible:outline-zl-primary`, and find the radio by role instead of by its position next to the label.
38
+
39
+ ## [1.12.0] - 2026-11-02
40
+
41
+ ### Added
42
+
43
+ - `TextArea` takes `aria-label`, `name`, `autoComplete`, `required`, `onBlur`, `onKeyDown` and `ref`, as `Input` and `Select` do, so it works with `register` from `react-hook-form`
44
+
45
+ ### Changed
46
+
47
+ - `TextArea` requires `label` or `aria-label` in its type, instead of always `label`
48
+ - `TextArea` draws a real `focus-visible` outline in the theme's `primary` color instead of a `focus:ring`, so focus stays visible in forced colors mode, and an app's own color reaches it
49
+ - `TextArea` border is now `gray-500` instead of `gray-300`, for 3:1 contrast, and its placeholder is `gray-500`, for 4.5:1
50
+
51
+ ### Fixed
52
+
53
+ - `TextArea` with `error` sets `aria-invalid` and points `aria-describedby` to the message, so a screen reader announces both
54
+ - `TextArea` uses `resize-y`. It had a class that doesn't exist in Tailwind, so it only resized vertically in apps with Tailwind's base styles
55
+
56
+ #### Migration
57
+
58
+ Only code that extends `TextAreaProps` and tests that assert its old classes need a change. To find them, run the commands below from the app's root:
59
+
60
+ ```sh
61
+ grep -rnE "\bTextAreaProps\b" . --include='*.ts' --include='*.tsx' --exclude-dir=node_modules --exclude-dir=.next
62
+ grep -rnE "border-gray-300|focus:ring-blue-500|focus:border-transparent|resize-vertical" . --include='*.test.*' --exclude-dir=node_modules --exclude-dir=.next
63
+ ```
64
+
65
+ Replace `interface X extends TextAreaProps` with `type X = TextAreaProps & { ... }`, because `TextAreaProps` is now a union. In tests, replace those classes with `focus-visible:outline-zl-primary`, `border-gray-500` and `resize-y`.
66
+
11
67
  ## [1.11.0] - 2026-11-02
12
68
 
13
69
  ### Added
@@ -106,7 +162,9 @@ Replace the shade with the class of the `color` being tested, for example `/(^|\
106
162
 
107
163
  The message of `Error` is `text-zl-danger`. `none` and `ghost` didn't change.
108
164
 
109
- [Unreleased]: https://github.com/alexandregme/zljs/compare/v1.11.0...HEAD
165
+ [Unreleased]: https://github.com/alexandregme/zljs/compare/v1.13.0...HEAD
166
+ [1.13.0]: https://github.com/alexandregme/zljs/compare/v1.12.0...v1.13.0
167
+ [1.12.0]: https://github.com/alexandregme/zljs/compare/v1.11.0...v1.12.0
110
168
  [1.11.0]: https://github.com/alexandregme/zljs/compare/v1.10.0...v1.11.0
111
169
  [1.10.0]: https://github.com/alexandregme/zljs/compare/v1.9.0...v1.10.0
112
170
  [1.9.0]: https://github.com/alexandregme/zljs/compare/v1.8.0...v1.9.0
package/dist/index.d.ts CHANGED
@@ -121,16 +121,26 @@ type InputProps = InputName & {
121
121
 
122
122
  declare const Input: react.ForwardRefExoticComponent<InputProps & react.RefAttributes<HTMLInputElement>>;
123
123
 
124
- interface RadioProps {
124
+ type RadioName = {
125
125
  label: string;
126
+ "aria-label"?: string;
127
+ } | {
128
+ label?: undefined;
129
+ "aria-label": string;
130
+ };
131
+ type RadioProps = RadioName & {
126
132
  name: string;
127
133
  value: string;
128
134
  checked?: boolean;
129
135
  disabled?: boolean;
136
+ required?: boolean;
130
137
  onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
131
- }
138
+ onBlur?: (event: FocusEvent<HTMLInputElement>) => void;
139
+ onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
140
+ ref?: Ref<HTMLInputElement>;
141
+ };
132
142
 
133
- declare const Radio: ({ label: labelText, name, value, checked, disabled, onChange, }: RadioProps) => react.JSX.Element;
143
+ declare const Radio: ({ label: labelText, "aria-label": ariaLabel, name, value, checked, disabled, required, onChange, onBlur, onKeyDown, ref, }: RadioProps) => react.JSX.Element;
134
144
 
135
145
  interface SelectOption {
136
146
  value: string;
@@ -160,17 +170,29 @@ type SelectProps = SelectName & {
160
170
 
161
171
  declare const Select: ({ label: labelText, "aria-label": ariaLabel, name, options, placeholder, autoComplete, value, error, disabled, required, onChange, onBlur, onKeyDown, ref, }: SelectProps) => react.JSX.Element;
162
172
 
163
- interface TextAreaProps {
173
+ type TextAreaName = {
164
174
  label: string;
175
+ "aria-label"?: string;
176
+ } | {
177
+ label?: undefined;
178
+ "aria-label": string;
179
+ };
180
+ type TextAreaProps = TextAreaName & {
181
+ name?: string;
165
182
  placeholder?: string;
183
+ autoComplete?: HTMLInputAutoCompleteAttribute;
166
184
  value?: string;
167
185
  rows?: number;
168
186
  error?: string;
169
187
  disabled?: boolean;
188
+ required?: boolean;
170
189
  onChange?: (event: ChangeEvent<HTMLTextAreaElement>) => void;
171
- }
190
+ onBlur?: (event: FocusEvent<HTMLTextAreaElement>) => void;
191
+ onKeyDown?: (event: KeyboardEvent<HTMLTextAreaElement>) => void;
192
+ ref?: Ref<HTMLTextAreaElement>;
193
+ };
172
194
 
173
- declare const TextArea: ({ label: labelText, placeholder, value, rows, error, disabled, onChange, }: TextAreaProps) => react.JSX.Element;
195
+ declare const TextArea: ({ label: labelText, "aria-label": ariaLabel, name, placeholder, autoComplete, value, rows, error, disabled, required, onChange, onBlur, onKeyDown, ref, }: TextAreaProps) => react.JSX.Element;
174
196
 
175
197
  declare const Icon: ({ name, size, "aria-label": ariaLabel, }: IconProps) => react.JSX.Element;
176
198
 
package/dist/index.js CHANGED
@@ -306,41 +306,51 @@ import { useId as useId2 } from "react";
306
306
  // core/forms/radio/radio.styles.ts
307
307
  import { tv as tv4 } from "tailwind-variants";
308
308
  var wrapper2 = tv4({
309
- base: "flex items-center gap-2"
309
+ base: "flex w-fit items-center gap-2 cursor-pointer has-disabled:cursor-not-allowed"
310
310
  });
311
311
  var radio = tv4({
312
- base: "w-4 h-4 text-blue-600 border-gray-300 focus:ring-2 focus:ring-blue-500 disabled:cursor-not-allowed"
312
+ base: "w-4 h-4 shrink-0 accent-zl-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zl-primary disabled:cursor-not-allowed"
313
313
  });
314
314
  var label2 = tv4({
315
- base: "text-sm text-gray-700 select-none cursor-pointer"
315
+ base: "text-sm text-gray-700 select-none"
316
316
  });
317
317
 
318
318
  // core/forms/radio/radio.tsx
319
319
  import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
320
320
  var Radio = ({
321
321
  label: labelText,
322
+ "aria-label": ariaLabel,
322
323
  name,
323
324
  value,
324
325
  checked,
325
326
  disabled,
326
- onChange
327
+ required,
328
+ onChange,
329
+ onBlur,
330
+ onKeyDown,
331
+ ref
327
332
  }) => {
328
333
  const id = useId2();
329
- return /* @__PURE__ */ jsxs3("div", { className: wrapper2(), children: [
334
+ return /* @__PURE__ */ jsxs3("label", { htmlFor: id, className: wrapper2(), children: [
330
335
  /* @__PURE__ */ jsx5(
331
336
  "input",
332
337
  {
338
+ ref,
333
339
  id,
334
340
  type: "radio",
335
341
  name,
336
342
  value,
337
343
  checked,
338
344
  disabled,
345
+ required,
339
346
  onChange,
347
+ onBlur,
348
+ onKeyDown,
349
+ "aria-label": ariaLabel,
340
350
  className: radio()
341
351
  }
342
352
  ),
343
- /* @__PURE__ */ jsx5("label", { htmlFor: id, className: label2(), children: labelText })
353
+ labelText && /* @__PURE__ */ jsx5("span", { className: label2(), children: labelText })
344
354
  ] });
345
355
  };
346
356
 
@@ -421,36 +431,53 @@ var label4 = tv6({
421
431
  base: "text-sm font-medium text-gray-700"
422
432
  });
423
433
  var textarea = tv6({
424
- base: "px-3 py-2 border border-gray-300 rounded-md text-sm resize-vertical focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed"
434
+ base: "px-3 py-2 border border-gray-500 rounded-md text-sm placeholder:text-gray-500 resize-y focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zl-primary disabled:bg-gray-100 disabled:cursor-not-allowed"
425
435
  });
426
436
 
427
437
  // core/forms/text-area/text-area.tsx
428
438
  import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
429
439
  var TextArea = ({
430
440
  label: labelText,
441
+ "aria-label": ariaLabel,
442
+ name,
431
443
  placeholder,
444
+ autoComplete,
432
445
  value,
433
446
  rows = 3,
434
447
  error,
435
448
  disabled,
436
- onChange
449
+ required,
450
+ onChange,
451
+ onBlur,
452
+ onKeyDown,
453
+ ref
437
454
  }) => {
438
455
  const id = useId4();
456
+ const errorId = `${id}-error`;
439
457
  return /* @__PURE__ */ jsxs5("div", { className: wrapper4(), children: [
440
- /* @__PURE__ */ jsx7("label", { htmlFor: id, className: label4(), children: labelText }),
458
+ labelText && /* @__PURE__ */ jsx7("label", { htmlFor: id, className: label4(), children: labelText }),
441
459
  /* @__PURE__ */ jsx7(
442
460
  "textarea",
443
461
  {
462
+ ref,
444
463
  id,
464
+ name,
445
465
  placeholder,
466
+ autoComplete,
446
467
  value,
447
468
  rows,
448
469
  disabled,
470
+ required,
449
471
  onChange,
472
+ onBlur,
473
+ onKeyDown,
474
+ "aria-label": ariaLabel,
475
+ "aria-invalid": error ? true : void 0,
476
+ "aria-describedby": error ? errorId : void 0,
450
477
  className: textarea()
451
478
  }
452
479
  ),
453
- error && /* @__PURE__ */ jsx7(Error, { message: error })
480
+ error && /* @__PURE__ */ jsx7(Error, { id: errorId, message: error })
454
481
  ] });
455
482
  };
456
483
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexandregme/zljs",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "description": "Opinionated, accessible React component library for enterprise apps (Next.js, Tailwind CSS v4).",
5
5
  "keywords": [
6
6
  "react",