@boostdev/design-system-components 0.1.4 → 0.1.6

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 (59) hide show
  1. package/dist/client.cjs +188 -107
  2. package/dist/client.css +463 -441
  3. package/dist/client.d.cts +15 -13
  4. package/dist/client.d.ts +15 -13
  5. package/dist/client.js +192 -110
  6. package/dist/index.cjs +188 -107
  7. package/dist/index.css +463 -441
  8. package/dist/index.d.cts +15 -13
  9. package/dist/index.d.ts +15 -13
  10. package/dist/index.js +192 -110
  11. package/package.json +2 -1
  12. package/src/components/interaction/Button/Button.tsx +35 -8
  13. package/src/components/interaction/Command/Command.module.css +6 -5
  14. package/src/components/interaction/Dialog/Dialog.module.css +8 -5
  15. package/src/components/interaction/Dialog/Dialog.spec.tsx +10 -10
  16. package/src/components/interaction/Dialog/Dialog.stories.tsx +2 -2
  17. package/src/components/interaction/Dialog/Dialog.tsx +24 -8
  18. package/src/components/interaction/Drawer/Drawer.module.css +3 -4
  19. package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +6 -4
  20. package/src/components/interaction/DropdownMenu/DropdownMenu.tsx +1 -1
  21. package/src/components/interaction/DropdownMenu/index.ts +1 -0
  22. package/src/components/interaction/Popover/Popover.module.css +3 -3
  23. package/src/components/interaction/Toast/Toast.module.css +25 -8
  24. package/src/components/interaction/Toast/Toast.tsx +10 -1
  25. package/src/components/interaction/form/Checkbox/Checkbox.module.css +1 -1
  26. package/src/components/interaction/form/Checkbox/Checkbox.tsx +3 -2
  27. package/src/components/interaction/form/Combobox/Combobox.module.css +3 -2
  28. package/src/components/interaction/form/Combobox/Combobox.tsx +11 -3
  29. package/src/components/interaction/form/Combobox/index.ts +1 -0
  30. package/src/components/interaction/form/FileInput/FileInput.tsx +15 -2
  31. package/src/components/interaction/form/FormInput/FormInput.tsx +5 -4
  32. package/src/components/interaction/form/NumberInput/NumberInput.tsx +21 -17
  33. package/src/components/interaction/form/Radio/Radio.module.css +1 -1
  34. package/src/components/interaction/form/Radio/Radio.tsx +3 -2
  35. package/src/components/interaction/form/Select/Select.spec.tsx +2 -1
  36. package/src/components/interaction/form/Select/Select.tsx +3 -3
  37. package/src/components/interaction/form/Select/index.ts +1 -0
  38. package/src/components/interaction/form/Slider/Slider.tsx +5 -4
  39. package/src/components/interaction/form/Switch/Switch.tsx +1 -1
  40. package/src/components/interaction/form/Textarea/Textarea.tsx +1 -1
  41. package/src/components/interaction/form/atoms/Message.tsx +2 -2
  42. package/src/components/layout/ButtonGroup/ButtonGroup.tsx +1 -1
  43. package/src/components/layout/Card/Card.module.css +3 -2
  44. package/src/components/layout/Card/Card.tsx +3 -0
  45. package/src/components/layout/SectionHeader/SectionHeader.tsx +2 -3
  46. package/src/components/ui/Accordion/Accordion.tsx +1 -1
  47. package/src/components/ui/Accordion/index.ts +1 -0
  48. package/src/components/ui/Breadcrumb/Breadcrumb.tsx +2 -2
  49. package/src/components/ui/Breadcrumb/index.ts +1 -0
  50. package/src/components/ui/Calendar/Calendar.tsx +8 -3
  51. package/src/components/ui/DescriptionList/DescriptionList.tsx +1 -1
  52. package/src/components/ui/DescriptionList/index.ts +1 -0
  53. package/src/components/ui/Link/Link.tsx +3 -1
  54. package/src/components/ui/Separator/Separator.module.css +1 -1
  55. package/src/components/ui/Table/Table.tsx +1 -1
  56. package/src/components/ui/Table/index.ts +1 -0
  57. package/src/components/ui/Tabs/Tabs.tsx +1 -1
  58. package/src/components/ui/Tabs/index.ts +1 -0
  59. package/src/index.ts +8 -0
