@m6d/cortex-react 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 (37) hide show
  1. package/dist/index.d.ts +70 -0
  2. package/dist/index.js +3 -0
  3. package/dist/styles.css +2652 -0
  4. package/dist/theme.css +69 -0
  5. package/index.ts +17 -0
  6. package/package.json +67 -0
  7. package/src/chat-session.tsx +379 -0
  8. package/src/components/AttachmentQueue.tsx +75 -0
  9. package/src/components/ChatComposer.tsx +211 -0
  10. package/src/components/CopyButton.tsx +73 -0
  11. package/src/components/CortexChatWidget.tsx +538 -0
  12. package/src/components/JsonTree.tsx +111 -0
  13. package/src/components/LlmUsageBreakdown.tsx +42 -0
  14. package/src/components/LlmUsageChips.tsx +37 -0
  15. package/src/components/Message.tsx +81 -0
  16. package/src/components/MessageAbortedFlag.tsx +29 -0
  17. package/src/components/MessageAttachments.tsx +55 -0
  18. package/src/components/MessageList.tsx +102 -0
  19. package/src/components/MessageLlmInspector.tsx +184 -0
  20. package/src/components/MessagePart.tsx +54 -0
  21. package/src/components/MessageReasoningAnimated.tsx +12 -0
  22. package/src/components/MessageReasoningPart.tsx +76 -0
  23. package/src/components/MessageTextPart.tsx +78 -0
  24. package/src/components/MessageTokenUsage.tsx +145 -0
  25. package/src/components/MessageToolCallAnimated.tsx +86 -0
  26. package/src/components/MessageToolCallOutcome.tsx +95 -0
  27. package/src/components/MessageToolCallPart.tsx +95 -0
  28. package/src/components/MessageToolCallStatus.tsx +35 -0
  29. package/src/components/SubtleActivity.tsx +74 -0
  30. package/src/components/ThreadList.tsx +219 -0
  31. package/src/components/ToolAnimation.tsx +22 -0
  32. package/src/components/ToolExecuteCodeAnimated.tsx +8 -0
  33. package/src/components/ToolQueryGraphAnimated.tsx +8 -0
  34. package/src/config.ts +14 -0
  35. package/src/context.ts +48 -0
  36. package/src/cx.ts +4 -0
  37. package/src/format.ts +4 -0
