mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 13:31:10 +00:00
c368f60853
Move src/features/annotations/classColors.ts to its own component directory src/class-colors/ with a proper barrel; update the 4 consumer imports to go through the barrel; remove the F3-pending exemption from STC-ARCH-01 and from the architecture test fixture; clean up the 5 coupled doc/script touchpoints. Closes baseline finding F3 and retires the 5-coupled-places carry-over surface logged in LESSONS.md 2026-05-12. - Add `class-colors` to scripts/check-arch-imports.mjs COMPONENT_DIRS so deep imports past the new barrel are caught symmetric to every other component. - Replace the architecture test "exemption WORKS" fixture with the stronger "deep import into class-colors NOW FAILS" assertion (Risk 4 mitigation). - module-layout.md: Layout Rules + Per-Component Mapping (11_class-colors, 06_annotations, 03_shared-ui) + Verification Needed #1 + shared/class-colors block all updated to reflect the new home. - 11_class-colors/description.md: Caveats §7 + Module Inventory updated. - architecture_compliance_baseline.md: F3 marked CLOSED with full pre-resolution context preserved (mirrors AZ-485/F4 + AZ-486/F7 pattern); F4 carry-forward exemption note retired. - 04_verification_log.md: open questions #1 + #8 marked RESOLVED. - Build passes with no circular-import warnings (AC-4); fast suite 231/13 skipped green (AC-5); static profile green (AC-3 — zero exemptions remain). Batch report: _docs/03_implementation/batch_14_cycle3_report.md Co-authored-by: Cursor <cursoragent@cursor.com>
104 lines
4.2 KiB
TypeScript
104 lines
4.2 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { MdOutlineWbSunny, MdOutlineNightlightRound } from 'react-icons/md'
|
|
import { FaRegSnowflake } from 'react-icons/fa'
|
|
import { api, endpoints } from '../api'
|
|
// classColors lives under 06_annotations until F3 moves it to its own home.
|
|
// Importing through the 06_annotations barrel would create a cycle
|
|
// (DetectionClasses -> 06_annotations barrel -> AnnotationsPage -> DetectionClasses).
|
|
// STC-ARCH-01 exempts this single path as an F3-pending edge.
|
|
import { getClassColor, FALLBACK_CLASS_NAMES } from '../class-colors'
|
|
import type { DetectionClass } from '../types'
|
|
|
|
interface Props {
|
|
selectedClassNum: number
|
|
onSelect: (classNum: number) => void
|
|
photoMode: number
|
|
onPhotoModeChange: (mode: number) => void
|
|
}
|
|
|
|
const FALLBACK_CLASSES: DetectionClass[] = [0, 20, 40].flatMap(modeOffset =>
|
|
FALLBACK_CLASS_NAMES.map((name, i) => ({
|
|
id: i + modeOffset,
|
|
name,
|
|
shortName: name.slice(0, 3),
|
|
color: getClassColor(i),
|
|
maxSizeM: 10,
|
|
photoMode: modeOffset,
|
|
})),
|
|
)
|
|
|
|
export default function DetectionClasses({ selectedClassNum, onSelect, photoMode, onPhotoModeChange }: Props) {
|
|
const { t } = useTranslation()
|
|
const [classes, setClasses] = useState<DetectionClass[]>([])
|
|
|
|
useEffect(() => {
|
|
api.get<DetectionClass[]>(endpoints.annotations.classes())
|
|
.then(list => setClasses(list?.length ? list : FALLBACK_CLASSES))
|
|
.catch(() => setClasses(FALLBACK_CLASSES))
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
const handler = (e: KeyboardEvent) => {
|
|
const num = parseInt(e.key)
|
|
if (num >= 1 && num <= 9) {
|
|
const idx = num - 1
|
|
const cls = classes[idx + photoMode]
|
|
if (cls) onSelect(cls.id)
|
|
}
|
|
}
|
|
window.addEventListener('keydown', handler)
|
|
return () => window.removeEventListener('keydown', handler)
|
|
}, [classes, photoMode, onSelect])
|
|
|
|
// Auto-select first class of current photoMode when mode changes or classes load
|
|
useEffect(() => {
|
|
const modeClasses = classes.filter(c => c.photoMode === photoMode)
|
|
const currentIsInMode = modeClasses.some(c => c.id === selectedClassNum)
|
|
if (!currentIsInMode && modeClasses.length > 0) {
|
|
onSelect(modeClasses[0].id)
|
|
}
|
|
}, [classes, photoMode, selectedClassNum, onSelect])
|
|
|
|
const modes = [
|
|
{ value: 0, label: t('annotations.regular'), icon: <MdOutlineWbSunny />, activeClass: 'bg-az-orange text-white', iconColor: 'text-az-orange' },
|
|
{ value: 20, label: t('annotations.winter'), icon: <FaRegSnowflake />, activeClass: 'bg-az-blue text-white', iconColor: 'text-az-blue' },
|
|
{ value: 40, label: t('annotations.night'), icon: <MdOutlineNightlightRound />, activeClass: 'bg-purple-600 text-white', iconColor: 'text-purple-400' },
|
|
]
|
|
|
|
return (
|
|
<div className="border-t border-az-border p-2">
|
|
<div className="text-xs text-az-muted mb-1 font-semibold">{t('annotations.classes')}</div>
|
|
<div className="space-y-0.5 max-h-48 overflow-y-auto mb-2">
|
|
{classes.filter(c => c.photoMode === photoMode).map((c, i) => (
|
|
<button
|
|
key={c.id}
|
|
onClick={() => onSelect(c.id)}
|
|
className={`w-full flex items-center gap-1.5 px-1.5 py-0.5 rounded text-xs text-left ${
|
|
selectedClassNum === c.id ? 'bg-az-border text-white' : 'text-az-text hover:bg-az-bg'
|
|
}`}
|
|
>
|
|
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: getClassColor(c.id) }} />
|
|
<span className="text-az-muted">{i + 1}.</span>
|
|
<span className="truncate">{c.name}</span>
|
|
<span className="text-az-muted ml-auto">{c.shortName}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="text-xs text-az-muted mb-1 font-semibold">{t('annotations.photoMode')}</div>
|
|
<div className="flex gap-1">
|
|
{modes.map(m => (
|
|
<button
|
|
key={m.value}
|
|
onClick={() => onPhotoModeChange(m.value)}
|
|
title={m.label}
|
|
className={`flex-1 flex items-center justify-center px-2 py-1 rounded text-base ${photoMode === m.value ? m.activeClass : `bg-az-bg ${m.iconColor} hover:brightness-125`}`}
|
|
>
|
|
{m.icon}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|