building new tarot frame component for custom layout

This commit is contained in:
2026-04-01 12:31:56 -07:00
parent d47e63df6d
commit a7d956cee8
11 changed files with 2359 additions and 79 deletions

12
app.js
View File

@@ -40,6 +40,7 @@ const holidaySectionEl = document.getElementById("holiday-section");
const audioCircleSectionEl = document.getElementById("audio-circle-section");
const audioNotesSectionEl = document.getElementById("audio-notes-section");
const tarotSectionEl = document.getElementById("tarot-section");
const tarotFrameSectionEl = document.getElementById("tarot-frame-section");
const tarotHouseSectionEl = document.getElementById("tarot-house-section");
const astronomySectionEl = document.getElementById("astronomy-section");
const natalSectionEl = document.getElementById("natal-section");
@@ -67,6 +68,7 @@ const openAudioEl = document.getElementById("open-audio");
const openAudioCircleEl = document.getElementById("open-audio-circle");
const openAudioNotesEl = document.getElementById("open-audio-notes");
const openTarotEl = document.getElementById("open-tarot");
const openTarotFrameEl = document.getElementById("open-tarot-frame");
const openTarotHouseEl = document.getElementById("open-tarot-house");
const openAstronomyEl = document.getElementById("open-astronomy");
const openPlanetsEl = document.getElementById("open-planets");
@@ -393,6 +395,11 @@ window.TarotSpreadUi?.init?.({
setActiveSection: (section) => sectionStateUi.setActiveSection?.(section)
});
window.TarotFrameUi?.init?.({
ensureTarotSection,
getCards: () => window.TarotSectionUi?.getCards?.() || []
});
sectionStateUi.init?.({
calendar,
tarotSpreadUi,
@@ -411,6 +418,7 @@ sectionStateUi.init?.({
audioCircleSectionEl,
audioNotesSectionEl,
tarotSectionEl,
tarotFrameSectionEl,
tarotHouseSectionEl,
astronomySectionEl,
natalSectionEl,
@@ -438,6 +446,7 @@ sectionStateUi.init?.({
openAudioCircleEl,
openAudioNotesEl,
openTarotEl,
openTarotFrameEl,
openTarotHouseEl,
openAstronomyEl,
openPlanetsEl,
@@ -460,6 +469,7 @@ sectionStateUi.init?.({
},
ensure: {
ensureTarotSection,
ensureTarotFrameSection: window.TarotFrameUi?.ensureTarotFrameSection,
ensurePlanetSection,
ensureCyclesSection,
ensureElementsSection,
@@ -540,6 +550,7 @@ navigationUi.init?.({
openAudioCircleEl,
openAudioNotesEl,
openTarotEl,
openTarotFrameEl,
openTarotHouseEl,
openAstronomyEl,
openPlanetsEl,
@@ -562,6 +573,7 @@ navigationUi.init?.({
},
ensure: {
ensureTarotSection,
ensureTarotFrameSection: window.TarotFrameUi?.ensureTarotFrameSection,
ensurePlanetSection,
ensureCyclesSection,
ensureElementsSection,

View File

@@ -0,0 +1,247 @@
(function () {
"use strict";
const DEFAULT_TIMEOUT = 10000;
const DEFAULT_INTERVAL = 50;
function sleep(ms) {
return new Promise((resolve) => {
window.setTimeout(resolve, ms);
});
}
async function waitFor(predicate, options = {}) {
const timeout = Number.isFinite(Number(options.timeout)) ? Number(options.timeout) : DEFAULT_TIMEOUT;
const interval = Number.isFinite(Number(options.interval)) ? Number(options.interval) : DEFAULT_INTERVAL;
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
try {
if (await predicate()) {
return true;
}
} catch {
}
await sleep(interval);
}
return false;
}
function textById(id) {
const element = document.getElementById(id);
return element instanceof HTMLElement ? String(element.textContent || "").trim() : "";
}
function getSectionElement(selector) {
const element = document.querySelector(selector);
return element instanceof HTMLElement ? element : null;
}
function getLayoutState(selector) {
const layout = getSectionElement(selector);
const classes = layout ? Array.from(layout.classList) : [];
return {
classes,
detailOnly: classes.includes("layout-sidebar-collapsed") && !classes.includes("layout-detail-collapsed")
};
}
function getSelectedText(sectionSelector) {
const section = getSectionElement(sectionSelector);
if (!section) {
return "";
}
const selected = section.querySelector('[aria-selected="true"]');
return selected instanceof HTMLElement ? String(selected.textContent || "").trim() : "";
}
function dispatchNavigationEvent(eventName, detail) {
document.dispatchEvent(new CustomEvent(eventName, {
detail,
bubbles: true
}));
}
function createSnapshot(test) {
const section = getSectionElement(test.sectionSelector);
const layoutState = getLayoutState(test.layoutSelector);
return {
sectionHidden: section ? section.hidden : true,
layoutClasses: layoutState.classes,
detailOnly: layoutState.detailOnly,
detailName: textById(test.detailNameId),
detailSub: test.detailSubId ? textById(test.detailSubId) : "",
detailBody: test.detailBodyId ? textById(test.detailBodyId) : "",
selectedText: getSelectedText(test.sectionSelector)
};
}
async function waitForAppReady() {
return waitFor(() => {
if (document.readyState !== "complete") {
return false;
}
const referenceData = window.TarotAppRuntime?.getReferenceData?.();
const magickDataset = window.TarotAppRuntime?.getMagickDataset?.();
return Boolean(
typeof window.TarotChromeUi?.showDetailOnly === "function"
&& referenceData
&& magickDataset
);
});
}
const TESTS = {
tarotTrumpNav: {
eventName: "nav:tarot-trump",
detail: { trumpNumber: 0 },
sectionSelector: "#tarot-section",
layoutSelector: "#tarot-browse-view .tarot-layout",
detailNameId: "tarot-detail-name",
matches: (state) => state.detailName === "The Fool"
},
kabViewTrump: {
eventName: "kab:view-trump",
detail: { trumpNumber: 0 },
sectionSelector: "#tarot-section",
layoutSelector: "#tarot-browse-view .tarot-layout",
detailNameId: "tarot-detail-name",
matches: (state) => state.detailName === "The Fool"
},
ichingHexagramNav: {
eventName: "nav:iching",
detail: { hexagramNumber: 1 },
sectionSelector: "#iching-section",
layoutSelector: "#iching-section .planet-layout",
detailNameId: "iching-detail-name",
matches: (state) => /Creative Force/i.test(state.detailName) && /#1/i.test(state.selectedText)
},
zodiacSignNav: {
eventName: "nav:zodiac",
detail: { signId: "aries" },
sectionSelector: "#zodiac-section",
layoutSelector: "#zodiac-section .planet-layout",
detailNameId: "zodiac-detail-name",
detailSubId: "zodiac-detail-sub",
matches: (state) => /Aries/i.test(state.detailSub) && /aries/i.test(state.selectedText)
},
planetNav: {
eventName: "nav:planet",
detail: { planetId: "mars" },
sectionSelector: "#planet-section",
layoutSelector: "#planet-section .planet-layout",
detailNameId: "planet-detail-name",
matches: (state) => state.detailName !== "--" && /mars/i.test(state.selectedText)
},
numberNav: {
eventName: "nav:number",
detail: { value: 1 },
sectionSelector: "#numbers-section",
layoutSelector: "#numbers-section .numbers-main-layout",
detailNameId: "numbers-detail-name",
matches: (state) => /Number 1|One/i.test(state.detailName) && /One/i.test(state.selectedText)
},
elementNav: {
eventName: "nav:elements",
detail: { elementId: "fire" },
sectionSelector: "#elements-section",
layoutSelector: "#elements-section .planet-layout",
detailNameId: "elements-detail-name",
matches: (state) => state.detailName !== "--" && /fire/i.test(state.selectedText)
},
calendarMonthNav: {
eventName: "nav:calendar-month",
detail: { calendarId: "gregorian", monthId: "january" },
sectionSelector: "#calendar-section",
layoutSelector: "#calendar-section .planet-layout",
detailNameId: "calendar-detail-name",
matches: (state) => /january/i.test(state.detailName) && /january/i.test(state.selectedText)
},
tarotViewKabPath: {
eventName: "tarot:view-kab-path",
detail: { pathNumber: 11 },
sectionSelector: "#kabbalah-tree-section",
layoutSelector: "#kabbalah-tree-section .kab-layout",
detailNameId: "kab-detail-name",
detailSubId: "kab-detail-sub",
detailBodyId: "kab-detail-body",
matches: (state) => /Path 11/i.test(state.detailName) && /The Fool/i.test(state.detailSub)
}
};
async function runTest(testId, options = {}) {
const test = TESTS[testId];
if (!test) {
throw new Error(`Unknown navigation detail test: ${testId}`);
}
const ready = await waitForAppReady();
if (!ready) {
return {
id: testId,
pass: false,
error: "App data did not finish loading in time."
};
}
dispatchNavigationEvent(test.eventName, test.detail);
const timeout = Number.isFinite(Number(options.timeout)) ? Number(options.timeout) : DEFAULT_TIMEOUT;
const pass = await waitFor(() => {
const state = createSnapshot(test);
return !state.sectionHidden && state.detailOnly && test.matches(state);
}, { timeout });
const state = createSnapshot(test);
return {
id: testId,
eventName: test.eventName,
detail: test.detail,
pass,
...state,
error: pass ? "" : "Destination detail pane did not settle into detail-only mode."
};
}
async function runAll(testIds = Object.keys(TESTS), options = {}) {
const results = [];
for (const testId of testIds) {
results.push(await runTest(testId, options));
}
const summary = {
total: results.length,
passed: results.filter((result) => result.pass).length,
failed: results.filter((result) => !result.pass).length
};
const payload = { summary, results };
window.__TAROT_NAVIGATION_DETAIL_TEST_RESULTS__ = payload;
return payload;
}
window.TarotNavigationDetailTestHarness = {
runAll,
runTest,
listTests: () => Object.keys(TESTS)
};
if (new URLSearchParams(window.location.search).has("navtest")) {
waitForAppReady().then((ready) => {
if (!ready) {
return;
}
runAll().then((result) => {
window.__TAROT_NAVIGATION_DETAIL_TEST_LAST_RESULT__ = result;
console.info("Navigation detail test harness results", result);
});
});
}
})();

View File

@@ -89,6 +89,9 @@
.topbar-menu-toggle:hover {
background: #3f3f46;
}
.topbar-settings-toggle {
margin-left: 0;
}
.topbar-actions {
display: none;
flex: 1 0 100%;
@@ -187,6 +190,9 @@
.topbar-menu-toggle {
min-height: 38px;
}
.topbar-settings-toggle {
min-height: 38px;
}
.topbar-panel-toggle {
min-height: 38px;
}
@@ -389,12 +395,21 @@
box-sizing: border-box;
overflow: hidden;
}
#tarot-frame-section {
height: calc(100vh - 61px);
background: #18181b;
box-sizing: border-box;
overflow: auto;
}
#tarot-section[hidden] {
display: none;
}
#tarot-house-section[hidden] {
display: none;
}
#tarot-frame-section[hidden] {
display: none;
}
#planet-section[hidden] {
display: none;
}
@@ -844,12 +859,430 @@
gap: 10px;
}
.tarot-frame-view {
min-height: 100%;
padding: 18px;
box-sizing: border-box;
}
.tarot-frame-shell {
width: min(1480px, 100%);
margin: 0 auto;
display: grid;
gap: 16px;
}
.tarot-frame-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.tarot-frame-title {
margin: 0;
font-size: clamp(28px, 4vw, 38px);
line-height: 1.05;
letter-spacing: 0.01em;
}
.tarot-frame-copy {
margin: 8px 0 0;
max-width: 820px;
color: #cbd5e1;
font-size: 14px;
line-height: 1.55;
}
.tarot-frame-actions {
position: relative;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.tarot-frame-action-btn {
padding: 10px 14px;
border: 1px solid #4c1d95;
border-radius: 999px;
background: linear-gradient(180deg, #312e81, #1d1b4b);
color: #eef2ff;
cursor: pointer;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.02em;
}
.tarot-frame-action-btn:hover {
border-color: #6d28d9;
background: linear-gradient(180deg, #4338ca, #312e81);
}
.tarot-frame-settings-panel {
position: absolute;
top: calc(100% + 10px);
right: 0;
z-index: 25;
min-width: 220px;
display: grid;
gap: 10px;
padding: 12px;
border: 1px solid #312e81;
border-radius: 16px;
background:
radial-gradient(circle at top, rgba(99, 102, 241, 0.14), transparent 40%),
linear-gradient(180deg, rgba(22, 22, 34, 0.98), rgba(10, 10, 18, 0.98));
box-shadow: 0 18px 38px rgba(0, 0, 0, 0.3);
}
.tarot-frame-settings-panel[hidden] {
display: none !important;
}
.tarot-frame-toggle {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 9px 10px;
border: 1px solid rgba(99, 102, 241, 0.34);
border-radius: 12px;
background: rgba(15, 23, 42, 0.5);
color: #e2e8f0;
font-size: 13px;
font-weight: 600;
cursor: pointer;
-webkit-user-select: none;
user-select: none;
}
.tarot-frame-toggle input {
margin: 0;
accent-color: #818cf8;
}
.tarot-frame-toggle input:disabled,
.tarot-frame-export-btn:disabled,
.tarot-frame-settings-toggle:disabled {
cursor: wait;
}
.tarot-frame-export-btn {
width: 100%;
justify-content: center;
}
.tarot-frame-status {
padding: 10px 14px;
border: 1px solid #27272a;
border-radius: 14px;
background: linear-gradient(180deg, rgba(20, 20, 32, 0.95), rgba(10, 10, 18, 0.95));
color: #d4d4d8;
font-size: 13px;
line-height: 1.4;
}
.tarot-frame-board-grid {
display: block;
}
.tarot-frame-panel {
--frame-cell-size: clamp(34px, 3.1vw, 52px);
--frame-gap: clamp(2px, 0.3vw, 6px);
display: grid;
gap: 14px;
padding: 18px;
border: 1px solid #27272a;
border-radius: 22px;
background:
radial-gradient(circle at top, rgba(59, 130, 246, 0.08), transparent 34%),
linear-gradient(180deg, #161622 0%, #0f0f17 100%);
box-shadow: 0 22px 54px rgba(0, 0, 0, 0.24);
overflow: auto;
}
.tarot-frame-panel-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
min-width: max-content;
}
.tarot-frame-panel-title {
margin: 0;
font-size: 17px;
line-height: 1.2;
color: #f8fafc;
}
.tarot-frame-panel-subtitle {
margin: 5px 0 0;
color: #94a3b8;
font-size: 13px;
line-height: 1.45;
}
.tarot-frame-panel-count {
align-self: center;
padding: 6px 10px;
border-radius: 999px;
background: rgba(15, 23, 42, 0.78);
color: #cbd5e1;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
white-space: nowrap;
}
.tarot-frame-legend {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.tarot-frame-legend-item {
display: grid;
gap: 4px;
padding: 10px 12px;
border: 1px solid rgba(71, 85, 105, 0.56);
border-radius: 14px;
background: rgba(15, 23, 42, 0.46);
color: #cbd5e1;
font-size: 12px;
line-height: 1.4;
}
.tarot-frame-legend-item strong {
color: #f8fafc;
font-size: 12px;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.tarot-frame-grid {
display: grid;
grid-template-columns: repeat(var(--frame-grid-size), var(--frame-cell-size));
grid-template-rows: repeat(var(--frame-grid-size), var(--frame-cell-size));
gap: var(--frame-gap);
justify-content: center;
align-content: center;
min-width: max-content;
}
.tarot-frame-slot {
position: relative;
width: var(--frame-cell-size);
height: var(--frame-cell-size);
border-radius: 8px;
transition: transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease;
}
.tarot-frame-slot.is-empty-slot {
border: 1px dashed rgba(148, 163, 184, 0.4);
}
.tarot-frame-slot.is-drop-target {
box-shadow: 0 0 0 2px #f59e0b, 0 0 0 6px rgba(245, 158, 11, 0.18);
border-radius: 8px;
}
.tarot-frame-slot.is-drag-source {
opacity: 0.42;
}
.tarot-frame-slot-empty {
width: 100%;
height: 100%;
display: block;
border: 0;
border-radius: 0;
background: transparent;
color: transparent;
font-size: 0;
}
.tarot-frame-slot-empty::before {
content: none;
}
.tarot-frame-card {
position: absolute;
inset: 0;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
overflow: visible;
cursor: grab;
box-shadow: none;
transition: transform 120ms ease, filter 120ms ease;
-webkit-user-select: none;
user-select: none;
}
.tarot-frame-card.is-empty {
cursor: default;
box-shadow: none;
background: transparent;
border-color: transparent;
}
.tarot-frame-card:hover {
transform: translateY(-2px);
filter: drop-shadow(0 10px 18px rgba(15, 23, 42, 0.38));
}
.tarot-frame-card.is-empty:hover {
border-color: transparent;
box-shadow: none;
transform: none;
}
.tarot-frame-card-image,
.tarot-frame-card-fallback {
display: block;
width: 100%;
height: 100%;
pointer-events: none;
-webkit-user-select: none;
user-select: none;
}
.tarot-frame-card-image {
object-fit: contain;
background: transparent;
border-radius: 0;
}
.tarot-frame-card-fallback {
display: grid;
place-items: center;
padding: 8px;
box-sizing: border-box;
color: #f8fafc;
font-size: 12px;
line-height: 1.2;
text-align: center;
background:
radial-gradient(circle at top, rgba(99, 102, 241, 0.18), transparent 50%),
linear-gradient(180deg, #1e1b4b 0%, #0f172a 100%);
}
.tarot-frame-card-badge {
position: absolute;
left: 4px;
right: 4px;
bottom: 4px;
padding: 4px 5px;
border-radius: 8px;
background: rgba(2, 6, 23, 0.84);
color: #f8fafc;
font-size: clamp(8px, 0.72vw, 10px);
font-weight: 700;
line-height: 1.15;
letter-spacing: 0.02em;
text-align: center;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
white-space: normal;
box-sizing: border-box;
pointer-events: none;
-webkit-user-select: none;
user-select: none;
}
.tarot-frame-grid.is-info-hidden .tarot-frame-card-badge {
display: none;
}
.tarot-frame-drag-ghost {
position: fixed;
z-index: 120;
width: 86px;
height: 129px;
pointer-events: none;
border-radius: 14px;
overflow: hidden;
border: 1px solid #818cf8;
background: linear-gradient(180deg, #18181b 0%, #09090b 100%);
box-shadow: 0 20px 42px rgba(0, 0, 0, 0.38);
transform: translate(-50%, -50%);
}
.tarot-frame-drag-ghost img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.tarot-frame-drag-ghost-label {
position: absolute;
left: 6px;
right: 6px;
bottom: 6px;
padding: 4px 5px;
border-radius: 999px;
background: rgba(2, 6, 23, 0.88);
color: #f8fafc;
font-size: 10px;
font-weight: 700;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-sizing: border-box;
}
body.is-tarot-frame-dragging {
cursor: grabbing;
-webkit-user-select: none;
user-select: none;
}
@media (max-width: 1180px) {
.tarot-frame-legend {
grid-template-columns: minmax(0, 1fr);
}
}
@media (max-width: 820px) {
.tarot-frame-view {
padding: 12px;
}
.tarot-frame-actions {
width: 100%;
}
.tarot-frame-settings-panel {
left: 0;
right: auto;
}
.tarot-frame-panel {
padding: 14px;
}
.tarot-frame-panel {
--frame-cell-size: 28px;
}
.tarot-frame-card-badge {
font-size: 7px;
padding: 3px 4px;
}
}
.tarot-house-card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
position: relative;
}
.tarot-house-card-actions {
@@ -857,6 +1290,28 @@
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-left: auto;
}
.tarot-house-settings-panel {
position: absolute;
top: calc(100% + 10px);
right: 0;
z-index: 3;
width: min(560px, 100%);
display: grid;
gap: 10px;
padding: 14px 16px;
border: 1px solid rgba(82, 82, 91, 0.9);
border-radius: 14px;
background: rgba(9, 9, 11, 0.96);
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.34);
-webkit-backdrop-filter: blur(14px);
backdrop-filter: blur(14px);
}
.tarot-house-settings-panel[hidden] {
display: none;
}
.tarot-house-toggle {
@@ -2897,12 +3352,13 @@
}
.tarot-house-card-actions {
display: grid;
grid-template-columns: minmax(0, 1fr);
display: flex;
align-items: stretch;
gap: 8px;
margin-left: 0;
}
.tarot-house-settings-toggle,
.tarot-house-toggle,
.tarot-house-filter-group,
.tarot-house-action-btn {
@@ -2910,6 +3366,12 @@
box-sizing: border-box;
}
.tarot-house-settings-panel {
position: static;
width: 100%;
max-width: 100%;
}
.tarot-house-filter-group {
justify-content: flex-start;
}

View File

@@ -455,11 +455,13 @@
const topbarEl = document.querySelector(".topbar");
const actionsEl = document.getElementById("topbar-actions");
const menuToggleEl = document.getElementById("topbar-menu-toggle");
const settingsToggleEl = document.getElementById("open-settings");
return {
topbarEl: topbarEl instanceof HTMLElement ? topbarEl : null,
actionsEl: actionsEl instanceof HTMLElement ? actionsEl : null,
menuToggleEl: menuToggleEl instanceof HTMLButtonElement ? menuToggleEl : null
menuToggleEl: menuToggleEl instanceof HTMLButtonElement ? menuToggleEl : null,
settingsToggleEl: settingsToggleEl instanceof HTMLButtonElement ? settingsToggleEl : null
};
}
@@ -482,7 +484,7 @@
}
function bindTopbarMobileMenu() {
const { topbarEl, actionsEl, menuToggleEl } = getTopbarElements();
const { topbarEl, actionsEl, menuToggleEl, settingsToggleEl } = getTopbarElements();
if (!(topbarEl instanceof HTMLElement) || !(actionsEl instanceof HTMLElement) || !(menuToggleEl instanceof HTMLButtonElement)) {
return;
}
@@ -500,6 +502,13 @@
setTopbarMenuOpen(nextOpen);
});
if (settingsToggleEl instanceof HTMLButtonElement && settingsToggleEl.dataset.topbarSettingsReady !== "1") {
settingsToggleEl.dataset.topbarSettingsReady = "1";
settingsToggleEl.addEventListener("click", () => {
setTopbarMenuOpen(false);
});
}
actionsEl.addEventListener("click", (event) => {
const button = event.target instanceof Element
? event.target.closest("button")

View File

@@ -22,6 +22,81 @@
return config.getMagickDataset?.() || null;
}
const DETAIL_VIEW_SELECTOR_BY_SECTION = {
tarot: "#tarot-browse-view .tarot-layout",
cube: "#cube-layout",
zodiac: "#zodiac-section .planet-layout",
"alphabet-letters": "#alphabet-letters-section .planet-layout",
numbers: "#numbers-section .numbers-main-layout",
iching: "#iching-section .planet-layout",
gods: "#gods-section .planet-layout",
calendar: "#calendar-section .planet-layout",
"kabbalah-tree": "#kabbalah-tree-section .kab-layout",
planets: "#planet-section .planet-layout",
elements: "#elements-section .planet-layout"
};
function showSectionDetailOnly(sectionKey, persist = false) {
const selector = DETAIL_VIEW_SELECTOR_BY_SECTION[sectionKey];
if (!selector) {
return false;
}
const target = document.querySelector(selector);
if (!(target instanceof HTMLElement)) {
return false;
}
return Boolean(window.TarotChromeUi?.showDetailOnly?.(target, persist));
}
function isSectionDetailOnly(sectionKey) {
const selector = DETAIL_VIEW_SELECTOR_BY_SECTION[sectionKey];
if (!selector) {
return false;
}
const target = document.querySelector(selector);
if (!(target instanceof HTMLElement)) {
return false;
}
return target.classList.contains("layout-sidebar-collapsed")
&& !target.classList.contains("layout-detail-collapsed");
}
function scheduleSectionDetailOnly(sectionKey, persist = false, attempts = 4) {
requestAnimationFrame(() => {
showSectionDetailOnly(sectionKey, persist);
if (!isSectionDetailOnly(sectionKey) && attempts > 1) {
scheduleSectionDetailOnly(sectionKey, persist, attempts - 1);
}
});
}
async function prepareTarotBrowseDetailView() {
const ensure = config.ensure || {};
const referenceData = getReferenceData();
const magickDataset = getMagickDataset();
setActiveSection("tarot");
config.tarotSpreadUi?.showCardsView?.();
if (typeof ensure.ensureTarotSection === "function" && referenceData) {
await ensure.ensureTarotSection(referenceData, magickDataset);
}
await new Promise((resolve) => {
requestAnimationFrame(resolve);
});
await new Promise((resolve) => {
requestAnimationFrame(resolve);
});
showSectionDetailOnly("tarot");
}
function bindClick(element, handler) {
if (!element) {
return;
@@ -60,6 +135,10 @@
}
});
bindClick(elements.openTarotFrameEl, () => {
setActiveSection(getActiveSection() === "tarot-frame" ? "home" : "tarot-frame");
});
bindClick(elements.openTarotHouseEl, () => {
setActiveSection(getActiveSection() === "tarot-house" ? "home" : "tarot-house");
});
@@ -170,6 +249,7 @@
if (!selected && detail?.wallId) {
ui?.selectWallById?.(detail.wallId);
}
scheduleSectionDetailOnly("cube");
});
});
@@ -184,6 +264,7 @@
if (signId) {
requestAnimationFrame(() => {
window.ZodiacSectionUi?.selectBySignId?.(signId);
scheduleSectionDetailOnly("zodiac");
});
}
});
@@ -207,22 +288,27 @@
const ui = window.AlphabetSectionUi;
if ((alphabet === "hebrew" || (!alphabet && hebrewLetterId)) && hebrewLetterId) {
ui?.selectLetterByHebrewId?.(hebrewLetterId);
scheduleSectionDetailOnly("alphabet-letters");
return;
}
if (alphabet === "greek" && greekName) {
ui?.selectGreekLetterByName?.(greekName);
scheduleSectionDetailOnly("alphabet-letters");
return;
}
if (alphabet === "english" && englishLetter) {
ui?.selectEnglishLetter?.(englishLetter);
scheduleSectionDetailOnly("alphabet-letters");
return;
}
if (alphabet === "arabic" && arabicName) {
ui?.selectArabicLetter?.(arabicName);
scheduleSectionDetailOnly("alphabet-letters");
return;
}
if (alphabet === "enochian" && enochianId) {
ui?.selectEnochianLetter?.(enochianId);
scheduleSectionDetailOnly("alphabet-letters");
}
});
});
@@ -241,6 +327,7 @@
if (typeof config.selectNumberEntry === "function") {
config.selectNumberEntry(normalizedValue);
}
scheduleSectionDetailOnly("numbers");
});
});
@@ -259,10 +346,12 @@
const ui = window.IChingSectionUi;
if (hexagramNumber != null) {
ui?.selectByHexagramNumber?.(hexagramNumber);
scheduleSectionDetailOnly("iching");
return;
}
if (planetaryInfluence) {
ui?.selectByPlanetaryInfluence?.(planetaryInfluence);
scheduleSectionDetailOnly("iching");
}
});
});
@@ -284,6 +373,7 @@
if (!viaId && !viaName && pathNo != null) {
ui?.selectByPathNo?.(pathNo);
}
scheduleSectionDetailOnly("gods");
});
});
@@ -307,6 +397,7 @@
window.CalendarSectionUi?.selectCalendarType?.(calendarId);
}
window.CalendarSectionUi?.selectByMonthId?.(monthId);
scheduleSectionDetailOnly("calendar");
});
});
@@ -320,6 +411,7 @@
if (pathNo != null) {
requestAnimationFrame(() => {
window.KabbalahSectionUi?.selectNode?.(pathNo);
scheduleSectionDetailOnly("kabbalah-tree");
});
}
});
@@ -337,6 +429,7 @@
setActiveSection("planets");
requestAnimationFrame(() => {
window.PlanetSectionUi?.selectByPlanetId?.(planetId);
scheduleSectionDetailOnly("planets");
});
});
@@ -355,38 +448,27 @@
requestAnimationFrame(() => {
window.ElementsSectionUi?.selectByElementId?.(elementId);
scheduleSectionDetailOnly("elements");
});
});
document.addEventListener("nav:tarot-trump", (event) => {
const referenceData = getReferenceData();
const magickDataset = getMagickDataset();
if (typeof ensure.ensureTarotSection === "function" && referenceData) {
ensure.ensureTarotSection(referenceData, magickDataset);
}
setActiveSection("tarot");
document.addEventListener("nav:tarot-trump", async (event) => {
await prepareTarotBrowseDetailView();
const { trumpNumber, cardName } = event?.detail || {};
requestAnimationFrame(() => {
if (trumpNumber != null) {
window.TarotSectionUi?.selectCardByTrump?.(trumpNumber);
} else if (cardName) {
window.TarotSectionUi?.selectCardByName?.(cardName);
}
});
if (trumpNumber != null) {
window.TarotSectionUi?.selectCardByTrump?.(trumpNumber);
} else if (cardName) {
window.TarotSectionUi?.selectCardByName?.(cardName);
}
});
document.addEventListener("kab:view-trump", (event) => {
const referenceData = getReferenceData();
const magickDataset = getMagickDataset();
setActiveSection("tarot");
document.addEventListener("kab:view-trump", async (event) => {
await prepareTarotBrowseDetailView();
const trumpNumber = event?.detail?.trumpNumber;
if (trumpNumber != null) {
if (typeof ensure.ensureTarotSection === "function" && referenceData) {
ensure.ensureTarotSection(referenceData, magickDataset);
}
requestAnimationFrame(() => {
window.TarotSectionUi?.selectCardByTrump?.(trumpNumber);
});
window.TarotSectionUi?.selectCardByTrump?.(trumpNumber);
}
});
@@ -401,6 +483,7 @@
} else {
kabbalahUi?.selectPathByNumber?.(pathNumber);
}
scheduleSectionDetailOnly("kabbalah-tree");
});
}
});

