@@ -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}`} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
@@ -263,4 +260,4 @@ export default async function HeritagePage({ params }: { params: Promise<{ local
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -199,4 +199,4 @@ export default function PartDetailsModal({ part, isOpen, onClose }: PartDetailsM
|
|||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<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} />}
|
const Icon = getIconForSlug(app.slug);
|
||||||
</div>
|
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'}`}>
|
||||||
|
<Icon size={20} strokeWidth={1.5} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
<span className="text-base font-medium">{app.title}</span>
|
<span className="text-base font-medium">{app.title}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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*(.*)/);
|
||||||
@@ -165,4 +166,4 @@ export const renderMarkdown = (text: string, onImageClick?: (url: string) => voi
|
|||||||
|
|
||||||
pushList(); pushTable();
|
pushList(); pushTable();
|
||||||
return <>{elements}</>;
|
return <>{elements}</>;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user