@donkit-ai/design-system 0.2.17 → 0.2.19

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/README.md CHANGED
@@ -37,12 +37,16 @@ npm install @donkit-ai/design-system@latest
37
37
 
38
38
  ## Компоненты
39
39
 
40
- - **Button** - кнопки с вариантами (primary, secondary, ghost) и размерами (small, medium, large). Поддерживает `href` для рендера как ссылка
41
- - **Tabs** - вкладки с состоянием selected в трех размерах (small, medium, large). Поддерживает `href` для рендера как ссылка
42
- - **Input** - текстовые поля с поддержкой иконок, ошибок и подсказок (small, medium)
43
- - **Stepper** - числовое поле с кнопками +/- для изменения значения (small, medium)
44
- - **Toggle** - переключатель для включения/выключения опций (small, medium)
45
- - **Card** - карточки двух типов: info (информационная, прозрачный фон) и interactive (интерактивная с hover эффектом)
40
+ - **Button** - кнопки с вариантами (primary, secondary, ghost) и размерами (xs, small, medium, large). Поддерживает `href` для рендера как ссылка
41
+ - **Tabs** - вкладки с состоянием selected в четырех размерах (xs, small, medium, large). Поддерживает `href` для рендера как ссылка
42
+ - **Input** - текстовые поля с поддержкой иконок, ошибок и подсказок (xs, small, medium)
43
+ - **Textarea** - многострочное текстовое поле (xs, small, medium)
44
+ - **Select** - выпадающий список с кастомным дизайном (xs, small, medium)
45
+ - **Stepper** - числовое поле с кнопками +/- для изменения значения (xs, small, medium)
46
+ - **Toggle** - переключатель для включения/выключения опций (xs, small, medium, large)
47
+ - **Checkbox** - чекбокс для выбора опций (xs, small, medium, large)
48
+ - **Radio** - радиокнопка для выбора одного варианта из группы (xs, small, medium, large)
49
+ - **Card** - карточки двух типов: info (информационная, прозрачный фон) и interactive (интерактивная с hover эффектом). Поддерживает `href` для рендера как ссылка
46
50
  - **Typography** - H1-H4, P1-P3 компоненты
47
51
  - **Code** - inline и block код с monospace шрифтом
48
52
  - **Link** - ссылки акцентным цветом, при hover цвет меняется и появляется подчеркивание
@@ -112,20 +116,25 @@ import { iconSizes } from '@donkit-ai/design-system';
112
116
  **Соответствие размеров компонентам:**
113
117
 
114
118
  - **16px (xs)** - очень мелкие элементы
119
+ - Extra Small кнопки (`size="xs"`)
120
+ - Extra Small табы (`size="xs"`)
121
+
115
122
  - **20px (s)** - компактные элементы
116
123
  - Small кнопки (`size="small"`)
117
- - Tabs (вкладки)
124
+ - Small табы (`size="small"`)
118
125
  - Modal (иконка закрытия)
119
126
  - Accordion, CodeAccordion
120
127
 
121
128
  - **24px (m)** - стандартные элементы
122
129
  - Medium кнопки (`size="medium"`, по умолчанию)
130
+ - Medium табы (`size="medium"`)
123
131
  - Input (иконки в полях ввода)
124
132
  - Alert (иконки статусов)
125
133
  - Select (иконка выпадающего списка)
126
134
 
127
135
  - **28px (l)** - крупные элементы
128
136
  - Large кнопки (`size="large"`)
137
+ - Large табы (`size="large"`)
129
138
 
130
139
  - **48px (xl)** - очень крупные элементы
131
140
 
@@ -333,6 +342,9 @@ document.documentElement.setAttribute('data-theme', 'light');
333
342
  <Button variant="ghost">Ghost</Button>
334
343
 
335
344
  // Размеры с соответствующими иконками
345
+ <Button size="xs" icon={<Mail size={16} strokeWidth={1.5} />}>
346
+ Extra Small
347
+ </Button>
336
348
  <Button size="small" icon={<Mail size={20} strokeWidth={1.5} />}>
337
349
  Small
338
350
  </Button>
@@ -375,7 +387,8 @@ import { AlertCircle } from 'lucide-react';
375
387
  </Tab>
376
388
  </Tabs>
377
389
 
378
- // With icons (всегда 20px для Tabs)
390
+ // With icons (размер иконки соответствует размеру таба)
391
+ // xs: 16px, small: 20px, medium: 24px, large: 28px
379
392
  <Tabs size="small">
380
393
  <Tab
381
394
  selected={selectedTab === 'alerts'}
@@ -416,7 +429,7 @@ import { AlertCircle } from 'lucide-react';
416
429
  - Hover: фон `--color-item-bg-hover`, текст `--color-txt-icon-1`
417
430
  - Default: текст `--color-txt-icon-2`
418
431
 
419
- **Размеры:** small, medium, large
432
+ **Размеры:** xs, small, medium, large
420
433
 
421
434
  **Дополнительно:** поддержка иконок и disabled состояния
422
435
 
@@ -550,7 +563,7 @@ import { AlertCircle } from 'lucide-react';
550
563
 
551
564
  ### Toggle
552
565
 
