mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
fix(ui): deep audit pass — palette navigation, crash fix, honest badges
- CommandPalette: full route navigation, project deep-links fetched on open, Create group only where onAddService is wired, dead Connect items removed, decorative kbd hints dropped; workspace passes its own view targets - NodeDetailPage crash: container status arrives as a Docker state object, not a string — flattened in listAgentContainers and guarded at render for persisted-cache payloads - env badge reflects import.meta.env.MODE instead of hardcoded 'production' - Relative times capped: >1y or future timestamps render 'n/a'/'—' (zero-time heartbeats no longer show 739898d ago) - Post-auth and setup redirects land on / (dashboard is the index) Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
24b9b1267e
commit
fc82c30765
9 files changed
+154
-153
No files matched your search
@@ -43,9 +43,11 @@ import {
|
|||||||
markAllNotificationsRead,
|
markAllNotificationsRead,
|
||||||
markNotificationRead,
|
markNotificationRead,
|
||||||
pullUpgradeImage,
|
pullUpgradeImage,
|
||||||
|
listProjects,
|
||||||
} from '@/lib/api-client';
|
} from '@/lib/api-client';
|
||||||
import { signOutAuthSession } from '@/lib/auth-client';
|
import { signOutAuthSession } from '@/lib/auth-client';
|
||||||
import { isDemoSearch } from '@/lib/demo-mode';
|
import { isDemoSearch } from '@/lib/demo-mode';
|
||||||
|
import { demoProjects } from '@/lib/demo-data';
|
||||||
import { useAuthSession } from '@/lib/use-auth-session';
|
import { useAuthSession } from '@/lib/use-auth-session';
|
||||||
import { BannerBar, BrandWordmark, CommandPalette, useToast } from '@/shared/components';
|
import { BannerBar, BrandWordmark, CommandPalette, useToast } from '@/shared/components';
|
||||||
|
|
||||||
@@ -273,16 +275,20 @@ export function PlatformShell() {
|
|||||||
.toUpperCase() || 'C';
|
.toUpperCase() || 'C';
|
||||||
|
|
||||||
const paletteNavigate = (target: string) => {
|
const paletteNavigate = (target: string) => {
|
||||||
const routes: Record<string, string> = {
|
navigate(href(target));
|
||||||
dashboard: '/',
|
|
||||||
canvas: '/projects',
|
|
||||||
logs: '/activity',
|
|
||||||
observability: '/operations',
|
|
||||||
settings: '/settings',
|
|
||||||
};
|
|
||||||
navigate(href(routes[target] ?? '/'));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Project deep-links for the palette — only fetched once it opens.
|
||||||
|
const paletteProjectsQuery = useQuery({
|
||||||
|
queryKey: ['palette-projects'],
|
||||||
|
queryFn: () => listProjects(),
|
||||||
|
enabled: paletteOpen && !isDemoMode,
|
||||||
|
staleTime: 60_000,
|
||||||
|
});
|
||||||
|
const paletteProjects = isDemoMode
|
||||||
|
? demoProjects
|
||||||
|
: paletteProjectsQuery.data ?? [];
|
||||||
|
|
||||||
if (!isDemoMode && signedIn && setupQuery.data?.needs_setup) {
|
if (!isDemoMode && signedIn && setupQuery.data?.needs_setup) {
|
||||||
return <Navigate to="/setup" replace />;
|
return <Navigate to="/setup" replace />;
|
||||||
}
|
}
|
||||||
@@ -474,7 +480,7 @@ export function PlatformShell() {
|
|||||||
<kbd>⌘ K</kbd>
|
<kbd>⌘ K</kbd>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span className="v-env">{isDemoMode ? 'env:demo' : 'env:production'}</span>
|
<span className="v-env">{isDemoMode ? 'env:demo' : `env:${import.meta.env.MODE}`}</span>
|
||||||
|
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
|
|
||||||
@@ -623,6 +629,11 @@ export function PlatformShell() {
|
|||||||
open={paletteOpen}
|
open={paletteOpen}
|
||||||
onClose={() => setPaletteOpen(false)}
|
onClose={() => setPaletteOpen(false)}
|
||||||
onNavigate={paletteNavigate}
|
onNavigate={paletteNavigate}
|
||||||
|
openTargets={paletteProjects.map((p) => ({
|
||||||
|
label: p.name,
|
||||||
|
target: `/projects/${p.id}`,
|
||||||
|
description: 'Project workspace',
|
||||||
|
}))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ import { GhostBtn, QuietBtn } from '@/shared/components/sentry';
|
|||||||
|
|
||||||
function sanitizeRedirect(raw: string | null): string {
|
function sanitizeRedirect(raw: string | null): string {
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return '/projects';
|
return '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = raw.trim();
|
const value = raw.trim();
|
||||||
if (!value.startsWith('/') || value.startsWith('//')) {
|
if (!value.startsWith('/') || value.startsWith('//')) {
|
||||||
return '/projects';
|
return '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
@@ -271,7 +271,7 @@ export function AcceptInvitePage() {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
if (sessionQuery.data) {
|
if (sessionQuery.data) {
|
||||||
return <Navigate to="/projects" replace />;
|
return <Navigate to="/" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundEmail = inviteQuery.data?.email ?? undefined;
|
const boundEmail = inviteQuery.data?.email ?? undefined;
|
||||||
@@ -288,7 +288,7 @@ export function AcceptInvitePage() {
|
|||||||
// account, sign-in creates the browser session.
|
// account, sign-in creates the browser session.
|
||||||
await signInWithEmail(effectiveEmail.trim(), password);
|
await signInWithEmail(effectiveEmail.trim(), password);
|
||||||
await queryClient.invalidateQueries({ queryKey: ['auth-session'] });
|
await queryClient.invalidateQueries({ queryKey: ['auth-session'] });
|
||||||
navigate('/projects', { replace: true });
|
navigate('/', { replace: true });
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
setError(exception instanceof Error ? exception.message : 'Failed to accept invite');
|
setError(exception instanceof Error ? exception.message : 'Failed to accept invite');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ type RangeKey = '1h' | '24h' | '7d';
|
|||||||
function formatRelative(date?: string): string {
|
function formatRelative(date?: string): string {
|
||||||
if (!date) return '—';
|
if (!date) return '—';
|
||||||
const diff = Date.now() - new Date(date).getTime();
|
const diff = Date.now() - new Date(date).getTime();
|
||||||
|
if (diff < 0 || diff > 365 * 86_400_000) return '—';
|
||||||
if (diff < 60_000) return 'just now';
|
if (diff < 60_000) return 'just now';
|
||||||
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
|
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
|
||||||
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
|
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
|
||||||
|
|||||||
@@ -376,7 +376,12 @@ export function NodeDetailPage() {
|
|||||||
<p className="truncate text-sm text-[var(--text-primary)]">{c.name ?? c.id}</p>
|
<p className="truncate text-sm text-[var(--text-primary)]">{c.name ?? c.id}</p>
|
||||||
<p className="truncate v-mono text-[11px] text-[var(--text-tertiary)]">{c.image}</p>
|
<p className="truncate v-mono text-[11px] text-[var(--text-tertiary)]">{c.image}</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="v-mono text-[11px] text-[var(--text-secondary)]">{c.status ?? '—'}</span>
|
{/* Persisted query cache can replay the raw status object — flatten defensively. */}
|
||||||
|
<span className="v-mono text-[11px] text-[var(--text-secondary)]">
|
||||||
|
{typeof c.status === 'string'
|
||||||
|
? c.status
|
||||||
|
: (c.status as { state?: string } | undefined)?.state ?? '—'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function SetupWizardPage() {
|
|||||||
mutationFn: completeSetup,
|
mutationFn: completeSetup,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['setup-status'] });
|
queryClient.invalidateQueries({ queryKey: ['setup-status'] });
|
||||||
navigate('/projects', { replace: true });
|
navigate('/', { replace: true });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ export function SetupWizardPage() {
|
|||||||
|
|
||||||
// Wizard is optional once the instance is already configured.
|
// Wizard is optional once the instance is already configured.
|
||||||
if (status && !status.needs_setup && !override) {
|
if (status && !status.needs_setup && !override) {
|
||||||
return <Navigate to="/projects" replace />;
|
return <Navigate to="/" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const steps: { key: Step; label: string }[] = [
|
const steps: { key: Step; label: string }[] = [
|
||||||
|
|||||||
@@ -1124,6 +1124,9 @@ export function ProjectWorkspacePage() {
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
navTargets={viewItems
|
||||||
|
.filter((item) => signedIn || !item.authOnly)
|
||||||
|
.map((item) => ({ label: `Go to ${item.label}`, target: item.key }))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Error Banner */}
|
{/* Error Banner */}
|
||||||
|
|||||||
@@ -1239,9 +1239,17 @@ export type AgentContainer = {
|
|||||||
created_at?: string;
|
created_at?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RawAgentContainer = Omit<AgentContainer, 'status'> & {
|
||||||
|
status?: string | { state?: string; health?: string };
|
||||||
|
};
|
||||||
|
|
||||||
export async function listAgentContainers(id: string): Promise<AgentContainer[]> {
|
export async function listAgentContainers(id: string): Promise<AgentContainer[]> {
|
||||||
const payload = await requestJson<{ containers?: AgentContainer[] }>(`/agents/${id}/containers`);
|
const payload = await requestJson<{ containers?: RawAgentContainer[] }>(`/agents/${id}/containers`);
|
||||||
return payload.containers ?? [];
|
// The wire sends status as a Docker state object — flatten to its state string.
|
||||||
|
return (payload.containers ?? []).map((c) => ({
|
||||||
|
...c,
|
||||||
|
status: typeof c.status === 'string' ? c.status : c.status?.state || '—',
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AgentCommand = {
|
export type AgentCommand = {
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ export function formatRelative(iso?: string): string {
|
|||||||
const hour = 60 * minute;
|
const hour = 60 * minute;
|
||||||
const day = 24 * hour;
|
const day = 24 * hour;
|
||||||
|
|
||||||
|
// Future-dated or year-plus-old timestamps are effectively meaningless —
|
||||||
|
// surface them as absent rather than "739898d ago".
|
||||||
|
if (diffMs < 0 || diffMs > 365 * day) {
|
||||||
|
return 'n/a';
|
||||||
|
}
|
||||||
if (diffMs < minute) {
|
if (diffMs < minute) {
|
||||||
return 'just now';
|
return 'just now';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Command } from 'cmdk';
|
import { Command } from 'cmdk';
|
||||||
import {
|
import {
|
||||||
|
ArrowRight,
|
||||||
Box,
|
Box,
|
||||||
|
CornerDownRight,
|
||||||
Database,
|
Database,
|
||||||
Github,
|
|
||||||
Image,
|
|
||||||
Clock,
|
Clock,
|
||||||
Search,
|
Search,
|
||||||
Settings,
|
|
||||||
FileText,
|
|
||||||
Activity,
|
|
||||||
LayoutGrid,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
export interface PaletteTarget {
|
||||||
|
label: string;
|
||||||
|
target: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface CommandItem {
|
interface CommandItem {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
icon: typeof Box;
|
icon: typeof Box;
|
||||||
shortcut?: string;
|
|
||||||
action: () => void;
|
action: () => void;
|
||||||
category?: string;
|
category?: string;
|
||||||
}
|
}
|
||||||
@@ -27,14 +28,39 @@ interface CommandPaletteProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onAddService?: (type: string) => void;
|
onAddService?: (type: string) => void;
|
||||||
onNavigate?: (path: string) => void;
|
onNavigate?: (target: string) => void;
|
||||||
|
/** Navigate-group items; falls back to the full app route map. */
|
||||||
|
navTargets?: PaletteTarget[];
|
||||||
|
/** Extra searchable "Open …" rows, e.g. projects. */
|
||||||
|
openTargets?: PaletteTarget[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const APP_NAV: PaletteTarget[] = [
|
||||||
|
{ label: 'Dashboard', target: '/', description: 'Index — stats, nodes, deploys' },
|
||||||
|
{ label: 'Projects', target: '/projects', description: 'All projects' },
|
||||||
|
{ label: 'Templates', target: '/templates', description: 'Template catalog' },
|
||||||
|
{ label: 'Builds', target: '/builds', description: 'Build pipeline' },
|
||||||
|
{ label: 'Databases', target: '/databases', description: 'Managed databases' },
|
||||||
|
{ label: 'Operations', target: '/operations', description: 'Backups, cron, maintenance' },
|
||||||
|
{ label: 'Activity', target: '/activity', description: 'Platform event feed' },
|
||||||
|
{ label: 'High Availability', target: '/ha', description: 'Failover & alerts' },
|
||||||
|
{ label: 'Security', target: '/security', description: 'Access, sessions, secrets' },
|
||||||
|
{ label: 'Usage', target: '/usage', description: 'Resource consumption' },
|
||||||
|
{ label: 'People', target: '/people', description: 'Team & access control' },
|
||||||
|
{ label: 'Audit Logs', target: '/settings/audit-logs', description: 'Recorded actions' },
|
||||||
|
{ label: 'Webhooks', target: '/settings/webhooks', description: 'Signed event delivery' },
|
||||||
|
{ label: 'Settings', target: '/settings', description: 'Platform preferences' },
|
||||||
|
{ label: 'Docs', target: '/docs', description: 'Guides & reference' },
|
||||||
|
{ label: 'Landing', target: '/landing', description: 'Public page' },
|
||||||
|
];
|
||||||
|
|
||||||
export function CommandPalette({
|
export function CommandPalette({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
onAddService,
|
onAddService,
|
||||||
onNavigate,
|
onNavigate,
|
||||||
|
navTargets,
|
||||||
|
openTargets,
|
||||||
}: CommandPaletteProps) {
|
}: CommandPaletteProps) {
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
@@ -43,6 +69,8 @@ export function CommandPalette({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onAddService={onAddService}
|
onAddService={onAddService}
|
||||||
onNavigate={onNavigate}
|
onNavigate={onNavigate}
|
||||||
|
navTargets={navTargets}
|
||||||
|
openTargets={openTargets}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -51,6 +79,8 @@ function CommandPaletteContent({
|
|||||||
onClose,
|
onClose,
|
||||||
onAddService,
|
onAddService,
|
||||||
onNavigate,
|
onNavigate,
|
||||||
|
navTargets,
|
||||||
|
openTargets,
|
||||||
}: Omit<CommandPaletteProps, 'open'>) {
|
}: Omit<CommandPaletteProps, 'open'>) {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
@@ -66,130 +96,69 @@ function CommandPaletteContent({
|
|||||||
return () => document.removeEventListener('keydown', down);
|
return () => document.removeEventListener('keydown', down);
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
const commands: CommandItem[] = [
|
const commands: CommandItem[] = [];
|
||||||
{
|
|
||||||
id: 'add-web',
|
// Create items only exist where onAddService is wired — elsewhere the
|
||||||
label: 'Add Web Service',
|
// entries would silently no-op.
|
||||||
description: 'Deploy a web application',
|
if (onAddService) {
|
||||||
icon: Box,
|
commands.push(
|
||||||
shortcut: 'W',
|
{
|
||||||
category: 'Create',
|
id: 'add-web',
|
||||||
action: () => {
|
label: 'Add Web Service',
|
||||||
onAddService?.('web');
|
description: 'Deploy a web application',
|
||||||
onClose();
|
icon: Box,
|
||||||
|
category: 'Create',
|
||||||
|
action: () => { onAddService('web'); onClose(); },
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
id: 'add-worker',
|
||||||
id: 'add-worker',
|
label: 'Add Worker Service',
|
||||||
label: 'Add Worker Service',
|
description: 'Deploy a background worker',
|
||||||
description: 'Deploy a background worker',
|
icon: Clock,
|
||||||
icon: Clock,
|
category: 'Create',
|
||||||
shortcut: 'K',
|
action: () => { onAddService('worker'); onClose(); },
|
||||||
category: 'Create',
|
|
||||||
action: () => {
|
|
||||||
onAddService?.('worker');
|
|
||||||
onClose();
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
id: 'add-database',
|
||||||
id: 'add-database',
|
label: 'Add Database',
|
||||||
label: 'Add Database',
|
description: 'Provision a PostgreSQL database',
|
||||||
description: 'Provision a PostgreSQL database',
|
icon: Database,
|
||||||
icon: Database,
|
category: 'Create',
|
||||||
shortcut: 'D',
|
action: () => { onAddService('database'); onClose(); },
|
||||||
category: 'Create',
|
|
||||||
action: () => {
|
|
||||||
onAddService?.('database');
|
|
||||||
onClose();
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
id: 'add-cron',
|
||||||
id: 'add-cron',
|
label: 'Add Cron Job',
|
||||||
label: 'Add Cron Job',
|
description: 'Schedule a recurring task',
|
||||||
description: 'Schedule a recurring task',
|
icon: Clock,
|
||||||
icon: Clock,
|
category: 'Create',
|
||||||
category: 'Create',
|
action: () => { onAddService('cron'); onClose(); },
|
||||||
action: () => {
|
|
||||||
onAddService?.('cron');
|
|
||||||
onClose();
|
|
||||||
},
|
},
|
||||||
},
|
);
|
||||||
{
|
}
|
||||||
id: 'connect-github',
|
|
||||||
label: 'Connect GitHub Repo',
|
if (onNavigate) {
|
||||||
description: 'Link a repository for auto-deploy',
|
(navTargets ?? APP_NAV).forEach((t, i) =>
|
||||||
icon: Github,
|
commands.push({
|
||||||
category: 'Connect',
|
id: `nav-${i}`,
|
||||||
action: () => {
|
label: t.label,
|
||||||
onClose();
|
description: t.description,
|
||||||
},
|
icon: CornerDownRight,
|
||||||
},
|
category: 'Navigate',
|
||||||
{
|
action: () => { onNavigate(t.target); onClose(); },
|
||||||
id: 'deploy-image',
|
}),
|
||||||
label: 'Deploy Docker Image',
|
);
|
||||||
description: 'Deploy from a container registry',
|
(openTargets ?? []).forEach((t, i) =>
|
||||||
icon: Image,
|
commands.push({
|
||||||
category: 'Connect',
|
id: `open-${i}`,
|
||||||
action: () => {
|
label: t.label,
|
||||||
onClose();
|
description: t.description,
|
||||||
},
|
icon: ArrowRight,
|
||||||
},
|
category: 'Open',
|
||||||
{
|
action: () => { onNavigate(t.target); onClose(); },
|
||||||
id: 'goto-dashboard',
|
}),
|
||||||
label: 'Go to Dashboard',
|
);
|
||||||
icon: LayoutGrid,
|
}
|
||||||
shortcut: 'H',
|
|
||||||
category: 'Navigate',
|
|
||||||
action: () => {
|
|
||||||
onNavigate?.('dashboard');
|
|
||||||
onClose();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'goto-canvas',
|
|
||||||
label: 'Go to Canvas',
|
|
||||||
icon: LayoutGrid,
|
|
||||||
shortcut: 'C',
|
|
||||||
category: 'Navigate',
|
|
||||||
action: () => {
|
|
||||||
onNavigate?.('canvas');
|
|
||||||
onClose();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'goto-logs',
|
|
||||||
label: 'Go to Logs',
|
|
||||||
icon: FileText,
|
|
||||||
shortcut: 'L',
|
|
||||||
category: 'Navigate',
|
|
||||||
action: () => {
|
|
||||||
onNavigate?.('logs');
|
|
||||||
onClose();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'goto-metrics',
|
|
||||||
label: 'Go to Metrics',
|
|
||||||
icon: Activity,
|
|
||||||
shortcut: 'M',
|
|
||||||
category: 'Navigate',
|
|
||||||
action: () => {
|
|
||||||
onNavigate?.('observability');
|
|
||||||
onClose();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'goto-settings',
|
|
||||||
label: 'Go to Settings',
|
|
||||||
icon: Settings,
|
|
||||||
shortcut: 'S',
|
|
||||||
category: 'Navigate',
|
|
||||||
action: () => {
|
|
||||||
onNavigate?.('settings');
|
|
||||||
onClose();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const filteredCommands = commands.filter(
|
const filteredCommands = commands.filter(
|
||||||
(cmd) =>
|
(cmd) =>
|
||||||
@@ -208,6 +177,10 @@ function CommandPaletteContent({
|
|||||||
{} as Record<string, CommandItem[]>
|
{} as Record<string, CommandItem[]>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const placeholder = onNavigate
|
||||||
|
? 'Search pages, projects, actions…'
|
||||||
|
: 'What would you like to create?';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50">
|
<div className="fixed inset-0 z-50">
|
||||||
<div
|
<div
|
||||||
@@ -224,7 +197,7 @@ function CommandPaletteContent({
|
|||||||
<Command.Input
|
<Command.Input
|
||||||
value={search}
|
value={search}
|
||||||
onValueChange={setSearch}
|
onValueChange={setSearch}
|
||||||
placeholder="What would you like to create?"
|
placeholder={placeholder}
|
||||||
className="flex-1 bg-transparent text-[var(--text-primary)] placeholder:text-[var(--text-muted)] text-sm outline-none"
|
className="flex-1 bg-transparent text-[var(--text-primary)] placeholder:text-[var(--text-muted)] text-sm outline-none"
|
||||||
/>
|
/>
|
||||||
<kbd className="px-2 py-0.5 rounded bg-[var(--surface-muted)] text-[10px] font-mono text-[var(--text-muted)]">
|
<kbd className="px-2 py-0.5 rounded bg-[var(--surface-muted)] text-[10px] font-mono text-[var(--text-muted)]">
|
||||||
@@ -251,18 +224,13 @@ function CommandPaletteContent({
|
|||||||
onSelect={item.action}
|
onSelect={item.action}
|
||||||
className="flex items-center gap-3 px-3 py-2 rounded-[var(--radius-md)] cursor-pointer text-[var(--text-secondary)] hover:bg-[var(--surface-muted)] hover:text-[var(--text-primary)] aria-selected:bg-[var(--accent-primary-soft)] aria-selected:text-[var(--accent-primary)] transition-colors"
|
className="flex items-center gap-3 px-3 py-2 rounded-[var(--radius-md)] cursor-pointer text-[var(--text-secondary)] hover:bg-[var(--surface-muted)] hover:text-[var(--text-primary)] aria-selected:bg-[var(--accent-primary-soft)] aria-selected:text-[var(--accent-primary)] transition-colors"
|
||||||
>
|
>
|
||||||
<span className="s-ibox !h-7 !w-7"><Icon /></span>
|
<span className="s-ibox !h-7 !w-7"><Icon size={13.5} /></span>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="text-sm font-medium">{item.label}</div>
|
<div className="text-sm font-medium">{item.label}</div>
|
||||||
{item.description && (
|
{item.description && (
|
||||||
<div className="text-xs text-[var(--text-muted)]">{item.description}</div>
|
<div className="text-xs text-[var(--text-muted)]">{item.description}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{item.shortcut && (
|
|
||||||
<kbd className="px-2 py-0.5 rounded bg-[var(--surface-muted)] text-[10px] font-mono text-[var(--text-muted)]">
|
|
||||||
{item.shortcut}
|
|
||||||
</kbd>
|
|
||||||
)}
|
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in new issue
Block a user