import * as React from 'react'; import type { FormattingOptions, FormattingPreset } from '../types'; import { ChevronDown, ChevronRight, ListChecks, ListX, ChevronsDown, ChevronsUp, X } from 'lucide-react'; import { getFormattingOptionsForPreset, type FormattingOptionsTokenImpact } from 'scn-ts-core'; import { cn } from '../lib/utils'; import { Checkbox } from './ui/checkbox'; import { Label } from './ui/label'; import { Input } from './ui/input'; import { Button } from './ui/button'; interface OutputOptionsProps { options: FormattingOptions; setOptions: React.Dispatch>; tokenImpact: FormattingOptionsTokenImpact | null; } type RegularOptionKey = keyof Omit; type OptionItem = RegularOptionKey | string | { name: string; children: OptionItem[] }; const symbolKindLabels: Record = { // TS/JS class: 'Classes', interface: 'Interfaces', function: 'Functions', method: 'Methods', constructor: 'Constructors', variable: 'Variables', property: 'Properties', enum: 'Enums', enum_member: 'Enum Members', type_alias: 'Type Aliases', module: 'Modules', // React react_component: 'React Components', styled_component: 'Styled Components', jsx_element: 'JSX Elements', // CSS css_class: 'CSS Classes', css_id: 'CSS IDs', css_tag: 'CSS Tags', css_at_rule: 'CSS At-Rules', css_variable: 'CSS Variables', // Go go_package: 'Go Packages', // Rust rust_struct: 'Rust Structs', rust_trait: 'Rust Traits', rust_impl: 'Rust Impls', }; const tsDeclarationKinds = ['class', 'interface', 'function', 'variable', 'enum', 'type_alias', 'module']; const tsMemberKinds = ['method', 'constructor', 'property', 'enum_member']; const reactKinds = ['react_component', 'styled_component', 'jsx_element']; const cssKinds = ['css_class', 'css_id', 'css_tag', 'css_at_rule', 'css_variable']; const goKinds = ['go_package']; const rustKinds = ['rust_struct', 'rust_trait', 'rust_impl']; const toFilter = (kind: string): string => `filter:${kind}`; const symbolVisibilityTree: OptionItem = { name: 'Symbol Visibility', children: [ { name: 'TypeScript/JavaScript', children: [ { name: 'Declarations', children: tsDeclarationKinds.map(toFilter) }, { name: 'Members', children: tsMemberKinds.map(toFilter) }, ], }, { name: 'React', children: reactKinds.map(toFilter) }, { name: 'CSS', children: cssKinds.map(toFilter) }, { name: 'Other Languages', children: [ { name: 'Go', children: goKinds.map(toFilter) }, { name: 'Rust', children: rustKinds.map(toFilter) }, ], }, ], }; const optionTree: OptionItem[] = [ { name: 'Display Elements', children: [ 'showIcons', { name: 'Indicators', children: ['showExportedIndicator', 'showPrivateIndicator'], }, 'showModifiers', 'showTags', { name: 'Identifiers', children: ['showFilePrefix', 'showFileIds', 'showSymbolIds'], }, ], }, { name: 'Relationships', children: ['showOutgoing', 'showIncoming'], }, { name: 'Structure', children: ['groupMembers', 'showOnlyExports'], }, symbolVisibilityTree, ]; const optionLabels: Record & Record = { ...symbolKindLabels, showIcons: 'Icons', showExportedIndicator: 'Exported (+)', showPrivateIndicator: 'Private (-)', showModifiers: 'Modifiers', showTags: 'Tags', showSymbolIds: 'Symbol IDs', showFilePrefix: 'File Prefix (ยง)', showFileIds: 'File IDs', showOutgoing: 'Outgoing', showIncoming: 'Incoming', groupMembers: 'Group Members', showOnlyExports: 'Show Only Exports', }; function getAllKeys(item: OptionItem): string[] { if (typeof item === 'string') { return [item]; } return item.children.flatMap(getAllKeys); } const getAllGroupNames = (items: OptionItem[]): string[] => { return items.flatMap(item => { if (typeof item === 'object' && 'name' in item) { return [item.name, ...getAllGroupNames(item.children)]; } return []; }); } export const OutputOptions: React.FC = ({ options, setOptions, tokenImpact }) => { const [expandedGroups, setExpandedGroups] = React.useState>( () => new Set([ 'Display Elements', 'Indicators', 'Relationships', 'Structure', 'TypeScript/JavaScript', 'React', 'Identifiers', ]) ); const [searchTerm, setSearchTerm] = React.useState(''); const allOptionKeys = React.useMemo(() => optionTree.flatMap(getAllKeys), []); const areAllSelected = React.useMemo(() => { if (allOptionKeys.length === 0) return false; return allOptionKeys.every(key => { if (key.startsWith('filter:')) { const kind = key.substring('filter:'.length); if (options.displayFilters && Object.hasOwn(options.displayFilters, kind)) { return options.displayFilters[kind] as boolean; } return true; // Default is selected } return options[key as RegularOptionKey] ?? true; }); }, [allOptionKeys, options]); const filteredOptionTree = React.useMemo(() => { if (!searchTerm.trim()) return optionTree; const lowerCaseSearchTerm = searchTerm.toLowerCase(); function filter(item: OptionItem): OptionItem | null { if (typeof item === 'string') { const label = optionLabels[item as keyof typeof optionLabels] || item; return label.toLowerCase().includes(lowerCaseSearchTerm) ? item : null; } if (item.name.toLowerCase().includes(lowerCaseSearchTerm)) { return item; // Keep group and all its children if group name matches } const filteredChildren = item.children.map(filter).filter((c): c is OptionItem => c !== null); if (filteredChildren.length > 0) { return { ...item, children: filteredChildren }; } return null; } return optionTree.map(filter).filter((i): i is OptionItem => i !== null); }, [searchTerm]); const allFilteredGroupNames = React.useMemo(() => getAllGroupNames(filteredOptionTree), [filteredOptionTree]); const areAllExpanded = React.useMemo(() => allFilteredGroupNames.length > 0 && allFilteredGroupNames.every(g => expandedGroups.has(g)), [allFilteredGroupNames, expandedGroups]); React.useEffect(() => { if (searchTerm.trim()) { setExpandedGroups(new Set(allFilteredGroupNames)); } }, [searchTerm, allFilteredGroupNames]); const expandAll = React.useCallback(() => setExpandedGroups(new Set(allFilteredGroupNames)), [allFilteredGroupNames]); const collapseAll = React.useCallback(() => { setExpandedGroups(new Set()); }, []); const selectAll = React.useCallback(() => { setOptions(prev => { const newOptions: FormattingOptions = { ...prev, preset: undefined }; const newDisplayFilters = { ...(prev.displayFilters ?? {}) }; for (const key of allOptionKeys) { if (key.startsWith('filter:')) { newDisplayFilters[key.substring('filter:'.length)] = true; } else { newOptions[key as RegularOptionKey] = true; } } newOptions.displayFilters = newDisplayFilters; return newOptions; }); }, [allOptionKeys]); const deselectAll = React.useCallback(() => { setOptions(prev => { const newOptions: FormattingOptions = { ...prev, preset: undefined }; const newDisplayFilters = { ...(prev.displayFilters ?? {}) }; for (const key of allOptionKeys) { if (key.startsWith('filter:')) { newDisplayFilters[key.substring('filter:'.length)] = false; } else { newOptions[key as RegularOptionKey] = false; } } newOptions.displayFilters = newDisplayFilters; return newOptions; }); }, [allOptionKeys]); const toggleGroup = (groupName: string) => { setExpandedGroups(prev => { const newSet = new Set(prev); if (newSet.has(groupName)) { newSet.delete(groupName); } else { newSet.add(groupName); } return newSet; }); }; const handleChange = (optionKey: string) => (checked: boolean | 'indeterminate') => { const isChecked = checked === true; if (optionKey.startsWith('filter:')) { const kind = optionKey.substring('filter:'.length); setOptions(prev => ({ ...prev, preset: undefined, displayFilters: { ...(prev.displayFilters ?? {}), [kind]: isChecked }, })); } else { setOptions(prev => ({ ...prev, preset: undefined, [optionKey]: isChecked })); } }; const handleGroupChange = (keys: ReadonlyArray) => (checked: boolean | 'indeterminate') => { const isChecked = checked === true; setOptions(prev => { const newOptions: FormattingOptions = { ...prev, preset: undefined }; const newDisplayFilters = { ...(prev.displayFilters ?? {}) }; for (const key of keys) { if (key.startsWith('filter:')) { newDisplayFilters[key.substring('filter:'.length)] = isChecked; } else { newOptions[key as RegularOptionKey] = isChecked; } } newOptions.displayFilters = newDisplayFilters; return newOptions; }); }; const renderItem = (item: OptionItem, level: number): React.ReactNode => { if (typeof item === 'string') { const key = item as string; const isFilter = key.startsWith('filter:'); const filterKind = isFilter ? key.substring('filter:'.length) : null; const labelKey = filterKind ?? key; return (
); } const { name, children } = item; const isExpanded = expandedGroups.has(name); const allKeys = getAllKeys(item); const allChecked = allKeys.every(key => { if (key.startsWith('filter:')) { const kind = key.substring('filter:'.length); return (options.displayFilters && Object.hasOwn(options.displayFilters, kind)) ? options.displayFilters[kind] : true; } return options[key as RegularOptionKey] ?? true; }); const groupTokenImpact = tokenImpact ? allKeys.reduce((sum, key) => { let impact: number | undefined; if (key.startsWith('filter:')) { const kind = key.substring('filter:'.length); if (tokenImpact.displayFilters && Object.hasOwn(tokenImpact.displayFilters, kind)) { impact = tokenImpact.displayFilters[kind]; } } else { impact = tokenImpact.options?.[key as RegularOptionKey]; } return sum + (impact ?? 0); }, 0) : null; const impactDisplay = tokenImpact && groupTokenImpact !== null ? ( {(() => { const impact = groupTokenImpact; if (impact === undefined) return null; return `${impact > 0 ? '+' : ''}${impact}`; })()} ) : null; return (
toggleGroup(name)} > {isExpanded ? : } e.stopPropagation()} // Prevent row click from firing />
{isExpanded && (
{children.map(child => renderItem(child, level + 1))}
)}
); }; const presets: FormattingPreset[] = ['minimal', 'compact', 'default', 'detailed', 'verbose']; return (
{presets.map(p => ( ))}
setSearchTerm(e.target.value)} className="h-8 text-xs pr-8" /> {searchTerm && ( )}
{filteredOptionTree.map(item => renderItem(item, 0))}
); };