kavyabhand commited on
Commit
ec8c56d
·
verified ·
1 Parent(s): 9bf7dd5

Deploy Aether Garden application

Browse files
Files changed (3) hide show
  1. app.py +118 -15
  2. ui/map.py +17 -1
  3. ui/styles.css +120 -10
app.py CHANGED
@@ -689,11 +689,25 @@ def build_app() -> gr.Blocks:
689
  '<div class="tome-ornament top" aria-hidden="true">✦ ─── ❧ ─── ✦</div>'
690
  '<p class="tome-printed-kicker">The Living Realm</p>'
691
  '<p class="tome-printed-hint">Tap a place on the map — a vision shall rise.</p>'
 
 
 
 
 
 
 
692
  )
693
  world_map = gr.HTML(
694
  value=render_world_map(),
695
  elem_id="world-map",
696
  )
 
 
 
 
 
 
 
697
  location_panel = gr.HTML(
698
  value=render_location_panel(None),
699
  elem_id="location-panel-host-wrap",
@@ -1749,34 +1763,122 @@ def build_app() -> gr.Blocks:
1749
  }
1750
 
1751
  const triggerGradioInput = (elemId, value) => {
1752
- const root = document.getElementById(elemId) || document.querySelector('#' + elemId);
 
 
 
1753
  if (!root) return false;
1754
- const input = root.querySelector('textarea')
1755
- || root.querySelector('input[type="text"]')
1756
- || root.querySelector('input:not([type="hidden"])');
 
1757
  if (!input) return false;
1758
  const proto = input.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
1759
  const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
1760
- if (setter) setter.call(input, String(value));
1761
- else input.value = String(value);
 
1762
  input.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
1763
  input.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
1764
  return true;
1765
  };
