Deploy Aether Garden application
Browse files- app.py +186 -46
- ui/book_display.py +36 -8
- ui/place_ribbon.py +5 -3
- ui/relationship_graph.py +16 -6
- ui/styles.css +269 -1
app.py
CHANGED
|
@@ -96,9 +96,10 @@ def opening_gate_html() -> str:
|
|
| 96 |
</div>
|
| 97 |
<div class="realm-opening-inner">
|
| 98 |
<div class="realm-book-stage">
|
| 99 |
-
<div class="realm-book-
|
|
|
|
| 100 |
<div class="realm-book-spine" aria-hidden="true"></div>
|
| 101 |
-
<div class="realm-cover-card">
|
| 102 |
<p class="realm-cover-kicker">Book of Ages, Vol. I</p>
|
| 103 |
<div class="realm-cover-calligraphy" aria-hidden="true"></div>
|
| 104 |
<h1 class="realm-cover-title"><span>AETHER</span><span>GARDEN</span></h1>
|
|
@@ -106,6 +107,15 @@ def opening_gate_html() -> str:
|
|
| 106 |
<button id="realm-opening-enter" type="button">Open The Tome</button>
|
| 107 |
</div>
|
| 108 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
<div class="realm-book-open" id="realm-book-open" aria-hidden="true">
|
| 110 |
<div class="realm-book-page realm-book-page-left"></div>
|
| 111 |
<div class="realm-book-page realm-book-page-right"></div>
|
|
@@ -213,11 +223,15 @@ def _prewarm_oracle() -> None:
|
|
| 213 |
|
| 214 |
def render_location_panel(location_id: int | None) -> str:
|
| 215 |
if not location_id:
|
| 216 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
|
| 218 |
location = get_location_by_id(location_id)
|
| 219 |
if not location:
|
| 220 |
-
return ""
|
| 221 |
|
| 222 |
entities = get_entities_by_location(location_id)
|
| 223 |
chips = "".join(
|
|
@@ -231,18 +245,23 @@ def render_location_panel(location_id: int | None) -> str:
|
|
| 231 |
special = location.get("special_property", "")
|
| 232 |
|
| 233 |
return f"""
|
| 234 |
-
<div
|
| 235 |
-
<div class="location-
|
| 236 |
-
<div class="location-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
</div>
|
| 247 |
</div>
|
| 248 |
"""
|
|
@@ -341,8 +360,18 @@ def filter_entities(location_filter, type_filter, status_filter, search):
|
|
| 341 |
|
| 342 |
|
| 343 |
def filter_book(day, entry_type, search):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
return render_book_for_day(
|
| 345 |
-
day=
|
| 346 |
entry_type=entry_type,
|
| 347 |
search=search,
|
| 348 |
limit=12,
|
|
@@ -616,10 +645,11 @@ def build_app() -> gr.Blocks:
|
|
| 616 |
with gr.Tab("The Realm", id="realm"):
|
| 617 |
with gr.Column(elem_classes=["tome-spread-shell"]):
|
| 618 |
with gr.Row(elem_classes=["tome-spread-row"]):
|
| 619 |
-
with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page"]):
|
| 620 |
gr.HTML(
|
|
|
|
| 621 |
'<p class="tome-printed-kicker">The Living Realm</p>'
|
| 622 |
-
'<p class="tome-printed-hint">Tap a place on the map β
|
| 623 |
)
|
| 624 |
world_map = gr.HTML(
|
| 625 |
value=render_world_map(),
|
|
@@ -627,9 +657,12 @@ def build_app() -> gr.Blocks:
|
|
| 627 |
)
|
| 628 |
location_panel = gr.HTML(
|
| 629 |
value=render_location_panel(None),
|
|
|
|
| 630 |
)
|
|
|
|
| 631 |
|
| 632 |
-
with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page"]):
|
|
|
|
| 633 |
realm_stats = gr.HTML(value=render_realm_stats())
|
| 634 |
current_event = gr.HTML(value=render_current_event())
|
| 635 |
realm_pulse = gr.HTML(value=render_realm_pulse(limit=4))
|
|
@@ -668,6 +701,7 @@ def build_app() -> gr.Blocks:
|
|
| 668 |
lines=2,
|
| 669 |
interactive=True,
|
| 670 |
)
|
|
|
|
| 671 |
|
| 672 |
with gr.Column(elem_classes=["tome-offpage-store"]):
|
| 673 |
world_vignette = gr.HTML(value=render_world_vignette())
|
|
@@ -708,7 +742,11 @@ def build_app() -> gr.Blocks:
|
|
| 708 |
with gr.Column(elem_classes=["tome-spread-shell"]):
|
| 709 |
with gr.Row(elem_classes=["tome-spread-row"]):
|
| 710 |
with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page", "tome-explore-index"]):
|
| 711 |
-
place_ribbon = gr.HTML(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 712 |
explore_place_pick = gr.Textbox(visible=False, elem_id="explore_place_pick")
|
| 713 |
wander_btn = gr.Button("β¦ Wander at random", variant="secondary", size="sm")
|
| 714 |
|
|
@@ -717,7 +755,12 @@ def build_app() -> gr.Blocks:
|
|
| 717 |
'<p class="tome-printed-kicker">You step inside</p>'
|
| 718 |
'<p class="explore-controls-hint">WASD walk Β· drag look Β· E talk Β· golden wisps are live visitors</p>'
|
| 719 |
)
|
| 720 |
-
diorama_view = gr.HTML(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 721 |
portal_pick = gr.Textbox(visible=False, elem_id="portal_location_pick")
|
| 722 |
|
| 723 |
with gr.Column(elem_classes=["tome-offpage-store"]):
|
|
@@ -752,6 +795,12 @@ def build_app() -> gr.Blocks:
|
|
| 752 |
with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page", "tome-book-index"]):
|
| 753 |
day_tabs = gr.HTML(value=render_day_tabs(_init_book_day))
|
| 754 |
book_day_pick = gr.Textbox(visible=False, elem_id="book_day_pick")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 755 |
book_filter = gr.Dropdown(
|
| 756 |
choices=[
|
| 757 |
("All", "all"),
|
|
@@ -765,7 +814,7 @@ def build_app() -> gr.Blocks:
|
|
| 765 |
label="Filter",
|
| 766 |
)
|
| 767 |
book_search = gr.Textbox(
|
| 768 |
-
placeholder=
|
| 769 |
label="Search",
|
| 770 |
)
|
| 771 |
with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page"]):
|
|
@@ -777,12 +826,29 @@ def build_app() -> gr.Blocks:
|
|
| 777 |
with gr.Column(elem_classes=["tome-spread-shell"]):
|
| 778 |
with gr.Row(elem_classes=["tome-spread-row"]):
|
| 779 |
with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page"]):
|
| 780 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 781 |
with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page"]):
|
| 782 |
rel_graph = gr.HTML(value=render_relationship_graph())
|
| 783 |
|
| 784 |
with gr.Tab("Souls of the Garden", id="entities"):
|
| 785 |
-
featured_chars = gr.HTML(value=render_featured_characters())
|
| 786 |
entity_pick = gr.Textbox(visible=False, elem_id="entity_pick")
|
| 787 |
|
| 788 |
with gr.Column(elem_classes=["tome-spread-shell"]):
|
|
@@ -810,9 +876,21 @@ def build_app() -> gr.Blocks:
|
|
| 810 |
scale=1,
|
| 811 |
)
|
| 812 |
entity_search = gr.Textbox(placeholder="Searchβ¦", label="Find", scale=1)
|
| 813 |
-
entity_grid = gr.HTML(
|
| 814 |
-
|
| 815 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 816 |
|
| 817 |
_summon_outputs = [
|
| 818 |
world_map, header, activity_feed, entity_result, location_panel,
|
|
@@ -994,14 +1072,37 @@ def build_app() -> gr.Blocks:
|
|
| 994 |
day = int(day_state or 1)
|
| 995 |
return day, render_day_tabs(day), filter_book(day, entry_type, search)
|
| 996 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 997 |
book_day_pick.change(
|
| 998 |
_book_day_change,
|
| 999 |
[book_day_pick, book_day, book_filter, book_search],
|
| 1000 |
[book_day, day_tabs, book_display],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1001 |
)
|
| 1002 |
book_filter.change(filter_book, [book_day, book_filter, book_search], [book_display])
|
| 1003 |
book_search.change(filter_book, [book_day, book_filter, book_search], [book_display])
|
| 1004 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1005 |
for f in [loc_filter, type_filter, status_filter, entity_search]:
|
| 1006 |
f.change(
|
| 1007 |
filter_entities,
|
|
@@ -1144,29 +1245,34 @@ def build_app() -> gr.Blocks:
|
|
| 1144 |
|
| 1145 |
const closeBook = () => {
|
| 1146 |
const gate = document.getElementById('realm-opening');
|
|
|
|
| 1147 |
if (!gate) return;
|
| 1148 |
gate.classList.remove('is-closed', 'is-opened', 'is-prologue', 'is-unfolded', 'is-unfolding');
|
| 1149 |
-
gate.classList.add('is-reclosed');
|
| 1150 |
-
const closedBook = document.getElementById('realm-book-closed');
|
| 1151 |
const openBook = document.getElementById('realm-book-open');
|
| 1152 |
const prologue = document.getElementById('realm-prologue');
|
| 1153 |
-
if (closedBook) {
|
| 1154 |
-
closedBook.classList.remove('is-opening');
|
| 1155 |
-
closedBook.style.opacity = '';
|
| 1156 |
-
closedBook.style.pointerEvents = '';
|
| 1157 |
-
}
|
| 1158 |
if (openBook) openBook.setAttribute('aria-hidden', 'true');
|
| 1159 |
if (prologue) {
|
| 1160 |
prologue.classList.remove('visible');
|
| 1161 |
prologue.setAttribute('aria-hidden', 'true');
|
| 1162 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1163 |
applyTurnFx('prev');
|
| 1164 |
};
|
| 1165 |
|
| 1166 |
const reopenBook = () => {
|
| 1167 |
const gate = document.getElementById('realm-opening');
|
|
|
|
| 1168 |
if (!gate) return;
|
| 1169 |
-
|
|
|
|
| 1170 |
gate.classList.add('is-opened', 'is-closed');
|
| 1171 |
playOpeningChime();
|
| 1172 |
};
|
|
@@ -1323,6 +1429,7 @@ def build_app() -> gr.Blocks:
|
|
| 1323 |
if (!gate || gate.dataset.bound) return;
|
| 1324 |
gate.dataset.bound = '1';
|
| 1325 |
|
|
|
|
| 1326 |
const closedBook = document.getElementById('realm-book-closed');
|
| 1327 |
const openBook = document.getElementById('realm-book-open');
|
| 1328 |
const prologue = document.getElementById('realm-prologue');
|
|
@@ -1388,7 +1495,7 @@ def build_app() -> gr.Blocks:
|
|
| 1388 |
const beginUnfold = () => {
|
| 1389 |
playOpeningChime();
|
| 1390 |
gate.classList.add('is-unfolding');
|
| 1391 |
-
if (
|
| 1392 |
if (openBook) openBook.setAttribute('aria-hidden', 'false');
|
| 1393 |
setTimeout(() => {
|
| 1394 |
gate.classList.add('is-unfolded', 'is-prologue');
|
|
@@ -1403,7 +1510,8 @@ def build_app() -> gr.Blocks:
|
|
| 1403 |
gate.addEventListener('click', (e) => {
|
| 1404 |
const target = e.target;
|
| 1405 |
if (!(target instanceof Element)) return;
|
| 1406 |
-
if (gate.classList.contains('is-reclosed') &&
|
|
|
|
| 1407 |
e.preventDefault();
|
| 1408 |
e.stopPropagation();
|
| 1409 |
reopenBook();
|
|
@@ -1461,19 +1569,31 @@ def build_app() -> gr.Blocks:
|
|
| 1461 |
}
|
| 1462 |
};
|
| 1463 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1464 |
const bindSpreadClicks = () => {
|
| 1465 |
document.querySelectorAll('.place-ribbon-item').forEach((btn) => {
|
| 1466 |
-
|
| 1467 |
-
btn.dataset.ribbonBound = '1';
|
| 1468 |
-
btn.addEventListener('click', () => {
|
| 1469 |
const id = btn.getAttribute('data-location-id');
|
| 1470 |
if (id) fireHiddenInput('#explore_place_pick', id);
|
| 1471 |
});
|
| 1472 |
});
|
| 1473 |
document.querySelectorAll('.day-tab').forEach((btn) => {
|
| 1474 |
-
|
| 1475 |
-
btn.dataset.dayBound = '1';
|
| 1476 |
-
btn.addEventListener('click', () => {
|
| 1477 |
const day = btn.getAttribute('data-day');
|
| 1478 |
if (day) fireHiddenInput('#book_day_pick', day);
|
| 1479 |
});
|
|
@@ -1482,16 +1602,36 @@ def build_app() -> gr.Blocks:
|
|
| 1482 |
if (card.dataset.entityBound) return;
|
| 1483 |
card.dataset.entityBound = '1';
|
| 1484 |
card.style.cursor = 'pointer';
|
| 1485 |
-
|
| 1486 |
const id = card.getAttribute('data-entity-id');
|
| 1487 |
if (id) fireHiddenInput('#entity_pick', id);
|
| 1488 |
});
|
| 1489 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1490 |
};
|
| 1491 |
|
| 1492 |
bind();
|
| 1493 |
bindSpreadClicks();
|
| 1494 |
-
new MutationObserver(() => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1495 |
|
| 1496 |
window._aetherHoverDiorama = false;
|
| 1497 |
window._aetherDioramaActive = false;
|
|
|
|
| 96 |
</div>
|
| 97 |
<div class="realm-opening-inner">
|
| 98 |
<div class="realm-book-stage">
|
| 99 |
+
<div class="realm-book-3d" id="realm-book-3d">
|
| 100 |
+
<div class="realm-book-face realm-book-front" id="realm-book-closed">
|
| 101 |
<div class="realm-book-spine" aria-hidden="true"></div>
|
| 102 |
+
<div class="realm-cover-card realm-cover-front">
|
| 103 |
<p class="realm-cover-kicker">Book of Ages, Vol. I</p>
|
| 104 |
<div class="realm-cover-calligraphy" aria-hidden="true"></div>
|
| 105 |
<h1 class="realm-cover-title"><span>AETHER</span><span>GARDEN</span></h1>
|
|
|
|
| 107 |
<button id="realm-opening-enter" type="button">Open The Tome</button>
|
| 108 |
</div>
|
| 109 |
</div>
|
| 110 |
+
<div class="realm-book-face realm-book-back" id="realm-book-back" aria-hidden="true">
|
| 111 |
+
<div class="realm-cover-card realm-cover-back">
|
| 112 |
+
<div class="realm-back-ornament" aria-hidden="true">β§</div>
|
| 113 |
+
<p class="realm-cover-kicker">End of Vol. I</p>
|
| 114 |
+
<p class="realm-back-text">The Realm remembereth all things.<br/>Close the cover β until thou returnest.</p>
|
| 115 |
+
<button id="realm-reopen-enter" type="button">Open The Tome</button>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
<div class="realm-book-open" id="realm-book-open" aria-hidden="true">
|
| 120 |
<div class="realm-book-page realm-book-page-left"></div>
|
| 121 |
<div class="realm-book-page realm-book-page-right"></div>
|
|
|
|
| 223 |
|
| 224 |
def render_location_panel(location_id: int | None) -> str:
|
| 225 |
if not location_id:
|
| 226 |
+
return (
|
| 227 |
+
'<div id="location-panel-host" class="location-panel-host">'
|
| 228 |
+
'<p class="feed-empty map-hint">Click a glowing place on the map to unveil its secrets.</p>'
|
| 229 |
+
'</div>'
|
| 230 |
+
)
|
| 231 |
|
| 232 |
location = get_location_by_id(location_id)
|
| 233 |
if not location:
|
| 234 |
+
return '<div id="location-panel-host" class="location-panel-host"></div>'
|
| 235 |
|
| 236 |
entities = get_entities_by_location(location_id)
|
| 237 |
chips = "".join(
|
|
|
|
| 245 |
special = location.get("special_property", "")
|
| 246 |
|
| 247 |
return f"""
|
| 248 |
+
<div id="location-panel-host" class="location-panel-host" data-open="1">
|
| 249 |
+
<div class="location-modal-overlay is-open" id="location-modal">
|
| 250 |
+
<div class="location-modal-card">
|
| 251 |
+
<div class="location-modal-close" role="button" tabindex="0" aria-label="Close">β</div>
|
| 252 |
+
<div class="location-panel location-panel-animate open">
|
| 253 |
+
<div class="location-panel-banner" style="{img_style}">
|
| 254 |
+
<div class="location-panel-scrim"></div>
|
| 255 |
+
<h3 class="location-panel-name">{location['name']}</h3>
|
| 256 |
+
</div>
|
| 257 |
+
<div class="location-panel-content">
|
| 258 |
+
<p class="location-panel-desc">{location['short_description']}</p>
|
| 259 |
+
<p class="location-panel-special">β¦ {special}</p>
|
| 260 |
+
<p class="location-soul-count">{len(entities)} souls currently here</p>
|
| 261 |
+
<div class="location-entity-list">{chips}{more}</div>
|
| 262 |
+
</div>
|
| 263 |
+
</div>
|
| 264 |
+
</div>
|
| 265 |
</div>
|
| 266 |
</div>
|
| 267 |
"""
|
|
|
|
| 360 |
|
| 361 |
|
| 362 |
def filter_book(day, entry_type, search):
|
| 363 |
+
from ui.book_display import parse_day_query
|
| 364 |
+
|
| 365 |
+
parsed = parse_day_query(search)
|
| 366 |
+
if parsed is not None:
|
| 367 |
+
day = parsed
|
| 368 |
+
search = ""
|
| 369 |
+
try:
|
| 370 |
+
day_int = int(day) if day is not None else None
|
| 371 |
+
except (TypeError, ValueError):
|
| 372 |
+
day_int = None
|
| 373 |
return render_book_for_day(
|
| 374 |
+
day=day_int,
|
| 375 |
entry_type=entry_type,
|
| 376 |
search=search,
|
| 377 |
limit=12,
|
|
|
|
| 645 |
with gr.Tab("The Realm", id="realm"):
|
| 646 |
with gr.Column(elem_classes=["tome-spread-shell"]):
|
| 647 |
with gr.Row(elem_classes=["tome-spread-row"]):
|
| 648 |
+
with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page", "tome-page-centered"]):
|
| 649 |
gr.HTML(
|
| 650 |
+
'<div class="tome-ornament top" aria-hidden="true">β¦ βββ β§ βββ β¦</div>'
|
| 651 |
'<p class="tome-printed-kicker">The Living Realm</p>'
|
| 652 |
+
'<p class="tome-printed-hint">Tap a place on the map β a vision shall rise.</p>'
|
| 653 |
)
|
| 654 |
world_map = gr.HTML(
|
| 655 |
value=render_world_map(),
|
|
|
|
| 657 |
)
|
| 658 |
location_panel = gr.HTML(
|
| 659 |
value=render_location_panel(None),
|
| 660 |
+
elem_id="location-panel-host-wrap",
|
| 661 |
)
|
| 662 |
+
gr.HTML('<div class="tome-ornament bottom" aria-hidden="true">β§</div>')
|
| 663 |
|
| 664 |
+
with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page", "tome-page-centered"]):
|
| 665 |
+
gr.HTML('<div class="tome-ornament top" aria-hidden="true">β¦ βββ β§ βββ β¦</div>')
|
| 666 |
realm_stats = gr.HTML(value=render_realm_stats())
|
| 667 |
current_event = gr.HTML(value=render_current_event())
|
| 668 |
realm_pulse = gr.HTML(value=render_realm_pulse(limit=4))
|
|
|
|
| 701 |
lines=2,
|
| 702 |
interactive=True,
|
| 703 |
)
|
| 704 |
+
gr.HTML('<div class="tome-ornament bottom" aria-hidden="true">β§</div>')
|
| 705 |
|
| 706 |
with gr.Column(elem_classes=["tome-offpage-store"]):
|
| 707 |
world_vignette = gr.HTML(value=render_world_vignette())
|
|
|
|
| 742 |
with gr.Column(elem_classes=["tome-spread-shell"]):
|
| 743 |
with gr.Row(elem_classes=["tome-spread-row"]):
|
| 744 |
with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page", "tome-explore-index"]):
|
| 745 |
+
place_ribbon = gr.HTML(
|
| 746 |
+
value=render_place_ribbon(_default_loc_id),
|
| 747 |
+
container=False,
|
| 748 |
+
padding=False,
|
| 749 |
+
)
|
| 750 |
explore_place_pick = gr.Textbox(visible=False, elem_id="explore_place_pick")
|
| 751 |
wander_btn = gr.Button("β¦ Wander at random", variant="secondary", size="sm")
|
| 752 |
|
|
|
|
| 755 |
'<p class="tome-printed-kicker">You step inside</p>'
|
| 756 |
'<p class="explore-controls-hint">WASD walk Β· drag look Β· E talk Β· golden wisps are live visitors</p>'
|
| 757 |
)
|
| 758 |
+
diorama_view = gr.HTML(
|
| 759 |
+
value=render_diorama(_default_loc_id),
|
| 760 |
+
container=False,
|
| 761 |
+
padding=False,
|
| 762 |
+
elem_id="explore-diorama-host",
|
| 763 |
+
)
|
| 764 |
portal_pick = gr.Textbox(visible=False, elem_id="portal_location_pick")
|
| 765 |
|
| 766 |
with gr.Column(elem_classes=["tome-offpage-store"]):
|
|
|
|
| 795 |
with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page", "tome-book-index"]):
|
| 796 |
day_tabs = gr.HTML(value=render_day_tabs(_init_book_day))
|
| 797 |
book_day_pick = gr.Textbox(visible=False, elem_id="book_day_pick")
|
| 798 |
+
_day_choices = list(range(max(1, _init_book_day - 14), _init_book_day + 1))
|
| 799 |
+
book_day_select = gr.Dropdown(
|
| 800 |
+
choices=[(f"Day {d}", d) for d in reversed(_day_choices)],
|
| 801 |
+
value=_init_book_day,
|
| 802 |
+
label="Jump to day",
|
| 803 |
+
)
|
| 804 |
book_filter = gr.Dropdown(
|
| 805 |
choices=[
|
| 806 |
("All", "all"),
|
|
|
|
| 814 |
label="Filter",
|
| 815 |
)
|
| 816 |
book_search = gr.Textbox(
|
| 817 |
+
placeholder='Search name⦠or type "Day 3"',
|
| 818 |
label="Search",
|
| 819 |
)
|
| 820 |
with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page"]):
|
|
|
|
| 826 |
with gr.Column(elem_classes=["tome-spread-shell"]):
|
| 827 |
with gr.Row(elem_classes=["tome-spread-row"]):
|
| 828 |
with gr.Column(scale=1, elem_classes=["tome-page-left", "tome-printed-page"]):
|
| 829 |
+
bonds_type_filter = gr.Dropdown(
|
| 830 |
+
choices=[
|
| 831 |
+
("All bonds", "all"),
|
| 832 |
+
("Allied", "allied"),
|
| 833 |
+
("Friends", "friends"),
|
| 834 |
+
("Rivals", "rivals"),
|
| 835 |
+
("Enemies", "enemies"),
|
| 836 |
+
("Mentor", "mentor"),
|
| 837 |
+
("Owes debt", "owes_debt"),
|
| 838 |
+
("Curious", "curious"),
|
| 839 |
+
],
|
| 840 |
+
value="all",
|
| 841 |
+
label="Show connections",
|
| 842 |
+
)
|
| 843 |
+
bonds_sidebar = gr.HTML(
|
| 844 |
+
value=render_bonds_sidebar(),
|
| 845 |
+
container=False,
|
| 846 |
+
padding=False,
|
| 847 |
+
)
|
| 848 |
with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page"]):
|
| 849 |
rel_graph = gr.HTML(value=render_relationship_graph())
|
| 850 |
|
| 851 |
with gr.Tab("Souls of the Garden", id="entities"):
|
|
|
|
| 852 |
entity_pick = gr.Textbox(visible=False, elem_id="entity_pick")
|
| 853 |
|
| 854 |
with gr.Column(elem_classes=["tome-spread-shell"]):
|
|
|
|
| 876 |
scale=1,
|
| 877 |
)
|
| 878 |
entity_search = gr.Textbox(placeholder="Searchβ¦", label="Find", scale=1)
|
| 879 |
+
entity_grid = gr.HTML(
|
| 880 |
+
value=render_entity_grid(get_all_entities()),
|
| 881 |
+
container=False,
|
| 882 |
+
padding=False,
|
| 883 |
+
)
|
| 884 |
+
with gr.Column(scale=1, elem_classes=["tome-page-right", "tome-printed-page", "tome-souls-stage"]):
|
| 885 |
+
_first_ent = get_all_entities(limit=1)
|
| 886 |
+
entity_detail = gr.HTML(
|
| 887 |
+
value=render_entity_hologram(_first_ent[0] if _first_ent else None),
|
| 888 |
+
container=False,
|
| 889 |
+
padding=False,
|
| 890 |
+
)
|
| 891 |
+
|
| 892 |
+
with gr.Column(elem_classes=["tome-offpage-store"]):
|
| 893 |
+
featured_chars = gr.HTML(value=render_featured_characters())
|
| 894 |
|
| 895 |
_summon_outputs = [
|
| 896 |
world_map, header, activity_feed, entity_result, location_panel,
|
|
|
|
| 1072 |
day = int(day_state or 1)
|
| 1073 |
return day, render_day_tabs(day), filter_book(day, entry_type, search)
|
| 1074 |
|
| 1075 |
+
def _book_day_select_change(day_val, entry_type, search):
|
| 1076 |
+
try:
|
| 1077 |
+
day = int(day_val)
|
| 1078 |
+
except (TypeError, ValueError):
|
| 1079 |
+
state = get_world_state()
|
| 1080 |
+
day = int(state.get("current_day", 1) or 1)
|
| 1081 |
+
return day, render_day_tabs(day), filter_book(day, entry_type, search)
|
| 1082 |
+
|
| 1083 |
book_day_pick.change(
|
| 1084 |
_book_day_change,
|
| 1085 |
[book_day_pick, book_day, book_filter, book_search],
|
| 1086 |
[book_day, day_tabs, book_display],
|
| 1087 |
+
).then(
|
| 1088 |
+
lambda d: d,
|
| 1089 |
+
inputs=[book_day],
|
| 1090 |
+
outputs=[book_day_select],
|
| 1091 |
+
)
|
| 1092 |
+
book_day_select.change(
|
| 1093 |
+
_book_day_select_change,
|
| 1094 |
+
[book_day_select, book_filter, book_search],
|
| 1095 |
+
[book_day, day_tabs, book_display],
|
| 1096 |
)
|
| 1097 |
book_filter.change(filter_book, [book_day, book_filter, book_search], [book_display])
|
| 1098 |
book_search.change(filter_book, [book_day, book_filter, book_search], [book_display])
|
| 1099 |
|
| 1100 |
+
bonds_type_filter.change(
|
| 1101 |
+
render_bonds_sidebar,
|
| 1102 |
+
[bonds_type_filter],
|
| 1103 |
+
[bonds_sidebar],
|
| 1104 |
+
)
|
| 1105 |
+
|
| 1106 |
for f in [loc_filter, type_filter, status_filter, entity_search]:
|
| 1107 |
f.change(
|
| 1108 |
filter_entities,
|
|
|
|
| 1245 |
|
| 1246 |
const closeBook = () => {
|
| 1247 |
const gate = document.getElementById('realm-opening');
|
| 1248 |
+
const book3d = document.getElementById('realm-book-3d');
|
| 1249 |
if (!gate) return;
|
| 1250 |
gate.classList.remove('is-closed', 'is-opened', 'is-prologue', 'is-unfolded', 'is-unfolding');
|
| 1251 |
+
gate.classList.add('is-closing', 'is-reclosed');
|
|
|
|
| 1252 |
const openBook = document.getElementById('realm-book-open');
|
| 1253 |
const prologue = document.getElementById('realm-prologue');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1254 |
if (openBook) openBook.setAttribute('aria-hidden', 'true');
|
| 1255 |
if (prologue) {
|
| 1256 |
prologue.classList.remove('visible');
|
| 1257 |
prologue.setAttribute('aria-hidden', 'true');
|
| 1258 |
}
|
| 1259 |
+
if (book3d) {
|
| 1260 |
+
book3d.classList.remove('is-opening');
|
| 1261 |
+
setTimeout(() => book3d.classList.add('show-back'), prefersReduced ? 0 : 80);
|
| 1262 |
+
}
|
| 1263 |
+
setTimeout(() => {
|
| 1264 |
+
gate.classList.remove('is-closing');
|
| 1265 |
+
gate.classList.add('is-back-visible');
|
| 1266 |
+
}, prefersReduced ? 60 : 1150);
|
| 1267 |
applyTurnFx('prev');
|
| 1268 |
};
|
| 1269 |
|
| 1270 |
const reopenBook = () => {
|
| 1271 |
const gate = document.getElementById('realm-opening');
|
| 1272 |
+
const book3d = document.getElementById('realm-book-3d');
|
| 1273 |
if (!gate) return;
|
| 1274 |
+
if (book3d) book3d.classList.remove('show-back', 'is-opening');
|
| 1275 |
+
gate.classList.remove('is-reclosed', 'is-back-visible', 'is-closing');
|
| 1276 |
gate.classList.add('is-opened', 'is-closed');
|
| 1277 |
playOpeningChime();
|
| 1278 |
};
|
|
|
|
| 1429 |
if (!gate || gate.dataset.bound) return;
|
| 1430 |
gate.dataset.bound = '1';
|
| 1431 |
|
| 1432 |
+
const book3d = document.getElementById('realm-book-3d');
|
| 1433 |
const closedBook = document.getElementById('realm-book-closed');
|
| 1434 |
const openBook = document.getElementById('realm-book-open');
|
| 1435 |
const prologue = document.getElementById('realm-prologue');
|
|
|
|
| 1495 |
const beginUnfold = () => {
|
| 1496 |
playOpeningChime();
|
| 1497 |
gate.classList.add('is-unfolding');
|
| 1498 |
+
if (book3d) book3d.classList.add('is-opening');
|
| 1499 |
if (openBook) openBook.setAttribute('aria-hidden', 'false');
|
| 1500 |
setTimeout(() => {
|
| 1501 |
gate.classList.add('is-unfolded', 'is-prologue');
|
|
|
|
| 1510 |
gate.addEventListener('click', (e) => {
|
| 1511 |
const target = e.target;
|
| 1512 |
if (!(target instanceof Element)) return;
|
| 1513 |
+
if (gate.classList.contains('is-reclosed') &&
|
| 1514 |
+
(target.id === 'realm-opening-enter' || target.id === 'realm-reopen-enter')) {
|
| 1515 |
e.preventDefault();
|
| 1516 |
e.stopPropagation();
|
| 1517 |
reopenBook();
|
|
|
|
| 1569 |
}
|
| 1570 |
};
|
| 1571 |
|
| 1572 |
+
const wireClickable = (el, handler) => {
|
| 1573 |
+
if (!el || el.dataset.boundClick) return;
|
| 1574 |
+
el.dataset.boundClick = '1';
|
| 1575 |
+
el.addEventListener('click', handler);
|
| 1576 |
+
el.addEventListener('keydown', (e) => {
|
| 1577 |
+
if (e.key === 'Enter' || e.key === ' ') {
|
| 1578 |
+
e.preventDefault();
|
| 1579 |
+
handler(e);
|
| 1580 |
+
}
|
| 1581 |
+
});
|
| 1582 |
+
};
|
| 1583 |
+
|
| 1584 |
+
const closeLocationModal = () => {
|
| 1585 |
+
document.querySelectorAll('.location-modal-overlay').forEach((m) => m.classList.remove('is-open'));
|
| 1586 |
+
};
|
| 1587 |
+
|
| 1588 |
const bindSpreadClicks = () => {
|
| 1589 |
document.querySelectorAll('.place-ribbon-item').forEach((btn) => {
|
| 1590 |
+
wireClickable(btn, () => {
|
|
|
|
|
|
|
| 1591 |
const id = btn.getAttribute('data-location-id');
|
| 1592 |
if (id) fireHiddenInput('#explore_place_pick', id);
|
| 1593 |
});
|
| 1594 |
});
|
| 1595 |
document.querySelectorAll('.day-tab').forEach((btn) => {
|
| 1596 |
+
wireClickable(btn, () => {
|
|
|
|
|
|
|
| 1597 |
const day = btn.getAttribute('data-day');
|
| 1598 |
if (day) fireHiddenInput('#book_day_pick', day);
|
| 1599 |
});
|
|
|
|
| 1602 |
if (card.dataset.entityBound) return;
|
| 1603 |
card.dataset.entityBound = '1';
|
| 1604 |
card.style.cursor = 'pointer';
|
| 1605 |
+
wireClickable(card, () => {
|
| 1606 |
const id = card.getAttribute('data-entity-id');
|
| 1607 |
if (id) fireHiddenInput('#entity_pick', id);
|
| 1608 |
});
|
| 1609 |
});
|
| 1610 |
+
document.querySelectorAll('.location-modal-close').forEach((btn) => {
|
| 1611 |
+
wireClickable(btn, closeLocationModal);
|
| 1612 |
+
});
|
| 1613 |
+
document.querySelectorAll('.location-modal-overlay').forEach((overlay) => {
|
| 1614 |
+
if (overlay.dataset.overlayBound) return;
|
| 1615 |
+
overlay.dataset.overlayBound = '1';
|
| 1616 |
+
overlay.addEventListener('click', (e) => {
|
| 1617 |
+
if (e.target === overlay) closeLocationModal();
|
| 1618 |
+
});
|
| 1619 |
+
});
|
| 1620 |
+
};
|
| 1621 |
+
|
| 1622 |
+
const showLocationModal = () => {
|
| 1623 |
+
const host = document.querySelector('#location-panel-host, [data-open="1"] .location-modal-overlay, .location-modal-overlay');
|
| 1624 |
+
const modal = document.querySelector('.location-modal-overlay');
|
| 1625 |
+
if (modal) modal.classList.add('is-open');
|
| 1626 |
};
|
| 1627 |
|
| 1628 |
bind();
|
| 1629 |
bindSpreadClicks();
|
| 1630 |
+
new MutationObserver(() => {
|
| 1631 |
+
bind();
|
| 1632 |
+
bindSpreadClicks();
|
| 1633 |
+
if (document.querySelector('.location-modal-overlay .location-panel-name')) showLocationModal();
|
| 1634 |
+
}).observe(document.body, { childList: true, subtree: true });
|
| 1635 |
|
| 1636 |
window._aetherHoverDiorama = false;
|
| 1637 |
window._aetherDioramaActive = false;
|
ui/book_display.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
-
from world.book_of_ages import get_entries
|
| 6 |
from world.database import get_world_state
|
| 7 |
|
| 8 |
ENTRY_TYPE_LABELS = {
|
|
@@ -95,7 +95,7 @@ def render_day_tabs(selected_day: int | None = None) -> str:
|
|
| 95 |
for day in reversed(days):
|
| 96 |
sel = " is-active" if day == selected_day else ""
|
| 97 |
buttons.append(
|
| 98 |
-
f'<
|
| 99 |
)
|
| 100 |
|
| 101 |
return f"""
|
|
@@ -108,6 +108,21 @@ def render_day_tabs(selected_day: int | None = None) -> str:
|
|
| 108 |
"""
|
| 109 |
|
| 110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
def render_book_for_day(
|
| 112 |
day: int | None = None,
|
| 113 |
entry_type: str = "all",
|
|
@@ -119,12 +134,25 @@ def render_book_for_day(
|
|
| 119 |
state = get_world_state()
|
| 120 |
day = int(state.get("current_day", 1) or 1)
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
if not entries:
|
| 130 |
return f"""
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
from world.book_of_ages import get_entries, get_entries_by_day
|
| 6 |
from world.database import get_world_state
|
| 7 |
|
| 8 |
ENTRY_TYPE_LABELS = {
|
|
|
|
| 95 |
for day in reversed(days):
|
| 96 |
sel = " is-active" if day == selected_day else ""
|
| 97 |
buttons.append(
|
| 98 |
+
f'<div class="day-tab{sel}" role="button" tabindex="0" data-day="{day}">Day {day}</div>'
|
| 99 |
)
|
| 100 |
|
| 101 |
return f"""
|
|
|
|
| 108 |
"""
|
| 109 |
|
| 110 |
|
| 111 |
+
def parse_day_query(text: str | None) -> int | None:
|
| 112 |
+
"""Parse 'Day 3', 'day3', or '3' from user input."""
|
| 113 |
+
if not text or not str(text).strip():
|
| 114 |
+
return None
|
| 115 |
+
import re
|
| 116 |
+
|
| 117 |
+
raw = str(text).strip()
|
| 118 |
+
m = re.match(r"^day\s*(\d+)$", raw, re.I)
|
| 119 |
+
if m:
|
| 120 |
+
return int(m.group(1))
|
| 121 |
+
if raw.isdigit():
|
| 122 |
+
return int(raw)
|
| 123 |
+
return None
|
| 124 |
+
|
| 125 |
+
|
| 126 |
def render_book_for_day(
|
| 127 |
day: int | None = None,
|
| 128 |
entry_type: str = "all",
|
|
|
|
| 134 |
state = get_world_state()
|
| 135 |
day = int(state.get("current_day", 1) or 1)
|
| 136 |
|
| 137 |
+
try:
|
| 138 |
+
day = int(day)
|
| 139 |
+
except (TypeError, ValueError):
|
| 140 |
+
state = get_world_state()
|
| 141 |
+
day = int(state.get("current_day", 1) or 1)
|
| 142 |
+
|
| 143 |
+
parsed = parse_day_query(search)
|
| 144 |
+
if parsed is not None:
|
| 145 |
+
day = parsed
|
| 146 |
+
search = ""
|
| 147 |
+
|
| 148 |
+
entries = get_entries_by_day(day)
|
| 149 |
+
if entry_type and entry_type != "all":
|
| 150 |
+
entries = [e for e in entries if e["entry_type"] == entry_type]
|
| 151 |
+
if search:
|
| 152 |
+
q = search.lower()
|
| 153 |
+
entries = [e for e in entries if q in (e.get("content") or "").lower()
|
| 154 |
+
or q in (e.get("title") or "").lower()]
|
| 155 |
+
entries = entries[:limit]
|
| 156 |
|
| 157 |
if not entries:
|
| 158 |
return f"""
|
ui/place_ribbon.py
CHANGED
|
@@ -20,18 +20,20 @@ def render_place_ribbon(selected_id: int | None = None) -> str:
|
|
| 20 |
else '<span class="ribbon-thumb ribbon-thumb-empty"></span>'
|
| 21 |
)
|
| 22 |
items.append(f"""
|
| 23 |
-
<
|
| 24 |
-
|
| 25 |
{thumb}
|
| 26 |
<span class="ribbon-label">{short}</span>
|
| 27 |
<span class="ribbon-count">{loc.get('entity_count', 0)} souls</span>
|
| 28 |
-
</
|
| 29 |
""")
|
| 30 |
|
| 31 |
return f"""
|
| 32 |
<div class="place-ribbon">
|
|
|
|
| 33 |
<p class="place-ribbon-kicker">Sacred Places</p>
|
| 34 |
<p class="place-ribbon-hint">Choose a realm β the world opens on the right page.</p>
|
| 35 |
<div class="place-ribbon-list">{"".join(items)}</div>
|
|
|
|
| 36 |
</div>
|
| 37 |
"""
|
|
|
|
| 20 |
else '<span class="ribbon-thumb ribbon-thumb-empty"></span>'
|
| 21 |
)
|
| 22 |
items.append(f"""
|
| 23 |
+
<div class="place-ribbon-item{sel}" role="button" tabindex="0"
|
| 24 |
+
data-location-id="{loc['id']}" title="{loc['name']}">
|
| 25 |
{thumb}
|
| 26 |
<span class="ribbon-label">{short}</span>
|
| 27 |
<span class="ribbon-count">{loc.get('entity_count', 0)} souls</span>
|
| 28 |
+
</div>
|
| 29 |
""")
|
| 30 |
|
| 31 |
return f"""
|
| 32 |
<div class="place-ribbon">
|
| 33 |
+
<div class="tome-ornament top" aria-hidden="true">β¦ βββ β§ βββ β¦</div>
|
| 34 |
<p class="place-ribbon-kicker">Sacred Places</p>
|
| 35 |
<p class="place-ribbon-hint">Choose a realm β the world opens on the right page.</p>
|
| 36 |
<div class="place-ribbon-list">{"".join(items)}</div>
|
| 37 |
+
<div class="tome-ornament bottom" aria-hidden="true">β§</div>
|
| 38 |
</div>
|
| 39 |
"""
|
ui/relationship_graph.py
CHANGED
|
@@ -242,13 +242,21 @@ def render_relationship_graph(selected_entity_id: str | None = None) -> str:
|
|
| 242 |
return render_bonds_graph_frame(src)
|
| 243 |
|
| 244 |
|
| 245 |
-
def render_bonds_sidebar() -> str:
|
| 246 |
relationships = get_all_relationships()
|
|
|
|
|
|
|
|
|
|
| 247 |
if not relationships:
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
<div class="bonds-sidebar">
|
| 250 |
<p class="tome-printed-kicker">The Web of Bonds</p>
|
| 251 |
-
<p class="bonds-sidebar-empty">
|
| 252 |
</div>
|
| 253 |
"""
|
| 254 |
|
|
@@ -257,17 +265,19 @@ def render_bonds_sidebar() -> str:
|
|
| 257 |
for k, c in REL_COLORS.items()
|
| 258 |
)
|
| 259 |
rel_list = []
|
| 260 |
-
for rel in relationships[:
|
| 261 |
t = TYPE_SHORT.get(rel["relationship_type"], rel["relationship_type"])
|
|
|
|
| 262 |
rel_list.append(
|
| 263 |
f'<li><b>{rel["entity_a_name"]}</b> β <b>{rel["entity_b_name"]}</b> '
|
| 264 |
-
f'Β· {t} β <em>{
|
| 265 |
)
|
| 266 |
|
|
|
|
| 267 |
return f"""
|
| 268 |
<div class="bonds-sidebar">
|
| 269 |
<p class="tome-printed-kicker">The Web of Bonds</p>
|
| 270 |
-
<p class="bonds-sidebar-desc">{len(relationships)}
|
| 271 |
<div class="rel-legend">{legend}</div>
|
| 272 |
<ul class="rel-list">{"".join(rel_list)}</ul>
|
| 273 |
</div>
|
|
|
|
| 242 |
return render_bonds_graph_frame(src)
|
| 243 |
|
| 244 |
|
| 245 |
+
def render_bonds_sidebar(rel_type: str = "all") -> str:
|
| 246 |
relationships = get_all_relationships()
|
| 247 |
+
if rel_type and rel_type != "all":
|
| 248 |
+
relationships = [r for r in relationships if r["relationship_type"] == rel_type]
|
| 249 |
+
|
| 250 |
if not relationships:
|
| 251 |
+
empty = (
|
| 252 |
+
"No bonds of this kind yet β try another thread type."
|
| 253 |
+
if rel_type and rel_type != "all"
|
| 254 |
+
else "No alliances forged yet β wait for the next realm tick."
|
| 255 |
+
)
|
| 256 |
+
return f"""
|
| 257 |
<div class="bonds-sidebar">
|
| 258 |
<p class="tome-printed-kicker">The Web of Bonds</p>
|
| 259 |
+
<p class="bonds-sidebar-empty">{empty}</p>
|
| 260 |
</div>
|
| 261 |
"""
|
| 262 |
|
|
|
|
| 265 |
for k, c in REL_COLORS.items()
|
| 266 |
)
|
| 267 |
rel_list = []
|
| 268 |
+
for rel in relationships[:10]:
|
| 269 |
t = TYPE_SHORT.get(rel["relationship_type"], rel["relationship_type"])
|
| 270 |
+
desc = (rel.get("description") or "A bond still formingβ¦")[:72]
|
| 271 |
rel_list.append(
|
| 272 |
f'<li><b>{rel["entity_a_name"]}</b> β <b>{rel["entity_b_name"]}</b> '
|
| 273 |
+
f'Β· {t} β <em>{desc}</em></li>'
|
| 274 |
)
|
| 275 |
|
| 276 |
+
total = len(get_all_relationships())
|
| 277 |
return f"""
|
| 278 |
<div class="bonds-sidebar">
|
| 279 |
<p class="tome-printed-kicker">The Web of Bonds</p>
|
| 280 |
+
<p class="bonds-sidebar-desc">{len(relationships)} shown Β· {total} threads total</p>
|
| 281 |
<div class="rel-legend">{legend}</div>
|
| 282 |
<ul class="rel-list">{"".join(rel_list)}</ul>
|
| 283 |
</div>
|
ui/styles.css
CHANGED
|
@@ -4347,10 +4347,278 @@ body,
|
|
| 4347 |
.tome-printed-page .current-event,
|
| 4348 |
.tome-printed-page .realm-pulse,
|
| 4349 |
.tome-printed-page .activity-feed,
|
| 4350 |
-
.tome-printed-page .location-panel,
|
| 4351 |
.tome-printed-page .book-container,
|
| 4352 |
.tome-printed-page .entity-hologram-empty .hologram-stage {
|
| 4353 |
background: rgba(255, 249, 232, 0.62) !important;
|
| 4354 |
box-shadow: none !important;
|
| 4355 |
border: 1px solid rgba(138, 106, 58, 0.22) !important;
|
| 4356 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4347 |
.tome-printed-page .current-event,
|
| 4348 |
.tome-printed-page .realm-pulse,
|
| 4349 |
.tome-printed-page .activity-feed,
|
|
|
|
| 4350 |
.tome-printed-page .book-container,
|
| 4351 |
.tome-printed-page .entity-hologram-empty .hologram-stage {
|
| 4352 |
background: rgba(255, 249, 232, 0.62) !important;
|
| 4353 |
box-shadow: none !important;
|
| 4354 |
border: 1px solid rgba(138, 106, 58, 0.22) !important;
|
| 4355 |
}
|
| 4356 |
+
|
| 4357 |
+
/* ββ Centered spread pages with ornaments ββ */
|
| 4358 |
+
.tome-page-centered {
|
| 4359 |
+
justify-content: center !important;
|
| 4360 |
+
gap: 0.45rem !important;
|
| 4361 |
+
}
|
| 4362 |
+
|
| 4363 |
+
.tome-ornament {
|
| 4364 |
+
text-align: center;
|
| 4365 |
+
font-family: 'Cinzel', serif;
|
| 4366 |
+
font-size: 0.72rem;
|
| 4367 |
+
letter-spacing: 0.22em;
|
| 4368 |
+
color: rgba(122, 90, 40, 0.55);
|
| 4369 |
+
margin: 0.15rem 0;
|
| 4370 |
+
flex-shrink: 0;
|
| 4371 |
+
}
|
| 4372 |
+
|
| 4373 |
+
.tome-ornament.top { margin-bottom: 0.35rem; }
|
| 4374 |
+
.tome-ornament.bottom { margin-top: auto; padding-top: 0.35rem; }
|
| 4375 |
+
|
| 4376 |
+
/* ββ Location popup modal ββ */
|
| 4377 |
+
.location-panel-host {
|
| 4378 |
+
position: relative;
|
| 4379 |
+
min-height: 0;
|
| 4380 |
+
}
|
| 4381 |
+
|
| 4382 |
+
.location-panel-host .map-hint {
|
| 4383 |
+
text-align: center;
|
| 4384 |
+
font-family: 'IM Fell English', serif;
|
| 4385 |
+
font-style: italic;
|
| 4386 |
+
color: #5c4228;
|
| 4387 |
+
padding: 0.5rem;
|
| 4388 |
+
}
|
| 4389 |
+
|
| 4390 |
+
.location-modal-overlay {
|
| 4391 |
+
display: none;
|
| 4392 |
+
position: fixed;
|
| 4393 |
+
inset: 0;
|
| 4394 |
+
z-index: 8800;
|
| 4395 |
+
background: rgba(12, 8, 5, 0.72);
|
| 4396 |
+
backdrop-filter: blur(4px);
|
| 4397 |
+
align-items: center;
|
| 4398 |
+
justify-content: center;
|
| 4399 |
+
padding: 1rem;
|
| 4400 |
+
}
|
| 4401 |
+
|
| 4402 |
+
.location-modal-overlay.is-open {
|
| 4403 |
+
display: flex;
|
| 4404 |
+
animation: modalFadeIn 0.35s ease;
|
| 4405 |
+
}
|
| 4406 |
+
|
| 4407 |
+
@keyframes modalFadeIn {
|
| 4408 |
+
from { opacity: 0; }
|
| 4409 |
+
to { opacity: 1; }
|
| 4410 |
+
}
|
| 4411 |
+
|
| 4412 |
+
.location-modal-card {
|
| 4413 |
+
position: relative;
|
| 4414 |
+
width: min(420px, 92vw);
|
| 4415 |
+
max-height: min(78vh, 560px);
|
| 4416 |
+
overflow: auto;
|
| 4417 |
+
border-radius: 14px;
|
| 4418 |
+
transform: perspective(900px) rotateX(6deg) scale(0.98);
|
| 4419 |
+
animation: modalPop 0.42s cubic-bezier(0.34, 1.2, 0.64, 1);
|
| 4420 |
+
box-shadow:
|
| 4421 |
+
0 28px 64px rgba(0, 0, 0, 0.55),
|
| 4422 |
+
0 0 40px rgba(201, 161, 59, 0.15);
|
| 4423 |
+
}
|
| 4424 |
+
|
| 4425 |
+
@keyframes modalPop {
|
| 4426 |
+
from { opacity: 0; transform: perspective(900px) rotateX(14deg) translateY(24px) scale(0.92); }
|
| 4427 |
+
to { opacity: 1; transform: perspective(900px) rotateX(6deg) scale(0.98); }
|
| 4428 |
+
}
|
| 4429 |
+
|
| 4430 |
+
.location-modal-close {
|
| 4431 |
+
position: absolute;
|
| 4432 |
+
top: 0.55rem;
|
| 4433 |
+
right: 0.65rem;
|
| 4434 |
+
z-index: 3;
|
| 4435 |
+
width: 28px;
|
| 4436 |
+
height: 28px;
|
| 4437 |
+
border-radius: 50%;
|
| 4438 |
+
background: rgba(20, 12, 8, 0.65);
|
| 4439 |
+
color: #f4e8c8;
|
| 4440 |
+
display: grid;
|
| 4441 |
+
place-items: center;
|
| 4442 |
+
cursor: pointer;
|
| 4443 |
+
font-size: 0.9rem;
|
| 4444 |
+
border: 1px solid rgba(201, 161, 59, 0.35);
|
| 4445 |
+
}
|
| 4446 |
+
|
| 4447 |
+
.location-modal-card .location-panel {
|
| 4448 |
+
margin: 0 !important;
|
| 4449 |
+
border: none !important;
|
| 4450 |
+
box-shadow: none !important;
|
| 4451 |
+
}
|
| 4452 |
+
|
| 4453 |
+
.location-soul-count {
|
| 4454 |
+
color: #5c4228;
|
| 4455 |
+
font-size: 0.8rem;
|
| 4456 |
+
margin-bottom: 0.75rem;
|
| 4457 |
+
font-family: 'IM Fell English', serif;
|
| 4458 |
+
}
|
| 4459 |
+
|
| 4460 |
+
/* ββ Explore spread β force 3D diorama visible ββ */
|
| 4461 |
+
.tome-explore-index,
|
| 4462 |
+
.tome-explore-stage {
|
| 4463 |
+
display: flex !important;
|
| 4464 |
+
flex-direction: column !important;
|
| 4465 |
+
min-height: 0 !important;
|
| 4466 |
+
flex: 1 1 auto !important;
|
| 4467 |
+
}
|
| 4468 |
+
|
| 4469 |
+
.tome-explore-index .place-ribbon {
|
| 4470 |
+
flex: 1 1 auto;
|
| 4471 |
+
min-height: 180px;
|
| 4472 |
+
}
|
| 4473 |
+
|
| 4474 |
+
.tome-explore-index .place-ribbon-list {
|
| 4475 |
+
flex: 1 1 auto;
|
| 4476 |
+
min-height: 140px;
|
| 4477 |
+
max-height: 380px;
|
| 4478 |
+
}
|
| 4479 |
+
|
| 4480 |
+
.tome-explore-stage > .block,
|
| 4481 |
+
.tome-explore-stage > .form,
|
| 4482 |
+
.tome-explore-stage #explore-diorama-host,
|
| 4483 |
+
.tome-explore-stage #explore-diorama-host > div {
|
| 4484 |
+
flex: 1 1 auto !important;
|
| 4485 |
+
min-height: 260px !important;
|
| 4486 |
+
display: flex !important;
|
| 4487 |
+
flex-direction: column !important;
|
| 4488 |
+
}
|
| 4489 |
+
|
| 4490 |
+
.tome-explore-stage .diorama-wrap {
|
| 4491 |
+
flex: 1 1 auto !important;
|
| 4492 |
+
min-height: 260px !important;
|
| 4493 |
+
max-height: 100% !important;
|
| 4494 |
+
margin: 0 !important;
|
| 4495 |
+
display: flex !important;
|
| 4496 |
+
flex-direction: column !important;
|
| 4497 |
+
}
|
| 4498 |
+
|
| 4499 |
+
.tome-explore-stage .diorama-header {
|
| 4500 |
+
display: none !important;
|
| 4501 |
+
}
|
| 4502 |
+
|
| 4503 |
+
.tome-explore-stage .diorama-stage {
|
| 4504 |
+
flex: 1 1 auto !important;
|
| 4505 |
+
min-height: 240px !important;
|
| 4506 |
+
}
|
| 4507 |
+
|
| 4508 |
+
.tome-explore-stage .diorama-frame {
|
| 4509 |
+
min-height: 240px !important;
|
| 4510 |
+
height: 100% !important;
|
| 4511 |
+
max-height: 400px !important;
|
| 4512 |
+
}
|
| 4513 |
+
|
| 4514 |
+
.tome-explore-stage .diorama-empty {
|
| 4515 |
+
flex: 1 1 auto;
|
| 4516 |
+
min-height: 240px;
|
| 4517 |
+
display: grid;
|
| 4518 |
+
place-items: center;
|
| 4519 |
+
padding: 1rem;
|
| 4520 |
+
text-align: center;
|
| 4521 |
+
border: 1px dashed rgba(138, 106, 58, 0.35);
|
| 4522 |
+
border-radius: 12px;
|
| 4523 |
+
font-family: 'IM Fell English', serif;
|
| 4524 |
+
}
|
| 4525 |
+
|
| 4526 |
+
/* ββ Souls spread fix ββ */
|
| 4527 |
+
.tome-souls-index,
|
| 4528 |
+
.tome-souls-stage {
|
| 4529 |
+
display: flex !important;
|
| 4530 |
+
flex-direction: column !important;
|
| 4531 |
+
min-height: 0 !important;
|
| 4532 |
+
}
|
| 4533 |
+
|
| 4534 |
+
.tome-souls-index .entity-grid {
|
| 4535 |
+
flex: 1 1 auto;
|
| 4536 |
+
max-height: none;
|
| 4537 |
+
min-height: 200px;
|
| 4538 |
+
}
|
| 4539 |
+
|
| 4540 |
+
.tome-souls-stage .entity-hologram {
|
| 4541 |
+
flex: 1 1 auto;
|
| 4542 |
+
min-height: 280px;
|
| 4543 |
+
}
|
| 4544 |
+
|
| 4545 |
+
#tome-spreads [role="tabpanel"]:has(#entities) .featured-section {
|
| 4546 |
+
display: none !important;
|
| 4547 |
+
}
|
| 4548 |
+
|
| 4549 |
+
/* ββ Book 3D cover flip ββ */
|
| 4550 |
+
.realm-book-3d {
|
| 4551 |
+
position: relative;
|
| 4552 |
+
width: min(440px, 90vw);
|
| 4553 |
+
min-height: min(520px, 72vh);
|
| 4554 |
+
transform-style: preserve-3d;
|
| 4555 |
+
transition: transform 1.15s cubic-bezier(0.45, 0, 0.2, 1);
|
| 4556 |
+
}
|
| 4557 |
+
|
| 4558 |
+
.realm-book-3d.show-back {
|
| 4559 |
+
transform: rotateY(180deg);
|
| 4560 |
+
}
|
| 4561 |
+
|
| 4562 |
+
.realm-book-3d.is-opening {
|
| 4563 |
+
opacity: 0;
|
| 4564 |
+
transform: rotateY(-12deg) translateZ(-30px);
|
| 4565 |
+
pointer-events: none;
|
| 4566 |
+
}
|
| 4567 |
+
|
| 4568 |
+
.realm-book-face {
|
| 4569 |
+
position: absolute;
|
| 4570 |
+
inset: 0;
|
| 4571 |
+
backface-visibility: hidden;
|
| 4572 |
+
-webkit-backface-visibility: hidden;
|
| 4573 |
+
}
|
| 4574 |
+
|
| 4575 |
+
.realm-book-back {
|
| 4576 |
+
transform: rotateY(180deg);
|
| 4577 |
+
}
|
| 4578 |
+
|
| 4579 |
+
.realm-cover-back {
|
| 4580 |
+
display: flex;
|
| 4581 |
+
flex-direction: column;
|
| 4582 |
+
align-items: center;
|
| 4583 |
+
justify-content: center;
|
| 4584 |
+
min-height: min(480px, 70vh);
|
| 4585 |
+
background:
|
| 4586 |
+
radial-gradient(circle at 30% 20%, rgba(255, 240, 198, 0.08), transparent 32%),
|
| 4587 |
+
radial-gradient(circle at 70% 80%, rgba(0, 0, 0, 0.28), transparent 40%),
|
| 4588 |
+
linear-gradient(155deg, #3d2616 0%, #24150c 42%, #120a06 100%) !important;
|
| 4589 |
+
}
|
| 4590 |
+
|
| 4591 |
+
.realm-back-ornament {
|
| 4592 |
+
font-size: 2.4rem;
|
| 4593 |
+
color: rgba(201, 161, 59, 0.45);
|
| 4594 |
+
margin-bottom: 0.75rem;
|
| 4595 |
+
}
|
| 4596 |
+
|
| 4597 |
+
.realm-back-text {
|
| 4598 |
+
font-family: 'IM Fell English', serif;
|
| 4599 |
+
font-style: italic;
|
| 4600 |
+
color: #d8c49a;
|
| 4601 |
+
line-height: 1.55;
|
| 4602 |
+
margin: 0 0 1.25rem;
|
| 4603 |
+
max-width: 22rem;
|
| 4604 |
+
}
|
| 4605 |
+
|
| 4606 |
+
.realm-opening.is-reclosed .realm-book-stage {
|
| 4607 |
+
display: flex !important;
|
| 4608 |
+
align-items: center;
|
| 4609 |
+
justify-content: center;
|
| 4610 |
+
}
|
| 4611 |
+
|
| 4612 |
+
.realm-opening.is-reclosed .realm-book-3d {
|
| 4613 |
+
opacity: 1 !important;
|
| 4614 |
+
pointer-events: auto !important;
|
| 4615 |
+
}
|
| 4616 |
+
|
| 4617 |
+
.realm-opening.is-closing .realm-book-stage {
|
| 4618 |
+
perspective: 1400px;
|
| 4619 |
+
}
|
| 4620 |
+
|
| 4621 |
+
.realm-opening.is-back-visible .realm-book-open,
|
| 4622 |
+
.realm-opening.is-back-visible .realm-prologue-card {
|
| 4623 |
+
display: none !important;
|
| 4624 |
+
}
|