Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 25x 25x 25x 25x 25x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 80x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 41x 80x 80x 80x 80x | 'use client'
// Disclosure — a self-contained "Customize ▸/▾" collapsible for the dark studio
// rails (Gitea epic #5, full-bleed CP1a). Factored from the viewer's inline
// Customize toggle so the same primitive tucks away the design sliders, the print
// settings editor, and the marker sheet's "advanced" (vector SVG) section behind
// progressive disclosure. Styled from theme.ts: the header wears the same ruled
// eyebrow as StudioSection's, so an open section and a closed fold read as one
// ladder and the only thing the difference carries is "you probably want this"
// vs "ask for it".
//
// Open state is uncontrolled by default and controlled when `open` is passed —
// PrintPanel needs to read whether the settings are showing (the floating rail
// widens for them) without a second copy of the state.
//
// `keepMounted` is the print-settings editor's requirement, not a nicety: that
// editor is expensive to build and holds unsaved edits, so it must not mount
// before someone asks for it and must not unmount when they fold it away. The
// children therefore mount on FIRST open and then stay, hidden with `hidden` +
// `display: none`. This replaces PrintPanel's hand-rolled `settingsEverOpened`.
import { type ReactNode, useId, useState } from 'react'
import { EYEBROW_RULED, STUDIO } from './theme'
export interface DisclosureProps {
label: string
children: ReactNode
defaultOpen?: boolean
/** controlled mode — when set, `open` wins and `onOpenChange` is the only writer */
open?: boolean
onOpenChange?: (open: boolean) => void
/** mount the children on first open and keep them mounted (hidden) after */
keepMounted?: boolean
dataElement?: string
dataAction?: string
}
export function Disclosure({
label,
children,
defaultOpen = false,
open: openProp,
onOpenChange,
keepMounted = false,
dataElement = 'disclosure',
dataAction = 'toggle-disclosure',
}: DisclosureProps) {
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen)
const controlled = openProp !== undefined
const open = controlled ? openProp : uncontrolledOpen
// `keepMounted` only has to remember that it has been open once; after that
// the panel is in the tree for good.
const [everOpened, setEverOpened] = useState(defaultOpen)
const panelId = useId()
const toggle = () => {
const next = !open
if (!controlled) setUncontrolledOpen(next)
if (next) setEverOpened(true)
onOpenChange?.(next)
}
const mounted = open || (keepMounted && everOpened)
return (
<div
data-component="disclosure"
data-element={dataElement}
style={{ display: 'flex', flexDirection: 'column', gap: STUDIO.space.section }}
>
<button
type="button"
data-action={dataAction}
aria-expanded={open}
aria-controls={mounted ? panelId : undefined}
onClick={toggle}
style={{
...EYEBROW_RULED,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
border: 'none',
borderBottom: EYEBROW_RULED.borderBottom,
background: 'transparent',
cursor: 'pointer',
textAlign: 'left',
}}
>
<span>{label}</span>
<span aria-hidden="true" style={{ color: STUDIO.color.muted }}>
{open ? '▾' : '▸'}
</span>
</button>
{mounted && (
<div
id={panelId}
hidden={!open}
style={{
display: open ? 'flex' : 'none',
flexDirection: 'column',
gap: STUDIO.space.section,
}}
>
{children}
</div>
)}
</div>
)
}
|