|
1 | 1 | <script lang="ts"> |
2 | 2 | import { slide } from 'svelte/transition'; |
3 | | - import { onDestroy } from 'svelte'; |
| 3 | + import { onDestroy, untrack } from 'svelte'; |
4 | 4 | import { |
5 | 5 | renderDocstring, |
6 | 6 | transformDefinitionListsToTables, |
|
17 | 17 | docstring?: string | undefined; |
18 | 18 | // Pre-rendered HTML (display directly) |
19 | 19 | docstringHtml?: string | undefined; |
| 20 | + // When true, render the docs immediately and hide the toggle button. |
| 21 | + alwaysExpanded?: boolean; |
20 | 22 | } |
21 | 23 |
|
22 | | - let { docstring, docstringHtml }: Props = $props(); |
| 24 | + let { docstring, docstringHtml, alwaysExpanded = false }: Props = $props(); |
23 | 25 |
|
24 | | - let expanded = $state(false); |
| 26 | + // alwaysExpanded is a static prop in practice; capture once for the |
| 27 | + // initial expanded state. Subsequent changes are handled by the reset |
| 28 | + // effect below. |
| 29 | + let expanded = $state(untrack(() => alwaysExpanded)); |
25 | 30 | let renderedDocs = $state<string>(''); |
26 | 31 | let loading = $state(false); |
27 | 32 | let container: HTMLDivElement | undefined = $state(); |
28 | 33 |
|
29 | 34 | // Check if we have any documentation to show |
30 | 35 | const hasDocumentation = $derived(!!docstring || !!docstringHtml); |
31 | 36 |
|
| 37 | + // Generation counter: each loadDocs invocation gets a unique id; if a |
| 38 | + // newer load starts while an older one is still in flight, the older |
| 39 | + // resolution must not overwrite the newer's result. |
| 40 | + let loadGen = 0; |
| 41 | +
|
| 42 | + async function loadDocs() { |
| 43 | + if (renderedDocs) return; |
| 44 | + const html = docstringHtml || docstring; |
| 45 | + if (!html) return; |
| 46 | + const gen = ++loadGen; |
| 47 | + loading = true; |
| 48 | + try { |
| 49 | + const result = await renderDocstring(html); |
| 50 | + if (gen !== loadGen) return; // superseded by a newer load |
| 51 | + renderedDocs = result; |
| 52 | + } catch (e) { |
| 53 | + if (gen !== loadGen) return; |
| 54 | + console.error('Failed to render docstring:', e); |
| 55 | + renderedDocs = '<p class="docs-error">Failed to render documentation.</p>'; |
| 56 | + } |
| 57 | + if (gen === loadGen) loading = false; |
| 58 | + } |
| 59 | +
|
32 | 60 | async function toggle() { |
33 | 61 | expanded = !expanded; |
34 | | -
|
35 | | - // Load and render if expanding and not already loaded |
36 | | - if (expanded && !renderedDocs) { |
37 | | - const html = docstringHtml || docstring; |
38 | | - if (!html) return; |
39 | | -
|
40 | | - loading = true; |
41 | | - try { |
42 | | - // renderDocstring handles both raw docstrings and pre-rendered HTML |
43 | | - // It applies KaTeX rendering to any .math elements |
44 | | - renderedDocs = await renderDocstring(html); |
45 | | - } catch (e) { |
46 | | - console.error('Failed to render docstring:', e); |
47 | | - renderedDocs = '<p class="docs-error">Failed to render documentation.</p>'; |
48 | | - } |
49 | | - loading = false; |
50 | | - } |
| 62 | + if (expanded) await loadDocs(); |
51 | 63 | } |
52 | 64 |
|
53 | 65 | // Apply DOM transformations after rendered HTML is inserted |
|
69 | 81 | } |
70 | 82 | }); |
71 | 83 |
|
72 | | - // Reset state when docstring changes |
| 84 | + // Reset state when docstring changes. In alwaysExpanded mode the toggle |
| 85 | + // state stays true and we re-load the new content. The loadDocs call |
| 86 | + // must be untracked — it reads renderedDocs internally, and without |
| 87 | + // untrack the subsequent write to renderedDocs would re-trigger this |
| 88 | + // effect in an infinite loop. |
73 | 89 | $effect(() => { |
74 | | - // Track both props |
75 | 90 | const _ = docstring || docstringHtml; |
76 | 91 | if (_) { |
77 | | - // Reset when content changes |
78 | 92 | cleanupCodeBlocks(); |
79 | 93 | renderedDocs = ''; |
80 | | - expanded = false; |
| 94 | + if (alwaysExpanded) { |
| 95 | + untrack(() => loadDocs()); |
| 96 | + } else { |
| 97 | + expanded = false; |
| 98 | + } |
81 | 99 | } |
82 | 100 | }); |
83 | 101 |
|
|
92 | 110 | </svelte:head> |
93 | 111 |
|
94 | 112 | {#if hasDocumentation} |
95 | | - <div class="docs-section"> |
96 | | - <button class="docs-toggle" onclick={toggle}> |
97 | | - <span class="toggle-icon" class:expanded> |
98 | | - <Icon name="chevron-right" size={12} /> |
99 | | - </span> |
100 | | - Documentation |
101 | | - </button> |
| 113 | + <div class="docs-section" class:always-expanded={alwaysExpanded}> |
| 114 | + {#if !alwaysExpanded} |
| 115 | + <button class="docs-toggle" onclick={toggle}> |
| 116 | + <span class="toggle-icon" class:expanded> |
| 117 | + <Icon name="chevron-right" size={12} /> |
| 118 | + </span> |
| 119 | + Documentation |
| 120 | + </button> |
| 121 | + {/if} |
102 | 122 | {#if expanded} |
103 | | - <div class="docs-content" transition:slide={{ duration: 200 }} bind:this={container}> |
104 | | - {#if loading} |
105 | | - <div class="docs-loading">Loading documentation...</div> |
106 | | - {:else} |
107 | | - {@html renderedDocs} |
108 | | - {/if} |
109 | | - </div> |
| 123 | + {#if alwaysExpanded} |
| 124 | + <div class="docs-content" bind:this={container}> |
| 125 | + {#if loading} |
| 126 | + <div class="docs-loading">Loading documentation...</div> |
| 127 | + {:else} |
| 128 | + {@html renderedDocs} |
| 129 | + {/if} |
| 130 | + </div> |
| 131 | + {:else} |
| 132 | + <div class="docs-content" transition:slide={{ duration: 200 }} bind:this={container}> |
| 133 | + {#if loading} |
| 134 | + <div class="docs-loading">Loading documentation...</div> |
| 135 | + {:else} |
| 136 | + {@html renderedDocs} |
| 137 | + {/if} |
| 138 | + </div> |
| 139 | + {/if} |
110 | 140 | {/if} |
111 | 141 | </div> |
112 | 142 | {/if} |
|
122 | 152 | padding-right: var(--space-md); |
123 | 153 | } |
124 | 154 |
|
| 155 | + /* Always-expanded uses no border / no negative margins so it can sit |
| 156 | + * inside the detail column without bleeding past container edges. */ |
| 157 | + .docs-section.always-expanded { |
| 158 | + border-top: none; |
| 159 | + padding: 0; |
| 160 | + margin: 0; |
| 161 | + } |
| 162 | +
|
125 | 163 | .docs-toggle { |
126 | 164 | display: flex; |
127 | 165 | align-items: center; |
|
0 commit comments