agroptima-design-system 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ 18.18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agroptima-design-system",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "scripts": {
5
5
  "dev": "npm run storybook",
6
6
  "storybook": "storybook dev -p 6006 --ci",
@@ -5,9 +5,14 @@ export type IconType = keyof typeof icons
5
5
 
6
6
  export interface IconProps extends React.SVGAttributes<HTMLOrSVGElement> {
7
7
  name: IconType
8
+ className?: string
8
9
  }
9
10
 
10
- export const Icon: React.FC<IconProps> = ({ name, ...props }) => {
11
- const cssClasses = ['icon', name === 'Loading' ? 'rotate' : ''].join(' ')
11
+ export const Icon: React.FC<IconProps> = ({ name, className, ...props }) => {
12
+ const cssClasses = [
13
+ 'icon',
14
+ className,
15
+ name === 'Loading' ? 'rotate' : '',
16
+ ].join(' ')
12
17
  return <span className={cssClasses}>{icons[name](props)}</span>
13
18
  }
@@ -11,7 +11,7 @@
11
11
  padding-left: 12px;
12
12
  }
13
13
 
14
- &:has(.input:invalid, .input:required) {
14
+ &:has(.input.invalid) {
15
15
  & .input-help-text {
16
16
  color: color_alias.$error-color-1000;
17
17
  }
@@ -45,7 +45,7 @@
45
45
  border: 1px solid color_alias.$primary-color-1000;
46
46
  }
47
47
 
48
- &:invalid {
48
+ &.invalid {
49
49
  border: 1px solid color_alias.$error-color-1000;
50
50
  }
51
51
 
@@ -61,7 +61,7 @@
61
61
  }
62
62
 
63
63
  .input-help-text {
64
- @include typography.input-help-text;
64
+ @include typography.form-help-text;
65
65
  }
66
66
 
67
67
  .input-label {
@@ -76,8 +76,6 @@
76
76
 
77
77
  .icon {
78
78
  position: absolute;
79
- top: 0.75rem;
80
- left: 0.7rem;
81
79
  width: config.$icon-size-3x;
82
80
  height: config.$icon-size-3x;
83
81
  > svg {
@@ -85,6 +83,16 @@
85
83
  height: 100%;
86
84
  }
87
85
  }
86
+
87
+ .left-icon {
88
+ top: 0.75rem;
89
+ left: 0.7rem;
90
+ }
91
+
92
+ .password-icon {
93
+ top: 0.75rem;
94
+ right: 0.7rem;
95
+ }
88
96
  }
89
97
 
90
98
  .input {
@@ -1,4 +1,5 @@
1
1
  import './Input.scss'
2
+ import React, { useState } from 'react'
2
3
  import { Icon, IconType } from './Icon'
3
4
 
4
5
  export type InputVariant = 'primary'
@@ -10,6 +11,7 @@ export interface InputProps extends React.ComponentPropsWithoutRef<'input'> {
10
11
  helpText?: string
11
12
  variant?: InputVariant
12
13
  id: string
14
+ invalid?: boolean
13
15
  }
14
16
 
15
17
  export function Input({
@@ -22,10 +24,28 @@ export function Input({
22
24
  type = 'text',
23
25
  name,
24
26
  id,
27
+ invalid,
25
28
  ...props
26
29
  }: InputProps): React.JSX.Element {
30
+ const [showPassword, setShowPassword] = useState(false)
27
31
  const iconClass = icon ? 'with-icon' : ''
28
- const cssClasses = ['input', iconClass].join(' ')
32
+ const invalidClass = invalid ? 'invalid' : ''
33
+ const cssClasses = ['input', iconClass, invalidClass].join(' ')
34
+
35
+ function handlePasswordIcon() {
36
+ return showPassword ? 'ShowOff' : 'Show'
37
+ }
38
+
39
+ function handleInputType() {
40
+ if (type !== 'password') return type
41
+
42
+ return showPassword ? 'text' : 'password'
43
+ }
44
+
45
+ function handlePasswordVisibility() {
46
+ setShowPassword(!showPassword)
47
+ }
48
+
29
49
  return (
30
50
  <div className={`input-group ${variant}`}>
31
51
  {!hideLabel && (
@@ -34,16 +54,23 @@ export function Input({
34
54
  </label>
35
55
  )}
36
56
  <div className="input-container">
37
- {icon && <Icon name={icon} />}
57
+ {icon && <Icon className="left-icon" name={icon} />}
38
58
  <input
39
59
  id={id}
40
60
  className={cssClasses}
41
61
  disabled={disabled}
42
- type={type}
62
+ type={handleInputType()}
43
63
  name={name}
44
64
  aria-label={label}
45
65
  {...props}
46
66
  />
67
+ {type === 'password' && (
68
+ <Icon
69
+ className="password-icon"
70
+ name={handlePasswordIcon()}
71
+ onClick={handlePasswordVisibility}
72
+ />
73
+ )}
47
74
  </div>
48
75
  {helpText && <span className="input-help-text">{helpText}</span>}
49
76
  </div>
@@ -0,0 +1,124 @@
1
+ @use '../settings/color_alias';
2
+ @use '../settings/typography';
3
+ @use '../settings/config';
4
+
5
+ .select-group {
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: config.$space-2x;
9
+
10
+ &:has(.selected-option.invalid) {
11
+ & .select-help-text {
12
+ color: color_alias.$error-color-1000;
13
+ }
14
+ }
15
+
16
+ &.primary {
17
+ .select-label {
18
+ @include typography.form-label;
19
+ }
20
+
21
+ .selected-option {
22
+ border-radius: config.$corner-radius-m;
23
+ border: 1px solid color_alias.$neutral-color-300;
24
+ background: color_alias.$neutral-white;
25
+ @include typography.select-text;
26
+
27
+ > .icon {
28
+ > svg {
29
+ fill: color_alias.$primary-color-1000;
30
+ path {
31
+ fill: color_alias.$primary-color-1000;
32
+ }
33
+ }
34
+ }
35
+
36
+ &.filled {
37
+ @include typography.select-option-text;
38
+ }
39
+
40
+ &:focus {
41
+ outline: color_alias.$primary-color-1000;
42
+ border: 1px solid color_alias.$primary-color-1000;
43
+ }
44
+
45
+ &.invalid {
46
+ border: 1px solid color_alias.$error-color-1000;
47
+ }
48
+
49
+ &.disabled {
50
+ border: 1px solid color_alias.$neutral-color-200;
51
+ background: color_alias.$neutral-color-50;
52
+ color: color_alias.$neutral-color-200;
53
+
54
+ > .icon {
55
+ > svg {
56
+ fill: color_alias.$neutral-color-200;
57
+ path {
58
+ fill: color_alias.$neutral-color-200;
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ .select-options {
66
+ border-radius: config.$corner-radius-xxs;
67
+ background: color_alias.$neutral-white;
68
+ box-shadow:
69
+ 0px 9px 28px 8px rgba(0, 0, 0, 0.05),
70
+ 0px 6px 16px 0px rgba(0, 0, 0, 0.08),
71
+ 0px 3px 6px -4px rgba(0, 0, 0, 0.12);
72
+
73
+ .option {
74
+ background: color_alias.$neutral-white;
75
+ @include typography.select-option-text;
76
+
77
+ &:hover {
78
+ background-color: color_alias.$primary-color-50;
79
+ }
80
+ }
81
+ }
82
+
83
+ .select-help-text {
84
+ @include typography.form-help-text;
85
+ }
86
+ }
87
+
88
+ .select-container {
89
+ display: inline-block;
90
+ text-align: left;
91
+ position: relative;
92
+ }
93
+
94
+ .selected-option {
95
+ display: flex;
96
+ justify-content: space-between;
97
+ align-items: center;
98
+ padding: config.$space-2x config.$space-3x;
99
+ cursor: default;
100
+
101
+ > .icon {
102
+ width: config.$icon-size-3x;
103
+ height: config.$icon-size-3x;
104
+ > svg {
105
+ width: 100%;
106
+ height: 100%;
107
+ }
108
+ }
109
+ }
110
+
111
+ .select-options {
112
+ margin: 0;
113
+ padding: config.$space-1x 0rem;
114
+ text-align: left;
115
+ position: absolute;
116
+ width: 100%;
117
+
118
+ .option {
119
+ cursor: default;
120
+ list-style-type: none;
121
+ padding: config.$space-2x config.$space-3x;
122
+ }
123
+ }
124
+ }
@@ -0,0 +1,97 @@
1
+ import './Select.scss'
2
+ import React, { useState } from 'react'
3
+ import { Icon } from './Icon'
4
+
5
+ export type Variant = 'primary'
6
+ export type Option = { id: string; label: string }
7
+
8
+ export interface SelectProps extends React.ComponentPropsWithoutRef<'select'> {
9
+ placeholder?: string
10
+ helpText?: string
11
+ variant?: Variant
12
+ options: Option[]
13
+ invalid?: boolean
14
+ label: string
15
+ }
16
+
17
+ export function Select({
18
+ placeholder,
19
+ helpText,
20
+ variant = 'primary',
21
+ disabled,
22
+ invalid,
23
+ name,
24
+ options,
25
+ label,
26
+ }: SelectProps): React.JSX.Element {
27
+ const [showOptionsList, setShowOptionsList] = useState(false)
28
+ const [selectedOption, setSelectedOption] = useState<Option>({
29
+ id: '',
30
+ label: '',
31
+ })
32
+
33
+ const optionsListOpenClass = showOptionsList ? 'open' : ''
34
+ const filledSelectClass = selectedOption.id ? 'filled' : ''
35
+ const disabledClass = disabled ? 'disabled' : ''
36
+ const invalidClass = invalid ? 'invalid' : ''
37
+
38
+ const cssClasses = [
39
+ 'selected-option',
40
+ optionsListOpenClass,
41
+ filledSelectClass,
42
+ disabledClass,
43
+ invalidClass,
44
+ ].join(' ')
45
+
46
+ function handleOptionsList() {
47
+ if (!disabled) setShowOptionsList(!showOptionsList)
48
+ }
49
+
50
+ function selectOption(option: Option) {
51
+ setSelectedOption(option)
52
+ setShowOptionsList(false)
53
+ }
54
+
55
+ function handleSelectIcon() {
56
+ return showOptionsList ? 'AngleUp' : 'AngleDown'
57
+ }
58
+
59
+ return (
60
+ <div className={`select-group ${variant}`}>
61
+ <span className="select-label">{label}</span>
62
+ <div className="select-container">
63
+ <div
64
+ className={cssClasses}
65
+ tabIndex={0}
66
+ onClick={handleOptionsList}
67
+ aria-label={label}
68
+ aria-live="assertive"
69
+ role="alert"
70
+ >
71
+ <span>{selectedOption.label || placeholder}</span>
72
+ <Icon name={handleSelectIcon()} />
73
+ </div>
74
+ {showOptionsList && (
75
+ <ul className="select-options" role="listbox">
76
+ {options.map((option) => {
77
+ return (
78
+ <li
79
+ className="option"
80
+ role="option"
81
+ aria-selected={selectedOption.id === option.id}
82
+ data-option={option}
83
+ key={option.id}
84
+ onClick={() => selectOption(option)}
85
+ >
86
+ {option.label}
87
+ </li>
88
+ )
89
+ })}
90
+ </ul>
91
+ )}
92
+ </div>
93
+ {helpText && <span className="select-help-text">{helpText}</span>}
94
+ <input type="hidden" name={name} value={selectedOption.id} />
95
+ </div>
96
+ )
97
+ }
@@ -12,6 +12,8 @@ import Export from './export.svg'
12
12
  import Info from './info.svg'
13
13
  import Loading from './loading.svg'
14
14
  import Search from './search.svg'
15
+ import Show from './show.svg'
16
+ import ShowOff from './show-off.svg'
15
17
  import Warning from './warning.svg'
16
18
 
17
19
  export {
@@ -29,5 +31,7 @@ export {
29
31
  Info,
30
32
  Loading,
31
33
  Search,
34
+ Show,
35
+ ShowOff,
32
36
  Warning,
33
37
  }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M10.005 5.579c2.51 0 4.547 2.004 4.547 4.474a4.28 4.28 0 0 1-.327 1.637l2.655 2.613a10.58 10.58 0 0 0 3.12-4.25c-1.573-3.928-5.457-6.71-10.005-6.71-1.273 0-2.492.223-3.62.625L8.34 5.901a4.477 4.477 0 0 1 1.665-.322ZM.91 3.136l2.073 2.04.419.412A10.58 10.58 0 0 0 0 10.053c1.573 3.927 5.457 6.71 10.005 6.71 1.41 0 2.755-.268 3.983-.751l.382.375L17.035 19l1.155-1.136L2.065 2 .91 3.136Zm5.03 4.948 1.409 1.387a2.486 2.486 0 0 0-.073.582c0 1.485 1.219 2.684 2.729 2.684.2 0 .4-.027.59-.072l1.41 1.387a4.568 4.568 0 0 1-2 .474c-2.51 0-4.548-2.004-4.548-4.473 0-.707.182-1.37.482-1.969Zm3.919-.698 2.865 2.819.018-.143c0-1.486-1.219-2.685-2.728-2.685l-.155.01Z" fill="#444"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M10 3C5.455 3 1.573 5.903 0 10c1.573 4.097 5.455 7 10 7 4.546 0 8.427-2.903 10-7-1.573-4.097-5.454-7-10-7Zm0 11.667c-2.51 0-4.545-2.091-4.545-4.667 0-2.576 2.036-4.667 4.545-4.667 2.51 0 4.546 2.091 4.546 4.667 0 2.576-2.037 4.667-4.546 4.667ZM10 7.2c-1.51 0-2.727 1.25-2.727 2.8 0 1.55 1.218 2.8 2.727 2.8 1.51 0 2.727-1.25 2.727-2.8 0-1.55-1.218-2.8-2.727-2.8Z" fill="#444"/></svg>
@@ -5,7 +5,7 @@
5
5
  $font-base-family:
6
6
  Noto Sans,
7
7
  sans-serif;
8
- $text-base-size: 16px;
8
+ $text-base-size: 1rem;
9
9
  $text-base-style: normal;
10
10
  $font-base-weight: 600;
11
11
  $line-height-base: normal;
@@ -14,6 +14,17 @@ $font-base-stretch: semi-condensed;
14
14
  $font-primary: $font-base-stretch $text-base-style $font-base-weight #{$text-base-size}/#{$line-height-base}
15
15
  $font-base-family;
16
16
 
17
+ @mixin form-help-text {
18
+ font-style: $text-base-style;
19
+ font-variant: $text-base-style;
20
+ font-weight: 300;
21
+ font-family: $font-base-family;
22
+ color: color_alias.$neutral-color-400;
23
+ // We need to apply a relation between font-size and line-height so the box envoloping the span
24
+ // is the most accurate in terms of top and bottom blank space.
25
+ font-size: 0.75rem;
26
+ line-height: 0.6875rem;
27
+ }
17
28
  @mixin input-text {
18
29
  font-style: $text-base-style;
19
30
  font-variant: $text-base-style;
@@ -29,16 +40,21 @@ $font-primary: $font-base-stretch $text-base-style $font-base-weight #{$text-bas
29
40
  color: color_alias.$neutral-color-300;
30
41
  // We shouldn't define a font-size for the placeholder: https://stackoverflow.com/questions/30916387/placeholder-font-size-bigger-than-16px
31
42
  }
32
- @mixin input-help-text {
43
+ @mixin select-text {
33
44
  font-style: $text-base-style;
34
45
  font-variant: $text-base-style;
35
- font-weight: 300;
46
+ font-weight: 400;
36
47
  font-family: $font-base-family;
37
- color: color_alias.$neutral-color-400;
38
- // We need to apply a relation between font-size and line-height so the box envoloping the span
39
- // is the most accurate in terms of top and bottom blank space. We'll check this with rem units.
40
- font-size: 12px;
41
- line-height: 11px;
48
+ font-size: 1em;
49
+ color: color_alias.$neutral-color-300;
50
+ }
51
+ @mixin select-option-text {
52
+ font-style: $text-base-style;
53
+ font-variant: $text-base-style;
54
+ font-weight: 400;
55
+ font-family: $font-base-family;
56
+ font-size: 1em;
57
+ color: color_alias.$neutral-color-1000;
42
58
  }
43
59
  @mixin form-label {
44
60
  font-style: $text-base-style;
@@ -59,3 +59,17 @@ export const Primary: Story = {
59
59
  },
60
60
  parameters: figmaPrimaryDesign,
61
61
  }
62
+
63
+ export const Password: Story = {
64
+ args: {
65
+ label: 'Password:',
66
+ placeholder: 'Password...',
67
+ variant: 'primary',
68
+ disabled: false,
69
+ helpText: 'This text can help you',
70
+ name: 'login_password',
71
+ type: 'password',
72
+ id: 'password_input',
73
+ },
74
+ parameters: figmaPrimaryDesign,
75
+ }
@@ -0,0 +1,62 @@
1
+ import { Select } from '../atoms/Select'
2
+ import { StoryObj } from '@storybook/react'
3
+
4
+ const meta = {
5
+ title: 'Design System/Atoms/Select',
6
+ component: Select,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ label: {
10
+ description: 'Label for the select',
11
+ },
12
+ variant: {
13
+ description: 'Select variant used',
14
+ },
15
+ disabled: {
16
+ description: 'Is the select in disabled state?',
17
+ },
18
+ required: {
19
+ description: 'Is the select in required state?',
20
+ },
21
+ helpText: {
22
+ description: 'Optional help text',
23
+ },
24
+ name: {
25
+ description: 'Set name property',
26
+ },
27
+ placeholder: {
28
+ description: 'Set select placeholder text',
29
+ },
30
+ options: {
31
+ description: 'Array of values to be displayed on the select list',
32
+ },
33
+ },
34
+ }
35
+
36
+ const figmaPrimaryDesign = {
37
+ design: {
38
+ type: 'figma',
39
+ url: 'https://www.figma.com/file/DN2ova21vWqCRvPspBXgI1/Design-System?type=design&node-id=188-1956&mode=dev',
40
+ },
41
+ }
42
+
43
+ export default meta
44
+ type Story = StoryObj<typeof meta>
45
+
46
+ export const Primary: Story = {
47
+ args: {
48
+ variant: 'primary',
49
+ disabled: false,
50
+ required: false,
51
+ helpText: 'This text can help you',
52
+ name: 'example',
53
+ label: 'Videogames',
54
+ placeholder: 'Select your favourite gaming system...',
55
+ options: [
56
+ { id: '1', label: 'Nintendo Switch' },
57
+ { id: '2', label: 'PlayStation 5' },
58
+ { id: '3', label: 'Xbox Series S/X' },
59
+ ],
60
+ },
61
+ parameters: figmaPrimaryDesign,
62
+ }