agroptima-design-system 0.9.3 → 0.11.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.
@@ -52,6 +52,13 @@ const config: StorybookConfig = {
52
52
  imageRule['exclude'] = /\.svg$/i
53
53
  }
54
54
 
55
+ if (config.resolve) {
56
+ config.resolve.alias = {
57
+ ...config.resolve.alias,
58
+ '@': resolve(__dirname, '../src'),
59
+ }
60
+ }
61
+
55
62
  return config
56
63
  },
57
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agroptima-design-system",
3
- "version": "0.9.3",
3
+ "version": "0.11.0",
4
4
  "scripts": {
5
5
  "dev": "npm run storybook",
6
6
  "storybook": "storybook dev -p 6006 --ci",
@@ -10,6 +10,7 @@
10
10
  align-items: center;
11
11
  gap: config.$space-2x;
12
12
  border-radius: config.$corner-radius-xxs;
13
+ pointer-events: none;
13
14
 
14
15
  &.fit-content {
15
16
  width: fit-content;
@@ -1,6 +1,7 @@
1
1
  import { IconButton, IconButtonProps } from './IconButton'
2
2
  import { Icon } from './Icon'
3
3
  import './Alert.scss'
4
+ import { classNames } from '@/utils/classNames'
4
5
 
5
6
  export type Variant = 'info' | 'success' | 'warning' | 'error'
6
7
 
@@ -22,14 +23,15 @@ export enum IconVariant {
22
23
  export function Alert({
23
24
  id,
24
25
  variant = 'success',
25
- className = '',
26
+ className,
26
27
  fitContent = false,
27
28
  text,
28
29
  button,
29
30
  ...props
30
31
  }: AlertProps): React.JSX.Element {
31
- const fitContentClass = fitContent ? 'fit-content' : ''
32
- const cssClasses = ['alert', variant, className, fitContentClass].join(' ')
32
+ const cssClasses = classNames('alert', variant, className, {
33
+ 'fit-content': fitContent,
34
+ })
33
35
 
34
36
  return (
35
37
  <div
@@ -1,3 +1,4 @@
1
+ import { classNames } from '@/utils/classNames'
1
2
  import './Badge.scss'
2
3
 
3
4
  export type Variant =
@@ -18,11 +19,12 @@ export interface BadgeProps extends React.ComponentPropsWithoutRef<'div'> {
18
19
 
19
20
  export function Badge({
20
21
  variant = 'info',
22
+ className,
21
23
  text,
22
24
  accessibilityLabel,
23
25
  ...props
24
26
  }: BadgeProps): React.JSX.Element {
25
- const cssClasses = ['badge', variant].join(' ')
27
+ const cssClasses = classNames('badge', variant, className)
26
28
 
27
29
  return (
28
30
  <span
@@ -1,6 +1,7 @@
1
1
  import NextLink from 'next/link'
2
2
  import './Button.scss'
3
3
  import { Icon, IconType } from './Icon'
4
+ import { classNames } from '@/utils/classNames'
4
5
 
5
6
  export interface BaseButtonProps {
6
7
  label: string
@@ -56,15 +57,15 @@ export function Button({
56
57
  if (loading) {
57
58
  leftIcon = 'Loading'
58
59
  }
59
- const cssClasses = ['button', variant].join(' ')
60
+ const cssClasses = classNames('button', variant, props.className)
60
61
 
61
62
  if (hasHref(props)) {
62
63
  return (
63
64
  <NextLink
64
65
  href={props.href || ''}
65
- className={cssClasses}
66
66
  aria-label={accessibilityLabel || label}
67
67
  {...props}
68
+ className={cssClasses}
68
69
  >
69
70
  {leftIcon && <Icon name={leftIcon} />}
70
71
  {label}
@@ -75,10 +76,10 @@ export function Button({
75
76
 
76
77
  return (
77
78
  <button
78
- className={cssClasses}
79
79
  disabled={loading || disabled}
80
80
  aria-label={accessibilityLabel || label}
81
81
  {...props}
82
+ className={cssClasses}
82
83
  >
83
84
  {leftIcon && <Icon name={leftIcon} />}
84
85
  {label}
@@ -3,18 +3,19 @@
3
3
  @use '../settings/config';
4
4
 
5
5
  .cards-table-list {
6
- width: 100%;
7
- border-collapse: separate;
8
- border-spacing: 0 config.$space-3x;
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: config.$space-3x;
9
9
 
10
- thead {
11
- background: transparent;
10
+ tbody {
11
+ display: flex;
12
+ flex-direction: column;
13
+ gap: config.$space-3x;
12
14
  }
13
15
 
14
- th,
15
- td {
16
- overflow: hidden;
17
- text-overflow: ellipsis;
16
+ tr {
17
+ display: flex;
18
+ flex-grow: 1;
18
19
  }
19
20
 
20
21
  .container {
@@ -22,11 +23,11 @@
22
23
  flex-direction: row;
23
24
  justify-content: space-between;
24
25
  align-items: center;
26
+ width: 100%;
25
27
  }
26
28
 
27
29
  th {
28
30
  padding: config.$space-2x config.$space-3x;
29
- white-space: nowrap;
30
31
  text-align: left;
31
32
 
32
33
  &.sortable {
@@ -44,24 +45,16 @@
44
45
  }
45
46
  }
46
47
 
47
- td {
48
- padding: config.$space-5x config.$space-3x;
49
- }
50
-
51
48
  .no-wrap {
52
49
  white-space: nowrap;
53
50
  }
54
51
 
55
52
  .alignment-left {
56
- text-align: left;
53
+ justify-content: flex-start;
57
54
  }
58
55
 
59
56
  .alignment-center {
60
- text-align: center;
61
- }
62
-
63
- .alignment-right {
64
- text-align: right;
57
+ justify-content: center;
65
58
  }
66
59
 
67
60
  &.primary {
@@ -126,6 +119,10 @@
126
119
  background: color_alias.$primary-color-50;
127
120
  }
128
121
 
122
+ tr > td:first-child {
123
+ @include typography.cards-table-list-highlight-text;
124
+ }
125
+
129
126
  tr.disabled {
130
127
  background: color_alias.$neutral-color-50;
131
128
 
@@ -133,9 +130,89 @@
133
130
  @include typography.cards-table-list-disabled-text;
134
131
  }
135
132
  }
133
+ }
136
134
 
137
- tr > td:first-child {
138
- @include typography.cards-table-list-highlight-text;
135
+ // Media queries
136
+ $small: 375px;
137
+ $medium: 768px;
138
+ $large: 1200px;
139
+
140
+ // Mobile & tablet cases
141
+ @media only screen and (min-width: $small) and (max-width: $large) {
142
+ thead {
143
+ display: none;
144
+ }
145
+
146
+ tr {
147
+ flex-direction: row;
148
+ flex-wrap: wrap;
149
+ position: relative;
150
+ gap: config.$space-1x;
151
+ padding: config.$space-3x;
152
+ }
153
+
154
+ td {
155
+ width: 100%;
156
+ }
157
+
158
+ td:first-child {
159
+ order: -2;
160
+ width: 50%;
161
+ margin-bottom: config.$space-2x;
162
+ }
163
+ td.actions {
164
+ order: -1;
165
+ width: 35%;
166
+ flex-grow: 1;
167
+ margin-bottom: config.$space-2x;
168
+
169
+ > div {
170
+ justify-content: flex-end;
171
+ }
172
+ }
173
+
174
+ .badge {
175
+ position: absolute;
176
+ inset: auto config.$space-3x config.$space-3x auto;
177
+ }
178
+ }
179
+ // Desktop case
180
+ @media only screen and (min-width: $large) {
181
+ thead {
182
+ display: flex;
183
+ }
184
+
185
+ tr {
186
+ flex-direction: row;
187
+ }
188
+
189
+ th,
190
+ td {
191
+ display: flex;
192
+ justify-content: flex-start;
193
+ align-items: center;
194
+ flex: 2;
195
+ }
196
+
197
+ td {
198
+ padding: config.$space-5x config.$space-3x;
199
+ }
200
+
201
+ th.actions {
202
+ flex: 1;
203
+ }
204
+ td.actions {
205
+ order: 0;
206
+ justify-content: center;
207
+ flex: 1;
208
+ }
209
+
210
+ td:has(.badge) {
211
+ gap: config.$space-2x;
212
+ }
213
+
214
+ .alignment-right {
215
+ justify-content: flex-end;
139
216
  }
140
217
  }
141
218
  }
@@ -1,3 +1,4 @@
1
+ import { classNames } from '@/utils/classNames'
1
2
  import './CardsTable.scss'
2
3
 
3
4
  export type Variant = 'primary'
@@ -8,13 +9,13 @@ export interface CardsTableProps
8
9
  }
9
10
 
10
11
  export function CardsTable({
12
+ className,
11
13
  summary,
12
14
  variant = 'primary',
13
15
  children,
14
16
  ...props
15
17
  }: CardsTableProps): React.JSX.Element {
16
- const cssClasses = ['cards-table-list', variant].join(' ')
17
-
18
+ const cssClasses = classNames('cards-table-list', variant, className)
18
19
  return (
19
20
  <table summary={summary} role="table" className={cssClasses} {...props}>
20
21
  {children}
@@ -1,3 +1,4 @@
1
+ import { classNames } from '@/utils/classNames'
1
2
  import './CardsTable.scss'
2
3
  import React from 'react'
3
4
 
@@ -17,13 +18,12 @@ export function CardsTableCell({
17
18
  noWrap = false,
18
19
  align = Alignment.Left,
19
20
  children,
21
+ className,
20
22
  ...props
21
23
  }: CardsTableCellProps): React.JSX.Element {
22
- const cssClasses = [
23
- 'cell',
24
- noWrap ? 'no-wrap' : '',
25
- `alignment-${align}`,
26
- ].join(' ')
24
+ const cssClasses = classNames('cell', `alignment-${align}`, className, {
25
+ 'no-wrap': noWrap,
26
+ })
27
27
  return (
28
28
  <td role="cell" className={cssClasses} {...props}>
29
29
  {children}
@@ -1,3 +1,5 @@
1
+ import { classNames } from '@/utils/classNames'
2
+
1
3
  export interface CardsTableHeaderProps
2
4
  extends React.ComponentPropsWithoutRef<'th'> {}
3
5
 
@@ -6,7 +8,7 @@ export function CardsTableHeader({
6
8
  className,
7
9
  ...props
8
10
  }: CardsTableHeaderProps) {
9
- const cssClasses = ['header', className].join(' ')
11
+ const cssClasses = classNames('header', className)
10
12
  return (
11
13
  <th scope="col" role="columnheader" className={cssClasses} {...props}>
12
14
  {children}
@@ -1,3 +1,4 @@
1
+ import { classNames } from '@/utils/classNames'
1
2
  import './Checkbox.scss'
2
3
 
3
4
  export type Variant = 'primary'
@@ -17,26 +18,28 @@ export function Checkbox({
17
18
  disabled,
18
19
  variant = 'primary',
19
20
  id,
21
+ name,
20
22
  ...props
21
23
  }: CheckboxProps) {
22
- const disabledClass = disabled ? 'disabled' : ''
23
- const cssClasses = ['checkbox', variant].join(' ')
24
+ const identifier = id || name
25
+ const inputCss = classNames('checkbox', variant)
26
+ const labelCss = classNames('checkbox-label-container', {
27
+ disabled: disabled,
28
+ })
24
29
 
25
30
  return (
26
31
  <div className={`checkbox-group ${variant}`}>
27
32
  <input
28
- id={id}
33
+ id={identifier}
34
+ name={name}
29
35
  type="checkbox"
30
- className={cssClasses}
36
+ className={inputCss}
31
37
  disabled={disabled}
32
38
  aria-label={accessibilityLabel}
33
39
  {...props}
34
40
  />
35
41
 
36
- <label
37
- className={`checkbox-label-container ${disabledClass}`}
38
- htmlFor={id}
39
- >
42
+ <label className={labelCss} htmlFor={identifier}>
40
43
  <span className="background-icon"></span>
41
44
  {!hideLabel && <span className="label">{label}</span>}
42
45
  </label>
@@ -1,3 +1,4 @@
1
+ import { classNames } from '@/utils/classNames'
1
2
  import { Button, ButtonProps } from './Button'
2
3
  import './EmptyState.scss'
3
4
  import { Icon, IconType } from './Icon'
@@ -12,12 +13,13 @@ export interface EmptyStateProps extends React.ComponentPropsWithoutRef<'div'> {
12
13
  }
13
14
 
14
15
  export function EmptyState({
16
+ className,
15
17
  icon = 'EmptyState',
16
18
  text = 'No data',
17
19
  variant = 'primary',
18
20
  button,
19
21
  }: EmptyStateProps): React.JSX.Element {
20
- const cssClasses = ['empty-state', variant].join(' ')
22
+ const cssClasses = classNames('empty-state', variant, className)
21
23
 
22
24
  return (
23
25
  <div className={cssClasses}>
@@ -1,6 +1,7 @@
1
1
  import './Icon.scss'
2
2
 
3
3
  import * as icons from '../icons'
4
+ import { classNames } from '@/utils/classNames'
4
5
  export type IconType = keyof typeof icons
5
6
 
6
7
  export interface IconProps extends React.SVGAttributes<HTMLOrSVGElement> {
@@ -9,11 +10,9 @@ export interface IconProps extends React.SVGAttributes<HTMLOrSVGElement> {
9
10
  }
10
11
 
11
12
  export const Icon: React.FC<IconProps> = ({ name, className, ...props }) => {
12
- const cssClasses = [
13
- 'icon',
14
- className,
15
- name === 'Loading' ? 'rotate' : '',
16
- ].join(' ')
13
+ const cssClasses = classNames('icon', className, {
14
+ rotate: name === 'Loading',
15
+ })
17
16
  return (
18
17
  <span role="img" title={name} className={cssClasses}>
19
18
  {icons[name](props)}
@@ -1,6 +1,7 @@
1
1
  import NextLink from 'next/link'
2
2
  import './IconButton.scss'
3
3
  import { Icon, IconType } from './Icon'
4
+ import { classNames } from '@/utils/classNames'
4
5
 
5
6
  export type Variant = 'primary'
6
7
 
@@ -29,15 +30,15 @@ export function IconButton({
29
30
  variant = 'primary',
30
31
  ...props
31
32
  }: IconButtonProps) {
32
- const cssClasses = ['icon-button', variant].join(' ')
33
+ const cssClasses = classNames('icon-button', variant, props.className)
33
34
 
34
35
  if (hasHref(props)) {
35
36
  return (
36
37
  <NextLink
37
38
  href={props.href || ''}
38
- className={cssClasses}
39
39
  aria-label={accessibilityLabel}
40
40
  {...props}
41
+ className={cssClasses}
41
42
  >
42
43
  <Icon name={icon} />
43
44
  </NextLink>
@@ -46,10 +47,10 @@ export function IconButton({
46
47
 
47
48
  return (
48
49
  <button
49
- className={cssClasses}
50
50
  disabled={disabled}
51
51
  aria-label={accessibilityLabel}
52
52
  {...props}
53
+ className={cssClasses}
53
54
  >
54
55
  <Icon name={icon} />
55
56
  </button>
@@ -1,6 +1,8 @@
1
- import './Input.scss'
2
1
  import React, { useState } from 'react'
3
2
  import { Icon, IconType } from './Icon'
3
+ import { classNames } from '@/utils/classNames'
4
+ import './Input.scss'
5
+ import { buildHelpText } from '@/utils/buildHelpText'
4
6
 
5
7
  export type InputVariant = 'primary'
6
8
 
@@ -18,6 +20,7 @@ export interface InputProps extends React.ComponentPropsWithoutRef<'input'> {
18
20
  export function Input({
19
21
  label,
20
22
  accessibilityLabel,
23
+ className,
21
24
  hideLabel = false,
22
25
  icon,
23
26
  helpText,
@@ -29,10 +32,13 @@ export function Input({
29
32
  errors,
30
33
  ...props
31
34
  }: InputProps): React.JSX.Element {
35
+ const identifier = id || name
32
36
  const [showPassword, setShowPassword] = useState(false)
33
- const iconClass = icon ? 'with-icon' : ''
34
- const invalidClass = errors ? 'invalid' : ''
35
- const cssClasses = ['input', iconClass, invalidClass].join(' ')
37
+ const cssClasses = classNames('input', className, {
38
+ 'with-icon': icon,
39
+ invalid: errors?.length,
40
+ })
41
+ const helpTexts = buildHelpText(helpText, errors)
36
42
 
37
43
  function handlePasswordIcon() {
38
44
  return showPassword ? 'ShowOff' : 'Show'
@@ -51,14 +57,14 @@ export function Input({
51
57
  return (
52
58
  <div className={`input-group ${variant}`}>
53
59
  {!hideLabel && (
54
- <label className="input-label" htmlFor={id}>
60
+ <label className="input-label" htmlFor={identifier}>
55
61
  {label}
56
62
  </label>
57
63
  )}
58
64
  <div className="input-container">
59
65
  {icon && <Icon className="left-icon" name={icon} />}
60
66
  <input
61
- id={id}
67
+ id={identifier}
62
68
  className={cssClasses}
63
69
  disabled={disabled}
64
70
  type={handleInputType()}
@@ -74,17 +80,11 @@ export function Input({
74
80
  />
75
81
  )}
76
82
  </div>
77
- {helpText && !errors && (
78
- <span className="input-help-text">{helpText}</span>
79
- )}
80
- {errors &&
81
- errors?.map((error, index) => {
82
- return (
83
- <span key={`error-${index}`} className="input-help-text">
84
- {error}
85
- </span>
86
- )
87
- })}
83
+ {helpTexts.map((helpText) => (
84
+ <span key={`${identifier}-${helpText}`} className="input-help-text">
85
+ {helpText}
86
+ </span>
87
+ ))}
88
88
  </div>
89
89
  )
90
90
  }
@@ -1,3 +1,4 @@
1
+ import { classNames } from '@/utils/classNames'
1
2
  import { Button, ButtonProps } from './Button'
2
3
  import { Icon } from './Icon'
3
4
  import './Modal.scss'
@@ -21,13 +22,14 @@ export interface ModalProps extends React.ComponentPropsWithoutRef<'div'> {
21
22
 
22
23
  export function Modal({
23
24
  id,
25
+ className,
24
26
  variant = 'info',
25
27
  title,
26
28
  buttons,
27
29
  children,
28
30
  ...props
29
31
  }: ModalProps): React.JSX.Element {
30
- const cssClasses = ['modal', variant].join(' ')
32
+ const cssClasses = classNames('modal', variant, className)
31
33
 
32
34
  return (
33
35
  <div className="modal-container">
@@ -1,6 +1,8 @@
1
1
  import './Multiselect.scss'
2
2
  import React, { useState } from 'react'
3
3
  import { Icon } from './Icon'
4
+ import { classNames } from '@/utils/classNames'
5
+ import { buildHelpText } from '@/utils/buildHelpText'
4
6
 
5
7
  export type Variant = 'primary'
6
8
  export type Option = { id: string; label: string }
@@ -20,6 +22,7 @@ export interface MultiselectProps
20
22
  }
21
23
 
22
24
  export function Multiselect({
25
+ className,
23
26
  placeholder,
24
27
  helpText,
25
28
  variant = 'primary',
@@ -34,23 +37,17 @@ export function Multiselect({
34
37
  selected,
35
38
  ...props
36
39
  }: MultiselectProps): React.JSX.Element {
40
+ const helpTexts = buildHelpText(helpText, errors)
37
41
  const [showOptionsList, setShowOptionsList] = useState(false)
38
42
  const [selectedOptionsIds, setSelectedOptionsIds] = useState<string[]>(
39
43
  selected?.map((option) => option.id) || [],
40
44
  )
41
-
42
- const optionsListOpenClass = showOptionsList ? 'open' : ''
43
- const filledSelectClass = selectedOptionsIds.length > 0 ? 'filled' : ''
44
- const disabledClass = disabled ? 'disabled' : ''
45
- const invalidClass = errors ? 'invalid' : ''
46
-
47
- const cssClasses = [
48
- 'selected-option',
49
- optionsListOpenClass,
50
- filledSelectClass,
51
- disabledClass,
52
- invalidClass,
53
- ].join(' ')
45
+ const cssClasses = classNames('selected-option', className, {
46
+ open: showOptionsList,
47
+ filled: selectedOptionsIds.length > 0,
48
+ disabled: disabled,
49
+ invalid: errors?.length,
50
+ })
54
51
 
55
52
  function handleOptionsList() {
56
53
  if (!disabled) setShowOptionsList(!showOptionsList)
@@ -130,19 +127,11 @@ export function Multiselect({
130
127
  </ul>
131
128
  )}
132
129
  </div>
133
- {helpText && !errors && (
134
- <span className={`multiselect-help-text ${invalidClass}`}>
130
+ {helpTexts.map((helpText) => (
131
+ <span key={`${name}-${helpText}`} className="multiselect-help-text">
135
132
  {helpText}
136
133
  </span>
137
- )}
138
- {errors &&
139
- errors?.map((error, index) => {
140
- return (
141
- <span key={`error-${index}`} className="multiselect-help-text">
142
- {error}
143
- </span>
144
- )
145
- })}
134
+ ))}
146
135
  <input
147
136
  type="hidden"
148
137
  name={name}
@@ -1,6 +1,8 @@
1
1
  import './Select.scss'
2
2
  import React, { useState } from 'react'
3
3
  import { Icon } from './Icon'
4
+ import { classNames } from '@/utils/classNames'
5
+ import { buildHelpText } from '@/utils/buildHelpText'
4
6
 
5
7
  export type Variant = 'primary'
6
8
  export type Option = { id: string; label: string }
@@ -23,6 +25,7 @@ export interface SelectProps extends InputPropsWithoutOnChange {
23
25
  }
24
26
 
25
27
  export function Select({
28
+ className,
26
29
  placeholder,
27
30
  helpText,
28
31
  variant = 'primary',
@@ -37,24 +40,19 @@ export function Select({
37
40
  onChange,
38
41
  ...props
39
42
  }: SelectProps): React.JSX.Element {
43
+ const helpTexts = buildHelpText(helpText, errors)
40
44
  const [showOptionsList, setShowOptionsList] = useState(false)
41
45
  const [selectedOption, setSelectedOption] = useState<Option>({
42
46
  id: selected?.id || '',
43
47
  label: selected?.label || '',
44
48
  })
45
49
 
46
- const optionsListOpenClass = showOptionsList ? 'open' : ''
47
- const filledSelectClass = selectedOption.id ? 'filled' : ''
48
- const disabledClass = disabled ? 'disabled' : ''
49
- const invalidClass = errors ? 'invalid' : ''
50
-
51
- const cssClasses = [
52
- 'selected-option',
53
- optionsListOpenClass,
54
- filledSelectClass,
55
- disabledClass,
56
- invalidClass,
57
- ].join(' ')
50
+ const cssClasses = classNames('selected-option', className, {
51
+ open: showOptionsList,
52
+ filled: selectedOption.id,
53
+ disabled: disabled,
54
+ invalid: errors?.length,
55
+ })
58
56
 
59
57
  function handleOptionsList() {
60
58
  if (!disabled) setShowOptionsList(!showOptionsList)
@@ -113,17 +111,11 @@ export function Select({
113
111
  </ul>
114
112
  )}
115
113
  </div>
116
- {helpText && !errors && (
117
- <span className="select-help-text">{helpText}</span>
118
- )}
119
- {errors &&
120
- errors?.map((error, index) => {
121
- return (
122
- <span key={`error-${index}`} className="select-help-text">
123
- {error}
124
- </span>
125
- )
126
- })}
114
+ {helpTexts.map((helpText) => (
115
+ <span key={`${name}-${helpText}`} className="select-help-text">
116
+ {helpText}
117
+ </span>
118
+ ))}
127
119
  <input type="hidden" name={name} value={selectedOption.id} {...props} />
128
120
  </div>
129
121
  )
@@ -0,0 +1,58 @@
1
+ import { classNames } from '@/utils/classNames'
2
+ import { buildHelpText } from '@/utils/buildHelpText'
3
+ import './Input.scss'
4
+
5
+ export type TextAreaVariant = 'primary'
6
+
7
+ export interface TextAreaProps
8
+ extends React.ComponentPropsWithoutRef<'textarea'> {
9
+ label: string
10
+ accessibilityLabel?: string
11
+ hideLabel?: boolean
12
+ helpText?: string
13
+ variant?: TextAreaVariant
14
+ id?: string
15
+ errors?: string[]
16
+ }
17
+
18
+ export default function TextArea({
19
+ id,
20
+ label,
21
+ className,
22
+ accessibilityLabel,
23
+ hideLabel = false,
24
+ helpText,
25
+ variant = 'primary',
26
+ disabled,
27
+ name,
28
+ errors,
29
+ ...props
30
+ }: TextAreaProps) {
31
+ const identifier = id || name
32
+ const cssClasses = classNames('input', className, { invalid: errors?.length })
33
+ const helpTexts = buildHelpText(helpText, errors)
34
+ return (
35
+ <div className={`input-group ${variant}`}>
36
+ {!hideLabel && (
37
+ <label className="input-label" htmlFor={identifier}>
38
+ {label}
39
+ </label>
40
+ )}
41
+ <div className="input-container">
42
+ <textarea
43
+ id={identifier}
44
+ className={cssClasses}
45
+ disabled={disabled}
46
+ name={name}
47
+ aria-label={accessibilityLabel || label}
48
+ {...props}
49
+ />
50
+ </div>
51
+ {helpTexts.map((helpText) => (
52
+ <span key={`${identifier}-${helpText}`} className="input-help-text">
53
+ {helpText}
54
+ </span>
55
+ ))}
56
+ </div>
57
+ )
58
+ }
@@ -115,6 +115,14 @@
115
115
  text-decoration-line: underline;
116
116
  }
117
117
 
118
+ @mixin body-link {
119
+ @include base;
120
+ font-weight: 400;
121
+ color: color_alias.$primary-color-600;
122
+ font-size: 1rem;
123
+ line-height: 1.5rem;
124
+ }
125
+
118
126
  @mixin footnote-primary {
119
127
  @include base;
120
128
  font-weight: 400;
@@ -6,12 +6,21 @@ import { CardsTableHeader } from '../atoms/CardsTableHeader'
6
6
  import { CardsTableRow } from '../atoms/CardsTableRow'
7
7
  import { CardsTableBody } from '../atoms/CardsTableBody'
8
8
  import { CardsTableCell } from '../atoms/CardsTableCell'
9
- import { Button } from '../atoms/Button'
9
+ import { IconButton } from '../atoms/IconButton'
10
+ import { Badge } from '../atoms/Badge'
11
+
12
+ const figmaPrimaryDesign = {
13
+ design: {
14
+ type: 'figma',
15
+ url: 'https://www.figma.com/file/DN2ova21vWqCRvPspBXgI1/Design-System?type=design&node-id=2331-990&mode=dev',
16
+ },
17
+ }
10
18
 
11
19
  const meta = {
12
20
  title: 'Design System/Atoms/CardsTable',
13
21
  component: CardsTable,
14
22
  tags: ['autodocs'],
23
+ parameters: figmaPrimaryDesign,
15
24
  }
16
25
 
17
26
  export default meta
@@ -24,34 +33,101 @@ export const Primary = {
24
33
  <CardsTableHeader>Game title</CardsTableHeader>
25
34
  <CardsTableHeader>Company address</CardsTableHeader>
26
35
  <CardsTableHeader>Customer service email</CardsTableHeader>
36
+ <CardsTableHeader>Price</CardsTableHeader>
37
+ <CardsTableHeader className="actions">Actions</CardsTableHeader>
27
38
  </CardsTableRow>
28
39
  </CardsTableHead>
29
40
  <CardsTableBody>
30
41
  <CardsTableRow>
31
- <CardsTableCell>Metal Gear Solid 5: The Phantom Pain</CardsTableCell>
32
- <CardsTableCell noWrap>
42
+ <CardsTableCell>
43
+ <span>Metal Gear Solid 5: The Phantom Pain</span>
44
+ <Badge
45
+ accessibilityLabel="Game is bought"
46
+ text="Bought"
47
+ variant="success-outlined"
48
+ />
49
+ </CardsTableCell>
50
+ <CardsTableCell>
33
51
  Konami Digital Entertainment Co., Ltd. 1-11-1, Ginza, Chuo-ku,
34
52
  Tokyo, 104-0061 Japan
35
53
  </CardsTableCell>
36
- <CardsTableCell align="right">konami@fakemail.com</CardsTableCell>
54
+ <CardsTableCell>konami@fakemail.com</CardsTableCell>
55
+ <CardsTableCell align="right">6,99 €</CardsTableCell>
56
+ <CardsTableCell className="actions" align="center">
57
+ <div style={{ display: 'flex', gap: '1.75rem' }}>
58
+ <IconButton
59
+ icon="Edit"
60
+ accessibilityLabel="Edit game"
61
+ href="link.com"
62
+ />
63
+ <IconButton
64
+ icon="Export"
65
+ accessibilityLabel="Export game"
66
+ href="link.com"
67
+ />
68
+ <IconButton
69
+ icon="Delete"
70
+ accessibilityLabel="Delete game"
71
+ href="link.com"
72
+ />
73
+ </div>
74
+ </CardsTableCell>
37
75
  </CardsTableRow>
38
76
 
39
77
  <CardsTableRow>
40
78
  <CardsTableCell>The Witcher 3</CardsTableCell>
41
- <CardsTableCell noWrap>
79
+ <CardsTableCell>
42
80
  CD PROJEKT S.A. ul. Jagiellońska 74 03-301 Warszawa Poland
43
81
  </CardsTableCell>
44
- <CardsTableCell align="right">cdprojekt@fakemail.com</CardsTableCell>
82
+ <CardsTableCell>cdprojekt@fakemail.com</CardsTableCell>
83
+ <CardsTableCell align="right">19,99 €</CardsTableCell>
84
+ <CardsTableCell className="actions" align="center">
85
+ <div style={{ display: 'flex', gap: '1.75rem' }}>
86
+ <IconButton
87
+ icon="Edit"
88
+ accessibilityLabel="Edit game"
89
+ href="link.com"
90
+ />
91
+ <IconButton
92
+ icon="Export"
93
+ accessibilityLabel="Export game"
94
+ href="link.com"
95
+ />
96
+ <IconButton
97
+ icon="Delete"
98
+ accessibilityLabel="Delete game"
99
+ href="link.com"
100
+ />
101
+ </div>
102
+ </CardsTableCell>
45
103
  </CardsTableRow>
46
104
 
47
105
  <CardsTableRow>
48
106
  <CardsTableCell>Tekken 8</CardsTableCell>
49
- <CardsTableCell noWrap>
107
+ <CardsTableCell>
50
108
  Bandai Namco Studios Inc. ; Address: 2-37-25 Eitai, Koto-ku, Tokyo
51
109
  135-0034, Japan
52
110
  </CardsTableCell>
53
- <CardsTableCell align="right">
54
- namco@fakemail.com <Button label="click" />
111
+ <CardsTableCell>namco@fakemail.com</CardsTableCell>
112
+ <CardsTableCell align="right">79,99 €</CardsTableCell>
113
+ <CardsTableCell className="actions" align="center">
114
+ <div style={{ display: 'flex', gap: '1.75rem' }}>
115
+ <IconButton
116
+ icon="Edit"
117
+ accessibilityLabel="Edit game"
118
+ href="link.com"
119
+ />
120
+ <IconButton
121
+ icon="Export"
122
+ accessibilityLabel="Export game"
123
+ href="link.com"
124
+ />
125
+ <IconButton
126
+ icon="Delete"
127
+ accessibilityLabel="Delete game"
128
+ href="link.com"
129
+ />
130
+ </div>
55
131
  </CardsTableCell>
56
132
  </CardsTableRow>
57
133
  </CardsTableBody>
@@ -26,7 +26,7 @@ const meta = {
26
26
  const figmaPrimaryDesign = {
27
27
  design: {
28
28
  type: 'figma',
29
- url: 'https://www.figma.com/file/DN2ova21vWqCRvPspBXgI1/Design-System?type=design&node-id=1272-1328&mode=dev',
29
+ url: 'https://www.figma.com/file/DN2ova21vWqCRvPspBXgI1/Design-System?type=design&node-id=2331-990&mode=dev',
30
30
  },
31
31
  }
32
32
 
@@ -3,6 +3,15 @@ import { Meta } from "@storybook/addon-docs";
3
3
  <Meta title="Changelog" />
4
4
  # Changelog
5
5
 
6
+ ## 0.11.0
7
+ - Added TextArea component to Storybook.
8
+ - Id prop is now optional in Input, Select, Multiselect and TextArea components.
9
+
10
+ ## 0.10.0
11
+
12
+ - Cards Table components now are responsive.
13
+ - Added Link style to Typography.
14
+
6
15
  ## 0.9.3
7
16
 
8
17
  - Fix alert component width.
@@ -31,7 +31,7 @@ const meta = {
31
31
  type: {
32
32
  description: 'Input type property',
33
33
  },
34
- leftIcon: {
34
+ icon: {
35
35
  description: 'Input left icon from a list of values',
36
36
  },
37
37
  id: {
@@ -0,0 +1,78 @@
1
+ import TextArea from '../atoms/TextArea'
2
+ import { StoryObj } from '@storybook/react'
3
+
4
+ const meta = {
5
+ title: 'Design System/Atoms/Textarea',
6
+ component: TextArea,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ label: {
10
+ description: 'Label for the textarea',
11
+ },
12
+ accessibilityLabel: {
13
+ description:
14
+ 'Describes the textarea purpose. If empty, label content will be used',
15
+ },
16
+ placeholder: {
17
+ description: 'Optional textarea placeholder text',
18
+ },
19
+ variant: {
20
+ description: 'Textarea variant used',
21
+ },
22
+ disabled: {
23
+ description: 'Is the textarea in disabled state?',
24
+ },
25
+ helpText: {
26
+ description: 'Optional help text',
27
+ },
28
+ name: {
29
+ description: 'Textarea name property',
30
+ },
31
+ id: {
32
+ description: 'Value needed for the label relation',
33
+ },
34
+ errors: {
35
+ description:
36
+ 'Optional array of errors. If passed, the errors are listed and invalid style is applied.',
37
+ },
38
+ },
39
+ }
40
+
41
+ const figmaPrimaryDesign = {
42
+ design: {
43
+ type: 'figma',
44
+ url: 'https://www.figma.com/file/DN2ova21vWqCRvPspBXgI1/Design-System?type=design&node-id=2371-2157&mode=dev',
45
+ },
46
+ }
47
+
48
+ export default meta
49
+ type Story = StoryObj<typeof meta>
50
+
51
+ export const Primary: Story = {
52
+ args: {
53
+ label: 'Textarea',
54
+ accessibilityLabel: 'Fill the textarea',
55
+ placeholder: 'Write here...',
56
+ variant: 'primary',
57
+ disabled: false,
58
+ helpText: 'This text can help you',
59
+ name: 'textarea',
60
+ id: 'textarea',
61
+ },
62
+ parameters: figmaPrimaryDesign,
63
+ }
64
+
65
+ export const WithErrors: Story = {
66
+ args: {
67
+ label: 'Textarea',
68
+ accessibilityLabel: 'Fill the form textarea',
69
+ placeholder: 'Write here...',
70
+ variant: 'primary',
71
+ disabled: false,
72
+ helpText: 'This text can help you',
73
+ name: 'textarea',
74
+ id: 'textarea',
75
+ errors: ['error1', 'error2'],
76
+ },
77
+ parameters: figmaPrimaryDesign,
78
+ }
@@ -0,0 +1,12 @@
1
+ export function buildHelpText(
2
+ helpText: string | undefined,
3
+ errors: string[] | undefined,
4
+ ): string[] {
5
+ if (Boolean(errors?.length)) {
6
+ return errors || []
7
+ }
8
+ if (helpText) {
9
+ return [helpText]
10
+ }
11
+ return []
12
+ }
@@ -0,0 +1,23 @@
1
+ type classNameProp = string | { [key: string]: any } | undefined
2
+
3
+ export const classNames = (...classNames: classNameProp[]): string => {
4
+ const resultClasses: string[] = []
5
+
6
+ classNames.forEach((className) => {
7
+ if (className === undefined) {
8
+ return
9
+ }
10
+
11
+ if (typeof className === 'string') {
12
+ return resultClasses.push(className)
13
+ }
14
+
15
+ Object.keys(className).forEach((key) => {
16
+ if (Boolean(className[key])) {
17
+ resultClasses.push(key)
18
+ }
19
+ })
20
+ })
21
+
22
+ return resultClasses.join(' ')
23
+ }
@@ -0,0 +1,128 @@
1
+ import React from 'react'
2
+ import { render } from '@testing-library/react'
3
+ import { CardsTable } from '@/atoms/CardsTable'
4
+ import { CardsTableHead } from '@/atoms/CardsTableHead'
5
+ import { CardsTableHeader } from '@/atoms/CardsTableHeader'
6
+ import { CardsTableRow } from '@/atoms/CardsTableRow'
7
+ import { CardsTableBody } from '@/atoms/CardsTableBody'
8
+ import { CardsTableCell, Alignment } from '@/atoms/CardsTableCell'
9
+ import { IconButton } from '@/atoms/IconButton'
10
+ import { Badge } from '@/atoms/Badge'
11
+
12
+ describe('CardsTable', () => {
13
+ it('renders', () => {
14
+ const { getAllByRole } = render(
15
+ <CardsTable>
16
+ <CardsTableHead>
17
+ <CardsTableRow>
18
+ <CardsTableHeader>Game title</CardsTableHeader>
19
+ <CardsTableHeader>Company address</CardsTableHeader>
20
+ <CardsTableHeader>Customer service email</CardsTableHeader>
21
+ <CardsTableHeader>Price</CardsTableHeader>
22
+ <CardsTableHeader className="actions">Actions</CardsTableHeader>
23
+ </CardsTableRow>
24
+ </CardsTableHead>
25
+ <CardsTableBody>
26
+ <CardsTableRow>
27
+ <CardsTableCell>
28
+ <span>Metal Gear Solid 5: The Phantom Pain</span>
29
+ <Badge
30
+ accessibilityLabel="Game is bought"
31
+ text="Bought"
32
+ variant="success-outlined"
33
+ />
34
+ </CardsTableCell>
35
+ <CardsTableCell>
36
+ Konami Digital Entertainment Co., Ltd. 1-11-1, Ginza, Chuo-ku,
37
+ Tokyo, 104-0061 Japan
38
+ </CardsTableCell>
39
+ <CardsTableCell>konami@fakemail.com</CardsTableCell>
40
+ <CardsTableCell align={Alignment.Right}>6,99 €</CardsTableCell>
41
+ <CardsTableCell className="actions" align={Alignment.Center}>
42
+ <div style={{ display: 'flex', gap: '1.75rem' }}>
43
+ <IconButton
44
+ icon="Edit"
45
+ accessibilityLabel="Edit game"
46
+ href="link.com"
47
+ />
48
+ <IconButton
49
+ icon="Export"
50
+ accessibilityLabel="Export game"
51
+ href="link.com"
52
+ />
53
+ <IconButton
54
+ icon="Delete"
55
+ accessibilityLabel="Delete game"
56
+ href="link.com"
57
+ />
58
+ </div>
59
+ </CardsTableCell>
60
+ </CardsTableRow>
61
+
62
+ <CardsTableRow>
63
+ <CardsTableCell>The Witcher 3</CardsTableCell>
64
+ <CardsTableCell>
65
+ CD PROJEKT S.A. ul. Jagiellońska 74 03-301 Warszawa Poland
66
+ </CardsTableCell>
67
+ <CardsTableCell>cdprojekt@fakemail.com</CardsTableCell>
68
+ <CardsTableCell align={Alignment.Right}>19,99 €</CardsTableCell>
69
+ <CardsTableCell className="actions" align={Alignment.Center}>
70
+ <div style={{ display: 'flex', gap: '1.75rem' }}>
71
+ <IconButton
72
+ icon="Edit"
73
+ accessibilityLabel="Edit game"
74
+ href="link.com"
75
+ />
76
+ <IconButton
77
+ icon="Export"
78
+ accessibilityLabel="Export game"
79
+ href="link.com"
80
+ />
81
+ <IconButton
82
+ icon="Delete"
83
+ accessibilityLabel="Delete game"
84
+ href="link.com"
85
+ />
86
+ </div>
87
+ </CardsTableCell>
88
+ </CardsTableRow>
89
+
90
+ <CardsTableRow>
91
+ <CardsTableCell>Tekken 8</CardsTableCell>
92
+ <CardsTableCell>
93
+ Bandai Namco Studios Inc. ; Address: 2-37-25 Eitai, Koto-ku, Tokyo
94
+ 135-0034, Japan
95
+ </CardsTableCell>
96
+ <CardsTableCell>namco@fakemail.com</CardsTableCell>
97
+ <CardsTableCell align={Alignment.Right}>79,99 €</CardsTableCell>
98
+ <CardsTableCell className="actions" align={Alignment.Center}>
99
+ <div style={{ display: 'flex', gap: '1.75rem' }}>
100
+ <IconButton
101
+ icon="Edit"
102
+ accessibilityLabel="Edit game"
103
+ href="link.com"
104
+ />
105
+ <IconButton
106
+ icon="Export"
107
+ accessibilityLabel="Export game"
108
+ href="link.com"
109
+ />
110
+ <IconButton
111
+ icon="Delete"
112
+ accessibilityLabel="Delete game"
113
+ href="link.com"
114
+ />
115
+ </div>
116
+ </CardsTableCell>
117
+ </CardsTableRow>
118
+ </CardsTableBody>
119
+ </CardsTable>,
120
+ )
121
+
122
+ expect(getAllByRole('table').length).toBe(1)
123
+ expect(getAllByRole('rowgroup').length).toBe(2)
124
+ expect(getAllByRole('columnheader').length).toBeGreaterThan(1)
125
+ expect(getAllByRole('row').length).toBeGreaterThan(1)
126
+ expect(getAllByRole('cell').length).toBeGreaterThan(1)
127
+ })
128
+ })
@@ -0,0 +1,13 @@
1
+ import { classNames } from '@/utils/classNames'
2
+
3
+ describe('classNames', () => {
4
+ it('returns a string of classes', () => {
5
+ expect(classNames('class1', 'class2')).toBe('class1 class2')
6
+ })
7
+ it('ignores undefined', () => {
8
+ expect(classNames('class1', undefined, 'class2')).toBe('class1 class2')
9
+ })
10
+ it('returns a string of classes with object', () => {
11
+ expect(classNames({ class1: true, class2: false })).toBe('class1')
12
+ })
13
+ })