@@ -0,0 +1,78 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import type { TextPart, UIMessage } from '@tanstack/ai';
3
+ import { renderMarkdown, StreamTextSmoother } from '@cortex/client';
4
+ import { cx } from '../cx';
5
+
6
+ type MessageTextPartProps = {
7
+ role: UIMessage['role'];
8
+ textPart: TextPart;
9
+ streaming?: boolean;
10
+ };
11
+
12
+ export function MessageTextPart(props: MessageTextPartProps) {
13
+ const { role, textPart, streaming = false } = props;
14
+
15
+ // Non-streaming text paints on the first frame — only a streaming assistant
16
+ // part starts empty and drains in through the smoother (Angular's constructor
17
+ // effect ran before the first render, so it never committed an empty bubble).
18
+ const initialText = role === 'assistant' && streaming ? '' : textPart.content;
19
+ const [displayedText, setDisplayedText] = useState(initialText);
20
+ const displayedTextRef = useRef(initialText);
21
+ const smootherRef = useRef<StreamTextSmoother | null>(null);
22
+ const isFirstRenderRef = useRef(true);
23
+
24
+ useEffect(() => {
25
+ function setText(text: string) {
26
+ displayedTextRef.current = text;
27
+ setDisplayedText(text);
28
+ }
29
+
30
+ if (role !== 'assistant' || !streaming) {
31
+ smootherRef.current?.destroy();
32
+ smootherRef.current = null;
33
+ setText(textPart.content);
34
+ isFirstRenderRef.current = false;
35
+ return;
36
+ }
37
+
38
+ if (!smootherRef.current) {
39
+ smootherRef.current = new StreamTextSmoother(setText);
40
+ smootherRef.current.seed(displayedTextRef.current);
41
+ }
42
+
43
+ if (isFirstRenderRef.current) {
44
+ isFirstRenderRef.current = false;
45
+ smootherRef.current.seed(textPart.content);
46
+ return;
47
+ }
48
+
49
+ smootherRef.current.update(textPart.content, false);
50
+ }, [role, streaming, textPart]);
51
+
52
+ useEffect(
53
+ () => () => {
54
+ smootherRef.current?.destroy();
55
+ smootherRef.current = null;
56
+ },
57
+ [],
58
+ );
59
+
60
+ return (
61
+ <div
62
+ className={cx(
63
+ 'cortex-text-part',
64
+ role === 'assistant' && 'cortex-text-part--assistant',
65
+ role === 'user' && 'cortex-text-part--user',
66
+ )}
67
+ >
68
+ <div
69
+ className={cx(
70
+ 'cortex-text-bubble',
71
+ role === 'assistant' && 'cortex-text-bubble--assistant',
72
+ role === 'user' && 'cortex-text-bubble--user',
73
+ )}
74
+ dangerouslySetInnerHTML={{ __html: renderMarkdown(displayedText) }}
75
+ />
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,145 @@
1
+ import { useState } from 'react';
2
+ import type { MessageMetadata } from '@cortex/contracts/wire';
3
+ import { cachePercent } from '@cortex/client';
4
+ import { useCortex } from '../context';
5
+ import { cx } from '../cx';
6
+ import { num } from '../format';
7
+
8
+ type MessageTokenUsageProps = {
9
+ usage: NonNullable<MessageMetadata['tokenUsage']>;
10
+ modelId?: string;
11
+ };
12
+
13
+ export function MessageTokenUsage({ usage, modelId }: MessageTokenUsageProps) {
14
+ const { t } = useCortex();
15
+ const [expanded, setExpanded] = useState(false);
16
+
17
+ const cacheRatio = cachePercent(usage) ?? 0;
18
+
19
+ return (
20
+ <div className={cx('cortex-token-usage', expanded && 'cortex-token-usage--expanded')}>
21
+ <button className="cortex-token-usage__summary" onClick={() => setExpanded((v) => !v)}>
22
+ <span className="cortex-token-usage__total">
23
+ <span className="cortex-token-usage__total-number">{num(usage.total)}</span>
24
+ <span className="cortex-token-usage__total-label">{t('translate_tokens')}</span>
25
+ </span>
26
+
27
+ {modelId ? <span className="cortex-token-usage__model">{modelId}</span> : null}
28
+
29
+ <span className="cortex-token-usage__pills">
30
+ <span className="cortex-token-usage__pill">
31
+ <span className="cortex-token-usage__dot cortex-token-usage__dot--input"></span>
32
+ {num(usage.input.total)}
33
+ </span>
34
+ <span className="cortex-token-usage__pill">
35
+ <span className="cortex-token-usage__dot cortex-token-usage__dot--output"></span>
36
+ {num(usage.output.total)}
37
+ </span>
38
+ </span>
39
+
40
+ {cacheRatio > 0 ? (
41
+ <span className="cortex-token-usage__cache-badge">{cacheRatio}%</span>
42
+ ) : null}
43
+
44
+ <svg
45
+ className="cortex-token-usage__chevron"
46
+ width="12"
47
+ height="12"
48
+ viewBox="0 0 12 12"
49
+ fill="none"
50
+ >
51
+ <path
52
+ d="M3 4.5L6 7.5L9 4.5"
53
+ stroke="currentColor"
54
+ strokeWidth="1.25"
55
+ strokeLinecap="round"
56
+ strokeLinejoin="round"
57
+ />
58
+ </svg>
59
+ </button>
60
+
61
+ <div className="cortex-token-usage__details">
62
+ <div className="cortex-token-usage__details-inner">
63
+ <div className="cortex-token-usage__columns">
64
+ <div className="cortex-token-usage__col">
65
+ <div className="cortex-token-usage__col-header">
66
+ <span className="cortex-token-usage__dot cortex-token-usage__dot--input"></span>
67
+ <span className="cortex-token-usage__col-label">{t('translate_input')}</span>
68
+ <span className="cortex-token-usage__col-total">{num(usage.input.total)}</span>
69
+ </div>
70
+ <div className="cortex-token-usage__rows">
71
+ {usage.input.noCache ? (
72
+ <div className="cortex-token-usage__row">
73
+ <span className="cortex-token-usage__row-label">{t('translate_fresh')}</span>
74
+ <span className="cortex-token-usage__row-value">
75
+ {num(usage.input.noCache)}
76
+ </span>
77
+ </div>
78
+ ) : null}
79
+ {usage.input.cacheRead ? (
80
+ <div className="cortex-token-usage__row">
81
+ <span className="cortex-token-usage__row-label">
82
+ {t('translate_cache_read')}
83
+ </span>
84
+ <span className="cortex-token-usage__row-value">
85
+ {num(usage.input.cacheRead)}
86
+ </span>
87
+ </div>
88
+ ) : null}
89
+ {usage.input.cacheWrite ? (
90
+ <div className="cortex-token-usage__row">
91
+ <span className="cortex-token-usage__row-label">
92
+ {t('translate_cache_write')}
93
+ </span>
94
+ <span className="cortex-token-usage__row-value">
95
+ {num(usage.input.cacheWrite)}
96
+ </span>
97
+ </div>
98
+ ) : null}
99
+ </div>
100
+ {cacheRatio > 0 ? (
101
+ <div className="cortex-token-usage__cache-bar-row">
102
+ <div className="cortex-token-usage__cache-bar">
103
+ <div
104
+ className="cortex-token-usage__cache-fill"
105
+ style={{ width: `${cacheRatio}%` }}
106
+ ></div>
107
+ </div>
108
+ <span className="cortex-token-usage__cache-label">
109
+ {t('translate_n_percent_cached', { percent: cacheRatio })}
110
+ </span>
111
+ </div>
112
+ ) : null}
113
+ </div>
114
+
115
+ <div className="cortex-token-usage__col">
116
+ <div className="cortex-token-usage__col-header">
117
+ <span className="cortex-token-usage__dot cortex-token-usage__dot--output"></span>
118
+ <span className="cortex-token-usage__col-label">{t('translate_output')}</span>
119
+ <span className="cortex-token-usage__col-total">{num(usage.output.total)}</span>
120
+ </div>
121
+ <div className="cortex-token-usage__rows">
122
+ {usage.output.text ? (
123
+ <div className="cortex-token-usage__row">
124
+ <span className="cortex-token-usage__row-label">{t('translate_text')}</span>
125
+ <span className="cortex-token-usage__row-value">{num(usage.output.text)}</span>
126
+ </div>
127
+ ) : null}
128
+ {usage.output.reasoning ? (
129
+ <div className="cortex-token-usage__row">
130
+ <span className="cortex-token-usage__row-label">
131
+ {t('translate_reasoning')}
132
+ </span>
133
+ <span className="cortex-token-usage__row-value">
134
+ {num(usage.output.reasoning)}
135
+ </span>
136
+ </div>
137
+ ) : null}
138
+ </div>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ );
145
+ }
@@ -0,0 +1,86 @@
1
+ import type { ToolCallPart, UIMessage } from '@tanstack/ai';
2
+ import { toolCallAnimation } from '@cortex/client';
3
+ import { cx } from '../cx';
4
+ import { useCortex } from '../context';
5
+
6
+ type MessageToolCallAnimatedProps = {
7
+ message: UIMessage;
8
+ toolCallPart: ToolCallPart;
9
+ };
10
+
11
+ export function MessageToolCallAnimated({ message, toolCallPart }: MessageToolCallAnimatedProps) {
12
+ const { config, t, addToolResult } = useCortex();
13
+
14
+ const Custom = config.toolComponents?.[toolCallPart.name];
15
+ if (Custom) {
16
+ return (
17
+ <div className="cortex-tool-call-animated">
18
+ <Custom
19
+ toolCallPart={toolCallPart}
20
+ message={message}
21
+ setOutput={(output) => addToolResult(toolCallPart.id, toolCallPart.name, output)}
22
+ />
23
+ </div>
24
+ );
25
+ }
26
+
27
+ const { state, active, titleKey } = toolCallAnimation(toolCallPart);
28
+
29
+ return (
30
+ <div className="cortex-tool-call-animated">
31
+ <div className="cortex-tool-pill">
32
+ {/* Spinner, then whichever glyph the call settled on */}
33
+ <span className="cortex-tool-pill__icon">
34
+ <span
35
+ className={cx(
36
+ 'cortex-tool-pill__spinner',
37
+ active && 'cortex-tool-pill__spinner--visible',
38
+ )}
39
+ ></span>
40
+ <svg
41
+ className={cx(
42
+ 'cortex-tool-pill__svg',
43
+ 'cortex-tool-pill__svg--check',
44
+ state === 'complete' && 'cortex-tool-pill__svg--visible',
45
+ )}
46
+ viewBox="0 0 20 20"
47
+ fill="none"
48
+ >
49
+ <path
50
+ d="M5.5 10.5 L8.5 13.5 L14.5 7"
51
+ stroke="currentColor"
52
+ strokeWidth="2"
53
+ strokeLinecap="round"
54
+ strokeLinejoin="round"
55
+ />
56
+ </svg>
57
+ <svg
58
+ className={cx(
59
+ 'cortex-tool-pill__svg',
60
+ 'cortex-tool-pill__svg--error',
61
+ state === 'error' && 'cortex-tool-pill__svg--visible',
62
+ )}
63
+ viewBox="0 0 20 20"
64
+ fill="none"
65
+ >
66
+ <path
67
+ d="M6.5 6.5 L13.5 13.5 M13.5 6.5 L6.5 13.5"
68
+ stroke="currentColor"
69
+ strokeWidth="2"
70
+ strokeLinecap="round"
71
+ />
72
+ </svg>
73
+ </span>
74
+
75
+ <span
76
+ className={cx(
77
+ 'cortex-tool-pill__title',
78
+ state === 'error' && 'cortex-tool-pill__title--error',
79
+ )}
80
+ >
81
+ {t(titleKey)}
82
+ </span>
83
+ </div>
84
+ </div>
85
+ );
86
+ }
@@ -0,0 +1,95 @@
1
+ import type { ToolCallPart } from '@tanstack/ai';
2
+ import { toolCallOutputText } from '@cortex/client';
3
+ import { useCortex } from '../context';
4
+ import { CopyButton } from './CopyButton';
5
+ import { JsonTree } from './JsonTree';
6
+
7
+ type MessageToolCallOutcomeProps = {
8
+ toolCallPart: ToolCallPart;
9
+ };
10
+
11
+ /**
12
+ * How a tool call ended: its output, its error, or the approval exchange that stopped
13
+ * it. Exactly one of those applies, so this is a single switch on the state rather
14
+ * than the four independent conditions the shape suggests.
15
+ */
16
+ export function MessageToolCallOutcome({ toolCallPart }: MessageToolCallOutcomeProps) {
17
+ const { t } = useCortex();
18
+ const { state, approval } = toolCallPart;
19
+ const output: unknown = toolCallPart.output;
20
+ const outputText = toolCallOutputText(toolCallPart);
21
+
22
+ function section() {
23
+ if (state === 'complete') {
24
+ return (
25
+ <div className="dbg-tool__section dbg-tool__section--success">
26
+ <div className="dbg-tool__section-bar">
27
+ <span className="dbg-tool__section-label dbg-tool__section-label--success">
28
+ {t('translate_output')}
29
+ </span>
30
+ <span className="dbg-tool__section-lang">json</span>
31
+ <CopyButton value={outputText} />
32
+ </div>
33
+ <div dir="ltr" className="dbg-tool__tree">
34
+ <JsonTree data={output} expandDepth={2} />
35
+ </div>
36
+ </div>
37
+ );
38
+ }
39
+
40
+ if (state === 'error') {
41
+ return (
42
+ <div className="dbg-tool__section dbg-tool__section--error">
43
+ <div className="dbg-tool__section-bar">
44
+ <span className="dbg-tool__section-label dbg-tool__section-label--error">
45
+ {t('translate_error')}
46
+ </span>
47
+ <CopyButton value={outputText} />
48
+ </div>
49
+ <pre dir="ltr" className="dbg-tool__error-pre">
50
+ {outputText}
51
+ </pre>
52
+ </div>
53
+ );
54
+ }
55
+
56
+ if (state === 'approval-requested') {
57
+ return (
58
+ <div className="dbg-tool__section dbg-tool__section--approval">
59
+ <div className="dbg-tool__section-bar">
60
+ <span className="dbg-tool__section-label dbg-tool__section-label--approval">
61
+ {t('translate_approval_requested')}
62
+ </span>
63
+ <span className="dbg-tool__section-lang">{approval?.id}</span>
64
+ </div>
65
+ <div className="dbg-tool__message dbg-tool__message--approval">
66
+ {t('translate_waiting_for_approval')}
67
+ </div>
68
+ </div>
69
+ );
70
+ }
71
+
72
+ if (state === 'approval-responded') {
73
+ return (
74
+ <div className="dbg-tool__section dbg-tool__section--approval">
75
+ <div className="dbg-tool__section-bar">
76
+ <span className="dbg-tool__section-label dbg-tool__section-label--approval">
77
+ {t('translate_approval_response')}
78
+ </span>
79
+ <span className="dbg-tool__section-lang">{approval?.id}</span>
80
+ </div>
81
+ <div className="dbg-tool__message dbg-tool__message--approval">
82
+ {t(approval?.approved ? 'translate_tool_approved' : 'translate_tool_response_received')}
83
+ </div>
84
+ </div>
85
+ );
86
+ }
87
+
88
+ return null;
89
+ }
90
+
91
+ // Angular renders this component inside its own host element, which keeps the
92
+ // outcome section out of `.dbg-tool__section:not(:first-child)` separators.
93
+ // This wrapper is that host's equivalent.
94
+ return <div>{section()}</div>;
95
+ }
@@ -0,0 +1,95 @@
1
+ import type { ToolCallPart } from '@tanstack/ai';
2
+ import { highlightCode, splitToolCallInput } from '@cortex/client';
3
+ import { useCortex } from '../context';
4
+ import { CopyButton } from './CopyButton';
5
+ import { JsonTree } from './JsonTree';
6
+ import { MessageToolCallOutcome } from './MessageToolCallOutcome';
7
+ import { MessageToolCallStatus } from './MessageToolCallStatus';
8
+
9
+ type MessageToolCallPartProps = {
10
+ toolCallPart: ToolCallPart;
11
+ };
12
+
13
+ /**
14
+ * One tool call in the debug view: what it was asked to do, and — delegated to the
15
+ * status badge and outcome panel — where it got to and how it ended.
16
+ */
17
+ export function MessageToolCallPart({ toolCallPart }: MessageToolCallPartProps) {
18
+ const { t } = useCortex();
19
+
20
+ const { codeSnippets, remainingInput, remainingInputText } = splitToolCallInput(
21
+ toolCallPart.input,
22
+ );
23
+
24
+ return (
25
+ <details className="dbg-tool" data-state={toolCallPart.state}>
26
+ <summary className="dbg-tool__summary">
27
+ <div className="dbg-tool__header">
28
+ <div className="dbg-tool__meta">
29
+ <div className="dbg-tool__title-row">
30
+ <span className="dbg-tool__name" title={toolCallPart.name}>
31
+ {toolCallPart.name}
32
+ </span>
33
+ </div>
34
+ <div className="dbg-tool__id-row">
35
+ <span className="dbg-tool__id">{toolCallPart.id}</span>
36
+ </div>
37
+ </div>
38
+
39
+ <div className="dbg-tool__actions">
40
+ <MessageToolCallStatus toolCallPart={toolCallPart} />
41
+
42
+ <svg
43
+ className="dbg-tool__chevron"
44
+ width="14"
45
+ height="14"
46
+ viewBox="0 0 20 20"
47
+ fill="none"
48
+ >
49
+ <path
50
+ d="m5.75 8.25 4.25 4.25 4.25-4.25"
51
+ stroke="currentColor"
52
+ strokeWidth="1.5"
53
+ strokeLinecap="round"
54
+ strokeLinejoin="round"
55
+ />
56
+ </svg>
57
+ </div>
58
+ </div>
59
+ </summary>
60
+
61
+ <div className="dbg-tool__body">
62
+ {codeSnippets.map((snippet) => (
63
+ <div className="dbg-tool__section" key={snippet.key}>
64
+ <div className="dbg-tool__section-bar">
65
+ <span className="dbg-tool__section-label">{snippet.key}</span>
66
+ <span className="dbg-tool__section-lang">{snippet.lang}</span>
67
+ <CopyButton value={snippet.value} />
68
+ </div>
69
+ <pre dir="ltr" className="dbg-tool__pre">
70
+ <code
71
+ className="hljs"
72
+ dangerouslySetInnerHTML={{ __html: highlightCode(snippet.value, snippet.lang) }}
73
+ ></code>
74
+ </pre>
75
+ </div>
76
+ ))}
77
+
78
+ {!!remainingInput && (
79
+ <div className="dbg-tool__section">
80
+ <div className="dbg-tool__section-bar">
81
+ <span className="dbg-tool__section-label">{t('translate_input')}</span>
82
+ <span className="dbg-tool__section-lang">json</span>
83
+ <CopyButton value={remainingInputText} />
84
+ </div>
85
+ <div dir="ltr" className="dbg-tool__tree">
86
+ <JsonTree data={remainingInput} expandDepth={2} />
87
+ </div>
88
+ </div>
89
+ )}
90
+
91
+ <MessageToolCallOutcome toolCallPart={toolCallPart} />
92
+ </div>
93
+ </details>
94
+ );
95
+ }
@@ -0,0 +1,35 @@
1
+ import type { ToolCallPart } from '@tanstack/ai';
2
+ import { toolCallBadge } from '@cortex/client';
3
+ import { cx } from '../cx';
4
+ import { useCortex } from '../context';
5
+
6
+ type MessageToolCallStatusProps = {
7
+ toolCallPart: ToolCallPart;
8
+ };
9
+
10
+ /**
11
+ * The badge in a tool call's header saying where the call has got to. Split out of
12
+ * the tool-call part so that part is left describing layout rather than branching
13
+ * seven ways on a state it does not otherwise care about.
14
+ */
15
+ export function MessageToolCallStatus({ toolCallPart }: MessageToolCallStatusProps) {
16
+ const { t } = useCortex();
17
+ const badge = toolCallBadge(toolCallPart);
18
+
19
+ // The outer span stands in for Angular's <cortex-message-tool-call-status>
20
+ // host, keeping the badge itself out of flex-item blockification.
21
+ return (
22
+ <span>
23
+ <span
24
+ className={cx('dbg-tool__state', badge.modifier && `dbg-tool__state--${badge.modifier}`)}
25
+ >
26
+ {badge.pulse && (
27
+ <span
28
+ className={cx('dbg-tool__pulse', badge.pulse === 'violet' && 'dbg-tool__pulse--violet')}
29
+ ></span>
30
+ )}
31
+ {t(badge.labelKey)}
32
+ </span>
33
+ </span>
34
+ );
35
+ }
@@ -0,0 +1,74 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { useCortex } from '../context';
3
+ import { cx } from '../cx';
4
+
5
+ type LabelState = 'idle' | 'exiting' | 'enter-start' | 'entering';
6
+
7
+ type SubtleActivityProps = {
8
+ labels: string[];
9
+ className?: string;
10
+ };
11
+
12
+ export function SubtleActivity({ labels, className }: SubtleActivityProps) {
13
+ const { t } = useCortex();
14
+ const [currentIndex, setCurrentIndex] = useState(0);
15
+ const [labelState, setLabelState] = useState<LabelState>('idle');
16
+
17
+ // Read through a ref so a changing labels array never restarts the cycle.
18
+ const labelCount = useRef(labels.length);
19
+ labelCount.current = labels.length;
20
+
21
+ useEffect(() => {
22
+ // The three timeouts run in sequence, so one handle covers all of them.
23
+ let timeout: ReturnType<typeof setTimeout> | undefined;
24
+ let frame: number | undefined;
25
+
26
+ function scheduleNextTransition() {
27
+ timeout = setTimeout(() => {
28
+ setLabelState('exiting');
29
+
30
+ timeout = setTimeout(() => {
31
+ setCurrentIndex((i) => (i + 1) % labelCount.current);
32
+ setLabelState('enter-start');
33
+
34
+ frame = requestAnimationFrame(() => {
35
+ setLabelState('entering');
36
+
37
+ timeout = setTimeout(() => {
38
+ setLabelState('idle');
39
+ scheduleNextTransition();
40
+ }, 300);
41
+ });
42
+ }, 300);
43
+ }, 2000);
44
+ }
45
+
46
+ scheduleNextTransition();
47
+
48
+ return () => {
49
+ if (timeout) clearTimeout(timeout);
50
+ if (frame) cancelAnimationFrame(frame);
51
+ };
52
+ }, []);
53
+
54
+ return (
55
+ <div className={cx('cortex-subtle-activity', className)}>
56
+ <div className="cortex-subtle-activity__dots">
57
+ <span className="cortex-subtle-activity__dot"></span>
58
+ <span className="cortex-subtle-activity__dot cortex-subtle-activity__dot--d1"></span>
59
+ <span className="cortex-subtle-activity__dot cortex-subtle-activity__dot--d2"></span>
60
+ </div>
61
+
62
+ <div className="cortex-subtle-activity__label-mask">
63
+ <span
64
+ className={cx(
65
+ 'cortex-subtle-activity__label',
66
+ `cortex-subtle-activity__label--${labelState}`,
67
+ )}
68
+ >
69
+ {t(labels[currentIndex])}
70
+ </span>
71
+ </div>
72
+ </div>
73
+ );
74
+ }