mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-09 18:15:50 +00:00
Converge AI Chat icon actions on ActionIconButton
This commit is contained in:
@@ -52,7 +52,6 @@ leave the transcript without exposing hidden provider/tool metadata.
|
||||
4. `internal/api/ai_handler.go` shared with `api-contracts`: Pulse Assistant handlers are both an AI runtime control surface and a canonical API payload contract boundary.
|
||||
5. `internal/api/ai_handlers.go` shared with `api-contracts`: AI settings and remediation handlers are both an AI runtime control surface and a canonical API payload contract boundary.
|
||||
6. `internal/api/ai_intelligence_handlers.go` shared with `api-contracts`: AI intelligence handlers are both an AI runtime control surface and a canonical API payload contract boundary.
|
||||
|
||||
## Extension Points
|
||||
|
||||
Guest-family AI runtime code paths (VMs and LXC system containers) share
|
||||
@@ -103,6 +102,13 @@ activity has arrived, and keeping successful raw output behind the details
|
||||
disclosure by default once the turn completes. Pending tools, skipped/canceled
|
||||
tools, failed tools, and completed-turn tool details must remain
|
||||
visible/inspectable; failed output may render inline because it is actionable.
|
||||
AI Chat icon-only actions own Assistant/session semantics, labels, and
|
||||
handlers, but their visible button chrome must compose the shared
|
||||
`ActionIconButton` primitive from `frontend-modern/src/components/shared/Button.tsx`.
|
||||
Header utilities, session row controls, transcript fallback close/download
|
||||
controls, activity-dock queued follow-up actions, composer send, footer
|
||||
help/route actions, and compact dismiss controls must not recreate local
|
||||
h-6/h-7/h-8/h-9 icon-button class shells.
|
||||
|
||||
Assistant live workflow status follows OpenCode's current-state timeline
|
||||
model. CANONICAL (supersedes the per-row transcript-status rules elsewhere in
|
||||
|
||||
@@ -557,6 +557,12 @@ not a replacement status card, CTA band, or page-local nested card.
|
||||
disabled treatment, title fallback, and accessible name wiring must come
|
||||
from that shared primitive rather than page-local `<button>` plus inline SVG
|
||||
shells.
|
||||
AI Chat follows that same boundary for drawer header controls, session row
|
||||
actions, transcript fallback close/download actions, activity-dock queued
|
||||
follow-up controls, composer send, footer help/route actions, and compact
|
||||
dismiss controls: the Assistant owns chat/session behavior and copy, while
|
||||
`ActionIconButton` owns h-6/h-7/h-8/h-9 sizing, outline, primary, accent,
|
||||
warning, info, danger, disabled, title, and focus chrome.
|
||||
Global app-shell prompts are part of the same action boundary.
|
||||
`frontend-modern/src/components/GitHubStarBanner.tsx` may own its display
|
||||
timing, product copy, and GitHub destination, but its primary, defer, and
|
||||
|
||||
@@ -1257,6 +1257,7 @@
|
||||
{ "path": "src/components/Alerts/AlertResourceTableMobile.tsx" },
|
||||
{ "path": "src/components/Alerts/AlertResourceTableRow.tsx" },
|
||||
{ "path": "src/components/Alerts/ResourceTable.tsx" },
|
||||
{ "path": "src/components/AI/Chat/index.tsx" },
|
||||
{ "path": "src/components/Settings/RolesPanel.tsx" },
|
||||
{ "path": "src/components/Settings/SSOProvidersPanel.tsx" }
|
||||
],
|
||||
@@ -1290,6 +1291,21 @@
|
||||
"text-slate-400 hover:text-white bg-surface hover:bg-slate-700 rounded-full p-1.5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "src/components/AI/Chat/index.tsx",
|
||||
"patterns": [
|
||||
"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-surface disabled:hover:text-muted",
|
||||
"flex-shrink-0 p-2 hover:text-base-content rounded-md hover:bg-surface-hover transition-colors",
|
||||
"inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950 disabled:cursor-wait disabled:opacity-70 dark:text-blue-200 dark:hover:bg-blue-900/60",
|
||||
"rounded p-1 text-muted opacity-0 transition-opacity hover:bg-blue-100 hover:text-blue-600 focus:opacity-100 group-hover:opacity-100 group-focus-within:opacity-100 dark:hover:bg-blue-900 dark:hover:text-blue-300",
|
||||
"order-2 flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md hover:text-base-content hover:bg-surface-hover transition-colors sm:order-none",
|
||||
"flex h-7 w-7 items-center justify-center rounded-md border border-amber-200 bg-surface text-amber-700 transition-colors hover:bg-amber-100 hover:text-amber-900 dark:border-amber-800 dark:bg-amber-950/60 dark:text-amber-200 dark:hover:bg-amber-900",
|
||||
"flex h-7 w-7 items-center justify-center rounded-md text-amber-700 transition-colors hover:bg-amber-100 hover:text-amber-900 dark:text-amber-200 dark:hover:bg-amber-900",
|
||||
"inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950 dark:text-blue-200 dark:hover:bg-blue-900/60",
|
||||
"flex h-9 w-9 items-center justify-center rounded-md bg-blue-600 text-white shadow-sm transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-45",
|
||||
"flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content focus:outline-none focus:ring-2 focus:ring-blue-500/30"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "src/components/Settings/SSOProvidersPanel.tsx",
|
||||
"patterns": [
|
||||
@@ -3170,6 +3186,136 @@
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ai-chat-action-icon-header-local-shell",
|
||||
"category": "action-button",
|
||||
"summary": "AI Chat header icon actions must not recreate local h-8 bordered icon-button shells; compose ActionIconButton.",
|
||||
"canonical": {
|
||||
"path": "src/components/shared/Button.tsx",
|
||||
"export": "ActionIconButton"
|
||||
},
|
||||
"scopes": ["src/components/AI/Chat"],
|
||||
"extensions": [".tsx"],
|
||||
"allPatterns": [
|
||||
"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content"
|
||||
],
|
||||
"legacyReason": "Retired migration debt. AI Chat header icon actions belong to ActionIconButton.",
|
||||
"allowedPaths": [],
|
||||
"ignoredPaths": [
|
||||
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
||||
"src/components/shared/Button.test.tsx"
|
||||
],
|
||||
"proof": [
|
||||
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
||||
"src/components/shared/Button.test.tsx",
|
||||
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ai-chat-action-icon-accent-local-shell",
|
||||
"category": "action-button",
|
||||
"summary": "AI Chat queued follow-up and session accent icon actions must not recreate local blue h-6/h-7 icon-button shells; compose ActionIconButton.",
|
||||
"canonical": {
|
||||
"path": "src/components/shared/Button.tsx",
|
||||
"export": "ActionIconButton"
|
||||
},
|
||||
"scopes": ["src/components/AI/Chat"],
|
||||
"extensions": [".tsx"],
|
||||
"allPatterns": [
|
||||
"inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950"
|
||||
],
|
||||
"legacyReason": "Retired migration debt. AI Chat accent icon actions belong to ActionIconButton.",
|
||||
"allowedPaths": [],
|
||||
"ignoredPaths": [
|
||||
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
||||
"src/components/shared/Button.test.tsx"
|
||||
],
|
||||
"proof": [
|
||||
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
||||
"src/components/shared/Button.test.tsx",
|
||||
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ai-chat-action-icon-primary-send-local-shell",
|
||||
"category": "action-button",
|
||||
"summary": "AI Chat composer send action must not recreate the local primary icon-button shell; compose ActionIconButton.",
|
||||
"canonical": {
|
||||
"path": "src/components/shared/Button.tsx",
|
||||
"export": "ActionIconButton"
|
||||
},
|
||||
"scopes": ["src/components/AI/Chat"],
|
||||
"extensions": [".tsx"],
|
||||
"allPatterns": [
|
||||
"flex h-9 w-9 items-center justify-center rounded-md bg-blue-600 text-white shadow-sm transition-colors hover:bg-blue-700"
|
||||
],
|
||||
"legacyReason": "Retired migration debt. AI Chat primary icon actions belong to ActionIconButton.",
|
||||
"allowedPaths": [],
|
||||
"ignoredPaths": [
|
||||
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
||||
"src/components/shared/Button.test.tsx"
|
||||
],
|
||||
"proof": [
|
||||
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
||||
"src/components/shared/Button.test.tsx",
|
||||
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ai-chat-action-icon-warning-local-shell",
|
||||
"category": "action-button",
|
||||
"summary": "AI Chat transcript fallback warning icon actions must not recreate local amber icon-button shells; compose ActionIconButton.",
|
||||
"canonical": {
|
||||
"path": "src/components/shared/Button.tsx",
|
||||
"export": "ActionIconButton"
|
||||
},
|
||||
"scopes": ["src/components/AI/Chat"],
|
||||
"extensions": [".tsx"],
|
||||
"allPatterns": [
|
||||
"flex h-7 w-7 items-center justify-center rounded-md border border-amber-200 bg-surface text-amber-700 transition-colors hover:bg-amber-100"
|
||||
],
|
||||
"legacyReason": "Retired migration debt. AI Chat warning icon actions belong to ActionIconButton.",
|
||||
"allowedPaths": [],
|
||||
"ignoredPaths": [
|
||||
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
||||
"src/components/shared/Button.test.tsx"
|
||||
],
|
||||
"proof": [
|
||||
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
||||
"src/components/shared/Button.test.tsx",
|
||||
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ai-chat-action-icon-footer-local-shell",
|
||||
"category": "action-button",
|
||||
"summary": "AI Chat composer footer icon actions must not recreate local bordered h-7 icon-button shells; compose ActionIconButton.",
|
||||
"canonical": {
|
||||
"path": "src/components/shared/Button.tsx",
|
||||
"export": "ActionIconButton"
|
||||
},
|
||||
"scopes": ["src/components/AI/Chat"],
|
||||
"extensions": [".tsx"],
|
||||
"allPatterns": [
|
||||
"flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content"
|
||||
],
|
||||
"legacyReason": "Retired migration debt. AI Chat footer icon actions belong to ActionIconButton.",
|
||||
"allowedPaths": [],
|
||||
"ignoredPaths": [
|
||||
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
||||
"src/components/shared/Button.test.tsx"
|
||||
],
|
||||
"proof": [
|
||||
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
||||
"src/components/shared/Button.test.tsx",
|
||||
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "settings-connection-editor-local-warning-callout-shell",
|
||||
"category": "callout",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Show, createSignal } from 'solid-js';
|
||||
import type { ChatMessage, ModelInfo, ModelRouteRecoveryOption } from '../types';
|
||||
import type { QueuedFollowUp } from '../hooks/useChat';
|
||||
import { WORKFLOW_STATUS_PACE_MS } from '../workflowStatusDisplay';
|
||||
import aiChatSource from '../index.tsx?raw';
|
||||
import { logger } from '@/utils/logger';
|
||||
|
||||
// ── Hoisted mocks (vi.mock factories reference these) ──────────────────────
|
||||
@@ -548,6 +549,28 @@ describe('AIChat', () => {
|
||||
// ── Rendering ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('rendering', () => {
|
||||
it('keeps icon-only action chrome on the shared ActionIconButton primitive', () => {
|
||||
expect(aiChatSource).toContain(
|
||||
"import { ActionIconButton } from '@/components/shared/Button';",
|
||||
);
|
||||
expect(aiChatSource).toContain('<ActionIconButton');
|
||||
for (const retiredActionIconShell of [
|
||||
'flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-surface disabled:hover:text-muted',
|
||||
'flex-shrink-0 p-2 hover:text-base-content rounded-md hover:bg-surface-hover transition-colors',
|
||||
'inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950 disabled:cursor-wait disabled:opacity-70 dark:text-blue-200 dark:hover:bg-blue-900/60',
|
||||
'rounded p-1 text-muted opacity-0 transition-opacity hover:bg-blue-100 hover:text-blue-600 focus:opacity-100 group-hover:opacity-100 group-focus-within:opacity-100 dark:hover:bg-blue-900 dark:hover:text-blue-300',
|
||||
'order-2 flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md hover:text-base-content hover:bg-surface-hover transition-colors sm:order-none',
|
||||
'flex h-7 w-7 items-center justify-center rounded-md border border-amber-200 bg-surface text-amber-700 transition-colors hover:bg-amber-100 hover:text-amber-900 dark:border-amber-800 dark:bg-amber-950/60 dark:text-amber-200 dark:hover:bg-amber-900',
|
||||
'flex h-7 w-7 items-center justify-center rounded-md text-amber-700 transition-colors hover:bg-amber-100 hover:text-amber-900 dark:text-amber-200 dark:hover:bg-amber-900',
|
||||
'inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950 dark:text-blue-200 dark:hover:bg-blue-900/60',
|
||||
'flex h-9 w-9 items-center justify-center rounded-md bg-blue-600 text-white shadow-sm transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-45',
|
||||
'flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content focus:outline-none focus:ring-2 focus:ring-blue-500/30',
|
||||
'disabled:opacity-45',
|
||||
]) {
|
||||
expect(aiChatSource).not.toContain(retiredActionIconShell);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders the header with title when open', () => {
|
||||
renderChat();
|
||||
expect(screen.getByText('Pulse Assistant')).toBeInTheDocument();
|
||||
@@ -2102,17 +2125,15 @@ describe('AIChat', () => {
|
||||
});
|
||||
|
||||
it('searches Assistant sessions from /sessions arguments without sending a provider prompt', async () => {
|
||||
mockAIChatAPI.listSessions
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
id: 's-backup',
|
||||
title: 'Backup Patrol Follow-up',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
message_count: 3,
|
||||
},
|
||||
]);
|
||||
mockAIChatAPI.listSessions.mockResolvedValueOnce([]).mockResolvedValueOnce([
|
||||
{
|
||||
id: 's-backup',
|
||||
title: 'Backup Patrol Follow-up',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
message_count: 3,
|
||||
},
|
||||
]);
|
||||
|
||||
renderChat();
|
||||
await waitFor(() => {
|
||||
@@ -2846,10 +2867,7 @@ describe('AIChat', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => expect(document.activeElement).toBe(queuedRow));
|
||||
expect(queuedRow).toHaveAttribute(
|
||||
'data-assistant-queue-command-target',
|
||||
'true',
|
||||
);
|
||||
expect(queuedRow).toHaveAttribute('data-assistant-queue-command-target', 'true');
|
||||
expect(textarea.value).toBe('');
|
||||
});
|
||||
|
||||
@@ -5689,7 +5707,9 @@ describe('AIChat', () => {
|
||||
'Selected route connection failed before any output; retrying. · attempt 2/3 · retrying in 1.9s',
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId('assistant-activity-dock').querySelector('[data-status-kind="retrying"]'),
|
||||
screen
|
||||
.getByTestId('assistant-activity-dock')
|
||||
.querySelector('[data-status-kind="retrying"]'),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
type ChatSession,
|
||||
type ChatSessionHandoffSummary,
|
||||
} from '@/api/aiChat';
|
||||
import { ActionIconButton } from '@/components/shared/Button';
|
||||
import { SearchField } from '@/components/shared/SearchField';
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
import { aiChatStore, type AIChatContext } from '@/stores/aiChat';
|
||||
@@ -153,9 +154,7 @@ import { AssistantCommandHelpDialog } from './AssistantCommandHelpDialog';
|
||||
import { ModelSelector } from './ModelSelector';
|
||||
import { MentionAutocomplete, type MentionResource } from './MentionAutocomplete';
|
||||
import { SlashCommandAutocomplete } from './SlashCommandAutocomplete';
|
||||
import {
|
||||
getAssistantActiveTurnStatus,
|
||||
} from './activeTurnStatus';
|
||||
import { getAssistantActiveTurnStatus } from './activeTurnStatus';
|
||||
import {
|
||||
createPacedWorkflowStatus,
|
||||
replaceLatestWorkflowStatusEventForDisplay,
|
||||
@@ -1588,8 +1587,7 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
(firstQueued ? queuedFollowUpRowRefs.get(firstQueued.id) : undefined) ||
|
||||
document.querySelector<HTMLElement>('[data-testid="assistant-queued-follow-up-row"]');
|
||||
const queuedRow = findQueuedRow();
|
||||
const targetQueuedId =
|
||||
firstQueued?.id ?? queuedRow?.dataset.assistantQueuedFollowUpId ?? null;
|
||||
const targetQueuedId = firstQueued?.id ?? queuedRow?.dataset.assistantQueuedFollowUpId ?? null;
|
||||
|
||||
if (!firstQueued && !queuedRow) {
|
||||
markQueuedFollowUpCommandTarget(null);
|
||||
@@ -2269,10 +2267,7 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
const modelRoute = normalizeAssistantModelRouteArgument(
|
||||
target,
|
||||
knownAssistantModelProviders(),
|
||||
);
|
||||
const modelRoute = normalizeAssistantModelRouteArgument(target, knownAssistantModelProviders());
|
||||
if (!modelRoute) {
|
||||
openModelSelector(target);
|
||||
focusComposer();
|
||||
@@ -2370,7 +2365,10 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
logger.error('[AIChat] Failed to load AI settings:', settingsResult.reason);
|
||||
}
|
||||
if (modelsResult.status === 'rejected') {
|
||||
logger.debug('[AIChat] Model catalog unavailable during initialization:', modelsResult.reason);
|
||||
logger.debug(
|
||||
'[AIChat] Model catalog unavailable during initialization:',
|
||||
modelsResult.reason,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[AIChat] Failed to initialize:', error);
|
||||
@@ -2472,17 +2470,14 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
const activeWorkflowStatusHistory = createMemo(() => {
|
||||
const message = activeAssistantMessage();
|
||||
if (!message || message.isStreaming === false) return [];
|
||||
return message.workflowStatusHistory || (message.workflowStatus ? [message.workflowStatus] : []);
|
||||
return (
|
||||
message.workflowStatusHistory || (message.workflowStatus ? [message.workflowStatus] : [])
|
||||
);
|
||||
});
|
||||
const activeWorkflowStatusPaceSequenceKey = createMemo(() => {
|
||||
const message = activeAssistantMessage();
|
||||
const first = activeWorkflowStatusHistory()[0];
|
||||
return [
|
||||
message?.id,
|
||||
first?.phase,
|
||||
first?.message,
|
||||
first?.startedAt,
|
||||
]
|
||||
return [message?.id, first?.phase, first?.message, first?.startedAt]
|
||||
.map((value) => String(value ?? ''))
|
||||
.join(':');
|
||||
});
|
||||
@@ -3961,22 +3956,18 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
</button>
|
||||
|
||||
<Show when={assistantNotificationsSupported()}>
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
onClick={() => {
|
||||
void handleToggleAttentionNotifications();
|
||||
}}
|
||||
class={`flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-border transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content ${
|
||||
assistantNotificationsEnabled()
|
||||
? 'bg-surface-alt text-base-content'
|
||||
: 'bg-surface text-muted'
|
||||
}`}
|
||||
tone={assistantNotificationsEnabled() ? 'outlineSelected' : 'outline'}
|
||||
size="md"
|
||||
label="Toggle Assistant attention notifications"
|
||||
title={
|
||||
assistantNotificationsEnabled()
|
||||
? 'Notifications on: you are alerted when the Assistant finishes or needs you while this tab is in the background'
|
||||
: 'Notify me when the Assistant finishes or needs me while this tab is in the background'
|
||||
}
|
||||
aria-label="Toggle Assistant attention notifications"
|
||||
aria-pressed={assistantNotificationsEnabled()}
|
||||
>
|
||||
<Show
|
||||
@@ -3985,18 +3976,18 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
>
|
||||
<BellIcon class="h-4 w-4" aria-hidden="true" />
|
||||
</Show>
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
</Show>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
onClick={() => {
|
||||
void handleForkSession();
|
||||
}}
|
||||
disabled={!canForkCurrentSession()}
|
||||
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-surface disabled:hover:text-muted"
|
||||
tone="outline"
|
||||
size="md"
|
||||
title={AI_CHAT_FORK_SESSION_LABEL}
|
||||
aria-label={AI_CHAT_FORK_SESSION_LABEL}
|
||||
label={AI_CHAT_FORK_SESSION_LABEL}
|
||||
aria-busy={forkingSession()}
|
||||
>
|
||||
<Show
|
||||
@@ -4005,17 +3996,17 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
>
|
||||
<LoaderCircleIcon class="h-4 w-4 animate-spin" aria-hidden="true" />
|
||||
</Show>
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
onClick={() => {
|
||||
void handleCompactSession();
|
||||
}}
|
||||
disabled={!canCompactCurrentSession()}
|
||||
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-surface disabled:hover:text-muted"
|
||||
tone="outline"
|
||||
size="md"
|
||||
title={AI_CHAT_COMPACT_SESSION_LABEL}
|
||||
aria-label={AI_CHAT_COMPACT_SESSION_LABEL}
|
||||
label={AI_CHAT_COMPACT_SESSION_LABEL}
|
||||
aria-busy={compactingSession()}
|
||||
>
|
||||
<Show
|
||||
@@ -4024,17 +4015,17 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
>
|
||||
<LoaderCircleIcon class="h-4 w-4 animate-spin" aria-hidden="true" />
|
||||
</Show>
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
onClick={() => {
|
||||
void handleUndoLastTurn();
|
||||
}}
|
||||
disabled={!canUndoLastTurn()}
|
||||
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-surface disabled:hover:text-muted"
|
||||
tone="outline"
|
||||
size="md"
|
||||
title={AI_CHAT_UNDO_LAST_TURN_LABEL}
|
||||
aria-label={AI_CHAT_UNDO_LAST_TURN_LABEL}
|
||||
label={AI_CHAT_UNDO_LAST_TURN_LABEL}
|
||||
aria-busy={undoingLastTurn()}
|
||||
>
|
||||
<Show
|
||||
@@ -4043,17 +4034,17 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
>
|
||||
<LoaderCircleIcon class="h-4 w-4 animate-spin" aria-hidden="true" />
|
||||
</Show>
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
onClick={() => {
|
||||
void handleRedoLastTurn();
|
||||
}}
|
||||
disabled={!canRedoLastTurn()}
|
||||
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-surface disabled:hover:text-muted"
|
||||
tone="outline"
|
||||
size="md"
|
||||
title={AI_CHAT_REDO_LAST_TURN_LABEL}
|
||||
aria-label={AI_CHAT_REDO_LAST_TURN_LABEL}
|
||||
label={AI_CHAT_REDO_LAST_TURN_LABEL}
|
||||
aria-busy={redoingLastTurn()}
|
||||
>
|
||||
<Show
|
||||
@@ -4062,61 +4053,61 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
>
|
||||
<LoaderCircleIcon class="h-4 w-4 animate-spin" aria-hidden="true" />
|
||||
</Show>
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
onClick={() => {
|
||||
void copyLastAssistantAnswer();
|
||||
}}
|
||||
disabled={!hasLastAssistantAnswer()}
|
||||
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-surface disabled:hover:text-muted"
|
||||
tone="outline"
|
||||
size="md"
|
||||
title={AI_CHAT_COPY_LAST_ANSWER_LABEL}
|
||||
aria-label={AI_CHAT_COPY_LAST_ANSWER_LABEL}
|
||||
label={AI_CHAT_COPY_LAST_ANSWER_LABEL}
|
||||
>
|
||||
<ClipboardCopyIcon class="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
onClick={() => {
|
||||
void copyAssistantTranscript();
|
||||
}}
|
||||
disabled={!hasCurrentTranscript()}
|
||||
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-surface disabled:hover:text-muted"
|
||||
tone="outline"
|
||||
size="md"
|
||||
title={AI_CHAT_COPY_TRANSCRIPT_LABEL}
|
||||
aria-label={AI_CHAT_COPY_TRANSCRIPT_LABEL}
|
||||
label={AI_CHAT_COPY_TRANSCRIPT_LABEL}
|
||||
>
|
||||
<CopyIcon class="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
onClick={exportAssistantTranscript}
|
||||
disabled={!hasCurrentTranscript()}
|
||||
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-surface disabled:hover:text-muted"
|
||||
tone="outline"
|
||||
size="md"
|
||||
title={AI_CHAT_EXPORT_TRANSCRIPT_LABEL}
|
||||
aria-label={AI_CHAT_EXPORT_TRANSCRIPT_LABEL}
|
||||
label={AI_CHAT_EXPORT_TRANSCRIPT_LABEL}
|
||||
>
|
||||
<DownloadIcon class="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
|
||||
{/* Session picker */}
|
||||
<div class="relative" data-dropdown>
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
ref={sessionButtonRef}
|
||||
onClick={() => {
|
||||
void handleToggleSessions();
|
||||
}}
|
||||
class="flex-shrink-0 p-2 hover:text-base-content rounded-md hover:bg-surface-hover transition-colors"
|
||||
tone="muted"
|
||||
size="md"
|
||||
title={AI_CHAT_SESSION_MENU_TITLE}
|
||||
aria-label={AI_CHAT_SESSION_MENU_TITLE}
|
||||
label={AI_CHAT_SESSION_MENU_TITLE}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={showSessions()}
|
||||
>
|
||||
<ClockIcon class="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
|
||||
<Show when={showSessions()}>
|
||||
<div
|
||||
@@ -4286,11 +4277,12 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
aria-label={`New title for ${session.title || 'Untitled'}`}
|
||||
class="min-w-0 flex-1 rounded border border-blue-300 bg-surface px-2 py-1 text-sm font-medium text-base-content outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 disabled:opacity-70 dark:border-blue-800"
|
||||
/>
|
||||
<button
|
||||
<ActionIconButton
|
||||
type="submit"
|
||||
disabled={sessionRenameSaving()}
|
||||
class="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950 disabled:cursor-wait disabled:opacity-70 dark:text-blue-200 dark:hover:bg-blue-900/60"
|
||||
aria-label={AI_CHAT_RENAME_SESSION_SAVE_LABEL}
|
||||
tone="accentGhost"
|
||||
size="sm"
|
||||
label={AI_CHAT_RENAME_SESSION_SAVE_LABEL}
|
||||
title={AI_CHAT_RENAME_SESSION_SAVE_LABEL}
|
||||
>
|
||||
<Show
|
||||
@@ -4304,60 +4296,65 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Show>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</ActionIconButton>
|
||||
<ActionIconButton
|
||||
disabled={sessionRenameSaving()}
|
||||
onClick={cancelRenamingSession}
|
||||
class="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted transition-colors hover:bg-surface-hover hover:text-base-content disabled:opacity-50"
|
||||
aria-label={AI_CHAT_RENAME_SESSION_CANCEL_LABEL}
|
||||
tone="muted"
|
||||
size="sm"
|
||||
label={AI_CHAT_RENAME_SESSION_CANCEL_LABEL}
|
||||
title={AI_CHAT_RENAME_SESSION_CANCEL_LABEL}
|
||||
>
|
||||
<XIcon class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
</div>
|
||||
</form>
|
||||
</Show>
|
||||
<Show when={!isSessionRenaming(session.id)}>
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-1 text-muted opacity-0 transition-opacity hover:bg-blue-100 hover:text-blue-600 focus:opacity-100 group-hover:opacity-100 group-focus-within:opacity-100 dark:hover:bg-blue-900 dark:hover:text-blue-300"
|
||||
<ActionIconButton
|
||||
tone="muted"
|
||||
size="xs"
|
||||
class="opacity-0 focus:opacity-100 group-hover:opacity-100 group-focus-within:opacity-100"
|
||||
onClick={(event) => startRenamingSession(session, event)}
|
||||
aria-label={getSessionRenameLabel(session)}
|
||||
label={getSessionRenameLabel(session)}
|
||||
title={getSessionRenameLabel(session)}
|
||||
>
|
||||
<PencilIcon class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={`rounded p-1 transition-opacity focus:opacity-100 hover:bg-blue-100 hover:text-blue-600 dark:hover:bg-blue-900 dark:hover:text-blue-300 ${
|
||||
</ActionIconButton>
|
||||
<ActionIconButton
|
||||
tone={
|
||||
isSessionPinned(session.id) ? 'accentGhost' : 'muted'
|
||||
}
|
||||
size="xs"
|
||||
class={`transition-opacity focus:opacity-100 ${
|
||||
isSessionPinned(session.id)
|
||||
? 'opacity-100 text-blue-600 dark:text-blue-300'
|
||||
: 'opacity-0 text-muted group-hover:opacity-100 group-focus-within:opacity-100'
|
||||
? 'opacity-100'
|
||||
: 'opacity-0 group-hover:opacity-100 group-focus-within:opacity-100'
|
||||
}`}
|
||||
onClick={(event) =>
|
||||
togglePinnedSession(session.id, event)
|
||||
}
|
||||
aria-pressed={isSessionPinned(session.id)}
|
||||
aria-label={getSessionPinLabel(session)}
|
||||
label={getSessionPinLabel(session)}
|
||||
title={getSessionPinLabel(session)}
|
||||
>
|
||||
<BookmarkIcon
|
||||
class={`h-3.5 w-3.5 ${isSessionPinned(session.id) ? 'fill-current' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-1 text-muted opacity-0 transition-opacity hover:bg-red-100 hover:text-red-500 focus:opacity-100 group-hover:opacity-100 group-focus-within:opacity-100 dark:hover:bg-red-900"
|
||||
</ActionIconButton>
|
||||
<ActionIconButton
|
||||
tone="danger"
|
||||
size="xs"
|
||||
class="opacity-0 focus:opacity-100 group-hover:opacity-100 group-focus-within:opacity-100"
|
||||
onClick={(event) =>
|
||||
handleDeleteSession(session.id, event)
|
||||
}
|
||||
aria-label={getSessionDeleteLabel(session)}
|
||||
label={getSessionDeleteLabel(session)}
|
||||
title={getSessionDeleteLabel(session)}
|
||||
>
|
||||
<Trash2Icon class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
@@ -4374,19 +4371,20 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
</div>
|
||||
|
||||
{/* Close button (Always visible as fallback) */}
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onClose();
|
||||
}}
|
||||
class="order-2 flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md hover:text-base-content hover:bg-surface-hover transition-colors sm:order-none"
|
||||
tone="neutral"
|
||||
size="lg"
|
||||
class="order-2 sm:order-none"
|
||||
title={AI_CHAT_CLOSE_LABEL}
|
||||
aria-label={AI_CHAT_CLOSE_LABEL}
|
||||
label={AI_CHAT_CLOSE_LABEL}
|
||||
data-testid="assistant-close-button"
|
||||
>
|
||||
<XIcon class="h-5 w-5" />
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
</div>
|
||||
|
||||
<Show when={transcriptCopyFallback()}>
|
||||
@@ -4398,24 +4396,24 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
<div class="mb-2 flex items-center justify-between gap-2">
|
||||
<div class="text-xs font-semibold">{AI_CHAT_TRANSCRIPT_FALLBACK_TITLE}</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
onClick={downloadFallbackTranscript}
|
||||
class="flex h-7 w-7 items-center justify-center rounded-md border border-amber-200 bg-surface text-amber-700 transition-colors hover:bg-amber-100 hover:text-amber-900 dark:border-amber-800 dark:bg-amber-950/60 dark:text-amber-200 dark:hover:bg-amber-900"
|
||||
tone="warningOutline"
|
||||
size="sm"
|
||||
title={AI_CHAT_TRANSCRIPT_FALLBACK_DOWNLOAD_LABEL}
|
||||
aria-label={AI_CHAT_TRANSCRIPT_FALLBACK_DOWNLOAD_LABEL}
|
||||
label={AI_CHAT_TRANSCRIPT_FALLBACK_DOWNLOAD_LABEL}
|
||||
>
|
||||
<DownloadIcon class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</ActionIconButton>
|
||||
<ActionIconButton
|
||||
onClick={() => setTranscriptCopyFallback(null)}
|
||||
class="flex h-7 w-7 items-center justify-center rounded-md text-amber-700 transition-colors hover:bg-amber-100 hover:text-amber-900 dark:text-amber-200 dark:hover:bg-amber-900"
|
||||
tone="warningGhost"
|
||||
size="sm"
|
||||
title={AI_CHAT_TRANSCRIPT_FALLBACK_CLOSE_LABEL}
|
||||
aria-label={AI_CHAT_TRANSCRIPT_FALLBACK_CLOSE_LABEL}
|
||||
label={AI_CHAT_TRANSCRIPT_FALLBACK_CLOSE_LABEL}
|
||||
>
|
||||
<XIcon class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
@@ -4551,22 +4549,15 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
{AI_CHAT_DISCOVERY_HINT_BODY}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
onClick={() => setDiscoveryHintDismissed(true)}
|
||||
class="p-1 rounded hover:bg-cyan-100 dark:hover:bg-cyan-800 text-cyan-500 dark:text-cyan-400"
|
||||
tone="infoGhost"
|
||||
size="xs"
|
||||
title={AI_CHAT_DISCOVERY_HINT_DISMISS_LABEL}
|
||||
aria-label={AI_CHAT_DISCOVERY_HINT_DISMISS_LABEL}
|
||||
label={AI_CHAT_DISCOVERY_HINT_DISMISS_LABEL}
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<XIcon class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</ActionIconButton>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -4696,19 +4687,20 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
onClick={stopActiveResponse}
|
||||
class={`inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md border bg-surface text-base-content shadow-sm transition-colors hover:bg-surface-hover ${
|
||||
tone="outline"
|
||||
size="sm"
|
||||
class={
|
||||
interruptArmed()
|
||||
? 'border-blue-400 ring-2 ring-blue-500/30'
|
||||
: 'border-border'
|
||||
}`}
|
||||
}
|
||||
title={interruptArmed() ? 'Stop response armed' : 'Stop'}
|
||||
aria-label={interruptArmed() ? 'Stop response armed' : 'Stop response'}
|
||||
label={interruptArmed() ? 'Stop response armed' : 'Stop response'}
|
||||
>
|
||||
<SquareIcon class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={autonomousWarningVisible()}>
|
||||
@@ -4735,15 +4727,15 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
>
|
||||
Switch to Approval
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
onClick={() => setAutonomousBannerDismissed(true)}
|
||||
class="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-red-500 transition-colors hover:bg-red-50 hover:text-red-700 dark:text-red-200 dark:hover:bg-red-950/40"
|
||||
tone="danger"
|
||||
size="xs"
|
||||
title={AI_CHAT_AUTONOMOUS_WARNING_DISMISS_LABEL}
|
||||
aria-label={AI_CHAT_AUTONOMOUS_WARNING_DISMISS_LABEL}
|
||||
label={AI_CHAT_AUTONOMOUS_WARNING_DISMISS_LABEL}
|
||||
>
|
||||
<XIcon class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={activityDockQueuedFollowUpCount() > 0}>
|
||||
@@ -4766,18 +4758,18 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
)}{' '}
|
||||
{chat.queuedFollowUpsPaused() ? 'paused' : 'queued'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
onClick={() => {
|
||||
chat.clearQueuedFollowUps();
|
||||
focusComposer();
|
||||
}}
|
||||
class="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-900 dark:text-blue-200 dark:hover:bg-blue-900/50"
|
||||
tone="accentGhost"
|
||||
size="xs"
|
||||
title="Clear queued follow-ups"
|
||||
aria-label="Clear queued follow-up messages"
|
||||
label="Clear queued follow-up messages"
|
||||
>
|
||||
<XIcon class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
</div>
|
||||
<div class="mt-1 max-h-24 space-y-1 overflow-y-auto" role="list">
|
||||
<For each={chat.queuedFollowUps()}>
|
||||
@@ -4824,48 +4816,48 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
</Show>
|
||||
</span>
|
||||
<Show when={chat.queuedFollowUpCount() > 1 && index() > 0}>
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
onClick={() => sendQueuedFollowUpNext(queued.id)}
|
||||
class="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950 dark:text-blue-200 dark:hover:bg-blue-900/60"
|
||||
tone="accentGhost"
|
||||
size="xs"
|
||||
title="Send queued follow-up next"
|
||||
aria-label={`Send queued follow-up next: ${preview()}`}
|
||||
label={`Send queued follow-up next: ${preview()}`}
|
||||
>
|
||||
<SendIcon class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
</Show>
|
||||
<Show when={chat.queuedFollowUpsPaused() && index() === 0}>
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
onClick={() => sendQueuedFollowUpNext(queued.id)}
|
||||
class="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950 dark:text-blue-200 dark:hover:bg-blue-900/60"
|
||||
tone="accentGhost"
|
||||
size="xs"
|
||||
title="Resume queued follow-up"
|
||||
aria-label={`Resume queued follow-up: ${preview()}`}
|
||||
label={`Resume queued follow-up: ${preview()}`}
|
||||
>
|
||||
<SendIcon class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
onClick={() => editQueuedFollowUp(queued.id)}
|
||||
class="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950 dark:text-blue-200 dark:hover:bg-blue-900/60"
|
||||
tone="accentGhost"
|
||||
size="xs"
|
||||
title="Edit queued follow-up"
|
||||
aria-label={`Edit queued follow-up: ${preview()}`}
|
||||
label={`Edit queued follow-up: ${preview()}`}
|
||||
>
|
||||
<PencilIcon class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</ActionIconButton>
|
||||
<ActionIconButton
|
||||
onClick={() => {
|
||||
chat.cancelQueuedFollowUp(queued.id);
|
||||
focusComposer();
|
||||
}}
|
||||
class="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950 dark:text-blue-200 dark:hover:bg-blue-900/60"
|
||||
tone="accentGhost"
|
||||
size="xs"
|
||||
title="Remove queued follow-up"
|
||||
aria-label={`Remove queued follow-up: ${preview()}`}
|
||||
label={`Remove queued follow-up: ${preview()}`}
|
||||
>
|
||||
<XIcon class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
@@ -4918,15 +4910,16 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
visible={slashCommandActive()}
|
||||
/>
|
||||
<div class="absolute bottom-2 right-2 flex items-center gap-1.5">
|
||||
<button
|
||||
<ActionIconButton
|
||||
type="submit"
|
||||
disabled={!input().trim()}
|
||||
class="flex h-9 w-9 items-center justify-center rounded-md bg-blue-600 text-white shadow-sm transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-45"
|
||||
tone="primary"
|
||||
size="lg"
|
||||
title={chat.isLoading() ? 'Queue follow-up' : 'Send'}
|
||||
aria-label={chat.isLoading() ? 'Queue follow-up' : 'Send message'}
|
||||
label={chat.isLoading() ? 'Queue follow-up' : 'Send message'}
|
||||
>
|
||||
<SendIcon class="h-4 w-4" />
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -4972,34 +4965,34 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
<ActionIconButton
|
||||
onClick={openAssistantCommandHelp}
|
||||
class="flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content focus:outline-none focus:ring-2 focus:ring-blue-500/30"
|
||||
tone="outline"
|
||||
size="sm"
|
||||
title={AI_CHAT_COMMAND_HELP_BUTTON_LABEL}
|
||||
aria-label={AI_CHAT_COMMAND_HELP_BUTTON_LABEL}
|
||||
label={AI_CHAT_COMMAND_HELP_BUTTON_LABEL}
|
||||
data-testid="assistant-command-help-trigger"
|
||||
>
|
||||
<CircleHelpIcon class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</ActionIconButton>
|
||||
<ActionIconButton
|
||||
onClick={() => cycleRecentModelRoute()}
|
||||
disabled={!nextRecentModelRoute()}
|
||||
class="flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:text-base-content disabled:cursor-not-allowed disabled:opacity-45"
|
||||
tone="outline"
|
||||
size="sm"
|
||||
title={
|
||||
nextRecentModelRouteLabel()
|
||||
? `${AI_CHAT_CYCLE_RECENT_MODEL_LABEL}: ${nextRecentModelRouteLabel()}`
|
||||
: AI_CHAT_CYCLE_RECENT_MODEL_LABEL
|
||||
}
|
||||
aria-label={
|
||||
label={
|
||||
nextRecentModelRouteLabel()
|
||||
? `${AI_CHAT_CYCLE_RECENT_MODEL_LABEL}: ${nextRecentModelRouteLabel()}`
|
||||
: AI_CHAT_CYCLE_RECENT_MODEL_LABEL
|
||||
}
|
||||
>
|
||||
<RotateCwIcon class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</ActionIconButton>
|
||||
|
||||
<div class="relative" data-dropdown>
|
||||
<button
|
||||
|
||||
@@ -231,9 +231,32 @@ describe('Button', () => {
|
||||
const onClick = vi.fn();
|
||||
|
||||
render(() => (
|
||||
<ActionIconButton label="Edit thresholds" tone="accent" size="xs" onClick={onClick}>
|
||||
<span aria-hidden="true">E</span>
|
||||
</ActionIconButton>
|
||||
<>
|
||||
<ActionIconButton label="Edit thresholds" tone="accent" size="xs" onClick={onClick}>
|
||||
<span aria-hidden="true">E</span>
|
||||
</ActionIconButton>
|
||||
<ActionIconButton label="Open help" tone="outline" size="md">
|
||||
<span aria-hidden="true">?</span>
|
||||
</ActionIconButton>
|
||||
<ActionIconButton label="Notifications enabled" tone="outlineSelected" size="md">
|
||||
<span aria-hidden="true">N</span>
|
||||
</ActionIconButton>
|
||||
<ActionIconButton label="Send message" tone="primary" size="lg">
|
||||
<span aria-hidden="true">S</span>
|
||||
</ActionIconButton>
|
||||
<ActionIconButton label="Promote queued follow-up" tone="accentGhost" size="xs">
|
||||
<span aria-hidden="true">P</span>
|
||||
</ActionIconButton>
|
||||
<ActionIconButton label="Dismiss warning" tone="warningGhost" size="sm">
|
||||
<span aria-hidden="true">W</span>
|
||||
</ActionIconButton>
|
||||
<ActionIconButton label="Download fallback" tone="warningOutline" size="sm">
|
||||
<span aria-hidden="true">D</span>
|
||||
</ActionIconButton>
|
||||
<ActionIconButton label="Dismiss info" tone="infoGhost" size="xs">
|
||||
<span aria-hidden="true">I</span>
|
||||
</ActionIconButton>
|
||||
</>
|
||||
));
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Edit thresholds' });
|
||||
@@ -243,6 +266,21 @@ describe('Button', () => {
|
||||
expect(button).toHaveClass('w-6');
|
||||
expect(button).toHaveClass('bg-blue-50');
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Open help' })).toHaveClass('border-border');
|
||||
expect(screen.getByRole('button', { name: 'Notifications enabled' })).toHaveClass(
|
||||
'bg-surface-alt',
|
||||
);
|
||||
expect(screen.getByRole('button', { name: 'Send message' })).toHaveClass('h-9');
|
||||
expect(screen.getByRole('button', { name: 'Send message' })).toHaveClass('bg-blue-600');
|
||||
expect(screen.getByRole('button', { name: 'Promote queued follow-up' })).toHaveClass(
|
||||
'text-blue-700',
|
||||
);
|
||||
expect(screen.getByRole('button', { name: 'Dismiss warning' })).toHaveClass('text-amber-700');
|
||||
expect(screen.getByRole('button', { name: 'Download fallback' })).toHaveClass(
|
||||
'border-amber-200',
|
||||
);
|
||||
expect(screen.getByRole('button', { name: 'Dismiss info' })).toHaveClass('text-cyan-500');
|
||||
|
||||
button.click();
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -2982,6 +2982,21 @@ describe('shared primitive guardrails', () => {
|
||||
const alertResourceActionGuard = registry.patternGuards?.find(
|
||||
(guard) => guard.id === 'alert-resource-action-local-svg-button-shell',
|
||||
);
|
||||
const aiChatActionIconHeaderGuard = registry.patternGuards?.find(
|
||||
(guard) => guard.id === 'ai-chat-action-icon-header-local-shell',
|
||||
);
|
||||
const aiChatActionIconAccentGuard = registry.patternGuards?.find(
|
||||
(guard) => guard.id === 'ai-chat-action-icon-accent-local-shell',
|
||||
);
|
||||
const aiChatActionIconPrimarySendGuard = registry.patternGuards?.find(
|
||||
(guard) => guard.id === 'ai-chat-action-icon-primary-send-local-shell',
|
||||
);
|
||||
const aiChatActionIconWarningGuard = registry.patternGuards?.find(
|
||||
(guard) => guard.id === 'ai-chat-action-icon-warning-local-shell',
|
||||
);
|
||||
const aiChatActionIconFooterGuard = registry.patternGuards?.find(
|
||||
(guard) => guard.id === 'ai-chat-action-icon-footer-local-shell',
|
||||
);
|
||||
const commandCopyGuard = registry.patternGuards?.find(
|
||||
(guard) => guard.id === 'button-command-copy-local-shell',
|
||||
);
|
||||
@@ -3817,6 +3832,7 @@ describe('shared primitive guardrails', () => {
|
||||
'src/components/Alerts/AlertResourceTableMobile.tsx',
|
||||
'src/components/Alerts/AlertResourceTableRow.tsx',
|
||||
'src/components/Alerts/ResourceTable.tsx',
|
||||
'src/components/AI/Chat/index.tsx',
|
||||
'src/components/Settings/RolesPanel.tsx',
|
||||
'src/components/Settings/SSOProvidersPanel.tsx',
|
||||
]);
|
||||
@@ -3855,6 +3871,21 @@ describe('shared primitive guardrails', () => {
|
||||
'text-slate-400 hover:text-white bg-surface hover:bg-slate-700 rounded-full p-1.5',
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
path: 'src/components/AI/Chat/index.tsx',
|
||||
patterns: expect.arrayContaining([
|
||||
'flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-surface disabled:hover:text-muted',
|
||||
'flex-shrink-0 p-2 hover:text-base-content rounded-md hover:bg-surface-hover transition-colors',
|
||||
'inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950 disabled:cursor-wait disabled:opacity-70 dark:text-blue-200 dark:hover:bg-blue-900/60',
|
||||
'rounded p-1 text-muted opacity-0 transition-opacity hover:bg-blue-100 hover:text-blue-600 focus:opacity-100 group-hover:opacity-100 group-focus-within:opacity-100 dark:hover:bg-blue-900 dark:hover:text-blue-300',
|
||||
'order-2 flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md hover:text-base-content hover:bg-surface-hover transition-colors sm:order-none',
|
||||
'flex h-7 w-7 items-center justify-center rounded-md border border-amber-200 bg-surface text-amber-700 transition-colors hover:bg-amber-100 hover:text-amber-900 dark:border-amber-800 dark:bg-amber-950/60 dark:text-amber-200 dark:hover:bg-amber-900',
|
||||
'flex h-7 w-7 items-center justify-center rounded-md text-amber-700 transition-colors hover:bg-amber-100 hover:text-amber-900 dark:text-amber-200 dark:hover:bg-amber-900',
|
||||
'inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950 dark:text-blue-200 dark:hover:bg-blue-900/60',
|
||||
'flex h-9 w-9 items-center justify-center rounded-md bg-blue-600 text-white shadow-sm transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-45',
|
||||
'flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content focus:outline-none focus:ring-2 focus:ring-blue-500/30',
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
path: 'src/components/Settings/SSOProvidersPanel.tsx',
|
||||
patterns: expect.arrayContaining([
|
||||
@@ -3882,6 +3913,37 @@ describe('shared primitive guardrails', () => {
|
||||
'src/components/shared/Button.test.tsx',
|
||||
'src/components/shared/SharedPrimitives.guardrails.test.ts',
|
||||
]);
|
||||
for (const guard of [
|
||||
aiChatActionIconHeaderGuard,
|
||||
aiChatActionIconAccentGuard,
|
||||
aiChatActionIconPrimarySendGuard,
|
||||
aiChatActionIconWarningGuard,
|
||||
aiChatActionIconFooterGuard,
|
||||
]) {
|
||||
expect(guard?.canonical?.path).toBe('src/components/shared/Button.tsx');
|
||||
expect(guard?.canonical?.export).toBe('ActionIconButton');
|
||||
expect(guard?.scopes).toEqual(['src/components/AI/Chat']);
|
||||
expect(guard?.allowedPaths ?? []).toHaveLength(0);
|
||||
expect(guard?.ignoredPaths).toEqual([
|
||||
'src/components/AI/Chat/__tests__/AIChat.test.tsx',
|
||||
'src/components/shared/Button.test.tsx',
|
||||
]);
|
||||
}
|
||||
expect(aiChatActionIconHeaderGuard?.allPatterns).toEqual([
|
||||
'flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content',
|
||||
]);
|
||||
expect(aiChatActionIconAccentGuard?.allPatterns).toEqual([
|
||||
'inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950',
|
||||
]);
|
||||
expect(aiChatActionIconPrimarySendGuard?.allPatterns).toEqual([
|
||||
'flex h-9 w-9 items-center justify-center rounded-md bg-blue-600 text-white shadow-sm transition-colors hover:bg-blue-700',
|
||||
]);
|
||||
expect(aiChatActionIconWarningGuard?.allPatterns).toEqual([
|
||||
'flex h-7 w-7 items-center justify-center rounded-md border border-amber-200 bg-surface text-amber-700 transition-colors hover:bg-amber-100',
|
||||
]);
|
||||
expect(aiChatActionIconFooterGuard?.allPatterns).toEqual([
|
||||
'flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content',
|
||||
]);
|
||||
expect(copyValueRule?.canonical?.path).toBe('src/components/shared/Button.tsx');
|
||||
expect(copyValueRule?.canonical?.export).toBe('CopyValueButton');
|
||||
expect(copyValueRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual([
|
||||
@@ -3962,6 +4024,14 @@ describe('shared primitive guardrails', () => {
|
||||
expect(buttonModelSource).toContain('COPY_VALUE_BUTTON_SIZE_CLASSES');
|
||||
expect(buttonModelSource).toContain('ACTION_ICON_BUTTON_TONE_CLASSES');
|
||||
expect(buttonModelSource).toContain('ACTION_ICON_BUTTON_SIZE_CLASSES');
|
||||
expect(buttonModelSource).toContain('outline:');
|
||||
expect(buttonModelSource).toContain('outlineSelected:');
|
||||
expect(buttonModelSource).toContain('primary:');
|
||||
expect(buttonModelSource).toContain('accentGhost:');
|
||||
expect(buttonModelSource).toContain('warningGhost:');
|
||||
expect(buttonModelSource).toContain('warningOutline:');
|
||||
expect(buttonModelSource).toContain('infoGhost:');
|
||||
expect(buttonModelSource).toContain(['lg', ": 'h-9 w-9'"].join(''));
|
||||
expect(buttonModelSource).toContain('dangerOutline:');
|
||||
expect(buttonModelSource).toContain('settingsAction:');
|
||||
expect(buttonModelSource).toContain('getCopyValueButtonClass');
|
||||
@@ -3993,6 +4063,22 @@ describe('shared primitive guardrails', () => {
|
||||
expect(chatMessagesSource).not.toContain(
|
||||
'rounded-md border border-border bg-surface px-3 py-1.5 text-xs font-medium text-base-content',
|
||||
);
|
||||
expect(aiChatSource).toContain('@/components/shared/Button');
|
||||
expect(aiChatSource).toContain('ActionIconButton');
|
||||
for (const retiredActionIconShell of [
|
||||
'flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-surface disabled:hover:text-muted',
|
||||
'flex-shrink-0 p-2 hover:text-base-content rounded-md hover:bg-surface-hover transition-colors',
|
||||
'inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950 disabled:cursor-wait disabled:opacity-70 dark:text-blue-200 dark:hover:bg-blue-900/60',
|
||||
'rounded p-1 text-muted opacity-0 transition-opacity hover:bg-blue-100 hover:text-blue-600 focus:opacity-100 group-hover:opacity-100 group-focus-within:opacity-100 dark:hover:bg-blue-900 dark:hover:text-blue-300',
|
||||
'order-2 flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md hover:text-base-content hover:bg-surface-hover transition-colors sm:order-none',
|
||||
'flex h-7 w-7 items-center justify-center rounded-md border border-amber-200 bg-surface text-amber-700 transition-colors hover:bg-amber-100 hover:text-amber-900 dark:border-amber-800 dark:bg-amber-950/60 dark:text-amber-200 dark:hover:bg-amber-900',
|
||||
'flex h-7 w-7 items-center justify-center rounded-md text-amber-700 transition-colors hover:bg-amber-100 hover:text-amber-900 dark:text-amber-200 dark:hover:bg-amber-900',
|
||||
'inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950 dark:text-blue-200 dark:hover:bg-blue-900/60',
|
||||
'flex h-9 w-9 items-center justify-center rounded-md bg-blue-600 text-white shadow-sm transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-45',
|
||||
'flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content focus:outline-none focus:ring-2 focus:ring-blue-500/30',
|
||||
]) {
|
||||
expect(aiChatSource).not.toContain(retiredActionIconShell);
|
||||
}
|
||||
expect(resourceDetailDrawerDebugTabSource).toContain('@/components/shared/Button');
|
||||
expect(resourceDetailDrawerDebugTabSource).not.toContain(
|
||||
'rounded-md border border-border bg-surface px-3 py-1.5 text-xs font-medium text-base-content',
|
||||
|
||||
@@ -80,8 +80,20 @@ export const getButtonClass = (options: ButtonClassOptions = {}): string =>
|
||||
|
||||
export type CopyValueButtonVariant = 'neutral' | 'ghost' | 'accent' | 'chip';
|
||||
export type CopyValueButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'chip';
|
||||
export type ActionIconButtonTone = 'neutral' | 'muted' | 'accent' | 'success' | 'danger';
|
||||
export type ActionIconButtonSize = 'xs' | 'sm' | 'md';
|
||||
export type ActionIconButtonTone =
|
||||
| 'neutral'
|
||||
| 'muted'
|
||||
| 'outline'
|
||||
| 'outlineSelected'
|
||||
| 'primary'
|
||||
| 'accent'
|
||||
| 'accentGhost'
|
||||
| 'success'
|
||||
| 'warningGhost'
|
||||
| 'warningOutline'
|
||||
| 'infoGhost'
|
||||
| 'danger';
|
||||
export type ActionIconButtonSize = 'xs' | 'sm' | 'md' | 'lg';
|
||||
|
||||
export const COPY_VALUE_BUTTON_BASE_CLASS =
|
||||
'inline-flex shrink-0 items-center justify-center rounded transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50';
|
||||
@@ -124,10 +136,22 @@ export const ACTION_ICON_BUTTON_BASE_CLASS =
|
||||
export const ACTION_ICON_BUTTON_TONE_CLASSES: Record<ActionIconButtonTone, string> = {
|
||||
neutral: 'text-base-content hover:bg-surface-hover hover:text-base-content',
|
||||
muted: 'text-muted hover:bg-surface-hover hover:text-base-content',
|
||||
outline:
|
||||
'border border-border bg-surface text-muted shadow-sm hover:border-border hover:bg-surface-hover hover:text-base-content',
|
||||
outlineSelected:
|
||||
'border border-border bg-surface-alt text-base-content shadow-sm hover:border-border hover:bg-surface-hover hover:text-base-content',
|
||||
primary: 'border border-transparent bg-blue-600 text-white shadow-sm hover:bg-blue-700',
|
||||
accent:
|
||||
'bg-blue-50 text-blue-600 hover:bg-blue-100 hover:text-blue-700 dark:bg-blue-900 dark:text-blue-400 dark:hover:bg-blue-950 dark:hover:text-blue-300',
|
||||
accentGhost:
|
||||
'text-blue-700 hover:bg-blue-100 hover:text-blue-950 dark:text-blue-200 dark:hover:bg-blue-900/60',
|
||||
success:
|
||||
'bg-green-50 text-green-600 hover:bg-green-100 hover:text-green-700 dark:bg-green-900 dark:text-green-400 dark:hover:bg-green-950 dark:hover:text-green-300',
|
||||
warningGhost:
|
||||
'text-amber-700 hover:bg-amber-100 hover:text-amber-900 dark:text-amber-200 dark:hover:bg-amber-900',
|
||||
warningOutline:
|
||||
'border border-amber-200 bg-surface text-amber-700 hover:bg-amber-100 hover:text-amber-900 dark:border-amber-800 dark:bg-amber-950/60 dark:text-amber-200 dark:hover:bg-amber-900',
|
||||
infoGhost: 'text-cyan-500 hover:bg-cyan-100 dark:text-cyan-400 dark:hover:bg-cyan-800',
|
||||
danger:
|
||||
'text-red-600 hover:bg-red-50 hover:text-red-700 dark:text-red-400 dark:hover:bg-red-950 dark:hover:text-red-300',
|
||||
};
|
||||
@@ -136,6 +160,7 @@ export const ACTION_ICON_BUTTON_SIZE_CLASSES: Record<ActionIconButtonSize, strin
|
||||
xs: 'h-6 w-6',
|
||||
sm: 'h-7 w-7',
|
||||
md: 'h-8 w-8',
|
||||
lg: 'h-9 w-9',
|
||||
};
|
||||
|
||||
export type ActionIconButtonClassOptions = {
|
||||
|
||||
Reference in New Issue
Block a user