View File

@@ -9,6 +9,7 @@
"audio-circle",
"audio-notes",
"tarot",
"tarot-frame",
"tarot-house",
"astronomy",
"planets",
@@ -94,8 +95,9 @@
const isAudioCircleOpen = activeSection === "audio-circle";
const isAudioMenuOpen = isAudioNotesOpen || isAudioCircleOpen;
const isTarotOpen = activeSection === "tarot";
const isTarotFrameOpen = activeSection === "tarot-frame";
const isTarotHouseOpen = activeSection === "tarot-house";
const isTarotMenuOpen = isTarotOpen || isTarotHouseOpen;
const isTarotMenuOpen = isTarotOpen || isTarotFrameOpen || isTarotHouseOpen;
const isAstronomyOpen = activeSection === "astronomy";
const isPlanetOpen = activeSection === "planets";
const isCyclesOpen = activeSection === "cycles";
@@ -123,6 +125,7 @@
setHidden(elements.audioCircleSectionEl, !isAudioCircleOpen);
setHidden(elements.audioNotesSectionEl, !isAudioNotesOpen);
setHidden(elements.tarotSectionEl, !isTarotOpen);
setHidden(elements.tarotFrameSectionEl, !isTarotFrameOpen);
setHidden(elements.tarotHouseSectionEl, !isTarotHouseOpen);
setHidden(elements.astronomySectionEl, !isAstronomyOpen);
setHidden(elements.planetSectionEl, !isPlanetOpen);
@@ -152,6 +155,7 @@
toggleActive(elements.openAudioCircleEl, isAudioCircleOpen);
toggleActive(elements.openAudioNotesEl, isAudioNotesOpen);
setPressed(elements.openTarotEl, isTarotMenuOpen);
toggleActive(elements.openTarotFrameEl, isTarotFrameOpen);
toggleActive(elements.openTarotHouseEl, isTarotHouseOpen);
config.tarotSpreadUi?.applyViewState?.();
setPressed(elements.openAstronomyEl, isAstronomyMenuOpen);
@@ -211,6 +215,11 @@
return;
}
if (isTarotFrameOpen) {
ensure.ensureTarotFrameSection?.(referenceData, magickDataset);
return;
}
if (isTarotHouseOpen) {
ensure.ensureTarotSection?.(referenceData, magickDataset);
return;

1223
app/ui-tarot-frame.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -58,6 +58,7 @@
getSelectedCardId: () => "",
getHouseTopCardsVisible: () => true,
getHouseTopInfoModes: () => ({}),
getMagickDataset: () => null,
getHouseBottomCardsVisible: () => true,
getHouseBottomInfoModes: () => ({})
};
@@ -326,12 +327,33 @@
const courtWindow = getFirstCardRelationByType(card, "courtDateWindow")?.data || null;
const decan = getFirstCardRelationByType(card, "decan")?.data || null;
const calendar = getFirstCardRelationByType(card, "calendarMonth")?.data || null;
const zodiac = getFirstCardRelationByType(card, "zodiacCorrespondence")?.data
|| getFirstCardRelationByType(card, "zodiac")?.data
|| null;
const primary = normalizeLabelText(courtWindow?.dateRange || decan?.dateRange || calendar?.dateRange || calendar?.name);
let zodiacDateRange = "";
if (zodiac?.signId || zodiac?.id || zodiac?.name) {
const zodiacId = normalizeLabelText(zodiac.signId || zodiac.id || zodiac.name).toLowerCase();
const sign = config.getMagickDataset?.()?.grouped?.astrology?.zodiac?.[zodiacId] || null;
const rulesFrom = Array.isArray(sign?.rulesFrom) ? sign.rulesFrom : [];
if (rulesFrom.length === 2) {
const [[startMonth, startDay], [endMonth, endDay]] = rulesFrom;
const monthLabels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const startLabel = Number.isFinite(Number(startMonth)) && Number.isFinite(Number(startDay))
? `${monthLabels[Math.max(0, Math.min(11, Number(startMonth) - 1))]} ${Number(startDay)}`
: "";
const endLabel = Number.isFinite(Number(endMonth)) && Number.isFinite(Number(endDay))
? `${monthLabels[Math.max(0, Math.min(11, Number(endMonth) - 1))]} ${Number(endDay)}`
: "";
zodiacDateRange = normalizeLabelText(startLabel && endLabel ? `${startLabel} - ${endLabel}` : startLabel || endLabel);
}
}
const primary = normalizeLabelText(courtWindow?.dateRange || decan?.dateRange || calendar?.dateRange || zodiacDateRange || calendar?.name);
const secondary = normalizeLabelText(
calendar?.name && primary !== calendar.name
? calendar.name
: decan?.signName
: decan?.signName || zodiac?.name
);
if (!primary) {
@@ -442,6 +464,10 @@
pushLine(buildPathNumberLabel(card)?.primary);
}
if (getTopInfoModeEnabled("date")) {
pushLine(buildDateLabel(card)?.primary);
}
if (!lines.length) {
return null;
}

