polats Claude Opus 4.8 (1M context) commited on
Commit
f29c07a
·
1 Parent(s): 4f0862a

Space: shared app-shell sidebar (nav.json IR + sidebar.css/js) wired into Gradio

Browse files

The same nav.json + sidebar.css + sidebar.js the React app uses (auto-battler
src/shell/*) now drive a slide-in sidebar on the Gradio Space: Python templates
the IR into markup, css_paths injects the shared CSS, head injects the shared
behaviour JS. Proves UI chrome is shareable across React and Gradio from one
source (Pixi excluded — it's canvas-only and already shared).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ __pycache__/
2
+ *.pyc
__pycache__/app.cpython-313.pyc DELETED
Binary file (5.72 kB)
 
app.py CHANGED
@@ -23,10 +23,38 @@ _manifest = json.load(open(os.path.join(WEB, "assets", "characters.json")))
23
  CHARACTERS = [(c["name"], c["slug"]) for p in _manifest["packs"] for c in p["characters"]]
24
  ANIMS = ["idle", "walk", "attack", "dmg", "die"]
25
 
26
- HEAD = '<script type="module" src="/web/tiny.js"></script>'
 
 
27
  STAGE = "height:56vh;border:1px solid #20262e;border-radius:12px;overflow:hidden;background:#0b0e12"
28
 
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  def diary(unit: str, traits: str) -> str:
31
  """Stub — replaced during the hack by a local llama.cpp small model."""
32
  t = (traits or "").strip() or "untested"
@@ -37,6 +65,7 @@ def diary(unit: str, traits: str) -> str:
37
 
38
 
39
  with gr.Blocks(title="Tiny Army") as demo:
 
40
  gr.Markdown("# ⚔️ Tiny Army\n*Every fighter writes its own legend — and the "
41
  "legend is true.* Built on Gradio; sprites + engine reused from "
42
  "auto-battler, rendered with Pixi.")
@@ -68,7 +97,8 @@ fastapi_app.mount("/web", StaticFiles(directory=WEB), name="web")
68
  # NOTE: serve sprite assets at /sprites, NOT /assets — Gradio serves its own UI
69
  # bundle from /assets, and mounting there shadows it (breaks the whole UI).
70
  fastapi_app.mount("/sprites", StaticFiles(directory=os.path.join(WEB, "assets")), name="sprites")
71
- app = gr.mount_gradio_app(fastapi_app, demo, path="/", head=HEAD, theme=gr.themes.Soft())
 
72
 
73
 
74
  if __name__ == "__main__":
 
23
  CHARACTERS = [(c["name"], c["slug"]) for p in _manifest["packs"] for c in p["characters"]]
24
  ANIMS = ["idle", "walk", "attack", "dmg", "die"]
25
 
26
+ HEAD = ('<script type="module" src="/web/tiny.js"></script>'
27
+ '<script src="/web/shell/sidebar.js"></script>')
28
+ SIDEBAR_CSS = os.path.join(WEB, "shell", "sidebar.css")
29
  STAGE = "height:56vh;border:1px solid #20262e;border-radius:12px;overflow:hidden;background:#0b0e12"
30
 
31
 
32
+ # Shared app-shell sidebar: rendered from the SAME nav.json + sidebar.css +
33
+ # sidebar.js the React app uses (src/shell/*). Here we just template the IR into
34
+ # the markup; the CSS styles it and the JS slides/collapses it — proving the
35
+ # chrome is shareable across React and Gradio from one source.
36
+ def build_sidebar(nav):
37
+ b = nav.get("brand", {})
38
+ out = ['<aside class="tac-sidebar">',
39
+ f'<div class="tac-brand"><span class="tac-brand-icon">{b.get("icon","")}</span>'
40
+ f'<span>{b.get("title","")}</span>'
41
+ f'<button class="tac-collapse tac-toggle" title="Collapse">‹</button></div>']
42
+ for sec in nav.get("sections", []):
43
+ out.append('<div class="tac-section">')
44
+ if sec.get("title"):
45
+ out.append(f'<div class="tac-section-title">{sec["title"]}</div>')
46
+ for it in sec.get("items", []):
47
+ out.append(f'<a class="tac-nav-item" data-target="{it["target"]}" href="#">'
48
+ f'<span class="tac-ico">{it.get("icon","")}</span><span>{it["label"]}</span></a>')
49
+ out.append('</div>')
50
+ out.append('</aside>')
51
+ out.append('<button class="tac-toggle tac-reopen" title="Open menu">›</button>')
52
+ return "".join(out)
53
+
54
+
55
+ SIDEBAR_HTML = build_sidebar(json.load(open(os.path.join(WEB, "shell", "nav.json"))))
56
+
57
+
58
  def diary(unit: str, traits: str) -> str:
59
  """Stub — replaced during the hack by a local llama.cpp small model."""
60
  t = (traits or "").strip() or "untested"
 
65
 
66
 
67
  with gr.Blocks(title="Tiny Army") as demo:
68
+ gr.HTML(SIDEBAR_HTML)
69
  gr.Markdown("# ⚔️ Tiny Army\n*Every fighter writes its own legend — and the "
70
  "legend is true.* Built on Gradio; sprites + engine reused from "
71
  "auto-battler, rendered with Pixi.")
 
97
  # NOTE: serve sprite assets at /sprites, NOT /assets — Gradio serves its own UI
98
  # bundle from /assets, and mounting there shadows it (breaks the whole UI).
99
  fastapi_app.mount("/sprites", StaticFiles(directory=os.path.join(WEB, "assets")), name="sprites")
100
+ app = gr.mount_gradio_app(fastapi_app, demo, path="/", head=HEAD,
101
+ css_paths=[SIDEBAR_CSS], theme=gr.themes.Soft())
102
 
103
 
104
  if __name__ == "__main__":
web/shell/nav.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "brand": { "title": "Tiny Army", "icon": "⚔️" },
3
+ "sections": [
4
+ {
5
+ "title": "Play",
6
+ "items": [
7
+ { "label": "Battle", "target": "Battle", "icon": "◆" }
8
+ ]
9
+ },
10
+ {
11
+ "title": "Sandbox",
12
+ "items": [
13
+ { "label": "Sprite Animations", "target": "Sprite Animations", "icon": "🎞" }
14
+ ]
15
+ },
16
+ {
17
+ "title": "Barracks",
18
+ "items": [
19
+ { "label": "War Diaries", "target": "Barracks", "icon": "📓" }
20
+ ]
21
+ }
22
+ ]
23
+ }
web/shell/sidebar.css ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Shared app-shell sidebar — ONE source for both the React app and the Gradio
2
+ * Space. Pure CSS (framework-neutral); behaviour is in sidebar.js, content in
3
+ * nav.json. Everything is namespaced `.tac-*` so it never collides with Gradio's
4
+ * own classes. The collapse/slide is driven by a `tac-collapsed` class that
5
+ * sidebar.js toggles on <body>.
6
+ */
7
+
8
+ :root {
9
+ --tac-w: 248px;
10
+ --tac-bg: #12161c;
11
+ --tac-bg-2: #0d1116;
12
+ --tac-ink: #e7e0cf;
13
+ --tac-muted: #8b95a3;
14
+ --tac-accent: #6b5bff;
15
+ --tac-border: #232a33;
16
+ }
17
+
18
+ /* The drawer — a fixed slide-in panel (works inside Gradio's flow). */
19
+ .tac-sidebar {
20
+ position: fixed; top: 0; left: 0; bottom: 0; z-index: 1000;
21
+ width: var(--tac-w); box-sizing: border-box;
22
+ background: var(--tac-bg); border-right: 1px solid var(--tac-border);
23
+ color: var(--tac-ink); font-family: ui-monospace, Menlo, monospace;
24
+ display: flex; flex-direction: column;
25
+ transform: translateX(0); transition: transform .22s ease;
26
+ overflow-y: auto;
27
+ }
28
+ body.tac-collapsed .tac-sidebar { transform: translateX(-100%); }
29
+
30
+ /* Push the page content over on desktop when the drawer is open; on mobile it
31
+ * just overlays (see media query). */
32
+ .gradio-container { transition: margin-left .22s ease; }
33
+ body:not(.tac-collapsed) .gradio-container { margin-left: var(--tac-w); }
34
+
35
+ .tac-brand {
36
+ display: flex; align-items: center; gap: 8px;
37
+ padding: 14px 14px 10px; font-weight: 700; font-size: 15px; letter-spacing: .02em;
38
+ border-bottom: 1px solid var(--tac-border);
39
+ }
40
+ .tac-brand .tac-brand-icon { font-size: 18px; }
41
+ .tac-brand .tac-collapse {
42
+ margin-left: auto; background: none; border: 0; color: var(--tac-muted);
43
+ cursor: pointer; font-size: 18px; line-height: 1; padding: 2px 6px; border-radius: 6px;
44
+ }
45
+ .tac-brand .tac-collapse:hover { background: var(--tac-bg-2); color: var(--tac-ink); }
46
+
47
+ .tac-section { padding: 10px 8px 0; }
48
+ .tac-section-title {
49
+ font-size: 10px; letter-spacing: .18em; text-transform: uppercase;
50
+ color: var(--tac-muted); padding: 4px 8px;
51
+ }
52
+ .tac-nav-item {
53
+ display: flex; align-items: center; gap: 8px; width: 100%;
54
+ padding: 8px 10px; margin: 1px 0; border: 0; border-radius: 8px;
55
+ background: none; color: var(--tac-ink); font: inherit; text-align: left;
56
+ cursor: pointer; text-decoration: none;
57
+ }
58
+ .tac-nav-item:hover { background: var(--tac-bg-2); }
59
+ .tac-nav-item.active { background: color-mix(in srgb, var(--tac-accent) 22%, transparent); }
60
+ .tac-nav-item .tac-ico { width: 18px; text-align: center; opacity: .9; }
61
+
62
+ /* Edge tab to reopen when collapsed. */
63
+ .tac-reopen {
64
+ position: fixed; top: 12px; left: 0; z-index: 1001;
65
+ display: none; background: var(--tac-bg); color: var(--tac-ink);
66
+ border: 1px solid var(--tac-border); border-left: 0;
67
+ border-radius: 0 8px 8px 0; padding: 8px 10px; cursor: pointer; font-size: 14px;
68
+ }
69
+ body.tac-collapsed .tac-reopen { display: block; }
70
+
71
+ /* Mobile: the drawer overlays full-bleed-ish and never pushes content. JS
72
+ * auto-collapses below this width on load + resize. */
73
+ @media (max-width: 768px) {
74
+ .tac-sidebar { width: min(280px, 86vw); box-shadow: 4px 0 24px rgba(0,0,0,.5); }
75
+ body:not(.tac-collapsed) .gradio-container { margin-left: 0; }
76
+ }
web/shell/sidebar.js ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Shared app-shell sidebar behaviour — ONE source for React and Gradio. Pure
2
+ * vanilla DOM (no framework): it toggles a `tac-collapsed` class on <body> (CSS
3
+ * does the slide), auto-collapses below 768px on load + resize, and routes nav
4
+ * clicks to the host (a Gradio tab, or a hash route in React). Because it's all
5
+ * event-delegation + class toggling, the SAME file runs in both frameworks.
6
+ */
7
+ (function () {
8
+ var MOBILE = '(max-width: 768px)'
9
+ var userToggled = false
10
+
11
+ function isMobile() { return window.matchMedia(MOBILE).matches }
12
+ function collapsed() { return document.body.classList.contains('tac-collapsed') }
13
+ function setCollapsed(c) { document.body.classList.toggle('tac-collapsed', !!c) }
14
+
15
+ // Auto-collapse on mobile unless the user has explicitly toggled this session.
16
+ function applyResponsive() { if (!userToggled) setCollapsed(isMobile()) }
17
+
18
+ // Highlight the active nav item by its target label.
19
+ function setActive(target) {
20
+ document.querySelectorAll('.tac-nav-item').forEach(function (el) {
21
+ el.classList.toggle('active', el.getAttribute('data-target') === target)
22
+ })
23
+ }
24
+
25
+ // Host hook: how a nav item activates a view. Override window.tacNavigate to
26
+ // customise; default switches the active Gradio tab by matching its label.
27
+ window.tacNavigate = window.tacNavigate || function (target) {
28
+ var tab = Array.prototype.find.call(
29
+ document.querySelectorAll('button[role="tab"]'),
30
+ function (b) { return b.textContent.trim() === target })
31
+ if (tab) tab.click()
32
+ }
33
+
34
+ document.addEventListener('click', function (e) {
35
+ if (e.target.closest('.tac-toggle')) {
36
+ userToggled = true; setCollapsed(!collapsed()); return
37
+ }
38
+ var item = e.target.closest('.tac-nav-item')
39
+ if (item) {
40
+ e.preventDefault()
41
+ var target = item.getAttribute('data-target')
42
+ setActive(target)
43
+ try { window.tacNavigate(target) } catch (_) {}
44
+ if (isMobile()) setCollapsed(true) // close the drawer after navigating on phones
45
+ }
46
+ })
47
+
48
+ window.addEventListener('resize', applyResponsive)
49
+ // Run once the DOM is ready (the sidebar markup may mount after this script).
50
+ if (document.readyState === 'loading') {
51
+ document.addEventListener('DOMContentLoaded', applyResponsive)
52
+ } else { applyResponsive() }
53
+ })()