Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1d8d529
feat: shadcn/ui theme setup with dark mode toggle
Mar 1, 2026
eec474f
feat: migrate dashboard to shadcn components (Switch, Input, Label, F…
Mar 1, 2026
a2f4719
feat: add RoleSelector and ChannelSelector components with auto-fill
Mar 1, 2026
77f734f
fix: apply Biome formatting (single quotes + semicolons) to shadcn UI…
Mar 1, 2026
6ba21bf
fix: add accessible labels and connect htmlFor/id pairs for screen re…
Mar 1, 2026
e73a841
fix: form context null guards, PopoverTitle h2, stale roles clear, re…
Mar 1, 2026
e6141b2
fix: move DialogHeader inside DialogContent and fix invalid Tailwind …
Mar 1, 2026
c1cc229
fix(channel-selector): clear stale channels on guild change
Mar 1, 2026
9c4166b
fix(channel-selector): show removable chips for unknown selected IDs
Mar 1, 2026
2eaed47
fix(role-selector): show removable chips for unknown selected role IDs
Mar 1, 2026
90ea779
fix(channel-selector): guard setChannels/setError against stale requests
BillChirico Mar 1, 2026
5e0eb28
fix(form): import LabelPrimitive as value not type
BillChirico Mar 1, 2026
bdb08b1
fix(dialog): add type="button" to footer close button
BillChirico Mar 1, 2026
389d176
fix(providers): wire Toaster theme to resolvedTheme
BillChirico Mar 1, 2026
a166a4f
fix(role-selector): guard setRoles/setError against stale requests
BillChirico Mar 1, 2026
d0afd59
fix: resolve merge conflicts with main
BillChirico Mar 1, 2026
fb97c1b
fix(form): protect FormControl a11y attrs from consumer override
Mar 1, 2026
a73e1b1
fix(form): protect FormLabel htmlFor from consumer override
Mar 1, 2026
458e281
fix(dialog): remove data-slot from non-rendered Radix primitives
Mar 1, 2026
a02c3d7
docs(providers): update JSDoc to reflect resolved theme usage
Mar 1, 2026
c6ffe2e
test(setup): add window.matchMedia polyfill for next-themes in jsdom
Mar 1, 2026
17ab20b
fix(deps): regenerate pnpm lockfile to sync with package.json changes
Mar 1, 2026
e1fa5e4
fix: add missing next-themes dependency to web package.json
Mar 1, 2026
4b6bc14
fix(deps): add react-hook-form dependency to web/package.json
Mar 1, 2026
00001c9
fix: use type-only import for LabelPrimitive in form.tsx
Mar 1, 2026
bddcb85
fix(lint): auto-fix all Biome errors (import type, organize imports, …
Mar 1, 2026
a8742e0
fix(tests): guard matchMedia polyfill with existence check and config…
Mar 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"recharts": "^3.7.0",
"server-only": "^0.0.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0"
"tailwind-merge": "^3.5.0",
"next-themes": "^0.4.6"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.2.1",
Expand Down
16 changes: 14 additions & 2 deletions web/src/components/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,21 @@

import { SessionProvider } from 'next-auth/react';
import type { ReactNode } from 'react';
import { useTheme } from 'next-themes';
import { Toaster } from 'sonner';
import { ThemeProvider } from '@/components/theme-provider';

function ThemedToaster() {
const { resolvedTheme } = useTheme();
return (
<Toaster
position="bottom-right"
theme={(resolvedTheme as 'light' | 'dark') ?? 'system'}
richColors
/>
);
}

/**
* Wraps application UI with NextAuth session context, theme provider, and a global toast container.
*
Expand All @@ -14,7 +26,7 @@ import { ThemeProvider } from '@/components/theme-provider';
* Theme defaults to system preference with CSS variable-based dark/light mode support.
*
* @returns A React element that renders providers around `children` and mounts a Toaster
* positioned at the bottom-right with system theme and rich colors enabled.
* positioned at the bottom-right with resolved theme (light/dark) and rich colors enabled.
*/
export function Providers({ children }: { children: ReactNode }) {
return (
Expand All @@ -26,7 +38,7 @@ export function Providers({ children }: { children: ReactNode }) {
disableTransitionOnChange={false}
>
{children}
<Toaster position="bottom-right" theme="system" richColors />
<ThemedToaster />
</ThemeProvider>
</SessionProvider>
);
Expand Down
8 changes: 6 additions & 2 deletions web/src/components/ui/channel-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,14 @@ export function ChannelSelector({
return a.name.localeCompare(b.name);
});

setChannels(sortedChannels);
if (abortControllerRef.current === controller) {
setChannels(sortedChannels);
}
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') return;
setError(err instanceof Error ? err.message : 'Failed to load channels');
if (abortControllerRef.current === controller) {
setError(err instanceof Error ? err.message : 'Failed to load channels');
}
} finally {
if (abortControllerRef.current === controller) {
setLoading(false);
Expand Down
6 changes: 3 additions & 3 deletions web/src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';

function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
return <DialogPrimitive.Root {...props} />;
}

function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}

function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
return <DialogPrimitive.Portal {...props} />;
}

function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
Expand Down Expand Up @@ -48,7 +48,7 @@ function DialogContent({
showCloseButton?: boolean;
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
Expand Down
12 changes: 6 additions & 6 deletions web/src/components/ui/form.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use client';

import * as React from 'react';
import type { Label as LabelPrimitive } from 'radix-ui';
import { Slot } from 'radix-ui';
import { Label as LabelPrimitive } from 'radix-ui';
import { Slot } from '@radix-ui/react-slot';
import {
Controller,
FormProvider,
Expand Down Expand Up @@ -88,25 +88,25 @@ function FormLabel({ className, ...props }: React.ComponentProps<typeof LabelPri

return (
<Label
{...props}
data-slot="form-label"
data-error={!!error}
className={cn('data-[error=true]:text-destructive', className)}
htmlFor={formItemId}
{...props}
/>
);
}

function FormControl({ ...props }: React.ComponentProps<typeof Slot.Root>) {
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();

return (
<Slot.Root
<Slot
{...props}
data-slot="form-control"
id={formItemId}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
);
}
Expand Down
8 changes: 6 additions & 2 deletions web/src/components/ui/role-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,14 @@ export function RoleSelector({
typeof (r as Record<string, unknown>).color === 'number',
);

setRoles(fetchedRoles);
if (abortControllerRef.current === controller) {
setRoles(fetchedRoles);
}
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') return;
setError(err instanceof Error ? err.message : 'Failed to load roles');
if (abortControllerRef.current === controller) {
setError(err instanceof Error ? err.message : 'Failed to load roles');
}
} finally {
if (abortControllerRef.current === controller) {
setLoading(false);
Expand Down
17 changes: 16 additions & 1 deletion web/tests/setup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { afterEach } from "vitest";
import { afterEach, vi } from "vitest";

// Polyfill window.matchMedia for next-themes (jsdom doesn't implement it)
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});

afterEach(() => {
cleanup();
Expand Down
Loading