update sequence nav to use new ui-sequence-nav component
This commit is contained in:
+125
@@ -2050,6 +2050,34 @@
|
|||||||
color: #f8fafc;
|
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 {
|
.tarot-frame-card-custom-editor {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 42;
|
z-index: 42;
|
||||||
@@ -3357,6 +3385,15 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.2;
|
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 {
|
.planet-meta-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
@@ -4771,6 +4808,90 @@
|
|||||||
word-break: break-word;
|
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 {
|
.alpha-gematria-matches {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
@@ -4865,6 +4986,10 @@
|
|||||||
.alpha-gematria-controls.is-reverse-cipher-mode {
|
.alpha-gematria-controls.is-reverse-cipher-mode {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alpha-gematria-keyboard-actions {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.alpha-tabs {
|
.alpha-tabs {
|
||||||
|
|||||||
+359
-29
@@ -14,7 +14,11 @@
|
|||||||
inputLabelEl: null,
|
inputLabelEl: null,
|
||||||
cipherLabelEl: null,
|
cipherLabelEl: null,
|
||||||
reverseCiphersEl: null,
|
reverseCiphersEl: null,
|
||||||
reverseCipherHintEl: null
|
reverseCipherHintEl: null,
|
||||||
|
keyboardEl: null,
|
||||||
|
keyboardScriptEl: null,
|
||||||
|
keyboardGridEl: null,
|
||||||
|
keyboardActionsEl: null
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,7 +39,8 @@
|
|||||||
dictionaryLookupCache: new Map(),
|
dictionaryLookupCache: new Map(),
|
||||||
reverseRequestId: 0,
|
reverseRequestId: 0,
|
||||||
anagramRequestId: 0,
|
anagramRequestId: 0,
|
||||||
dictionaryRequestId: 0
|
dictionaryRequestId: 0,
|
||||||
|
keyboardScriptId: "english"
|
||||||
};
|
};
|
||||||
|
|
||||||
function getAlphabets() {
|
function getAlphabets() {
|
||||||
@@ -57,10 +62,184 @@
|
|||||||
inputLabelEl: null,
|
inputLabelEl: null,
|
||||||
cipherLabelEl: null,
|
cipherLabelEl: null,
|
||||||
reverseCiphersEl: 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() {
|
function isReverseMode() {
|
||||||
return state.activeMode === "reverse";
|
return state.activeMode === "reverse";
|
||||||
}
|
}
|
||||||
@@ -142,14 +321,57 @@
|
|||||||
|
|
||||||
function addScriptCharMapEntry(map, scriptChar, mappedLetters) {
|
function addScriptCharMapEntry(map, scriptChar, mappedLetters) {
|
||||||
const key = String(scriptChar || "").trim();
|
const key = String(scriptChar || "").trim();
|
||||||
const value = String(mappedLetters || "").trim();
|
if (!key) {
|
||||||
if (!key || !value) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
map.set(key, value);
|
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) {
|
function buildGematriaScriptMap(baseAlphabet) {
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
const alphabets = getAlphabets() || {};
|
const alphabets = getAlphabets() || {};
|
||||||
@@ -157,35 +379,51 @@
|
|||||||
const greekClassicalLetters = Array.isArray(alphabets.greek) ? alphabets.greek : [];
|
const greekClassicalLetters = Array.isArray(alphabets.greek) ? alphabets.greek : [];
|
||||||
const greekArchaicLetters = Array.isArray(alphabets.greekArchaic) ? alphabets.greekArchaic : [];
|
const greekArchaicLetters = Array.isArray(alphabets.greekArchaic) ? alphabets.greekArchaic : [];
|
||||||
const greekLetters = [...greekClassicalLetters, ...greekArchaicLetters];
|
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) => {
|
hebrewLetters.forEach((entry) => {
|
||||||
const mapped = transliterationToBaseLetters(entry?.transliteration, baseAlphabet);
|
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) => {
|
greekLetters.forEach((entry) => {
|
||||||
const mapped = transliterationToBaseLetters(entry?.transliteration, baseAlphabet);
|
const mapped = transliterationToBaseLetters(entry?.transliteration, baseAlphabet);
|
||||||
addScriptCharMapEntry(map, entry?.char, mapped);
|
if (!map.has(String(entry?.char || "").trim())) {
|
||||||
addScriptCharMapEntry(map, entry?.charLower, mapped);
|
addScriptCharMapEntry(map, entry?.char, mapped);
|
||||||
addScriptCharMapEntry(map, entry?.charFinal, mapped);
|
}
|
||||||
});
|
if (!map.has(String(entry?.charLower || "").trim())) {
|
||||||
|
addScriptCharMapEntry(map, entry?.charLower, mapped);
|
||||||
const hebrewFinalForms = {
|
}
|
||||||
ך: "k",
|
if (!map.has(String(entry?.charFinal || "").trim())) {
|
||||||
ם: "m",
|
addScriptCharMapEntry(map, entry?.charFinal, mapped);
|
||||||
ן: "n",
|
|
||||||
ף: "p",
|
|
||||||
ץ: "t"
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.entries(hebrewFinalForms).forEach(([char, mapped]) => {
|
|
||||||
if (!map.has(char) && baseAlphabet.includes(mapped)) {
|
|
||||||
addScriptCharMapEntry(map, char, mapped);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!map.has("ς") && baseAlphabet.includes("s")) {
|
const hebrewFinalForms = {
|
||||||
addScriptCharMapEntry(map, "ς", "s");
|
ך: "כ",
|
||||||
|
ם: "מ",
|
||||||
|
ן: "נ",
|
||||||
|
ף: "פ",
|
||||||
|
ץ: "צ"
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
return map;
|
||||||
@@ -423,7 +661,11 @@
|
|||||||
inputLabelEl,
|
inputLabelEl,
|
||||||
cipherLabelEl,
|
cipherLabelEl,
|
||||||
reverseCiphersEl,
|
reverseCiphersEl,
|
||||||
reverseCipherHintEl
|
reverseCipherHintEl,
|
||||||
|
keyboardEl,
|
||||||
|
keyboardScriptEl,
|
||||||
|
keyboardGridEl,
|
||||||
|
keyboardActionsEl
|
||||||
} = getElements();
|
} = getElements();
|
||||||
|
|
||||||
const reverseMode = isReverseMode();
|
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) {
|
if (!reverseMode && !anagramMode && !dictionaryMode) {
|
||||||
clearReverseMatches(matchesEl);
|
clearReverseMatches(matchesEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!reverseMode) {
|
||||||
|
renderKeyboardLayout();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseReverseLookupValue(rawValue) {
|
function parseReverseLookupValue(rawValue) {
|
||||||
@@ -901,10 +1163,44 @@
|
|||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
[...normalizedInput].forEach((char) => {
|
[...normalizedInput].forEach((char) => {
|
||||||
const mappedLetters = baseAlphabet.includes(char)
|
if (baseAlphabet.includes(char)) {
|
||||||
? char
|
const index = baseAlphabet.indexOf(char);
|
||||||
: (scriptMap.get(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) {
|
if (!mappedLetters) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -984,7 +1280,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindGematriaListeners() {
|
function bindGematriaListeners() {
|
||||||
const { cipherEl, inputEl, modeEls, reverseCiphersEl } = getElements();
|
const { cipherEl, inputEl, modeEls, reverseCiphersEl, keyboardScriptEl, keyboardGridEl, keyboardActionsEl } = getElements();
|
||||||
if (state.listenersBound || !cipherEl || !inputEl) {
|
if (state.listenersBound || !cipherEl || !inputEl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1033,6 +1329,39 @@
|
|||||||
renderGematriaResult();
|
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;
|
state.listenersBound = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1048,6 +1377,7 @@
|
|||||||
void loadGematriaDb().then(() => {
|
void loadGematriaDb().then(() => {
|
||||||
refreshScriptMap((state.db || getFallbackGematriaDb()).baseAlphabet);
|
refreshScriptMap((state.db || getFallbackGematriaDb()).baseAlphabet);
|
||||||
renderGematriaCipherOptions();
|
renderGematriaCipherOptions();
|
||||||
|
renderKeyboardLayout();
|
||||||
renderGematriaResult();
|
renderGematriaResult();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-1
@@ -57,6 +57,7 @@
|
|||||||
let gematriaCipherEl, gematriaInputEl, gematriaResultEl, gematriaBreakdownEl;
|
let gematriaCipherEl, gematriaInputEl, gematriaResultEl, gematriaBreakdownEl;
|
||||||
let gematriaModeEls, gematriaMatchesEl, gematriaInputLabelEl, gematriaCipherLabelEl;
|
let gematriaModeEls, gematriaMatchesEl, gematriaInputLabelEl, gematriaCipherLabelEl;
|
||||||
let gematriaReverseCiphersEl, gematriaReverseCipherHintEl;
|
let gematriaReverseCiphersEl, gematriaReverseCipherHintEl;
|
||||||
|
let gematriaKeyboardEl, gematriaKeyboardScriptEl, gematriaKeyboardGridEl, gematriaKeyboardActionsEl;
|
||||||
|
|
||||||
function getElements() {
|
function getElements() {
|
||||||
alphabetLettersSectionEl = document.getElementById("alphabet-letters-section");
|
alphabetLettersSectionEl = document.getElementById("alphabet-letters-section");
|
||||||
@@ -88,6 +89,10 @@
|
|||||||
gematriaCipherLabelEl = document.getElementById("alpha-gematria-cipher-label");
|
gematriaCipherLabelEl = document.getElementById("alpha-gematria-cipher-label");
|
||||||
gematriaReverseCiphersEl = document.getElementById("alpha-gematria-reverse-ciphers");
|
gematriaReverseCiphersEl = document.getElementById("alpha-gematria-reverse-ciphers");
|
||||||
gematriaReverseCipherHintEl = document.getElementById("alpha-gematria-reverse-cipher-hint");
|
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() {
|
function getGematriaElements() {
|
||||||
@@ -102,7 +107,11 @@
|
|||||||
inputLabelEl: gematriaInputLabelEl,
|
inputLabelEl: gematriaInputLabelEl,
|
||||||
cipherLabelEl: gematriaCipherLabelEl,
|
cipherLabelEl: gematriaCipherLabelEl,
|
||||||
reverseCiphersEl: gematriaReverseCiphersEl,
|
reverseCiphersEl: gematriaReverseCiphersEl,
|
||||||
reverseCipherHintEl: gematriaReverseCipherHintEl
|
reverseCipherHintEl: gematriaReverseCipherHintEl,
|
||||||
|
keyboardEl: gematriaKeyboardEl,
|
||||||
|
keyboardScriptEl: gematriaKeyboardScriptEl,
|
||||||
|
keyboardGridEl: gematriaKeyboardGridEl,
|
||||||
|
keyboardActionsEl: gematriaKeyboardActionsEl
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
const sidebarControllers = new WeakMap();
|
const sidebarControllers = new WeakMap();
|
||||||
const sidebarControllerRegistry = new Map();
|
const sidebarControllerRegistry = new Map();
|
||||||
const detailControllers = new WeakMap();
|
const detailControllers = new WeakMap();
|
||||||
|
const detailExportInProgress = new WeakSet();
|
||||||
const AUTO_COLLAPSE_ENTRY_SELECTOR = [
|
const AUTO_COLLAPSE_ENTRY_SELECTOR = [
|
||||||
".planet-list-item",
|
".planet-list-item",
|
||||||
".tarot-list-item",
|
".tarot-list-item",
|
||||||
@@ -403,6 +404,7 @@
|
|||||||
const sectionId = layout.closest("section")?.id || `layout-${index + 1}`;
|
const sectionId = layout.closest("section")?.id || `layout-${index + 1}`;
|
||||||
const panelId = detailPanel.id || `${sectionId}-detail-panel`;
|
const panelId = detailPanel.id || `${sectionId}-detail-panel`;
|
||||||
detailPanel.id = panelId;
|
detailPanel.id = panelId;
|
||||||
|
ensureDetailPaneExportControl(detailPanel, sectionId);
|
||||||
|
|
||||||
const detailStorageKey = `${DETAIL_COLLAPSE_STORAGE_PREFIX}${sectionId}`;
|
const detailStorageKey = `${DETAIL_COLLAPSE_STORAGE_PREFIX}${sectionId}`;
|
||||||
const sidebarStorageKey = `${SIDEBAR_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) {
|
function setTopbarDropdownOpen(dropdownEl, isOpen) {
|
||||||
if (!(dropdownEl instanceof HTMLElement)) {
|
if (!(dropdownEl instanceof HTMLElement)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
+380
-89
@@ -62,7 +62,7 @@
|
|||||||
const FRAME_TOUCH_DRAG_ACTIVATION_DELAY_MS = 140;
|
const FRAME_TOUCH_DRAG_ACTIVATION_DELAY_MS = 140;
|
||||||
const FRAME_CUSTOM_CARD_PREFIX = "frame-custom-text:";
|
const FRAME_CUSTOM_CARD_PREFIX = "frame-custom-text:";
|
||||||
const FRAME_CUSTOM_CARD_ACTION_ID = "create-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_SLOT_HEIGHT = Math.round((EXPORT_SLOT_WIDTH * TAROT_CARD_HEIGHT_RATIO) / TAROT_CARD_WIDTH_RATIO);
|
||||||
const EXPORT_CARD_INSET = 0;
|
const EXPORT_CARD_INSET = 0;
|
||||||
const EXPORT_GRID_GAP = 10;
|
const EXPORT_GRID_GAP = 10;
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
webp: {
|
webp: {
|
||||||
mimeType: "image/webp",
|
mimeType: "image/webp",
|
||||||
extension: "webp",
|
extension: "webp",
|
||||||
quality: 0.98
|
quality: 1
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const FRAME_LAYOUT_GROUPS = [
|
const FRAME_LAYOUT_GROUPS = [
|
||||||
@@ -226,6 +226,8 @@
|
|||||||
currentLayoutId: "frames",
|
currentLayoutId: "frames",
|
||||||
customLayouts: [],
|
customLayouts: [],
|
||||||
customCards: new Map(),
|
customCards: new Map(),
|
||||||
|
slotDeckOverrides: new Map(),
|
||||||
|
frameDeckId: "",
|
||||||
layoutNotesById: {},
|
layoutNotesById: {},
|
||||||
exportInProgress: false,
|
exportInProgress: false,
|
||||||
exportFormat: "webp",
|
exportFormat: "webp",
|
||||||
@@ -253,6 +255,9 @@
|
|||||||
let cardActionMenuFlipEl = null;
|
let cardActionMenuFlipEl = null;
|
||||||
let cardActionMenuOpenEl = null;
|
let cardActionMenuOpenEl = null;
|
||||||
let cardActionMenuRemoveEl = null;
|
let cardActionMenuRemoveEl = null;
|
||||||
|
let cardActionMenuDeckApplyEl = null;
|
||||||
|
let cardActionMenuDeckResetEl = null;
|
||||||
|
let cardActionMenuDeckSelectEl = null;
|
||||||
let cardPickerEl = null;
|
let cardPickerEl = null;
|
||||||
let cardPickerTitleEl = null;
|
let cardPickerTitleEl = null;
|
||||||
let cardPickerSearchEl = null;
|
let cardPickerSearchEl = null;
|
||||||
@@ -307,6 +312,7 @@
|
|||||||
tarotFrameSettingsToggleEl: document.getElementById("tarot-frame-settings-toggle"),
|
tarotFrameSettingsToggleEl: document.getElementById("tarot-frame-settings-toggle"),
|
||||||
tarotFrameSettingsPanelEl: document.getElementById("tarot-frame-settings-panel"),
|
tarotFrameSettingsPanelEl: document.getElementById("tarot-frame-settings-panel"),
|
||||||
tarotFrameGridZoomEl: document.getElementById("tarot-frame-grid-zoom"),
|
tarotFrameGridZoomEl: document.getElementById("tarot-frame-grid-zoom"),
|
||||||
|
tarotFrameDeckSelectEl: document.getElementById("tarot-frame-deck-select"),
|
||||||
tarotFrameShowInfoEl: document.getElementById("tarot-frame-show-info"),
|
tarotFrameShowInfoEl: document.getElementById("tarot-frame-show-info"),
|
||||||
tarotFrameHouseSettingsEl: document.getElementById("tarot-frame-house-settings"),
|
tarotFrameHouseSettingsEl: document.getElementById("tarot-frame-house-settings"),
|
||||||
tarotFrameHouseTopCardsVisibleEl: document.getElementById("tarot-frame-house-top-cards-visible"),
|
tarotFrameHouseTopCardsVisibleEl: document.getElementById("tarot-frame-house-top-cards-visible"),
|
||||||
@@ -542,12 +548,130 @@
|
|||||||
return (leftSlot.row - rightSlot.row) || (leftSlot.column - rightSlot.column);
|
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() {
|
function buildFrameSettingsSnapshot() {
|
||||||
const topModes = config.getHouseTopInfoModes?.() || {};
|
const topModes = config.getHouseTopInfoModes?.() || {};
|
||||||
const bottomModes = config.getHouseBottomInfoModes?.() || {};
|
const bottomModes = config.getHouseBottomInfoModes?.() || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showInfo: Boolean(state.showInfo),
|
showInfo: Boolean(state.showInfo),
|
||||||
|
frameDeckId: normalizeFrameDeckId(state.frameDeckId),
|
||||||
gridZoomScale: getGridZoomScale(),
|
gridZoomScale: getGridZoomScale(),
|
||||||
gridZoomStepIndex: state.gridZoomStepIndex,
|
gridZoomStepIndex: state.gridZoomStepIndex,
|
||||||
houseTopCardsVisible: config.getHouseTopCardsVisible?.() !== false,
|
houseTopCardsVisible: config.getHouseTopCardsVisible?.() !== false,
|
||||||
@@ -575,6 +699,9 @@
|
|||||||
: fallback.gridZoomScale);
|
: fallback.gridZoomScale);
|
||||||
return {
|
return {
|
||||||
showInfo: raw.showInfo === undefined ? fallback.showInfo : Boolean(raw.showInfo),
|
showInfo: raw.showInfo === undefined ? fallback.showInfo : Boolean(raw.showInfo),
|
||||||
|
frameDeckId: raw.frameDeckId === undefined
|
||||||
|
? normalizeFrameDeckId(fallback.frameDeckId)
|
||||||
|
: normalizeFrameDeckId(raw.frameDeckId),
|
||||||
gridZoomScale: normalizedZoomScale,
|
gridZoomScale: normalizedZoomScale,
|
||||||
gridZoomStepIndex: Number.isFinite(rawZoomIndex)
|
gridZoomStepIndex: Number.isFinite(rawZoomIndex)
|
||||||
? Math.max(0, Math.min(FRAME_GRID_ZOOM_STEPS.length - 1, rawZoomIndex))
|
? Math.max(0, Math.min(FRAME_GRID_ZOOM_STEPS.length - 1, rawZoomIndex))
|
||||||
@@ -602,7 +729,8 @@
|
|||||||
.map(([slotId, cardId]) => ({
|
.map(([slotId, cardId]) => ({
|
||||||
slotId: String(slotId || "").trim(),
|
slotId: String(slotId || "").trim(),
|
||||||
cardId: String(cardId || "").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))
|
.filter((entry) => isValidSlotId(entry.slotId) && validCardIds.has(entry.cardId))
|
||||||
.sort((left, right) => {
|
.sort((left, right) => {
|
||||||
@@ -646,7 +774,8 @@
|
|||||||
.map((entry) => ({
|
.map((entry) => ({
|
||||||
slotId: String(entry?.slotId || "").trim(),
|
slotId: String(entry?.slotId || "").trim(),
|
||||||
cardId: String(entry?.cardId || "").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)
|
.filter((entry) => isValidSlotId(entry.slotId) && entry.cardId)
|
||||||
: [];
|
: [];
|
||||||
@@ -915,6 +1044,7 @@
|
|||||||
function applyFrameSettingsSnapshot(rawSettings) {
|
function applyFrameSettingsSnapshot(rawSettings) {
|
||||||
const settings = normalizeFrameSettingsSnapshot(rawSettings);
|
const settings = normalizeFrameSettingsSnapshot(rawSettings);
|
||||||
state.showInfo = settings.showInfo;
|
state.showInfo = settings.showInfo;
|
||||||
|
state.frameDeckId = normalizeFrameDeckId(settings.frameDeckId);
|
||||||
state.gridZoomScale = settings.gridZoomScale;
|
state.gridZoomScale = settings.gridZoomScale;
|
||||||
state.gridZoomStepIndex = settings.gridZoomStepIndex;
|
state.gridZoomStepIndex = settings.gridZoomStepIndex;
|
||||||
config.setHouseTopCardsVisible?.(settings.houseTopCardsVisible);
|
config.setHouseTopCardsVisible?.(settings.houseTopCardsVisible);
|
||||||
@@ -1426,7 +1556,36 @@
|
|||||||
cardActionMenuRemoveEl.dataset.cardAction = "remove";
|
cardActionMenuRemoveEl.dataset.cardAction = "remove";
|
||||||
cardActionMenuRemoveEl.textContent = "Remove Card";
|
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", () => {
|
cardActionMenuEl.addEventListener("pointerdown", () => {
|
||||||
state.cardActionMenu.clickArmed = true;
|
state.cardActionMenu.clickArmed = true;
|
||||||
});
|
});
|
||||||
@@ -1452,9 +1611,9 @@
|
|||||||
const slotId = String(state.cardActionMenu.slotId || "").trim();
|
const slotId = String(state.cardActionMenu.slotId || "").trim();
|
||||||
const cardId = String(state.cardActionMenu.cardId || "").trim();
|
const cardId = String(state.cardActionMenu.cardId || "").trim();
|
||||||
const action = String(actionButton.dataset.cardAction || "").trim();
|
const action = String(actionButton.dataset.cardAction || "").trim();
|
||||||
closeCardActionMenu();
|
|
||||||
|
|
||||||
if (action === "select") {
|
if (action === "select") {
|
||||||
|
closeCardActionMenu();
|
||||||
const selectionUpdate = setSelectedSlotIds(
|
const selectionUpdate = setSelectedSlotIds(
|
||||||
isSlotSelected(slotId)
|
isSlotSelected(slotId)
|
||||||
? getSelectedSlotIds().filter((selectedSlotId) => selectedSlotId !== slotId)
|
? getSelectedSlotIds().filter((selectedSlotId) => selectedSlotId !== slotId)
|
||||||
@@ -1468,6 +1627,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action === "flip") {
|
if (action === "flip") {
|
||||||
|
closeCardActionMenu();
|
||||||
if (toggleCardFlip(slotId)) {
|
if (toggleCardFlip(slotId)) {
|
||||||
render({ preserveViewport: true });
|
render({ preserveViewport: true });
|
||||||
}
|
}
|
||||||
@@ -1475,14 +1635,46 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action === "remove") {
|
if (action === "remove") {
|
||||||
|
closeCardActionMenu();
|
||||||
const removedCard = removeCardFromSlot(slotId);
|
const removedCard = removeCardFromSlot(slotId);
|
||||||
if (removedCard) {
|
if (removedCard) {
|
||||||
render({ preserveViewport: true });
|
render({ preserveViewport: true });
|
||||||
setStatus(`${getDisplayCardName(removedCard)} removed from ${describeSlot(slotId)}.`);
|
setStatus(`${getDisplayCardName(removedCard, slotId)} removed from ${describeSlot(slotId)}.`);
|
||||||
}
|
}
|
||||||
return;
|
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);
|
openCardLightbox(cardId, slotId);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1550,6 +1742,17 @@
|
|||||||
if (cardActionMenuRemoveEl) {
|
if (cardActionMenuRemoveEl) {
|
||||||
cardActionMenuRemoveEl.textContent = "Remove Card";
|
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 = {}) {
|
function openCardActionMenu(slotId, cardId, anchorX, anchorY, options = {}) {
|
||||||
@@ -1927,6 +2130,9 @@
|
|||||||
state.customCards.set(nextCard.id, nextCard);
|
state.customCards.set(nextCard.id, nextCard);
|
||||||
const previousSlotId = findAssignedSlotIdByCardId(nextCard.id);
|
const previousSlotId = findAssignedSlotIdByCardId(nextCard.id);
|
||||||
const inheritedFlip = previousSlotId && previousSlotId !== targetSlotId ? isSlotFlipped(previousSlotId) : isSlotFlipped(targetSlotId);
|
const inheritedFlip = previousSlotId && previousSlotId !== targetSlotId ? isSlotFlipped(previousSlotId) : isSlotFlipped(targetSlotId);
|
||||||
|
const inheritedDeckId = previousSlotId && previousSlotId !== targetSlotId
|
||||||
|
? getSlotDeckOverride(previousSlotId)
|
||||||
|
: getSlotDeckOverride(targetSlotId);
|
||||||
if (previousSlotId && previousSlotId !== targetSlotId) {
|
if (previousSlotId && previousSlotId !== targetSlotId) {
|
||||||
clearSlotAssignmentState(previousSlotId);
|
clearSlotAssignmentState(previousSlotId);
|
||||||
}
|
}
|
||||||
@@ -1934,7 +2140,8 @@
|
|||||||
assignCardToSlot(
|
assignCardToSlot(
|
||||||
targetSlotId,
|
targetSlotId,
|
||||||
nextCard.id,
|
nextCard.id,
|
||||||
inheritedFlip
|
inheritedFlip,
|
||||||
|
inheritedDeckId
|
||||||
);
|
);
|
||||||
pruneUnusedCustomCards();
|
pruneUnusedCustomCards();
|
||||||
state.layoutReady = true;
|
state.layoutReady = true;
|
||||||
@@ -2174,6 +2381,9 @@
|
|||||||
|
|
||||||
const previousSlotId = findAssignedSlotIdByCardId(targetCardId);
|
const previousSlotId = findAssignedSlotIdByCardId(targetCardId);
|
||||||
const inheritedFlip = previousSlotId && previousSlotId !== targetSlotId ? isSlotFlipped(previousSlotId) : isSlotFlipped(targetSlotId);
|
const inheritedFlip = previousSlotId && previousSlotId !== targetSlotId ? isSlotFlipped(previousSlotId) : isSlotFlipped(targetSlotId);
|
||||||
|
const inheritedDeckId = previousSlotId && previousSlotId !== targetSlotId
|
||||||
|
? getSlotDeckOverride(previousSlotId)
|
||||||
|
: getSlotDeckOverride(targetSlotId);
|
||||||
if (previousSlotId && previousSlotId !== targetSlotId) {
|
if (previousSlotId && previousSlotId !== targetSlotId) {
|
||||||
clearSlotAssignmentState(previousSlotId);
|
clearSlotAssignmentState(previousSlotId);
|
||||||
}
|
}
|
||||||
@@ -2181,13 +2391,14 @@
|
|||||||
assignCardToSlot(
|
assignCardToSlot(
|
||||||
targetSlotId,
|
targetSlotId,
|
||||||
targetCardId,
|
targetCardId,
|
||||||
inheritedFlip
|
inheritedFlip,
|
||||||
|
inheritedDeckId
|
||||||
);
|
);
|
||||||
pruneUnusedCustomCards();
|
pruneUnusedCustomCards();
|
||||||
state.layoutReady = true;
|
state.layoutReady = true;
|
||||||
render({ preserveViewport: true });
|
render({ preserveViewport: true });
|
||||||
syncControls();
|
syncControls();
|
||||||
setStatus(`${getDisplayCardName(card)} placed at ${describeSlot(targetSlotId)}.`);
|
setStatus(`${getDisplayCardName(card, targetSlotId)} placed at ${describeSlot(targetSlotId)}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearGrid() {
|
function clearGrid() {
|
||||||
@@ -2203,6 +2414,7 @@
|
|||||||
|
|
||||||
state.slotAssignments.clear();
|
state.slotAssignments.clear();
|
||||||
state.slotFlips.clear();
|
state.slotFlips.clear();
|
||||||
|
state.slotDeckOverrides.clear();
|
||||||
state.customCards.clear();
|
state.customCards.clear();
|
||||||
state.selectedSlotIds.clear();
|
state.selectedSlotIds.clear();
|
||||||
state.layoutReady = true;
|
state.layoutReady = true;
|
||||||
@@ -3079,8 +3291,8 @@
|
|||||||
return cards.map((card) => getCardId(card)).filter(Boolean).sort().join("|");
|
return cards.map((card) => getCardId(card)).filter(Boolean).sort().join("|");
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDeckOptions(card) {
|
function resolveDeckOptions(card, slotId = "") {
|
||||||
const deckId = String(tarotCardImages.getActiveDeck?.() || "").trim();
|
const deckId = getResolvedSlotDeckId(slotId);
|
||||||
const trumpNumber = card?.arcana === "Major" && Number.isFinite(Number(card?.number))
|
const trumpNumber = card?.arcana === "Major" && Number.isFinite(Number(card?.number))
|
||||||
? Number(card.number)
|
? Number(card.number)
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -3095,7 +3307,7 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveCardThumbnail(card) {
|
function resolveCardThumbnail(card, slotId = "") {
|
||||||
if (!card) {
|
if (!card) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -3104,7 +3316,7 @@
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const deckOptions = resolveDeckOptions(card) || undefined;
|
const deckOptions = resolveDeckOptions(card, slotId) || undefined;
|
||||||
return String(
|
return String(
|
||||||
tarotCardImages.resolveTarotCardThumbnail?.(card.name, deckOptions)
|
tarotCardImages.resolveTarotCardThumbnail?.(card.name, deckOptions)
|
||||||
|| tarotCardImages.resolveTarotCardImage?.(card.name, deckOptions)
|
|| tarotCardImages.resolveTarotCardImage?.(card.name, deckOptions)
|
||||||
@@ -3112,12 +3324,12 @@
|
|||||||
).trim();
|
).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDisplayCardName(card) {
|
function getDisplayCardName(card, slotId = "") {
|
||||||
if (isCustomFrameCard(card)) {
|
if (isCustomFrameCard(card)) {
|
||||||
return parseCustomCardText(card?.customText).primary || "Custom 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";
|
return String(label || card?.name || "Tarot").trim() || "Tarot";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3434,7 +3646,7 @@
|
|||||||
return config.getHouseBottomCardsVisible?.() !== false;
|
return config.getHouseBottomCardsVisible?.() !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCardTextFaceModel(card) {
|
function buildCardTextFaceModel(card, slotId = "") {
|
||||||
if (isCustomFrameCard(card)) {
|
if (isCustomFrameCard(card)) {
|
||||||
const customText = parseCustomCardText(card?.customText);
|
const customText = parseCustomCardText(card?.customText);
|
||||||
const backgroundColor = normalizeCustomCardBackgroundColor(card?.backgroundColor);
|
const backgroundColor = normalizeCustomCardBackgroundColor(card?.backgroundColor);
|
||||||
@@ -3449,7 +3661,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const label = state.showInfo ? buildHouseLabel(card) : null;
|
const label = state.showInfo ? buildHouseLabel(card) : null;
|
||||||
const displayName = normalizeLabelText(getDisplayCardName(card));
|
const displayName = normalizeLabelText(getDisplayCardName(card, slotId));
|
||||||
|
|
||||||
if (card?.arcana !== "Major" && label?.primary) {
|
if (card?.arcana !== "Major" && label?.primary) {
|
||||||
return {
|
return {
|
||||||
@@ -3581,9 +3793,10 @@
|
|||||||
|
|
||||||
state.slotAssignments.delete(targetSlotId);
|
state.slotAssignments.delete(targetSlotId);
|
||||||
state.slotFlips.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 targetSlotId = String(slotId || "").trim();
|
||||||
const targetCardId = String(cardId || "").trim();
|
const targetCardId = String(cardId || "").trim();
|
||||||
if (!isValidSlotId(targetSlotId) || !targetCardId) {
|
if (!isValidSlotId(targetSlotId) || !targetCardId) {
|
||||||
@@ -3592,6 +3805,7 @@
|
|||||||
|
|
||||||
state.slotAssignments.set(targetSlotId, targetCardId);
|
state.slotAssignments.set(targetSlotId, targetCardId);
|
||||||
setSlotFlipState(targetSlotId, isFlipped);
|
setSlotFlipState(targetSlotId, isFlipped);
|
||||||
|
setSlotDeckOverride(targetSlotId, deckId);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3608,8 +3822,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isFlipped = isSlotFlipped(fromSlotId);
|
const isFlipped = isSlotFlipped(fromSlotId);
|
||||||
|
const deckId = getSlotDeckOverride(fromSlotId);
|
||||||
clearSlotAssignmentState(fromSlotId);
|
clearSlotAssignmentState(fromSlotId);
|
||||||
assignCardToSlot(toSlotId, cardId, isFlipped);
|
assignCardToSlot(toSlotId, cardId, isFlipped, deckId);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3628,12 +3843,16 @@
|
|||||||
|
|
||||||
const sourceFlip = isSlotFlipped(fromSlotId);
|
const sourceFlip = isSlotFlipped(fromSlotId);
|
||||||
const targetFlip = isSlotFlipped(toSlotId);
|
const targetFlip = isSlotFlipped(toSlotId);
|
||||||
|
const sourceDeckId = getSlotDeckOverride(fromSlotId);
|
||||||
|
const targetDeckId = getSlotDeckOverride(toSlotId);
|
||||||
|
|
||||||
state.slotAssignments.set(toSlotId, sourceCardId);
|
state.slotAssignments.set(toSlotId, sourceCardId);
|
||||||
setSlotFlipState(toSlotId, sourceFlip);
|
setSlotFlipState(toSlotId, sourceFlip);
|
||||||
|
setSlotDeckOverride(toSlotId, sourceDeckId);
|
||||||
if (targetCardId) {
|
if (targetCardId) {
|
||||||
state.slotAssignments.set(fromSlotId, targetCardId);
|
state.slotAssignments.set(fromSlotId, targetCardId);
|
||||||
setSlotFlipState(fromSlotId, targetFlip);
|
setSlotFlipState(fromSlotId, targetFlip);
|
||||||
|
setSlotDeckOverride(fromSlotId, targetDeckId);
|
||||||
} else {
|
} else {
|
||||||
clearSlotAssignmentState(fromSlotId);
|
clearSlotAssignmentState(fromSlotId);
|
||||||
}
|
}
|
||||||
@@ -3656,7 +3875,7 @@
|
|||||||
setSlotFlipState(targetSlotId, nextIsFlipped);
|
setSlotFlipState(targetSlotId, nextIsFlipped);
|
||||||
state.layoutReady = true;
|
state.layoutReady = true;
|
||||||
const card = getCardMap(getCards()).get(cardId) || null;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3673,6 +3892,7 @@
|
|||||||
state.currentLayoutId = layoutPreset.id;
|
state.currentLayoutId = layoutPreset.id;
|
||||||
state.slotAssignments.clear();
|
state.slotAssignments.clear();
|
||||||
state.slotFlips.clear();
|
state.slotFlips.clear();
|
||||||
|
state.slotDeckOverrides.clear();
|
||||||
state.selectedSlotIds.clear();
|
state.selectedSlotIds.clear();
|
||||||
state.customCards.clear();
|
state.customCards.clear();
|
||||||
|
|
||||||
@@ -3697,10 +3917,11 @@
|
|||||||
state.currentLayoutId = savedLayout.id;
|
state.currentLayoutId = savedLayout.id;
|
||||||
state.slotAssignments.clear();
|
state.slotAssignments.clear();
|
||||||
state.slotFlips.clear();
|
state.slotFlips.clear();
|
||||||
|
state.slotDeckOverrides.clear();
|
||||||
state.selectedSlotIds.clear();
|
state.selectedSlotIds.clear();
|
||||||
savedLayout.slotAssignments.forEach((entry) => {
|
savedLayout.slotAssignments.forEach((entry) => {
|
||||||
if (cardMap.has(entry.cardId)) {
|
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);
|
applyFrameSettingsSnapshot(savedLayout.settings);
|
||||||
@@ -3912,7 +4133,7 @@
|
|||||||
return cardMap.get(cardId) || null;
|
return cardMap.get(cardId) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCardOverlayLabel(card) {
|
function getCardOverlayLabel(card, slotId = "") {
|
||||||
if (!state.showInfo) {
|
if (!state.showInfo) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -3927,7 +4148,7 @@
|
|||||||
return structuredLabel;
|
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) {
|
function getOccupiedGridBounds(gridTrackEl) {
|
||||||
@@ -4206,18 +4427,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
button.dataset.cardId = getCardId(card);
|
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.setAttribute("aria-label", `${getDisplayCardName(card, slotId)} in row ${row}, column ${column}${flipped ? ", flipped upside down" : ""}${selected ? ", selected for group move" : ""}`);
|
||||||
button.title = `${getDisplayCardName(card)}${flipped ? " (flipped upside down)" : ""}`;
|
button.title = `${getDisplayCardName(card, slotId)}${flipped ? " (flipped upside down)" : ""}`;
|
||||||
|
|
||||||
const showImage = shouldShowCardImage(card);
|
const showImage = shouldShowCardImage(card);
|
||||||
|
|
||||||
const imageSrc = resolveCardThumbnail(card);
|
const imageSrc = resolveCardThumbnail(card, slotId);
|
||||||
if (showImage && imageSrc) {
|
if (showImage && imageSrc) {
|
||||||
const imageCached = tarotCardImages.isImageLoaded?.(imageSrc) === true;
|
const imageCached = tarotCardImages.isImageLoaded?.(imageSrc) === true;
|
||||||
const image = document.createElement("img");
|
const image = document.createElement("img");
|
||||||
image.className = "tarot-frame-card-image";
|
image.className = "tarot-frame-card-image";
|
||||||
image.src = imageSrc;
|
image.src = imageSrc;
|
||||||
image.alt = getDisplayCardName(card);
|
image.alt = getDisplayCardName(card, slotId);
|
||||||
image.loading = imageCached ? "eager" : "lazy";
|
image.loading = imageCached ? "eager" : "lazy";
|
||||||
image.decoding = "async";
|
image.decoding = "async";
|
||||||
image.draggable = false;
|
image.draggable = false;
|
||||||
@@ -4225,16 +4446,16 @@
|
|||||||
} else if (showImage) {
|
} else if (showImage) {
|
||||||
const fallback = document.createElement("span");
|
const fallback = document.createElement("span");
|
||||||
fallback.className = "tarot-frame-card-fallback";
|
fallback.className = "tarot-frame-card-fallback";
|
||||||
fallback.textContent = getDisplayCardName(card);
|
fallback.textContent = getDisplayCardName(card, slotId);
|
||||||
button.appendChild(fallback);
|
button.appendChild(fallback);
|
||||||
} else {
|
} else {
|
||||||
button.appendChild(createCardTextFaceElement(buildCardTextFaceModel(card)));
|
button.appendChild(createCardTextFaceElement(buildCardTextFaceModel(card, slotId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showImage && state.showInfo) {
|
if (showImage && state.showInfo) {
|
||||||
const overlay = document.createElement("span");
|
const overlay = document.createElement("span");
|
||||||
overlay.className = "tarot-frame-card-badge";
|
overlay.className = "tarot-frame-card-badge";
|
||||||
overlay.textContent = getCardOverlayLabel(card);
|
overlay.textContent = getCardOverlayLabel(card, slotId);
|
||||||
button.appendChild(overlay);
|
button.appendChild(overlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4462,6 +4683,7 @@
|
|||||||
tarotFrameSettingsToggleEl,
|
tarotFrameSettingsToggleEl,
|
||||||
tarotFrameSettingsPanelEl,
|
tarotFrameSettingsPanelEl,
|
||||||
tarotFrameGridZoomEl,
|
tarotFrameGridZoomEl,
|
||||||
|
tarotFrameDeckSelectEl,
|
||||||
tarotFrameShowInfoEl,
|
tarotFrameShowInfoEl,
|
||||||
tarotFrameHouseSettingsEl,
|
tarotFrameHouseSettingsEl,
|
||||||
tarotFrameHouseTopCardsVisibleEl,
|
tarotFrameHouseTopCardsVisibleEl,
|
||||||
@@ -4523,6 +4745,11 @@
|
|||||||
tarotFrameGridZoomEl.disabled = Boolean(state.exportInProgress);
|
tarotFrameGridZoomEl.disabled = Boolean(state.exportInProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tarotFrameDeckSelectEl) {
|
||||||
|
buildDeckSelectOptions(tarotFrameDeckSelectEl, normalizeFrameDeckId(state.frameDeckId) || getFrameDeckId(), false);
|
||||||
|
tarotFrameDeckSelectEl.disabled = Boolean(state.exportInProgress);
|
||||||
|
}
|
||||||
|
|
||||||
if (tarotFrameShowInfoEl) {
|
if (tarotFrameShowInfoEl) {
|
||||||
tarotFrameShowInfoEl.checked = Boolean(state.showInfo);
|
tarotFrameShowInfoEl.checked = Boolean(state.showInfo);
|
||||||
tarotFrameShowInfoEl.disabled = Boolean(state.exportInProgress);
|
tarotFrameShowInfoEl.disabled = Boolean(state.exportInProgress);
|
||||||
@@ -4665,7 +4892,8 @@
|
|||||||
sourceSlotId,
|
sourceSlotId,
|
||||||
targetSlotId,
|
targetSlotId,
|
||||||
cardId,
|
cardId,
|
||||||
isFlipped: isSlotFlipped(sourceSlotId)
|
isFlipped: isSlotFlipped(sourceSlotId),
|
||||||
|
deckId: getSlotDeckOverride(sourceSlotId)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4688,7 +4916,7 @@
|
|||||||
clearSlotAssignmentState(move.sourceSlotId);
|
clearSlotAssignmentState(move.sourceSlotId);
|
||||||
});
|
});
|
||||||
plan.moves.forEach((move) => {
|
plan.moves.forEach((move) => {
|
||||||
assignCardToSlot(move.targetSlotId, move.cardId, move.isFlipped);
|
assignCardToSlot(move.targetSlotId, move.cardId, move.isFlipped, move.deckId);
|
||||||
});
|
});
|
||||||
state.layoutReady = true;
|
state.layoutReady = true;
|
||||||
pruneUnusedCustomCards();
|
pruneUnusedCustomCards();
|
||||||
@@ -4713,20 +4941,20 @@
|
|||||||
ghost.appendChild(countEl);
|
ghost.appendChild(countEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageSrc = resolveCardThumbnail(card);
|
const imageSrc = resolveCardThumbnail(card, sourceSlotId);
|
||||||
if (imageSrc) {
|
if (imageSrc) {
|
||||||
const image = document.createElement("img");
|
const image = document.createElement("img");
|
||||||
image.src = imageSrc;
|
image.src = imageSrc;
|
||||||
image.alt = "";
|
image.alt = "";
|
||||||
ghost.appendChild(image);
|
ghost.appendChild(image);
|
||||||
} else {
|
} else {
|
||||||
ghost.appendChild(createCardTextFaceElement(buildCardTextFaceModel(card)));
|
ghost.appendChild(createCardTextFaceElement(buildCardTextFaceModel(card, sourceSlotId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.showInfo) {
|
if (state.showInfo) {
|
||||||
const label = document.createElement("span");
|
const label = document.createElement("span");
|
||||||
label.className = "tarot-frame-drag-ghost-label";
|
label.className = "tarot-frame-drag-ghost-label";
|
||||||
label.textContent = getCardOverlayLabel(card);
|
label.textContent = getCardOverlayLabel(card, sourceSlotId);
|
||||||
if (label.textContent) {
|
if (label.textContent) {
|
||||||
ghost.appendChild(label);
|
ghost.appendChild(label);
|
||||||
}
|
}
|
||||||
@@ -4914,7 +5142,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deckOptions = resolveDeckOptions(card);
|
const deckOptions = resolveDeckOptions(card, slotId);
|
||||||
const src = String(
|
const src = String(
|
||||||
tarotCardImages.resolveTarotCardImage?.(card.name, deckOptions)
|
tarotCardImages.resolveTarotCardImage?.(card.name, deckOptions)
|
||||||
|| tarotCardImages.resolveTarotCardThumbnail?.(card.name, deckOptions)
|
|| tarotCardImages.resolveTarotCardThumbnail?.(card.name, deckOptions)
|
||||||
@@ -4925,13 +5153,13 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const label = getDisplayCardName(card);
|
const label = getDisplayCardName(card, slotId);
|
||||||
window.TarotUiLightbox?.open?.({
|
window.TarotUiLightbox?.open?.({
|
||||||
src,
|
src,
|
||||||
altText: label,
|
altText: label,
|
||||||
label,
|
label,
|
||||||
cardId: getCardId(card),
|
cardId: getCardId(card),
|
||||||
deckId: String(tarotCardImages.getActiveDeck?.() || "").trim()
|
deckId: getResolvedSlotDeckId(slotId)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5528,7 +5756,7 @@
|
|||||||
context.restore();
|
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) {
|
if (!card) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -5538,7 +5766,7 @@
|
|||||||
const cardWidth = width - (EXPORT_CARD_INSET * 2);
|
const cardWidth = width - (EXPORT_CARD_INSET * 2);
|
||||||
const cardHeight = height - (EXPORT_CARD_INSET * 2);
|
const cardHeight = height - (EXPORT_CARD_INSET * 2);
|
||||||
const showImage = shouldShowCardImage(card);
|
const showImage = shouldShowCardImage(card);
|
||||||
const faceModel = showImage ? null : buildCardTextFaceModel(card);
|
const faceModel = showImage ? null : buildCardTextFaceModel(card, slotId);
|
||||||
|
|
||||||
context.save();
|
context.save();
|
||||||
if (isFlipped) {
|
if (isFlipped) {
|
||||||
@@ -5557,7 +5785,7 @@
|
|||||||
context.textAlign = "center";
|
context.textAlign = "center";
|
||||||
context.textBaseline = "middle";
|
context.textBaseline = "middle";
|
||||||
context.font = "700 14px 'Segoe UI', sans-serif";
|
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;
|
const lineHeight = 18;
|
||||||
let currentY = cardY + (cardHeight / 2) - (((Math.max(1, lines.length) - 1) * lineHeight) / 2);
|
let currentY = cardY + (cardHeight / 2) - (((Math.max(1, lines.length) - 1) * lineHeight) / 2);
|
||||||
lines.forEach((line) => {
|
lines.forEach((line) => {
|
||||||
@@ -5579,7 +5807,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showImage && state.showInfo) {
|
if (showImage && state.showInfo) {
|
||||||
const overlayText = getCardOverlayLabel(card);
|
const overlayText = getCardOverlayLabel(card, slotId);
|
||||||
if (overlayText) {
|
if (overlayText) {
|
||||||
const overlayHeight = 30;
|
const overlayHeight = 30;
|
||||||
const overlayX = cardX + 4;
|
const overlayX = cardX + 4;
|
||||||
@@ -5605,47 +5833,91 @@
|
|||||||
context.restore();
|
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) => {
|
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();
|
const image = new Image();
|
||||||
image.decoding = "async";
|
image.decoding = "async";
|
||||||
image.onload = () => resolve(image);
|
if (crossOrigin) {
|
||||||
image.onerror = () => resolve(null);
|
image.crossOrigin = crossOrigin;
|
||||||
image.src = normalizedSrc;
|
}
|
||||||
|
|
||||||
|
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) {
|
function isExportFormatSupported(format) {
|
||||||
const exportFormat = EXPORT_FORMATS[format];
|
const exportFormat = EXPORT_FORMATS[format];
|
||||||
if (!exportFormat) {
|
if (!exportFormat) {
|
||||||
@@ -5660,13 +5932,17 @@
|
|||||||
function canvasToBlobByFormat(canvas, format) {
|
function canvasToBlobByFormat(canvas, format) {
|
||||||
const exportFormat = EXPORT_FORMATS[format] || EXPORT_FORMATS.webp;
|
const exportFormat = EXPORT_FORMATS[format] || EXPORT_FORMATS.webp;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
canvas.toBlob((blob) => {
|
try {
|
||||||
if (blob) {
|
canvas.toBlob((blob) => {
|
||||||
resolve(blob);
|
if (blob) {
|
||||||
return;
|
resolve(blob);
|
||||||
}
|
return;
|
||||||
reject(new Error("Canvas export failed."));
|
}
|
||||||
}, exportFormat.mimeType, exportFormat.quality);
|
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 contentHeight = (MASTER_GRID_SIZE * EXPORT_SLOT_HEIGHT) + ((MASTER_GRID_SIZE - 1) * EXPORT_GRID_GAP);
|
||||||
const canvasWidth = contentWidth + (EXPORT_PADDING * 2);
|
const canvasWidth = contentWidth + (EXPORT_PADDING * 2);
|
||||||
const canvasHeight = contentHeight + (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");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = Math.ceil(canvasWidth * scale);
|
canvas.width = Math.ceil(canvasWidth * scale);
|
||||||
canvas.height = Math.ceil(canvasHeight * scale);
|
canvas.height = Math.ceil(canvasHeight * scale);
|
||||||
@@ -5698,7 +5974,8 @@
|
|||||||
|
|
||||||
const imageCache = new Map();
|
const imageCache = new Map();
|
||||||
cards.forEach((card) => {
|
cards.forEach((card) => {
|
||||||
const src = resolveCardThumbnail(card);
|
const slotId = findAssignedSlotIdByCardId(getCardId(card));
|
||||||
|
const src = resolveCardThumbnail(card, slotId);
|
||||||
if (src && !imageCache.has(src)) {
|
if (src && !imageCache.has(src)) {
|
||||||
imageCache.set(src, loadCardImage(src));
|
imageCache.set(src, loadCardImage(src));
|
||||||
}
|
}
|
||||||
@@ -5706,7 +5983,8 @@
|
|||||||
|
|
||||||
const resolvedImages = new Map();
|
const resolvedImages = new Map();
|
||||||
await Promise.all(cards.map(async (card) => {
|
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;
|
const image = src ? await imageCache.get(src) : null;
|
||||||
resolvedImages.set(getCardId(card), image || null);
|
resolvedImages.set(getCardId(card), image || null);
|
||||||
}));
|
}));
|
||||||
@@ -5741,7 +6019,8 @@
|
|||||||
card,
|
card,
|
||||||
card ? resolvedImages.get(getCardId(card)) : null,
|
card ? resolvedImages.get(getCardId(card)) : null,
|
||||||
card ? resolvedCustomBackgroundImages.get(getCardId(card)) : null,
|
card ? resolvedCustomBackgroundImages.get(getCardId(card)) : null,
|
||||||
card ? isSlotFlipped(slotId) : false
|
card ? isSlotFlipped(slotId) : false,
|
||||||
|
slotId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5792,6 +6071,7 @@
|
|||||||
tarotFrameSettingsToggleEl,
|
tarotFrameSettingsToggleEl,
|
||||||
tarotFrameSettingsPanelEl,
|
tarotFrameSettingsPanelEl,
|
||||||
tarotFrameGridZoomEl,
|
tarotFrameGridZoomEl,
|
||||||
|
tarotFrameDeckSelectEl,
|
||||||
tarotFrameShowInfoEl,
|
tarotFrameShowInfoEl,
|
||||||
tarotFrameHouseTopCardsVisibleEl,
|
tarotFrameHouseTopCardsVisibleEl,
|
||||||
tarotFrameHouseTopInfoHebrewEl,
|
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)],
|
[tarotFrameHouseTopCardsVisibleEl, (checked) => config.setHouseTopCardsVisible?.(checked)],
|
||||||
[tarotFrameHouseTopInfoHebrewEl, (checked) => config.setHouseTopInfoMode?.("hebrew", checked)],
|
[tarotFrameHouseTopInfoHebrewEl, (checked) => config.setHouseTopInfoMode?.("hebrew", checked)],
|
||||||
|
|||||||
+19
@@ -460,6 +460,10 @@
|
|||||||
<option value="14">1400%</option>
|
<option value="14">1400%</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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">
|
<label class="tarot-frame-toggle" for="tarot-frame-show-info">
|
||||||
<input id="tarot-frame-show-info" type="checkbox" checked>
|
<input id="tarot-frame-show-info" type="checkbox" checked>
|
||||||
<span>Display Info</span>
|
<span>Display Info</span>
|
||||||
@@ -1017,6 +1021,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="alpha-gematria-result" class="alpha-gematria-result">Total: --</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-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 id="alpha-gematria-matches" class="alpha-gematria-matches" aria-live="polite" hidden></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -1347,6 +1365,7 @@
|
|||||||
<script src="node_modules/@toast-ui/calendar/dist/toastui-calendar.min.js"></script>
|
<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/suncalc/suncalc.js"></script>
|
||||||
<script src="node_modules/astronomy-engine/astronomy.browser.min.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/astro-calcs.js"></script>
|
||||||
<script src="app/app-config.js?v=20260529-access-gating-01"></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>
|
<script src="app/data-service.js?v=20260601-now-polling-03"></script>
|
||||||
|
|||||||
Generated
+50
@@ -12,6 +12,7 @@
|
|||||||
"@fontsource/noto-serif": "^5.2.9",
|
"@fontsource/noto-serif": "^5.2.9",
|
||||||
"@toast-ui/calendar": "^2.1.3",
|
"@toast-ui/calendar": "^2.1.3",
|
||||||
"astronomy-engine": "^2.1.19",
|
"astronomy-engine": "^2.1.19",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"suncalc": "^1.9.0"
|
"suncalc": "^1.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -318,6 +319,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/basic-auth": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
||||||
@@ -418,6 +428,15 @@
|
|||||||
"node": ">= 0.4.0"
|
"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": {
|
"node_modules/css-tree": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
||||||
@@ -703,6 +722,19 @@
|
|||||||
"node": ">=12"
|
"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": {
|
"node_modules/http-proxy": {
|
||||||
"version": "1.18.1",
|
"version": "1.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
"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==",
|
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/tldts": {
|
||||||
"version": "7.0.25",
|
"version": "7.0.25",
|
||||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.25.tgz",
|
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.25.tgz",
|
||||||
@@ -1265,6 +1306,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/w3c-xmlserializer": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"@fontsource/noto-serif": "^5.2.9",
|
"@fontsource/noto-serif": "^5.2.9",
|
||||||
"@toast-ui/calendar": "^2.1.3",
|
"@toast-ui/calendar": "^2.1.3",
|
||||||
"astronomy-engine": "^2.1.19",
|
"astronomy-engine": "^2.1.19",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"suncalc": "^1.9.0"
|
"suncalc": "^1.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
Reference in New Issue
Block a user