@arcote.tech/arc-ds 0.7.7 → 0.7.9
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.
- package/package.json +2 -2
- package/src/ds/chat/chat-input.tsx +36 -6
- package/src/ds/chat/chat-labels.tsx +27 -21
- package/src/ds/chat/chat-message.tsx +3 -4
- package/src/ds/chat/chat.tsx +8 -1
- package/src/ds/form/fields/textarea-field.tsx +9 -1
- package/src/ds/suggestion-list/suggestion-list.tsx +49 -5
- package/src/index.ts +1 -0
- package/src/layout/layout.tsx +13 -10
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-ds",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.9",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Przemysław Krasiński [arcote.tech]",
|
|
7
7
|
"description": "Design System for Arc framework — CVA-based components with display modes and variant overrides",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"tailwind-merge": "^3.5.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@arcote.tech/arc": "^0.7.
|
|
33
|
+
"@arcote.tech/arc": "^0.7.9",
|
|
34
34
|
"framer-motion": "^12.0.0",
|
|
35
35
|
"lucide-react": ">=0.400.0",
|
|
36
36
|
"radix-ui": "^1.0.0",
|
|
@@ -7,6 +7,18 @@ import { useState, type ReactNode } from "react";
|
|
|
7
7
|
import type { ChatModel, SendMessageOptions } from "./types";
|
|
8
8
|
import { useChatLabels } from "./chat-labels";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Slot props dla custom textarea — używane przez `renderTextarea` żeby
|
|
12
|
+
* wymienić default TextareaField na np. VoiceTextarea z `@arcote.tech/arc-ai-voice`.
|
|
13
|
+
* Wszystkie pola są wymagane (provider musi je podpiąć), poza placeholder/rows.
|
|
14
|
+
*/
|
|
15
|
+
export interface ChatInputTextareaSlotProps {
|
|
16
|
+
value: string;
|
|
17
|
+
onChange: (value: string) => void;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
rows?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
10
22
|
interface ChatInputProps {
|
|
11
23
|
onSend: (message: string, options: SendMessageOptions) => void;
|
|
12
24
|
models: ChatModel[];
|
|
@@ -25,6 +37,12 @@ interface ChatInputProps {
|
|
|
25
37
|
onClick: () => void;
|
|
26
38
|
disabled: boolean;
|
|
27
39
|
}) => ReactNode;
|
|
40
|
+
/**
|
|
41
|
+
* Slot na pole tekstowe — default to `<TextareaField>`. Konsumer może
|
|
42
|
+
* podpiąć `VoiceTextarea` z `@arcote.tech/arc-ai-voice` aby włączyć
|
|
43
|
+
* dyktowanie głosowe bez sprzęgania DS z fragmentem AI.
|
|
44
|
+
*/
|
|
45
|
+
renderTextarea?: (props: ChatInputTextareaSlotProps) => ReactNode;
|
|
28
46
|
/** Disable input (e.g., during generation) */
|
|
29
47
|
disabled?: boolean;
|
|
30
48
|
}
|
|
@@ -37,6 +55,7 @@ export function ChatInput({
|
|
|
37
55
|
showModelSelector = true,
|
|
38
56
|
showWebSearch = true,
|
|
39
57
|
renderSendButton,
|
|
58
|
+
renderTextarea,
|
|
40
59
|
disabled = false,
|
|
41
60
|
}: ChatInputProps) {
|
|
42
61
|
const labels = useChatLabels();
|
|
@@ -61,12 +80,23 @@ export function ChatInput({
|
|
|
61
80
|
{/* Input + send */}
|
|
62
81
|
<div className="flex items-center gap-2">
|
|
63
82
|
<div className="flex-1">
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
83
|
+
{renderTextarea ? (
|
|
84
|
+
renderTextarea({
|
|
85
|
+
value: message,
|
|
86
|
+
onChange: setMessage,
|
|
87
|
+
placeholder: disabled
|
|
88
|
+
? labels.placeholderGenerating
|
|
89
|
+
: labels.placeholder,
|
|
90
|
+
rows: 1,
|
|
91
|
+
})
|
|
92
|
+
) : (
|
|
93
|
+
<TextareaField
|
|
94
|
+
value={message}
|
|
95
|
+
onChange={(val) => setMessage(val ?? "")}
|
|
96
|
+
placeholder={disabled ? labels.placeholderGenerating : labels.placeholder}
|
|
97
|
+
rows={1}
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
70
100
|
</div>
|
|
71
101
|
{renderSendButton ? (
|
|
72
102
|
renderSendButton({ onClick: handleSend, disabled: sendDisabled })
|
|
@@ -1,39 +1,45 @@
|
|
|
1
1
|
import { createContext, useContext, useMemo, type ReactNode } from "react";
|
|
2
2
|
|
|
3
3
|
export interface ChatLabels {
|
|
4
|
-
//
|
|
4
|
+
// HTML attribute strings (placeholder na <input>/<textarea>) muszą zostać
|
|
5
|
+
// `string` — DOM attribute API nie przyjmuje ReactNode.
|
|
5
6
|
placeholder: string;
|
|
6
7
|
placeholderGenerating: string;
|
|
7
8
|
modelPlaceholder: string;
|
|
8
|
-
webSearchLabel: string;
|
|
9
|
-
// QuestionTabs (wizard)
|
|
10
|
-
submitLabel: string;
|
|
11
9
|
customPlaceholder: string;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
// Wszystkie poniższe są renderowane w drzewie React (Button label, span,
|
|
11
|
+
// h2 itp.) — `ReactNode` pozwala używać `<Trans>...</Trans>` żeby labels
|
|
12
|
+
// reagowały na zmianę locale runtime'owo.
|
|
13
|
+
webSearchLabel: ReactNode;
|
|
14
|
+
// QuestionTabs (wizard)
|
|
15
|
+
submitLabel: ReactNode;
|
|
16
|
+
nextLabel: ReactNode;
|
|
17
|
+
backLabel: ReactNode;
|
|
18
|
+
summaryTitle: ReactNode;
|
|
19
|
+
editLabel: ReactNode;
|
|
20
|
+
noAnswerLabel: ReactNode;
|
|
21
|
+
questionOfLabel: (current: number, total: number) => ReactNode;
|
|
18
22
|
/** Secondary button in QuestionTabs — "Continue discussion" instead of answering */
|
|
19
|
-
discussLabel:
|
|
23
|
+
discussLabel: ReactNode;
|
|
20
24
|
// ChatMessage
|
|
21
|
-
questionsLabel:
|
|
22
|
-
answerLabel:
|
|
23
|
-
// Tool execution log
|
|
25
|
+
questionsLabel: ReactNode;
|
|
26
|
+
answerLabel: ReactNode;
|
|
27
|
+
// Tool execution log + error fallback — używane też w template literals
|
|
28
|
+
// (`${chatLabels.errorLabel}: ...`) i jako `content` ChatMessage. Stąd
|
|
29
|
+
// string, nie ReactNode.
|
|
24
30
|
toolCallingLabel: string;
|
|
25
31
|
toolDoneLabel: string;
|
|
26
32
|
errorLabel: string;
|
|
27
33
|
// askQuestions tool view
|
|
28
|
-
answerBelowLabel:
|
|
34
|
+
answerBelowLabel: ReactNode;
|
|
29
35
|
// completeStage tool view
|
|
30
|
-
stageCompleteLabel:
|
|
31
|
-
advanceStageLabel:
|
|
32
|
-
continueStageLabel:
|
|
33
|
-
stageAdvancedLabel:
|
|
34
|
-
stageContinuedLabel:
|
|
36
|
+
stageCompleteLabel: ReactNode;
|
|
37
|
+
advanceStageLabel: ReactNode;
|
|
38
|
+
continueStageLabel: ReactNode;
|
|
39
|
+
stageAdvancedLabel: ReactNode;
|
|
40
|
+
stageContinuedLabel: ReactNode;
|
|
35
41
|
/** Label for the "Przejdź do następnego" button on the final stage. */
|
|
36
|
-
finishConsultationLabel:
|
|
42
|
+
finishConsultationLabel: ReactNode;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
export const defaultChatLabels: ChatLabels = {
|
|
@@ -48,14 +48,13 @@ export function ChatMessage({
|
|
|
48
48
|
: "bg-card border border-border rounded-tl-sm"
|
|
49
49
|
}`}
|
|
50
50
|
>
|
|
51
|
-
<div
|
|
51
|
+
<div
|
|
52
|
+
data-streaming={message.isStreaming || undefined}
|
|
53
|
+
className="chat-markdown space-y-2 text-left [&_p]:m-0 [&_ul]:my-1 [&_ul]:pl-5 [&_ol]:my-1 [&_ol]:pl-5 [&_li]:my-0.5 [&_a]:text-primary [&_a]:underline [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-xs [&_pre]:bg-muted [&_pre]:p-2 [&_pre]:rounded [&_pre]:text-xs [&_pre]:overflow-x-auto [&_strong]:font-semibold [&_em]:italic [&_h1]:text-base [&_h1]:font-semibold [&_h2]:text-sm [&_h2]:font-semibold [&_h3]:text-sm [&_h3]:font-medium [&_blockquote]:border-l-2 [&_blockquote]:border-primary/30 [&_blockquote]:pl-3 [&_blockquote]:italic data-[streaming]:[&_>*:last-child]:after:content-[''] data-[streaming]:[&_>*:last-child]:after:inline-block data-[streaming]:[&_>*:last-child]:after:w-[0.4em] data-[streaming]:[&_>*:last-child]:after:h-[1em] data-[streaming]:[&_>*:last-child]:after:bg-foreground/60 data-[streaming]:[&_>*:last-child]:after:animate-pulse data-[streaming]:[&_>*:last-child]:after:ml-0.5 data-[streaming]:[&_>*:last-child]:after:align-text-bottom">
|
|
52
54
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
|
53
55
|
{message.content}
|
|
54
56
|
</ReactMarkdown>
|
|
55
57
|
</div>
|
|
56
|
-
{message.isStreaming && (
|
|
57
|
-
<span className="inline-block w-1.5 h-4 bg-foreground/60 animate-pulse ml-0.5 -mb-0.5" />
|
|
58
|
-
)}
|
|
59
58
|
</div>
|
|
60
59
|
|
|
61
60
|
{/* Tool uses */}
|
package/src/ds/chat/chat.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
SendMessageOptions,
|
|
7
7
|
} from "./types";
|
|
8
8
|
import { ChatMessage } from "./chat-message";
|
|
9
|
-
import { ChatInput } from "./chat-input";
|
|
9
|
+
import { ChatInput, type ChatInputTextareaSlotProps } from "./chat-input";
|
|
10
10
|
import { useChatInput } from "./chat-input-provider";
|
|
11
11
|
import { QuestionTabs } from "./question-tabs";
|
|
12
12
|
|
|
@@ -44,6 +44,11 @@ export interface ChatProps {
|
|
|
44
44
|
onClick: () => void;
|
|
45
45
|
disabled: boolean;
|
|
46
46
|
}) => ReactNode;
|
|
47
|
+
/**
|
|
48
|
+
* Slot na pole tekstowe w ChatInput — przekazywane do `<ChatInput>`.
|
|
49
|
+
* Konsumer może podać `VoiceTextarea` z `@arcote.tech/arc-ai-voice`.
|
|
50
|
+
*/
|
|
51
|
+
renderTextarea?: (props: ChatInputTextareaSlotProps) => ReactNode;
|
|
47
52
|
/** Max width class for the message area */
|
|
48
53
|
maxWidth?: string;
|
|
49
54
|
/** Disable input (during generation) */
|
|
@@ -63,6 +68,7 @@ export function Chat({
|
|
|
63
68
|
showModelSelector = true,
|
|
64
69
|
showWebSearch = true,
|
|
65
70
|
renderSendButton,
|
|
71
|
+
renderTextarea,
|
|
66
72
|
maxWidth = "max-w-3xl",
|
|
67
73
|
disabled = false,
|
|
68
74
|
}: ChatProps) {
|
|
@@ -133,6 +139,7 @@ export function Chat({
|
|
|
133
139
|
showModelSelector={showModelSelector}
|
|
134
140
|
showWebSearch={showWebSearch}
|
|
135
141
|
renderSendButton={renderSendButton}
|
|
142
|
+
renderTextarea={renderTextarea}
|
|
136
143
|
disabled={disabled}
|
|
137
144
|
/>
|
|
138
145
|
)}
|
|
@@ -10,10 +10,17 @@ export interface TextareaFieldProps {
|
|
|
10
10
|
onChange?: (value: string) => void;
|
|
11
11
|
rows?: number;
|
|
12
12
|
maxHeight?: number;
|
|
13
|
+
/** Klasa na OUTER divie (label + pole). */
|
|
13
14
|
className?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Klasa dorzucona do INNER contentEditable. Używaj gdy chcesz nadpisać
|
|
17
|
+
* padding (np. zostawić miejsce na absolute-positioned ikonę po prawej)
|
|
18
|
+
* albo dodatkowe modyfikatory typografii.
|
|
19
|
+
*/
|
|
20
|
+
inputClassName?: string;
|
|
14
21
|
}
|
|
15
22
|
|
|
16
|
-
export function TextareaField({ label, placeholder, value, onChange, rows = 1, maxHeight, className }: TextareaFieldProps) {
|
|
23
|
+
export function TextareaField({ label, placeholder, value, onChange, rows = 1, maxHeight, className, inputClassName }: TextareaFieldProps) {
|
|
17
24
|
const fieldCtx = useContext(FormFieldContext);
|
|
18
25
|
const hasError = fieldCtx?.errors && fieldCtx.messages?.length > 0;
|
|
19
26
|
const ref = useRef<HTMLDivElement>(null);
|
|
@@ -66,6 +73,7 @@ export function TextareaField({ label, placeholder, value, onChange, rows = 1, m
|
|
|
66
73
|
"whitespace-pre-wrap break-words",
|
|
67
74
|
maxHeight && "overflow-y-auto",
|
|
68
75
|
"empty:before:content-[attr(data-placeholder)] empty:before:text-muted-foreground empty:before:pointer-events-none",
|
|
76
|
+
inputClassName,
|
|
69
77
|
)}
|
|
70
78
|
style={{
|
|
71
79
|
minHeight,
|
|
@@ -28,6 +28,13 @@ export interface SuggestionListProps<T> {
|
|
|
28
28
|
placeholder?: string;
|
|
29
29
|
max?: number;
|
|
30
30
|
initialCloud?: InitialCloudConfig;
|
|
31
|
+
/**
|
|
32
|
+
* Po dodaniu itemu (z sugestii, custom lub cloudu) ustaw focus na pierwszy
|
|
33
|
+
* fokusowalny input/textarea/contenteditable wewnątrz nowo wyrenderowanego
|
|
34
|
+
* elementu i zamknij dropdown wyszukiwania. Przydatne gdy `renderItem`
|
|
35
|
+
* zawiera pole tekstowe (np. URL kanału) i user ma od razu wpisywać dane.
|
|
36
|
+
*/
|
|
37
|
+
focusItemOnAdd?: boolean;
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
const defaultGetKey = <T,>(item: T) => String(item);
|
|
@@ -49,12 +56,16 @@ export function SuggestionList<T>({
|
|
|
49
56
|
placeholder = "Wyszukaj...",
|
|
50
57
|
max,
|
|
51
58
|
initialCloud,
|
|
59
|
+
focusItemOnAdd = false,
|
|
52
60
|
}: SuggestionListProps<T>) {
|
|
53
61
|
const [inputValue, setInputValue] = useState("");
|
|
54
62
|
const [isEditing, setIsEditing] = useState(false);
|
|
55
63
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
56
64
|
// Once user interacts (adds/removes or clicks "more"), we leave cloud mode permanently
|
|
57
65
|
const [cloudDismissed, setCloudDismissed] = useState(false);
|
|
66
|
+
// Index świeżo dodanego itemu — wyzwala useEffect który ustawia focus na
|
|
67
|
+
// pierwszy input w jego wrapperze (tylko gdy focusItemOnAdd === true).
|
|
68
|
+
const [pendingFocusIndex, setPendingFocusIndex] = useState<number | null>(null);
|
|
58
69
|
|
|
59
70
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
60
71
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
@@ -112,16 +123,42 @@ export function SuggestionList<T>({
|
|
|
112
123
|
}
|
|
113
124
|
}, [activeIndex, isEditing]);
|
|
114
125
|
|
|
126
|
+
// Auto-focus pierwszego inputa w nowo dodanym itemie. Effect odpala się po
|
|
127
|
+
// commit (DOM gotowy) — szukamy wrappera po data-suggestion-item-index i
|
|
128
|
+
// pierwszego focusable child. requestAnimationFrame daje pewność że motion
|
|
129
|
+
// ukończył mount (item rozwija się z height 0).
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (pendingFocusIndex === null) return;
|
|
132
|
+
if (!containerRef.current) return;
|
|
133
|
+
const target = pendingFocusIndex;
|
|
134
|
+
setPendingFocusIndex(null);
|
|
135
|
+
requestAnimationFrame(() => {
|
|
136
|
+
const wrapper = containerRef.current?.querySelector<HTMLElement>(
|
|
137
|
+
`[data-suggestion-item-index="${target}"]`,
|
|
138
|
+
);
|
|
139
|
+
const input = wrapper?.querySelector<HTMLElement>(
|
|
140
|
+
"input, textarea, [contenteditable='true']",
|
|
141
|
+
);
|
|
142
|
+
input?.focus();
|
|
143
|
+
});
|
|
144
|
+
}, [pendingFocusIndex, safeItems.length]);
|
|
145
|
+
|
|
115
146
|
const addItem = useCallback(
|
|
116
147
|
(item: T) => {
|
|
117
148
|
if (max && safeItems.length >= max) return;
|
|
149
|
+
const newIndex = safeItems.length;
|
|
118
150
|
onChange([...safeItems, item]);
|
|
119
151
|
setInputValue("");
|
|
120
152
|
setActiveIndex(0);
|
|
121
153
|
setCloudDismissed(true);
|
|
122
|
-
|
|
154
|
+
if (focusItemOnAdd) {
|
|
155
|
+
setIsEditing(false);
|
|
156
|
+
setPendingFocusIndex(newIndex);
|
|
157
|
+
} else {
|
|
158
|
+
requestAnimationFrame(() => inputRef.current?.focus());
|
|
159
|
+
}
|
|
123
160
|
},
|
|
124
|
-
[safeItems, onChange, max],
|
|
161
|
+
[safeItems, onChange, max, focusItemOnAdd],
|
|
125
162
|
);
|
|
126
163
|
|
|
127
164
|
const removeItemByIndex = useCallback(
|
|
@@ -213,14 +250,20 @@ export function SuggestionList<T>({
|
|
|
213
250
|
const addFromCloud = useCallback(
|
|
214
251
|
(item: T) => {
|
|
215
252
|
if (max && safeItems.length >= max) return;
|
|
253
|
+
const newIndex = safeItems.length;
|
|
216
254
|
onChange([...safeItems, item]);
|
|
217
255
|
setInputValue("");
|
|
218
256
|
setActiveIndex(0);
|
|
219
257
|
setCloudDismissed(true);
|
|
220
|
-
|
|
221
|
-
|
|
258
|
+
if (focusItemOnAdd) {
|
|
259
|
+
setIsEditing(false);
|
|
260
|
+
setPendingFocusIndex(newIndex);
|
|
261
|
+
} else {
|
|
262
|
+
setIsEditing(true);
|
|
263
|
+
requestAnimationFrame(() => inputRef.current?.focus());
|
|
264
|
+
}
|
|
222
265
|
},
|
|
223
|
-
[safeItems, onChange, max],
|
|
266
|
+
[safeItems, onChange, max, focusItemOnAdd],
|
|
224
267
|
);
|
|
225
268
|
|
|
226
269
|
// ── Cloud view ──────────────────────────────────────────────
|
|
@@ -301,6 +344,7 @@ export function SuggestionList<T>({
|
|
|
301
344
|
{safeItems.map((item, index) => (
|
|
302
345
|
<motion.div
|
|
303
346
|
key={`${getKey(item)}-${index}`}
|
|
347
|
+
data-suggestion-item-index={index}
|
|
304
348
|
initial={{ height: 0, opacity: 0 }}
|
|
305
349
|
animate={{ height: "auto", opacity: 1 }}
|
|
306
350
|
exit={{ height: 0, opacity: 0 }}
|
package/src/index.ts
CHANGED
|
@@ -110,6 +110,7 @@ export { Chat } from "./ds/chat/chat";
|
|
|
110
110
|
export type { ChatProps } from "./ds/chat/chat";
|
|
111
111
|
export { ChatMessage } from "./ds/chat/chat-message";
|
|
112
112
|
export { ChatInput } from "./ds/chat/chat-input";
|
|
113
|
+
export type { ChatInputTextareaSlotProps } from "./ds/chat/chat-input";
|
|
113
114
|
export { QuestionTabs } from "./ds/chat/question-tabs";
|
|
114
115
|
export { ToolUseBlock } from "./ds/chat/tool-use-block";
|
|
115
116
|
export { ChatToolLog } from "./ds/chat/chat-tool-log";
|
package/src/layout/layout.tsx
CHANGED
|
@@ -48,11 +48,11 @@ export function Layout({ children }: { children?: ReactNode }) {
|
|
|
48
48
|
function SubNavSlot() {
|
|
49
49
|
const content = useDynamicSlotContent("sub-nav");
|
|
50
50
|
if (content.length === 0) return null;
|
|
51
|
-
// `relative z-
|
|
52
|
-
//
|
|
53
|
-
// above the main content area that follows
|
|
51
|
+
// `relative z-20` creates an explicit stacking context below the toolbar
|
|
52
|
+
// (Z.BASE = 30) so the sub-nav doesn't paint over the fixed toolbar, but
|
|
53
|
+
// still sits above the main content area that follows in document flow.
|
|
54
54
|
return (
|
|
55
|
-
<div className="relative z-
|
|
55
|
+
<div className="relative z-20 flex justify-center py-2">
|
|
56
56
|
<Box layoutId="sub-nav" className="flex items-center gap-1 px-3 py-2">
|
|
57
57
|
{content}
|
|
58
58
|
</Box>
|
|
@@ -167,33 +167,36 @@ function DesktopToolbar() {
|
|
|
167
167
|
return (
|
|
168
168
|
<>
|
|
169
169
|
<div
|
|
170
|
-
data-arc-toolbar="left"
|
|
171
170
|
className="fixed left-4 top-4"
|
|
172
171
|
style={{ zIndex: zIndex("workspace") }}
|
|
173
172
|
>
|
|
174
173
|
<Box layout={false} className="flex flex-col px-3 py-2">
|
|
175
|
-
|
|
174
|
+
<div data-arc-toolbar="left">
|
|
175
|
+
{renderSlot("toolbar-left", { className: "flex items-center gap-2" })}
|
|
176
|
+
</div>
|
|
176
177
|
<ExpandedSection targetId="workspace" />
|
|
177
178
|
</Box>
|
|
178
179
|
</div>
|
|
179
180
|
|
|
180
181
|
<div
|
|
181
|
-
data-arc-toolbar="center"
|
|
182
182
|
className="fixed left-1/2 top-4 max-w-[calc(100vw-26rem)] -translate-x-1/2"
|
|
183
183
|
style={{ zIndex: zIndex("center") }}
|
|
184
184
|
>
|
|
185
185
|
<Box layout={false} className="flex min-w-0 items-center px-3 py-2">
|
|
186
|
-
|
|
186
|
+
<div data-arc-toolbar="center" className="flex min-w-0 flex-1">
|
|
187
|
+
{renderSlot("toolbar-center", { className: "flex min-w-0 flex-1 items-center gap-1" })}
|
|
188
|
+
</div>
|
|
187
189
|
</Box>
|
|
188
190
|
</div>
|
|
189
191
|
|
|
190
192
|
<div
|
|
191
|
-
data-arc-toolbar="right"
|
|
192
193
|
className="fixed right-4 top-4"
|
|
193
194
|
style={{ zIndex: zIndex("settings") }}
|
|
194
195
|
>
|
|
195
196
|
<Box layout={false} className="flex flex-col px-3 py-2">
|
|
196
|
-
|
|
197
|
+
<div data-arc-toolbar="right">
|
|
198
|
+
{renderSlot("toolbar-right", { className: "flex items-center gap-2" })}
|
|
199
|
+
</div>
|
|
197
200
|
<ExpandedSection targetId="settings" />
|
|
198
201
|
</Box>
|
|
199
202
|
</div>
|