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:
parent
0b7382f1b6
commit
804fb1a2f6
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue