@madecki/ui 1.5.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,
@@ -306,6 +389,7 @@ var Input = ({
306
389
  defaultValue,
307
390
  placeholder,
308
391
  label,
392
+ labelVisibility = "visible",
309
393
  variant = "primary",
310
394
  type = "text",
311
395
  maxLength,
@@ -319,6 +403,7 @@ var Input = ({
319
403
  icon,
320
404
  testId
321
405
  }) => {
406
+ const labelId = react.useId();
322
407
  const isControlled = valueProp !== void 0;
323
408
  const [internalValue, setInternalValue] = react.useState(() => defaultValue ?? "");
324
409
  const [isFocused, setIsFocused] = react.useState(false);
@@ -354,8 +439,15 @@ var Input = ({
354
439
  }
355
440
  onChange?.(next);
356
441
  };
357
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: /* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: name, children: [
358
- /* @__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
+ ),
359
451
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: inputWrapperClassNames.join(" "), children: [
360
452
  icon && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center pl-4", children: icon }),
361
453
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -375,7 +467,7 @@ var Input = ({
375
467
  required,
376
468
  pattern,
377
469
  title,
378
- "aria-label": ariaLabel || label || name,
470
+ "aria-label": ariaLabel,
379
471
  spellCheck,
380
472
  disabled,
381
473
  "data-testid": testId
@@ -384,6 +476,94 @@ var Input = ({
384
476
  ] })
385
477
  ] }) });
386
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
+ };
387
567
  function optionTestSlug(value) {
388
568
  return value.replace(/[^a-zA-Z0-9_-]/g, "_");
389
569
  }
@@ -468,6 +648,7 @@ function Select(props) {
468
648
  const {
469
649
  name,
470
650
  label,
651
+ labelVisibility = "visible",
471
652
  options,
472
653
  placeholder = "Select\u2026",
473
654
  variant = "primary",
@@ -475,6 +656,7 @@ function Select(props) {
475
656
  className = "",
476
657
  testId: testIdProp
477
658
  } = props;
659
+ const labelId = react.useId();
478
660
  const isMulti = props.multi === true;
479
661
  const singleValueProp = !isMulti ? props.value : void 0;
480
662
  const multiValueProp = isMulti ? props.value : void 0;
@@ -670,8 +852,15 @@ function Select(props) {
670
852
  const activeDescendant = open && filteredOptions[highlightIndex] ? `${name}-option-${optionTestSlug(filteredOptions[highlightIndex].value)}` : void 0;
671
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";
672
854
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className: `relative ${className}`.trim(), children: [
673
- /* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: name, children: [
674
- /* @__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
+ ),
675
864
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: inputWrapperClassNames.join(" "), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: innerFieldClassNames.join(" "), children: [
676
865
  /* @__PURE__ */ jsxRuntime.jsx(
677
866
  "input",
@@ -685,7 +874,6 @@ function Select(props) {
685
874
  disabled,
686
875
  placeholder,
687
876
  value: inputValue,
688
- "aria-label": label,
689
877
  "aria-expanded": open,
690
878
  "aria-haspopup": "listbox",
691
879
  "aria-controls": listboxId,
@@ -710,7 +898,7 @@ function Select(props) {
710
898
  id: listboxId,
711
899
  role: "listbox",
712
900
  "aria-multiselectable": isMulti,
713
- "aria-label": label,
901
+ "aria-labelledby": labelId,
714
902
  "data-testid": `${baseTestId}-listbox`,
715
903
  tabIndex: -1,
716
904
  className: listboxClass,
@@ -862,7 +1050,7 @@ var ContentBox = ({
862
1050
  }
863
1051
  );
864
1052
  };
865
- var sizeStyles = {
1053
+ var sizeStyles2 = {
866
1054
  sm: "max-w-screen-sm",
867
1055
  md: "max-w-screen-md",
868
1056
  lg: "max-w-screen-lg",
@@ -879,7 +1067,7 @@ var Container = ({
879
1067
  return /* @__PURE__ */ jsxRuntime.jsx(
880
1068
  "div",
881
1069
  {
882
- className: `w-full px-5 ${sizeStyles[size]} ${centeredClass} ${className}`,
1070
+ className: `w-full px-5 ${sizeStyles2[size]} ${centeredClass} ${className}`,
883
1071
  children
884
1072
  }
885
1073
  );
@@ -974,7 +1162,7 @@ var GridItem = ({
974
1162
  }) => {
975
1163
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `${colSpanStyles[colSpan]} ${className}`, children });
976
1164
  };
977
- var sizeStyles2 = {
1165
+ var sizeStyles3 = {
978
1166
  xs: "text-xs",
979
1167
  sm: "text-sm",
980
1168
  md: "text-md",
@@ -984,13 +1172,13 @@ var sizeStyles2 = {
984
1172
  "3xl": "text-3xl",
985
1173
  "4xl": "text-4xl"
986
1174
  };
987
- var weightStyles = {
1175
+ var weightStyles2 = {
988
1176
  normal: "font-normal",
989
1177
  medium: "font-medium",
990
1178
  semibold: "font-semibold",
991
1179
  bold: "font-bold"
992
1180
  };
993
- var colorStyles = {
1181
+ var colorStyles2 = {
994
1182
  default: "text-white dark:text-white",
995
1183
  muted: "text-lightgray dark:text-lightgray",
996
1184
  primary: "text-primary dark:text-white",
@@ -1019,47 +1207,11 @@ var Heading = ({
1019
1207
  return react.createElement(
1020
1208
  tag,
1021
1209
  {
1022
- className: `${sizeStyles2[resolvedSize]} ${weightStyles[weight]} ${colorStyles[color]} ${className}`
1210
+ className: `${sizeStyles3[resolvedSize]} ${weightStyles2[weight]} ${colorStyles2[color]} ${className}`
1023
1211
  },
1024
1212
  children
1025
1213
  );
1026
1214
  };
1027
- var sizeStyles3 = {
1028
- xs: "text-xs",
1029
- sm: "text-sm",
1030
- md: "text-md",
1031
- lg: "text-lg"
1032
- };
1033
- var weightStyles2 = {
1034
- normal: "font-normal",
1035
- medium: "font-medium",
1036
- semibold: "font-semibold",
1037
- bold: "font-bold"
1038
- };
1039
- var colorStyles2 = {
1040
- default: "text-white dark:text-white",
1041
- muted: "text-lightgray dark:text-lightgray",
1042
- primary: "text-primary dark:text-white",
1043
- success: "text-success",
1044
- warning: "text-warning",
1045
- danger: "text-danger"
1046
- };
1047
- var Text = ({
1048
- children,
1049
- size = "md",
1050
- weight = "normal",
1051
- color = "default",
1052
- as: Tag2 = "p",
1053
- className = ""
1054
- }) => {
1055
- return /* @__PURE__ */ jsxRuntime.jsx(
1056
- Tag2,
1057
- {
1058
- className: `${sizeStyles3[size]} ${weightStyles2[weight]} ${colorStyles2[color]} ${className}`,
1059
- children
1060
- }
1061
- );
1062
- };
1063
1215
  var Heart = ({
1064
1216
  variant = "outline",
1065
1217
  className = "",
@@ -1455,6 +1607,7 @@ exports.Stack = Stack;
1455
1607
  exports.Tabs = Tabs;
1456
1608
  exports.Tag = Tag;
1457
1609
  exports.Text = Text;
1610
+ exports.Textarea = Textarea;
1458
1611
  exports.TwitterIcon = TwitterIcon;
1459
1612
  exports.Warning = Warning;
1460
1613
  //# sourceMappingURL=index.cjs.map