W naszej międzygalaktycznej podróży przez świat Reacta, dotarliśmy do jednego z najważniejszych aspektów budowania zaawansowanych interfejsów - systemów design. Jeśli frameworki komponentów są jak gotowe stacje kosmiczne, to systemy design są jak plany do budowy całych cywilizacji - spójnych, funkcjonalnych i skalowalnych.
System design (system projektowy) to zbiór standardów, komponentów i wytycznych, które definiują sposób tworzenia interfejsów użytkownika w całej aplikacji lub ekosystemie produktów. To jak kodeks budowlany dla kosmicznych stacji i pojazdów - zapewnia, że wszystkie elementy pasują do siebie, tworząc spójną, funkcjonalną i estetyczną całość.
Dobry system design obejmuje:
Tworzenie własnego systemu design może wydawać się dużym wyzwaniem, ale przynosi liczne korzyści:
Zbudowanie własnego systemu design jest jak terraformowanie nowej planety - wymaga wizji, planowania i systematycznego podejścia. Przejdźmy przez kluczowe kroki tego procesu.
Zacznij od fundamentalnych zasad, które będą kierować całym systemem. To jak konstytucja twojej kosmicznej cywilizacji. Przykładowe zasady:
Tokeny designu to zmienne, które przechowują podstawowe wartości stylistyczne. To jak pierwiastki chemiczne, z których zbudowany jest twój świat.
1// design-tokens.js
2
3export const colors = {
4 // Podstawowa paleta
5 primary: {
6 50: '#e3f2fd',
7 100: '#bbdefb',
8 200: '#90caf9',
9 300: '#64b5f6',
10 400: '#42a5f5',
11 500: '#2196f3', // Główny kolor
12 600: '#1e88e5',
13 700: '#1976d2',
14 800: '#1565c0',
15 900: '#0d47a1',
16 },
17
18 // Kolory semantyczne
19 success: '#4caf50',
20 warning: '#ff9800',
21 error: '#f44336',
22 info: '#2196f3',
23
24 // Neutralne
25 neutral: {
26 50: '#fafafa',
27 100: '#f5f5f5',
28 200: '#eeeeee',
29 300: '#e0e0e0',
30 400: '#bdbdbd',
31 500: '#9e9e9e',
32 600: '#757575',
33 700: '#616161',
34 800: '#424242',
35 900: '#212121',
36 },
37};1// design-tokens.js
2
3export const typography = {
4 fontFamily: {
5 base: '"Inter", system-ui, -apple-system, sans-serif',
6 heading: '"Space Grotesk", sans-serif',
7 monospace: '"Fira Code", monospace',
8 },
9
10 fontSize: {
11 xs: '0.75rem', // 12px
12 sm: '0.875rem', // 14px
13 base: '1rem', // 16px
14 lg: '1.125rem', // 18px
15 xl: '1.25rem', // 20px
16 '2xl': '1.5rem', // 24px
17 '3xl': '1.875rem', // 30px
18 '4xl': '2.25rem', // 36px
19 '5xl': '3rem', // 48px
20 },
21
22 fontWeight: {
23 light: 300,
24 regular: 400,
25 medium: 500,
26 semibold: 600,
27 bold: 700,
28 },
29
30 lineHeight: {
31 none: 1,
32 tight: 1.25,
33 snug: 1.375,
34 normal: 1.5,
35 relaxed: 1.625,
36 loose: 2,
37 },
38};1// design-tokens.js
2
3export const spacing = {
4 px: '1px',
5 0: '0',
6 0.5: '0.125rem', // 2px
7 1: '0.25rem', // 4px
8 1.5: '0.375rem', // 6px
9 2: '0.5rem', // 8px
10 2.5: '0.625rem', // 10px
11 3: '0.75rem', // 12px
12 3.5: '0.875rem', // 14px
13 4: '1rem', // 16px
14 5: '1.25rem', // 20px
15 6: '1.5rem', // 24px
16 8: '2rem', // 32px
17 10: '2.5rem', // 40px
18 12: '3rem', // 48px
19 16: '4rem', // 64px
20 20: '5rem', // 80px
21 24: '6rem', // 96px
22 32: '8rem', // 128px
23 40: '10rem', // 160px
24 48: '12rem', // 192px
25 56: '14rem', // 224px
26 64: '16rem', // 256px
27};1// design-tokens.js
2
3export const shadows = {
4 sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
5 md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
6 lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
7 xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
8 '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
9 inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
10 outline: '0 0 0 3px rgba(66, 153, 225, 0.5)',
11 none: 'none',
12};1// design-tokens.js
2
3export const breakpoints = {
4 xs: '0px',
5 sm: '600px',
6 md: '960px',
7 lg: '1280px',
8 xl: '1920px',
9};Teraz przejdźmy do budowania podstawowych, atomowych komponentów, które będą stanowić fundamentalne bloki twojego systemu. To jak tworzenie pierwszych budynków na nowej planecie.
1import React from 'react';
2import styled, { css } from 'styled-components';
3import { colors, typography, spacing } from '../design-tokens';
4
5// Definiujemy warianty przycisku
6const variants = {
7 primary: css`
8 background-color: ${colors.primary[500]};
9 color: white;
10
11 &:hover {
12 background-color: ${colors.primary[600]};
13 }
14
15 &:focus {
16 box-shadow: 0 0 0 3px ${colors.primary[300]};
17 }
18 `,
19
20 secondary: css`
21 background-color: white;
22 color: ${colors.primary[500]};
23 border: 1px solid ${colors.primary[500]};
24
25 &:hover {
26 background-color: ${colors.primary[50]};
27 }
28
29 &:focus {
30 box-shadow: 0 0 0 3px ${colors.primary[300]};
31 }
32 `,
33
34 danger: css`
35 background-color: ${colors.error};
36 color: white;
37
38 &:hover {
39 background-color: #d32f2f;
40 }
41
42 &:focus {
43 box-shadow: 0 0 0 3px rgba(244, 67, 54, 0.4);
44 }
45 `,
46};
47
48// Definiujemy rozmiary przycisków
49const sizes = {
50 small: css`
51 padding: ${spacing[1]} ${spacing[2]};
52 font-size: ${typography.fontSize.sm};
53 `,
54
55 medium: css`
56 padding: ${spacing[2]} ${spacing[4]};
57 font-size: ${typography.fontSize.base};
58 `,
59
60 large: css`
61 padding: ${spacing[3]} ${spacing[6]};
62 font-size: ${typography.fontSize.lg};
63 `,
64};
65
66// Komponent przycisku
67const StyledButton = styled.button`
68 font-family: ${typography.fontFamily.base};
69 font-weight: ${typography.fontWeight.medium};
70 border-radius: 4px;
71 border: none;
72 cursor: pointer;
73 transition: all 0.2s ease-in-out;
74 display: inline-flex;
75 align-items: center;
76 justify-content: center;
77
78 &:disabled {
79 opacity: 0.6;
80 cursor: not-allowed;
81 }
82
83 /* Zastosuj wariant */
84 ${props => variants[props.variant || 'primary']}
85
86 /* Zastosuj rozmiar */
87 ${props => sizes[props.size || 'medium']}
88`;
89
90const Button = ({ children, variant, size, ...rest }) => (
91 <StyledButton variant={variant} size={size} {...rest}>
92 {children}
93 </StyledButton>
94);
95
96export default Button;1import React from 'react';
2import styled from 'styled-components';
3import { spacing, shadows, colors } from '../design-tokens';
4
5const StyledCard = styled.div`
6 background-color: white;
7 border-radius: 8px;
8 box-shadow: ${shadows.md};
9 overflow: hidden;
10`;
11
12const CardHeader = styled.div`
13 padding: ${spacing[4]} ${spacing[4]} ${spacing[2]};
14 border-bottom: 1px solid ${colors.neutral[200]};
15`;
16
17const CardTitle = styled.h3`
18 margin: 0;
19 font-size: ${props => props.theme.typography.fontSize.xl};
20 font-weight: ${props => props.theme.typography.fontWeight.semibold};
21`;
22
23const CardSubtitle = styled.div`
24 color: ${colors.neutral[600]};
25 margin-top: ${spacing[1]};
26`;
27
28const CardContent = styled.div`
29 padding: ${spacing[4]};
30`;
31
32const CardFooter = styled.div`
33 padding: ${spacing[2]} ${spacing[4]};
34 background-color: ${colors.neutral[50]};
35 border-top: 1px solid ${colors.neutral[200]};
36 display: flex;
37 justify-content: flex-end;
38`;
39
40const Card = ({ children, ...rest }) => {
41 return <StyledCard {...rest}>{children}</StyledCard>;
42};
43
44Card.Header = CardHeader;
45Card.Title = CardTitle;
46Card.Subtitle = CardSubtitle;
47Card.Content = CardContent;
48Card.Footer = CardFooter;
49
50export default Card;Gdy masz już podstawowe komponenty, możesz zacząć budować bardziej złożone elementy interfejsu, wykorzystując atomowe komponenty jako budulce. To jak tworzenie zaawansowanych struktur po zbudowaniu fundamentów.
1import React, { useEffect, useRef } from 'react';
2import styled from 'styled-components';
3import Button from './Button';
4import { colors, spacing, shadows } from '../design-tokens';
5
6const Overlay = styled.div`
7 position: fixed;
8 top: 0;
9 left: 0;
10 right: 0;
11 bottom: 0;
12 background-color: rgba(0, 0, 0, 0.5);
13 display: flex;
14 align-items: center;
15 justify-content: center;
16 z-index: 1000;
17`;
18
19const DialogContainer = styled.div`
20 background-color: white;
21 border-radius: 8px;
22 box-shadow: ${shadows.xl};
23 width: 100%;
24 max-width: ${props =>
25 props.size === 'small' ? '400px' : props.size === 'large' ? '800px' : '600px'};
26 max-height: 90vh;
27 overflow-y: auto;
28 display: flex;
29 flex-direction: column;
30`;
31
32const DialogHeader = styled.div`
33 padding: ${spacing[4]};
34 border-bottom: 1px solid ${colors.neutral[200]};
35 display: flex;
36 align-items: center;
37 justify-content: space-between;
38`;
39
40const DialogTitle = styled.h3`
41 margin: 0;
42 font-weight: 600;
43`;
44
45const CloseButton = styled.button`
46 background: none;
47 border: none;
48 cursor: pointer;
49 color: ${colors.neutral[500]};
50 font-size: 1.5rem;
51 line-height: 1;
52
53 &:hover {
54 color: ${colors.neutral[700]};
55 }
56`;
57
58const DialogContent = styled.div`
59 padding: ${spacing[4]};
60 flex: 1;
61`;
62
63const DialogFooter = styled.div`
64 padding: ${spacing[3]} ${spacing[4]};
65 border-top: 1px solid ${colors.neutral[200]};
66 display: flex;
67 justify-content: flex-end;
68 gap: ${spacing[2]};
69`;
70
71const Dialog = ({
72 isOpen,
73 onClose,
74 title,
75 children,
76 size = 'medium',
77 confirmText = 'Potwierdź',
78 cancelText = 'Anuluj',
79 onConfirm,
80 hideActions = false
81}) => {
82 const dialogRef = useRef(null);
83
84 // Zamykanie dialogu po kliknięciu ESC
85 useEffect(() => {
86 const handleKeyDown = (e) => {
87 if (e.key === 'Escape') {
88 onClose();
89 }
90 };
91
92 if (isOpen) {
93 window.addEventListener('keydown', handleKeyDown);
94 }
95
96 return () => {
97 window.removeEventListener('keydown', handleKeyDown);
98 };
99 }, [isOpen, onClose]);
100
101 // Zatrzymanie propagacji kliknięcia wewnątrz dialogu
102 const handleDialogClick = (e) => {
103 e.stopPropagation();
104 };
105
106 if (!isOpen) return null;
107
108 return (
109 <Overlay onClick={onClose}>
110 <DialogContainer size={size} onClick={handleDialogClick} ref={dialogRef}>
111 <DialogHeader>
112 <DialogTitle>{title}</DialogTitle>
113 <CloseButton onClick={onClose}>×</CloseButton>
114 </DialogHeader>
115 <DialogContent>{children}</DialogContent>
116 {!hideActions && (
117 <DialogFooter>
118 <Button variant="secondary" onClick={onClose}>
119 {cancelText}
120 </Button>
121 {onConfirm && (
122 <Button variant="primary" onClick={onConfirm}>
123 {confirmText}
124 </Button>
125 )}
126 </DialogFooter>
127 )}
128 </DialogContainer>
129 </Overlay>
130 );
131};
132
133export default Dialog;Wzorce interakcji to standardowe sposoby, w jakie użytkownicy wchodzą w interakcję z interfejsem. Oto przykład implementacji wzorca "Powiadomienia" (Toast):
1import React, { useState, useEffect } from 'react';
2import styled, { css, keyframes } from 'styled-components';
3import { colors, spacing, shadows } from '../design-tokens';
4
5// Animacja wejścia
6const slideIn = keyframes`
7 from {
8 transform: translateX(100%);
9 opacity: 0;
10 }
11 to {
12 transform: translateX(0);
13 opacity: 1;
14 }
15`;
16
17// Animacja wyjścia
18const slideOut = keyframes`
19 from {
20 transform: translateX(0);
21 opacity: 1;
22 }
23 to {
24 transform: translateX(100%);
25 opacity: 0;
26 }
27`;
28
29// Różne typy powiadomień
30const toastTypes = {
31 success: css`
32 background-color: ${colors.success};
33 color: white;
34 `,
35 error: css`
36 background-color: ${colors.error};
37 color: white;
38 `,
39 warning: css`
40 background-color: ${colors.warning};
41 color: white;
42 `,
43 info: css`
44 background-color: ${colors.info};
45 color: white;
46 `,
47};
48
49const ToastContainer = styled.div`
50 position: fixed;
51 top: ${spacing[4]};
52 right: ${spacing[4]};
53 z-index: 1500;
54 display: flex;
55 flex-direction: column;
56 gap: ${spacing[2]};
57`;
58
59const Toast = styled.div`
60 min-width: 300px;
61 max-width: 400px;
62 padding: ${spacing[3]};
63 border-radius: 4px;
64 box-shadow: ${shadows.lg};
65 display: flex;
66 align-items: center;
67 justify-content: space-between;
68 animation: ${props => (props.leaving ? slideOut : slideIn)} 0.3s ease-in-out forwards;
69
70 ${props => toastTypes[props.type || 'info']}
71`;
72
73const CloseButton = styled.button`
74 background: none;
75 border: none;
76 color: inherit;
77 opacity: 0.7;
78 cursor: pointer;
79 font-size: 1.25rem;
80
81 &:hover {
82 opacity: 1;
83 }
84`;
85
86// Kontekst dla powiadomień
87const ToastContext = React.createContext();
88
89// Provider powiadomień
90export const ToastProvider = ({ children }) => {
91 const [toasts, setToasts] = useState([]);
92
93 const addToast = (message, type = 'info', duration = 5000) => {
94 const id = Date.now();
95 setToasts(prev => [...prev, { id, message, type, duration }]);
96
97 if (duration !== Infinity) {
98 setTimeout(() => removeToast(id), duration);
99 }
100
101 return id;
102 };
103
104 const removeToast = (id) => {
105 setToasts(prev => {
106 const toast = prev.find(t => t.id === id);
107 if (toast) {
108 // Oznaczenie tosta jako wychodzącego
109 return prev.map(t => t.id === id ? { ...t, leaving: true } : t);
110 }
111 return prev;
112 });
113
114 // Usuń toast po zakończeniu animacji
115 setTimeout(() => {
116 setToasts(prev => prev.filter(t => t.id !== id));
117 }, 300); // Czas trwania animacji
118 };
119
120 return (
121 <ToastContext.Provider value={{ addToast, removeToast }}>
122 {children}
123 <ToastContainer>
124 {toasts.map(toast => (
125 <Toast key={toast.id} type={toast.type} leaving={toast.leaving}>
126 <span>{toast.message}</span>
127 <CloseButton onClick={() => removeToast(toast.id)}>×</CloseButton>
128 </Toast>
129 ))}
130 </ToastContainer>
131 </ToastContext.Provider>
132 );
133};
134
135// Hook do używania powiadomień
136export const useToast = () => {
137 const context = React.useContext(ToastContext);
138 if (!context) {
139 throw new Error('useToast must be used within a ToastProvider');
140 }
141 return context;
142};
143
144// Przykład użycia:
145// const { addToast } = useToast();
146// addToast('Operacja zakończona sukcesem!', 'success');Ostatnim, ale nie mniej ważnym krokiem, jest stworzenie dokumentacji, która pozwoli innym deweloperom i projektantom korzystać z twojego systemu design. Popularnymi narzędziami do tworzenia dokumentacji są:
Przykład konfiguracji komponentu w Storybook:
1// Button.stories.js
2
3import React from 'react';
4import Button from './Button';
5
6export default {
7 title: 'Atoms/Button',
8 component: Button,
9 argTypes: {
10 variant: {
11 control: { type: 'select', options: ['primary', 'secondary', 'danger'] },
12 description: 'Wariant przycisku określający jego styl i przeznaczenie',
13 table: {
14 type: { summary: 'string' },
15 defaultValue: { summary: 'primary' },
16 },
17 },
18 size: {
19 control: { type: 'select', options: ['small', 'medium', 'large'] },
20 description: 'Rozmiar przycisku',
21 table: {
22 type: { summary: 'string' },
23 defaultValue: { summary: 'medium' },
24 },
25 },
26 disabled: {
27 control: 'boolean',
28 description: 'Czy przycisk jest wyłączony',
29 table: {
30 type: { summary: 'boolean' },
31 defaultValue: { summary: false },
32 },
33 },
34 },
35};
36
37// Podstawowe użycie
38export const Primary = () => <Button variant="primary">Przycisk główny</Button>;
39
40// Warianty
41export const Variants = () => (
42 <div style={{ display: 'flex', gap: '8px' }}>
43 <Button variant="primary">Główny</Button>
44 <Button variant="secondary">Drugorzędny</Button>
45 <Button variant="danger">Niebezpieczny</Button>
46 </div>
47);
48
49// Rozmiary
50export const Sizes = () => (
51 <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
52 <Button size="small">Mały</Button>
53 <Button size="medium">Średni</Button>
54 <Button size="large">Duży</Button>
55 </div>
56);
57
58// Stan wyłączony
59export const Disabled = () => <Button disabled>Przycisk wyłączony</Button>;Wdrożenie systemu design w istniejącym projekcie to jak modernizacja działającej stacji kosmicznej - wymaga planowania i stopniowego podejścia:
Zaprojektuj i zaimplementuj mały system design dla aplikacji do monitorowania kosmicznych misji. Twój system powinien zawierać:
Pamiętaj, że dobry system design to nie tylko estetyka, ale przede wszystkim funkcjonalność, spójność i łatwość użycia. Twórz system z myślą o rozwoju aplikacji w przyszłości.
Gdy tworzysz własny system design, stajesz się nie tylko programistą, ale również architektem kosmicznej przyszłości interfejsów. Podobnie jak twórcy pierwszych kolonii na innych planetach, tworzysz fundamenty, na których będą budować przyszłe pokolenia deweloperów i użytkowników.