update ui webp export
This commit is contained in:
5
app.js
5
app.js
@@ -36,6 +36,7 @@ const timelineSectionEl = document.getElementById("timeline-section");
|
|||||||
const calendarSectionEl = document.getElementById("calendar-section");
|
const calendarSectionEl = document.getElementById("calendar-section");
|
||||||
const holidaySectionEl = document.getElementById("holiday-section");
|
const holidaySectionEl = document.getElementById("holiday-section");
|
||||||
const tarotSectionEl = document.getElementById("tarot-section");
|
const tarotSectionEl = document.getElementById("tarot-section");
|
||||||
|
const tarotHouseSectionEl = document.getElementById("tarot-house-section");
|
||||||
const astronomySectionEl = document.getElementById("astronomy-section");
|
const astronomySectionEl = document.getElementById("astronomy-section");
|
||||||
const natalSectionEl = document.getElementById("natal-section");
|
const natalSectionEl = document.getElementById("natal-section");
|
||||||
const planetSectionEl = document.getElementById("planet-section");
|
const planetSectionEl = document.getElementById("planet-section");
|
||||||
@@ -59,6 +60,7 @@ const openCalendarTimelineEl = document.getElementById("open-calendar-timeline")
|
|||||||
const openCalendarMonthsEl = document.getElementById("open-calendar-months");
|
const openCalendarMonthsEl = document.getElementById("open-calendar-months");
|
||||||
const openHolidaysEl = document.getElementById("open-holidays");
|
const openHolidaysEl = document.getElementById("open-holidays");
|
||||||
const openTarotEl = document.getElementById("open-tarot");
|
const openTarotEl = document.getElementById("open-tarot");
|
||||||
|
const openTarotHouseEl = document.getElementById("open-tarot-house");
|
||||||
const openAstronomyEl = document.getElementById("open-astronomy");
|
const openAstronomyEl = document.getElementById("open-astronomy");
|
||||||
const openPlanetsEl = document.getElementById("open-planets");
|
const openPlanetsEl = document.getElementById("open-planets");
|
||||||
const openCyclesEl = document.getElementById("open-cycles");
|
const openCyclesEl = document.getElementById("open-cycles");
|
||||||
@@ -399,6 +401,7 @@ sectionStateUi.init?.({
|
|||||||
calendarSectionEl,
|
calendarSectionEl,
|
||||||
holidaySectionEl,
|
holidaySectionEl,
|
||||||
tarotSectionEl,
|
tarotSectionEl,
|
||||||
|
tarotHouseSectionEl,
|
||||||
astronomySectionEl,
|
astronomySectionEl,
|
||||||
natalSectionEl,
|
natalSectionEl,
|
||||||
planetSectionEl,
|
planetSectionEl,
|
||||||
@@ -422,6 +425,7 @@ sectionStateUi.init?.({
|
|||||||
openCalendarMonthsEl,
|
openCalendarMonthsEl,
|
||||||
openHolidaysEl,
|
openHolidaysEl,
|
||||||
openTarotEl,
|
openTarotEl,
|
||||||
|
openTarotHouseEl,
|
||||||
openAstronomyEl,
|
openAstronomyEl,
|
||||||
openPlanetsEl,
|
openPlanetsEl,
|
||||||
openCyclesEl,
|
openCyclesEl,
|
||||||
@@ -517,6 +521,7 @@ navigationUi.init?.({
|
|||||||
openCalendarMonthsEl,
|
openCalendarMonthsEl,
|
||||||
openHolidaysEl,
|
openHolidaysEl,
|
||||||
openTarotEl,
|
openTarotEl,
|
||||||
|
openTarotHouseEl,
|
||||||
openAstronomyEl,
|
openAstronomyEl,
|
||||||
openPlanetsEl,
|
openPlanetsEl,
|
||||||
openCyclesEl,
|
openCyclesEl,
|
||||||
|
|||||||
207
app/styles.css
207
app/styles.css
@@ -18,6 +18,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
border-bottom: 1px solid #27272a;
|
border-bottom: 1px solid #27272a;
|
||||||
background: #18181b;
|
background: #18181b;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -43,7 +44,9 @@
|
|||||||
color: #fbbf24;
|
color: #fbbf24;
|
||||||
}
|
}
|
||||||
.topbar-menu-toggle {
|
.topbar-menu-toggle {
|
||||||
display: none;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
padding: 7px 12px;
|
padding: 7px 12px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: 1px solid #3f3f46;
|
border: 1px solid #3f3f46;
|
||||||
@@ -59,55 +62,53 @@
|
|||||||
background: #3f3f46;
|
background: #3f3f46;
|
||||||
}
|
}
|
||||||
.topbar-actions {
|
.topbar-actions {
|
||||||
display: flex;
|
display: none;
|
||||||
align-items: center;
|
flex: 1 0 100%;
|
||||||
gap: 8px;
|
width: 100%;
|
||||||
flex: 1 1 auto;
|
padding: 12px;
|
||||||
min-width: 0;
|
margin: 4px 0 0;
|
||||||
justify-content: flex-start;
|
border: 1px solid #2f2f39;
|
||||||
overflow-x: auto;
|
border-radius: 16px;
|
||||||
overflow-y: hidden;
|
background: linear-gradient(180deg, rgba(24, 24, 34, 0.98), rgba(12, 12, 18, 0.98));
|
||||||
padding-bottom: 132px;
|
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.34);
|
||||||
margin-bottom: -130px;
|
gap: 10px;
|
||||||
pointer-events: none;
|
max-height: calc(100svh - 88px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.topbar.is-menu-open .topbar-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
.topbar-actions > * {
|
.topbar-actions > * {
|
||||||
pointer-events: auto;
|
width: 100%;
|
||||||
}
|
|
||||||
.topbar-actions::-webkit-scrollbar {
|
|
||||||
height: 6px;
|
|
||||||
}
|
}
|
||||||
.topbar-dropdown {
|
.topbar-dropdown {
|
||||||
position: relative;
|
display: grid;
|
||||||
display: inline-flex;
|
width: 100%;
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
.topbar-dropdown-menu {
|
.topbar-dropdown-menu {
|
||||||
position: absolute;
|
position: static;
|
||||||
top: 100%;
|
min-width: 0;
|
||||||
left: 0;
|
width: 100%;
|
||||||
min-width: 140px;
|
|
||||||
display: none;
|
display: none;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border-radius: 10px;
|
margin-top: 6px;
|
||||||
border: 1px solid #3f3f46;
|
border-radius: 12px;
|
||||||
background: #18181b;
|
border: 1px solid #31313d;
|
||||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3);
|
background: rgba(10, 10, 16, 0.92);
|
||||||
|
box-shadow: none;
|
||||||
z-index: 40;
|
z-index: 40;
|
||||||
}
|
}
|
||||||
.topbar-dropdown.is-open .topbar-dropdown-menu {
|
.topbar-dropdown.is-open .topbar-dropdown-menu {
|
||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
.topbar-dropdown:hover .topbar-dropdown-menu,
|
|
||||||
.topbar-dropdown:focus-within .topbar-dropdown-menu {
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
.topbar-sub-trigger {
|
.topbar-sub-trigger {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
min-height: 40px;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
}
|
}
|
||||||
.topbar-sub-trigger.is-active {
|
.topbar-sub-trigger.is-active {
|
||||||
@@ -115,6 +116,12 @@
|
|||||||
border-color: #52525b;
|
border-color: #52525b;
|
||||||
}
|
}
|
||||||
.settings-trigger {
|
.settings-trigger {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 42px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
text-align: left;
|
||||||
padding: 7px 12px;
|
padding: 7px 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid #3f3f46;
|
border: 1px solid #3f3f46;
|
||||||
@@ -146,70 +153,15 @@
|
|||||||
min-height: 38px;
|
min-height: 38px;
|
||||||
}
|
}
|
||||||
.topbar-menu-toggle {
|
.topbar-menu-toggle {
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 38px;
|
min-height: 38px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
.topbar-actions {
|
.topbar-actions {
|
||||||
display: none;
|
|
||||||
flex: 1 0 100%;
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
margin: 4px 0 0;
|
|
||||||
border: 1px solid #2f2f39;
|
|
||||||
border-radius: 16px;
|
|
||||||
background: linear-gradient(180deg, rgba(24, 24, 34, 0.98), rgba(12, 12, 18, 0.98));
|
|
||||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.34);
|
|
||||||
overflow: visible;
|
|
||||||
pointer-events: auto;
|
|
||||||
gap: 10px;
|
|
||||||
max-height: calc(100svh - 88px);
|
max-height: calc(100svh - 88px);
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
.topbar.is-menu-open .topbar-actions {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
.topbar-actions > * {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.topbar-dropdown {
|
|
||||||
display: grid;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.topbar-dropdown-menu {
|
|
||||||
position: static;
|
|
||||||
min-width: 0;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 6px;
|
|
||||||
padding: 6px;
|
|
||||||
border-radius: 12px;
|
|
||||||
border-color: #31313d;
|
|
||||||
background: rgba(10, 10, 16, 0.92);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
.topbar-dropdown:hover .topbar-dropdown-menu,
|
|
||||||
.topbar-dropdown:focus-within .topbar-dropdown-menu {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.topbar-dropdown.is-open .topbar-dropdown-menu {
|
|
||||||
display: grid;
|
|
||||||
}
|
}
|
||||||
.settings-trigger {
|
.settings-trigger {
|
||||||
width: 100%;
|
|
||||||
min-height: 42px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
text-align: left;
|
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
}
|
}
|
||||||
.topbar-sub-trigger {
|
|
||||||
min-height: 40px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.topbar {
|
.topbar {
|
||||||
@@ -366,9 +318,18 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
#tarot-house-section {
|
||||||
|
height: calc(100vh - 61px);
|
||||||
|
background: #18181b;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
#tarot-section[hidden] {
|
#tarot-section[hidden] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
#tarot-house-section[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
#planet-section[hidden] {
|
#planet-section[hidden] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -1172,22 +1133,11 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tarot-browse-view.is-house-focus {
|
#tarot-house-view.is-house-focus {
|
||||||
grid-template-rows: minmax(0, 1fr);
|
padding: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tarot-browse-view.is-house-focus .tarot-section-house-top {
|
#tarot-house-view.is-house-focus .tarot-house-layout {
|
||||||
max-height: none;
|
|
||||||
height: 100%;
|
|
||||||
padding: 14px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tarot-browse-view.is-house-focus .tarot-layout {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tarot-browse-view.is-house-focus .tarot-house-layout {
|
|
||||||
--tarot-house-card-gap: clamp(4px, 0.6vw, 8px);
|
--tarot-house-card-gap: clamp(4px, 0.6vw, 8px);
|
||||||
--tarot-house-row-gap: clamp(6px, 0.9vw, 10px);
|
--tarot-house-row-gap: clamp(6px, 0.9vw, 10px);
|
||||||
--tarot-house-section-gap: clamp(12px, 1.4vw, 16px);
|
--tarot-house-section-gap: clamp(12px, 1.4vw, 16px);
|
||||||
@@ -1197,7 +1147,7 @@
|
|||||||
align-content: start;
|
align-content: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tarot-browse-view.is-house-focus .tarot-house-trumps {
|
#tarot-house-view.is-house-focus .tarot-house-trumps {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1802,6 +1752,10 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cube-export-btn[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.cube-rotation-controls {
|
.cube-rotation-controls {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||||
@@ -2285,6 +2239,33 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kab-export-btn {
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border: 1px solid #3f3f46;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #18181b;
|
||||||
|
color: #d4d4d8;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-export-btn:hover:not(:disabled) {
|
||||||
|
background: #27272a;
|
||||||
|
border-color: #52525b;
|
||||||
|
color: #f4f4f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-export-btn:disabled {
|
||||||
|
opacity: 0.65;
|
||||||
|
cursor: progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kab-export-btn[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.kab-detail-panel {
|
.kab-detail-panel {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -2783,7 +2764,7 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
grid-template-rows: minmax(0, 1fr);
|
||||||
grid-row: 1 / -1;
|
grid-row: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2791,6 +2772,28 @@
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#tarot-house-view {
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #151520;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tarot-house-view .tarot-section-house-top {
|
||||||
|
max-height: none;
|
||||||
|
height: auto;
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
overflow: visible;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tarot-house-view .tarot-house-card {
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Tarot Spread View ─────────────────────────────── */
|
/* ── Tarot Spread View ─────────────────────────────── */
|
||||||
#tarot-spread-view {
|
#tarot-spread-view {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
const DETAIL_COLLAPSE_STORAGE_PREFIX = "tarot-detail-collapsed:v2:";
|
const DETAIL_COLLAPSE_STORAGE_PREFIX = "tarot-detail-collapsed:v2:";
|
||||||
const DEFAULT_DATASET_ENTRY_COLLAPSED = false;
|
const DEFAULT_DATASET_ENTRY_COLLAPSED = false;
|
||||||
const DEFAULT_DATASET_DETAIL_COLLAPSED = true;
|
const DEFAULT_DATASET_DETAIL_COLLAPSED = true;
|
||||||
const MOBILE_TOPBAR_MEDIA_QUERY = "(max-width: 900px)";
|
|
||||||
const sidebarControllers = new WeakMap();
|
const sidebarControllers = new WeakMap();
|
||||||
const detailControllers = new WeakMap();
|
const detailControllers = new WeakMap();
|
||||||
const AUTO_COLLAPSE_ENTRY_SELECTOR = [
|
const AUTO_COLLAPSE_ENTRY_SELECTOR = [
|
||||||
@@ -354,10 +353,6 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMobileTopbarViewport() {
|
|
||||||
return window.matchMedia(MOBILE_TOPBAR_MEDIA_QUERY).matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTopbarMenuOpen(isOpen) {
|
function setTopbarMenuOpen(isOpen) {
|
||||||
const { topbarEl, menuToggleEl } = getTopbarElements();
|
const { topbarEl, menuToggleEl } = getTopbarElements();
|
||||||
if (!(topbarEl instanceof HTMLElement) || !(menuToggleEl instanceof HTMLButtonElement)) {
|
if (!(topbarEl instanceof HTMLElement) || !(menuToggleEl instanceof HTMLButtonElement)) {
|
||||||
@@ -407,18 +402,13 @@
|
|||||||
|
|
||||||
if (!isDropdownTrigger || isMenuItem) {
|
if (!isDropdownTrigger || isMenuItem) {
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
if (isMobileTopbarViewport()) {
|
|
||||||
setTopbarMenuOpen(false);
|
setTopbarMenuOpen(false);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
const clickTarget = event.target;
|
const clickTarget = event.target;
|
||||||
if (!isMobileTopbarViewport()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (clickTarget instanceof Node && topbarEl.contains(clickTarget)) {
|
if (clickTarget instanceof Node && topbarEl.contains(clickTarget)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -426,12 +416,6 @@
|
|||||||
setTopbarMenuOpen(false);
|
setTopbarMenuOpen(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("resize", () => {
|
|
||||||
if (!isMobileTopbarViewport()) {
|
|
||||||
setTopbarMenuOpen(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("keydown", (event) => {
|
document.addEventListener("keydown", (event) => {
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
setTopbarMenuOpen(false);
|
setTopbarMenuOpen(false);
|
||||||
@@ -463,20 +447,6 @@
|
|||||||
|
|
||||||
setTopbarDropdownOpen(dropdownEl, false);
|
setTopbarDropdownOpen(dropdownEl, false);
|
||||||
|
|
||||||
dropdownEl.addEventListener("mouseenter", () => {
|
|
||||||
if (isMobileTopbarViewport()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setTopbarDropdownOpen(dropdownEl, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
dropdownEl.addEventListener("mouseleave", () => {
|
|
||||||
if (isMobileTopbarViewport()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setTopbarDropdownOpen(dropdownEl, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
dropdownEl.addEventListener("focusout", (event) => {
|
dropdownEl.addEventListener("focusout", (event) => {
|
||||||
const nextTarget = event.relatedTarget;
|
const nextTarget = event.relatedTarget;
|
||||||
if (!(nextTarget instanceof Node) || !dropdownEl.contains(nextTarget)) {
|
if (!(nextTarget instanceof Node) || !dropdownEl.contains(nextTarget)) {
|
||||||
|
|||||||
246
app/ui-cube.js
246
app/ui-cube.js
@@ -16,9 +16,20 @@
|
|||||||
selectedConnectorId: null,
|
selectedConnectorId: null,
|
||||||
selectedWallId: null,
|
selectedWallId: null,
|
||||||
selectedEdgeId: null,
|
selectedEdgeId: null,
|
||||||
focusMode: false
|
focusMode: false,
|
||||||
|
exportInProgress: false,
|
||||||
|
exportFormat: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CUBE_EXPORT_FORMATS = {
|
||||||
|
webp: {
|
||||||
|
mimeType: "image/webp",
|
||||||
|
extension: "webp",
|
||||||
|
quality: 0.98
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const CUBE_EXPORT_BACKGROUND = "#111118";
|
||||||
|
|
||||||
const CUBE_VERTICES = [
|
const CUBE_VERTICES = [
|
||||||
[-1, -1, -1],
|
[-1, -1, -1],
|
||||||
[1, -1, -1],
|
[1, -1, -1],
|
||||||
@@ -124,6 +135,7 @@
|
|||||||
const cubeChassisUi = window.CubeChassisUi || {};
|
const cubeChassisUi = window.CubeChassisUi || {};
|
||||||
const cubeMathHelpers = window.CubeMathHelpers || {};
|
const cubeMathHelpers = window.CubeMathHelpers || {};
|
||||||
const cubeSelectionHelpers = window.CubeSelectionHelpers || {};
|
const cubeSelectionHelpers = window.CubeSelectionHelpers || {};
|
||||||
|
let webpExportSupported = null;
|
||||||
|
|
||||||
function getElements() {
|
function getElements() {
|
||||||
return {
|
return {
|
||||||
@@ -136,6 +148,7 @@
|
|||||||
rotateDownEl: document.getElementById("cube-rotate-down"),
|
rotateDownEl: document.getElementById("cube-rotate-down"),
|
||||||
rotateResetEl: document.getElementById("cube-rotate-reset"),
|
rotateResetEl: document.getElementById("cube-rotate-reset"),
|
||||||
focusToggleEl: document.getElementById("cube-focus-toggle"),
|
focusToggleEl: document.getElementById("cube-focus-toggle"),
|
||||||
|
exportWebpEl: document.getElementById("cube-export-webp"),
|
||||||
markerModeEl: document.getElementById("cube-marker-mode"),
|
markerModeEl: document.getElementById("cube-marker-mode"),
|
||||||
connectorToggleEl: document.getElementById("cube-connector-toggle"),
|
connectorToggleEl: document.getElementById("cube-connector-toggle"),
|
||||||
primalToggleEl: document.getElementById("cube-primal-toggle"),
|
primalToggleEl: document.getElementById("cube-primal-toggle"),
|
||||||
@@ -344,6 +357,233 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isExportFormatSupported(format) {
|
||||||
|
const exportFormat = CUBE_EXPORT_FORMATS[format];
|
||||||
|
if (!exportFormat) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format === "webp" && typeof webpExportSupported === "boolean") {
|
||||||
|
return webpExportSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
const probeCanvas = document.createElement("canvas");
|
||||||
|
const dataUrl = probeCanvas.toDataURL(exportFormat.mimeType);
|
||||||
|
const isSupported = dataUrl.startsWith(`data:${exportFormat.mimeType}`);
|
||||||
|
if (format === "webp") {
|
||||||
|
webpExportSupported = isSupported;
|
||||||
|
}
|
||||||
|
return isSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncExportControls(elements) {
|
||||||
|
if (!(elements?.exportWebpEl instanceof HTMLButtonElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const supportsWebp = isExportFormatSupported("webp");
|
||||||
|
elements.exportWebpEl.hidden = !supportsWebp;
|
||||||
|
elements.exportWebpEl.disabled = Boolean(state.exportInProgress) || !supportsWebp;
|
||||||
|
elements.exportWebpEl.textContent = state.exportInProgress && state.exportFormat === "webp"
|
||||||
|
? "Exporting..."
|
||||||
|
: "Export WebP";
|
||||||
|
|
||||||
|
if (supportsWebp) {
|
||||||
|
elements.exportWebpEl.title = "Download the current cube view as a WebP image.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyComputedStyles(sourceEl, targetEl) {
|
||||||
|
if (!(sourceEl instanceof Element) || !(targetEl instanceof Element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const computedStyle = window.getComputedStyle(sourceEl);
|
||||||
|
Array.from(computedStyle).forEach((propertyName) => {
|
||||||
|
targetEl.style.setProperty(
|
||||||
|
propertyName,
|
||||||
|
computedStyle.getPropertyValue(propertyName),
|
||||||
|
computedStyle.getPropertyPriority(propertyName)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
targetEl.style.setProperty("animation", "none");
|
||||||
|
targetEl.style.setProperty("transition", "none");
|
||||||
|
}
|
||||||
|
|
||||||
|
function inlineSvgStyles(sourceNode, targetNode) {
|
||||||
|
if (!(sourceNode instanceof Element) || !(targetNode instanceof Element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
copyComputedStyles(sourceNode, targetNode);
|
||||||
|
|
||||||
|
const sourceChildren = Array.from(sourceNode.children);
|
||||||
|
const targetChildren = Array.from(targetNode.children);
|
||||||
|
const childCount = Math.min(sourceChildren.length, targetChildren.length);
|
||||||
|
for (let index = 0; index < childCount; index += 1) {
|
||||||
|
inlineSvgStyles(sourceChildren[index], targetChildren[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function absolutizeSvgImageLinks(svgEl) {
|
||||||
|
if (!(svgEl instanceof SVGSVGElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
svgEl.querySelectorAll("image").forEach((imageEl) => {
|
||||||
|
const href = imageEl.getAttribute("href")
|
||||||
|
|| imageEl.getAttributeNS("http://www.w3.org/1999/xlink", "href");
|
||||||
|
if (!href) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const absoluteHref = new URL(href, document.baseURI).href;
|
||||||
|
imageEl.setAttribute("href", absoluteHref);
|
||||||
|
imageEl.setAttributeNS("http://www.w3.org/1999/xlink", "href", absoluteHref);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareSvgMarkupForExport(svgEl) {
|
||||||
|
if (!(svgEl instanceof SVGSVGElement)) {
|
||||||
|
throw new Error("Cube view is not ready to export yet.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const bounds = svgEl.getBoundingClientRect();
|
||||||
|
const viewBox = svgEl.viewBox?.baseVal || null;
|
||||||
|
const width = Math.max(
|
||||||
|
240,
|
||||||
|
Math.round(bounds.width),
|
||||||
|
Number.isFinite(viewBox?.width) ? Math.round(viewBox.width) : 0
|
||||||
|
);
|
||||||
|
const height = Math.max(
|
||||||
|
220,
|
||||||
|
Math.round(bounds.height),
|
||||||
|
Number.isFinite(viewBox?.height) ? Math.round(viewBox.height) : 0
|
||||||
|
);
|
||||||
|
|
||||||
|
const clone = svgEl.cloneNode(true);
|
||||||
|
if (!(clone instanceof SVGSVGElement)) {
|
||||||
|
throw new Error("Cube export could not clone the current SVG view.");
|
||||||
|
}
|
||||||
|
|
||||||
|
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
||||||
|
clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
||||||
|
clone.setAttribute("width", String(width));
|
||||||
|
clone.setAttribute("height", String(height));
|
||||||
|
clone.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
||||||
|
|
||||||
|
inlineSvgStyles(svgEl, clone);
|
||||||
|
absolutizeSvgImageLinks(clone);
|
||||||
|
|
||||||
|
const backgroundRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||||
|
backgroundRect.setAttribute("x", "0");
|
||||||
|
backgroundRect.setAttribute("y", "0");
|
||||||
|
backgroundRect.setAttribute("width", "100%");
|
||||||
|
backgroundRect.setAttribute("height", "100%");
|
||||||
|
backgroundRect.setAttribute("fill", CUBE_EXPORT_BACKGROUND);
|
||||||
|
backgroundRect.setAttribute("pointer-events", "none");
|
||||||
|
clone.insertBefore(backgroundRect, clone.firstChild);
|
||||||
|
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
markup: new XMLSerializer().serializeToString(clone)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSvgImage(markup) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const svgBlob = new Blob([markup], { type: "image/svg+xml;charset=utf-8" });
|
||||||
|
const svgUrl = URL.createObjectURL(svgBlob);
|
||||||
|
const image = new Image();
|
||||||
|
|
||||||
|
image.decoding = "async";
|
||||||
|
image.onload = () => {
|
||||||
|
URL.revokeObjectURL(svgUrl);
|
||||||
|
resolve(image);
|
||||||
|
};
|
||||||
|
image.onerror = () => {
|
||||||
|
URL.revokeObjectURL(svgUrl);
|
||||||
|
reject(new Error("Cube export renderer could not load the current SVG view."));
|
||||||
|
};
|
||||||
|
image.src = svgUrl;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function canvasToBlobByFormat(canvas, format) {
|
||||||
|
const exportFormat = CUBE_EXPORT_FORMATS[format];
|
||||||
|
if (!exportFormat) {
|
||||||
|
return Promise.reject(new Error("Unsupported export format."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (blob) {
|
||||||
|
resolve(blob);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reject(new Error("Canvas export failed."));
|
||||||
|
}, exportFormat.mimeType, exportFormat.quality);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportCubeView(format = "webp") {
|
||||||
|
const exportFormat = CUBE_EXPORT_FORMATS[format];
|
||||||
|
if (!exportFormat || state.exportInProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elements = getElements();
|
||||||
|
const svgEl = elements.viewContainerEl?.querySelector("svg.cube-svg");
|
||||||
|
if (!(svgEl instanceof SVGSVGElement)) {
|
||||||
|
window.alert("Cube view is not ready to export yet.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.exportInProgress = true;
|
||||||
|
state.exportFormat = format;
|
||||||
|
syncExportControls(elements);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { width, height, markup } = prepareSvgMarkupForExport(svgEl);
|
||||||
|
const image = await loadSvgImage(markup);
|
||||||
|
const scale = Math.max(2, Math.min(4, Number(window.devicePixelRatio) || 1));
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = Math.max(1, Math.ceil(width * scale));
|
||||||
|
canvas.height = Math.max(1, Math.ceil(height * scale));
|
||||||
|
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("Canvas context is unavailable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.scale(scale, scale);
|
||||||
|
context.imageSmoothingEnabled = true;
|
||||||
|
context.imageSmoothingQuality = "high";
|
||||||
|
context.fillStyle = CUBE_EXPORT_BACKGROUND;
|
||||||
|
context.fillRect(0, 0, width, height);
|
||||||
|
context.drawImage(image, 0, 0, width, height);
|
||||||
|
|
||||||
|
const blob = await canvasToBlobByFormat(canvas, format);
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
const downloadLink = document.createElement("a");
|
||||||
|
const stamp = new Date().toISOString().slice(0, 10);
|
||||||
|
downloadLink.href = blobUrl;
|
||||||
|
downloadLink.download = `cube-of-space-${stamp}.${exportFormat.extension}`;
|
||||||
|
document.body.appendChild(downloadLink);
|
||||||
|
downloadLink.click();
|
||||||
|
downloadLink.remove();
|
||||||
|
setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
|
||||||
|
} catch (error) {
|
||||||
|
window.alert(error instanceof Error ? error.message : "Unable to export the current cube view.");
|
||||||
|
} finally {
|
||||||
|
state.exportInProgress = false;
|
||||||
|
state.exportFormat = "";
|
||||||
|
syncExportControls(getElements());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function bindRotationControls(elements) {
|
function bindRotationControls(elements) {
|
||||||
if (state.controlsBound) {
|
if (state.controlsBound) {
|
||||||
return;
|
return;
|
||||||
@@ -358,6 +598,9 @@
|
|||||||
state.focusMode = !state.focusMode;
|
state.focusMode = !state.focusMode;
|
||||||
syncFocusControls(getElements());
|
syncFocusControls(getElements());
|
||||||
});
|
});
|
||||||
|
elements.exportWebpEl?.addEventListener("click", () => {
|
||||||
|
void exportCubeView("webp");
|
||||||
|
});
|
||||||
|
|
||||||
elements.markerModeEl?.addEventListener("change", (event) => {
|
elements.markerModeEl?.addEventListener("change", (event) => {
|
||||||
const nextMode = normalizeId(event?.target?.value);
|
const nextMode = normalizeId(event?.target?.value);
|
||||||
@@ -695,6 +938,7 @@
|
|||||||
|
|
||||||
function render(elements) {
|
function render(elements) {
|
||||||
syncFocusControls(elements);
|
syncFocusControls(elements);
|
||||||
|
syncExportControls(elements);
|
||||||
|
|
||||||
if (elements?.markerModeEl) {
|
if (elements?.markerModeEl) {
|
||||||
elements.markerModeEl.value = state.markerDisplayMode;
|
elements.markerModeEl.value = state.markerDisplayMode;
|
||||||
|
|||||||
@@ -58,11 +58,22 @@
|
|||||||
showPathNumbers: true,
|
showPathNumbers: true,
|
||||||
showPathTarotCards: false,
|
showPathTarotCards: false,
|
||||||
selectedSephiraNumber: null,
|
selectedSephiraNumber: null,
|
||||||
selectedPathNumber: null
|
selectedPathNumber: null,
|
||||||
|
exportInProgress: false,
|
||||||
|
exportFormat: ""
|
||||||
};
|
};
|
||||||
|
const TREE_EXPORT_FORMATS = {
|
||||||
|
webp: {
|
||||||
|
mimeType: "image/webp",
|
||||||
|
extension: "webp",
|
||||||
|
quality: 0.98
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const TREE_EXPORT_BACKGROUND = "#02030a";
|
||||||
|
|
||||||
const kabbalahDetailUi = window.KabbalahDetailUi || {};
|
const kabbalahDetailUi = window.KabbalahDetailUi || {};
|
||||||
const kabbalahViewsUi = window.KabbalahViewsUi || {};
|
const kabbalahViewsUi = window.KabbalahViewsUi || {};
|
||||||
|
let webpExportSupported = null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof kabbalahViewsUi.renderTree !== "function"
|
typeof kabbalahViewsUi.renderTree !== "function"
|
||||||
@@ -259,6 +270,7 @@
|
|||||||
pathLetterToggleEl: document.getElementById("kab-path-letter-toggle"),
|
pathLetterToggleEl: document.getElementById("kab-path-letter-toggle"),
|
||||||
pathNumberToggleEl: document.getElementById("kab-path-number-toggle"),
|
pathNumberToggleEl: document.getElementById("kab-path-number-toggle"),
|
||||||
pathTarotToggleEl: document.getElementById("kab-path-tarot-toggle"),
|
pathTarotToggleEl: document.getElementById("kab-path-tarot-toggle"),
|
||||||
|
treeExportWebpEl: document.getElementById("kab-tree-export-webp"),
|
||||||
roseCrossContainerEl: document.getElementById("kab-rose-cross-container"),
|
roseCrossContainerEl: document.getElementById("kab-rose-cross-container"),
|
||||||
roseDetailNameEl: document.getElementById("kab-rose-detail-name"),
|
roseDetailNameEl: document.getElementById("kab-rose-detail-name"),
|
||||||
roseDetailSubEl: document.getElementById("kab-rose-detail-sub"),
|
roseDetailSubEl: document.getElementById("kab-rose-detail-sub"),
|
||||||
@@ -476,6 +488,233 @@
|
|||||||
kabbalahViewsUi.renderTree(getViewRenderContext(elements));
|
kabbalahViewsUi.renderTree(getViewRenderContext(elements));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isExportFormatSupported(format) {
|
||||||
|
const exportFormat = TREE_EXPORT_FORMATS[format];
|
||||||
|
if (!exportFormat) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format === "webp" && typeof webpExportSupported === "boolean") {
|
||||||
|
return webpExportSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
const probeCanvas = document.createElement("canvas");
|
||||||
|
const dataUrl = probeCanvas.toDataURL(exportFormat.mimeType);
|
||||||
|
const isSupported = dataUrl.startsWith(`data:${exportFormat.mimeType}`);
|
||||||
|
if (format === "webp") {
|
||||||
|
webpExportSupported = isSupported;
|
||||||
|
}
|
||||||
|
return isSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncExportControls(elements) {
|
||||||
|
if (!(elements?.treeExportWebpEl instanceof HTMLButtonElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const supportsWebp = isExportFormatSupported("webp");
|
||||||
|
elements.treeExportWebpEl.hidden = !supportsWebp;
|
||||||
|
elements.treeExportWebpEl.disabled = Boolean(state.exportInProgress) || !supportsWebp;
|
||||||
|
elements.treeExportWebpEl.textContent = state.exportInProgress && state.exportFormat === "webp"
|
||||||
|
? "Exporting..."
|
||||||
|
: "Export WebP";
|
||||||
|
|
||||||
|
if (supportsWebp) {
|
||||||
|
elements.treeExportWebpEl.title = "Download the current Tree of Life view as a WebP image.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyComputedStyles(sourceEl, targetEl) {
|
||||||
|
if (!(sourceEl instanceof Element) || !(targetEl instanceof Element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const computedStyle = window.getComputedStyle(sourceEl);
|
||||||
|
Array.from(computedStyle).forEach((propertyName) => {
|
||||||
|
targetEl.style.setProperty(
|
||||||
|
propertyName,
|
||||||
|
computedStyle.getPropertyValue(propertyName),
|
||||||
|
computedStyle.getPropertyPriority(propertyName)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
targetEl.style.setProperty("animation", "none");
|
||||||
|
targetEl.style.setProperty("transition", "none");
|
||||||
|
}
|
||||||
|
|
||||||
|
function inlineSvgStyles(sourceNode, targetNode) {
|
||||||
|
if (!(sourceNode instanceof Element) || !(targetNode instanceof Element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
copyComputedStyles(sourceNode, targetNode);
|
||||||
|
|
||||||
|
const sourceChildren = Array.from(sourceNode.children);
|
||||||
|
const targetChildren = Array.from(targetNode.children);
|
||||||
|
const childCount = Math.min(sourceChildren.length, targetChildren.length);
|
||||||
|
for (let index = 0; index < childCount; index += 1) {
|
||||||
|
inlineSvgStyles(sourceChildren[index], targetChildren[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function absolutizeSvgImageLinks(svgEl) {
|
||||||
|
if (!(svgEl instanceof SVGSVGElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
svgEl.querySelectorAll("image").forEach((imageEl) => {
|
||||||
|
const href = imageEl.getAttribute("href")
|
||||||
|
|| imageEl.getAttributeNS("http://www.w3.org/1999/xlink", "href");
|
||||||
|
if (!href) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const absoluteHref = new URL(href, document.baseURI).href;
|
||||||
|
imageEl.setAttribute("href", absoluteHref);
|
||||||
|
imageEl.setAttributeNS("http://www.w3.org/1999/xlink", "href", absoluteHref);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareSvgMarkupForExport(svgEl) {
|
||||||
|
if (!(svgEl instanceof SVGSVGElement)) {
|
||||||
|
throw new Error("Tree view is not ready to export yet.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const bounds = svgEl.getBoundingClientRect();
|
||||||
|
const viewBox = svgEl.viewBox?.baseVal || null;
|
||||||
|
const width = Math.max(
|
||||||
|
240,
|
||||||
|
Math.round(bounds.width),
|
||||||
|
Number.isFinite(viewBox?.width) ? Math.round(viewBox.width) : 0
|
||||||
|
);
|
||||||
|
const height = Math.max(
|
||||||
|
470,
|
||||||
|
Math.round(bounds.height),
|
||||||
|
Number.isFinite(viewBox?.height) ? Math.round(viewBox.height) : 0
|
||||||
|
);
|
||||||
|
|
||||||
|
const clone = svgEl.cloneNode(true);
|
||||||
|
if (!(clone instanceof SVGSVGElement)) {
|
||||||
|
throw new Error("Tree export could not clone the current SVG view.");
|
||||||
|
}
|
||||||
|
|
||||||
|
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
||||||
|
clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
||||||
|
clone.setAttribute("width", String(width));
|
||||||
|
clone.setAttribute("height", String(height));
|
||||||
|
clone.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
||||||
|
|
||||||
|
inlineSvgStyles(svgEl, clone);
|
||||||
|
absolutizeSvgImageLinks(clone);
|
||||||
|
|
||||||
|
const backgroundRect = document.createElementNS(NS, "rect");
|
||||||
|
backgroundRect.setAttribute("x", "0");
|
||||||
|
backgroundRect.setAttribute("y", "0");
|
||||||
|
backgroundRect.setAttribute("width", "100%");
|
||||||
|
backgroundRect.setAttribute("height", "100%");
|
||||||
|
backgroundRect.setAttribute("fill", TREE_EXPORT_BACKGROUND);
|
||||||
|
backgroundRect.setAttribute("pointer-events", "none");
|
||||||
|
clone.insertBefore(backgroundRect, clone.firstChild);
|
||||||
|
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
markup: new XMLSerializer().serializeToString(clone)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSvgImage(markup) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const svgBlob = new Blob([markup], { type: "image/svg+xml;charset=utf-8" });
|
||||||
|
const svgUrl = URL.createObjectURL(svgBlob);
|
||||||
|
const image = new Image();
|
||||||
|
|
||||||
|
image.decoding = "async";
|
||||||
|
image.onload = () => {
|
||||||
|
URL.revokeObjectURL(svgUrl);
|
||||||
|
resolve(image);
|
||||||
|
};
|
||||||
|
image.onerror = () => {
|
||||||
|
URL.revokeObjectURL(svgUrl);
|
||||||
|
reject(new Error("Tree export renderer could not load the current SVG view."));
|
||||||
|
};
|
||||||
|
image.src = svgUrl;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function canvasToBlobByFormat(canvas, format) {
|
||||||
|
const exportFormat = TREE_EXPORT_FORMATS[format];
|
||||||
|
if (!exportFormat) {
|
||||||
|
return Promise.reject(new Error("Unsupported export format."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (blob) {
|
||||||
|
resolve(blob);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reject(new Error("Canvas export failed."));
|
||||||
|
}, exportFormat.mimeType, exportFormat.quality);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportTreeView(format = "webp") {
|
||||||
|
const exportFormat = TREE_EXPORT_FORMATS[format];
|
||||||
|
if (!exportFormat || state.exportInProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elements = getElements();
|
||||||
|
const svgEl = elements.treeContainerEl?.querySelector("svg.kab-svg");
|
||||||
|
if (!(svgEl instanceof SVGSVGElement)) {
|
||||||
|
window.alert("Tree view is not ready to export yet.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.exportInProgress = true;
|
||||||
|
state.exportFormat = format;
|
||||||
|
syncExportControls(elements);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { width, height, markup } = prepareSvgMarkupForExport(svgEl);
|
||||||
|
const image = await loadSvgImage(markup);
|
||||||
|
const scale = Math.max(2, Math.min(4, Number(window.devicePixelRatio) || 1));
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = Math.max(1, Math.ceil(width * scale));
|
||||||
|
canvas.height = Math.max(1, Math.ceil(height * scale));
|
||||||
|
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("Canvas context is unavailable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.scale(scale, scale);
|
||||||
|
context.imageSmoothingEnabled = true;
|
||||||
|
context.imageSmoothingQuality = "high";
|
||||||
|
context.fillStyle = TREE_EXPORT_BACKGROUND;
|
||||||
|
context.fillRect(0, 0, width, height);
|
||||||
|
context.drawImage(image, 0, 0, width, height);
|
||||||
|
|
||||||
|
const blob = await canvasToBlobByFormat(canvas, format);
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
const downloadLink = document.createElement("a");
|
||||||
|
const stamp = new Date().toISOString().slice(0, 10);
|
||||||
|
downloadLink.href = blobUrl;
|
||||||
|
downloadLink.download = `tree-of-life-${stamp}.${exportFormat.extension}`;
|
||||||
|
document.body.appendChild(downloadLink);
|
||||||
|
downloadLink.click();
|
||||||
|
downloadLink.remove();
|
||||||
|
setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
|
||||||
|
} catch (error) {
|
||||||
|
window.alert(error instanceof Error ? error.message : "Unable to export the current Tree of Life view.");
|
||||||
|
} finally {
|
||||||
|
state.exportInProgress = false;
|
||||||
|
state.exportFormat = "";
|
||||||
|
syncExportControls(getElements());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderCurrentSelection(elements) {
|
function renderCurrentSelection(elements) {
|
||||||
if (!state.tree) {
|
if (!state.tree) {
|
||||||
return;
|
return;
|
||||||
@@ -538,6 +777,14 @@
|
|||||||
bindPathDisplayToggle(elements.pathNumberToggleEl, "showPathNumbers");
|
bindPathDisplayToggle(elements.pathNumberToggleEl, "showPathNumbers");
|
||||||
bindPathDisplayToggle(elements.pathTarotToggleEl, "showPathTarotCards");
|
bindPathDisplayToggle(elements.pathTarotToggleEl, "showPathTarotCards");
|
||||||
|
|
||||||
|
syncExportControls(elements);
|
||||||
|
if (elements.treeExportWebpEl && !elements.treeExportWebpEl.dataset.bound) {
|
||||||
|
elements.treeExportWebpEl.addEventListener("click", () => {
|
||||||
|
void exportTreeView("webp");
|
||||||
|
});
|
||||||
|
elements.treeExportWebpEl.dataset.bound = "true";
|
||||||
|
}
|
||||||
|
|
||||||
renderTree(elements);
|
renderTree(elements);
|
||||||
renderCurrentSelection(elements);
|
renderCurrentSelection(elements);
|
||||||
renderRoseCross(elements);
|
renderRoseCross(elements);
|
||||||
|
|||||||
@@ -46,6 +46,10 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bindClick(elements.openTarotHouseEl, () => {
|
||||||
|
setActiveSection(getActiveSection() === "tarot-house" ? "home" : "tarot-house");
|
||||||
|
});
|
||||||
|
|
||||||
bindClick(elements.openAstronomyEl, () => {
|
bindClick(elements.openAstronomyEl, () => {
|
||||||
setActiveSection(getActiveSection() === "astronomy" ? "home" : "astronomy");
|
setActiveSection(getActiveSection() === "astronomy" ? "home" : "astronomy");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"calendar",
|
"calendar",
|
||||||
"holidays",
|
"holidays",
|
||||||
"tarot",
|
"tarot",
|
||||||
|
"tarot-house",
|
||||||
"astronomy",
|
"astronomy",
|
||||||
"planets",
|
"planets",
|
||||||
"cycles",
|
"cycles",
|
||||||
@@ -88,6 +89,8 @@
|
|||||||
const isHolidaysOpen = activeSection === "holidays";
|
const isHolidaysOpen = activeSection === "holidays";
|
||||||
const isCalendarMenuOpen = isTimelineOpen || isCalendarOpen || isHolidaysOpen;
|
const isCalendarMenuOpen = isTimelineOpen || isCalendarOpen || isHolidaysOpen;
|
||||||
const isTarotOpen = activeSection === "tarot";
|
const isTarotOpen = activeSection === "tarot";
|
||||||
|
const isTarotHouseOpen = activeSection === "tarot-house";
|
||||||
|
const isTarotMenuOpen = isTarotOpen || isTarotHouseOpen;
|
||||||
const isAstronomyOpen = activeSection === "astronomy";
|
const isAstronomyOpen = activeSection === "astronomy";
|
||||||
const isPlanetOpen = activeSection === "planets";
|
const isPlanetOpen = activeSection === "planets";
|
||||||
const isCyclesOpen = activeSection === "cycles";
|
const isCyclesOpen = activeSection === "cycles";
|
||||||
@@ -113,6 +116,7 @@
|
|||||||
setHidden(elements.calendarSectionEl, !isCalendarOpen);
|
setHidden(elements.calendarSectionEl, !isCalendarOpen);
|
||||||
setHidden(elements.holidaySectionEl, !isHolidaysOpen);
|
setHidden(elements.holidaySectionEl, !isHolidaysOpen);
|
||||||
setHidden(elements.tarotSectionEl, !isTarotOpen);
|
setHidden(elements.tarotSectionEl, !isTarotOpen);
|
||||||
|
setHidden(elements.tarotHouseSectionEl, !isTarotHouseOpen);
|
||||||
setHidden(elements.astronomySectionEl, !isAstronomyOpen);
|
setHidden(elements.astronomySectionEl, !isAstronomyOpen);
|
||||||
setHidden(elements.planetSectionEl, !isPlanetOpen);
|
setHidden(elements.planetSectionEl, !isPlanetOpen);
|
||||||
setHidden(elements.cyclesSectionEl, !isCyclesOpen);
|
setHidden(elements.cyclesSectionEl, !isCyclesOpen);
|
||||||
@@ -137,7 +141,8 @@
|
|||||||
toggleActive(elements.openCalendarTimelineEl, isTimelineOpen);
|
toggleActive(elements.openCalendarTimelineEl, isTimelineOpen);
|
||||||
toggleActive(elements.openCalendarMonthsEl, isCalendarOpen);
|
toggleActive(elements.openCalendarMonthsEl, isCalendarOpen);
|
||||||
toggleActive(elements.openHolidaysEl, isHolidaysOpen);
|
toggleActive(elements.openHolidaysEl, isHolidaysOpen);
|
||||||
setPressed(elements.openTarotEl, isTarotOpen);
|
setPressed(elements.openTarotEl, isTarotMenuOpen);
|
||||||
|
toggleActive(elements.openTarotHouseEl, isTarotHouseOpen);
|
||||||
config.tarotSpreadUi?.applyViewState?.();
|
config.tarotSpreadUi?.applyViewState?.();
|
||||||
setPressed(elements.openAstronomyEl, isAstronomyMenuOpen);
|
setPressed(elements.openAstronomyEl, isAstronomyMenuOpen);
|
||||||
toggleActive(elements.openPlanetsEl, isPlanetOpen);
|
toggleActive(elements.openPlanetsEl, isPlanetOpen);
|
||||||
@@ -185,6 +190,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isTarotHouseOpen) {
|
||||||
|
ensure.ensureTarotSection?.(referenceData, magickDataset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isPlanetOpen) {
|
if (isPlanetOpen) {
|
||||||
ensure.ensurePlanetSection?.(referenceData, magickDataset);
|
ensure.ensurePlanetSection?.(referenceData, magickDataset);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
normalizeTarotCardLookupName: (value) => String(value || "").trim().toLowerCase(),
|
normalizeTarotCardLookupName: (value) => String(value || "").trim().toLowerCase(),
|
||||||
selectCardById: () => {},
|
selectCardById: () => {},
|
||||||
openCardLightbox: () => {},
|
openCardLightbox: () => {},
|
||||||
|
shouldOpenCardLightboxOnSelect: () => false,
|
||||||
isHouseFocusMode: () => false,
|
isHouseFocusMode: () => false,
|
||||||
getCards: () => [],
|
getCards: () => [],
|
||||||
getSelectedCardId: () => "",
|
getSelectedCardId: () => "",
|
||||||
@@ -730,8 +731,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
|
const shouldOpenLightbox = Boolean(config.isHouseFocusMode?.())
|
||||||
|
|| Boolean(config.shouldOpenCardLightboxOnSelect?.(elements, card));
|
||||||
|
|
||||||
config.selectCardById(card.id, elements);
|
config.selectCardById(card.id, elements);
|
||||||
if (config.isHouseFocusMode?.() === true && imageUrl) {
|
if (shouldOpenLightbox && imageUrl) {
|
||||||
config.openCardLightbox?.(
|
config.openCardLightbox?.(
|
||||||
imageUrl,
|
imageUrl,
|
||||||
cardDisplayName || card.name || "Tarot card enlarged image",
|
cardDisplayName || card.name || "Tarot card enlarged image",
|
||||||
|
|||||||
@@ -244,6 +244,8 @@
|
|||||||
|
|
||||||
function getElements() {
|
function getElements() {
|
||||||
return {
|
return {
|
||||||
|
tarotSectionEl: document.getElementById("tarot-section"),
|
||||||
|
tarotHouseSectionEl: document.getElementById("tarot-house-section"),
|
||||||
tarotCardListEl: document.getElementById("tarot-card-list"),
|
tarotCardListEl: document.getElementById("tarot-card-list"),
|
||||||
tarotSearchInputEl: document.getElementById("tarot-search-input"),
|
tarotSearchInputEl: document.getElementById("tarot-search-input"),
|
||||||
tarotSearchClearEl: document.getElementById("tarot-search-clear"),
|
tarotSearchClearEl: document.getElementById("tarot-search-clear"),
|
||||||
@@ -278,6 +280,7 @@
|
|||||||
tarotKabPathEl: document.getElementById("tarot-kab-path"),
|
tarotKabPathEl: document.getElementById("tarot-kab-path"),
|
||||||
tarotHouseOfCardsEl: document.getElementById("tarot-house-of-cards"),
|
tarotHouseOfCardsEl: document.getElementById("tarot-house-of-cards"),
|
||||||
tarotBrowseViewEl: document.getElementById("tarot-browse-view"),
|
tarotBrowseViewEl: document.getElementById("tarot-browse-view"),
|
||||||
|
tarotHouseViewEl: document.getElementById("tarot-house-view"),
|
||||||
tarotHouseTopCardsVisibleEl: document.getElementById("tarot-house-top-cards-visible"),
|
tarotHouseTopCardsVisibleEl: document.getElementById("tarot-house-top-cards-visible"),
|
||||||
tarotHouseTopInfoHebrewEl: document.getElementById("tarot-house-top-info-hebrew"),
|
tarotHouseTopInfoHebrewEl: document.getElementById("tarot-house-top-info-hebrew"),
|
||||||
tarotHouseTopInfoPlanetEl: document.getElementById("tarot-house-top-info-planet"),
|
tarotHouseTopInfoPlanetEl: document.getElementById("tarot-house-top-info-planet"),
|
||||||
@@ -539,8 +542,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function syncHouseControls(elements) {
|
function syncHouseControls(elements) {
|
||||||
if (elements?.tarotBrowseViewEl) {
|
if (elements?.tarotHouseViewEl) {
|
||||||
elements.tarotBrowseViewEl.classList.toggle("is-house-focus", Boolean(state.houseFocusMode));
|
elements.tarotHouseViewEl.classList.toggle("is-house-focus", Boolean(state.houseFocusMode));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elements?.tarotHouseTopCardsVisibleEl) {
|
if (elements?.tarotHouseTopCardsVisibleEl) {
|
||||||
@@ -838,6 +841,10 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
shouldOpenCardLightboxOnSelect: (latestElements) => Boolean(
|
||||||
|
latestElements?.tarotHouseSectionEl instanceof HTMLElement
|
||||||
|
&& latestElements.tarotHouseSectionEl.hidden === false
|
||||||
|
),
|
||||||
isHouseFocusMode: () => state.houseFocusMode,
|
isHouseFocusMode: () => state.houseFocusMode,
|
||||||
getCards: () => state.cards,
|
getCards: () => state.cards,
|
||||||
getSelectedCardId: () => state.selectedCardId,
|
getSelectedCardId: () => state.selectedCardId,
|
||||||
|
|||||||
209
index.html
209
index.html
@@ -16,58 +16,59 @@
|
|||||||
<link rel="stylesheet" href="node_modules/@fontsource/amiri/arabic-400.css">
|
<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/amiri/arabic-700.css">
|
||||||
<link rel="stylesheet" href="node_modules/@fontsource/noto-naskh-arabic/arabic-400.css">
|
<link rel="stylesheet" href="node_modules/@fontsource/noto-naskh-arabic/arabic-400.css">
|
||||||
<link rel="stylesheet" href="app/styles.css?v=20260312-global-search-04">
|
<link rel="stylesheet" href="app/styles.css?v=20260312-house-cube-01">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="topbar">
|
<div class="topbar">
|
||||||
<button id="open-home" class="topbar-home-button" type="button" aria-pressed="true">Tarot Time!</button>
|
<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="topbar-menu-toggle" class="topbar-menu-toggle" type="button" aria-expanded="false" aria-controls="topbar-actions" aria-label="Open navigation menu">Menu</button>
|
||||||
<div id="topbar-actions" class="topbar-actions">
|
<div id="topbar-actions" class="topbar-actions">
|
||||||
<div class="topbar-dropdown" aria-label="Calendar menu">
|
<div class="topbar-dropdown" aria-label="Alphabet menu">
|
||||||
<button id="open-calendar" class="settings-trigger" type="button" aria-pressed="false" aria-haspopup="menu" aria-controls="calendar-subpages" aria-expanded="false">Calendar ▾</button>
|
<button id="open-alphabet" class="settings-trigger" type="button" aria-pressed="false" aria-haspopup="menu" aria-controls="alphabet-subpages" aria-expanded="false">Alphabet ▾</button>
|
||||||
<div id="calendar-subpages" class="topbar-dropdown-menu" role="menu" aria-label="Calendar subpages">
|
<div id="alphabet-subpages" class="topbar-dropdown-menu" role="menu" aria-label="Alphabet subpages">
|
||||||
<button id="open-calendar-timeline" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Timeline</button>
|
<button id="open-alphabet-letters" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Letter Page</button>
|
||||||
<button id="open-calendar-months" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Months</button>
|
<button id="open-alphabet-text" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Text</button>
|
||||||
<button id="open-holidays" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Holidays</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar-dropdown" aria-label="Astronomy menu">
|
<div class="topbar-dropdown" aria-label="Astronomy menu">
|
||||||
<button id="open-astronomy" class="settings-trigger" type="button" aria-pressed="false" aria-haspopup="menu" aria-controls="astronomy-subpages" aria-expanded="false">Astronomy ▾</button>
|
<button id="open-astronomy" class="settings-trigger" type="button" aria-pressed="false" aria-haspopup="menu" aria-controls="astronomy-subpages" aria-expanded="false">Astronomy ▾</button>
|
||||||
<div id="astronomy-subpages" class="topbar-dropdown-menu" role="menu" aria-label="Astronomy subpages">
|
<div id="astronomy-subpages" class="topbar-dropdown-menu" role="menu" aria-label="Astronomy subpages">
|
||||||
<button id="open-planets" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Planet</button>
|
|
||||||
<button id="open-cycles" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Cycles</button>
|
<button id="open-cycles" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Cycles</button>
|
||||||
<button id="open-zodiac" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Zodiac</button>
|
|
||||||
<button id="open-natal" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Natal Chart</button>
|
<button id="open-natal" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Natal Chart</button>
|
||||||
|
<button id="open-planets" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Planet</button>
|
||||||
|
<button id="open-zodiac" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Zodiac</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="topbar-dropdown" aria-label="Calendar menu">
|
||||||
|
<button id="open-calendar" class="settings-trigger" type="button" aria-pressed="false" aria-haspopup="menu" aria-controls="calendar-subpages" aria-expanded="false">Calendar ▾</button>
|
||||||
|
<div id="calendar-subpages" class="topbar-dropdown-menu" role="menu" aria-label="Calendar subpages">
|
||||||
|
<button id="open-holidays" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Holidays</button>
|
||||||
|
<button id="open-calendar-months" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Months</button>
|
||||||
|
<button id="open-calendar-timeline" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Timeline</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button id="open-elements" class="settings-trigger" type="button" aria-pressed="false">Elements</button>
|
<button id="open-elements" class="settings-trigger" type="button" aria-pressed="false">Elements</button>
|
||||||
|
<button id="open-enochian" class="settings-trigger" type="button" aria-pressed="false">Enochian</button>
|
||||||
|
<button id="open-gods" class="settings-trigger" type="button" aria-pressed="false">Gods</button>
|
||||||
<button id="open-iching" class="settings-trigger" type="button" aria-pressed="false">I Ching</button>
|
<button id="open-iching" class="settings-trigger" type="button" aria-pressed="false">I Ching</button>
|
||||||
<div class="topbar-dropdown" aria-label="Kabbalah menu">
|
<div class="topbar-dropdown" aria-label="Kabbalah menu">
|
||||||
<button id="open-kabbalah" class="settings-trigger" type="button" aria-pressed="false" aria-haspopup="menu" aria-controls="kabbalah-subpages" aria-expanded="false">Kabbalah ▾</button>
|
<button id="open-kabbalah" class="settings-trigger" type="button" aria-pressed="false" aria-haspopup="menu" aria-controls="kabbalah-subpages" aria-expanded="false">Kabbalah ▾</button>
|
||||||
<div id="kabbalah-subpages" class="topbar-dropdown-menu" role="menu" aria-label="Kabbalah subpages">
|
<div id="kabbalah-subpages" class="topbar-dropdown-menu" role="menu" aria-label="Kabbalah subpages">
|
||||||
<button id="open-kabbalah-tree" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Tree</button>
|
|
||||||
<button id="open-kabbalah-cube" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Cube</button>
|
<button id="open-kabbalah-cube" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Cube</button>
|
||||||
</div>
|
<button id="open-kabbalah-tree" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Tree</button>
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
<div id="alphabet-subpages" class="topbar-dropdown-menu" role="menu" aria-label="Alphabet subpages">
|
|
||||||
<button id="open-alphabet-text" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Text</button>
|
|
||||||
<button id="open-alphabet-letters" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Letter Page</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button id="open-numbers" class="settings-trigger" type="button" aria-pressed="false">Numbers</button>
|
<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-quiz" class="settings-trigger" type="button" aria-pressed="false">Quiz</button>
|
||||||
<button id="open-gods" class="settings-trigger" type="button" aria-pressed="false">Gods</button>
|
|
||||||
<button id="open-enochian" class="settings-trigger" type="button" aria-pressed="false">Enochian</button>
|
|
||||||
<button id="open-settings" class="settings-trigger" type="button" aria-haspopup="dialog" aria-expanded="false">Settings</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-house" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">House</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="connection-gate" class="connection-gate" hidden>
|
<div id="connection-gate" class="connection-gate" hidden>
|
||||||
@@ -203,73 +204,6 @@
|
|||||||
</section>
|
</section>
|
||||||
<section id="tarot-section" hidden>
|
<section id="tarot-section" hidden>
|
||||||
<div id="tarot-browse-view">
|
<div id="tarot-browse-view">
|
||||||
<section class="tarot-misc-section tarot-section-house-top" aria-label="Tarot misc">
|
|
||||||
<div class="tarot-meta-card tarot-house-card">
|
|
||||||
<div class="tarot-house-card-head">
|
|
||||||
<strong>House of Cards</strong>
|
|
||||||
<div class="tarot-house-card-actions">
|
|
||||||
<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>
|
|
||||||
</label>
|
|
||||||
<fieldset class="tarot-house-filter-group">
|
|
||||||
<legend>Top Info</legend>
|
|
||||||
<label class="tarot-house-mini-toggle" for="tarot-house-top-info-hebrew">
|
|
||||||
<input id="tarot-house-top-info-hebrew" type="checkbox" checked>
|
|
||||||
<span>Hebrew</span>
|
|
||||||
</label>
|
|
||||||
<label class="tarot-house-mini-toggle" for="tarot-house-top-info-planet">
|
|
||||||
<input id="tarot-house-top-info-planet" type="checkbox" checked>
|
|
||||||
<span>Planet</span>
|
|
||||||
</label>
|
|
||||||
<label class="tarot-house-mini-toggle" for="tarot-house-top-info-zodiac">
|
|
||||||
<input id="tarot-house-top-info-zodiac" type="checkbox" checked>
|
|
||||||
<span>Zodiac</span>
|
|
||||||
</label>
|
|
||||||
<label class="tarot-house-mini-toggle" for="tarot-house-top-info-trump">
|
|
||||||
<input id="tarot-house-top-info-trump" type="checkbox" checked>
|
|
||||||
<span>Trump</span>
|
|
||||||
</label>
|
|
||||||
<label class="tarot-house-mini-toggle" for="tarot-house-top-info-path">
|
|
||||||
<input id="tarot-house-top-info-path" type="checkbox" checked>
|
|
||||||
<span>Path</span>
|
|
||||||
</label>
|
|
||||||
</fieldset>
|
|
||||||
<label class="tarot-house-toggle" for="tarot-house-bottom-cards-visible">
|
|
||||||
<input id="tarot-house-bottom-cards-visible" type="checkbox" checked>
|
|
||||||
<span>Show Bottom Cards</span>
|
|
||||||
</label>
|
|
||||||
<fieldset class="tarot-house-filter-group">
|
|
||||||
<legend>Bottom Info</legend>
|
|
||||||
<label class="tarot-house-mini-toggle" for="tarot-house-bottom-info-zodiac">
|
|
||||||
<input id="tarot-house-bottom-info-zodiac" type="checkbox" checked>
|
|
||||||
<span>Sign</span>
|
|
||||||
</label>
|
|
||||||
<label class="tarot-house-mini-toggle" for="tarot-house-bottom-info-decan">
|
|
||||||
<input id="tarot-house-bottom-info-decan" type="checkbox" checked>
|
|
||||||
<span>Decan</span>
|
|
||||||
</label>
|
|
||||||
<label class="tarot-house-mini-toggle" for="tarot-house-bottom-info-month">
|
|
||||||
<input id="tarot-house-bottom-info-month" type="checkbox" checked>
|
|
||||||
<span>Month</span>
|
|
||||||
</label>
|
|
||||||
<label class="tarot-house-mini-toggle" for="tarot-house-bottom-info-ruler">
|
|
||||||
<input id="tarot-house-bottom-info-ruler" type="checkbox" checked>
|
|
||||||
<span>Ruler</span>
|
|
||||||
</label>
|
|
||||||
<label class="tarot-house-mini-toggle" for="tarot-house-bottom-info-date">
|
|
||||||
<input id="tarot-house-bottom-info-date" type="checkbox">
|
|
||||||
<span>Date</span>
|
|
||||||
</label>
|
|
||||||
</fieldset>
|
|
||||||
<button id="tarot-house-focus-toggle" class="tarot-house-action-btn" type="button" aria-pressed="false">Focus House</button>
|
|
||||||
<button id="tarot-house-export" class="tarot-house-action-btn" type="button">Export PNG</button>
|
|
||||||
<button id="tarot-house-export-webp" class="tarot-house-action-btn" type="button">Export WebP</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="tarot-house-of-cards" class="tarot-house-layout" aria-live="polite"></div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<div class="tarot-layout">
|
<div class="tarot-layout">
|
||||||
<aside class="tarot-list-panel">
|
<aside class="tarot-list-panel">
|
||||||
<div class="tarot-list-header">
|
<div class="tarot-list-header">
|
||||||
@@ -365,6 +299,77 @@
|
|||||||
<div id="tarot-spread-board" class="tarot-spread-board" aria-live="polite"></div>
|
<div id="tarot-spread-board" class="tarot-spread-board" aria-live="polite"></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
||||||
|
<div class="tarot-meta-card tarot-house-card">
|
||||||
|
<div class="tarot-house-card-head">
|
||||||
|
<strong>House of Cards</strong>
|
||||||
|
<div class="tarot-house-card-actions">
|
||||||
|
<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>
|
||||||
|
</label>
|
||||||
|
<fieldset class="tarot-house-filter-group">
|
||||||
|
<legend>Top Info</legend>
|
||||||
|
<label class="tarot-house-mini-toggle" for="tarot-house-top-info-hebrew">
|
||||||
|
<input id="tarot-house-top-info-hebrew" type="checkbox" checked>
|
||||||
|
<span>Hebrew</span>
|
||||||
|
</label>
|
||||||
|
<label class="tarot-house-mini-toggle" for="tarot-house-top-info-planet">
|
||||||
|
<input id="tarot-house-top-info-planet" type="checkbox" checked>
|
||||||
|
<span>Planet</span>
|
||||||
|
</label>
|
||||||
|
<label class="tarot-house-mini-toggle" for="tarot-house-top-info-zodiac">
|
||||||
|
<input id="tarot-house-top-info-zodiac" type="checkbox" checked>
|
||||||
|
<span>Zodiac</span>
|
||||||
|
</label>
|
||||||
|
<label class="tarot-house-mini-toggle" for="tarot-house-top-info-trump">
|
||||||
|
<input id="tarot-house-top-info-trump" type="checkbox" checked>
|
||||||
|
<span>Trump</span>
|
||||||
|
</label>
|
||||||
|
<label class="tarot-house-mini-toggle" for="tarot-house-top-info-path">
|
||||||
|
<input id="tarot-house-top-info-path" type="checkbox" checked>
|
||||||
|
<span>Path</span>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<label class="tarot-house-toggle" for="tarot-house-bottom-cards-visible">
|
||||||
|
<input id="tarot-house-bottom-cards-visible" type="checkbox" checked>
|
||||||
|
<span>Show Bottom Cards</span>
|
||||||
|
</label>
|
||||||
|
<fieldset class="tarot-house-filter-group">
|
||||||
|
<legend>Bottom Info</legend>
|
||||||
|
<label class="tarot-house-mini-toggle" for="tarot-house-bottom-info-zodiac">
|
||||||
|
<input id="tarot-house-bottom-info-zodiac" type="checkbox" checked>
|
||||||
|
<span>Sign</span>
|
||||||
|
</label>
|
||||||
|
<label class="tarot-house-mini-toggle" for="tarot-house-bottom-info-decan">
|
||||||
|
<input id="tarot-house-bottom-info-decan" type="checkbox" checked>
|
||||||
|
<span>Decan</span>
|
||||||
|
</label>
|
||||||
|
<label class="tarot-house-mini-toggle" for="tarot-house-bottom-info-month">
|
||||||
|
<input id="tarot-house-bottom-info-month" type="checkbox" checked>
|
||||||
|
<span>Month</span>
|
||||||
|
</label>
|
||||||
|
<label class="tarot-house-mini-toggle" for="tarot-house-bottom-info-ruler">
|
||||||
|
<input id="tarot-house-bottom-info-ruler" type="checkbox" checked>
|
||||||
|
<span>Ruler</span>
|
||||||
|
</label>
|
||||||
|
<label class="tarot-house-mini-toggle" for="tarot-house-bottom-info-date">
|
||||||
|
<input id="tarot-house-bottom-info-date" type="checkbox">
|
||||||
|
<span>Date</span>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<button id="tarot-house-focus-toggle" class="tarot-house-action-btn" type="button" aria-pressed="false">Focus House</button>
|
||||||
|
<button id="tarot-house-export" class="tarot-house-action-btn" type="button">Export PNG</button>
|
||||||
|
<button id="tarot-house-export-webp" class="tarot-house-action-btn" type="button">Export WebP</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="tarot-house-of-cards" class="tarot-house-layout" aria-live="polite"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<section id="planet-section" hidden>
|
<section id="planet-section" hidden>
|
||||||
<div class="planet-layout">
|
<div class="planet-layout">
|
||||||
<aside class="planet-list-panel">
|
<aside class="planet-list-panel">
|
||||||
@@ -612,6 +617,7 @@
|
|||||||
<input id="kab-path-tarot-toggle" type="checkbox">
|
<input id="kab-path-tarot-toggle" type="checkbox">
|
||||||
<span>Show Tarot cards on paths</span>
|
<span>Show Tarot cards on paths</span>
|
||||||
</label>
|
</label>
|
||||||
|
<button id="kab-tree-export-webp" class="kab-export-btn" type="button">Export WebP</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="kab-tree-container" class="kab-tree-container"></div>
|
<div id="kab-tree-container" class="kab-tree-container"></div>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -770,6 +776,7 @@
|
|||||||
<button id="cube-rotate-down" class="cube-rotation-btn" type="button" title="Rotate down">↓</button>
|
<button id="cube-rotate-down" class="cube-rotation-btn" type="button" title="Rotate down">↓</button>
|
||||||
<button id="cube-rotate-reset" class="cube-rotation-btn" type="button" title="Reset rotation">Reset</button>
|
<button id="cube-rotate-reset" class="cube-rotation-btn" type="button" title="Reset rotation">Reset</button>
|
||||||
<button id="cube-focus-toggle" class="cube-rotation-btn cube-focus-toggle" type="button" aria-pressed="false">Focus Cube</button>
|
<button id="cube-focus-toggle" class="cube-rotation-btn cube-focus-toggle" type="button" aria-pressed="false">Focus Cube</button>
|
||||||
|
<button id="cube-export-webp" class="cube-rotation-btn cube-export-btn" type="button">Export WebP</button>
|
||||||
<div class="cube-marker-mode-control">
|
<div class="cube-marker-mode-control">
|
||||||
<label for="cube-marker-mode" class="cube-marker-mode-label">Marker display</label>
|
<label for="cube-marker-mode" class="cube-marker-mode-label">Marker display</label>
|
||||||
<select id="cube-marker-mode" class="cube-marker-mode-select" aria-label="Cube marker display mode">
|
<select id="cube-marker-mode" class="cube-marker-mode-select" aria-label="Cube marker display mode">
|
||||||
@@ -956,7 +963,7 @@
|
|||||||
<script src="app/calendar-events.js"></script>
|
<script src="app/calendar-events.js"></script>
|
||||||
<script src="app/card-images.js?v=20260309-gate"></script>
|
<script src="app/card-images.js?v=20260309-gate"></script>
|
||||||
<script src="app/ui-tarot-lightbox.js?v=20260312-compare-zoom-01"></script>
|
<script src="app/ui-tarot-lightbox.js?v=20260312-compare-zoom-01"></script>
|
||||||
<script src="app/ui-tarot-house.js?v=20260307b"></script>
|
<script src="app/ui-tarot-house.js?v=20260312-house-cube-01"></script>
|
||||||
<script src="app/ui-tarot-relations.js"></script>
|
<script src="app/ui-tarot-relations.js"></script>
|
||||||
<script src="app/ui-now-helpers.js"></script>
|
<script src="app/ui-now-helpers.js"></script>
|
||||||
<script src="app/ui-now.js"></script>
|
<script src="app/ui-now.js"></script>
|
||||||
@@ -975,7 +982,7 @@
|
|||||||
<script src="app/ui-tarot-card-derivations.js?v=20260307b"></script>
|
<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-detail.js?v=20260307b"></script>
|
||||||
<script src="app/ui-tarot-relation-display.js?v=20260307b"></script>
|
<script src="app/ui-tarot-relation-display.js?v=20260307b"></script>
|
||||||
<script src="app/ui-tarot.js?v=20260307b"></script>
|
<script src="app/ui-tarot.js?v=20260312-house-cube-01"></script>
|
||||||
<script src="app/ui-planets-references.js"></script>
|
<script src="app/ui-planets-references.js"></script>
|
||||||
<script src="app/ui-planets.js"></script>
|
<script src="app/ui-planets.js"></script>
|
||||||
<script src="app/ui-cycles.js"></script>
|
<script src="app/ui-cycles.js"></script>
|
||||||
@@ -985,12 +992,12 @@
|
|||||||
<script src="app/ui-rosicrucian-cross.js"></script>
|
<script src="app/ui-rosicrucian-cross.js"></script>
|
||||||
<script src="app/ui-kabbalah-detail.js"></script>
|
<script src="app/ui-kabbalah-detail.js"></script>
|
||||||
<script src="app/ui-kabbalah-views.js"></script>
|
<script src="app/ui-kabbalah-views.js"></script>
|
||||||
<script src="app/ui-kabbalah.js"></script>
|
<script src="app/ui-kabbalah.js?v=20260312-tree-export-01"></script>
|
||||||
<script src="app/ui-cube-detail.js"></script>
|
<script src="app/ui-cube-detail.js"></script>
|
||||||
<script src="app/ui-cube-chassis.js"></script>
|
<script src="app/ui-cube-chassis.js"></script>
|
||||||
<script src="app/ui-cube-math.js"></script>
|
<script src="app/ui-cube-math.js"></script>
|
||||||
<script src="app/ui-cube-selection.js"></script>
|
<script src="app/ui-cube-selection.js"></script>
|
||||||
<script src="app/ui-cube.js?v=20260310-cube-focus-01"></script>
|
<script src="app/ui-cube.js?v=20260312-house-cube-01"></script>
|
||||||
<script src="app/ui-alphabet-gematria.js?v=20260308b"></script>
|
<script src="app/ui-alphabet-gematria.js?v=20260308b"></script>
|
||||||
<script src="app/ui-alphabet-browser.js?v=20260309-enochian-api"></script>
|
<script src="app/ui-alphabet-browser.js?v=20260309-enochian-api"></script>
|
||||||
<script src="app/ui-alphabet-references.js"></script>
|
<script src="app/ui-alphabet-references.js"></script>
|
||||||
@@ -1014,13 +1021,13 @@
|
|||||||
<script src="app/ui-numbers.js"></script>
|
<script src="app/ui-numbers.js"></script>
|
||||||
<script src="app/ui-tarot-spread.js"></script>
|
<script src="app/ui-tarot-spread.js"></script>
|
||||||
<script src="app/ui-settings.js?v=20260309-gate"></script>
|
<script src="app/ui-settings.js?v=20260309-gate"></script>
|
||||||
<script src="app/ui-chrome.js?v=20260312-panel-toggle-02"></script>
|
<script src="app/ui-chrome.js?v=20260312-menu-unify-01"></script>
|
||||||
<script src="app/ui-navigation.js?v=20260309-alphabet-text-01"></script>
|
<script src="app/ui-navigation.js?v=20260312-house-cube-01"></script>
|
||||||
<script src="app/ui-calendar-formatting.js?v=20260307b"></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-calendar-visuals.js?v=20260307b"></script>
|
||||||
<script src="app/ui-home-calendar.js"></script>
|
<script src="app/ui-home-calendar.js"></script>
|
||||||
<script src="app/ui-section-state.js?v=20260309-alphabet-text-01"></script>
|
<script src="app/ui-section-state.js?v=20260312-house-cube-01"></script>
|
||||||
<script src="app/app-runtime.js?v=20260309-gate"></script>
|
<script src="app/app-runtime.js?v=20260309-gate"></script>
|
||||||
<script src="app.js?v=20260309-alphabet-text-01"></script>
|
<script src="app.js?v=20260312-house-cube-01"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user