Skip to content

Commit bd7089a

Browse files
feat(sdui-demos): add node-panel debug collapsible to CollapsibleDemo
Matches the Node array panel behaviour present in StreamingOutput and AdaptiveOutput: opens automatically when the stream starts, auto-closes after 8s, and can be toggled manually. Panel open state resets on Regenerate (seed change). Includes the node-panel CSS in each framework's appropriate scope (shadow styles for Lit, shared StreamingOutput.css import for React, non-scoped style block for Vue).
1 parent 45be637 commit bd7089a

File tree

3 files changed

+202
-10
lines changed

3 files changed

+202
-10
lines changed

v2/sdui/demo-lit/src/components/CollapsibleDemo.ts

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
import { LitElement, html, css } from 'lit';
1+
import { LitElement, html, css, nothing } from 'lit';
22
import { property, state } from 'lit/decorators.js';
33
import '@agnosticui/render-lit';
4+
import 'agnosticui-core/collapsible';
5+
import type { CollapsibleToggleEvent } from 'agnosticui-core/collapsible';
46
import type { AgNode } from '@agnosticui/schema';
57
import { collapsibleFixture } from '../../../demo/src/fixtures/collapsible-demo';
68
import { streamFixture } from '../../../demo/src/lib/stream';
79

10+
const PANEL_AUTO_CLOSE_MS = 8000;
11+
812
export class CollapsibleDemo extends LitElement {
913
static styles = css`
1014
:host { display: block; }
@@ -14,11 +18,72 @@ export class CollapsibleDemo extends LitElement {
1418
flex-direction: column;
1519
gap: var(--ag-space-4, 1rem);
1620
}
21+
22+
ag-collapsible.node-panel {
23+
display: block;
24+
border: 1px solid var(--ag-border, #e5e7eb);
25+
border-radius: 6px;
26+
overflow: hidden;
27+
margin-block-end: 1rem;
28+
}
29+
30+
ag-collapsible.node-panel::part(ag-collapsible-summary) {
31+
padding: 0.4rem 0.75rem;
32+
background: var(--ag-background-secondary, #f3f4f6);
33+
font-size: 0.7rem;
34+
font-weight: 600;
35+
text-transform: uppercase;
36+
letter-spacing: 0.08em;
37+
color: var(--ag-text-muted, #666);
38+
font-family: monospace;
39+
}
40+
41+
ag-collapsible.node-panel::part(ag-collapsible-indicator) {
42+
color: var(--ag-text-muted, #666);
43+
}
44+
45+
ag-collapsible.node-panel::part(ag-collapsible-content) {
46+
padding: 0;
47+
border-top: 1px solid var(--ag-border, #e5e7eb);
48+
max-height: 320px;
49+
overflow-y: auto;
50+
}
51+
52+
.node-panel-pre {
53+
margin: 0;
54+
padding: 0.75rem;
55+
font-family: monospace;
56+
font-size: 0.72rem;
57+
line-height: 1.5;
58+
color: var(--ag-text-primary, #111);
59+
background: var(--ag-background-primary, #fff);
60+
white-space: pre-wrap;
61+
word-break: break-all;
62+
}
1763
`;
1864

1965
@property({ type: Number }) seed = 0;
2066
@state() private nodes: AgNode[] = [];
67+
@state() private panelOpen = false;
2168
private cancelStream: (() => void) | null = null;
69+
private fadeTimer: ReturnType<typeof setTimeout> | null = null;
70+
71+
private openPanel() {
72+
this.panelOpen = true;
73+
if (this.fadeTimer) clearTimeout(this.fadeTimer);
74+
this.fadeTimer = setTimeout(() => { this.panelOpen = false; }, PANEL_AUTO_CLOSE_MS);
75+
}
76+
77+
private _onCollapsibleToggle(e: CollapsibleToggleEvent) {
78+
const nowOpen = e.detail.open;
79+
this.panelOpen = nowOpen;
80+
if (nowOpen) {
81+
if (this.fadeTimer) clearTimeout(this.fadeTimer);
82+
this.fadeTimer = setTimeout(() => { this.panelOpen = false; }, PANEL_AUTO_CLOSE_MS);
83+
} else {
84+
if (this.fadeTimer) clearTimeout(this.fadeTimer);
85+
}
86+
}
2287

2388
private actions = {
2489
COLLAPSIBLE_TOGGLE: (payload: unknown) => {
@@ -51,22 +116,35 @@ export class CollapsibleDemo extends LitElement {
51116

52117
override connectedCallback() {
53118
super.connectedCallback();
119+
this.openPanel();
54120
this.runStream();
55121
}
56122

57123
override updated(changed: Map<string, unknown>) {
58124
if (changed.has('seed') && changed.get('seed') !== undefined) {
125+
this.openPanel();
59126
this.runStream();
60127
}
61128
}
62129

63130
override disconnectedCallback() {
64131
super.disconnectedCallback();
65132
if (this.cancelStream) this.cancelStream();
133+
if (this.fadeTimer) clearTimeout(this.fadeTimer);
66134
}
67135

68136
render() {
69-
return html`<ag-dynamic-renderer .nodes=${this.nodes} .actions=${this.actions}></ag-dynamic-renderer>`;
137+
return html`
138+
<ag-collapsible
139+
class="node-panel"
140+
.open=${this.panelOpen}
141+
.onToggle=${(e: CollapsibleToggleEvent) => this._onCollapsibleToggle(e)}
142+
>
143+
<span slot="summary">Node array</span>
144+
<pre class="node-panel-pre">${JSON.stringify(this.nodes, null, 2)}</pre>
145+
</ag-collapsible>
146+
<ag-dynamic-renderer .nodes=${this.nodes} .actions=${this.actions}></ag-dynamic-renderer>
147+
`;
70148
}
71149
}
72150

v2/sdui/demo-vue/src/components/CollapsibleDemo.vue

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,50 @@
22
import { ref, onMounted, onUnmounted } from 'vue';
33
import type { AgNode } from '@agnosticui/schema';
44
import { AgDynamicRenderer } from '@agnosticui/render-vue';
5+
import { VueCollapsible } from 'agnosticui-core/collapsible/vue';
6+
import type { CollapsibleToggleEvent } from 'agnosticui-core/collapsible';
57
import { collapsibleFixture } from '../../../demo/src/fixtures/collapsible-demo';
68
import { streamFixture } from '../../../demo/src/lib/stream';
79
10+
const PANEL_AUTO_CLOSE_MS = 8000;
11+
812
const nodes = ref<AgNode[]>([]);
9-
let cancelled = false;
13+
const panelOpen = ref(false);
14+
let fadeTimer: ReturnType<typeof setTimeout> | null = null;
15+
let cancelCurrentStream: () => void = () => {};
16+
17+
function openPanel() {
18+
panelOpen.value = true;
19+
if (fadeTimer) clearTimeout(fadeTimer);
20+
fadeTimer = setTimeout(() => { panelOpen.value = false; }, PANEL_AUTO_CLOSE_MS);
21+
}
22+
23+
function handleCollapsibleToggle(e: CollapsibleToggleEvent) {
24+
const nowOpen = e.detail.open;
25+
panelOpen.value = nowOpen;
26+
if (nowOpen) {
27+
if (fadeTimer) clearTimeout(fadeTimer);
28+
fadeTimer = setTimeout(() => { panelOpen.value = false; }, PANEL_AUTO_CLOSE_MS);
29+
} else {
30+
if (fadeTimer) clearTimeout(fadeTimer);
31+
}
32+
}
1033
1134
async function runStream(fixture: AgNode[]) {
12-
cancelled = false;
35+
cancelCurrentStream();
36+
let cancelled = false;
37+
cancelCurrentStream = () => { cancelled = true; };
1338
nodes.value = [];
1439
for await (const node of streamFixture(fixture)) {
1540
if (cancelled) break;
1641
nodes.value = [...nodes.value, node];
1742
}
1843
}
1944
20-
onMounted(() => runStream(collapsibleFixture));
21-
onUnmounted(() => { cancelled = true; });
45+
onMounted(() => { openPanel(); runStream(collapsibleFixture); });
46+
onUnmounted(() => { cancelCurrentStream(); if (fadeTimer) clearTimeout(fadeTimer); });
2247
2348
const actions = {
24-
// When a collapsible toggles, open it and close all others (accordion behavior).
2549
COLLAPSIBLE_TOGGLE: (payload: unknown) => {
2650
const { id, value } = payload as { id: string; value: boolean };
2751
nodes.value = nodes.value.map(n => {
@@ -41,5 +65,57 @@ const actions = {
4165
</script>
4266

4367
<template>
68+
<VueCollapsible
69+
class="node-panel"
70+
:open="panelOpen"
71+
:onToggle="handleCollapsibleToggle"
72+
>
73+
<template #summary>Node array</template>
74+
<pre class="node-panel-pre">{{ JSON.stringify(nodes, null, 2) }}</pre>
75+
</VueCollapsible>
4476
<AgDynamicRenderer :nodes="nodes" :actions="actions" />
4577
</template>
78+
79+
<style>
80+
ag-collapsible.node-panel {
81+
display: block;
82+
border: 1px solid var(--ag-border, #e5e7eb);
83+
border-radius: 6px;
84+
overflow: hidden;
85+
margin-block-end: 1rem;
86+
}
87+
88+
ag-collapsible.node-panel::part(ag-collapsible-summary) {
89+
padding: 0.4rem 0.75rem;
90+
background: var(--ag-background-secondary, #f3f4f6);
91+
font-size: 0.7rem;
92+
font-weight: 600;
93+
text-transform: uppercase;
94+
letter-spacing: 0.08em;
95+
color: var(--ag-text-muted, #666);
96+
font-family: monospace;
97+
}
98+
99+
ag-collapsible.node-panel::part(ag-collapsible-indicator) {
100+
color: var(--ag-text-muted, #666);
101+
}
102+
103+
ag-collapsible.node-panel::part(ag-collapsible-content) {
104+
padding: 0;
105+
border-top: 1px solid var(--ag-border, #e5e7eb);
106+
max-height: 320px;
107+
overflow-y: auto;
108+
}
109+
110+
.node-panel-pre {
111+
margin: 0;
112+
padding: 0.75rem;
113+
font-family: monospace;
114+
font-size: 0.72rem;
115+
line-height: 1.5;
116+
color: var(--ag-text-primary, #111);
117+
background: var(--ag-background-primary, #fff);
118+
white-space: pre-wrap;
119+
word-break: break-all;
120+
}
121+
</style>

v2/sdui/demo/src/components/CollapsibleDemo.tsx

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,36 @@
11
import { useState, useEffect, useRef, useCallback } from 'react';
22
import type { AgNode } from '@agnosticui/schema';
33
import { AgDynamicRenderer } from '@agnosticui/render-react';
4+
import { ReactCollapsible } from 'agnosticui-core/collapsible/react';
5+
import type { CollapsibleToggleEvent } from 'agnosticui-core/collapsible';
46
import { collapsibleFixture } from '../fixtures/collapsible-demo';
57
import { streamFixture } from '../lib/stream';
8+
import './StreamingOutput.css';
9+
10+
const PANEL_AUTO_CLOSE_MS = 8000;
611

712
export function CollapsibleDemo() {
813
const [nodes, setNodes] = useState<AgNode[]>([]);
14+
const [panelOpen, setPanelOpen] = useState(false);
915
const cancelRef = useRef<() => void>(() => {});
16+
const fadeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
17+
18+
const openPanel = useCallback(() => {
19+
setPanelOpen(true);
20+
if (fadeTimerRef.current) clearTimeout(fadeTimerRef.current);
21+
fadeTimerRef.current = setTimeout(() => setPanelOpen(false), PANEL_AUTO_CLOSE_MS);
22+
}, []);
23+
24+
const handleCollapsibleToggle = useCallback((e: CollapsibleToggleEvent) => {
25+
const nowOpen = e.detail.open;
26+
setPanelOpen(nowOpen);
27+
if (nowOpen) {
28+
if (fadeTimerRef.current) clearTimeout(fadeTimerRef.current);
29+
fadeTimerRef.current = setTimeout(() => setPanelOpen(false), PANEL_AUTO_CLOSE_MS);
30+
} else {
31+
if (fadeTimerRef.current) clearTimeout(fadeTimerRef.current);
32+
}
33+
}, []);
1034

1135
const runStream = useCallback(async (fixture: AgNode[]) => {
1236
cancelRef.current();
@@ -20,12 +44,14 @@ export function CollapsibleDemo() {
2044
}, []);
2145

2246
useEffect(() => {
47+
openPanel();
2348
runStream(collapsibleFixture);
2449
return () => cancelRef.current();
25-
}, [runStream]);
50+
}, [runStream, openPanel]);
51+
52+
useEffect(() => () => { if (fadeTimerRef.current) clearTimeout(fadeTimerRef.current); }, []);
2653

2754
const actions = {
28-
// When a collapsible toggles, open it and close all others (accordion behavior).
2955
COLLAPSIBLE_TOGGLE: (payload: unknown) => {
3056
const { id, value } = payload as { id: string; value: boolean };
3157
setNodes(prev =>
@@ -45,5 +71,17 @@ export function CollapsibleDemo() {
4571
},
4672
};
4773

48-
return <AgDynamicRenderer nodes={nodes} actions={actions} />;
74+
return (
75+
<>
76+
<ReactCollapsible
77+
className="node-panel"
78+
open={panelOpen}
79+
onToggle={handleCollapsibleToggle}
80+
>
81+
<span slot="summary">Node array</span>
82+
<pre className="node-panel-pre">{JSON.stringify(nodes, null, 2)}</pre>
83+
</ReactCollapsible>
84+
<AgDynamicRenderer nodes={nodes} actions={actions} />
85+
</>
86+
);
4987
}

0 commit comments

Comments
 (0)