From e177cc77582e9f99fbba84b39333eefe35651258 Mon Sep 17 00:00:00 2001
From: "cubecraft-agents[bot]"
<3458173+cubecraft-agents[bot]@users.noreply.github.com>
Date: Sun, 26 Apr 2026 03:59:39 +0000
Subject: [PATCH] CUB-61: add BreakroomSceneComponent with 16-bit office layout
- Created BreakroomSceneComponent with CSS Grid layout
- Left side: dev breakroom + 3 desks
- Right side: business breakroom + 3 desks (mirrored)
- Center divider with neon glow effect
- Static background SVG with desks, couches, TVs, banana bowls
- Integration with MinionComponent from CUB-60
- Minions positioned at desks or breakroom based on state
- Updated BreakroomPageComponent to use scene layout
- Collapsible controls panel for demo/testing
- Touch-optimized with 48px minimum targets
- Responsive breakpoints for tablet and mobile
- Accessibility: reduced motion, high contrast support
---
.gitignore | 1 +
frontend/public/scene/breakroom.svg | 273 +++++++++
.../breakroom-scene.component.html | 224 ++++++++
.../breakroom-scene.component.scss | 536 ++++++++++++++++++
.../breakroom-scene.component.ts | 115 ++++
.../breakroom/breakroom-page.component.html | 194 +++----
.../breakroom/breakroom-page.component.scss | 184 +++---
.../breakroom/breakroom-page.component.ts | 23 +-
8 files changed, 1296 insertions(+), 254 deletions(-)
create mode 100644 .gitignore
create mode 100644 frontend/public/scene/breakroom.svg
create mode 100644 frontend/src/app/components/breakroom-scene/breakroom-scene.component.html
create mode 100644 frontend/src/app/components/breakroom-scene/breakroom-scene.component.scss
create mode 100644 frontend/src/app/components/breakroom-scene/breakroom-scene.component.ts
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..21df648
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+backend/
diff --git a/frontend/public/scene/breakroom.svg b/frontend/public/scene/breakroom.svg
new file mode 100644
index 0000000..c61c133
--- /dev/null
+++ b/frontend/public/scene/breakroom.svg
@@ -0,0 +1,273 @@
+
\ No newline at end of file
diff --git a/frontend/src/app/components/breakroom-scene/breakroom-scene.component.html b/frontend/src/app/components/breakroom-scene/breakroom-scene.component.html
new file mode 100644
index 0000000..88a4ab9
--- /dev/null
+++ b/frontend/src/app/components/breakroom-scene/breakroom-scene.component.html
@@ -0,0 +1,224 @@
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+ @for (deskIndex of [0, 1, 2]; track deskIndex) {
+
+
+
Desk {{ deskIndex + 1 }}
+
+
+ @if (!getDeskOccupant('dev', deskIndex)) {
+
+ 💻
+ Empty
+
+ }
+
+
+ @if (getDeskOccupant('dev', deskIndex); as occupant) {
+
+ }
+
+ }
+
+
+
+
+
+
+
+
+
+ 🛋️
+ Couch
+
+
+ 📺
+ TV
+
+
+ 🍌
+ Bananas
+
+
+
+
+
+ @for (minion of devBreakroomMinions(); track minion.agentName) {
+
+ }
+
+
+ @for (minion of devReturningMinions(); track minion.agentName) {
+
+ }
+
+ @if (devBreakroomMinions().length === 0 && devReturningMinions().length === 0) {
+
+ No minions on break 🍌
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+ @for (deskIndex of [0, 1, 2]; track deskIndex) {
+
+
+
Desk {{ deskIndex + 1 }}
+
+
+ @if (!getDeskOccupant('business', deskIndex)) {
+
+ 💻
+ Empty
+
+ }
+
+
+ @if (getDeskOccupant('business', deskIndex); as occupant) {
+
+ }
+
+ }
+
+
+
+
+
+
+
+
+
+ 🛋️
+ Couch
+
+
+ 📺
+ TV
+
+
+ 🍌
+ Bananas
+
+
+
+
+
+ @for (minion of businessBreakroomMinions(); track minion.agentName) {
+
+ }
+
+
+ @for (minion of businessReturningMinions(); track minion.agentName) {
+
+ }
+
+ @if (businessBreakroomMinions().length === 0 && businessReturningMinions().length === 0) {
+
+ No minions on break 🍌
+
+ }
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/app/components/breakroom-scene/breakroom-scene.component.scss b/frontend/src/app/components/breakroom-scene/breakroom-scene.component.scss
new file mode 100644
index 0000000..b2b6c45
--- /dev/null
+++ b/frontend/src/app/components/breakroom-scene/breakroom-scene.component.scss
@@ -0,0 +1,536 @@
+// ============================================================================
+// Breakroom Scene Styles — 16-bit Office Scene Layout
+// Per CUB-61: CSS Grid layout, 1920×1080 viewport, touch-optimized
+// ============================================================================
+
+// ---------------------------------------------------------------------------
+// Layout Constants
+// ---------------------------------------------------------------------------
+$scene-width: 1920px;
+$scene-height: 1080px;
+$divider-width: 4px;
+$desk-count: 3;
+$pixel: 4px;
+
+// ---------------------------------------------------------------------------
+// Colors — Tactical Dark + Retro Accents
+// ---------------------------------------------------------------------------
+$scene-bg: #0D0F12;
+$scene-surface: #13161A;
+$scene-container: #1C2027;
+$scene-container-high: #252B33;
+$scene-outline: #2D3748;
+$scene-on-surface: #E2E8F0;
+$scene-on-surface-variant: #8A9BB0;
+
+$dev-accent: #38BDF8;
+$dev-accent-dim: rgba(56, 189, 248, 0.15);
+$business-accent: #2DD4BF;
+$business-accent-dim: rgba(45, 212, 191, 0.15);
+
+$desk-surface: #1E2430;
+$desk-border: #2D3748;
+$desk-hover: #252B33;
+$breakroom-bg: rgba(45, 212, 191, 0.04);
+
+// ---------------------------------------------------------------------------
+// Animations
+// ---------------------------------------------------------------------------
+@keyframes dividerPulse {
+ 0%, 100% { opacity: 0.6; }
+ 50% { opacity: 1; }
+}
+
+@keyframes emptyDeskPulse {
+ 0%, 100% { opacity: 0.4; }
+ 50% { opacity: 0.7; }
+}
+
+@keyframes slideInFromBottom {
+ from {
+ opacity: 0;
+ transform: translateY(12px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Scene Root — Full viewport container
+// ---------------------------------------------------------------------------
+.scene {
+ position: relative;
+ width: 100%;
+ max-width: $scene-width;
+ height: 100%;
+ min-height: 600px;
+ aspect-ratio: 16 / 9;
+ display: grid;
+ grid-template-columns: 1fr $divider-width 1fr;
+ grid-template-rows: 1fr;
+ overflow: hidden;
+ background-color: $scene-bg;
+ border-radius: var(--cc-card-border-radius, 16px);
+ image-rendering: pixelated;
+}
+
+// ---------------------------------------------------------------------------
+// Background Image Layer
+// ---------------------------------------------------------------------------
+.scene__background {
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ pointer-events: none;
+ overflow: hidden;
+}
+
+.scene__bg-image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ opacity: 0.35;
+}
+
+// ---------------------------------------------------------------------------
+// Center Divider — Glowing neon line
+// ---------------------------------------------------------------------------
+.scene__divider {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: $divider-width;
+ height: 100%;
+ z-index: 10;
+ pointer-events: none;
+}
+
+.scene__divider-glow {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 24px;
+ height: 100%;
+ background: linear-gradient(
+ 180deg,
+ rgba(56, 189, 248, 0.08) 0%,
+ rgba(56, 189, 248, 0.25) 20%,
+ rgba(56, 189, 248, 0.25) 80%,
+ rgba(56, 189, 248, 0.08) 100%
+ );
+ filter: blur(6px);
+ animation: dividerPulse 4s ease-in-out infinite;
+}
+
+.scene__divider-line {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: $divider-width;
+ height: 100%;
+ background: linear-gradient(
+ 180deg,
+ rgba(56, 189, 248, 0.3) 0%,
+ rgba(56, 189, 248, 0.8) 15%,
+ rgba(56, 189, 248, 0.8) 85%,
+ rgba(56, 189, 248, 0.3) 100%
+ );
+ box-shadow:
+ 0 0 8px rgba(56, 189, 248, 0.6),
+ 0 0 16px rgba(56, 189, 248, 0.3);
+}
+
+// ---------------------------------------------------------------------------
+// Side Sections — Left (Dev) & Right (Business)
+// ---------------------------------------------------------------------------
+.scene__side {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ flex-direction: column;
+ padding: 24px;
+ gap: 20px;
+ overflow-y: auto;
+
+ &--dev {
+ grid-column: 1;
+ background: linear-gradient(
+ 135deg,
+ rgba(56, 189, 248, 0.03) 0%,
+ transparent 60%
+ );
+ }
+
+ &--business {
+ grid-column: 3;
+ background: linear-gradient(
+ 135deg,
+ rgba(45, 212, 191, 0.03) 0%,
+ transparent 60%
+ );
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Side Header
+// ---------------------------------------------------------------------------
+.scene__side-header {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ padding-bottom: 12px;
+ border-bottom: 1px solid $scene-outline;
+}
+
+.scene__side-title {
+ font-family: 'Inter', 'Roboto', sans-serif;
+ font-size: 24px;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ margin: 0;
+
+ &--dev {
+ color: $dev-accent;
+ text-shadow: 0 0 12px rgba(56, 189, 248, 0.4);
+ }
+
+ &--business {
+ color: $business-accent;
+ text-shadow: 0 0 12px rgba(45, 212, 191, 0.4);
+ }
+}
+
+.scene__side-subtitle {
+ font-size: 12px;
+ color: $scene-on-surface-variant;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+}
+
+// ---------------------------------------------------------------------------
+// Desks Area — 3-column grid
+// ---------------------------------------------------------------------------
+.scene__desks {
+ display: grid;
+ grid-template-columns: repeat($desk-count, 1fr);
+ gap: 16px;
+ flex-shrink: 0;
+}
+
+// ---------------------------------------------------------------------------
+// Individual Desk
+// ---------------------------------------------------------------------------
+.scene__desk {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 180px;
+ background-color: $desk-surface;
+ border: 2px solid $desk-border;
+ border-radius: 12px;
+ padding: 12px;
+ transition: border-color 0.2s ease, background-color 0.2s ease;
+
+ &--occupied {
+ border-color: $dev-accent;
+ background-color: rgba(56, 189, 248, 0.06);
+ box-shadow: 0 0 12px rgba(56, 189, 248, 0.1);
+ }
+
+ &--empty {
+ border-style: dashed;
+ }
+}
+
+// Business side desk accent
+.scene__side--business .scene__desk--occupied {
+ border-color: $business-accent;
+ background-color: rgba(45, 212, 191, 0.06);
+ box-shadow: 0 0 12px rgba(45, 212, 191, 0.1);
+}
+
+.scene__desk-label {
+ position: absolute;
+ top: 6px;
+ left: 8px;
+ font-size: 10px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: $scene-on-surface-variant;
+ opacity: 0.6;
+}
+
+// Empty desk placeholder
+.scene__desk-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ opacity: 0.4;
+ animation: emptyDeskPulse 3s ease-in-out infinite;
+}
+
+.scene__desk-empty-icon {
+ font-size: 32px;
+}
+
+.scene__desk-empty-text {
+ font-size: 12px;
+ color: $scene-on-surface-variant;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+// ---------------------------------------------------------------------------
+// Breakroom Area (bottom of each side)
+// ---------------------------------------------------------------------------
+.scene__breakroom {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 16px;
+ background-color: $breakroom-bg;
+ border: 1px solid $scene-outline;
+ border-radius: 12px;
+ min-height: 200px;
+
+ &--dev {
+ border-top: 2px solid rgba(56, 189, 248, 0.2);
+ }
+
+ &--business {
+ border-top: 2px solid rgba(45, 212, 191, 0.2);
+ }
+}
+
+.scene__breakroom-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.scene__breakroom-icon {
+ font-size: 20px;
+}
+
+.scene__breakroom-label {
+ font-size: 14px;
+ font-weight: 600;
+ color: $scene-on-surface;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+// ---------------------------------------------------------------------------
+// Breakroom Furniture (emoji placeholders)
+// ---------------------------------------------------------------------------
+.scene__breakroom-furniture {
+ display: flex;
+ gap: 16px;
+ flex-wrap: wrap;
+}
+
+.scene__furniture-item {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 8px;
+ background-color: rgba(255, 255, 255, 0.05);
+ border-radius: 6px;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+.scene__furniture-emoji {
+ font-size: 16px;
+}
+
+.scene__furniture-label {
+ font-size: 11px;
+ color: $scene-on-surface-variant;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+
+// ---------------------------------------------------------------------------
+// Breakroom Minions Container
+// ---------------------------------------------------------------------------
+.scene__breakroom-minions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+ align-items: flex-end;
+ justify-content: center;
+ flex: 1;
+ min-height: 100px;
+ padding: 12px;
+ background-color: rgba(0, 0, 0, 0.15);
+ border-radius: 8px;
+ border: 1px dashed rgba(255, 255, 255, 0.06);
+}
+
+.scene__breakroom-empty {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ padding: 24px;
+ color: $scene-on-surface-variant;
+ font-size: 14px;
+ font-style: italic;
+ opacity: 0.6;
+}
+
+// ---------------------------------------------------------------------------
+// Minion slide-in animation
+// ---------------------------------------------------------------------------
+.scene__breakroom-minions app-minion {
+ animation: slideInFromBottom 0.3s ease-out;
+}
+
+.scene__desks app-minion {
+ animation: slideInFromBottom 0.3s ease-out;
+}
+
+// ---------------------------------------------------------------------------
+// Touch optimization
+// ---------------------------------------------------------------------------
+.scene__desk {
+ min-width: 48px;
+ min-height: 48px;
+
+ // Ensure the desk area is touch-friendly
+ &:active {
+ transform: scale(0.98);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Responsive — Scale down on smaller viewports
+// ---------------------------------------------------------------------------
+@media (max-width: 1200px) {
+ .scene {
+ aspect-ratio: auto;
+ min-height: 800px;
+ }
+
+ .scene__side {
+ padding: 16px;
+ gap: 12px;
+ }
+
+ .scene__side-title {
+ font-size: 20px;
+ }
+
+ .scene__desks {
+ gap: 12px;
+ }
+
+ .scene__desk {
+ min-height: 140px;
+ padding: 8px;
+ }
+}
+
+@media (max-width: 900px) {
+ .scene {
+ // Switch to single column on mobile
+ grid-template-columns: 1fr;
+ grid-template-rows: auto;
+ aspect-ratio: auto;
+ }
+
+ .scene__divider {
+ display: none;
+ }
+
+ .scene__side {
+ &--dev {
+ grid-column: 1;
+ }
+
+ &--business {
+ grid-column: 1;
+ }
+ }
+
+ .scene__desks {
+ grid-template-columns: repeat($desk-count, 1fr);
+ gap: 8px;
+ }
+
+ .scene__desk {
+ min-height: 120px;
+ }
+}
+
+@media (max-width: 599px) {
+ .scene {
+ border-radius: 0;
+ }
+
+ .scene__side {
+ padding: 12px;
+ gap: 8px;
+ }
+
+ .scene__side-title {
+ font-size: 16px;
+ }
+
+ .scene__desk {
+ min-height: 100px;
+ padding: 6px;
+ }
+
+ .scene__breakroom {
+ padding: 12px;
+ min-height: 140px;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Accessibility: Reduced Motion
+// ---------------------------------------------------------------------------
+@media (prefers-reduced-motion: reduce) {
+ .scene__divider-glow {
+ animation: none;
+ }
+
+ .scene__desk-empty {
+ animation: none;
+ }
+
+ .scene__breakroom-minions app-minion,
+ .scene__desks app-minion {
+ animation: none;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// High contrast mode
+// ---------------------------------------------------------------------------
+@media (prefers-contrast: high) {
+ .scene__desk {
+ border-width: 3px;
+ }
+
+ .scene__divider-line {
+ background: $dev-accent;
+ box-shadow: 0 0 16px $dev-accent;
+ }
+
+ .scene__desk-label {
+ opacity: 1;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/components/breakroom-scene/breakroom-scene.component.ts b/frontend/src/app/components/breakroom-scene/breakroom-scene.component.ts
new file mode 100644
index 0000000..ad382bb
--- /dev/null
+++ b/frontend/src/app/components/breakroom-scene/breakroom-scene.component.ts
@@ -0,0 +1,115 @@
+// ============================================================================
+// Breakroom Scene Component — 16-bit Office Scene Layout
+// Per CUB-61: Breakroom & Desk Scene Layout
+// ============================================================================
+import {
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ inject,
+ signal,
+} from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { MinionComponent } from '../minion/minion.component';
+import { MinionStateService } from '../../services/minion-state.service';
+import { MinionData, MinionSide } from '../../models/minion.model';
+
+/**
+ * Configuration for a desk slot in the scene.
+ * Each desk has a fixed position index and assigned side.
+ */
+interface DeskSlot {
+ side: MinionSide;
+ index: number;
+ /** Grid area identifier for CSS Grid placement */
+ gridArea: string;
+}
+
+/**
+ * BreakroomSceneComponent renders the full 16-bit office layout:
+ *
+ * ┌─────────────────┬─────────────────┐
+ * │ DEV SIDE │ BUSINESS SIDE │
+ * │ │ │
+ * │ [Desk 0] [Desk 2]│[Desk 0] [Desk 2]│
+ * │ [Desk 1] │[Desk 1] │
+ * │ │ │
+ * │ 🛋️ Breakroom │ 🛋️ Breakroom │
+ * │ 📺 TV 🍌 Bowl │ 📺 TV 🍌 Bowl │
+ * └─────────────────┴─────────────────┘
+ *
+ * Minions are positioned at their assigned desk or in the breakroom
+ * area based on their current state (idle → breakroom, working → desk).
+ */
+@Component({
+ selector: 'app-breakroom-scene',
+ standalone: true,
+ imports: [CommonModule, MinionComponent],
+ templateUrl: './breakroom-scene.component.html',
+ styleUrl: './breakroom-scene.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class BreakroomSceneComponent {
+ private readonly minionStateService = inject(MinionStateService);
+
+ /** All minion data from the state service */
+ readonly minions = this.minionStateService.minionList;
+
+ /** Dev-side minions (idle or in breakroom) */
+ readonly devBreakroomMinions = computed(() =>
+ this.minions().filter((m) => m.side === 'dev' && m.state === 'idle'),
+ );
+
+ /** Business-side minions (idle or in breakroom) */
+ readonly businessBreakroomMinions = computed(() =>
+ this.minions().filter((m) => m.side === 'business' && m.state === 'idle'),
+ );
+
+ /** Dev-side minions currently at desks (working or walking to desk) */
+ readonly devDeskMinions = computed(() =>
+ this.minions().filter(
+ (m) => m.side === 'dev' && (m.state === 'working' || m.state === 'walking'),
+ ),
+ );
+
+ /** Business-side minions currently at desks (working or walking to desk) */
+ readonly businessDeskMinions = computed(() =>
+ this.minions().filter(
+ (m) =>
+ m.side === 'business' &&
+ (m.state === 'working' || m.state === 'walking'),
+ ),
+ );
+
+ /** Dev-side minions returning to breakroom */
+ readonly devReturningMinions = computed(() =>
+ this.minions().filter((m) => m.side === 'dev' && m.state === 'returning'),
+ );
+
+ /** Business-side minions returning to breakroom */
+ readonly businessReturningMinions = computed(() =>
+ this.minions().filter(
+ (m) => m.side === 'business' && m.state === 'returning',
+ ),
+ );
+
+ /**
+ * Check if a desk slot is occupied.
+ * Returns the minion data if occupied, undefined if empty.
+ */
+ getDeskOccupant(side: MinionSide, deskIndex: number): MinionData | undefined {
+ return this.minions().find(
+ (m) =>
+ m.side === side &&
+ m.deskIndex === deskIndex &&
+ (m.state === 'working' || m.state === 'walking'),
+ );
+ }
+
+ /**
+ * Track minions by agent name for efficient @for rendering.
+ */
+ trackByAgentName(_index: number, minion: MinionData): string {
+ return minion.agentName;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/pages/breakroom/breakroom-page.component.html b/frontend/src/app/pages/breakroom/breakroom-page.component.html
index 7d4485a..82280dc 100644
--- a/frontend/src/app/pages/breakroom/breakroom-page.component.html
+++ b/frontend/src/app/pages/breakroom/breakroom-page.component.html
@@ -1,136 +1,76 @@
-
-
-
-
- weekend
- Breakroom
-
-
- @for (minion of minions(); track minion.agentName) {
- @if (minion.state === 'idle') {
-
- }
- }
-
-
-
-