@cloud-ru/ds-ai-queue 1.0.0

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +201 -0
  3. package/README.md +56 -0
  4. package/dist/cjs/AiQueue.d.ts +3 -0
  5. package/dist/cjs/AiQueue.js +29 -0
  6. package/dist/cjs/AiQueueStepStatus.d.ts +7 -0
  7. package/dist/cjs/AiQueueStepStatus.js +25 -0
  8. package/dist/cjs/constants.d.ts +15 -0
  9. package/dist/cjs/constants.js +22 -0
  10. package/dist/cjs/index.d.ts +3 -0
  11. package/dist/cjs/index.js +19 -0
  12. package/dist/cjs/package.json +3 -0
  13. package/dist/cjs/style.css +148 -0
  14. package/dist/cjs/styles.module.css +148 -0
  15. package/dist/cjs/types.d.ts +38 -0
  16. package/dist/cjs/types.js +2 -0
  17. package/dist/cjs/utils.d.ts +3 -0
  18. package/dist/cjs/utils.js +29 -0
  19. package/dist/esm/AiQueue.d.ts +3 -0
  20. package/dist/esm/AiQueue.js +23 -0
  21. package/dist/esm/AiQueueStepStatus.d.ts +7 -0
  22. package/dist/esm/AiQueueStepStatus.js +19 -0
  23. package/dist/esm/constants.d.ts +15 -0
  24. package/dist/esm/constants.js +19 -0
  25. package/dist/esm/index.d.ts +3 -0
  26. package/dist/esm/index.js +3 -0
  27. package/dist/esm/style.css +148 -0
  28. package/dist/esm/styles.module.css +148 -0
  29. package/dist/esm/types.d.ts +38 -0
  30. package/dist/esm/types.js +1 -0
  31. package/dist/esm/utils.d.ts +3 -0
  32. package/dist/esm/utils.js +25 -0
  33. package/dist/tsconfig.cjs.tsbuildinfo +1 -0
  34. package/dist/tsconfig.esm.tsbuildinfo +1 -0
  35. package/package.json +45 -0
  36. package/src/AiQueue.tsx +84 -0
  37. package/src/AiQueueStepStatus.tsx +27 -0
  38. package/src/constants.ts +23 -0
  39. package/src/index.ts +3 -0
  40. package/src/styles.module.scss +145 -0
  41. package/src/types.ts +47 -0
  42. package/src/utils.ts +33 -0
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@cloud-ru/ds-ai-queue",
3
+ "version": "1.0.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/cloud-ru-tech/snack-v2.git",
7
+ "directory": "packages/ai-queue"
8
+ },
9
+ "homepage": "https://github.com/cloud-ru-tech/snack-v2/tree/master/packages/ai-queue",
10
+ "license": "Apache-2.0",
11
+ "author": "Артем Сауляк <avsaulyak@cloud.ru>",
12
+ "type": "module",
13
+ "main": "./dist/cjs/index.js",
14
+ "module": "./dist/esm/index.js",
15
+ "types": "./dist/esm/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "source": "./src/index.ts",
19
+ "types": "./dist/esm/index.d.ts",
20
+ "import": "./dist/esm/index.js",
21
+ "require": "./dist/cjs/index.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "src",
27
+ "./CHANGELOG.md",
28
+ "./LICENSE"
29
+ ],
30
+ "sideEffects": [
31
+ "*.css",
32
+ "**/*.css"
33
+ ],
34
+ "dependencies": {
35
+ "classnames": "^2.5.1",
36
+ "@cloud-ru/ds-ai-button-chevron": "^1.0.0",
37
+ "@cloud-ru/ds-icons": "^1.0.0",
38
+ "@cloud-ru/ds-utils": "^1.0.0",
39
+ "@cloud-ru/ds-loader": "^1.0.0",
40
+ "@cloud-ru/ds-scroll": "^1.0.0"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
45
+ }
@@ -0,0 +1,84 @@
1
+ import { AiButtonChevron } from '@cloud-ru/ds-ai-button-chevron';
2
+ import { Scroll } from '@cloud-ru/ds-scroll';
3
+ import { useUncontrolledProp } from '@cloud-ru/ds-utils';
4
+ import cn from 'classnames';
5
+ import { ReactElement, useMemo } from 'react';
6
+
7
+ import { AiQueueStepStatus } from './AiQueueStepStatus';
8
+ import { DEFAULT_LABELS, TEST_IDS } from './constants';
9
+ import styles from './styles.module.scss';
10
+ import { AiQueueProps } from './types';
11
+ import { calculateSummary, resolveStepState } from './utils';
12
+
13
+ export function AiQueue({
14
+ className,
15
+ steps = [],
16
+ summary,
17
+ labels,
18
+ open: openProp,
19
+ defaultOpen = false,
20
+ onOpenChange,
21
+ 'data-test-id': dataTestId = TEST_IDS.root,
22
+ ...rest
23
+ }: AiQueueProps): ReactElement {
24
+ const [open, setOpen] = useUncontrolledProp(openProp, defaultOpen, onOpenChange);
25
+
26
+ const mergedLabels = { ...DEFAULT_LABELS, ...labels };
27
+ const derivedSummary = useMemo(() => calculateSummary(steps), [steps]);
28
+ const total = summary?.total ?? derivedSummary.total;
29
+ const planned = summary?.planned ?? derivedSummary.planned;
30
+ const progress = summary?.progress ?? derivedSummary.progress;
31
+ const done = summary?.done ?? derivedSummary.done;
32
+
33
+ return (
34
+ <div {...rest} className={cn(styles.root, className)} data-open={open || undefined} data-test-id={dataTestId}>
35
+ <button
36
+ type='button'
37
+ className={styles.trigger}
38
+ aria-expanded={open}
39
+ data-test-id={TEST_IDS.trigger}
40
+ onClick={() => setOpen(!open)}
41
+ >
42
+ <span className={styles.summary} data-test-id={TEST_IDS.summary}>
43
+ <span>{total}</span>
44
+ <span>{mergedLabels.tasks}</span>
45
+ <span>(</span>
46
+ <span>{planned}</span>
47
+ <span>{mergedLabels.planned}</span>
48
+ <span>/</span>
49
+ <span>{progress}</span>
50
+ <span>{mergedLabels.inProgress}</span>
51
+ <span>/</span>
52
+ <span>{done}</span>
53
+ <span>{mergedLabels.done}</span>
54
+ <span>)</span>
55
+ </span>
56
+ <AiButtonChevron opened={open} interactive={false} className={styles.chevron} />
57
+ </button>
58
+
59
+ {open && steps.length > 0 && (
60
+ <Scroll size='s' barHideStrategy='leave' className={styles.content} data-test-id={TEST_IDS.content}>
61
+ <div className={styles.steps}>
62
+ {steps.map((step, index) => {
63
+ const state = resolveStepState(step.state);
64
+
65
+ return (
66
+ <div
67
+ key={step.id ?? `${index}-${step.label}`}
68
+ className={styles.step}
69
+ data-state={state}
70
+ data-test-id={`${TEST_IDS.step}-${index}`}
71
+ >
72
+ <span className={styles.stepStatus} aria-hidden>
73
+ <AiQueueStepStatus state={state} />
74
+ </span>
75
+ <span className={styles.stepLabel}>{step.label}</span>
76
+ </div>
77
+ );
78
+ })}
79
+ </div>
80
+ </Scroll>
81
+ )}
82
+ </div>
83
+ );
84
+ }
@@ -0,0 +1,27 @@
1
+ import { CheckSVG, CrossSVG } from '@cloud-ru/ds-icons/interface/system';
2
+ import { Sun, SUN_SIZE } from '@cloud-ru/ds-loader';
3
+ import cn from 'classnames';
4
+ import { ReactElement } from 'react';
5
+
6
+ import { AI_QUEUE_STEP_STATE } from './constants';
7
+ import styles from './styles.module.scss';
8
+ import { AiQueueStepState } from './types';
9
+
10
+ const STEP_STATUS_ICON_SIZE = 16;
11
+
12
+ type AiQueueStepStatusProps = {
13
+ state: AiQueueStepState;
14
+ };
15
+
16
+ export function AiQueueStepStatus({ state }: AiQueueStepStatusProps): ReactElement {
17
+ switch (state) {
18
+ case AI_QUEUE_STEP_STATE.Done:
19
+ return <CheckSVG size={STEP_STATUS_ICON_SIZE} className={cn(styles.stepStatusIcon, styles.stepStatusSuccess)} />;
20
+ case AI_QUEUE_STEP_STATE.Error:
21
+ return <CrossSVG size={STEP_STATUS_ICON_SIZE} className={cn(styles.stepStatusIcon, styles.stepStatusError)} />;
22
+ case AI_QUEUE_STEP_STATE.Progress:
23
+ return <Sun size={SUN_SIZE.XS} className={cn(styles.stepStatusIcon, styles.stepStatusProgress)} />;
24
+ default:
25
+ return <span className={styles.stepStatusPending} />;
26
+ }
27
+ }
@@ -0,0 +1,23 @@
1
+ import type { AiQueueLabels } from './types';
2
+
3
+ export const AI_QUEUE_STEP_STATE = {
4
+ Planned: 'planned',
5
+ Progress: 'progress',
6
+ Done: 'done',
7
+ Error: 'error',
8
+ } as const;
9
+
10
+ export const DEFAULT_LABELS: AiQueueLabels = {
11
+ tasks: 'задач',
12
+ planned: 'запланировано',
13
+ inProgress: 'в процессе',
14
+ done: 'готово',
15
+ };
16
+
17
+ export const TEST_IDS = {
18
+ root: 'ai-queue',
19
+ trigger: 'ai-queue__trigger',
20
+ summary: 'ai-queue__summary',
21
+ content: 'ai-queue__content',
22
+ step: 'ai-queue__step',
23
+ } as const;
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './AiQueue';
2
+ export * from './constants';
3
+ export * from './types';
@@ -0,0 +1,145 @@
1
+ @use '@cloud-ru/ds-figma-variables/build/scss/styles/styles.module' as base;
2
+
3
+ .root {
4
+ position: relative;
5
+
6
+ display: flex;
7
+ flex-direction: column;
8
+ row-gap: 0;
9
+ align-items: stretch;
10
+
11
+ width: 100%;
12
+ padding-top: base.$sn-primitive-dimension-4;
13
+ border-radius: base.$sn-density-radius-2xs;
14
+ }
15
+
16
+ .root[data-open] {
17
+ row-gap: base.$sn-primitive-dimension-8;
18
+ }
19
+
20
+ .trigger {
21
+ @include base.composite-var(base.$base-styles, 'sn', 'thin', 'label', 's');
22
+
23
+ cursor: pointer;
24
+
25
+ display: flex;
26
+ column-gap: base.$sn-primitive-dimension-4;
27
+ align-items: center;
28
+
29
+ width: 100%;
30
+ min-width: 0;
31
+ margin: 0;
32
+ padding: 0;
33
+ border: 0;
34
+ border-radius: base.$sn-density-radius-2xs;
35
+
36
+ color: base.$sn-theme-color-available-version-textSecondary;
37
+ text-align: left;
38
+
39
+ appearance: none;
40
+ background: transparent;
41
+ outline: none;
42
+
43
+ &:focus-visible {
44
+ outline: base.$sn-primitive-strokeWeight-strokeSemiBold solid base.$sn-theme-color-primary-accent;
45
+ outline-offset: 0;
46
+ }
47
+ }
48
+
49
+ .summary {
50
+ overflow: hidden;
51
+ display: inline-flex;
52
+ column-gap: base.$sn-primitive-dimension-2;
53
+ align-items: center;
54
+
55
+ text-overflow: ellipsis;
56
+ white-space: nowrap;
57
+ }
58
+
59
+ .chevron {
60
+ flex-shrink: 0;
61
+ color: base.$sn-theme-color-available-version-textTertiary;
62
+ }
63
+
64
+ .content {
65
+ max-height: 140px;
66
+ }
67
+
68
+ .steps {
69
+ display: flex;
70
+ flex-direction: column;
71
+ row-gap: base.$sn-primitive-dimension-8;
72
+ align-items: stretch;
73
+
74
+ padding-right: base.$sn-primitive-dimension-8;
75
+ }
76
+
77
+ .step {
78
+ display: flex;
79
+ column-gap: base.$sn-primitive-dimension-4;
80
+ align-items: center;
81
+
82
+ width: 100%;
83
+ min-width: 0;
84
+ }
85
+
86
+ .stepStatus {
87
+ display: inline-flex;
88
+ flex-shrink: 0;
89
+ align-items: center;
90
+ justify-content: center;
91
+
92
+ width: base.$sn-primitive-dimension-16;
93
+ height: base.$sn-primitive-dimension-16;
94
+ }
95
+
96
+ .stepStatusIcon {
97
+ width: 100%;
98
+ height: 100%;
99
+ }
100
+
101
+ .stepStatusSuccess {
102
+ color: base.$sn-theme-color-primary-accent;
103
+ }
104
+
105
+ .stepStatusError {
106
+ color: base.$sn-theme-color-red-accent;
107
+ }
108
+
109
+ .stepStatusProgress {
110
+ color: base.$sn-theme-color-blue-accent;
111
+ }
112
+
113
+ .stepStatusPending {
114
+ box-sizing: border-box;
115
+ width: base.$sn-primitive-dimension-8;
116
+ height: base.$sn-primitive-dimension-8;
117
+ border: base.$sn-primitive-strokeWeight-strokeRegular solid base.$sn-theme-color-neutral-decor;
118
+ border-radius: base.$sn-density-radius-2xs;
119
+ }
120
+
121
+ .stepLabel {
122
+ @include base.composite-var(base.$base-styles, 'sn', 'thin', 'label', 's');
123
+
124
+ overflow: hidden;
125
+ flex: 1 1 auto;
126
+
127
+ min-width: 0;
128
+
129
+ color: base.$sn-theme-color-available-version-textMain;
130
+ text-overflow: ellipsis;
131
+ white-space: nowrap;
132
+ }
133
+
134
+ .step[data-state='progress'] .stepLabel {
135
+ @include base.composite-var(base.$base-styles, 'sn', 'regular', 'label', 's');
136
+ }
137
+
138
+ .step[data-state='error'] .stepLabel,
139
+ .step[data-state='done'] .stepLabel {
140
+ color: base.$sn-theme-color-available-version-textTertiary;
141
+ }
142
+
143
+ .step[data-state='done'] .stepLabel {
144
+ text-decoration: line-through;
145
+ }
package/src/types.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { ValueOf, WithSupportProps } from '@cloud-ru/ds-utils';
2
+ import type { ComponentPropsWithoutRef } from 'react';
3
+
4
+ import { AI_QUEUE_STEP_STATE } from './constants';
5
+
6
+ export type AiQueueStepState = ValueOf<typeof AI_QUEUE_STEP_STATE>;
7
+
8
+ export type AiQueueStep = {
9
+ id?: string | number;
10
+ label: string;
11
+ state?: AiQueueStepState;
12
+ };
13
+
14
+ export type AiQueueSummary = {
15
+ total?: number;
16
+ planned?: number;
17
+ progress?: number;
18
+ done?: number;
19
+ };
20
+
21
+ export type AiQueueLabels = {
22
+ tasks: string;
23
+ planned: string;
24
+ inProgress: string;
25
+ done: string;
26
+ };
27
+
28
+ export type AiQueueOwnProps = {
29
+ /** Шаги очереди. */
30
+ steps?: AiQueueStep[];
31
+ /** Принудительные счётчики в заголовке; если не заданы, считаются по `steps`. */
32
+ summary?: AiQueueSummary;
33
+ /** Контролируемое состояние раскрытия. */
34
+ open?: boolean;
35
+ /** Начальное состояние раскрытия в uncontrolled-режиме. */
36
+ defaultOpen?: boolean;
37
+ /** Коллбек изменения раскрытия. */
38
+ onOpenChange?: (open: boolean) => void;
39
+ /** Тексты счётчика в заголовке. */
40
+ labels?: Partial<AiQueueLabels>;
41
+ /** Дополнительный CSS-класс корня. */
42
+ className?: string;
43
+ };
44
+
45
+ export type AiQueueProps = WithSupportProps<
46
+ AiQueueOwnProps & Omit<ComponentPropsWithoutRef<'div'>, keyof AiQueueOwnProps | 'children'>
47
+ >;
package/src/utils.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { AI_QUEUE_STEP_STATE } from './constants';
2
+ import { AiQueueStep, AiQueueSummary } from './types';
3
+
4
+ export function resolveStepState(state?: AiQueueStep['state']) {
5
+ return state ?? AI_QUEUE_STEP_STATE.Planned;
6
+ }
7
+
8
+ export function calculateSummary(steps: AiQueueStep[]): Required<AiQueueSummary> {
9
+ return steps.reduce<Required<AiQueueSummary>>(
10
+ (acc, step) => {
11
+ const state = resolveStepState(step.state);
12
+
13
+ acc.total += 1;
14
+ if (state === AI_QUEUE_STEP_STATE.Planned) {
15
+ acc.planned += 1;
16
+ }
17
+ if (state === AI_QUEUE_STEP_STATE.Progress) {
18
+ acc.progress += 1;
19
+ }
20
+ if (state === AI_QUEUE_STEP_STATE.Done) {
21
+ acc.done += 1;
22
+ }
23
+
24
+ return acc;
25
+ },
26
+ {
27
+ total: 0,
28
+ planned: 0,
29
+ progress: 0,
30
+ done: 0,
31
+ },
32
+ );
33
+ }