mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-08 04:38:45 +02:00
refactor: decompose large frontend components into focused modules
Split six monolithic component files into smaller, single-responsibility modules organized by feature domain: - ChatPane (1054→~250 lines): extract InlinePills, ApprovalBanner, ThinkingDots into chat/ directory - SettingsPanel (1027→~280 lines): extract McpServerEditor, LspProfileEditor, ToolingEditorShell into settings/ directory; mutation helpers into lib/settingsHelpers.ts - PatternEditor: use shared ToggleSwitch from ui/ - App.tsx: extract useTheme and useSidecarCapabilities into hooks/ - Sidebar: extracted accessibility improvements inline New shared primitives: - hooks/useClickOutside: replaces 5 duplicated click-outside listeners - components/ui/: ToggleSwitch, PopoverToggleRow, FormField, TextInput, TextareaInput, SelectInput, InfoCallout Accessibility improvements: - NewSessionModal: role=dialog, aria-modal, Escape-to-close - Pill dropdowns: aria-expanded, aria-haspopup, role=listbox/option - Sidebar context menu: role=menu/menuitem, Escape-to-close - SessionItem: Space key activation alongside Enter - ToolbarButton: aria-pressed for toggle state - ApprovalBanner: role=alert - QueuedApprovalsList: aria-expanded on toggle - ThinkingDots: aria-label No behavioral changes. All 137 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { AppearanceTheme } from '@shared/domain/tooling';
|
||||
|
||||
/** Resolves the effective theme and applies it to the document root. */
|
||||
export function useTheme(themeSetting: AppearanceTheme) {
|
||||
useEffect(() => {
|
||||
function resolveEffective(pref: AppearanceTheme): 'dark' | 'light' {
|
||||
if (pref === 'dark' || pref === 'light') return pref;
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
const apply = () => {
|
||||
document.documentElement.dataset.theme = resolveEffective(themeSetting);
|
||||
};
|
||||
|
||||
apply();
|
||||
|
||||
if (themeSetting === 'system') {
|
||||
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
mq.addEventListener('change', apply);
|
||||
return () => mq.removeEventListener('change', apply);
|
||||
}
|
||||
}, [themeSetting]);
|
||||
}
|
||||
|
||||
/** Manages sidecar capabilities loading and refresh. */
|
||||
export function useSidecarCapabilities(api: {
|
||||
describeSidecarCapabilities: () => Promise<import('@shared/contracts/sidecar').SidecarCapabilities>;
|
||||
refreshSidecarCapabilities: () => Promise<import('@shared/contracts/sidecar').SidecarCapabilities>;
|
||||
}) {
|
||||
const [capabilities, setCapabilities] = useState<import('@shared/contracts/sidecar').SidecarCapabilities>();
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
|
||||
void api
|
||||
.describeSidecarCapabilities()
|
||||
.then((c) => !disposed && setCapabilities(c))
|
||||
.catch((e) => {
|
||||
if (!disposed) console.warn('Failed to load sidecar capabilities', e);
|
||||
});
|
||||
|
||||
return () => { disposed = true; };
|
||||
}, [api]);
|
||||
|
||||
async function refresh() {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
const c = await api.refreshSidecarCapabilities();
|
||||
setCapabilities(c);
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}
|
||||
|
||||
return { capabilities, isRefreshing, refresh };
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useEffect, useRef, type RefObject } from 'react';
|
||||
|
||||
/**
|
||||
* Calls `onClose` when a click lands outside the referenced element.
|
||||
* Only attaches the listener while `active` is true.
|
||||
*/
|
||||
export function useClickOutside<T extends HTMLElement>(
|
||||
onClose: () => void,
|
||||
active: boolean,
|
||||
): RefObject<T | null> {
|
||||
const ref = useRef<T | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
|
||||
function handleClick(e: MouseEvent) {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClick);
|
||||
return () => document.removeEventListener('mousedown', handleClick);
|
||||
}, [active, onClose]);
|
||||
|
||||
return ref;
|
||||
}
|
||||
Reference in New Issue
Block a user