@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/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ ## [2.0.0](https://github.com/madecki/ui/compare/v1.5.0...v2.0.0) (2026-04-08)
2
+
3
+ ### ⚠ BREAKING CHANGES
4
+
5
+ * RadioButtons requires `label`. Group uses radiogroup; options use radio + aria-checked.
6
+
7
+ Made-with: Cursor
8
+
9
+ ### Features
10
+
11
+ * add visible form labels, Textarea, and automated changelog ([f00476c](https://github.com/madecki/ui/commit/f00476ca7c548f0e7c1397c22e426449bed2d3f1))
12
+
13
+ ### Bug Fixes
14
+
15
+ * unblock release checks for Textarea story and workflows ([6aed8a7](https://github.com/madecki/ui/commit/6aed8a77fbb46ae9734024e0195cf323176abd31))
16
+
17
+ # Changelog
18
+
19
+ All release notes in this file are **generated automatically** by [semantic-release](https://semantic-release.gitbook.io/) when a version is published to npm. They are built **only** from [Conventional Commits](https://www.conventionalcommits.org/) on **`main`** (see **Releasing** in the repository README).
20
+
21
+ **Reading this file:** The **latest** published release is always the **topmost** `## [x.y.z]` section. It should match `"version"` in `package.json` inside the package you installed.
22
+
23
+ **Contributing:** Do not hand-edit versioned sections. Describe user-visible work in commits: use **`feat`**, **`fix`**, or **`perf`** so they appear in the notes. For **breaking** API or behavior changes, use a **`!`** after the type (e.g. **`feat!:`**) and/or a **`BREAKING CHANGE:`** paragraph in the commit **body**. [commitlint](https://commitlint.js.org/) validates messages on **`git commit`** (Husky) and on **every push to `main`** (CI).
package/README.md CHANGED
@@ -574,30 +574,44 @@ src/components/ComponentName/
574
574
 
575
575
  ## Releasing
576
576
 
577
- Releases are fully automated using [semantic-release](https://semantic-release.gitbook.io/). When commits are pushed to `main`, the CI automatically:
577
+ Releases are fully automated using [semantic-release](https://semantic-release.gitbook.io/). When commits are pushed to `main`, the **Release** workflow runs `typecheck`, `lint`, `build`, and **`npm test`**, then **`npx semantic-release`**, which in order:
578
578
 
579
- 1. Analyzes commit messages to determine the version bump
580
- 2. Updates `package.json` and `CHANGELOG.md`
581
- 3. Creates a Git tag and GitHub Release
582
- 4. Publishes to NPM with provenance
579
+ 1. Analyzes **conventional** commit messages since the last release to pick the **semver** bump
580
+ 2. Generates release notes **only** from those commits
581
+ 3. **Prepends** a new `## [version]` section to **`CHANGELOG.md`** and sets **`package.json`** `"version"`
582
+ 4. Publishes to npm (the `prepublishOnly` script builds again)
583
+ 5. Pushes the version commit, creates a Git tag, and opens a **GitHub Release**
584
+
585
+ There is **no** separate manual changelog step: if it is not described in a merged commit on `main`, it will not appear in **`CHANGELOG.md`**.
583
586
 
584
587
  ### Commit Message Format
585
588
 
586
- This project uses [Conventional Commits](https://www.conventionalcommits.org/) enforced by [commitlint](https://commitlint.js.org/).
589
+ This project uses [Conventional Commits](https://www.conventionalcommits.org/) enforced by [commitlint](https://commitlint.js.org/):
590
+
591
+ - **Locally:** Husky runs **`commitlint`** on the **`commit-msg`** hook, so **`git commit` fails** if the message does not follow the rules (bypass only with **`git commit --no-verify`**).
592
+ - **On `main`:** CI runs commitlint on **every push** for the commits in that push, so bad messages are caught even if someone skipped the hook.
587
593
 
588
594
  **Format:** `type(scope?): description`
589
595
 
590
- | Type | Description | Release |
591
- |------|-------------|---------|
592
- | `feat` | New feature | Minor |
593
- | `fix` | Bug fix | Patch |
594
- | `docs` | Documentation only | None |
596
+ | Type | Description | Release / changelog |
597
+ |------|-------------|---------------------|
598
+ | `feat` | New feature | Minor; appears under **Features** |
599
+ | `fix` | Bug fix | Patch; appears under **Bug Fixes** |
600
+ | `perf` | Performance improvement | Patch; appears under **Performance Improvements** |
601
+ | `docs` | Documentation only | None (not in changelog by default) |
595
602
  | `style` | Code style (formatting) | None |
596
603
  | `refactor` | Code change (no fix/feat) | None |
597
- | `perf` | Performance improvement | Patch |
598
604
  | `test` | Adding/updating tests | None |
599
605
  | `chore` | Maintenance tasks | None |
600
606
 
607
+ **Breaking changes** (major bump): use **`feat!:`** / **`fix!:`** and/or a **`BREAKING CHANGE:`** paragraph in the commit **body** (after a blank line). Example body:
608
+
609
+ ```text
610
+ feat!: require label on RadioButtons
611
+
612
+ BREAKING CHANGE: RadioButtons now requires a `label` prop.
613
+ ```
614
+
601
615
  ### Examples
602
616
 
603
617
  ```bash
package/dist/index.cjs CHANGED
@@ -101,7 +101,9 @@ var Button = ({
101
101
  label,
102
102
  disabled,
103
103
  className = "",
104
- type = "button"
104
+ type = "button",
105
+ role,
106
+ ariaChecked
105
107
  }) => {
106
108
  if (typeof isActive === "boolean" && id === void 0) {
107
109
  throw Error("If button has isActive props, it must have id props too");
@@ -118,6 +120,8 @@ var Button = ({
118
120
  "button",
119
121
  {
120
122
  type,
123
+ role,
124
+ "aria-checked": ariaChecked,
121
125
  className: surfaceClassName,
122
126
  onClick: () => {
123
127
  if (isActive === true) {
@@ -254,27 +258,106 @@ var GradientButton = ({
254
258
  }
255
259
  );
256
260
  };
261
+ var sizeStyles = {
262
+ xs: "text-xs",
263
+ sm: "text-sm",
264
+ md: "text-md",
265
+ lg: "text-lg"
266
+ };
267
+ var weightStyles = {
268
+ normal: "font-normal",
269
+ medium: "font-medium",
270
+ semibold: "font-semibold",
271
+ bold: "font-bold"
272
+ };
273
+ var colorStyles = {
274
+ default: "text-white dark:text-white",
275
+ muted: "text-lightgray dark:text-lightgray",
276
+ primary: "text-primary dark:text-white",
277
+ success: "text-success",
278
+ warning: "text-warning",
279
+ danger: "text-danger"
280
+ };
281
+ var Text = ({
282
+ children,
283
+ id,
284
+ size = "md",
285
+ weight = "normal",
286
+ color = "default",
287
+ as: Tag2 = "p",
288
+ className = ""
289
+ }) => {
290
+ return /* @__PURE__ */ jsxRuntime.jsx(
291
+ Tag2,
292
+ {
293
+ id,
294
+ className: `${sizeStyles[size]} ${weightStyles[weight]} ${colorStyles[color]} ${className}`,
295
+ children
296
+ }
297
+ );
298
+ };
299
+ function FormFieldLabel({
300
+ label,
301
+ labelVisibility,
302
+ id
303
+ }) {
304
+ return /* @__PURE__ */ jsxRuntime.jsx(
305
+ Text,
306
+ {
307
+ as: "span",
308
+ id,
309
+ size: "sm",
310
+ weight: "medium",
311
+ color: "muted",
312
+ className: labelVisibility === "sr-only" ? "sr-only" : "mb-2 block",
313
+ children: label
314
+ }
315
+ );
316
+ }
257
317
  var RadioButtons = ({
318
+ label,
319
+ labelVisibility = "visible",
258
320
  items,
259
321
  onChange,
260
322
  size = "md",
261
323
  className = ""
262
324
  }) => {
325
+ const labelId = react.useId();
263
326
  const [selectedButton, setSelectedButton] = react.useState();
264
327
  const onButtonClick = (id) => {
265
328
  setSelectedButton(id);
266
329
  onChange(id ?? "");
267
330
  };
268
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `flex flex-wrap gap-2 ${className}`, children: items.map((item) => /* @__PURE__ */ react.createElement(
269
- Button,
270
- {
271
- ...item,
272
- size,
273
- key: item.id,
274
- isActive: selectedButton === item.id,
275
- onClick: onButtonClick
276
- }
277
- )) });
331
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, children: [
332
+ /* @__PURE__ */ jsxRuntime.jsx(
333
+ FormFieldLabel,
334
+ {
335
+ id: labelId,
336
+ label,
337
+ labelVisibility
338
+ }
339
+ ),
340
+ /* @__PURE__ */ jsxRuntime.jsx(
341
+ "div",
342
+ {
343
+ role: "radiogroup",
344
+ "aria-labelledby": labelId,
345
+ className: "flex flex-wrap gap-2",
346
+ children: items.map((item) => /* @__PURE__ */ react.createElement(
347
+ Button,
348
+ {
349
+ ...item,
350
+ size,
351
+ key: item.id,
352
+ role: "radio",
353
+ ariaChecked: selectedButton === item.id,
354
+ isActive: selectedButton === item.id,
355
+ onClick: onButtonClick
356
+ }
357
+ ))
358
+ }
359
+ )
360
+ ] });
278
361
  };
279
362
  var Tag = ({
280
363
  variant,
@@ -302,11 +385,14 @@ var Tag = ({
302
385
  var Input = ({
303
386
  name,
304
387
  onChange,
388
+ value: valueProp,
305
389
  defaultValue,
306
390
  placeholder,
307
391
  label,
392
+ labelVisibility = "visible",
308
393
  variant = "primary",
309
394
  type = "text",
395
+ maxLength,
310
396
  required = false,
311
397
  pattern,
312
398
  title,
@@ -314,38 +400,54 @@ var Input = ({
314
400
  spellCheck,
315
401
  disabled = false,
316
402
  className = "",
317
- icon
403
+ icon,
404
+ testId
318
405
  }) => {
319
- const [value, setValue] = react.useState(defaultValue);
406
+ const labelId = react.useId();
407
+ const isControlled = valueProp !== void 0;
408
+ const [internalValue, setInternalValue] = react.useState(() => defaultValue ?? "");
320
409
  const [isFocused, setIsFocused] = react.useState(false);
410
+ const value = isControlled ? valueProp : internalValue;
321
411
  const inputClassNames = ["rounded-sm font-sans z-10 w-full"];
322
412
  const spacings = "py-4 px-5";
323
413
  const outline = "outline-hidden";
324
414
  inputClassNames.push(spacings, outline);
325
415
  const inputWrapperClassNames = ["flex rounded-smb p-px w-full"];
326
- if (isFocused) inputWrapperClassNames.push("bg-gradient");
416
+ if (isFocused) {
417
+ inputWrapperClassNames.push("bg-gradient");
418
+ } else if (variant === "primary" || variant === "tertiary") {
419
+ inputWrapperClassNames.push("bg-lightgray");
420
+ }
327
421
  switch (variant) {
328
422
  case "primary":
329
423
  inputClassNames.push("text-primary bg-neutral");
330
- inputWrapperClassNames.push("bg-lightgray");
331
424
  break;
332
425
  case "secondary":
333
426
  inputClassNames.push("text-neutral bg-neutral dark:bg-gray");
334
427
  break;
335
428
  case "tertiary":
336
429
  inputClassNames.push("text-neutral bg-neutral dark:bg-primary");
337
- inputWrapperClassNames.push("bg-lightgray");
338
430
  break;
339
431
  }
340
432
  if (disabled) {
341
433
  inputClassNames.push("cursor-not-allowed opacity-50");
342
434
  }
343
435
  const onInputChange = (event) => {
344
- setValue(event.target.value);
345
- onChange?.(event.target.value);
436
+ const next = event.target.value;
437
+ if (!isControlled) {
438
+ setInternalValue(next);
439
+ }
440
+ onChange?.(next);
346
441
  };
347
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: /* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: name, children: [
348
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: label }),
442
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: /* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: name, className: "block", children: [
443
+ /* @__PURE__ */ jsxRuntime.jsx(
444
+ FormFieldLabel,
445
+ {
446
+ id: labelId,
447
+ label,
448
+ labelVisibility
449
+ }
450
+ ),
349
451
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: inputWrapperClassNames.join(" "), children: [
350
452
  icon && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center pl-4", children: icon }),
351
453
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -354,24 +456,114 @@ var Input = ({
354
456
  id: name,
355
457
  name,
356
458
  placeholder,
357
- value: value || "",
459
+ value,
358
460
  className: inputClassNames.join(" "),
359
461
  autoComplete: "off",
360
462
  onChange: onInputChange,
361
463
  onFocus: () => setIsFocused(true),
362
464
  onBlur: () => setIsFocused(false),
363
465
  type,
466
+ maxLength,
364
467
  required,
365
468
  pattern,
366
469
  title,
367
- "aria-label": ariaLabel || label || name,
470
+ "aria-label": ariaLabel,
368
471
  spellCheck,
369
- disabled
472
+ disabled,
473
+ "data-testid": testId
370
474
  }
371
475
  )
372
476
  ] })
373
477
  ] }) });
374
478
  };
479
+ var Textarea = ({
480
+ name,
481
+ onChange,
482
+ value: valueProp,
483
+ defaultValue,
484
+ placeholder,
485
+ label,
486
+ labelVisibility = "visible",
487
+ variant = "primary",
488
+ rows = 4,
489
+ maxLength,
490
+ required = false,
491
+ ariaLabel,
492
+ spellCheck,
493
+ disabled = false,
494
+ className = "",
495
+ testId
496
+ }) => {
497
+ const labelId = react.useId();
498
+ const isControlled = valueProp !== void 0;
499
+ const [internalValue, setInternalValue] = react.useState(() => defaultValue ?? "");
500
+ const [isFocused, setIsFocused] = react.useState(false);
501
+ const value = isControlled ? valueProp : internalValue;
502
+ const fieldClassNames = [
503
+ "min-h-[6rem] resize-y rounded-sm font-sans z-10 w-full"
504
+ ];
505
+ const spacings = "py-4 px-5";
506
+ const outline = "outline-hidden";
507
+ fieldClassNames.push(spacings, outline);
508
+ const inputWrapperClassNames = ["rounded-smb p-px w-full"];
509
+ if (isFocused) {
510
+ inputWrapperClassNames.push("bg-gradient");
511
+ } else if (variant === "primary" || variant === "tertiary") {
512
+ inputWrapperClassNames.push("bg-lightgray");
513
+ }
514
+ switch (variant) {
515
+ case "primary":
516
+ fieldClassNames.push("text-primary bg-neutral");
517
+ break;
518
+ case "secondary":
519
+ fieldClassNames.push("text-neutral bg-neutral dark:bg-gray");
520
+ break;
521
+ case "tertiary":
522
+ fieldClassNames.push("text-neutral bg-neutral dark:bg-primary");
523
+ break;
524
+ }
525
+ if (disabled) {
526
+ fieldClassNames.push("cursor-not-allowed opacity-50");
527
+ }
528
+ const onFieldChange = (event) => {
529
+ const next = event.target.value;
530
+ if (!isControlled) {
531
+ setInternalValue(next);
532
+ }
533
+ onChange?.(next);
534
+ };
535
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: /* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: name, className: "block", children: [
536
+ /* @__PURE__ */ jsxRuntime.jsx(
537
+ FormFieldLabel,
538
+ {
539
+ id: labelId,
540
+ label,
541
+ labelVisibility
542
+ }
543
+ ),
544
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: inputWrapperClassNames.join(" "), children: /* @__PURE__ */ jsxRuntime.jsx(
545
+ "textarea",
546
+ {
547
+ id: name,
548
+ name,
549
+ rows,
550
+ placeholder,
551
+ value,
552
+ className: fieldClassNames.join(" "),
553
+ autoComplete: "off",
554
+ onChange: onFieldChange,
555
+ onFocus: () => setIsFocused(true),
556
+ onBlur: () => setIsFocused(false),
557
+ maxLength,
558
+ required,
559
+ "aria-label": ariaLabel,
560
+ spellCheck,
561
+ disabled,
562
+ "data-testid": testId
563
+ }
564
+ ) })
565
+ ] }) });
566
+ };
375
567
  function optionTestSlug(value) {
376
568
  return value.replace(/[^a-zA-Z0-9_-]/g, "_");
377
569
  }
@@ -456,6 +648,7 @@ function Select(props) {
456
648
  const {
457
649
  name,
458
650
  label,
651
+ labelVisibility = "visible",
459
652
  options,
460
653
  placeholder = "Select\u2026",
461
654
  variant = "primary",
@@ -463,6 +656,7 @@ function Select(props) {
463
656
  className = "",
464
657
  testId: testIdProp
465
658
  } = props;
659
+ const labelId = react.useId();
466
660
  const isMulti = props.multi === true;
467
661
  const singleValueProp = !isMulti ? props.value : void 0;
468
662
  const multiValueProp = isMulti ? props.value : void 0;
@@ -658,8 +852,15 @@ function Select(props) {
658
852
  const activeDescendant = open && filteredOptions[highlightIndex] ? `${name}-option-${optionTestSlug(filteredOptions[highlightIndex].value)}` : void 0;
659
853
  const listboxClass = "absolute left-0 right-0 top-full z-50 mt-1 max-h-60 overflow-auto rounded-sm border border-lightgray bg-neutral py-1 shadow-lg dark:border-gray dark:bg-gray dark:text-white";
660
854
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className: `relative ${className}`.trim(), children: [
661
- /* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: name, children: [
662
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: label }),
855
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: name, className: "block", children: [
856
+ /* @__PURE__ */ jsxRuntime.jsx(
857
+ FormFieldLabel,
858
+ {
859
+ id: labelId,
860
+ label,
861
+ labelVisibility
862
+ }
863
+ ),
663
864
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: inputWrapperClassNames.join(" "), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: innerFieldClassNames.join(" "), children: [
664
865
  /* @__PURE__ */ jsxRuntime.jsx(
665
866
  "input",
@@ -673,7 +874,6 @@ function Select(props) {
673
874
  disabled,
674
875
  placeholder,
675
876
  value: inputValue,
676
- "aria-label": label,
677
877
  "aria-expanded": open,
678
878
  "aria-haspopup": "listbox",
679
879
  "aria-controls": listboxId,
@@ -698,7 +898,7 @@ function Select(props) {
698
898
  id: listboxId,
699
899
  role: "listbox",
700
900
  "aria-multiselectable": isMulti,
701
- "aria-label": label,
901
+ "aria-labelledby": labelId,
702
902
  "data-testid": `${baseTestId}-listbox`,
703
903
  tabIndex: -1,
704
904
  className: listboxClass,
@@ -850,7 +1050,7 @@ var ContentBox = ({
850
1050
  }
851
1051
  );
852
1052
  };
853
- var sizeStyles = {
1053
+ var sizeStyles2 = {
854
1054
  sm: "max-w-screen-sm",
855
1055
  md: "max-w-screen-md",
856
1056
  lg: "max-w-screen-lg",
@@ -867,7 +1067,7 @@ var Container = ({
867
1067
  return /* @__PURE__ */ jsxRuntime.jsx(
868
1068
  "div",
869
1069
  {
870
- className: `w-full px-5 ${sizeStyles[size]} ${centeredClass} ${className}`,
1070
+ className: `w-full px-5 ${sizeStyles2[size]} ${centeredClass} ${className}`,
871
1071
  children
872
1072
  }
873
1073
  );
@@ -962,7 +1162,7 @@ var GridItem = ({
962
1162
  }) => {
963
1163
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `${colSpanStyles[colSpan]} ${className}`, children });
964
1164
  };
965
- var sizeStyles2 = {
1165
+ var sizeStyles3 = {
966
1166
  xs: "text-xs",
967
1167
  sm: "text-sm",
968
1168
  md: "text-md",
@@ -972,13 +1172,13 @@ var sizeStyles2 = {
972
1172
  "3xl": "text-3xl",
973
1173
  "4xl": "text-4xl"
974
1174
  };
975
- var weightStyles = {
1175
+ var weightStyles2 = {
976
1176
  normal: "font-normal",
977
1177
  medium: "font-medium",
978
1178
  semibold: "font-semibold",
979
1179
  bold: "font-bold"
980
1180
  };
981
- var colorStyles = {
1181
+ var colorStyles2 = {
982
1182
  default: "text-white dark:text-white",
983
1183
  muted: "text-lightgray dark:text-lightgray",
984
1184
  primary: "text-primary dark:text-white",
@@ -1007,47 +1207,11 @@ var Heading = ({
1007
1207
  return react.createElement(
1008
1208
  tag,
1009
1209
  {
1010
- className: `${sizeStyles2[resolvedSize]} ${weightStyles[weight]} ${colorStyles[color]} ${className}`
1210
+ className: `${sizeStyles3[resolvedSize]} ${weightStyles2[weight]} ${colorStyles2[color]} ${className}`
1011
1211
  },
1012
1212
  children
1013
1213
  );
1014
1214
  };
1015
- var sizeStyles3 = {
1016
- xs: "text-xs",
1017
- sm: "text-sm",
1018
- md: "text-md",
1019
- lg: "text-lg"
1020
- };
1021
- var weightStyles2 = {
1022
- normal: "font-normal",
1023
- medium: "font-medium",
1024
- semibold: "font-semibold",
1025
- bold: "font-bold"
1026
- };
1027
- var colorStyles2 = {
1028
- default: "text-white dark:text-white",
1029
- muted: "text-lightgray dark:text-lightgray",
1030
- primary: "text-primary dark:text-white",
1031
- success: "text-success",
1032
- warning: "text-warning",
1033
- danger: "text-danger"
1034
- };
1035
- var Text = ({
1036
- children,
1037
- size = "md",
1038
- weight = "normal",
1039
- color = "default",
1040
- as: Tag2 = "p",
1041
- className = ""
1042
- }) => {
1043
- return /* @__PURE__ */ jsxRuntime.jsx(
1044
- Tag2,
1045
- {
1046
- className: `${sizeStyles3[size]} ${weightStyles2[weight]} ${colorStyles2[color]} ${className}`,
1047
- children
1048
- }
1049
- );
1050
- };
1051
1215
  var Heart = ({
1052
1216
  variant = "outline",
1053
1217
  className = "",
@@ -1443,6 +1607,7 @@ exports.Stack = Stack;
1443
1607
  exports.Tabs = Tabs;
1444
1608
  exports.Tag = Tag;
1445
1609
  exports.Text = Text;
1610
+ exports.Textarea = Textarea;
1446
1611
  exports.TwitterIcon = TwitterIcon;
1447
1612
  exports.Warning = Warning;
1448
1613
  //# sourceMappingURL=index.cjs.map