Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ff8d574
Add optional rightColumn snippet to ResizablePanel for two-column lay…
milanofthe May 6, 2026
3f9e43a
Add alwaysExpanded prop to DocumentationSection
milanofthe May 6, 2026
eac541e
Add NodeBlockDetail and EventDetail components for library hover preview
milanofthe May 6, 2026
422c09a
Wire NodeLibrary hover-detail state with open/close delays
milanofthe May 6, 2026
89b357c
Wire Block library detail-column expansion in main route
milanofthe May 6, 2026
4f0b5cd
Wire Events panel detail-column expansion with same hover pattern
milanofthe May 6, 2026
aec3531
Gate ResizablePanel right column on rightColumnActive flag
milanofthe May 6, 2026
b075ad9
Fix DocumentationSection alwaysExpanded reload loop and drop block-ty…
milanofthe May 6, 2026
2d88e60
Drop detail-header bar in library detail panels
milanofthe May 6, 2026
c608d96
Use section-title style header for library detail with inline toolbox…
milanofthe May 6, 2026
a3200cb
Match toolbox info typography to section title and put in parentheses
milanofthe May 6, 2026
60dd13f
Make library detail column wider via configurable rightColumnWidth
milanofthe May 6, 2026
525b428
Add switch delay so brushing past tiles doesn't flip detail content
milanofthe May 6, 2026
ffc821a
Add CanvasBlockPreview matching BaseNode visuals with port handles
milanofthe May 6, 2026
812c862
Use muted text color for block name in CanvasBlockPreview
milanofthe May 6, 2026
69fb1be
Add vertical padding around detail preview and separator before docs
milanofthe May 6, 2026
3fd078d
Use raw port array length for preview handles to match canvas behavior
milanofthe May 6, 2026
89530c1
Polish: timer cleanup on destroy, race-safe docstring loads, tour wor…
milanofthe May 6, 2026
38ab706
Clean up dead fit prop and missing state declaration
milanofthe May 6, 2026
c6dc26c
Move library footer into ResizablePanel footer slot to span both columns
milanofthe May 6, 2026
fc5097a
Mention hover detail panel in events tour copy
milanofthe May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 55 additions & 3 deletions src/lib/components/ResizablePanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@
toolbar?: import('svelte').Snippet;
footer?: import('svelte').Snippet;
children?: import('svelte').Snippet;
/** Optional second column rendered next to the main content. When set
* AND `rightColumnActive` is true, the panel body splits into two
* columns; otherwise the panel layout is unchanged. */
rightColumn?: import('svelte').Snippet;
/** Controls whether the right column is currently rendered. Lets the
* parent define the snippet up front but defer the layout split until
* there's something to show (so the column doesn't eat panel width
* while empty). Defaults to true when a `rightColumn` is supplied. */
rightColumnActive?: boolean;
/** Width of the right column in px. */
rightColumnWidth?: number;
}

let {
Expand All @@ -46,9 +57,14 @@
actions,
toolbar,
footer,
children
children,
rightColumn,
rightColumnActive = true,
rightColumnWidth = 320
}: Props = $props();

const showRightColumn = $derived(!!rightColumn && rightColumnActive);