553
- Переключатель для включения/выключения опций. Высота совпадает с другими элементами интерфейса.
566
+ Переключатель для включения/выключения опций. Размеры привязаны к размерам иконок.
554
567
 
555
568
  ```jsx
556
569
  // Basic toggle
@@ -560,7 +573,15 @@ import { AlertCircle } from 'lucide-react';
560
573
  label="Enable notifications"
561
574
  />
562
575
 
563
- // Small size
576
+ // Extra Small size (16px)
577
+ <Toggle
578
+ size="xs"
579
+ checked={isCompact}
580
+ onChange={setIsCompact}
581
+ label="Compact mode"
582
+ />
583
+
584
+ // Small size (20px)
564
585
  <Toggle
565
586
  size="small"
566
587
  checked={isEnabled}
@@ -568,7 +589,7 @@ import { AlertCircle } from 'lucide-react';
568
589
  label="Auto-save"
569
590
  />
570
591
 
571
- // Medium size (default)
592
+ // Medium size (24px, default)
572
593
  <Toggle
573
594
  size="medium"
574
595
  checked={darkMode}
@@ -576,6 +597,14 @@ import { AlertCircle } from 'lucide-react';
576
597
  label="Dark mode"
577
598
  />
578
599
 
600
+ // Large size (28px)
601
+ <Toggle
602
+ size="large"
603
+ checked={isEnabled}
604
+ onChange={setIsEnabled}
605
+ label="Enable feature"
606
+ />
607
+
579
608
  // Without label (for tables/cards where context is clear)
580
609
  <Toggle
581
610
  checked={isEnabled}
@@ -601,17 +630,220 @@ import { AlertCircle } from 'lucide-react';
601
630
  **Параметры:**
602
631
  - `checked` - состояние переключателя (true/false)
603
632
  - `onChange` - функция обработки изменения состояния, получает новое значение (boolean)
604
- - `size` - размер: "small" или "medium" (default: "medium")
633
+ - `size` - размер: "xs", "small", "medium" или "large" (default: "medium")
605
634
  - `label` - текст подписи (опционально)
606
635
  - `disabled` - отключить переключатель
607
636
  - `id` - пользовательский ID (по умолчанию генерируется автоматически)
608
637
 
609
638
  **Стиль:**
610
- - Высота совпадает с Input, Select, Stepper (`--height-s` / `--height-m`)
639
+ - Размеры привязаны к иконкам: xs (16px), small (20px), medium (24px), large (28px)
611
640
  - Ширина трека: 1.75x от высоты
612
641
  - Включенное состояние: фон `--color-status-success`, border `--color-status-success` (зеленый)
613
642
  - Выключенное состояние: фон `--color-item-bg`, border `--color-border`, hover `--color-border-hover`
614
643
 
644
+ ### Checkbox
645
+
646
+ Чекбокс для выбора опций. Размеры привязаны к размерам иконок.
647
+
648
+ ```jsx
649
+ // Basic checkbox
650
+ <Checkbox
651
+ checked={isAccepted}
652
+ onChange={setIsAccepted}
653
+ label="Accept terms and conditions"
654
+ />
655
+
656
+ // Extra Small size (16px)
657
+ <Checkbox
658
+ size="xs"
659
+ checked={isCompact}
660
+ onChange={setIsCompact}
661
+ label="Compact view"
662
+ />
663
+
664
+ // Small size (20px)
665
+ <Checkbox
666
+ size="small"
667
+ checked={isEnabled}
668
+ onChange={setIsEnabled}
669
+ label="Enable feature"
670
+ />
671
+
672
+ // Medium size (24px, default)
673
+ <Checkbox
674
+ size="medium"
675
+ checked={isSubscribed}
676
+ onChange={setIsSubscribed}
677
+ label="Subscribe to newsletter"
678
+ />
679
+
680
+ // Large size (28px)
681
+ <Checkbox
682
+ size="large"
683
+ checked={isAccepted}
684
+ onChange={setIsAccepted}
685
+ label="I accept the terms"
686
+ />
687
+
688
+ // Without label (for tables/lists where context is clear)
689
+ <Checkbox
690
+ checked={isSelected}
691
+ onChange={setIsSelected}
692
+ />
693
+
694
+ // Disabled states
695
+ <Checkbox
696
+ checked={false}
697
+ onChange={() => {}}
698
+ label="Disabled unchecked"
699
+ disabled
700
+ />
701
+
702
+ <Checkbox
703
+ checked={true}
704
+ onChange={() => {}}
705
+ label="Disabled checked"
706
+ disabled
707
+ />
708
+ ```
709
+
710
+ **Параметры:**
711
+ - `checked` - состояние чекбокса (true/false)
712
+ - `onChange` - функция обработки изменения состояния, получает новое значение (boolean)
713
+ - `size` - размер: "xs", "small", "medium" или "large" (default: "medium")
714
+ - `label` - текст подписи (опционально)
715
+ - `disabled` - отключить чекбокс
716
+ - `id` - пользовательский ID (по умолчанию генерируется автоматически)
717
+
718
+ **Стиль:**
719
+ - Размеры привязаны к иконкам: xs (16px), small (20px), medium (24px), large (28px)
720
+ - Выбранное состояние: фон `--color-status-success`, border `--color-status-success` (зеленый)
721
+ - Невыбранное состояние: фон `--color-item-bg`, border `--color-border`, hover `--color-border-hover`
722
+ - Иконка галочки (Check) белого цвета при checked
723
+
724
+ ### Radio
725
+
726
+ Радиокнопка для выбора одного варианта из группы. Размеры привязаны к размерам иконок.
727
+
728
+ ```jsx
729
+ // Basic radio
730
+ <Radio
731
+ checked={selectedOption === 'option1'}
732
+ onChange={() => setSelectedOption('option1')}
733
+ name="options"
734
+ value="option1"
735
+ label="Option 1"
736
+ />
737
+
738
+ // Extra Small size (16px)
739
+ <Radio
740
+ size="xs"
741
+ checked={size === 'xs'}
742
+ onChange={() => setSize('xs')}
743
+ name="size"
744
+ value="xs"
745
+ label="Extra Small"
746
+ />
747
+
748
+ // Small size (20px)
749
+ <Radio
750
+ size="small"
751
+ checked={size === 'small'}
752
+ onChange={() => setSize('small')}
753
+ name="size"
754
+ value="small"
755
+ label="Small"
756
+ />
757
+
758
+ // Medium size (24px, default)
759
+ <Radio
760
+ size="medium"
761
+ checked={size === 'medium'}
762
+ onChange={() => setSize('medium')}
763
+ name="size"
764
+ value="medium"
765
+ label="Medium"
766
+ />
767
+
768
+ // Large size (28px)
769
+ <Radio
770
+ size="large"
771
+ checked={size === 'large'}
772
+ onChange={() => setSize('large')}
773
+ name="size"
774
+ value="large"
775
+ label="Large"
776
+ />
777
+
778
+ // Radio group example
779
+ <div>
780
+ <Radio
781
+ checked={paymentMethod === 'card'}
782
+ onChange={() => setPaymentMethod('card')}
783
+ name="payment"
784
+ value="card"
785
+ label="Credit Card"
786
+ />
787
+ <Radio
788
+ checked={paymentMethod === 'paypal'}
789
+ onChange={() => setPaymentMethod('paypal')}
790
+ name="payment"
791
+ value="paypal"
792
+ label="PayPal"
793
+ />
794
+ <Radio
795
+ checked={paymentMethod === 'bank'}
796
+ onChange={() => setPaymentMethod('bank')}
797
+ name="payment"
798
+ value="bank"
799
+ label="Bank Transfer"
800
+ />
801
+ </div>
802
+
803
+ // Without label (for tables/lists where context is clear)
804
+ <Radio
805
+ checked={isSelected}
806
+ onChange={() => setIsSelected(true)}
807
+ name="selection"
808
+ value="item1"
809
+ />
810
+
811
+ // Disabled states
812
+ <Radio
813
+ checked={false}
814
+ onChange={() => {}}
815
+ name="disabled-group"
816
+ value="disabled1"
817
+ label="Disabled unchecked"
818
+ disabled
819
+ />
820
+
821
+ <Radio
822
+ checked={true}
823
+ onChange={() => {}}
824
+ name="disabled-group"
825
+ value="disabled2"
826
+ label="Disabled checked"
827
+ disabled
828
+ />
829
+ ```
830
+
831
+ **Параметры:**
832
+ - `checked` - состояние радиокнопки (true/false)
833
+ - `onChange` - функция обработки изменения состояния, получает новое значение (boolean)
834
+ - `name` - имя группы радиокнопок (обязательно для группировки)
835
+ - `value` - значение радиокнопки
836
+ - `size` - размер: "xs", "small", "medium" или "large" (default: "medium")
837
+ - `label` - текст подписи (опционально)
838
+ - `disabled` - отключить радиокнопку
839
+ - `id` - пользовательский ID (по умолчанию генерируется автоматически)
840
+
841
+ **Стиль:**
842
+ - Размеры привязаны к иконкам: xs (16px), small (20px), medium (24px), large (28px)
843
+ - Выбранное состояние: фон `--color-status-success`, border `--color-status-success` (зеленый), белая точка внутри
844
+ - Невыбранное состояние: фон `--color-item-bg`, border `--color-border`, hover `--color-border-hover`
845
+ - Круглая форма (border-radius: 50%)
846
+
615
847
  ### Card
616
848
 
617
849
  Карточки бывают двух типов:
@@ -637,11 +869,45 @@ import { AlertCircle } from 'lucide-react';
637
869
  <Card padding="small">Small padding</Card>
638
870
  <Card padding="medium">Medium padding</Card>
639
871
  <Card padding="large">Large padding</Card>
872
+
873
+ // Card как ссылка (рендерит <a> вместо <div>)
874
+ // При обычном клике: вызывается onClick (preventDefault автоматический)
875
+ // При Cmd/Ctrl+Click или Middle Click: открывается новая вкладка
876
+ <Card
877
+ padding="medium"
878
+ variant="interactive"
879
+ href="/dashboard"
880
+ onClick={() => navigate('/dashboard')}
881
+ >
882
+ <H4>Dashboard</H4>
883
+ <P1>Go to dashboard page</P1>
884
+ </Card>
885
+
886
+ <Card
887
+ padding="medium"
888
+ variant="interactive"
889
+ href="https://example.com"
890
+ >
891
+ <H4>External Link</H4>
892
+ <P1>Visit external site</P1>
893
+ </Card>
894
+
895
+ // Disabled Card Link
896
+ <Card
897
+ padding="medium"
898
+ variant="interactive"
899
+ href="/disabled"
900
+ disabled
901
+ >
902
+ <H4>Disabled</H4>
903
+ <P1>This card is disabled</P1>
904
+ </Card>
640
905
  ```
