Maxluria Claude Opus 4.8 commited on
Commit
8027c32
·
1 Parent(s): e418392

Front map: systems orbit a central star as live mini solar-systems

Browse files

Replace the scattered chart layout with a circular orbit: the 7 systems are
evenly spaced on a slowly revolving ring around a central pulsing star, with
labels counter-rotated to stay upright and the orbit pausing on hover. Each
system is now a hand-built animated SVG (rotating gears/rays/rune-ticks,
planet-dots orbiting at varied speeds, pulsing suns). Borderless and
theme-tinted; collapses to a tappable 2-up grid on mobile. Navigation runs
through the JS click-bridge; on_nav no-ops the empty load-time value so the map
is not auto-navigated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +285 -60
app.py CHANGED
@@ -11,6 +11,7 @@ adapted from the Coppermind wiki (CC BY-NC-SA). Unofficial fan project, not
11
  affiliated with or endorsed by Brandon Sanderson or Dragonsteel.
12
  """
13
 
 
14
  import re
15
  from threading import Thread
16
 
@@ -141,17 +142,174 @@ def build_orrery(shard, mode="full"):
141
  )
142
 
143
 
144
- def _emblem_css():
145
- """Per-system float timing + theme color, applied by column elem_id."""
146
- rules = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  for i, sid in enumerate(SHARD_ORDER):
148
- c = SHARDS[sid]["color"]
149
- dur = 7 + (i % 4) * 1.3 # gentle, varied bob
 
 
150
  delay = (i * 0.7) % 4
151
- rules.append(
152
- f"#emb-{sid} {{ --c:{c}; animation-duration:{dur}s; animation-delay:-{delay}s; }}"
 
 
 
 
 
 
 
 
153
  )
154
- return "\n".join(rules)
 
 
 
155
 
156
 
157
  def build_system_panel(shard):
@@ -199,6 +357,20 @@ def enter_shard(shard_id):
199
  )
200
 
201
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  def travel_home():
203
  """Clear the active voice and return to the star map."""
204
  return (
@@ -277,34 +449,82 @@ CSS = """
277
  #codex-header .intro { color:#9c97bd; font-size:.95rem; max-width:660px; margin:8px auto 0; }
278
  #map-hint { text-align:center; color:#8d88ad; font-size:.9rem; margin:4px 0 6px; letter-spacing:.04em; }
279
 
280
- /* ---- floating systems (front page): each system's own star chart, adrift ---- */
281
- #cosmos {
282
- display:flex !important; flex-wrap:wrap; justify-content:center; align-items:flex-start;
283
- gap:2px 24px; max-width:1180px; margin:6px auto 12px; padding:6px;
 
 
 
 
 
 
 
 
 
284
  }
285
- .sys-emblem {
286
- width:170px !important; flex:0 0 auto !important; min-width:0 !important;
287
- padding:0 !important; cursor:pointer; text-align:center;
288
- animation-name:floaty; animation-timing-function:ease-in-out; animation-iteration-count:infinite;
 
 
 
 
 
 
 
289
  }
290
- .sys-emblem, .sys-emblem > *, .sys-img-comp, .sys-img-comp > * {
291
- background:transparent !important; border:none !important; box-shadow:none !important;
292
  }
