@bspk/ui 1.1.14 → 1.1.15

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.
Files changed (40) hide show
  1. package/dist/Link.d.ts +2 -1
  2. package/dist/Link.js.map +1 -1
  3. package/dist/NumberInput.d.ts +2 -6
  4. package/dist/NumberInput.js +11 -3
  5. package/dist/NumberInput.js.map +1 -1
  6. package/dist/SegmentedControl.d.ts +11 -7
  7. package/dist/SegmentedControl.js +7 -6
  8. package/dist/SegmentedControl.js.map +1 -1
  9. package/dist/StylesProviderAnywhere.js +1 -1
  10. package/dist/StylesProviderBetterHomesGardens.js +1 -1
  11. package/dist/StylesProviderCartus.js +1 -1
  12. package/dist/StylesProviderCentury21.js +1 -1
  13. package/dist/StylesProviderColdwellBanker.js +1 -1
  14. package/dist/StylesProviderCorcoran.js +1 -1
  15. package/dist/StylesProviderDenaliBoss.js +1 -1
  16. package/dist/StylesProviderEra.js +1 -1
  17. package/dist/StylesProviderSothebys.js +1 -1
  18. package/dist/TabGroup.d.ts +15 -6
  19. package/dist/TabGroup.js +13 -5
  20. package/dist/TabGroup.js.map +1 -1
  21. package/dist/Textarea.d.ts +18 -8
  22. package/dist/Textarea.js +14 -7
  23. package/dist/Textarea.js.map +1 -1
  24. package/dist/base.css +1 -1
  25. package/dist/hooks/{useNavOptions.d.ts → useOptionIconsInvalid.d.ts} +4 -5
  26. package/dist/hooks/useOptionIconsInvalid.js +21 -0
  27. package/dist/hooks/useOptionIconsInvalid.js.map +1 -0
  28. package/dist/textarea.css +1 -1
  29. package/package.json +1 -1
  30. package/src/Link.tsx +28 -25
  31. package/src/NumberInput.tsx +20 -13
  32. package/src/SegmentedControl.tsx +18 -13
  33. package/src/TabGroup.tsx +33 -12
  34. package/src/Textarea.tsx +29 -16
  35. package/src/base.scss +4 -3
  36. package/src/hooks/useOptionIconsInvalid.ts +49 -0
  37. package/src/textarea.scss +5 -2
  38. package/dist/hooks/useNavOptions.js +0 -43
  39. package/dist/hooks/useNavOptions.js.map +0 -1
  40. package/src/hooks/useNavOptions.ts +0 -76
