Skip to content
Merged
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ See [AGENTS.md](./AGENTS.md) for full project context, architecture, and coding
- Removed `process.env.GUILD_ID` runtime reads from startup/reload command registration.
- Updated `.env.example` and `README.md` to remove `GUILD_ID` as a persisted env var.
- Preserved dev-only guild-scoped deploy support via CLI flag: `pnpm deploy -- --guild-id <guild_id>`.
- Web dashboard config editor redesign shipped:
- Replaced monolithic settings stack with category workspace navigation (`AI & Automation`, `Onboarding & Growth`, `Moderation & Safety`, `Community Tools`, `Support & Integrations`) in `web/src/components/dashboard/config-workspace/`.
- Added metadata-driven config search with cross-category quick jump, focus/scroll targeting, and advanced-section auto-open when search hits advanced controls.
- Refactored config feature presentation to reusable `SettingsFeatureCard` pattern (header + master toggle + Basic/Advanced blocks).
- Kept save contract unchanged: global save/discard, diff-modal confirmation, per-section PATCH batching, and partial-failure behavior.
- Updated config editor tests from stale autosave assumptions to explicit manual-save workspace behavior and added coverage for category switching/search/dirty badges.
2,302 changes: 1,196 additions & 1,106 deletions web/src/components/dashboard/config-editor.tsx

Large diffs are not rendered by default.

1,226 changes: 718 additions & 508 deletions web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use client';

import { Bot, MessageSquareWarning, Sparkles, Ticket, Users } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { CONFIG_CATEGORIES } from './config-categories';
import type { ConfigCategoryIcon, ConfigCategoryId } from './types';

const CATEGORY_ICONS: Record<ConfigCategoryIcon, typeof Sparkles> = {
sparkles: Sparkles,
users: Users,
'message-square-warning': MessageSquareWarning,
bot: Bot,
ticket: Ticket,
};

interface CategoryNavigationProps {
activeCategoryId: ConfigCategoryId;
dirtyCounts: Record<ConfigCategoryId, number>;
onCategoryChange: (categoryId: ConfigCategoryId) => void;
}

/**
* Render responsive category navigation with selectable categories and per-category dirty counts.
*
* @param activeCategoryId - The id of the currently active category.
* @param dirtyCounts - A record mapping category ids to their dirty/unsaved item counts.
* @param onCategoryChange - Callback invoked with a `ConfigCategoryId` when the user selects or clicks a category.
* @returns A React element that renders a labeled select for mobile and a vertical list of category buttons for desktop; each item shows an icon, label, and an optional badge with the dirty count.
*/
export function CategoryNavigation({
activeCategoryId,
dirtyCounts,
onCategoryChange,
}: CategoryNavigationProps) {
return (
<>
<div className="space-y-2 md:hidden">
<Label htmlFor="config-category-picker" className="text-xs text-muted-foreground">
Category
</Label>
<select
id="config-category-picker"
className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm"
value={activeCategoryId}
onChange={(event) => onCategoryChange(event.target.value as ConfigCategoryId)}
>
{CONFIG_CATEGORIES.map((category) => {
const dirtyCount = dirtyCounts[category.id];
const dirtyLabel = dirtyCount > 0 ? ` (${dirtyCount})` : '';
return (
<option key={category.id} value={category.id}>
{category.label}
{dirtyLabel}
</option>
);
})}
</select>
</div>

<aside className="hidden md:block">
<div className="sticky top-24 space-y-2 rounded-lg border bg-card p-3">
{CONFIG_CATEGORIES.map((category) => {
const Icon = CATEGORY_ICONS[category.icon];
const isActive = activeCategoryId === category.id;
const dirtyCount = dirtyCounts[category.id];

return (
<Button
key={category.id}
variant={isActive ? 'secondary' : 'ghost'}
className="h-auto w-full justify-between px-3 py-2 text-left"
onClick={() => onCategoryChange(category.id)}
aria-current={isActive ? 'page' : undefined}
>
<span className="flex items-center gap-2">
<Icon className="h-4 w-4" aria-hidden="true" />
<span className="text-sm">{category.label}</span>
</span>
{dirtyCount > 0 && (
<Badge variant="default" className="min-w-5 justify-center px-1.5">
{dirtyCount}
</Badge>
)}
</Button>
);
})}
</div>
</aside>
</>
);
}
Loading
Loading