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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DEVELOPMENT + Dev Minions + + + BUSINESS + Business Minions + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + COMFY SOFA + + + + + + + + + + + + + + + + + + + + + MONITOR + + + + + + + + + + + + + 🍌 BANANA BOWL + + + + + + + + + + + + COMFY SOFA + + + + + + + + + + + + + + + + + MONITOR + + + + + + + + + + 🍌 BANANA BOWL + + + + + + + + \ 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 @@ + + +
+ +
+ +
+ + + + + +
+ + +
+

DEVELOPMENT

+ Dev Minions +
+ + +
+ @for (deskIndex of [0, 1, 2]; track deskIndex) { +
+ + Desk {{ deskIndex + 1 }} + + + @if (!getDeskOccupant('dev', deskIndex)) { +
+ 💻 + Empty +
+ } + + + @if (getDeskOccupant('dev', deskIndex); as occupant) { + + } +
+ } +
+ + +
+
+ 🛋️ + Dev Breakroom +
+ + +
+
+ 🛋️ + 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 🍌 +
+ } +
+
+
+ + +
+ + +
+

BUSINESS

+ Business Minions +
+ + +
+ @for (deskIndex of [0, 1, 2]; track deskIndex) { +
+ + Desk {{ deskIndex + 1 }} + + + @if (!getDeskOccupant('business', deskIndex)) { +
+ 💻 + Empty +
+ } + + + @if (getDeskOccupant('business', deskIndex); as occupant) { + + } +
+ } +
+ + +
+
+ 🛋️ + Business Breakroom +
+ + +
+
+ 🛋️ + 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') { - - } - } -
-
- -
-
-

- computer - Office +
+ + + + +
+
+

+ gamepad + Controls

-
+ {{ controlsExpanded() ? 'expand_less' : 'expand_more' }} + - -
-

Dev Desks

