diff --git a/MEMORY.md b/MEMORY.md
new file mode 100644
index 0000000..3a83677
--- /dev/null
+++ b/MEMORY.md
@@ -0,0 +1,11 @@
+- [HANDOFF Sesion 2 → 3 (LEER PRIMERO)](handoff_session_2_to_3.md) — estado al 2026-04-13, tareas inmediatas, decisiones pendientes, prueba visual del Navigator no ejecutada
+- [Convencion de naming Origin (acordada 2026-04-13)](decision_naming_convention.md) — arkhe-* render/audio | nigiro-* networking/P2P | origin-* cross-domain. PENDIENTE confirmar rename origin-aoi → nigiro-aoi
+- [Plan C activo — Larihana porteando Hypercore Rust](project_plan_c_hypercore_rust.md) — port completo Holepunch JS → Rust. Artifact: nigiro-hypercore (no arkhe-)
+- [Origin Palace ACTIVO via MCP](system_palace_active.md) — 19+ tools, usar palace_* para memoria persistente. PENDIENTE poblar con hitos Fase 2
+- [David Herran — Director de Convergencia Tecnologica](user_david_herran.md) — Rust/TS/Svelte, filosofia local-first, estilo directo. Arkhos primero, Ergon segundo (2026-04-12)
+- [Roadmap Ergon — 5 fases hacia demo MSF](project_roadmap_ergon.md) — Fase 1 4/6 + Fase 2 10/10 COMPLETE. Formalizado en docs/ORIGIN_ROADMAP_ERGON.md (sync commit b1e1762)
+- [Ecosistema de crates-regalo](project_gift_crates_ecosystem.md) — spatial-ui, sdf-text, audio, origin-aoi LISTOS. arkhe-anim en construccion. Formalizado en docs/ORIGIN_CRATE_WISHLIST.md
+- [Audit profundo NIGIRO + ARKHE](project_deep_audit_pending.md) — NIGIRO audit COMPLETADO (NIGIRO_AUDIT_V1.md ahora v1.1 por hilo doc-audit). ARKHE render audit pendiente post-demo
+- [Auditar codigo antes de hacer claims](feedback_audit_before_claims.md) — regla heredada: leer Cargo.toml workspace, verificar miembros, contar lineas, compilar ANTES de declarar
+- [Mision Claude-Tools — port origin-palace a Rust](project_origin_palace_mission.md) — briefing v1.0 sellado. Palace YA INTEGRADO y corriendo (ver system_palace_active.md)
+- [Hechos clave del upstream mempalace (verificados 2026-04-09)](project_palace_upstream_facts.md) — referencia historica del audit upstream
diff --git a/src/app/[locale]/applications/[slug]/ApplicationClient.tsx b/src/app/[locale]/applications/[slug]/ApplicationClient.tsx
index 1887d44..0fb96b0 100644
--- a/src/app/[locale]/applications/[slug]/ApplicationClient.tsx
+++ b/src/app/[locale]/applications/[slug]/ApplicationClient.tsx
@@ -8,6 +8,7 @@ import Image from "next/image";
import Script from "next/script";
import { ArrowLeft, CheckCircle2, Zap, LayoutDashboard, Cpu, PencilRuler, Factory, MapPin, ChevronDown, Play, FileText, Box, Loader2, Maximize, X, ChevronLeft, ChevronRight } from "lucide-react";
import BreathingField from "@/components/visuals/BreathingField";
+import AutoPlayVideo from "@/components/AutoPlayVideo";
// 🔥 EL TRUCO DEFINITIVO PARA TYPESCRIPT Y WEB COMPONENTS 🔥
// Al asignar el string a una variable con 'as any', TypeScript deja de
@@ -467,7 +468,7 @@ const renderMarkdown = (text: string, onImageClick: (url: string) => void) => {
{isLocalMp4 ? (
-
+
) : (
)}
@@ -887,7 +888,7 @@ function ExpandedCaseStudy({ node }: { node: any }) {
return (
{isLocalMp4 ? (
-
+
) : (
)}
diff --git a/src/app/[locale]/heritage/page.tsx b/src/app/[locale]/heritage/page.tsx
index 0ff5378..6d35fd1 100644
--- a/src/app/[locale]/heritage/page.tsx
+++ b/src/app/[locale]/heritage/page.tsx
@@ -5,6 +5,7 @@ import Image from "next/image";
import { prisma } from "@/lib/prisma";
import { ArrowLeft } from "lucide-react";
import BreathingField from "@/components/visuals/BreathingField";
+import AutoPlayVideo from "@/components/AutoPlayVideo";
// 🔥 IMPORTACIONES DE IDIOMAS
import { getLocalizedData } from "@/lib/i18nHelper";
@@ -145,7 +146,7 @@ const renderMarkdown = (text: string) => {
return;
}
- const ulMatch = trimmed.match(/^[-*]\s*(.*)/);
+ const ulMatch = trimmed.match(/^[-*]\s+(.*)/);
if (ulMatch) {
isOrderedList = false;
listItems.push(
{parseInline(ulMatch[1])});
@@ -242,17 +243,13 @@ export default async function HeritagePage({ params }: { params: Promise<{ local
)}
- {/* 🔥 BLOQUE DE VIDEO NATIVO (MP4 LOCAL) 🔥 */}
+ {/* 🔥 BLOQUE DE VIDEO NATIVO (MP4 LOCAL) — AUTOPLAY ON SCROLL 🔥 */}
{sec.type === 'video' && sec.mediaUrl && (
-
)}
@@ -263,4 +260,4 @@ export default async function HeritagePage({ params }: { params: Promise<{ local
);
-}
\ No newline at end of file
+}
diff --git a/src/app/[locale]/parts/_components/PartDetailsModal.tsx b/src/app/[locale]/parts/_components/PartDetailsModal.tsx
index 0161ba3..aa09140 100644
--- a/src/app/[locale]/parts/_components/PartDetailsModal.tsx
+++ b/src/app/[locale]/parts/_components/PartDetailsModal.tsx
@@ -5,6 +5,7 @@ import { X, Wrench, ShoppingBag, ChevronLeft, ChevronRight, Tag, Info, Play, Loc
import { useState } from "react";
import { useUIStore } from "@/lib/store/uiStore";
import { useTranslations } from "next-intl";
+import AutoPlayVideo from "@/components/AutoPlayVideo";
// 🔥 IMPORTAMOS TU SUPER PARSER
import { renderMarkdown } from "@/lib/markdownParser";
@@ -73,9 +74,8 @@ export default function PartDetailsModal({ part, isOpen, onClose }: PartDetailsM
{media.length > 0 ? (
<>
{media[currentMediaIdx].endsWith('.mp4') || media[currentMediaIdx].endsWith('.mov') ? (
-
) : (
@@ -199,4 +199,4 @@ export default function PartDetailsModal({ part, isOpen, onClose }: PartDetailsM
)}
);
-}
\ No newline at end of file
+}
diff --git a/src/components/AutoPlayVideo.tsx b/src/components/AutoPlayVideo.tsx
new file mode 100644
index 0000000..93b2116
--- /dev/null
+++ b/src/components/AutoPlayVideo.tsx
@@ -0,0 +1,81 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+
+interface AutoPlayVideoProps {
+ src: string;
+ className?: string;
+ poster?: string;
+ threshold?: number;
+ loop?: boolean;
+ controls?: boolean;
+}
+
+export default function AutoPlayVideo({
+ src,
+ className = "",
+ poster,
+ threshold = 0.5,
+ loop = false,
+ controls = true,
+}: AutoPlayVideoProps) {
+ const videoRef = useRef(null);
+ const userInteractedRef = useRef(false);
+
+ useEffect(() => {
+ const video = videoRef.current;
+ if (!video) return;
+
+ // Track manual user interaction (clicks on play/pause controls).
+ // We only mark as user interaction if the event was trusted (real user click).
+ const handlePlay = (e: Event) => {
+ if ((e as Event).isTrusted) userInteractedRef.current = true;
+ };
+ const handlePause = (e: Event) => {
+ if ((e as Event).isTrusted) userInteractedRef.current = true;
+ };
+
+ video.addEventListener("play", handlePlay);
+ video.addEventListener("pause", handlePause);
+
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (!video) return;
+
+ // If the user has manually controlled the video, respect their decision.
+ if (userInteractedRef.current) return;
+
+ if (entry.isIntersecting) {
+ video.play().catch(() => {
+ // Autoplay may be blocked by the browser — that's fine.
+ });
+ } else {
+ video.pause();
+ }
+ },
+ { threshold }
+ );
+
+ observer.observe(video);
+
+ return () => {
+ observer.disconnect();
+ video.removeEventListener("play", handlePlay);
+ video.removeEventListener("pause", handlePause);
+ };
+ }, [threshold]);
+
+ return (
+
+ );
+}
diff --git a/src/components/sections/ApplicationsDashboard.tsx b/src/components/sections/ApplicationsDashboard.tsx
index 54bedb4..2970be0 100644
--- a/src/components/sections/ApplicationsDashboard.tsx
+++ b/src/components/sections/ApplicationsDashboard.tsx
@@ -2,10 +2,10 @@
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
-import { ArrowRight, Zap, Scale, ShieldCheck, Cpu } from "lucide-react";
-// 🔥 Importamos Link de nuestro i18n
+import { ArrowRight, Zap, Scale, Cpu } from "lucide-react";
import { Link } from "@/i18n/routing";
-import { useTranslations } from "next-intl"; // 🔥
+import { useTranslations } from "next-intl";
+import { getIconForSlug } from "@/lib/applicationIcons";
export default function ApplicationsDashboard({ dbApps = [] }: { dbApps?: any[] }) {
const activeApps = dbApps.filter(app => app.isActive);
@@ -54,9 +54,14 @@ export default function ApplicationsDashboard({ dbApps = [] }: { dbApps?: any[]
: "bg-transparent border-transparent text-[#86868B] dark:text-[#A1A1A6] hover:bg-black/5 dark:hover:bg-white/5"
}`}
>
-
- {app.slug.includes("food") ? : }
-
+ {(() => {
+ const Icon = getIconForSlug(app.slug);
+ return (
+
+
+
+ );
+ })()}
{app.title}
))}
diff --git a/src/components/sections/ApplicationsDeep.tsx b/src/components/sections/ApplicationsDeep.tsx
index 8844c3b..805a2cc 100644
--- a/src/components/sections/ApplicationsDeep.tsx
+++ b/src/components/sections/ApplicationsDeep.tsx
@@ -1,22 +1,10 @@
"use client";
-import { motion } from "framer-motion";
-import { ArrowRight, Waves, Microscope, Snowflake, ShieldCheck, ThermometerSun, FlaskConical, Box, Droplets, Zap, Leaf } from "lucide-react";
import { Link } from "@/i18n/routing";
import { useTranslations } from "next-intl";
-
-const getIconForSlug = (slug: string) => {
- if (slug.includes("textile")) return Waves;
- if (slug.includes("lab")) return Microscope;
- if (slug.includes("temper") || slug.includes("defrost")) return Snowflake;
- if (slug.includes("pasteuriz")) return ShieldCheck;
- if (slug.includes("bak")) return ThermometerSun;
- if (slug.includes("vulcaniz")) return FlaskConical;
- if (slug.includes("foam")) return Box;
- if (slug.includes("print")) return Droplets;
- if (slug.includes("cannabis") || slug.includes("herb")) return Leaf;
- return Zap;
-};
+import { ArrowRight } from "lucide-react";
+import { motion } from "framer-motion";
+import { getIconForSlug } from "@/lib/applicationIcons";
export default function ApplicationsDeep({ dbApps = [] }: { dbApps?: any[] }) {
const t = useTranslations("AppsDeep");
diff --git a/src/components/sections/GlobalOperations.tsx b/src/components/sections/GlobalOperations.tsx
index 16350a9..a620f8f 100644
--- a/src/components/sections/GlobalOperations.tsx
+++ b/src/components/sections/GlobalOperations.tsx
@@ -163,9 +163,37 @@ function MapNode({ marker, isSelected, hqPos, onSelect, isDark, globeMode, camDi
gRef.current.scale.setScalar(scaleFactor * (isSelected ? 1.5 : 1.0));
});
- const dist = hqPos.distanceTo(pos);
- const apex = hqPos.clone().lerp(pos, 0.5).normalize()
- .multiplyScalar(RADIUS + dist * 0.28 + 0.14);
+ // Calculate the great-circle angle between the two points (0 = same point, π = antipodes)
+ const dotProduct = hqPos.clone().normalize().dot(pos.clone().normalize());
+ const angle = Math.acos(Math.max(-1, Math.min(1, dotProduct)));
+
+ // Compute apex direction using slerp (spherical interpolation), not lerp (linear).
+ // For near-antipodal points, lerp returns ~origin and normalize becomes unstable.
+ // Slerp gives the correct great-circle midpoint on the sphere surface.
+ let apexDir: THREE.Vector3;
+ if (angle < 0.001) {
+ // Same point — fallback
+ apexDir = hqPos.clone().normalize();
+ } else if (angle > Math.PI - 0.05) {
+ // Near antipodes — slerp is undefined, pick a perpendicular direction
+ // that goes "over the top" of the globe
+ const arbitrary = Math.abs(hqPos.y) < 0.9 ? new THREE.Vector3(0, 1, 0) : new THREE.Vector3(1, 0, 0);
+ apexDir = hqPos.clone().normalize().cross(arbitrary).normalize();
+ } else {
+ // Standard slerp midpoint
+ const sinAngle = Math.sin(angle);
+ const a = Math.sin(0.5 * angle) / sinAngle;
+ const b = Math.sin(0.5 * angle) / sinAngle;
+ apexDir = hqPos.clone().normalize().multiplyScalar(a)
+ .add(pos.clone().normalize().multiplyScalar(b))
+ .normalize();
+ }
+
+ // Arc height scales with the angle: short arcs stay close to surface,
+ // long arcs (near antipodes) lift high above the globe.
+ // angle ranges from 0 to π. Height factor scales from 0.15 to ~1.4 of RADIUS.
+ const heightFactor = 0.15 + (angle / Math.PI) * 1.25;
+ const apex = apexDir.multiplyScalar(RADIUS * (1 + heightFactor));
// ── ARC LINE COLORS & OPACITY ──
// Photo mode uses high-contrast colors (orange/yellow) instead of blue
diff --git a/src/lib/applicationIcons.ts b/src/lib/applicationIcons.ts
new file mode 100644
index 0000000..56b8be7
--- /dev/null
+++ b/src/lib/applicationIcons.ts
@@ -0,0 +1,14 @@
+import { Waves, Microscope, Snowflake, ShieldCheck, ThermometerSun, FlaskConical, Box, Droplets, Zap, Leaf } from "lucide-react";
+
+export const getIconForSlug = (slug: string) => {
+ if (slug.includes("textile")) return Waves;
+ if (slug.includes("lab")) return Microscope;
+ if (slug.includes("temper") || slug.includes("defrost")) return Snowflake;
+ if (slug.includes("pasteuriz") || slug.includes("cooking")) return ShieldCheck;
+ if (slug.includes("bak")) return ThermometerSun;
+ if (slug.includes("vulcaniz") || slug.includes("vucaniz")) return FlaskConical;
+ if (slug.includes("foam")) return Box;
+ if (slug.includes("print")) return Droplets;
+ if (slug.includes("cannabis") || slug.includes("herb")) return Leaf;
+ return Zap;
+};
\ No newline at end of file
diff --git a/src/lib/markdownParser.tsx b/src/lib/markdownParser.tsx
index 75545dc..c7919e6 100644
--- a/src/lib/markdownParser.tsx
+++ b/src/lib/markdownParser.tsx
@@ -1,5 +1,6 @@
import React from "react";
import { Play, Maximize2 } from "lucide-react";
+import AutoPlayVideo from "@/components/AutoPlayVideo";
// Nota: No incluí el componente 3D directamente aquí para no complicar dependencias,
// pero soporta tablas, listas, citas, videos e imágenes con lightbox.
@@ -113,7 +114,7 @@ export const renderMarkdown = (text: string, onImageClick?: (url: string) => voi
return;
}
- // ── VIDEO ──
+ // ── VIDEO (autoplay on viewport) ──
const videoMatch = trimmed.match(/^\[VIDEO:(.*?)\]$/);
if (videoMatch) {
pushList(); pushTable();
@@ -123,7 +124,7 @@ export const renderMarkdown = (text: string, onImageClick?: (url: string) => voi
-
+
);
return;
@@ -152,7 +153,7 @@ export const renderMarkdown = (text: string, onImageClick?: (url: string) => voi
}
// ── LISTS ──
- const ulMatch = trimmed.match(/^[-*]\s*(.*)/);
+ const ulMatch = trimmed.match(/^[-*]\s+(.*)/);
if (ulMatch) { isOrderedList = false; listItems.push({parseInline(ulMatch[1])}); return; }
const olMatch = trimmed.match(/^\d+\.\s*(.*)/);
@@ -165,4 +166,4 @@ export const renderMarkdown = (text: string, onImageClick?: (url: string) => voi
pushList(); pushTable();
return <>{elements}>;
-};
\ No newline at end of file
+};