Skip to content

Commit 057f7ca

Browse files
committed
fix: separate app-level version loading from Go library loading overlay
Two distinct loading phases, clearly separated: 1. **App-level (example app):** Don't render Go until effectiveVersion is resolved. This avoids the useCdn=false → useCdn=true double-mount when effectiveVersion transitions from undefined to a real semver. Show a simple "Resolving package version…" placeholder instead. 2. **Go library (ExampleContent):** The LoadingOverlay with visible={!initState} covers the CDN metadata fetch phase. This is Go's own universal loading state — it works wherever Go is embedded, not just in this example app.
1 parent 7d635ce commit 057f7ca

File tree

1 file changed

+111
-87
lines changed

1 file changed

+111
-87
lines changed

example/src/main.tsx

Lines changed: 111 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ const StandaloneCodeBlock = ({
135135
declare global {
136136
interface ImportMeta {
137137
env: {
138-
EXAMPLES: string[];
138+
EXAMPLES: { name: string; version: string }[];
139139
SSG_PREVIEWS: Record<string, string>;
140140
};
141141
}
@@ -152,21 +152,25 @@ type PackagesSource = 'build' | 'fetching' | 'live' | 'error';
152152
function useExamplePackages() {
153153
// Initialize immediately from build-time data — zero latency on first render.
154154
// Build-time names like "vue-basic" must map back to their npm scope.
155+
// Each entry now carries the version that was pinned at build time so the
156+
// version dropdown is populated immediately (before the live npm fetch).
155157
const [packages, setPackages] = useState<NpmPackageInfo[]>(() =>
156-
(import.meta.env.EXAMPLES ?? ['hello-world']).map((name: string) => {
157-
const scopeConfig =
158-
EXAMPLE_SCOPES.find((s) => s.prefix && name.startsWith(s.prefix)) ??
159-
EXAMPLE_SCOPES[0];
160-
const rawName = scopeConfig.prefix
161-
? name.slice(scopeConfig.prefix.length)
162-
: name;
163-
return {
164-
name: `${scopeConfig.scope}${rawName}`,
165-
shortName: name,
166-
scope: scopeConfig,
167-
version: '',
168-
};
169-
}),
158+
(import.meta.env.EXAMPLES ?? [{ name: 'hello-world', version: '' }]).map(
159+
({ name, version }: { name: string; version: string }) => {
160+
const scopeConfig =
161+
EXAMPLE_SCOPES.find((s) => s.prefix && name.startsWith(s.prefix)) ??
162+
EXAMPLE_SCOPES[0];
163+
const rawName = scopeConfig.prefix
164+
? name.slice(scopeConfig.prefix.length)
165+
: name;
166+
return {
167+
name: `${scopeConfig.scope}${rawName}`,
168+
shortName: name,
169+
scope: scopeConfig,
170+
version,
171+
};
172+
},
173+
),
170174
);
171175
const [source, setSource] = useState<PackagesSource>('build');
172176

@@ -191,6 +195,7 @@ function usePackageVersions(packageName: string) {
191195
const [loading, setLoading] = useState(false);
192196
useEffect(() => {
193197
if (!packageName) return;
198+
setVersions([]);
194199
setLoading(true);
195200
fetchPackageVersions(packageName)
196201
.then(setVersions)
@@ -474,7 +479,7 @@ function ColumnResizer({
474479
const startW = widthRef.current!;
475480
const sign = reverse ? -1 : 1;
476481
const onPointerMove = (ev: PointerEvent) => {
477-
onWidthChange(Math.max(80, startW + (ev.clientX - startX) * sign));
482+
onWidthChange(Math.max(160, startW + (ev.clientX - startX) * sign));
478483
};
479484
const onPointerUp = () => {
480485
el.removeEventListener('pointermove', onPointerMove);
@@ -613,7 +618,13 @@ function App() {
613618
const [defaultFile, setDefaultFile] = useState(
614619
initial.file ?? ((initial.example ?? 'hello-world').startsWith('vue-') ? 'src/App.vue' : 'src/App.tsx'),
615620
);
616-
const [version, setVersion] = useState<string | undefined>(initial.version);
621+
const [version, setVersion] = useState<string | undefined>(
622+
initial.version ??
623+
((import.meta.env.EXAMPLES ?? []).find(
624+
(e: { name: string; version: string }) =>
625+
e.name === (initial.example ?? 'hello-world'),
626+
)?.version || undefined),
627+
);
617628
const [copied, setCopied] = useState(false);
618629
const [exampleSearch, setExampleSearch] = useState('');
619630
const [entrySearch, setEntrySearch] = useState('');
@@ -629,20 +640,30 @@ function App() {
629640
const currentPkg = examplePackages.find((p) => p.shortName === example);
630641
const currentPkgName = currentPkg?.name ?? `@lynx-example/${example}`;
631642

643+
// The version that was pinned when this site was built (from build-time EXAMPLES).
644+
const getBuildVersion = useCallback(
645+
(name: string) =>
646+
(import.meta.env.EXAMPLES ?? []).find(
647+
(e: { name: string; version: string }) => e.name === name,
648+
)?.version ?? '',
649+
[],
650+
);
651+
const buildVersion = getBuildVersion(example);
652+
632653
const { versions: packageVersions } = usePackageVersions(currentPkgName);
633654

634655
// Auto-select the newest concrete version once packageVersions loads.
635656
// Never pass the virtual 'latest' tag to jsdelivr; the data API requires real semver.
636657
const effectiveVersion =
637658
version ?? currentPkg?.version ?? packageVersions[0]?.version;
638659

639-
// Once packageVersions is available, pin `version` state to a real semver
640-
// so the dropdown shows a selected value and the URL gets a concrete version.
660+
// Fallback: if no build version is known for this example (e.g. a newly
661+
// published package not yet in the build), auto-select the newest live version.
641662
useEffect(() => {
642-
if (!version && packageVersions.length > 0) {
663+
if (!version && !buildVersion && packageVersions.length > 0) {
643664
setVersion(packageVersions[0].version);
644665
}
645-
}, [version, packageVersions]);
666+
}, [version, buildVersion, packageVersions]);
646667

647668
// Metadata & entry state
648669
const [metadata, setMetadata] = useState<Record<string, any> | null>(null);
@@ -661,15 +682,18 @@ function App() {
661682
const [metadataHtml, setMetadataHtml] = useState('');
662683

663684
// Resizable column widths
664-
const col1Ref = useRef(180);
665-
const col2Ref = useRef(180);
666-
const col4Ref = useRef(300);
667-
const [col1W, setCol1W] = useState(180);
668-
const [col2W, setCol2W] = useState(180);
669-
const [col4W, setCol4W] = useState(300);
685+
const col1Ref = useRef(220);
686+
const col2Ref = useRef(220);
687+
const col3Ref = useRef(220);
688+
const col4Ref = useRef(220);
689+
const [col1W, setCol1W] = useState(220);
690+
const [col2W, setCol2W] = useState(220);
691+
const [col3W, setCol3W] = useState(220);
692+
const [col4W, setCol4W] = useState(220);
670693

671694
const setCol1 = useCallback((w: number) => { col1Ref.current = w; setCol1W(w); }, []);
672695
const setCol2 = useCallback((w: number) => { col2Ref.current = w; setCol2W(w); }, []);
696+
const setCol3 = useCallback((w: number) => { col3Ref.current = w; setCol3W(w); }, []);
673697
const setCol4 = useCallback((w: number) => { col4Ref.current = w; setCol4W(w); }, []);
674698

675699
const jsxString = useMemo(
@@ -1050,7 +1074,7 @@ function App() {
10501074
data-active={example === name}
10511075
onClick={() => {
10521076
setExample(name);
1053-
setVersion(undefined);
1077+
setVersion(getBuildVersion(name) || undefined);
10541078
setDefaultFile(
10551079
source === 'vue' ? 'src/App.vue' : 'src/App.tsx',
10561080
);
@@ -1133,13 +1157,14 @@ function App() {
11331157
style={{
11341158
...panelLabelStyle,
11351159
padding: '0 4px',
1136-
marginBottom: 2,
1160+
marginBottom: 4,
11371161
display: 'flex',
11381162
alignItems: 'center',
1139-
gap: 6,
1163+
gap: 4,
1164+
flexWrap: 'wrap',
11401165
}}
11411166
>
1142-
<span>Entries</span>
1167+
<span style={{ flexShrink: 0 }}>Entries</span>
11431168
<input
11441169
type="text"
11451170
value={entrySearch}
@@ -1155,22 +1180,9 @@ function App() {
11551180
fontSize: 10,
11561181
fontFamily: 'inherit',
11571182
outline: 'none',
1158-
minWidth: 0,
1183+
minWidth: 40,
11591184
}}
11601185
/>
1161-
</div>
1162-
<div
1163-
style={{
1164-
padding: '0 4px',
1165-
marginBottom: 4,
1166-
display: 'flex',
1167-
alignItems: 'center',
1168-
gap: 4,
1169-
}}
1170-
>
1171-
<span style={{ fontSize: 10, color: 'var(--sb-text-dim)', flexShrink: 0 }}>
1172-
Version
1173-
</span>
11741186
<select
11751187
value={version ?? ''}
11761188
onChange={(e) => setVersion(e.target.value || undefined)}
@@ -1179,14 +1191,15 @@ function App() {
11791191
fontSize: 10,
11801192
padding: '1px 20px 1px 6px',
11811193
borderRadius: 4,
1194+
flexShrink: 0,
11821195
}}
11831196
>
11841197
{packageVersions.length === 0 && (
11851198
<option value="" disabled>Loading…</option>
11861199
)}
11871200
{packageVersions.map((v) => (
11881201
<option key={v.version} value={v.version}>
1189-
{v.version}
1202+
{v.version}{v.version === buildVersion ? ' (build)' : ''}
11901203
</option>
11911204
))}
11921205
</select>
@@ -1241,8 +1254,8 @@ function App() {
12411254
{/* Col 3: controls */}
12421255
<div
12431256
style={{
1244-
flex: '1 1 0',
1245-
minWidth: 120,
1257+
flex: `0 0 ${col3W}px`,
1258+
minWidth: 160,
12461259
padding: '10px 16px',
12471260
overflow: 'hidden',
12481261
display: 'grid',
@@ -1306,9 +1319,9 @@ function App() {
13061319
/>
13071320
</div>
13081321

1309-
<ColumnResizer widthRef={col4Ref} onWidthChange={setCol4} reverse />
1322+
<ColumnResizer widthRef={col3Ref} onWidthChange={setCol3} />
13101323

1311-
{/* Right: metadata JSON */}
1324+
{/* Col 4: metadata JSON */}
13121325
<div
13131326
style={{
13141327
flex: `0 0 ${col4W}px`,
@@ -1375,45 +1388,18 @@ function App() {
13751388
</div>
13761389

13771390
{/* ── Go component(s) — Desktop + Mobile ── */}
1391+
{/* App-level concern: don't render Go until we have a resolved CDN version.
1392+
This avoids the useCdn=false → useCdn=true double-mount when
1393+
effectiveVersion transitions from undefined to a real semver. */}
13781394
<main>
13791395
<PreviewErrorBoundary>
13801396
<GoConfigProvider config={goConfig}>
1381-
<div className="dual-view">
1382-
{/* Desktop */}
1383-
<div style={{ flex: '1 1 500px', minWidth: 0 }}>
1384-
<Go
1385-
key={`desktop-${example}-${selectedEntry}-${defaultTab}-${effectiveVersion}`}
1386-
example={example}
1387-
defaultFile={defaultFile}
1388-
defaultTab={defaultTab}
1389-
defaultEntryFile={defaultEntryFile || undefined}
1390-
entry={entryFilter || undefined}
1391-
highlight={highlight || undefined}
1392-
img={img || undefined}
1393-
schema={schema || undefined}
1394-
version={effectiveVersion}
1395-
/>
1396-
<div className="figure-caption">Desktop</div>
1397-
</div>
1398-
{/* Mobile — fixed 320×660 */}
1399-
<div
1400-
className="mobile-preview"
1401-
style={{
1402-
flex: '0 0 320px',
1403-
maxWidth: 320,
1404-
overflow: 'hidden',
1405-
containerType: 'inline-size' as any,
1406-
}}
1407-
>
1408-
<div
1409-
style={{
1410-
height: 660,
1411-
overflow: 'hidden',
1412-
borderRadius: 16,
1413-
}}
1414-
>
1397+
{effectiveVersion ? (
1398+
<div className="dual-view">
1399+
{/* Desktop */}
1400+
<div style={{ flex: '1 1 500px', minWidth: 0 }}>
14151401
<Go
1416-
key={`mobile-${example}-${selectedEntry}-${defaultTab}-${effectiveVersion}`}
1402+
key={`desktop-${example}-${selectedEntry}-${defaultTab}-${effectiveVersion}`}
14171403
example={example}
14181404
defaultFile={defaultFile}
14191405
defaultTab={defaultTab}
@@ -1424,10 +1410,48 @@ function App() {
14241410
schema={schema || undefined}
14251411
version={effectiveVersion}
14261412
/>
1413+
<div className="figure-caption">Desktop</div>
1414+
</div>
1415+
{/* Mobile — fixed 320×660 */}
1416+
<div
1417+
className="mobile-preview"
1418+
style={{
1419+
flex: '0 0 320px',
1420+
maxWidth: 320,
1421+
overflow: 'hidden',
1422+
containerType: 'inline-size' as any,
1423+
}}
1424+
>
1425+
<div
1426+
style={{
1427+
height: 660,
1428+
overflow: 'hidden',
1429+
borderRadius: 16,
1430+
}}
1431+
>
1432+
<Go
1433+
key={`mobile-${example}-${selectedEntry}-${defaultTab}-${effectiveVersion}`}
1434+
example={example}
1435+
defaultFile={defaultFile}
1436+
defaultTab={defaultTab}
1437+
defaultEntryFile={defaultEntryFile || undefined}
1438+
entry={entryFilter || undefined}
1439+
highlight={highlight || undefined}
1440+
img={img || undefined}
1441+
schema={schema || undefined}
1442+
version={effectiveVersion}
1443+
/>
1444+
</div>
1445+
<div className="figure-caption">Mobile (320 × 660)</div>
14271446
</div>
1428-
<div className="figure-caption">Mobile (320 × 660)</div>
14291447
</div>
1430-
</div>
1448+
) : (
1449+
<div className="dual-view" style={{ alignItems: 'center', justifyContent: 'center', minHeight: 400 }}>
1450+
<span style={{ color: 'var(--semi-color-text-2)', fontSize: 13 }}>
1451+
Resolving package version…
1452+
</span>
1453+
</div>
1454+
)}
14311455
</GoConfigProvider>
14321456
</PreviewErrorBoundary>
14331457
</main>

0 commit comments

Comments
 (0)