Files
clg-kit/lib/design.ts
Aditya Gupta 2aac9c3fb4 feat: redesign day picker with expandable calendar, status markers & Playwright E2E testing
- Replace modal calendar with inline expandable calendar widget integrated into the day picker.
- Add status indicator dots (attended/absent/pending) to month calendar grid matching day picker.
- Disable and gray out weekend days in calendar when weekend schedule is turned off.
- Redesign extend bar with integrated handle that aligns with the palette.
- Remove start-to-end timing on Today screen's class time rail, showing only start time.
- Implement Playwright E2E and visual testing suite with multi-screen capture scripts.
- Add cascade DELETE endpoints for subjects and versioned recurring timetable entries.
2026-09-05 09:00:17 +05:30

41 lines
2.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { colors, type SemanticTone, type SubjectTone } from '@/components/ui';
const subjectTones = Object.keys(colors.subject) as SubjectTone[];
const hash = (value: string | number) => {
const input = String(value);
let result = 0;
for (let index = 0; index < input.length; index += 1) result = ((result << 5) - result + input.charCodeAt(index)) | 0;
return Math.abs(result);
};
/** Maps persisted subjects to one stable accessible surface/accent pair. */
export function subjectToneFor(identity: string | number, savedColor?: string): SubjectTone {
const normalized = savedColor?.toUpperCase();
const matched = subjectTones.find((tone) => colors.subject[tone].accent.toUpperCase() === normalized || colors.subject[tone].surface.toUpperCase() === normalized);
return matched ?? subjectTones[hash(identity) % subjectTones.length];
}
export function subjectShortLabel(name: string, code: string, shortName?: string): string {
if (shortName && shortName.trim()) return shortName.trim().slice(0, 5);
const parts = name.trim().split(/\s+/).filter(Boolean);
if (parts.length >= 2) {
return parts.map((w) => w[0]).join('').toUpperCase().slice(0, 4);
}
return code.trim().slice(0, 4);
}
export function attendanceTone(percentage: number, total: number, threshold = 75): SemanticTone {
if (!total) return 'neutral';
if (percentage >= threshold) return 'success';
if (percentage >= threshold - 10) return 'warning';
return 'danger';
}
export function attendanceMessage(percentage: number, total: number, threshold = 75) {
if (!total) return { title: 'No attendance yet', message: 'Mark your first class to start tracking progress.' };
if (percentage >= threshold + 5) return { title: 'Youre on track', message: `${percentage}% overall · Keep it above ${threshold}%.` };
if (percentage >= threshold) return { title: 'Youre just above the target', message: `${percentage}% overall · Your target is ${threshold}%.` };
return { title: 'Attendance needs attention', message: `${percentage}% overall · Your target is ${threshold}%.` };
}