View File

@@ -4,6 +4,8 @@
let overlayEl = null;
let backdropEl = null;
let toolbarEl = null;
let settingsButtonEl = null;
let settingsPanelEl = null;
let helpButtonEl = null;
let helpPanelEl = null;
let compareButtonEl = null;
@@ -79,6 +81,7 @@
onSelectCardId: null,
overlayOpacity: LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY,
zoomScale: LIGHTBOX_ZOOM_SCALE,
settingsMenuOpen: false,
helpOpen: false,
primaryRotated: false,
overlayRotated: false,
@@ -350,6 +353,27 @@
}
}
function closeSettingsMenu() {
lightboxState.settingsMenuOpen = false;
if (settingsPanelEl) {
settingsPanelEl.style.display = "none";
}
}
function toggleSettingsMenu() {
if (!lightboxState.isOpen || zoomed) {
return;
}
const nextOpen = !lightboxState.settingsMenuOpen;
lightboxState.settingsMenuOpen = nextOpen;
if (nextOpen) {
lightboxState.helpOpen = false;
closeDeckComparePanel();
}
applyComparePresentation();
}
function suppressDeckCompareToggle(durationMs = 400) {
suppressDeckCompareToggleUntil = Date.now() + Math.max(0, Number(durationMs) || 0);
}
@@ -417,6 +441,7 @@
function toggleDeckComparePanel() {
if (!lightboxState.allowDeckCompare) {
closeSettingsMenu();
lightboxState.deckComparePickerOpen = true;
lightboxState.deckCompareMessage = "Add another registered deck to use deck compare.";
applyComparePresentation();
@@ -426,6 +451,7 @@
if (lightboxState.deckComparePickerOpen) {
closeDeckComparePanel();
} else {
closeSettingsMenu();
lightboxState.deckComparePickerOpen = true;
}
lightboxState.deckCompareMessage = lightboxState.availableCompareDecks.length
@@ -798,13 +824,33 @@
&& mobileInfoPanelEl
&& mobileInfoPanelEl.style.display !== "none"
);
const settingsPanelVisible = Boolean(
lightboxState.settingsMenuOpen
&& settingsPanelEl
&& settingsPanelEl.style.display !== "none"
);
const helpPanelVisible = Boolean(
lightboxState.helpOpen
&& helpPanelEl
&& helpPanelEl.style.display !== "none"
);
const deckPickerVisible = Boolean(
lightboxState.deckComparePickerOpen
&& deckComparePanelEl
&& deckComparePanelEl.style.display !== "none"
);
const toolbarHeight = toolbarEl instanceof HTMLElement && toolbarEl.style.display !== "none"
? toolbarEl.offsetHeight
: 0;
const infoPanelHeight = mobileInfoPanelVisible && mobileInfoPanelEl instanceof HTMLElement
? mobileInfoPanelEl.offsetHeight
: 0;
const bottomOffset = toolbarHeight + (mobileInfoPanelVisible ? infoPanelHeight + 32 : 24);
const floatingPanelHeight = Math.max(
settingsPanelVisible && settingsPanelEl instanceof HTMLElement ? settingsPanelEl.offsetHeight + 12 : 0,
helpPanelVisible && helpPanelEl instanceof HTMLElement ? helpPanelEl.offsetHeight + 12 : 0,
deckPickerVisible && deckComparePanelEl instanceof HTMLElement ? deckComparePanelEl.offsetHeight + 12 : 0
);
const bottomOffset = toolbarHeight + floatingPanelHeight + (mobileInfoPanelVisible ? infoPanelHeight + 32 : 24);
mobilePrevButtonEl.style.top = "auto";
mobileNextButtonEl.style.top = "auto";
@@ -902,6 +948,19 @@
setOverlayOpacity(lightboxState.overlayOpacity);
}
function syncSettingsUi() {
if (!settingsButtonEl || !settingsPanelEl) {
return;
}
const canShow = lightboxState.isOpen && !zoomed;
settingsButtonEl.style.display = canShow ? "inline-flex" : "none";
settingsButtonEl.textContent = lightboxState.settingsMenuOpen ? "Hide Settings" : "Settings";
settingsButtonEl.setAttribute("aria-expanded", canShow && lightboxState.settingsMenuOpen ? "true" : "false");
settingsPanelEl.style.display = canShow && lightboxState.settingsMenuOpen ? "flex" : "none";
settingsPanelEl.style.pointerEvents = canShow && lightboxState.settingsMenuOpen ? "auto" : "none";
}
function syncDeckComparePicker() {
if (!deckCompareButtonEl || !deckComparePanelEl || !deckCompareMessageEl || !deckCompareDeckListEl) {
return;
@@ -1077,21 +1136,6 @@
return;
}
const isCompact = isCompactLightboxLayout();
if (isCompact) {
if (helpButtonEl.parentElement !== toolbarEl) {
toolbarEl.insertBefore(helpButtonEl, zoomControlEl || null);
}
helpButtonEl.style.position = "static";
helpButtonEl.style.zIndex = "auto";
} else {
if (helpButtonEl.parentElement !== overlayEl) {
overlayEl.appendChild(helpButtonEl);
}
helpButtonEl.style.position = "fixed";
helpButtonEl.style.zIndex = "2";
}
const canShow = lightboxState.isOpen && !zoomed;
helpButtonEl.style.display = canShow ? "inline-flex" : "none";
helpPanelEl.style.display = canShow && lightboxState.helpOpen ? "flex" : "none";
@@ -1121,18 +1165,22 @@
const isCompact = isCompactLightboxLayout();
if (!isCompact) {
helpButtonEl.style.right = "auto";
helpButtonEl.style.top = "24px";
helpButtonEl.style.left = "24px";
settingsPanelEl.style.top = "72px";
settingsPanelEl.style.right = "24px";
settingsPanelEl.style.bottom = "auto";
settingsPanelEl.style.left = "auto";
settingsPanelEl.style.width = "min(320px, calc(100vw - 48px))";
settingsPanelEl.style.maxHeight = "none";
settingsPanelEl.style.overflowY = "visible";
helpPanelEl.style.top = "72px";
helpPanelEl.style.right = "auto";
helpPanelEl.style.right = "24px";
helpPanelEl.style.bottom = "auto";
helpPanelEl.style.left = "24px";
helpPanelEl.style.left = "auto";
helpPanelEl.style.width = "min(320px, calc(100vw - 48px))";
helpPanelEl.style.maxHeight = "none";
helpPanelEl.style.overflowY = "visible";
deckComparePanelEl.style.top = "24px";
deckComparePanelEl.style.right = "176px";
deckComparePanelEl.style.top = "72px";
deckComparePanelEl.style.right = "24px";
deckComparePanelEl.style.bottom = "auto";
deckComparePanelEl.style.left = "auto";
deckComparePanelEl.style.width = "min(280px, calc(100vw - 48px))";
@@ -1145,6 +1193,7 @@
|| !lightboxState.allowOverlayCompare
|| (!isCompact && lightboxState.compareMode && !hasSecondaryCard());
compareButtonEl.textContent = lightboxState.compareMode ? "Done Overlay" : "Overlay";
syncSettingsUi();
syncHelpUi();
syncZoomControl();
syncOpacityControl();
@@ -1205,10 +1254,13 @@
toolbarEl.style.flexWrap = "wrap";
toolbarEl.style.alignItems = "center";
toolbarEl.style.justifyContent = "center";
helpButtonEl.style.top = "auto";
helpButtonEl.style.right = "auto";
helpButtonEl.style.bottom = "auto";
helpButtonEl.style.left = "auto";
settingsPanelEl.style.top = "auto";
settingsPanelEl.style.right = "12px";
settingsPanelEl.style.bottom = "calc(72px + env(safe-area-inset-bottom, 0px))";
settingsPanelEl.style.left = "12px";
settingsPanelEl.style.width = "auto";
settingsPanelEl.style.maxHeight = "min(56svh, 440px)";
settingsPanelEl.style.overflowY = "auto";
helpPanelEl.style.top = "auto";
helpPanelEl.style.right = "12px";
helpPanelEl.style.bottom = "calc(72px + env(safe-area-inset-bottom, 0px))";
@@ -1540,15 +1592,28 @@
overlayEl.style.pointerEvents = "none";
overlayEl.style.overscrollBehavior = "contain";
settingsButtonEl = document.createElement("button");
settingsButtonEl.type = "button";
settingsButtonEl.textContent = "Settings";
settingsButtonEl.style.display = "none";
settingsButtonEl.style.alignItems = "center";
settingsButtonEl.style.justifyContent = "center";
settingsButtonEl.style.border = "1px solid rgba(255, 255, 255, 0.2)";
settingsButtonEl.style.background = "rgba(15, 23, 42, 0.84)";
settingsButtonEl.style.color = "#f8fafc";
settingsButtonEl.style.borderRadius = "999px";
settingsButtonEl.style.padding = "10px 14px";
settingsButtonEl.style.font = "600 13px/1.1 sans-serif";
settingsButtonEl.style.cursor = "pointer";
settingsButtonEl.style.backdropFilter = "blur(12px)";
helpButtonEl = document.createElement("button");
helpButtonEl.type = "button";
helpButtonEl.textContent = "Help";
helpButtonEl.style.position = "fixed";
helpButtonEl.style.top = "24px";
helpButtonEl.style.left = "24px";
helpButtonEl.style.display = "none";
helpButtonEl.style.alignItems = "center";
helpButtonEl.style.justifyContent = "center";
helpButtonEl.style.width = "100%";
helpButtonEl.style.border = "1px solid rgba(255, 255, 255, 0.2)";
helpButtonEl.style.background = "rgba(15, 23, 42, 0.84)";
helpButtonEl.style.color = "#f8fafc";
@@ -1557,8 +1622,28 @@
helpButtonEl.style.font = "600 13px/1.1 sans-serif";
helpButtonEl.style.cursor = "pointer";
helpButtonEl.style.backdropFilter = "blur(12px)";
helpButtonEl.style.pointerEvents = "auto";
helpButtonEl.style.zIndex = "2";
settingsPanelEl = document.createElement("div");
settingsPanelEl.style.position = "fixed";
settingsPanelEl.style.top = "72px";
settingsPanelEl.style.right = "24px";
settingsPanelEl.style.display = "none";
settingsPanelEl.style.flexDirection = "column";
settingsPanelEl.style.gap = "10px";
settingsPanelEl.style.width = "min(320px, calc(100vw - 48px))";
settingsPanelEl.style.padding = "14px 16px";
settingsPanelEl.style.borderRadius = "18px";
settingsPanelEl.style.background = "rgba(2, 6, 23, 0.88)";
settingsPanelEl.style.border = "1px solid rgba(148, 163, 184, 0.16)";
settingsPanelEl.style.color = "#f8fafc";
settingsPanelEl.style.boxShadow = "0 16px 42px rgba(0, 0, 0, 0.34)";
settingsPanelEl.style.backdropFilter = "blur(12px)";
settingsPanelEl.style.pointerEvents = "auto";
settingsPanelEl.style.zIndex = "3";
const settingsTitleEl = document.createElement("div");
settingsTitleEl.textContent = "Lightbox Settings";
settingsTitleEl.style.font = "700 13px/1.3 sans-serif";
helpPanelEl = document.createElement("div");
helpPanelEl.style.position = "fixed";
@@ -1640,6 +1725,10 @@
compareButtonEl.style.font = "600 13px/1.1 sans-serif";
compareButtonEl.style.cursor = "pointer";
compareButtonEl.style.backdropFilter = "blur(12px)";
compareButtonEl.style.display = "inline-flex";
compareButtonEl.style.alignItems = "center";
compareButtonEl.style.justifyContent = "center";
compareButtonEl.style.width = "100%";
deckCompareButtonEl = document.createElement("button");
deckCompareButtonEl.type = "button";
@@ -1652,10 +1741,15 @@
deckCompareButtonEl.style.font = "600 13px/1.1 sans-serif";
deckCompareButtonEl.style.cursor = "pointer";
deckCompareButtonEl.style.backdropFilter = "blur(12px)";
deckCompareButtonEl.style.alignItems = "center";
deckCompareButtonEl.style.justifyContent = "center";
deckCompareButtonEl.style.width = "100%";
zoomControlEl = document.createElement("label");
zoomControlEl.style.display = "flex";
zoomControlEl.style.alignItems = "center";
zoomControlEl.style.justifyContent = "space-between";
zoomControlEl.style.width = "100%";
zoomControlEl.style.gap = "8px";
zoomControlEl.style.padding = "10px 14px";
zoomControlEl.style.border = "1px solid rgba(255, 255, 255, 0.2)";
@@ -1687,6 +1781,8 @@
opacityControlEl = document.createElement("label");
opacityControlEl.style.display = "none";
opacityControlEl.style.alignItems = "center";
opacityControlEl.style.justifyContent = "space-between";
opacityControlEl.style.width = "100%";
opacityControlEl.style.gap = "8px";
opacityControlEl.style.padding = "10px 14px";
opacityControlEl.style.border = "1px solid rgba(255, 255, 255, 0.2)";
@@ -1780,6 +1876,9 @@
mobileInfoButtonEl.style.font = "600 13px/1.1 sans-serif";
mobileInfoButtonEl.style.cursor = "pointer";
mobileInfoButtonEl.style.backdropFilter = "blur(12px)";
mobileInfoButtonEl.style.alignItems = "center";
mobileInfoButtonEl.style.justifyContent = "center";
mobileInfoButtonEl.style.width = "100%";
mobileInfoPrimaryTabEl = document.createElement("button");
mobileInfoPrimaryTabEl.type = "button";
@@ -1793,6 +1892,9 @@
mobileInfoPrimaryTabEl.style.font = "600 13px/1.1 sans-serif";
mobileInfoPrimaryTabEl.style.cursor = "pointer";
mobileInfoPrimaryTabEl.style.backdropFilter = "blur(12px)";
mobileInfoPrimaryTabEl.style.alignItems = "center";
mobileInfoPrimaryTabEl.style.justifyContent = "center";
mobileInfoPrimaryTabEl.style.width = "100%";
mobileInfoSecondaryTabEl = document.createElement("button");
mobileInfoSecondaryTabEl.type = "button";
@@ -1806,16 +1908,22 @@
mobileInfoSecondaryTabEl.style.font = "600 13px/1.1 sans-serif";
mobileInfoSecondaryTabEl.style.cursor = "pointer";
mobileInfoSecondaryTabEl.style.backdropFilter = "blur(12px)";
mobileInfoSecondaryTabEl.style.alignItems = "center";
mobileInfoSecondaryTabEl.style.justifyContent = "center";
mobileInfoSecondaryTabEl.style.width = "100%";
toolbarEl.append(
settingsPanelEl.append(
settingsTitleEl,
compareButtonEl,
deckCompareButtonEl,
mobileInfoButtonEl,
mobileInfoPrimaryTabEl,
mobileInfoSecondaryTabEl,
helpButtonEl,
zoomControlEl,
opacityControlEl
);
toolbarEl.append(settingsButtonEl);
stageEl = document.createElement("div");
stageEl.style.position = "fixed";
@@ -2106,7 +2214,7 @@
overlayLayerEl.appendChild(overlayImageEl);
frameEl.append(baseLayerEl, overlayLayerEl, mobileInfoPanelEl);
stageEl.append(frameEl, compareGridEl, primaryInfoEl, secondaryInfoEl);
overlayEl.append(backdropEl, stageEl, toolbarEl, deckComparePanelEl, helpButtonEl, helpPanelEl, mobilePrevButtonEl, mobileNextButtonEl);
overlayEl.append(backdropEl, stageEl, toolbarEl, settingsPanelEl, deckComparePanelEl, helpPanelEl, mobilePrevButtonEl, mobileNextButtonEl);
const close = () => {
if (!overlayEl || !imageEl || !overlayImageEl) {
@@ -2134,6 +2242,7 @@
lightboxState.onSelectCardId = null;
lightboxState.overlayOpacity = LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY;
lightboxState.zoomScale = LIGHTBOX_ZOOM_SCALE;
lightboxState.settingsMenuOpen = false;
lightboxState.helpOpen = false;
lightboxState.primaryRotated = false;
lightboxState.overlayRotated = false;
@@ -2169,8 +2278,6 @@
lightboxState.compareMode = !lightboxState.compareMode;
if (!lightboxState.compareMode) {
clearSecondaryCard();
} else if (isCompactLightboxLayout()) {
lightboxState.mobileInfoOpen = true;
}
applyComparePresentation();
}
@@ -2311,9 +2418,24 @@
backdropEl.addEventListener("click", close);
helpButtonEl.addEventListener("click", () => {
lightboxState.helpOpen = !lightboxState.helpOpen;
if (lightboxState.helpOpen) {
closeSettingsMenu();
}
syncHelpUi();
restoreLightboxFocus();
});
settingsButtonEl.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
toggleSettingsMenu();
restoreLightboxFocus();
});
settingsPanelEl.addEventListener("pointerdown", (event) => {
event.stopPropagation();
});
settingsPanelEl.addEventListener("click", (event) => {
event.stopPropagation();
});
compareButtonEl.addEventListener("click", () => {
toggleCompareMode();
restoreLightboxFocus();
@@ -2646,10 +2768,11 @@
: null;
lightboxState.overlayOpacity = LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY;
lightboxState.zoomScale = LIGHTBOX_ZOOM_SCALE;
lightboxState.settingsMenuOpen = false;
lightboxState.helpOpen = false;
lightboxState.primaryRotated = false;
lightboxState.overlayRotated = false;
lightboxState.mobileInfoOpen = isCompactLightboxLayout();
lightboxState.mobileInfoOpen = false;
lightboxState.mobileInfoView = "primary";
imageEl.src = normalizedPrimary.src;

View File

@@ -27,7 +27,8 @@
planet: true,
zodiac: true,
trump: true,
path: true
path: true,
date: false
},
houseBottomCardsVisible: true,
houseBottomInfoModes: {
@@ -39,6 +40,7 @@
},
houseExportInProgress: false,
houseExportFormat: "png",
houseSettingsOpen: false,
magickDataset: null,
referenceData: null,
monthRefsByCardId: new Map(),
@@ -281,12 +283,15 @@
tarotHouseOfCardsEl: document.getElementById("tarot-house-of-cards"),
tarotBrowseViewEl: document.getElementById("tarot-browse-view"),
tarotHouseViewEl: document.getElementById("tarot-house-view"),
tarotHouseSettingsToggleEl: document.getElementById("tarot-house-settings-toggle"),
tarotHouseSettingsPanelEl: document.getElementById("tarot-house-settings-panel"),
tarotHouseTopCardsVisibleEl: document.getElementById("tarot-house-top-cards-visible"),
tarotHouseTopInfoHebrewEl: document.getElementById("tarot-house-top-info-hebrew"),
tarotHouseTopInfoPlanetEl: document.getElementById("tarot-house-top-info-planet"),
tarotHouseTopInfoZodiacEl: document.getElementById("tarot-house-top-info-zodiac"),
tarotHouseTopInfoTrumpEl: document.getElementById("tarot-house-top-info-trump"),
tarotHouseTopInfoPathEl: document.getElementById("tarot-house-top-info-path"),
tarotHouseTopInfoDateEl: document.getElementById("tarot-house-top-info-date"),
tarotHouseBottomCardsVisibleEl: document.getElementById("tarot-house-bottom-cards-visible"),
tarotHouseBottomInfoZodiacEl: document.getElementById("tarot-house-bottom-info-zodiac"),
tarotHouseBottomInfoDecanEl: document.getElementById("tarot-house-bottom-info-decan"),
@@ -544,6 +549,15 @@
elements.tarotHouseViewEl.classList.toggle("is-house-focus", Boolean(state.houseFocusMode));
}
if (elements?.tarotHouseSettingsToggleEl) {
elements.tarotHouseSettingsToggleEl.setAttribute("aria-expanded", state.houseSettingsOpen ? "true" : "false");
elements.tarotHouseSettingsToggleEl.textContent = state.houseSettingsOpen ? "Hide Settings" : "Settings";
}
if (elements?.tarotHouseSettingsPanelEl) {
elements.tarotHouseSettingsPanelEl.hidden = !state.houseSettingsOpen;
}
if (elements?.tarotHouseTopCardsVisibleEl) {
elements.tarotHouseTopCardsVisibleEl.checked = Boolean(state.houseTopCardsVisible);
elements.tarotHouseTopCardsVisibleEl.disabled = Boolean(state.houseExportInProgress);
@@ -554,6 +568,7 @@
setHouseBottomInfoCheckboxState(elements?.tarotHouseTopInfoZodiacEl, state.houseTopInfoModes.zodiac);
setHouseBottomInfoCheckboxState(elements?.tarotHouseTopInfoTrumpEl, state.houseTopInfoModes.trump);
setHouseBottomInfoCheckboxState(elements?.tarotHouseTopInfoPathEl, state.houseTopInfoModes.path);
setHouseBottomInfoCheckboxState(elements?.tarotHouseTopInfoDateEl, state.houseTopInfoModes.date);
if (elements?.tarotHouseBottomCardsVisibleEl) {
elements.tarotHouseBottomCardsVisibleEl.checked = Boolean(state.houseBottomCardsVisible);
@@ -839,6 +854,7 @@
getSelectedCardId: () => state.selectedCardId,
getHouseTopCardsVisible: () => state.houseTopCardsVisible,
getHouseTopInfoModes: () => ({ ...state.houseTopInfoModes }),
getMagickDataset: () => state.magickDataset,
getHouseBottomCardsVisible: () => state.houseBottomCardsVisible,
getHouseBottomInfoModes: () => ({ ...state.houseBottomInfoModes })
});
@@ -929,12 +945,47 @@
});
}
if (elements.tarotHouseSettingsToggleEl) {
elements.tarotHouseSettingsToggleEl.addEventListener("click", (event) => {
event.stopPropagation();
state.houseSettingsOpen = !state.houseSettingsOpen;
syncHouseControls(elements);
});
}
if (elements.tarotHouseSettingsPanelEl) {
elements.tarotHouseSettingsPanelEl.addEventListener("click", (event) => {
event.stopPropagation();
});
}
document.addEventListener("click", (event) => {
if (!state.houseSettingsOpen) {
return;
}
const target = event.target;
if (!(target instanceof Node)) {
return;
}
const settingsPanelEl = elements.tarotHouseSettingsPanelEl;
const settingsToggleEl = elements.tarotHouseSettingsToggleEl;
if (settingsPanelEl?.contains(target) || settingsToggleEl?.contains(target)) {
return;
}
state.houseSettingsOpen = false;
syncHouseControls(elements);
});
[
[elements.tarotHouseTopInfoHebrewEl, "hebrew"],
[elements.tarotHouseTopInfoPlanetEl, "planet"],
[elements.tarotHouseTopInfoZodiacEl, "zodiac"],
[elements.tarotHouseTopInfoTrumpEl, "trump"],
[elements.tarotHouseTopInfoPathEl, "path"]
[elements.tarotHouseTopInfoPathEl, "path"],
[elements.tarotHouseTopInfoDateEl, "date"]
].forEach(([checkbox, key]) => {
if (!checkbox) {
return;

View File

@@ -16,12 +16,13 @@
<link rel="stylesheet" href="node_modules/@fontsource/amiri/arabic-400.css">
<link rel="stylesheet" href="node_modules/@fontsource/amiri/arabic-700.css">
<link rel="stylesheet" href="node_modules/@fontsource/noto-naskh-arabic/arabic-400.css">
<link rel="stylesheet" href="app/styles.css?v=20260328-house-mobile-01">
<link rel="stylesheet" href="app/styles.css?v=20260401-tarot-frame-09">
</head>
<body>
<div class="topbar">
<button id="open-home" class="topbar-home-button" type="button" aria-pressed="true">Tarot Time!</button>
<button id="topbar-menu-toggle" class="topbar-menu-toggle" type="button" aria-expanded="false" aria-controls="topbar-actions" aria-label="Open navigation menu">Menu</button>
<button id="open-settings" class="topbar-menu-toggle topbar-settings-toggle" type="button" aria-haspopup="dialog" aria-expanded="false">Settings</button>
<div id="topbar-actions" class="topbar-actions">
<div class="topbar-dropdown" aria-label="Alphabet menu">
<button id="open-alphabet" class="settings-trigger" type="button" aria-pressed="false" aria-haspopup="menu" aria-controls="alphabet-subpages" aria-expanded="false">Alphabet ▾</button>
@@ -68,12 +69,12 @@
</div>
<button id="open-numbers" class="settings-trigger" type="button" aria-pressed="false">Numbers</button>
<button id="open-quiz" class="settings-trigger" type="button" aria-pressed="false">Quiz</button>
<button id="open-settings" class="settings-trigger" type="button" aria-haspopup="dialog" aria-expanded="false">Settings</button>
<div class="topbar-dropdown" aria-label="Tarot menu">
<button id="open-tarot" class="settings-trigger" type="button" aria-pressed="false" aria-haspopup="menu" aria-controls="tarot-subpages" aria-expanded="false">Tarot ▾</button>
<div id="tarot-subpages" class="topbar-dropdown-menu" role="menu" aria-label="Tarot subpages">
<button id="open-tarot-cards" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Cards</button>
<button id="open-tarot-spread" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Draw Spread</button>
<button id="open-tarot-frame" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Frame</button>
<button id="open-tarot-house" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">House</button>
</div>
</div>
@@ -307,6 +308,31 @@
<div id="tarot-spread-board" class="tarot-spread-board" aria-live="polite"></div>
</div>
</section>
<section id="tarot-frame-section" hidden>
<div id="tarot-frame-view" class="tarot-frame-view">
<div class="tarot-frame-shell">
<div class="tarot-frame-header">
<div>
<h2 class="tarot-frame-title">Tarot Frame</h2>
<p class="tarot-frame-copy">Arrange all 78 tarot cards inside one master 18x18 grid. The extra cards sit across the top row, and every square stays available for custom layouts.</p>
</div>
<div class="tarot-frame-actions">
<button id="tarot-frame-settings-toggle" class="tarot-frame-action-btn tarot-frame-settings-toggle" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="tarot-frame-settings-panel">Settings</button>
<button id="tarot-frame-reset" class="tarot-frame-action-btn" type="button">Reset Layout</button>
<div id="tarot-frame-settings-panel" class="tarot-frame-settings-panel" role="dialog" aria-label="Tarot Frame settings" hidden>
<label class="tarot-frame-toggle" for="tarot-frame-show-info">
<input id="tarot-frame-show-info" type="checkbox" checked>
<span>Display Info</span>
</label>
<button id="tarot-frame-export-webp" class="tarot-frame-action-btn tarot-frame-export-btn" type="button">Export WebP</button>
</div>
</div>
</div>
<div id="tarot-frame-status" class="tarot-frame-status" aria-live="polite">Loading tarot cards...</div>
<div id="tarot-frame-board" class="tarot-frame-board-grid"></div>
</div>
</div>
</section>
<section id="tarot-house-section" hidden>
<div id="tarot-house-view" class="tarot-house-view">
<section class="tarot-misc-section tarot-section-house-top tarot-section-house-page" aria-label="Tarot house of cards">
@@ -314,6 +340,9 @@
<div class="tarot-house-card-head">
<strong>House of Cards</strong>
<div class="tarot-house-card-actions">
<button id="tarot-house-settings-toggle" class="tarot-house-action-btn tarot-house-settings-toggle" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="tarot-house-settings-panel">Settings</button>
</div>
<div id="tarot-house-settings-panel" class="tarot-house-settings-panel" role="dialog" aria-label="House of Cards settings" hidden>
<label class="tarot-house-toggle" for="tarot-house-top-cards-visible">
<input id="tarot-house-top-cards-visible" type="checkbox" checked>
<span>Show Top Cards</span>
@@ -340,6 +369,10 @@
<input id="tarot-house-top-info-path" type="checkbox" checked>
<span>Path</span>
</label>
<label class="tarot-house-mini-toggle" for="tarot-house-top-info-date">
<input id="tarot-house-top-info-date" type="checkbox">
<span>Date</span>
</label>
</fieldset>
<label class="tarot-house-toggle" for="tarot-house-bottom-cards-visible">
<input id="tarot-house-bottom-cards-visible" type="checkbox" checked>
@@ -1037,8 +1070,8 @@
<script src="app/data-service.js?v=20260319-word-dictionary-01"></script>
<script src="app/calendar-events.js"></script>
<script src="app/card-images.js?v=20260309-gate"></script>
<script src="app/ui-tarot-lightbox.js?v=20260328-mobile-compare-02"></script>
<script src="app/ui-tarot-house.js?v=20260328-house-mobile-01"></script>
<script src="app/ui-tarot-lightbox.js?v=20260328-lightbox-settings-01"></script>
<script src="app/ui-tarot-house.js?v=20260401-house-top-date-01"></script>
<script src="app/ui-tarot-relations.js"></script>
<script src="app/ui-now-helpers.js?v=20260314-now-planets-grid-01"></script>
<script src="app/ui-now.js?v=20260314-now-planets-grid-01"></script>
@@ -1057,7 +1090,7 @@
<script src="app/ui-tarot-card-derivations.js?v=20260307b"></script>
<script src="app/ui-tarot-detail.js?v=20260307b"></script>
<script src="app/ui-tarot-relation-display.js?v=20260307b"></script>
<script src="app/ui-tarot.js?v=20260328-house-mobile-01"></script>
<script src="app/ui-tarot.js?v=20260401-house-top-date-01"></script>
<script src="app/ui-planets-references.js"></script>
<script src="app/ui-planets.js"></script>
<script src="app/ui-cycles.js"></script>
@@ -1097,14 +1130,16 @@
<script src="app/ui-numbers-detail.js"></script>
<script src="app/ui-numbers.js"></script>
<script src="app/ui-tarot-spread.js"></script>
<script src="app/ui-tarot-frame.js?v=20260401-tarot-frame-09"></script>
<script src="app/ui-settings.js?v=20260309-gate"></script>
<script src="app/ui-chrome.js?v=20260314-topbar-panel-toggle-02"></script>
<script src="app/ui-navigation.js?v=20260314-audio-circle-01"></script>
<script src="app/ui-chrome.js?v=20260328-topbar-settings-01"></script>
<script src="app/ui-navigation.js?v=20260401-tarot-frame-01"></script>
<script src="app/ui-calendar-formatting.js?v=20260307b"></script>
<script src="app/ui-calendar-visuals.js?v=20260307b"></script>
<script src="app/ui-home-calendar.js"></script>
<script src="app/ui-section-state.js?v=20260314-audio-circle-01"></script>
<script src="app/ui-section-state.js?v=20260401-tarot-frame-01"></script>
<script src="app/app-runtime.js?v=20260309-gate"></script>
<script src="app.js?v=20260314-audio-circle-01"></script>
<script src="app.js?v=20260401-tarot-frame-01"></script>
<script src="app/navigation-detail-test-harness.js?v=20260401-universal-detail-02"></script>
</body>
</html>