@donkit-ai/design-system 0.2.3

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.
@@ -0,0 +1,35 @@
1
+ import React, { useState } from 'react';
2
+ import { ChevronDown } from 'lucide-react';
3
+ import './CodeAccordion.css';
4
+
5
+ export function CodeAccordion({
6
+ children,
7
+ title = 'Code',
8
+ defaultExpanded = false,
9
+ ...props
10
+ }) {
11
+ const [isExpanded, setIsExpanded] = useState(defaultExpanded);
12
+
13
+ return (
14
+ <div className="ds-code-accordion">
15
+ <button
16
+ type="button"
17
+ className="ds-code-accordion__header"
18
+ onClick={() => setIsExpanded(!isExpanded)}
19
+ aria-expanded={isExpanded}
20
+ >
21
+ <span className="ds-code-accordion__title">{title}</span>
22
+ <ChevronDown
23
+ size={20}
24
+ strokeWidth={1.5}
25
+ className={`ds-code-accordion__icon ${isExpanded ? 'ds-code-accordion__icon--expanded' : ''}`}
26
+ />
27
+ </button>
28
+ {isExpanded && (
29
+ <pre className="ds-code-accordion__content" {...props}>
30
+ <code>{children}</code>
31
+ </pre>
32
+ )}
33
+ </div>
34
+ );
35
+ }
@@ -0,0 +1,140 @@
1
+ .ds-input-wrapper {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--space-xs);
5
+ }
6
+
7
+ .ds-input-wrapper--full {
8
+ width: 100%;
9
+ }
10
+
11
+ .ds-input-wrapper--disabled {
12
+ opacity: 0.5;
13
+ cursor: not-allowed;
14
+ }
15
+
16
+ .ds-input-label {
17
+ font-size: var(--font-size-p2);
18
+ font-weight: 400;
19
+ color: var(--color-txt-icon-1);
20
+ }
21
+
22
+ .ds-input-container {
23
+ position: relative;
24
+ display: flex;
25
+ align-items: center;
26
+ }
27
+
28
+ .ds-input {
29
+ width: 100%;
30
+ font-family: inherit;
31
+ color: var(--color-txt-icon-1);
32
+ background-color: transparent;
33
+ border: 1px solid var(--color-border);
34
+ transition: border-color var(--transition-normal);
35
+ line-height: 1.5;
36
+ }
37
+
38
+ .ds-input::placeholder {
39
+ color: var(--color-txt-icon-2);
40
+ }
41
+
42
+ .ds-input:hover:not(:disabled) {
43
+ border-color: var(--color-border-hover);
44
+ }
45
+
46
+ .ds-input:focus,
47
+ .ds-input:active {
48
+ outline: none;
49
+ border-color: var(--color-border-hover);
50
+ }
51
+
52
+ .ds-input:disabled {
53
+ cursor: not-allowed;
54
+ }
55
+
56
+ .ds-input--error {
57
+ border-color: var(--color-error);
58
+ }
59
+
60
+ /* Sizes */
61
+ .ds-input--small {
62
+ height: calc(20px + var(--space-xs) * 2);
63
+ padding: 0 var(--space-s);
64
+ font-size: var(--font-size-p2);
65
+ border-radius: var(--radius-xs);
66
+ }
67
+
68
+ .ds-input--medium {
69
+ height: calc(24px + var(--space-s) * 2);
70
+ padding: 0 var(--space-s);
71
+ font-size: var(--font-size-p1);
72
+ border-radius: var(--radius-s);
73
+ }
74
+
75
+ .ds-input--with-icon.ds-input--small {
76
+ padding-left: calc(var(--space-s) + 20px + var(--space-xs));
77
+ }
78
+
79
+ .ds-input--with-icon.ds-input--medium {
80
+ padding-left: calc(var(--space-s) + 24px + var(--space-xs));
81
+ }
82
+
83
+ .ds-input--with-icon-right.ds-input--small {
84
+ padding-right: calc(var(--space-s) + 20px + var(--space-xs));
85
+ }
86
+
87
+ .ds-input--with-icon-right.ds-input--medium {
88
+ padding-right: calc(var(--space-s) + 24px + var(--space-xs));
89
+ }
90
+
91
+ .ds-input-icon {
92
+ position: absolute;
93
+ display: flex;
94
+ align-items: center;
95
+ color: var(--color-txt-icon-2);
96
+ pointer-events: none;
97
+ }
98
+
99
+ .ds-input-icon--small {
100
+ left: var(--space-xs);
101
+ }
102
+
103
+ .ds-input-icon--medium {
104
+ left: var(--space-s);
105
+ }
106
+
107
+ .ds-input-icon-right {
108
+ position: absolute;
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ background: none;
113
+ border: none;
114
+ color: var(--color-txt-icon-2);
115
+ cursor: pointer;
116
+ transition: color var(--transition-normal);
117
+ padding: 0;
118
+ }
119
+
120
+ .ds-input-icon-right:hover {
121
+ color: var(--color-txt-icon-1);
122
+ }
123
+
124
+ .ds-input-icon-right--small {
125
+ right: var(--space-xs);
126
+ }
127
+
128
+ .ds-input-icon-right--medium {
129
+ right: var(--space-s);
130
+ }
131
+
132
+ .ds-input-hint {
133
+ font-size: var(--font-size-p2);
134
+ color: var(--color-txt-icon-2);
135
+ }
136
+
137
+ .ds-input-error {
138
+ font-size: var(--font-size-p2);
139
+ color: var(--color-error);
140
+ }
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+ import './Input.css';
3
+
4
+ export function Input({
5
+ label,
6
+ error,
7
+ hint,
8
+ fullWidth = true,
9
+ icon,
10
+ iconRight,
11
+ onIconRightClick,
12
+ size = 'medium',
13
+ disabled,
14
+ id,
15
+ ...props
16
+ }) {
17
+ const inputId = id || `input-${React.useId()}`;
18
+ const hintId = hint ? `${inputId}-hint` : undefined;
19
+ const errorId = error ? `${inputId}-error` : undefined;
20
+ const describedBy = errorId || hintId;
21
+
22
+ return (
23
+ <div className={`ds-input-wrapper ${fullWidth ? 'ds-input-wrapper--full' : ''} ${disabled ? 'ds-input-wrapper--disabled' : ''}`}>
24
+ {label && (
25
+ <label className="ds-input-label" htmlFor={inputId}>
26
+ {label}
27
+ </label>
28
+ )}
29
+ <div className="ds-input-container">
30
+ {icon && <span className={`ds-input-icon ds-input-icon--${size}`} aria-hidden="true">{icon}</span>}
31
+ <input
32
+ id={inputId}
33
+ className={`ds-input ds-input--${size} ${icon ? 'ds-input--with-icon' : ''} ${iconRight ? 'ds-input--with-icon-right' : ''} ${error ? 'ds-input--error' : ''}`}
34
+ disabled={disabled}
35
+ aria-invalid={error ? 'true' : 'false'}
36
+ aria-describedby={describedBy}
37
+ {...props}
38
+ />
39
+ {iconRight && (
40
+ <button
41
+ type="button"
42
+ className={`ds-input-icon-right ds-input-icon-right--${size}`}
43
+ onClick={onIconRightClick}
44
+ tabIndex={-1}
45
+ aria-label="Toggle visibility"
46
+ >
47
+ {iconRight}
48
+ </button>
49
+ )}
50
+ </div>
51
+ {hint && !error && <span id={hintId} className="ds-input-hint">{hint}</span>}
52
+ {error && <span id={errorId} className="ds-input-error" role="alert">{error}</span>}
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,18 @@
1
+ .ds-link {
2
+ color: var(--color-accent);
3
+ text-decoration: none;
4
+ cursor: pointer;
5
+ transition: color var(--transition-fast), text-decoration var(--transition-fast);
6
+ font-size: inherit;
7
+ font-weight: inherit;
8
+ line-height: inherit;
9
+ }
10
+
11
+ .ds-link:hover {
12
+ color: var(--color-accent-hover);
13
+ text-decoration: underline;
14
+ }
15
+
16
+ .ds-link:active {
17
+ color: var(--color-accent-hover);
18
+ }
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import './Link.css';
3
+
4
+ export function Link({ href, children, onClick, target, rel, ...props }) {
5
+ // Add security attributes for external links
6
+ const isExternal = target === '_blank';
7
+ const secureRel = isExternal ? (rel ? `${rel} noopener noreferrer` : 'noopener noreferrer') : rel;
8
+
9
+ return (
10
+ <a
11
+ href={href}
12
+ className="ds-link"
13
+ onClick={onClick}
14
+ target={target}
15
+ rel={secureRel}
16
+ {...props}
17
+ >
18
+ {children}
19
+ </a>
20
+ );
21
+ }
@@ -0,0 +1,66 @@
1
+ .ds-modal-overlay {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ right: 0;
6
+ bottom: 0;
7
+ background-color: var(--color-overlay);
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ z-index: 1000;
12
+ padding: var(--space-m);
13
+ }
14
+
15
+ .ds-modal {
16
+ background-color: var(--color-bg);
17
+ border: 1px solid var(--color-border);
18
+ border-radius: var(--radius-s);
19
+ max-height: 90vh;
20
+ overflow-y: auto;
21
+ display: flex;
22
+ flex-direction: column;
23
+ }
24
+
25
+ .ds-modal--small {
26
+ width: 100%;
27
+ max-width: 400px;
28
+ }
29
+
30
+ .ds-modal--medium {
31
+ width: 100%;
32
+ max-width: 600px;
33
+ }
34
+
35
+ .ds-modal--large {
36
+ width: 100%;
37
+ max-width: 900px;
38
+ }
39
+
40
+ .ds-modal__header {
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: space-between;
44
+ padding: var(--space-l);
45
+ }
46
+
47
+ .ds-modal__title {
48
+ font-size: var(--font-size-h3);
49
+ font-weight: 400;
50
+ color: var(--color-txt-icon-1);
51
+ margin: 0;
52
+ }
53
+
54
+ .ds-modal__body {
55
+ padding: 0 var(--space-l) var(--space-s);
56
+ flex: 1;
57
+ overflow-y: auto;
58
+ }
59
+
60
+ .ds-modal__footer {
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: flex-start;
64
+ gap: var(--space-s);
65
+ padding: var(--space-l);
66
+ }
@@ -0,0 +1,71 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { X } from 'lucide-react';
3
+ import { Button } from './Button';
4
+ import './Modal.css';
5
+
6
+ export function Modal({
7
+ children,
8
+ title,
9
+ onClose,
10
+ size = 'medium',
11
+ ...props
12
+ }) {
13
+ const modalRef = useRef(null);
14
+ const titleId = React.useId();
15
+
16
+ useEffect(() => {
17
+ const handleEscape = (e) => {
18
+ if (e.key === 'Escape') onClose?.();
19
+ };
20
+ const handleClickOutside = (e) => {
21
+ if (modalRef.current && !modalRef.current.contains(e.target)) {
22
+ onClose?.();
23
+ }
24
+ };
25
+
26
+ document.addEventListener('keydown', handleEscape);
27
+ document.addEventListener('mousedown', handleClickOutside);
28
+ return () => {
29
+ document.removeEventListener('keydown', handleEscape);
30
+ document.removeEventListener('mousedown', handleClickOutside);
31
+ };
32
+ }, [onClose]);
33
+
34
+ // Separate ModalFooter from other children
35
+ const childrenArray = React.Children.toArray(children);
36
+ const footer = childrenArray.find(child => child?.type === ModalFooter);
37
+ const bodyContent = childrenArray.filter(child => child?.type !== ModalFooter);
38
+
39
+ return (
40
+ <div className="ds-modal-overlay" {...props}>
41
+ <div
42
+ className={`ds-modal ds-modal--${size}`}
43
+ ref={modalRef}
44
+ role="dialog"
45
+ aria-modal="true"
46
+ aria-labelledby={title ? titleId : undefined}
47
+ >
48
+ {(title || onClose) && (
49
+ <div className="ds-modal__header">
50
+ {title && <h3 id={titleId} className="ds-modal__title">{title}</h3>}
51
+ {onClose && (
52
+ <Button
53
+ variant="ghost"
54
+ size="small"
55
+ icon={<X size={20} strokeWidth={1.5} />}
56
+ onClick={onClose}
57
+ aria-label="Close modal"
58
+ />
59
+ )}
60
+ </div>
61
+ )}
62
+ <div className="ds-modal__body">{bodyContent}</div>
63
+ {footer}
64
+ </div>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ export function ModalFooter({ children }) {
70
+ return <div className="ds-modal__footer">{children}</div>;
71
+ }
@@ -0,0 +1,150 @@
1
+ .ds-select-wrapper {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--space-xs);
5
+ }
6
+
7
+ .ds-select-wrapper--full {
8
+ width: 100%;
9
+ }
10
+
11
+ .ds-select-wrapper--disabled {
12
+ opacity: 0.5;
13
+ cursor: not-allowed;
14
+ }
15
+
16
+ .ds-select-label {
17
+ font-size: var(--font-size-p2);
18
+ font-weight: 400;
19
+ color: var(--color-txt-icon-1);
20
+ }
21
+
22
+ .ds-select-container {
23
+ position: relative;
24
+ }
25
+
26
+ .ds-select-trigger {
27
+ width: 100%;
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: space-between;
31
+ gap: var(--space-s);
32
+ font-family: inherit;
33
+ color: var(--color-txt-icon-1);
34
+ background-color: transparent;
35
+ border: 1px solid var(--color-border);
36
+ cursor: pointer;
37
+ transition: border-color var(--transition-normal);
38
+ text-align: left;
39
+ line-height: 1.5;
40
+ }
41
+
42
+ .ds-select-trigger:hover:not(:disabled) {
43
+ border-color: var(--color-border-hover);
44
+ }
45
+
46
+ .ds-select-trigger:disabled {
47
+ cursor: not-allowed;
48
+ }
49
+
50
+ .ds-select-trigger--error {
51
+ border-color: var(--color-error);
52
+ }
53
+
54
+ /* Sizes */
55
+ .ds-select-trigger--small {
56
+ height: calc(20px + var(--space-xs) * 2);
57
+ padding: 0 var(--space-s);
58
+ font-size: var(--font-size-p2);
59
+ border-radius: var(--radius-xs);
60
+ }
61
+
62
+ .ds-select-trigger--medium {
63
+ height: calc(24px + var(--space-s) * 2);
64
+ padding: 0 var(--space-s);
65
+ font-size: var(--font-size-p1);
66
+ border-radius: var(--radius-s);
67
+ }
68
+
69
+ .ds-select-placeholder {
70
+ color: var(--color-txt-icon-2);
71
+ }
72
+
73
+ .ds-select-icon {
74
+ flex-shrink: 0;
75
+ color: var(--color-txt-icon-2);
76
+ transition: transform var(--transition-normal);
77
+ }
78
+
79
+ .ds-select-icon--open {
80
+ transform: rotate(180deg);
81
+ }
82
+
83
+ .ds-select-icon--up {
84
+ transform: rotate(0deg);
85
+ }
86
+
87
+ .ds-select-dropdown {
88
+ position: absolute;
89
+ left: 0;
90
+ right: 0;
91
+ background-color: var(--color-bg);
92
+ border: 1px solid var(--color-border);
93
+ z-index: 100;
94
+ max-height: 300px;
95
+ overflow-y: auto;
96
+ }
97
+
98
+ /* Dropdown direction */
99
+ .ds-select-dropdown--down {
100
+ top: calc(100% + 4px);
101
+ }
102
+
103
+ .ds-select-dropdown--up {
104
+ bottom: calc(100% + 4px);
105
+ }
106
+
107
+ .ds-select-dropdown--small {
108
+ border-radius: var(--radius-xs);
109
+ }
110
+
111
+ .ds-select-dropdown--medium {
112
+ border-radius: var(--radius-s);
113
+ }
114
+
115
+ .ds-select-option {
116
+ width: 100%;
117
+ font-family: inherit;
118
+ color: var(--color-txt-icon-1);
119
+ background-color: transparent;
120
+ border: none;
121
+ cursor: pointer;
122
+ text-align: left;
123
+ transition: background-color var(--transition-normal);
124
+ line-height: 1.5;
125
+ }
126
+
127
+ .ds-select-option--small {
128
+ height: calc(20px + var(--space-xs) * 2);
129
+ padding: 0 var(--space-s);
130
+ font-size: var(--font-size-p2);
131
+ }
132
+
133
+ .ds-select-option--medium {
134
+ height: calc(24px + var(--space-s) * 2);
135
+ padding: 0 var(--space-s);
136
+ font-size: var(--font-size-p1);
137
+ }
138
+
139
+ .ds-select-option:hover {
140
+ background-color: var(--color-item-bg-hover);
141
+ }
142
+
143
+ .ds-select-option--selected {
144
+ background-color: var(--color-item-bg-selected);
145
+ }
146
+
147
+ .ds-select-error {
148
+ font-size: var(--font-size-p2);
149
+ color: var(--color-error);
150
+ }
@@ -0,0 +1,117 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { ChevronDown } from 'lucide-react';
3
+ import './Select.css';
4
+
5
+ export function Select({
6
+ label,
7
+ value,
8
+ onChange,
9
+ options = [],
10
+ placeholder = 'Select option',
11
+ error,
12
+ fullWidth = true,
13
+ size = 'medium',
14
+ disabled = false,
15
+ id,
16
+ ...props
17
+ }) {
18
+ const [isOpen, setIsOpen] = useState(false);
19
+ const [dropdownDirection, setDropdownDirection] = useState('down');
20
+ const selectRef = useRef(null);
21
+ const dropdownRef = useRef(null);
22
+ const selectId = id || `select-${React.useId()}`;
23
+ const labelId = `${selectId}-label`;
24
+ const errorId = error ? `${selectId}-error` : undefined;
25
+
26
+ useEffect(() => {
27
+ const handleClickOutside = (e) => {
28
+ if (selectRef.current && !selectRef.current.contains(e.target)) {
29
+ setIsOpen(false);
30
+ }
31
+ };
32
+
33
+ document.addEventListener('mousedown', handleClickOutside);
34
+ return () => document.removeEventListener('mousedown', handleClickOutside);
35
+ }, []);
36
+
37
+ useEffect(() => {
38
+ if (isOpen && selectRef.current) {
39
+ const triggerRect = selectRef.current.getBoundingClientRect();
40
+ const viewportHeight = window.innerHeight;
41
+ const spaceBelow = viewportHeight - triggerRect.bottom;
42
+ const spaceAbove = triggerRect.top;
43
+ const dropdownHeight = 300; // max-height of dropdown
44
+
45
+ // Open upward if not enough space below and more space above
46
+ if (spaceBelow < dropdownHeight && spaceAbove > spaceBelow) {
47
+ setDropdownDirection('up');
48
+ } else {
49
+ setDropdownDirection('down');
50
+ }
51
+ }
52
+ }, [isOpen]);
53
+
54
+ const selectedOption = options.find(opt => opt.value === value);
55
+ const iconSize = size === 'small' ? 20 : 24;
56
+
57
+ return (
58
+ <div className={`ds-select-wrapper ${fullWidth ? 'ds-select-wrapper--full' : ''} ${disabled ? 'ds-select-wrapper--disabled' : ''}`}>
59
+ {label && (
60
+ <label id={labelId} className="ds-select-label">
61
+ {label}
62
+ </label>
63
+ )}
64
+ <div className="ds-select-container" ref={selectRef}>
65
+ <button
66
+ type="button"
67
+ id={selectId}
68
+ role="combobox"
69
+ aria-haspopup="listbox"
70
+ aria-expanded={isOpen}
71
+ aria-labelledby={label ? labelId : undefined}
72
+ aria-invalid={error ? 'true' : 'false'}
73
+ aria-describedby={errorId}
74
+ className={`ds-select-trigger ds-select-trigger--${size} ${error ? 'ds-select-trigger--error' : ''}`}
75
+ onClick={() => !disabled && setIsOpen(!isOpen)}
76
+ disabled={disabled}
77
+ {...props}
78
+ >
79
+ <span className={selectedOption ? '' : 'ds-select-placeholder'}>
80
+ {selectedOption?.label || placeholder}
81
+ </span>
82
+ <ChevronDown
83
+ size={iconSize}
84
+ strokeWidth={1.5}
85
+ className={`ds-select-icon ${isOpen && dropdownDirection === 'down' ? 'ds-select-icon--open' : ''} ${isOpen && dropdownDirection === 'up' ? 'ds-select-icon--up' : ''}`}
86
+ aria-hidden="true"
87
+ />
88
+ </button>
89
+ {isOpen && (
90
+ <div
91
+ ref={dropdownRef}
92
+ role="listbox"
93
+ aria-labelledby={label ? labelId : undefined}
94
+ className={`ds-select-dropdown ds-select-dropdown--${size} ds-select-dropdown--${dropdownDirection}`}
95
+ >
96
+ {options.map((option) => (
97
+ <button
98
+ key={option.value}
99
+ type="button"
100
+ role="option"
101
+ aria-selected={value === option.value}
102
+ className={`ds-select-option ds-select-option--${size} ${value === option.value ? 'ds-select-option--selected' : ''}`}
103
+ onClick={() => {
104
+ onChange?.(option.value);
105
+ setIsOpen(false);
106
+ }}
107
+ >
108
+ {option.label}
109
+ </button>
110
+ ))}
111
+ </div>
112
+ )}
113
+ </div>
114
+ {error && <span id={errorId} className="ds-select-error" role="alert">{error}</span>}
115
+ </div>
116
+ );
117
+ }