package/dist/Textarea.js CHANGED
@@ -3,24 +3,31 @@ import { styleAdd } from './utils/styleAdd';
3
3
  styleAdd(`[data-bspk=textarea]{/*!
4
4
  --min-rows: is set via inline style
5
5
  --max-rows: is set via inline style
6
- */display:grid;width:100%;--font: var(--body-base);--line-height: 20px;--padding: var(--spacing-sizing-03)}[data-bspk=textarea][data-size=small]{--font: var(--body-small);--line-height: 20px;--padding: var(--spacing-sizing-02)}[data-bspk=textarea][data-size=large]{--font: var(--body-large);--line-height: 24px;--padding: var(--spacing-sizing-03)}[data-bspk=textarea] [data-replicated-value]{white-space:pre-wrap;visibility:hidden}[data-bspk=textarea] textarea,[data-bspk=textarea] [data-replicated-value]{width:100%;font:var(--font);border:1px solid var(--border-color);padding:var(--padding);grid-area:1/1/2/2;min-height:calc(var(--line-height)*var(--min-rows) + var(--padding)*2);max-height:calc(var(--line-height)*var(--max-rows) + var(--padding)*2)}[data-bspk=textarea] textarea{--border-color: var(--stroke-neutral-base);resize:vertical;color:var(--foreground-neutral-on-surface);background-color:var(--surface-neutral-t1-base);border-radius:var(--radius-small)}[data-bspk=textarea] textarea:focus-within{--border-color: var(--stroke-neutral-focus);outline:none;color:var(--foreground-neutral-on-surface)}[data-bspk=textarea] textarea:disabled{pointer-events:none;background:linear-gradient(var(--interactions-disabled-opacity), var(--interactions-disabled-opacity)),linear-gradient(var(--surface-neutral-t1-base), var(--surface-neutral-t1-base));color:var(--foreground-neutral-disabled-on-surface)}[data-bspk=textarea] textarea:read-only{background:linear-gradient(var(--interactions-disabled-opacity), var(--interactions-disabled-opacity)),linear-gradient(var(--surface-neutral-t1-base), var(--surface-neutral-t1-base));color:var(--foreground-neutral-on-surface-variant-02);cursor:not-allowed}[data-bspk=textarea] textarea[aria-invalid]{--border-color: var(--status-error)}`);;
6
+ */display:grid;width:100%;--font: var(--body-base);--line-height: 20px;--padding: var(--spacing-sizing-03)}[data-bspk=textarea][data-size=small]{--font: var(--body-small);--line-height: 20px;--padding: var(--spacing-sizing-02)}[data-bspk=textarea][data-size=large]{--font: var(--body-large);--line-height: 24px;--padding: var(--spacing-sizing-03)}[data-bspk=textarea] [data-replicated-value]{white-space:pre-wrap;visibility:hidden}[data-bspk=textarea] textarea,[data-bspk=textarea] [data-replicated-value]{width:100%;font:var(--font);border:1px solid var(--border-color);padding:var(--padding);grid-area:1/1/2/2;min-height:calc(var(--line-height)*var(--min-rows) + var(--padding)*2);max-height:calc(var(--line-height)*var(--max-rows) + var(--padding)*2)}[data-bspk=textarea] textarea{--border-color: var(--stroke-neutral-base);resize:none;color:var(--foreground-neutral-on-surface);background-color:var(--surface-neutral-t1-base);border-radius:var(--radius-small)}[data-bspk=textarea] textarea::placeholder{color:var(--foreground-neutral-on-surface-variant-03)}[data-bspk=textarea] textarea:focus-within{--border-color: var(--stroke-neutral-focus);outline:none;color:var(--foreground-neutral-on-surface)}[data-bspk=textarea] textarea:disabled{pointer-events:none;background:linear-gradient(var(--interactions-disabled-opacity), var(--interactions-disabled-opacity)),linear-gradient(var(--surface-neutral-t1-base), var(--surface-neutral-t1-base));color:var(--foreground-neutral-disabled-on-surface)}[data-bspk=textarea] textarea:read-only{background:linear-gradient(var(--interactions-disabled-opacity), var(--interactions-disabled-opacity)),linear-gradient(var(--surface-neutral-t1-base), var(--surface-neutral-t1-base));cursor:not-allowed}[data-bspk=textarea] textarea[aria-invalid]{--border-color: var(--status-error)}`);;
7
7
  import { useId } from './hooks/useId';
8
- const MIN_ROWS = 3;
9
- const MAX_ROWS = 10;
8
+ const DEFAULT = {
9
+ minRows: 3,
10
+ maxRows: 10,
11
+ textSize: 'medium',
12
+ };
10
13
  /**
11
14
  * A component that allows users to input large amounts of text that could span multiple lines.
12
15
  *
16
+ * This component gives you a textarea HTML element that automatically adjusts its height to match the length of the
17
+ * content within maximum and minimum rows. A character counter when a maxLength is set to show the number of characters
18
+ * remaining below the limit.
19
+ *
13
20
  * @element
14
21
  *
15
22
  * @name Textarea
16
23
  */
17
- function Textarea({ invalid: invalidProp, onChange, size = 'medium', value = '', name, 'aria-label': ariaLabel, innerRef, placeholder, id: idProp, minRows: minRowsProp = MIN_ROWS, maxRows: maxRowsProp = MAX_ROWS, errorMessage, ...otherProps }) {
24
+ function Textarea({ invalid: invalidProp, onChange, textSize = DEFAULT.textSize, value = '', name, 'aria-label': ariaLabel, innerRef, placeholder, id: idProp, minRows: minRowsProp = DEFAULT.minRows, maxRows: maxRowsProp = DEFAULT.maxRows, errorMessage, ...otherProps }) {
18
25
  const id = useId(idProp);
19
26
  const invalid = !otherProps.readOnly && !otherProps.disabled && invalidProp;
20
27
  // ensure minRows and maxRows are within bounds
21
- const minRows = Math.min(MAX_ROWS, Math.max(minRowsProp, MIN_ROWS));
22
- const maxRows = Math.max(MIN_ROWS, Math.min(maxRowsProp, MAX_ROWS));
23
- return (_jsxs("div", { "data-bspk": "textarea", "data-size": size, style: {
28
+ const minRows = Math.min(DEFAULT.maxRows, Math.max(minRowsProp, DEFAULT.minRows));
29
+ const maxRows = Math.max(DEFAULT.minRows, Math.min(maxRowsProp, DEFAULT.maxRows));
30
+ return (_jsxs("div", { "data-bspk": "textarea", "data-size": textSize, style: {
24
31
  '--min-rows': minRows,
25
32
  '--max-rows': maxRows,
26
33
  }, children: [_jsx("textarea", { ...otherProps, "aria-errormessage": errorMessage || undefined, "aria-invalid": invalid || undefined, "aria-label": ariaLabel, id: id, name: name, onBlur: (event) => {
@@ -1 +1 @@
1
- {"version":3,"file":"Textarea.js","sourceRoot":"","sources":["../src/Textarea.tsx"],"names":[],"mappings":";AAAA,OAAO,iBAAiB,CAAC;AAGzB,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAqDtC,MAAM,QAAQ,GAAG,CAAC,CAAC;AACnB,MAAM,QAAQ,GAAG,EAAE,CAAC;AAEpB;;;;;;GAMG;AACH,SAAS,QAAQ,CAAC,EACd,OAAO,EAAE,WAAW,EACpB,QAAQ,EACR,IAAI,GAAG,QAAQ,EACf,KAAK,GAAG,EAAE,EACV,IAAI,EACJ,YAAY,EAAE,SAAS,EACvB,QAAQ,EACR,WAAW,EACX,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,WAAW,GAAG,QAAQ,EAC/B,OAAO,EAAE,WAAW,GAAG,QAAQ,EAC/B,YAAY,EACZ,GAAG,UAAU,EACD;IACZ,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IACzB,MAAM,OAAO,GAAG,CAAC,UAAU,CAAC,QAAQ,IAAI,CAAC,UAAU,CAAC,QAAQ,IAAI,WAAW,CAAC;IAC5E,+CAA+C;IAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC;IACpE,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC;IAEpE,OAAO,CACH,4BACc,UAAU,eACT,IAAI,EACf,KAAK,EACD;YACI,YAAY,EAAE,OAAO;YACrB,YAAY,EAAE,OAAO;SACP,aAGtB,sBACQ,UAAU,uBACK,YAAY,IAAI,SAAS,kBAC9B,OAAO,IAAI,SAAS,gBACtB,SAAS,EACrB,EAAE,EAAE,EAAE,EACN,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;oBACd,MAAM,MAAM,GAAG,KAAK,CAAC,MAA6B,CAAC;oBACnD,MAAM,CAAC,SAAS,GAAG,CAAC,CAAC;gBACzB,CAAC,EACD,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EACxD,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;oBACf,MAAM,MAAM,GAAG,KAAK,CAAC,MAA6B,CAAC;oBACnD,qEAAqE;oBACrE,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM;wBAAE,OAAO;oBAC/B,MAAM,CAAC,WAA2B,CAAC,SAAS,GAAG,GAAG,MAAM,CAAC,KAAK,IAAI,CAAC;gBACxE,CAAC,EACD,WAAW,EAAE,WAAW,EACxB,GAAG,EAAE,QAAQ,EACb,KAAK,EAAE,KAAK,GACd,EACF,mEAAyC,IACvC,CACT,CAAC;AACN,CAAC;AAED,QAAQ,CAAC,QAAQ,GAAG,UAAU,CAAC;AAE/B,OAAO,EAAE,QAAQ,EAAE,CAAC"}
1
+ {"version":3,"file":"Textarea.js","sourceRoot":"","sources":["../src/Textarea.tsx"],"names":[],"mappings":";AAAA,OAAO,iBAAiB,CAAC;AAGzB,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAItC,MAAM,OAAO,GAAG;IACZ,OAAO,EAAE,CAAC;IACV,OAAO,EAAE,EAAE;IACX,QAAQ,EAAE,QAAQ;CACZ,CAAC;AAyDX;;;;;;;;;;GAUG;AACH,SAAS,QAAQ,CAAC,EACd,OAAO,EAAE,WAAW,EACpB,QAAQ,EACR,QAAQ,GAAG,OAAO,CAAC,QAAQ,EAC3B,KAAK,GAAG,EAAE,EACV,IAAI,EACJ,YAAY,EAAE,SAAS,EACvB,QAAQ,EACR,WAAW,EACX,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,EACtC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,EACtC,YAAY,EACZ,GAAG,UAAU,EACD;IACZ,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IACzB,MAAM,OAAO,GAAG,CAAC,UAAU,CAAC,QAAQ,IAAI,CAAC,UAAU,CAAC,QAAQ,IAAI,WAAW,CAAC;IAC5E,+CAA+C;IAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IAClF,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IAElF,OAAO,CACH,4BACc,UAAU,eACT,QAAQ,EACnB,KAAK,EACD;YACI,YAAY,EAAE,OAAO;YACrB,YAAY,EAAE,OAAO;SACP,aAGtB,sBACQ,UAAU,uBACK,YAAY,IAAI,SAAS,kBAC9B,OAAO,IAAI,SAAS,gBACtB,SAAS,EACrB,EAAE,EAAE,EAAE,EACN,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;oBACd,MAAM,MAAM,GAAG,KAAK,CAAC,MAA6B,CAAC;oBACnD,MAAM,CAAC,SAAS,GAAG,CAAC,CAAC;gBACzB,CAAC,EACD,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EACxD,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;oBACf,MAAM,MAAM,GAAG,KAAK,CAAC,MAA6B,CAAC;oBACnD,qEAAqE;oBACrE,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM;wBAAE,OAAO;oBAC/B,MAAM,CAAC,WAA2B,CAAC,SAAS,GAAG,GAAG,MAAM,CAAC,KAAK,IAAI,CAAC;gBACxE,CAAC,EACD,WAAW,EAAE,WAAW,EACxB,GAAG,EAAE,QAAQ,EACb,KAAK,EAAE,KAAK,GACd,EACF,mEAAyC,IACvC,CACT,CAAC;AACN,CAAC;AAED,QAAQ,CAAC,QAAQ,GAAG,UAAU,CAAC;AAE/B,OAAO,EAAE,QAAQ,EAAE,CAAC"}
package/dist/base.css CHANGED
@@ -1 +1 @@
1
- [data-color=grey]{--foreground: var(--foreground-neutral-on-surface-variant-01);--background: var(--surface-neutral-t2-lowest)}[data-color=white]{--foreground: var(--foreground-neutral-on-surface-variant-01);--background: var(--surface-neutral-t1-base)}[data-color=primary]{--foreground: var(--foreground-brand-primary-depth);--background: var(--surface-brand-primary-highlight)}[data-color=secondary]{--foreground: var(--foreground-brand-secondary-depth);--background: var(--surface-brand-secondary-highlight)}[data-color=blue]{--foreground: var(--foreground-spectrum-blue);--background: var(--surface-spectrum-blue)}[data-color=green]{--foreground: var(--foreground-spectrum-green);--background: var(--surface-spectrum-green)}[data-color=lime]{--foreground: var(--foreground-spectrum-lime);--background: var(--surface-spectrum-lime)}[data-color=magenta]{--foreground: var(--foreground-spectrum-magenta);--background: var(--surface-spectrum-magenta)}[data-color=orange]{--foreground: var(--foreground-spectrum-orange);--background: var(--surface-spectrum-orange)}[data-color=pink]{--foreground: var(--foreground-spectrum-pink);--background: var(--surface-spectrum-pink)}[data-color=purple]{--foreground: var(--foreground-spectrum-purple);--background: var(--surface-spectrum-purple)}[data-color=red]{--foreground: var(--foreground-spectrum-red);--background: var(--surface-spectrum-red)}[data-color=teal]{--foreground: var(--foreground-spectrum-teal);--background: var(--surface-spectrum-teal)}[data-color=yellow]{--foreground: var(--foreground-spectrum-yellow);--background: var(--surface-spectrum-yellow)}:root{--z-index-tooltip-popover: 1100;--z-index-dialog: 1000;--z-index-dropdown: 900;--z-index-fab: 800;--z-index-navbar: 700;--z-index-footer: 600}*,*::before,*::after{box-sizing:border-box}*{margin:0;padding:0}@media(prefers-reduced-motion){[data-animated]{animation:none !important}}body,html{height:100%;scroll-behavior:smooth}body{font:var(--body-base);background-color:var(--background-base);color:var(--foreground-neutral-on-surface)}a{color:var(--foreground-link-text-default)}a:hover{color:var(--foreground-link-text-default-hovered)}a:visited{color:var(--foreground-link-text-default-visited)}a:disabled{pointer-events:none;color:var(--foreground-link-text-default-disabled)}a[data-subtle]{color:var(--foreground-neutral-on-surface)}a[data-subtle]:hover{color:var(--foreground-link-text-subtle-hovered)}a[data-subtle]:disabled{pointer-events:none;color:var(--foreground-link-text-subtle-disabled)}a[data-subtle-inverse]{color:var(--foreground-neutral-inverse-on-surface)}a[data-subtle-inverse]:hover{color:var(--foreground-link-text-subtle-inverse-hovered)}a[data-subtle-inverse]:disabled{pointer-events:none;color:var(--foreground-link-text-subtle-inversed-disabled)}input:-internal-autofill-previewed,input:-internal-autofill-selected,textarea:-internal-autofill-previewed,textarea:-internal-autofill-selected,select:-internal-autofill-previewed,select:-internal-autofill-selected{transition:color calc(infinity*1s) step-end,background-color calc(infinity*1s) step-end}[data-sr-only]{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border-width:0}ol{list-style-type:decimal}ol li{list-style-type:decimal;margin-left:var(--spacing-sizing-05)}ul{list-style-type:disc}ul li{list-style-type:disc;margin-left:var(--spacing-sizing-05)}[data-touch-target]{display:none;touch-action:manipulation;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-touch-callout:none;user-select:none;position:absolute;z-index:1;height:100%;width:100%;top:auto;left:auto}[data-touch-target]~*{position:relative;z-index:2}@media(any-pointer: coarse){[data-touch-target]{display:block}}
1
+ [data-color=grey]{--foreground: var(--foreground-neutral-on-surface-variant-01);--background: var(--surface-neutral-t2-lowest)}[data-color=white]{--foreground: var(--foreground-neutral-on-surface-variant-01);--background: var(--surface-neutral-t1-base)}[data-color=primary]{--foreground: var(--foreground-brand-primary-depth);--background: var(--surface-brand-primary-highlight)}[data-color=secondary]{--foreground: var(--foreground-brand-secondary-depth);--background: var(--surface-brand-secondary-highlight)}[data-color=blue]{--foreground: var(--foreground-spectrum-blue);--background: var(--surface-spectrum-blue)}[data-color=green]{--foreground: var(--foreground-spectrum-green);--background: var(--surface-spectrum-green)}[data-color=lime]{--foreground: var(--foreground-spectrum-lime);--background: var(--surface-spectrum-lime)}[data-color=magenta]{--foreground: var(--foreground-spectrum-magenta);--background: var(--surface-spectrum-magenta)}[data-color=orange]{--foreground: var(--foreground-spectrum-orange);--background: var(--surface-spectrum-orange)}[data-color=pink]{--foreground: var(--foreground-spectrum-pink);--background: var(--surface-spectrum-pink)}[data-color=purple]{--foreground: var(--foreground-spectrum-purple);--background: var(--surface-spectrum-purple)}[data-color=red]{--foreground: var(--foreground-spectrum-red);--background: var(--surface-spectrum-red)}[data-color=teal]{--foreground: var(--foreground-spectrum-teal);--background: var(--surface-spectrum-teal)}[data-color=yellow]{--foreground: var(--foreground-spectrum-yellow);--background: var(--surface-spectrum-yellow)}:root{--z-index-tooltip-popover: 1100;--z-index-dialog: 1000;--z-index-dropdown: 900;--z-index-fab: 800;--z-index-navbar: 700;--z-index-footer: 600}*,*::before,*::after{box-sizing:border-box}*{margin:0;padding:0}@media(prefers-reduced-motion){[data-animated]{animation:none !important}}body,html{height:100%;scroll-behavior:smooth}body{font:var(--body-base);background-color:var(--background-base);color:var(--foreground-neutral-on-surface)}a{color:var(--foreground-link-text-default)}a:not([disabled]):hover{color:var(--foreground-link-text-default-hovered)}a:not([disabled]):visited{color:var(--foreground-link-text-default-visited)}a[disabled]{pointer-events:none;cursor:text;color:var(--foreground-link-text-default-disabled)}a[data-subtle]{color:var(--foreground-neutral-on-surface)}a[data-subtle]:hover{color:var(--foreground-link-text-subtle-hovered)}a[data-subtle]:disabled{pointer-events:none;color:var(--foreground-link-text-subtle-disabled)}a[data-subtle-inverse]{color:var(--foreground-neutral-inverse-on-surface)}a[data-subtle-inverse]:hover{color:var(--foreground-link-text-subtle-inverse-hovered)}a[data-subtle-inverse]:disabled{pointer-events:none;color:var(--foreground-link-text-subtle-inversed-disabled)}input:-internal-autofill-previewed,input:-internal-autofill-selected,textarea:-internal-autofill-previewed,textarea:-internal-autofill-selected,select:-internal-autofill-previewed,select:-internal-autofill-selected{transition:color calc(infinity*1s) step-end,background-color calc(infinity*1s) step-end}[data-sr-only]{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border-width:0}ol{list-style-type:decimal}ol li{list-style-type:decimal;margin-left:var(--spacing-sizing-05)}ul{list-style-type:disc}ul li{list-style-type:disc;margin-left:var(--spacing-sizing-05)}[data-touch-target]{display:none;touch-action:manipulation;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-touch-callout:none;user-select:none;position:absolute;z-index:1;height:100%;width:100%;top:auto;left:auto}[data-touch-target]~*{position:relative;z-index:2}@media(any-pointer: coarse){[data-touch-target]{display:block}}
@@ -1,11 +1,10 @@
1
1
  /**
2
- * A utility hook used within navigation components. Returns options ready for use in a navigation component. Validates
3
- * the icons and sets the icon and iconActive properties.
2
+ * A utility hook used within navigation components. Returns true if the icons are invalid.
4
3
  *
5
- * @param options [NavOption[]] The options to display. Each option has a label and an optional leading icon.
6
- * @returns {undefined} NavOption[] The options with the validated icon and iconActive properties set.
4
+ * @param options [NavOption[]] The options to display. Each option has an optional leading icon.
5
+ * @returns {boolean} True if the icons are invalid.
7
6
  */
8
- export declare function useNavOptions<T extends NavOption>(options: T[] | undefined): T[];
7
+ export declare function useOptionIconsInvalid<T extends NavOption>(options: T[] | undefined): boolean;
9
8
  export type NavOption = {
10
9
  /**
11
10
  * The label of the option. This is the text that will be displayed on the option.
@@ -0,0 +1,21 @@
1
+ import { useMemo } from 'react';
2
+ import { isValidIcon } from '../utils/children';
3
+ import { useErrorLogger } from '../utils/errors';
4
+ /**
5
+ * A utility hook used within navigation components. Returns true if the icons are invalid.
6
+ *
7
+ * @param options [NavOption[]] The options to display. Each option has an optional leading icon.
8
+ * @returns {boolean} True if the icons are invalid.
9
+ */
10
+ export function useOptionIconsInvalid(options) {
11
+ const { logError } = useErrorLogger();
12
+ return useMemo(() => {
13
+ if (!options || !Array.isArray(options))
14
+ return true;
15
+ const iconsInvalid = options.some((o) => o.icon) && !options.every((option) => isValidIcon(option.icon));
16
+ logError(iconsInvalid, 'useNavOptions - Every option either must have a valid icon or none at all. All icons removed.');
17
+ return iconsInvalid;
18
+ }, [logError, options]);
19
+ }
20
+ /** Copyright 2025 Anywhere Real Estate - CC BY 4.0 */
21
+ //# sourceMappingURL=useOptionIconsInvalid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useOptionIconsInvalid.js","sourceRoot":"","sources":["../../src/hooks/useOptionIconsInvalid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAEhC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAsB,OAAwB;IAC/E,MAAM,EAAE,QAAQ,EAAE,GAAG,cAAc,EAAE,CAAC;IAEtC,OAAO,OAAO,CAAC,GAAG,EAAE;QAChB,IAAI,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;QAErD,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QAEzG,QAAQ,CACJ,YAAY,EACZ,+FAA+F,CAClG,CAAC;QAEF,OAAO,YAAY,CAAC;IACxB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;AAC5B,CAAC;AAsBD,sDAAsD"}
package/dist/textarea.css CHANGED
@@ -1,4 +1,4 @@
1
1
  [data-bspk=textarea]{/*!
2
2
  --min-rows: is set via inline style
3
3
  --max-rows: is set via inline style
4
- */display:grid;width:100%;--font: var(--body-base);--line-height: 20px;--padding: var(--spacing-sizing-03)}[data-bspk=textarea][data-size=small]{--font: var(--body-small);--line-height: 20px;--padding: var(--spacing-sizing-02)}[data-bspk=textarea][data-size=large]{--font: var(--body-large);--line-height: 24px;--padding: var(--spacing-sizing-03)}[data-bspk=textarea] [data-replicated-value]{white-space:pre-wrap;visibility:hidden}[data-bspk=textarea] textarea,[data-bspk=textarea] [data-replicated-value]{width:100%;font:var(--font);border:1px solid var(--border-color);padding:var(--padding);grid-area:1/1/2/2;min-height:calc(var(--line-height)*var(--min-rows) + var(--padding)*2);max-height:calc(var(--line-height)*var(--max-rows) + var(--padding)*2)}[data-bspk=textarea] textarea{--border-color: var(--stroke-neutral-base);resize:vertical;color:var(--foreground-neutral-on-surface);background-color:var(--surface-neutral-t1-base);border-radius:var(--radius-small)}[data-bspk=textarea] textarea:focus-within{--border-color: var(--stroke-neutral-focus);outline:none;color:var(--foreground-neutral-on-surface)}[data-bspk=textarea] textarea:disabled{pointer-events:none;background:linear-gradient(var(--interactions-disabled-opacity), var(--interactions-disabled-opacity)),linear-gradient(var(--surface-neutral-t1-base), var(--surface-neutral-t1-base));color:var(--foreground-neutral-disabled-on-surface)}[data-bspk=textarea] textarea:read-only{background:linear-gradient(var(--interactions-disabled-opacity), var(--interactions-disabled-opacity)),linear-gradient(var(--surface-neutral-t1-base), var(--surface-neutral-t1-base));color:var(--foreground-neutral-on-surface-variant-02);cursor:not-allowed}[data-bspk=textarea] textarea[aria-invalid]{--border-color: var(--status-error)}
4
+ */display:grid;width:100%;--font: var(--body-base);--line-height: 20px;--padding: var(--spacing-sizing-03)}[data-bspk=textarea][data-size=small]{--font: var(--body-small);--line-height: 20px;--padding: var(--spacing-sizing-02)}[data-bspk=textarea][data-size=large]{--font: var(--body-large);--line-height: 24px;--padding: var(--spacing-sizing-03)}[data-bspk=textarea] [data-replicated-value]{white-space:pre-wrap;visibility:hidden}[data-bspk=textarea] textarea,[data-bspk=textarea] [data-replicated-value]{width:100%;font:var(--font);border:1px solid var(--border-color);padding:var(--padding);grid-area:1/1/2/2;min-height:calc(var(--line-height)*var(--min-rows) + var(--padding)*2);max-height:calc(var(--line-height)*var(--max-rows) + var(--padding)*2)}[data-bspk=textarea] textarea{--border-color: var(--stroke-neutral-base);resize:none;color:var(--foreground-neutral-on-surface);background-color:var(--surface-neutral-t1-base);border-radius:var(--radius-small)}[data-bspk=textarea] textarea::placeholder{color:var(--foreground-neutral-on-surface-variant-03)}[data-bspk=textarea] textarea:focus-within{--border-color: var(--stroke-neutral-focus);outline:none;color:var(--foreground-neutral-on-surface)}[data-bspk=textarea] textarea:disabled{pointer-events:none;background:linear-gradient(var(--interactions-disabled-opacity), var(--interactions-disabled-opacity)),linear-gradient(var(--surface-neutral-t1-base), var(--surface-neutral-t1-base));color:var(--foreground-neutral-disabled-on-surface)}[data-bspk=textarea] textarea:read-only{background:linear-gradient(var(--interactions-disabled-opacity), var(--interactions-disabled-opacity)),linear-gradient(var(--surface-neutral-t1-base), var(--surface-neutral-t1-base));cursor:not-allowed}[data-bspk=textarea] textarea[aria-invalid]{--border-color: var(--status-error)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bspk/ui",
3
- "version": "1.1.14",
3
+ "version": "1.1.15",
4
4
  "license": "CC-BY-4.0",
5
5
  "type": "module",
6
6
  "files": [
package/src/Link.tsx CHANGED
@@ -4,31 +4,34 @@ import { SvgOpenInNew } from '@bspk/icons/OpenInNew';
4
4
  import './link.scss';
5
5
  import { AnchorHTMLAttributes } from 'react';
6
6
 
7
- export type LinkProps = Pick<AnchorHTMLAttributes<unknown>, 'target'> & {
8
- /**
9
- * The label of the link.
10
- *
11
- * @required
12
- */
13
- label: string;
14
- /** The variant of the link. Controls the icon that is displayed and link target. */
15
- trailingIcon?: 'chevron' | 'external' | 'link';
16
- /** The [href](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a#href) of the link. */
17
- href: AnchorHTMLAttributes<unknown>['href'];
18
- /**
19
- * The size of the link.
20
- *
21
- * @default base
22
- */
23
- size?: 'base' | 'large' | 'small';
24
- /**
25
- * Change the color of the link to a subtle color. This is useful for links that are not primary actions, for
26
- * example footer menus.
27
- *
28
- * @default default
29
- */
30
- variant?: 'default' | 'subtle-inverse' | 'subtle';
31
- };
7
+ import { CommonPropsLibrary } from '.';
8
+
9
+ export type LinkProps = Pick<AnchorHTMLAttributes<unknown>, 'target'> &
10
+ Pick<CommonPropsLibrary, 'disabled'> & {
11
+ /**
12
+ * The label of the link.
13
+ *
14
+ * @required
15
+ */
16
+ label: string;
17
+ /** The variant of the link. Controls the icon that is displayed and link target. */
18
+ trailingIcon?: 'chevron' | 'external' | 'link';
19
+ /** The [href](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a#href) of the link. */
20
+ href: AnchorHTMLAttributes<unknown>['href'];
21
+ /**
22
+ * The size of the link.
23
+ *
24
+ * @default base
25
+ */
26
+ size?: 'base' | 'large' | 'small';
27
+ /**
28
+ * Change the color of the link to a subtle color. This is useful for links that are not primary actions, for
29
+ * example footer menus.
30
+ *
31
+ * @default default
32
+ */
33
+ variant?: 'default' | 'subtle-inverse' | 'subtle';
34
+ };
32
35
 
33
36
  /**
34
37
  * This is the standalone link component. Inline links can use the native `a` element.
@@ -7,6 +7,13 @@ import { useLongPress } from './hooks/useLongPress';
7
7
 
8
8
  import { CommonProps, InvalidPropsLibrary } from '.';
9
9
 
10
+ const DEFAULT = {
11
+ align: 'center',
12
+ size: 'medium',
13
+ disabled: false,
14
+ readOnly: false,
15
+ } as const;
16
+
10
17
  function isNumber(value: unknown): number | undefined {
11
18
  if (typeof value === 'number') return value;
12
19
  if (typeof value !== 'string') return undefined;
@@ -16,14 +23,10 @@ function isNumber(value: unknown): number | undefined {
16
23
 
17
24
  export type NumberInputProps = CommonProps<'aria-label' | 'disabled' | 'id' | 'name' | 'readOnly' | 'size'> &
18
25
  InvalidPropsLibrary & {
19
- /**
20
- * The value of the control.
21
- *
22
- * @required
23
- */
26
+ /** The value of the control. */
24
27
  value?: number;
25
28
  /** Callback when the value changes. */
26
- onChange: (value: number) => void;
29
+ onChange: (value: number | undefined) => void;
27
30
  /**
28
31
  * The alignment of the input box. Centered between the plus and minus buttons or to the left of the buttons.
29
32
  *
@@ -57,13 +60,12 @@ export type NumberInputProps = CommonProps<'aria-label' | 'disabled' | 'id' | 'n
57
60
  * @name NumberInput
58
61
  */
59
62
  function NumberInput({
60
- //
61
- value = 1,
63
+ value,
62
64
  onChange,
63
- align = 'center',
64
- size = 'medium',
65
- disabled = false,
66
- readOnly = false,
65
+ align = DEFAULT.align,
66
+ size = DEFAULT.size,
67
+ disabled = DEFAULT.disabled,
68
+ readOnly = DEFAULT.readOnly,
67
69
  name,
68
70
  id: inputIdProp,
69
71
  invalid,
@@ -76,7 +78,12 @@ function NumberInput({
76
78
  const max = isNumber(maxProp);
77
79
  const min = isNumber(minProp);
78
80
 
79
- const fix = (next: number = value) => {
81
+ const fix = (next: number | undefined = value) => {
82
+ if (typeof next !== 'number') {
83
+ onChange(undefined);
84
+ return;
85
+ }
86
+
80
87
  let fixValue = next;
81
88
  if (typeof min !== 'undefined' && next < min) fixValue = min;
82
89
  if (typeof max !== 'undefined' && next > max) fixValue = max;
@@ -2,11 +2,11 @@ import './segmented-control.scss';
2
2
  import { Fragment } from 'react';
3
3
 
4
4
  import { Tooltip } from './Tooltip';
5
- import { useNavOptions } from './hooks/useNavOptions';
5
+ import { useOptionIconsInvalid } from './hooks/useOptionIconsInvalid';
6
6
 
7
7
  import { ElementProps } from './';
8
8
 
9
- export type SegmentedToggleOption = {
9
+ export type SegmentedControlOption = {
10
10
  /**
11
11
  * The label of the option. This is the text that will be displayed on the option.
12
12
  *
@@ -30,18 +30,22 @@ export type SegmentedControlProps = {
30
30
  /**
31
31
  * The options to display. Each option has a label and an optional leading icon.
32
32
  *
33
- * @type SegmentedToggleOption[]
33
+ * @type SegmentedControlOption[]
34
34
  * @required
35
35
  */
36
- options: SegmentedToggleOption[];
37
- /** The id of the selected option. */
38
- value?: SegmentedToggleOption['value'];
36
+ options: SegmentedControlOption[];
37
+ /**
38
+ * The id of the selected option.
39
+ *
40
+ * @required
41
+ */
42
+ value: SegmentedControlOption['value'];
39
43
  /**
40
44
  * The function to call when the option is clicked.
41
45
  *
42
46
  * @required
43
47
  */
44
- onChange: (value: SegmentedToggleOption['value']) => void;
48
+ onChange: (value: SegmentedControlOption['value']) => void;
45
49
  /**
46
50
  * The size of the options.
47
51
  *
@@ -73,18 +77,19 @@ function SegmentedControl({
73
77
  onChange,
74
78
  value,
75
79
  size = 'medium',
76
- options,
80
+ options: optionsProp,
77
81
  width = 'hug',
78
82
  showLabels: showLabelsProp = true,
79
83
  ...containerProps
80
84
  }: ElementProps<SegmentedControlProps, 'div'>) {
81
- const items = useNavOptions(options);
85
+ const options = Array.isArray(optionsProp) ? optionsProp : [];
86
+ useOptionIconsInvalid(options);
82
87
 
83
- const hideLabels = showLabelsProp === false && items.every((item) => item.icon && item.label);
88
+ const hideLabels = showLabelsProp === false && options.every((item) => item.icon && item.label);
84
89
 
85
90
  return (
86
91
  <div {...containerProps} data-bspk="segmented-control" data-size={size} data-width={width}>
87
- {items.map((item, index) => {
92
+ {options.map((item, index) => {
88
93
  const isActive = item.value === value;
89
94
  return (
90
95
  <Fragment key={item.value}>
@@ -92,10 +97,10 @@ function SegmentedControl({
92
97
  <button
93
98
  aria-label={item.label}
94
99
  data-first={index === 0 || undefined}
95
- data-last={index === items.length - 1 || undefined}
100
+ data-last={index === options.length - 1 || undefined}
96
101
  data-selected={isActive || undefined}
97
102
  disabled={item.disabled || undefined}
98
- onClick={() => onChange(item.value)}
103
+ onClick={() => onChange(item.value || item.label)}
99
104
  >
100
105
  <span data-outer>
101
106
  <span data-inner>
package/src/TabGroup.tsx CHANGED
@@ -1,11 +1,19 @@
1
1
  import './tab-group.scss';
2
2
  import { ReactNode } from 'react';
3
3
 
4
- import { Badge } from './Badge';
5
- import { useNavOptions } from './hooks/useNavOptions';
4
+ import { Badge, BadgeProps } from './Badge';
5
+ import { useOptionIconsInvalid } from './hooks/useOptionIconsInvalid';
6
6
 
7
7
  import { ElementProps } from './';
8
8
 
9
+ export type TabGroupSize = 'large' | 'medium' | 'small';
10
+
11
+ const TAB_BADGE_SIZES: Record<TabGroupSize, BadgeProps['size']> = {
12
+ large: 'small',
13
+ medium: 'x-small',
14
+ small: 'x-small',
15
+ };
16
+
9
17
  export type TabGroupOption = {
10
18
  /**
11
19
  * The label of the tab. This is the text that will be displayed on the tab.
@@ -19,7 +27,11 @@ export type TabGroupOption = {
19
27
  * @default false
20
28
  */
21
29
  disabled?: boolean;
22
- /** The value of the tab. If not provided, the label will be used as the value. */
30
+ /**
31
+ * The value of the tab sent to onChange when selected.
32
+ *
33
+ * If not provided, the label will be used as the value.
34
+ */
23
35
  value?: string;
24
36
  /** The icon to display on the left side of the tab. */
25
37
  icon?: ReactNode;
@@ -37,20 +49,24 @@ export type TabGroupProps = {
37
49
  * @required
38
50
  */
39
51
  options: TabGroupOption[];
40
- /** The id of the selected tab. */
41
- value?: TabGroupOption['value'];
52
+ /**
53
+ * The id of the selected tab.
54
+ *
55
+ * @required
56
+ */
57
+ value: TabGroupOption['value'];
42
58
  /**
43
59
  * The function to call when the tab is clicked.
44
60
  *
45
61
  * @required
46
62
  */
47
- onChange: (tabId: TabGroupOption['value']) => void;
63
+ onChange: (tabValue: string, index: number) => void;
48
64
  /**
49
65
  * The size of the tabs.
50
66
  *
51
67
  * @default medium
52
68
  */
53
- size?: 'large' | 'medium' | 'small';
69
+ size?: TabGroupSize;
54
70
  /**
55
71
  * When 'fill' the options will fill the width of the container. When 'hug', the options will be as wide as their
56
72
  * content.
@@ -77,12 +93,13 @@ function TabGroup({
77
93
  onChange: onTabChange,
78
94
  value,
79
95
  size = 'medium',
80
- options,
96
+ options: optionsProp,
81
97
  width = 'hug',
82
98
  showTrail = false,
83
99
  ...containerProps
84
100
  }: ElementProps<TabGroupProps, 'div'>) {
85
- const items = useNavOptions(options);
101
+ const options = Array.isArray(optionsProp) ? optionsProp : [];
102
+ useOptionIconsInvalid(options);
86
103
 
87
104
  return (
88
105
  <div
@@ -92,7 +109,7 @@ function TabGroup({
92
109
  data-size={size}
93
110
  data-width={width}
94
111
  >
95
- {items.map((item) => {
112
+ {options.map((item, itemIndex) => {
96
113
  const isActive = item.value === value;
97
114
 
98
115
  return (
@@ -100,12 +117,16 @@ function TabGroup({
100
117
  data-active={isActive || undefined}
101
118
  disabled={item.disabled || undefined}
102
119
  key={item.value}
103
- onClick={() => onTabChange(item.value)}
120
+ onClick={() => {
121
+ onTabChange(item.value || item.label, itemIndex);
122
+ }}
104
123
  >
105
124
  <span>
106
125
  {(isActive && item.iconActive) || item.icon}
107
126
  {item.label}
108
- {item.badge && <Badge count={item.badge} size="x-small" />}
127
+ {item.badge && !item.disabled && !isActive && (
128
+ <Badge count={item.badge} size={TAB_BADGE_SIZES[size]} />
129
+ )}
109
130
  </span>
110
131
  </button>
111
132
  );
package/src/Textarea.tsx CHANGED
@@ -5,6 +5,12 @@ import { useId } from './hooks/useId';
5
5
 
6
6
  import { CommonProps, InvalidPropsLibrary } from './';
7
7
 
8
+ const DEFAULT = {
9
+ minRows: 3,
10
+ maxRows: 10,
11
+ textSize: 'medium',
12
+ } as const;
13
+
8
14
  export type TextareaProps = CommonProps<'aria-label' | 'disabled' | 'id' | 'readOnly' | 'required'> &
9
15
  InvalidPropsLibrary & {
10
16
  /**
@@ -15,12 +21,16 @@ export type TextareaProps = CommonProps<'aria-label' | 'disabled' | 'id' | 'read
15
21
  */
16
22
  onChange: (next: string, event?: ChangeEvent<HTMLTextAreaElement>) => void;
17
23
  /**
18
- * The size of the field.
24
+ * The text size of the field.
19
25
  *
20
26
  * @default medium
21
27
  */
22
- size?: 'large' | 'medium' | 'small';
23
- /** The value of the field. */
28
+ textSize?: 'large' | 'medium' | 'small';
29
+ /**
30
+ * The value of the field.
31
+ *
32
+ * @type multiline
33
+ */
24
34
  value?: string;
25
35
  /**
26
36
  * The textarea control name of the field.
@@ -39,27 +49,30 @@ export type TextareaProps = CommonProps<'aria-label' | 'disabled' | 'id' | 'read
39
49
  */
40
50
  maxLength?: number;
41
51
  /**
42
- * The minimum number of rows that the textarea should have. If set the textarea will automatically grow and
43
- * shrink to fit the content.
52
+ * The minimum number of rows that the textarea will show.
44
53
  *
54
+ * @default 3
45
55
  * @minimum 3
56
+ * @maximum 10
46
57
  */
47
58
  minRows?: number;
48
59
  /**
49
- * The maximum number of rows that the textarea should have. If set the textarea will automatically grow and
50
- * shrink to fit the content.
60
+ * The maximum number of rows that the textarea will show.
51
61
  *
62
+ * @default 10
63
+ * @minimum 3
52
64
  * @maximum 10
53
65
  */
54
66
  maxRows?: number;
55
67
  };
56
68
 
57
- const MIN_ROWS = 3;
58
- const MAX_ROWS = 10;
59
-
60
69
  /**
61
70
  * A component that allows users to input large amounts of text that could span multiple lines.
62
71
  *
72
+ * This component gives you a textarea HTML element that automatically adjusts its height to match the length of the
73
+ * content within maximum and minimum rows. A character counter when a maxLength is set to show the number of characters
74
+ * remaining below the limit.
75
+ *
63
76
  * @element
64
77
  *
65
78
  * @name Textarea
@@ -67,28 +80,28 @@ const MAX_ROWS = 10;
67
80
  function Textarea({
68
81
  invalid: invalidProp,
69
82
  onChange,
70
- size = 'medium',
83
+ textSize = DEFAULT.textSize,
71
84
  value = '',
72
85
  name,
73
86
  'aria-label': ariaLabel,
74
87
  innerRef,
75
88
  placeholder,
76
89
  id: idProp,
77
- minRows: minRowsProp = MIN_ROWS,
78
- maxRows: maxRowsProp = MAX_ROWS,
90
+ minRows: minRowsProp = DEFAULT.minRows,
91
+ maxRows: maxRowsProp = DEFAULT.maxRows,
79
92
  errorMessage,
80
93
  ...otherProps
81
94
  }: TextareaProps) {
82
95
  const id = useId(idProp);
83
96
  const invalid = !otherProps.readOnly && !otherProps.disabled && invalidProp;
84
97
  // ensure minRows and maxRows are within bounds
85
- const minRows = Math.min(MAX_ROWS, Math.max(minRowsProp, MIN_ROWS));
86
- const maxRows = Math.max(MIN_ROWS, Math.min(maxRowsProp, MAX_ROWS));
98
+ const minRows = Math.min(DEFAULT.maxRows, Math.max(minRowsProp, DEFAULT.minRows));
99
+ const maxRows = Math.max(DEFAULT.minRows, Math.min(maxRowsProp, DEFAULT.maxRows));
87
100
 
88
101
  return (
89
102
  <div
90
103
  data-bspk="textarea"
91
- data-size={size}
104
+ data-size={textSize}
92
105
  style={
93
106
  {
94
107
  '--min-rows': minRows,
package/src/base.scss CHANGED
@@ -51,16 +51,17 @@ body {
51
51
  a {
52
52
  color: var(--foreground-link-text-default);
53
53
 
54
- &:hover {
54
+ &:not([disabled]):hover {
55
55
  color: var(--foreground-link-text-default-hovered);
56
56
  }
57
57
 
58
- &:visited {
58
+ &:not([disabled]):visited {
59
59
  color: var(--foreground-link-text-default-visited);
60
60
  }
61
61
 
62
- &:disabled {
62
+ &[disabled] {
63
63
  pointer-events: none;
64
+ cursor: text;
64
65
  color: var(--foreground-link-text-default-disabled);
65
66
  }
66
67
  }
@@ -0,0 +1,49 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import { isValidIcon } from '../utils/children';
4
+ import { useErrorLogger } from '../utils/errors';
5
+
6
+ /**
7
+ * A utility hook used within navigation components. Returns true if the icons are invalid.
8
+ *
9
+ * @param options [NavOption[]] The options to display. Each option has an optional leading icon.
10
+ * @returns {boolean} True if the icons are invalid.
11
+ */
12
+ export function useOptionIconsInvalid<T extends NavOption>(options: T[] | undefined): boolean {
13
+ const { logError } = useErrorLogger();
14
+
15
+ return useMemo(() => {
16
+ if (!options || !Array.isArray(options)) return true;
17
+
18
+ const iconsInvalid = options.some((o) => o.icon) && !options.every((option) => isValidIcon(option.icon));
19
+
20
+ logError(
21
+ iconsInvalid,
22
+ 'useNavOptions - Every option either must have a valid icon or none at all. All icons removed.',
23
+ );
24
+
25
+ return iconsInvalid;
26
+ }, [logError, options]);
27
+ }
28
+
29
+ export type NavOption = {
30
+ /**
31
+ * The label of the option. This is the text that will be displayed on the option.
32
+ *
33
+ * @required
34
+ */
35
+ label: string;
36
+ /**
37
+ * Determines if the element is [disabled](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled).
38
+ *
39
+ * @default false
40
+ */
41
+ disabled?: boolean;
42
+ /** The value of the option. If not provided, the label will be used as the value. */
43
+ value?: string;
44
+ /** The the icon to display before the label. */
45
+ icon?: React.ReactNode;
46
+ iconActive?: React.ReactNode;
47
+ };
48
+
49
+ /** Copyright 2025 Anywhere Real Estate - CC BY 4.0 */
package/src/textarea.scss CHANGED
@@ -43,7 +43,11 @@
43
43
  textarea {
44
44
  --border-color: var(--stroke-neutral-base);
45
45
 
46
- resize: vertical;
46
+ &::placeholder {
47
+ color: var(--foreground-neutral-on-surface-variant-03);
48
+ }
49
+
50
+ resize: none;
47
51
  color: var(--foreground-neutral-on-surface);
48
52
  background-color: var(--surface-neutral-t1-base);
49
53
  border-radius: var(--radius-small);
@@ -71,7 +75,6 @@
71
75
 
72
76
  linear-gradient(var(--interactions-disabled-opacity), var(--interactions-disabled-opacity)),
73
77
  linear-gradient(var(--surface-neutral-t1-base), var(--surface-neutral-t1-base));
74
- color: var(--foreground-neutral-on-surface-variant-02);
75
78
  cursor: not-allowed;
76
79
  }
77
80