641
906
 
642
907
  **Стиль:**
643
908
  - **variant="info"** (по умолчанию): прозрачный фон, border `--color-border`
644
- - **variant="interactive"**: фон `--color-item-bg`, при hover фон `--color-item-bg-hover` и border `--color-border-hover`, курсор pointer
909
+ - **variant="interactive"**: фон `--color-item-bg`, при hover фон `--color-item-bg-hover`, курсор pointer
910
+ - Поддержка `href` для рендера как ссылка с умной обработкой кликов
645
911
 
646
912
  ### Code
647
913
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkit-ai/design-system",
3
- "version": "0.2.17",
3
+ "version": "0.2.19",
4
4
  "description": "Donkit Design System - minimal design tokens and React components",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -61,6 +61,13 @@
61
61
  }
62
62
 
63
63
  /* Sizes */
64
+ .ds-button--xs {
65
+ height: var(--height-xs);
66
+ padding: 0 calc(var(--height-xs) / 2);
67
+ font-size: var(--font-size-p3);
68
+ border-radius: var(--radius-xs);
69
+ }
70
+
64
71
  .ds-button--small {
65
72
  height: var(--height-s);
66
73
  padding: 0 calc(var(--height-s) / 2);
@@ -3,6 +3,8 @@
3
3
  border-radius: var(--radius-s);
4
4
  border: 1px solid var(--color-border);
5
5
  transition: border-color var(--transition-normal), background-color var(--transition-normal);
6
+ text-decoration: none;
7
+ color: inherit;
6
8
  }
