Skip to content

Commit 4090706

Browse files
balzssclaude
andcommitted
feat(docs-app,ui-buttons): extend prop playground to all components
Auto-inject the PropEditor playground on every component doc page from `Document`, using the react-docgen metadata it already has (no runtime fetch). Simple standalone components get a single-element form; compound components get a curated composition playground that edits the parent and a representative child together, driven by a registry. - Generalize PropEditor to N sections plus a JSX template with `{{id}}` attribute placeholders; simple mode is one section with the default serializer. - Add a registry of custom composition playgrounds (Menu, Tabs, Table, Modal, Select, Flex, Grid, ... 17 total) plus a force-simple set for prop-driven components whose subcomponents are internal (Rating, Pagination, Calendar). - Gate: real compound components (dotted subcomponent ids) fall back to their README examples unless a custom entry exists; components whose only children are internal facades (Checkbox, FormField) still get the simple form. - Stabilize config identity and key the seed effect on a structural signature so composition edits are not wiped on every render. - Drop the now-redundant manual PropEditor embed from the Button v2 README. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent cb618ac commit 4090706

6 files changed

Lines changed: 786 additions & 66 deletions

File tree

packages/__docs__/src/Document/index.tsx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ import { Returns } from '../Returns'
4242
import { ComponentTheme } from '../ComponentTheme'
4343
import { TableOfContents } from '../TableOfContents'
4444
import { Heading } from '../Heading'
45+
import { PropEditor } from '../PropEditor'
46+
import type { PropEditorSection, ReactDocgenProps } from '../PropEditor/props'
47+
import {
48+
shouldAutoInject,
49+
getAutoInjectConfig,
50+
getCustomPlayground,
51+
isForceSimple
52+
} from '../PropEditor/registry'
4553

4654
import { AppContext } from '../appContext'
4755
import { navigateTo } from '../navigationUtils'
@@ -154,6 +162,84 @@ class Document extends Component<DocumentProps, DocumentState> {
154162
) : null
155163
}
156164

165+
renderPlayground(doc: DocDataType) {
166+
const editor = this.getPlaygroundEditor(doc)
167+
if (!editor) return null
168+
169+
return (
170+
<View margin="x-large 0" display="block">
171+
<Heading
172+
level="h2"
173+
as="h3"
174+
id={`${doc.id}Playground`}
175+
margin="0 0 small 0"
176+
>
177+
Playground
178+
</Heading>
179+
{editor}
180+
</View>
181+
)
182+
}
183+
184+
// Which playground (if any) a component gets:
185+
// - a curated composition, when a custom playground is registered;
186+
// - the auto single-element form, for simple standalone components;
187+
// - none, for compound components without a custom entry (they lean on
188+
// their README examples) and anything the gate rejects.
189+
getPlaygroundEditor(doc: DocDataType) {
190+
const custom = getCustomPlayground(doc.id)
191+
192+
if (custom) {
193+
const sections = custom.sections
194+
.map<PropEditorSection | null>((section) => {
195+
const sectionProps =
196+
section.id === doc.id
197+
? doc.props
198+
: doc.children?.find((child) => child.id === section.id)?.props
199+
return sectionProps
200+
? {
201+
id: section.id,
202+
label: section.label,
203+
props: sectionProps as ReactDocgenProps,
204+
config: section.config
205+
}
206+
: null
207+
})
208+
.filter((section): section is PropEditorSection => section !== null)
209+
210+
// Bail if any section's metadata is missing rather than render a partial,
211+
// broken composition.
212+
if (sections.length !== custom.sections.length) return null
213+
214+
return (
215+
<PropEditor
216+
componentId={doc.id}
217+
sections={sections}
218+
template={custom.template}
219+
/>
220+
)
221+
}
222+
223+
// Real compound components (whose children are composable subcomponents
224+
// with dotted ids like `Menu.Item`) fall back to examples when they have no
225+
// custom playground. Components whose only "children" are internal facades
226+
// (non-dotted ids, e.g. Checkbox's `CheckboxFacade`) are not composed by
227+
// consumers, so they still get the simple single-element form.
228+
const hasSubcomponents = doc.children?.some((child) =>
229+
child.id?.includes('.')
230+
)
231+
if (hasSubcomponents && !isForceSimple(doc.id)) return null
232+
if (!shouldAutoInject(doc)) return null
233+
234+
return (
235+
<PropEditor
236+
componentId={doc.id}
237+
props={doc.props as ReactDocgenProps}
238+
config={getAutoInjectConfig(doc.id)}
239+
/>
240+
)
241+
}
242+
157243
renderTheme(doc: DocDataType) {
158244
const { themeVariables } = this.props
159245
const { componentTheme } = this.state
@@ -467,6 +553,9 @@ import { ${importName} } from '${versionedPackageName}'`
467553
{pageRef && <TableOfContents doc={doc} pageElement={pageRef} />}
468554
{['.js', '.ts', '.tsx'].includes(doc.extension) && this.renderUsage()}
469555
{this.renderDescription(doc, this.props.description)}
556+
{/* Playground sits above the per-subcomponent details tabs so it always
557+
applies to the whole component, not the selected tab. */}
558+
{this.renderPlayground(doc)}
470559
{details}
471560
{this.renderEditOnGithub()}
472561
{repository && layout !== 'small' && (

0 commit comments

Comments
 (0)