rvlib-shipkit 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +137 -0
- package/README.md +23 -8
- package/package.json +15 -3
- package/src/asks/asks-queue.ts +20 -7
- package/src/assets/gen/comfy-gen.ts +106 -0
- package/src/assets/gen/generate-roll.ts +57 -0
- package/src/assets/gen/workflow-loader.ts +67 -0
- package/src/assets/image-ops.ts +30 -0
- package/src/assets/json-store.ts +27 -0
- package/src/assets/kit-config.ts +179 -0
- package/src/assets/kits/identity-kit.ts +119 -0
- package/src/assets/native-deps.ts +18 -0
- package/src/assets/panels/geometry-rects.ts +84 -0
- package/src/assets/roll-workspace.ts +41 -0
- package/src/assets/run-assets.ts +142 -0
- package/src/assets/server/studio-page.ts +620 -0
- package/src/assets/server/studio-queue.ts +58 -0
- package/src/assets/server/studio-server.ts +412 -0
- package/src/assets/vite.ts +62 -0
- package/src/cli/cmd_Asks.ts +83 -0
- package/src/cli/cmd_Assets.ts +120 -0
- package/src/cli/cmd_Check.ts +30 -0
- package/src/cli/cmd_Ci.ts +41 -0
- package/src/cli/cmd_Contracts.ts +35 -0
- package/src/cli/cmd_Init.ts +28 -0
- package/src/cli/cmd_Serve.ts +34 -0
- package/src/cli/cmd_Tui.ts +36 -0
- package/src/cli/cmd_Wiki.ts +53 -0
- package/src/cli.ts +44 -25
- package/src/contracts/run-contracts.ts +33 -0
- package/src/core/check-ctx.ts +3 -16
- package/src/core/check-suite.ts +16 -2
- package/src/core/discover-checks.ts +48 -0
- package/src/core/run-check.ts +73 -0
- package/src/core/run-ci.ts +54 -0
- package/src/core/spawn-timeout.ts +65 -0
- package/src/core/task-runner.ts +36 -43
- package/src/core/wire.ts +20 -0
- package/src/core/write-status.ts +70 -0
- package/src/init-repo.ts +60 -0
- package/src/repo-config.ts +82 -15
- package/src/serve/hub-pages.ts +84 -0
- package/src/serve/run-serve.ts +85 -0
- package/src/serve/serve-hub.ts +77 -0
- package/src/shipkit.ts +39 -3
- package/src/tui/checks-panel.ts +39 -0
- package/src/tui/tasks-panel.ts +54 -0
- package/src/tui/tui-app.tsx +91 -0
- package/src/tui/tui-panel.ts +57 -0
- package/src/tui/tui-store.ts +98 -0
- package/src/wiki/run-wiki.ts +50 -0
- package/src/cli/shipkit-asks.ts +0 -26
- package/src/cli/shipkit-check.ts +0 -26
- package/src/cli/shipkit-ci.ts +0 -30
- package/src/cli/shipkit-contracts.ts +0 -26
- package/src/cli/shipkit-init.ts +0 -41
- package/src/cli/shipkit-wiki.ts +0 -74
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
// The studio's single-page UI (served by studio-server.ts). No build step,
|
|
2
|
+
// vanilla JS on purpose — proven twice; the whole page is one template
|
|
3
|
+
// string. Kind/asset structure is read-only here (kit.ts is code-reviewed);
|
|
4
|
+
// the page edits WORKSPACE state: picks, alts, prompt overlays, meta
|
|
5
|
+
// sidecars, geometry rects. Repo-defined custom panels (kit customPanel)
|
|
6
|
+
// mount as extra drawer tabs: dynamic import of /panel/<kind>/<id>.js,
|
|
7
|
+
// mount(el, api) with the meta-sidecar transport. Look & feel changes are
|
|
8
|
+
// playtested by a human, never self-verified.
|
|
9
|
+
export const page = (): string => `<!doctype html><html><head><meta charset=utf-8>
|
|
10
|
+
<meta name=viewport content="width=device-width,initial-scale=1"><title>atelier studio</title>
|
|
11
|
+
<style>
|
|
12
|
+
:root{--bg:#141210;--panel:#1e1a15;--panel2:#262019;--line:#332b22;--ink:#f3ebe0;--dim:#a89a88;
|
|
13
|
+
--accent:#e88c28;--gold:#ffcf5c;--teal:#58c0b0;--red:#e05252;--blue:#6ca0e8;--chk1:#221d17;--chk2:#1a1611}
|
|
14
|
+
*{box-sizing:border-box}html,body{height:100%}
|
|
15
|
+
body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 system-ui,sans-serif;
|
|
16
|
+
display:grid;grid-template-columns:270px 1fr;overflow:hidden}
|
|
17
|
+
aside{border-right:1px solid var(--line);background:var(--panel);overflow-y:auto;height:100vh}
|
|
18
|
+
.shead{position:sticky;top:0;background:var(--panel);padding:12px 14px;border-bottom:1px solid var(--line);
|
|
19
|
+
font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--dim);z-index:2}
|
|
20
|
+
.gname{padding:8px 14px 4px;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--accent);
|
|
21
|
+
display:flex;justify-content:space-between;align-items:center}
|
|
22
|
+
.gname .cn{cursor:pointer}.gname .cn:hover{color:var(--gold)}
|
|
23
|
+
.fold{cursor:pointer;color:var(--dim);width:12px}.fold:hover{color:var(--ink)}
|
|
24
|
+
.catgen button{background:var(--panel2);color:var(--dim);border:1px solid var(--line);border-radius:6px;
|
|
25
|
+
padding:1px 6px;font-size:11px;font-weight:700;margin-left:3px;cursor:pointer}
|
|
26
|
+
.catgen button:hover{color:var(--ink);border-color:var(--accent)}
|
|
27
|
+
.arow{display:flex;align-items:center;gap:8px;padding:3px 12px;cursor:pointer;border-left:3px solid transparent}
|
|
28
|
+
.arow:hover{background:var(--panel2)}
|
|
29
|
+
.arow.sel{background:rgba(232,140,40,.18);border-left-color:var(--accent)}
|
|
30
|
+
.arow .lb{flex:1;min-width:0}.arow .lb b{display:block;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
31
|
+
.arow .lb span{font-size:11px;color:var(--dim)}
|
|
32
|
+
.dot{width:8px;height:8px;border-radius:50%;flex:none;background:var(--line)}
|
|
33
|
+
.dot.done{background:var(--gold)}.dot.some{background:var(--accent)}
|
|
34
|
+
main{display:flex;flex-direction:column;height:100vh;overflow:hidden}
|
|
35
|
+
.bar{display:flex;gap:12px;align-items:flex-start;padding:12px 16px;border-bottom:1px solid var(--line);background:var(--panel)}
|
|
36
|
+
.promptwrap{flex:1}
|
|
37
|
+
textarea{width:100%;min-height:64px;background:var(--bg);color:var(--ink);border:1px solid var(--line);
|
|
38
|
+
border-radius:9px;padding:9px 11px;font:12px/1.5 ui-monospace,Menlo,monospace;resize:vertical}
|
|
39
|
+
.ctl{display:flex;flex-direction:column;gap:7px;min-width:150px}
|
|
40
|
+
button{background:var(--accent);color:#1a1206;border:0;border-radius:9px;padding:8px 13px;font-weight:650;cursor:pointer}
|
|
41
|
+
button.ghost{background:var(--panel2);color:var(--ink);border:1px solid var(--line)}
|
|
42
|
+
button:disabled{opacity:.45;cursor:default}
|
|
43
|
+
select,input[type=number]{background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px}
|
|
44
|
+
.status{font-size:12px;color:var(--dim);min-height:16px}
|
|
45
|
+
.detnote{font-size:11px;color:var(--teal);line-height:1.25;max-width:150px}
|
|
46
|
+
/* ── per-asset info surface (W2-2) ── */
|
|
47
|
+
.info{display:none;flex-wrap:wrap;gap:6px;align-items:center;padding:6px 16px;border-bottom:1px solid var(--line);background:var(--panel2)}
|
|
48
|
+
.info .chip{display:inline-flex;align-items:center;gap:5px;background:var(--bg);border:1px solid var(--line);border-radius:6px;
|
|
49
|
+
padding:2px 8px;font-size:11px;color:var(--ink);max-width:360px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
50
|
+
.info .chip b{color:var(--accent);font-weight:650;letter-spacing:.05em;text-transform:uppercase;font-size:10px}
|
|
51
|
+
.info .chip.warn{border-color:var(--red);color:#f0c0c0}.info .chip.warn b{color:var(--red)}
|
|
52
|
+
.info .chip code{font:11px ui-monospace,Menlo,monospace;color:var(--dim)}
|
|
53
|
+
.errs{background:#3a1717;border-bottom:1px solid #5a2222;color:#f0c0c0;font:12px/1.5 ui-monospace,Menlo,monospace;
|
|
54
|
+
padding:6px 16px;white-space:pre-wrap;display:none}
|
|
55
|
+
.stage{position:relative;flex:1;min-height:0;display:flex;align-items:center;justify-content:center;padding:18px;overflow:hidden;
|
|
56
|
+
background:conic-gradient(var(--chk1) 90deg,var(--chk2) 0 180deg,var(--chk1) 0 270deg,var(--chk2) 0) 0/34px 34px}
|
|
57
|
+
.stage img.roll{max-width:100%;max-height:100%;object-fit:contain;filter:drop-shadow(0 6px 20px #0008);z-index:5;position:relative}
|
|
58
|
+
.stage .ph{color:var(--dim)}
|
|
59
|
+
/* ── geometry overlay ── */
|
|
60
|
+
#ov{position:absolute;border:2px solid var(--gold);border-radius:50%;background:rgba(255,207,92,.14);cursor:move;z-index:10;display:none}
|
|
61
|
+
#ov .hdl{position:absolute;right:-7px;top:-7px;width:14px;height:14px;background:var(--gold);border-radius:3px;cursor:nesw-resize}
|
|
62
|
+
#ov .arrow{position:absolute;left:0;right:0;top:50%;border-top:1px dashed var(--gold);opacity:.8}
|
|
63
|
+
#ov .sz{position:absolute;left:50%;top:50%;transform:translate(-50%,-120%);font-size:11px;color:var(--gold);
|
|
64
|
+
background:#000a;padding:0 5px;border-radius:4px;white-space:nowrap}
|
|
65
|
+
.plumb{position:absolute;width:0;border-left:1px dashed rgba(255,207,92,.5);top:-2000px;bottom:-2000px;z-index:9;display:none}
|
|
66
|
+
.leg{position:absolute;z-index:10;border:1px solid;border-radius:3px}
|
|
67
|
+
.leg.far{border-color:var(--blue);z-index:3}
|
|
68
|
+
.leg.near{border-color:var(--red);z-index:7}
|
|
69
|
+
.leg .hip{position:absolute;top:-3px;left:50%;transform:translateX(-50%);width:6px;height:6px;border-radius:50%;background:currentColor}
|
|
70
|
+
.leg .foot{position:absolute;bottom:-4px;left:50%;transform:translateX(-50%);width:12px;height:7px;border-radius:50%;background:var(--gold)}
|
|
71
|
+
.leg .lh{position:absolute;right:-6px;top:-6px;width:11px;height:11px;background:currentColor;border-radius:3px;cursor:nesw-resize}
|
|
72
|
+
.leg .tag{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-size:9px;opacity:.85;white-space:nowrap}
|
|
73
|
+
.pill{position:absolute;border-radius:999px;z-index:3;opacity:.9}
|
|
74
|
+
.pill.near{z-index:7}
|
|
75
|
+
#rectbar{position:absolute;left:14px;bottom:14px;z-index:11;display:none;gap:8px;align-items:center;
|
|
76
|
+
background:#000b;border:1px solid var(--line);border-radius:10px;padding:7px 10px;font-size:12px}
|
|
77
|
+
#rectbar input[type=number]{width:64px;padding:4px}
|
|
78
|
+
#rectbar .ro{color:var(--dim);font:11px ui-monospace,Menlo,monospace}
|
|
79
|
+
/* ── category grid + lineup ── */
|
|
80
|
+
#catgrid{flex:1;min-height:0;overflow-y:auto;display:none;padding:14px}
|
|
81
|
+
.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px}
|
|
82
|
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:8px}
|
|
83
|
+
.card .th{position:relative;aspect-ratio:1;border-radius:8px;overflow:hidden;
|
|
84
|
+
background:conic-gradient(var(--chk1) 90deg,var(--chk2) 0 180deg,var(--chk1) 0 270deg,var(--chk2) 0) 0/14px 14px}
|
|
85
|
+
.card .th img{width:100%;height:100%;object-fit:contain}
|
|
86
|
+
.card .th .ovmini{position:absolute;border:1.5px solid var(--gold);border-radius:50%;background:rgba(255,207,92,.12)}
|
|
87
|
+
.card .nm{font-size:12px;margin:6px 0 4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
|
|
88
|
+
.card .nm:hover{color:var(--gold)}
|
|
89
|
+
.card .row{display:flex;gap:6px;align-items:center;font-size:11px;color:var(--dim)}
|
|
90
|
+
.card .row input{width:58px;padding:3px}
|
|
91
|
+
#allbar{display:flex;gap:8px;align-items:center;margin:0 0 12px;font-size:12px;color:var(--dim)}
|
|
92
|
+
#mixsec{margin-top:16px}
|
|
93
|
+
#mixbox{position:relative;height:420px;overflow-x:auto;overflow-y:hidden;background:var(--panel);
|
|
94
|
+
border:1px solid var(--line);border-radius:10px}
|
|
95
|
+
#mixinner{position:relative;height:100%}
|
|
96
|
+
#mixground{position:absolute;left:0;right:0;bottom:0;height:44px;background:#1d2b1a;border-top:1px solid #2c4527}
|
|
97
|
+
.mixsprite{position:absolute;image-rendering:auto;pointer-events:none}
|
|
98
|
+
.mixlabel{position:absolute;font-size:10px;color:var(--dim);text-align:center;transform:translateX(-50%)}
|
|
99
|
+
.ruler{position:absolute;left:0;right:0;bottom:44px;top:0;pointer-events:none;
|
|
100
|
+
background:
|
|
101
|
+
repeating-linear-gradient(to top,rgba(255,255,255,.28) 0 1px,transparent 1px 240px),
|
|
102
|
+
repeating-linear-gradient(to top,rgba(255,255,255,.16) 0 1px,transparent 1px 120px),
|
|
103
|
+
repeating-linear-gradient(to top,rgba(255,255,255,.07) 0 1px,transparent 1px 24px)}
|
|
104
|
+
.rlabel{position:absolute;left:4px;font-size:9px;color:var(--dim)}
|
|
105
|
+
.rlabel.b{font-weight:700;color:var(--ink)}
|
|
106
|
+
.strip{height:150px;flex:none;border-top:1px solid var(--line);background:var(--panel);display:flex;gap:10px;padding:12px;overflow-x:auto}
|
|
107
|
+
.vig{position:relative;height:100%;aspect-ratio:1;flex:none;border:2px solid var(--line);border-radius:10px;overflow:hidden;cursor:pointer;
|
|
108
|
+
background:conic-gradient(var(--chk1) 90deg,var(--chk2) 0 180deg,var(--chk1) 0 270deg,var(--chk2) 0) 0/12px 12px}
|
|
109
|
+
.vig img{width:100%;height:100%;object-fit:contain}
|
|
110
|
+
.vig.active{border-color:var(--ink)}
|
|
111
|
+
.vig.picked{border-color:var(--gold);box-shadow:0 0 0 2px var(--gold) inset}
|
|
112
|
+
.vig.alt{border-color:var(--teal)}
|
|
113
|
+
.vig .st{position:absolute;top:4px;right:6px;font-size:16px;filter:drop-shadow(0 1px 2px #000)}
|
|
114
|
+
.vig .btns{position:absolute;bottom:4px;left:4px;right:4px;display:flex;gap:4px;justify-content:center;opacity:0;transition:opacity .15s}
|
|
115
|
+
.vig:hover .btns{opacity:1}
|
|
116
|
+
.vig .btns button{padding:2px 7px;font-size:11px;border-radius:6px}
|
|
117
|
+
.vig .btns .b-alt{background:var(--teal);color:#06342c}
|
|
118
|
+
.vig .btns .b-del{background:var(--red);color:#fff}
|
|
119
|
+
.strip .ph{color:var(--dim);align-self:center;padding:0 12px}
|
|
120
|
+
.drawer{flex:none;border-top:1px solid var(--line);background:var(--panel)}
|
|
121
|
+
.tabs{display:flex;gap:2px;padding:6px 12px 0}
|
|
122
|
+
.tabs button{background:var(--panel2);color:var(--dim);border:1px solid var(--line);border-bottom:0;border-radius:8px 8px 0 0;
|
|
123
|
+
padding:5px 12px;font-size:12px}
|
|
124
|
+
.tabs button.on{color:var(--ink);background:var(--bg)}
|
|
125
|
+
.tabs .sp{flex:1}
|
|
126
|
+
#drawerbody{display:none;padding:10px 12px}
|
|
127
|
+
#drawerbody.on{display:block}
|
|
128
|
+
#jsonta{min-height:140px}
|
|
129
|
+
.jrow{display:flex;gap:8px;margin-top:6px;align-items:center}
|
|
130
|
+
.jmsg{font-size:12px;color:var(--dim)}
|
|
131
|
+
kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:0 5px;font-size:11px}
|
|
132
|
+
.help{font-size:11px;color:var(--dim);padding:4px 16px;border-top:1px solid var(--line)}
|
|
133
|
+
</style></head><body>
|
|
134
|
+
<aside><div class=shead>Assets · <span id=count></span></div><div id=list></div></aside>
|
|
135
|
+
<main>
|
|
136
|
+
<div class=errs id=errs></div>
|
|
137
|
+
<div class=bar id=topbar>
|
|
138
|
+
<div class=ctl>
|
|
139
|
+
<select id=n><option>1</option><option selected>3</option><option>6</option><option>9</option><option>12</option></select>
|
|
140
|
+
<button id=go disabled>Generate</button>
|
|
141
|
+
<div id=detnote class=detnote style="display:none">deterministic — rolls identical</div>
|
|
142
|
+
<button id=qclear class=ghost title="drop queued jobs">clear queue</button>
|
|
143
|
+
</div>
|
|
144
|
+
<div class=promptwrap>
|
|
145
|
+
<textarea id=prompt placeholder="select an asset ←"></textarea>
|
|
146
|
+
<div class=jrow><button id=saveprompt class=ghost disabled>save prompt → overlay</button>
|
|
147
|
+
<button id=copyprompt class=ghost>copy</button><span class=jmsg id=promptmsg></span></div>
|
|
148
|
+
</div>
|
|
149
|
+
<div class=ctl><div class=status id=status></div></div>
|
|
150
|
+
</div>
|
|
151
|
+
<div class=info id=info></div>
|
|
152
|
+
<div class=stage id=stage><div class=ph>Pick an asset on the left, then Generate.</div>
|
|
153
|
+
<div class=plumb id=plumbL></div><div class=plumb id=plumbR></div>
|
|
154
|
+
<div id=ov><div class=hdl></div><div class=arrow></div><div class=sz id=ovsz></div></div>
|
|
155
|
+
<div id=rectbar>
|
|
156
|
+
<label>size <input type=number id=rsize step=0.1 min=0.1></label>
|
|
157
|
+
<button id=rlegs class=ghost>legs</button>
|
|
158
|
+
<input type=color id=rcolor title="leg color">
|
|
159
|
+
<button id=rlegsx class=ghost title="remove legs">✕</button>
|
|
160
|
+
<span class=ro id=rreadout></span>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
<div id=catgrid>
|
|
164
|
+
<div id=allbar><label>size for ALL <span id=allcat></span> <input type=number id=allsize step=0.1 min=0.1></label>
|
|
165
|
+
<button id=allapply class=ghost>apply</button><span class=jmsg id=allmsg></span></div>
|
|
166
|
+
<div class=cards id=cards></div>
|
|
167
|
+
<div id=mixsec><div id=mixbox><div id=mixinner><div class=ruler></div><div id=mixground></div></div></div></div>
|
|
168
|
+
</div>
|
|
169
|
+
<div class=strip id=strip><div class=ph>no rolls yet</div></div>
|
|
170
|
+
<div class=drawer>
|
|
171
|
+
<div class=tabs>
|
|
172
|
+
<button data-tab=asset>asset</button><button data-tab=meta>roll meta</button><span id=ctabs></span>
|
|
173
|
+
<span class=sp></span><button id=drawertoggle class=ghost>JSON ▾</button>
|
|
174
|
+
</div>
|
|
175
|
+
<div id=drawerbody>
|
|
176
|
+
<div id=jsonwrap>
|
|
177
|
+
<textarea id=jsonta spellcheck=false></textarea>
|
|
178
|
+
<div class=jrow><button id=jsonsave>save</button><span class=jmsg id=jsonmsg></span></div>
|
|
179
|
+
</div>
|
|
180
|
+
<div id=panelhost style="display:none"></div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
<div class=help><kbd>←</kbd><kbd>→</kbd> roll · <kbd>Enter</kbd> pick · <kbd>A</kbd> alt · <kbd>X</kbd> discard — picks ship via the kit's ship hook when present · category title = grid view</div>
|
|
184
|
+
</main>
|
|
185
|
+
<script>
|
|
186
|
+
const $=s=>document.querySelector(s)
|
|
187
|
+
let assets=[],cur=null,rolls=[],active=null,tab='asset',openCats=new Set()
|
|
188
|
+
let customs=[],panelMod=null,panelKey=null // repo-defined editor panels (custom tabs)
|
|
189
|
+
let lastSideSig=null,lastRollSig=null,catMode=null,lastCatSig=null,catRows=[]
|
|
190
|
+
let rect=null,rectRatio=0.6,hasGeom=false,saveTimer=null
|
|
191
|
+
const TEX=1024,MIX_UNIT=24,MIX_GROUND=44
|
|
192
|
+
const V=()=>Date.now()>>13 // ~8s cache buckets: thumbs refresh without unique-URL spam
|
|
193
|
+
const R=u=>u.replace(/^\\//,'') // resolve against the page URL: '/' standalone AND '/assets/' under the shipkit hub
|
|
194
|
+
const j=async(u,o)=>{const r=await fetch(R(u),o);return r.json()}
|
|
195
|
+
const post=(u,b)=>j(u,{method:'POST',body:JSON.stringify(b)})
|
|
196
|
+
const imgUrl=(id,n)=>R('/img?id='+encodeURIComponent(id)+'&name='+encodeURIComponent(n)+'&t='+V())
|
|
197
|
+
const thumbUrl=id=>R('/api/thumb?id='+encodeURIComponent(id)+'&t='+V())
|
|
198
|
+
const esc=s=>String(s).replace(/&/g,'&').replace(/</g,'<').replace(/"/g,'"')
|
|
199
|
+
const jsq=s=>String(s).replace(/\\\\/g,'\\\\\\\\').replace(/'/g,"\\\\'")
|
|
200
|
+
// ── hash deep-links: #<asset-id> or #cat-<Category> ──
|
|
201
|
+
function setHash(h){if(location.hash.slice(1)!==h)location.hash=h}
|
|
202
|
+
function restoreFromHash(){
|
|
203
|
+
const h=decodeURIComponent(location.hash.slice(1))
|
|
204
|
+
if(!h)return
|
|
205
|
+
if(h.startsWith('cat-')){const c=h.slice(4);if(assets.some(a=>a.cat===c)&&catMode!==c)showCat(c,false);return}
|
|
206
|
+
if(assets.some(a=>a.id===h)&&cur!==h)selectAsset(h,false)
|
|
207
|
+
}
|
|
208
|
+
window.addEventListener('hashchange',restoreFromHash)
|
|
209
|
+
function toggleFold(c){openCats.has(c)?openCats.delete(c):openCats.add(c);lastSideSig=null;renderSide(assets)}
|
|
210
|
+
function renderSide(list){
|
|
211
|
+
assets=list;$('#count').textContent=list.length
|
|
212
|
+
const sig=JSON.stringify(list.map(a=>[a.id,a.rolls,a.picked]))+'|'+cur+'|'+catMode+'|'+[...openCats].join(',')
|
|
213
|
+
if(sig===lastSideSig&&$('#list').innerHTML!=='')return; lastSideSig=sig
|
|
214
|
+
const cats=[...new Set(list.map(a=>a.cat))]
|
|
215
|
+
$('#list').innerHTML=cats.map(c=>{
|
|
216
|
+
const open=openCats.has(c)
|
|
217
|
+
const head='<div class=gname><span><span class=fold onclick="toggleFold(\\''+jsq(c)+'\\')">'+(open?'▾':'▸')+'</span> '+
|
|
218
|
+
'<span class=cn onclick="showCat(\\''+jsq(c)+'\\')" title="grid view">'+esc(c)+'</span>'+
|
|
219
|
+
'</span><span class=catgen>'+[1,2,3].map(n=>'<button onclick="genCat(\\''+jsq(c)+'\\','+n+')">+'+n+'</button>').join('')+'</span></div>'
|
|
220
|
+
if(!open)return head
|
|
221
|
+
return head+list.filter(a=>a.cat===c).map(a=>
|
|
222
|
+
'<div class="arow'+(a.id===cur?' sel':'')+'" data-id="'+esc(a.id)+'" onclick="selectAsset(\\''+jsq(a.id)+'\\')"><div class=lb><b>'+esc(a.label)+
|
|
223
|
+
'</b><span>'+a.rolls+' rolls</span></div><div class="dot '+(a.picked?'done':(a.rolls?'some':''))+'"></div></div>').join('')
|
|
224
|
+
}).join('')
|
|
225
|
+
}
|
|
226
|
+
// ── per-asset info surface (W2-2): backend, workflow provenance, ship target,
|
|
227
|
+
// meta file, kit source — so 'bun assets' is never a blank stare ──
|
|
228
|
+
function renderInfo(info){
|
|
229
|
+
const box=$('#info')
|
|
230
|
+
if(!info){box.style.display='none';box.innerHTML='';return}
|
|
231
|
+
const chip=(k,v,cls)=>'<span class="chip'+(cls?' '+cls:'')+'"><b>'+esc(k)+'</b> '+v+'</span>'
|
|
232
|
+
const chips=[chip('backend',esc(info.backend))]
|
|
233
|
+
if(info.workflow){const w=info.workflow
|
|
234
|
+
if(w.id)chips.push(chip('workflow',esc(w.id)+(w.path?' <code>'+esc(w.path)+'</code>':'')))
|
|
235
|
+
else chips.push(chip('workflow',esc(w.note||'unresolved'),'warn'))}
|
|
236
|
+
const sh=info.ship
|
|
237
|
+
const shv=sh.mode==='hook'?'<code>'+esc(sh.path)+'</code>':
|
|
238
|
+
sh.mode==='built-in'?'built-in hook — '+esc(sh.note||''):esc(sh.note||'pick.json only')
|
|
239
|
+
chips.push(chip('ship',shv))
|
|
240
|
+
if(info.metaFile)chips.push(chip('meta','<code>'+esc(info.metaFile)+'</code>'))
|
|
241
|
+
chips.push(chip('kit','<code>'+esc(info.kitSource)+'</code> · '+esc(info.kindId)))
|
|
242
|
+
box.innerHTML=chips.join('');box.style.display='flex'
|
|
243
|
+
}
|
|
244
|
+
// ── asset view ──
|
|
245
|
+
async function selectAsset(id,writeHash=true){
|
|
246
|
+
exitCat()
|
|
247
|
+
cur=id;active=null;lastSideSig=null;lastRollSig=null;rect=null
|
|
248
|
+
const a=assets.find(x=>x.id===id)
|
|
249
|
+
if(!a)return
|
|
250
|
+
openCats.add(a.cat);renderSide(assets)
|
|
251
|
+
if(writeHash)setHash(id)
|
|
252
|
+
const row=document.querySelector('.arow[data-id="'+CSS.escape(id)+'"]')
|
|
253
|
+
if(row)row.scrollIntoView({block:'center'})
|
|
254
|
+
const r=await j('/api/asset?id='+encodeURIComponent(id))
|
|
255
|
+
if(cur!==id)return // selection changed mid-fetch — a late response must not clobber it
|
|
256
|
+
hasGeom=!!(r.asset&&r.asset.geometry)&&!a.icon
|
|
257
|
+
customs=(r.asset&&r.asset.custom)||[]
|
|
258
|
+
if(tab.indexOf('c:')===0&&!customs.some(c=>'c:'+c.id===tab))tab='asset' // new asset may not carry the open panel
|
|
259
|
+
renderCustomTabs()
|
|
260
|
+
$('#prompt').value=r.asset?r.asset.prompt:a.prompt
|
|
261
|
+
$('#go').disabled=false;$('#saveprompt').disabled=false;$('#promptmsg').textContent=''
|
|
262
|
+
// deterministic canvas kinds: a slot machine with one outcome — cap rolls at 1
|
|
263
|
+
const det=!!(r.info&&r.info.deterministic),n=$('#n')
|
|
264
|
+
n.disabled=det;if(det)n.value='1';$('#detnote').style.display=det?'':'none'
|
|
265
|
+
renderInfo(r.info)
|
|
266
|
+
await renderRolls(true);loadTab()
|
|
267
|
+
}
|
|
268
|
+
function setStage(src){
|
|
269
|
+
const st=$('#stage');let img=st.querySelector('img.roll')
|
|
270
|
+
if(!img){st.querySelector('.ph')?.remove();img=document.createElement('img');img.className='roll';st.appendChild(img);img.onload=positionOverlay}
|
|
271
|
+
img.src=src
|
|
272
|
+
}
|
|
273
|
+
async function renderRolls(reset){
|
|
274
|
+
if(!cur)return
|
|
275
|
+
const r=await j('/api/asset?id='+encodeURIComponent(cur))
|
|
276
|
+
rolls=r.rolls
|
|
277
|
+
const sig=cur+'|'+rolls.map(x=>x.name+(x.picked?'*':'')+(x.alt?'~':'')).join(',')
|
|
278
|
+
if(!reset&&sig===lastRollSig)return; lastRollSig=sig
|
|
279
|
+
const strip=$('#strip')
|
|
280
|
+
if(!rolls.length){strip.innerHTML='<div class=ph>no rolls — hit Generate</div>';hideOverlay()
|
|
281
|
+
const st=$('#stage');st.querySelector('img.roll')?.remove()
|
|
282
|
+
if(!st.querySelector('.ph')){const ph=document.createElement('div');ph.className='ph';ph.textContent='no rolls yet';st.appendChild(ph)}
|
|
283
|
+
active=null;return}
|
|
284
|
+
if(reset||!rolls.some(x=>x.name===active))active=(rolls.find(x=>x.picked)||rolls[0]).name
|
|
285
|
+
setStage(imgUrl(cur,active))
|
|
286
|
+
if(hasGeom)await loadRect()
|
|
287
|
+
strip.innerHTML=rolls.map(x=>
|
|
288
|
+
'<div class="vig'+(x.name===active?' active':'')+(x.picked?' picked':'')+(x.alt?' alt':'')+'" data-n="'+x.name+'">'+
|
|
289
|
+
'<img src="'+imgUrl(cur,x.name)+'">'+(x.picked?'<span class=st>★</span>':'')+
|
|
290
|
+
'<div class=btns><button class=b-pick>pick</button><button class=b-alt>alt</button><button class=b-del>✕</button></div></div>').join('')
|
|
291
|
+
strip.querySelectorAll('.vig').forEach(v=>{
|
|
292
|
+
const n=v.dataset.n
|
|
293
|
+
v.onmouseenter=()=>setStage(imgUrl(cur,n))
|
|
294
|
+
v.onmouseleave=()=>setStage(imgUrl(cur,active))
|
|
295
|
+
v.onclick=()=>{active=n;lastRollSig=null;renderRolls(false);if(tab!=='asset')loadTab()} // meta + custom panels are per-roll
|
|
296
|
+
v.querySelector('.b-pick').onclick=e=>{e.stopPropagation();pickRoll(n)}
|
|
297
|
+
v.querySelector('.b-alt').onclick=e=>{e.stopPropagation();post('/api/alt',{id:cur,name:n}).then(()=>renderRolls(false))}
|
|
298
|
+
v.querySelector('.b-del').onclick=e=>{e.stopPropagation();discard(n)}
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
async function pickRoll(n){
|
|
302
|
+
const res=await post('/api/pick',{id:cur,name:n})
|
|
303
|
+
$('#status').textContent=res.error?('ship FAILED: '+res.error):(res.shipped?('shipped → '+res.shipped):'picked (pick.json)')
|
|
304
|
+
active=n;lastRollSig=null;lastSideSig=null;await renderRolls(false)
|
|
305
|
+
}
|
|
306
|
+
async function discard(n){
|
|
307
|
+
await post('/api/discard',{id:cur,name:n})
|
|
308
|
+
if(active===n)active=null
|
|
309
|
+
lastRollSig=null;lastSideSig=null;await renderRolls(true)
|
|
310
|
+
}
|
|
311
|
+
async function genCat(c,n){await post('/api/generate-cat',{cat:c,count:n});heartbeat()}
|
|
312
|
+
$('#go').onclick=async()=>{if(!cur)return;await post('/api/generate',{id:cur,prompt:$('#prompt').value,count:+$('#n').value});heartbeat()}
|
|
313
|
+
$('#qclear').onclick=()=>post('/api/queue/clear')
|
|
314
|
+
$('#saveprompt').onclick=async()=>{
|
|
315
|
+
const r=await post('/api/prompt',{id:cur,prompt:$('#prompt').value})
|
|
316
|
+
$('#promptmsg').textContent=r.error?('FAIL: '+r.error):'saved (overlay)'
|
|
317
|
+
setTimeout(()=>$('#promptmsg').textContent='',1500)
|
|
318
|
+
}
|
|
319
|
+
$('#copyprompt').onclick=()=>{navigator.clipboard.writeText($('#prompt').value);$('#promptmsg').textContent='copied'
|
|
320
|
+
setTimeout(()=>$('#promptmsg').textContent='',900)}
|
|
321
|
+
$('#prompt').addEventListener('input',e=>{e.target.style.height='auto';e.target.style.height=e.target.scrollHeight+'px'})
|
|
322
|
+
// ── geometry overlay ──
|
|
323
|
+
async function loadRect(){
|
|
324
|
+
if(!hasGeom||!cur||!active){hideOverlay();return}
|
|
325
|
+
const r=await j('/api/rect?id='+encodeURIComponent(cur)+'&name='+encodeURIComponent(active))
|
|
326
|
+
if(r.error){hideOverlay();return}
|
|
327
|
+
rect=r.rect
|
|
328
|
+
rectRatio=rect.w>0?rect.h/rect.w:0.6 // client re-locks with the server's ratio
|
|
329
|
+
positionOverlay();renderRectbar()
|
|
330
|
+
}
|
|
331
|
+
function imgBox(){
|
|
332
|
+
const img=$('#stage img.roll')
|
|
333
|
+
if(!img||!img.naturalWidth)return null
|
|
334
|
+
const b=img.getBoundingClientRect(),st=$('#stage').getBoundingClientRect()
|
|
335
|
+
const scale=Math.min(b.width/img.naturalWidth,b.height/img.naturalHeight)
|
|
336
|
+
const w=img.naturalWidth*scale,h=img.naturalHeight*scale
|
|
337
|
+
return{scale,x:b.left-st.left+(b.width-w)/2,y:b.top-st.top+(b.height-h)/2}
|
|
338
|
+
}
|
|
339
|
+
function hideOverlay(){$('#ov').style.display='none';$('#rectbar').style.display='none'
|
|
340
|
+
$('#plumbL').style.display='none';$('#plumbR').style.display='none'
|
|
341
|
+
document.querySelectorAll('.leg,.pill').forEach(e=>e.remove())}
|
|
342
|
+
function positionOverlay(){
|
|
343
|
+
if(!hasGeom||!rect){hideOverlay();return}
|
|
344
|
+
const bx=imgBox();if(!bx){hideOverlay();return}
|
|
345
|
+
const ov=$('#ov'),px=v=>v*bx.scale
|
|
346
|
+
ov.style.display='block'
|
|
347
|
+
ov.style.left=(bx.x+px(rect.x))+'px';ov.style.top=(bx.y+px(rect.y))+'px'
|
|
348
|
+
ov.style.width=px(rect.w)+'px';ov.style.height=px(rect.h)+'px'
|
|
349
|
+
$('#ovsz').textContent=(+rect.size.toFixed(2))+'m'
|
|
350
|
+
const pl=$('#plumbL'),pr=$('#plumbR')
|
|
351
|
+
pl.style.display=pr.style.display='block'
|
|
352
|
+
pl.style.left=(bx.x+px(rect.x))+'px';pr.style.left=(bx.x+px(rect.x+rect.w))+'px'
|
|
353
|
+
document.querySelectorAll('.leg,.pill').forEach(e=>e.remove())
|
|
354
|
+
const tags=['L·front','L·back','R·back','R·front']
|
|
355
|
+
;(rect.legs||[]).forEach((l,i)=>{
|
|
356
|
+
const far=i<2,el=document.createElement('div')
|
|
357
|
+
el.className='leg '+(far?'far':'near');el.dataset.i=i
|
|
358
|
+
el.style.color=far?'var(--blue)':'var(--red)'
|
|
359
|
+
el.style.left=(bx.x+px(l.x))+'px';el.style.top=(bx.y+px(l.y))+'px'
|
|
360
|
+
el.style.width=px(l.w)+'px';el.style.height=px(l.h)+'px'
|
|
361
|
+
el.innerHTML='<div class=hip></div><div class=foot></div><div class=tag>'+tags[i]+'</div>'+(i===0?'<div class=lh></div>':'')
|
|
362
|
+
$('#stage').appendChild(el)
|
|
363
|
+
const pill=document.createElement('div')
|
|
364
|
+
pill.className='pill'+(far?'':' near')
|
|
365
|
+
pill.style.left=(bx.x+px(l.x))+'px';pill.style.top=(bx.y+px(l.y))+'px'
|
|
366
|
+
pill.style.width=px(l.w)+'px';pill.style.height=px(l.h)+'px'
|
|
367
|
+
pill.style.background=rect.legColor||'#c8a070'
|
|
368
|
+
$('#stage').appendChild(pill)
|
|
369
|
+
dragLeg(el,i)
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
function renderRectbar(){
|
|
373
|
+
if(!hasGeom||!rect){$('#rectbar').style.display='none';return}
|
|
374
|
+
$('#rectbar').style.display='flex'
|
|
375
|
+
$('#rsize').value=rect.size
|
|
376
|
+
$('#rcolor').value=/^#[0-9a-f]{6}$/i.test(rect.legColor||'')?rect.legColor:'#c8a070'
|
|
377
|
+
$('#rlegsx').style.display=rect.legs?'':'none'
|
|
378
|
+
const cx=Math.round(rect.x+rect.w/2),cy=Math.round(rect.y+rect.h/2)
|
|
379
|
+
$('#rreadout').textContent='cx'+cx+' cy'+cy+' w'+Math.round(rect.w)+'px'
|
|
380
|
+
}
|
|
381
|
+
function saveRect(){ // 250ms trailing debounce → POST; server re-cleans + measures art
|
|
382
|
+
clearTimeout(saveTimer)
|
|
383
|
+
saveTimer=setTimeout(async()=>{
|
|
384
|
+
if(!cur||!active||!rect)return
|
|
385
|
+
const r=await post('/api/rect',{id:cur,name:active,rect})
|
|
386
|
+
if(r.rect){rect=r.rect;positionOverlay();renderRectbar()}
|
|
387
|
+
else if(r.error)$('#status').textContent='rect FAIL: '+r.error
|
|
388
|
+
},250)
|
|
389
|
+
}
|
|
390
|
+
$('#rsize').oninput=()=>{if(!rect)return;rect.size=Math.max(.1,+$('#rsize').value||rect.size);positionOverlay();saveRect()}
|
|
391
|
+
$('#rcolor').oninput=()=>{if(!rect)return;rect.legColor=$('#rcolor').value;positionOverlay();saveRect()}
|
|
392
|
+
$('#rlegs').onclick=async()=>{ // non-destructive: server creates defaults only when absent
|
|
393
|
+
if(!cur||!active)return
|
|
394
|
+
const r=await post('/api/legs-default',{id:cur,name:active})
|
|
395
|
+
if(r.rect){rect=r.rect;positionOverlay();renderRectbar()}
|
|
396
|
+
}
|
|
397
|
+
$('#rlegsx').onclick=()=>{
|
|
398
|
+
if(!rect||!rect.legs||!confirm('remove all 4 legs?'))return
|
|
399
|
+
delete rect.legs;delete rect.legColor;positionOverlay();renderRectbar();saveRect()
|
|
400
|
+
}
|
|
401
|
+
// oval drag/resize (legs follow: move 1:1, resize scales around left-tip + center-y anchor)
|
|
402
|
+
;(()=>{
|
|
403
|
+
const ov=$('#ov')
|
|
404
|
+
let mode=null,start=null
|
|
405
|
+
ov.addEventListener('pointerdown',e=>{
|
|
406
|
+
if(!rect)return
|
|
407
|
+
mode=e.target.classList.contains('hdl')?'resize':'move'
|
|
408
|
+
const legs=(rect.legs||[]).map(l=>({...l}))
|
|
409
|
+
start={px:e.clientX,py:e.clientY,x:rect.x,y:rect.y,w:rect.w,legs,cy:rect.y+rect.h/2}
|
|
410
|
+
ov.setPointerCapture(e.pointerId);e.preventDefault()
|
|
411
|
+
})
|
|
412
|
+
ov.addEventListener('pointermove',e=>{
|
|
413
|
+
if(!mode||!start||!rect)return
|
|
414
|
+
const bx=imgBox();if(!bx)return
|
|
415
|
+
const dx=(e.clientX-start.px)/bx.scale,dy=(e.clientY-start.py)/bx.scale
|
|
416
|
+
if(mode==='move'){
|
|
417
|
+
rect.x=Math.max(0,start.x+dx);rect.y=Math.max(0,start.y+dy)
|
|
418
|
+
rect.legs=(start.legs||[]).length?start.legs.map(l=>({...l,x:l.x+(rect.x-start.x),y:l.y+(rect.y-start.y)})):rect.legs
|
|
419
|
+
}else{
|
|
420
|
+
rect.w=Math.max(20,start.w+dx)
|
|
421
|
+
rect.h=rect.w*rectRatio
|
|
422
|
+
rect.y=start.cy-rect.h/2 // anchored: left edge + center-y
|
|
423
|
+
const f=rect.w/start.w
|
|
424
|
+
if((start.legs||[]).length)rect.legs=start.legs.map(l=>({x:start.x+(l.x-start.x)*f,y:start.cy+(l.y-start.cy)*f,w:l.w*f,h:l.h*f}))
|
|
425
|
+
}
|
|
426
|
+
positionOverlay();renderRectbar();saveRect()
|
|
427
|
+
})
|
|
428
|
+
ov.addEventListener('pointerup',()=>{mode=null;start=null;saveRect()})
|
|
429
|
+
})()
|
|
430
|
+
function dragLeg(el,i){
|
|
431
|
+
let mode=null,start=null
|
|
432
|
+
el.addEventListener('pointerdown',e=>{
|
|
433
|
+
if(!rect||!rect.legs)return
|
|
434
|
+
mode=e.target.classList.contains('lh')?'resize':'move'
|
|
435
|
+
start={px:e.clientX,py:e.clientY,legs:rect.legs.map(l=>({...l}))}
|
|
436
|
+
el.setPointerCapture(e.pointerId);e.preventDefault();e.stopPropagation()
|
|
437
|
+
})
|
|
438
|
+
el.addEventListener('pointermove',e=>{
|
|
439
|
+
if(!mode||!start||!rect||!rect.legs)return
|
|
440
|
+
const bx=imgBox();if(!bx)return
|
|
441
|
+
const dx=(e.clientX-start.px)/bx.scale,dy=(e.clientY-start.py)/bx.scale
|
|
442
|
+
if(mode==='move'){const o=start.legs[i];rect.legs[i]={...o,x:o.x+dx,y:o.y+dy}}
|
|
443
|
+
else{ // one size for all 4 — every leg keeps its own FOOT planted
|
|
444
|
+
const o=start.legs[i],w=Math.max(6,o.w+dx),h=Math.max(6,o.h-dy)
|
|
445
|
+
rect.legs=start.legs.map(l=>({x:l.x+(l.w-w)/2,y:l.y+l.h-h,w,h}))
|
|
446
|
+
}
|
|
447
|
+
positionOverlay();saveRect()
|
|
448
|
+
})
|
|
449
|
+
el.addEventListener('pointerup',()=>{mode=null;start=null;saveRect()})
|
|
450
|
+
}
|
|
451
|
+
window.addEventListener('resize',positionOverlay)
|
|
452
|
+
// ── category grid + size-true lineup ──
|
|
453
|
+
async function showCat(c,writeHash=true){
|
|
454
|
+
catMode=c;cur=null;lastSideSig=null;lastCatSig=null
|
|
455
|
+
if(writeHash)setHash('cat-'+c)
|
|
456
|
+
$('#topbar').style.display='none';$('#stage').style.display='none';$('#strip').style.display='none';$('#info').style.display='none'
|
|
457
|
+
$('#catgrid').style.display='block';$('#allcat').textContent=c
|
|
458
|
+
openCats.add(c);renderSide(assets)
|
|
459
|
+
await fillCat(true)
|
|
460
|
+
}
|
|
461
|
+
function exitCat(){
|
|
462
|
+
if(catMode===null)return
|
|
463
|
+
catMode=null;lastCatSig=null
|
|
464
|
+
$('#topbar').style.display='';$('#stage').style.display='';$('#strip').style.display=''
|
|
465
|
+
$('#catgrid').style.display='none'
|
|
466
|
+
}
|
|
467
|
+
async function fillCat(force){
|
|
468
|
+
if(!catMode)return
|
|
469
|
+
const mode=catMode
|
|
470
|
+
const r=await j('/api/catrects?cat='+encodeURIComponent(mode))
|
|
471
|
+
if(catMode!==mode)return // user navigated away mid-fetch
|
|
472
|
+
catRows=r.rows||[]
|
|
473
|
+
const sig=JSON.stringify(catRows)
|
|
474
|
+
if(!force&&sig===lastCatSig)return; lastCatSig=sig
|
|
475
|
+
const scroll=$('#catgrid').scrollTop
|
|
476
|
+
const pc=v=>(v/(TEX/100))+'%'
|
|
477
|
+
$('#cards').innerHTML=catRows.map(row=>{
|
|
478
|
+
const fl=row.rect&&row.rect.flip
|
|
479
|
+
const ovm=row.rect?('<div class=ovmini style="left:'+pc(fl?TEX-row.rect.x-row.rect.w:row.rect.x)+';top:'+pc(row.rect.y)+
|
|
480
|
+
';width:'+pc(row.rect.w)+';height:'+pc(row.rect.h)+'"></div>'):''
|
|
481
|
+
return '<div class=card><div class=th><img src="'+thumbUrl(row.id)+'"'+(fl?' style="transform:scaleX(-1)"':'')+'>'+ovm+'</div>'+
|
|
482
|
+
'<div class=nm onclick="selectAsset(\\''+jsq(row.id)+'\\')">'+esc(row.label)+'</div>'+
|
|
483
|
+
(row.icon?'':('<div class=row><label>size <input type=number step=0.1 min=0.1 value="'+(row.rect?row.rect.size:1)+'"'+
|
|
484
|
+
' onchange="setSize(\\''+jsq(row.id)+'\\',\\''+jsq(row.name)+'\\',this.value)"></label>'+
|
|
485
|
+
'<button class=ghost onclick="toggleFlip(\\''+jsq(row.id)+'\\',\\''+jsq(row.name)+'\\')">⇆ flip</button></div>'))+'</div>'
|
|
486
|
+
}).join('')
|
|
487
|
+
$('#catgrid').scrollTop=scroll
|
|
488
|
+
renderMix()
|
|
489
|
+
}
|
|
490
|
+
async function setSize(id,name,v){
|
|
491
|
+
const row=catRows.find(r=>r.id===id)
|
|
492
|
+
if(!row||!row.rect)return
|
|
493
|
+
row.rect.size=Math.max(.1,+v||row.rect.size)
|
|
494
|
+
await post('/api/rect',{id,name,rect:row.rect})
|
|
495
|
+
renderMix() // lineup only — no grid refetch
|
|
496
|
+
}
|
|
497
|
+
async function toggleFlip(id,name){
|
|
498
|
+
await post('/api/flip',{id,name})
|
|
499
|
+
lastCatSig=null;await fillCat(true)
|
|
500
|
+
}
|
|
501
|
+
$('#allapply').onclick=async()=>{
|
|
502
|
+
const v=Math.max(.1,+$('#allsize').value||0)
|
|
503
|
+
if(!v)return
|
|
504
|
+
for(const row of catRows.filter(r=>!r.icon&&r.rect)){row.rect.size=v;await post('/api/rect',{id:row.id,name:row.name,rect:row.rect})}
|
|
505
|
+
$('#allmsg').textContent='applied';setTimeout(()=>$('#allmsg').textContent='',1200)
|
|
506
|
+
lastCatSig=null;await fillCat(true)
|
|
507
|
+
}
|
|
508
|
+
function renderMix(){
|
|
509
|
+
const inner=$('#mixinner')
|
|
510
|
+
inner.querySelectorAll('.mixsprite,.mixlabel,.rlabel').forEach(e=>e.remove())
|
|
511
|
+
const items=catRows.filter(r=>!r.icon&&r.rect).sort((a,b)=>(a.rect.size-b.rect.size)||a.label.localeCompare(b.label))
|
|
512
|
+
const boxH=420,baseY=boxH-MIX_GROUND
|
|
513
|
+
let cx=30
|
|
514
|
+
for(const it of items){
|
|
515
|
+
const ts=it.rect.size*MIX_UNIT/it.rect.w
|
|
516
|
+
const slot=Math.max(it.rect.size*MIX_UNIT*1.35,TEX*ts*.62,40)
|
|
517
|
+
cx+=slot/2
|
|
518
|
+
const img=document.createElement('img')
|
|
519
|
+
img.className='mixsprite';img.src=thumbUrl(it.id)
|
|
520
|
+
const fx=it.rect.flip?TEX-it.rect.x-it.rect.w:it.rect.x
|
|
521
|
+
img.style.width=img.style.height=(TEX*ts)+'px'
|
|
522
|
+
img.style.left=(cx-(fx+it.rect.w/2)*ts)+'px'
|
|
523
|
+
img.style.top=(baseY-(it.rect.y+it.rect.h/2)*ts)+'px'
|
|
524
|
+
if(it.rect.flip)img.style.transform='scaleX(-1)'
|
|
525
|
+
img.style.zIndex=Math.round(cx)
|
|
526
|
+
inner.appendChild(img)
|
|
527
|
+
const lb=document.createElement('div')
|
|
528
|
+
lb.className='mixlabel';lb.textContent=it.label+' · '+(+it.rect.size.toFixed(2))
|
|
529
|
+
lb.style.left=cx+'px';lb.style.bottom='14px'
|
|
530
|
+
inner.appendChild(lb)
|
|
531
|
+
cx+=slot/2+10
|
|
532
|
+
}
|
|
533
|
+
inner.style.width=Math.max(cx+30,($('#mixbox').clientWidth||600))+'px'
|
|
534
|
+
for(let u=5;u*MIX_UNIT<baseY;u+=5){
|
|
535
|
+
const l=document.createElement('div')
|
|
536
|
+
l.className='rlabel'+(u%10===0?' b':'');l.textContent=u
|
|
537
|
+
l.style.bottom=(MIX_GROUND+u*MIX_UNIT-5)+'px'
|
|
538
|
+
inner.appendChild(l)
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// ── JSON drawer (asset = read-only kit view; meta = roll sidecar;
|
|
542
|
+
// c:<id> tabs = repo-defined custom panels, dynamic-imported modules) ──
|
|
543
|
+
$('#drawertoggle').onclick=()=>{$('#drawerbody').classList.toggle('on')}
|
|
544
|
+
document.querySelectorAll('.tabs [data-tab]').forEach(b=>b.onclick=()=>{tab=b.dataset.tab;loadTab();$('#drawerbody').classList.add('on')})
|
|
545
|
+
function renderCustomTabs(){
|
|
546
|
+
$('#ctabs').innerHTML=customs.map(c=>'<button data-tab="c:'+esc(c.id)+'">'+esc(c.id)+'</button>').join('')
|
|
547
|
+
$('#ctabs').querySelectorAll('[data-tab]').forEach(b=>b.onclick=()=>{tab=b.dataset.tab;loadTab();$('#drawerbody').classList.add('on')})
|
|
548
|
+
document.querySelectorAll('.tabs [data-tab]').forEach(b=>b.classList.toggle('on',b.dataset.tab===tab))
|
|
549
|
+
}
|
|
550
|
+
async function unmountPanel(){
|
|
551
|
+
if(panelMod&&typeof panelMod.unmount==='function'){try{await panelMod.unmount()}catch(e){$('#status').textContent='panel unmount FAILED: '+(e&&e.message||e)}}
|
|
552
|
+
panelMod=null;panelKey=null;$('#panelhost').innerHTML=''
|
|
553
|
+
}
|
|
554
|
+
function panelApi(c){ // frozen per mount: a roll/asset switch remounts with a fresh api
|
|
555
|
+
const id=cur,name=active
|
|
556
|
+
return{assetId:id,kindId:c.kind,roll:name,imgUrl:imgUrl(id,name),
|
|
557
|
+
getMeta:async()=>{const r=await post('/api/meta',{id,name});if(r.error)throw new Error(r.error);return r.text?JSON.parse(r.text):null},
|
|
558
|
+
saveMeta:async(data)=>{const r=await post('/api/meta',{id,name,text:JSON.stringify(data)});if(r.error)throw new Error(r.error)}}
|
|
559
|
+
}
|
|
560
|
+
async function mountPanel(c){
|
|
561
|
+
const key=cur+'|'+active+'|'+c.id
|
|
562
|
+
if(panelKey===key)return
|
|
563
|
+
await unmountPanel()
|
|
564
|
+
const host=$('#panelhost')
|
|
565
|
+
try{
|
|
566
|
+
const mod=await import(c.url)
|
|
567
|
+
if(typeof mod.mount!=='function')throw new Error(c.url+' must export mount(el, api)')
|
|
568
|
+
await mod.mount(host,panelApi(c))
|
|
569
|
+
panelMod=mod;panelKey=key
|
|
570
|
+
}catch(e){host.textContent='panel FAILED: '+(e&&e.message||e)} // loud in the drawer, never blank
|
|
571
|
+
}
|
|
572
|
+
async function loadTab(){
|
|
573
|
+
document.querySelectorAll('.tabs [data-tab]').forEach(b=>b.classList.toggle('on',b.dataset.tab===tab))
|
|
574
|
+
const isCustom=tab.indexOf('c:')===0
|
|
575
|
+
$('#jsonwrap').style.display=isCustom?'none':''
|
|
576
|
+
$('#panelhost').style.display=isCustom?'':'none'
|
|
577
|
+
if(isCustom){
|
|
578
|
+
const c=customs.find(x=>'c:'+x.id===tab)
|
|
579
|
+
if(c&&cur&&active)await mountPanel(c)
|
|
580
|
+
else{await unmountPanel();$('#panelhost').textContent=c?'no roll selected — generate or pick one first':'panel not on this asset'}
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
await unmountPanel()
|
|
584
|
+
const ta=$('#jsonta');$('#jsonmsg').textContent=''
|
|
585
|
+
$('#jsonsave').disabled=(tab==='asset') // kind/asset structure is git-reviewed TS — read-only here
|
|
586
|
+
ta.readOnly=(tab==='asset')
|
|
587
|
+
if(tab==='asset'){const r=cur?await j('/api/asset?id='+encodeURIComponent(cur)):null;ta.value=r&&r.asset?JSON.stringify(r.asset,null,2):''}
|
|
588
|
+
else ta.value=cur&&active?(await post('/api/meta',{id:cur,name:active})).text:''
|
|
589
|
+
}
|
|
590
|
+
$('#jsonsave').onclick=async()=>{
|
|
591
|
+
if(tab!=='meta'||!cur||!active)return
|
|
592
|
+
const r=await post('/api/meta',{id:cur,name:active,text:$('#jsonta').value})
|
|
593
|
+
$('#jsonmsg').textContent=r.error?('FAIL: '+r.error):'saved'
|
|
594
|
+
}
|
|
595
|
+
// ── keyboard ──
|
|
596
|
+
document.addEventListener('keydown',e=>{
|
|
597
|
+
if(e.target.tagName==='TEXTAREA'||e.target.tagName==='INPUT'||!cur||!rolls.length)return
|
|
598
|
+
const i=rolls.findIndex(x=>x.name===active)
|
|
599
|
+
if(e.key==='ArrowRight'&&i<rolls.length-1){active=rolls[i+1].name;lastRollSig=null;renderRolls(false)}
|
|
600
|
+
else if(e.key==='ArrowLeft'&&i>0){active=rolls[i-1].name;lastRollSig=null;renderRolls(false)}
|
|
601
|
+
else if(e.key==='Enter'&&active)pickRoll(active)
|
|
602
|
+
else if((e.key==='a'||e.key==='A')&&active)post('/api/alt',{id:cur,name:active}).then(()=>renderRolls(false))
|
|
603
|
+
else if((e.key==='x'||e.key==='X')&&active)discard(active)
|
|
604
|
+
else return
|
|
605
|
+
e.preventDefault()
|
|
606
|
+
})
|
|
607
|
+
// ── heartbeat: status + change-signature refreshes (grid OR asset view) ──
|
|
608
|
+
let firstBeat=true
|
|
609
|
+
async function heartbeat(){
|
|
610
|
+
const s=await j('/api/state')
|
|
611
|
+
renderSide(s.assets)
|
|
612
|
+
$('#status').textContent=s.generating?('generating '+(s.current||'…')+' '+s.done+'/'+s.total+(s.queued?(' · '+s.queued+' queued'):'')):''
|
|
613
|
+
const e=$('#errs')
|
|
614
|
+
if(s.errors.length){e.style.display='block';e.textContent=s.errors.join('\\n')}else e.style.display='none'
|
|
615
|
+
if(catMode)await fillCat(false)
|
|
616
|
+
else if(cur&&!document.querySelector('.vig:hover'))await renderRolls(false)
|
|
617
|
+
if(firstBeat){firstBeat=false;restoreFromHash()}
|
|
618
|
+
}
|
|
619
|
+
heartbeat();setInterval(heartbeat,1500)
|
|
620
|
+
</script></body></html>`
|