mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
refactor: convert settings page to separate view panels
- Replace scroll-based navigation with view switching - Add useSettingsView hook for managing active panel state - Extract Audio section into its own component - Remove scroll-mt-6 classes and IDs from section components - Update navigation config to reflect current sections - Create barrel export for settings-view hooks This improves performance by only rendering the active section instead of all sections in a single scrollable container. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useAppStore } from "@/store/app-store";
|
import { useAppStore } from "@/store/app-store";
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import {
|
import {
|
||||||
Key,
|
Key,
|
||||||
Palette,
|
Palette,
|
||||||
@@ -11,12 +10,13 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
Settings2,
|
Settings2,
|
||||||
Volume2,
|
Volume2,
|
||||||
VolumeX,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
|
|
||||||
import { useCliStatus } from "./settings-view/hooks/use-cli-status";
|
import {
|
||||||
import { useScrollTracking } from "@/hooks/use-scroll-tracking";
|
useCliStatus,
|
||||||
|
useSettingsView,
|
||||||
|
type SettingsViewId,
|
||||||
|
} from "./settings-view/hooks";
|
||||||
import { SettingsHeader } from "./settings-view/components/settings-header";
|
import { SettingsHeader } from "./settings-view/components/settings-header";
|
||||||
import { KeyboardMapDialog } from "./settings-view/components/keyboard-map-dialog";
|
import { KeyboardMapDialog } from "./settings-view/components/keyboard-map-dialog";
|
||||||
import { DeleteProjectDialog } from "./settings-view/components/delete-project-dialog";
|
import { DeleteProjectDialog } from "./settings-view/components/delete-project-dialog";
|
||||||
@@ -24,6 +24,7 @@ import { SettingsNavigation } from "./settings-view/components/settings-navigati
|
|||||||
import { ApiKeysSection } from "./settings-view/api-keys/api-keys-section";
|
import { ApiKeysSection } from "./settings-view/api-keys/api-keys-section";
|
||||||
import { ClaudeCliStatus } from "./settings-view/cli-status/claude-cli-status";
|
import { ClaudeCliStatus } from "./settings-view/cli-status/claude-cli-status";
|
||||||
import { AppearanceSection } from "./settings-view/appearance/appearance-section";
|
import { AppearanceSection } from "./settings-view/appearance/appearance-section";
|
||||||
|
import { AudioSection } from "./settings-view/audio/audio-section";
|
||||||
import { KeyboardShortcutsSection } from "./settings-view/keyboard-shortcuts/keyboard-shortcuts-section";
|
import { KeyboardShortcutsSection } from "./settings-view/keyboard-shortcuts/keyboard-shortcuts-section";
|
||||||
import { FeatureDefaultsSection } from "./settings-view/feature-defaults/feature-defaults-section";
|
import { FeatureDefaultsSection } from "./settings-view/feature-defaults/feature-defaults-section";
|
||||||
import { DangerZoneSection } from "./settings-view/danger-zone/danger-zone-section";
|
import { DangerZoneSection } from "./settings-view/danger-zone/danger-zone-section";
|
||||||
@@ -91,23 +92,72 @@ export function SettingsView() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Use CLI status hook
|
// Use CLI status hook
|
||||||
const {
|
const { claudeCliStatus, isCheckingClaudeCli, handleRefreshClaudeCli } =
|
||||||
claudeCliStatus,
|
useCliStatus();
|
||||||
isCheckingClaudeCli,
|
|
||||||
handleRefreshClaudeCli,
|
|
||||||
} = useCliStatus();
|
|
||||||
|
|
||||||
// Use scroll tracking hook
|
// Use settings view navigation hook
|
||||||
const { activeSection, scrollToSection, scrollContainerRef } =
|
const { activeView, navigateTo } = useSettingsView();
|
||||||
useScrollTracking({
|
|
||||||
items: NAV_ITEMS,
|
|
||||||
filterFn: (item) => item.id !== "danger" || !!currentProject,
|
|
||||||
initialSection: "api-keys",
|
|
||||||
});
|
|
||||||
|
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
const [showKeyboardMapDialog, setShowKeyboardMapDialog] = useState(false);
|
const [showKeyboardMapDialog, setShowKeyboardMapDialog] = useState(false);
|
||||||
|
|
||||||
|
// Render the active section based on current view
|
||||||
|
const renderActiveSection = () => {
|
||||||
|
switch (activeView) {
|
||||||
|
case "api-keys":
|
||||||
|
return <ApiKeysSection />;
|
||||||
|
case "claude":
|
||||||
|
return (
|
||||||
|
<ClaudeCliStatus
|
||||||
|
status={claudeCliStatus}
|
||||||
|
isChecking={isCheckingClaudeCli}
|
||||||
|
onRefresh={handleRefreshClaudeCli}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "appearance":
|
||||||
|
return (
|
||||||
|
<AppearanceSection
|
||||||
|
effectiveTheme={effectiveTheme}
|
||||||
|
currentProject={settingsProject}
|
||||||
|
onThemeChange={handleSetTheme}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "keyboard":
|
||||||
|
return (
|
||||||
|
<KeyboardShortcutsSection
|
||||||
|
onOpenKeyboardMap={() => setShowKeyboardMapDialog(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "audio":
|
||||||
|
return (
|
||||||
|
<AudioSection
|
||||||
|
muteDoneSound={muteDoneSound}
|
||||||
|
onMuteDoneSoundChange={setMuteDoneSound}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "defaults":
|
||||||
|
return (
|
||||||
|
<FeatureDefaultsSection
|
||||||
|
showProfilesOnly={showProfilesOnly}
|
||||||
|
defaultSkipTests={defaultSkipTests}
|
||||||
|
useWorktrees={useWorktrees}
|
||||||
|
onShowProfilesOnlyChange={setShowProfilesOnly}
|
||||||
|
onDefaultSkipTestsChange={setDefaultSkipTests}
|
||||||
|
onUseWorktreesChange={setUseWorktrees}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "danger":
|
||||||
|
return (
|
||||||
|
<DangerZoneSection
|
||||||
|
project={settingsProject}
|
||||||
|
onDeleteClick={() => setShowDeleteDialog(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <ApiKeysSection />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex-1 flex flex-col overflow-hidden content-bg"
|
className="flex-1 flex flex-col overflow-hidden content-bg"
|
||||||
@@ -118,107 +168,17 @@ export function SettingsView() {
|
|||||||
|
|
||||||
{/* Content Area with Sidebar */}
|
{/* Content Area with Sidebar */}
|
||||||
<div className="flex-1 flex overflow-hidden">
|
<div className="flex-1 flex overflow-hidden">
|
||||||
{/* Sticky Side Navigation */}
|
{/* Side Navigation - No longer scrolls, just switches views */}
|
||||||
<SettingsNavigation
|
<SettingsNavigation
|
||||||
navItems={NAV_ITEMS}
|
navItems={NAV_ITEMS}
|
||||||
activeSection={activeSection}
|
activeSection={activeView}
|
||||||
currentProject={currentProject}
|
currentProject={currentProject}
|
||||||
onNavigate={scrollToSection}
|
onNavigate={(id) => navigateTo(id as SettingsViewId)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Scrollable Content */}
|
{/* Content Panel - Shows only the active section */}
|
||||||
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto p-8">
|
<div className="flex-1 overflow-y-auto p-8">
|
||||||
<div className="max-w-4xl mx-auto space-y-6 pb-96">
|
<div className="max-w-4xl mx-auto">{renderActiveSection()}</div>
|
||||||
{/* API Keys Section */}
|
|
||||||
<ApiKeysSection />
|
|
||||||
|
|
||||||
{/* Claude CLI Status Section */}
|
|
||||||
{claudeCliStatus && (
|
|
||||||
<ClaudeCliStatus
|
|
||||||
status={claudeCliStatus}
|
|
||||||
isChecking={isCheckingClaudeCli}
|
|
||||||
onRefresh={handleRefreshClaudeCli}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Appearance Section */}
|
|
||||||
<AppearanceSection
|
|
||||||
effectiveTheme={effectiveTheme}
|
|
||||||
currentProject={settingsProject}
|
|
||||||
onThemeChange={handleSetTheme}
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
{/* Keyboard Shortcuts Section */}
|
|
||||||
<KeyboardShortcutsSection
|
|
||||||
onOpenKeyboardMap={() => setShowKeyboardMapDialog(true)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Audio Section */}
|
|
||||||
<div
|
|
||||||
id="audio"
|
|
||||||
className="rounded-2xl border border-border/50 bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl shadow-sm shadow-black/5 overflow-hidden scroll-mt-6"
|
|
||||||
>
|
|
||||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
|
||||||
<div className="flex items-center gap-3 mb-2">
|
|
||||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
|
||||||
<Volume2 className="w-5 h-5 text-brand-500" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">
|
|
||||||
Audio
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
|
||||||
Configure audio and notification settings.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-6 space-y-4">
|
|
||||||
{/* Mute Done Sound Setting */}
|
|
||||||
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
|
|
||||||
<Checkbox
|
|
||||||
id="mute-done-sound"
|
|
||||||
checked={muteDoneSound}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
setMuteDoneSound(checked === true)
|
|
||||||
}
|
|
||||||
className="mt-1"
|
|
||||||
data-testid="mute-done-sound-checkbox"
|
|
||||||
/>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Label
|
|
||||||
htmlFor="mute-done-sound"
|
|
||||||
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<VolumeX className="w-4 h-4 text-brand-500" />
|
|
||||||
Mute notification sound when agents complete
|
|
||||||
</Label>
|
|
||||||
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
|
||||||
When enabled, disables the "ding" sound that
|
|
||||||
plays when an agent completes a feature. The feature
|
|
||||||
will still move to the completed column, but without
|
|
||||||
audio notification.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Feature Defaults Section */}
|
|
||||||
<FeatureDefaultsSection
|
|
||||||
showProfilesOnly={showProfilesOnly}
|
|
||||||
defaultSkipTests={defaultSkipTests}
|
|
||||||
useWorktrees={useWorktrees}
|
|
||||||
onShowProfilesOnlyChange={setShowProfilesOnly}
|
|
||||||
onDefaultSkipTestsChange={setDefaultSkipTests}
|
|
||||||
onUseWorktreesChange={setUseWorktrees}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Danger Zone Section - Only show when a project is selected */}
|
|
||||||
<DangerZoneSection
|
|
||||||
project={settingsProject}
|
|
||||||
onDeleteClick={() => setShowDeleteDialog(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -58,9 +58,8 @@ export function ApiKeysSection() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="api-keys"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-2xl overflow-hidden scroll-mt-6",
|
"rounded-2xl overflow-hidden",
|
||||||
"border border-border/50",
|
"border border-border/50",
|
||||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||||
"shadow-sm shadow-black/5"
|
"shadow-sm shadow-black/5"
|
||||||
|
|||||||
@@ -18,9 +18,8 @@ export function AppearanceSection({
|
|||||||
}: AppearanceSectionProps) {
|
}: AppearanceSectionProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="appearance"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-2xl overflow-hidden scroll-mt-6",
|
"rounded-2xl overflow-hidden",
|
||||||
"border border-border/50",
|
"border border-border/50",
|
||||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||||
"shadow-sm shadow-black/5"
|
"shadow-sm shadow-black/5"
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Volume2, VolumeX } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface AudioSectionProps {
|
||||||
|
muteDoneSound: boolean;
|
||||||
|
onMuteDoneSoundChange: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AudioSection({
|
||||||
|
muteDoneSound,
|
||||||
|
onMuteDoneSoundChange,
|
||||||
|
}: AudioSectionProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"rounded-2xl overflow-hidden",
|
||||||
|
"border border-border/50",
|
||||||
|
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||||
|
"shadow-sm shadow-black/5"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||||
|
<Volume2 className="w-5 h-5 text-brand-500" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-lg font-semibold text-foreground tracking-tight">
|
||||||
|
Audio
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||||
|
Configure audio and notification settings.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
|
||||||
|
<Checkbox
|
||||||
|
id="mute-done-sound"
|
||||||
|
checked={muteDoneSound}
|
||||||
|
onCheckedChange={(checked) => onMuteDoneSoundChange(checked === true)}
|
||||||
|
className="mt-1"
|
||||||
|
data-testid="mute-done-sound-checkbox"
|
||||||
|
/>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label
|
||||||
|
htmlFor="mute-done-sound"
|
||||||
|
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<VolumeX className="w-4 h-4 text-brand-500" />
|
||||||
|
Mute notification sound when agents complete
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||||
|
When enabled, disables the "ding" sound that plays when
|
||||||
|
an agent completes a feature. The feature will still move to the
|
||||||
|
completed column, but without audio notification.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,9 +23,8 @@ export function ClaudeCliStatus({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="claude"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-2xl overflow-hidden scroll-mt-6",
|
"rounded-2xl overflow-hidden",
|
||||||
"border border-border/50",
|
"border border-border/50",
|
||||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||||
"shadow-sm shadow-black/5"
|
"shadow-sm shadow-black/5"
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
Key,
|
Key,
|
||||||
Terminal,
|
Terminal,
|
||||||
Palette,
|
Palette,
|
||||||
LayoutGrid,
|
|
||||||
Settings2,
|
Settings2,
|
||||||
|
Volume2,
|
||||||
FlaskConical,
|
FlaskConical,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -20,8 +20,8 @@ export const NAV_ITEMS: NavigationItem[] = [
|
|||||||
{ id: "api-keys", label: "API Keys", icon: Key },
|
{ id: "api-keys", label: "API Keys", icon: Key },
|
||||||
{ id: "claude", label: "Claude", icon: Terminal },
|
{ id: "claude", label: "Claude", icon: Terminal },
|
||||||
{ id: "appearance", label: "Appearance", icon: Palette },
|
{ id: "appearance", label: "Appearance", icon: Palette },
|
||||||
{ id: "kanban", label: "Kanban Display", icon: LayoutGrid },
|
|
||||||
{ id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 },
|
{ id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 },
|
||||||
|
{ id: "audio", label: "Audio", icon: Volume2 },
|
||||||
{ id: "defaults", label: "Feature Defaults", icon: FlaskConical },
|
{ id: "defaults", label: "Feature Defaults", icon: FlaskConical },
|
||||||
{ id: "danger", label: "Danger Zone", icon: Trash2 },
|
{ id: "danger", label: "Danger Zone", icon: Trash2 },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ export function DangerZoneSection({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="danger"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-2xl overflow-hidden scroll-mt-6",
|
"rounded-2xl overflow-hidden",
|
||||||
"border border-destructive/30",
|
"border border-destructive/30",
|
||||||
"bg-gradient-to-br from-destructive/5 via-card/70 to-card/80 backdrop-blur-xl",
|
"bg-gradient-to-br from-destructive/5 via-card/70 to-card/80 backdrop-blur-xl",
|
||||||
"shadow-sm shadow-destructive/5"
|
"shadow-sm shadow-destructive/5"
|
||||||
|
|||||||
@@ -22,9 +22,8 @@ export function FeatureDefaultsSection({
|
|||||||
}: FeatureDefaultsSectionProps) {
|
}: FeatureDefaultsSectionProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="defaults"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-2xl overflow-hidden scroll-mt-6",
|
"rounded-2xl overflow-hidden",
|
||||||
"border border-border/50",
|
"border border-border/50",
|
||||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||||
"shadow-sm shadow-black/5"
|
"shadow-sm shadow-black/5"
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { useCliStatus } from "./use-cli-status";
|
||||||
|
export { useSettingsView, type SettingsViewId } from "./use-settings-view";
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { useState, useCallback } from "react";
|
||||||
|
|
||||||
|
export type SettingsViewId =
|
||||||
|
| "api-keys"
|
||||||
|
| "claude"
|
||||||
|
| "appearance"
|
||||||
|
| "keyboard"
|
||||||
|
| "audio"
|
||||||
|
| "defaults"
|
||||||
|
| "danger";
|
||||||
|
|
||||||
|
interface UseSettingsViewOptions {
|
||||||
|
initialView?: SettingsViewId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSettingsView({
|
||||||
|
initialView = "api-keys",
|
||||||
|
}: UseSettingsViewOptions = {}) {
|
||||||
|
const [activeView, setActiveView] = useState<SettingsViewId>(initialView);
|
||||||
|
|
||||||
|
const navigateTo = useCallback((viewId: SettingsViewId) => {
|
||||||
|
setActiveView(viewId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeView,
|
||||||
|
navigateTo,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,9 +11,8 @@ export function KeyboardShortcutsSection({
|
|||||||
}: KeyboardShortcutsSectionProps) {
|
}: KeyboardShortcutsSectionProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="keyboard"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-2xl overflow-hidden scroll-mt-6",
|
"rounded-2xl overflow-hidden",
|
||||||
"border border-border/50",
|
"border border-border/50",
|
||||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||||
"shadow-sm shadow-black/5"
|
"shadow-sm shadow-black/5"
|
||||||
|
|||||||
Reference in New Issue
Block a user