7
9
 
8
10
  .ds-card--interactive {
@@ -11,10 +13,16 @@
11
13
  border: none;
12
14
  }
13
15
 
14
- .ds-card--interactive:hover {
16
+ .ds-card--interactive:hover:not([aria-disabled="true"]) {
15
17
  background-color: var(--color-item-bg-hover);
16
18
  }
17
19
 
20
+ .ds-card[aria-disabled="true"] {
21
+ opacity: 0.5;
22
+ cursor: not-allowed;
23
+ pointer-events: none;
24
+ }
25
+
18
26
  .ds-card--none {
19
27
  padding: 0;
20
28
  }
@@ -7,10 +7,12 @@ export function Card({
7
7
  variant = 'info',
8
8
  hover = false, // deprecated, use variant="interactive"
9
9
  onClick,
10
+ href,
11
+ disabled = false,
10
12
  ...props
11
13
  }) {
12
14
  const cardVariant = hover ? 'interactive' : variant;
13
- const isInteractive = cardVariant === 'interactive' || onClick;
15
+ const isInteractive = cardVariant === 'interactive' || onClick || href;
14
16
 
15
17
  const className = [
16
18
  'ds-card',
@@ -18,10 +20,41 @@ export function Card({
18
20
  isInteractive && 'ds-card--interactive',
19
21
  ].filter(Boolean).join(' ');
20
22
 
23
+ // Render as link if href is provided
24
+ if (href) {
25
+ const handleClick = (e) => {
26
+ if (disabled) {
27
+ e.preventDefault();
28
+ return;
29
+ }
30
+ // При Cmd/Ctrl+Click или Middle Click — пусть браузер откроет новую вкладку
31
+ if (e.metaKey || e.ctrlKey || e.button === 1) {
32
+ return;
33
+ }
34
+ // Обычный клик — preventDefault и вызов onClick
35
+ e.preventDefault();
36
+ onClick?.(e);
37
+ };
38
+
39
+ return (
40
+ <a
41
+ className={className}
42
+ href={disabled ? undefined : href}
43
+ onClick={handleClick}
44
+ aria-disabled={disabled ? 'true' : undefined}
45
+ {...props}
46
+ >
47
+ {children}
48
+ </a>
49
+ );
50
+ }
51
+
52
+ // Render as button if interactive with onClick
21
53
  const Element = isInteractive && onClick ? 'button' : 'div';
22
54
  const interactiveProps = isInteractive && onClick ? {
23
55
  type: 'button',
24
56
  onClick,
57
+ disabled,
25
58
  } : {};
26
59
 
27
60
  return (
@@ -0,0 +1,88 @@
1
+ .ds-checkbox {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: var(--space-s);
5
+ cursor: pointer;
6
+ }
7
+
8
+ .ds-checkbox--disabled {
9
+ opacity: 0.5;
10
+ cursor: not-allowed;
11
+ }
12
+
13
+ .ds-checkbox__input {
14
+ position: absolute;
15
+ opacity: 0;
16
+ pointer-events: none;
17
+ }
18
+
19
+ .ds-checkbox__box {
20
+ position: relative;
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ background-color: var(--color-item-bg);
25
+ border: 1px solid var(--color-border);
26
+ transition: background-color var(--transition-normal), border-color var(--transition-normal);
27
+ flex-shrink: 0;
28
+ border-radius: 4px;
29
+ }
30
+
31
+ .ds-checkbox__input:checked + .ds-checkbox__box {
32
+ background-color: var(--color-status-success);
33
+ border-color: var(--color-status-success);
34
+ }
35
+
36
+ .ds-checkbox__input:not(:checked) + .ds-checkbox__box:hover {
37
+ border-color: var(--color-border-hover);
38
+ }
39
+
40
+ .ds-checkbox__icon {
41
+ color: var(--color-white);
42
+ }
43
+
44
+ .ds-checkbox__label {
45
+ font-size: var(--font-size-p1);
46
+ color: var(--color-txt-icon-1);
47
+ user-select: none;
48
+ }
49
+
50
+ /* Extra Small */
51
+ .ds-checkbox--xs .ds-checkbox__box {
52
+ width: var(--icon-xs);
53
+ height: var(--icon-xs);
54
+ }
55
+
56
+ .ds-checkbox--xs .ds-checkbox__label {
57
+ font-size: var(--font-size-p3);
58
+ }
59
+
60
+ /* Small */
61
+ .ds-checkbox--small .ds-checkbox__box {
62
+ width: var(--icon-s);
63
+ height: var(--icon-s);
64
+ }
65
+
66
+ .ds-checkbox--small .ds-checkbox__label {
67
+ font-size: var(--font-size-p2);
68
+ }
69
+
70
+ /* Medium */
71
+ .ds-checkbox--medium .ds-checkbox__box {
72
+ width: var(--icon-m);
73
+ height: var(--icon-m);
74
+ }
75
+
76
+ .ds-checkbox--medium .ds-checkbox__label {
77
+ font-size: var(--font-size-p1);
78
+ }
79
+
80
+ /* Large */
81
+ .ds-checkbox--large .ds-checkbox__box {
82
+ width: var(--icon-l);
83
+ height: var(--icon-l);
84
+ }
85
+
86
+ .ds-checkbox--large .ds-checkbox__label {
87
+ font-size: var(--font-size-p1);
88
+ }
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import { Check } from 'lucide-react';
3
+ import './Checkbox.css';
4
+
5
+ export function Checkbox({
6
+ checked = false,
7
+ onChange,
8
+ size = 'medium',
9
+ disabled = false,
10
+ label,
11
+ id,
12
+ ...props
13
+ }) {
14
+ const checkboxId = id || `checkbox-${React.useId()}`;
15
+
16
+ const className = [
17
+ 'ds-checkbox',
18
+ `ds-checkbox--${size}`,
19
+ disabled && 'ds-checkbox--disabled',
20
+ ].filter(Boolean).join(' ');
21
+
22
+ const iconSize = size === 'xs' ? 10 : size === 'small' ? 14 : size === 'large' ? 20 : 16;
23
+
24
+ return (
25
+ <label className={className} htmlFor={checkboxId}>
26
+ <input
27
+ type="checkbox"
28
+ id={checkboxId}
29
+ className="ds-checkbox__input"
30
+ checked={checked}
31
+ onChange={(e) => onChange?.(e.target.checked)}
32
+ disabled={disabled}
33
+ {...props}
34
+ />
35
+ <span className="ds-checkbox__box">
36
+ {checked && (
37
+ <Check
38
+ size={iconSize}
39
+ strokeWidth={2.5}
40
+ className="ds-checkbox__icon"
41
+ />
42
+ )}
43
+ </span>
44
+ {label && <span className="ds-checkbox__label">{label}</span>}
45
+ </label>
46
+ );
47
+ }
@@ -58,6 +58,13 @@
58
58
  }
59
59
 
60
60
  /* Sizes */
61
+ .ds-input--xs {
62
+ height: var(--height-xs);
63
+ padding: 0 calc(var(--height-xs) / 4);
64
+ font-size: var(--font-size-p3);
65
+ border-radius: var(--radius-xs);
66
+ }
67
+
61
68
  .ds-input--small {
62
69
  height: var(--height-s);
63
70
  padding: 0 calc(var(--height-s) / 4);
@@ -72,6 +79,10 @@
72
79
  border-radius: var(--radius-s);
73
80
  }
74
81
 
82
+ .ds-input--with-icon.ds-input--xs {
83
+ padding-left: calc(var(--height-xs) / 4 + 16px + var(--height-xs) / 4);
84
+ }
85
+
75
86
  .ds-input--with-icon.ds-input--small {
76
87
  padding-left: calc(var(--height-s) / 4 + 20px + var(--height-s) / 4);
77
88
  }
@@ -80,6 +91,10 @@
80
91
  padding-left: calc(var(--height-m) / 4 + 24px + var(--height-m) / 4);
81
92
  }
82
93
 
94
+ .ds-input--with-icon-right.ds-input--xs {
95
+ padding-right: calc(var(--height-xs) / 4 + 16px + var(--height-xs) / 4);
96
+ }
97
+
83
98
  .ds-input--with-icon-right.ds-input--small {
84
99
  padding-right: calc(var(--height-s) / 4 + 20px + var(--height-s) / 4);
85
100
  }
@@ -96,6 +111,10 @@
96
111
  pointer-events: none;
97
112
  }
98
113
 
114
+ .ds-input-icon--xs {
115
+ left: 6px;
116
+ }
117
+
99
118
  .ds-input-icon--small {
100
119
  left: 8px;
101
120
  }
@@ -121,6 +140,10 @@
121
140
  color: var(--color-txt-icon-1);
122
141
  }
123
142
 
143
+ .ds-input-icon-right--xs {
144
+ right: 6px;
145
+ }
146
+
124
147
  .ds-input-icon-right--small {
125
148
  right: 8px;
126
149
  }
@@ -0,0 +1,115 @@
1
+ .ds-radio {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: var(--space-s);
5
+ cursor: pointer;
6
+ }
7
+
8
+ .ds-radio--disabled {
9
+ opacity: 0.5;
10
+ cursor: not-allowed;
11
+ }
12
+
13
+ .ds-radio__input {
14
+ position: absolute;
15
+ opacity: 0;
16
+ pointer-events: none;
17
+ }
18
+
19
+ .ds-radio__circle {
20
+ position: relative;
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ background-color: var(--color-item-bg);
25
+ border: 1px solid var(--color-border);
26
+ transition: background-color var(--transition-normal), border-color var(--transition-normal);
27
+ flex-shrink: 0;
28
+ border-radius: 50%;
29
+ }
30
+
31
+ .ds-radio__input:checked + .ds-radio__circle {
32
+ background-color: var(--color-status-success);
33
+ border-color: var(--color-status-success);
34
+ }
35
+
36
+ .ds-radio__input:not(:checked) + .ds-radio__circle:hover {
37
+ border-color: var(--color-border-hover);
38
+ }
39
+
40
+ .ds-radio__dot {
41
+ background-color: var(--color-white);
42
+ border-radius: 50%;
43
+ opacity: 0;
44
+ transition: opacity var(--transition-normal);
45
+ }
46
+
47
+ .ds-radio__input:checked + .ds-radio__circle .ds-radio__dot {
48
+ opacity: 1;
49
+ }
50
+
51
+ .ds-radio__label {
52
+ font-size: var(--font-size-p1);
53
+ color: var(--color-txt-icon-1);
54
+ user-select: none;
55
+ }
56
+
57
+ /* Extra Small */
58
+ .ds-radio--xs .ds-radio__circle {
59
+ width: var(--icon-xs);
60
+ height: var(--icon-xs);
61
+ }
62
+
63
+ .ds-radio--xs .ds-radio__dot {
64
+ width: 6px;
65
+ height: 6px;
66
+ }
67
+
68
+ .ds-radio--xs .ds-radio__label {
69
+ font-size: var(--font-size-p3);
70
+ }
71
+
72
+ /* Small */
73
+ .ds-radio--small .ds-radio__circle {
74
+ width: var(--icon-s);
75
+ height: var(--icon-s);
76
+ }
77
+
78
+ .ds-radio--small .ds-radio__dot {
79
+ width: 8px;
80
+ height: 8px;
81
+ }
82
+
83
+ .ds-radio--small .ds-radio__label {
84
+ font-size: var(--font-size-p2);
85
+ }
86
+
87
+ /* Medium */
88
+ .ds-radio--medium .ds-radio__circle {
89
+ width: var(--icon-m);
90
+ height: var(--icon-m);
91
+ }
92
+
93
+ .ds-radio--medium .ds-radio__dot {
94
+ width: 10px;
95
+ height: 10px;
96
+ }
97
+
98
+ .ds-radio--medium .ds-radio__label {
99
+ font-size: var(--font-size-p1);
100
+ }
101
+
102
+ /* Large */
103
+ .ds-radio--large .ds-radio__circle {
104
+ width: var(--icon-l);
105
+ height: var(--icon-l);
106
+ }
107
+
108
+ .ds-radio--large .ds-radio__dot {
109
+ width: 12px;
110
+ height: 12px;
111
+ }
112
+
113
+ .ds-radio--large .ds-radio__label {
114
+ font-size: var(--font-size-p1);
115
+ }
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import './Radio.css';
3
+
4
+ export function Radio({
5
+ checked = false,
6
+ onChange,
7
+ size = 'medium',
8
+ disabled = false,
9
+ label,
10
+ name,
11
+ value,
12
+ id,
13
+ ...props
14
+ }) {
15
+ const radioId = id || `radio-${React.useId()}`;
16
+
17
+ const className = [
18
+ 'ds-radio',
19
+ `ds-radio--${size}`,
20
+ disabled && 'ds-radio--disabled',
21
+ ].filter(Boolean).join(' ');
22
+
23
+ return (
24
+ <label className={className} htmlFor={radioId}>
25
+ <input
26
+ type="radio"
27
+ id={radioId}
28
+ className="ds-radio__input"
29
+ checked={checked}
30
+ onChange={(e) => onChange?.(e.target.checked)}
31
+ disabled={disabled}
32
+ name={name}
33
+ value={value}
34
+ {...props}
35
+ />
36
+ <span className="ds-radio__circle">
37
+ <span className="ds-radio__dot" />
38
+ </span>
39
+ {label && <span className="ds-radio__label">{label}</span>}
40
+ </label>
41
+ );
42
+ }
@@ -60,6 +60,13 @@
60
60
  }
