Download src/styles/dark_theme.py from jofaichow/roamify: direct link, hf CLI and curl.
- Browser
- Download file 33.7 kB
-
https://huggingface.co/spaces/jofaichow/roamify/resolve/main/src/styles/dark_theme.py
- Command line
-
hf download hf://spaces/jofaichow/roamify/src/styles/dark_theme.py
-
curl -L -o dark_theme.py https://huggingface.co/spaces/jofaichow/roamify/resolve/main/src/styles/dark_theme.py
33.7 kB
| """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) | |
| # Use st.iframe to execute JS (st.markdown strips <script> tags) | |
| 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) |