293
- .sys-img-comp { min-height:0 !important; }
294
- .sys-img-comp .image-frame, .sys-img-comp button { display:flex !important; justify-content:center !important; }
295
- .sys-img-comp img {
296
- width:150px !important; height:150px !important; object-fit:cover; border-radius:50%;
297
- margin:0 auto; cursor:pointer;
298
- -webkit-mask-image: radial-gradient(circle at 50% 50%, #000 64%, transparent 82%);
299
- mask-image: radial-gradient(circle at 50% 50%, #000 64%, transparent 82%);
300
- filter: drop-shadow(0 0 4px rgba(255,255,255,0.14));
301
- transition: transform .35s ease, filter .35s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  }
303
- .sys-emblem:hover .sys-img-comp img { transform:scale(1.08); filter:drop-shadow(0 0 18px var(--c)); }
304
- .sys-label { margin-top:6px; pointer-events:none; }
305
- .sys-name { font:600 .8rem/1.25 'Georgia',serif; color:#e9e4f6; letter-spacing:.04em; }
306
- .sys-shard { font:italic 1rem/1.25 'Georgia',serif; color:var(--c); text-shadow:0 0 12px var(--c); }
307
- @keyframes floaty { 0%,100% { transform:translateY(0); } 50% { transform:translateY(-11px); } }
308
 
309
  /* ---- orrery ---- */
310
  .orrery { position:relative; margin:0 auto; }
@@ -374,7 +594,32 @@ CSS = """
374
  /* footer */
375
  #codex-footer { text-align:center; color:#7d789c; font-size:.78rem; margin:22px auto 8px; max-width:760px; line-height:1.5; }
376
  #codex-footer a { color:#a89ce0; }
377
- """ + _emblem_css()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
 
379
 
380
  # ---------------------------------------------------------------------------- #
@@ -382,6 +627,7 @@ CSS = """
382
  # ---------------------------------------------------------------------------- #
383
  with gr.Blocks(title="Cosmere Codex") as demo:
384
  active_shard = gr.State("")
 
385
 
386
  gr.HTML(
387
  """
@@ -398,24 +644,7 @@ with gr.Blocks(title="Cosmere Codex") as demo:
398
  # ---------------------------- MAP VIEW ---------------------------------- #
399
  with gr.Column(visible=True) as map_view:
400
  gr.HTML('<div id="map-hint">✦ Click a floating system to enter ✦</div>')
401
- with gr.Row(elem_id="cosmos"):
402
- emblems = {}
403
- for sid in SHARD_ORDER:
404
- s = SHARDS[sid]
405
- with gr.Column(elem_id=f"emb-{sid}", elem_classes=["sys-emblem"], min_width=140):
406
- emblems[sid] = gr.Image(
407
- value=s["map_image"],
408
- elem_classes=["sys-img-comp"],
409
- show_label=False,
410
- interactive=False,
411
- container=False,
412
- buttons=[],
413
- )
414
- gr.HTML(
415
- f'<div class="sys-label">'
416
- f'<div class="sys-name">{s["system"]}</div>'
417
- f'<div class="sys-shard">{s["name"]}</div></div>'
418
- )
419
 
420
  # -------------------------- CONVERSATION VIEW --------------------------- #
421
  with gr.Column(visible=False) as chat_view:
@@ -457,14 +686,10 @@ with gr.Blocks(title="Cosmere Codex") as demo:
457
  shard_skin, system_orrery, user_box, send_btn,
458
  ]
459
 
460
- # A click anywhere on a floating system's chart enters that system.
461
- def _make_enter(target_id):
462
- def _enter(*_):
463
- return enter_shard(target_id)
464
- return _enter
465
-
466
- for sid, img in emblems.items():
467
- img.select(_make_enter(sid), inputs=None, outputs=nav_outputs)
468
 
469
  travel_btn.click(
470
  travel_home, inputs=None,
@@ -476,4 +701,4 @@ with gr.Blocks(title="Cosmere Codex") as demo:
476
 
477
 
478
  if __name__ == "__main__":
479
- demo.queue().launch(css=CSS, theme=gr.themes.Base())
 
11
  affiliated with or endorsed by Brandon Sanderson or Dragonsteel.
12
  """
13
 
14
+ import math
15
  import re
16
  from threading import Thread
17
 
 
142
  )
143
 
144
 
145
+ # ---------------------------------------------------------------------------- #
146
+ # Front-page system symbols hand-built vector glyphs, one per system.
147
+ # Each <svg> is borderless and uses currentColor (the system's theme color, set
148
+ # via .sys-symbol{color:var(--c)}) so it tints + glows to match the voice.
149
+ # ---------------------------------------------------------------------------- #
150
+ def _ring(r, sw=1.0, op=0.7, color="currentColor"):
151
+ return (f'<circle cx="50" cy="50" r="{r}" fill="none" '
152
+ f'stroke="{color}" stroke-width="{sw}" opacity="{op}"/>')
153
+
154
+
155
+ def _around(r, n, fn, phase=0.0):
156
+ """Place n items evenly around a circle of radius r; fn(x, y, i) -> svg."""
157
+ out = []
158
+ for i in range(n):
159
+ a = phase + i * (2 * math.pi / n)
160
+ out.append(fn(50 + r * math.cos(a), 50 + r * math.sin(a), i))
161
+ return "".join(out)
162
+
163
+
164
+ def _rays(r0, r1, n, sw=1.4, color="currentColor", op=0.9):
165
+ def line(_x, _y, i):
166
+ a = i * (2 * math.pi / n)
167
+ x0, y0 = 50 + r0 * math.cos(a), 50 + r0 * math.sin(a)
168
+ x1, y1 = 50 + r1 * math.cos(a), 50 + r1 * math.sin(a)
169
+ return (f'<line x1="{x0:.1f}" y1="{y0:.1f}" x2="{x1:.1f}" y2="{y1:.1f}" '
170
+ f'stroke="{color}" stroke-width="{sw}" opacity="{op}"/>')
171
+ return _around(r0, n, line)
172
+
173
+
174
+ def _dots(r, n, rad, color="currentColor", op=0.95, phase=0.0):
175
+ return _around(
176
+ r, n,
177
+ lambda x, y, i: f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{rad}" fill="{color}" opacity="{op}"/>',
178
+ phase,
179
+ )
180
+
181
+
182
+ def _svg(body):
183
+ return (f'<svg class="sym-art" viewBox="0 0 100 100" '
184
+ f'xmlns="http://www.w3.org/2000/svg">{body}</svg>')
185
+
186
+
187
+ def _spin(body, sp, rev=False):
188
+ """Wrap SVG in a group that rotates about the symbol center (view-box origin)."""
189
+ cls = "spin-vb spin-rev" if rev else "spin-vb"
190
+ return f'<g class="{cls}" style="--sp:{sp}s">{body}</g>'
191
+
192
+
193
+ def _pulse(body, pp=4.5):
194
+ return f'<g class="pulse" style="--pp:{pp}s">{body}</g>'
195
+
196
+
197
+ def build_symbol(sid):
198
+ """Return an inline SVG glyph for a system — a little moving solar system:
199
+ rings hold, planet-dots/rays/gears orbit at varied speeds, suns pulse."""
200
+ c2 = SHARDS[sid]["accent"]
201
+ sun = '<circle cx="50" cy="50" r="6.5" fill="#fff7e0"/>'
202
+
203
+ if sid == "drominad": # Hoid — concentric rings, orbiting motes, pulsing sun
204
+ body = (
205
+ "".join(_ring(r, 1, 0.55) for r in (45, 38, 31, 24, 17))
206
+ + _pulse(sun + _ring(7.5, 1.4, 0.9), 5)
207
+ + _spin(_dots(45, 1, 2.2), 40)
208
+ + _spin(_dots(31, 1, 2.0, phase=2.1), 28, rev=True)
209
+ + _spin(_dots(24, 1, 1.8, phase=1.0), 34)
210
+ )
211
+ return _svg(body)
212
+
213
+ if sid == "harmony": # Scadrian — turning clockwork gear + radiant pulsing sun
214
+ gear = ('<circle cx="50" cy="50" r="40" fill="none" stroke="currentColor" '
215
+ 'stroke-width="6" stroke-dasharray="4.2 4.2" opacity="0.85"/>')
216
+ body = (
217
+ _spin(gear, 46)
218
+ + _ring(31, 1, 0.5)
219
+ + _spin(_rays(13, 23, 16, 1.2, "#fde6b0", 0.85), 22, rev=True)
220
+ + _pulse('<circle cx="50" cy="50" r="11" fill="#f6e2a0"/>' + _ring(11, 1.5, 0.95), 4.5)
221
+ )
222
+ return _svg(body)
223
+
224
+ if sid == "devotion_dominion": # Selish — sigil ring of orbiting nodes + dual core
225
+ dual = (
226
+ '<circle cx="44" cy="50" r="10" fill="none" stroke="currentColor" stroke-width="1.6" opacity="0.95"/>'
227
+ f'<circle cx="56" cy="50" r="10" fill="none" stroke="{c2}" stroke-width="1.6" opacity="0.95"/>'
228
+ '<circle cx="50" cy="50" r="2.4" fill="#eaf3ff"/>'
229
+ )
230
+ body = (
231
+ _ring(43, 1, 0.7) + _ring(35, 1, 0.45)
232
+ + _spin(_dots(39, 8, 2.0, op=0.85), 52)
233
+ + _pulse(dual, 5.5)
234
+ )
235
+ return _svg(body)
236
+
237
+ if sid == "autonomy": # Taldain — slowly spinning figure-8 binary loops
238
+ big = (
239
+ '<circle cx="50" cy="34" r="19" fill="none" stroke="currentColor" stroke-width="2" opacity="0.9"/>'
240
+ '<circle cx="50" cy="66" r="19" fill="none" stroke="currentColor" stroke-width="2" opacity="0.9"/>'
241
+ )
242
+ inner = (
243
+ '<circle cx="50" cy="34" r="13" fill="none" stroke="currentColor" stroke-width="1" opacity="0.5"/>'
244
+ '<circle cx="50" cy="66" r="13" fill="none" stroke="currentColor" stroke-width="1" opacity="0.5"/>'
245
+ '<circle cx="50" cy="34" r="3" fill="#fff"/>'
246
+ '<circle cx="50" cy="66" r="3" fill="#cfe0ff"/>'
247
+ )
248
+ body = _spin(big, 55) + _spin(inner, 34, rev=True)
249
+ return _svg(body)
250
+
251
+ if sid == "endowment": # Nalthian — iridescent rings, each planet on its own orbit
252
+ cols = ["#e85aa0", "#f2c14e", "#5ad1c8", "#7aa0ff"]
253
+ radii = [44, 35, 26, 17]
254
+ speeds = [40, 30, 22, 15]
255
+ body = "".join(_ring(r, 1.4, 0.85, col) for r, col in zip(radii, cols))
256
+ body += "".join(
257
+ _spin(_dots(r, 1, 2.4, col, phase=i * 1.3), sp, rev=(i % 2 == 1))
258
+ for i, (r, col, sp) in enumerate(zip(radii, cols, speeds))
259
+ )
260
+ body += _pulse('<circle cx="50" cy="50" r="7" fill="#fff6ef"/>', 4)
261
+ return _svg(body)
262
+
263
+ if sid in ("odium", "cultivation"): # Rosharan — turning rune ring + glowing core
264
+ body = (
265
+ _ring(44, 1, 0.7) + _ring(36, 1, 0.55)
266
+ + _spin(_rays(36, 44, 28, 1.0, "currentColor", 0.6), 64) # rune ticks
267
+ + _ring(21, 1, 0.5)
268
+ + _spin(_dots(28, 3, 1.6, "currentColor", op=0.7), 30, rev=True)
269
+ + _pulse('<circle cx="50" cy="50" r="8" fill="currentColor" opacity="0.9"/>'
270
+ '<circle cx="50" cy="50" r="3.4" fill="#fff" opacity="0.85"/>', 4.2)
271
+ )
272
+ return _svg(body)
273
+
274
+ return _svg(_ring(40) + sun) # fallback
275
+
276
+
277
+ def build_core():
278
+ """The central star the systems orbit (a nod to Adonalsium). Non-clickable."""
279
+ return _svg(
280
+ _spin(_rays(20, 33, 24, 1.0, "#ffe7a8", 0.55), 90)
281
+ + '<circle cx="50" cy="50" r="30" fill="#f7d98a" opacity="0.10"/>'
282
+ + _pulse('<circle cx="50" cy="50" r="13" fill="#fff2c8"/>'
283
+ '<circle cx="50" cy="50" r="13" fill="none" stroke="#ffe7a8" stroke-width="1.5"/>', 5)
284
+ )
285
+
286
+
287
+ def build_cosmos_scene():
288
+ """The front page: systems arranged in a circle, slowly orbiting a central star,
289
+ each one its own little moving solar system."""
290
+ n = len(SHARD_ORDER)
291
+ slots = []
292
  for i, sid in enumerate(SHARD_ORDER):
293
+ s = SHARDS[sid]
294
+ angle = -90 + i * 360.0 / n # start at top, even spacing
295
+ sz = 132
296
+ dur = 7 + (i % 4) * 1.3 # gentle, varied bob
297
  delay = (i * 0.7) % 4
298
+ slots.append(
299
+ f'<div class="slot" style="--a:{angle:.2f}deg">'
300
+ f'<div class="deslot"><div class="counter">'
301
+ f'<div class="sys-symbol" data-shard="{sid}" '
302
+ f'style="--c:{s["color"]};--c2:{s["accent"]};--sz:{sz}px;'
303
+ f'--dur:{dur}s;--delay:-{delay}s;">'
304
+ f'<div class="sym-float">{build_symbol(sid)}'
305
+ f'<div class="sys-label"><div class="sys-name">{s["system"]}</div>'
306
+ f'<div class="sys-shard">{s["name"]}</div></div></div>'
307
+ f'</div></div></div></div>'
308
  )
309
+ return (
310
+ f'<div id="cosmos"><div class="ring">{"".join(slots)}</div>'
311
+ f'<div class="core-star">{build_core()}</div></div>'
312
+ )
313
 
314
 
315
  def build_system_panel(shard):
 
357
  )
358
 
359
 
360
+ def on_nav(value):
361
+ """Handle a click from the floating map, relayed via the hidden JS proxy box.
362
+
363
+ The value is "<shard_id>|<nonce>"; the nonce guarantees a fresh change event
364
+ even when the same system is chosen twice in a row. Gradio also emits an
365
+ empty value on initial page load — we treat that (and any unknown id) as a
366
+ no-op so the map is not auto-navigated."""
367
+ sid = (value or "").split("|", 1)[0]
368
+ if sid not in SHARDS:
369
+ return (gr.update(), gr.update(), "", gr.update(),
370
+ gr.update(), gr.update(), gr.update(), gr.update())
371
+ return enter_shard(sid)
372
+
373
+
374
  def travel_home():
375
  """Clear the active voice and return to the star map."""
376
  return (
 
449
  #codex-header .intro { color:#9c97bd; font-size:.95rem; max-width:660px; margin:8px auto 0; }
450
  #map-hint { text-align:center; color:#8d88ad; font-size:.9rem; margin:4px 0 6px; letter-spacing:.04em; }
451
 
452
+ /* hidden proxy that the JS click-bridge writes into to trigger navigation */
453
+ #nav-proxy { position:absolute !important; left:-9999px !important; top:0 !important;
454
+ width:1px !important; height:1px !important; overflow:hidden !important;
455
+ opacity:0 !important; pointer-events:none !important; }
456
+
457
+ /* ---- systems in a circle, orbiting a central star (a "moving solar system") ---- */
458
+ #cosmos { position:relative; height:clamp(470px, 50vw, 580px); margin:0 auto; }
459
+
460
+ /* the ring slowly revolves; .counter spins back so each symbol stays upright */
461
+ .ring { position:absolute; inset:0; animation:ring-spin var(--T,150s) linear infinite; }
462
+ .slot {
463
+ position:absolute; left:50%; top:50%; width:0; height:0;
464
+ transform:rotate(var(--a)) translate(var(--R,205px));
465
  }
466
+ .deslot { position:absolute; left:0; top:0; width:0; height:0;
467
+ transform:rotate(calc(-1 * var(--a))); }
468
+ .counter { position:absolute; left:0; top:0; width:0; height:0;
469
+ animation:ring-spin var(--T,150s) linear infinite reverse; }
470
+ /* hovering the map gently pauses the orbit so a symbol is easy to read/click */
471
+ #cosmos:hover .ring, #cosmos:hover .counter { animation-play-state:paused; }
472
+ @keyframes ring-spin { to { transform:rotate(360deg); } }
473
+
474
+ .sys-symbol {
475
+ position:absolute; left:0; top:0; width:var(--sz);
476
+ transform:translate(-50%,-50%); cursor:pointer; color:var(--c); text-align:center;
477
  }
478
+ .sym-float {
479
+ animation:floaty var(--dur,8s) ease-in-out infinite; animation-delay:var(--delay,0s);
480
  }
481
+ .sym-art {
482
+ display:block; width:100%; height:auto; overflow:visible;
483
+ filter:drop-shadow(0 0 5px color-mix(in srgb, var(--c) 45%, transparent));
484
+ transition:transform .35s ease, filter .35s ease;
485
+ }
486
+ .sys-symbol:hover { z-index:5; }
487
+ .sys-symbol:hover .sym-art { transform:scale(1.12); filter:drop-shadow(0 0 22px var(--c)); }
488
+ .sys-label { margin-top:2px; pointer-events:none; transition:opacity .35s ease; }
489
+ .sys-name { font:600 .78rem/1.25 'Georgia',serif; color:#d7d2ea; letter-spacing:.04em; }
490
+ .sys-shard { font:italic 1.02rem/1.25 'Georgia',serif; color:var(--c); text-shadow:0 0 12px var(--c); }
491
+ .sys-symbol:hover .sys-name { color:#f3eeff; }
492
+ @keyframes floaty { 0%,100% { transform:translateY(0); } 50% { transform:translateY(-9px); } }
493
+
494
+ /* central star */
495
+ .core-star {
496
+ position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
497
+ width:128px; height:128px; pointer-events:none; color:#ffe7a8;
498
+ }
499
+
500
+ /* inner symbol motion: groups that orbit / pulse about the symbol center */
501
+ .spin-vb { transform-box:view-box; transform-origin:50px 50px;
502
+ animation:svg-spin var(--sp,30s) linear infinite; }
503
+ .spin-vb.spin-rev { animation-direction:reverse; }
504
+ .pulse { transform-box:fill-box; transform-origin:center;
505
+ animation:sun-pulse var(--pp,4.5s) ease-in-out infinite; }
506
+ @keyframes svg-spin { to { transform:rotate(360deg); } }
507
+ @keyframes sun-pulse { 0%,100% { opacity:.9; transform:scale(1); }
508
+ 50% { opacity:1; transform:scale(1.14); } }
509
+
510
+ /* mobile: unwind the circle into a clean, tappable 2-up grid */
511
+ @media (max-width:760px) {
512
+ #cosmos { height:auto; }
513
+ .ring {
514
+ position:static; inset:auto; transform:none !important; animation:none !important;
515
+ display:flex; flex-wrap:wrap; justify-content:center; align-items:flex-start;
516
+ gap:16px 4%; padding:6px 0 12px;
517
+ }
518
+ .slot, .deslot, .counter {
519
+ position:static; width:auto; height:auto;
520
+ transform:none !important; animation:none !important; display:contents;
521
+ }
522
+ .sys-symbol {
523
+ position:static !important; transform:none !important;
524
+ width:43% !important; max-width:200px;
525
+ }
526
+ .core-star { display:none; }
527
  }
 
 
 
 
 
528
 
529
  /* ---- orrery ---- */
530
  .orrery { position:relative; margin:0 auto; }
 
594
  /* footer */
595
  #codex-footer { text-align:center; color:#7d789c; font-size:.78rem; margin:22px auto 8px; max-width:760px; line-height:1.5; }
596
  #codex-footer a { color:#a89ce0; }
597
+ """
598
+
599
+ # Injected into <head> so it runs as a real script: one document-level click
600
+ # delegate that turns any element carrying data-shard into a navigation event by
601
+ # writing "<id>|<nonce>" into the hidden nav-proxy textbox and firing its input.
602
+ HEAD_JS = """
603
+ <script>
604
+ (function () {
605
+ function go(id) {
606
+ var t = document.querySelector('#nav-proxy textarea') ||
607
+ document.querySelector('#nav-proxy input');
608
+ if (!t) { return; }
609
+ var proto = (t.tagName === 'TEXTAREA')
610
+ ? window.HTMLTextAreaElement.prototype
611
+ : window.HTMLInputElement.prototype;
612
+ var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
613
+ setter.call(t, id + '|' + Date.now());
614
+ t.dispatchEvent(new Event('input', { bubbles: true }));
615
+ }
616
+ document.addEventListener('click', function (e) {
617
+ var el = e.target.closest ? e.target.closest('[data-shard]') : null;
618
+ if (el) { go(el.getAttribute('data-shard')); }
619
+ });
620
+ })();
621
+ </script>
622
+ """
623
 
624
 
625
  # ---------------------------------------------------------------------------- #
 
627
  # ---------------------------------------------------------------------------- #
628
  with gr.Blocks(title="Cosmere Codex") as demo:
629
  active_shard = gr.State("")
630
+ nav_proxy = gr.Textbox(elem_id="nav-proxy", show_label=False)
631
 
632
  gr.HTML(
633
  """
 
644
  # ---------------------------- MAP VIEW ---------------------------------- #
645
  with gr.Column(visible=True) as map_view:
646
  gr.HTML('<div id="map-hint">✦ Click a floating system to enter ✦</div>')
647
+ gr.HTML(build_cosmos_scene())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
648
 
649
  # -------------------------- CONVERSATION VIEW --------------------------- #
650
  with gr.Column(visible=False) as chat_view:
 
686
  shard_skin, system_orrery, user_box, send_btn,
687
  ]
688
 
689
+ # A click on any floating symbol writes "<id>|<nonce>" into nav_proxy (via the
690
+ # head script's click delegate); .change handles it. on_nav no-ops the empty
691
+ # value Gradio emits at load, so the map is not auto-navigated.
692
+ nav_proxy.change(on_nav, inputs=nav_proxy, outputs=nav_outputs)
 
 
 
 
693
 
694
  travel_btn.click(
695
  travel_home, inputs=None,
 
701
 
702
 
703
  if __name__ == "__main__":
704
+ demo.queue().launch(css=CSS, theme=gr.themes.Base(), head=HEAD_JS)