Skip to content

Commit d8528a9

Browse files
bloveclaude
andauthored
feat(cockpit-render): redesign the other 5 render examples (encapsulated CSS, no Tailwind) (#761)
* docs: render examples redesign follow-up plan (5 siblings) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(cockpit-render): move json-highlight tokenizer to render/shared for reuse * feat(cockpit-render): redesign registry example with encapsulated CSS + syntax-colored JSON * feat(cockpit-render): redesign computed-functions example with encapsulated CSS + syntax-colored JSON * feat(cockpit-render): redesign element-rendering example (encapsulated CSS, JSON console, styled Show-Detail control) * feat(cockpit-render): redesign state-management example (encapsulated CSS, JSON console, styled state form) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(cockpit-render): redesign repeat-loops example (encapsulated CSS, JSON console, styled list controls) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a172d67 commit d8528a9

19 files changed

Lines changed: 1715 additions & 383 deletions

File tree

cockpit/render/computed-functions/angular/src/app/computed-functions.component.ts

Lines changed: 199 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,37 @@ import { StreamingSimulator } from '../../../../shared/streaming-simulator';
1111
import { StreamingTimelineComponent } from '../../../../shared/streaming-timeline.component';
1212
import { ExampleSplitLayoutComponent } from '@threadplane/example-layouts';
1313
import { 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
})
6574
class 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);
Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
11
@import "@threadplane/example-layouts/theme.css";
22

3-
html, body {
3+
html,
4+
body {
45
height: 100%;
56
margin: 0;
6-
background: var(--tplane-chat-bg);
7-
color: var(--tplane-chat-text);
8-
font-family: var(--tplane-chat-font-family);
7+
background: var(--ds-canvas, #111);
8+
color: var(--ds-text-primary, #f5f5f5);
9+
font-family: var(--ds-font-sans, system-ui, sans-serif);
910
}
1011

11-
@keyframes shimmer {
12-
0% { background-position: -200% 0; }
13-
100% { background-position: 200% 0; }
14-
}
15-
16-
.skeleton-shimmer {
12+
/* Shared skeleton loader for streamed-but-incomplete demo elements. */
13+
.sr-skeleton {
14+
border-radius: 5px;
1715
background: linear-gradient(
1816
90deg,
19-
transparent 0%,
20-
var(--tplane-chat-surface-alt) 50%,
21-
transparent 100%
17+
var(--ds-surface-tinted, #2c2c2c) 0%,
18+
rgba(90, 90, 90, 0.55) 50%,
19+
var(--ds-surface-tinted, #2c2c2c) 100%
2220
);
2321
background-size: 200% 100%;
24-
animation: shimmer 1.5s ease-in-out infinite;
22+
animation: sr-shimmer 1.5s ease-in-out infinite;
23+
}
24+
25+
@keyframes sr-shimmer {
26+
0% { background-position: 200% 0; }
27+
100% { background-position: -200% 0; }
2528
}

cockpit/render/computed-functions/angular/tsconfig.app.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
},
77
"files": ["src/main.ts"],
88
"include": ["src/**/*.d.ts", "src/**/*.ts"],
9+
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"],
910
"references": [
1011
{
1112
"path": "../../../../libs/cockpit-telemetry"

0 commit comments

Comments
 (0)