feat: make scaffolding and preview collapsible

Make pedagogical scaffolding controls collapsible (power user feature):
- Pedagogical Scaffolding section collapsed by default
- Live Preview nested inside, also collapsed by default
- Use Radix UI Collapsible for smooth accordion behavior

Changes:
- Add DisplayControlsPanel component with collapsible sections
- Pedagogical Scaffolding header with "(Advanced)" label
- Live Preview header with "(Optional)" label
- Animated arrow indicators for expand/collapse state

🤖 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-10 18:12:22 -06:00
parent 0b7382f1b6
commit 804fb1a2f6
1 changed files with 307 additions and 0 deletions

View File

@ -0,0 +1,307 @@
'use client'
import { useState } from 'react'
import * as Collapsible from '@radix-ui/react-collapsible'
import { css } from '../../../../../../styled-system/css'
import { stack } from '../../../../../../styled-system/patterns'
import type { WorksheetFormState } from '../types'
import type { DisplayRules } from '../displayRules'
import { defaultAdditionConfig } from '@/app/create/worksheets/config-schemas'
import { RuleThermometer } from './config-panel/RuleThermometer'
import { DisplayOptionsPreview } from './DisplayOptionsPreview'
export interface DisplayControlsPanelProps {
formState: WorksheetFormState
onChange: (updates: Partial<WorksheetFormState>) => void
isDark?: boolean
}
export function DisplayControlsPanel({
formState,
onChange,
isDark = false,
}: DisplayControlsPanelProps) {
const [isOpen, setIsOpen] = useState(false)
const [isPreviewOpen, setIsPreviewOpen] = useState(false)
// Get current displayRules or use defaults
const displayRules: DisplayRules = formState.displayRules ?? defaultAdditionConfig.displayRules
// Helper to update a single display rule
const updateRule = (key: keyof DisplayRules, value: DisplayRules[keyof DisplayRules]) => {
onChange({
displayRules: {
...displayRules,
[key]: value,
},
})
}
return (
<Collapsible.Root open={isOpen} onOpenChange={setIsOpen}>
<div data-section="display-controls" className={stack({ gap: '3' })}>
<Collapsible.Trigger asChild>
<button
type="button"
className={css({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
cursor: 'pointer',
bg: 'transparent',
border: 'none',
_hover: {
'& > div:first-child': {
color: isDark ? 'gray.300' : 'gray.600',
},
},
})}
>
<div
className={css({
display: 'flex',
alignItems: 'center',
gap: '2',
})}
>
<div
className={css({
fontSize: 'xs',
fontWeight: 'semibold',
color: isDark ? 'gray.400' : 'gray.500',
textTransform: 'uppercase',
letterSpacing: 'wider',
transition: 'color 0.2s',
})}
>
Pedagogical Scaffolding
</div>
<div
className={css({
fontSize: '2xs',
color: isDark ? 'gray.500' : 'gray.400',
fontStyle: 'italic',
})}
>
(Advanced)
</div>
</div>
<div
className={css({
fontSize: 'sm',
color: isDark ? 'gray.500' : 'gray.400',
transition: 'transform 0.2s',
transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)',
})}
>
</div>
</button>
</Collapsible.Trigger>
<Collapsible.Content>
<div className={stack({ gap: '3' })}>
<div className={css({ display: 'flex', gap: '1.5', justifyContent: 'flex-end' })}>
<button
onClick={() =>
onChange({
displayRules: {
...displayRules,
carryBoxes: 'always',
answerBoxes: 'always',
placeValueColors: 'always',
tenFrames: 'always',
borrowNotation: 'always',
borrowingHints: 'always',
},
})
}
className={css({
px: '2',
py: '0.5',
fontSize: '2xs',
color: isDark ? 'brand.300' : 'brand.600',
border: '1px solid',
borderColor: isDark ? 'brand.500' : 'brand.300',
bg: isDark ? 'gray.700' : 'white',
rounded: 'md',
cursor: 'pointer',
_hover: { bg: isDark ? 'gray.600' : 'brand.50' },
})}
>
All Always
</button>
<button
onClick={() =>
onChange({
displayRules: {
...displayRules,
carryBoxes: 'never',
answerBoxes: 'never',
placeValueColors: 'never',
tenFrames: 'never',
borrowNotation: 'never',
borrowingHints: 'never',
},
})
}
className={css({
px: '2',
py: '0.5',
fontSize: '2xs',
color: isDark ? 'gray.300' : 'gray.600',
border: '1px solid',
borderColor: isDark ? 'gray.500' : 'gray.300',
bg: isDark ? 'gray.700' : 'white',
rounded: 'md',
cursor: 'pointer',
_hover: { bg: isDark ? 'gray.600' : 'gray.50' },
})}
>
Minimal
</button>
</div>
{/* Pedagogical scaffolding thermometers */}
<div className={stack({ gap: '3' })}>
<RuleThermometer
label="Answer Boxes"
description="Guide students to write organized, aligned answers"
value={displayRules.answerBoxes}
onChange={(value) => updateRule('answerBoxes', value)}
isDark={isDark}
/>
<RuleThermometer
label="Place Value Colors"
description="Reinforce place value understanding visually"
value={displayRules.placeValueColors}
onChange={(value) => updateRule('placeValueColors', value)}
isDark={isDark}
/>
<RuleThermometer
label={
formState.operator === 'subtraction'
? 'Borrow Boxes'
: formState.operator === 'mixed'
? 'Carry/Borrow Boxes'
: 'Carry Boxes'
}
description={
formState.operator === 'subtraction'
? 'Help students track borrowing during subtraction'
: formState.operator === 'mixed'
? 'Help students track regrouping (carrying in addition, borrowing in subtraction)'
: 'Help students track regrouping during addition'
}
value={displayRules.carryBoxes}
onChange={(value) => updateRule('carryBoxes', value)}
isDark={isDark}
/>
{(formState.operator === 'subtraction' || formState.operator === 'mixed') && (
<RuleThermometer
label="Borrowed 10s Box"
description="Box for adding 10 to borrowing digit"
value={displayRules.borrowNotation}
onChange={(value) => updateRule('borrowNotation', value)}
isDark={isDark}
/>
)}
{(formState.operator === 'subtraction' || formState.operator === 'mixed') && (
<RuleThermometer
label="Borrowing Hints"
description="Show arrows and calculations guiding the borrowing process"
value={displayRules.borrowingHints}
onChange={(value) => updateRule('borrowingHints', value)}
isDark={isDark}
/>
)}
<RuleThermometer
label="Ten-Frames"
description="Visualize regrouping with concrete counting tools"
value={displayRules.tenFrames}
onChange={(value) => updateRule('tenFrames', value)}
isDark={isDark}
/>
</div>
{/* Live Preview - Collapsible */}
<Collapsible.Root open={isPreviewOpen} onOpenChange={setIsPreviewOpen}>
<Collapsible.Trigger asChild>
<button
type="button"
className={css({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
cursor: 'pointer',
bg: 'transparent',
border: 'none',
mt: '2',
_hover: {
'& > div:first-child': {
color: isDark ? 'gray.300' : 'gray.600',
},
},
})}
>
<div
className={css({
display: 'flex',
alignItems: 'center',
gap: '2',
})}
>
<div
className={css({
fontSize: 'xs',
fontWeight: 'semibold',
color: isDark ? 'gray.400' : 'gray.500',
textTransform: 'uppercase',
letterSpacing: 'wider',
transition: 'color 0.2s',
})}
>
Live Preview
</div>
<div
className={css({
fontSize: '2xs',
color: isDark ? 'gray.500' : 'gray.400',
fontStyle: 'italic',
})}
>
(Optional)
</div>
</div>
<div
className={css({
fontSize: 'sm',
color: isDark ? 'gray.500' : 'gray.400',
transition: 'transform 0.2s',
transform: isPreviewOpen ? 'rotate(180deg)' : 'rotate(0deg)',
})}
>
</div>
</button>
</Collapsible.Trigger>
<Collapsible.Content>
<div className={css({ mt: '2' })}>
<DisplayOptionsPreview formState={formState} />
</div>
</Collapsible.Content>
</Collapsible.Root>
</div>
</Collapsible.Content>
</div>
</Collapsible.Root>
)
}