@@ -11,23 +11,37 @@ import { StreamingSimulator } from '../../../../shared/streaming-simulator';
1111import { StreamingTimelineComponent } from '../../../../shared/streaming-timeline.component' ;
1212import { ExampleSplitLayoutComponent } from '@threadplane/example-layouts' ;
1313import { COMPUTED_FUNCTIONS_SPECS } from './specs' ;
14+ import { highlightJson } from '../../../../shared/json-highlight' ;
1415
15- // --- Inline view components ---
16+ // --- Inline view components registered in the demo registry ---
1617
1718@Component ( {
1819 selector : 'demo-value' ,
1920 standalone : true ,
21+ styles : `
22+ .row { display: flex; align-items: center; gap: 0.5rem; padding: 4px 0; }
23+ .row__label {
24+ min-width: 120px;
25+ font-size: 11px;
26+ font-weight: 600;
27+ text-transform: uppercase;
28+ color: var(--ds-text-muted, #a0a0a0);
29+ }
30+ .row__value {
31+ font-family: var(--ds-font-mono, ui-monospace, monospace);
32+ font-size: 13px;
33+ color: var(--ds-text-primary, #f5f5f5);
34+ }
35+ ` ,
2036 template : `
2137 @if (label() || value()) {
22- <div class="flex items-center gap-2 py-1 ">
23- <span class="text-[var(--tplane-chat-text-muted)] text-xs uppercase font-semibold min-w-[120px] ">{{ label() }}:</span>
24- <span class="text-[var(--tplane-chat-text)] text-sm font-mono ">{{ value() }}</span>
38+ <div class="row ">
39+ <span class="row__label ">{{ label() }}:</span>
40+ <span class="row__value ">{{ value() }}</span>
2541 </div>
2642 } @else if (loading()) {
27- <div class="space-y-1.5 py-1">
28- <div class="h-3 w-full bg-[var(--tplane-chat-surface-alt)] rounded skeleton-shimmer"></div>
29- <div class="h-3 w-2/3 bg-[var(--tplane-chat-surface-alt)] rounded skeleton-shimmer"></div>
30- </div>
43+ <div class="sr-skeleton" style="height: 11px; width: 100%; margin: 3px 0;"></div>
44+ <div class="sr-skeleton" style="height: 11px; width: 66%; margin: 3px 0;"></div>
3145 }
3246 ` ,
3347} )
@@ -45,21 +59,16 @@ class DemoValueComponent {
4559 selector : 'demo-heading' ,
4660 standalone : true ,
4761 imports : [ RenderElementComponent ] ,
62+ styles : `.h { margin: 0 0 0.5rem; font-size: 1.05rem; font-weight: 700; color: var(--ds-text-primary, #f5f5f5); }` ,
4863 template : `
4964 @if (displayContent()) {
50- <h2 class="text-lg font-bold text-[var(--tplane-chat-text)] mb-2 ">{{ displayContent() }}</h2>
65+ <h2 class="h ">{{ displayContent() }}</h2>
5166 } @else if (loading()) {
52- <div class="h-5 w-48 bg-[var(--tplane-chat-surface-alt)] rounded skeleton-shimmer mb-2 "></div>
67+ <div class="sr-skeleton" style="height: 18px; width: 12rem; margin-bottom: 0.5rem; "></div>
5368 }
5469 @for (key of childKeys(); track key) {
5570 <render-element [elementKey]="key" [spec]="spec()!" />
5671 }
57- @if (!childKeys().length && loading()) {
58- <div class="space-y-2 mt-2">
59- <div class="h-3 w-full bg-[var(--tplane-chat-surface-alt)] rounded skeleton-shimmer"></div>
60- <div class="h-3 w-5/6 bg-[var(--tplane-chat-surface-alt)] rounded skeleton-shimmer"></div>
61- </div>
62- }
6372 ` ,
6473} )
6574class DemoHeadingComponent {
@@ -79,23 +88,36 @@ class DemoHeadingComponent {
7988 selector : 'demo-card' ,
8089 standalone : true ,
8190 imports : [ RenderElementComponent ] ,
91+ styles : `
92+ .card {
93+ margin-bottom: 0.75rem;
94+ padding: 16px;
95+ background: var(--ds-surface, #1c1c1c);
96+ border: 1px solid var(--ds-border, #2d2d2d);
97+ border-radius: var(--ds-radius-lg, 14px);
98+ box-shadow: var(--ds-shadow-md, 0 4px 6px -1px rgba(0, 0, 0, 0.3));
99+ }
100+ .card__title {
101+ margin: 0 0 0.625rem;
102+ font-size: 13px;
103+ font-weight: 600;
104+ color: var(--ds-text-primary, #f5f5f5);
105+ }
106+ ` ,
82107 template : `
83- <div class="rounded-lg border border-[var(--tplane-chat-separator)] bg-[var(--tplane-chat-surface-alt)] p-4 mb-3 transition-opacity"
84- [class.animate-pulse]="loading() && !childKeys().length">
108+ <div class="card">
85109 @if (title()) {
86- <h3 class="text-sm font-semibold text-[var(--tplane-chat-text)] mb-2 ">{{ title() }}</h3 >
110+ <div class="card__title ">{{ title() }}</div >
87111 } @else if (loading()) {
88- <div class="h-4 w-32 bg-[var(--tplane-chat-surface-alt)] rounded skeleton-shimmer mb-2 "></div>
112+ <div class="sr-skeleton" style="height: 14px; width: 8rem; margin-bottom: 0.625rem; "></div>
89113 }
90114 @if (childKeys().length) {
91115 @for (key of childKeys(); track key) {
92116 <render-element [elementKey]="key" [spec]="spec()!" />
93117 }
94118 } @else if (loading()) {
95- <div class="space-y-2">
96- <div class="h-3 w-full bg-[var(--tplane-chat-surface-alt)] rounded skeleton-shimmer"></div>
97- <div class="h-3 w-3/4 bg-[var(--tplane-chat-surface-alt)] rounded skeleton-shimmer"></div>
98- </div>
119+ <div class="sr-skeleton" style="height: 11px; width: 100%; margin: 4px 0;"></div>
120+ <div class="sr-skeleton" style="height: 11px; width: 75%; margin: 4px 0;"></div>
99121 }
100122 </div>
101123 ` ,
@@ -113,46 +135,160 @@ class DemoCardComponent {
113135 selector : 'app-computed-functions' ,
114136 standalone : true ,
115137 imports : [ RenderSpecComponent , StreamingTimelineComponent , ExampleSplitLayoutComponent ] ,
138+ styles : `
139+ .bar { display: flex; align-items: center; gap: 0.875rem; padding: 0.75rem 1rem; }
140+ .bar__lbl {
141+ font-size: 10px;
142+ font-weight: 700;
143+ text-transform: uppercase;
144+ letter-spacing: 0.12em;
145+ color: var(--ds-text-muted, #a0a0a0);
146+ }
147+ .tabs {
148+ display: flex;
149+ gap: 4px;
150+ padding: 4px;
151+ border-radius: 999px;
152+ background: var(--ds-surface-dim, #0a0a0a);
153+ border: 1px solid var(--ds-border, #2d2d2d);
154+ }
155+ .tab {
156+ padding: 6px 14px;
157+ font-size: 12px;
158+ border: 0;
159+ border-radius: 999px;
160+ background: transparent;
161+ color: var(--ds-text-muted, #a0a0a0);
162+ cursor: pointer;
163+ transition: color 0.15s ease, background 0.15s ease;
164+ }
165+ .tab:hover { color: var(--ds-text-secondary, #c8c8c8); }
166+ .tab--on {
167+ background: var(--ds-render-green, #1a7a40);
168+ color: #eafff2;
169+ font-weight: 600;
170+ box-shadow: 0 1px 8px rgba(26, 122, 64, 0.5);
171+ }
172+ .status {
173+ margin-left: auto;
174+ display: flex;
175+ align-items: center;
176+ gap: 7px;
177+ font-size: 11px;
178+ color: var(--ds-text-muted, #a0a0a0);
179+ }
180+ .status__dot {
181+ width: 8px;
182+ height: 8px;
183+ border-radius: 50%;
184+ background: #35b06a;
185+ box-shadow: 0 0 0 4px rgba(53, 176, 106, 0.13);
186+ }
187+ .status__dot--live { animation: sr-pulse 1.6s ease-in-out infinite; }
188+ @keyframes sr-pulse { 50% { opacity: 0.4; } }
189+ .cap {
190+ margin-bottom: 1rem;
191+ font-size: 10px;
192+ font-weight: 700;
193+ text-transform: uppercase;
194+ letter-spacing: 0.14em;
195+ color: var(--ds-text-muted, #a0a0a0);
196+ }
197+ .placeholder { font-size: 13px; font-style: italic; color: var(--ds-text-muted, #a0a0a0); }
198+ /* The JSON viewer is a code console: pin it to a dark surface with a
199+ fixed palette in BOTH themes so the syntax colors stay legible. */
200+ .json-pane {
201+ --code-fg: rgb(200, 200, 200);
202+ --code-muted: rgb(150, 150, 150);
203+ --code-border: rgba(255, 255, 255, 0.09);
204+ display: flex;
205+ flex-direction: column;
206+ flex: 1;
207+ min-height: 0;
208+ padding: 1rem;
209+ background: #0b0b0b;
210+ }
211+ .json-pane .cap { color: var(--code-muted); }
212+ .json {
213+ flex: 1;
214+ min-height: 0;
215+ overflow-y: auto;
216+ margin: 0;
217+ font-family: var(--ds-font-mono, ui-monospace, monospace);
218+ font-size: 11.5px;
219+ line-height: 1.7;
220+ white-space: pre-wrap;
221+ word-break: break-all;
222+ color: var(--code-fg);
223+ }
224+ .json .j-key { color: #7fe0a3; }
225+ .json .j-string { color: #c9c17f; }
226+ .json .j-number { color: #e6b673; }
227+ .json .j-literal { color: #7fd6ff; }
228+ .json .j-punct { color: var(--code-muted); }
229+ .json__cursor {
230+ display: inline-block;
231+ width: 7px;
232+ height: 14px;
233+ vertical-align: -2px;
234+ background: #35b06a;
235+ animation: sr-blink 1s step-end infinite;
236+ }
237+ @keyframes sr-blink { 50% { opacity: 0; } }
238+ .json__foot {
239+ display: flex;
240+ justify-content: space-between;
241+ margin-top: 0.75rem;
242+ padding-top: 0.625rem;
243+ font-size: 10.5px;
244+ border-top: 1px solid var(--code-border);
245+ }
246+ .json__foot .state { color: #35b06a; font-weight: 600; }
247+ .json__foot .state--idle { color: var(--code-muted); font-weight: 400; }
248+ .json__foot .pct { color: var(--code-muted); font-variant-numeric: tabular-nums; }
249+ ` ,
116250 template : `
117251 <example-split-layout>
118- <!-- Spec picker -->
119- <div header class="flex items-center gap-2 px-4 py-3">
120- <span class="text-xs text-[var(--tplane-chat-text-muted)] uppercase tracking-wide font-semibold mr-2">Spec:</span>
121- @for (spec of specs; track spec.label; let i = $index) {
122- <button
123- class="text-xs px-3 py-1.5 rounded-md transition-colors"
124- [class]="i === activeIndex ? 'bg-[var(--tplane-chat-primary)] text-[var(--tplane-chat-on-primary)] font-semibold' : 'bg-[var(--tplane-chat-surface-alt)] text-[var(--tplane-chat-text-muted)] hover:text-[var(--tplane-chat-text)]'"
125- (click)="selectSpec(i)">
126- {{ spec.label }}
127- </button>
128- }
252+ <!-- Spec picker + streaming status -->
253+ <div header class="bar">
254+ <span class="bar__lbl">Spec</span>
255+ <div class="tabs">
256+ @for (spec of specs; track spec.label; let i = $index) {
257+ <button type="button" class="tab" [class.tab--on]="i === activeIndex" (click)="selectSpec(i)">
258+ {{ spec.label }}
259+ </button>
260+ }
261+ </div>
262+ <span class="status">
263+ <span class="status__dot" [class.status__dot--live]="simulator.playing()"></span>
264+ {{ statusLabel() }}
265+ </span>
129266 </div>
130267
131- <!-- Left: Live Render Output -->
268+ <!-- Left: live render output -->
132269 <div primary>
133- <div class="text-[10px] text-[var(--tplane-chat-text-muted)] uppercase tracking-widest font-semibold mb-4 ">Live Render Output</div>
270+ <div class="cap ">Live Render Output</div>
134271 @if (simulator.spec(); as renderedSpec) {
135272 <render-spec [spec]="renderedSpec" [registry]="registry" [store]="store" [loading]="simulator.playing()" />
136273 } @else {
137- <div class="text-[var(--tplane-chat-text-muted)] text-sm italic ">Press play to start streaming... </div>
274+ <div class="placeholder ">Press play to start streaming… </div>
138275 }
139276 </div>
140277
141- <!-- Right: JSON + Controls -->
142- <div secondary class="flex flex-col h-full">
143- <!-- Streaming JSON (scrollable) -->
144- <div class="flex-1 overflow-y-auto p-4" #jsonPane>
145- <div class="text-[10px] text-[var(--tplane-chat-text-muted)] uppercase tracking-widest font-semibold mb-4">Streaming JSON</div>
146- <pre class="text-[11px] font-mono text-[var(--tplane-chat-text)] leading-relaxed whitespace-pre-wrap break-all">{{ simulator.rawJson() }}<span class="text-[var(--tplane-chat-primary)] animate-pulse">|</span></pre>
147- <div class="mt-3 flex justify-between text-[10px]">
148- <span class="text-[var(--tplane-chat-primary)]">{{ simulator.playing() ? 'Streaming...' : simulator.position() >= simulator.total() ? 'Complete' : 'Paused' }}</span>
149- <span class="text-[var(--tplane-chat-text-muted)]">{{ percent() }}%</span>
278+ <!-- Right: syntax-colored streaming JSON -->
279+ <div secondary>
280+ <div class="json-pane">
281+ <div class="cap">Streaming JSON</div>
282+ <pre class="json" #jsonScroll>@for (tok of jsonTokens(); track $index) {<span [class]="'j-' + tok.kind">{{ tok.text }}</span>}<span class="json__cursor"></span></pre>
283+ <div class="json__foot">
284+ <span class="state" [class.state--idle]="!simulator.playing()">{{ streamStateLabel() }}</span>
285+ <span class="pct">{{ percent() }}%</span>
150286 </div>
151287 </div>
152288 </div>
153289
154- <!-- Timeline bar -->
155- <streaming-timeline footer [simulator]="simulator" class="border-t border-[var(--tplane-chat-separator)]" />
290+ <!-- Transport -->
291+ <streaming-timeline footer [simulator]="simulator" />
156292 </example-split-layout>
157293 ` ,
158294} )
@@ -162,12 +298,14 @@ export class ComputedFunctionsComponent implements OnDestroy {
162298
163299 protected readonly simulator = new StreamingSimulator ( this . specs [ 0 ] . json ) ;
164300
165- private readonly jsonPane = viewChild < ElementRef < HTMLElement > > ( 'jsonPane' ) ;
301+ protected readonly jsonTokens = computed ( ( ) => highlightJson ( this . simulator . rawJson ( ) ) ) ;
302+
303+ private readonly jsonScroll = viewChild < ElementRef < HTMLElement > > ( 'jsonScroll' ) ;
166304
167305 constructor ( ) {
168306 effect ( ( ) => {
169307 this . simulator . rawJson ( ) ;
170- const el = this . jsonPane ( ) ?. nativeElement ;
308+ const el = this . jsonScroll ( ) ?. nativeElement ;
171309 if ( el ) {
172310 requestAnimationFrame ( ( ) => {
173311 el . scrollTop = el . scrollHeight ;
@@ -188,6 +326,16 @@ export class ComputedFunctionsComponent implements OnDestroy {
188326 return Math . round ( this . simulator . progress ( ) * 100 ) ;
189327 }
190328
329+ protected statusLabel ( ) : string {
330+ if ( this . simulator . playing ( ) ) return 'Streaming' ;
331+ return this . simulator . position ( ) >= this . simulator . total ( ) ? 'Complete' : 'Paused' ;
332+ }
333+
334+ protected streamStateLabel ( ) : string {
335+ if ( this . simulator . playing ( ) ) return 'Streaming…' ;
336+ return this . simulator . position ( ) >= this . simulator . total ( ) ? 'Complete' : 'Paused' ;
337+ }
338+
191339 protected selectSpec ( index : number ) : void {
192340 this . activeIndex = index ;
193341 this . simulator . setSource ( this . specs [ index ] . json ) ;
0 commit comments