@@ -1,9 +1,9 @@
1
- import { useId, useRef } from 'react';
1
+ import {useId, useRef, useState} from 'react';
2
2
  import css from './NumberInput.module.css';
3
- import { Label } from '../atoms/Label';
4
- import { Message } from '../atoms/Message';
5
- import { InputContainer } from '../atoms/InputContainer';
6
- import { cn } from '@boostdev/design-system-foundation';
3
+ import {Label} from '../atoms/Label';
4
+ import {Message} from '../atoms/Message';
5
+ import {InputContainer} from '../atoms/InputContainer';
6
+ import {cn} from '@boostdev/design-system-foundation';
7
7
 
8
8
  interface NumberInputProps {
9
9
  label: string;
@@ -37,20 +37,21 @@ export function NumberInput({
37
37
  const uid = name + useId();
38
38
  const hintId = uid + 'hint';
39
39
  const errorId = uid + 'error';
40
- const describedBy = error ? errorId : hintId;
40
+ const describedBy = [error && errorId, hint && hintId].filter(Boolean).join(' ') || undefined;
41
41
  const inputRef = useRef<HTMLInputElement>(null);
42
42
 
43
+ const isControlled = value !== undefined;
44
+ const [internalValue, setInternalValue] = useState(defaultValue ?? 0);
45
+ const currentValue = isControlled ? value! : internalValue;
46
+
43
47
  const clamp = (v: number): number => {
44
48
  const withMin = min !== undefined ? Math.max(min, v) : v;
45
- const withMax = max !== undefined ? Math.min(max, withMin) : withMin;
46
- return withMax;
49
+ return max !== undefined ? Math.min(max, withMin) : withMin;
47
50
  };
48
51
 
49
52
  const adjust = (delta: number) => {
50
- if (!inputRef.current) return;
51
- const current = parseFloat(inputRef.current.value) || 0;
52
- const next = clamp(current + delta);
53
- inputRef.current.value = String(next);
53
+ const next = clamp(currentValue + delta);
54
+ if (!isControlled) setInternalValue(next);
54
55
  onChange?.(next);
55
56
  };
56
57
 
@@ -62,7 +63,7 @@ export function NumberInput({
62
63
  type="button"
63
64
  className={css.stepper}
64
65
  aria-label="Decrease"
65
- disabled={disabled || (min !== undefined && (value ?? defaultValue ?? 0) <= min)}
66
+ disabled={disabled || (min !== undefined && currentValue <= min)}
66
67
  onClick={() => adjust(-step)}
67
68
  tabIndex={-1}
68
69
  >
@@ -75,8 +76,7 @@ export function NumberInput({
75
76
  id={uid}
76
77
  type="number"
77
78
  name={name}
78
- defaultValue={defaultValue}
79
- value={value}
79
+ {...(isControlled ? { value } : { defaultValue })}
80
80
  min={min}
81
81
  max={max}
82
82
  step={step}
@@ -84,13 +84,17 @@ export function NumberInput({
84
84
  aria-invalid={!!error}
85
85
  aria-describedby={describedBy}
86
86
  className={cn(css.input, error ? css.inputError : undefined)}
87
- onChange={e => onChange?.(parseFloat(e.target.value))}
87
+ onChange={e => {
88
+ const v = parseFloat(e.target.value);
89
+ if (!isControlled) setInternalValue(isNaN(v) ? 0 : v);
90
+ onChange?.(v);
91
+ }}
88
92
  />
89
93
  <button
90
94
  type="button"
91
95
  className={css.stepper}
92
96
  aria-label="Increase"
93
- disabled={disabled || (max !== undefined && (value ?? defaultValue ?? 0) >= max)}
97
+ disabled={disabled || (max !== undefined && currentValue >= max)}
94
98
  onClick={() => adjust(step)}
95
99
  tabIndex={-1}
96
100
  >
@@ -18,7 +18,7 @@
18
18
  margin-block-start: 0.25em;
19
19
  width: var(--inputSize);
20
20
  height: var(--inputSize);
21
- border: 1px solid var(--color_border);
21
+ border: 1px solid var(--color_on-bg);
22
22
  border-radius: 50%;
23
23
  appearance: none;
24
24
  background-color: var(--color_bg);
@@ -17,17 +17,18 @@ export function Radio({ label, name, error, hint, className, ...props }: RadioPr
17
17
  const id = name + useId();
18
18
  const hintId = id + 'hint';
19
19
  const errorId = id + 'error';
20
- const describedBy = !!error ? errorId : hintId;
20
+ const describedBy = [error && errorId, hint && hintId].filter(Boolean).join(' ') || undefined;
21
21
 
22
22
  return (
23
23
  <InputContainer className={cn(css.radioGroup, className)}>
24
24
  <div className={css.inputWrapper}>
25
25
  <input
26
26
  aria-describedby={describedBy}
27
+ aria-invalid={!!error}
27
28
  type="radio"
28
29
  id={id}
29
30
  name={name}
30
- className={`${css.radio} ${error ? css.radioError : ''}`}
31
+ className={cn(css.radio, error && css.radioError)}
31
32
  {...props}
32
33
  />
33
34
  <Label id={id} label={label} />
@@ -28,7 +28,8 @@ describe('Select', () => {
28
28
 
29
29
  it('renders a placeholder option', () => {
30
30
  render(<Select label="Country" name="country" options={options} placeholder="Pick a country" />);
31
- expect(screen.getByRole('option', { name: 'Pick a country' })).toBeInTheDocument();
31
+ // placeholder is hidden from the a11y tree (hidden attribute) but present in the DOM
32
+ expect(screen.getByText('Pick a country')).toBeInTheDocument();
32
33
  });
33
34
 
34
35
  it('marks a disabled option as disabled', () => {
@@ -5,7 +5,7 @@ import { InputContainer } from '../atoms/InputContainer';
5
5
  import { Label } from '../atoms/Label';
6
6
  import { Message } from '../atoms/Message';
7
7
 
8
- interface SelectOption {
8
+ export interface SelectOption {
9
9
  value: string;
10
10
  label: string;
11
11
  disabled?: boolean;
@@ -34,7 +34,7 @@ export function Select({
34
34
  const id = name + useId();
35
35
  const hintId = id + 'hint';
36
36
  const errorId = id + 'error';
37
- const describedBy = error ? errorId : hintId;
37
+ const describedBy = [error && errorId, hint && hintId].filter(Boolean).join(' ') || undefined;
38
38
 
39
39
  return (
40
40
  <InputContainer className={cn(css.formGroup, className)}>
@@ -49,7 +49,7 @@ export function Select({
49
49
  {...props}
50
50
  >
51
51
  {placeholder && (
52
- <option value="" disabled>
52
+ <option value="" disabled hidden>
53
53
  {placeholder}
54
54
  </option>
55
55
  )}
@@ -1 +1,2 @@
1
1
  export { Select } from './Select';
2
+ export type { SelectOption } from './Select';
@@ -31,15 +31,16 @@ export function Slider({
31
31
  const id = name + useId();
32
32
  const hintId = id + 'hint';
33
33
  const errorId = id + 'error';
34
- const describedBy = error ? errorId : hintId;
34
+ const describedBy = [error && errorId, hint && hintId].filter(Boolean).join(' ') || undefined;
35
35
 
36
- const initialValue = Number(props.value ?? props.defaultValue ?? min);
37
- const [currentValue, setCurrentValue] = useState(initialValue);
36
+ const isControlled = props.value !== undefined;
37
+ const [internalValue, setInternalValue] = useState(Number(props.defaultValue ?? min));
38
+ const currentValue = isControlled ? Number(props.value) : internalValue;
38
39
 
39
40
  const fillPct = ((currentValue - min) / (max - min)) * 100;
40
41
 
41
42
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
42
- setCurrentValue(Number(e.target.value));
43
+ if (!isControlled) setInternalValue(Number(e.target.value));
43
44
  onChange?.(e);
44
45
  };
45
46
 
@@ -26,7 +26,7 @@ export function Switch({
26
26
  const id = name + useId();
27
27
  const hintId = id + 'hint';
28
28
  const errorId = id + 'error';
29
- const describedBy = error ? errorId : hintId;
29
+ const describedBy = [error && errorId, hint && hintId].filter(Boolean).join(' ') || undefined;
30
30
 
31
31
  return (
32
32
  <InputContainer className={cn(css.switchGroup, css[`--size_${size}`], className)}>
@@ -24,7 +24,7 @@ export function Textarea({
24
24
  const id = name + useId();
25
25
  const hintId = id + 'hint';
26
26
  const errorId = id + 'error';
27
- const describedBy = error ? errorId : hintId;
27
+ const describedBy = [error && errorId, hint && hintId].filter(Boolean).join(' ') || undefined;
28
28
 
29
29
  return (
30
30
  <InputContainer className={cn(css.formGroup, className)}>
@@ -10,8 +10,8 @@ export const Message = ({ message, type, inputId }: MessageProps) => {
10
10
  if (!message) return null;
11
11
 
12
12
  return (
13
- <label id={inputId + type} htmlFor={inputId} className={css[type]}>
13
+ <p id={inputId + type} className={css[type]}>
14
14
  {message}
15
- </label>
15
+ </p>
16
16
  );
17
17
  };
@@ -10,7 +10,7 @@ interface ButtonGroupProps {
10
10
 
11
11
  export function ButtonGroup({ children, className, variant }: ButtonGroupProps) {
12
12
  return (
13
- <div className={cn(css.buttonGroup, className, variant && css[`--variant__${variant}`])}>
13
+ <div className={cn(css.buttonGroup, className, variant && css[`--variant_${variant}`])}>
14
14
  <div className={css.container}>{children}</div>
15
15
  </div>
16
16
  );
@@ -1,6 +1,7 @@
1
1
  @layer component {
2
2
  .card {
3
- background-color: var(--color_bg);
3
+ background-color: var(--card_color, var(--color_bg));
4
+ color: var(--card_on-color, var(--color_on-bg));
4
5
  border-radius: var(--border_radius--m);
5
6
  transition: all 0.3s ease;
6
7
  position: relative;
@@ -15,7 +16,7 @@
15
16
  }
16
17
 
17
18
  .card.--outlined {
18
- border: 1px solid var(--color_on-bg);
19
+ border: 1px solid currentcolor;
19
20
  box-shadow: var(--shadow_s);
20
21
  }
21
22
 
@@ -10,6 +10,7 @@ interface CardProps {
10
10
  textAlign?: 'start' | 'center' | 'end';
11
11
  style?: CSSProperties;
12
12
  onClick?: () => void;
13
+ 'aria-label'?: string;
13
14
  }
14
15
 
15
16
  export function Card({
@@ -20,6 +21,7 @@ export function Card({
20
21
  textAlign = 'start',
21
22
  style,
22
23
  onClick,
24
+ 'aria-label': ariaLabel,
23
25
  }: CardProps) {
24
26
  const classNames = cn(
25
27
  css.card,
@@ -37,6 +39,7 @@ export function Card({
37
39
  className={classNames}
38
40
  onClick={onClick}
39
41
  style={style}
42
+ aria-label={ariaLabel}
40
43
  {...(onClick && { type: 'button' as const })}
41
44
  >
42
45
  {children}
@@ -1,9 +1,8 @@
1
+ import type { JSX } from 'react';
1
2
  import css from './SectionHeader.module.css';
2
3
  import { cn } from '@boostdev/design-system-foundation';
3
- import {JSX} from "react/jsx-runtime";
4
- type IntrinsicElements = JSX.IntrinsicElements;
5
4
 
6
- type IntrinsicElement = keyof IntrinsicElements
5
+ type IntrinsicElement = keyof JSX.IntrinsicElements;
7
6
 
8
7
  type SectionHeaderProps = {
9
8
  title: string;
@@ -2,7 +2,7 @@ import { ReactNode, useId, useState } from 'react';
2
2
  import css from './Accordion.module.css';
3
3
  import { cn } from '@boostdev/design-system-foundation';
4
4
 
5
- interface AccordionItem {
5
+ export interface AccordionItem {
6
6
  id: string;
7
7
  title: ReactNode;
8
8
  content: ReactNode;
@@ -1 +1,2 @@
1
1
  export { Accordion } from './Accordion';
2
+ export type { AccordionItem } from './Accordion';
@@ -1,7 +1,7 @@
1
1
  import css from './Breadcrumb.module.css';
2
2
  import { cn } from '@boostdev/design-system-foundation';
3
3
 
4
- interface BreadcrumbItem {
4
+ export interface BreadcrumbItem {
5
5
  label: string;
6
6
  href?: string;
7
7
  }
@@ -19,7 +19,7 @@ export function Breadcrumb({ items, className }: Readonly<BreadcrumbProps>) {
19
19
  const isLast = index === items.length - 1;
20
20
 
21
21
  return (
22
- <li key={item.label} className={css.item}>
22
+ <li key={index} className={css.item}>
23
23
  {isLast ? (
24
24
  <span aria-current="page" className={css.current}>
25
25
  {item.label}
@@ -1 +1,2 @@
1
1
  export { Breadcrumb } from './Breadcrumb';
2
+ export type { BreadcrumbItem } from './Breadcrumb';
@@ -23,10 +23,15 @@ function isSameDay(a: Date, b: Date): boolean {
23
23
  a.getDate() === b.getDate();
24
24
  }
25
25
 
26
+ function toMidnight(date: Date): Date {
27
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
28
+ }
29
+
26
30
  function isOutOfRange(date: Date, min?: Date, max?: Date): boolean {
27
- if (min && date < min) return true;
28
- if (max && date > max) return true;
29
- return false;
31
+ const d = toMidnight(date);
32
+ if (min && d < toMidnight(min)) return true;
33
+ return !!(max && d > toMidnight(max));
34
+
30
35
  }
31
36
 
32
37
  function getDaysInMonth(year: number, month: number): number {
@@ -2,7 +2,7 @@ import { ReactNode } from 'react';
2
2
  import css from './DescriptionList.module.css';
3
3
  import { cn } from '@boostdev/design-system-foundation';
4
4
 
5
- interface DescriptionItem {
5
+ export interface DescriptionItem {
6
6
  term: ReactNode;
7
7
  details: ReactNode | ReactNode[];
8
8
  }
@@ -1 +1,2 @@
1
1
  export { DescriptionList } from './DescriptionList';
2
+ export type { DescriptionItem } from './DescriptionList';
@@ -7,6 +7,7 @@ type LinkOwnProps<T extends ElementType> = {
7
7
  children: ReactNode;
8
8
  variant?: 'default' | 'subtle' | 'standalone';
9
9
  external?: boolean;
10
+ externalLabel?: string;
10
11
  className?: string;
11
12
  };
12
13
 
@@ -18,6 +19,7 @@ export function Link<T extends ElementType = 'a'>({
18
19
  children,
19
20
  variant = 'default',
20
21
  external = false,
22
+ externalLabel = '(opens in new tab)',
21
23
  className,
22
24
  ...props
23
25
  }: LinkProps<T>) {
@@ -35,7 +37,7 @@ export function Link<T extends ElementType = 'a'>({
35
37
  >
36
38
  {children}
37
39
  {external && (
38
- <span className={css.externalLabel}> (opens in new tab)</span>
40
+ <span className={css.externalLabel}>{externalLabel}</span>
39
41
  )}
40
42
  </Component>
41
43
  );
@@ -1,7 +1,7 @@
1
1
  @layer component {
2
2
  .separator {
3
3
  border: none;
4
- background-color: var(--separator_color, var(--color_border));
4
+ background-color: var(--separator_color, var(--color_on-bg));
5
5
  }
6
6
 
7
7
  .separator.--horizontal {
@@ -2,7 +2,7 @@ import { ReactNode } from 'react';
2
2
  import css from './Table.module.css';
3
3
  import { cn } from '@boostdev/design-system-foundation';
4
4
 
5
- interface TableColumn<Row> {
5
+ export interface TableColumn<Row> {
6
6
  key: string;
7
7
  header: ReactNode;
8
8
  sortable?: boolean;
@@ -1 +1,2 @@
1
1
  export { Table } from './Table';
2
+ export type { TableColumn } from './Table';
@@ -2,7 +2,7 @@ import { KeyboardEvent, ReactNode, useId, useRef, useState } from 'react';
2
2
  import css from './Tabs.module.css';
3
3
  import { cn } from '@boostdev/design-system-foundation';
4
4
 
5
- interface TabItem {
5
+ export interface TabItem {
6
6
  id: string;
7
7
  label: ReactNode;
8
8
  content: ReactNode;
@@ -1 +1,2 @@
1
1
  export { Tabs } from './Tabs';
2
+ export type { TabItem } from './Tabs';
package/src/index.ts CHANGED
@@ -1,12 +1,15 @@
1
1
  // UI
2
2
  export { Accordion } from './components/ui/Accordion';
3
+ export type { AccordionItem } from './components/ui/Accordion';
3
4
  export { Alert } from './components/ui/Alert';
4
5
  export { Avatar } from './components/ui/Avatar';
5
6
  export { Badge } from './components/ui/Badge';
6
7
  export { Breadcrumb } from './components/ui/Breadcrumb';
8
+ export type { BreadcrumbItem } from './components/ui/Breadcrumb';
7
9
  export { Calendar } from './components/ui/Calendar';
8
10
  export { Carousel } from './components/ui/Carousel';
9
11
  export { DescriptionList } from './components/ui/DescriptionList';
12
+ export type { DescriptionItem } from './components/ui/DescriptionList';
10
13
  export { Link } from './components/ui/Link';
11
14
  export { Loading } from './components/ui/Loading';
12
15
  export { NotificationBanner } from './components/ui/NotificationBanner';
@@ -17,7 +20,9 @@ export { Separator } from './components/ui/Separator';
17
20
  export { Skeleton } from './components/ui/Skeleton';
18
21
  export { SkipLink } from './components/ui/SkipLink';
19
22
  export { Table } from './components/ui/Table';
23
+ export type { TableColumn } from './components/ui/Table';
20
24
  export { Tabs } from './components/ui/Tabs';
25
+ export type { TabItem } from './components/ui/Tabs';
21
26
  export { Tooltip } from './components/ui/Tooltip';
22
27
  export { Typography } from './components/ui/Typography';
23
28
 
@@ -28,6 +33,7 @@ export type { CommandItem } from './components/interaction/Command';
28
33
  export { Dialog } from './components/interaction/Dialog';
29
34
  export { Drawer } from './components/interaction/Drawer';
30
35
  export { DropdownMenu } from './components/interaction/DropdownMenu';
36
+ export type { DropdownMenuItem } from './components/interaction/DropdownMenu';
31
37
  export { Popover } from './components/interaction/Popover';
32
38
  export { Rating } from './components/interaction/Rating';
33
39
  export { ToastProvider, useToast } from './components/interaction/Toast';
@@ -35,11 +41,13 @@ export { ToastProvider, useToast } from './components/interaction/Toast';
35
41
  // Form
36
42
  export { Checkbox } from './components/interaction/form/Checkbox';
37
43
  export { Combobox } from './components/interaction/form/Combobox';
44
+ export type { ComboboxOption } from './components/interaction/form/Combobox';
38
45
  export { FileInput } from './components/interaction/form/FileInput';
39
46
  export { FormInput } from './components/interaction/form/FormInput';
40
47
  export { NumberInput } from './components/interaction/form/NumberInput';
41
48
  export { Radio } from './components/interaction/form/Radio';
42
49
  export { Select } from './components/interaction/form/Select';
50
+ export type { SelectOption } from './components/interaction/form/Select';
43
51
  export { Slider } from './components/interaction/form/Slider';
44
52
  export { Switch } from './components/interaction/form/Switch';
45
53
  export { Textarea } from './components/interaction/form/Textarea';