v2.2.17: Google Service Control & Astra Office Flow Layer
This commit is contained in:
+64
-11
@@ -155,7 +155,9 @@ button,input,select{font:inherit}
|
||||
background:rgba(255,255,255,.035);
|
||||
transition:border-color .16s ease,background .16s ease,transform .16s ease;
|
||||
}
|
||||
.roster-item[data-agent]{cursor:pointer}
|
||||
.roster-item.active{border-color:rgba(138,124,255,.48);background:linear-gradient(180deg,rgba(138,124,255,.16),rgba(138,124,255,.08));transform:translateX(2px)}
|
||||
.roster-item.preview{border-color:rgba(255,255,255,.2);background:rgba(255,255,255,.07)}
|
||||
.roster-avatar{
|
||||
width:33px;height:33px;border-radius:12px;
|
||||
display:grid;place-items:center;
|
||||
@@ -170,6 +172,12 @@ button,input,select{font:inherit}
|
||||
.roster-status{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,.28)}
|
||||
.roster-item.active .roster-status{background:var(--role-color,var(--accent));box-shadow:0 0 12px var(--role-color,var(--accent))}
|
||||
.office-shell{min-width:0;display:grid;grid-template-rows:auto auto minmax(0,1fr);overflow:hidden}
|
||||
.office-shell[data-roster-empty="true"] .office{filter:saturate(.72) brightness(.9)}
|
||||
.office-shell[data-roster-empty="true"] .stage{opacity:.86}
|
||||
.office-shell[data-status="executing"] .office{box-shadow:inset 0 0 0 1px rgba(138,124,255,.12),0 0 0 1px rgba(138,124,255,.08)}
|
||||
.office-shell[data-status="reviewing"] .office{box-shadow:inset 0 0 0 1px rgba(70,216,255,.14),0 0 0 1px rgba(70,216,255,.08)}
|
||||
.office-shell[data-status="waiting_approval"] .office{box-shadow:inset 0 0 0 1px rgba(245,196,90,.16),0 0 0 1px rgba(245,196,90,.08)}
|
||||
.office-shell[data-status="error"] .office{box-shadow:inset 0 0 0 1px rgba(255,107,122,.18),0 0 0 1px rgba(255,107,122,.08)}
|
||||
.mission-strip{
|
||||
min-height:72px;
|
||||
display:flex;
|
||||
@@ -209,11 +217,18 @@ button,input,select{font:inherit}
|
||||
radial-gradient(circle at 18% 100%,rgba(70,216,255,.12),transparent 28%),
|
||||
linear-gradient(135deg,#31283A,#201A29 72%);
|
||||
}
|
||||
.office:before{content:'';position:absolute;left:0;right:0;top:16%;bottom:0;background-image:linear-gradient(rgba(255,255,255,.032) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.032) 1px,transparent 1px);background-size:48px 48px}
|
||||
.office:after{content:'';position:absolute;inset:0;background:radial-gradient(circle at 50% 50%,transparent 0 40%,rgba(0,0,0,.18) 100%);pointer-events:none}
|
||||
.office.has-art{
|
||||
background:
|
||||
linear-gradient(180deg,rgba(6,10,18,.06),rgba(6,10,18,.12)),
|
||||
var(--office-backdrop) center center / cover no-repeat;
|
||||
}
|
||||
.office:before{content:'';position:absolute;inset:0;background-image:linear-gradient(rgba(255,255,255,.018) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.018) 1px,transparent 1px);background-size:48px 48px;opacity:.28}
|
||||
.office.has-art:before{background:linear-gradient(180deg,rgba(4,7,14,.06),transparent 28%,rgba(4,7,14,.1));opacity:1}
|
||||
.office:after{content:'';position:absolute;inset:0;background:radial-gradient(circle at 50% 50%,transparent 0 42%,rgba(0,0,0,.2) 100%);pointer-events:none}
|
||||
.stage{position:relative;width:720px;height:585px;margin:0;z-index:2}
|
||||
.wall-window{position:absolute;top:16px;width:86px;height:42px;border:2px solid rgba(215,228,255,.35);border-radius:8px;background:linear-gradient(180deg,rgba(160,208,255,.34),rgba(110,150,210,.08));box-shadow:inset 0 0 0 1px rgba(15,20,31,.55)}
|
||||
.wall-window.w1{left:84px}.wall-window.w2{left:550px}
|
||||
.office.has-art .wall-window{display:none}
|
||||
.obj,.desk,.char{position:absolute;image-rendering:pixelated}
|
||||
.obj{filter:drop-shadow(3px 5px 0 rgba(0,0,0,.28));z-index:4}
|
||||
.desk{width:112px;z-index:5;filter:drop-shadow(4px 7px 0 rgba(0,0,0,.28))}
|
||||
@@ -237,6 +252,8 @@ button,input,select{font:inherit}
|
||||
.char[data-agent="support"],.desk[data-agent="support"],.roster-item[data-agent="support"]{--role-color:#94A3B8}
|
||||
.char[data-agent="writer"],.desk[data-agent="writer"],.roster-item[data-agent="writer"]{--role-color:#FBBF24}
|
||||
.desk::after{content:'';position:absolute;inset:-4px;border-radius:10px;border:1px solid transparent;pointer-events:none;transition:border-color .2s ease,box-shadow .2s ease}
|
||||
.desk.preview::after{border-color:rgba(255,255,255,.28);box-shadow:0 0 0 1px rgba(255,255,255,.04),0 0 18px rgba(255,255,255,.16)}
|
||||
.char.preview::before{content:'';position:absolute;left:18px;top:-14px;width:20px;height:20px;border-radius:50%;border:1px solid rgba(255,255,255,.42);box-shadow:0 0 18px rgba(255,255,255,.18)}
|
||||
.stage:has(.char.active[data-agent="ceo"]) .desk[data-agent="ceo"]::after,
|
||||
.stage:has(.char.active[data-agent="planner"]) .desk[data-agent="planner"]::after,
|
||||
.stage:has(.char.active[data-agent="researcher"]) .desk[data-agent="researcher"]::after,
|
||||
@@ -247,7 +264,7 @@ button,input,select{font:inherit}
|
||||
.stage:has(.char.active[data-agent="support"]) .desk[data-agent="support"]::after,
|
||||
.stage:has(.char.active[data-agent="writer"]) .desk[data-agent="writer"]::after{border-color:var(--role-color);box-shadow:0 0 0 1px rgba(255,255,255,.06),0 0 18px color-mix(in srgb,var(--role-color) 35%,transparent)}
|
||||
.shadow{position:absolute;left:12px;bottom:0;width:28px;height:7px;background:radial-gradient(ellipse,rgba(0,0,0,.55),transparent 70%)}
|
||||
.bubble{position:absolute;z-index:20;transform:translate(-50%,-100%);max-width:180px;padding:7px 10px;border-radius:999px;border:1px solid rgba(255,255,255,.14);background:rgba(10,14,24,.92);color:var(--text);font-size:11px;line-height:1.2;box-shadow:0 10px 24px rgba(0,0,0,.28);white-space:nowrap}
|
||||
.bubble{position:absolute;z-index:20;transform:translate(-50%,-100%);max-width:180px;padding:7px 10px;border-radius:14px;border:1px solid rgba(255,255,255,.14);background:rgba(10,14,24,.92);color:var(--text);font-size:11px;line-height:1.35;box-shadow:0 10px 24px rgba(0,0,0,.28);white-space:normal}
|
||||
.brief-grid{display:flex;flex-direction:column;gap:10px}
|
||||
.brief-card{
|
||||
padding:14px;
|
||||
@@ -453,6 +470,23 @@ body[data-edit-mode="true"] .char .shadow{display:none}
|
||||
|
||||
<script>(function(){
|
||||
const base='http://127.0.0.1:8765/assets/pixelOffice/derived'; const stage=document.getElementById('stage');
|
||||
const officeEl = stage && stage.closest ? stage.closest('.office') : null;
|
||||
if(officeEl){
|
||||
officeEl.style.setProperty('--office-backdrop', 'url("'+base+'/office-backdrop-astra-v2.png")');
|
||||
officeEl.classList.add('has-art');
|
||||
}
|
||||
let _stageScale = 1;
|
||||
function _fitStage(){
|
||||
const shell = stage && stage.closest ? stage.closest('.office') : null;
|
||||
if(!stage || !shell) return;
|
||||
const sx = Math.max(.62, (shell.clientWidth - 28) / 720);
|
||||
const sy = Math.max(.62, (shell.clientHeight - 28) / 585);
|
||||
_stageScale = Math.min(1, sx, sy);
|
||||
stage.style.transform = 'scale(' + _stageScale + ')';
|
||||
stage.style.transformOrigin = 'center center';
|
||||
}
|
||||
window.addEventListener('resize', _fitStage);
|
||||
setTimeout(_fitStage, 0);
|
||||
// ── 데이터 모델 ──
|
||||
// stations: 책상 + 캐릭터 정의 배열 (let — 추가/제거 가능).
|
||||
// key = 안정적 식별자 (DOM dataset.role 로도 사용). 사용자가 새로 만든 책상은 자동 생성.
|
||||
@@ -479,10 +513,10 @@ const DESK_SPRITE_CHOICES=['desk-main','desk-boss','desk-dark-mirror','desk-main
|
||||
// 추가 가능한 프랍 sprite 후보.
|
||||
const PROP_SPRITE_CHOICES=['board','plant-tall','bookshelf','plant-bushy','partition','cooler','filing','couch','rug','shelf','printer','monitor-blue','monitor-black','chair-blue','crt'];
|
||||
const DEFAULT_PROPS=[
|
||||
{name:'board',x:316,y:12,w:88},{name:'plant-tall',x:44,y:92,w:42},{name:'bookshelf',x:86,y:70,w:54},
|
||||
{name:'plant-bushy',x:642,y:96,w:42},{name:'partition',x:520,y:208,w:72},{name:'cooler',x:640,y:248,w:38},
|
||||
{name:'filing',x:620,y:330,w:42},{name:'couch',x:578,y:432,w:96},{name:'rug',x:560,y:510,w:126},
|
||||
{name:'shelf',x:40,y:504,w:118},{name:'printer',x:520,y:520,w:58},{name:'monitor-blue',x:356,y:56,w:44},
|
||||
{name:'plant-tall',x:40,y:118,w:42},{name:'bookshelf',x:86,y:88,w:54},
|
||||
{name:'plant-bushy',x:640,y:118,w:42},{name:'cooler',x:646,y:286,w:38},
|
||||
{name:'filing',x:618,y:374,w:42},{name:'couch',x:584,y:452,w:96},
|
||||
{name:'rug',x:560,y:514,w:126},{name:'printer',x:520,y:526,w:58},
|
||||
];
|
||||
|
||||
let stations=[]; // mutable, 시작 시 default 또는 saved layout 로 채움.
|
||||
@@ -823,6 +857,8 @@ function apply(s){
|
||||
_lastState = s; // D. 컨텍스트 메뉴 / 세부보기에서 사용.
|
||||
const st = s?.status || 'idle';
|
||||
const meta = _statusMeta(st);
|
||||
const officeShell = document.querySelector('.office-shell');
|
||||
if(officeShell) officeShell.dataset.status = st;
|
||||
// 정적 갱신은 *항상* — 헤더/태스크/단계/로그/프로그레스.
|
||||
const statusEl = document.getElementById('status');
|
||||
const phasePill = document.getElementById('phasePill');
|
||||
@@ -900,6 +936,7 @@ function apply(s){
|
||||
} else {
|
||||
mm.style.display = 'none';
|
||||
}
|
||||
setTimeout(_fitStage, 0);
|
||||
// 활성 캐릭터 결정. roleMap 은 agentKey → 실제 존재하는 station.key 로 lookup
|
||||
// 하므로, 매핑된 책상이 없으면 null. 사용자가 default ceo 책상을 지워도 안전.
|
||||
let role = null;
|
||||
@@ -1100,6 +1137,8 @@ function _renderRoster(roster, activeAgentId){
|
||||
const count = document.getElementById('rosterCount');
|
||||
if(!wrap || !count) return;
|
||||
const list = Array.isArray(roster) ? roster : [];
|
||||
const officeShell = document.querySelector('.office-shell');
|
||||
if(officeShell) officeShell.dataset.rosterEmpty = list.length ? 'false' : 'true';
|
||||
count.textContent = String(list.length);
|
||||
if(!list.length){
|
||||
wrap.innerHTML = '<div class="roster-item"><div class="roster-copy"><strong>등록된 팀이 없습니다</strong><span class="roster-meta">회사 모드를 켜면 라인업이 표시됩니다.</span></div></div>';
|
||||
@@ -1117,6 +1156,20 @@ function _renderRoster(roster, activeAgentId){
|
||||
'</div>'+
|
||||
'</div>';
|
||||
}).join('');
|
||||
wrap.querySelectorAll('.roster-item[data-agent]').forEach((item)=>{
|
||||
const id = item.dataset.agent;
|
||||
item.addEventListener('mouseenter', ()=>_previewAgent(id, true, item));
|
||||
item.addEventListener('mouseleave', ()=>_previewAgent(id, false, item));
|
||||
});
|
||||
}
|
||||
function _previewAgent(agentId, on, item){
|
||||
if(item) item.classList.toggle('preview', !!on);
|
||||
const st = findStationByAgent(agentId);
|
||||
if(!st) return;
|
||||
const desk = __deskWrap[st.key];
|
||||
const ch = chars[st.key];
|
||||
if(desk) desk.classList.toggle('preview', !!on);
|
||||
if(ch) ch.classList.toggle('preview', !!on);
|
||||
}
|
||||
// refactor #G-full — roster 에 있는 agent 중 desk 가 없는 경우 자동 생성.
|
||||
// 한 번 처리된 agentId 는 _autoDeskedFor 에 기록 → 사용자가 그 desk 를 지워도 재생성 안 함.
|
||||
@@ -1664,8 +1717,8 @@ stage.addEventListener('mousedown', e=>{
|
||||
const rect = stage.getBoundingClientRect();
|
||||
const tx = parseFloat(target.style.left)||0;
|
||||
const ty = parseFloat(target.style.top)||0;
|
||||
_dragDX = e.clientX - rect.left - tx;
|
||||
_dragDY = e.clientY - rect.top - ty;
|
||||
_dragDX = ((e.clientX - rect.left) / _stageScale) - tx;
|
||||
_dragDY = ((e.clientY - rect.top) / _stageScale) - ty;
|
||||
target.classList.add('dragging');
|
||||
});
|
||||
|
||||
@@ -1700,8 +1753,8 @@ document.addEventListener('keydown', e=>{
|
||||
document.addEventListener('mousemove', e=>{
|
||||
if(!_editMode || !_drag) return;
|
||||
const rect = stage.getBoundingClientRect();
|
||||
let x = e.clientX - rect.left - _dragDX;
|
||||
let y = e.clientY - rect.top - _dragDY;
|
||||
let x = ((e.clientX - rect.left) / _stageScale) - _dragDX;
|
||||
let y = ((e.clientY - rect.top) / _stageScale) - _dragDY;
|
||||
// 4px 격자 snap
|
||||
x = Math.round(x/4)*4;
|
||||
y = Math.round(y/4)*4;
|
||||
|
||||
Reference in New Issue
Block a user