| """Dark theme based on Bootswatch Cyborg ('Jet black and electric blue').""" |
|
|
| DARK_THEME_CSS = """ |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600;700&display=swap'); |
| |
| /* ── Cyborg palette ── */ |
| :root { |
| --bg-primary: #060606; |
| --bg-card: #111111; |
| --bg-card-open: #1a1a1a; |
| --accent: #2a9fd6; |
| --accent-hover: #1a7099; |
| --text-primary: #dee2e6; |
| --text-muted: #adafae; |
| --heading: #ffffff; |
| --border: #222222; |
| } |
| |
| /* ── Global ── */ |
| html, body, [class*="css"] { |
| font-family: 'Inter', 'IBMPlexMono', sans-serif; |
| font-size: 20px !important; |
| } |
| |
| /* ── Main background ── */ |
| .stApp { |
| background-color: var(--bg-primary) !important; |
| color: #ffffff !important; |
| } |
| |
| /* ── Hide the top header bar ── */ |
| header[data-testid="stHeader"] { |
| display: none !important; |
| } |
| [data-testid="stToolbar"] { |
| display: none !important; |
| } |
| [data-testid="stAppDeployButton"] { |
| display: none !important; |
| } |
| /* Hide heading anchor links that appear on hover */ |
| h1 a, h2 a, h3 a, h1 a:link, h2 a:link, h3 a:link, |
| h1 a:visited, h2 a:visited, h3 a:visited { |
| display: none !important; |
| } |
| .headerlink { |
| display: none !important; |
| } |
| |
| /* ── Headings ── */ |
| h1, h2, h3 { |
| color: var(--accent) !important; |
| font-weight: 700 !important; |
| } |
| h1 { font-size: 2.4rem !important; } |
| h2 { font-size: 1.8rem !important; } |
| h3 { font-size: 1.4rem !important; } |
| |
| /* ── Expander cards ── */ |
| .stExpander { |
| background-color: var(--bg-card) !important; |
| border: 1px solid var(--border) !important; |
| border-radius: 10px !important; |
| margin-bottom: 8px !important; |
| } |
| .stExpander details:not([open]) { |
| min-height: 82px !important; |
| } |
| .stExpander:hover { |
| border-color: var(--accent) !important; |
| } |
| .stExpander:hover details[open] { |
| border-color: var(--accent) !important; |
| } |
| .stExpander details[open] { |
| background-color: var(--bg-card-open) !important; |
| border-color: var(--border) !important; |
| } |
| .stExpander summary { |
| font-size: 20px !important; |
| color: var(--accent) !important; |
| font-weight: 700 !important; |
| line-height: 1.5 !important; |
| background-color: var(--bg-card) !important; |
| } |
| .stExpander summary strong { |
| color: var(--accent) !important; |
| font-weight: 700 !important; |
| font-size: 22px !important; |
| display: block !important; |
| white-space: nowrap !important; |
| overflow: hidden !important; |
| text-overflow: ellipsis !important; |
| } |
| .stExpander summary p { |
| font-size: 16px !important; |
| color: var(--text-muted) !important; |
| } |
| .stExpander summary:hover { |
| color: var(--accent-hover) !important; |
| background-color: var(--bg-card) !important; |
| } |
| /* ── Expanded card content ── */ |
| .stExpander div[data-testid="stExpanderDetails"] { |
| border-top-color: var(--border) !important; |
| } |
| .stExpander div[data-testid="stExpanderDetails"] p, |
| .stExpander div[data-testid="stExpanderDetails"] span { |
| font-size: 16px !important; |
| line-height: 1.5 !important; |
| } |
| .stExpander div[data-testid="stExpanderDetails"] em { |
| font-size: 15px !important; |
| } |
| /* ── Uniform card heights via fixed desc area ── */ |
| .card-desc { |
| font-size: 16px !important; |
| line-height: 1.5 !important; |
| min-height: 100px !important; |
| display: block !important; |
| margin-bottom: 4px !important; |
| } |
| |
| /* ── Input widgets ── */ |
| .stTextInput > div > div > input, |
| .stDateInput input, |
| .stNumberInput input, |
| .stSelectbox div > div > input { |
| font-size: 18px !important; |
| background-color: var(--bg-primary) !important; |
| color: #ffffff !important; |
| border-color: var(--border) !important; |
| } |
| /* All widget labels — force white text */ |
| .stSelectbox label, |
| .stTextInput label, |
| .stNumberInput label, |
| .stDateInput label, |
| .stRadio label, |
| label[data-baseweb="label"] { |
| color: #ffffff !important; |
| } |
| /* Selectbox trigger and dropdown — dark theme */ |
| .stSelectbox div[data-baseweb="select"] > div { |
| background-color: var(--bg-primary) !important; |
| color: #ffffff !important; |
| border-color: var(--border) !important; |
| } |
| .stSelectbox div[data-baseweb="select"] > div:hover { |
| border-color: var(--accent) !important; |
| } |
| [data-baseweb="popover"] { |
| background-color: var(--bg-card) !important; |
| border: 1px solid var(--border) !important; |
| border-radius: 8px !important; |
| } |
| [data-baseweb="popover"] li[role="option"] { |
| background-color: var(--bg-card) !important; |
| color: #ffffff !important; |
| font-size: 16px !important; |
| } |
| [data-baseweb="popover"] li[role="option"]:hover { |
| background-color: var(--bg-card-open) !important; |
| color: #ffffff !important; |
| } |
| [data-baseweb="popover"] li[role="option"][aria-selected="true"] { |
| background-color: var(--accent) !important; |
| color: #ffffff !important; |
| } |
| |
| /* ── Button ── */ |
| .stButton > button, |
| button[type="submit"], |
| button[kind="secondaryFormSubmit"], |
| button[kind="formSubmit"] { |
| font-size: 13px !important; |
| padding: 10px 16px !important; |
| background-color: var(--accent) !important; |
| color: #ffffff !important; |
| border: none !important; |
| border-radius: 8px !important; |
| white-space: nowrap !important; |
| } |
| .stButton > button:hover, |
| button[type="submit"]:hover, |
| button[kind="secondaryFormSubmit"]:hover, |
| button[kind="formSubmit"]:hover { |
| background-color: var(--accent-hover) !important; |
| } |
| |
| /* ── Spinner ── */ |
| .stSpinner > div { |
| font-size: 18px !important; |
| color: var(--accent) !important; |
| } |
| |
| /* ── Map: fill full width ── */ |
| .stFolium { |
| width: 100% !important; |
| } |
| .stFolium > div { |
| width: 100% !important; |
| } |
| .stFolium iframe { |
| width: 100% !important; |
| } |
| /* Push map container down to align with cards */ |
| .stCustomComponentV1 { |
| display: block !important; |
| } |
| iframe[title="streamlit_folium.st_folium"] { |
| display: block !important; |
| } |
| /* Hide Leaflet attribution label */ |
| .leaflet-control-attribution { |
| display: none !important; |
| } |
| /* Hide Leaflet zoom controls (+ and -) */ |
| .leaflet-control-zoom { |
| display: none !important; |
| } |
| |
| /* ── Reduce top padding ── */ |
| .block-container { |
| padding-top: 0 !important; |
| } |
| /* Squeeze title closer to top */ |
| .main > div:first-child { |
| margin-top: -8px !important; |
| } |
| h1 { |
| margin-top: 0 !important; |
| padding-top: 0 !important; |
| } |
| |
| /* ── Hide JS-tool iframes ── */ |
| iframe[title="st.iframe"] { |
| display: none !important; |
| } |
| |
| /* ── Hide scrollbars on all panels (keep scroll functionality) ── */ |
| ::-webkit-scrollbar { |
| display: none !important; |
| width: 0 !important; |
| height: 0 !important; |
| } |
| * { |
| scrollbar-width: none !important; |
| -ms-overflow-style: none !important; |
| } |
| |
| /* ── Hide sidebar completely ── */ |
| section[data-testid="stSidebar"] { |
| display: none !important; |
| } |
| section[data-testid="stSidebar"] + div { |
| margin-left: 0 !important; |
| } |
| |
| |
| |
| /* ── Flexible panel heights: dynamically set by JS ── */ |
| /* Fallback height (JS overrides with !important) */ |
| .stVerticalBlock[data-testid="stVerticalBlock"] > [data-testid="stLayoutWrapper"] > .stVerticalBlock { |
| max-height: 800px; |
| } |
| /* ── Map container: cap height to prevent unbounded growth ── */ |
| .stCustomComponentV1 { |
| height: 800px !important; |
| max-height: 800px !important; |
| overflow: hidden !important; |
| } |
| .stCustomComponentV1 iframe { |
| max-height: 800px !important; |
| } |
| |
| /* ── Category filter: horizontal radio pills ── */ |
| .stRadio label[data-baseweb="label"] { |
| font-size: 12px !important; |
| color: #ffffff !important; |
| margin-bottom: 0 !important; |
| } |
| .stRadio > div[role="radiogroup"] { |
| flex-direction: row !important; |
| gap: 8px 3px !important; |
| flex-wrap: wrap !important; |
| } |
| .stRadio > div[role="radiogroup"] > label { |
| background-color: var(--bg-card) !important; |
| border: 1px solid var(--border) !important; |
| border-radius: 20px !important; |
| padding: 4px 12px !important; |
| min-height: 42px !important; |
| line-height: 20px !important; |
| color: #ffffff !important; |
| font-size: 16px !important; |
| font-weight: 500 !important; |
| cursor: pointer !important; |
| transition: all 0.15s ease !important; |
| display: inline-flex !important; |
| align-items: center !important; |
| justify-content: center !important; |
| white-space: nowrap !important; |
| } |
| /* Pill inner text — emoji + name */ |
| .stRadio > div[role="radiogroup"] > label > div:last-child { |
| font-size: 13px !important; |
| line-height: 1 !important; |
| } |
| /* Force white text on all inner elements of radio pills */ |
| .stRadio > div[role="radiogroup"] > label * { |
| color: #ffffff !important; |
| } |
| .stRadio > div[role="radiogroup"] > label:hover { |
| border-color: var(--accent) !important; |
| color: #ffffff !important; |
| } |
| .stRadio > div[role="radiogroup"] > label[data-baseweb="radio"] { |
| justify-content: center !important; |
| } |
| /* Selected / checked pill */ |
| .stRadio > div[role="radiogroup"] > label:has(input:checked), |
| .stRadio > div[role="radiogroup"] > label[aria-checked="true"] { |
| background-color: var(--accent) !important; |
| border-color: var(--accent) !important; |
| color: #ffffff !important; |
| font-weight: 700 !important; |
| } |
| /* Hide the native radio circle */ |
| .stRadio > div[role="radiogroup"] > label > div:first-child { |
| display: none !important; |
| } |
| .stRadio > div[role="radiogroup"] > label > div:last-child { |
| margin-left: 0 !important; |
| padding-left: 0 !important; |
| } |
| |
| /* ── Responsive search controls — tight gaps, proportional columns ── */ |
| [data-testid="stHorizontalBlock"] { |
| flex-wrap: wrap !important; |
| gap: 0.2rem 15px !important; |
| } |
| /* Stack layout: all columns full width, JS handles search controls 4-row layout */ |
| .stack-columns [data-testid="stHorizontalBlock"] > [data-testid="stColumn"] { |
| flex: 0 1 100% !important; |
| } |
| /* Mobile: hide the map entirely */ |
| .stack-columns [data-testid="stHorizontalBlock"] .stCustomComponentV1 { |
| display: none !important; |
| } |
| /* Mobile: hide the '🗺️ Map' title too */ |
| .stack-columns [data-testid="stHorizontalBlock"] [data-testid="stColumn"]:has(.stCustomComponentV1) h3 { |
| display: none !important; |
| } |
| /* Align search/refresh buttons with selectbox dropdown (not label) */ |
| [data-testid="stHorizontalBlock"] > [data-testid="stColumn"]:nth-child(5) > div:first-child, |
| [data-testid="stHorizontalBlock"] > [data-testid="stColumn"]:nth-child(6) > div:first-child { |
| padding-top: 30px !important; |
| } |
| /* Make all form inputs fill their column width */ |
| .stTextInput, |
| .stSelectbox, |
| .stRadio, |
| .stButton { |
| width: 100% !important; |
| } |
| .stTextInput > div, |
| .stSelectbox > div, |
| .stSelectbox > div > div { |
| width: 100% !important; |
| } |
| /* Compact selectbox — shrink height */ |
| .stSelectbox { |
| padding-top: 0 !important; |
| margin-top: 0 !important; |
| } |
| .stSelectbox > div > div:first-child { |
| padding: 0 8px !important; |
| min-height: 38px !important; |
| } |
| .stSelectbox div[data-baseweb="select"] { |
| height: 38px !important; |
| } |
| .stSelectbox div[data-baseweb="select"] > div { |
| min-height: 38px !important; |
| padding: 0 8px !important; |
| font-size: 18px !important; |
| } |
| /* Dropdown popover menu — dark bg + light text */ |
| [data-baseweb="popover"] { |
| background-color: var(--bg-card) !important; |
| border: 1px solid var(--border) !important; |
| border-radius: 8px !important; |
| } |
| [data-baseweb="popover"] li[role="option"] { |
| background-color: var(--bg-card) !important; |
| color: #ffffff !important; |
| font-size: 16px !important; |
| } |
| [data-baseweb="popover"] li[role="option"]:hover { |
| background-color: var(--bg-card-open) !important; |
| color: #ffffff !important; |
| } |
| [data-baseweb="popover"] li[role="option"][aria-selected="true"] { |
| background-color: var(--accent) !important; |
| color: #ffffff !important; |
| } |
| .stSelectbox label { |
| font-size: 13px !important; |
| margin-bottom: 4px !important; |
| color: #ffffff !important; |
| } |
| /* Compact text input to match */ |
| .stTextInput > div > div > input { |
| min-height: 38px !important; |
| padding: 0 8px !important; |
| } |
| .stTextInput label { |
| font-size: 13px !important; |
| margin-bottom: 4px !important; |
| color: #ffffff !important; |
| } |
| |
| /* ── Force dark on all wrappers (iframes, parent tries) ── */ |
| html, body, .stApp { |
| background-color: #060606 !important; |
| } |
| /* Aggressive full-viewport dark fill */ |
| body::before { |
| content: ''; |
| position: fixed; |
| top: 0; left: 0; width: 100vw; height: 100vh; |
| background: #060606; |
| z-index: -1; |
| pointer-events: none; |
| } |
| /* Darken any iframe wrapper or embed container */ |
| iframe[title="streamlit_folium"], |
| iframe[src*="streamlit"], |
| [data-testid="stAppViewContainer"], |
| .main, |
| .block-container { |
| background-color: #060606 !important; |
| } |
| |
| /* ── Global responsive breakpoints ── */ |
| |
| /* Tablets / small laptops (≤1024px) */ |
| @media (max-width: 1024px) { |
| /* Shrink form column gaps */ |
| div[data-testid="column"] { |
| padding-left: 4px !important; |
| padding-right: 4px !important; |
| } |
| /* Reduce selectbox font sizes */ |
| .stSelectbox label, .stSelectbox div[data-baseweb="select"] { |
| font-size: 13px !important; |
| } |
| /* Shrink form button text */ |
| .stForm button p { |
| font-size: 13px !important; |
| } |
| } |
| |
| /* Narrow tablets / large phones (≤768px) */ |
| @media (max-width: 768px) { |
| /* Hero section compact */ |
| .hero-section { |
| min-height: 400px !important; |
| padding: 40px 16px 20px !important; |
| } |
| .hero-icon { |
| font-size: 72px !important; |
| } |
| .hero-title { |
| font-size: 28px !important; |
| } |
| .hero-subtitle { |
| font-size: 16px !important; |
| } |
| .hero-card { |
| width: 100% !important; |
| max-width: 280px !important; |
| padding: 16px 20px !important; |
| } |
| |
| /* Results container */ |
| div[data-testid="stVerticalBlock"] > div.stScrollableContainer { |
| height: auto !important; |
| max-height: 500px !important; |
| } |
| |
| /* Map container */ |
| iframe[title="streamlit_folium"] { |
| height: 400px !important; |
| } |
| |
| /* Card description font size */ |
| .card-desc { |
| font-size: 13px !important; |
| } |
| } |
| |
| /* Phones (≤480px) */ |
| @media (max-width: 480px) { |
| /* Card expander font */ |
| summary { |
| font-size: 13px !important; |
| } |
| } |
| </style> |
| """ |
|
|
| CARD_EQUALIZER_JS = """ |
| <script> |
| (function() { |
| // st.components renders in an iframe — reach out to the parent document |
| const doc = window.parent.document; |
| |
| function equalizeCardDescriptions() { |
| const expanders = doc.querySelectorAll('.stExpander details[open]'); |
| if (!expanders.length) { |
| // Cards not yet rendered — retry |
| setTimeout(equalizeCardDescriptions, 300); |
| return; |
| } |
| |
| const rows = {}; |
| expanders.forEach(details => { |
| const rect = details.getBoundingClientRect(); |
| const rowKey = Math.round(rect.top / 20) * 20; |
| if (!rows[rowKey]) rows[rowKey] = []; |
| rows[rowKey].push(details); |
| }); |
| |
| Object.values(rows).forEach(rowItems => { |
| // Reset all description heights in the row |
| rowItems.forEach(details => { |
| const pTags = details.querySelectorAll('.stMarkdown p'); |
| for (const p of pTags) { |
| if (!p.textContent.startsWith('💡') && !p.closest('.stMarkdown').querySelector('img')) { |
| p.closest('.stMarkdown').style.minHeight = ''; |
| } |
| } |
| }); |
| |
| // Measure tallest description |
| let maxH = 0; |
| const descs = []; |
| rowItems.forEach(details => { |
| const pTags = details.querySelectorAll('.stMarkdown p'); |
| for (const p of pTags) { |
| const parent = p.closest('.stMarkdown'); |
| if (parent && !p.textContent.startsWith('💡') && !parent.querySelector('img')) { |
| const h = parent.getBoundingClientRect().height; |
| if (h > maxH) maxH = h; |
| descs.push(parent); |
| break; |
| } |
| } |
| }); |
| |
| // Set all to tallest |
| descs.forEach(desc => { desc.style.minHeight = maxH + 'px'; }); |
| }); |
| } |
| |
| // Start with a delay to let Streamlit render cards |
| setTimeout(equalizeCardDescriptions, 500); |
| |
| // Watch for DOM changes in the parent |
| new MutationObserver(() => { |
| clearTimeout(window._cardEqTimer); |
| window._cardEqTimer = setTimeout(equalizeCardDescriptions, 200); |
| }).observe(doc.body, { childList: true, subtree: true }); |
| })(); |
| </script> |
| """ |
|
|
| EMOJI_MAP = { |
| "attractions": "✨", |
| } |
|
|
| FLEX_PANELS_JS = """<!DOCTYPE html> |
| <html> |
| <body> |
| <script> |
| (function() { |
| // We run inside a Streamlit component iframe — target the parent document |
| const doc = window.parent.document; |
| |
| let _resizing = false; |
| |
| function resizePanels() { |
| if (_resizing) return; |
| _resizing = true; |
| const vh = window.parent.innerHeight; |
| if (!vh) { _resizing = false; return; } |
| |
| // Strategy: find the scrollable card container and map iframe, |
| // then set their height so they fill the remaining viewport. |
| // Use getBoundingClientRect for accurate positioning. |
| |
| const cardContainer = Array.from( |
| doc.querySelectorAll('[data-testid="stVerticalBlock"]') |
| ).find(el => doc.defaultView.getComputedStyle(el).overflowY === 'auto'); |
| |
| if (cardContainer) { |
| // In narrow/stacked mode, let cards flow naturally without height cap |
| if (doc.body.classList.contains('stack-columns')) { |
| cardContainer.style.setProperty('height', 'auto', 'important'); |
| cardContainer.style.setProperty('max-height', 'none', 'important'); |
| if (cardContainer.parentElement?.getAttribute('data-testid') === 'stLayoutWrapper') { |
| cardContainer.parentElement.style.setProperty('height', 'auto', 'important'); |
| } |
| } else { |
| const rect = cardContainer.getBoundingClientRect(); |
| const panelHeight = Math.max(300, Math.min(vh - rect.top - 24, vh * 0.85)); |
| |
| cardContainer.style.setProperty('height', panelHeight + 'px', 'important'); |
| cardContainer.style.setProperty('max-height', panelHeight + 'px', 'important'); |
| |
| // Also resize parent LayoutWrapper |
| if (cardContainer.parentElement?.getAttribute('data-testid') === 'stLayoutWrapper') { |
| cardContainer.parentElement.style.setProperty('height', panelHeight + 'px', 'important'); |
| } |
| } |
| } |
| |
| // Find the folium iframe container and set its height similarly |
| const foliumContainer = doc.querySelector('.stCustomComponentV1'); |
| if (foliumContainer) { |
| foliumContainer.style.setProperty('max-height', '800px', 'important'); |
| const rect = foliumContainer.getBoundingClientRect(); |
| const mapHeight = Math.max(300, Math.min(vh - rect.top - 24, vh * 0.85)); |
| foliumContainer.style.setProperty('height', mapHeight + 'px', 'important'); |
| } |
| doc.querySelectorAll('.stCustomComponentV1 iframe').forEach(iframe => { |
| iframe.style.setProperty('max-height', '800px', 'important'); |
| const rect = iframe.getBoundingClientRect(); |
| const mapHeight = Math.max(300, Math.min(vh - rect.top - 24, vh * 0.85)); |
| iframe.style.setProperty('height', mapHeight + 'px', 'important'); |
| }); |
| _resizing = false; |
| } |
| |
| // Run on load (delayed to let Streamlit render) |
| setTimeout(resizePanels, 200); |
| |
| // Run on resize |
| window.parent.addEventListener('resize', () => { |
| clearTimeout(window._panelResizeTimer); |
| window._panelResizeTimer = setTimeout(resizePanels, 100); |
| }); |
| |
| // Watch for DOM changes in parent (Streamlit re-renders) |
| new MutationObserver(() => { |
| clearTimeout(window._panelResizeTimer); |
| window._panelResizeTimer = setTimeout(resizePanels, 300); |
| }).observe(doc.body, { childList: true, subtree: true }); |
| })(); |
| </script> |
| </body> |
| </html> |
| """ |
|
|
| CARD_HOVER_JS = """<!DOCTYPE html> |
| <html> |
| <body> |
| <script> |
| (function() { |
| const doc = window.parent.document; |
| |
| function getFoliumWin() { |
| var iframe = doc.querySelector('.stFolium iframe, iframe[title="streamlit_folium.st_folium"]'); |
| return iframe ? iframe.contentWindow || iframe.contentWindow : null; |
| } |
| |
| function getFoliumDoc() { |
| var iframe = doc.querySelector('.stFolium iframe, iframe[title="streamlit_folium.st_folium"]'); |
| return iframe ? iframe.contentDocument || iframe.contentWindow.document : null; |
| } |
| |
| function findLeafletMap() { |
| var win = getFoliumWin(); |
| if (!win) return null; |
| // Leaflet map instances are stored as global variables; find one |
| for (var k in win) { |
| try { |
| if (win[k] && win[k]._container && win[k]._layers) return win[k]; |
| } catch(e) {} |
| } |
| return null; |
| } |
| |
| function highlightMarker(idx) { |
| var fdoc = getFoliumDoc(); |
| if (!fdoc) return; |
| var el = fdoc.querySelector('.spider-marker[data-idx="'+idx+'"]'); |
| if (!el) return; |
| el.style.background = '#f59e0b'; |
| el.style.transform = 'scale(1.35)'; |
| el.style.boxShadow = '0 0 14px rgba(245,158,11,0.6)'; |
| el.style.zIndex = '1000'; |
| // Open popup |
| var map = findLeafletMap(); |
| if (map) { |
| map.eachLayer(function(layer) { |
| if (layer._icon === el.parentElement && layer._map) { |
| layer.openPopup(); |
| } |
| }); |
| } |
| } |
| |
| function unhighlightMarker(idx) { |
| var fdoc = getFoliumDoc(); |
| if (!fdoc) return; |
| var el = fdoc.querySelector('.spider-marker[data-idx="'+idx+'"]'); |
| if (!el) return; |
| el.style.background = '#2a9fd6'; |
| el.style.transform = ''; |
| el.style.boxShadow = '0 2px 6px rgba(0,0,0,0.5)'; |
| el.style.zIndex = ''; |
| // Close popup |
| var map = findLeafletMap(); |
| if (map) { |
| map.eachLayer(function(layer) { |
| if (layer._icon === el.parentElement && layer._map) { |
| layer.closePopup(); |
| } |
| }); |
| } |
| } |
| |
| function setupCardHover() { |
| var pins = doc.querySelectorAll('.card-pin[data-card-idx]'); |
| if (!pins.length) { setTimeout(setupCardHover, 300); return; } |
| |
| pins.forEach(function(pin) { |
| if (pin._hoverSetup) return; |
| pin._hoverSetup = true; |
| |
| var idx = parseInt(pin.getAttribute('data-card-idx')); |
| var column = pin.closest('[data-testid="stColumn"]') || pin.parentElement; |
| var expander = column ? column.querySelector('.stExpander') : null; |
| if (!expander) return; |
| |
| expander.addEventListener('mouseenter', function() { |
| highlightMarker(idx); |
| }); |
| expander.addEventListener('mouseleave', function() { |
| unhighlightMarker(idx); |
| }); |
| }); |
| } |
| |
| setTimeout(setupCardHover, 500); |
| new MutationObserver(function() { |
| clearTimeout(window._hoverObTimer); |
| window._hoverObTimer = setTimeout(setupCardHover, 300); |
| }).observe(doc.body, { childList: true, subtree: true }); |
| })(); |
| </script> |
| </body> |
| </html> |
| """ |
|
|
| SMART_IMAGE_POSITION_JS = """<!DOCTYPE html> |
| <html> |
| <body> |
| <script> |
| (function() { |
| var doc = window.parent.document; |
| |
| function repositionPortraitImages() { |
| var imgs = doc.querySelectorAll('.card-img'); |
| var found = 0; |
| imgs.forEach(function(img) { |
| // If natural dimensions are available, check immediately |
| if (img.naturalHeight > 0 && img.naturalWidth > 0) { |
| found++; |
| if (img.naturalHeight > img.naturalWidth) { |
| // Portrait: show upper third to capture the attraction, not the ground |
| img.style.objectPosition = '50% 25%'; |
| } else { |
| img.style.objectPosition = '50% 50%'; |
| } |
| } |
| }); |
| // Retry if no images have loaded yet |
| if (found === 0 && imgs.length > 0) { |
| setTimeout(repositionPortraitImages, 300); |
| } |
| } |
| |
| // Also handle lazy-loaded images — they'll fire 'load' after becoming visible |
| doc.addEventListener('load', function(e) { |
| if (e.target && e.target.classList && e.target.classList.contains('card-img')) { |
| if (e.target.naturalHeight > e.target.naturalWidth) { |
| e.target.style.objectPosition = '50% 25%'; |
| } |
| } |
| }, true); |
| |
| // Initial run after DOM settles |
| setTimeout(repositionPortraitImages, 500); |
| })(); |
| </script> |
| </body> |
| </html> |
| """ |
|
|
| FORCE_DARK_WRAPPER_JS = """<!DOCTYPE html> |
| <script> |
| (function() { |
| /* Best-effort attempt to force dark on parent wrapper (HF Spaces). |
| Cross-origin restrictions silently fail — no breakage. */ |
| try { |
| if (window.parent !== window) { |
| var p = window.parent; |
| var html = p.document.documentElement; |
| var body = p.document.body; |
| if (html) { |
| html.style.backgroundColor = '#060606'; |
| html.classList.add('dark'); |
| } |
| if (body) body.style.backgroundColor = '#060606'; |
| /* Also try to find and style the main wrapper container */ |
| var containers = p.document.querySelectorAll('.SpacePage, .main-container, main, [class*="container"]'); |
| for (var i = 0; i < containers.length; i++) { |
| var bg = getComputedStyle(containers[i]).backgroundColor; |
| if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') { |
| containers[i].style.backgroundColor = '#060606'; |
| } |
| } |
| } |
| } catch(e) { |
| /* Cross-origin iframe — can't access parent. This is expected. */ |
| } |
| })(); |
| </script>""" |
|
|
| STACK_CONTROLS_JS = """<!DOCTYPE html> |
| <html> |
| <body> |
| <script> |
| (function() { |
| try { |
| var p = window.parent; |
| if (!p) return; |
| |
| // Fixed breakpoint matching CSS @media (max-width: 768px) |
| var BREAKPOINT = 768; |
| |
| function updateStack() { |
| var body = p.document.body; |
| if (!body) return; |
| var isNarrow = p.window.innerWidth <= BREAKPOINT; |
| body.classList.toggle('stack-columns', isNarrow); |
| |
| // Apply 4-row layout to search controls (first horizontal block) only |
| var firstHB = body.querySelector('[data-testid="stHorizontalBlock"]'); |
| if (isNarrow && firstHB) { |
| var cols = firstHB.querySelectorAll('[data-testid="stColumn"]'); |
| // Columns 3-6: 50% width (Picks, Translation, Search, Refresh) |
| for (var i = 2; i < 6 && i < cols.length; i++) { |
| cols[i].style.flex = '0 1 calc(50% - 7.5px)'; |
| } |
| // Spacing above each control row |
| for (var i = 0; i < cols.length; i++) { |
| var inner = cols[i].querySelector('div'); |
| if (inner) inner.style.paddingTop = '12px'; |
| } |
| } else if (firstHB) { |
| // Clear inline styles when unstacked |
| var cols = firstHB.querySelectorAll('[data-testid="stColumn"]'); |
| for (var i = 0; i < cols.length; i++) { |
| cols[i].style.flex = ''; |
| var inner = cols[i].querySelector('div'); |
| if (inner) inner.style.paddingTop = ''; |
| } |
| } |
| |
| if (isNarrow) { |
| // Hide the map container directly (more reliable than CSS alone) |
| var mapContainer = body.querySelector('.stCustomComponentV1'); |
| if (mapContainer) { |
| mapContainer.style.setProperty('display', 'none', 'important'); |
| } |
| // Hide the map title |
| var mapTitles = body.querySelectorAll('h3'); |
| for (var i = 0; i < mapTitles.length; i++) { |
| if (mapTitles[i].textContent.trim() === '🗺️ Map') { |
| mapTitles[i].style.setProperty('display', 'none', 'important'); |
| } |
| } |
| } else { |
| // Restore map visibility |
| var mapContainer = body.querySelector('.stCustomComponentV1'); |
| if (mapContainer) { |
| mapContainer.style.display = ''; |
| } |
| var mapTitles = body.querySelectorAll('h3'); |
| for (var i = 0; i < mapTitles.length; i++) { |
| if (mapTitles[i].textContent.trim() === '🗺️ Map') { |
| mapTitles[i].style.display = ''; |
| } |
| } |
| } |
| } |
| |
| // Collapse all cards except the first when stacking activates |
| function collapseCards() { |
| if (p.window.innerWidth > BREAKPOINT) return; |
| var body = p.document.body; |
| if (!body) return; |
| var expanders = body.querySelectorAll('.stExpander details'); |
| for (var i = 0; i < expanders.length; i++) { |
| if (i === 0) { |
| expanders[i].setAttribute('open', ''); |
| } else { |
| expanders[i].removeAttribute('open'); |
| } |
| } |
| } |
| |
| // Run on load — collapse cards after Streamlit renders them |
| if (document.readyState === 'complete') { |
| setTimeout(updateStack, 100); |
| setTimeout(collapseCards, 300); |
| setTimeout(collapseCards, 1000); |
| } else { |
| p.window.addEventListener('load', function() { |
| setTimeout(updateStack, 100); |
| setTimeout(collapseCards, 300); |
| setTimeout(collapseCards, 1000); |
| }); |
| } |
| |
| // Debounced resize — update layout and re-collapse |
| var timer; |
| p.window.addEventListener('resize', function() { |
| clearTimeout(timer); |
| timer = setTimeout(function() { |
| updateStack(); |
| collapseCards(); |
| }, 100); |
| }); |
| |
| // Watch for DOM changes — Streamlit renders content via websocket |
| // Only update layout (map hide/show, search controls), NEVER collapse |
| // so user can freely tap to expand/collapse cards |
| var moTimer; |
| var observer = new MutationObserver(function() { |
| clearTimeout(moTimer); |
| moTimer = setTimeout(updateStack, 200); |
| }); |
| if (p.document.body) { |
| observer.observe(p.document.body, { childList: true, subtree: true }); |
| } |
| } catch(e) { |
| /* Cross-origin — silently skip */ |
| } |
| })(); |
| </script> |
| </body> |
| </html>""" |
|
|
| def apply_dark_theme(): |
| """Inject dark-theme CSS, flexible panel JS, card↔map hover JS, and smart image positioning JS.""" |
| import streamlit as st |
| st.markdown(DARK_THEME_CSS, unsafe_allow_html=True) |
| |
| st.iframe(FORCE_DARK_WRAPPER_JS, height=1) |
| st.iframe(FLEX_PANELS_JS, height=1) |
| st.iframe(CARD_HOVER_JS, height=1) |
| st.iframe(SMART_IMAGE_POSITION_JS, height=1) |
| st.iframe(STACK_CONTROLS_JS, height=1) |