1766
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1767
  const bind = () => {
1768
- document.querySelectorAll('#world-map [data-location-id]').forEach((node) => {
1769
- if (node.dataset.mapBound) return;
1770
- node.dataset.mapBound = '1';
1771
  node.style.cursor = 'pointer';
1772
- node.addEventListener('click', (e) => {
1773
- e.preventDefault();
1774
- e.stopPropagation();
1775
- const id = node.getAttribute('data-location-id');
1776
- if (id) triggerGradioInput('map_location_pick', id);
1777
- });
1778
  });
1779
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1780
  const fireHiddenInput = (id, value) => {
1781
  const elemId = (id || '').replace(/^#/, '');
1782
  triggerGradioInput(elemId, value);
@@ -1797,6 +1899,7 @@ def build_app() -> gr.Blocks:
1797
  const closeLocationModal = () => {
1798
  document.querySelectorAll('.location-modal-overlay').forEach((m) => m.classList.remove('is-open'));
1799
  };
 
1800
 
1801
  const bindSpreadClicks = () => {
1802
  document.querySelectorAll('.place-ribbon-item').forEach((btn) => {
 
689
  '<div class="tome-ornament top" aria-hidden="true">✦ ─── ❧ ─── ✦</div>'
690
  '<p class="tome-printed-kicker">The Living Realm</p>'
691
  '<p class="tome-printed-hint">Tap a place on the map — a vision shall rise.</p>'
692
+ '<div class="tome-map-chrome" aria-hidden="true">'
693
+ '<span class="tome-map-chrome-glyph">⟡</span>'
694
+ '<span class="tome-map-chrome-line"></span>'
695
+ '<span class="tome-map-chrome-label">Map of the Canopy</span>'
696
+ '<span class="tome-map-chrome-line"></span>'
697
+ '<span class="tome-map-chrome-glyph">⟡</span>'
698
+ '</div>'
699
  )
700
  world_map = gr.HTML(
701
  value=render_world_map(),
702
  elem_id="world-map",
703
  )
704
+ gr.HTML(
705
+ '<div class="tome-map-divider" aria-hidden="true">'
706
+ '<span class="tome-map-vine left"></span>'
707
+ '<span class="tome-map-divider-text">✧ Sacred Places ✧</span>'
708
+ '<span class="tome-map-vine right"></span>'
709
+ '</div>'
710
+ )
711
  location_panel = gr.HTML(
712
  value=render_location_panel(None),
713
  elem_id="location-panel-host-wrap",
 
1763
  }
1764
 
1765
  const triggerGradioInput = (elemId, value) => {
1766
+ const clean = String(elemId || '').replace(/^#/, '');
1767
+ const root = document.getElementById(clean)
1768
+ || document.querySelector('#' + clean)
1769
+ || document.querySelector('[id="' + clean + '"]');
1770
  if (!root) return false;
1771
+ const input = (root.tagName === 'TEXTAREA' || root.tagName === 'INPUT') ? root
1772
+ : (root.querySelector('textarea:not([disabled])')
1773
+ || root.querySelector('input[type="text"]')
1774
+ || root.querySelector('input:not([type="hidden"])'));
1775
  if (!input) return false;
1776
  const proto = input.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
1777
  const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
1778
+ const next = String(value);
1779
+ if (setter) setter.call(input, next);
1780
+ else input.value = next;
1781
  input.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
1782
  input.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
1783
  return true;
1784
  };
1785
 
1786
+ const ensureLocationModal = () => {
1787
+ let modal = document.getElementById('aether-location-modal');
1788
+ if (modal) return modal;
1789
+ modal = document.createElement('div');
1790
+ modal.id = 'aether-location-modal';
1791
+ modal.className = 'location-modal-overlay';
1792
+ modal.innerHTML = `
1793
+ <div class="location-modal-card">
1794
+ <div class="location-modal-close" role="button" tabindex="0" aria-label="Close">✕</div>
1795
+ <div class="location-panel location-panel-animate open">
1796
+ <div class="location-panel-banner">
1797
+ <div class="location-panel-scrim"></div>
1798
+ <h3 class="location-panel-name"></h3>
1799
+ </div>
1800
+ <div class="location-panel-content">
1801
+ <p class="location-panel-desc"></p>
1802
+ <p class="location-panel-special"></p>
1803
+ <p class="location-soul-count"></p>
1804
+ </div>
1805
+ </div>
1806
+ </div>`;
1807
+ document.body.appendChild(modal);
1808
+ const closeBtn = modal.querySelector('.location-modal-close');
1809
+ if (closeBtn) {
1810
+ closeBtn.addEventListener('click', () => modal.classList.remove('is-open'));
1811
+ closeBtn.addEventListener('keydown', (e) => {
1812
+ if (e.key === 'Enter' || e.key === ' ') {
1813
+ e.preventDefault();
1814
+ modal.classList.remove('is-open');
1815
+ }
1816
+ });
1817
+ }
1818
+ modal.addEventListener('click', (e) => {
1819
+ if (e.target === modal) modal.classList.remove('is-open');
1820
+ });
1821
+ return modal;
1822
+ };
1823
+
1824
+ const openLocationFromNode = (node) => {
1825
+ if (!node) return;
1826
+ const id = node.getAttribute('data-location-id');
1827
+ if (!id) return;
1828
+ const modal = ensureLocationModal();
1829
+ const name = node.getAttribute('data-name') || 'Unknown place';
1830
+ const desc = node.getAttribute('data-desc') || '';
1831
+ const special = node.getAttribute('data-special') || '';
1832
+ const souls = node.getAttribute('data-souls') || '0';
1833
+ const img = node.getAttribute('data-img') || '';
1834
+ const nameEl = modal.querySelector('.location-panel-name');
1835
+ const descEl = modal.querySelector('.location-panel-desc');
1836
+ const specialEl = modal.querySelector('.location-panel-special');
1837
+ const soulsEl = modal.querySelector('.location-soul-count');
1838
+ const banner = modal.querySelector('.location-panel-banner');
1839
+ if (nameEl) nameEl.textContent = name;
1840
+ if (descEl) descEl.textContent = desc;
1841
+ if (specialEl) specialEl.textContent = special ? ('✦ ' + special) : '';
1842
+ if (soulsEl) soulsEl.textContent = souls + ' souls currently here';
1843
+ if (banner) {
1844
+ banner.style.backgroundImage = img ? ("url('" + img.replace(/'/g, "%27") + "')") : '';
1845
+ banner.style.backgroundSize = 'cover';
1846
+ banner.style.backgroundPosition = 'center';
1847
+ }
1848
+ modal.classList.add('is-open');
1849
+ document.querySelectorAll('.location-node').forEach((n) => {
1850
+ n.classList.toggle('location-selected', n.getAttribute('data-location-id') === id);
1851
+ });
1852
+ triggerGradioInput('map_location_pick', id);
1853
+ };
1854
+
1855
  const bind = () => {
1856
+ document.querySelectorAll('#world-map .location-node, [id="world-map"] .location-node').forEach((node) => {
 
 
1857
  node.style.cursor = 'pointer';
 
 
 
 
 
 
1858
  });
1859
  };
1860
+
1861
+ if (!window._aetherMapDelegationBound) {
1862
+ window._aetherMapDelegationBound = true;
1863
+ document.addEventListener('click', (e) => {
1864
+ const node = e.target.closest('.location-node[data-location-id]');
1865
+ if (!node) return;
1866
+ const inMap = node.closest('#world-map') || node.closest('[id="world-map"]');
1867
+ if (!inMap) return;
1868
+ e.preventDefault();
1869
+ e.stopPropagation();
1870
+ openLocationFromNode(node);
1871
+ }, true);
1872
+ document.addEventListener('keydown', (e) => {
1873
+ if (e.key !== 'Enter' && e.key !== ' ') return;
1874
+ const node = e.target.closest('.location-node[data-location-id]');
1875
+ if (!node) return;
1876
+ const inMap = node.closest('#world-map') || node.closest('[id="world-map"]');
1877
+ if (!inMap) return;
1878
+ e.preventDefault();
1879
+ openLocationFromNode(node);
1880
+ });
1881
+ }
1882
  const fireHiddenInput = (id, value) => {
1883
  const elemId = (id || '').replace(/^#/, '');
1884
  triggerGradioInput(elemId, value);
 
1899
  const closeLocationModal = () => {
1900
  document.querySelectorAll('.location-modal-overlay').forEach((m) => m.classList.remove('is-open'));
1901
  };
1902
+ window._aetherCloseLocationModal = closeLocationModal;
1903
 
1904
  const bindSpreadClicks = () => {
1905
  document.querySelectorAll('.place-ribbon-item').forEach((btn) => {
ui/map.py CHANGED
@@ -2,6 +2,7 @@
2
 
3
  from __future__ import annotations
4
 
 
5
  import math
6
  import random
7
 
@@ -126,9 +127,24 @@ def render_world_map(selected_location_id: int | None = None) -> str:
126
  count = loc["entity_count"]
127
  icon_path = LOCATION_ICONS.get(loc["slug"], "M0,-6 A6,6 0 1,1 0,6")
128
 
 
 
 
 
129
  svg_parts.append(
130
  f'<g class="location-node{pulse_class}{selected_class}" '
131
- f'data-location-id="{loc["id"]}" transform="translate({x},{y})">'
 
 
 
 
 
 
 
 
 
 
 
132
  )
133
 
134
  filter_id = "pulse-glow" if is_affected else "glow"
 
2
 
3
  from __future__ import annotations
4
 
5
+ import html
6
  import math
7
  import random
8
 
 
127
  count = loc["entity_count"]
128
  icon_path = LOCATION_ICONS.get(loc["slug"], "M0,-6 A6,6 0 1,1 0,6")
129
 
130
+ img = assets.location_image_url(loc.get("slug")) or ""
131
+ special = loc.get("special_property", "") or ""
132
+ desc = loc.get("short_description", "") or ""
133
+
134
  svg_parts.append(
135
  f'<g class="location-node{pulse_class}{selected_class}" '
136
+ f'data-location-id="{loc["id"]}" '
137
+ f'data-name="{html.escape(loc["name"], quote=True)}" '
138
+ f'data-desc="{html.escape(desc, quote=True)}" '
139
+ f'data-special="{html.escape(special, quote=True)}" '
140
+ f'data-img="{html.escape(img, quote=True)}" '
141
+ f'data-souls="{count}" '
142
+ f'role="button" tabindex="0" '
143
+ f'transform="translate({x},{y})">'
144
+ )
145
+
146
+ svg_parts.append(
147
+ '<circle r="50" fill="transparent" class="location-hit" pointer-events="all"/>'
148
  )
149
 
150
  filter_id = "pulse-glow" if is_affected else "glow"
ui/styles.css CHANGED
@@ -1966,6 +1966,12 @@ body,
1966
 
1967
  .location-node {
1968
  filter: drop-shadow(0 7px 12px rgba(0,0,0,0.36));
 
 
 
 
 
 
1969
  }
1970
 
1971
  .location-selected .location-circle {
@@ -3866,11 +3872,11 @@ body,
3866
  font-family: 'Cinzel', serif !important;
3867
  }
3868
 
3869
- .tome-printed-page #world-map,
3870
- .tome-printed-page .realm-map {
3871
- flex: 1 1 auto;
3872
- min-height: 0;
3873
- max-height: 360px;
3874
  width: 100% !important;
3875
  border: 1px solid rgba(98, 62, 28, 0.28) !important;
3876
  border-radius: 2px !important;
@@ -4774,8 +4780,16 @@ body.aether-realm-entered #realm-opening-host {
4774
  0 18px 38px rgba(76, 50, 22, 0.26) !important;
4775
  }
4776
 
4777
- /* Page I right distribute content vertically */
4778
- .tome-page-centered {
 
 
 
 
 
 
 
 
4779
  display: flex !important;
4780
  flex-direction: column !important;
4781
  justify-content: space-between !important;
@@ -4783,6 +4797,77 @@ body.aether-realm-entered #realm-opening-host {
4783
  min-height: 0 !important;
4784
  }
4785
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4786
  .tome-page-centered .realm-stats-strip,
4787
  .tome-page-centered .current-event,
4788
  .tome-page-centered .realm-pulse,
@@ -4799,9 +4884,34 @@ body.aether-realm-entered #realm-opening-host {
4799
 
4800
  .tome-page-left .realm-map,
4801
  .tome-page-left #world-map {
4802
- flex: 1 1 auto;
4803
- min-height: 200px;
4804
- max-height: 42%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4805
  }
4806
 
4807
  /* Explore left — padding */
 
1966
 
1967
  .location-node {
1968
  filter: drop-shadow(0 7px 12px rgba(0,0,0,0.36));
1969
+ cursor: pointer;
1970
+ pointer-events: all;
1971
+ }
1972
+
1973
+ .location-node .location-hit {
1974
+ pointer-events: all;
1975
  }
1976
 
1977
  .location-selected .location-circle {
 
3872
  font-family: 'Cinzel', serif !important;
3873
  }
3874
 
3875
+ .tome-page-left.tome-printed-page #world-map,
3876
+ .tome-page-left.tome-printed-page .realm-map {
3877
+ flex: 0 0 auto !important;
3878
+ min-height: 200px !important;
3879
+ max-height: min(300px, 36vh) !important;
3880
  width: 100% !important;
3881
  border: 1px solid rgba(98, 62, 28, 0.28) !important;
3882
  border-radius: 2px !important;
 
4780
  0 18px 38px rgba(76, 50, 22, 0.26) !important;
4781
  }
4782
 
4783
+ /* Page I — left: tight map stack; right: distribute vertically */
4784
+ .tome-page-left.tome-page-centered {
4785
+ display: flex !important;
4786
+ flex-direction: column !important;
4787
+ justify-content: flex-start !important;
4788
+ gap: 0.28rem !important;
4789
+ min-height: 0 !important;
4790
+ }
4791
+
4792
+ .tome-page-right.tome-page-centered {
4793
  display: flex !important;
4794
  flex-direction: column !important;
4795
  justify-content: space-between !important;
 
4797
  min-height: 0 !important;
4798
  }
4799
 
4800
+ .tome-page-left .tome-ornament.bottom {
4801
+ margin-top: auto !important;
4802
+ padding-top: 0.2rem !important;
4803
+ }
4804
+
4805
+ .tome-map-chrome {
4806
+ display: flex;
4807
+ align-items: center;
4808
+ justify-content: center;
4809
+ gap: 0.45rem;
4810
+ margin: 0.1rem 0 0.15rem;
4811
+ flex-shrink: 0;
4812
+ }
4813
+
4814
+ .tome-map-chrome-line {
4815
+ flex: 1;
4816
+ max-width: 72px;
4817
+ height: 1px;
4818
+ background: linear-gradient(90deg, transparent, rgba(138, 106, 58, 0.35), transparent);
4819
+ }
4820
+
4821
+ .tome-map-chrome-label {
4822
+ font-family: 'Cinzel', serif;
4823
+ font-size: 0.58rem;
4824
+ letter-spacing: 0.2em;
4825
+ text-transform: uppercase;
4826
+ color: rgba(122, 90, 40, 0.55);
4827
+ white-space: nowrap;
4828
+ }
4829
+
4830
+ .tome-map-chrome-glyph {
4831
+ color: rgba(138, 106, 58, 0.45);
4832
+ font-size: 0.7rem;
4833
+ }
4834
+
4835
+ .tome-map-divider {
4836
+ display: flex;
4837
+ align-items: center;
4838
+ justify-content: center;
4839
+ gap: 0.5rem;
4840
+ margin: 0.12rem 0 0.08rem;
4841
+ flex-shrink: 0;
4842
+ }
4843
+
4844
+ .tome-map-divider-text {
4845
+ font-family: 'Cinzel', serif;
4846
+ font-size: 0.58rem;
4847
+ letter-spacing: 0.16em;
4848
+ color: rgba(122, 90, 40, 0.5);
4849
+ white-space: nowrap;
4850
+ }
4851
+
4852
+ .tome-map-vine {
4853
+ flex: 1;
4854
+ height: 10px;
4855
+ max-width: 88px;
4856
+ background-repeat: no-repeat;
4857
+ background-size: contain;
4858
+ opacity: 0.55;
4859
+ }
4860
+
4861
+ .tome-map-vine.left {
4862
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 80 10'%3E%3Cpath d='M0 5 Q20 0 40 5 T80 5' fill='none' stroke='%238a6a3a' stroke-width='0.8'/%3E%3Ccircle cx='40' cy='5' r='1.2' fill='%23c9a13b'/%3E%3C/svg%3E");
4863
+ background-position: right center;
4864
+ }
4865
+
4866
+ .tome-map-vine.right {
4867
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 80 10'%3E%3Cpath d='M0 5 Q20 10 40 5 T80 5' fill='none' stroke='%238a6a3a' stroke-width='0.8'/%3E%3Ccircle cx='40' cy='5' r='1.2' fill='%23c9a13b'/%3E%3C/svg%3E");
4868
+ background-position: left center;
4869
+ }
4870
+
4871
  .tome-page-centered .realm-stats-strip,
4872
  .tome-page-centered .current-event,
4873
  .tome-page-centered .realm-pulse,
 
4884
 
4885
  .tome-page-left .realm-map,
4886
  .tome-page-left #world-map {
4887
+ flex: 0 0 auto !important;
4888
+ width: 100% !important;
4889
+ min-height: 200px !important;
4890
+ max-height: min(300px, 36vh) !important;
4891
+ margin: 0 !important;
4892
+ }
4893
+
4894
+ .tome-page-left #world-map .html-container,
4895
+ .tome-page-left #world-map .prose {
4896
+ min-height: 0 !important;
4897
+ }
4898
+
4899
+ .tome-page-left .location-panel-host {
4900
+ flex-shrink: 0 !important;
4901
+ margin: 0 !important;
4902
+ }
4903
+
4904
+ .tome-page-left .location-panel-host .map-hint {
4905
+ padding: 0.35rem 0.55rem !important;
4906
+ margin: 0 !important;
4907
+ font-size: 0.78rem !important;
4908
+ border: 1px solid rgba(138, 106, 58, 0.22);
4909
+ border-radius: 4px;
4910
+ background: rgba(255, 249, 232, 0.5);
4911
+ }
4912
+
4913
+ #aether-location-modal {
4914
+ z-index: 90000 !important;
4915
  }
4916
 
4917
  /* Explore left — padding */