@salutejs/plasma-new-hope 0.154.0-canary.1428.11047556363.0 → 0.155.0-canary.1443.11051428691.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,304 @@
1
+ ---
2
+ id: steps
3
+ title: Steps
4
+ ---
5
+
6
+ # Steps
7
+ Шаги могут отображаться в нескольких размерах, в упрощенном виде и в горизонтальной/вертикальной ориентации
8
+
9
+ ```tsx live
10
+ import { ReactNode } from 'react';
11
+
12
+ export type StepStatus = 'active' | 'inactive' | 'completed';
13
+
14
+ export interface StepItemProps {
15
+ /**
16
+ * Заголовок
17
+ */
18
+ title?: string;
19
+ /**
20
+ * Контент, может быть как React компонентом, так и функцией
21
+ */
22
+ content?: string | ReactNode | ((status: StepStatus, index: number, items: StepItemProps[]) => ReactNode);
23
+ /**
24
+ * Индикатор шага, может быть как React компонентом, так и функцией
25
+ */
26
+ indicator?: number | string | ReactNode | ((status: StepStatus, item: StepItemProps) => ReactNode | null);
27
+ /**
28
+ * Статус шага: активный, инактивный, завершенный
29
+ * @description
30
+ * active - активный шаг
31
+ * inactive - не пройденный шаг
32
+ * completed - пройденный шаг
33
+ */
34
+ status?: StepStatus;
35
+ /**
36
+ * Отключенный шаг
37
+ * @default false
38
+ */
39
+ disabled?: boolean;
40
+ }
41
+
42
+ interface BaseSteps {
43
+ /**
44
+ * Ориентация компонента
45
+ * @default 'horizontal'
46
+ */
47
+ orientation?: 'horizontal' | 'vertical';
48
+ /**
49
+ * Выравнивание контента в шагах
50
+ * @default 'left'
51
+ */
52
+ contentAlign?: 'left' | 'center';
53
+ /**
54
+ * Включает разделительную линию
55
+ * @default true
56
+ */
57
+ hasLine?: boolean;
58
+ /**
59
+ * Индекс текущего шага, для uncontrolled компонента
60
+ */
61
+ current?: number;
62
+ /**
63
+ * Статус текущего шага, имеет приоритет над item.status
64
+ */
65
+ status?: StepStatus;
66
+ /**
67
+ * Обработчик изменения шага, делает индикаторы и заголовки шагов кликабельными
68
+ */
69
+ onChange?: (item: StepItemProps, index: number, prevIndex?: number) => void;
70
+ /**
71
+ * Массив шагов
72
+ */
73
+ items: StepItemProps[];
74
+ }
75
+
76
+ type IndicatorSizeSimple = 8 | 16;
77
+ type IndicatorSize = 24 | 36;
78
+
79
+ export type Steps = BaseSteps & {
80
+ /**
81
+ * Размер шага
82
+ */
83
+ size: 'xs' | 's';
84
+ indicatorSize: never;
85
+ } & {
86
+ /**
87
+ * Размер шага
88
+ */
89
+ size: 'm' | 'l';
90
+ /**
91
+ * Размер индикатора
92
+ */
93
+ indicatorSize: IndicatorSize | IndicatorSizeSimple;
94
+ };
95
+
96
+ ```
97
+
98
+ примеры использования https://codesandbox.io/p/devbox/crzql2?file=%2Fsrc%2FDemo.tsx%3A23%2C1
99
+
100
+ ```
101
+ import React, { useState } from 'react';
102
+ import { fullItems } from './App.data.tsx';
103
+ import { Steps } from './Steps/Steps.tsx';
104
+ import { StepItemProps, StepStatus } from './Steps/Steps.types';
105
+
106
+ const DemoContent: React.FC<{ onClick: () => void; status: string; index?: number; current?: number }> = ({
107
+ onClick,
108
+ status,
109
+ index,
110
+ current = 0,
111
+ children,
112
+ }) => {
113
+ return (
114
+ <>
115
+ <div>Content</div>
116
+ <button onClick={onClick} disabled={index !== (current || 0)}>
117
+ {children}
118
+ </button>
119
+ </>
120
+ );
121
+ };
122
+
123
+ export const DemoWithExternalControl = () => {
124
+ const items = [
125
+ {
126
+ title: 'Title',
127
+ indicator: '1',
128
+ content: 'Content',
129
+ },
130
+ {
131
+ title: 'Title',
132
+ indicator: '2',
133
+ content: 'Content',
134
+ },
135
+ {
136
+ title: 'Title',
137
+ indicator: '3',
138
+ content: 'Content',
139
+ },
140
+ {
141
+ title: 'Title',
142
+ indicator: '4',
143
+ content: 'Content',
144
+ },
145
+ ];
146
+
147
+ const [current, setCurrent] = useState(1);
148
+ const [status, setStatus] = useState('active');
149
+
150
+ const isLast = current === items.length - 1;
151
+
152
+ const onClick = () => {
153
+ if (isLast) {
154
+ setStatus('completed');
155
+ } else {
156
+ setCurrent(current + 1);
157
+ }
158
+ };
159
+
160
+ return (
161
+ <>
162
+ <Steps style={{ width: '1000px' }} current={current} status={status} items={items} />
163
+ <br />
164
+ <div>
165
+ External control:{' '}
166
+ <button onClick={onClick} style={{ width: '5rem' }}>
167
+ {isLast ? 'complete' : 'next'}
168
+ </button>
169
+ </div>
170
+ <hr />
171
+ </>
172
+ );
173
+ };
174
+
175
+ export const DemoWithInternalControls = () => {
176
+ const [current, setCurrent] = useState(undefined);
177
+ const [status, setStatus] = useState<StepStatus | undefined>();
178
+
179
+ const renderContent = (status, index, items) => {
180
+ const isFirst = index === 0 && current === undefined;
181
+ const isLast = index === items.length - 1;
182
+
183
+ const onNext = () => setCurrent((current) => (current === undefined ? 0 : current + 1));
184
+ const onComplete = () => {
185
+ setStatus('completed');
186
+ };
187
+
188
+ return (
189
+ <DemoContent onClick={isLast ? onComplete : onNext} index={index} status={status} current={current}>
190
+ {isFirst && 'go'}
191
+ {isLast && 'complete'}
192
+ {!isFirst && !isLast && 'next'}
193
+ </DemoContent>
194
+ );
195
+ };
196
+
197
+ return (
198
+ <Steps
199
+ current={current}
200
+ status={status}
201
+ items={[
202
+ {
203
+ title: 'Title',
204
+ indicator: '1',
205
+ content: renderContent,
206
+ },
207
+ {
208
+ title: 'Title',
209
+ indicator: '2',
210
+ content: renderContent,
211
+ },
212
+ {
213
+ title: 'Title',
214
+ indicator: '3',
215
+ content: renderContent,
216
+ },
217
+ {
218
+ title: 'Title',
219
+ indicator: '4',
220
+ content: renderContent,
221
+ },
222
+ ]}
223
+ />
224
+ );
225
+ };
226
+
227
+ export const DemoWithControlledUsage = () => {
228
+ const [exampleItems, setExampleItems] = useState<StepItemProps[]>(() =>
229
+ fullItems.map((item) => ({ ...item, status: 'inactive' }))
230
+ );
231
+
232
+ return (
233
+ <Steps
234
+ style={{ width: '1000px' }}
235
+ onChange={(item, index) => {
236
+ setExampleItems(
237
+ exampleItems.map((item, itemIndex) => {
238
+ if (index > itemIndex) {
239
+ return { ...item, status: 'completed' };
240
+ }
241
+
242
+ if (index === itemIndex) {
243
+ return { ...item, status: 'active' };
244
+ }
245
+
246
+ return { ...item, status: 'inactive' };
247
+ })
248
+ );
249
+ }}
250
+ items={exampleItems}
251
+ />
252
+ );
253
+ };
254
+
255
+ export const DemoWithControlledFreeUsage = () => {
256
+ const [items, setItems] = useState<StepItemProps[]>(() => fullItems.map((item) => ({ ...item, status: 'inactive' })));
257
+
258
+ return (
259
+ <Steps
260
+ style={{ width: '1000px' }}
261
+ onChange={(item, index, prevIndex) => {
262
+ console.log('!!! onChange', index, prevIndex);
263
+
264
+ const newItems = [...items];
265
+ if (prevIndex !== undefined) {
266
+ newItems[prevIndex].status = 'completed';
267
+ }
268
+ newItems[index].status = 'active';
269
+
270
+ setItems(newItems);
271
+ }}
272
+ items={items}
273
+ />
274
+ );
275
+ };
276
+
277
+ export const DemoControlledWithStatus = () => {
278
+ const [items, setItems] = useState<StepItemProps[]>(() => fullItems.map(({ status, ...item }) => ({ ...item })));
279
+ return (
280
+ <Steps
281
+ style={{ width: '1000px' }}
282
+ current={2}
283
+ status="active"
284
+ onClick={(item, index) => {
285
+ setItems(
286
+ items.map((item, itemIndex) => {
287
+ if (index > itemIndex) {
288
+ return { ...item, status: 'completed' };
289
+ }
290
+
291
+ if (index === itemIndex) {
292
+ return { ...item, status: 'active' };
293
+ }
294
+
295
+ return { ...item, status: 'inactive' };
296
+ })
297
+ );
298
+ }}
299
+ items={items}
300
+ />
301
+ );
302
+ };
303
+
304
+ ```
@@ -0,0 +1,304 @@
1
+ ---
2
+ id: steps
3
+ title: Steps
4
+ ---
5
+
6
+ # Steps
7
+ Шаги могут отображаться в нескольких размерах, в упрощенном виде и в горизонтальной/вертикальной ориентации
8
+
9
+ ```tsx live
10
+ import { ReactNode } from 'react';
11
+
12
+ export type StepStatus = 'active' | 'inactive' | 'completed';
13
+
14
+ export interface StepItemProps {
15
+ /**
16
+ * Заголовок
17
+ */
18
+ title?: string;
19
+ /**
20
+ * Контент, может быть как React компонентом, так и функцией
21
+ */
22
+ content?: string | ReactNode | ((status: StepStatus, index: number, items: StepItemProps[]) => ReactNode);
23
+ /**
24
+ * Индикатор шага, может быть как React компонентом, так и функцией
25
+ */
26
+ indicator?: number | string | ReactNode | ((status: StepStatus, item: StepItemProps) => ReactNode | null);
27
+ /**
28
+ * Статус шага: активный, инактивный, завершенный
29
+ * @description
30
+ * active - активный шаг
31
+ * inactive - не пройденный шаг
32
+ * completed - пройденный шаг
33
+ */
34
+ status?: StepStatus;
35
+ /**
36
+ * Отключенный шаг
37
+ * @default false
38
+ */
39
+ disabled?: boolean;
40
+ }
41
+
42
+ interface BaseSteps {
43
+ /**
44
+ * Ориентация компонента
45
+ * @default 'horizontal'
46
+ */
47
+ orientation?: 'horizontal' | 'vertical';
48
+ /**
49
+ * Выравнивание контента в шагах
50
+ * @default 'left'
51
+ */
52
+ contentAlign?: 'left' | 'center';
53
+ /**
54
+ * Включает разделительную линию
55
+ * @default true
56
+ */
57
+ hasLine?: boolean;
58
+ /**
59
+ * Индекс текущего шага, для uncontrolled компонента
60
+ */
61
+ current?: number;
62
+ /**
63
+ * Статус текущего шага, имеет приоритет над item.status
64
+ */
65
+ status?: StepStatus;
66
+ /**
67
+ * Обработчик изменения шага, делает индикаторы и заголовки шагов кликабельными
68
+ */
69
+ onChange?: (item: StepItemProps, index: number, prevIndex?: number) => void;
70
+ /**
71
+ * Массив шагов
72
+ */
73
+ items: StepItemProps[];
74
+ }
75
+
76
+ type IndicatorSizeSimple = 8 | 16;
77
+ type IndicatorSize = 24 | 36;
78
+
79
+ export type Steps = BaseSteps & {
80
+ /**
81
+ * Размер шага
82
+ */
83
+ size: 'xs' | 's';
84
+ indicatorSize: never;
85
+ } & {
86
+ /**
87
+ * Размер шага
88
+ */
89
+ size: 'm' | 'l';
90
+ /**
91
+ * Размер индикатора
92
+ */
93
+ indicatorSize: IndicatorSize | IndicatorSizeSimple;
94
+ };
95
+
96
+ ```
97
+
98
+ примеры использования https://codesandbox.io/p/devbox/crzql2?file=%2Fsrc%2FDemo.tsx%3A23%2C1
99
+
100
+ ```
101
+ import React, { useState } from 'react';
102
+ import { fullItems } from './App.data.tsx';
103
+ import { Steps } from './Steps/Steps.tsx';
104
+ import { StepItemProps, StepStatus } from './Steps/Steps.types';
105
+
106
+ const DemoContent: React.FC<{ onClick: () => void; status: string; index?: number; current?: number }> = ({
107
+ onClick,
108
+ status,
109
+ index,
110
+ current = 0,
111
+ children,
112
+ }) => {
113
+ return (
114
+ <>
115
+ <div>Content</div>
116
+ <button onClick={onClick} disabled={index !== (current || 0)}>
117
+ {children}
118
+ </button>
119
+ </>
120
+ );
121
+ };
122
+
123
+ export const DemoWithExternalControl = () => {
124
+ const items = [
125
+ {
126
+ title: 'Title',
127
+ indicator: '1',
128
+ content: 'Content',
129
+ },
130
+ {
131
+ title: 'Title',
132
+ indicator: '2',
133
+ content: 'Content',
134
+ },
135
+ {
136
+ title: 'Title',
137
+ indicator: '3',
138
+ content: 'Content',
139
+ },
140
+ {
141
+ title: 'Title',
142
+ indicator: '4',
143
+ content: 'Content',
144
+ },
145
+ ];
146
+
147
+ const [current, setCurrent] = useState(1);
148
+ const [status, setStatus] = useState('active');
149
+
150
+ const isLast = current === items.length - 1;
151
+
152
+ const onClick = () => {
153
+ if (isLast) {
154
+ setStatus('completed');
155
+ } else {
156
+ setCurrent(current + 1);
157
+ }
158
+ };
159
+
160
+ return (
161
+ <>
162
+ <Steps style={{ width: '1000px' }} current={current} status={status} items={items} />
163
+ <br />
164
+ <div>
165
+ External control:{' '}
166
+ <button onClick={onClick} style={{ width: '5rem' }}>
167
+ {isLast ? 'complete' : 'next'}
168
+ </button>
169
+ </div>
170
+ <hr />
171
+ </>
172
+ );
173
+ };
174
+
175
+ export const DemoWithInternalControls = () => {
176
+ const [current, setCurrent] = useState(undefined);
177
+ const [status, setStatus] = useState<StepStatus | undefined>();
178
+
179
+ const renderContent = (status, index, items) => {
180
+ const isFirst = index === 0 && current === undefined;
181
+ const isLast = index === items.length - 1;
182
+
183
+ const onNext = () => setCurrent((current) => (current === undefined ? 0 : current + 1));
184
+ const onComplete = () => {
185
+ setStatus('completed');
186
+ };
187
+
188
+ return (
189
+ <DemoContent onClick={isLast ? onComplete : onNext} index={index} status={status} current={current}>
190
+ {isFirst && 'go'}
191
+ {isLast && 'complete'}
192
+ {!isFirst && !isLast && 'next'}
193
+ </DemoContent>
194
+ );
195
+ };
196
+
197
+ return (
198
+ <Steps
199
+ current={current}
200
+ status={status}
201
+ items={[
202
+ {
203
+ title: 'Title',
204
+ indicator: '1',
205
+ content: renderContent,
206
+ },
207
+ {
208
+ title: 'Title',
209
+ indicator: '2',
210
+ content: renderContent,
211
+ },
212
+ {
213
+ title: 'Title',
214
+ indicator: '3',
215
+ content: renderContent,
216
+ },
217
+ {
218
+ title: 'Title',
219
+ indicator: '4',
220
+ content: renderContent,
221
+ },
222
+ ]}
223
+ />
224
+ );
225
+ };
226
+
227
+ export const DemoWithControlledUsage = () => {
228
+ const [exampleItems, setExampleItems] = useState<StepItemProps[]>(() =>
229
+ fullItems.map((item) => ({ ...item, status: 'inactive' }))
230
+ );
231
+
232
+ return (
233
+ <Steps
234
+ style={{ width: '1000px' }}
235
+ onChange={(item, index) => {
236
+ setExampleItems(
237
+ exampleItems.map((item, itemIndex) => {
238
+ if (index > itemIndex) {
239
+ return { ...item, status: 'completed' };
240
+ }
241
+
242
+ if (index === itemIndex) {
243
+ return { ...item, status: 'active' };
244
+ }
245
+
246
+ return { ...item, status: 'inactive' };
247
+ })
248
+ );
249
+ }}
250
+ items={exampleItems}
251
+ />
252
+ );
253
+ };
254
+
255
+ export const DemoWithControlledFreeUsage = () => {
256
+ const [items, setItems] = useState<StepItemProps[]>(() => fullItems.map((item) => ({ ...item, status: 'inactive' })));
257
+
258
+ return (
259
+ <Steps
260
+ style={{ width: '1000px' }}
261
+ onChange={(item, index, prevIndex) => {
262
+ console.log('!!! onChange', index, prevIndex);
263
+
264
+ const newItems = [...items];
265
+ if (prevIndex !== undefined) {
266
+ newItems[prevIndex].status = 'completed';
267
+ }
268
+ newItems[index].status = 'active';
269
+
270
+ setItems(newItems);
271
+ }}
272
+ items={items}
273
+ />
274
+ );
275
+ };
276
+
277
+ export const DemoControlledWithStatus = () => {
278
+ const [items, setItems] = useState<StepItemProps[]>(() => fullItems.map(({ status, ...item }) => ({ ...item })));
279
+ return (
280
+ <Steps
281
+ style={{ width: '1000px' }}
282
+ current={2}
283
+ status="active"
284
+ onClick={(item, index) => {
285
+ setItems(
286
+ items.map((item, itemIndex) => {
287
+ if (index > itemIndex) {
288
+ return { ...item, status: 'completed' };
289
+ }
290
+
291
+ if (index === itemIndex) {
292
+ return { ...item, status: 'active' };
293
+ }
294
+
295
+ return { ...item, status: 'inactive' };
296
+ })
297
+ );
298
+ }}
299
+ items={items}
300
+ />
301
+ );
302
+ };
303
+
304
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salutejs/plasma-new-hope",
3
- "version": "0.154.0-canary.1428.11047556363.0",
3
+ "version": "0.155.0-canary.1443.11051428691.0",
4
4
  "description": "Salute Design System blueprint",
5
5
  "main": "cjs/index.js",
6
6
  "module": "es/index.js",
@@ -123,5 +123,5 @@
123
123
  "react-popper": "2.3.0",
124
124
  "storeon": "3.1.5"
125
125
  },
126
- "gitHead": "f3bb2cd3c37b6050b31249e5c96aa11fc6231245"
126
+ "gitHead": "c0acc065687f22c1ff8f7e1afb3389e0de690999"
127
127
  }
@@ -0,0 +1,304 @@
1
+ ---
2
+ id: steps
3
+ title: Steps
4
+ ---
5
+
6
+ # Steps
7
+ Шаги могут отображаться в нескольких размерах, в упрощенном виде и в горизонтальной/вертикальной ориентации
8
+
9
+ ```tsx live
10
+ import { ReactNode } from 'react';
11
+
12
+ export type StepStatus = 'active' | 'inactive' | 'completed';
13
+
14
+ export interface StepItemProps {
15
+ /**
16
+ * Заголовок
17
+ */
18
+ title?: string;
19
+ /**
20
+ * Контент, может быть как React компонентом, так и функцией
21
+ */
22
+ content?: string | ReactNode | ((status: StepStatus, index: number, items: StepItemProps[]) => ReactNode);
23
+ /**
24
+ * Индикатор шага, может быть как React компонентом, так и функцией
25
+ */
26
+ indicator?: number | string | ReactNode | ((status: StepStatus, item: StepItemProps) => ReactNode | null);
27
+ /**
28
+ * Статус шага: активный, инактивный, завершенный
29
+ * @description
30
+ * active - активный шаг
31
+ * inactive - не пройденный шаг
32
+ * completed - пройденный шаг
33
+ */
34
+ status?: StepStatus;
35
+ /**
36
+ * Отключенный шаг
37
+ * @default false
38
+ */
39
+ disabled?: boolean;
40
+ }
41
+
42
+ interface BaseSteps {
43
+ /**
44
+ * Ориентация компонента
45
+ * @default 'horizontal'
46
+ */
47
+ orientation?: 'horizontal' | 'vertical';
48
+ /**
49
+ * Выравнивание контента в шагах
50
+ * @default 'left'
51
+ */
52
+ contentAlign?: 'left' | 'center';
53
+ /**
54
+ * Включает разделительную линию
55
+ * @default true
56
+ */
57
+ hasLine?: boolean;
58
+ /**
59
+ * Индекс текущего шага, для uncontrolled компонента
60
+ */
61
+ current?: number;
62
+ /**
63
+ * Статус текущего шага, имеет приоритет над item.status
64
+ */
65
+ status?: StepStatus;
66
+ /**
67
+ * Обработчик изменения шага, делает индикаторы и заголовки шагов кликабельными
68
+ */
69
+ onChange?: (item: StepItemProps, index: number, prevIndex?: number) => void;
70
+ /**
71
+ * Массив шагов
72
+ */
73
+ items: StepItemProps[];
74
+ }
75
+
76
+ type IndicatorSizeSimple = 8 | 16;
77
+ type IndicatorSize = 24 | 36;
78
+
79
+ export type Steps = BaseSteps & {
80
+ /**
81
+ * Размер шага
82
+ */
83
+ size: 'xs' | 's';
84
+ indicatorSize: never;
85
+ } & {
86
+ /**
87
+ * Размер шага
88
+ */
89
+ size: 'm' | 'l';
90
+ /**
91
+ * Размер индикатора
92
+ */
93
+ indicatorSize: IndicatorSize | IndicatorSizeSimple;
94
+ };
95
+
96
+ ```
97
+
98
+ примеры использования https://codesandbox.io/p/devbox/crzql2?file=%2Fsrc%2FDemo.tsx%3A23%2C1
99
+
100
+ ```
101
+ import React, { useState } from 'react';
102
+ import { fullItems } from './App.data.tsx';
103
+ import { Steps } from './Steps/Steps.tsx';
104
+ import { StepItemProps, StepStatus } from './Steps/Steps.types';
105
+
106
+ const DemoContent: React.FC<{ onClick: () => void; status: string; index?: number; current?: number }> = ({
107
+ onClick,
108
+ status,
109
+ index,
110
+ current = 0,
111
+ children,
112
+ }) => {
113
+ return (
114
+ <>
115
+ <div>Content</div>
116
+ <button onClick={onClick} disabled={index !== (current || 0)}>
117
+ {children}
118
+ </button>
119
+ </>
120
+ );
121
+ };
122
+
123
+ export const DemoWithExternalControl = () => {
124
+ const items = [
125
+ {
126
+ title: 'Title',
127
+ indicator: '1',
128
+ content: 'Content',
129
+ },
130
+ {
131
+ title: 'Title',
132
+ indicator: '2',
133
+ content: 'Content',
134
+ },
135
+ {
136
+ title: 'Title',
137
+ indicator: '3',
138
+ content: 'Content',
139
+ },
140
+ {
141
+ title: 'Title',
142
+ indicator: '4',
143
+ content: 'Content',
144
+ },
145
+ ];
146
+
147
+ const [current, setCurrent] = useState(1);
148
+ const [status, setStatus] = useState('active');
149
+
150
+ const isLast = current === items.length - 1;
151
+
152
+ const onClick = () => {
153
+ if (isLast) {
154
+ setStatus('completed');
155
+ } else {
156
+ setCurrent(current + 1);
157
+ }
158
+ };
159
+
160
+ return (
161
+ <>
162
+ <Steps style={{ width: '1000px' }} current={current} status={status} items={items} />
163
+ <br />
164
+ <div>
165
+ External control:{' '}
166
+ <button onClick={onClick} style={{ width: '5rem' }}>
167
+ {isLast ? 'complete' : 'next'}
168
+ </button>
169
+ </div>
170
+ <hr />
171
+ </>
172
+ );
173
+ };
174
+
175
+ export const DemoWithInternalControls = () => {
176
+ const [current, setCurrent] = useState(undefined);
177
+ const [status, setStatus] = useState<StepStatus | undefined>();
178
+
179
+ const renderContent = (status, index, items) => {
180
+ const isFirst = index === 0 && current === undefined;
181
+ const isLast = index === items.length - 1;
182
+
183
+ const onNext = () => setCurrent((current) => (current === undefined ? 0 : current + 1));
184
+ const onComplete = () => {
185
+ setStatus('completed');
186
+ };
187
+
188
+ return (
189
+ <DemoContent onClick={isLast ? onComplete : onNext} index={index} status={status} current={current}>
190
+ {isFirst && 'go'}
191
+ {isLast && 'complete'}
192
+ {!isFirst && !isLast && 'next'}
193
+ </DemoContent>
194
+ );
195
+ };
196
+
197
+ return (
198
+ <Steps
199
+ current={current}
200
+ status={status}
201
+ items={[
202
+ {
203
+ title: 'Title',
204
+ indicator: '1',
205
+ content: renderContent,
206
+ },
207
+ {
208
+ title: 'Title',
209
+ indicator: '2',
210
+ content: renderContent,
211
+ },
212
+ {
213
+ title: 'Title',
214
+ indicator: '3',
215
+ content: renderContent,
216
+ },
217
+ {
218
+ title: 'Title',
219
+ indicator: '4',
220
+ content: renderContent,
221
+ },
222
+ ]}
223
+ />
224
+ );
225
+ };
226
+
227
+ export const DemoWithControlledUsage = () => {
228
+ const [exampleItems, setExampleItems] = useState<StepItemProps[]>(() =>
229
+ fullItems.map((item) => ({ ...item, status: 'inactive' }))
230
+ );
231
+
232
+ return (
233
+ <Steps
234
+ style={{ width: '1000px' }}
235
+ onChange={(item, index) => {
236
+ setExampleItems(
237
+ exampleItems.map((item, itemIndex) => {
238
+ if (index > itemIndex) {
239
+ return { ...item, status: 'completed' };
240
+ }
241
+
242
+ if (index === itemIndex) {
243
+ return { ...item, status: 'active' };
244
+ }
245
+
246
+ return { ...item, status: 'inactive' };
247
+ })
248
+ );
249
+ }}
250
+ items={exampleItems}
251
+ />
252
+ );
253
+ };
254
+
255
+ export const DemoWithControlledFreeUsage = () => {
256
+ const [items, setItems] = useState<StepItemProps[]>(() => fullItems.map((item) => ({ ...item, status: 'inactive' })));
257
+
258
+ return (
259
+ <Steps
260
+ style={{ width: '1000px' }}
261
+ onChange={(item, index, prevIndex) => {
262
+ console.log('!!! onChange', index, prevIndex);
263
+
264
+ const newItems = [...items];
265
+ if (prevIndex !== undefined) {
266
+ newItems[prevIndex].status = 'completed';
267
+ }
268
+ newItems[index].status = 'active';
269
+
270
+ setItems(newItems);
271
+ }}
272
+ items={items}
273
+ />
274
+ );
275
+ };
276
+
277
+ export const DemoControlledWithStatus = () => {
278
+ const [items, setItems] = useState<StepItemProps[]>(() => fullItems.map(({ status, ...item }) => ({ ...item })));
279
+ return (
280
+ <Steps
281
+ style={{ width: '1000px' }}
282
+ current={2}
283
+ status="active"
284
+ onClick={(item, index) => {
285
+ setItems(
286
+ items.map((item, itemIndex) => {
287
+ if (index > itemIndex) {
288
+ return { ...item, status: 'completed' };
289
+ }
290
+
291
+ if (index === itemIndex) {
292
+ return { ...item, status: 'active' };
293
+ }
294
+
295
+ return { ...item, status: 'inactive' };
296
+ })
297
+ );
298
+ }}
299
+ items={items}
300
+ />
301
+ );
302
+ };
303
+
304
+ ```
@@ -0,0 +1,304 @@
1
+ ---
2
+ id: steps
3
+ title: Steps
4
+ ---
5
+
6
+ # Steps
7
+ Шаги могут отображаться в нескольких размерах, в упрощенном виде и в горизонтальной/вертикальной ориентации
8
+
9
+ ```tsx live
10
+ import { ReactNode } from 'react';
11
+
12
+ export type StepStatus = 'active' | 'inactive' | 'completed';
13
+
14
+ export interface StepItemProps {
15
+ /**
16
+ * Заголовок
17
+ */
18
+ title?: string;
19
+ /**
20
+ * Контент, может быть как React компонентом, так и функцией
21
+ */
22
+ content?: string | ReactNode | ((status: StepStatus, index: number, items: StepItemProps[]) => ReactNode);
23
+ /**
24
+ * Индикатор шага, может быть как React компонентом, так и функцией
25
+ */
26
+ indicator?: number | string | ReactNode | ((status: StepStatus, item: StepItemProps) => ReactNode | null);
27
+ /**
28
+ * Статус шага: активный, инактивный, завершенный
29
+ * @description
30
+ * active - активный шаг
31
+ * inactive - не пройденный шаг
32
+ * completed - пройденный шаг
33
+ */
34
+ status?: StepStatus;
35
+ /**
36
+ * Отключенный шаг
37
+ * @default false
38
+ */
39
+ disabled?: boolean;
40
+ }
41
+
42
+ interface BaseSteps {
43
+ /**
44
+ * Ориентация компонента
45
+ * @default 'horizontal'
46
+ */
47
+ orientation?: 'horizontal' | 'vertical';
48
+ /**
49
+ * Выравнивание контента в шагах
50
+ * @default 'left'
51
+ */
52
+ contentAlign?: 'left' | 'center';
53
+ /**
54
+ * Включает разделительную линию
55
+ * @default true
56
+ */
57
+ hasLine?: boolean;
58
+ /**
59
+ * Индекс текущего шага, для uncontrolled компонента
60
+ */
61
+ current?: number;
62
+ /**
63
+ * Статус текущего шага, имеет приоритет над item.status
64
+ */
65
+ status?: StepStatus;
66
+ /**
67
+ * Обработчик изменения шага, делает индикаторы и заголовки шагов кликабельными
68
+ */
69
+ onChange?: (item: StepItemProps, index: number, prevIndex?: number) => void;
70
+ /**
71
+ * Массив шагов
72
+ */
73
+ items: StepItemProps[];
74
+ }
75
+
76
+ type IndicatorSizeSimple = 8 | 16;
77
+ type IndicatorSize = 24 | 36;
78
+
79
+ export type Steps = BaseSteps & {
80
+ /**
81
+ * Размер шага
82
+ */
83
+ size: 'xs' | 's';
84
+ indicatorSize: never;
85
+ } & {
86
+ /**
87
+ * Размер шага
88
+ */
89
+ size: 'm' | 'l';
90
+ /**
91
+ * Размер индикатора
92
+ */
93
+ indicatorSize: IndicatorSize | IndicatorSizeSimple;
94
+ };
95
+
96
+ ```
97
+
98
+ примеры использования https://codesandbox.io/p/devbox/crzql2?file=%2Fsrc%2FDemo.tsx%3A23%2C1
99
+
100
+ ```
101
+ import React, { useState } from 'react';
102
+ import { fullItems } from './App.data.tsx';
103
+ import { Steps } from './Steps/Steps.tsx';
104
+ import { StepItemProps, StepStatus } from './Steps/Steps.types';
105
+
106
+ const DemoContent: React.FC<{ onClick: () => void; status: string; index?: number; current?: number }> = ({
107
+ onClick,
108
+ status,
109
+ index,
110
+ current = 0,
111
+ children,
112
+ }) => {
113
+ return (
114
+ <>
115
+ <div>Content</div>
116
+ <button onClick={onClick} disabled={index !== (current || 0)}>
117
+ {children}
118
+ </button>
119
+ </>
120
+ );
121
+ };
122
+
123
+ export const DemoWithExternalControl = () => {
124
+ const items = [
125
+ {
126
+ title: 'Title',
127
+ indicator: '1',
128
+ content: 'Content',
129
+ },
130
+ {
131
+ title: 'Title',
132
+ indicator: '2',
133
+ content: 'Content',
134
+ },
135
+ {
136
+ title: 'Title',
137
+ indicator: '3',
138
+ content: 'Content',
139
+ },
140
+ {
141
+ title: 'Title',
142
+ indicator: '4',
143
+ content: 'Content',
144
+ },
145
+ ];
146
+
147
+ const [current, setCurrent] = useState(1);
148
+ const [status, setStatus] = useState('active');
149
+
150
+ const isLast = current === items.length - 1;
151
+
152
+ const onClick = () => {
153
+ if (isLast) {
154
+ setStatus('completed');
155
+ } else {
156
+ setCurrent(current + 1);
157
+ }
158
+ };
159
+
160
+ return (
161
+ <>
162
+ <Steps style={{ width: '1000px' }} current={current} status={status} items={items} />
163
+ <br />
164
+ <div>
165
+ External control:{' '}
166
+ <button onClick={onClick} style={{ width: '5rem' }}>
167
+ {isLast ? 'complete' : 'next'}
168
+ </button>
169
+ </div>
170
+ <hr />
171
+ </>
172
+ );
173
+ };
174
+
175
+ export const DemoWithInternalControls = () => {
176
+ const [current, setCurrent] = useState(undefined);
177
+ const [status, setStatus] = useState<StepStatus | undefined>();
178
+
179
+ const renderContent = (status, index, items) => {
180
+ const isFirst = index === 0 && current === undefined;
181
+ const isLast = index === items.length - 1;
182
+
183
+ const onNext = () => setCurrent((current) => (current === undefined ? 0 : current + 1));
184
+ const onComplete = () => {
185
+ setStatus('completed');
186
+ };
187
+
188
+ return (
189
+ <DemoContent onClick={isLast ? onComplete : onNext} index={index} status={status} current={current}>
190
+ {isFirst && 'go'}
191
+ {isLast && 'complete'}
192
+ {!isFirst && !isLast && 'next'}
193
+ </DemoContent>
194
+ );
195
+ };
196
+
197
+ return (
198
+ <Steps
199
+ current={current}
200
+ status={status}
201
+ items={[
202
+ {
203
+ title: 'Title',
204
+ indicator: '1',
205
+ content: renderContent,
206
+ },
207
+ {
208
+ title: 'Title',
209
+ indicator: '2',
210
+ content: renderContent,
211
+ },
212
+ {
213
+ title: 'Title',
214
+ indicator: '3',
215
+ content: renderContent,
216
+ },
217
+ {
218
+ title: 'Title',
219
+ indicator: '4',
220
+ content: renderContent,
221
+ },
222
+ ]}
223
+ />
224
+ );
225
+ };
226
+
227
+ export const DemoWithControlledUsage = () => {
228
+ const [exampleItems, setExampleItems] = useState<StepItemProps[]>(() =>
229
+ fullItems.map((item) => ({ ...item, status: 'inactive' }))
230
+ );
231
+
232
+ return (
233
+ <Steps
234
+ style={{ width: '1000px' }}
235
+ onChange={(item, index) => {
236
+ setExampleItems(
237
+ exampleItems.map((item, itemIndex) => {
238
+ if (index > itemIndex) {
239
+ return { ...item, status: 'completed' };
240
+ }
241
+
242
+ if (index === itemIndex) {
243
+ return { ...item, status: 'active' };
244
+ }
245
+
246
+ return { ...item, status: 'inactive' };
247
+ })
248
+ );
249
+ }}
250
+ items={exampleItems}
251
+ />
252
+ );
253
+ };
254
+
255
+ export const DemoWithControlledFreeUsage = () => {
256
+ const [items, setItems] = useState<StepItemProps[]>(() => fullItems.map((item) => ({ ...item, status: 'inactive' })));
257
+
258
+ return (
259
+ <Steps
260
+ style={{ width: '1000px' }}
261
+ onChange={(item, index, prevIndex) => {
262
+ console.log('!!! onChange', index, prevIndex);
263
+
264
+ const newItems = [...items];
265
+ if (prevIndex !== undefined) {
266
+ newItems[prevIndex].status = 'completed';
267
+ }
268
+ newItems[index].status = 'active';
269
+
270
+ setItems(newItems);
271
+ }}
272
+ items={items}
273
+ />
274
+ );
275
+ };
276
+
277
+ export const DemoControlledWithStatus = () => {
278
+ const [items, setItems] = useState<StepItemProps[]>(() => fullItems.map(({ status, ...item }) => ({ ...item })));
279
+ return (
280
+ <Steps
281
+ style={{ width: '1000px' }}
282
+ current={2}
283
+ status="active"
284
+ onClick={(item, index) => {
285
+ setItems(
286
+ items.map((item, itemIndex) => {
287
+ if (index > itemIndex) {
288
+ return { ...item, status: 'completed' };
289
+ }
290
+
291
+ if (index === itemIndex) {
292
+ return { ...item, status: 'active' };
293
+ }
294
+
295
+ return { ...item, status: 'inactive' };
296
+ })
297
+ );
298
+ }}
299
+ items={items}
300
+ />
301
+ );
302
+ };
303
+
304
+ ```