diff --git a/app.js b/app.js index b9c14b5..6db48c8 100644 --- a/app.js +++ b/app.js @@ -20,7 +20,7 @@ const { ensureEnochianSection } = window.EnochianSectionUi || {}; const { ensureCalendarSection } = window.CalendarSectionUi || {}; const { ensureHolidaySection } = window.HolidaySectionUi || {}; const { ensureNatalPanel } = window.TarotNatalUi || {}; -const { ensureNumbersSection, selectNumberEntry, normalizeNumberValue } = window.TarotNumbersUi || {}; +const { ensureNumbersSection, selectNumberEntry, normalizeNumberValue, showBrowseView, showTheoryView } = window.TarotNumbersUi || {}; const tarotSpreadUi = window.TarotSpreadUi || {}; const settingsUi = window.TarotSettingsUi || {}; const chromeUi = window.TarotChromeUi || {}; @@ -93,6 +93,8 @@ const openAlphabetWordEl = document.getElementById("open-alphabet-word"); const openAlphabetLettersEl = document.getElementById("open-alphabet-letters"); const openAlphabetTextEl = document.getElementById("open-alphabet-text"); const openNumbersEl = document.getElementById("open-numbers"); +const openNumbersBrowseEl = document.getElementById("open-numbers-browse"); +const openNumbersTheoryEl = document.getElementById("open-numbers-theory"); const openZodiacEl = document.getElementById("open-zodiac"); const openNatalEl = document.getElementById("open-natal"); const openQuizEl = document.getElementById("open-quiz"); @@ -512,7 +514,9 @@ function bindConnectionGate() { window.TarotNumbersUi?.init?.({ getReferenceData: () => appRuntime.getReferenceData?.() || null, getMagickDataset: () => appRuntime.getMagickDataset?.() || null, - ensureTarotSection + ensureTarotSection, + getActiveSection: () => sectionStateUi.getActiveSection?.() || "home", + setActiveSection: (section) => sectionStateUi.setActiveSection?.(section) }); window.TarotSpreadUi?.init?.({ @@ -608,6 +612,8 @@ sectionStateUi.init?.({ openAlphabetLettersEl, openAlphabetTextEl, openNumbersEl, + openNumbersBrowseEl, + openNumbersTheoryEl, openZodiacEl, openNatalEl, openQuizEl, @@ -698,6 +704,8 @@ navigationUi.init?.({ getMagickDataset: () => appRuntime.getMagickDataset?.() || null, normalizeNumberValue, selectNumberEntry, + showNumbersBrowseView: showBrowseView, + showNumbersTheoryView: showTheoryView, elements: { openHomeEl, openSettingsEl, @@ -728,6 +736,8 @@ navigationUi.init?.({ openAlphabetLettersEl, openAlphabetTextEl, openNumbersEl, + openNumbersBrowseEl, + openNumbersTheoryEl, openZodiacEl, openNatalEl, openQuizEl, diff --git a/app/card-images.js b/app/card-images.js index 1ac23c8..69c4006 100644 --- a/app/card-images.js +++ b/app/card-images.js @@ -565,6 +565,111 @@ } } + function normalizeDerivedDeckId(value) { + return String(value || "") + .trim() + .toLowerCase() + .replace(/\.json$/i, "") + .replace(/[_\s]+/g, "-") + .replace(/[^a-z0-9-]+/g, "-") + .replace(/-{2,}/g, "-") + .replace(/^-+|-+$/g, ""); + } + + function toPathSegments(pathValue) { + const normalizedPath = String(pathValue || "").trim(); + if (!normalizedPath) { + return []; + } + + let pathname = normalizedPath; + try { + pathname = new URL(normalizedPath).pathname; + } catch { + pathname = normalizedPath.split("?")[0].split("#")[0]; + } + + return pathname + .split("/") + .map((segment) => String(segment || "").trim()) + .filter(Boolean) + .map((segment) => { + try { + return decodeURIComponent(segment); + } catch { + return segment; + } + }); + } + + function deriveDeckIdFromPath(pathValue) { + const segments = toPathSegments(pathValue); + if (!segments.length) { + return ""; + } + + const lowerSegments = segments.map((segment) => segment.toLowerCase()); + for (let index = 0; index < lowerSegments.length - 1; index += 1) { + if (lowerSegments[index] !== "decks") { + continue; + } + + const deckId = normalizeDerivedDeckId(segments[index + 1]); + if (deckId && deckId !== "manifest") { + return deckId; + } + } + + const lastIndex = segments.length - 1; + const lastLower = lowerSegments[lastIndex]; + if ((lastLower === "deck.json" || lastLower === "manifest" || lastLower === "manifest.json") && lastIndex > 0) { + return normalizeDerivedDeckId(segments[lastIndex - 1]); + } + + const fallbackId = normalizeDerivedDeckId(segments[lastIndex]); + if (!fallbackId || fallbackId === "decks" || fallbackId === "deck" || fallbackId === "manifest") { + return ""; + } + + return fallbackId; + } + + function deriveDeckIdFromSource(entry) { + const explicitId = normalizeDerivedDeckId(entry?.id || entry?.deckId || ""); + if (explicitId) { + return explicitId; + } + + const manifestPathId = deriveDeckIdFromPath(entry?.manifestPath); + if (manifestPathId) { + return manifestPathId; + } + + return deriveDeckIdFromPath(entry?.basePath); + } + + function formatDeckLabelFromId(deckId) { + const normalizedDeckId = normalizeDerivedDeckId(deckId); + if (!normalizedDeckId) { + return ""; + } + + return normalizedDeckId + .split("-") + .filter(Boolean) + .map((token) => token.charAt(0).toUpperCase() + token.slice(1)) + .join(" "); + } + + function resolveDeckSourceLabel(entry, deckId) { + const explicitLabel = String(entry?.label || "").trim(); + if (explicitLabel) { + return explicitLabel; + } + + return formatDeckLabelFromId(deckId) || deckId; + } + function toDeckSourceMap(sourceList) { const sourceMap = {}; if (!Array.isArray(sourceList)) { @@ -572,7 +677,7 @@ } sourceList.forEach((entry) => { - const id = String(entry?.id || "").trim().toLowerCase(); + const id = deriveDeckIdFromSource(entry); const basePath = String(entry?.basePath || "").trim().replace(/\/$/, ""); const manifestPath = String(entry?.manifestPath || "").trim(); if (!id || !manifestPath) { @@ -581,7 +686,7 @@ sourceMap[id] = { id, - label: String(entry?.label || id), + label: resolveDeckSourceLabel(entry, id), basePath, manifestPath, cardBackPath: String(entry?.cardBackPath || "").trim(), @@ -604,11 +709,24 @@ const registry = readManifestJsonSync(registryUrl); const registryDecks = Array.isArray(registry?.decks) - ? registry.decks.map((entry) => ({ - id: entry?.id, - label: entry?.label, - manifestPath: window.TarotDataService?.buildApiUrl?.(`/api/v1/decks/${encodeURIComponent(String(entry?.id || "").trim().toLowerCase())}/manifest`) || `${getApiBaseUrl()}/api/v1/decks/${encodeURIComponent(String(entry?.id || "").trim().toLowerCase())}/manifest` - })) + ? registry.decks.map((entry) => { + const normalizedDeckId = normalizeDerivedDeckId(entry?.id || entry?.deckId || ""); + const apiManifestPath = normalizedDeckId + ? ( + window.TarotDataService?.buildApiUrl?.(`/api/v1/decks/${encodeURIComponent(normalizedDeckId)}/manifest`) + || `${getApiBaseUrl()}/api/v1/decks/${encodeURIComponent(normalizedDeckId)}/manifest` + ) + : ""; + + return { + id: normalizedDeckId, + label: entry?.label, + basePath: entry?.basePath, + manifestPath: String(entry?.manifestPath || "").trim() || apiManifestPath, + cardBackPath: entry?.cardBackPath, + thumbnailRoot: entry?.thumbnailRoot + }; + }) : []; return toDeckSourceMap(registryDecks); diff --git a/app/styles.css b/app/styles.css index c0aa8f4..9f4528c 100644 --- a/app/styles.css +++ b/app/styles.css @@ -6600,6 +6600,90 @@ padding: 0 10px; } + .numbers-theory-card { + gap: 10px; + } + + .numbers-theory-controls { + display: grid; + grid-template-columns: minmax(180px, 1fr) auto auto; + gap: 8px; + align-items: center; + } + + .numbers-theory-input { + width: 100%; + min-height: 32px; + border-radius: 8px; + border: 1px solid #3f3f46; + background: #141420; + color: #e4e4e7; + padding: 6px 10px; + font-size: 13px; + outline: none; + box-sizing: border-box; + } + + .numbers-theory-input:focus { + border-color: #7c3aed; + box-shadow: 0 0 0 1px rgba(124, 58, 237, 0.25); + } + + .numbers-theory-output { + display: grid; + gap: 10px; + } + + .numbers-theory-section { + border: 1px solid #2f2f37; + border-radius: 10px; + background: #0f0f16; + padding: 10px; + display: grid; + gap: 8px; + } + + .numbers-theory-section-heading { + margin: 0; + color: #d4d4d8; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + } + + .numbers-theory-grid { + display: grid; + grid-template-columns: minmax(140px, 210px) minmax(0, 1fr); + gap: 6px 10px; + align-items: start; + } + + .numbers-theory-label { + color: #a1a1aa; + font-size: 12px; + line-height: 1.35; + font-weight: 600; + } + + .numbers-theory-value { + color: #e4e4e7; + font-size: 12px; + line-height: 1.35; + overflow-wrap: anywhere; + word-break: break-word; + } + + @media (max-width: 980px) { + .numbers-theory-controls { + grid-template-columns: 1fr; + } + + .numbers-theory-grid { + grid-template-columns: 1fr; + } + } + /* ── Gods section ────────────────────────────────────────────────────── */ #gods-section { height: calc(100vh - 61px); diff --git a/app/ui-navigation.js b/app/ui-navigation.js index f055b8a..1fccc3c 100644 --- a/app/ui-navigation.js +++ b/app/ui-navigation.js @@ -227,7 +227,22 @@ }); bindClick(elements.openNumbersEl, () => { - setActiveSection(getActiveSection() === "numbers" ? "home" : "numbers"); + const shouldOpenNumbers = getActiveSection() !== "numbers"; + setActiveSection(shouldOpenNumbers ? "numbers" : "home"); + if (shouldOpenNumbers) { + config.showNumbersBrowseView?.(false); + } + }); + + bindClick(elements.openNumbersBrowseEl, () => { + setActiveSection("numbers"); + config.showNumbersBrowseView?.(false); + }); + + bindClick(elements.openNumbersTheoryEl, () => { + setActiveSection("numbers"); + config.showNumbersTheoryView?.(false); + scheduleSectionDetailOnly("numbers"); }); bindClick(elements.openZodiacEl, () => { @@ -362,6 +377,7 @@ } setActiveSection("numbers"); + config.showNumbersBrowseView?.(false); requestAnimationFrame(() => { if (typeof config.selectNumberEntry === "function") { config.selectNumberEntry(normalizedValue); diff --git a/app/ui-numbers-detail.js b/app/ui-numbers-detail.js index eb4bfc8..2b4402e 100644 --- a/app/ui-numbers-detail.js +++ b/app/ui-numbers-detail.js @@ -1,6 +1,624 @@ (function () { "use strict"; + const THEORY_DEFAULT_INPUT = "108"; + const THEORY_MAX_FACTORIZATION_ABS = 10000000000; + const THEORY_MAX_COLLATZ_START = 10000000; + const theoryState = { + inputValue: THEORY_DEFAULT_INPUT + }; + + function formatTheoryNumber(value, fractionDigits = 12) { + if (!Number.isFinite(value)) { + return "--"; + } + + if (Object.is(value, -0)) { + return "0"; + } + + const abs = Math.abs(value); + if (abs !== 0 && (abs >= 1e15 || abs < 1e-6)) { + return value.toExponential(8); + } + + const formatter = new Intl.NumberFormat("en-US", { + maximumFractionDigits: fractionDigits + }); + return formatter.format(value); + } + + function formatTheoryList(values) { + if (!Array.isArray(values) || !values.length) { + return "--"; + } + return values.join(", "); + } + + function parseTheoryInput(rawInput) { + const text = String(rawInput ?? "").trim(); + if (!text) { + return { + ok: false, + message: "Enter a number to analyze." + }; + } + + const normalized = text.replace(/[,_\s]/g, ""); + const value = Number(normalized); + if (!Number.isFinite(value)) { + return { + ok: false, + message: "Could not parse that value as a finite number." + }; + } + + return { + ok: true, + raw: text, + normalized, + value + }; + } + + function gcd(a, b) { + let x = Math.abs(Math.trunc(a)); + let y = Math.abs(Math.trunc(b)); + while (y !== 0) { + const next = x % y; + x = y; + y = next; + } + return x; + } + + function getDigitStats(integerValue) { + const absInt = Math.abs(integerValue); + const digits = String(absInt); + const digitArray = digits.split("").map((digit) => Number(digit)); + const digitSum = digitArray.reduce((sum, digit) => sum + digit, 0); + + let additivePersistence = 0; + let additiveCurrent = absInt; + while (additiveCurrent >= 10) { + additiveCurrent = String(additiveCurrent) + .split("") + .reduce((sum, digit) => sum + Number(digit), 0); + additivePersistence += 1; + } + + let multiplicativePersistence = 0; + let multiplicativeCurrent = absInt; + while (multiplicativeCurrent >= 10) { + multiplicativeCurrent = String(multiplicativeCurrent) + .split("") + .reduce((product, digit) => product * Number(digit), 1); + multiplicativePersistence += 1; + + if (multiplicativePersistence > 20) { + break; + } + } + + return { + digits, + digitCount: digits.length, + digitSum, + digitalRoot: additiveCurrent, + additivePersistence, + multiplicativePersistence + }; + } + + function factorizeInteger(absInteger) { + if (!Number.isSafeInteger(absInteger) || absInteger < 2) { + return []; + } + + let remainder = absInteger; + const factors = []; + + let exponent = 0; + while (remainder % 2 === 0) { + remainder /= 2; + exponent += 1; + } + if (exponent > 0) { + factors.push({ prime: 2, exponent }); + } + + let divisor = 3; + while (divisor * divisor <= remainder) { + exponent = 0; + while (remainder % divisor === 0) { + remainder /= divisor; + exponent += 1; + } + if (exponent > 0) { + factors.push({ prime: divisor, exponent }); + } + divisor += 2; + } + + if (remainder > 1) { + factors.push({ prime: remainder, exponent: 1 }); + } + + return factors; + } + + function createDivisorsFromFactors(factors) { + if (!Array.isArray(factors) || !factors.length) { + return [1]; + } + + let divisors = [1]; + factors.forEach(({ prime, exponent }) => { + const next = []; + divisors.forEach((existing) => { + let multiplier = 1; + for (let power = 0; power <= exponent; power += 1) { + next.push(existing * multiplier); + multiplier *= prime; + } + }); + divisors = next; + }); + + return divisors.sort((left, right) => left - right); + } + + function getRomanNumeral(value) { + const integer = Math.trunc(value); + if (!Number.isFinite(integer) || integer < 1 || integer > 3999) { + return "--"; + } + + const symbols = [ + [1000, "M"], + [900, "CM"], + [500, "D"], + [400, "CD"], + [100, "C"], + [90, "XC"], + [50, "L"], + [40, "XL"], + [10, "X"], + [9, "IX"], + [5, "V"], + [4, "IV"], + [1, "I"] + ]; + + let remainder = integer; + let output = ""; + symbols.forEach(([weight, glyph]) => { + while (remainder >= weight) { + remainder -= weight; + output += glyph; + } + }); + + return output; + } + + function isPerfectSquare(value) { + if (!Number.isSafeInteger(value) || value < 0) { + return false; + } + + const root = Math.trunc(Math.sqrt(value)); + return root * root === value; + } + + function isPerfectCube(value) { + if (!Number.isSafeInteger(value)) { + return false; + } + + const root = Math.round(Math.cbrt(value)); + return root * root * root === value; + } + + function fibonacciMembership(value) { + if (!Number.isSafeInteger(value) || value < 0) { + return false; + } + + const candidateA = 5 * value * value + 4; + const candidateB = 5 * value * value - 4; + return isPerfectSquare(candidateA) || isPerfectSquare(candidateB); + } + + function factorialMembership(value) { + if (!Number.isSafeInteger(value) || value < 1) { + return null; + } + + let current = 1; + let index = 1; + while (current < value && current <= Number.MAX_SAFE_INTEGER) { + index += 1; + current *= index; + } + + return current === value ? index : null; + } + + function collatzStats(value) { + if (!Number.isSafeInteger(value) || value <= 0 || value > THEORY_MAX_COLLATZ_START) { + return { + available: false, + reason: `Available for positive integers up to ${formatTheoryNumber(THEORY_MAX_COLLATZ_START, 0)}.` + }; + } + + let current = value; + let steps = 0; + let peak = value; + while (current !== 1 && steps < 20000) { + if (current % 2 === 0) { + current /= 2; + } else { + current = (3 * current) + 1; + } + if (current > peak) { + peak = current; + } + steps += 1; + } + + return { + available: true, + steps, + peak, + completed: current === 1 + }; + } + + function happyNumberStats(value) { + if (!Number.isSafeInteger(value) || value <= 0) { + return { + available: false, + isHappy: false + }; + } + + const seen = new Set(); + let current = value; + while (current !== 1 && !seen.has(current)) { + seen.add(current); + current = String(current) + .split("") + .reduce((sum, digit) => { + const numberDigit = Number(digit); + return sum + (numberDigit * numberDigit); + }, 0); + } + + return { + available: true, + isHappy: current === 1 + }; + } + + function getIntegerProperties(integerValue) { + const absInteger = Math.abs(integerValue); + const digitStats = getDigitStats(integerValue); + const canFactorExactly = absInteger <= THEORY_MAX_FACTORIZATION_ABS; + const factors = canFactorExactly ? factorizeInteger(absInteger) : []; + + let divisorCount = null; + let sigma = null; + let properDivisorSum = null; + let divisors = null; + let eulerPhi = null; + let mobius = null; + let liouville = null; + let isPrime = false; + let isComposite = false; + let semiprime = false; + + if (canFactorExactly && absInteger >= 1) { + divisorCount = factors.reduce((product, item) => product * (item.exponent + 1), 1); + sigma = factors.reduce((product, item) => { + const numerator = (item.prime ** (item.exponent + 1)) - 1; + return product * (numerator / (item.prime - 1)); + }, 1); + properDivisorSum = absInteger === 1 ? 0 : sigma - absInteger; + eulerPhi = absInteger === 1 + ? 1 + : factors.reduce((product, item) => product * ((item.prime - 1) * (item.prime ** (item.exponent - 1))), 1); + const hasSquareFactor = factors.some((item) => item.exponent > 1); + mobius = absInteger === 1 ? 1 : (hasSquareFactor ? 0 : (factors.length % 2 === 0 ? 1 : -1)); + liouville = absInteger === 1 + ? 1 + : (factors.reduce((sum, item) => sum + item.exponent, 0) % 2 === 0 ? 1 : -1); + + isPrime = absInteger > 1 && factors.length === 1 && factors[0].exponent === 1; + isComposite = absInteger > 1 && !isPrime; + semiprime = absInteger > 1 && factors.reduce((sum, item) => sum + item.exponent, 0) === 2; + if (divisorCount <= 256) { + divisors = createDivisorsFromFactors(factors); + } + } + + const abundanceClass = properDivisorSum == null + ? "Unknown" + : (properDivisorSum === absInteger ? "Perfect" : (properDivisorSum > absInteger ? "Abundant" : "Deficient")); + + const triangularIndex = absInteger >= 0 + ? ((Math.sqrt((8 * absInteger) + 1) - 1) / 2) + : null; + const pentagonalIndex = absInteger >= 0 + ? ((1 + Math.sqrt((24 * absInteger) + 1)) / 6) + : null; + const hexagonalIndex = absInteger >= 0 + ? ((1 + Math.sqrt((8 * absInteger) + 1)) / 4) + : null; + + const factorialIndex = factorialMembership(absInteger); + const collatz = collatzStats(absInteger); + const happy = happyNumberStats(absInteger); + + const residueMods = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + .map((modulus) => `mod ${modulus}: ${((integerValue % modulus) + modulus) % modulus}`); + + const divisibility = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + .map((modulus) => `${modulus}:${integerValue % modulus === 0 ? "Y" : "N"}`); + + const parity = integerValue % 2 === 0 ? "Even" : "Odd"; + const powerOfTwo = absInteger > 0 ? Number.isInteger(Math.log2(absInteger)) : false; + const powerOfTen = absInteger > 0 ? Number.isInteger(Math.log10(absInteger)) : false; + + const binary = absInteger <= Number.MAX_SAFE_INTEGER + ? integerValue.toString(2) + : "--"; + const octal = absInteger <= Number.MAX_SAFE_INTEGER + ? integerValue.toString(8) + : "--"; + const hex = absInteger <= Number.MAX_SAFE_INTEGER + ? integerValue.toString(16).toUpperCase() + : "--"; + + const decimalText = String(absInteger); + const isPalindrome = decimalText === decimalText.split("").reverse().join(""); + const isHarshad = digitStats.digitSum > 0 ? absInteger % digitStats.digitSum === 0 : false; + const armstrongPower = digitStats.digitCount; + const armstrongSum = decimalText + .split("") + .reduce((sum, digit) => sum + (Number(digit) ** armstrongPower), 0); + const isArmstrong = armstrongSum === absInteger; + + const theoryRows = { + integerMeta: [ + ["Parity", parity], + ["Safe integer", Number.isSafeInteger(integerValue) ? "Yes" : "No"], + ["GCD(n, 360)", formatTheoryNumber(gcd(integerValue, 360), 0)], + ["Power of 2", powerOfTwo ? "Yes" : "No"], + ["Power of 10", powerOfTen ? "Yes" : "No"] + ], + digits: [ + ["Digits", digitStats.digits], + ["Digit count", formatTheoryNumber(digitStats.digitCount, 0)], + ["Digit sum", formatTheoryNumber(digitStats.digitSum, 0)], + ["Digital root", formatTheoryNumber(digitStats.digitalRoot, 0)], + ["Additive persistence", formatTheoryNumber(digitStats.additivePersistence, 0)], + ["Multiplicative persistence", formatTheoryNumber(digitStats.multiplicativePersistence, 0)] + ], + numberTheory: [ + ["Prime", isPrime ? "Yes" : "No"], + ["Composite", isComposite ? "Yes" : "No"], + ["Semiprime", semiprime ? "Yes" : "No"], + ["Prime factorization", factors.length ? factors.map((item) => `${item.prime}^${item.exponent}`).join(" x ") : (absInteger < 2 ? "--" : "Unavailable")], + ["Divisor count (tau)", divisorCount == null ? "Unavailable" : formatTheoryNumber(divisorCount, 0)], + ["Sum of divisors (sigma)", sigma == null ? "Unavailable" : formatTheoryNumber(sigma, 0)], + ["Proper divisor sum", properDivisorSum == null ? "Unavailable" : formatTheoryNumber(properDivisorSum, 0)], + ["Abundance class", abundanceClass], + ["Euler totient phi(n)", eulerPhi == null ? "Unavailable" : formatTheoryNumber(eulerPhi, 0)], + ["Mobius mu(n)", mobius == null ? "Unavailable" : formatTheoryNumber(mobius, 0)], + ["Liouville lambda(n)", liouville == null ? "Unavailable" : formatTheoryNumber(liouville, 0)] + ], + sequences: [ + ["Triangular", Number.isInteger(triangularIndex) ? `Yes (T_${formatTheoryNumber(triangularIndex, 0)})` : "No"], + ["Square", isPerfectSquare(absInteger) ? "Yes" : "No"], + ["Cube", isPerfectCube(integerValue) ? "Yes" : "No"], + ["Pentagonal", Number.isInteger(pentagonalIndex) ? `Yes (P_${formatTheoryNumber(pentagonalIndex, 0)})` : "No"], + ["Hexagonal", Number.isInteger(hexagonalIndex) ? `Yes (H_${formatTheoryNumber(hexagonalIndex, 0)})` : "No"], + ["Fibonacci", fibonacciMembership(absInteger) ? "Yes" : "No"], + ["Factorial", factorialIndex ? `Yes (${formatTheoryNumber(factorialIndex, 0)}!)` : "No"] + ], + digitTraits: [ + ["Palindrome (base 10)", isPalindrome ? "Yes" : "No"], + ["Harshad", isHarshad ? "Yes" : "No"], + ["Armstrong/Narcissistic", isArmstrong ? "Yes" : "No"], + ["Happy", happy.available ? (happy.isHappy ? "Yes" : "No") : "Unavailable"] + ], + modular: [ + ["Divisibility (2..12)", formatTheoryList(divisibility)], + ["Residues", formatTheoryList(residueMods)] + ], + representations: [ + ["Binary", binary], + ["Octal", octal], + ["Hex", hex], + ["Roman numeral", getRomanNumeral(absInteger)] + ], + dynamic: [ + ["Collatz", collatz.available + ? `${formatTheoryNumber(collatz.steps, 0)} steps, peak ${formatTheoryNumber(collatz.peak, 0)}${collatz.completed ? "" : " (stopped early)"}` + : collatz.reason], + ["Divisors", divisors + ? formatTheoryList(divisors.map((item) => formatTheoryNumber(item, 0))) + : (canFactorExactly + ? "List omitted (over 256 divisors)." + : `Unavailable above |n| > ${formatTheoryNumber(THEORY_MAX_FACTORIZATION_ABS, 0)}.`)] + ] + }; + + return { + absInteger, + canFactorExactly, + rows: theoryRows + }; + } + + function buildTheorySection(title, rows) { + const sectionEl = document.createElement("section"); + sectionEl.className = "numbers-theory-section"; + + const headingEl = document.createElement("h4"); + headingEl.className = "numbers-theory-section-heading"; + headingEl.textContent = title; + + const bodyEl = document.createElement("div"); + bodyEl.className = "numbers-theory-grid"; + + rows.forEach((row) => { + if (!Array.isArray(row) || row.length < 2) { + return; + } + + const labelEl = document.createElement("div"); + labelEl.className = "numbers-theory-label"; + labelEl.textContent = String(row[0]); + + const valueEl = document.createElement("div"); + valueEl.className = "numbers-theory-value"; + valueEl.textContent = String(row[1]); + + bodyEl.append(labelEl, valueEl); + }); + + sectionEl.append(headingEl, bodyEl); + return sectionEl; + } + + function buildTheoryAnalysisView(rawInput) { + const parsed = parseTheoryInput(rawInput); + const fragment = document.createDocumentFragment(); + + if (!parsed.ok) { + const messageEl = document.createElement("div"); + messageEl.className = "numbers-detail-text numbers-detail-text--muted"; + messageEl.textContent = parsed.message; + fragment.appendChild(messageEl); + return fragment; + } + + const { value } = parsed; + const abs = Math.abs(value); + const isInteger = Number.isInteger(value); + const reciprocal = value !== 0 ? (1 / value) : null; + const integerPart = Math.trunc(value); + const fractionalPart = value - integerPart; + + fragment.appendChild(buildTheorySection("General", [ + ["Input", parsed.raw], + ["Normalized", formatTheoryNumber(value)], + ["Sign", value > 0 ? "Positive" : (value < 0 ? "Negative" : "Zero")], + ["Absolute value", formatTheoryNumber(abs)], + ["Integer", isInteger ? "Yes" : "No"], + ["Whole number", isInteger && value >= 0 ? "Yes" : "No"], + ["Natural number", isInteger && value >= 1 ? "Yes" : "No"], + ["Opposite", formatTheoryNumber(-value)], + ["Reciprocal", reciprocal == null ? "Undefined" : formatTheoryNumber(reciprocal)], + ["Floor", formatTheoryNumber(Math.floor(value), 0)], + ["Ceiling", formatTheoryNumber(Math.ceil(value), 0)], + ["Truncated", formatTheoryNumber(integerPart, 0)], + ["Fractional part", formatTheoryNumber(fractionalPart)], + ["Scientific notation", value.toExponential(8)] + ])); + + if (!isInteger) { + fragment.appendChild(buildTheorySection("Non-integer notes", [ + ["Detail", "Integer-only number theory metrics are shown for integers. Enter a whole number for full analysis."] + ])); + return fragment; + } + + if (!Number.isSafeInteger(value)) { + fragment.appendChild(buildTheorySection("Safe integer limit", [ + ["Detail", "This value is outside JavaScript safe integer precision. Exact factor/divisor properties are skipped."] + ])); + return fragment; + } + + const integerProperties = getIntegerProperties(value); + fragment.appendChild(buildTheorySection("Integer metadata", integerProperties.rows.integerMeta)); + fragment.appendChild(buildTheorySection("Digit analysis", integerProperties.rows.digits)); + fragment.appendChild(buildTheorySection("Number theory", integerProperties.rows.numberTheory)); + fragment.appendChild(buildTheorySection("Sequence membership", integerProperties.rows.sequences)); + fragment.appendChild(buildTheorySection("Digit traits", integerProperties.rows.digitTraits)); + fragment.appendChild(buildTheorySection("Modular arithmetic", integerProperties.rows.modular)); + fragment.appendChild(buildTheorySection("Representations", integerProperties.rows.representations)); + fragment.appendChild(buildTheorySection("Dynamic behavior", integerProperties.rows.dynamic)); + + return fragment; + } + + function buildTheoryCard(selectedNumberValue) { + const cardEl = document.createElement("div"); + cardEl.className = "numbers-detail-card numbers-theory-card"; + + const headingEl = document.createElement("strong"); + headingEl.textContent = "Theory"; + + const introEl = document.createElement("div"); + introEl.className = "numbers-detail-text numbers-detail-text--muted"; + introEl.textContent = "Enter any number to inspect deep number theory and arithmetic properties."; + + const controlsEl = document.createElement("div"); + controlsEl.className = "numbers-theory-controls"; + + const inputEl = document.createElement("input"); + inputEl.type = "text"; + inputEl.inputMode = "decimal"; + inputEl.className = "numbers-theory-input"; + inputEl.placeholder = "e.g. 108, 137, -42, 12.5"; + inputEl.value = theoryState.inputValue || ""; + inputEl.setAttribute("aria-label", "Number to analyze"); + + const analyzeBtn = document.createElement("button"); + analyzeBtn.type = "button"; + analyzeBtn.className = "numbers-nav-btn"; + analyzeBtn.textContent = "Analyze"; + + const selectedBtn = document.createElement("button"); + selectedBtn.type = "button"; + selectedBtn.className = "numbers-nav-btn"; + selectedBtn.textContent = `Use Selected Number (${selectedNumberValue})`; + + controlsEl.append(inputEl, analyzeBtn, selectedBtn); + + const outputEl = document.createElement("div"); + outputEl.className = "numbers-theory-output"; + + const renderOutput = () => { + theoryState.inputValue = inputEl.value; + const content = buildTheoryAnalysisView(inputEl.value); + outputEl.replaceChildren(content); + }; + + analyzeBtn.addEventListener("click", renderOutput); + inputEl.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + renderOutput(); + } + }); + + selectedBtn.addEventListener("click", () => { + inputEl.value = String(selectedNumberValue); + renderOutput(); + }); + + renderOutput(); + + cardEl.append(headingEl, introEl, controlsEl, outputEl); + return cardEl; + } + function hasTarotAccess() { return window.TarotAppConfig?.hasTarotAccess?.() === true; } @@ -244,6 +862,13 @@ return; } + const isTheoryView = context.activeNumbersView === "theory"; + specialPanelEl.hidden = isTheoryView; + if (isTheoryView) { + specialPanelEl.replaceChildren(); + return; + } + const playingSuit = entry?.associations?.playingSuit || "hearts"; const boardCardEl = buildNumbersSpecialCardSlots(context, playingSuit); specialPanelEl.replaceChildren(boardCardEl); @@ -460,6 +1085,18 @@ return; } + const isTheoryView = context.activeNumbersView === "theory"; + + const detailHeadingEl = detailNameEl instanceof HTMLElement + ? detailNameEl.closest(".planet-detail-heading") + : null; + const sequenceNavEl = detailHeadingEl instanceof HTMLElement + ? detailHeadingEl.querySelector(".detail-sequence-nav") + : null; + if (sequenceNavEl instanceof HTMLElement) { + sequenceNavEl.hidden = isTheoryView; + } + const normalized = entry.value; const opposite = entry.opposite; const rootTarget = normalizeNumberValue(entry.digitalRoot); @@ -469,11 +1106,13 @@ } if (detailTypeEl) { - detailTypeEl.textContent = `Opposite: ${opposite}`; + detailTypeEl.textContent = isTheoryView ? "Theory Analyzer" : `Opposite: ${opposite}`; } if (detailSummaryEl) { - detailSummaryEl.textContent = entry.summary || ""; + detailSummaryEl.textContent = isTheoryView + ? "Analyze any value with an extensive set of arithmetic and number theory properties." + : (entry.summary || ""); } renderNumbersSpecialPanel(context, normalized, entry); @@ -484,6 +1123,12 @@ detailBodyEl.replaceChildren(); + const theoryCardEl = buildTheoryCard(normalized); + if (isTheoryView) { + detailBodyEl.append(theoryCardEl); + return; + } + const pairCardEl = document.createElement("div"); pairCardEl.className = "numbers-detail-card"; @@ -601,7 +1246,7 @@ calendarCardEl.append(calendarHeadingEl, calendarLinksWrapEl); - const detailCards = [pairCardEl, kabbalahCardEl, alphabetCardEl]; + const detailCards = [theoryCardEl, pairCardEl, kabbalahCardEl, alphabetCardEl]; if (hasTarotAccess()) { const tarotCardEl = document.createElement("div"); diff --git a/app/ui-numbers.js b/app/ui-numbers.js index 7769a55..1838c63 100644 --- a/app/ui-numbers.js +++ b/app/ui-numbers.js @@ -3,10 +3,14 @@ let initialized = false; let activeNumberValue = 0; + let activeNumbersView = "browse"; + let sectionChangeListenerBound = false; let config = { getReferenceData: () => null, getMagickDataset: () => null, - ensureTarotSection: null + ensureTarotSection: null, + getActiveSection: () => "home", + setActiveSection: null }; const NUMBERS_SPECIAL_BASE_VALUES = [1, 2, 3, 4]; @@ -88,6 +92,12 @@ return typeof config.getMagickDataset === "function" ? config.getMagickDataset() : null; } + function getActiveSection() { + return typeof config.getActiveSection === "function" + ? config.getActiveSection() + : "home"; + } + function getElements() { return { countEl: document.getElementById("numbers-count"), @@ -96,10 +106,29 @@ detailTypeEl: document.getElementById("numbers-detail-type"), detailSummaryEl: document.getElementById("numbers-detail-summary"), detailBodyEl: document.getElementById("numbers-detail-body"), - specialPanelEl: document.getElementById("numbers-special-panel") + specialPanelEl: document.getElementById("numbers-special-panel"), + openNumbersBrowseEl: document.getElementById("open-numbers-browse"), + openNumbersTheoryEl: document.getElementById("open-numbers-theory") }; } + function normalizeNumbersView(value) { + return value === "theory" ? "theory" : "browse"; + } + + function applyNumbersViewUiState() { + const { openNumbersBrowseEl, openNumbersTheoryEl } = getElements(); + const isNumbersSectionActive = getActiveSection() === "numbers"; + + if (openNumbersBrowseEl) { + openNumbersBrowseEl.classList.toggle("is-active", isNumbersSectionActive && activeNumbersView === "browse"); + } + + if (openNumbersTheoryEl) { + openNumbersTheoryEl.classList.toggle("is-active", isNumbersSectionActive && activeNumbersView === "theory"); + } + } + function normalizeNumberValue(value) { const parsed = Number(value); if (!Number.isFinite(parsed)) { @@ -257,6 +286,7 @@ computeDigitalRoot, rankLabelToTarotMinorRank, ensureTarotSection: config.ensureTarotSection, + activeNumbersView, selectNumberEntry, NUMBERS_SPECIAL_BASE_VALUES, numbersSpecialFlipState, @@ -282,6 +312,7 @@ function ensureNumbersSection() { const { listEl } = getElements(); if (!listEl) { + applyNumbersViewUiState(); return; } @@ -309,6 +340,33 @@ renderNumbersList(); renderNumberDetail(activeNumberValue); + applyNumbersViewUiState(); + } + + function setNumbersView(view, openNumbersSection = false) { + activeNumbersView = normalizeNumbersView(view); + + if (openNumbersSection && typeof config.setActiveSection === "function") { + config.setActiveSection("numbers"); + } + + if (getActiveSection() === "numbers" || openNumbersSection) { + ensureNumbersSection(); + const { detailBodyEl } = getElements(); + if (detailBodyEl instanceof HTMLElement) { + detailBodyEl.scrollTop = 0; + } + } else { + applyNumbersViewUiState(); + } + } + + function showBrowseView(openNumbersSection = false) { + setNumbersView("browse", openNumbersSection); + } + + function showTheoryView(openNumbersSection = false) { + setNumbersView("theory", openNumbersSection); } function init(nextConfig = {}) { @@ -316,6 +374,15 @@ ...config, ...nextConfig }; + + if (!sectionChangeListenerBound) { + document.addEventListener("section:changed", () => { + applyNumbersViewUiState(); + }); + sectionChangeListenerBound = true; + } + + applyNumbersViewUiState(); } window.TarotNumbersUi = { @@ -323,6 +390,11 @@ init, ensureNumbersSection, selectNumberEntry, - normalizeNumberValue + normalizeNumberValue, + showBrowseView, + showTheoryView, + getActiveView() { + return activeNumbersView; + } }; })(); diff --git a/index.html b/index.html index e155974..fa60457 100644 --- a/index.html +++ b/index.html @@ -71,7 +71,13 @@ - +