feat(rcal): docked map layout + theme-aware tiles; emoji tab badges

- rCal: default map to docked (side-by-side) layout instead of floating overlay
- rCal: switch map tiles between Voyager (light) and dark_all (dark) based on theme
- rCal: boost dark mode map brightness/contrast for readability
- rCal: watch for theme changes via MutationObserver for live tile swapping
- Tab bar: replace text badges with emoji icons, fix badge colors for light themes
- App switcher: fix badge text color to dark for gradient backgrounds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-10 14:13:28 -07:00
parent 2131d79f38
commit 53af7fc057
3 changed files with 91 additions and 46 deletions

View File

@ -130,7 +130,8 @@ class FolkCalendarView extends HTMLElement {
private zoomCoupled = true;
// Map panel state (replaces old tab system)
private mapPanelState: "minimized" | "floating" | "docked" = "floating";
private mapPanelState: "minimized" | "floating" | "docked" = "docked";
private currentTileLayer: any = null;
private lunarOverlayExpanded = false;
private _wheelTimer: ReturnType<typeof setTimeout> | null = null;
@ -2000,11 +2001,11 @@ class FolkCalendarView extends HTMLElement {
center, zoom, zoomControl: false,
});
L.tileLayer("https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", {
attribution: '\u00A9 <a href="https://www.openstreetmap.org/copyright">OSM</a> \u00A9 <a href="https://carto.com/">CARTO</a>',
subdomains: "abcd",
maxZoom: 19,
}).addTo(this.leafletMap);
this.applyMapTileLayer();
// Watch for theme changes
const observer = new MutationObserver(() => this.applyMapTileLayer());
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
this.mapMarkerLayer = L.layerGroup().addTo(this.leafletMap);
this.transitLineLayer = L.layerGroup().addTo(this.leafletMap);
@ -2028,6 +2029,39 @@ class FolkCalendarView extends HTMLElement {
this.updateTransitLines();
}
private isDarkTheme(): boolean {
const theme = document.documentElement.getAttribute("data-theme");
if (theme) return theme === "dark";
return window.matchMedia("(prefers-color-scheme: dark)").matches;
}
private applyMapTileLayer() {
const L = (window as any).L;
if (!L || !this.leafletMap) return;
if (this.currentTileLayer) {
this.leafletMap.removeLayer(this.currentTileLayer);
}
const dark = this.isDarkTheme();
const tileUrl = dark
? "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
: "https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png";
this.currentTileLayer = L.tileLayer(tileUrl, {
attribution: '\u00A9 <a href="https://www.openstreetmap.org/copyright">OSM</a> \u00A9 <a href="https://carto.com/">CARTO</a>',
subdomains: "abcd",
maxZoom: 19,
}).addTo(this.leafletMap);
// Apply brightness filter for dark mode to improve contrast
const container = this.leafletMap.getContainer();
const tilePane = container?.querySelector(".leaflet-tile-pane") as HTMLElement;
if (tilePane) {
tilePane.style.filter = dark ? "brightness(1.4) contrast(1.15)" : "none";
}
}
private updateMapMarkers() {
const L = (window as any).L;
if (!L || !this.mapMarkerLayer) return;

View File

@ -390,7 +390,7 @@ const STYLES = `
.trigger-badge {
display: inline-flex; align-items: center; justify-content: center;
width: 22px; height: 22px; border-radius: 5px;
font-size: 0.65rem; font-weight: 900; color: var(--rs-text-inverse);
font-size: 0.65rem; font-weight: 900; color: #1e293b;
line-height: 1; flex-shrink: 0; white-space: nowrap;
}
.trigger-badge.rstack-gradient {
@ -448,7 +448,7 @@ a.rstack-header:hover { background: var(--rs-bg-hover); }
display: flex; align-items: center; justify-content: center;
width: 28px; height: 28px; border-radius: 8px;
background: linear-gradient(135deg, #67e8f9, #c4b5fd, #fda4af);
font-size: 0.7rem; font-weight: 900; color: var(--rs-text-inverse); line-height: 1;
font-size: 0.7rem; font-weight: 900; color: #1e293b; line-height: 1;
flex-shrink: 0;
}
.rstack-info { display: flex; flex-direction: column; }
@ -493,7 +493,7 @@ a.rstack-header:hover { background: var(--rs-bg-hover); }
.item-badge {
display: flex; align-items: center; justify-content: center;
width: 28px; height: 28px; border-radius: 6px;
font-size: 0.7rem; font-weight: 900; color: var(--rs-text-inverse);
font-size: 0.7rem; font-weight: 900; color: #1e293b;
line-height: 1; flex-shrink: 0; white-space: nowrap;
}
.item-icon { font-size: 1.3rem; width: 28px; text-align: center; flex-shrink: 0; }

View File

@ -25,32 +25,36 @@ import { FLOW_COLORS, FLOW_LABELS } from "../../lib/layer-types";
// Badge info for tab display
const MODULE_BADGES: Record<string, { badge: string; color: string }> = {
rspace: { badge: "rS", color: "#5eead4" },
rnotes: { badge: "rN", color: "#fcd34d" },
rpubs: { badge: "rP", color: "#fda4af" },
rswag: { badge: "rSw", color: "#fda4af" },
rsplat: { badge: "r3", color: "#d8b4fe" },
rcal: { badge: "rC", color: "#7dd3fc" },
rtrips: { badge: "rT", color: "#6ee7b7" },
rmaps: { badge: "rM", color: "#86efac" },
rchats: { badge: "rCh", color: "#6ee7b7" },
rinbox: { badge: "rI", color: "#a5b4fc" },
rmail: { badge: "rMa", color: "#93c5fd" },
rforum: { badge: "rFo", color: "#fcd34d" },
rchoices: { badge: "rCo", color: "#f0abfc" },
rvote: { badge: "rV", color: "#c4b5fd" },
rflows: { badge: "rFl", color: "#bef264" },
rwallet: { badge: "rW", color: "#fde047" },
rcart: { badge: "rCt", color: "#fdba74" },
rauctions: { badge: "rA", color: "#fca5a5" },
rtube: { badge: "rTu", color: "#f9a8d4" },
rphotos: { badge: "rPh", color: "#f9a8d4" },
rnetwork: { badge: "rNe", color: "#93c5fd" },
rsocials: { badge: "rSo", color: "#7dd3fc" },
rfiles: { badge: "rFi", color: "#67e8f9" },
rbooks: { badge: "rB", color: "#fda4af" },
rdata: { badge: "rD", color: "#d8b4fe" },
rwork: { badge: "rWo", color: "#cbd5e1" },
rspace: { badge: "r🎨", color: "#5eead4" },
rnotes: { badge: "r📝", color: "#fcd34d" },
rpubs: { badge: "r📖", color: "#fda4af" },
rswag: { badge: "r👕", color: "#fda4af" },
rsplat: { badge: "r🔮", color: "#d8b4fe" },
rcal: { badge: "r📅", color: "#7dd3fc" },
rtrips: { badge: "r✈", color: "#6ee7b7" },
rmaps: { badge: "r🗺", color: "#86efac" },
rchats: { badge: "r🗨", color: "#6ee7b7" },
rinbox: { badge: "r📨", color: "#a5b4fc" },
rmail: { badge: "r✉", color: "#93c5fd" },
rforum: { badge: "r💬", color: "#fcd34d" },
rmeets: { badge: "r📹", color: "#67e8f9" },
rchoices: { badge: "r☑", color: "#f0abfc" },
rvote: { badge: "r🗳", color: "#c4b5fd" },
rflows: { badge: "r🌊", color: "#bef264" },
rwallet: { badge: "r💰", color: "#fde047" },
rcart: { badge: "r🛒", color: "#fdba74" },
rauctions: { badge: "r🏛", color: "#fca5a5" },
rtube: { badge: "r🎬", color: "#f9a8d4" },
rphotos: { badge: "r📸", color: "#f9a8d4" },
rnetwork: { badge: "r🌐", color: "#93c5fd" },
rsocials: { badge: "r📢", color: "#7dd3fc" },
rfiles: { badge: "r📁", color: "#67e8f9" },
rbooks: { badge: "r📚", color: "#fda4af" },
rdata: { badge: "r📊", color: "#d8b4fe" },
rwork: { badge: "r📋", color: "#cbd5e1" },
rschedule: { badge: "r⏱", color: "#a5b4fc" },
rids: { badge: "r🪪", color: "#6ee7b7" },
rstack: { badge: "r✨", color: "" },
};
// Category definitions for the + menu dropdown grouping
@ -1431,14 +1435,16 @@ const STYLES = `
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
min-width: 20px;
height: 20px;
border-radius: 4px;
font-size: 0.55rem;
font-size: 0.6rem;
font-weight: 900;
color: var(--rs-text-inverse);
color: #1e293b;
line-height: 1;
flex-shrink: 0;
white-space: nowrap;
padding: 0 2px;
}
.tab-label {
@ -1589,13 +1595,15 @@ const STYLES = `
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
min-width: 24px;
height: 24px;
border-radius: 5px;
font-size: 0.55rem;
font-size: 0.65rem;
font-weight: 900;
color: var(--rs-text-inverse);
color: #1e293b;
flex-shrink: 0;
white-space: nowrap;
padding: 0 3px;
}
.add-menu-icon {
@ -1847,13 +1855,15 @@ const STYLES = `
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
min-width: 24px;
height: 24px;
border-radius: 5px;
font-size: 0.55rem;
font-size: 0.65rem;
font-weight: 900;
color: var(--rs-text-inverse);
color: #1e293b;
flex-shrink: 0;
white-space: nowrap;
padding: 0 3px;
}
.layer-name {
@ -2154,9 +2164,10 @@ const STYLES = `
justify-content: center;
padding: 3px 8px;
border-radius: 5px;
font-size: 0.6rem;
font-size: 0.65rem;
font-weight: 900;
color: var(--rs-text-inverse);
color: #1e293b;
white-space: nowrap;
}
.flow-dialog-arrow {