feat: show visual feedback for auto-resolved scaffolding values

Add secondary color highlighting to show which scaffolding option "auto" is currently deferring to in mastery mode.

**Changes:**

1. **RuleThermometer.tsx**
   - Add `resolvedValue` prop to show what "auto" resolves to
   - When "auto" is selected, highlight the resolved option with green secondary color
   - Add border and semibold font to the auto-resolved button
   - Update tooltip to show "(currently selected by Auto)"
   - Distinct visual styles:
     - Primary selection (user's choice): Brand blue with white text
     - Auto-resolved: Green background with green border
     - Inactive: Gray background

2. **ScaffoldingTab.tsx**
   - Import getSkillById to fetch skill recommendations
   - Calculate resolvedDisplayRules based on current mode and skill
   - Pass resolvedValue to each RuleThermometer component
   - Support both single operator and mixed operator modes

**Visual Design:**

When user selects "Auto":
- "Auto" button: Highlighted in brand blue (selected)
- Resolved option (e.g., "Regroup"): Highlighted in green (what auto defers to)
- Other options: Gray (inactive)

**Example:**
```
User selects: "Auto" for Answer Boxes
Skill recommends: "always"

Result:
[Auto] ← Blue (selected)
[Always] ← Green (auto-resolved)
[Regroup] [2+] [3+ dig] [Never] ← Gray (inactive)
```

This provides clear visual feedback about what "auto" is actually doing without requiring users to check skill documentation.

🤖 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-18 06:57:07 -06:00
parent a945a620c4
commit fbe776ac09
2 changed files with 69 additions and 6 deletions

View File

@ -9,6 +9,8 @@ export interface RuleThermometerProps {
value: RuleMode
onChange: (value: RuleMode) => void
isDark?: boolean
/** When value is 'auto', this shows which value 'auto' resolves to */
resolvedValue?: RuleMode
}
const RULE_OPTIONS: Array<{ value: RuleMode; label: string; short: string }> = [
@ -26,8 +28,10 @@ export function RuleThermometer({
value,
onChange,
isDark = false,
resolvedValue,
}: RuleThermometerProps) {
const selectedIndex = RULE_OPTIONS.findIndex((opt) => opt.value === value)
const isAutoSelected = value === 'auto'
return (
<div
@ -74,6 +78,8 @@ export function RuleThermometer({
>
{RULE_OPTIONS.map((option, index) => {
const isSelected = value === option.value
// When 'auto' is selected, show which value it resolves to with secondary color
const isAutoResolved = isAutoSelected && resolvedValue === option.value
const isLeftmost = index === 0
const isRightmost = index === RULE_OPTIONS.length - 1
@ -82,16 +88,31 @@ export function RuleThermometer({
key={option.value}
type="button"
onClick={() => onChange(option.value)}
title={option.label}
title={isAutoResolved ? `${option.label} (currently selected by Auto)` : option.label}
className={css({
flex: 1,
px: '2',
py: '1.5',
fontSize: '2xs',
fontWeight: isSelected ? 'bold' : 'medium',
color: isSelected ? (isDark ? 'white' : 'white') : isDark ? 'gray.400' : 'gray.600',
bg: isSelected ? 'brand.500' : 'transparent',
border: 'none',
fontWeight: isSelected ? 'bold' : isAutoResolved ? 'semibold' : 'medium',
color: isSelected
? 'white'
: isAutoResolved
? isDark
? 'green.300'
: 'green.700'
: isDark
? 'gray.400'
: 'gray.600',
bg: isSelected
? 'brand.500'
: isAutoResolved
? isDark
? 'green.900/30'
: 'green.100'
: 'transparent',
border: isAutoResolved ? '1px solid' : 'none',
borderColor: isAutoResolved ? (isDark ? 'green.700' : 'green.300') : 'transparent',
borderTopLeftRadius: isLeftmost ? 'md' : '0',
borderBottomLeftRadius: isLeftmost ? 'md' : '0',
borderTopRightRadius: isRightmost ? 'md' : '0',
@ -99,7 +120,15 @@ export function RuleThermometer({
cursor: 'pointer',
transition: 'all 0.15s',
_hover: {
bg: isSelected ? 'brand.600' : isDark ? 'gray.600' : 'gray.200',
bg: isSelected
? 'brand.600'
: isAutoResolved
? isDark
? 'green.800/40'
: 'green.200'
: isDark
? 'gray.600'
: 'gray.200',
color: isSelected ? 'white' : isDark ? 'gray.200' : 'gray.800',
},
})}

View File

@ -7,6 +7,7 @@ import { useTheme } from '@/contexts/ThemeContext'
import { RuleThermometer } from '../config-panel/RuleThermometer'
import type { DisplayRules } from '../../displayRules'
import { defaultAdditionConfig } from '@/app/create/worksheets/config-schemas'
import { getSkillById } from '../../skills'
export function ScaffoldingTab() {
const { formState, onChange } = useWorksheetConfig()
@ -18,6 +19,33 @@ export function ScaffoldingTab() {
// Check if we're in mastery+mixed mode (needs operator-specific rules)
const isMasteryMixed = formState.mode === 'mastery' && formState.operator === 'mixed'
// Get resolved display rules for showing what 'auto' defers to
let resolvedDisplayRules: DisplayRules | undefined
if (formState.mode === 'mastery') {
const operator = formState.operator ?? 'addition'
if (operator === 'mixed') {
// Mixed mode: Use addition skill's recommendations for now (could show both)
const skillId = formState.currentAdditionSkillId
if (skillId) {
const skill = getSkillById(skillId as any)
resolvedDisplayRules = skill?.recommendedScaffolding
}
} else {
// Single operator: Use its skill's recommendations
const skillId =
operator === 'addition'
? formState.currentAdditionSkillId
: formState.currentSubtractionSkillId
if (skillId) {
const skill = getSkillById(skillId as any)
resolvedDisplayRules = skill?.recommendedScaffolding
}
}
}
const updateRule = (key: keyof DisplayRules, value: DisplayRules[keyof DisplayRules]) => {
const newDisplayRules = {
...displayRules,
@ -199,6 +227,7 @@ export function ScaffoldingTab() {
value={displayRules.answerBoxes}
onChange={(value) => updateRule('answerBoxes', value)}
isDark={isDark}
resolvedValue={resolvedDisplayRules?.answerBoxes}
/>
<RuleThermometer
@ -207,6 +236,7 @@ export function ScaffoldingTab() {
value={displayRules.placeValueColors}
onChange={(value) => updateRule('placeValueColors', value)}
isDark={isDark}
resolvedValue={resolvedDisplayRules?.placeValueColors}
/>
<RuleThermometer
@ -227,6 +257,7 @@ export function ScaffoldingTab() {
value={displayRules.carryBoxes}
onChange={(value) => updateRule('carryBoxes', value)}
isDark={isDark}
resolvedValue={resolvedDisplayRules?.carryBoxes}
/>
{(formState.operator === 'subtraction' || formState.operator === 'mixed') && (
@ -236,6 +267,7 @@ export function ScaffoldingTab() {
value={displayRules.borrowNotation}
onChange={(value) => updateRule('borrowNotation', value)}
isDark={isDark}
resolvedValue={resolvedDisplayRules?.borrowNotation}
/>
)}
@ -246,6 +278,7 @@ export function ScaffoldingTab() {
value={displayRules.borrowingHints}
onChange={(value) => updateRule('borrowingHints', value)}
isDark={isDark}
resolvedValue={resolvedDisplayRules?.borrowingHints}
/>
)}
@ -255,6 +288,7 @@ export function ScaffoldingTab() {
value={displayRules.tenFrames}
onChange={(value) => updateRule('tenFrames', value)}
isDark={isDark}
resolvedValue={resolvedDisplayRules?.tenFrames}
/>
</div>
)