trama-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CATALOG.md +1237 -0
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/THIRD-PARTY.md +4 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Ancient/brown.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Ancient/brown_inlay.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Ancient/brown_pressed.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Ancient/grey.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Ancient/grey_inlay.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Ancient/grey_pressed.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Ancient/tan.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Ancient/tan_inlay.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Ancient/tan_pressed.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Ancient/white.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Ancient/white_inlay.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Ancient/white_pressed.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Colored/blue.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Colored/blue_pressed.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Colored/green.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Colored/green_pressed.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Colored/grey.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Colored/grey_pressed.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Colored/red.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Colored/red_pressed.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Colored/yellow.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Colored/yellow_pressed.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Outline/blue.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Outline/blue_pressed.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Outline/green.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Outline/green_pressed.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Outline/red.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Outline/red_pressed.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Outline/yellow.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/Outline/yellow_pressed.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/list.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/space.png +0 -0
- package/assets/pixel/kenney-pixel-ui/9-slice/space_inlay.png +0 -0
- package/assets/pixel/kenney-pixel-ui/License.txt +23 -0
- package/assets/pixel/kenney-pixel-ui/spritesheet/sheet.png +0 -0
- package/bin/trama-ui.mjs +27 -0
- package/dist/components/ui/Accordion.d.ts +12 -0
- package/dist/components/ui/Accordion.js +12 -0
- package/dist/components/ui/Alert.d.ts +15 -0
- package/dist/components/ui/Alert.js +15 -0
- package/dist/components/ui/ArticlesSection.d.ts +30 -0
- package/dist/components/ui/ArticlesSection.js +18 -0
- package/dist/components/ui/AsciiBackground.d.ts +58 -0
- package/dist/components/ui/AsciiBackground.js +173 -0
- package/dist/components/ui/AsciiCard.d.ts +78 -0
- package/dist/components/ui/AsciiCard.js +253 -0
- package/dist/components/ui/AsciiChart.d.ts +18 -0
- package/dist/components/ui/AsciiChart.js +47 -0
- package/dist/components/ui/Badge.d.ts +15 -0
- package/dist/components/ui/Badge.js +8 -0
- package/dist/components/ui/BitmapText.d.ts +25 -0
- package/dist/components/ui/BitmapText.js +21 -0
- package/dist/components/ui/BlogCard.d.ts +33 -0
- package/dist/components/ui/BlogCard.js +13 -0
- package/dist/components/ui/Breadcrumbs.d.ts +9 -0
- package/dist/components/ui/Breadcrumbs.js +7 -0
- package/dist/components/ui/Button.d.ts +74 -0
- package/dist/components/ui/Button.js +48 -0
- package/dist/components/ui/CTASection.d.ts +13 -0
- package/dist/components/ui/CTASection.js +7 -0
- package/dist/components/ui/Carousel.d.ts +30 -0
- package/dist/components/ui/Carousel.js +80 -0
- package/dist/components/ui/CheckboxGroup.d.ts +11 -0
- package/dist/components/ui/CheckboxGroup.js +20 -0
- package/dist/components/ui/CodeBlock.d.ts +11 -0
- package/dist/components/ui/CodeBlock.js +20 -0
- package/dist/components/ui/ContactForm.d.ts +25 -0
- package/dist/components/ui/ContactForm.js +26 -0
- package/dist/components/ui/CrtOverlay.d.ts +10 -0
- package/dist/components/ui/CrtOverlay.js +5 -0
- package/dist/components/ui/Divider.d.ts +12 -0
- package/dist/components/ui/Divider.js +7 -0
- package/dist/components/ui/Drawer.d.ts +16 -0
- package/dist/components/ui/Drawer.js +25 -0
- package/dist/components/ui/Dropdown.d.ts +10 -0
- package/dist/components/ui/Dropdown.js +21 -0
- package/dist/components/ui/FAQSection.d.ts +14 -0
- package/dist/components/ui/FAQSection.js +7 -0
- package/dist/components/ui/FeatureGrid.d.ts +18 -0
- package/dist/components/ui/FeatureGrid.js +14 -0
- package/dist/components/ui/Footer.d.ts +14 -0
- package/dist/components/ui/Footer.js +16 -0
- package/dist/components/ui/GlyphCursor.d.ts +22 -0
- package/dist/components/ui/GlyphCursor.js +50 -0
- package/dist/components/ui/GridBackground.d.ts +15 -0
- package/dist/components/ui/GridBackground.js +6 -0
- package/dist/components/ui/Hero.d.ts +21 -0
- package/dist/components/ui/Hero.js +12 -0
- package/dist/components/ui/HoverFX.d.ts +29 -0
- package/dist/components/ui/HoverFX.js +75 -0
- package/dist/components/ui/Icon.d.ts +33 -0
- package/dist/components/ui/Icon.js +29 -0
- package/dist/components/ui/ImageGallery.d.ts +31 -0
- package/dist/components/ui/ImageGallery.js +79 -0
- package/dist/components/ui/LogoCloud.d.ts +12 -0
- package/dist/components/ui/LogoCloud.js +9 -0
- package/dist/components/ui/MagneticButton.d.ts +12 -0
- package/dist/components/ui/MagneticButton.js +49 -0
- package/dist/components/ui/Marquee.d.ts +29 -0
- package/dist/components/ui/Marquee.js +15 -0
- package/dist/components/ui/Modal.d.ts +40 -0
- package/dist/components/ui/Modal.js +51 -0
- package/dist/components/ui/NavBar.d.ts +14 -0
- package/dist/components/ui/NavBar.js +12 -0
- package/dist/components/ui/NeonSign.d.ts +14 -0
- package/dist/components/ui/NeonSign.js +7 -0
- package/dist/components/ui/OtpInput.d.ts +9 -0
- package/dist/components/ui/OtpInput.js +27 -0
- package/dist/components/ui/Pagination.d.ts +10 -0
- package/dist/components/ui/Pagination.js +24 -0
- package/dist/components/ui/Panel.d.ts +21 -0
- package/dist/components/ui/Panel.js +8 -0
- package/dist/components/ui/PixelFrame.d.ts +61 -0
- package/dist/components/ui/PixelFrame.js +29 -0
- package/dist/components/ui/Presence.d.ts +51 -0
- package/dist/components/ui/Presence.js +343 -0
- package/dist/components/ui/PricingCard.d.ts +30 -0
- package/dist/components/ui/PricingCard.js +21 -0
- package/dist/components/ui/PricingSection.d.ts +35 -0
- package/dist/components/ui/PricingSection.js +21 -0
- package/dist/components/ui/Progress.d.ts +17 -0
- package/dist/components/ui/Progress.js +9 -0
- package/dist/components/ui/RadioGroup.d.ts +10 -0
- package/dist/components/ui/RadioGroup.js +11 -0
- package/dist/components/ui/RangeSlider.d.ts +14 -0
- package/dist/components/ui/RangeSlider.js +11 -0
- package/dist/components/ui/Reveal.d.ts +48 -0
- package/dist/components/ui/Reveal.js +181 -0
- package/dist/components/ui/SceneFlash.d.ts +14 -0
- package/dist/components/ui/SceneFlash.js +51 -0
- package/dist/components/ui/ScrambleText.d.ts +31 -0
- package/dist/components/ui/ScrambleText.js +51 -0
- package/dist/components/ui/ScrollArea.d.ts +15 -0
- package/dist/components/ui/ScrollArea.js +18 -0
- package/dist/components/ui/SectionHeader.d.ts +12 -0
- package/dist/components/ui/SectionHeader.js +6 -0
- package/dist/components/ui/Select.d.ts +17 -0
- package/dist/components/ui/Select.js +31 -0
- package/dist/components/ui/Skeleton.d.ts +12 -0
- package/dist/components/ui/Skeleton.js +18 -0
- package/dist/components/ui/Spinner.d.ts +15 -0
- package/dist/components/ui/Spinner.js +24 -0
- package/dist/components/ui/Spotlight.d.ts +16 -0
- package/dist/components/ui/Spotlight.js +16 -0
- package/dist/components/ui/Sprite.d.ts +27 -0
- package/dist/components/ui/Sprite.js +19 -0
- package/dist/components/ui/StatCounter.d.ts +24 -0
- package/dist/components/ui/StatCounter.js +70 -0
- package/dist/components/ui/StatsSection.d.ts +14 -0
- package/dist/components/ui/StatsSection.js +13 -0
- package/dist/components/ui/Stepper.d.ts +10 -0
- package/dist/components/ui/Stepper.js +7 -0
- package/dist/components/ui/Table.d.ts +10 -0
- package/dist/components/ui/Table.js +8 -0
- package/dist/components/ui/Tabs.d.ts +12 -0
- package/dist/components/ui/Tabs.js +18 -0
- package/dist/components/ui/TeamSection.d.ts +18 -0
- package/dist/components/ui/TeamSection.js +19 -0
- package/dist/components/ui/TerminalTyper.d.ts +20 -0
- package/dist/components/ui/TerminalTyper.js +45 -0
- package/dist/components/ui/Testimonial.d.ts +19 -0
- package/dist/components/ui/Testimonial.js +9 -0
- package/dist/components/ui/TestimonialSection.d.ts +20 -0
- package/dist/components/ui/TestimonialSection.js +15 -0
- package/dist/components/ui/TextField.d.ts +26 -0
- package/dist/components/ui/TextField.js +13 -0
- package/dist/components/ui/TextmodeBackground.d.ts +52 -0
- package/dist/components/ui/TextmodeBackground.js +83 -0
- package/dist/components/ui/Timeline.d.ts +13 -0
- package/dist/components/ui/Timeline.js +8 -0
- package/dist/components/ui/Toast.d.ts +30 -0
- package/dist/components/ui/Toast.js +47 -0
- package/dist/components/ui/Toggle.d.ts +13 -0
- package/dist/components/ui/Toggle.js +10 -0
- package/dist/components/ui/Tooltip.d.ts +20 -0
- package/dist/components/ui/Tooltip.js +20 -0
- package/dist/components/ui/Typewriter.d.ts +20 -0
- package/dist/components/ui/Typewriter.js +46 -0
- package/dist/components/ui/VideoPlayer.d.ts +81 -0
- package/dist/components/ui/VideoPlayer.js +379 -0
- package/dist/components/ui/fill.d.ts +32 -0
- package/dist/components/ui/fill.js +27 -0
- package/dist/components/ui/intent.d.ts +20 -0
- package/dist/components/ui/intent.js +23 -0
- package/dist/components/ui/styles/kit.css +31 -0
- package/dist/components/ui/styles/ui-carousel.css +113 -0
- package/dist/components/ui/styles/ui-kit.css +1135 -0
- package/dist/components/ui/styles/ui-video.css +75 -0
- package/dist/components/ui/swiperCss.d.ts +15 -0
- package/dist/components/ui/swiperCss.js +15 -0
- package/dist/components/ui/variants.d.ts +12 -0
- package/dist/components/ui/variants.js +10 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +77 -0
- package/dist/lib/asciify/painters.d.ts +25 -0
- package/dist/lib/asciify/painters.js +604 -0
- package/dist/lib/asciify/pointer.d.ts +12 -0
- package/dist/lib/asciify/pointer.js +54 -0
- package/dist/lib/asciify/shape-painters.d.ts +44 -0
- package/dist/lib/asciify/shape-painters.js +335 -0
- package/dist/lib/asciify/theme-painters.d.ts +8 -0
- package/dist/lib/asciify/theme-painters.js +437 -0
- package/dist/lib/cells.d.ts +46 -0
- package/dist/lib/cells.js +114 -0
- package/dist/lib/scroll/store.d.ts +21 -0
- package/dist/lib/scroll/store.js +25 -0
- package/dist/lib/sketches/aurora.d.ts +3 -0
- package/dist/lib/sketches/aurora.js +42 -0
- package/dist/lib/sketches/fire.d.ts +3 -0
- package/dist/lib/sketches/fire.js +80 -0
- package/dist/lib/sketches/flow.d.ts +3 -0
- package/dist/lib/sketches/flow.js +70 -0
- package/dist/lib/sketches/life.d.ts +3 -0
- package/dist/lib/sketches/life.js +107 -0
- package/dist/lib/sketches/matrix.d.ts +3 -0
- package/dist/lib/sketches/matrix.js +64 -0
- package/dist/lib/sketches/ocean.d.ts +3 -0
- package/dist/lib/sketches/ocean.js +56 -0
- package/dist/lib/sketches/plasma.d.ts +3 -0
- package/dist/lib/sketches/plasma.js +29 -0
- package/dist/lib/sketches/registry.d.ts +6 -0
- package/dist/lib/sketches/registry.js +60 -0
- package/dist/lib/sketches/ripples.d.ts +3 -0
- package/dist/lib/sketches/ripples.js +84 -0
- package/dist/lib/sketches/scroll-assemble.d.ts +8 -0
- package/dist/lib/sketches/scroll-assemble.js +90 -0
- package/dist/lib/sketches/scroll-parallax.d.ts +6 -0
- package/dist/lib/sketches/scroll-parallax.js +76 -0
- package/dist/lib/sketches/scroll-rain.d.ts +6 -0
- package/dist/lib/sketches/scroll-rain.js +82 -0
- package/dist/lib/sketches/scroll-stats.d.ts +7 -0
- package/dist/lib/sketches/scroll-stats.js +81 -0
- package/dist/lib/sketches/scroll-terminal.d.ts +6 -0
- package/dist/lib/sketches/scroll-terminal.js +87 -0
- package/dist/lib/sketches/scroll-zoom.d.ts +7 -0
- package/dist/lib/sketches/scroll-zoom.js +49 -0
- package/dist/lib/sketches/starfield.d.ts +3 -0
- package/dist/lib/sketches/starfield.js +54 -0
- package/dist/lib/sketches/text-3d.d.ts +6 -0
- package/dist/lib/sketches/text-3d.js +62 -0
- package/dist/lib/sketches/text-combo-figlet.d.ts +6 -0
- package/dist/lib/sketches/text-combo-figlet.js +91 -0
- package/dist/lib/sketches/text-glitch.d.ts +3 -0
- package/dist/lib/sketches/text-glitch.js +57 -0
- package/dist/lib/sketches/text-marquee.d.ts +3 -0
- package/dist/lib/sketches/text-marquee.js +35 -0
- package/dist/lib/sketches/text-particles.d.ts +9 -0
- package/dist/lib/sketches/text-particles.js +91 -0
- package/dist/lib/sketches/text-rain.d.ts +3 -0
- package/dist/lib/sketches/text-rain.js +73 -0
- package/dist/lib/sketches/text-scramble.d.ts +6 -0
- package/dist/lib/sketches/text-scramble.js +49 -0
- package/dist/lib/sketches/text-wave.d.ts +3 -0
- package/dist/lib/sketches/text-wave.js +43 -0
- package/dist/lib/sketches/theme-orbit.d.ts +6 -0
- package/dist/lib/sketches/theme-orbit.js +119 -0
- package/dist/lib/sketches/theme-pulse.d.ts +7 -0
- package/dist/lib/sketches/theme-pulse.js +84 -0
- package/dist/lib/sketches/tunnel.d.ts +3 -0
- package/dist/lib/sketches/tunnel.js +28 -0
- package/dist/lib/text/banner.d.ts +5 -0
- package/dist/lib/text/banner.js +35 -0
- package/dist/lib/text/mask.d.ts +40 -0
- package/dist/lib/text/mask.js +143 -0
- package/dist/lib/text/store.d.ts +23 -0
- package/dist/lib/text/store.js +86 -0
- package/dist/lib/ui/card-fields.d.ts +52 -0
- package/dist/lib/ui/card-fields.js +193 -0
- package/dist/lib/ui/fonts.d.ts +56 -0
- package/dist/lib/ui/fonts.js +94 -0
- package/dist/lib/ui/fx.d.ts +42 -0
- package/dist/lib/ui/fx.js +162 -0
- package/dist/lib/ui/icons.d.ts +437 -0
- package/dist/lib/ui/icons.js +314 -0
- package/dist/lib/ui/placeholder.d.ts +11 -0
- package/dist/lib/ui/placeholder.js +65 -0
- package/dist/lib/ui/tokens.d.ts +49 -0
- package/dist/lib/ui/tokens.js +129 -0
- package/dist/lib/ui/useTokens.d.ts +10 -0
- package/dist/lib/ui/useTokens.js +32 -0
- package/package.json +80 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { resolveImage } from "../../lib/ui/placeholder.js";
|
|
5
|
+
import { renderGlyph } from "./Icon.js";
|
|
6
|
+
import { vcls } from "./variants.js";
|
|
7
|
+
/**
|
|
8
|
+
* Reconoce enlaces de YouTube (watch, youtu.be, shorts, live, embed, playlist, con `list=` y `t=`) y de Vimeo; cualquier
|
|
9
|
+
* otra cosa se trata como un archivo de vídeo.
|
|
10
|
+
*/
|
|
11
|
+
export function parseEmbed(src) {
|
|
12
|
+
const url = src.trim();
|
|
13
|
+
const yt = /(?:(?:www\.|m\.|music\.)?youtube(?:-nocookie)?\.com\/(?:watch\?(?:[^#]*&)?v=|shorts\/|embed\/|live\/|v\/)|youtu\.be\/)([\w-]{11})/.exec(url);
|
|
14
|
+
const list = /[?&]list=([\w-]+)/.exec(url)?.[1];
|
|
15
|
+
const t = /[?&#](?:t|start)=(\d+)/.exec(url)?.[1];
|
|
16
|
+
const start = t ? parseInt(t, 10) : undefined;
|
|
17
|
+
if (yt)
|
|
18
|
+
return { kind: "youtube", id: yt[1], list, start, url };
|
|
19
|
+
if (list && /youtube\.com\/playlist/.test(url))
|
|
20
|
+
return { kind: "youtube", id: "", list, url };
|
|
21
|
+
const vm = /vimeo\.com\/(?:video\/)?(\d+)/.exec(url);
|
|
22
|
+
if (vm)
|
|
23
|
+
return { kind: "vimeo", id: vm[1], start, url };
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Carátula de un vídeo alojado en Cloudinary: la propia URL con otra extensión y una transformación que extrae un fotograma
|
|
28
|
+
* (`so_` = segundo, `w_` = ancho). Devuelve `undefined` para cualquier otra fuente.
|
|
29
|
+
*/
|
|
30
|
+
export function cloudinaryPoster(src, second = 1, width = 960) {
|
|
31
|
+
if (!/res\.cloudinary\.com\/[^/]+\/video\/upload\//.test(src))
|
|
32
|
+
return undefined;
|
|
33
|
+
return src.replace("/video/upload/", `/video/upload/so_${second},w_${width}/`).replace(/\.[a-z0-9]+$/i, ".jpg");
|
|
34
|
+
}
|
|
35
|
+
const fmt = (s) => {
|
|
36
|
+
if (!isFinite(s) || s < 0)
|
|
37
|
+
s = 0;
|
|
38
|
+
const m = Math.floor(s / 60);
|
|
39
|
+
return `${m}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
|
|
40
|
+
};
|
|
41
|
+
const SPEEDS = [0.5, 1, 1.25, 1.5, 2];
|
|
42
|
+
/** Barra de valor 0..1 con relleno cuadrado, arrastrable y accesible (progreso y volumen comparten esta pieza). */
|
|
43
|
+
function Bar({ value, buffered = 0, onChange, label, valueText, step = 0.05, className = "" }) {
|
|
44
|
+
const drag = useRef(false);
|
|
45
|
+
const set = (e) => {
|
|
46
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
47
|
+
onChange(Math.min(1, Math.max(0, (e.clientX - r.left) / r.width)));
|
|
48
|
+
};
|
|
49
|
+
const onKey = (e) => {
|
|
50
|
+
const d = e.key === "ArrowRight" || e.key === "ArrowUp" ? step : e.key === "ArrowLeft" || e.key === "ArrowDown" ? -step : e.key === "Home" ? -1 : e.key === "End" ? 1 : 0;
|
|
51
|
+
if (!d)
|
|
52
|
+
return;
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
e.stopPropagation(); // que la barra no dispare también los atajos del reproductor
|
|
55
|
+
onChange(Math.min(1, Math.max(0, value + d)));
|
|
56
|
+
};
|
|
57
|
+
return (_jsxs("div", { className: `ui-vid__bar ${className}`, role: "slider", tabIndex: 0, "aria-label": label, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(value * 100), "aria-valuetext": valueText, onPointerDown: (e) => {
|
|
58
|
+
drag.current = true;
|
|
59
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
60
|
+
set(e);
|
|
61
|
+
}, onPointerMove: (e) => drag.current && set(e), onPointerUp: () => (drag.current = false), onPointerCancel: () => (drag.current = false), onKeyDown: onKey, children: [_jsx("span", { className: "ui-vid__bar-buf", style: { width: `${buffered * 100}%` } }), _jsx("span", { className: "ui-vid__bar-fill", style: { width: `${value * 100}%` } }), _jsx("span", { className: "ui-vid__bar-thumb", style: { left: `${value * 100}%` } })] }));
|
|
62
|
+
}
|
|
63
|
+
/** YouTube/Vimeo bajo demanda: hasta pulsar play solo hay una imagen; el iframe (sin cookies) se crea entonces. */
|
|
64
|
+
function EmbedFrame({ embed, poster, title }) {
|
|
65
|
+
const [on, setOn] = useState(false);
|
|
66
|
+
const thumb = poster ? resolveImage(poster) : embed.kind === "youtube" && embed.id ? `https://i.ytimg.com/vi/${embed.id}/hqdefault.jpg` : undefined;
|
|
67
|
+
const q = new URLSearchParams({ autoplay: "1", playsinline: "1" });
|
|
68
|
+
if (embed.start)
|
|
69
|
+
q.set(embed.kind === "youtube" ? "start" : "t", embed.kind === "youtube" ? String(embed.start) : `${embed.start}s`);
|
|
70
|
+
let url;
|
|
71
|
+
if (embed.kind === "youtube") {
|
|
72
|
+
q.set("rel", "0");
|
|
73
|
+
q.set("modestbranding", "1");
|
|
74
|
+
if (embed.list)
|
|
75
|
+
q.set("list", embed.list);
|
|
76
|
+
// Solo una lista de reproducción: YouTube la sirve como «videoseries»
|
|
77
|
+
url = `https://www.youtube-nocookie.com/embed/${embed.id || "videoseries"}?${q}`;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
q.set("dnt", "1");
|
|
81
|
+
url = `https://player.vimeo.com/video/${embed.id}?${q}`;
|
|
82
|
+
}
|
|
83
|
+
const label = embed.kind === "youtube" ? "YouTube" : "Vimeo";
|
|
84
|
+
if (on)
|
|
85
|
+
return (_jsxs(_Fragment, { children: [_jsx("iframe", { className: "ui-vid__frame", src: url, title: title, referrerPolicy: "strict-origin-when-cross-origin", allow: "autoplay; fullscreen; picture-in-picture; encrypted-media", allowFullScreen: true }), _jsxs("a", { className: "ui-vid__tag ui-vid__tag--link", href: embed.url, target: "_blank", rel: "noopener noreferrer", children: ["Abrir en ", label, " \u2197"] })] }));
|
|
86
|
+
return (_jsxs("button", { type: "button", className: "ui-vid__facade", onClick: () => setOn(true), "aria-label": `Reproducir: ${title}`, children: [thumb && _jsx("img", { src: thumb, alt: "", loading: "lazy", draggable: false }), _jsx("span", { className: "ui-vid__big", "aria-hidden": true, children: renderGlyph("icon:play", 34) }), _jsxs("span", { className: "ui-vid__tag", children: [label, embed.list ? " · lista" : "", " \u00B7 se carga al pulsar"] })] }));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Reproductor de vídeo del kit, construido sobre el `<video>` nativo: sin dependencias, con controles propios del estilo
|
|
90
|
+
* (cuadrados, borde duro, acento) y todo lo que una landing necesita — poster, atajos de teclado, arrastre en la barra,
|
|
91
|
+
* velocidad, volumen, subtítulos, imagen en imagen, pantalla completa, pausa fuera de pantalla y modo fondo.
|
|
92
|
+
* Los enlaces de YouTube/Vimeo se cargan con una carátula que solo crea el iframe (sin cookies) al pulsar play.
|
|
93
|
+
*
|
|
94
|
+
* Atajos con el reproductor enfocado: espacio/K play·pausa, ←/→ ±5 s (J/L ±10 s), ↑/↓ volumen, M silencio, F pantalla completa, C subtítulos, 0–9 salta al 0–90 %.
|
|
95
|
+
*/
|
|
96
|
+
export default function VideoPlayer({ src, poster, title = "Vídeo", captions, ratio = "16/9", autoplay = false, muted = false, loop = false, controls = "full", startAt = 0, background = false, pauseOffscreen = true, scanlines = false, ascii = false, asciiStyle = "ascii", asciiCell = 6, asciiColor = "source", asciiHover = "water", asciiHoverStrength = 0.7, asciiHoverRadius = 0.28, variant = "retro", className = "" }) {
|
|
97
|
+
const embed = background ? null : parseEmbed(src);
|
|
98
|
+
const asciiHost = useRef(null);
|
|
99
|
+
const asciiPlayer = useRef(null);
|
|
100
|
+
const [asciiFailed, setAsciiFailed] = useState(false);
|
|
101
|
+
const asciiOn = ascii && !embed && !asciiFailed;
|
|
102
|
+
useEffect(() => setAsciiFailed(false), [src, ascii]); // otra fuente o volver a activarlo: se vuelve a intentar
|
|
103
|
+
const wrap = useRef(null);
|
|
104
|
+
const video = useRef(null);
|
|
105
|
+
const idleTimer = useRef(undefined);
|
|
106
|
+
const autoPaused = useRef(false);
|
|
107
|
+
const [playing, setPlaying] = useState(false);
|
|
108
|
+
const [started, setStarted] = useState(false);
|
|
109
|
+
const [time, setTime] = useState(0);
|
|
110
|
+
const [duration, setDuration] = useState(0);
|
|
111
|
+
const [buffered, setBuffered] = useState(0);
|
|
112
|
+
const [isMuted, setIsMuted] = useState(muted || background || autoplay);
|
|
113
|
+
const [volume, setVolume] = useState(1);
|
|
114
|
+
const [speed, setSpeed] = useState(1);
|
|
115
|
+
const [full, setFull] = useState(false);
|
|
116
|
+
const [idle, setIdle] = useState(false);
|
|
117
|
+
const [capOn, setCapOn] = useState(false);
|
|
118
|
+
const [failed, setFailed] = useState(false);
|
|
119
|
+
const [reduced, setReduced] = useState(false);
|
|
120
|
+
const cap = captions?.split("|");
|
|
121
|
+
const bg = background;
|
|
122
|
+
const showControls = !bg && controls !== "none";
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
const mq = matchMedia("(prefers-reduced-motion: reduce)");
|
|
125
|
+
setReduced(mq.matches);
|
|
126
|
+
const on = () => setReduced(mq.matches);
|
|
127
|
+
mq.addEventListener("change", on);
|
|
128
|
+
return () => mq.removeEventListener("change", on);
|
|
129
|
+
}, []);
|
|
130
|
+
const play = useCallback(() => {
|
|
131
|
+
const v = video.current;
|
|
132
|
+
if (!v)
|
|
133
|
+
return;
|
|
134
|
+
autoPaused.current = false;
|
|
135
|
+
void v.play().catch(() => { });
|
|
136
|
+
}, []);
|
|
137
|
+
const pause = useCallback(() => video.current?.pause(), []);
|
|
138
|
+
const toggle = useCallback(() => (video.current?.paused ? play() : pause()), [play, pause]);
|
|
139
|
+
const seek = useCallback((t) => {
|
|
140
|
+
const v = video.current;
|
|
141
|
+
if (v && isFinite(v.duration))
|
|
142
|
+
v.currentTime = Math.min(v.duration, Math.max(0, t));
|
|
143
|
+
}, []);
|
|
144
|
+
const setVol = useCallback((x) => {
|
|
145
|
+
const v = video.current;
|
|
146
|
+
if (!v)
|
|
147
|
+
return;
|
|
148
|
+
v.volume = x;
|
|
149
|
+
v.muted = x === 0;
|
|
150
|
+
}, []);
|
|
151
|
+
const toggleMute = useCallback(() => {
|
|
152
|
+
const v = video.current;
|
|
153
|
+
if (v)
|
|
154
|
+
v.muted = !v.muted;
|
|
155
|
+
}, []);
|
|
156
|
+
const toggleFull = useCallback(() => {
|
|
157
|
+
const el = wrap.current;
|
|
158
|
+
if (!el)
|
|
159
|
+
return;
|
|
160
|
+
if (document.fullscreenElement)
|
|
161
|
+
void document.exitFullscreen();
|
|
162
|
+
else
|
|
163
|
+
void el.requestFullscreen?.();
|
|
164
|
+
}, []);
|
|
165
|
+
const toggleCap = useCallback(() => {
|
|
166
|
+
const v = video.current;
|
|
167
|
+
const track = v?.textTracks[0];
|
|
168
|
+
if (!track)
|
|
169
|
+
return;
|
|
170
|
+
const on = track.mode !== "showing";
|
|
171
|
+
track.mode = on ? "showing" : "hidden";
|
|
172
|
+
setCapOn(on);
|
|
173
|
+
}, []);
|
|
174
|
+
// Autoplay / modo fondo: solo si el usuario no pidió menos movimiento, y siempre mudo (política de los navegadores)
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
const v = video.current;
|
|
177
|
+
if (!v || embed)
|
|
178
|
+
return;
|
|
179
|
+
if ((autoplay || bg) && !reduced) {
|
|
180
|
+
v.muted = true;
|
|
181
|
+
void v.play().catch(() => { });
|
|
182
|
+
}
|
|
183
|
+
else if (bg && reduced)
|
|
184
|
+
v.pause();
|
|
185
|
+
}, [autoplay, bg, reduced, embed, src]);
|
|
186
|
+
// Pausa fuera de pantalla: ahorra CPU y batería en landings con varios vídeos
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
const el = wrap.current;
|
|
189
|
+
if (!el || embed || !pauseOffscreen)
|
|
190
|
+
return;
|
|
191
|
+
const io = new IntersectionObserver(([e]) => {
|
|
192
|
+
const v = video.current;
|
|
193
|
+
if (!v)
|
|
194
|
+
return;
|
|
195
|
+
if (!e.isIntersecting && !v.paused) {
|
|
196
|
+
autoPaused.current = true;
|
|
197
|
+
v.pause();
|
|
198
|
+
}
|
|
199
|
+
else if (e.isIntersecting && autoPaused.current && !reduced) {
|
|
200
|
+
autoPaused.current = false;
|
|
201
|
+
void v.play().catch(() => { });
|
|
202
|
+
}
|
|
203
|
+
}, { threshold: 0.2 });
|
|
204
|
+
io.observe(el);
|
|
205
|
+
return () => io.disconnect();
|
|
206
|
+
}, [pauseOffscreen, embed, reduced]);
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
const on = () => setFull(document.fullscreenElement === wrap.current);
|
|
209
|
+
document.addEventListener("fullscreenchange", on);
|
|
210
|
+
return () => document.removeEventListener("fullscreenchange", on);
|
|
211
|
+
}, []);
|
|
212
|
+
// Efecto ASCII: asciify-engine Studio toma el propio <video> como fuente y pinta encima. Se monta una vez por vídeo y los
|
|
213
|
+
// ajustes se aplican en caliente (efecto de abajo); se pausa con el vídeo para no gastar CPU en un fotograma que no cambia.
|
|
214
|
+
const asciiSettings = useCallback(() => ({
|
|
215
|
+
style: asciiStyle,
|
|
216
|
+
cellSize: asciiCell,
|
|
217
|
+
colorMode: asciiColor,
|
|
218
|
+
ink: (wrap.current && getComputedStyle(wrap.current).getPropertyValue("--acc").trim()) || "#7cc4ff",
|
|
219
|
+
effects: { bloom: 0, scanlines: 0, vignette: 0, grain: 0, glitch: 0 },
|
|
220
|
+
hover: { effect: asciiHover, strength: asciiHoverStrength, radius: asciiHoverRadius },
|
|
221
|
+
}), [asciiStyle, asciiCell, asciiColor, asciiHover, asciiHoverStrength, asciiHoverRadius]);
|
|
222
|
+
const settingsRef = useRef(asciiSettings);
|
|
223
|
+
settingsRef.current = asciiSettings;
|
|
224
|
+
// El lienzo solo corre si está a la vista y hay algo que animar: el vídeo reproduciéndose o un hover que reaccione al puntero
|
|
225
|
+
const asciiLive = useRef({ visible: true, playing: false, hover: asciiHover });
|
|
226
|
+
asciiLive.current.hover = asciiHover;
|
|
227
|
+
const syncAscii = useCallback(() => {
|
|
228
|
+
const l = asciiLive.current;
|
|
229
|
+
asciiPlayer.current?.pause(!(l.visible && (l.playing || l.hover !== "none")));
|
|
230
|
+
}, []);
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
const host = asciiHost.current;
|
|
233
|
+
const v = video.current;
|
|
234
|
+
if (!asciiOn || !host || !v)
|
|
235
|
+
return;
|
|
236
|
+
let disposed = false;
|
|
237
|
+
let ro;
|
|
238
|
+
const canvas = document.createElement("canvas");
|
|
239
|
+
canvas.className = "ui-fill-canvas";
|
|
240
|
+
host.appendChild(canvas);
|
|
241
|
+
const start = async () => {
|
|
242
|
+
const { mountStudioMedia, normalizeStudioSettings } = await import("asciify-engine/studio");
|
|
243
|
+
if (disposed)
|
|
244
|
+
return;
|
|
245
|
+
const media = { source: v, width: v.videoWidth || 640, height: v.videoHeight || 360, duration: v.duration || 0, animated: true, seek: async () => { }, frame: () => v, destroy: () => { } };
|
|
246
|
+
const w = Math.max(2, Math.round(host.clientWidth));
|
|
247
|
+
const h = Math.max(2, Math.round(host.clientHeight));
|
|
248
|
+
const p = mountStudioMedia(canvas, media, {
|
|
249
|
+
settings: normalizeStudioSettings(settingsRef.current()),
|
|
250
|
+
width: w,
|
|
251
|
+
height: h,
|
|
252
|
+
fps: 30,
|
|
253
|
+
onError: () => setAsciiFailed(true), // p. ej. lienzo contaminado: el servidor del vídeo no permite CORS
|
|
254
|
+
});
|
|
255
|
+
p.resize(w, h, Math.min(devicePixelRatio || 1, 2));
|
|
256
|
+
asciiPlayer.current = p;
|
|
257
|
+
syncAscii();
|
|
258
|
+
ro = new ResizeObserver(() => {
|
|
259
|
+
const rw = Math.round(host.clientWidth);
|
|
260
|
+
const rh = Math.round(host.clientHeight);
|
|
261
|
+
if (rw > 1 && rh > 1)
|
|
262
|
+
p.resize(rw, rh, Math.min(devicePixelRatio || 1, 2));
|
|
263
|
+
});
|
|
264
|
+
ro.observe(host);
|
|
265
|
+
};
|
|
266
|
+
if (v.readyState >= 1)
|
|
267
|
+
void start();
|
|
268
|
+
else
|
|
269
|
+
v.addEventListener("loadedmetadata", () => void start(), { once: true });
|
|
270
|
+
const io = new IntersectionObserver(([e]) => {
|
|
271
|
+
asciiLive.current.visible = e.isIntersecting;
|
|
272
|
+
syncAscii();
|
|
273
|
+
});
|
|
274
|
+
io.observe(host);
|
|
275
|
+
const redraw = () => asciiPlayer.current?.redraw();
|
|
276
|
+
v.addEventListener("seeked", redraw);
|
|
277
|
+
v.addEventListener("loadeddata", redraw);
|
|
278
|
+
return () => {
|
|
279
|
+
disposed = true;
|
|
280
|
+
v.removeEventListener("seeked", redraw);
|
|
281
|
+
v.removeEventListener("loadeddata", redraw);
|
|
282
|
+
ro?.disconnect();
|
|
283
|
+
io.disconnect();
|
|
284
|
+
try {
|
|
285
|
+
asciiPlayer.current?.destroy?.();
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
/* ya liberado */
|
|
289
|
+
}
|
|
290
|
+
asciiPlayer.current = null;
|
|
291
|
+
canvas.remove();
|
|
292
|
+
};
|
|
293
|
+
}, [asciiOn, src, syncAscii]);
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
asciiPlayer.current?.update(settingsRef.current());
|
|
296
|
+
}, [asciiStyle, asciiCell, asciiColor, asciiHover, asciiHoverStrength, asciiHoverRadius]);
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
asciiLive.current.playing = playing;
|
|
299
|
+
syncAscii();
|
|
300
|
+
if (!playing)
|
|
301
|
+
asciiPlayer.current?.redraw();
|
|
302
|
+
}, [playing, asciiHover, syncAscii]);
|
|
303
|
+
// Controles que se ocultan tras unos segundos sin movimiento mientras se reproduce
|
|
304
|
+
const wake = useCallback(() => {
|
|
305
|
+
setIdle(false);
|
|
306
|
+
clearTimeout(idleTimer.current);
|
|
307
|
+
idleTimer.current = setTimeout(() => setIdle(true), 2600);
|
|
308
|
+
}, []);
|
|
309
|
+
useEffect(() => () => clearTimeout(idleTimer.current), []);
|
|
310
|
+
const onKey = (e) => {
|
|
311
|
+
if (bg || embed || controls === "none")
|
|
312
|
+
return;
|
|
313
|
+
const v = video.current;
|
|
314
|
+
if (!v)
|
|
315
|
+
return;
|
|
316
|
+
const k = e.key.toLowerCase();
|
|
317
|
+
let handled = true;
|
|
318
|
+
if (k === " " || k === "k")
|
|
319
|
+
toggle();
|
|
320
|
+
else if (k === "arrowleft")
|
|
321
|
+
seek(v.currentTime - 5);
|
|
322
|
+
else if (k === "arrowright")
|
|
323
|
+
seek(v.currentTime + 5);
|
|
324
|
+
else if (k === "j")
|
|
325
|
+
seek(v.currentTime - 10);
|
|
326
|
+
else if (k === "l")
|
|
327
|
+
seek(v.currentTime + 10);
|
|
328
|
+
else if (k === "arrowup")
|
|
329
|
+
setVol(Math.min(1, v.volume + 0.1));
|
|
330
|
+
else if (k === "arrowdown")
|
|
331
|
+
setVol(Math.max(0, v.volume - 0.1));
|
|
332
|
+
else if (k === "m")
|
|
333
|
+
toggleMute();
|
|
334
|
+
else if (k === "f")
|
|
335
|
+
toggleFull();
|
|
336
|
+
else if (k === "c")
|
|
337
|
+
toggleCap();
|
|
338
|
+
else if (/^[0-9]$/.test(k))
|
|
339
|
+
seek((v.duration || 0) * (parseInt(k, 10) / 10));
|
|
340
|
+
else
|
|
341
|
+
handled = false;
|
|
342
|
+
if (handled) {
|
|
343
|
+
e.preventDefault();
|
|
344
|
+
wake();
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
const cls = ["ui-vid", bg ? "ui-vid--bg" : `ui-surface ${vcls(variant)}`, playing ? "is-playing" : "", started ? "is-started" : "", idle && playing ? "is-idle" : "", full ? "is-full" : "", failed ? "is-failed" : "", scanlines ? "has-scan" : "", className].filter(Boolean).join(" ");
|
|
348
|
+
const style = { "--vid-ratio": ratio };
|
|
349
|
+
const pos = poster ? resolveImage(poster) : undefined;
|
|
350
|
+
const canPip = typeof document !== "undefined" && "pictureInPictureEnabled" in document && document.pictureInPictureEnabled;
|
|
351
|
+
if (embed) {
|
|
352
|
+
return (_jsxs("div", { className: `${cls} ui-vid--embed`, style: style, children: [_jsx(EmbedFrame, { embed: embed, poster: poster, title: title }), ascii && _jsx("span", { className: "ui-vid__tag ui-vid__tag--note", children: "El efecto ASCII no aplica a enlaces de YouTube/Vimeo" })] }));
|
|
353
|
+
}
|
|
354
|
+
return (_jsxs("div", { ref: wrap, className: cls, style: style, tabIndex: bg ? -1 : 0, role: bg ? undefined : "group", "aria-label": bg ? undefined : title, "aria-hidden": bg ? true : undefined, onKeyDown: onKey, onMouseMove: bg ? undefined : wake, onMouseLeave: () => playing && setIdle(true), children: [_jsx("video", { ref: video, className: "ui-vid__video", crossOrigin: asciiOn ? "anonymous" : undefined, src: src, poster: pos, muted: isMuted, loop: loop || bg, playsInline: true, preload: autoplay || bg ? "auto" : "metadata", onClick: bg || controls === "none" ? undefined : toggle, onPlay: () => {
|
|
355
|
+
setPlaying(true);
|
|
356
|
+
setStarted(true);
|
|
357
|
+
wake();
|
|
358
|
+
}, onPause: () => setPlaying(false), onEnded: () => setIdle(false), onLoadedMetadata: (e) => {
|
|
359
|
+
setDuration(e.currentTarget.duration);
|
|
360
|
+
if (startAt > 0)
|
|
361
|
+
e.currentTarget.currentTime = startAt;
|
|
362
|
+
const t = e.currentTarget.textTracks[0];
|
|
363
|
+
if (t)
|
|
364
|
+
t.mode = "hidden";
|
|
365
|
+
}, onTimeUpdate: (e) => setTime(e.currentTarget.currentTime), onProgress: (e) => {
|
|
366
|
+
const v = e.currentTarget;
|
|
367
|
+
if (v.buffered.length && v.duration)
|
|
368
|
+
setBuffered(v.buffered.end(v.buffered.length - 1) / v.duration);
|
|
369
|
+
}, onVolumeChange: (e) => {
|
|
370
|
+
setIsMuted(e.currentTarget.muted);
|
|
371
|
+
setVolume(e.currentTarget.volume);
|
|
372
|
+
}, onRateChange: (e) => setSpeed(e.currentTarget.playbackRate), onError: () => {
|
|
373
|
+
// Con CORS activado un servidor que no lo permite hace fallar la carga: se reintenta sin ASCII antes de dar el error
|
|
374
|
+
if (asciiOn)
|
|
375
|
+
setAsciiFailed(true);
|
|
376
|
+
else
|
|
377
|
+
setFailed(true);
|
|
378
|
+
}, children: cap?.[0] && _jsx("track", { kind: "subtitles", src: cap[0], srcLang: cap[1] || "es", label: cap[2] || "Subtítulos" }) }, `${src}-${asciiOn}`), asciiOn && _jsx("div", { ref: asciiHost, className: `ui-vid__ascii ${asciiHover !== "none" ? "has-hover" : ""}`, "aria-hidden": true, onClick: bg || controls === "none" ? undefined : toggle }), ascii && asciiFailed && !embed && _jsx("span", { className: "ui-vid__tag ui-vid__tag--top", children: "ASCII no disponible: el servidor del v\u00EDdeo no permite CORS" }), scanlines && _jsx("span", { className: "ui-vid__scan", "aria-hidden": true }), failed && (_jsxs("div", { className: "ui-vid__err", role: "alert", children: [_jsx("strong", { children: "No se pudo cargar el v\u00EDdeo" }), _jsx("span", { children: src })] })), !bg && !failed && !started && (_jsxs("button", { type: "button", className: "ui-vid__cover", onClick: play, "aria-label": `Reproducir: ${title}`, children: [_jsx("span", { className: "ui-vid__big", "aria-hidden": true, children: renderGlyph("icon:play", 34) }), _jsx("span", { className: "ui-vid__title", children: title })] })), showControls && started && (_jsxs("div", { className: "ui-vid__ctl", role: "toolbar", "aria-label": "Controles del v\u00EDdeo", children: [_jsx("button", { type: "button", className: "ui-vid__btn", onClick: toggle, "aria-label": playing ? "Pausar" : "Reproducir", children: renderGlyph(playing ? "icon:pause" : "icon:play", 18) }), _jsxs("span", { className: "ui-vid__time", children: [fmt(time), controls === "full" && _jsxs(_Fragment, { children: [" / ", fmt(duration)] })] }), _jsx(Bar, { className: "ui-vid__seek", value: duration ? time / duration : 0, buffered: buffered, onChange: (r) => seek(r * duration), label: "Progreso", valueText: `${fmt(time)} de ${fmt(duration)}`, step: 0.02 }), controls === "full" && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: `ui-vid__btn ${isMuted ? "is-off" : ""}`, onClick: toggleMute, "aria-label": isMuted ? "Activar sonido" : "Silenciar", "aria-pressed": isMuted, children: renderGlyph("icon:volume", 18) }), _jsx(Bar, { className: "ui-vid__vol", value: isMuted ? 0 : volume, onChange: setVol, label: "Volumen", step: 0.1 }), _jsxs("button", { type: "button", className: "ui-vid__btn ui-vid__btn--txt", onClick: () => video.current && (video.current.playbackRate = SPEEDS[(SPEEDS.indexOf(speed) + 1) % SPEEDS.length]), "aria-label": `Velocidad ${speed}x`, children: [speed, "x"] }), cap?.[0] && (_jsx("button", { type: "button", className: `ui-vid__btn ui-vid__btn--txt ${capOn ? "is-on" : ""}`, onClick: toggleCap, "aria-label": "Subt\u00EDtulos", "aria-pressed": capOn, children: "CC" })), canPip && (_jsx("button", { type: "button", className: "ui-vid__btn ui-vid__btn--txt", onClick: () => void (document.pictureInPictureElement ? document.exitPictureInPicture() : video.current?.requestPictureInPicture()), "aria-label": "Imagen en imagen", children: "PiP" }))] })), _jsx("button", { type: "button", className: "ui-vid__btn", onClick: toggleFull, "aria-label": full ? "Salir de pantalla completa" : "Pantalla completa", children: _jsx("svg", { width: "18", height: "18", viewBox: "0 0 18 18", shapeRendering: "crispEdges", fill: "currentColor", "aria-hidden": true, children: full ? _jsx("path", { d: "M6 2h2v6H2V6h4V2Zm4 0h2v4h4v2h-6V2ZM2 10h6v6H6v-4H2v-2Zm8 0h6v2h-4v4h-2v-6Z" }) : _jsx("path", { d: "M2 2h6v2H4v4H2V2Zm8 0h6v6h-2V4h-4V2ZM2 10h2v4h4v2H2v-6Zm12 0h2v6h-6v-2h4v-4Z" }) }) })] }))] }));
|
|
379
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Fondos de tarjeta compartidos por todas las tarjetas del kit (Panel, PricingCard, BlogCard, Testimonial, AsciiCard…).
|
|
4
|
+
* La variante (`glass`, `solid`…) decide borde, radio, sombra y tipografía; el `fill` decide QUÉ hay detrás del
|
|
5
|
+
* contenido. Son capas independientes: cualquier fill con cualquier variante. Todo sale de los tokens.
|
|
6
|
+
*
|
|
7
|
+
* - `surface` el fondo propio de la variante (lo de siempre)
|
|
8
|
+
* - `tint` el acento mezclado con el fondo del tema
|
|
9
|
+
* - `gradient` degradado suave de `--acc` a `--acc2`
|
|
10
|
+
* - `accent` invertida: fondo de acento y texto con el color de fondo (los hijos heredan la paleta invertida)
|
|
11
|
+
* - `pattern` trama de CSS (`pattern`: puntos, rejilla, diagonales, ondas, damero, rayos) sobre un tinte muy leve
|
|
12
|
+
* - `image` imagen de fondo (`image`: URL o `gen:N`) con un velo del color de fondo para que el texto se lea
|
|
13
|
+
*/
|
|
14
|
+
export declare const CARD_FILLS: readonly ["surface", "tint", "gradient", "accent", "pattern", "image"];
|
|
15
|
+
export type CardFill = (typeof CARD_FILLS)[number];
|
|
16
|
+
export declare const CARD_PATTERNS: readonly ["dots", "grid", "diagonal", "waves", "checker", "rays"];
|
|
17
|
+
export type CardPattern = (typeof CARD_PATTERNS)[number];
|
|
18
|
+
/** qué acento usa la tarjeta: el principal o el secundario (alternarlos da variedad en una rejilla) */
|
|
19
|
+
export type CardTone = "acc" | "acc2";
|
|
20
|
+
export type FillOptions = {
|
|
21
|
+
fill?: CardFill;
|
|
22
|
+
pattern?: CardPattern;
|
|
23
|
+
tone?: CardTone;
|
|
24
|
+
image?: string;
|
|
25
|
+
};
|
|
26
|
+
/** Clases y estilo inline de un fondo de tarjeta. Se aplican al raíz (el mismo que lleva `ui-surface`). */
|
|
27
|
+
export declare function fillProps({ fill, pattern, tone, image }: FillOptions): {
|
|
28
|
+
className: string;
|
|
29
|
+
style?: CSSProperties;
|
|
30
|
+
};
|
|
31
|
+
/** Elige un valor de una lista a partir de un índice: para que cada tarjeta de una rejilla sea distinta sin configurarla. */
|
|
32
|
+
export declare const pick: <T>(list: readonly T[], i: number) => T;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { resolveImage } from "../../lib/ui/placeholder.js";
|
|
2
|
+
/**
|
|
3
|
+
* Fondos de tarjeta compartidos por todas las tarjetas del kit (Panel, PricingCard, BlogCard, Testimonial, AsciiCard…).
|
|
4
|
+
* La variante (`glass`, `solid`…) decide borde, radio, sombra y tipografía; el `fill` decide QUÉ hay detrás del
|
|
5
|
+
* contenido. Son capas independientes: cualquier fill con cualquier variante. Todo sale de los tokens.
|
|
6
|
+
*
|
|
7
|
+
* - `surface` el fondo propio de la variante (lo de siempre)
|
|
8
|
+
* - `tint` el acento mezclado con el fondo del tema
|
|
9
|
+
* - `gradient` degradado suave de `--acc` a `--acc2`
|
|
10
|
+
* - `accent` invertida: fondo de acento y texto con el color de fondo (los hijos heredan la paleta invertida)
|
|
11
|
+
* - `pattern` trama de CSS (`pattern`: puntos, rejilla, diagonales, ondas, damero, rayos) sobre un tinte muy leve
|
|
12
|
+
* - `image` imagen de fondo (`image`: URL o `gen:N`) con un velo del color de fondo para que el texto se lea
|
|
13
|
+
*/
|
|
14
|
+
export const CARD_FILLS = ["surface", "tint", "gradient", "accent", "pattern", "image"];
|
|
15
|
+
export const CARD_PATTERNS = ["dots", "grid", "diagonal", "waves", "checker", "rays"];
|
|
16
|
+
/** Clases y estilo inline de un fondo de tarjeta. Se aplican al raíz (el mismo que lleva `ui-surface`). */
|
|
17
|
+
export function fillProps({ fill = "surface", pattern = "dots", tone = "acc", image }) {
|
|
18
|
+
const cls = ["ui-cfill", `ui-cfill--${fill}`];
|
|
19
|
+
if (fill === "pattern")
|
|
20
|
+
cls.push(`ui-pat--${pattern}`);
|
|
21
|
+
if (tone === "acc2")
|
|
22
|
+
cls.push("ui-tone--acc2");
|
|
23
|
+
const style = fill === "image" && image ? { "--fill-img": `url("${resolveImage(image)}")` } : undefined;
|
|
24
|
+
return { className: cls.join(" "), style };
|
|
25
|
+
}
|
|
26
|
+
/** Elige un valor de una lista a partir de un índice: para que cada tarjeta de una rejilla sea distinta sin configurarla. */
|
|
27
|
+
export const pick = (list, i) => list[((i % list.length) + list.length) % list.length];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color semántico común a todo el kit: la misma palabra y el mismo color en Button, Badge, Alert, TextField, Toast y
|
|
3
|
+
* Modal. Las clases `.ui-intent--<intent>` (ui-kit.css) fijan `--tone` (lo que leen insignias, avisos y campos) y
|
|
4
|
+
* `--acc` (lo que leen los botones y lo que va dentro), con los tokens semánticos `--ok --info --warn --bad`.
|
|
5
|
+
*
|
|
6
|
+
* - `accent` el acento del tema (por defecto en botones e insignias)
|
|
7
|
+
* - `neutral` el color del texto: sin connotación
|
|
8
|
+
* - `success` bien hecho, guardado, disponible
|
|
9
|
+
* - `info` dato o aviso informativo
|
|
10
|
+
* - `warning` atención: revisar antes de seguir
|
|
11
|
+
* - `danger` error, fallo o acción destructiva
|
|
12
|
+
*/
|
|
13
|
+
export declare const INTENTS: readonly ["accent", "neutral", "success", "info", "warning", "danger"];
|
|
14
|
+
export type Intent = (typeof INTENTS)[number];
|
|
15
|
+
/** Nombres antiguos que se siguen aceptando (Badge usaba `warn`; Alert, TextField y Toast usaban `error`). */
|
|
16
|
+
export type LegacyIntent = "warn" | "error";
|
|
17
|
+
/** Normaliza un intent que puede venir con un nombre antiguo. */
|
|
18
|
+
export declare function toIntent(value: Intent | LegacyIntent | undefined, fallback: Intent): Intent;
|
|
19
|
+
/** Clase CSS del intent. */
|
|
20
|
+
export declare const intentCls: (intent: Intent) => string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color semántico común a todo el kit: la misma palabra y el mismo color en Button, Badge, Alert, TextField, Toast y
|
|
3
|
+
* Modal. Las clases `.ui-intent--<intent>` (ui-kit.css) fijan `--tone` (lo que leen insignias, avisos y campos) y
|
|
4
|
+
* `--acc` (lo que leen los botones y lo que va dentro), con los tokens semánticos `--ok --info --warn --bad`.
|
|
5
|
+
*
|
|
6
|
+
* - `accent` el acento del tema (por defecto en botones e insignias)
|
|
7
|
+
* - `neutral` el color del texto: sin connotación
|
|
8
|
+
* - `success` bien hecho, guardado, disponible
|
|
9
|
+
* - `info` dato o aviso informativo
|
|
10
|
+
* - `warning` atención: revisar antes de seguir
|
|
11
|
+
* - `danger` error, fallo o acción destructiva
|
|
12
|
+
*/
|
|
13
|
+
export const INTENTS = ["accent", "neutral", "success", "info", "warning", "danger"];
|
|
14
|
+
/** Normaliza un intent que puede venir con un nombre antiguo. */
|
|
15
|
+
export function toIntent(value, fallback) {
|
|
16
|
+
if (value === "warn")
|
|
17
|
+
return "warning";
|
|
18
|
+
if (value === "error")
|
|
19
|
+
return "danger";
|
|
20
|
+
return value ?? fallback;
|
|
21
|
+
}
|
|
22
|
+
/** Clase CSS del intent. */
|
|
23
|
+
export const intentCls = (intent) => `ui-intent--${intent}`;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* =====================================================================
|
|
2
|
+
Punto de entrada único del CSS del kit. En un proyecto nuevo basta con:
|
|
3
|
+
|
|
4
|
+
// app/layout.tsx
|
|
5
|
+
import "@/components/ui/styles/kit.css";
|
|
6
|
+
|
|
7
|
+
1) tokens por defecto (tema neutro oscuro) — con :where() para que cualquier tema
|
|
8
|
+
del proyecto los sobrescriba sin pelear por especificidad ni por orden de carga
|
|
9
|
+
2) sistema de estilos --s-* y componentes (ui-kit.css)
|
|
10
|
+
3) carruseles y galerías sobre Swiper (ui-carousel.css) y reproductor de vídeo (ui-video.css)
|
|
11
|
+
===================================================================== */
|
|
12
|
+
@import "./ui-kit.css";
|
|
13
|
+
@import "./ui-carousel.css";
|
|
14
|
+
@import "./ui-video.css";
|
|
15
|
+
|
|
16
|
+
/* Mismos valores que `neutralTokens` en lib/ui/tokens.ts */
|
|
17
|
+
:where(:root) {
|
|
18
|
+
--bg: #07080f;
|
|
19
|
+
--fg: #e8ecf4;
|
|
20
|
+
--mut: #9aa3b5;
|
|
21
|
+
--acc: #7cc4ff;
|
|
22
|
+
--acc2: #c084fc;
|
|
23
|
+
--card: rgba(255, 255, 255, 0.06);
|
|
24
|
+
--ln: rgba(255, 255, 255, 0.16);
|
|
25
|
+
--r: 14px;
|
|
26
|
+
/* colores semánticos (intent.ts); .ui-s los repite para las superficies */
|
|
27
|
+
--ok: #3ddc84;
|
|
28
|
+
--info: #4cb8ff;
|
|
29
|
+
--warn: #ffb020;
|
|
30
|
+
--bad: #ff5470;
|
|
31
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/* =====================================================================
|
|
2
|
+
Carrusel y galerías (Swiper). Prefijos ui-car (Carousel) y ui-gal (ImageGallery).
|
|
3
|
+
Solo consumen tokens (--bg --fg --acc --ln …) y las variables --s-* de la variante:
|
|
4
|
+
los controles de Swiper toman borde, radio y sombra del estilo activo, como cualquier otro componente.
|
|
5
|
+
===================================================================== */
|
|
6
|
+
|
|
7
|
+
.ui-car, .ui-gal {
|
|
8
|
+
--swiper-navigation-size: 18px;
|
|
9
|
+
--swiper-navigation-color: var(--acc);
|
|
10
|
+
--swiper-theme-color: var(--acc);
|
|
11
|
+
--swiper-pagination-color: var(--acc);
|
|
12
|
+
--swiper-pagination-bullet-inactive-color: var(--fg);
|
|
13
|
+
--swiper-pagination-bullet-inactive-opacity: 0.35;
|
|
14
|
+
--swiper-pagination-bullet-border-radius: min(var(--s-r, 0px), 3px);
|
|
15
|
+
--swiper-pagination-bullet-width: 12px;
|
|
16
|
+
--swiper-pagination-bullet-height: 12px;
|
|
17
|
+
--swiper-pagination-bullet-horizontal-gap: 5px;
|
|
18
|
+
--swiper-pagination-progressbar-bg-color: var(--ln);
|
|
19
|
+
--swiper-pagination-progressbar-size: 6px;
|
|
20
|
+
width: 100%; min-width: 0; margin-inline: auto; color: var(--fg); font-family: var(--s-ff, inherit);
|
|
21
|
+
}
|
|
22
|
+
.ui-car .swiper, .ui-gal .swiper { width: 100%; min-width: 0; }
|
|
23
|
+
|
|
24
|
+
/* Flechas: cuadros con la superficie del estilo (borde, radio y sombra de --s-*) */
|
|
25
|
+
.ui-car .swiper-button-prev, .ui-car .swiper-button-next,
|
|
26
|
+
.ui-gal .swiper-button-prev, .ui-gal .swiper-button-next {
|
|
27
|
+
width: 40px; height: 40px; margin-top: -20px;
|
|
28
|
+
background: var(--s-bg, var(--card)); border: var(--s-bd, 1px solid var(--ln)); border-radius: var(--s-r, 0); box-shadow: var(--s-sh, none);
|
|
29
|
+
-webkit-backdrop-filter: var(--s-bf, none); backdrop-filter: var(--s-bf, none);
|
|
30
|
+
padding: 10px; transition: none;
|
|
31
|
+
}
|
|
32
|
+
.ui-car .swiper-button-prev:hover, .ui-car .swiper-button-next:hover,
|
|
33
|
+
.ui-gal .swiper-button-prev:hover, .ui-gal .swiper-button-next:hover { background: var(--acc); color: var(--bg); }
|
|
34
|
+
.ui-car .swiper-button-prev:focus-visible, .ui-car .swiper-button-next:focus-visible,
|
|
35
|
+
.ui-gal .swiper-button-prev:focus-visible, .ui-gal .swiper-button-next:focus-visible { outline: 2px solid var(--fg); outline-offset: 3px; }
|
|
36
|
+
.ui-car .swiper-pagination-fraction, .ui-gal .swiper-pagination-fraction { color: var(--fg); font-weight: 700; letter-spacing: 0.1em; }
|
|
37
|
+
.ui-car .swiper-pagination-progressbar, .ui-gal .swiper-pagination-progressbar { border-radius: 0; }
|
|
38
|
+
.ui-car .swiper-pagination-progressbar-fill, .ui-gal .swiper-pagination-progressbar-fill { border-radius: 0; }
|
|
39
|
+
.ui-car .swiper-pagination-bullet, .ui-gal .swiper-pagination-bullet { transition: none; }
|
|
40
|
+
|
|
41
|
+
/* ---------- Carousel ---------- */
|
|
42
|
+
.ui-car { max-width: var(--car-max, 980px); }
|
|
43
|
+
/* Holgura para la sombra dura de las tarjetas, que un overflow:hidden recortaría, y hueco abajo para puntos y flechas */
|
|
44
|
+
.ui-car .swiper { padding: 4px 10px 56px 4px; }
|
|
45
|
+
.ui-car .swiper-slide { height: auto; display: flex; }
|
|
46
|
+
.ui-car .swiper-slide > * { width: 100%; min-width: 0; }
|
|
47
|
+
.ui-car .swiper .swiper-button-prev, .ui-car .swiper .swiper-button-next { top: auto; bottom: 0; margin-top: 0; }
|
|
48
|
+
.ui-car .swiper .swiper-button-prev { left: 4px; }
|
|
49
|
+
.ui-car .swiper .swiper-button-next { right: 10px; }
|
|
50
|
+
.ui-car .swiper-pagination { --swiper-pagination-bottom: 14px; bottom: 14px; }
|
|
51
|
+
.ui-car .swiper-pagination-fraction { bottom: 10px; }
|
|
52
|
+
.ui-car--cards .swiper { max-width: 440px; }
|
|
53
|
+
.ui-car--fade .swiper-slide { pointer-events: none; }
|
|
54
|
+
.ui-car--fade .swiper-slide-active { pointer-events: auto; }
|
|
55
|
+
|
|
56
|
+
/* Cinta continua: sin controles, avance lineal, diapositivas de ancho propio */
|
|
57
|
+
.ui-car--ticker .swiper { padding: 4px 0; }
|
|
58
|
+
.ui-car--ticker .swiper-wrapper { transition-timing-function: linear !important; }
|
|
59
|
+
.ui-car--ticker .swiper-slide { width: auto; padding-right: 0; }
|
|
60
|
+
|
|
61
|
+
/* ---------- ImageGallery ---------- */
|
|
62
|
+
.ui-gal { max-width: var(--gal-max, 860px); }
|
|
63
|
+
.ui-gal .swiper { padding: 4px 10px 10px 4px; }
|
|
64
|
+
.ui-gal .swiper-slide { height: auto; }
|
|
65
|
+
|
|
66
|
+
.ui-gal__shot { position: relative; margin: 0; width: 100%; aspect-ratio: var(--gal-ratio, 4 / 3); overflow: hidden; background: var(--s-bg, var(--card)); }
|
|
67
|
+
.ui-gal__shot img { display: block; width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; user-select: none; }
|
|
68
|
+
.ui-gal__shot .swiper-zoom-container { width: 100%; height: 100%; }
|
|
69
|
+
.ui-gal__shot--fill { aspect-ratio: auto; height: 100%; }
|
|
70
|
+
|
|
71
|
+
/* Pie de foto: bloque duro sobre la imagen, como los carteles de la landing */
|
|
72
|
+
.ui-gal__cap { position: absolute; left: 10px; bottom: 10px; max-width: calc(100% - 20px); display: grid; gap: 2px; padding: 6px 10px; background: var(--bg); color: var(--fg); border: 2px solid var(--fg); font-size: 12px; line-height: 1.3; text-align: left; text-shadow: none; text-transform: none; pointer-events: none; }
|
|
73
|
+
.ui-gal__cap strong { color: var(--acc); font-size: 13px; letter-spacing: 0.06em; }
|
|
74
|
+
|
|
75
|
+
/* thumbs: imagen grande + tira */
|
|
76
|
+
.ui-gal--thumbs .ui-gal__main { padding-bottom: 8px; }
|
|
77
|
+
.ui-gal--thumbs .ui-gal__thumbs { padding-top: 6px; padding-bottom: 10px; }
|
|
78
|
+
.ui-gal--thumbs .ui-gal__thumbs .swiper-slide { cursor: pointer; opacity: 0.5; }
|
|
79
|
+
.ui-gal--thumbs .ui-gal__thumbs .swiper-slide-thumb-active { opacity: 1; }
|
|
80
|
+
.ui-gal--thumbs .ui-gal__thumbs .swiper-slide-thumb-active .ui-gal__thumb { outline: 3px solid var(--acc); outline-offset: 1px; }
|
|
81
|
+
.ui-gal__thumb { margin: 0; aspect-ratio: 4 / 3; overflow: hidden; }
|
|
82
|
+
.ui-gal__thumb img { display: block; width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; }
|
|
83
|
+
|
|
84
|
+
/* coverflow y creative: 3D, con holgura vertical para el giro */
|
|
85
|
+
.ui-gal--coverflow .swiper { padding: 24px 0 44px; }
|
|
86
|
+
.ui-gal--coverflow .swiper-slide { width: min(62%, 380px); }
|
|
87
|
+
.ui-gal--creative .swiper { padding: 10px 20px 44px 10px; overflow: visible; }
|
|
88
|
+
.ui-gal--creative, .ui-gal--cube, .ui-gal--cards { overflow: visible; }
|
|
89
|
+
|
|
90
|
+
/* cards y cube: una sola pieza centrada */
|
|
91
|
+
.ui-gal--cards .swiper { width: min(100%, 360px); padding: 10px 0 24px; overflow: visible; }
|
|
92
|
+
.ui-gal--cube .swiper { width: min(100%, 420px); padding: 14px 0 46px; overflow: visible; }
|
|
93
|
+
.ui-gal--cube .swiper-slide { background: var(--bg); }
|
|
94
|
+
|
|
95
|
+
/* fade: pantalla completa dentro del bloque */
|
|
96
|
+
.ui-gal--fade .swiper-pagination-fraction { bottom: 14px; left: auto; right: 14px; width: auto; padding: 3px 9px; background: var(--bg); border: 2px solid var(--fg); }
|
|
97
|
+
|
|
98
|
+
/* filmstrip: mismo alto, ancho propio (--w es la proporción ancho/alto) */
|
|
99
|
+
.ui-gal--filmstrip { --gal-h: 260px; }
|
|
100
|
+
.ui-gal--filmstrip .swiper { padding-bottom: 26px; }
|
|
101
|
+
.ui-gal--filmstrip .swiper-slide { width: calc(var(--gal-h) * var(--w, 1.2)); height: var(--gal-h); }
|
|
102
|
+
.ui-gal--filmstrip .swiper-pagination-progressbar { top: auto; bottom: 0; }
|
|
103
|
+
|
|
104
|
+
/* mosaic: dos filas; el alto lo fija el contenedor porque Swiper Grid reparte la altura entre filas */
|
|
105
|
+
.ui-gal--mosaic { max-width: 980px; }
|
|
106
|
+
.ui-gal--mosaic .swiper { height: 440px; padding-bottom: 34px; }
|
|
107
|
+
.ui-gal--mosaic .swiper-slide { height: calc((100% - 12px) / 2); }
|
|
108
|
+
.ui-gal--mosaic .swiper-pagination { bottom: 4px; }
|
|
109
|
+
|
|
110
|
+
@media (max-width: 720px) {
|
|
111
|
+
.ui-gal--filmstrip { --gal-h: 200px; }
|
|
112
|
+
.ui-gal--mosaic .swiper { height: 380px; }
|
|
113
|
+
}
|