61
61
 
62
62
  /* Sizes */
63
+ .ds-select-trigger--xs {
64
+ height: var(--height-xs);
65
+ padding: 0 calc(var(--height-xs) / 4);
66
+ font-size: var(--font-size-p3);
67
+ border-radius: var(--radius-xs);
68
+ }
69
+
63
70
  .ds-select-trigger--small {
64
71
  height: var(--height-s);
65
72
  padding: 0 calc(var(--height-s) / 4);
@@ -53,7 +53,7 @@ export function Select({
53
53
  }, [isOpen]);
54
54
 
55
55
  const selectedOption = options.find(opt => opt.value === value);
56
- const iconSize = size === 'small' ? iconSizes.s : iconSizes.m;
56
+ const iconSize = size === 'xs' ? iconSizes.xs : size === 'small' ? iconSizes.s : iconSizes.m;
57
57
 
58
58
  return (
59
59
  <div className={`ds-select-wrapper ${fullWidth ? 'ds-select-wrapper--full' : ''} ${disabled ? 'ds-select-wrapper--disabled' : ''}`}>
@@ -98,6 +98,22 @@
98
98
 
99
99
  /* Sizes */
100
100
 
101
+ /* Extra Small */
102
+ .ds-stepper--xs {
103
+ height: var(--height-xs);
104
+ }
105
+
106
+ .ds-stepper--xs .ds-stepper-button {
107
+ width: var(--height-xs);
108
+ padding: 0;
109
+ }
110
+
111
+ .ds-stepper--xs .ds-stepper-input {
112
+ font-size: var(--font-size-p3);
113
+ letter-spacing: var(--letter-spacing-p3);
114
+ padding: 0 var(--space-xs);
115
+ }
116
+
101
117
  /* Small */
102
118
  .ds-stepper--small {
103
119
  height: var(--height-s);
@@ -57,7 +57,7 @@ export function Stepper({
57
57
  disabled && 'ds-stepper--disabled',
58
58
  ].filter(Boolean).join(' ');
59
59
 
60
- const iconSize = size === 'small' ? iconSizes.s : iconSizes.m;
60
+ const iconSize = size === 'xs' ? iconSizes.xs : size === 'small' ? iconSizes.s : iconSizes.m;
61
61
 
62
62
  return (
63
63
  <div className={className}>
@@ -37,6 +37,13 @@
37
37
  }
38
38
 
39
39
  /* Sizes */
40
+ .ds-tab--xs {
41
+ height: var(--height-xs);
42
+ padding: 0 calc(var(--height-xs) / 2);
43
+ font-size: var(--font-size-p3);
44
+ border-radius: var(--radius-xs);
45
+ }
46
+
40
47
  .ds-tab--small {
41
48
  height: var(--height-s);
42
49
  padding: 0 calc(var(--height-s) / 2);
@@ -87,6 +87,12 @@
87
87
  }
88
88
 
89
89
  /* Sizes */
90
+ .ds-textarea--xs {
91
+ padding: var(--space-xs);
92
+ font-size: var(--font-size-p3);
93
+ border-radius: var(--radius-xs);
94
+ }
95
+
90
96
  .ds-textarea--small {
91
97
  padding: var(--space-xs) var(--space-s);
92
98
  font-size: var(--font-size-p2);
@@ -48,21 +48,42 @@
48
48
  user-select: none;
49
49
  }
50
50
 
51
+ /* Extra Small */
52
+ .ds-toggle--xs .ds-toggle__track {
53
+ width: calc(var(--icon-xs) * 1.75);
54
+ height: var(--icon-xs);
55
+ border-radius: calc(var(--icon-xs) / 2);
56
+ padding: 2px;
57
+ }
58
+
59
+ .ds-toggle--xs .ds-toggle__thumb {
60
+ width: calc(var(--icon-xs) - 4px);
61
+ height: calc(var(--icon-xs) - 4px);
62
+ }
63
+
64
+ .ds-toggle--xs .ds-toggle__input:checked + .ds-toggle__track .ds-toggle__thumb {
65
+ transform: translateX(calc(var(--icon-xs) * 0.75));
66
+ }
67
+
68
+ .ds-toggle--xs .ds-toggle__label {
69
+ font-size: var(--font-size-p3);
70
+ }
71
+
51
72
  /* Small */
52
73
  .ds-toggle--small .ds-toggle__track {
53
- width: calc(var(--height-s) * 1.75);
54
- height: var(--height-s);
55
- border-radius: calc(var(--height-s) / 2);
56
- padding: 3px;
74
+ width: calc(var(--icon-s) * 1.75);
75
+ height: var(--icon-s);
76
+ border-radius: calc(var(--icon-s) / 2);
77
+ padding: 2px;
57
78
  }
58
79
 
59
80
  .ds-toggle--small .ds-toggle__thumb {
60
- width: calc(var(--height-s) - 8px);
61
- height: calc(var(--height-s) - 8px);
81
+ width: calc(var(--icon-s) - 4px);
82
+ height: calc(var(--icon-s) - 4px);
62
83
  }
63
84
 
64
85
  .ds-toggle--small .ds-toggle__input:checked + .ds-toggle__track .ds-toggle__thumb {
65
- transform: translateX(calc(var(--height-s) * 0.75));
86
+ transform: translateX(calc(var(--icon-s) * 0.75));
66
87
  }
67
88
 
68
89
  .ds-toggle--small .ds-toggle__label {
@@ -71,21 +92,42 @@
71
92
 
72
93
  /* Medium */
73
94
  .ds-toggle--medium .ds-toggle__track {
74
- width: calc(var(--height-m) * 1.75);
75
- height: var(--height-m);
76
- border-radius: calc(var(--height-m) / 2);
77
- padding: 4px;
95
+ width: calc(var(--icon-m) * 1.75);
96
+ height: var(--icon-m);
97
+ border-radius: calc(var(--icon-m) / 2);
98
+ padding: 2px;
78
99
  }
79
100
 
80
101
  .ds-toggle--medium .ds-toggle__thumb {
81
- width: calc(var(--height-m) - 10px);
82
- height: calc(var(--height-m) - 10px);
102
+ width: calc(var(--icon-m) - 4px);
103
+ height: calc(var(--icon-m) - 4px);
83
104
  }
84
105
 
85
106
  .ds-toggle--medium .ds-toggle__input:checked + .ds-toggle__track .ds-toggle__thumb {
86
- transform: translateX(calc(var(--height-m) * 0.75));
107
+ transform: translateX(calc(var(--icon-m) * 0.75));
87
108
  }
88
109
 
89
110
  .ds-toggle--medium .ds-toggle__label {
90
111
  font-size: var(--font-size-p1);
91
112
  }
113
+
114
+ /* Large */
115
+ .ds-toggle--large .ds-toggle__track {
116
+ width: calc(var(--icon-l) * 1.75);
117
+ height: var(--icon-l);
118
+ border-radius: calc(var(--icon-l) / 2);
119
+ padding: 3px;
120
+ }
121
+
122
+ .ds-toggle--large .ds-toggle__thumb {
123
+ width: calc(var(--icon-l) - 6px);
124
+ height: calc(var(--icon-l) - 6px);
125
+ }
126
+
127
+ .ds-toggle--large .ds-toggle__input:checked + .ds-toggle__track .ds-toggle__thumb {
128
+ transform: translateX(calc(var(--icon-l) * 0.75));
129
+ }
130
+
131
+ .ds-toggle--large .ds-toggle__label {
132
+ font-size: var(--font-size-p1);
133
+ }
@@ -12,6 +12,7 @@ export function Tooltip({
12
12
  const [offset, setOffset] = useState({ x: 0, arrowOffset: 0 });
13
13
  const wrapperRef = useRef(null);
14
14
  const tooltipRef = useRef(null);
15
+ const isTouchDevice = useRef(false);
15
16
 
16
17
  useEffect(() => {
17
18
  if (isVisible && !position && wrapperRef.current && tooltipRef.current) {
@@ -81,14 +82,51 @@ export function Tooltip({
81
82
  }
82
83
  }, [isVisible, computedPosition]);
83
84
 
85
+ // Close tooltip when clicking outside (for touch interfaces)
86
+ useEffect(() => {
87
+ if (!isVisible) return;
88
+
89
+ const handleClickOutside = (event) => {
90
+ if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
91
+ setIsVisible(false);
92
+ }
93
+ };
94
+
95
+ document.addEventListener('touchstart', handleClickOutside);
96
+
97
+ return () => {
98
+ document.removeEventListener('touchstart', handleClickOutside);
99
+ };
100
+ }, [isVisible]);
101
+
102
+ const handleTouchStart = () => {
103
+ isTouchDevice.current = true;
104
+ setIsVisible((prev) => !prev);
105
+ };
106
+
107
+ const handleMouseEnter = () => {
108
+ // Ignore mouse events on touch devices
109
+ if (!isTouchDevice.current) {
110
+ setIsVisible(true);
111
+ }
112
+ };
113
+
114
+ const handleMouseLeave = () => {
115
+ // Ignore mouse events on touch devices
116
+ if (!isTouchDevice.current) {
117
+ setIsVisible(false);
118
+ }
119
+ };
120
+
84
121
  if (!content) return children;
85
122
 
86
123
  return (
87
124
  <div
88
125
  ref={wrapperRef}
89
126
  className="ds-tooltip-wrapper"
90
- onMouseEnter={() => setIsVisible(true)}
91
- onMouseLeave={() => setIsVisible(false)}
127
+ onMouseEnter={handleMouseEnter}
128
+ onMouseLeave={handleMouseLeave}
129
+ onTouchStart={handleTouchStart}
92
130
  {...props}
93
131
  >
94
132
  {children}
package/src/index.js CHANGED
@@ -20,3 +20,5 @@ export { Accordion } from './components/Accordion';
20
20
  export { CodeAccordion } from './components/CodeAccordion';
21
21
  export { Tooltip } from './components/Tooltip';
22
22
  export { Toggle } from './components/Toggle';
23
+ export { Checkbox } from './components/Checkbox';
24
+ export { Radio } from './components/Radio';
@@ -171,6 +171,7 @@
171
171
  /* =====================================
172
172
  * 5) Component heights
173
173
  * ===================================*/
174
+ --height-xs: 24px;
174
175
  --height-s: 32px;
175
176
  --height-m: 44px;
176
177
  --height-l: 56px;