Skip to content

Commit eac9d01

Browse files
alireza787bclaude
andcommitted
fix(dashboard): consistent layout + Active chip refinement
- All sidebar categories start collapsed (removed auto-expand) - "Active" chip only on per-follower sections (MC_*/GM_*/FW_*) + SmartTracker - Remove forced card layout for Safety/Follower sections - Hybrid desktop layout: simple params in table, specialized editors (Safety/Follower) as full-width cards below - Consistent responsive behavior across all sections Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fecda9e commit eac9d01

File tree

2 files changed

+172
-145
lines changed

2 files changed

+172
-145
lines changed

dashboard/src/components/config/SectionEditor.js

Lines changed: 163 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -627,10 +627,8 @@ const SectionEditor = ({ sectionName, highlightParam = null, onHighlightComplete
627627
const { isMobile, isTablet, isCompactDesktop, compactTable } = useResponsive();
628628
const globalState = useConfigGlobalState();
629629

630-
// Use card layout on mobile/tablet/compact-desktop, OR always for Safety/Follower sections
631-
const isSafetySection = sectionName === 'Safety' || sectionName === 'SafetyLimits';
632-
const isFollowerSection = sectionName === 'Follower';
633-
const useCardLayout = isMobile || isTablet || isCompactDesktop || isSafetySection || isFollowerSection;
630+
// Use card layout on mobile/tablet/compact-desktop (responsive breakpoint only)
631+
const useCardLayout = isMobile || isTablet || isCompactDesktop;
634632

635633
const [localValues, setLocalValues] = useState({});
636634
const [saveStatuses, setSaveStatuses] = useState({}); // 'saving' | 'saved' | 'error' | null
@@ -825,6 +823,16 @@ const SectionEditor = ({ sectionName, highlightParam = null, onHighlightComplete
825823
return localValues[param] !== undefined ? localValues[param] : config[param];
826824
};
827825

826+
// Identify specialized object params that need full-width rendering (Safety/Follower editors)
827+
const isSpecializedParam = (param) => {
828+
if (parameters[param]?.type !== 'object') return false;
829+
if (param === 'GlobalLimits' || (param === 'FollowerOverrides' && 'GlobalLimits' in config)) return true;
830+
if ((param === 'General' || param === 'FollowerOverrides') && 'General' in config) return true;
831+
return false;
832+
};
833+
const simpleParams = paramNames.filter(p => !isSpecializedParam(p));
834+
const specializedParams = paramNames.filter(p => isSpecializedParam(p));
835+
828836
return (
829837
<Paper
830838
ref={containerRef}
@@ -929,136 +937,159 @@ const SectionEditor = ({ sectionName, highlightParam = null, onHighlightComplete
929937
))}
930938
</Box>
931939
) : (
932-
// Desktop (>= 900px): Table Layout with responsive overflow
933-
<TableContainer sx={{ overflowX: 'auto' }}>
934-
<Table size="small" sx={{ tableLayout: 'fixed', width: '100%' }}>
935-
<TableHead>
936-
<TableRow>
937-
<TableCell sx={{ fontWeight: 'bold', width: compactTable ? '22%' : '20%' }}>Parameter</TableCell>
938-
<TableCell sx={{ fontWeight: 'bold', width: compactTable ? '38%' : '35%' }}>Value</TableCell>
939-
<TableCell sx={{ fontWeight: 'bold', width: compactTable ? '14%' : '18%' }}>Default</TableCell>
940-
<TableCell sx={{ fontWeight: 'bold', width: compactTable ? '12%' : '14%' }}>Info</TableCell>
941-
<TableCell sx={{ fontWeight: 'bold', width: compactTable ? '14%' : '13%', whiteSpace: 'nowrap' }}>Actions</TableCell>
942-
</TableRow>
943-
</TableHead>
944-
<TableBody>
945-
{paramNames.map((param) => {
946-
const paramSchema = parameters[param];
947-
const currentValue = getValue(param);
948-
const defaultValue = defaultConfig[param] ?? paramSchema?.default;
949-
const modified = !isDeepEqual(currentValue, defaultValue);
950-
const hasPending = param in pendingChanges;
951-
const saveStatus = saveStatuses[param];
952-
953-
return (
954-
<TableRow
955-
key={param}
956-
data-param={param}
957-
sx={{
958-
bgcolor: modified ? 'action.selected' : undefined,
959-
'&:hover': { bgcolor: 'action.hover' }
960-
}}
961-
>
962-
<TableCell sx={{ overflow: 'hidden' }}>
963-
<Box>
964-
<Typography variant="body2" sx={{ fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
965-
{param}
966-
</Typography>
967-
{paramSchema?.description && (
968-
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
969-
{paramSchema.description.slice(0, 60)}
970-
{paramSchema.description.length > 60 ? '...' : ''}
971-
</Typography>
972-
)}
973-
</Box>
974-
</TableCell>
975-
976-
<TableCell sx={{ overflow: 'hidden' }}>
977-
<ParameterInput
978-
param={param}
979-
schema={schema}
980-
value={currentValue}
981-
defaultValue={defaultValue}
982-
onChange={handleLocalChange}
983-
onSave={handleSave}
984-
saveStatus={saveStatus}
985-
configValues={config}
986-
autoSaveEnabled={autoSaveEnabled}
987-
/>
988-
</TableCell>
989-
990-
<TableCell sx={{ overflow: 'hidden' }}>
991-
<Typography
992-
variant="body2"
993-
color="text.secondary"
994-
noWrap
995-
sx={{ fontFamily: 'monospace', fontSize: '0.75rem' }}
940+
// Desktop: Table for simple params, full-width cards for specialized editors
941+
<Box>
942+
{/* Table for simple (non-specialized) params */}
943+
{simpleParams.length > 0 && (
944+
<TableContainer sx={{ overflowX: 'auto' }}>
945+
<Table size="small" sx={{ tableLayout: 'fixed', width: '100%' }}>
946+
<TableHead>
947+
<TableRow>
948+
<TableCell sx={{ fontWeight: 'bold', width: compactTable ? '22%' : '20%' }}>Parameter</TableCell>
949+
<TableCell sx={{ fontWeight: 'bold', width: compactTable ? '38%' : '35%' }}>Value</TableCell>
950+
<TableCell sx={{ fontWeight: 'bold', width: compactTable ? '14%' : '18%' }}>Default</TableCell>
951+
<TableCell sx={{ fontWeight: 'bold', width: compactTable ? '12%' : '14%' }}>Info</TableCell>
952+
<TableCell sx={{ fontWeight: 'bold', width: compactTable ? '14%' : '13%', whiteSpace: 'nowrap' }}>Actions</TableCell>
953+
</TableRow>
954+
</TableHead>
955+
<TableBody>
956+
{simpleParams.map((param) => {
957+
const paramSchema = parameters[param];
958+
const currentValue = getValue(param);
959+
const defaultValue = defaultConfig[param] ?? paramSchema?.default;
960+
const modified = !isDeepEqual(currentValue, defaultValue);
961+
const hasPending = param in pendingChanges;
962+
const saveStatus = saveStatuses[param];
963+
964+
return (
965+
<TableRow
966+
key={param}
967+
data-param={param}
968+
sx={{
969+
bgcolor: modified ? 'action.selected' : undefined,
970+
'&:hover': { bgcolor: 'action.hover' }
971+
}}
996972
>
997-
{typeof defaultValue === 'object'
998-
? JSON.stringify(defaultValue).slice(0, 20)
999-
: String(defaultValue)}
1000-
</Typography>
1001-
</TableCell>
1002-
1003-
<TableCell>
1004-
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
1005-
{paramSchema?.reload_tier && (
1006-
<ReloadTierChip tier={paramSchema.reload_tier} size="small" />
1007-
)}
1008-
{paramSchema?.unit && (
1009-
<Chip label={paramSchema.unit} size="small" variant="outlined" />
1010-
)}
1011-
{hasPending && (
1012-
<Chip label="Unsaved" size="small" color="info" />
1013-
)}
1014-
{modified && !hasPending && (
1015-
<Chip label="Modified" size="small" color="warning" />
1016-
)}
1017-
</Box>
1018-
</TableCell>
1019-
1020-
<TableCell>
1021-
<Box sx={{ display: 'flex', gap: 0.5 }}>
1022-
<Tooltip title="Open detail editor">
1023-
<IconButton
1024-
size="small"
1025-
onClick={() => setSelectedParam(param)}
973+
<TableCell sx={{ overflow: 'hidden' }}>
974+
<Box>
975+
<Typography variant="body2" sx={{ fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
976+
{param}
977+
</Typography>
978+
{paramSchema?.description && (
979+
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
980+
{paramSchema.description.slice(0, 60)}
981+
{paramSchema.description.length > 60 ? '...' : ''}
982+
</Typography>
983+
)}
984+
</Box>
985+
</TableCell>
986+
987+
<TableCell sx={{ overflow: 'hidden' }}>
988+
<ParameterInput
989+
param={param}
990+
schema={schema}
991+
value={currentValue}
992+
defaultValue={defaultValue}
993+
onChange={handleLocalChange}
994+
onSave={handleSave}
995+
saveStatus={saveStatus}
996+
configValues={config}
997+
autoSaveEnabled={autoSaveEnabled}
998+
/>
999+
</TableCell>
1000+
1001+
<TableCell sx={{ overflow: 'hidden' }}>
1002+
<Typography
1003+
variant="body2"
1004+
color="text.secondary"
1005+
noWrap
1006+
sx={{ fontFamily: 'monospace', fontSize: '0.75rem' }}
10261007
>
1027-
<OpenInNew fontSize="small" />
1028-
</IconButton>
1029-
</Tooltip>
1030-
{hasPending && (
1031-
<Tooltip title="Save this parameter">
1032-
<IconButton
1033-
size="small"
1034-
color="primary"
1035-
onClick={() => handleSave(param, currentValue)}
1036-
disabled={saveStatus === 'saving'}
1037-
>
1038-
<Save fontSize="small" />
1039-
</IconButton>
1040-
</Tooltip>
1041-
)}
1042-
{modified && (
1043-
<Tooltip title="Revert to default">
1044-
<IconButton
1045-
size="small"
1046-
onClick={() => handleRevert(param)}
1047-
disabled={saveStatus === 'saving'}
1048-
>
1049-
<Undo fontSize="small" />
1050-
</IconButton>
1051-
</Tooltip>
1052-
)}
1053-
</Box>
1054-
</TableCell>
1055-
</TableRow>
1056-
);
1057-
})}
1058-
</TableBody>
1059-
</Table>
1060-
</TableContainer>
1061-
)}
1008+
{typeof defaultValue === 'object'
1009+
? JSON.stringify(defaultValue).slice(0, 20)
1010+
: String(defaultValue)}
1011+
</Typography>
1012+
</TableCell>
1013+
1014+
<TableCell>
1015+
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
1016+
{paramSchema?.reload_tier && (
1017+
<ReloadTierChip tier={paramSchema.reload_tier} size="small" />
1018+
)}
1019+
{paramSchema?.unit && (
1020+
<Chip label={paramSchema.unit} size="small" variant="outlined" />
1021+
)}
1022+
{hasPending && (
1023+
<Chip label="Unsaved" size="small" color="info" />
1024+
)}
1025+
{modified && !hasPending && (
1026+
<Chip label="Modified" size="small" color="warning" />
1027+
)}
1028+
</Box>
1029+
</TableCell>
1030+
1031+
<TableCell>
1032+
<Box sx={{ display: 'flex', gap: 0.5 }}>
1033+
<Tooltip title="Open detail editor">
1034+
<IconButton
1035+
size="small"
1036+
onClick={() => setSelectedParam(param)}
1037+
>
1038+
<OpenInNew fontSize="small" />
1039+
</IconButton>
1040+
</Tooltip>
1041+
{hasPending && (
1042+
<Tooltip title="Save this parameter">
1043+
<IconButton
1044+
size="small"
1045+
color="primary"
1046+
onClick={() => handleSave(param, currentValue)}
1047+
disabled={saveStatus === 'saving'}
1048+
>
1049+
<Save fontSize="small" />
1050+
</IconButton>
1051+
</Tooltip>
1052+
)}
1053+
{modified && (
1054+
<Tooltip title="Revert to default">
1055+
<IconButton
1056+
size="small"
1057+
onClick={() => handleRevert(param)}
1058+
disabled={saveStatus === 'saving'}
1059+
>
1060+
<Undo fontSize="small" />
1061+
</IconButton>
1062+
</Tooltip>
1063+
)}
1064+
</Box>
1065+
</TableCell>
1066+
</TableRow>
1067+
);
1068+
})}
1069+
</TableBody>
1070+
</Table>
1071+
</TableContainer>
1072+
)}
1073+
1074+
{/* Full-width specialized editors (Safety/Follower) */}
1075+
{specializedParams.map((param) => (
1076+
<ParameterCard
1077+
key={param}
1078+
param={param}
1079+
schema={schema}
1080+
value={getValue(param)}
1081+
defaultValue={defaultConfig[param] ?? parameters[param]?.default}
1082+
saveStatus={saveStatuses[param]}
1083+
onLocalChange={handleLocalChange}
1084+
onSave={handleSave}
1085+
onRevert={handleRevert}
1086+
onOpenDetails={() => setSelectedParam(param)}
1087+
configValues={config}
1088+
autoSaveEnabled={autoSaveEnabled}
1089+
/>
1090+
))}
1091+
</Box>
1092+
)}
10621093

10631094
{paramNames.length === 0 && (
10641095
<Box sx={{ py: 4, textAlign: 'center' }}>

dashboard/src/pages/SettingsPage.js

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -108,18 +108,6 @@ const SettingsPageContent = () => {
108108
}
109109
}, [sections]);
110110

111-
// Auto-expand the category containing the active mode section
112-
useEffect(() => {
113-
if (modeSpecificSections.length > 0 && Object.keys(expandedCategories).length === 0) {
114-
for (const [category, catSections] of Object.entries(groupedSections)) {
115-
if (catSections.some(s => modeSpecificSections.includes(s.name))) {
116-
setExpandedCategories(prev => ({ ...prev, [category]: true }));
117-
break;
118-
}
119-
}
120-
}
121-
}, [modeSpecificSections, groupedSections]); // eslint-disable-line react-hooks/exhaustive-deps
122-
123111
// Close search dropdown on outside click
124112
useEffect(() => {
125113
const handleClickOutside = (e) => {
@@ -153,6 +141,14 @@ const SettingsPageContent = () => {
153141
return categoryOrder.filter(cat => sortedGroupedSections[cat]?.length > 0);
154142
}, [sortedGroupedSections]);
155143

144+
// Compute which sections get the green "Active" chip:
145+
// Only per-follower sections (MC_*/GM_*/FW_*) + SmartTracker when follower is active
146+
const activeChipSections = useMemo(() => {
147+
const perFollower = modeSpecificSections.filter(s => /^(MC_|GM_|FW_)/.test(s));
148+
if (followerActive) perFollower.push('SmartTracker');
149+
return perFollower;
150+
}, [modeSpecificSections, followerActive]);
151+
156152
const toggleCategory = (category) => {
157153
setExpandedCategories(prev => ({
158154
...prev,
@@ -619,7 +615,7 @@ const SettingsPageContent = () => {
619615
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
620616
<List component="div" disablePadding dense>
621617
{catSections.map((section) => {
622-
const isModeSpecific = modeSpecificSections.includes(section.name);
618+
const isModeSpecific = activeChipSections.includes(section.name);
623619
return (
624620
<ListItemButton
625621
key={section.name}

0 commit comments

Comments
 (0)