// Calculate dynamic max height for bottom panels (viewport - nav bar - gaps)
function getEffectiveMaxHeight(): number {
if (maxHeight !== undefined) {
Expand Down Expand Up @@ -228,8 +244,15 @@
{@render toolbar()}
</div>
{/if}
<div class="panel-content">
{@render children?.()}
<div class="panel-body" class:split={showRightColumn}>
<div class="panel-content">
{@render children?.()}
</div>
{#if showRightColumn}
<div class="panel-right" style="width: {rightColumnWidth}px;">
{@render rightColumn!()}
</div>
{/if}
</div>
{#if footer}
<div class="panel-footer">
Expand Down Expand Up @@ -342,6 +365,20 @@
border-bottom: 1px solid var(--border);
}

/* When the panel has no second column, panel-body is layout-transparent,
* so children sit in resizable-panel's column flex exactly like before. */
.panel-body {
display: contents;
}

.panel-body.split {
display: flex;
flex-direction: row;
flex: 1;
min-height: 0;
overflow: hidden;
}

.panel-content {
display: flex;
flex-direction: column;
Expand All @@ -350,6 +387,21 @@
min-height: 0;
}

.panel-body.split .panel-content {
min-width: 0;
}

.panel-right {
flex-shrink: 0;
width: 320px;
min-width: 0;
display: flex;
flex-direction: column;
overflow: hidden;
border-left: 1px solid var(--border);
background: var(--surface);
}

.panel-footer {
flex-shrink: 0;
background: var(--surface-raised);
Expand Down
114 changes: 76 additions & 38 deletions src/lib/components/dialogs/shared/DocumentationSection.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { slide } from 'svelte/transition';
import { onDestroy } from 'svelte';
import { onDestroy, untrack } from 'svelte';
import {
renderDocstring,
transformDefinitionListsToTables,
Expand All @@ -17,37 +17,49 @@
docstring?: string | undefined;
// Pre-rendered HTML (display directly)
docstringHtml?: string | undefined;
// When true, render the docs immediately and hide the toggle button.
alwaysExpanded?: boolean;
}

let { docstring, docstringHtml }: Props = $props();
let { docstring, docstringHtml, alwaysExpanded = false }: Props = $props();

let expanded = $state(false);
// alwaysExpanded is a static prop in practice; capture once for the
// initial expanded state. Subsequent changes are handled by the reset
// effect below.
let expanded = $state(untrack(() => alwaysExpanded));
let renderedDocs = $state<string>('');
let loading = $state(false);
let container: HTMLDivElement | undefined = $state();

// Check if we have any documentation to show
const hasDocumentation = $derived(!!docstring || !!docstringHtml);

// Generation counter: each loadDocs invocation gets a unique id; if a
// newer load starts while an older one is still in flight, the older
// resolution must not overwrite the newer's result.
let loadGen = 0;

async function loadDocs() {
if (renderedDocs) return;
const html = docstringHtml || docstring;
if (!html) return;
const gen = ++loadGen;
loading = true;
try {
const result = await renderDocstring(html);
if (gen !== loadGen) return; // superseded by a newer load
renderedDocs = result;
} catch (e) {
if (gen !== loadGen) return;
console.error('Failed to render docstring:', e);
renderedDocs = '<p class="docs-error">Failed to render documentation.</p>';
}
if (gen === loadGen) loading = false;
}

async function toggle() {
expanded = !expanded;

// Load and render if expanding and not already loaded
if (expanded && !renderedDocs) {
const html = docstringHtml || docstring;
if (!html) return;

loading = true;
try {
// renderDocstring handles both raw docstrings and pre-rendered HTML
// It applies KaTeX rendering to any .math elements
renderedDocs = await renderDocstring(html);
} catch (e) {
console.error('Failed to render docstring:', e);
renderedDocs = '<p class="docs-error">Failed to render documentation.</p>';
}
loading = false;
}
if (expanded) await loadDocs();
}

// Apply DOM transformations after rendered HTML is inserted
Expand All @@ -69,15 +81,21 @@
}
});

// Reset state when docstring changes
// Reset state when docstring changes. In alwaysExpanded mode the toggle
// state stays true and we re-load the new content. The loadDocs call
// must be untracked — it reads renderedDocs internally, and without
// untrack the subsequent write to renderedDocs would re-trigger this
// effect in an infinite loop.
$effect(() => {
// Track both props
const _ = docstring || docstringHtml;
if (_) {
// Reset when content changes
cleanupCodeBlocks();
renderedDocs = '';
expanded = false;
if (alwaysExpanded) {
untrack(() => loadDocs());
} else {
expanded = false;
}
}
});

Expand All @@ -92,21 +110,33 @@
</svelte:head>

{#if hasDocumentation}
<div class="docs-section">
<button class="docs-toggle" onclick={toggle}>
<span class="toggle-icon" class:expanded>
<Icon name="chevron-right" size={12} />
</span>
Documentation
</button>
<div class="docs-section" class:always-expanded={alwaysExpanded}>
{#if !alwaysExpanded}
<button class="docs-toggle" onclick={toggle}>
<span class="toggle-icon" class:expanded>
<Icon name="chevron-right" size={12} />
</span>
Documentation
</button>
{/if}
{#if expanded}
<div class="docs-content" transition:slide={{ duration: 200 }} bind:this={container}>
{#if loading}
<div class="docs-loading">Loading documentation...</div>
{:else}
{@html renderedDocs}
{/if}
</div>
{#if alwaysExpanded}
<div class="docs-content" bind:this={container}>
{#if loading}
<div class="docs-loading">Loading documentation...</div>
{:else}
{@html renderedDocs}
{/if}
</div>
{:else}
<div class="docs-content" transition:slide={{ duration: 200 }} bind:this={container}>
{#if loading}
<div class="docs-loading">Loading documentation...</div>
{:else}
{@html renderedDocs}
{/if}
</div>
{/if}
{/if}
</div>
{/if}
Expand All @@ -122,6 +152,14 @@
padding-right: var(--space-md);
}

/* Always-expanded uses no border / no negative margins so it can sit
* inside the detail column without bleeding past container edges. */
.docs-section.always-expanded {
border-top: none;
padding: 0;
margin: 0;
}

.docs-toggle {
display: flex;
align-items: center;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/icons/BlockIcon.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
{:else if def.kind === 'surface'}
<IconSurface fn={def.fn} rows={def.rows} cols={def.cols} />
{:else if def.kind === 'math'}
<IconMath latex={def.latex} fit={def.fit} />
<IconMath latex={def.latex} />
{:else if def.kind === 'glyph'}
<IconGlyph text={def.text} size={def.size} />
{:else if def.kind === 'svg' && svgRaw}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/icons/blocks/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type IconDef =
| { kind: 'plot'; samples: () => Sample[]; xRange?: [number, number]; yRange?: [number, number]; axes?: AxesMode; markers?: boolean; decoration?: 'arrow-up' | 'arrow-down' }
| { kind: 'scope'; samples: () => Sample[]; samples2?: () => Sample[]; yRange?: [number, number]; gridX?: number; gridY?: number }
| { kind: 'surface'; fn?: (u: number, v: number) => number; rows?: number; cols?: number }
| { kind: 'math'; latex: string; fit?: number }
| { kind: 'math'; latex: string }
| { kind: 'glyph'; text: string; size?: number }
| { kind: 'svg'; name: string };

Expand Down
Loading
Loading