updated tarot frame for mobile and desktop usability

This commit is contained in:
2026-04-13 14:28:03 -07:00
parent 7149bbfa7f
commit 4872e814c9
6 changed files with 1217 additions and 102 deletions
+7 -2
View File
@@ -378,9 +378,14 @@
}));
}
async function loadGematriaWordsByValue(value) {
async function loadGematriaWordsByValue(value, options = {}) {
const ciphers = Array.isArray(options?.ciphers)
? options.ciphers.map((cipherId) => String(cipherId || "").trim()).filter(Boolean).join(",")
: String(options?.ciphers || "").trim();
return fetchJson(buildApiUrl("/api/v1/gematria/words", {
value
value,
ciphers
}));
}
+140 -11
View File
@@ -907,6 +907,30 @@
flex-wrap: wrap;
}
.tarot-frame-selection-chip {
padding: 10px 14px;
border: 1px solid rgba(56, 189, 248, 0.55);
border-radius: 999px;
background: linear-gradient(180deg, rgba(8, 47, 73, 0.96), rgba(12, 74, 110, 0.98));
color: #e0f2fe;
cursor: pointer;
font-size: 13px;
font-weight: 800;
letter-spacing: 0.02em;
box-shadow: 0 10px 24px rgba(2, 132, 199, 0.18);
}
.tarot-frame-selection-chip:hover,
.tarot-frame-selection-chip:focus-visible {
border-color: rgba(125, 211, 252, 0.92);
background: linear-gradient(180deg, rgba(12, 74, 110, 0.98), rgba(14, 116, 144, 1));
color: #f0f9ff;
}
.tarot-frame-selection-chip[hidden] {
display: none !important;
}
.tarot-frame-layout-panel {
position: absolute;
top: calc(100% + 10px);
@@ -1681,7 +1705,8 @@
gap: 12px;
}
.tarot-frame-card-insert-menu {
.tarot-frame-card-insert-menu,
.tarot-frame-card-action-menu {
position: fixed;
z-index: 41;
width: min(240px, calc(100vw - 24px));
@@ -1696,11 +1721,13 @@
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.42);
}
.tarot-frame-card-insert-menu[hidden] {
.tarot-frame-card-insert-menu[hidden],
.tarot-frame-card-action-menu[hidden] {
display: none !important;
}
.tarot-frame-card-insert-menu-item {
.tarot-frame-card-insert-menu-item,
.tarot-frame-card-action-menu-item {
padding: 11px 12px;
border: 1px solid rgba(99, 102, 241, 0.22);
border-radius: 12px;
@@ -1713,7 +1740,9 @@
}
.tarot-frame-card-insert-menu-item:hover,
.tarot-frame-card-insert-menu-item:focus-visible {
.tarot-frame-card-insert-menu-item:focus-visible,
.tarot-frame-card-action-menu-item:hover,
.tarot-frame-card-action-menu-item:focus-visible {
border-color: rgba(165, 180, 252, 0.9);
background: rgba(49, 46, 129, 0.34);
color: #f8fafc;
@@ -2052,6 +2081,11 @@
border-radius: 8px;
}
.tarot-frame-slot.is-selected {
box-shadow: 0 0 0 2px #38bdf8, 0 0 0 6px rgba(56, 189, 248, 0.2);
border-radius: 8px;
}
.tarot-frame-slot.is-drag-source {
opacity: 0.42;
}
@@ -2072,6 +2106,8 @@
}
.tarot-frame-card {
--frame-card-rotation: 0deg;
--frame-card-hover-lift: 0px;
position: absolute;
inset: 0;
padding: 0;
@@ -2086,6 +2122,12 @@
user-select: none;
touch-action: none;
-webkit-touch-callout: none;
transform: translateY(var(--frame-card-hover-lift)) rotate(var(--frame-card-rotation));
transform-origin: center center;
}
.tarot-frame-card.is-flipped {
--frame-card-rotation: 180deg;
}
.tarot-frame-card.is-empty {
@@ -2096,13 +2138,18 @@
}
.tarot-frame-card:hover {
transform: translateY(-2px);
--frame-card-hover-lift: -2px;
filter: drop-shadow(0 10px 18px rgba(15, 23, 42, 0.38));
}
.tarot-frame-card.is-selected {
filter: drop-shadow(0 12px 20px rgba(14, 165, 233, 0.34));
}
.tarot-frame-card.is-empty:hover {
border-color: transparent;
box-shadow: none;
--frame-card-hover-lift: 0px;
transform: none;
}
@@ -2112,6 +2159,7 @@
}
.tarot-frame-card:hover {
--frame-card-hover-lift: 0px;
transform: none;
filter: none;
}
@@ -2288,6 +2336,28 @@
transform: translate(-50%, -50%);
}
.tarot-frame-drag-ghost.is-flipped {
transform: translate(-50%, -50%) rotate(180deg);
}
.tarot-frame-drag-ghost-count {
position: absolute;
top: 8px;
right: 8px;
min-width: 26px;
height: 26px;
padding: 0 8px;
border-radius: 999px;
display: grid;
place-items: center;
background: rgba(56, 189, 248, 0.94);
color: #082f49;
font-size: 12px;
font-weight: 800;
line-height: 1;
box-shadow: 0 8px 20px rgba(2, 132, 199, 0.34);
}
.tarot-frame-drag-ghost img {
width: 100%;
height: 100%;
@@ -2410,6 +2480,13 @@
width: 100%;
}
.tarot-frame-selection-chip {
order: -1;
width: 100%;
justify-content: center;
text-align: center;
}
.tarot-frame-settings-panel {
left: 0;
right: auto;
@@ -2430,8 +2507,8 @@
}
.tarot-frame-card-badge {
font-size: 7px;
padding: 3px 4px;
font-size: 11px;
padding: 4px 5px;
}
.tarot-frame-card-text-face {
@@ -4227,6 +4304,56 @@
font-size: 13px;
}
.alpha-gematria-cipher[hidden] {
display: none;
}
.alpha-gematria-reverse-ciphers {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.alpha-gematria-reverse-ciphers[hidden] {
display: none;
}
.alpha-gematria-reverse-cipher-option {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border: 1px solid #3f3f46;
border-radius: 999px;
background: #101018;
color: #d4d4d8;
cursor: pointer;
-webkit-user-select: none;
user-select: none;
}
.alpha-gematria-reverse-cipher-option:has(input:checked) {
border-color: #6366f1;
background: #1c1b35;
color: #eef2ff;
}
.alpha-gematria-reverse-cipher-option input {
margin: 0;
accent-color: #818cf8;
}
.alpha-gematria-reverse-cipher-name {
font-size: 12px;
line-height: 1.2;
}
.alpha-gematria-reverse-cipher-hint {
color: #a1a1aa;
font-size: 11px;
line-height: 1.4;
}
.alpha-gematria-input {
min-height: 54px;
resize: vertical;
@@ -4330,15 +4457,17 @@
}
@media (max-width: 900px) {
.alpha-gematria-controls.is-input-priority-mode,
.alpha-gematria-controls:has(.alpha-gematria-cipher:disabled) {
.alpha-gematria-controls.is-input-priority-mode {
grid-template-columns: minmax(0, 1fr);
}
.alpha-gematria-controls.is-input-priority-mode .alpha-gematria-field-cipher,
.alpha-gematria-controls:has(.alpha-gematria-cipher:disabled) .alpha-gematria-field-cipher {
.alpha-gematria-controls.is-input-priority-mode .alpha-gematria-field-cipher {
display: none;
}
.alpha-gematria-controls.is-reverse-cipher-mode {
grid-template-columns: minmax(0, 1fr);
}
}
.alpha-tabs {
+144 -13
View File
@@ -12,7 +12,9 @@
modeEls: [],
matchesEl: null,
inputLabelEl: null,
cipherLabelEl: null
cipherLabelEl: null,
reverseCiphersEl: null,
reverseCipherHintEl: null
})
};
@@ -23,6 +25,7 @@
activeCipherId: "",
forwardInputText: "",
reverseInputText: "",
reverseSelectedCipherIds: null,
anagramInputText: "",
dictionaryInputText: "",
activeMode: "forward",
@@ -52,7 +55,9 @@
modeEls: [],
matchesEl: null,
inputLabelEl: null,
cipherLabelEl: null
cipherLabelEl: null,
reverseCiphersEl: null,
reverseCipherHintEl: null
};
}
@@ -273,6 +278,80 @@
return ciphers.find((cipher) => cipher.id === selectedId) || ciphers[0];
}
function getGematriaCiphers() {
const db = state.db || getFallbackGematriaDb();
return Array.isArray(db.ciphers) ? db.ciphers : [];
}
function normalizeSelectedReverseCipherIds(rawCipherIds) {
const ciphers = getGematriaCiphers();
const requestedIds = Array.isArray(rawCipherIds)
? rawCipherIds.map((cipherId) => String(cipherId || "").trim()).filter(Boolean)
: [];
const requestedIdSet = new Set(requestedIds);
return ciphers
.map((cipher) => cipher.id)
.filter((cipherId) => requestedIdSet.has(cipherId));
}
function getDefaultReverseCipherIds() {
const activeCipher = getActiveGematriaCipher();
if (activeCipher?.id) {
return [activeCipher.id];
}
const firstCipher = getGematriaCiphers()[0];
return firstCipher?.id ? [firstCipher.id] : [];
}
function getSelectedReverseCipherIds() {
if (state.reverseSelectedCipherIds === null) {
return getDefaultReverseCipherIds();
}
return normalizeSelectedReverseCipherIds(state.reverseSelectedCipherIds);
}
function setSelectedReverseCipherIds(rawCipherIds) {
state.reverseSelectedCipherIds = normalizeSelectedReverseCipherIds(rawCipherIds);
}
function renderReverseCipherOptions() {
const { reverseCiphersEl } = getElements();
if (!reverseCiphersEl) {
return;
}
const ciphers = getGematriaCiphers();
const selectedCipherIds = new Set(getSelectedReverseCipherIds());
const fragment = document.createDocumentFragment();
ciphers.forEach((cipher) => {
const optionEl = document.createElement("label");
optionEl.className = "alpha-gematria-reverse-cipher-option";
const checkboxEl = document.createElement("input");
checkboxEl.type = "checkbox";
checkboxEl.value = cipher.id;
checkboxEl.checked = selectedCipherIds.has(cipher.id);
checkboxEl.setAttribute("aria-label", cipher.name);
optionEl.appendChild(checkboxEl);
const textEl = document.createElement("span");
textEl.className = "alpha-gematria-reverse-cipher-name";
textEl.textContent = cipher.name;
if (cipher.description) {
textEl.title = cipher.description;
}
optionEl.appendChild(textEl);
fragment.appendChild(optionEl);
});
reverseCiphersEl.replaceChildren(fragment);
}
function renderGematriaCipherOptions() {
const { cipherEl } = getElements();
if (!cipherEl) {
@@ -296,6 +375,14 @@
const activeCipher = getActiveGematriaCipher();
state.activeCipherId = activeCipher?.id || "";
cipherEl.value = state.activeCipherId;
if (state.reverseSelectedCipherIds === null) {
state.reverseSelectedCipherIds = getDefaultReverseCipherIds();
} else {
state.reverseSelectedCipherIds = normalizeSelectedReverseCipherIds(state.reverseSelectedCipherIds);
}
renderReverseCipherOptions();
}
function setMatchesMessage(matchesEl, message) {
@@ -332,12 +419,14 @@
modeEls,
matchesEl,
inputLabelEl,
cipherLabelEl
cipherLabelEl,
reverseCiphersEl,
reverseCipherHintEl
} = getElements();
const reverseMode = isReverseMode();
const anagramMode = isAnagramMode();
const dictionaryMode = isDictionaryMode();
const dictionaryMode = isDictionaryMode();
const radioEls = getModeElements(modeEls);
radioEls.forEach((element) => {
@@ -351,16 +440,32 @@
}
if (cipherLabelEl) {
cipherLabelEl.textContent = (reverseMode || anagramMode || dictionaryMode) ? "Cipher (not used in this mode)" : "Cipher";
cipherLabelEl.textContent = reverseMode
? "Ciphers"
: ((anagramMode || dictionaryMode) ? "Cipher (not used in this mode)" : "Cipher");
}
if (cipherEl) {
const disableCipher = reverseMode || anagramMode || dictionaryMode;
const hideCipherField = anagramMode || dictionaryMode;
cipherEl.disabled = disableCipher;
cipherEl.hidden = reverseMode;
const cipherFieldEl = cipherEl.closest(".alpha-gematria-field");
const controlsEl = cipherEl.closest(".alpha-gematria-controls");
cipherFieldEl?.classList.toggle("is-disabled", disableCipher);
controlsEl?.classList.toggle("is-input-priority-mode", disableCipher);
cipherFieldEl?.classList.toggle("is-disabled", hideCipherField);
controlsEl?.classList.toggle("is-input-priority-mode", hideCipherField);
controlsEl?.classList.toggle("is-reverse-cipher-mode", reverseMode);
}
if (reverseCiphersEl) {
if (reverseMode) {
renderReverseCipherOptions();
}
reverseCiphersEl.hidden = !reverseMode;
}
if (reverseCipherHintEl) {
reverseCipherHintEl.hidden = !reverseMode;
}
if (inputEl) {
@@ -400,12 +505,15 @@
}
async function loadReverseLookup(value) {
const cacheKey = String(value);
const selectedCipherIds = getSelectedReverseCipherIds();
const cacheKey = `${String(value)}::${selectedCipherIds.join(",")}`;
if (state.reverseLookupCache.has(cacheKey)) {
return state.reverseLookupCache.get(cacheKey);
}
const payload = await window.TarotDataService?.loadGematriaWordsByValue?.(value);
const payload = await window.TarotDataService?.loadGematriaWordsByValue?.(value, {
ciphers: selectedCipherIds
});
state.reverseLookupCache.set(cacheKey, payload);
return payload;
}
@@ -493,7 +601,7 @@
.map((cipher) => `${String(cipher?.name || cipher?.id || "Unknown")} ${formatCount(cipher?.count)}`)
.join(" · ");
breakdownEl.textContent = `Found ${formatCount(displayCount)} matches across ${formatCount(displayCipherCount)} ciphers.${topCipherSummary ? ` Top ciphers: ${topCipherSummary}.` : ""}${displayCount > visibleMatches.length ? ` Showing first ${formatCount(visibleMatches.length)}.` : ""}`;
breakdownEl.textContent = `Found ${formatCount(displayCount)} matches across ${formatCount(displayCipherCount)} selected ciphers.${topCipherSummary ? ` Top ciphers: ${topCipherSummary}.` : ""}${displayCount > visibleMatches.length ? ` Showing first ${formatCount(visibleMatches.length)}.` : ""}`;
const fragment = document.createDocumentFragment();
visibleMatches.forEach((match) => {
@@ -546,11 +654,20 @@
}
const rawValue = state.reverseInputText;
const selectedCipherIds = getSelectedReverseCipherIds();
if (!String(rawValue || "").trim()) {
resultEl.textContent = "Value: --";
breakdownEl.textContent = "Enter a whole number to find words across all available ciphers.";
breakdownEl.textContent = "Enter a whole number and choose one or more ciphers to narrow reverse matches.";
matchesEl.hidden = false;
setMatchesMessage(matchesEl, "Reverse lookup searches the API-backed gematria word index.");
setMatchesMessage(matchesEl, "Reverse lookup searches the API-backed gematria word index using the selected ciphers only.");
return;
}
if (!selectedCipherIds.length) {
resultEl.textContent = "Value: --";
breakdownEl.textContent = "Choose at least one cipher before running reverse lookup.";
matchesEl.hidden = false;
setMatchesMessage(matchesEl, "Select one or more ciphers to limit reverse gematria matches.");
return;
}
@@ -865,7 +982,7 @@
}
function bindGematriaListeners() {
const { cipherEl, inputEl, modeEls } = getElements();
const { cipherEl, inputEl, modeEls, reverseCiphersEl } = getElements();
if (state.listenersBound || !cipherEl || !inputEl) {
return;
}
@@ -900,6 +1017,20 @@
});
});
reverseCiphersEl?.addEventListener("change", (event) => {
const target = event.target;
if (!(target instanceof HTMLInputElement) || target.type !== "checkbox") {
return;
}
const nextSelectedCipherIds = Array.from(reverseCiphersEl.querySelectorAll("input[type='checkbox']:checked"))
.map((element) => String(element.value || "").trim())
.filter(Boolean);
setSelectedReverseCipherIds(nextSelectedCipherIds);
renderReverseCipherOptions();
renderGematriaResult();
});
state.listenersBound = true;
}
+6 -1
View File
@@ -53,6 +53,7 @@
let searchInputEl, searchClearEl, typeFilterEl;
let gematriaCipherEl, gematriaInputEl, gematriaResultEl, gematriaBreakdownEl;
let gematriaModeEls, gematriaMatchesEl, gematriaInputLabelEl, gematriaCipherLabelEl;
let gematriaReverseCiphersEl, gematriaReverseCipherHintEl;
function getElements() {
listEl = document.getElementById("alpha-letter-list");
@@ -77,6 +78,8 @@
gematriaMatchesEl = document.getElementById("alpha-gematria-matches");
gematriaInputLabelEl = document.getElementById("alpha-gematria-input-label");
gematriaCipherLabelEl = document.getElementById("alpha-gematria-cipher-label");
gematriaReverseCiphersEl = document.getElementById("alpha-gematria-reverse-ciphers");
gematriaReverseCipherHintEl = document.getElementById("alpha-gematria-reverse-cipher-hint");
}
function getGematriaElements() {
@@ -89,7 +92,9 @@
modeEls: gematriaModeEls,
matchesEl: gematriaMatchesEl,
inputLabelEl: gematriaInputLabelEl,
cipherLabelEl: gematriaCipherLabelEl
cipherLabelEl: gematriaCipherLabelEl,
reverseCiphersEl: gematriaReverseCiphersEl,
reverseCipherHintEl: gematriaReverseCipherHintEl
};
}
+911 -69
View File
File diff suppressed because it is too large Load Diff