update sequence nav to use new ui-sequence-nav component
This commit is contained in:
+125
@@ -2050,6 +2050,34 @@
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.tarot-frame-card-action-deck {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 8px 2px 2px;
|
||||
color: #cbd5e1;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.tarot-frame-card-action-deck-select {
|
||||
width: 100%;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid rgba(99, 102, 241, 0.34);
|
||||
border-radius: 12px;
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
color: #f8fafc;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tarot-frame-card-action-deck-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tarot-frame-card-custom-editor {
|
||||
position: fixed;
|
||||
z-index: 42;
|
||||
@@ -3357,6 +3385,15 @@
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.detail-pane-export-controls {
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.detail-export-btn {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.planet-meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
@@ -4771,6 +4808,90 @@
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.alpha-gematria-keyboard {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
border: 1px solid #2f2f39;
|
||||
border-radius: 12px;
|
||||
background: #0f0f17;
|
||||
}
|
||||
|
||||
.alpha-gematria-keyboard[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alpha-gematria-keyboard-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.alpha-gematria-keyboard-label {
|
||||
color: #a1a1aa;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.alpha-gematria-keyboard-script {
|
||||
min-width: 140px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #3f3f46;
|
||||
background: #09090b;
|
||||
color: #f4f4f5;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.alpha-gematria-keyboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(9, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.alpha-gematria-keyboard-grid.is-disabled {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.alpha-gematria-keyboard-key,
|
||||
.alpha-gematria-keyboard-action {
|
||||
border: 1px solid #3f3f46;
|
||||
border-radius: 8px;
|
||||
background: #161622;
|
||||
color: #f4f4f5;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
min-height: 36px;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.alpha-gematria-keyboard-key:hover,
|
||||
.alpha-gematria-keyboard-action:hover {
|
||||
border-color: #6366f1;
|
||||
background: #1d1d31;
|
||||
}
|
||||
|
||||
.alpha-gematria-keyboard-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.alpha-gematria-keyboard-actions.is-disabled {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.alpha-gematria-keyboard-action {
|
||||
font-size: 12px;
|
||||
min-height: 34px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.alpha-gematria-matches {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
@@ -4865,6 +4986,10 @@
|
||||
.alpha-gematria-controls.is-reverse-cipher-mode {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.alpha-gematria-keyboard-actions {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.alpha-tabs {
|
||||
|
||||
+359
-29
@@ -14,7 +14,11 @@
|
||||
inputLabelEl: null,
|
||||
cipherLabelEl: null,
|
||||
reverseCiphersEl: null,
|
||||
reverseCipherHintEl: null
|
||||
reverseCipherHintEl: null,
|
||||
keyboardEl: null,
|
||||
keyboardScriptEl: null,
|
||||
keyboardGridEl: null,
|
||||
keyboardActionsEl: null
|
||||
})
|
||||
};
|
||||
|
||||
@@ -35,7 +39,8 @@
|
||||
dictionaryLookupCache: new Map(),
|
||||
reverseRequestId: 0,
|
||||
anagramRequestId: 0,
|
||||
dictionaryRequestId: 0
|
||||
dictionaryRequestId: 0,
|
||||
keyboardScriptId: "english"
|
||||
};
|
||||
|
||||
function getAlphabets() {
|
||||
@@ -57,10 +62,184 @@
|
||||
inputLabelEl: null,
|
||||
cipherLabelEl: null,
|
||||
reverseCiphersEl: null,
|
||||
reverseCipherHintEl: null
|
||||
reverseCipherHintEl: null,
|
||||
keyboardEl: null,
|
||||
keyboardScriptEl: null,
|
||||
keyboardGridEl: null,
|
||||
keyboardActionsEl: null
|
||||
};
|
||||
}
|
||||
|
||||
function uniqueChars(values) {
|
||||
return Array.from(new Set((Array.isArray(values) ? values : [])
|
||||
.map((value) => String(value || "").trim())
|
||||
.filter(Boolean)));
|
||||
}
|
||||
|
||||
function collectAlphabetChars(entries, keys) {
|
||||
const sourceEntries = Array.isArray(entries) ? entries : [];
|
||||
const sourceKeys = Array.isArray(keys) ? keys : [];
|
||||
const chars = [];
|
||||
|
||||
sourceEntries.forEach((entry) => {
|
||||
sourceKeys.forEach((key) => {
|
||||
const value = String(entry?.[key] || "").trim();
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstChar = [...value][0] || "";
|
||||
if (firstChar) {
|
||||
chars.push(firstChar);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return uniqueChars(chars);
|
||||
}
|
||||
|
||||
function getKeyboardLayouts() {
|
||||
const db = state.db || getFallbackGematriaDb();
|
||||
const alphabets = getAlphabets() || {};
|
||||
const englishChars = uniqueChars(String(db.baseAlphabet || "abcdefghijklmnopqrstuvwxyz").toLowerCase().split(""));
|
||||
const hebrewChars = collectAlphabetChars(alphabets.hebrew, ["char"]);
|
||||
const greekChars = collectAlphabetChars([...(alphabets.greek || []), ...(alphabets.greekArchaic || [])], ["charLower", "char", "charFinal"]);
|
||||
const arabicChars = collectAlphabetChars(alphabets.arabic, ["char"]);
|
||||
const enochianChars = collectAlphabetChars(alphabets.enochian, ["char"]);
|
||||
|
||||
return [
|
||||
{ id: "english", label: "English", chars: englishChars },
|
||||
{ id: "hebrew", label: "Hebrew", chars: hebrewChars },
|
||||
{ id: "greek", label: "Greek", chars: greekChars },
|
||||
{ id: "arabic", label: "Arabic", chars: arabicChars },
|
||||
{ id: "enochian", label: "Enochian", chars: enochianChars }
|
||||
].filter((layout) => layout.chars.length > 0);
|
||||
}
|
||||
|
||||
function setGematriaInputValue(nextValue) {
|
||||
const { inputEl } = getElements();
|
||||
if (!(inputEl instanceof HTMLTextAreaElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
inputEl.value = String(nextValue || "");
|
||||
inputEl.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
inputEl.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
function insertGematriaText(insertValue) {
|
||||
const { inputEl } = getElements();
|
||||
if (!(inputEl instanceof HTMLTextAreaElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = String(insertValue || "");
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
const start = Number.isFinite(inputEl.selectionStart) ? inputEl.selectionStart : inputEl.value.length;
|
||||
const end = Number.isFinite(inputEl.selectionEnd) ? inputEl.selectionEnd : start;
|
||||
const nextValue = `${inputEl.value.slice(0, start)}${text}${inputEl.value.slice(end)}`;
|
||||
setGematriaInputValue(nextValue);
|
||||
|
||||
const nextCaret = start + text.length;
|
||||
inputEl.setSelectionRange(nextCaret, nextCaret);
|
||||
}
|
||||
|
||||
function backspaceGematriaText() {
|
||||
const { inputEl } = getElements();
|
||||
if (!(inputEl instanceof HTMLTextAreaElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const start = Number.isFinite(inputEl.selectionStart) ? inputEl.selectionStart : inputEl.value.length;
|
||||
const end = Number.isFinite(inputEl.selectionEnd) ? inputEl.selectionEnd : start;
|
||||
if (start === 0 && end === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let nextStart = start;
|
||||
let nextEnd = end;
|
||||
if (start === end) {
|
||||
nextStart = Math.max(0, start - 1);
|
||||
}
|
||||
|
||||
const nextValue = `${inputEl.value.slice(0, nextStart)}${inputEl.value.slice(nextEnd)}`;
|
||||
setGematriaInputValue(nextValue);
|
||||
inputEl.setSelectionRange(nextStart, nextStart);
|
||||
}
|
||||
|
||||
function renderKeyboardLayout() {
|
||||
const { keyboardScriptEl, keyboardGridEl } = getElements();
|
||||
if (!(keyboardScriptEl instanceof HTMLSelectElement) || !(keyboardGridEl instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const layouts = getKeyboardLayouts();
|
||||
if (!layouts.length) {
|
||||
keyboardScriptEl.replaceChildren();
|
||||
keyboardGridEl.replaceChildren();
|
||||
return;
|
||||
}
|
||||
|
||||
const preferredLayoutId = String(state.keyboardScriptId || "english").trim();
|
||||
const activeLayout = layouts.find((layout) => layout.id === preferredLayoutId) || layouts[0];
|
||||
state.keyboardScriptId = activeLayout.id;
|
||||
|
||||
keyboardScriptEl.replaceChildren();
|
||||
layouts.forEach((layout) => {
|
||||
const optionEl = document.createElement("option");
|
||||
optionEl.value = layout.id;
|
||||
optionEl.textContent = layout.label;
|
||||
keyboardScriptEl.appendChild(optionEl);
|
||||
});
|
||||
keyboardScriptEl.value = state.keyboardScriptId;
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
activeLayout.chars.forEach((char) => {
|
||||
const buttonEl = document.createElement("button");
|
||||
buttonEl.type = "button";
|
||||
buttonEl.className = "alpha-gematria-keyboard-key";
|
||||
buttonEl.dataset.keyboardInsert = char;
|
||||
buttonEl.textContent = char;
|
||||
fragment.appendChild(buttonEl);
|
||||
});
|
||||
keyboardGridEl.replaceChildren(fragment);
|
||||
keyboardGridEl.setAttribute("lang", state.keyboardScriptId);
|
||||
}
|
||||
|
||||
function handleKeyboardAction(action) {
|
||||
const normalizedAction = String(action || "").trim().toLowerCase();
|
||||
if (!normalizedAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalizedAction === "backspace") {
|
||||
backspaceGematriaText();
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalizedAction === "space") {
|
||||
insertGematriaText(" ");
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalizedAction === "star") {
|
||||
insertGematriaText("*");
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalizedAction === "question") {
|
||||
insertGematriaText("?");
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalizedAction === "clear") {
|
||||
setGematriaInputValue("");
|
||||
}
|
||||
}
|
||||
|
||||
function isReverseMode() {
|
||||
return state.activeMode === "reverse";
|
||||
}
|
||||
@@ -142,14 +321,57 @@
|
||||
|
||||
function addScriptCharMapEntry(map, scriptChar, mappedLetters) {
|
||||
const key = String(scriptChar || "").trim();
|
||||
const value = String(mappedLetters || "").trim();
|
||||
if (!key || !value) {
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = typeof mappedLetters === "string"
|
||||
? { mappedLetters: String(mappedLetters || "").trim() }
|
||||
: mappedLetters;
|
||||
if (!value || (typeof value === "object" && !Number.isFinite(Number(value.ordinal)) && !String(value.mappedLetters || "").trim())) {
|
||||
return;
|
||||
}
|
||||
|
||||
map.set(key, value);
|
||||
}
|
||||
|
||||
function getEntryOrdinal(entry, fallbackOrdinal) {
|
||||
const explicitOrdinal = Number(entry?.index);
|
||||
if (Number.isFinite(explicitOrdinal) && explicitOrdinal > 0) {
|
||||
return Math.trunc(explicitOrdinal);
|
||||
}
|
||||
return Math.trunc(Number(fallbackOrdinal) || 0);
|
||||
}
|
||||
|
||||
function addScriptOrdinalEntries(map, entries, charKeys) {
|
||||
const sourceEntries = Array.isArray(entries) ? entries : [];
|
||||
const sourceCharKeys = Array.isArray(charKeys) ? charKeys : [];
|
||||
|
||||
sourceEntries.forEach((entry, entryIndex) => {
|
||||
const ordinal = getEntryOrdinal(entry, entryIndex + 1);
|
||||
if (!ordinal) {
|
||||
return;
|
||||
}
|
||||
|
||||
sourceCharKeys.forEach((charKey) => {
|
||||
const charValue = String(entry?.[charKey] || "").trim();
|
||||
if (!charValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstChar = [...charValue][0] || "";
|
||||
if (!firstChar) {
|
||||
return;
|
||||
}
|
||||
|
||||
addScriptCharMapEntry(map, firstChar, {
|
||||
ordinal,
|
||||
displayChar: firstChar
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function buildGematriaScriptMap(baseAlphabet) {
|
||||
const map = new Map();
|
||||
const alphabets = getAlphabets() || {};
|
||||
@@ -157,35 +379,51 @@
|
||||
const greekClassicalLetters = Array.isArray(alphabets.greek) ? alphabets.greek : [];
|
||||
const greekArchaicLetters = Array.isArray(alphabets.greekArchaic) ? alphabets.greekArchaic : [];
|
||||
const greekLetters = [...greekClassicalLetters, ...greekArchaicLetters];
|
||||
const arabicLetters = Array.isArray(alphabets.arabic) ? alphabets.arabic : [];
|
||||
const enochianLetters = Array.isArray(alphabets.enochian) ? alphabets.enochian : [];
|
||||
|
||||
addScriptOrdinalEntries(map, hebrewLetters, ["char"]);
|
||||
addScriptOrdinalEntries(map, greekLetters, ["char", "charLower", "charFinal"]);
|
||||
addScriptOrdinalEntries(map, arabicLetters, ["char"]);
|
||||
addScriptOrdinalEntries(map, enochianLetters, ["char"]);
|
||||
|
||||
hebrewLetters.forEach((entry) => {
|
||||
const mapped = transliterationToBaseLetters(entry?.transliteration, baseAlphabet);
|
||||
addScriptCharMapEntry(map, entry?.char, mapped);
|
||||
if (!map.has(String(entry?.char || "").trim())) {
|
||||
addScriptCharMapEntry(map, entry?.char, mapped);
|
||||
}
|
||||
});
|
||||
|
||||
greekLetters.forEach((entry) => {
|
||||
const mapped = transliterationToBaseLetters(entry?.transliteration, baseAlphabet);
|
||||
addScriptCharMapEntry(map, entry?.char, mapped);
|
||||
addScriptCharMapEntry(map, entry?.charLower, mapped);
|
||||
addScriptCharMapEntry(map, entry?.charFinal, mapped);
|
||||
});
|
||||
|
||||
const hebrewFinalForms = {
|
||||
ך: "k",
|
||||
ם: "m",
|
||||
ן: "n",
|
||||
ף: "p",
|
||||
ץ: "t"
|
||||
};
|
||||
|
||||
Object.entries(hebrewFinalForms).forEach(([char, mapped]) => {
|
||||
if (!map.has(char) && baseAlphabet.includes(mapped)) {
|
||||
addScriptCharMapEntry(map, char, mapped);
|
||||
if (!map.has(String(entry?.char || "").trim())) {
|
||||
addScriptCharMapEntry(map, entry?.char, mapped);
|
||||
}
|
||||
if (!map.has(String(entry?.charLower || "").trim())) {
|
||||
addScriptCharMapEntry(map, entry?.charLower, mapped);
|
||||
}
|
||||
if (!map.has(String(entry?.charFinal || "").trim())) {
|
||||
addScriptCharMapEntry(map, entry?.charFinal, mapped);
|
||||
}
|
||||
});
|
||||
|
||||
if (!map.has("ς") && baseAlphabet.includes("s")) {
|
||||
addScriptCharMapEntry(map, "ς", "s");
|
||||
const hebrewFinalForms = {
|
||||
ך: "כ",
|
||||
ם: "מ",
|
||||
ן: "נ",
|
||||
ף: "פ",
|
||||
ץ: "צ"
|
||||
};
|
||||
|
||||
Object.entries(hebrewFinalForms).forEach(([char, sourceChar]) => {
|
||||
const sourceEntry = map.get(sourceChar);
|
||||
if (!map.has(char) && sourceEntry) {
|
||||
addScriptCharMapEntry(map, char, sourceEntry);
|
||||
}
|
||||
});
|
||||
|
||||
if (!map.has("ς") && map.has("σ")) {
|
||||
addScriptCharMapEntry(map, "ς", map.get("σ"));
|
||||
}
|
||||
|
||||
return map;
|
||||
@@ -423,7 +661,11 @@
|
||||
inputLabelEl,
|
||||
cipherLabelEl,
|
||||
reverseCiphersEl,
|
||||
reverseCipherHintEl
|
||||
reverseCipherHintEl,
|
||||
keyboardEl,
|
||||
keyboardScriptEl,
|
||||
keyboardGridEl,
|
||||
keyboardActionsEl
|
||||
} = getElements();
|
||||
|
||||
const reverseMode = isReverseMode();
|
||||
@@ -483,9 +725,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (keyboardEl) {
|
||||
keyboardEl.hidden = reverseMode;
|
||||
}
|
||||
|
||||
if (keyboardScriptEl) {
|
||||
keyboardScriptEl.disabled = reverseMode;
|
||||
}
|
||||
|
||||
if (keyboardGridEl) {
|
||||
keyboardGridEl.classList.toggle("is-disabled", reverseMode);
|
||||
}
|
||||
|
||||
if (keyboardActionsEl) {
|
||||
keyboardActionsEl.classList.toggle("is-disabled", reverseMode);
|
||||
}
|
||||
|
||||
if (!reverseMode && !anagramMode && !dictionaryMode) {
|
||||
clearReverseMatches(matchesEl);
|
||||
}
|
||||
|
||||
if (!reverseMode) {
|
||||
renderKeyboardLayout();
|
||||
}
|
||||
}
|
||||
|
||||
function parseReverseLookupValue(rawValue) {
|
||||
@@ -901,10 +1163,44 @@
|
||||
let count = 0;
|
||||
|
||||
[...normalizedInput].forEach((char) => {
|
||||
const mappedLetters = baseAlphabet.includes(char)
|
||||
? char
|
||||
: (scriptMap.get(char) || "");
|
||||
if (baseAlphabet.includes(char)) {
|
||||
const index = baseAlphabet.indexOf(char);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = Number(cipher.values[index]);
|
||||
if (!Number.isFinite(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
total += value;
|
||||
letterParts.push(`${char.toUpperCase()}(${value})`);
|
||||
return;
|
||||
}
|
||||
|
||||
const mappedEntry = scriptMap.get(char);
|
||||
if (!mappedEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ordinal = Number(mappedEntry.ordinal);
|
||||
if (Number.isFinite(ordinal) && ordinal > 0) {
|
||||
const index = Math.trunc(ordinal) - 1;
|
||||
const value = Number(cipher.values[index]);
|
||||
if (!Number.isFinite(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const displayChar = String(mappedEntry.displayChar || char || "").trim() || char;
|
||||
count += 1;
|
||||
total += value;
|
||||
letterParts.push(`${displayChar}(${value})`);
|
||||
return;
|
||||
}
|
||||
|
||||
const mappedLetters = String(mappedEntry.mappedLetters || "");
|
||||
if (!mappedLetters) {
|
||||
return;
|
||||
}
|
||||
@@ -984,7 +1280,7 @@
|
||||
}
|
||||
|
||||
function bindGematriaListeners() {
|
||||
const { cipherEl, inputEl, modeEls, reverseCiphersEl } = getElements();
|
||||
const { cipherEl, inputEl, modeEls, reverseCiphersEl, keyboardScriptEl, keyboardGridEl, keyboardActionsEl } = getElements();
|
||||
if (state.listenersBound || !cipherEl || !inputEl) {
|
||||
return;
|
||||
}
|
||||
@@ -1033,6 +1329,39 @@
|
||||
renderGematriaResult();
|
||||
});
|
||||
|
||||
keyboardScriptEl?.addEventListener("change", () => {
|
||||
state.keyboardScriptId = String(keyboardScriptEl.value || "english").trim() || "english";
|
||||
renderKeyboardLayout();
|
||||
});
|
||||
|
||||
keyboardGridEl?.addEventListener("click", (event) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keyButton = target.closest("[data-keyboard-insert]");
|
||||
if (!(keyButton instanceof HTMLButtonElement) || keyButton.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
insertGematriaText(String(keyButton.dataset.keyboardInsert || ""));
|
||||
});
|
||||
|
||||
keyboardActionsEl?.addEventListener("click", (event) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actionButton = target.closest("[data-keyboard-action]");
|
||||
if (!(actionButton instanceof HTMLButtonElement) || actionButton.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleKeyboardAction(actionButton.dataset.keyboardAction);
|
||||
});
|
||||
|
||||
state.listenersBound = true;
|
||||
}
|
||||
|
||||
@@ -1048,6 +1377,7 @@
|
||||
void loadGematriaDb().then(() => {
|
||||
refreshScriptMap((state.db || getFallbackGematriaDb()).baseAlphabet);
|
||||
renderGematriaCipherOptions();
|
||||
renderKeyboardLayout();
|
||||
renderGematriaResult();
|
||||
});
|
||||
}
|
||||
|
||||
+10
-1
@@ -57,6 +57,7 @@
|
||||
let gematriaCipherEl, gematriaInputEl, gematriaResultEl, gematriaBreakdownEl;
|
||||
let gematriaModeEls, gematriaMatchesEl, gematriaInputLabelEl, gematriaCipherLabelEl;
|
||||
let gematriaReverseCiphersEl, gematriaReverseCipherHintEl;
|
||||
let gematriaKeyboardEl, gematriaKeyboardScriptEl, gematriaKeyboardGridEl, gematriaKeyboardActionsEl;
|
||||
|
||||
function getElements() {
|
||||
alphabetLettersSectionEl = document.getElementById("alphabet-letters-section");
|
||||
@@ -88,6 +89,10 @@
|
||||
gematriaCipherLabelEl = document.getElementById("alpha-gematria-cipher-label");
|
||||
gematriaReverseCiphersEl = document.getElementById("alpha-gematria-reverse-ciphers");
|
||||
gematriaReverseCipherHintEl = document.getElementById("alpha-gematria-reverse-cipher-hint");
|
||||
gematriaKeyboardEl = document.getElementById("alpha-gematria-keyboard");
|
||||
gematriaKeyboardScriptEl = document.getElementById("alpha-gematria-keyboard-script");
|
||||
gematriaKeyboardGridEl = document.getElementById("alpha-gematria-keyboard-grid");
|
||||
gematriaKeyboardActionsEl = document.getElementById("alpha-gematria-keyboard-actions");
|
||||
}
|
||||
|
||||
function getGematriaElements() {
|
||||
@@ -102,7 +107,11 @@
|
||||
inputLabelEl: gematriaInputLabelEl,
|
||||
cipherLabelEl: gematriaCipherLabelEl,
|
||||
reverseCiphersEl: gematriaReverseCiphersEl,
|
||||
reverseCipherHintEl: gematriaReverseCipherHintEl
|
||||
reverseCipherHintEl: gematriaReverseCipherHintEl,
|
||||
keyboardEl: gematriaKeyboardEl,
|
||||
keyboardScriptEl: gematriaKeyboardScriptEl,
|
||||
keyboardGridEl: gematriaKeyboardGridEl,
|
||||
keyboardActionsEl: gematriaKeyboardActionsEl
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
const sidebarControllers = new WeakMap();
|
||||
const sidebarControllerRegistry = new Map();
|
||||
const detailControllers = new WeakMap();
|
||||
const detailExportInProgress = new WeakSet();
|
||||
const AUTO_COLLAPSE_ENTRY_SELECTOR = [
|
||||
".planet-list-item",
|
||||
".tarot-list-item",
|
||||
@@ -403,6 +404,7 @@
|
||||
const sectionId = layout.closest("section")?.id || `layout-${index + 1}`;
|
||||
const panelId = detailPanel.id || `${sectionId}-detail-panel`;
|
||||
detailPanel.id = panelId;
|
||||
ensureDetailPaneExportControl(detailPanel, sectionId);
|
||||
|
||||
const detailStorageKey = `${DETAIL_COLLAPSE_STORAGE_PREFIX}${sectionId}`;
|
||||
const sidebarStorageKey = `${SIDEBAR_COLLAPSE_STORAGE_PREFIX}${sectionId}`;
|
||||
@@ -439,6 +441,166 @@
|
||||
});
|
||||
}
|
||||
|
||||
function sanitizeExportToken(value, fallback = "detail") {
|
||||
const normalized = String(value || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
return normalized || fallback;
|
||||
}
|
||||
|
||||
function canvasToWebpBlob(canvas, quality = 1) {
|
||||
return new Promise((resolve, reject) => {
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
return;
|
||||
}
|
||||
reject(new Error("Detail pane export failed."));
|
||||
}, "image/webp", quality);
|
||||
});
|
||||
}
|
||||
|
||||
async function exportDetailPaneAsWebp(detailPanel, sectionId, buttonEl = null) {
|
||||
if (!(detailPanel instanceof HTMLElement) || detailExportInProgress.has(detailPanel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
detailExportInProgress.add(detailPanel);
|
||||
const originalButtonLabel = buttonEl instanceof HTMLButtonElement ? buttonEl.textContent : "";
|
||||
if (buttonEl instanceof HTMLButtonElement) {
|
||||
buttonEl.disabled = true;
|
||||
buttonEl.textContent = "Exporting...";
|
||||
}
|
||||
|
||||
let exportBlobUrl = "";
|
||||
let sandboxEl = null;
|
||||
|
||||
try {
|
||||
const html2canvas = window.html2canvas;
|
||||
if (typeof html2canvas !== "function") {
|
||||
throw new Error("Detail export library is unavailable. Refresh the page and try again.");
|
||||
}
|
||||
|
||||
const exportClone = detailPanel.cloneNode(true);
|
||||
if (!(exportClone instanceof HTMLElement)) {
|
||||
throw new Error("Detail pane is not ready to export.");
|
||||
}
|
||||
|
||||
exportClone.querySelectorAll(".detail-pane-export-controls").forEach((node) => node.remove());
|
||||
|
||||
const contentWidth = Math.max(
|
||||
320,
|
||||
Math.ceil(detailPanel.clientWidth || detailPanel.getBoundingClientRect().width || 0)
|
||||
);
|
||||
const contentHeight = Math.max(
|
||||
240,
|
||||
Math.ceil(detailPanel.scrollHeight || detailPanel.getBoundingClientRect().height || 0)
|
||||
);
|
||||
|
||||
sandboxEl = document.createElement("div");
|
||||
sandboxEl.style.position = "fixed";
|
||||
sandboxEl.style.left = "-100000px";
|
||||
sandboxEl.style.top = "0";
|
||||
sandboxEl.style.width = `${contentWidth}px`;
|
||||
sandboxEl.style.height = `${contentHeight}px`;
|
||||
sandboxEl.style.overflow = "hidden";
|
||||
sandboxEl.style.pointerEvents = "none";
|
||||
sandboxEl.style.opacity = "0";
|
||||
|
||||
exportClone.style.width = `${contentWidth}px`;
|
||||
exportClone.style.minWidth = `${contentWidth}px`;
|
||||
exportClone.style.maxWidth = `${contentWidth}px`;
|
||||
exportClone.style.height = `${contentHeight}px`;
|
||||
exportClone.style.minHeight = `${contentHeight}px`;
|
||||
exportClone.style.maxHeight = `${contentHeight}px`;
|
||||
exportClone.style.overflow = "visible";
|
||||
|
||||
sandboxEl.appendChild(exportClone);
|
||||
document.body.appendChild(sandboxEl);
|
||||
|
||||
const exportScale = Math.max(2, Math.min(2.5, Number(window.devicePixelRatio) || 1));
|
||||
const canvas = await html2canvas(exportClone, {
|
||||
backgroundColor: null,
|
||||
scale: exportScale,
|
||||
useCORS: true,
|
||||
allowTaint: false,
|
||||
logging: false,
|
||||
width: contentWidth,
|
||||
height: contentHeight,
|
||||
windowWidth: contentWidth,
|
||||
windowHeight: contentHeight,
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
imageTimeout: 12000,
|
||||
onclone(clonedDocument) {
|
||||
clonedDocument.querySelectorAll(".detail-pane-export-controls").forEach((node) => node.remove());
|
||||
}
|
||||
});
|
||||
|
||||
const exportBlob = await canvasToWebpBlob(canvas, 1);
|
||||
exportBlobUrl = URL.createObjectURL(exportBlob);
|
||||
|
||||
const headingEl = detailPanel.querySelector("h1, h2, h3");
|
||||
const labelToken = sanitizeExportToken(headingEl?.textContent || detailPanel.id || "detail-pane", "detail-pane");
|
||||
const sectionToken = sanitizeExportToken(sectionId, "section");
|
||||
const dateToken = new Date().toISOString().slice(0, 10);
|
||||
|
||||
const downloadLink = document.createElement("a");
|
||||
downloadLink.href = exportBlobUrl;
|
||||
downloadLink.download = `${sectionToken}-${labelToken}-${dateToken}.webp`;
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
downloadLink.remove();
|
||||
} catch (error) {
|
||||
window.alert(error instanceof Error ? error.message : "Unable to export this detail pane.");
|
||||
} finally {
|
||||
detailExportInProgress.delete(detailPanel);
|
||||
if (buttonEl instanceof HTMLButtonElement) {
|
||||
buttonEl.disabled = false;
|
||||
buttonEl.textContent = originalButtonLabel || "Export WebP";
|
||||
}
|
||||
|
||||
if (sandboxEl instanceof HTMLElement) {
|
||||
sandboxEl.remove();
|
||||
}
|
||||
|
||||
if (exportBlobUrl) {
|
||||
URL.revokeObjectURL(exportBlobUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensureDetailPaneExportControl(detailPanel, sectionId) {
|
||||
if (!(detailPanel instanceof HTMLElement) || detailPanel.querySelector(".detail-pane-export-controls")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hostEl = detailPanel.querySelector(".detail-sequence-nav")
|
||||
|| detailPanel.querySelector(".planet-detail-heading")
|
||||
|| detailPanel.querySelector(".tarot-detail-heading")
|
||||
|| detailPanel.querySelector(".kab-detail-heading")
|
||||
|| detailPanel;
|
||||
if (!(hostEl instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const controlsEl = document.createElement("div");
|
||||
controlsEl.className = "detail-pane-export-controls";
|
||||
|
||||
const exportButtonEl = document.createElement("button");
|
||||
exportButtonEl.type = "button";
|
||||
exportButtonEl.className = "detail-sequence-btn detail-export-btn";
|
||||
exportButtonEl.textContent = "Export WebP";
|
||||
exportButtonEl.addEventListener("click", () => {
|
||||
exportDetailPaneAsWebp(detailPanel, sectionId, exportButtonEl);
|
||||
});
|
||||
|
||||
controlsEl.appendChild(exportButtonEl);
|
||||
hostEl.appendChild(controlsEl);
|
||||
}
|
||||
|
||||
function setTopbarDropdownOpen(dropdownEl, isOpen) {
|
||||
if (!(dropdownEl instanceof HTMLElement)) {
|
||||
return;
|
||||
|
||||
+380
-89
@@ -62,7 +62,7 @@
|
||||
const FRAME_TOUCH_DRAG_ACTIVATION_DELAY_MS = 140;
|
||||
const FRAME_CUSTOM_CARD_PREFIX = "frame-custom-text:";
|
||||
const FRAME_CUSTOM_CARD_ACTION_ID = "create-custom-text";
|
||||
const EXPORT_SLOT_WIDTH = 120;
|
||||
const EXPORT_SLOT_WIDTH = 180;
|
||||
const EXPORT_SLOT_HEIGHT = Math.round((EXPORT_SLOT_WIDTH * TAROT_CARD_HEIGHT_RATIO) / TAROT_CARD_WIDTH_RATIO);
|
||||
const EXPORT_CARD_INSET = 0;
|
||||
const EXPORT_GRID_GAP = 10;
|
||||
@@ -76,7 +76,7 @@
|
||||
webp: {
|
||||
mimeType: "image/webp",
|
||||
extension: "webp",
|
||||
quality: 0.98
|
||||
quality: 1
|
||||
}
|
||||
};
|
||||
const FRAME_LAYOUT_GROUPS = [
|
||||
@@ -226,6 +226,8 @@
|
||||
currentLayoutId: "frames",
|
||||
customLayouts: [],
|
||||
customCards: new Map(),
|
||||
slotDeckOverrides: new Map(),
|
||||
frameDeckId: "",
|
||||
layoutNotesById: {},
|
||||
exportInProgress: false,
|
||||
exportFormat: "webp",
|
||||
@@ -253,6 +255,9 @@
|
||||
let cardActionMenuFlipEl = null;
|
||||
let cardActionMenuOpenEl = null;
|
||||
let cardActionMenuRemoveEl = null;
|
||||
let cardActionMenuDeckApplyEl = null;
|
||||
let cardActionMenuDeckResetEl = null;
|
||||
let cardActionMenuDeckSelectEl = null;
|
||||
let cardPickerEl = null;
|
||||
let cardPickerTitleEl = null;
|
||||
let cardPickerSearchEl = null;
|
||||
@@ -307,6 +312,7 @@
|
||||
tarotFrameSettingsToggleEl: document.getElementById("tarot-frame-settings-toggle"),
|
||||
tarotFrameSettingsPanelEl: document.getElementById("tarot-frame-settings-panel"),
|
||||
tarotFrameGridZoomEl: document.getElementById("tarot-frame-grid-zoom"),
|
||||
tarotFrameDeckSelectEl: document.getElementById("tarot-frame-deck-select"),
|
||||
tarotFrameShowInfoEl: document.getElementById("tarot-frame-show-info"),
|
||||
tarotFrameHouseSettingsEl: document.getElementById("tarot-frame-house-settings"),
|
||||
tarotFrameHouseTopCardsVisibleEl: document.getElementById("tarot-frame-house-top-cards-visible"),
|
||||
@@ -542,12 +548,130 @@
|
||||
return (leftSlot.row - rightSlot.row) || (leftSlot.column - rightSlot.column);
|
||||
}
|
||||
|
||||
function normalizeDeckIdValue(deckId) {
|
||||
return String(deckId || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function getDeckOptionsList() {
|
||||
const options = Array.isArray(tarotCardImages.getDeckOptions?.())
|
||||
? tarotCardImages.getDeckOptions()
|
||||
: [];
|
||||
|
||||
const seen = new Set();
|
||||
return options
|
||||
.map((option) => ({
|
||||
id: normalizeDeckIdValue(option?.id),
|
||||
label: normalizeLabelText(option?.label || option?.id)
|
||||
}))
|
||||
.filter((option) => {
|
||||
if (!option.id || seen.has(option.id)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(option.id);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function getFallbackDeckId() {
|
||||
const options = getDeckOptionsList();
|
||||
const activeDeckId = normalizeDeckIdValue(tarotCardImages.getActiveDeck?.());
|
||||
if (activeDeckId && options.some((option) => option.id === activeDeckId)) {
|
||||
return activeDeckId;
|
||||
}
|
||||
return options[0]?.id || "";
|
||||
}
|
||||
|
||||
function normalizeFrameDeckId(deckId) {
|
||||
const normalizedDeckId = normalizeDeckIdValue(deckId);
|
||||
if (!normalizedDeckId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return getDeckOptionsList().some((option) => option.id === normalizedDeckId)
|
||||
? normalizedDeckId
|
||||
: "";
|
||||
}
|
||||
|
||||
function getFrameDeckId() {
|
||||
const normalized = normalizeFrameDeckId(state.frameDeckId);
|
||||
return normalized || getFallbackDeckId();
|
||||
}
|
||||
|
||||
function getSlotDeckOverride(slotId) {
|
||||
const targetSlotId = String(slotId || "").trim();
|
||||
if (!isValidSlotId(targetSlotId)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return normalizeFrameDeckId(state.slotDeckOverrides.get(targetSlotId));
|
||||
}
|
||||
|
||||
function getResolvedSlotDeckId(slotId) {
|
||||
return getSlotDeckOverride(slotId) || getFrameDeckId();
|
||||
}
|
||||
|
||||
function setFrameDeckId(deckId) {
|
||||
const nextDeckId = normalizeFrameDeckId(deckId);
|
||||
const previousDeckId = normalizeFrameDeckId(state.frameDeckId);
|
||||
state.frameDeckId = nextDeckId;
|
||||
return previousDeckId !== nextDeckId;
|
||||
}
|
||||
|
||||
function setSlotDeckOverride(slotId, deckId) {
|
||||
const targetSlotId = String(slotId || "").trim();
|
||||
if (!isValidSlotId(targetSlotId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nextDeckId = normalizeFrameDeckId(deckId);
|
||||
const previousDeckId = getSlotDeckOverride(targetSlotId);
|
||||
if (!nextDeckId) {
|
||||
state.slotDeckOverrides.delete(targetSlotId);
|
||||
return Boolean(previousDeckId);
|
||||
}
|
||||
|
||||
state.slotDeckOverrides.set(targetSlotId, nextDeckId);
|
||||
return previousDeckId !== nextDeckId;
|
||||
}
|
||||
|
||||
function buildDeckSelectOptions(selectEl, selectedDeckId, includeDefaultOption = false) {
|
||||
if (!(selectEl instanceof HTMLSelectElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = getDeckOptionsList();
|
||||
const resolvedFrameDeckId = getFrameDeckId();
|
||||
const requestedDeckId = normalizeDeckIdValue(selectedDeckId);
|
||||
const normalizedSelectedDeckId = options.some((option) => option.id === requestedDeckId)
|
||||
? requestedDeckId
|
||||
: (includeDefaultOption ? "" : resolvedFrameDeckId || options[0]?.id || "");
|
||||
|
||||
selectEl.replaceChildren();
|
||||
if (includeDefaultOption) {
|
||||
const defaultOptionEl = document.createElement("option");
|
||||
defaultOptionEl.value = "";
|
||||
const defaultDeckLabel = options.find((option) => option.id === resolvedFrameDeckId)?.label || "Current";
|
||||
defaultOptionEl.textContent = `Frame Default (${defaultDeckLabel})`;
|
||||
selectEl.appendChild(defaultOptionEl);
|
||||
}
|
||||
|
||||
options.forEach((option) => {
|
||||
const optionEl = document.createElement("option");
|
||||
optionEl.value = option.id;
|
||||
optionEl.textContent = option.label || option.id;
|
||||
selectEl.appendChild(optionEl);
|
||||
});
|
||||
|
||||
selectEl.value = normalizedSelectedDeckId;
|
||||
}
|
||||
|
||||
function buildFrameSettingsSnapshot() {
|
||||
const topModes = config.getHouseTopInfoModes?.() || {};
|
||||
const bottomModes = config.getHouseBottomInfoModes?.() || {};
|
||||
|
||||
return {
|
||||
showInfo: Boolean(state.showInfo),
|
||||
frameDeckId: normalizeFrameDeckId(state.frameDeckId),
|
||||
gridZoomScale: getGridZoomScale(),
|
||||
gridZoomStepIndex: state.gridZoomStepIndex,
|
||||
houseTopCardsVisible: config.getHouseTopCardsVisible?.() !== false,
|
||||
@@ -575,6 +699,9 @@
|
||||
: fallback.gridZoomScale);
|
||||
return {
|
||||
showInfo: raw.showInfo === undefined ? fallback.showInfo : Boolean(raw.showInfo),
|
||||
frameDeckId: raw.frameDeckId === undefined
|
||||
? normalizeFrameDeckId(fallback.frameDeckId)
|
||||
: normalizeFrameDeckId(raw.frameDeckId),
|
||||
gridZoomScale: normalizedZoomScale,
|
||||
gridZoomStepIndex: Number.isFinite(rawZoomIndex)
|
||||
? Math.max(0, Math.min(FRAME_GRID_ZOOM_STEPS.length - 1, rawZoomIndex))
|
||||
@@ -602,7 +729,8 @@
|
||||
.map(([slotId, cardId]) => ({
|
||||
slotId: String(slotId || "").trim(),
|
||||
cardId: String(cardId || "").trim(),
|
||||
isFlipped: Boolean(state.slotFlips.get(String(slotId || "").trim()))
|
||||
isFlipped: Boolean(state.slotFlips.get(String(slotId || "").trim())),
|
||||
deckId: getSlotDeckOverride(slotId)
|
||||
}))
|
||||
.filter((entry) => isValidSlotId(entry.slotId) && validCardIds.has(entry.cardId))
|
||||
.sort((left, right) => {
|
||||
@@ -646,7 +774,8 @@
|
||||
.map((entry) => ({
|
||||
slotId: String(entry?.slotId || "").trim(),
|
||||
cardId: String(entry?.cardId || "").trim(),
|
||||
isFlipped: Boolean(entry?.isFlipped)
|
||||
isFlipped: Boolean(entry?.isFlipped),
|
||||
deckId: normalizeFrameDeckId(entry?.deckId)
|
||||
}))
|
||||
.filter((entry) => isValidSlotId(entry.slotId) && entry.cardId)
|
||||
: [];
|
||||
@@ -915,6 +1044,7 @@
|
||||
function applyFrameSettingsSnapshot(rawSettings) {
|
||||
const settings = normalizeFrameSettingsSnapshot(rawSettings);
|
||||
state.showInfo = settings.showInfo;
|
||||
state.frameDeckId = normalizeFrameDeckId(settings.frameDeckId);
|
||||
state.gridZoomScale = settings.gridZoomScale;
|
||||
state.gridZoomStepIndex = settings.gridZoomStepIndex;
|
||||
config.setHouseTopCardsVisible?.(settings.houseTopCardsVisible);
|
||||
@@ -1426,7 +1556,36 @@
|
||||
cardActionMenuRemoveEl.dataset.cardAction = "remove";
|
||||
cardActionMenuRemoveEl.textContent = "Remove Card";
|
||||
|
||||
cardActionMenuEl.append(cardActionMenuSelectEl, cardActionMenuFlipEl, cardActionMenuOpenEl, cardActionMenuRemoveEl);
|
||||
const cardActionMenuDeckWrapEl = document.createElement("label");
|
||||
cardActionMenuDeckWrapEl.className = "tarot-frame-card-action-deck";
|
||||
const cardActionMenuDeckLabelEl = document.createElement("span");
|
||||
cardActionMenuDeckLabelEl.textContent = "Card Deck";
|
||||
cardActionMenuDeckSelectEl = document.createElement("select");
|
||||
cardActionMenuDeckSelectEl.className = "tarot-frame-card-action-deck-select";
|
||||
cardActionMenuDeckWrapEl.append(cardActionMenuDeckLabelEl, cardActionMenuDeckSelectEl);
|
||||
|
||||
const cardActionMenuDeckActionsEl = document.createElement("div");
|
||||
cardActionMenuDeckActionsEl.className = "tarot-frame-card-action-deck-actions";
|
||||
cardActionMenuDeckApplyEl = document.createElement("button");
|
||||
cardActionMenuDeckApplyEl.type = "button";
|
||||
cardActionMenuDeckApplyEl.className = "tarot-frame-card-action-menu-item";
|
||||
cardActionMenuDeckApplyEl.dataset.cardAction = "deck-apply";
|
||||
cardActionMenuDeckApplyEl.textContent = "Apply Deck";
|
||||
cardActionMenuDeckResetEl = document.createElement("button");
|
||||
cardActionMenuDeckResetEl.type = "button";
|
||||
cardActionMenuDeckResetEl.className = "tarot-frame-card-action-menu-item";
|
||||
cardActionMenuDeckResetEl.dataset.cardAction = "deck-reset";
|
||||
cardActionMenuDeckResetEl.textContent = "Use Frame Deck";
|
||||
cardActionMenuDeckActionsEl.append(cardActionMenuDeckApplyEl, cardActionMenuDeckResetEl);
|
||||
|
||||
cardActionMenuEl.append(
|
||||
cardActionMenuSelectEl,
|
||||
cardActionMenuFlipEl,
|
||||
cardActionMenuOpenEl,
|
||||
cardActionMenuRemoveEl,
|
||||
cardActionMenuDeckWrapEl,
|
||||
cardActionMenuDeckActionsEl
|
||||
);
|
||||
cardActionMenuEl.addEventListener("pointerdown", () => {
|
||||
state.cardActionMenu.clickArmed = true;
|
||||
});
|
||||
@@ -1452,9 +1611,9 @@
|
||||
const slotId = String(state.cardActionMenu.slotId || "").trim();
|
||||
const cardId = String(state.cardActionMenu.cardId || "").trim();
|
||||
const action = String(actionButton.dataset.cardAction || "").trim();
|
||||
closeCardActionMenu();
|
||||
|
||||
if (action === "select") {
|
||||
closeCardActionMenu();
|
||||
const selectionUpdate = setSelectedSlotIds(
|
||||
isSlotSelected(slotId)
|
||||
? getSelectedSlotIds().filter((selectedSlotId) => selectedSlotId !== slotId)
|
||||
@@ -1468,6 +1627,7 @@
|
||||
}
|
||||
|
||||
if (action === "flip") {
|
||||
closeCardActionMenu();
|
||||
if (toggleCardFlip(slotId)) {
|
||||
render({ preserveViewport: true });
|
||||
}
|
||||
@@ -1475,14 +1635,46 @@
|
||||
}
|
||||
|
||||
if (action === "remove") {
|
||||
closeCardActionMenu();
|
||||
const removedCard = removeCardFromSlot(slotId);
|
||||
if (removedCard) {
|
||||
render({ preserveViewport: true });
|
||||
setStatus(`${getDisplayCardName(removedCard)} removed from ${describeSlot(slotId)}.`);
|
||||
setStatus(`${getDisplayCardName(removedCard, slotId)} removed from ${describeSlot(slotId)}.`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "deck-apply") {
|
||||
const selectedDeckId = normalizeFrameDeckId(cardActionMenuDeckSelectEl?.value);
|
||||
const changed = setSlotDeckOverride(slotId, selectedDeckId);
|
||||
closeCardActionMenu();
|
||||
if (changed) {
|
||||
render({ preserveViewport: true });
|
||||
const card = getCardMap(getCards()).get(cardId) || null;
|
||||
if (selectedDeckId) {
|
||||
const deckLabel = getDeckOptionsList().find((option) => option.id === selectedDeckId)?.label || selectedDeckId;
|
||||
setStatus(`${getDisplayCardName(card, slotId)} now uses the ${deckLabel} deck at ${describeSlot(slotId)}.`);
|
||||
} else {
|
||||
const frameDeckLabel = getDeckOptionsList().find((option) => option.id === getFrameDeckId())?.label || "frame default";
|
||||
setStatus(`${getDisplayCardName(card, slotId)} now follows the frame deck (${frameDeckLabel}) at ${describeSlot(slotId)}.`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "deck-reset") {
|
||||
const changed = setSlotDeckOverride(slotId, "");
|
||||
closeCardActionMenu();
|
||||
if (changed) {
|
||||
render({ preserveViewport: true });
|
||||
const card = getCardMap(getCards()).get(cardId) || null;
|
||||
const frameDeckLabel = getDeckOptionsList().find((option) => option.id === getFrameDeckId())?.label || "frame default";
|
||||
setStatus(`${getDisplayCardName(card, slotId)} now follows the frame deck (${frameDeckLabel}) at ${describeSlot(slotId)}.`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
closeCardActionMenu();
|
||||
openCardLightbox(cardId, slotId);
|
||||
});
|
||||
|
||||
@@ -1550,6 +1742,17 @@
|
||||
if (cardActionMenuRemoveEl) {
|
||||
cardActionMenuRemoveEl.textContent = "Remove Card";
|
||||
}
|
||||
|
||||
buildDeckSelectOptions(cardActionMenuDeckSelectEl, getSlotDeckOverride(targetSlotId), true);
|
||||
if (cardActionMenuDeckSelectEl) {
|
||||
cardActionMenuDeckSelectEl.disabled = Boolean(state.exportInProgress);
|
||||
}
|
||||
if (cardActionMenuDeckResetEl) {
|
||||
cardActionMenuDeckResetEl.disabled = !getSlotDeckOverride(targetSlotId) || Boolean(state.exportInProgress);
|
||||
}
|
||||
if (cardActionMenuDeckApplyEl) {
|
||||
cardActionMenuDeckApplyEl.disabled = Boolean(state.exportInProgress);
|
||||
}
|
||||
}
|
||||
|
||||
function openCardActionMenu(slotId, cardId, anchorX, anchorY, options = {}) {
|
||||
@@ -1927,6 +2130,9 @@
|
||||
state.customCards.set(nextCard.id, nextCard);
|
||||
const previousSlotId = findAssignedSlotIdByCardId(nextCard.id);
|
||||
const inheritedFlip = previousSlotId && previousSlotId !== targetSlotId ? isSlotFlipped(previousSlotId) : isSlotFlipped(targetSlotId);
|
||||
const inheritedDeckId = previousSlotId && previousSlotId !== targetSlotId
|
||||
? getSlotDeckOverride(previousSlotId)
|
||||
: getSlotDeckOverride(targetSlotId);
|
||||
if (previousSlotId && previousSlotId !== targetSlotId) {
|
||||
clearSlotAssignmentState(previousSlotId);
|
||||
}
|
||||
@@ -1934,7 +2140,8 @@
|
||||
assignCardToSlot(
|
||||
targetSlotId,
|
||||
nextCard.id,
|
||||
inheritedFlip
|
||||
inheritedFlip,
|
||||
inheritedDeckId
|
||||
);
|
||||
pruneUnusedCustomCards();
|
||||
state.layoutReady = true;
|
||||
@@ -2174,6 +2381,9 @@
|
||||
|
||||
const previousSlotId = findAssignedSlotIdByCardId(targetCardId);
|
||||
const inheritedFlip = previousSlotId && previousSlotId !== targetSlotId ? isSlotFlipped(previousSlotId) : isSlotFlipped(targetSlotId);
|
||||
const inheritedDeckId = previousSlotId && previousSlotId !== targetSlotId
|
||||
? getSlotDeckOverride(previousSlotId)
|
||||
: getSlotDeckOverride(targetSlotId);
|
||||
if (previousSlotId && previousSlotId !== targetSlotId) {
|
||||
clearSlotAssignmentState(previousSlotId);
|
||||
}
|
||||
@@ -2181,13 +2391,14 @@
|
||||
assignCardToSlot(
|
||||
targetSlotId,
|
||||
targetCardId,
|
||||
inheritedFlip
|
||||
inheritedFlip,
|
||||
inheritedDeckId
|
||||
);
|
||||
pruneUnusedCustomCards();
|
||||
state.layoutReady = true;
|
||||
render({ preserveViewport: true });
|
||||
syncControls();
|
||||
setStatus(`${getDisplayCardName(card)} placed at ${describeSlot(targetSlotId)}.`);
|
||||
setStatus(`${getDisplayCardName(card, targetSlotId)} placed at ${describeSlot(targetSlotId)}.`);
|
||||
}
|
||||
|
||||
function clearGrid() {
|
||||
@@ -2203,6 +2414,7 @@
|
||||
|
||||
state.slotAssignments.clear();
|
||||
state.slotFlips.clear();
|
||||
state.slotDeckOverrides.clear();
|
||||
state.customCards.clear();
|
||||
state.selectedSlotIds.clear();
|
||||
state.layoutReady = true;
|
||||
@@ -3079,8 +3291,8 @@
|
||||
return cards.map((card) => getCardId(card)).filter(Boolean).sort().join("|");
|
||||
}
|
||||
|
||||
function resolveDeckOptions(card) {
|
||||
const deckId = String(tarotCardImages.getActiveDeck?.() || "").trim();
|
||||
function resolveDeckOptions(card, slotId = "") {
|
||||
const deckId = getResolvedSlotDeckId(slotId);
|
||||
const trumpNumber = card?.arcana === "Major" && Number.isFinite(Number(card?.number))
|
||||
? Number(card.number)
|
||||
: undefined;
|
||||
@@ -3095,7 +3307,7 @@
|
||||
};
|
||||
}
|
||||
|
||||
function resolveCardThumbnail(card) {
|
||||
function resolveCardThumbnail(card, slotId = "") {
|
||||
if (!card) {
|
||||
return "";
|
||||
}
|
||||
@@ -3104,7 +3316,7 @@
|
||||
return "";
|
||||
}
|
||||
|
||||
const deckOptions = resolveDeckOptions(card) || undefined;
|
||||
const deckOptions = resolveDeckOptions(card, slotId) || undefined;
|
||||
return String(
|
||||
tarotCardImages.resolveTarotCardThumbnail?.(card.name, deckOptions)
|
||||
|| tarotCardImages.resolveTarotCardImage?.(card.name, deckOptions)
|
||||
@@ -3112,12 +3324,12 @@
|
||||
).trim();
|
||||
}
|
||||
|
||||
function getDisplayCardName(card) {
|
||||
function getDisplayCardName(card, slotId = "") {
|
||||
if (isCustomFrameCard(card)) {
|
||||
return parseCustomCardText(card?.customText).primary || "Custom Card";
|
||||
}
|
||||
|
||||
const label = tarotCardImages.getTarotCardDisplayName?.(card?.name, resolveDeckOptions(card) || undefined);
|
||||
const label = tarotCardImages.getTarotCardDisplayName?.(card?.name, resolveDeckOptions(card, slotId) || undefined);
|
||||
return String(label || card?.name || "Tarot").trim() || "Tarot";
|
||||
}
|
||||
|
||||
@@ -3434,7 +3646,7 @@
|
||||
return config.getHouseBottomCardsVisible?.() !== false;
|
||||
}
|
||||
|
||||
function buildCardTextFaceModel(card) {
|
||||
function buildCardTextFaceModel(card, slotId = "") {
|
||||
if (isCustomFrameCard(card)) {
|
||||
const customText = parseCustomCardText(card?.customText);
|
||||
const backgroundColor = normalizeCustomCardBackgroundColor(card?.backgroundColor);
|
||||
@@ -3449,7 +3661,7 @@
|
||||
}
|
||||
|
||||
const label = state.showInfo ? buildHouseLabel(card) : null;
|
||||
const displayName = normalizeLabelText(getDisplayCardName(card));
|
||||
const displayName = normalizeLabelText(getDisplayCardName(card, slotId));
|
||||
|
||||
if (card?.arcana !== "Major" && label?.primary) {
|
||||
return {
|
||||
@@ -3581,9 +3793,10 @@
|
||||
|
||||
state.slotAssignments.delete(targetSlotId);
|
||||
state.slotFlips.delete(targetSlotId);
|
||||
state.slotDeckOverrides.delete(targetSlotId);
|
||||
}
|
||||
|
||||
function assignCardToSlot(slotId, cardId, isFlipped = false) {
|
||||
function assignCardToSlot(slotId, cardId, isFlipped = false, deckId = "") {
|
||||
const targetSlotId = String(slotId || "").trim();
|
||||
const targetCardId = String(cardId || "").trim();
|
||||
if (!isValidSlotId(targetSlotId) || !targetCardId) {
|
||||
@@ -3592,6 +3805,7 @@
|
||||
|
||||
state.slotAssignments.set(targetSlotId, targetCardId);
|
||||
setSlotFlipState(targetSlotId, isFlipped);
|
||||
setSlotDeckOverride(targetSlotId, deckId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3608,8 +3822,9 @@
|
||||
}
|
||||
|
||||
const isFlipped = isSlotFlipped(fromSlotId);
|
||||
const deckId = getSlotDeckOverride(fromSlotId);
|
||||
clearSlotAssignmentState(fromSlotId);
|
||||
assignCardToSlot(toSlotId, cardId, isFlipped);
|
||||
assignCardToSlot(toSlotId, cardId, isFlipped, deckId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3628,12 +3843,16 @@
|
||||
|
||||
const sourceFlip = isSlotFlipped(fromSlotId);
|
||||
const targetFlip = isSlotFlipped(toSlotId);
|
||||
const sourceDeckId = getSlotDeckOverride(fromSlotId);
|
||||
const targetDeckId = getSlotDeckOverride(toSlotId);
|
||||
|
||||
state.slotAssignments.set(toSlotId, sourceCardId);
|
||||
setSlotFlipState(toSlotId, sourceFlip);
|
||||
setSlotDeckOverride(toSlotId, sourceDeckId);
|
||||
if (targetCardId) {
|
||||
state.slotAssignments.set(fromSlotId, targetCardId);
|
||||
setSlotFlipState(fromSlotId, targetFlip);
|
||||
setSlotDeckOverride(fromSlotId, targetDeckId);
|
||||
} else {
|
||||
clearSlotAssignmentState(fromSlotId);
|
||||
}
|
||||
@@ -3656,7 +3875,7 @@
|
||||
setSlotFlipState(targetSlotId, nextIsFlipped);
|
||||
state.layoutReady = true;
|
||||
const card = getCardMap(getCards()).get(cardId) || null;
|
||||
setStatus(`${getDisplayCardName(card)} flipped ${nextIsFlipped ? "upside down" : "upright"} at ${describeSlot(targetSlotId)}.`);
|
||||
setStatus(`${getDisplayCardName(card, targetSlotId)} flipped ${nextIsFlipped ? "upside down" : "upright"} at ${describeSlot(targetSlotId)}.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3673,6 +3892,7 @@
|
||||
state.currentLayoutId = layoutPreset.id;
|
||||
state.slotAssignments.clear();
|
||||
state.slotFlips.clear();
|
||||
state.slotDeckOverrides.clear();
|
||||
state.selectedSlotIds.clear();
|
||||
state.customCards.clear();
|
||||
|
||||
@@ -3697,10 +3917,11 @@
|
||||
state.currentLayoutId = savedLayout.id;
|
||||
state.slotAssignments.clear();
|
||||
state.slotFlips.clear();
|
||||
state.slotDeckOverrides.clear();
|
||||
state.selectedSlotIds.clear();
|
||||
savedLayout.slotAssignments.forEach((entry) => {
|
||||
if (cardMap.has(entry.cardId)) {
|
||||
assignCardToSlot(entry.slotId, entry.cardId, Boolean(entry.isFlipped));
|
||||
assignCardToSlot(entry.slotId, entry.cardId, Boolean(entry.isFlipped), entry.deckId);
|
||||
}
|
||||
});
|
||||
applyFrameSettingsSnapshot(savedLayout.settings);
|
||||
@@ -3912,7 +4133,7 @@
|
||||
return cardMap.get(cardId) || null;
|
||||
}
|
||||
|
||||
function getCardOverlayLabel(card) {
|
||||
function getCardOverlayLabel(card, slotId = "") {
|
||||
if (!state.showInfo) {
|
||||
return "";
|
||||
}
|
||||
@@ -3927,7 +4148,7 @@
|
||||
return structuredLabel;
|
||||
}
|
||||
|
||||
return getCardOverlayDate(card) || formatMonthDay(getRelation(card, "decan")?.data?.dateStart) || getDisplayCardName(card);
|
||||
return getCardOverlayDate(card) || formatMonthDay(getRelation(card, "decan")?.data?.dateStart) || getDisplayCardName(card, slotId);
|
||||
}
|
||||
|
||||
function getOccupiedGridBounds(gridTrackEl) {
|
||||
@@ -4206,18 +4427,18 @@
|
||||
}
|
||||
|
||||
button.dataset.cardId = getCardId(card);
|
||||
button.setAttribute("aria-label", `${getDisplayCardName(card)} in row ${row}, column ${column}${flipped ? ", flipped upside down" : ""}${selected ? ", selected for group move" : ""}`);
|
||||
button.title = `${getDisplayCardName(card)}${flipped ? " (flipped upside down)" : ""}`;
|
||||
button.setAttribute("aria-label", `${getDisplayCardName(card, slotId)} in row ${row}, column ${column}${flipped ? ", flipped upside down" : ""}${selected ? ", selected for group move" : ""}`);
|
||||
button.title = `${getDisplayCardName(card, slotId)}${flipped ? " (flipped upside down)" : ""}`;
|
||||
|
||||
const showImage = shouldShowCardImage(card);
|
||||
|
||||
const imageSrc = resolveCardThumbnail(card);
|
||||
const imageSrc = resolveCardThumbnail(card, slotId);
|
||||
if (showImage && imageSrc) {
|
||||
const imageCached = tarotCardImages.isImageLoaded?.(imageSrc) === true;
|
||||
const image = document.createElement("img");
|
||||
image.className = "tarot-frame-card-image";
|
||||
image.src = imageSrc;
|
||||
image.alt = getDisplayCardName(card);
|
||||
image.alt = getDisplayCardName(card, slotId);
|
||||
image.loading = imageCached ? "eager" : "lazy";
|
||||
image.decoding = "async";
|
||||
image.draggable = false;
|
||||
@@ -4225,16 +4446,16 @@
|
||||
} else if (showImage) {
|
||||
const fallback = document.createElement("span");
|
||||
fallback.className = "tarot-frame-card-fallback";
|
||||
fallback.textContent = getDisplayCardName(card);
|
||||
fallback.textContent = getDisplayCardName(card, slotId);
|
||||
button.appendChild(fallback);
|
||||
} else {
|
||||
button.appendChild(createCardTextFaceElement(buildCardTextFaceModel(card)));
|
||||
button.appendChild(createCardTextFaceElement(buildCardTextFaceModel(card, slotId)));
|
||||
}
|
||||
|
||||
if (showImage && state.showInfo) {
|
||||
const overlay = document.createElement("span");
|
||||
overlay.className = "tarot-frame-card-badge";
|
||||
overlay.textContent = getCardOverlayLabel(card);
|
||||
overlay.textContent = getCardOverlayLabel(card, slotId);
|
||||
button.appendChild(overlay);
|
||||
}
|
||||
|
||||
@@ -4462,6 +4683,7 @@
|
||||
tarotFrameSettingsToggleEl,
|
||||
tarotFrameSettingsPanelEl,
|
||||
tarotFrameGridZoomEl,
|
||||
tarotFrameDeckSelectEl,
|
||||
tarotFrameShowInfoEl,
|
||||
tarotFrameHouseSettingsEl,
|
||||
tarotFrameHouseTopCardsVisibleEl,
|
||||
@@ -4523,6 +4745,11 @@
|
||||
tarotFrameGridZoomEl.disabled = Boolean(state.exportInProgress);
|
||||
}
|
||||
|
||||
if (tarotFrameDeckSelectEl) {
|
||||
buildDeckSelectOptions(tarotFrameDeckSelectEl, normalizeFrameDeckId(state.frameDeckId) || getFrameDeckId(), false);
|
||||
tarotFrameDeckSelectEl.disabled = Boolean(state.exportInProgress);
|
||||
}
|
||||
|
||||
if (tarotFrameShowInfoEl) {
|
||||
tarotFrameShowInfoEl.checked = Boolean(state.showInfo);
|
||||
tarotFrameShowInfoEl.disabled = Boolean(state.exportInProgress);
|
||||
@@ -4665,7 +4892,8 @@
|
||||
sourceSlotId,
|
||||
targetSlotId,
|
||||
cardId,
|
||||
isFlipped: isSlotFlipped(sourceSlotId)
|
||||
isFlipped: isSlotFlipped(sourceSlotId),
|
||||
deckId: getSlotDeckOverride(sourceSlotId)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4688,7 +4916,7 @@
|
||||
clearSlotAssignmentState(move.sourceSlotId);
|
||||
});
|
||||
plan.moves.forEach((move) => {
|
||||
assignCardToSlot(move.targetSlotId, move.cardId, move.isFlipped);
|
||||
assignCardToSlot(move.targetSlotId, move.cardId, move.isFlipped, move.deckId);
|
||||
});
|
||||
state.layoutReady = true;
|
||||
pruneUnusedCustomCards();
|
||||
@@ -4713,20 +4941,20 @@
|
||||
ghost.appendChild(countEl);
|
||||
}
|
||||
|
||||
const imageSrc = resolveCardThumbnail(card);
|
||||
const imageSrc = resolveCardThumbnail(card, sourceSlotId);
|
||||
if (imageSrc) {
|
||||
const image = document.createElement("img");
|
||||
image.src = imageSrc;
|
||||
image.alt = "";
|
||||
ghost.appendChild(image);
|
||||
} else {
|
||||
ghost.appendChild(createCardTextFaceElement(buildCardTextFaceModel(card)));
|
||||
ghost.appendChild(createCardTextFaceElement(buildCardTextFaceModel(card, sourceSlotId)));
|
||||
}
|
||||
|
||||
if (state.showInfo) {
|
||||
const label = document.createElement("span");
|
||||
label.className = "tarot-frame-drag-ghost-label";
|
||||
label.textContent = getCardOverlayLabel(card);
|
||||
label.textContent = getCardOverlayLabel(card, sourceSlotId);
|
||||
if (label.textContent) {
|
||||
ghost.appendChild(label);
|
||||
}
|
||||
@@ -4914,7 +5142,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const deckOptions = resolveDeckOptions(card);
|
||||
const deckOptions = resolveDeckOptions(card, slotId);
|
||||
const src = String(
|
||||
tarotCardImages.resolveTarotCardImage?.(card.name, deckOptions)
|
||||
|| tarotCardImages.resolveTarotCardThumbnail?.(card.name, deckOptions)
|
||||
@@ -4925,13 +5153,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const label = getDisplayCardName(card);
|
||||
const label = getDisplayCardName(card, slotId);
|
||||
window.TarotUiLightbox?.open?.({
|
||||
src,
|
||||
altText: label,
|
||||
label,
|
||||
cardId: getCardId(card),
|
||||
deckId: String(tarotCardImages.getActiveDeck?.() || "").trim()
|
||||
deckId: getResolvedSlotDeckId(slotId)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5528,7 +5756,7 @@
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function drawSlotToCanvas(context, x, y, width, height, card, image, customBackgroundImage = null, isFlipped = false) {
|
||||
function drawSlotToCanvas(context, x, y, width, height, card, image, customBackgroundImage = null, isFlipped = false, slotId = "") {
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
@@ -5538,7 +5766,7 @@
|
||||
const cardWidth = width - (EXPORT_CARD_INSET * 2);
|
||||
const cardHeight = height - (EXPORT_CARD_INSET * 2);
|
||||
const showImage = shouldShowCardImage(card);
|
||||
const faceModel = showImage ? null : buildCardTextFaceModel(card);
|
||||
const faceModel = showImage ? null : buildCardTextFaceModel(card, slotId);
|
||||
|
||||
context.save();
|
||||
if (isFlipped) {
|
||||
@@ -5557,7 +5785,7 @@
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
context.font = "700 14px 'Segoe UI', sans-serif";
|
||||
const lines = wrapCanvasText(context, getDisplayCardName(card), cardWidth - 18, 4);
|
||||
const lines = wrapCanvasText(context, getDisplayCardName(card, slotId), cardWidth - 18, 4);
|
||||
const lineHeight = 18;
|
||||
let currentY = cardY + (cardHeight / 2) - (((Math.max(1, lines.length) - 1) * lineHeight) / 2);
|
||||
lines.forEach((line) => {
|
||||
@@ -5579,7 +5807,7 @@
|
||||
}
|
||||
|
||||
if (showImage && state.showInfo) {
|
||||
const overlayText = getCardOverlayLabel(card);
|
||||
const overlayText = getCardOverlayLabel(card, slotId);
|
||||
if (overlayText) {
|
||||
const overlayHeight = 30;
|
||||
const overlayX = cardX + 4;
|
||||
@@ -5605,47 +5833,91 @@
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function loadCardImage(src) {
|
||||
function isCrossOriginImageSource(src) {
|
||||
const normalizedSrc = String(src || "").trim();
|
||||
if (!normalizedSrc) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (normalizedSrc.startsWith("data:") || normalizedSrc.startsWith("blob:")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const sourceUrl = new URL(normalizedSrc, window.location.href);
|
||||
return sourceUrl.origin !== window.location.origin;
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function loadImageElementFromUrl(src, { crossOrigin = "", revokeOnLoad = false } = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const normalizedSrc = String(src || "").trim();
|
||||
if (!normalizedSrc) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const ensureLoaded = window.TarotCardImages?.ensureImageLoaded;
|
||||
const attachImageFallback = () => {
|
||||
const image = new Image();
|
||||
image.decoding = "async";
|
||||
image.onload = () => resolve(image);
|
||||
image.onerror = () => resolve(null);
|
||||
image.src = normalizedSrc;
|
||||
};
|
||||
|
||||
if (typeof ensureLoaded === "function") {
|
||||
Promise.resolve(ensureLoaded(normalizedSrc))
|
||||
.then((cachedImage) => {
|
||||
if (cachedImage?.naturalWidth) {
|
||||
resolve(cachedImage);
|
||||
return;
|
||||
}
|
||||
|
||||
attachImageFallback();
|
||||
})
|
||||
.catch(() => {
|
||||
attachImageFallback();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const image = new Image();
|
||||
image.decoding = "async";
|
||||
image.onload = () => resolve(image);
|
||||
image.onerror = () => resolve(null);
|
||||
image.src = normalizedSrc;
|
||||
if (crossOrigin) {
|
||||
image.crossOrigin = crossOrigin;
|
||||
}
|
||||
|
||||
image.onload = () => {
|
||||
if (revokeOnLoad) {
|
||||
try {
|
||||
URL.revokeObjectURL(src);
|
||||
} catch (_error) {
|
||||
// Ignore object URL cleanup failures.
|
||||
}
|
||||
}
|
||||
resolve(image);
|
||||
};
|
||||
image.onerror = () => {
|
||||
if (revokeOnLoad) {
|
||||
try {
|
||||
URL.revokeObjectURL(src);
|
||||
} catch (_error) {
|
||||
// Ignore object URL cleanup failures.
|
||||
}
|
||||
}
|
||||
resolve(null);
|
||||
};
|
||||
image.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCardImage(src) {
|
||||
const normalizedSrc = String(src || "").trim();
|
||||
if (!normalizedSrc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Export uses blob-backed image URLs so the canvas remains origin-clean.
|
||||
try {
|
||||
const response = await fetch(normalizedSrc, {
|
||||
method: "GET",
|
||||
mode: "cors",
|
||||
credentials: "omit",
|
||||
cache: "force-cache"
|
||||
});
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
if (blob.size > 0) {
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const image = await loadImageElementFromUrl(blobUrl, { revokeOnLoad: true });
|
||||
if (image) {
|
||||
return image;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_error) {
|
||||
// Fall through to direct image loading path.
|
||||
}
|
||||
|
||||
if (isCrossOriginImageSource(normalizedSrc)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return loadImageElementFromUrl(normalizedSrc, { crossOrigin: "anonymous" });
|
||||
}
|
||||
|
||||
function isExportFormatSupported(format) {
|
||||
const exportFormat = EXPORT_FORMATS[format];
|
||||
if (!exportFormat) {
|
||||
@@ -5660,13 +5932,17 @@
|
||||
function canvasToBlobByFormat(canvas, format) {
|
||||
const exportFormat = EXPORT_FORMATS[format] || EXPORT_FORMATS.webp;
|
||||
return new Promise((resolve, reject) => {
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
return;
|
||||
}
|
||||
reject(new Error("Canvas export failed."));
|
||||
}, exportFormat.mimeType, exportFormat.quality);
|
||||
try {
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
return;
|
||||
}
|
||||
reject(new Error("Canvas export failed. Some deck images may block export without CORS access."));
|
||||
}, exportFormat.mimeType, exportFormat.quality);
|
||||
} catch (_error) {
|
||||
reject(new Error("Canvas export failed because one or more images could not be read for export (CORS)."));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5678,7 +5954,7 @@
|
||||
const contentHeight = (MASTER_GRID_SIZE * EXPORT_SLOT_HEIGHT) + ((MASTER_GRID_SIZE - 1) * EXPORT_GRID_GAP);
|
||||
const canvasWidth = contentWidth + (EXPORT_PADDING * 2);
|
||||
const canvasHeight = contentHeight + (EXPORT_PADDING * 2);
|
||||
const scale = Math.max(1.5, Math.min(2, Number(window.devicePixelRatio) || 1));
|
||||
const scale = Math.max(2, Math.min(2.5, Number(window.devicePixelRatio) || 1));
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = Math.ceil(canvasWidth * scale);
|
||||
canvas.height = Math.ceil(canvasHeight * scale);
|
||||
@@ -5698,7 +5974,8 @@
|
||||
|
||||
const imageCache = new Map();
|
||||
cards.forEach((card) => {
|
||||
const src = resolveCardThumbnail(card);
|
||||
const slotId = findAssignedSlotIdByCardId(getCardId(card));
|
||||
const src = resolveCardThumbnail(card, slotId);
|
||||
if (src && !imageCache.has(src)) {
|
||||
imageCache.set(src, loadCardImage(src));
|
||||
}
|
||||
@@ -5706,7 +5983,8 @@
|
||||
|
||||
const resolvedImages = new Map();
|
||||
await Promise.all(cards.map(async (card) => {
|
||||
const src = resolveCardThumbnail(card);
|
||||
const slotId = findAssignedSlotIdByCardId(getCardId(card));
|
||||
const src = resolveCardThumbnail(card, slotId);
|
||||
const image = src ? await imageCache.get(src) : null;
|
||||
resolvedImages.set(getCardId(card), image || null);
|
||||
}));
|
||||
@@ -5741,7 +6019,8 @@
|
||||
card,
|
||||
card ? resolvedImages.get(getCardId(card)) : null,
|
||||
card ? resolvedCustomBackgroundImages.get(getCardId(card)) : null,
|
||||
card ? isSlotFlipped(slotId) : false
|
||||
card ? isSlotFlipped(slotId) : false,
|
||||
slotId
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5792,6 +6071,7 @@
|
||||
tarotFrameSettingsToggleEl,
|
||||
tarotFrameSettingsPanelEl,
|
||||
tarotFrameGridZoomEl,
|
||||
tarotFrameDeckSelectEl,
|
||||
tarotFrameShowInfoEl,
|
||||
tarotFrameHouseTopCardsVisibleEl,
|
||||
tarotFrameHouseTopInfoHebrewEl,
|
||||
@@ -6002,6 +6282,17 @@
|
||||
});
|
||||
}
|
||||
|
||||
if (tarotFrameDeckSelectEl) {
|
||||
tarotFrameDeckSelectEl.addEventListener("change", () => {
|
||||
if (setFrameDeckId(tarotFrameDeckSelectEl.value)) {
|
||||
render({ preserveViewport: true });
|
||||
}
|
||||
syncControls();
|
||||
const deckLabel = getDeckOptionsList().find((option) => option.id === getFrameDeckId())?.label || "selected";
|
||||
setStatus(`Frame deck set to ${deckLabel}. Use card action menu to override individual slots.`);
|
||||
});
|
||||
}
|
||||
|
||||
[
|
||||
[tarotFrameHouseTopCardsVisibleEl, (checked) => config.setHouseTopCardsVisible?.(checked)],
|
||||
[tarotFrameHouseTopInfoHebrewEl, (checked) => config.setHouseTopInfoMode?.("hebrew", checked)],
|
||||
|
||||
+19
@@ -460,6 +460,10 @@
|
||||
<option value="14">1400%</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="tarot-frame-field" for="tarot-frame-deck-select">
|
||||
<span>Frame Deck</span>
|
||||
<select id="tarot-frame-deck-select"></select>
|
||||
</label>
|
||||
<label class="tarot-frame-toggle" for="tarot-frame-show-info">
|
||||
<input id="tarot-frame-show-info" type="checkbox" checked>
|
||||
<span>Display Info</span>
|
||||
@@ -1017,6 +1021,20 @@
|
||||
</div>
|
||||
<div id="alpha-gematria-result" class="alpha-gematria-result">Total: --</div>
|
||||
<div id="alpha-gematria-breakdown" class="alpha-gematria-breakdown">Type text to calculate.</div>
|
||||
<div id="alpha-gematria-keyboard" class="alpha-gematria-keyboard" aria-label="On-screen language keyboard">
|
||||
<div class="alpha-gematria-keyboard-head">
|
||||
<label class="alpha-gematria-keyboard-label" for="alpha-gematria-keyboard-script">Keyboard</label>
|
||||
<select id="alpha-gematria-keyboard-script" class="alpha-gematria-keyboard-script" aria-label="Choose keyboard language"></select>
|
||||
</div>
|
||||
<div id="alpha-gematria-keyboard-grid" class="alpha-gematria-keyboard-grid" role="group" aria-label="Language letters"></div>
|
||||
<div id="alpha-gematria-keyboard-actions" class="alpha-gematria-keyboard-actions" role="group" aria-label="Keyboard actions">
|
||||
<button type="button" class="alpha-gematria-keyboard-action" data-keyboard-action="backspace">Backspace</button>
|
||||
<button type="button" class="alpha-gematria-keyboard-action" data-keyboard-action="space">Space</button>
|
||||
<button type="button" class="alpha-gematria-keyboard-action" data-keyboard-action="star">*</button>
|
||||
<button type="button" class="alpha-gematria-keyboard-action" data-keyboard-action="question">?</button>
|
||||
<button type="button" class="alpha-gematria-keyboard-action" data-keyboard-action="clear">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="alpha-gematria-matches" class="alpha-gematria-matches" aria-live="polite" hidden></div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1347,6 +1365,7 @@
|
||||
<script src="node_modules/@toast-ui/calendar/dist/toastui-calendar.min.js"></script>
|
||||
<script src="node_modules/suncalc/suncalc.js"></script>
|
||||
<script src="node_modules/astronomy-engine/astronomy.browser.min.js"></script>
|
||||
<script src="node_modules/html2canvas/dist/html2canvas.min.js"></script>
|
||||
<script src="app/astro-calcs.js"></script>
|
||||
<script src="app/app-config.js?v=20260529-access-gating-01"></script>
|
||||
<script src="app/data-service.js?v=20260601-now-polling-03"></script>
|
||||
|
||||
Generated
+50
@@ -12,6 +12,7 @@
|
||||
"@fontsource/noto-serif": "^5.2.9",
|
||||
"@toast-ui/calendar": "^2.1.3",
|
||||
"astronomy-engine": "^2.1.19",
|
||||
"html2canvas": "^1.4.1",
|
||||
"suncalc": "^1.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -318,6 +319,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/basic-auth": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
||||
@@ -418,6 +428,15 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-line-break": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
||||
@@ -703,6 +722,19 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||
@@ -1180,6 +1212,15 @@
|
||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "7.0.25",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.25.tgz",
|
||||
@@ -1265,6 +1306,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"@fontsource/noto-serif": "^5.2.9",
|
||||
"@toast-ui/calendar": "^2.1.3",
|
||||
"astronomy-engine": "^2.1.19",
|
||||
"html2canvas": "^1.4.1",
|
||||
"suncalc": "^1.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Reference in New Issue
Block a user