-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Expand file tree
/
Copy pathVisualExample.tsx
More file actions
180 lines (167 loc) · 5.28 KB
/
VisualExample.tsx
File metadata and controls
180 lines (167 loc) · 5.28 KB
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import {CodeOutput, Control, Output, VisualExampleClient} from './VisualExampleClient';
import {Files, getFiles} from './CodeBlock';
import json5 from 'json5';
import path from 'path';
import React, {ReactNode} from 'react';
import {renderHTMLfromMarkdown, TComponent, TInterface, TProperty, Type} from './types';
import {style} from '@react-spectrum/s2/style' with { type: 'macro' };
const exampleStyle = style({
backgroundColor: 'layer-1',
padding: {
default: 12,
lg: 24
},
marginTop: {
default: 20,
':is([data-example-switcher] *)': 0
},
borderRadius: 'xl',
display: 'grid',
gridTemplateAreas: {
default: [
'example',
'controls',
'files'
],
lg: {
layout: {
narrow: [
'example controls',
'files controls'
],
wide: [
'example controls',
'files files'
]
}
}
},
gridTemplateColumns: {
default: ['1fr'],
lg: ['1fr', 'auto']
},
gridTemplateRows: {
default: ['auto', 'auto', 'auto'],
lg: ['1fr', 'auto']
},
gap: {
default: 12,
lg: 24
},
width: 'full',
boxSizing: 'border-box'
});
const controlsStyle = style({
display: 'grid',
gridTemplateColumns: {
default: 'repeat(auto-fit, minmax(200px, 1fr))',
lg: ['1fr']
},
gridAutoFlow: 'dense',
gridAutoRows: 'min-content',
maxWidth: 'full',
// overflow: 'hidden',
gap: {
default: 12,
lg: 16
},
gridArea: 'controls'
});
export interface VisualExampleProps {
/** The component to render. */
component: any,
/** The TS docs for this component. */
docs: TComponent | TInterface,
links: any,
/** The props to display as controls. */
props: string[],
/** Component children slots that should have controls. */
slots?: {[slot: string]: boolean},
/** Initial values for the prop controls. */
initialProps?: {[prop: string]: any},
controlOptions?: {[prop: string]: any},
importSource?: string,
/** When provided, the source code for the listed filenames will be included as tabs. */
files?: string[],
type?: 'vanilla' | 'tailwind' | 's2',
code?: ReactNode,
wide?: boolean,
align?: 'center' | 'start' | 'end',
acceptOrientation?: boolean,
propsObject?: string
}
export interface PropControl extends Omit<TProperty, 'description'> {
description: ReactNode,
default: any,
valueType: ReactNode,
slots?: {[slot: string]: boolean},
options?: any
}
/**
* Displays a component example with controls for changing the props.
*/
export function VisualExample({component, docs, links, importSource, props, initialProps, controlOptions, files, code, wide, slots, align, acceptOrientation, type, propsObject}: VisualExampleProps) {
let componentProps = docs.type === 'interface' ? docs : docs.props;
if (componentProps?.type !== 'interface') {
return null;
}
// Filter down the list of controls from the TS docs to only the ones we want to display.
// This reduces the amount of data we need to send to the client.
let controls = Object.fromEntries(props.map(name => {
let prop = componentProps.properties[name];
if (prop.type === 'method') {
throw new Error('Unexpected method in props.');
}
// Resolve the value type if it is a type alias.
if (prop.value?.type === 'link' && links?.[prop.value.id]) {
let value = links[prop.value.id];
if (value?.type === 'alias') {
value = value.value;
}
prop = {...prop, value};
}
// Try to parse the default value from the JSDocs as JSON.
let defaultValue = prop.default ?? undefined;
if (typeof defaultValue === 'string') {
defaultValue = defaultValue.replace(/^['"](.+)['"].*$/, '"$1"');
try {
defaultValue = json5.parse(defaultValue);
} catch {
// ignore
}
}
let renderedProp: PropControl = {
...prop,
description: renderHTMLfromMarkdown(prop.description, {forceInline: true}),
default: defaultValue,
valueType: <Type type={prop.value} />,
slots: name === 'children' ? slots : undefined,
options: controlOptions?.[name]
};
return [name, renderedProp];
}));
if (!importSource && files) {
importSource = './' + path.basename(files[0], path.extname(files[0]));
}
let output = (
<CodeOutput
code={code}
files={files ? getFiles(files) : undefined}
type={type}
registryUrl={type === 's2' || docs.type !== 'component' ? undefined : `${process.env.REGISTRY_URL || 'http://localhost:8081'}/${type}/${docs.name}.json`} />
);
// Render the corresponding client component to make the controls interactive.
return (
<VisualExampleClient component={component} name={docs.name} importSource={importSource} controls={controls} initialProps={initialProps} propsObject={propsObject}>
<div role="group" aria-label="Example" className={exampleStyle({layout: files || wide ? 'wide' : 'narrow'})}>
<Output align={align} acceptOrientation={acceptOrientation} />
<div role="group" aria-label="Controls" className={controlsStyle}>
{Object.keys(controls).map(control => <Control key={control} name={control} />)}
</div>
<div style={{gridArea: 'files', overflow: 'hidden'}}>
{files ? <Files files={files}>{output}</Files> : output}
</div>
</div>
</VisualExampleClient>
);
}