diff --git a/compiler/ds-codegen/src/js_emitter.rs b/compiler/ds-codegen/src/js_emitter.rs index 69539c1..73f0728 100644 --- a/compiler/ds-codegen/src/js_emitter.rs +++ b/compiler/ds-codegen/src/js_emitter.rs @@ -564,9 +564,14 @@ impl JsEmitter { // Layout props (x, y, width, height) or arbitrary style let js_val = self.emit_expr(val); // Check if this is a signal reference: emit_expr on signal `foo` - // returns `foo.value`, so also check with `.value` stripped + // returns `foo.value`, so also check with `.value` stripped. + // For dotted access like `game.value.p1y`, check if .value appears anywhere. let raw_name = js_val.strip_suffix(".value").unwrap_or(&js_val); - if graph.name_to_id.contains_key(raw_name) || graph.name_to_id.contains_key(&js_val) || self.is_signal_ref(&js_val) { + if graph.name_to_id.contains_key(raw_name) + || graph.name_to_id.contains_key(&js_val) + || self.is_signal_ref(&js_val) + || js_val.contains(".value") + { self.emit_line(&format!( "DS.effect(() => {{ {}.style.{} = {} + 'px'; }});", node_var, key, js_val diff --git a/examples/pong-viewer.ds b/examples/pong-viewer.ds new file mode 100644 index 0000000..af3280a --- /dev/null +++ b/examples/pong-viewer.ds @@ -0,0 +1,57 @@ +-- DreamStack Pong Viewer (Spectator) +-- Watches the pong game live via streaming relay. +-- Renders the full visual court from streamed signal state. +-- +-- Run with: +-- Tab 1: cargo run -p ds-stream (relay on :9100) +-- Tab 2: dreamstack dev examples/game-pong.ds (player) +-- Tab 3: dreamstack dev examples/pong-viewer.ds --port 3001 (viewer) + +import { Badge } from "../registry/components/badge" + +-- Connect to the pong game stream +let game = stream from "ws://localhost:9100/stream/pong" + +view viewer = column [ + text "👁️ Pong Spectator" { variant: "title" } + + -- Score bar + row [ + Badge { label: "LIVE 🔴", variant: "error" } + Badge { label: "P1: {game.score1}", variant: "info" } + Badge { label: "Rally: {game.rally}", variant: "warning" } + Badge { label: "AI: {game.score2}", variant: "error" } + ] + + -- ── Spectator Court ── + stack { style: "position:relative; width:600px; height:400px; background:linear-gradient(180deg,#0f172a,#1e293b); border:2px solid #334155; border-radius:16px; margin:0.5rem auto; overflow:hidden; box-shadow:0 0 40px rgba(99,102,241,0.15)" } [ + -- Center line + column { style: "position:absolute; left:298px; top:0; width:4px; height:400px; background:repeating-linear-gradient(to bottom, #475569 0px, #475569 12px, transparent 12px, transparent 24px)" } [] + + -- Center circle + column { style: "position:absolute; left:260px; top:160px; width:80px; height:80px; border:2px solid #475569; border-radius:50%" } [] + + -- P1 paddle (blue, left) + column { style: "position:absolute; left:8px; width:12px; height:80px; background:linear-gradient(180deg,#818cf8,#6366f1); border-radius:6px; box-shadow:0 0 12px rgba(129,140,248,0.5); transition:top 0.1s", top: game.p1y } [] + + -- P2/AI paddle (red, right) + column { style: "position:absolute; left:580px; width:12px; height:80px; background:linear-gradient(180deg,#f87171,#ef4444); border-radius:6px; box-shadow:0 0 12px rgba(248,113,113,0.5); transition:top 0.1s", top: game.p2y } [] + + -- Ball (yellow glow) + column { style: "position:absolute; width:14px; height:14px; background:radial-gradient(circle,#fde68a,#f59e0b); border-radius:50%; box-shadow:0 0 16px #fbbf24,0 0 4px #fbbf24", top: game.ballY, left: game.ballX } [] + + -- Score overlay on court + row { style: "position:absolute; top:12px; left:0; width:100%; justify-content:center; gap:140px; pointer-events:none" } [ + text "{game.score1}" { style: "font-size:56px; color:rgba(129,140,248,0.2); font-weight:900; font-family:monospace" } + text "{game.score2}" { style: "font-size:56px; color:rgba(248,113,113,0.2); font-weight:900; font-family:monospace" } + ] + + -- SPECTATING overlay + row { style: "position:absolute; bottom:12px; left:0; width:100%; justify-content:center; pointer-events:none" } [ + text "SPECTATING" { style: "font-size:12px; color:rgba(255,255,255,0.15); letter-spacing:4px; font-weight:700" } + ] + ] + + when game.paused -> text "⏸ Game is paused" { variant: "muted" } + text "View-only • no controls • game state received via relay" { variant: "muted" } +]