-
- @for (minion of minions(); track minion.agentName) { - @if (minion.side === 'dev' && (minion.state === 'working' || minion.state === 'walking')) { -
- -
- } + @if (controlsExpanded()) { +

+ Spawn minions and control their state transitions +

+ +
+ @for (demo of demoMinions; track demo.agentName) { +
+ {{ demo.displayName }} + + {{ demo.side }} + + + @if (!isSpawned(demo.agentName)) { + + } @else { + {{ getStateLabel(demo.agentName) }} + + + + + + + + + + + } +
}
-
- - -
-

Business Desks

-
- @for (minion of minions(); track minion.agentName) { - @if (minion.side === 'business' && (minion.state === 'working' || minion.state === 'walking')) { -
- -
- } - } -
-
-

- - -
-

- gamepad - Controls -

-

Spawn minions and control their state transitions

- -
- @for (demo of demoMinions; track demo.agentName) { -
- {{ demo.displayName }} - - @if (!isSpawned(demo.agentName)) { - - } @else { - {{ getStateLabel(demo.agentName) }} - - - - - - - - - - - } -
- } -
+ }
\ No newline at end of file diff --git a/frontend/src/app/pages/breakroom/breakroom-page.component.scss b/frontend/src/app/pages/breakroom/breakroom-page.component.scss index f9b14bb..8de5427 100644 --- a/frontend/src/app/pages/breakroom/breakroom-page.component.scss +++ b/frontend/src/app/pages/breakroom/breakroom-page.component.scss @@ -1,30 +1,55 @@ // ============================================================================ // Breakroom Page Styles -// Per CUB-60: Demo/test page for minion state & animation +// Per CUB-61: Updated for scene-based layout with collapsible controls // ============================================================================ -.breakroom { +.breakroom-page { display: flex; flex-direction: column; - gap: 24px; + gap: 20px; padding: var(--cc-section-padding, 24px); min-height: 100%; } // --------------------------------------------------------------------------- -// Zone (breakroom or office area) +// Scene container // --------------------------------------------------------------------------- -.breakroom__zone { +.breakroom-page__scene { + width: 100%; + border-radius: var(--cc-card-border-radius, 16px); + overflow: hidden; + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.3), + 0 4px 16px rgba(0, 0, 0, 0.2); +} + +// --------------------------------------------------------------------------- +// Controls section (collapsible) +// --------------------------------------------------------------------------- +.breakroom-page__controls { background-color: var(--cc-surface-container); border-radius: var(--cc-card-border-radius, 16px); padding: var(--cc-card-padding, 20px); } -.breakroom__zone-title { +.breakroom-page__controls-header { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + user-select: none; + padding: 4px 0; + + &:hover { + opacity: 0.9; + } +} + +.breakroom-page__controls-title { display: flex; align-items: center; gap: 8px; - margin: 0 0 16px; + margin: 0; font-size: 18px; font-weight: 600; color: var(--cc-on-surface); @@ -36,117 +61,22 @@ } } -// --------------------------------------------------------------------------- -// Breakroom zone (idle minions) -// --------------------------------------------------------------------------- -.breakroom__zone--breakroom { - background: linear-gradient( - 135deg, - var(--cc-surface-container) 0%, - rgba(45, 212, 191, 0.06) 100% - ); - min-height: 120px; -} - -.breakroom__minions--idle { - display: flex; - flex-wrap: wrap; - gap: 16px; - justify-content: center; - min-height: 80px; - align-items: flex-end; - - &:empty::after { - content: 'No minions on break 🍌'; - color: var(--cc-on-surface-variant); - font-size: 14px; - text-align: center; - padding: 20px; - display: block; - } -} - -// --------------------------------------------------------------------------- -// Office zone (working minions) -// --------------------------------------------------------------------------- -.breakroom__zone--office { - background: linear-gradient( - 135deg, - var(--cc-surface-container) 0%, - rgba(56, 189, 248, 0.06) 100% - ); -} - -.breakroom__desk-row { - margin-bottom: 16px; -} - -.breakroom__desk-label { - font-size: 13px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--cc-on-surface-variant); - margin: 0 0 8px; -} - -.breakroom__desks { - display: flex; - flex-wrap: wrap; - gap: 20px; - min-height: 100px; - padding: 12px; - border: 2px dashed var(--cc-outline); - border-radius: 12px; - background-color: rgba(0, 0, 0, 0.1); - align-items: flex-end; - - &:empty::after { - content: 'No minions at their desks 💻'; - color: var(--cc-on-surface-variant); - font-size: 14px; - text-align: center; - padding: 20px; - display: block; - width: 100%; - } -} - -.breakroom__desk { - display: flex; - flex-direction: column; - align-items: center; - justify-content: flex-end; - min-width: 90px; - min-height: 110px; - background-color: var(--cc-surface-container-high); - border-radius: 8px; - padding: 8px 4px 4px; - border: 1px solid var(--cc-outline); -} - -// --------------------------------------------------------------------------- -// Controls section -// --------------------------------------------------------------------------- -.breakroom__controls { - background-color: var(--cc-surface-container); - border-radius: var(--cc-card-border-radius, 16px); - padding: var(--cc-card-padding, 20px); -} - -.breakroom__controls-hint { +.breakroom-page__controls-hint { font-size: 13px; color: var(--cc-on-surface-variant); margin: 0 0 16px; } -.breakroom__button-grid { +// --------------------------------------------------------------------------- +// Button Grid +// --------------------------------------------------------------------------- +.breakroom-page__button-grid { display: flex; flex-direction: column; gap: 12px; } -.breakroom__control-row { +.breakroom-page__control-row { display: flex; align-items: center; gap: 8px; @@ -157,14 +87,34 @@ min-height: 48px; } -.breakroom__agent-name { +.breakroom-page__agent-name { font-weight: 600; font-size: 14px; min-width: 60px; color: var(--cc-on-surface); } -.breakroom__state-chip { +.breakroom-page__agent-side { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + padding: 2px 8px; + border-radius: 10px; + color: #fff; + + &--dev { + background-color: rgba(56, 189, 248, 0.25); + color: #38BDF8; + } + + &--business { + background-color: rgba(45, 212, 191, 0.25); + color: #2DD4BF; + } +} + +.breakroom-page__state-chip { font-size: 12px; font-family: var(--cc-font-mono); padding: 4px 10px; @@ -175,8 +125,8 @@ } // Button sizing for touch -.breakroom__control-row .mat-mdc-raised-button, -.breakroom__control-row .mat-mdc-outlined-button { +.breakroom-page__control-row .mat-mdc-raised-button, +.breakroom-page__control-row .mat-mdc-outlined-button { min-height: 40px; min-width: 48px; font-size: 13px; @@ -193,17 +143,17 @@ // Responsive // --------------------------------------------------------------------------- @media (max-width: 599px) { - .breakroom { - padding: 16px; - gap: 16px; + .breakroom-page { + padding: 12px; + gap: 12px; } - .breakroom__control-row { + .breakroom-page__control-row { flex-wrap: wrap; gap: 6px; } - .breakroom__agent-name { + .breakroom-page__agent-name { min-width: 50px; } } \ No newline at end of file diff --git a/frontend/src/app/pages/breakroom/breakroom-page.component.ts b/frontend/src/app/pages/breakroom/breakroom-page.component.ts index 1cc8e5e..94374fb 100644 --- a/frontend/src/app/pages/breakroom/breakroom-page.component.ts +++ b/frontend/src/app/pages/breakroom/breakroom-page.component.ts @@ -1,15 +1,14 @@ // ============================================================================ -// Breakroom Page — Demo/Test Page for Minion State & Animation -// Per CUB-60: Validation page with spawn/transition/reset controls +// Breakroom Page — Hosts the BreakroomSceneComponent with demo controls +// Per CUB-61: Updated to use scene layout with integrated controls // ============================================================================ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatChipsModule } from '@angular/material/chips'; -import { MinionComponent } from '../../components/minion/minion.component'; +import { BreakroomSceneComponent } from '../../components/breakroom-scene/breakroom-scene.component'; import { MinionStateService } from '../../services/minion-state.service'; -import { MinionData } from '../../models/minion.model'; interface DemoMinion { agentName: string; @@ -26,7 +25,7 @@ interface DemoMinion { MatButtonModule, MatIconModule, MatChipsModule, - MinionComponent, + BreakroomSceneComponent, ], templateUrl: './breakroom-page.component.html', styleUrl: './breakroom-page.component.scss', @@ -35,15 +34,19 @@ interface DemoMinion { export class BreakroomPageComponent { protected readonly minionStateService = inject(MinionStateService); + /** Whether the controls panel is expanded */ + readonly controlsExpanded = signal(true); + /** Predefined demo minions */ readonly demoMinions: DemoMinion[] = [ { agentName: 'otto', displayName: 'Otto', side: 'dev', deskIndex: 0 }, { agentName: 'rex', displayName: 'Rex', side: 'dev', deskIndex: 1 }, { agentName: 'dex', displayName: 'Dex', side: 'dev', deskIndex: 2 }, - { agentName: 'hex', displayName: 'Hex', side: 'dev', deskIndex: 3 }, - { agentName: 'pip', displayName: 'Pip', side: 'business', deskIndex: 0 }, - { agentName: 'nano', displayName: 'Nano', side: 'business', deskIndex: 1 }, - { agentName: 'flip', displayName: 'Flip', side: 'business', deskIndex: 2 }, + { agentName: 'hex', displayName: 'Hex', side: 'business', deskIndex: 0 }, + { agentName: 'pip', displayName: 'Pip', side: 'business', deskIndex: 1 }, + { agentName: 'nano', displayName: 'Nano', side: 'business', deskIndex: 2 }, + { agentName: 'sketch', displayName: 'Sketch', side: 'dev', deskIndex: 3 }, + { agentName: 'flip', displayName: 'Flip', side: 'business', deskIndex: 3 }, ]; /** All minions currently in the state service */