update and add number theory page
This commit is contained in:
@@ -20,7 +20,7 @@ const { ensureEnochianSection } = window.EnochianSectionUi || {};
|
|||||||
const { ensureCalendarSection } = window.CalendarSectionUi || {};
|
const { ensureCalendarSection } = window.CalendarSectionUi || {};
|
||||||
const { ensureHolidaySection } = window.HolidaySectionUi || {};
|
const { ensureHolidaySection } = window.HolidaySectionUi || {};
|
||||||
const { ensureNatalPanel } = window.TarotNatalUi || {};
|
const { ensureNatalPanel } = window.TarotNatalUi || {};
|
||||||
const { ensureNumbersSection, selectNumberEntry, normalizeNumberValue } = window.TarotNumbersUi || {};
|
const { ensureNumbersSection, selectNumberEntry, normalizeNumberValue, showBrowseView, showTheoryView } = window.TarotNumbersUi || {};
|
||||||
const tarotSpreadUi = window.TarotSpreadUi || {};
|
const tarotSpreadUi = window.TarotSpreadUi || {};
|
||||||
const settingsUi = window.TarotSettingsUi || {};
|
const settingsUi = window.TarotSettingsUi || {};
|
||||||
const chromeUi = window.TarotChromeUi || {};
|
const chromeUi = window.TarotChromeUi || {};
|
||||||
@@ -93,6 +93,8 @@ const openAlphabetWordEl = document.getElementById("open-alphabet-word");
|
|||||||
const openAlphabetLettersEl = document.getElementById("open-alphabet-letters");
|
const openAlphabetLettersEl = document.getElementById("open-alphabet-letters");
|
||||||
const openAlphabetTextEl = document.getElementById("open-alphabet-text");
|
const openAlphabetTextEl = document.getElementById("open-alphabet-text");
|
||||||
const openNumbersEl = document.getElementById("open-numbers");
|
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 openZodiacEl = document.getElementById("open-zodiac");
|
||||||
const openNatalEl = document.getElementById("open-natal");
|
const openNatalEl = document.getElementById("open-natal");
|
||||||
const openQuizEl = document.getElementById("open-quiz");
|
const openQuizEl = document.getElementById("open-quiz");
|
||||||
@@ -512,7 +514,9 @@ function bindConnectionGate() {
|
|||||||
window.TarotNumbersUi?.init?.({
|
window.TarotNumbersUi?.init?.({
|
||||||
getReferenceData: () => appRuntime.getReferenceData?.() || null,
|
getReferenceData: () => appRuntime.getReferenceData?.() || null,
|
||||||
getMagickDataset: () => appRuntime.getMagickDataset?.() || null,
|
getMagickDataset: () => appRuntime.getMagickDataset?.() || null,
|
||||||
ensureTarotSection
|
ensureTarotSection,
|
||||||
|
getActiveSection: () => sectionStateUi.getActiveSection?.() || "home",
|
||||||
|
setActiveSection: (section) => sectionStateUi.setActiveSection?.(section)
|
||||||
});
|
});
|
||||||
|
|
||||||
window.TarotSpreadUi?.init?.({
|
window.TarotSpreadUi?.init?.({
|
||||||
@@ -608,6 +612,8 @@ sectionStateUi.init?.({
|
|||||||
openAlphabetLettersEl,
|
openAlphabetLettersEl,
|
||||||
openAlphabetTextEl,
|
openAlphabetTextEl,
|
||||||
openNumbersEl,
|
openNumbersEl,
|
||||||
|
openNumbersBrowseEl,
|
||||||
|
openNumbersTheoryEl,
|
||||||
openZodiacEl,
|
openZodiacEl,
|
||||||
openNatalEl,
|
openNatalEl,
|
||||||
openQuizEl,
|
openQuizEl,
|
||||||
@@ -698,6 +704,8 @@ navigationUi.init?.({
|
|||||||
getMagickDataset: () => appRuntime.getMagickDataset?.() || null,
|
getMagickDataset: () => appRuntime.getMagickDataset?.() || null,
|
||||||
normalizeNumberValue,
|
normalizeNumberValue,
|
||||||
selectNumberEntry,
|
selectNumberEntry,
|
||||||
|
showNumbersBrowseView: showBrowseView,
|
||||||
|
showNumbersTheoryView: showTheoryView,
|
||||||
elements: {
|
elements: {
|
||||||
openHomeEl,
|
openHomeEl,
|
||||||
openSettingsEl,
|
openSettingsEl,
|
||||||
@@ -728,6 +736,8 @@ navigationUi.init?.({
|
|||||||
openAlphabetLettersEl,
|
openAlphabetLettersEl,
|
||||||
openAlphabetTextEl,
|
openAlphabetTextEl,
|
||||||
openNumbersEl,
|
openNumbersEl,
|
||||||
|
openNumbersBrowseEl,
|
||||||
|
openNumbersTheoryEl,
|
||||||
openZodiacEl,
|
openZodiacEl,
|
||||||
openNatalEl,
|
openNatalEl,
|
||||||
openQuizEl,
|
openQuizEl,
|
||||||
|
|||||||
+125
-7
@@ -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) {
|
function toDeckSourceMap(sourceList) {
|
||||||
const sourceMap = {};
|
const sourceMap = {};
|
||||||
if (!Array.isArray(sourceList)) {
|
if (!Array.isArray(sourceList)) {
|
||||||
@@ -572,7 +677,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
sourceList.forEach((entry) => {
|
sourceList.forEach((entry) => {
|
||||||
const id = String(entry?.id || "").trim().toLowerCase();
|
const id = deriveDeckIdFromSource(entry);
|
||||||
const basePath = String(entry?.basePath || "").trim().replace(/\/$/, "");
|
const basePath = String(entry?.basePath || "").trim().replace(/\/$/, "");
|
||||||
const manifestPath = String(entry?.manifestPath || "").trim();
|
const manifestPath = String(entry?.manifestPath || "").trim();
|
||||||
if (!id || !manifestPath) {
|
if (!id || !manifestPath) {
|
||||||
@@ -581,7 +686,7 @@
|
|||||||
|
|
||||||
sourceMap[id] = {
|
sourceMap[id] = {
|
||||||
id,
|
id,
|
||||||
label: String(entry?.label || id),
|
label: resolveDeckSourceLabel(entry, id),
|
||||||
basePath,
|
basePath,
|
||||||
manifestPath,
|
manifestPath,
|
||||||
cardBackPath: String(entry?.cardBackPath || "").trim(),
|
cardBackPath: String(entry?.cardBackPath || "").trim(),
|
||||||
@@ -604,11 +709,24 @@
|
|||||||
|
|
||||||
const registry = readManifestJsonSync(registryUrl);
|
const registry = readManifestJsonSync(registryUrl);
|
||||||
const registryDecks = Array.isArray(registry?.decks)
|
const registryDecks = Array.isArray(registry?.decks)
|
||||||
? registry.decks.map((entry) => ({
|
? registry.decks.map((entry) => {
|
||||||
id: entry?.id,
|
const normalizedDeckId = normalizeDerivedDeckId(entry?.id || entry?.deckId || "");
|
||||||
label: entry?.label,
|
const apiManifestPath = normalizedDeckId
|
||||||
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`
|
? (
|
||||||
}))
|
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);
|
return toDeckSourceMap(registryDecks);
|
||||||
|
|||||||
@@ -6600,6 +6600,90 @@
|
|||||||
padding: 0 10px;
|
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 ────────────────────────────────────────────────────── */
|
||||||
#gods-section {
|
#gods-section {
|
||||||
height: calc(100vh - 61px);
|
height: calc(100vh - 61px);
|
||||||
|
|||||||
+17
-1
@@ -227,7 +227,22 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
bindClick(elements.openNumbersEl, () => {
|
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, () => {
|
bindClick(elements.openZodiacEl, () => {
|
||||||
@@ -362,6 +377,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
setActiveSection("numbers");
|
setActiveSection("numbers");
|
||||||
|
config.showNumbersBrowseView?.(false);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (typeof config.selectNumberEntry === "function") {
|
if (typeof config.selectNumberEntry === "function") {
|
||||||
config.selectNumberEntry(normalizedValue);
|
config.selectNumberEntry(normalizedValue);
|
||||||
|
|||||||
+648
-3
@@ -1,6 +1,624 @@
|
|||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"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() {
|
function hasTarotAccess() {
|
||||||
return window.TarotAppConfig?.hasTarotAccess?.() === true;
|
return window.TarotAppConfig?.hasTarotAccess?.() === true;
|
||||||
}
|
}
|
||||||
@@ -244,6 +862,13 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isTheoryView = context.activeNumbersView === "theory";
|
||||||
|
specialPanelEl.hidden = isTheoryView;
|
||||||
|
if (isTheoryView) {
|
||||||
|
specialPanelEl.replaceChildren();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const playingSuit = entry?.associations?.playingSuit || "hearts";
|
const playingSuit = entry?.associations?.playingSuit || "hearts";
|
||||||
const boardCardEl = buildNumbersSpecialCardSlots(context, playingSuit);
|
const boardCardEl = buildNumbersSpecialCardSlots(context, playingSuit);
|
||||||
specialPanelEl.replaceChildren(boardCardEl);
|
specialPanelEl.replaceChildren(boardCardEl);
|
||||||
@@ -460,6 +1085,18 @@
|
|||||||
return;
|
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 normalized = entry.value;
|
||||||
const opposite = entry.opposite;
|
const opposite = entry.opposite;
|
||||||
const rootTarget = normalizeNumberValue(entry.digitalRoot);
|
const rootTarget = normalizeNumberValue(entry.digitalRoot);
|
||||||
@@ -469,11 +1106,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (detailTypeEl) {
|
if (detailTypeEl) {
|
||||||
detailTypeEl.textContent = `Opposite: ${opposite}`;
|
detailTypeEl.textContent = isTheoryView ? "Theory Analyzer" : `Opposite: ${opposite}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (detailSummaryEl) {
|
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);
|
renderNumbersSpecialPanel(context, normalized, entry);
|
||||||
@@ -484,6 +1123,12 @@
|
|||||||
|
|
||||||
detailBodyEl.replaceChildren();
|
detailBodyEl.replaceChildren();
|
||||||
|
|
||||||
|
const theoryCardEl = buildTheoryCard(normalized);
|
||||||
|
if (isTheoryView) {
|
||||||
|
detailBodyEl.append(theoryCardEl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const pairCardEl = document.createElement("div");
|
const pairCardEl = document.createElement("div");
|
||||||
pairCardEl.className = "numbers-detail-card";
|
pairCardEl.className = "numbers-detail-card";
|
||||||
|
|
||||||
@@ -601,7 +1246,7 @@
|
|||||||
|
|
||||||
calendarCardEl.append(calendarHeadingEl, calendarLinksWrapEl);
|
calendarCardEl.append(calendarHeadingEl, calendarLinksWrapEl);
|
||||||
|
|
||||||
const detailCards = [pairCardEl, kabbalahCardEl, alphabetCardEl];
|
const detailCards = [theoryCardEl, pairCardEl, kabbalahCardEl, alphabetCardEl];
|
||||||
|
|
||||||
if (hasTarotAccess()) {
|
if (hasTarotAccess()) {
|
||||||
const tarotCardEl = document.createElement("div");
|
const tarotCardEl = document.createElement("div");
|
||||||
|
|||||||
+75
-3
@@ -3,10 +3,14 @@
|
|||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
let activeNumberValue = 0;
|
let activeNumberValue = 0;
|
||||||
|
let activeNumbersView = "browse";
|
||||||
|
let sectionChangeListenerBound = false;
|
||||||
let config = {
|
let config = {
|
||||||
getReferenceData: () => null,
|
getReferenceData: () => null,
|
||||||
getMagickDataset: () => null,
|
getMagickDataset: () => null,
|
||||||
ensureTarotSection: null
|
ensureTarotSection: null,
|
||||||
|
getActiveSection: () => "home",
|
||||||
|
setActiveSection: null
|
||||||
};
|
};
|
||||||
|
|
||||||
const NUMBERS_SPECIAL_BASE_VALUES = [1, 2, 3, 4];
|
const NUMBERS_SPECIAL_BASE_VALUES = [1, 2, 3, 4];
|
||||||
@@ -88,6 +92,12 @@
|
|||||||
return typeof config.getMagickDataset === "function" ? config.getMagickDataset() : null;
|
return typeof config.getMagickDataset === "function" ? config.getMagickDataset() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getActiveSection() {
|
||||||
|
return typeof config.getActiveSection === "function"
|
||||||
|
? config.getActiveSection()
|
||||||
|
: "home";
|
||||||
|
}
|
||||||
|
|
||||||
function getElements() {
|
function getElements() {
|
||||||
return {
|
return {
|
||||||
countEl: document.getElementById("numbers-count"),
|
countEl: document.getElementById("numbers-count"),
|
||||||
@@ -96,10 +106,29 @@
|
|||||||
detailTypeEl: document.getElementById("numbers-detail-type"),
|
detailTypeEl: document.getElementById("numbers-detail-type"),
|
||||||
detailSummaryEl: document.getElementById("numbers-detail-summary"),
|
detailSummaryEl: document.getElementById("numbers-detail-summary"),
|
||||||
detailBodyEl: document.getElementById("numbers-detail-body"),
|
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) {
|
function normalizeNumberValue(value) {
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
if (!Number.isFinite(parsed)) {
|
if (!Number.isFinite(parsed)) {
|
||||||
@@ -257,6 +286,7 @@
|
|||||||
computeDigitalRoot,
|
computeDigitalRoot,
|
||||||
rankLabelToTarotMinorRank,
|
rankLabelToTarotMinorRank,
|
||||||
ensureTarotSection: config.ensureTarotSection,
|
ensureTarotSection: config.ensureTarotSection,
|
||||||
|
activeNumbersView,
|
||||||
selectNumberEntry,
|
selectNumberEntry,
|
||||||
NUMBERS_SPECIAL_BASE_VALUES,
|
NUMBERS_SPECIAL_BASE_VALUES,
|
||||||
numbersSpecialFlipState,
|
numbersSpecialFlipState,
|
||||||
@@ -282,6 +312,7 @@
|
|||||||
function ensureNumbersSection() {
|
function ensureNumbersSection() {
|
||||||
const { listEl } = getElements();
|
const { listEl } = getElements();
|
||||||
if (!listEl) {
|
if (!listEl) {
|
||||||
|
applyNumbersViewUiState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,6 +340,33 @@
|
|||||||
|
|
||||||
renderNumbersList();
|
renderNumbersList();
|
||||||
renderNumberDetail(activeNumberValue);
|
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 = {}) {
|
function init(nextConfig = {}) {
|
||||||
@@ -316,6 +374,15 @@
|
|||||||
...config,
|
...config,
|
||||||
...nextConfig
|
...nextConfig
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!sectionChangeListenerBound) {
|
||||||
|
document.addEventListener("section:changed", () => {
|
||||||
|
applyNumbersViewUiState();
|
||||||
|
});
|
||||||
|
sectionChangeListenerBound = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyNumbersViewUiState();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.TarotNumbersUi = {
|
window.TarotNumbersUi = {
|
||||||
@@ -323,6 +390,11 @@
|
|||||||
init,
|
init,
|
||||||
ensureNumbersSection,
|
ensureNumbersSection,
|
||||||
selectNumberEntry,
|
selectNumberEntry,
|
||||||
normalizeNumberValue
|
normalizeNumberValue,
|
||||||
|
showBrowseView,
|
||||||
|
showTheoryView,
|
||||||
|
getActiveView() {
|
||||||
|
return activeNumbersView;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
+11
-5
@@ -71,7 +71,13 @@
|
|||||||
<button id="open-kabbalah-cube" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Cube</button>
|
<button id="open-kabbalah-cube" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Cube</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button id="open-numbers" class="settings-trigger" type="button" aria-pressed="false">Numbers</button>
|
<div class="topbar-dropdown" aria-label="Numbers menu">
|
||||||
|
<button id="open-numbers" class="settings-trigger" type="button" aria-pressed="false" aria-haspopup="menu" aria-controls="numbers-subpages" aria-expanded="false">Numbers ▾</button>
|
||||||
|
<div id="numbers-subpages" class="topbar-dropdown-menu" role="menu" aria-label="Numbers subpages">
|
||||||
|
<button id="open-numbers-browse" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Browse</button>
|
||||||
|
<button id="open-numbers-theory" class="settings-trigger topbar-sub-trigger" type="button" role="menuitem">Theory</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button id="open-quiz" class="settings-trigger" type="button" aria-pressed="false">Quiz</button>
|
<button id="open-quiz" class="settings-trigger" type="button" aria-pressed="false">Quiz</button>
|
||||||
<div class="topbar-dropdown" aria-label="Tarot menu">
|
<div class="topbar-dropdown" aria-label="Tarot menu">
|
||||||
<button id="open-tarot" class="settings-trigger" type="button" aria-pressed="false" aria-haspopup="menu" aria-controls="tarot-subpages" aria-expanded="false">Tarot ▾</button>
|
<button id="open-tarot" class="settings-trigger" type="button" aria-pressed="false" aria-haspopup="menu" aria-controls="tarot-subpages" aria-expanded="false">Tarot ▾</button>
|
||||||
@@ -1404,19 +1410,19 @@
|
|||||||
<script src="app/ui-gods-references.js"></script>
|
<script src="app/ui-gods-references.js"></script>
|
||||||
<script src="app/ui-gods.js"></script>
|
<script src="app/ui-gods.js"></script>
|
||||||
<script src="app/ui-enochian.js?v=20260529-access-gating-02"></script>
|
<script src="app/ui-enochian.js?v=20260529-access-gating-02"></script>
|
||||||
<script src="app/ui-numbers-detail.js?v=20260529-access-gating-02"></script>
|
<script src="app/ui-numbers-detail.js?v=20260628-numbers-theory-nav-02"></script>
|
||||||
<script src="app/ui-numbers.js"></script>
|
<script src="app/ui-numbers.js?v=20260628-numbers-theory-nav-02"></script>
|
||||||
<script src="app/ui-tarot-spread.js"></script>
|
<script src="app/ui-tarot-spread.js"></script>
|
||||||
<script src="app/ui-tarot-frame.js?v=20260424-frame-export-01"></script>
|
<script src="app/ui-tarot-frame.js?v=20260424-frame-export-01"></script>
|
||||||
<script src="app/ui-settings.js?v=20260603-detail-text-scale-01"></script>
|
<script src="app/ui-settings.js?v=20260603-detail-text-scale-01"></script>
|
||||||
<script src="app/ui-chrome.js?v=20260328-topbar-settings-01"></script>
|
<script src="app/ui-chrome.js?v=20260328-topbar-settings-01"></script>
|
||||||
<script src="app/ui-navigation.js?v=20260528-kabbalah-cross-split-06"></script>
|
<script src="app/ui-navigation.js?v=20260628-numbers-theory-nav-02"></script>
|
||||||
<script src="app/ui-calendar-formatting.js?v=20260307b"></script>
|
<script src="app/ui-calendar-formatting.js?v=20260307b"></script>
|
||||||
<script src="app/ui-calendar-visuals.js?v=20260307b"></script>
|
<script src="app/ui-calendar-visuals.js?v=20260307b"></script>
|
||||||
<script src="app/ui-home-calendar.js?v=20260415-stellarium-toggle-01"></script>
|
<script src="app/ui-home-calendar.js?v=20260415-stellarium-toggle-01"></script>
|
||||||
<script src="app/ui-section-state.js?v=20260531-now-polling-02"></script>
|
<script src="app/ui-section-state.js?v=20260531-now-polling-02"></script>
|
||||||
<script src="app/app-runtime.js?v=20260531-now-polling-02"></script>
|
<script src="app/app-runtime.js?v=20260531-now-polling-02"></script>
|
||||||
<script src="app.js?v=20260603-detail-text-scale-01"></script>
|
<script src="app.js?v=20260628-numbers-theory-nav-02"></script>
|
||||||
<script src="app/navigation-detail-test-harness.js?v=20260401-universal-detail-02"></script>
|
<script src="app/navigation-detail-test-harness.js?v=20260401-universal-detail-02"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -41,25 +41,30 @@ Use this companion repository for downloadable decks:
|
|||||||
Typical flow:
|
Typical flow:
|
||||||
|
|
||||||
1. Clone the deck repository somewhere local.
|
1. Clone the deck repository somewhere local.
|
||||||
2. Copy one or more deck folders into `asset/tarot deck/`.
|
2. Copy a deck folder into `asset/tarot deck/` (repeat for more decks as needed).
|
||||||
3. Validate and regenerate the deck registry.
|
3. Start the app to pick up the deck list.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
git clone https://code.glowers.club/goyimnose/tarot-deck.git
|
git clone https://code.glowers.club/goyimnose/tarot-deck.git
|
||||||
# Copy selected deck folder(s) into Tarot Time deck directory.
|
# Copy a deck folder into asset/tarot deck/.
|
||||||
npm run validate:decks
|
|
||||||
npm run start
|
npm run start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Auto-discovery behavior:
|
||||||
|
|
||||||
|
- Deck ID is automatically inferred from the deck folder path when possible.
|
||||||
|
- Deck name is automatically generated from the ID when no label is provided.
|
||||||
|
- Example: `rider-waite-smith` becomes `Rider Waite Smith`.
|
||||||
|
|
||||||
## Deck Authoring and Validation
|
## Deck Authoring and Validation
|
||||||
|
|
||||||
Deck discovery is registry-driven and generated automatically.
|
Deck discovery is folder-driven and automatic.
|
||||||
|
|
||||||
1. Copy `asset/tarot deck/_template/` to a new folder under `asset/tarot deck/`.
|
1. Copy `asset/tarot deck/_template/` to a new folder under `asset/tarot deck/`.
|
||||||
2. Rename the new folder and update its `deck.json`.
|
2. Rename the folder to the deck ID you want (for example `thoth-crowley`).
|
||||||
3. Add card image files matching the naming rules in the manifest.
|
3. Update `deck.json` and add card image files matching the naming rules in the manifest.
|
||||||
4. Run `npm run validate:decks` before publishing/testing.
|
4. `label` in `deck.json` is optional; when omitted, a readable name is auto-generated from the folder-based ID.
|
||||||
5. Run `npm run start` (or `npm run generate:decks`) to rebuild `asset/tarot deck/decks.json`.
|
5. Run `npm run start` and confirm the deck appears in Settings → Tarot Deck.
|
||||||
|
|
||||||
Rules and behavior:
|
Rules and behavior:
|
||||||
|
|
||||||
@@ -70,15 +75,14 @@ Rules and behavior:
|
|||||||
- Decks can define `cardBack` in `deck.json`; if omitted, `back.webp/png/jpg/jpeg/avif/gif` in the deck root is auto-detected.
|
- Decks can define `cardBack` in `deck.json`; if omitted, `back.webp/png/jpg/jpeg/avif/gif` in the deck root is auto-detected.
|
||||||
- Manifests may override labels with `nameOverrides` and `minorNameOverrides`.
|
- Manifests may override labels with `nameOverrides` and `minorNameOverrides`.
|
||||||
- Invalid manifests or missing mapped files are skipped with terminal warnings.
|
- Invalid manifests or missing mapped files are skipped with terminal warnings.
|
||||||
|
- If a deck entry does not include an explicit ID, the app derives it from the manifest path/folder.
|
||||||
|
|
||||||
## NPM Scripts
|
## NPM Scripts
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `npm run start` | Generate deck registry, then serve the app locally and open `index.html`. |
|
| `npm run start` | Serve the app locally and open `index.html`. |
|
||||||
| `npm run dev` | Alias of `npm run start`. |
|
| `npm run dev` | Alias of `npm run start`. |
|
||||||
| `npm run generate:decks` | Rebuild `asset/tarot deck/decks.json`. |
|
|
||||||
| `npm run validate:decks` | Strict validation only (no write), exits on manifest/file problems. |
|
|
||||||
|
|
||||||
## Project Links
|
## Project Links
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user