From 30e6f37d973a9376fc116413f6c68cd723e7f8ae Mon Sep 17 00:00:00 2001 From: Omri Katz Date: Tue, 4 Aug 2026 12:22:19 +0300 Subject: [PATCH 1/2] The example embed loads like it means it: skeleton, error state, faster mount The module-backed on /docs used to sit as a bare bordered box reading "Loading example" for the whole mount, and a failed fixture load left that text on screen forever because the render was data ? playground : loading. Three fixes: A failed load now says so. The error card is a fumadocs Callout naming the example and what went wrong. Recovery is honest: a failed dynamic import is cached permanently in the browser module map, so refetch issues no request at all, and the affordance is a page reload, which was verified to recover. When the example is simply absent from the build there is no button, because nothing the reader can do would help. retry is off so the failure surfaces in about a second instead of after silent backoff. The loading state is now the editor it is about to become. The skeleton reuses the chrome classes the real surface exports, so it is the same frame, dots, header, file strip and split, with shimmer bars where the code will land. It measures 524px against the mounted 526px at 1440, and 891px against 891px at 390. The bars are the shadcn Skeleton primitive taken from the registry, and they stop moving under prefers-reduced-motion. The chunk and the fixture no longer load one after the other. The impl chunk and the fixture JSON are warmed on idle from the mount point LazyMount already owns, so the fixture stops waiting on the chunk that renders it. The skeleton keeps the dark github palette rather than the fd theme variables, because the mounted embed is that palette in both site themes and an fd-coloured placeholder would flash from light to dark on mount. Co-Authored-By: Claude Fable 5 --- .../napl-example/napl-example-impl.tsx | 40 +++++++++- .../napl-example/napl-example-skeleton.tsx | 73 +++++++++++++++++++ .../components/napl-example/napl-example.tsx | 35 ++++++--- apps/site/src/components/ui/skeleton.tsx | 9 +++ apps/site/src/lib/lazy-mount.tsx | 11 ++- 5 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 apps/site/src/components/napl-example/napl-example-skeleton.tsx create mode 100644 apps/site/src/components/ui/skeleton.tsx diff --git a/apps/site/src/components/napl-example/napl-example-impl.tsx b/apps/site/src/components/napl-example/napl-example-impl.tsx index 331c495d..5562d68e 100644 --- a/apps/site/src/components/napl-example/napl-example-impl.tsx +++ b/apps/site/src/components/napl-example/napl-example-impl.tsx @@ -1,8 +1,11 @@ -import {useMemo, type ReactElement} from 'react' +import {useMemo, type ReactElement, type ReactNode} from 'react' import {useQuery} from '@tanstack/react-query' +import {Callout} from 'fumadocs-ui/components/callout' import {loadShowcaseModule} from '@/lib/fixtures' import type {ShowcaseModule} from '@/lib/fixtures' import {NaplPlaygroundClient} from '@/components/playground/napl-playground-impl' +import {Button} from '@/components/ui/button' +import {NaplExampleSkeleton} from './napl-example-skeleton' export interface InlineFile { name: string @@ -47,6 +50,17 @@ const synthModule = (files: InlineFile[]): ShowcaseModule => { } } +function ExampleNotice({title, children}: {title: string; children: ReactNode}): ReactElement { + return ( + + {children} + + ) +} + +const isMissingFromBuild = (error: unknown): boolean => + error instanceof Error && error.message.startsWith('unknown fixture module:') + export function NaplExampleClient({ module: moduleName, files, @@ -63,11 +77,33 @@ export function NaplExampleClient({ }, enabled: Boolean(moduleName) && !inline && typeof window !== 'undefined', staleTime: Number.POSITIVE_INFINITY, + retry: false, }) const data = inline ?? query.data ?? null if (!data) { - return
Loading example…
+ if (query.isError) { + const missing = isMissingFromBuild(query.error) + return ( + + + {moduleName ? `The example "${moduleName}" could not be read. ` : 'The example could not be read. '} + {missing ? 'It is not part of this build.' : 'It did not finish downloading.'} + + {missing ? null : ( + + )} + + ) + } + if (!moduleName) { + return ( + Nothing was given for this example to show. + ) + } + return } return ( diff --git a/apps/site/src/components/napl-example/napl-example-skeleton.tsx b/apps/site/src/components/napl-example/napl-example-skeleton.tsx new file mode 100644 index 00000000..9c2384ff --- /dev/null +++ b/apps/site/src/components/napl-example/napl-example-skeleton.tsx @@ -0,0 +1,73 @@ +import '@napl-lang/editor/styles.css' +import {CHROME, DOT, DOT_AMBER, DOT_GREEN, DOT_RED, DOTS, EDITOR_PANE, PLAYGROUND} from '@napl-lang/editor' +import type {ReactElement} from 'react' +import {Skeleton} from '@/components/ui/skeleton' +import {PANE, PANE_DIVIDER, PANES, PANES_HOST, TAB_STRIP} from '@/components/playground/playground-classes' + +const BAR = 'bg-[var(--napl-border)] motion-reduce:animate-none' + +const PROMPT_LINES = ['w-[34%]', 'w-[72%]', 'w-[58%]', 'w-[66%]', 'w-[28%]', 'w-[70%]', 'w-[44%]', 'w-[61%]'] + +const GENERATED_LINES = ['w-[52%]', 'w-[36%]', 'w-[68%]', 'w-[45%]', 'w-[59%]'] + +const PROMPT_TABS = ['w-16', 'w-20'] + +const GENERATED_TABS = ['w-14', 'w-[68px]', 'w-[76px]'] + +function TabStripSkeleton({widths}: {widths: string[]}): ReactElement { + return ( + + ) +} + +function LinesSkeleton({widths, height}: {widths: string[]; height: string}): ReactElement { + return ( +
+ +
+ ) +} + +export function NaplExampleSkeleton(): ReactElement { + return ( +
+
+ + + + +
+
+
+
+ + +
+
+ + +
+
+
+
+ ) +} diff --git a/apps/site/src/components/napl-example/napl-example.tsx b/apps/site/src/components/napl-example/napl-example.tsx index 2571d651..f5fa944c 100644 --- a/apps/site/src/components/napl-example/napl-example.tsx +++ b/apps/site/src/components/napl-example/napl-example.tsx @@ -1,8 +1,27 @@ import {Suspense, lazy, type ReactElement, type ReactNode} from 'react' import {ClientOnly} from '@tanstack/react-router' import {LazyMount} from '@/lib/lazy-mount' +import {loadShowcaseModule} from '@/lib/fixtures' +import {NaplExampleSkeleton} from './napl-example-skeleton' import type {InlineFile} from './napl-example-impl' +const warmed = new Set() + +const warmNow = (moduleName: string | undefined): void => { + void import('./napl-example-impl').catch(() => undefined) + if (moduleName) void loadShowcaseModule(moduleName).catch(() => undefined) +} + +const warmOnIdle = (moduleName: string | undefined): void => { + if (typeof window === 'undefined') return + const key = moduleName ?? '' + if (warmed.has(key)) return + warmed.add(key) + const run = (): void => warmNow(moduleName) + if (typeof window.requestIdleCallback === 'function') window.requestIdleCallback(run) + else window.setTimeout(run, 200) +} + const NaplExampleClient = lazy(() => import('./napl-example-impl').then((module) => ({default: module.NaplExampleClient})), ) @@ -31,28 +50,21 @@ const resolveFiles = (props: NaplExampleProps): InlineFile[] | undefined => { return [{name: props.filename ?? 'example.napl', content: toText(raw)}] } -function ExamplePlaceholder(): ReactElement { - return ( -
-
Loading example…
-
- ) -} - export function NaplExample(props: NaplExampleProps): ReactElement { const {module, filename} = props const inlineFiles = resolveFiles(props) const fallbackSource = inlineFiles?.[0]?.content ?? '' const fallbackName = filename ?? inlineFiles?.[0]?.name ?? 'example.napl' + const warmKey = inlineFiles ? undefined : module const staticFallback = fallbackSource ? ( - }> - }> + }> + }> ) : ( - + ) return ( @@ -60,6 +72,7 @@ export function NaplExample(props: NaplExampleProps): ReactElement { className="block my-5" dataAttributes={{'data-napl-example': module ?? filename ?? 'snippet'}} fallback={staticFallback} + onAttach={() => warmOnIdle(warmKey)} > ): ReactElement { + return
+} + +export {Skeleton} diff --git a/apps/site/src/lib/lazy-mount.tsx b/apps/site/src/lib/lazy-mount.tsx index e603ce68..2eae0cff 100644 --- a/apps/site/src/lib/lazy-mount.tsx +++ b/apps/site/src/lib/lazy-mount.tsx @@ -8,8 +8,9 @@ interface VisibilityStore { getServerSnapshot: () => boolean } -function createVisibilityStore(rootMargin: string): VisibilityStore { +function createVisibilityStore(rootMargin: string, onAttach: (() => void) | undefined): VisibilityStore { let visible = false + let attached = false let observer: IntersectionObserver | null = null const listeners = new Set<() => void>() @@ -27,6 +28,10 @@ function createVisibilityStore(rootMargin: string): VisibilityStore { return { attach(node) { + if (node && !attached) { + attached = true + onAttach?.() + } if (visible) return observer?.disconnect() observer = null @@ -64,6 +69,7 @@ export interface LazyMountProps { rootMargin?: string className?: string dataAttributes?: Record + onAttach?: () => void } export function LazyMount({ @@ -72,8 +78,9 @@ export function LazyMount({ rootMargin = '240px', className, dataAttributes, + onAttach, }: LazyMountProps): ReactElement { - const store = useStable(() => createVisibilityStore(rootMargin)) + const store = useStable(() => createVisibilityStore(rootMargin, onAttach)) const visible = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot) return ( From 090fb10652ed7a56b922a51daa725bf3bc0205a7 Mon Sep 17 00:00:00 2001 From: Omri Katz Date: Tue, 4 Aug 2026 12:49:55 +0300 Subject: [PATCH 2/2] Split the example embed states into single-purpose pieces The audit gate flagged three findings introduced by the previous commit: NaplExampleClient at 11 cyclomatic, LazyMount's attach at 7, and warmOnIdle at 5. The effective bar is 4, because CRAP is cyclomatic squared plus cyclomatic against a threshold of 30. Decomposed rather than suppressed, with no behavior change: The four embed states are now their own components. ExamplePending picks between ExampleLoadError, ExampleMissingSource and the skeleton, ExampleLoadError owns the missing-versus-download wording and the reload button, and NaplExampleClient is left holding the query and the ready case at 2 cyclomatic. The query options, the inline synth, the data resolution and the playground flags moved to named helpers, which also removes the duplicate defaulting that had compact, readOnly and showGen resolved in both files. The visibility store grew stopObserving, startObserving and announceAttach, so attach reads as five statements and reveal and the subscribe cleanup stop repeating the disconnect pair. warmOnIdle hands scheduling to scheduleIdle and keys the warm set on the module directly, so the placeholder key is gone. The fallback tree in napl-example.tsx became ExampleFallback, and the name and title lookups became small functions instead of chained optional reads. Every touched function is now 4 cyclomatic or lower, CRAP 20 or lower. The audit passes with zero introduced findings. Geometry, copy, testids and both error paths were re-verified in the browser after the split: mounted 526px and 891px, skeleton 524px and 891px, reload still recovers, and the missing case still shows no button. The one remaining audit item is the unused @j178/prek devDependency, which is inherited rather than introduced, and is genuinely used: scripts/git-hooks invokes it as the pre-commit runner, so removing it would break the hooks. Co-Authored-By: Claude Fable 5 --- .../napl-example/napl-example-impl.tsx | 156 +++++++++++------- .../components/napl-example/napl-example.tsx | 80 ++++++--- apps/site/src/lib/lazy-mount.tsx | 55 +++--- 3 files changed, 184 insertions(+), 107 deletions(-) diff --git a/apps/site/src/components/napl-example/napl-example-impl.tsx b/apps/site/src/components/napl-example/napl-example-impl.tsx index 5562d68e..2379ef67 100644 --- a/apps/site/src/components/napl-example/napl-example-impl.tsx +++ b/apps/site/src/components/napl-example/napl-example-impl.tsx @@ -20,25 +20,35 @@ export interface NaplExampleImplProps { showGen?: boolean } +const isPromptFile = (file: InlineFile): boolean => file.name.endsWith('.napl') || file.name.endsWith('.🧑') + +const promptFile = (files: InlineFile[]): InlineFile | undefined => files.find(isPromptFile) ?? files[0] + +const promptName = (prompt: InlineFile | undefined): string => prompt?.name ?? 'example.napl' + +const promptContent = (prompt: InlineFile | undefined): string => prompt?.content ?? '' + +const generatedFile = (file: InlineFile): ShowcaseModule['files'][number] => ({ + path: file.name, + journalPath: file.name, + content: file.content, + language: 'text', +}) + const synthModule = (files: InlineFile[]): ShowcaseModule => { - const prompt = files.find((f) => f.name.endsWith('.napl') || f.name.endsWith('.🧑')) ?? files[0] - const promptName = prompt?.name ?? 'example.napl' - const promptContent = prompt?.content ?? '' - const generated = files.filter((f) => f !== prompt) + const prompt = promptFile(files) + const name = promptName(prompt) + const content = promptContent(prompt) + const generated = files.filter((file) => file !== prompt) return { - module: promptName.replace(/\.(napl|🧑)$/, ''), + module: name.replace(/\.(napl|🧑)$/, ''), collection: 'example', target: '', targets: [], - prompt: {file: promptName, path: promptName, content: promptContent}, + prompt: {file: name, path: name, content}, promptAtGen: null, summary: '', - files: generated.map((f) => ({ - path: f.name, - journalPath: f.name, - content: f.content, - language: 'text', - })), + files: generated.map(generatedFile), attribution: [], attributionYaml: '', mapl: [], @@ -46,10 +56,40 @@ const synthModule = (files: InlineFile[]): ShowcaseModule => { maplFile: '', journal: [], lock: [], - session: {task: '', files: {[promptName]: promptContent}, events: []}, + session: {task: '', files: {[name]: content}, events: []}, } } +const synthOrNull = (files: InlineFile[] | undefined): ShowcaseModule | null => (files ? synthModule(files) : null) + +const resolveData = (inline: ShowcaseModule | null, fetched: ShowcaseModule | undefined): ShowcaseModule | null => + inline ?? fetched ?? null + +const fetchShowcaseModule = (moduleName: string | undefined): Promise => { + if (!moduleName) throw new Error('missing module') + return loadShowcaseModule(moduleName) +} + +const showcaseModuleQuery = (moduleName: string | undefined, hasInline: boolean) => ({ + queryKey: ['showcase-module', moduleName], + queryFn: () => fetchShowcaseModule(moduleName), + enabled: Boolean(moduleName) && !hasInline && typeof window !== 'undefined', + staleTime: Number.POSITIVE_INFINITY, + retry: false, +}) + +const playgroundFlags = (props: NaplExampleImplProps): {compact: boolean; readOnly: boolean; showGen: boolean} => ({ + compact: props.compact ?? true, + readOnly: props.readOnly ?? false, + showGen: props.showGen ?? false, +}) + +const isMissingFromBuild = (error: unknown): boolean => + error instanceof Error && error.message.startsWith('unknown fixture module:') + +const readFailure = (moduleName: string | undefined): string => + moduleName ? `The example "${moduleName}" could not be read.` : 'The example could not be read.' + function ExampleNotice({title, children}: {title: string; children: ReactNode}): ReactElement { return ( @@ -58,60 +98,56 @@ function ExampleNotice({title, children}: {title: string; children: ReactNode}): ) } -const isMissingFromBuild = (error: unknown): boolean => - error instanceof Error && error.message.startsWith('unknown fixture module:') +function ReloadPageButton(): ReactElement { + return ( + + ) +} -export function NaplExampleClient({ - module: moduleName, - files, - compact = true, - readOnly = false, - showGen = false, -}: NaplExampleImplProps): ReactElement { - const inline = useMemo(() => (files ? synthModule(files) : null), [files]) - const query = useQuery({ - queryKey: ['showcase-module', moduleName], - queryFn: () => { - if (!moduleName) throw new Error('missing module') - return loadShowcaseModule(moduleName) - }, - enabled: Boolean(moduleName) && !inline && typeof window !== 'undefined', - staleTime: Number.POSITIVE_INFINITY, - retry: false, - }) - - const data = inline ?? query.data ?? null - if (!data) { - if (query.isError) { - const missing = isMissingFromBuild(query.error) - return ( - - - {moduleName ? `The example "${moduleName}" could not be read. ` : 'The example could not be read. '} - {missing ? 'It is not part of this build.' : 'It did not finish downloading.'} - - {missing ? null : ( - - )} - - ) - } - if (!moduleName) { - return ( - Nothing was given for this example to show. - ) - } - return - } +function ExampleLoadError({moduleName, error}: {moduleName: string | undefined; error: unknown}): ReactElement { + const missing = isMissingFromBuild(error) + return ( + + + {readFailure(moduleName)} {missing ? 'It is not part of this build.' : 'It did not finish downloading.'} + + {missing ? null : } + + ) +} + +function ExampleMissingSource(): ReactElement { + return Nothing was given for this example to show. +} + +interface ExamplePendingProps { + moduleName: string | undefined + isError: boolean + error: unknown +} + +function ExamplePending({moduleName, isError, error}: ExamplePendingProps): ReactElement { + if (isError) return + if (!moduleName) return + return +} + +export function NaplExampleClient(props: NaplExampleImplProps): ReactElement { + const {module: moduleName, files} = props + const inline = useMemo(() => synthOrNull(files), [files]) + const query = useQuery(showcaseModuleQuery(moduleName, Boolean(inline))) + const data = resolveData(inline, query.data) + + if (!data) return return (
- +
) } diff --git a/apps/site/src/components/napl-example/napl-example.tsx b/apps/site/src/components/napl-example/napl-example.tsx index f5fa944c..8b77aac7 100644 --- a/apps/site/src/components/napl-example/napl-example.tsx +++ b/apps/site/src/components/napl-example/napl-example.tsx @@ -5,21 +5,26 @@ import {loadShowcaseModule} from '@/lib/fixtures' import {NaplExampleSkeleton} from './napl-example-skeleton' import type {InlineFile} from './napl-example-impl' -const warmed = new Set() +const warmed = new Set() const warmNow = (moduleName: string | undefined): void => { void import('./napl-example-impl').catch(() => undefined) if (moduleName) void loadShowcaseModule(moduleName).catch(() => undefined) } +const scheduleIdle = (run: () => void): void => { + if (typeof window.requestIdleCallback === 'function') { + window.requestIdleCallback(run) + return + } + window.setTimeout(run, 200) +} + const warmOnIdle = (moduleName: string | undefined): void => { if (typeof window === 'undefined') return - const key = moduleName ?? '' - if (warmed.has(key)) return - warmed.add(key) - const run = (): void => warmNow(moduleName) - if (typeof window.requestIdleCallback === 'function') window.requestIdleCallback(run) - else window.setTimeout(run, 200) + if (warmed.has(moduleName)) return + warmed.add(moduleName) + scheduleIdle(() => warmNow(moduleName)) } const NaplExampleClient = lazy(() => @@ -43,44 +48,71 @@ export interface NaplExampleProps { const toText = (value: string): string => value.replace(/\n$/, '') +const rawSource = (props: NaplExampleProps): string | null => { + if (typeof props.code === 'string') return props.code + if (typeof props.children === 'string') return props.children + return null +} + const resolveFiles = (props: NaplExampleProps): InlineFile[] | undefined => { if (props.files) return props.files - const raw = typeof props.code === 'string' ? props.code : typeof props.children === 'string' ? props.children : null + const raw = rawSource(props) if (raw === null) return undefined return [{name: props.filename ?? 'example.napl', content: toText(raw)}] } -export function NaplExample(props: NaplExampleProps): ReactElement { - const {module, filename} = props - const inlineFiles = resolveFiles(props) - const fallbackSource = inlineFiles?.[0]?.content ?? '' - const fallbackName = filename ?? inlineFiles?.[0]?.name ?? 'example.napl' - const warmKey = inlineFiles ? undefined : module +const firstFile = (files: InlineFile[] | undefined): InlineFile | undefined => files?.[0] + +const fallbackTitle = (filename: string | undefined, files: InlineFile[] | undefined): string | undefined => + filename ?? firstFile(files)?.name + +const fallbackName = (filename: string | undefined, files: InlineFile[] | undefined): string => + fallbackTitle(filename, files) ?? 'example.napl' + +const exampleSlug = (module: string | undefined, filename: string | undefined): string => + module ?? filename ?? 'snippet' - const staticFallback = fallbackSource ? ( +interface ExampleFallbackProps { + files: InlineFile[] | undefined + filename: string | undefined +} + +function ExampleFallback({files, filename}: ExampleFallbackProps): ReactElement { + const source = firstFile(files)?.content ?? '' + if (!source) return + return ( }> }> - + - ) : ( - ) +} + +export function NaplExample(props: NaplExampleProps): ReactElement { + const {module, filename} = props + const inlineFiles = resolveFiles(props) + const warmKey = inlineFiles ? undefined : module + const fallback = return ( warmOnIdle(warmKey)} > - + diff --git a/apps/site/src/lib/lazy-mount.tsx b/apps/site/src/lib/lazy-mount.tsx index 2eae0cff..503ff7a3 100644 --- a/apps/site/src/lib/lazy-mount.tsx +++ b/apps/site/src/lib/lazy-mount.tsx @@ -8,6 +8,8 @@ interface VisibilityStore { getServerSnapshot: () => boolean } +const isIntersecting = (entry: IntersectionObserverEntry): boolean => entry.isIntersecting + function createVisibilityStore(rootMargin: string, onAttach: (() => void) | undefined): VisibilityStore { let visible = false let attached = false @@ -18,44 +20,51 @@ function createVisibilityStore(rootMargin: string, onAttach: (() => void) | unde for (const listener of listeners) listener() } + const stopObserving = (): void => { + observer?.disconnect() + observer = null + } + const reveal = (): void => { if (visible) return visible = true - observer?.disconnect() - observer = null + stopObserving() notify() } + const startObserving = (node: Element): void => { + if (typeof IntersectionObserver === 'undefined') { + reveal() + return + } + observer = new IntersectionObserver( + (entries) => { + if (entries.some(isIntersecting)) reveal() + }, + {rootMargin}, + ) + observer.observe(node) + } + + const announceAttach = (): void => { + if (attached) return + attached = true + onAttach?.() + } + return { attach(node) { - if (node && !attached) { - attached = true - onAttach?.() - } + if (node) announceAttach() if (visible) return - observer?.disconnect() - observer = null + stopObserving() if (!node) return - if (typeof IntersectionObserver === 'undefined') { - reveal() - return - } - observer = new IntersectionObserver( - (entries) => { - if (entries.some((entry) => entry.isIntersecting)) reveal() - }, - {rootMargin}, - ) - observer.observe(node) + startObserving(node) }, subscribe(onChange) { listeners.add(onChange) return () => { listeners.delete(onChange) - if (listeners.size === 0) { - observer?.disconnect() - observer = null - } + if (listeners.size === 0) stopObserving() } }, getSnapshot: () => visible,