This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
// @vitest-environment jsdom
|
||||
import '../../test/dom-setup';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { SpaceChatTabBar } from './SpaceChatTabBar';
|
||||
|
||||
const TABS = [
|
||||
{ id: 'chat', labelKey: 'tabs.chat' },
|
||||
{ id: 'overview', labelKey: 'tabs.overview' },
|
||||
{ id: 'files', labelKey: 'tabs.files' },
|
||||
];
|
||||
|
||||
function renderBar(overrides: Partial<Parameters<typeof SpaceChatTabBar>[0]> = {}) {
|
||||
const onSelect = vi.fn();
|
||||
const utils = render(
|
||||
<SpaceChatTabBar
|
||||
tabs={TABS}
|
||||
activeTab="chat"
|
||||
onSelect={onSelect}
|
||||
ariaLabel="チャットタブ"
|
||||
renderLabel={(tb) => tb.labelKey}
|
||||
appearClass={() => ''}
|
||||
actions={<button data-testid="space-chat-delete">del</button>}
|
||||
{...overrides}
|
||||
/>
|
||||
);
|
||||
return { onSelect, ...utils };
|
||||
}
|
||||
|
||||
describe('SpaceChatTabBar', () => {
|
||||
it('タブと actions を描画し、actions はスクロールする tablist の外にある', () => {
|
||||
renderBar();
|
||||
const tablist = screen.getByRole('tablist');
|
||||
const del = screen.getByTestId('space-chat-delete');
|
||||
// actions がタブのスクロールコンテナ(tablist)の子孫だと、タブが溢れたとき
|
||||
// 一緒にスクロールして画面外に流れる。兄弟であることを構造で保証する。
|
||||
expect(tablist.contains(del)).toBe(false);
|
||||
expect(screen.getByTestId('space-chat-actions').contains(del)).toBe(true);
|
||||
expect(tablist.className).toContain('overflow-x-auto');
|
||||
});
|
||||
|
||||
it('ArrowRight で次のタブを選択しフォーカスを移す(末尾で wrap)', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { onSelect } = renderBar();
|
||||
screen.getByRole('tab', { name: 'tabs.chat' }).focus();
|
||||
await user.keyboard('{ArrowRight}');
|
||||
expect(onSelect).toHaveBeenCalledWith('overview');
|
||||
});
|
||||
|
||||
it('End で末尾タブ、Home で先頭タブを選択する', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { onSelect } = renderBar();
|
||||
screen.getByRole('tab', { name: 'tabs.chat' }).focus();
|
||||
await user.keyboard('{End}');
|
||||
expect(onSelect).toHaveBeenCalledWith('files');
|
||||
await user.keyboard('{Home}');
|
||||
expect(onSelect).toHaveBeenCalledWith('chat');
|
||||
});
|
||||
|
||||
it('aria-selected が activeTab にだけ付く', () => {
|
||||
renderBar({ activeTab: 'overview' });
|
||||
expect(screen.getByRole('tab', { name: 'tabs.overview' })).toHaveAttribute('aria-selected', 'true');
|
||||
expect(screen.getByRole('tab', { name: 'tabs.chat' })).toHaveAttribute('aria-selected', 'false');
|
||||
});
|
||||
|
||||
it('actions なしのときアクションゾーンを描画しない', () => {
|
||||
renderBar({ actions: undefined });
|
||||
expect(screen.queryByTestId('space-chat-actions')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* SpaceChatTabBar — 会話画面の単一タブバー(2ゾーン構造)。
|
||||
*
|
||||
* 左: タブ群(overflow-x-auto のスクロールゾーン)
|
||||
* 右: actions(shrink-0 の固定ゾーン。継続 / 共有 / 削除ボタンが入る)
|
||||
*
|
||||
* タブが溢れて横スクロールになっても、actions は右端に固定されたまま
|
||||
* スクロールで流れない。これが2ゾーンに分ける理由(旧: アクション専用行
|
||||
* space-chat-actions を廃止して縦約40pxを回収した)。
|
||||
*/
|
||||
import { useRef } from 'react';
|
||||
|
||||
export interface SpaceChatTabDef {
|
||||
id: string;
|
||||
labelKey: string;
|
||||
}
|
||||
|
||||
interface SpaceChatTabBarProps {
|
||||
tabs: SpaceChatTabDef[];
|
||||
activeTab: string;
|
||||
onSelect: (id: string) => void;
|
||||
ariaLabel: string;
|
||||
renderLabel: (tab: SpaceChatTabDef) => string;
|
||||
/** browser/ssh タブの出現アニメ用クラス(detailTabs.tabAppearClass を渡す) */
|
||||
appearClass: (id: string) => string;
|
||||
/** 右端固定ゾーンに置くアクション群。無ければゾーンごと描画しない。 */
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function SpaceChatTabBar({
|
||||
tabs,
|
||||
activeTab,
|
||||
onSelect,
|
||||
ariaLabel,
|
||||
renderLabel,
|
||||
appearClass,
|
||||
actions,
|
||||
}: SpaceChatTabBarProps) {
|
||||
const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
||||
|
||||
// 矢印キーでタブ間移動(端で wrap)+ Home/End。選択とフォーカスを同時に動かす
|
||||
// (role=tablist の標準操作)。SpaceDetail から移設。
|
||||
const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
|
||||
let next: number;
|
||||
if (e.key === 'ArrowRight') next = (index + 1) % tabs.length;
|
||||
else if (e.key === 'ArrowLeft') next = (index - 1 + tabs.length) % tabs.length;
|
||||
else if (e.key === 'Home') next = 0;
|
||||
else if (e.key === 'End') next = tabs.length - 1;
|
||||
else return;
|
||||
e.preventDefault();
|
||||
onSelect(tabs[next].id);
|
||||
tabRefs.current[next]?.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center border-b border-hairline pl-3 pr-2">
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label={ariaLabel}
|
||||
data-testid="space-chat-tabs"
|
||||
className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto"
|
||||
>
|
||||
{tabs.map((tb, i) => {
|
||||
const active = tb.id === activeTab;
|
||||
return (
|
||||
<button
|
||||
key={tb.id}
|
||||
ref={(el) => { tabRefs.current[i] = el; }}
|
||||
type="button"
|
||||
role="tab"
|
||||
id={`space-chat-tab-${tb.id}`}
|
||||
data-testid={`space-chat-tab-${tb.id}`}
|
||||
aria-selected={active}
|
||||
aria-controls="space-chat-tabpanel"
|
||||
tabIndex={active ? 0 : -1}
|
||||
onClick={() => onSelect(tb.id)}
|
||||
onKeyDown={(e) => handleKeyDown(e, i)}
|
||||
className={`-mb-px shrink-0 whitespace-nowrap border-b-2 px-3 py-2 text-sm font-semibold transition-colors ${appearClass(tb.id)} ${
|
||||
active
|
||||
? 'border-[var(--brand-primary)] text-slate-800'
|
||||
: 'border-transparent text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
{renderLabel(tb)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{actions && (
|
||||
<div data-testid="space-chat-actions" className="ml-2 flex shrink-0 items-center gap-1.5">
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -47,6 +47,7 @@ import { AppRunner } from './AppRunner';
|
||||
import { createSpaceGateway } from './app-file-gateway';
|
||||
import { detectAppEntry } from './app-bridge';
|
||||
import { ChatDetailSplit } from './ChatDetailSplit';
|
||||
import { SpaceChatTabBar } from './SpaceChatTabBar';
|
||||
import { OutputPreviewProvider } from '../../lib/output-preview-context';
|
||||
import { stripOutputPrefix } from '../../lib/output-path-detect';
|
||||
import {
|
||||
@@ -134,7 +135,7 @@ export function SpaceDetail({ spaceId, spaceTaskId, onSelectSpace, onSelectSpace
|
||||
|
||||
const dot = space.brandColor ?? 'var(--brand-primary)';
|
||||
|
||||
// 狭幅でチャットを開いているときは、スペースタイトルと「チャット|ファイル」タブ行を
|
||||
// 狭幅でチャットを開いているときは、ドット/タイトル/タブ群を1本化した統合ヘッダー行を
|
||||
// 隠してヘッダーの段数を減らす(会話画面が複数バーに埋もれないように)。広幅(md+)では
|
||||
// 常時表示、チャット未選択(一覧表示)の狭幅でも表示する。
|
||||
const chatOpen = tab === 'chat' && spaceTaskId != null;
|
||||
@@ -142,33 +143,32 @@ export function SpaceDetail({ spaceId, spaceTaskId, onSelectSpace, onSelectSpace
|
||||
|
||||
return (
|
||||
<div ref={containerRef} data-testid="space-detail" className="flex h-full flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className={`${hideOnMobileWhenChatOpen} items-center gap-2 border-b border-hairline bg-surface px-4 py-3`}>
|
||||
{/* Header + Tabs 統合行: 左からドット/タイトル/タブ群(スクロール)/右端固定
|
||||
(アバター・削除)。2行(約86px)→1行(約40px)。タイトルは max-w-[28ch] で
|
||||
truncate し、タブ群が flex-1 を持つ。 */}
|
||||
<div className={`${hideOnMobileWhenChatOpen} items-center gap-2 border-b border-hairline bg-surface pl-4 pr-2`}>
|
||||
<span className="h-2.5 w-2.5 shrink-0 rounded-full" style={{ background: dot }} aria-hidden />
|
||||
<SpaceHeaderTitle
|
||||
space={space}
|
||||
canManage={canManage}
|
||||
/>
|
||||
{space.kind === 'case' && (
|
||||
<SpaceMemberAvatars spaceId={spaceId} onManage={() => setTab('settings')} />
|
||||
)}
|
||||
{space.kind === 'case' && canManage && (
|
||||
<SpaceDeleteButton
|
||||
spaceId={spaceId}
|
||||
title={space.title}
|
||||
onDeleted={() => onSelectSpace?.(undefined)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className={`${hideOnMobileWhenChatOpen} items-center gap-1 border-b border-hairline px-3`}>
|
||||
<TabButton testid="space-tab-chat" active={tab === 'chat'} onClick={() => setTab('chat')}>{t('detail.tab.chat')}</TabButton>
|
||||
<TabButton testid="space-tab-files" active={tab === 'files'} onClick={() => setTab('files')}>{t('detail.tab.files')}</TabButton>
|
||||
<TabButton testid="space-tab-apps" active={tab === 'apps'} onClick={() => setTab('apps')}>{t('detail.tab.apps')}</TabButton>
|
||||
<TabButton testid="space-tab-calendar" active={tab === 'calendar'} onClick={() => setTab('calendar')}>{t('detail.tab.calendar')}</TabButton>
|
||||
<TabButton testid="space-tab-schedules" active={tab === 'schedules'} onClick={() => setTab('schedules')}>{t('detail.tab.schedules')}</TabButton>
|
||||
<TabButton testid="space-tab-settings" active={tab === 'settings'} onClick={() => setTab('settings')}>{t('detail.tab.settings')}</TabButton>
|
||||
<SpaceHeaderTitle space={space} canManage={canManage} />
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto">
|
||||
<TabButton testid="space-tab-chat" active={tab === 'chat'} onClick={() => setTab('chat')}>{t('detail.tab.chat')}</TabButton>
|
||||
<TabButton testid="space-tab-files" active={tab === 'files'} onClick={() => setTab('files')}>{t('detail.tab.files')}</TabButton>
|
||||
<TabButton testid="space-tab-apps" active={tab === 'apps'} onClick={() => setTab('apps')}>{t('detail.tab.apps')}</TabButton>
|
||||
<TabButton testid="space-tab-calendar" active={tab === 'calendar'} onClick={() => setTab('calendar')}>{t('detail.tab.calendar')}</TabButton>
|
||||
<TabButton testid="space-tab-schedules" active={tab === 'schedules'} onClick={() => setTab('schedules')}>{t('detail.tab.schedules')}</TabButton>
|
||||
<TabButton testid="space-tab-settings" active={tab === 'settings'} onClick={() => setTab('settings')}>{t('detail.tab.settings')}</TabButton>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{space.kind === 'case' && (
|
||||
<SpaceMemberAvatars spaceId={spaceId} onManage={() => setTab('settings')} />
|
||||
)}
|
||||
{space.kind === 'case' && canManage && (
|
||||
<SpaceDeleteButton
|
||||
spaceId={spaceId}
|
||||
title={space.title}
|
||||
onDeleted={() => onSelectSpace?.(undefined)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
@@ -210,8 +210,9 @@ export function SpaceDetail({ spaceId, spaceTaskId, onSelectSpace, onSelectSpace
|
||||
}
|
||||
|
||||
/**
|
||||
* ヘッダーのワークスペース名 + 説明。タイトル右に説明文を薄字で表示し、管理権限が
|
||||
* あれば鉛筆ボタンで編集ダイアログ(名前・色・説明)を開く。権限が無ければ表示のみ。
|
||||
* ヘッダーのワークスペース名。説明文はタイトルの title 属性(ツールチップ)に退避し、
|
||||
* 行の幅は max-w-[28ch] で制約してタブ群に flex-1 を譲る。管理権限があれば鉛筆ボタンで
|
||||
* 編集ダイアログ(名前・色・説明)を開く。権限が無ければ表示のみ。
|
||||
*/
|
||||
function SpaceHeaderTitle({
|
||||
space,
|
||||
@@ -224,17 +225,13 @@ function SpaceHeaderTitle({
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<h1 className="shrink truncate text-[15px] font-bold text-slate-800">{space.title}</h1>
|
||||
{space.description && (
|
||||
<span
|
||||
data-testid="space-description"
|
||||
title={space.description}
|
||||
className="hidden min-w-0 shrink truncate text-xs text-slate-400 sm:inline"
|
||||
>
|
||||
{space.description}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex min-w-0 max-w-[28ch] shrink items-center gap-1.5">
|
||||
<h1
|
||||
title={space.description || undefined}
|
||||
className="min-w-0 truncate text-[15px] font-bold text-slate-800"
|
||||
>
|
||||
{space.title}
|
||||
</h1>
|
||||
{canManage && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -394,7 +391,7 @@ function TabButton({
|
||||
type="button"
|
||||
data-testid={testid}
|
||||
onClick={onClick}
|
||||
className={`-mb-px border-b-2 px-3 py-2 text-sm font-semibold transition-colors ${
|
||||
className={`-mb-px shrink-0 whitespace-nowrap border-b-2 px-3 py-2 text-sm font-semibold transition-colors ${
|
||||
active
|
||||
? 'border-[var(--brand-primary)] text-slate-800'
|
||||
: 'border-transparent text-slate-500 hover:text-slate-700'
|
||||
@@ -667,7 +664,6 @@ function SpaceConversation({ taskId, onBack }: { taskId: number; onBack: () => v
|
||||
const detailTabs = useVisibleDetailTabs(taskId);
|
||||
const [activeTab, setActiveTab] = useState<DetailTabId | 'chat'>('chat');
|
||||
const tabs = [{ id: 'chat' as const, labelKey: 'tabs.chat' }, ...detailTabs];
|
||||
const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
||||
const fileBrowser = useFileBrowser(taskId);
|
||||
// モバイル(<md)だけスワイプ UI を「単一 DOM 枝」としてマウントする。両枝を CSS で
|
||||
// 隠して二重に描画すると ChatPane の textarea 等が複製され strict locator が壊れる
|
||||
@@ -682,20 +678,6 @@ function SpaceConversation({ taskId, onBack }: { taskId: number; onBack: () => v
|
||||
if (!tabs.some(tb => tb.id === activeTab)) setActiveTab('chat');
|
||||
}, [tabs, activeTab]);
|
||||
|
||||
// 矢印キーでタブ間移動(端で wrap)+ Home/End で先頭・末尾へ。選択とフォーカスを
|
||||
// 同時に動かす(role=tablist の標準操作)。
|
||||
const handleTabKeyDown = (e: React.KeyboardEvent, index: number) => {
|
||||
let next: number;
|
||||
if (e.key === 'ArrowRight') next = (index + 1) % tabs.length;
|
||||
else if (e.key === 'ArrowLeft') next = (index - 1 + tabs.length) % tabs.length;
|
||||
else if (e.key === 'Home') next = 0;
|
||||
else if (e.key === 'End') next = tabs.length - 1;
|
||||
else return;
|
||||
e.preventDefault();
|
||||
setActiveTab(tabs[next].id);
|
||||
tabRefs.current[next]?.focus();
|
||||
};
|
||||
|
||||
// タスクのファイルにアップロード/削除できるか。スペースメンバーでもタスク所有者で
|
||||
// なければサーバ側 checkTaskOwnership が 403 を返すため、UI も owner/admin/no-auth に限る。
|
||||
const auth = useAuthState();
|
||||
@@ -801,87 +783,43 @@ function SpaceConversation({ taskId, onBack }: { taskId: number; onBack: () => v
|
||||
{ts('conversation.backToList')}
|
||||
</button>
|
||||
|
||||
{/* アクション行: 削除 / 共有 / 継続 / 可視性。Tasks ページの DetailHeader と
|
||||
同じ実体(ShareButton・ContinueButton・ContinueWithPieceDialog・
|
||||
updateLocalTask)を再利用し、機能パリティを保つ。タブバーとは別行に置き、
|
||||
狭幅でも横並びのまま収まるアイコン主体の密度にする。 */}
|
||||
{chatReady && task && (
|
||||
<div
|
||||
data-testid="space-chat-actions"
|
||||
className="flex items-center gap-1.5 border-b border-hairline px-3 py-1.5"
|
||||
>
|
||||
{/* 公開範囲は選択不可: スペース内のチャットは常にそのスペースの
|
||||
メンバーだけに公開される。セレクタの代わりに静的な説明チップを置く。 */}
|
||||
<span
|
||||
data-testid="space-chat-visibility-note"
|
||||
title={ts('conversation.visibilityTitle')}
|
||||
className="inline-flex h-7 items-center rounded-md border border-hairline bg-canvas px-2 text-2xs text-slate-500"
|
||||
>
|
||||
{ts('conversation.visibilityNote')}
|
||||
</span>
|
||||
|
||||
<div className="ml-auto flex items-center gap-1">
|
||||
<ContinueButton
|
||||
testid="space-chat-continue"
|
||||
latestJobStatus={task.latestJob?.status ?? null}
|
||||
onClick={() => setContinueOpen(true)}
|
||||
/>
|
||||
<ShareButton
|
||||
testid="space-chat-share"
|
||||
taskId={taskId}
|
||||
shareToken={task.shareToken ?? null}
|
||||
onShareChange={refetchTask}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="space-chat-delete"
|
||||
onClick={() => void confirmAndDelete()}
|
||||
title={ts('common:delete')}
|
||||
aria-label={ts('common:delete')}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-md border border-hairline bg-canvas text-slate-500 transition-colors hover:border-red-200 hover:bg-red-50 hover:text-red-700 dark:hover:bg-red-500/15 dark:hover:text-red-300"
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M3 4h10M6.5 4V2.5h3V4M5 4l.5 9h5l.5-9" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 単一インプレース・タブバー。会話+詳細タブ(ファイルを除く)を1本に集約し、
|
||||
戻る導線を「会話」タブのみに統一する(オーバーレイ・✕ を廃止)。 */}
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label={t('chatTabsLabel')}
|
||||
data-testid="space-chat-tabs"
|
||||
className="flex items-center gap-1 overflow-x-auto border-b border-hairline px-3"
|
||||
>
|
||||
{tabs.map((tb, i) => {
|
||||
const active = tb.id === activeTab;
|
||||
return (
|
||||
<button
|
||||
key={tb.id}
|
||||
ref={(el) => { tabRefs.current[i] = el; }}
|
||||
type="button"
|
||||
role="tab"
|
||||
id={`space-chat-tab-${tb.id}`}
|
||||
data-testid={`space-chat-tab-${tb.id}`}
|
||||
aria-selected={active}
|
||||
aria-controls="space-chat-tabpanel"
|
||||
tabIndex={active ? 0 : -1}
|
||||
onClick={() => setActiveTab(tb.id)}
|
||||
onKeyDown={(e) => handleTabKeyDown(e, i)}
|
||||
className={`-mb-px shrink-0 whitespace-nowrap border-b-2 px-3 py-2 text-sm font-semibold transition-colors ${tabAppearClass(tb.id)} ${
|
||||
active
|
||||
? 'border-[var(--brand-primary)] text-slate-800'
|
||||
: 'border-transparent text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
{t(tb.labelKey)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<SpaceChatTabBar
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onSelect={(id) => setActiveTab(id as DetailTabId | 'chat')}
|
||||
ariaLabel={t('chatTabsLabel')}
|
||||
renderLabel={(tb) => t(tb.labelKey)}
|
||||
appearClass={(id) => tabAppearClass(id as DetailTabId | 'chat')}
|
||||
actions={
|
||||
chatReady && task ? (
|
||||
<>
|
||||
<ContinueButton
|
||||
testid="space-chat-continue"
|
||||
latestJobStatus={task.latestJob?.status ?? null}
|
||||
onClick={() => setContinueOpen(true)}
|
||||
/>
|
||||
<ShareButton
|
||||
testid="space-chat-share"
|
||||
taskId={taskId}
|
||||
shareToken={task.shareToken ?? null}
|
||||
onShareChange={refetchTask}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="space-chat-delete"
|
||||
onClick={() => void confirmAndDelete()}
|
||||
title={ts('common:delete')}
|
||||
aria-label={ts('common:delete')}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-md border border-hairline bg-canvas text-slate-500 transition-colors hover:border-red-200 hover:bg-red-50 hover:text-red-700 dark:hover:bg-red-500/15 dark:hover:text-red-300"
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M3 4h10M6.5 4V2.5h3V4M5 4l.5 9h5l.5-9" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
<div
|
||||
id="space-chat-tabpanel"
|
||||
|
||||
Reference in New Issue
Block a user