/ concepts / concept5-terminal.jsx
concept5-terminal.jsx
  1  // Concept 5 — DATA-DENSE TERMINAL GRID
  2  // Bloomberg-meets-Elite-Dangerous. Mono-everything, ultra-dense info grid.
  3  // Looks like a real trader's procurement terminal: tables, mini-sparklines,
  4  // green-on-black with market chatter scroll. Every pixel earns its place.
  5  
  6  function Concept5_Terminal({ seed = "C5-default" }) {
  7    const ship = makeShip(seed);
  8    const bg = "#000906";
  9    const panel = "#031510";
 10    const fg = "#d6ffe8";
 11    const g1 = "#4dff9a";        // up/active
 12    const g2 = "#1e7a4a";        // dim
 13    const y1 = "#ffe066";        // highlight
 14    const r1 = "#ff5a5a";        // down/alert
 15    const cyan = "#5de8ff";
 16    const mute = "#4a6b60";
 17  
 18    // sparkline
 19    const Spark = ({ seedStr, color = g1, w = 90, h = 22, up = true }) => {
 20      const rng = mulberry32(hashStr(seedStr));
 21      const pts = [];
 22      let y = 0.5;
 23      for (let i = 0; i < 24; i++) {
 24        y += (rng() - (up ? 0.4 : 0.55)) * 0.15;
 25        y = Math.max(0.05, Math.min(0.95, y));
 26        pts.push([i/23*w, h - y*h]);
 27      }
 28      return (
 29        <svg width={w} height={h} style={{display:"block"}}>
 30          <polyline points={pts.map(p=>p.join(",")).join(" ")} fill="none" stroke={color} strokeWidth={1.2}/>
 31          <circle cx={pts[pts.length-1][0]} cy={pts[pts.length-1][1]} r={1.8} fill={color}/>
 32        </svg>
 33      );
 34    };
 35  
 36    const Cell = ({ children, col = fg, right, bold }) => (
 37      <td style={{padding:"3px 8px", color:col, textAlign: right?"right":"left", fontWeight: bold?700:400, whiteSpace:"nowrap", borderRight:`1px solid ${g2}33`}}>{children}</td>
 38    );
 39  
 40    // Similar units row data
 41    const similar = Array.from({length: 7}).map((_,i) => makeShip(seed+"-alt-"+i));
 42  
 43    return (
 44      <div style={{
 45        width:1280, height:880, background: bg, color: fg,
 46        fontFamily:"'IBM Plex Mono', 'Menlo', monospace", fontSize:11, position:"relative", overflow:"hidden",
 47      }}>
 48        {/* TOP BAR — Bloomberg-ish */}
 49        <div style={{display:"flex", alignItems:"center", background:"#000", borderBottom:`1px solid ${g1}`, padding:"5px 12px", gap:16, fontSize:10}}>
 50          <span style={{color:g1, fontWeight:700, letterSpacing:3}}>FLEET//PROCURE</span>
 51          <span style={{color:mute}}>F1 CATALOG</span>
 52          <span style={{color:mute}}>F2 BROKERS</span>
 53          <span style={{color:mute}}>F3 ORDERS</span>
 54          <span style={{color:mute}}>F4 WATCH</span>
 55          <span style={{color:mute}}>F5 ESCROW</span>
 56          <span style={{color:mute}}>F6 FLEET</span>
 57          <span style={{marginLeft:"auto", color:g1}}>◉ LIVE</span>
 58          <span style={{color:mute}}>CMD:{seed.slice(0,6).toUpperCase()}</span>
 59          <span style={{color:y1}}>2396.114.0841z</span>
 60        </div>
 61  
 62        {/* COMMAND LINE */}
 63        <div style={{padding:"6px 12px", background:"#021008", borderBottom:`1px solid ${g2}66`, color:g1, fontSize:11, display:"flex", alignItems:"center", gap:8}}>
 64          <span style={{color:mute}}>›</span>
 65          <span>DESC {ship.serial}</span>
 66          <span style={{color:mute}}>—</span>
 67          <span style={{color:fg}}>{ship.mfg.name} · {ship.cls.name}</span>
 68          <span style={{marginLeft:"auto", color:y1, fontSize:10}}>[TAB] switch panel · [/] search · [P] procure</span>
 69        </div>
 70  
 71        {/* MAIN GRID */}
 72        <div style={{display:"grid", gridTemplateColumns:"380px 1fr 340px", gridTemplateRows:"auto 1fr auto", height:"calc(100% - 58px - 26px)"}}>
 73  
 74          {/* HEADER STRIP — spans */}
 75          <div style={{gridColumn:"1 / -1", borderBottom:`1px solid ${g2}66`, padding:"10px 14px", display:"flex", alignItems:"center", justifyContent:"space-between", background:panel}}>
 76            <div style={{display:"flex", alignItems:"center", gap:14}}>
 77              <span style={{fontFamily:"Orbitron", fontSize:22, fontWeight:700, color:fg, letterSpacing:1}}>{ship.model}</span>
 78              <span style={{color:g1, fontSize:12, letterSpacing:2}}>[{ship.serial}]</span>
 79              <span style={{color:g2, fontSize:10}}>·</span>
 80              <span style={{color:mute, fontSize:10, letterSpacing:1.5}}>{ship.cls.name.toUpperCase()} / {ship.cls.role.toUpperCase()}</span>
 81              <span style={{border:`1px solid ${g1}`, color:g1, padding:"1px 6px", fontSize:9, letterSpacing:1.5}}>{ship.avail}</span>
 82              <span style={{border:`1px solid ${y1}`, color:y1, padding:"1px 6px", fontSize:9, letterSpacing:1.5}}>{ship.cond}</span>
 83              <span style={{border:`1px solid ${cyan}`, color:cyan, padding:"1px 6px", fontSize:9, letterSpacing:1.5}}>{ship.faction.toUpperCase()}</span>
 84            </div>
 85            <div style={{display:"flex", alignItems:"baseline", gap:10}}>
 86              <span style={{color:mute, fontSize:10, letterSpacing:2}}>LAST</span>
 87              <span style={{fontFamily:"Orbitron", fontWeight:700, color:g1, fontSize:26}}>₵{formatCred(ship.price)}</span>
 88              <span style={{color:g1, fontSize:11}}>+2.3%</span>
 89              <Spark seedStr={seed+"-hdr"} color={g1} w={120} h={20} up={true}/>
 90            </div>
 91          </div>
 92  
 93          {/* LEFT COLUMN — KeyVal dump */}
 94          <div style={{borderRight:`1px solid ${g2}66`, background:panel, padding:"10px 0", overflow:"hidden"}}>
 95            <KV title="IDENTITY">
 96              <KVRow k="MFG"      v={ship.mfg.name}/>
 97              <KVRow k="YARD"     v={ship.mfg.loc}/>
 98              <KVRow k="MODEL"    v={ship.model}/>
 99              <KVRow k="SERIAL"   v={ship.serial} c={g1}/>
100              <KVRow k="TYPE"     v={`${ship.cls.code} · ${ship.cls.name}`}/>
101              <KVRow k="ERA"      v={`${ship.year} CMN`}/>
102              <KVRow k="FACTION"  v={ship.faction}/>
103              <KVRow k="FLAG"     v="COALITION BONDED"/>
104            </KV>
105            <KV title="DIMENSIONAL">
106              <KVRow k="LENGTH"   v={`${ship.length_m} m`}/>
107              <KVRow k="MASS"     v={`${ship.mass.toLocaleString()} t`}/>
108              <KVRow k="CARGO"    v={`${ship.cargo_t.toLocaleString()} t`}/>
109              <KVRow k="CREW"     v={`${ship.crew} pax`}/>
110              <KVRow k="LIFE SUP" v={`${ship.crew * 6} pax-days`}/>
111            </KV>
112            <KV title="PROPULSION">
113              <KVRow k="DRIVE"    v={ship.drive} c={cyan}/>
114              <KVRow k="TOP"      v={`${ship.topSpeed.toLocaleString()} m/s`}/>
115              <KVRow k="THRUST"   v={`${ship.thrust} g`}/>
116              <KVRow k="JUMP"     v={`${ship.jumpRange} ly`}/>
117              <KVRow k="MANEUV"   v={`${ship.maneuver}%`}/>
118            </KV>
119            <KV title="COMBAT">
120              <KVRow k="ARMOR"    v={`${ship.armor.toLocaleString()} mm`}/>
121              <KVRow k="SHIELD"   v={`${ship.shields.toLocaleString()} GJ`}/>
122              <KVRow k="HARDPT"   v={ship.hardpoints}/>
123              <KVRow k="SIG"      v={`${ship.signature} dB`} c={y1}/>
124            </KV>
125          </div>
126  
127          {/* CENTER — viewport + chart + similar */}
128          <div style={{borderRight:`1px solid ${g2}66`, display:"flex", flexDirection:"column"}}>
129            {/* Ortho viewport */}
130            <div style={{padding:"10px 14px", borderBottom:`1px solid ${g2}66`, background:bg}}>
131              <HdrBar left="◢ ORTHO VIEW" right={`SCALE 1:${Math.round(ship.length_m/8)} · ${ship.length_m}m × ${Math.round(ship.length_m*0.45)}m`}/>
132              <div style={{position:"relative", height:260, border:`1px solid ${g2}66`, background:"#01090a",
133                backgroundImage:`linear-gradient(${g2}22 1px, transparent 1px), linear-gradient(90deg, ${g2}22 1px, transparent 1px)`,
134                backgroundSize:"20px 20px"}}>
135                <div style={{position:"absolute", inset:"10% 8%"}}>
136                  <ShipSilhouette ship={ship} stroke={g1} fill="rgba(77,255,154,0.05)" glow strokeWidth={1.2}/>
137                </div>
138                {/* corner labels */}
139                <div style={{position:"absolute", top:4, left:6, color:mute, fontSize:9, letterSpacing:1.5}}>[0,0]</div>
140                <div style={{position:"absolute", top:4, right:6, color:mute, fontSize:9, letterSpacing:1.5}}>VW-A · PORT</div>
141                <div style={{position:"absolute", bottom:4, left:6, color:mute, fontSize:9, letterSpacing:1.5}}>◉ RENDER · WIRE · 60fps</div>
142                <div style={{position:"absolute", bottom:4, right:6, color:g1, fontSize:9, letterSpacing:1.5}}>HUD OVERLAY ON</div>
143              </div>
144              <div style={{display:"flex", marginTop:6, gap:4}}>
145                {["ORTHO","TOP","BOW","STERN","SECTION","SYSTEMS"].map((v,i) => (
146                  <div key={v} style={{padding:"3px 10px", fontSize:10, letterSpacing:2, border:`1px solid ${i===0?g1:g2}66`, color: i===0?g1:mute, background: i===0?`${g1}11`:"transparent"}}>{v}</div>
147                ))}
148              </div>
149            </div>
150  
151            {/* Price chart */}
152            <div style={{padding:"10px 14px", borderBottom:`1px solid ${g2}66`, background:bg}}>
153              <HdrBar left="◢ PRICE · 90 CYCLE" right={`LAST ₵${formatCred(ship.price)} · VOL 412`}/>
154              <div style={{position:"relative", height:90, border:`1px solid ${g2}66`}}>
155                <PriceChart seed={seed} color={g1} yellow={y1} red={r1} mute={mute}/>
156              </div>
157              <div style={{display:"flex", justifyContent:"space-between", fontSize:9, color:mute, marginTop:4, letterSpacing:1}}>
158                <span>−90c</span><span>−60c</span><span>−30c</span><span style={{color:g1}}>NOW</span>
159              </div>
160            </div>
161  
162            {/* Similar units table */}
163            <div style={{padding:"10px 14px", background:bg, flex:1, overflow:"hidden"}}>
164              <HdrBar left="◢ COMPARABLE UNITS" right="8 MATCHES · SORT: PRICE"/>
165              <table style={{width:"100%", borderCollapse:"collapse", fontSize:10, marginTop:4}}>
166                <thead>
167                  <tr style={{color:mute, borderBottom:`1px solid ${g2}66`, textAlign:"left"}}>
168                    <th style={{padding:"3px 8px", fontWeight:400, letterSpacing:1.5}}>SERIAL</th>
169                    <th style={{padding:"3px 8px", fontWeight:400, letterSpacing:1.5}}>MODEL</th>
170                    <th style={{padding:"3px 8px", fontWeight:400, letterSpacing:1.5}}>CLS</th>
171                    <th style={{padding:"3px 8px", fontWeight:400, letterSpacing:1.5, textAlign:"right"}}>MASS</th>
172                    <th style={{padding:"3px 8px", fontWeight:400, letterSpacing:1.5, textAlign:"right"}}>JUMP</th>
173                    <th style={{padding:"3px 8px", fontWeight:400, letterSpacing:1.5, textAlign:"right"}}>PRICE</th>
174                    <th style={{padding:"3px 8px", fontWeight:400, letterSpacing:1.5}}>CHART</th>
175                    <th style={{padding:"3px 8px", fontWeight:400, letterSpacing:1.5, textAlign:"right"}}>Δ</th>
176                  </tr>
177                </thead>
178                <tbody>
179                  {similar.map((s,i) => {
180                    const delta = ((hashStr(s.seed) % 200) - 100) / 10;
181                    const up = delta >= 0;
182                    return (
183                      <tr key={i} style={{borderBottom:`1px dotted ${g2}33`, background: i===0 ? `${g1}11` : "transparent"}}>
184                        <Cell col={g1}>{s.serial}</Cell>
185                        <Cell>{s.model}</Cell>
186                        <Cell col={cyan}>{s.cls.code}</Cell>
187                        <Cell right>{s.mass.toLocaleString()}</Cell>
188                        <Cell right>{s.jumpRange}</Cell>
189                        <Cell right bold>₵{formatCred(s.price)}</Cell>
190                        <td style={{padding:"1px 8px", borderRight:`1px solid ${g2}33`}}><Spark seedStr={s.seed} color={up?g1:r1} w={70} h={16} up={up}/></td>
191                        <Cell right col={up?g1:r1}>{up?"+":""}{delta.toFixed(1)}%</Cell>
192                      </tr>
193                    );
194                  })}
195                </tbody>
196              </table>
197            </div>
198          </div>
199  
200          {/* RIGHT COLUMN — procurement + order book + news */}
201          <div style={{display:"flex", flexDirection:"column", background:panel}}>
202            <div style={{padding:"10px 14px", borderBottom:`1px solid ${g2}66`}}>
203              <HdrBar left="◢ PROCUREMENT" right=""/>
204              <div style={{fontSize:10, color:mute, letterSpacing:1.5, marginTop:6}}>UNIT PRICE</div>
205              <div style={{fontFamily:"Orbitron", fontWeight:900, fontSize:28, color:g1}}>₵{formatCred(ship.price)}</div>
206              <div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:6, marginTop:8, fontSize:10}}>
207                <KVSmall k="LEASE/MO" v={`₵${formatCred(Math.round(ship.price/120))}`}/>
208                <KVSmall k="INSURE" v="1.8%/y"/>
209                <KVSmall k="DELIVERY" v={`${ship.delivery} cyc`}/>
210                <KVSmall k="WARRANTY" v={ship.warranty}/>
211              </div>
212              <div style={{marginTop:10, display:"grid", gridTemplateColumns:"1fr 1fr", gap:6}}>
213                <button style={{padding:"10px 0", background:g1, color:"#001208", border:"none", fontFamily:"Orbitron", fontWeight:700, fontSize:11, letterSpacing:3, cursor:"pointer"}}>BUY</button>
214                <button style={{padding:"10px 0", background:"transparent", color:y1, border:`1px solid ${y1}`, fontFamily:"Orbitron", fontWeight:700, fontSize:11, letterSpacing:3, cursor:"pointer"}}>BID</button>
215              </div>
216            </div>
217  
218            {/* order book */}
219            <div style={{padding:"10px 14px", borderBottom:`1px solid ${g2}66`}}>
220              <HdrBar left="◢ ORDER BOOK" right="DEPTH 5"/>
221              <table style={{width:"100%", borderCollapse:"collapse", fontSize:10, marginTop:4}}>
222                <tbody>
223                  {[[r1,"ASK","12.8M","₵428.0M",2],
224                    [r1,"ASK","4.2M", "₵425.1M",1],
225                    [r1,"ASK","1.0M", "₵423.3M",1],
226                    [g1,"BID","2.1M", "₵421.9M",1],
227                    [g1,"BID","6.6M", "₵419.4M",3],
228                    [g1,"BID","18.0M","₵414.2M",4],
229                  ].map((r,i) => (
230                    <tr key={i} style={{borderBottom:`1px dotted ${g2}33`}}>
231                      <td style={{padding:"3px 6px", color:r[0], width:36, fontWeight:700}}>{r[1]}</td>
232                      <td style={{padding:"3px 6px", textAlign:"right", color:mute}}>{r[2]}</td>
233                      <td style={{padding:"3px 6px", textAlign:"right", color:fg, fontWeight:600}}>{r[3]}</td>
234                      <td style={{padding:"3px 6px", textAlign:"right", color:r[0]}}>×{r[4]}</td>
235                    </tr>
236                  ))}
237                </tbody>
238              </table>
239            </div>
240  
241            {/* news feed */}
242            <div style={{padding:"10px 14px", flex:1, overflow:"hidden"}}>
243              <HdrBar left="◢ WIRE · FLEET DESK" right="LIVE"/>
244              <div style={{fontSize:10, color:fg, lineHeight:1.55, marginTop:6}}>
245                {[
246                  [y1,"08:39","KRV Yards expand Ceres-II dry-dock. Throughput +18%."],
247                  [g1,"08:21","Coalition lifts export tariff on Tier-2 interceptors."],
248                  [r1,"07:58","Obsidian Forge recall · MN-class drive bolts."],
249                  [cyan,"07:32","Syndicate fleet liquidation · 41 lots entering market."],
250                  [g1,"06:44","Grav-slip drive certification standard ratified."],
251                  [fg,"06:11","Pleasure yacht index stable at 112.4."],
252                ].map((row,i) => (
253                  <div key={i} style={{display:"flex", gap:8, padding:"2px 0", borderBottom:`1px dotted ${g2}33`}}>
254                    <span style={{color:mute, width:36}}>{row[1]}</span>
255                    <span style={{color:row[0], flex:1}}>{row[2]}</span>
256                  </div>
257                ))}
258              </div>
259            </div>
260          </div>
261  
262          {/* BOTTOM STATUS — spans all */}
263          <div style={{gridColumn:"1 / -1", borderTop:`1px solid ${g2}66`, padding:"5px 14px", background:"#000", display:"flex", justifyContent:"space-between", fontSize:9, color:mute, letterSpacing:1.5}}>
264            <span>◉ 14,208 SIMILAR UNITS IDX</span>
265            <span>HEARTBEAT 22ms</span>
266            <span>AUTH · CMDR VEYR · COMMODORE TIER</span>
267            <span>ENCRYPT · Q-KEY 4096</span>
268            <span style={{color:g1}}>SESSION 04:12:08</span>
269          </div>
270        </div>
271      </div>
272    );
273  }
274  
275  function KV({ title, children }) {
276    return (
277      <div style={{padding:"6px 14px", borderBottom:`1px solid #1e7a4a33`}}>
278        <div style={{color:"#4a6b60", fontSize:9, letterSpacing:2, marginBottom:4}}>▸ {title}</div>
279        <div>{children}</div>
280      </div>
281    );
282  }
283  function KVRow({ k, v, c = "#d6ffe8" }) {
284    return (
285      <div style={{display:"flex", justifyContent:"space-between", fontSize:11, padding:"1px 0"}}>
286        <span style={{color:"#4a6b60", letterSpacing:1.5}}>{k}</span>
287        <span style={{color:c, fontWeight:500}}>{v}</span>
288      </div>
289    );
290  }
291  function KVSmall({ k, v }) {
292    return (
293      <div>
294        <div style={{color:"#4a6b60", fontSize:9, letterSpacing:1.5}}>{k}</div>
295        <div style={{color:"#d6ffe8", fontWeight:600, fontSize:12}}>{v}</div>
296      </div>
297    );
298  }
299  function HdrBar({ left, right }) {
300    return (
301      <div style={{display:"flex", justifyContent:"space-between", fontSize:10, letterSpacing:2, color:"#4dff9a", paddingBottom:4, borderBottom:"1px solid #1e7a4a66"}}>
302        <span>{left}</span>
303        <span style={{color:"#4a6b60"}}>{right}</span>
304      </div>
305    );
306  }
307  function PriceChart({ seed, color, yellow, red, mute }) {
308    const rng = mulberry32(hashStr(seed+"chart"));
309    const w = 600, h = 90;
310    const N = 90;
311    const pts = [];
312    let y = 0.5;
313    for (let i = 0; i < N; i++) {
314      y += (rng() - 0.48) * 0.07;
315      y = Math.max(0.1, Math.min(0.9, y));
316      pts.push([i/(N-1)*w, h - y*h]);
317    }
318    const area = `M 0,${h} ${pts.map(p=>`L ${p[0]},${p[1]}`).join(" ")} L ${w},${h} Z`;
319    const line = `M ${pts.map(p=>`${p[0]},${p[1]}`).join(" L ")}`;
320    return (
321      <svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" style={{width:"100%", height:"100%"}}>
322        {/* gridlines */}
323        {[0.25, 0.5, 0.75].map(f => <line key={f} x1={0} y1={h*f} x2={w} y2={h*f} stroke={mute} strokeOpacity={0.25} strokeDasharray="2,4"/>)}
324        <path d={area} fill={color} fillOpacity={0.15}/>
325        <path d={line} fill="none" stroke={color} strokeWidth={1.4}/>
326        <circle cx={pts[pts.length-1][0]-2} cy={pts[pts.length-1][1]} r={3} fill={yellow}/>
327        {/* avg line */}
328        <line x1={0} y1={h*0.55} x2={w} y2={h*0.55} stroke={yellow} strokeOpacity={0.5} strokeDasharray="4,4"/>
329      </svg>
330    );
331  }
332  
333  Object.assign(window, { Concept5_Terminal });