feat(worksheets): add color-coding to difficulty presets with interpolation

Added subtle color progression from green (easy) to red (hard):
- Beginner: Green
- Early Learner: Cyan
- Intermediate: Yellow
- Advanced: Orange
- Expert: Red

Features:
- Dropdown button background/border uses preset color
- Dropdown menu items have colored left border accent
- Custom configurations interpolate color based on pythagorean distance
  between two nearest presets in 2D difficulty space
- Hover states use subtle opacity changes to avoid visual clash

Colors are intentionally subtle (using .50 backgrounds, .400 borders,
.700 text) to avoid being distracting while still providing visual
feedback about difficulty level.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-11-07 18:13:55 -06:00
parent 3a43149995
commit b1201b83c0
2 changed files with 86 additions and 9 deletions

View File

@ -14,6 +14,7 @@ import { ModeSelector } from './ModeSelector'
import {
DIFFICULTY_PROFILES,
DIFFICULTY_PROGRESSION,
DIFFICULTY_COLORS,
makeHarder,
makeEasier,
calculateOverallDifficulty,
@ -24,6 +25,7 @@ import {
SCAFFOLDING_PROGRESSION,
findNearestValidState,
getProfileFromConfig,
getInterpolatedColor,
type DifficultyLevel,
type DifficultyMode,
} from '../difficultyProfiles'
@ -422,6 +424,15 @@ export function ConfigPanel({ formState, onChange }: ConfigPanelProps) {
let nearestEasier: DifficultyLevel | null = null
let nearestHarder: DifficultyLevel | null = null
let customDescription: React.ReactNode = ''
let buttonColors = isCustom
? {
bg: 'orange.50' as const,
border: 'orange.400' as const,
text: 'orange.600' as const,
}
: currentProfile
? DIFFICULTY_COLORS[currentProfile]
: DIFFICULTY_COLORS.earlyLearner
if (isCustom) {
const currentRegrouping = calculateRegroupingIntensity(pAnyStart, pAllStart)
@ -473,6 +484,14 @@ export function ConfigPanel({ formState, onChange }: ConfigPanelProps) {
? harderPresets[0].presetName
: distances[distances.length - 1].presetName
// Get interpolated color based on position between nearest presets
buttonColors = getInterpolatedColor(
nearestEasier,
nearestHarder,
currentRegrouping,
currentScaffolding
)
// Generate custom description
const regroupingPercent = Math.round(currentRegrouping * 10)
const scaffoldingSummary = getScaffoldingSummary(displayRules)
@ -588,8 +607,8 @@ export function ConfigPanel({ formState, onChange }: ConfigPanelProps) {
px: '3',
py: '2.5',
border: '2px solid',
borderColor: isCustom ? 'orange.400' : 'gray.300',
bg: isCustom ? 'orange.50' : 'white',
borderColor: buttonColors.border,
bg: buttonColors.bg,
rounded: 'lg',
cursor: 'pointer',
transition: 'all 0.15s',
@ -599,7 +618,7 @@ export function ConfigPanel({ formState, onChange }: ConfigPanelProps) {
textAlign: 'left',
gap: '2',
_hover: {
borderColor: isCustom ? 'orange.500' : 'brand.400',
opacity: 0.9,
},
})}
>
@ -637,7 +656,7 @@ export function ConfigPanel({ formState, onChange }: ConfigPanelProps) {
<div
className={css({
fontSize: 'xs',
color: isCustom ? 'orange.600' : 'gray.500',
color: buttonColors.text,
lineHeight: '1.3',
h: '14',
display: 'flex',
@ -705,6 +724,7 @@ export function ConfigPanel({ formState, onChange }: ConfigPanelProps) {
{DIFFICULTY_PROGRESSION.map((presetName) => {
const preset = DIFFICULTY_PROFILES[presetName]
const isSelected = currentProfile === presetName && !isCustom
const presetColors = DIFFICULTY_COLORS[presetName]
// Generate preset description
const regroupingPercent = Math.round(
@ -743,12 +763,14 @@ export function ConfigPanel({ formState, onChange }: ConfigPanelProps) {
rounded: 'md',
cursor: 'pointer',
outline: 'none',
bg: isSelected ? 'brand.50' : 'transparent',
bg: isSelected ? presetColors.bg : 'transparent',
borderLeft: '3px solid',
borderColor: presetColors.border,
_hover: {
bg: 'brand.50',
bg: presetColors.bg,
},
_focus: {
bg: 'brand.100',
bg: presetColors.bg,
},
})}
>
@ -756,7 +778,7 @@ export function ConfigPanel({ formState, onChange }: ConfigPanelProps) {
className={css({
fontSize: 'sm',
fontWeight: 'semibold',
color: isSelected ? 'brand.700' : 'gray.700',
color: presetColors.text,
})}
>
{preset.label}
@ -764,7 +786,7 @@ export function ConfigPanel({ formState, onChange }: ConfigPanelProps) {
<div
className={css({
fontSize: 'xs',
color: isSelected ? 'brand.600' : 'gray.500',
color: presetColors.text,
lineHeight: '1.3',
})}
>

View File

@ -363,6 +363,61 @@ export interface DifficultyProfile {
* Pre-defined difficulty profiles that map to pedagogical progression
* Each profile balances problem complexity (regrouping) with scaffolding support
*/
/**
* Color palette for difficulty levels
* Subtle progression from green (easy) to red (hard)
*/
export const DIFFICULTY_COLORS = {
beginner: { bg: 'green.50', border: 'green.400', text: 'green.700' },
earlyLearner: { bg: 'cyan.50', border: 'cyan.400', text: 'cyan.700' },
intermediate: { bg: 'yellow.50', border: 'yellow.400', text: 'yellow.700' },
advanced: { bg: 'orange.50', border: 'orange.400', text: 'orange.700' },
expert: { bg: 'red.50', border: 'red.400', text: 'red.700' },
} as const
/**
* Get interpolated color between two presets based on distance
* Uses pythagorean distance in 2D difficulty space to blend colors
*/
export function getInterpolatedColor(
nearestEasier: DifficultyLevel,
nearestHarder: DifficultyLevel,
currentRegrouping: number,
currentScaffolding: number
): { bg: string; border: string; text: string } {
const easierProfile = DIFFICULTY_PROFILES[nearestEasier]
const harderProfile = DIFFICULTY_PROFILES[nearestHarder]
// Calculate positions in 2D space
const easierRegrouping = calculateRegroupingIntensity(
easierProfile.regrouping.pAnyStart,
easierProfile.regrouping.pAllStart
)
const easierScaffolding = calculateScaffoldingLevel(easierProfile.displayRules, easierRegrouping)
const harderRegrouping = calculateRegroupingIntensity(
harderProfile.regrouping.pAnyStart,
harderProfile.regrouping.pAllStart
)
const harderScaffolding = calculateScaffoldingLevel(harderProfile.displayRules, harderRegrouping)
// Calculate distances
const distanceToEasier = Math.sqrt(
(currentRegrouping - easierRegrouping) ** 2 + (currentScaffolding - easierScaffolding) ** 2
)
const distanceToHarder = Math.sqrt(
(currentRegrouping - harderRegrouping) ** 2 + (currentScaffolding - harderScaffolding) ** 2
)
// Calculate interpolation weight (0 = easier, 1 = harder)
const totalDistance = distanceToEasier + distanceToHarder
const weight = totalDistance > 0 ? distanceToEasier / totalDistance : 0.5
// For now, use discrete color based on which is closer
// (True color interpolation would require RGB conversion)
return weight < 0.5 ? DIFFICULTY_COLORS[nearestEasier] : DIFFICULTY_COLORS[nearestHarder]
}
export const DIFFICULTY_PROFILES: Record<string, DifficultyProfile> = {
beginner: {
name: 'beginner',