fixes Markdown
Deploy to VPS / deploy (push) Has been cancelled

This commit is contained in:
2026-04-16 10:29:20 -05:00
parent 69eb449da8
commit 21d0f9ee1c
10 changed files with 170 additions and 44 deletions
+11
View File
@@ -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
@@ -8,6 +8,7 @@ import Image from "next/image";
import Script from "next/script"; 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 { 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 BreathingField from "@/components/visuals/BreathingField";
import AutoPlayVideo from "@/components/AutoPlayVideo";
// 🔥 EL TRUCO DEFINITIVO PARA TYPESCRIPT Y WEB COMPONENTS 🔥 // 🔥 EL TRUCO DEFINITIVO PARA TYPESCRIPT Y WEB COMPONENTS 🔥
// Al asignar el string a una variable con 'as any', TypeScript deja de // 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) => {
</div> </div>
<div className="aspect-video"> <div className="aspect-video">
{isLocalMp4 ? ( {isLocalMp4 ? (
<video src={videoSrc} controls playsInline preload="metadata" className="w-full h-full object-contain" /> <AutoPlayVideo src={videoSrc} className="w-full h-full object-contain" />
) : ( ) : (
<iframe src={videoSrc} className="w-full h-full" allowFullScreen title="Embedded video" /> <iframe src={videoSrc} className="w-full h-full" allowFullScreen title="Embedded video" />
)} )}
@@ -887,7 +888,7 @@ function ExpandedCaseStudy({ node }: { node: any }) {
return ( return (
<div key={`v-${idx}`} className="relative w-full aspect-video rounded-2xl md:rounded-3xl overflow-hidden border border-black/10 dark:border-white/10 shadow-xl bg-black col-span-1 md:col-span-2"> <div key={`v-${idx}`} className="relative w-full aspect-video rounded-2xl md:rounded-3xl overflow-hidden border border-black/10 dark:border-white/10 shadow-xl bg-black col-span-1 md:col-span-2">
{isLocalMp4 ? ( {isLocalMp4 ? (
<video src={videoSrc} controls playsInline preload="metadata" className="absolute inset-0 w-full h-full object-contain" /> <AutoPlayVideo src={videoSrc} className="absolute inset-0 w-full h-full object-contain" />
) : ( ) : (
<iframe src={videoSrc} className="absolute inset-0 w-full h-full" allowFullScreen title={`Video ${idx + 1}`} /> <iframe src={videoSrc} className="absolute inset-0 w-full h-full" allowFullScreen title={`Video ${idx + 1}`} />
)} )}
+4 -7
View File
@@ -5,6 +5,7 @@ import Image from "next/image";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import BreathingField from "@/components/visuals/BreathingField"; import BreathingField from "@/components/visuals/BreathingField";
import AutoPlayVideo from "@/components/AutoPlayVideo";
// 🔥 IMPORTACIONES DE IDIOMAS // 🔥 IMPORTACIONES DE IDIOMAS
import { getLocalizedData } from "@/lib/i18nHelper"; import { getLocalizedData } from "@/lib/i18nHelper";
@@ -145,7 +146,7 @@ const renderMarkdown = (text: string) => {
return; return;
} }
const ulMatch = trimmed.match(/^[-*]\s*(.*)/); const ulMatch = trimmed.match(/^[-*]\s+(.*)/);
if (ulMatch) { if (ulMatch) {
isOrderedList = false; isOrderedList = false;
listItems.push(<li key={idx} className="leading-relaxed pl-2">{parseInline(ulMatch[1])}</li>); listItems.push(<li key={idx} className="leading-relaxed pl-2">{parseInline(ulMatch[1])}</li>);
@@ -242,17 +243,13 @@ export default async function HeritagePage({ params }: { params: Promise<{ local
</div> </div>
)} )}
{/* 🔥 BLOQUE DE VIDEO NATIVO (MP4 LOCAL) 🔥 */} {/* 🔥 BLOQUE DE VIDEO NATIVO (MP4 LOCAL) — AUTOPLAY ON SCROLL 🔥 */}
{sec.type === 'video' && sec.mediaUrl && ( {sec.type === 'video' && sec.mediaUrl && (
<div className="relative w-full aspect-video rounded-3xl overflow-hidden border border-white/10 shadow-2xl bg-black"> <div className="relative w-full aspect-video rounded-3xl overflow-hidden border border-white/10 shadow-2xl bg-black">
<video <AutoPlayVideo
src={`/heritage/videos/${sec.mediaUrl}`} src={`/heritage/videos/${sec.mediaUrl}`}
className="absolute inset-0 w-full h-full object-cover" className="absolute inset-0 w-full h-full object-cover"
autoPlay
loop loop
muted
playsInline
controls
/> />
</div> </div>
)} )}
@@ -5,6 +5,7 @@ import { X, Wrench, ShoppingBag, ChevronLeft, ChevronRight, Tag, Info, Play, Loc
import { useState } from "react"; import { useState } from "react";
import { useUIStore } from "@/lib/store/uiStore"; import { useUIStore } from "@/lib/store/uiStore";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import AutoPlayVideo from "@/components/AutoPlayVideo";
// 🔥 IMPORTAMOS TU SUPER PARSER // 🔥 IMPORTAMOS TU SUPER PARSER
import { renderMarkdown } from "@/lib/markdownParser"; import { renderMarkdown } from "@/lib/markdownParser";
@@ -73,9 +74,8 @@ export default function PartDetailsModal({ part, isOpen, onClose }: PartDetailsM
{media.length > 0 ? ( {media.length > 0 ? (
<> <>
{media[currentMediaIdx].endsWith('.mp4') || media[currentMediaIdx].endsWith('.mov') ? ( {media[currentMediaIdx].endsWith('.mp4') || media[currentMediaIdx].endsWith('.mov') ? (
<video <AutoPlayVideo
src={`/parts/${part.sku.toLowerCase()}/${media[currentMediaIdx]}`} src={`/parts/${part.sku.toLowerCase()}/${media[currentMediaIdx]}`}
controls
className="w-full h-full object-contain" className="w-full h-full object-contain"
/> />
) : ( ) : (
+81
View File
@@ -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<HTMLVideoElement>(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 (
<video
ref={videoRef}
src={src}
poster={poster}
controls={controls}
muted
playsInline
loop={loop}
preload="metadata"
className={className}
/>
);
}
@@ -2,10 +2,10 @@
import { useState } from "react"; import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { ArrowRight, Zap, Scale, ShieldCheck, Cpu } from "lucide-react"; import { ArrowRight, Zap, Scale, Cpu } from "lucide-react";
// 🔥 Importamos Link de nuestro i18n
import { Link } from "@/i18n/routing"; 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[] }) { export default function ApplicationsDashboard({ dbApps = [] }: { dbApps?: any[] }) {
const activeApps = dbApps.filter(app => app.isActive); 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" : "bg-transparent border-transparent text-[#86868B] dark:text-[#A1A1A6] hover:bg-black/5 dark:hover:bg-white/5"
}`} }`}
> >
{(() => {
const Icon = getIconForSlug(app.slug);
return (
<div className={`p-2 rounded-xl ${activeSlug === app.slug ? 'bg-[#0066CC]/10 text-[#0066CC] dark:bg-[#4DA6FF]/10 dark:text-[#4DA6FF]' : 'bg-transparent text-current'}`}> <div className={`p-2 rounded-xl ${activeSlug === app.slug ? 'bg-[#0066CC]/10 text-[#0066CC] dark:bg-[#4DA6FF]/10 dark:text-[#4DA6FF]' : 'bg-transparent text-current'}`}>
{app.slug.includes("food") ? <ShieldCheck size={20} /> : <Zap size={20} />} <Icon size={20} strokeWidth={1.5} />
</div> </div>
);
})()}
<span className="text-base font-medium">{app.title}</span> <span className="text-base font-medium">{app.title}</span>
</button> </button>
))} ))}
+3 -15
View File
@@ -1,22 +1,10 @@
"use client"; "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 { Link } from "@/i18n/routing";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { ArrowRight } from "lucide-react";
const getIconForSlug = (slug: string) => { import { motion } from "framer-motion";
if (slug.includes("textile")) return Waves; import { getIconForSlug } from "@/lib/applicationIcons";
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;
};
export default function ApplicationsDeep({ dbApps = [] }: { dbApps?: any[] }) { export default function ApplicationsDeep({ dbApps = [] }: { dbApps?: any[] }) {
const t = useTranslations("AppsDeep"); const t = useTranslations("AppsDeep");
+31 -3
View File
@@ -163,9 +163,37 @@ function MapNode({ marker, isSelected, hqPos, onSelect, isDark, globeMode, camDi
gRef.current.scale.setScalar(scaleFactor * (isSelected ? 1.5 : 1.0)); gRef.current.scale.setScalar(scaleFactor * (isSelected ? 1.5 : 1.0));
}); });
const dist = hqPos.distanceTo(pos); // Calculate the great-circle angle between the two points (0 = same point, π = antipodes)
const apex = hqPos.clone().lerp(pos, 0.5).normalize() const dotProduct = hqPos.clone().normalize().dot(pos.clone().normalize());
.multiplyScalar(RADIUS + dist * 0.28 + 0.14); 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 ── // ── ARC LINE COLORS & OPACITY ──
// Photo mode uses high-contrast colors (orange/yellow) instead of blue // Photo mode uses high-contrast colors (orange/yellow) instead of blue
+14
View File
@@ -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;
};
+4 -3
View File
@@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { Play, Maximize2 } from "lucide-react"; import { Play, Maximize2 } from "lucide-react";
import AutoPlayVideo from "@/components/AutoPlayVideo";
// Nota: No incluí el componente 3D directamente aquí para no complicar dependencias, // Nota: No incluí el componente 3D directamente aquí para no complicar dependencias,
// pero soporta tablas, listas, citas, videos e imágenes con lightbox. // 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; return;
} }
// ── VIDEO ── // ── VIDEO (autoplay on viewport) ──
const videoMatch = trimmed.match(/^\[VIDEO:(.*?)\]$/); const videoMatch = trimmed.match(/^\[VIDEO:(.*?)\]$/);
if (videoMatch) { if (videoMatch) {
pushList(); pushTable(); pushList(); pushTable();
@@ -123,7 +124,7 @@ export const renderMarkdown = (text: string, onImageClick?: (url: string) => voi
<div className="absolute top-2 left-2 z-10 pointer-events-none bg-black/70 backdrop-blur-sm rounded-full px-2 py-0.5 flex items-center gap-1"> <div className="absolute top-2 left-2 z-10 pointer-events-none bg-black/70 backdrop-blur-sm rounded-full px-2 py-0.5 flex items-center gap-1">
<Play size={10} className="text-white" /><span className="text-[9px] font-bold text-white uppercase tracking-widest">Video</span> <Play size={10} className="text-white" /><span className="text-[9px] font-bold text-white uppercase tracking-widest">Video</span>
</div> </div>
<video src={videoSrc} controls playsInline className="w-full h-full object-contain" /> <AutoPlayVideo src={videoSrc} className="w-full h-full object-contain" />
</div> </div>
); );
return; return;
@@ -152,7 +153,7 @@ export const renderMarkdown = (text: string, onImageClick?: (url: string) => voi
} }
// ── LISTS ── // ── LISTS ──
const ulMatch = trimmed.match(/^[-*]\s*(.*)/); const ulMatch = trimmed.match(/^[-*]\s+(.*)/);
if (ulMatch) { isOrderedList = false; listItems.push(<li key={idx}>{parseInline(ulMatch[1])}</li>); return; } if (ulMatch) { isOrderedList = false; listItems.push(<li key={idx}>{parseInline(ulMatch[1])}</li>); return; }
const olMatch = trimmed.match(/^\d+\.\s*(.*)/); const olMatch = trimmed.match(/^\d+\.\s*(.*)/);