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,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { vcls } from "./variants.js";
|
|
5
|
+
/**
|
|
6
|
+
* Desplegable con estilo propio, no un `<select>` nativo: un `<select>` real no se puede reskinar por
|
|
7
|
+
* dentro (la lista abierta la pinta el sistema operativo), así que esto es un botón + una lista
|
|
8
|
+
* absoluta, igual de accesible (`aria-haspopup`, `role="listbox"`) pero con los mismos 7 estilos que
|
|
9
|
+
* el resto del kit, abierto o cerrado.
|
|
10
|
+
*/
|
|
11
|
+
export default function Select({ label = "País", options = "España, Francia, Portugal, Italia, Alemania", defaultValue = "", placeholder = "Elige una opción", size = "md", variant = "glass", className = "", }) {
|
|
12
|
+
const list = useMemo(() => options.split(",").map((s) => s.trim()).filter(Boolean), [options]);
|
|
13
|
+
const [value, setValue] = useState(defaultValue);
|
|
14
|
+
const [open, setOpen] = useState(false);
|
|
15
|
+
const root = useRef(null);
|
|
16
|
+
useEffect(() => setValue(defaultValue), [defaultValue]);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!open)
|
|
19
|
+
return;
|
|
20
|
+
const onDoc = (e) => {
|
|
21
|
+
if (root.current && !root.current.contains(e.target))
|
|
22
|
+
setOpen(false);
|
|
23
|
+
};
|
|
24
|
+
document.addEventListener("mousedown", onDoc);
|
|
25
|
+
return () => document.removeEventListener("mousedown", onDoc);
|
|
26
|
+
}, [open]);
|
|
27
|
+
return (_jsxs("div", { ref: root, className: `ui-select ui-select--${size} ${className}`, children: [label && _jsx("span", { className: "ui-select__label", children: label }), _jsxs("button", { type: "button", className: `ui-select__trigger ui-surface ${vcls(variant)}`, "aria-haspopup": "listbox", "aria-expanded": open, onClick: () => setOpen((v) => !v), children: [_jsx("span", { className: value ? undefined : "ui-select__placeholder", children: value || placeholder }), _jsx("span", { className: "ui-select__mark", "aria-hidden": true, children: "\u25BE" })] }), open && (_jsx("ul", { className: `ui-select__list ui-surface ${vcls(variant)}`, role: "listbox", children: list.map((o) => (_jsx("li", { children: _jsx("button", { type: "button", role: "option", "aria-selected": o === value, className: `ui-select__opt ${o === value ? "is-selected" : ""}`, onClick: () => {
|
|
28
|
+
setValue(o);
|
|
29
|
+
setOpen(false);
|
|
30
|
+
}, children: o }) }, o))) }))] }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Tone } from "./variants.js";
|
|
2
|
+
export type SkeletonProps = {
|
|
3
|
+
kind?: "text" | "card" | "avatar" | "table";
|
|
4
|
+
lines?: number;
|
|
5
|
+
tone?: Tone;
|
|
6
|
+
className?: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Placeholder de carga con barrido animado. Deliberadamente sin `--s-*`: un esqueleto no lleva el
|
|
10
|
+
* estilo de marca del contenido que va a sustituir, solo el tono de color (`tone`).
|
|
11
|
+
*/
|
|
12
|
+
export default function Skeleton({ kind, lines, tone, className }: SkeletonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { tcls } from "./variants.js";
|
|
3
|
+
/**
|
|
4
|
+
* Placeholder de carga con barrido animado. Deliberadamente sin `--s-*`: un esqueleto no lleva el
|
|
5
|
+
* estilo de marca del contenido que va a sustituir, solo el tono de color (`tone`).
|
|
6
|
+
*/
|
|
7
|
+
export default function Skeleton({ kind = "text", lines = 3, tone = "mut", className = "" }) {
|
|
8
|
+
if (kind === "avatar") {
|
|
9
|
+
return _jsx("div", { className: `ui-skel ui-skel--avatar ${tcls(tone)} ${className}`, "aria-hidden": true });
|
|
10
|
+
}
|
|
11
|
+
if (kind === "card") {
|
|
12
|
+
return (_jsxs("div", { className: `ui-skel-card ${className}`, "aria-hidden": true, children: [_jsx("div", { className: `ui-skel ui-skel--block ${tcls(tone)}` }), _jsx("div", { className: `ui-skel ui-skel--line ${tcls(tone)}`, style: { width: "70%" } }), _jsx("div", { className: `ui-skel ui-skel--line ${tcls(tone)}`, style: { width: "45%" } })] }));
|
|
13
|
+
}
|
|
14
|
+
if (kind === "table") {
|
|
15
|
+
return (_jsx("div", { className: `ui-skel-table ${className}`, "aria-hidden": true, children: Array.from({ length: Math.max(2, lines) }, (_, i) => (_jsx("div", { className: `ui-skel ui-skel--row ${tcls(tone)}` }, i))) }));
|
|
16
|
+
}
|
|
17
|
+
return (_jsx("div", { className: `ui-skel-lines ${className}`, "aria-hidden": true, children: Array.from({ length: Math.max(1, lines) }, (_, i) => (_jsx("div", { className: `ui-skel ui-skel--line ${tcls(tone)}`, style: { width: i === Math.max(1, lines) - 1 ? "60%" : "100%" } }, i))) }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Variant } from "./variants.js";
|
|
2
|
+
export type SpinnerProps = {
|
|
3
|
+
kind?: "ring" | "line" | "braille" | "dots" | "bar" | "blocks";
|
|
4
|
+
label?: string;
|
|
5
|
+
/** fotogramas por segundo de las variantes de texto */
|
|
6
|
+
fps?: number;
|
|
7
|
+
/** tamaño en px (el texto va al 70 %) */
|
|
8
|
+
fontSize?: number;
|
|
9
|
+
/** @deprecated usa `fontSize` */
|
|
10
|
+
size?: number;
|
|
11
|
+
variant?: Variant;
|
|
12
|
+
className?: string;
|
|
13
|
+
};
|
|
14
|
+
/** Indicador de carga: anillo CSS o animaciones de texto (línea, braille, puntos, barra, bloques). */
|
|
15
|
+
export default function Spinner({ kind, label, fps, fontSize, size: legacySize, variant, className }: SpinnerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { vcls } from "./variants.js";
|
|
5
|
+
const FRAMES = {
|
|
6
|
+
line: ["|", "/", "-", "\\"],
|
|
7
|
+
braille: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
|
8
|
+
dots: ["· ", "·· ", "···", " ··", " ·", " "],
|
|
9
|
+
bar: ["[= ]", "[== ]", "[=== ]", "[==== ]", "[ ==== ]", "[ ====]", "[ ===]", "[ ==]", "[ =]", "[ ]"],
|
|
10
|
+
blocks: ["▖", "▘", "▝", "▗"],
|
|
11
|
+
};
|
|
12
|
+
/** Indicador de carga: anillo CSS o animaciones de texto (línea, braille, puntos, barra, bloques). */
|
|
13
|
+
export default function Spinner({ kind = "braille", label = "Cargando…", fps = 12, fontSize, size: legacySize, variant = "terminal", className = "" }) {
|
|
14
|
+
const size = fontSize ?? legacySize ?? 20;
|
|
15
|
+
const [i, setI] = useState(0);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (kind === "ring")
|
|
18
|
+
return;
|
|
19
|
+
const id = setInterval(() => setI((n) => n + 1), 1000 / Math.max(1, fps));
|
|
20
|
+
return () => clearInterval(id);
|
|
21
|
+
}, [kind, fps]);
|
|
22
|
+
const frames = kind === "ring" ? null : FRAMES[kind];
|
|
23
|
+
return (_jsxs("div", { className: `ui-spin ${vcls(variant)} ${className}`, role: "status", "aria-label": label, style: { fontSize: size }, children: [frames ? _jsx("span", { className: "ui-spin__frames", children: frames[i % frames.length] }) : _jsx("span", { className: "ui-spin__ring" }), label && _jsx("span", { style: { fontSize: Math.max(12, size * 0.7) }, children: label })] }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type Tone, type Variant } from "./variants.js";
|
|
3
|
+
export type SpotlightProps = {
|
|
4
|
+
/** radio del foco en px */
|
|
5
|
+
radius?: number;
|
|
6
|
+
/** intensidad del brillo, 0–100 */
|
|
7
|
+
intensity?: number;
|
|
8
|
+
/** también ilumina el borde bajo el puntero */
|
|
9
|
+
border?: boolean;
|
|
10
|
+
tone?: Extract<Tone, "acc" | "acc2" | "fg">;
|
|
11
|
+
variant?: Variant;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
};
|
|
15
|
+
/** Superficie con un foco de luz que sigue al puntero (variables CSS, sin JS por fotograma en el árbol de React). */
|
|
16
|
+
export default function Spotlight({ radius, intensity, border, tone, variant, children, className }: SpotlightProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useRef } from "react";
|
|
4
|
+
import { tcls, vcls } from "./variants.js";
|
|
5
|
+
/** Superficie con un foco de luz que sigue al puntero (variables CSS, sin JS por fotograma en el árbol de React). */
|
|
6
|
+
export default function Spotlight({ radius = 220, intensity = 30, border = true, tone = "acc", variant = "glass", children, className = "" }) {
|
|
7
|
+
const ref = useRef(null);
|
|
8
|
+
return (_jsx("div", { ref: ref, className: `ui-spot ui-surface ${border ? "ui-spot--border" : ""} ${tcls(tone)} ${vcls(variant)} ${className}`, style: { "--spot-r": `${radius}px`, "--spot-i": `${intensity}%` }, onPointerMove: (e) => {
|
|
9
|
+
const el = ref.current;
|
|
10
|
+
if (!el)
|
|
11
|
+
return;
|
|
12
|
+
const r = el.getBoundingClientRect();
|
|
13
|
+
el.style.setProperty("--mx", `${e.clientX - r.left}px`);
|
|
14
|
+
el.style.setProperty("--my", `${e.clientY - r.top}px`);
|
|
15
|
+
}, children: children }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Geometría de la hoja de Kenney "Pixel UI" (UIpackSheet_transparent.png): baldosas de 16 px con 2 px de margen. */
|
|
2
|
+
export declare const SPRITE_SHEET: {
|
|
3
|
+
readonly src: "/pixel/kenney-pixel-ui/spritesheet/sheet.png";
|
|
4
|
+
readonly tile: 16;
|
|
5
|
+
readonly gap: 2;
|
|
6
|
+
readonly width: 538;
|
|
7
|
+
readonly height: 592;
|
|
8
|
+
readonly cols: 30;
|
|
9
|
+
readonly rows: 33;
|
|
10
|
+
};
|
|
11
|
+
export type SpriteProps = {
|
|
12
|
+
/** columna de la baldosa en la hoja, empezando en 0 */
|
|
13
|
+
col: number;
|
|
14
|
+
/** fila de la baldosa en la hoja, empezando en 0 */
|
|
15
|
+
row: number;
|
|
16
|
+
/** píxeles CSS por píxel del sprite (números enteros mantienen los píxeles nítidos) */
|
|
17
|
+
scale?: number;
|
|
18
|
+
/** nombre accesible; sin él el sprite es decorativo */
|
|
19
|
+
label?: string;
|
|
20
|
+
className?: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Una baldosa 16×16 de la hoja de sprites de Kenney "Pixel UI" (CC0): flechas, casillas, punteros, botones…
|
|
24
|
+
* Son píxeles ya coloreados, no se tiñen con los tokens del tema (a diferencia de `Icon`). Las coordenadas se
|
|
25
|
+
* ven en el catálogo (`/componentes` → Pixel art → Sprite), que dibuja la hoja con su rejilla.
|
|
26
|
+
*/
|
|
27
|
+
export default function Sprite({ col, row, scale, label, className }: SpriteProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** Geometría de la hoja de Kenney "Pixel UI" (UIpackSheet_transparent.png): baldosas de 16 px con 2 px de margen. */
|
|
3
|
+
export const SPRITE_SHEET = {
|
|
4
|
+
src: "/pixel/kenney-pixel-ui/spritesheet/sheet.png",
|
|
5
|
+
tile: 16,
|
|
6
|
+
gap: 2,
|
|
7
|
+
width: 538,
|
|
8
|
+
height: 592,
|
|
9
|
+
cols: 30,
|
|
10
|
+
rows: 33,
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Una baldosa 16×16 de la hoja de sprites de Kenney "Pixel UI" (CC0): flechas, casillas, punteros, botones…
|
|
14
|
+
* Son píxeles ya coloreados, no se tiñen con los tokens del tema (a diferencia de `Icon`). Las coordenadas se
|
|
15
|
+
* ven en el catálogo (`/componentes` → Pixel art → Sprite), que dibuja la hoja con su rejilla.
|
|
16
|
+
*/
|
|
17
|
+
export default function Sprite({ col, row, scale = 2, label, className = "" }) {
|
|
18
|
+
return (_jsx("span", { className: `ui-sprite ${className}`.trim(), style: { "--sp-img": `url(${SPRITE_SHEET.src})`, "--sp-s": scale, "--sp-x": col, "--sp-y": row }, role: label ? "img" : undefined, "aria-label": label, "aria-hidden": label ? undefined : true }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Tone } from "./variants.js";
|
|
2
|
+
export type StatCounterProps = {
|
|
3
|
+
value: number;
|
|
4
|
+
suffix?: string;
|
|
5
|
+
prefix?: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
char?: string;
|
|
8
|
+
scale?: number;
|
|
9
|
+
duration?: number;
|
|
10
|
+
/** int = entero; decimal = con decimales; compact = 1,2K / 3,4M */
|
|
11
|
+
format?: "int" | "decimal" | "compact";
|
|
12
|
+
decimals?: number;
|
|
13
|
+
/** cuándo cuenta: al montar, al entrar en pantalla o al pasar el ratón */
|
|
14
|
+
trigger?: "mount" | "inview" | "hover";
|
|
15
|
+
tone?: Tone;
|
|
16
|
+
align?: "left" | "center" | "right";
|
|
17
|
+
playKey?: number;
|
|
18
|
+
className?: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Cifra grande en ASCII (fuente bitmap 7×7 de asciify-engine) que cuenta hacia arriba con GSAP.
|
|
22
|
+
* Pensada para secciones de "resultados" de una landing.
|
|
23
|
+
*/
|
|
24
|
+
export default function StatCounter({ value, suffix, prefix, label, char, scale, duration, format, decimals, trigger, tone, align, playKey, className, }: StatCounterProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { tcls } from "./variants.js";
|
|
5
|
+
const fmt = (n, format, decimals) => {
|
|
6
|
+
if (format === "decimal")
|
|
7
|
+
return n.toFixed(decimals);
|
|
8
|
+
if (format === "compact")
|
|
9
|
+
return n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : `${Math.round(n)}`;
|
|
10
|
+
return `${Math.round(n)}`;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Cifra grande en ASCII (fuente bitmap 7×7 de asciify-engine) que cuenta hacia arriba con GSAP.
|
|
14
|
+
* Pensada para secciones de "resultados" de una landing.
|
|
15
|
+
*/
|
|
16
|
+
export default function StatCounter({ value, suffix = "", prefix = "", label, char = "#", scale = 1, duration = 1.6, format = "int", decimals = 1, trigger = "mount", tone = "acc", align = "center", playKey = 0, className = "", }) {
|
|
17
|
+
const root = useRef(null);
|
|
18
|
+
const [bitmap, setBitmap] = useState(null);
|
|
19
|
+
const [shown, setShown] = useState(0);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
let live = true;
|
|
22
|
+
import("asciify-engine/core").then((m) => live && setBitmap(() => m.asciifyText));
|
|
23
|
+
return () => {
|
|
24
|
+
live = false;
|
|
25
|
+
};
|
|
26
|
+
}, []);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const el = root.current;
|
|
29
|
+
if (!el)
|
|
30
|
+
return;
|
|
31
|
+
let disposed = false;
|
|
32
|
+
const cleanups = [];
|
|
33
|
+
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
34
|
+
(async () => {
|
|
35
|
+
const { gsap } = await import("gsap");
|
|
36
|
+
if (disposed)
|
|
37
|
+
return;
|
|
38
|
+
const obj = { v: 0 };
|
|
39
|
+
const run = () => {
|
|
40
|
+
gsap.killTweensOf(obj);
|
|
41
|
+
obj.v = 0;
|
|
42
|
+
gsap.to(obj, { v: value, duration, ease: "power2.out", onUpdate: () => setShown(obj.v) });
|
|
43
|
+
};
|
|
44
|
+
if (reduce) {
|
|
45
|
+
setShown(value);
|
|
46
|
+
}
|
|
47
|
+
else if (trigger === "hover") {
|
|
48
|
+
setShown(value);
|
|
49
|
+
el.addEventListener("mouseenter", run);
|
|
50
|
+
cleanups.push(() => el.removeEventListener("mouseenter", run));
|
|
51
|
+
}
|
|
52
|
+
else if (trigger === "inview") {
|
|
53
|
+
const io = new IntersectionObserver(([e]) => e.isIntersecting && run(), { threshold: 0.4 });
|
|
54
|
+
io.observe(el);
|
|
55
|
+
cleanups.push(() => io.disconnect());
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
run();
|
|
59
|
+
}
|
|
60
|
+
cleanups.push(() => gsap.killTweensOf(obj));
|
|
61
|
+
})();
|
|
62
|
+
return () => {
|
|
63
|
+
disposed = true;
|
|
64
|
+
cleanups.reverse().forEach((f) => f());
|
|
65
|
+
};
|
|
66
|
+
}, [value, duration, trigger, playKey]);
|
|
67
|
+
const text = `${prefix}${fmt(shown, format, decimals)}${suffix}`;
|
|
68
|
+
const art = useMemo(() => (bitmap ? bitmap(text, { char, scale }) : text), [bitmap, text, char, scale]);
|
|
69
|
+
return (_jsxs("div", { ref: root, className: `ui-stat ui-stat--${align} ${tcls(tone)} ${className}`, children: [_jsx("pre", { className: "ui-stat__art", role: "img", "aria-label": `${prefix}${fmt(value, format, decimals)}${suffix} ${label ?? ""}`, children: art }), label && _jsx("span", { className: "ui-stat__label", children: label })] }));
|
|
70
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Tone } from "./variants.js";
|
|
2
|
+
export type StatsSectionProps = {
|
|
3
|
+
kicker?: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
subtitle?: string;
|
|
6
|
+
/** una cifra por línea: «valor|sufijo|etiqueta» (sufijo puede ir vacío) */
|
|
7
|
+
stats?: string;
|
|
8
|
+
tone?: Tone;
|
|
9
|
+
/** cambia el número para repetir la animación de conteo desde fuera */
|
|
10
|
+
playKey?: number;
|
|
11
|
+
className?: string;
|
|
12
|
+
};
|
|
13
|
+
/** Fila de cifras animadas (`StatCounter`) con cabecera: la sección de "resultados" típica de una landing. */
|
|
14
|
+
export default function StatsSection({ kicker, title, subtitle, stats, tone, playKey, className, }: StatsSectionProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import SectionHeader from "./SectionHeader.js";
|
|
5
|
+
import StatCounter from "./StatCounter.js";
|
|
6
|
+
// El sufijo se dibuja con la fuente bitmap 7×7 de asciify-engine (solo ASCII): símbolos como + o %
|
|
7
|
+
// funcionan bien, pero letras acentuadas o "★" no. Cualquier palabra va en la etiqueta (texto normal).
|
|
8
|
+
const DEFAULT_STATS = "500|+|Alumnos formados\n4.9||Valoración media\n12||Años de experiencia";
|
|
9
|
+
/** Fila de cifras animadas (`StatCounter`) con cabecera: la sección de "resultados" típica de una landing. */
|
|
10
|
+
export default function StatsSection({ kicker = "En números", title = "Diez años enseñando a leer el mar", subtitle = "", stats = DEFAULT_STATS, tone = "acc", playKey = 0, className = "", }) {
|
|
11
|
+
const rows = useMemo(() => stats.split("\n").map((l) => l.split("|")).filter((r) => r[0]?.trim()), [stats]);
|
|
12
|
+
return (_jsxs("section", { className: `ui-stats ${className}`, children: [_jsx(SectionHeader, { kicker: kicker, title: title, subtitle: subtitle, align: "center", variant: "minimal" }), _jsx("div", { className: "ui-stats__row", children: rows.map(([value, suffix, label], i) => (_jsx(StatCounter, { value: parseFloat(value) || 0, suffix: suffix || "", label: label, tone: tone, format: value.includes(".") ? "decimal" : "int", playKey: playKey }, (label ?? value) + i))) })] }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Variant } from "./variants.js";
|
|
2
|
+
export type StepperProps = {
|
|
3
|
+
steps?: string;
|
|
4
|
+
/** índice (0-based) del paso activo; los anteriores se marcan como hechos */
|
|
5
|
+
current?: number;
|
|
6
|
+
variant?: Variant;
|
|
7
|
+
className?: string;
|
|
8
|
+
};
|
|
9
|
+
/** Indicador de progreso por pasos (alta, checkout, onboarding…). No gestiona el contenido de cada paso. */
|
|
10
|
+
export default function Stepper({ steps, current, variant, className }: StepperProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { vcls } from "./variants.js";
|
|
3
|
+
/** Indicador de progreso por pasos (alta, checkout, onboarding…). No gestiona el contenido de cada paso. */
|
|
4
|
+
export default function Stepper({ steps = "Datos, Envío, Pago, Confirmación", current = 1, variant = "glass", className = "" }) {
|
|
5
|
+
const list = steps.split(",").map((s) => s.trim()).filter(Boolean);
|
|
6
|
+
return (_jsx("ol", { className: `ui-steps ${vcls(variant)} ${className}`, children: list.map((s, i) => (_jsxs("li", { className: `ui-steps__item ${i < current ? "is-done" : ""} ${i === current ? "is-current" : ""}`, children: [_jsx("span", { className: "ui-steps__mark", "aria-hidden": true, children: i < current ? "✓" : i + 1 }), _jsx("span", { children: s })] }, s + i))) }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Variant } from "./variants.js";
|
|
2
|
+
export type TableProps = {
|
|
3
|
+
/** CSV: la primera línea son las cabeceras; separa con «,» */
|
|
4
|
+
csv?: string;
|
|
5
|
+
striped?: boolean;
|
|
6
|
+
variant?: Variant;
|
|
7
|
+
className?: string;
|
|
8
|
+
};
|
|
9
|
+
/** Tabla de datos a partir de texto CSV. Cabecera y filas se adaptan a la variante. */
|
|
10
|
+
export default function Table({ csv, striped, variant, className }: TableProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { vcls } from "./variants.js";
|
|
3
|
+
/** Tabla de datos a partir de texto CSV. Cabecera y filas se adaptan a la variante. */
|
|
4
|
+
export default function Table({ csv = "Plan, Clases, Precio\nIniciación, 1, 35 €\nBono 5, 5, 150 €\nSurf trip, 6, 240 €", striped = true, variant = "solid", className = "" }) {
|
|
5
|
+
const rows = csv.split("\n").map((l) => l.split(",").map((c) => c.trim())).filter((r) => r.some(Boolean));
|
|
6
|
+
const [head, ...body] = rows;
|
|
7
|
+
return (_jsx("div", { className: `ui-table-wrap ui-surface ${vcls(variant)} ${className}`, style: { padding: 4 }, children: _jsxs("table", { className: `ui-table ${striped ? "ui-table--striped" : ""}`, children: [head && (_jsx("thead", { children: _jsx("tr", { children: head.map((h, i) => (_jsx("th", { children: h }, h + i))) }) })), _jsx("tbody", { children: body.map((r, i) => (_jsx("tr", { children: r.map((c, j) => (_jsx("td", { children: c }, j))) }, i))) })] }) }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Variant } from "./variants.js";
|
|
2
|
+
export type TabsProps = {
|
|
3
|
+
/** pestañas separadas por comas */
|
|
4
|
+
items?: string;
|
|
5
|
+
/** contenido de cada pestaña: una línea por pestaña */
|
|
6
|
+
content?: string;
|
|
7
|
+
defaultIndex?: number;
|
|
8
|
+
variant?: Variant;
|
|
9
|
+
className?: string;
|
|
10
|
+
};
|
|
11
|
+
/** Pestañas con teclado (←/→). El indicador de la activa cambia con la variante. */
|
|
12
|
+
export default function Tabs({ items, content, defaultIndex, variant, className, }: TabsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { vcls } from "./variants.js";
|
|
5
|
+
/** Pestañas con teclado (←/→). El indicador de la activa cambia con la variante. */
|
|
6
|
+
export default function Tabs({ items = "Resumen, Detalles, Reseñas", content = "Una vista general del producto.\nEspecificaciones técnicas y medidas.\nLo que dicen quienes ya lo usan.", defaultIndex = 0, variant = "glass", className = "", }) {
|
|
7
|
+
const tabs = useMemo(() => items.split(",").map((s) => s.trim()).filter(Boolean), [items]);
|
|
8
|
+
const panels = useMemo(() => content.split("\n"), [content]);
|
|
9
|
+
const [i, setI] = useState(defaultIndex);
|
|
10
|
+
useEffect(() => setI(Math.min(defaultIndex, Math.max(0, tabs.length - 1))), [defaultIndex, tabs.length]);
|
|
11
|
+
const cur = Math.min(i, Math.max(0, tabs.length - 1));
|
|
12
|
+
return (_jsxs("div", { className: `ui-tabs ${vcls(variant)} ${className}`, children: [_jsx("div", { role: "tablist", className: "ui-tabs__list", onKeyDown: (e) => {
|
|
13
|
+
if (e.key === "ArrowRight")
|
|
14
|
+
setI((cur + 1) % tabs.length);
|
|
15
|
+
if (e.key === "ArrowLeft")
|
|
16
|
+
setI((cur - 1 + tabs.length) % tabs.length);
|
|
17
|
+
}, children: tabs.map((t, k) => (_jsx("button", { role: "tab", "aria-selected": k === cur, tabIndex: k === cur ? 0 : -1, className: "ui-tabs__tab", onClick: () => setI(k), children: t }, t + k))) }), _jsx("div", { role: "tabpanel", className: "ui-tabs__panel", children: panels[cur] ?? "" })] }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Variant } from "./variants.js";
|
|
2
|
+
export type TeamSectionProps = {
|
|
3
|
+
kicker?: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
subtitle?: string;
|
|
6
|
+
/** una por línea: «nombre|rol» */
|
|
7
|
+
people?: string;
|
|
8
|
+
/** grid = rejilla · carousel = carrusel de Swiper */
|
|
9
|
+
layout?: "grid" | "carousel";
|
|
10
|
+
/** con layout="carousel": tarjetas visibles en pantallas anchas */
|
|
11
|
+
perView?: number;
|
|
12
|
+
/** con layout="carousel": segundos entre tarjetas (0 = manual) */
|
|
13
|
+
autoplay?: number;
|
|
14
|
+
variant?: Variant;
|
|
15
|
+
className?: string;
|
|
16
|
+
};
|
|
17
|
+
/** Cabecera de sección + grid de perfiles. Sin fotos: usa el mismo avatar de iniciales que `Testimonial`. */
|
|
18
|
+
export default function TeamSection({ kicker, title, subtitle, people, layout, perView, autoplay, variant, className }: TeamSectionProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Carousel from "./Carousel.js";
|
|
3
|
+
import SectionHeader from "./SectionHeader.js";
|
|
4
|
+
import { vcls } from "./variants.js";
|
|
5
|
+
const DEFAULT_PEOPLE = "Marta Solé|Directora y monitora\nEnzo Rial|Monitor de iniciación\nCarla Duque|Monitora y fotógrafa";
|
|
6
|
+
/** Cabecera de sección + grid de perfiles. Sin fotos: usa el mismo avatar de iniciales que `Testimonial`. */
|
|
7
|
+
export default function TeamSection({ kicker = "El equipo", title = "Quién te va a enseñar", subtitle = "", people = DEFAULT_PEOPLE, layout = "grid", perView = 3, autoplay = 0, variant = "glass", className = "" }) {
|
|
8
|
+
const rows = people.split("\n").map((l) => l.split("|")).filter((r) => r[0]?.trim());
|
|
9
|
+
const cards = rows.map(([name, role], i) => {
|
|
10
|
+
const initials = (name ?? "")
|
|
11
|
+
.split(" ")
|
|
12
|
+
.map((w) => w[0])
|
|
13
|
+
.slice(0, 2)
|
|
14
|
+
.join("")
|
|
15
|
+
.toUpperCase();
|
|
16
|
+
return (_jsxs("div", { className: `ui-team__card ui-surface ${vcls(variant)}`, children: [_jsx("span", { className: "ui-team__avatar", "aria-hidden": true, children: initials }), _jsx("strong", { children: name }), _jsx("span", { children: role })] }, name + i));
|
|
17
|
+
});
|
|
18
|
+
return (_jsxs("section", { className: `ui-team ${className}`, children: [_jsx(SectionHeader, { kicker: kicker, title: title, subtitle: subtitle, align: "center", variant: "minimal" }), layout === "carousel" ? (_jsx(Carousel, { perView: perView, autoplay: autoplay, variant: variant, label: title, children: cards })) : (_jsx("div", { className: "ui-team__grid", children: cards }))] }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Variant } from "./variants.js";
|
|
2
|
+
export type TerminalTyperProps = {
|
|
3
|
+
/** una línea por renglón; las que empiezan por el prompt se pintan con `--acc2` */
|
|
4
|
+
lines: string;
|
|
5
|
+
/** caracteres por segundo */
|
|
6
|
+
charsPerSecond?: number;
|
|
7
|
+
/** @deprecated usa `charsPerSecond` */
|
|
8
|
+
speed?: number;
|
|
9
|
+
variant?: Variant;
|
|
10
|
+
/** barra superior con botones y título */
|
|
11
|
+
chrome?: boolean;
|
|
12
|
+
title?: string;
|
|
13
|
+
cursor?: "block" | "bar" | "underscore";
|
|
14
|
+
prompt?: string;
|
|
15
|
+
loop?: boolean;
|
|
16
|
+
playKey?: number;
|
|
17
|
+
className?: string;
|
|
18
|
+
};
|
|
19
|
+
/** Terminal que escribe sola, carácter a carácter. Con movimiento reducido muestra todo de golpe. */
|
|
20
|
+
export default function TerminalTyper({ lines, charsPerSecond, speed: legacySpeed, variant, chrome, title, cursor, prompt, loop, playKey, className, }: TerminalTyperProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { vcls } from "./variants.js";
|
|
5
|
+
/** Terminal que escribe sola, carácter a carácter. Con movimiento reducido muestra todo de golpe. */
|
|
6
|
+
export default function TerminalTyper({ lines, charsPerSecond, speed: legacySpeed, variant = "terminal", chrome = true, title = "zsh — proyecto", cursor = "block", prompt = "$", loop = true, playKey = 0, className = "", }) {
|
|
7
|
+
const speed = charsPerSecond ?? legacySpeed ?? 32;
|
|
8
|
+
const rows = useMemo(() => lines.split("\n"), [lines]);
|
|
9
|
+
const total = useMemo(() => rows.reduce((n, r) => n + r.length + 6, 0), [rows]);
|
|
10
|
+
const [typed, setTyped] = useState(0);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
13
|
+
setTyped(total);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
setTyped(0);
|
|
17
|
+
let raf = 0;
|
|
18
|
+
let start = 0;
|
|
19
|
+
const tick = (now) => {
|
|
20
|
+
if (!start)
|
|
21
|
+
start = now;
|
|
22
|
+
const n = ((now - start) / 1000) * speed;
|
|
23
|
+
if (n >= total + speed * 1.2) {
|
|
24
|
+
if (loop)
|
|
25
|
+
start = now;
|
|
26
|
+
else {
|
|
27
|
+
setTyped(total);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
setTyped(Math.min(total, Math.floor(n)));
|
|
32
|
+
raf = requestAnimationFrame(tick);
|
|
33
|
+
};
|
|
34
|
+
raf = requestAnimationFrame(tick);
|
|
35
|
+
return () => cancelAnimationFrame(raf);
|
|
36
|
+
}, [total, speed, loop, playKey]);
|
|
37
|
+
let left = typed;
|
|
38
|
+
return (_jsxs("div", { className: `ui-term ui-surface ${vcls(variant)} ${className}`, children: [chrome && (_jsxs("div", { className: "ui-term__bar", "aria-hidden": true, children: [_jsx("i", { style: { background: "#ff5f57" } }), _jsx("i", { style: { background: "#febc2e" } }), _jsx("i", { style: { background: "#28c840" } }), _jsx("span", { style: { marginLeft: 8 }, children: title })] })), _jsx("pre", { className: "ui-term__body", "aria-label": lines, children: rows.map((row, i) => {
|
|
39
|
+
const n = Math.max(0, Math.min(row.length, left));
|
|
40
|
+
left -= row.length + 6;
|
|
41
|
+
const active = n > 0 && n < row.length;
|
|
42
|
+
const cls = row.startsWith(prompt) ? "ui-term__cmd" : row.startsWith("[ok]") ? "ui-term__ok" : "";
|
|
43
|
+
return (_jsxs("span", { className: `ui-term__line ${cls}`, children: [row.slice(0, n), active && _jsx("b", { className: `ui-term__cursor ui-term__cursor--${cursor}` }), "\n"] }, i));
|
|
44
|
+
}) })] }));
|
|
45
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type CardFill, type CardPattern, type CardTone } from "./fill.js";
|
|
2
|
+
import { type Variant } from "./variants.js";
|
|
3
|
+
export type TestimonialProps = {
|
|
4
|
+
quote?: string;
|
|
5
|
+
author?: string;
|
|
6
|
+
role?: string;
|
|
7
|
+
/** 0 = sin estrellas */
|
|
8
|
+
rating?: number;
|
|
9
|
+
variant?: Variant;
|
|
10
|
+
/** fondo de la tarjeta: surface · tint · gradient · accent (invertida) · pattern · image (ver fill.ts) */
|
|
11
|
+
fill?: CardFill;
|
|
12
|
+
pattern?: CardPattern;
|
|
13
|
+
tone?: CardTone;
|
|
14
|
+
/** con fill="image": URL/ruta o `gen:N` */
|
|
15
|
+
image?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
};
|
|
18
|
+
/** Testimonio con avatar de iniciales, valoración y cita. */
|
|
19
|
+
export default function Testimonial({ quote, author, role, rating, variant, fill, pattern, tone, image, className }: TestimonialProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { fillProps } from "./fill.js";
|
|
3
|
+
import { vcls } from "./variants.js";
|
|
4
|
+
/** Testimonio con avatar de iniciales, valoración y cita. */
|
|
5
|
+
export default function Testimonial({ quote = "Levanté la tabla el primer día. El equipo es paciente y el mar, una maravilla.", author = "Lucía Pardo", role = "Alumna, curso de iniciación", rating = 5, variant = "glass", fill, pattern, tone, image, className = "" }) {
|
|
6
|
+
const f = fillProps({ fill, pattern, tone, image });
|
|
7
|
+
const initials = author.split(" ").map((w) => w[0]).slice(0, 2).join("").toUpperCase();
|
|
8
|
+
return (_jsxs("figure", { className: `ui-quote ui-surface ${f.className} ${vcls(variant)} ${className}`, style: { margin: 0, ...f.style }, children: [rating > 0 && (_jsxs("div", { className: "ui-quote__stars", "aria-label": `${rating} de 5`, children: ["★".repeat(Math.min(5, rating)), _jsx("span", { style: { opacity: 0.25 }, children: "★".repeat(5 - Math.min(5, rating)) })] })), _jsxs("blockquote", { children: ["\u201C", quote, "\u201D"] }), _jsxs("figcaption", { className: "ui-quote__by", children: [_jsx("span", { className: "ui-quote__avatar", "aria-hidden": true, children: initials }), _jsxs("span", { children: [_jsx("strong", { children: author }), _jsx("span", { children: role })] })] })] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Variant } from "./variants.js";
|
|
2
|
+
export type TestimonialSectionProps = {
|
|
3
|
+
kicker?: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
subtitle?: string;
|
|
6
|
+
/** un testimonio por línea: «cita|autor|rol|valoración (0-5)» */
|
|
7
|
+
items?: string;
|
|
8
|
+
/** grid = rejilla · carousel = carrusel de Swiper */
|
|
9
|
+
layout?: "grid" | "carousel";
|
|
10
|
+
/** con layout="carousel": tarjetas visibles en pantallas anchas */
|
|
11
|
+
perView?: number;
|
|
12
|
+
/** con layout="carousel": segundos entre tarjetas (0 = manual) */
|
|
13
|
+
autoplay?: number;
|
|
14
|
+
/** con layout="carousel": slide = desplazamiento · fade = una cita que se funde con la siguiente · cards = baraja */
|
|
15
|
+
effect?: "slide" | "fade" | "cards";
|
|
16
|
+
variant?: Variant;
|
|
17
|
+
className?: string;
|
|
18
|
+
};
|
|
19
|
+
/** Grid de reseñas (`Testimonial`) con cabecera: prueba social lista para pegar en una landing. */
|
|
20
|
+
export default function TestimonialSection({ kicker, title, subtitle, items, layout, perView, autoplay, effect, variant, className, }: TestimonialSectionProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import Carousel from "./Carousel.js";
|
|
5
|
+
import SectionHeader from "./SectionHeader.js";
|
|
6
|
+
import Testimonial from "./Testimonial.js";
|
|
7
|
+
const DEFAULT_ITEMS = "Levanté la tabla el primer día. El equipo es paciente y el mar, una maravilla.|Lucía Pardo|Alumna, iniciación|5\n" +
|
|
8
|
+
"Vinimos en familia y los peques no querían salir del agua.|Marcos Ibáñez|Curso familiar|5\n" +
|
|
9
|
+
"El monitor grabó mis olas y corregimos la técnica al momento.|Diego Rus|Clases privadas|4";
|
|
10
|
+
/** Grid de reseñas (`Testimonial`) con cabecera: prueba social lista para pegar en una landing. */
|
|
11
|
+
export default function TestimonialSection({ kicker = "Lo que dicen", title = "Alumnos que ya cogieron su ola", subtitle = "", items = DEFAULT_ITEMS, layout = "grid", perView = 3, autoplay = 0, effect = "slide", variant = "glass", className = "", }) {
|
|
12
|
+
const rows = useMemo(() => items.split("\n").map((l) => l.split("|")).filter((r) => r[0]?.trim()), [items]);
|
|
13
|
+
const cards = rows.map(([quote, author, role, rating], i) => (_jsx(Testimonial, { quote: quote, author: author, role: role, rating: parseInt(rating, 10) || 0, variant: variant }, author + i)));
|
|
14
|
+
return (_jsxs("section", { className: `ui-testimonials ${className}`, children: [_jsx(SectionHeader, { kicker: kicker, title: title, subtitle: subtitle, align: "center", variant: "minimal" }), layout === "carousel" ? (_jsx(Carousel, { perView: perView, autoplay: autoplay, effect: effect, loop: true, variant: variant, label: title, children: cards })) : (_jsx("div", { className: "ui-testimonials__grid", children: cards }))] }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Intent, type LegacyIntent } from "./intent.js";
|
|
2
|
+
import { type Variant } from "./variants.js";
|
|
3
|
+
export type TextFieldProps = {
|
|
4
|
+
label?: string;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
defaultValue?: string;
|
|
7
|
+
hint?: string;
|
|
8
|
+
/** estado de validación con el vocabulario común (ver intent.ts): `danger` marca el campo como inválido (aria-invalid) */
|
|
9
|
+
intent?: Exclude<Intent, "accent" | "neutral"> | LegacyIntent;
|
|
10
|
+
/** @deprecated nombre antiguo de `intent`: "default" | "error" | "success" */
|
|
11
|
+
state?: "default" | "error" | "success";
|
|
12
|
+
variant?: Variant;
|
|
13
|
+
size?: "sm" | "md" | "lg";
|
|
14
|
+
/** carácter delante del texto (@, >, $…) */
|
|
15
|
+
prefix?: string;
|
|
16
|
+
type?: "text" | "email" | "password" | "search";
|
|
17
|
+
/** nombre del campo, para leerlo con `FormData` (ver `ContactForm`) */
|
|
18
|
+
name?: string;
|
|
19
|
+
/** dibuja un `<textarea>` en vez de un `<input>` */
|
|
20
|
+
multiline?: boolean;
|
|
21
|
+
rows?: number;
|
|
22
|
+
required?: boolean;
|
|
23
|
+
className?: string;
|
|
24
|
+
};
|
|
25
|
+
/** Campo de texto con etiqueta, ayuda y estados. El foco y los estados usan `--acc` / colores semánticos. */
|
|
26
|
+
export default function TextField({ label, placeholder, defaultValue, hint, intent, state, variant, size, prefix, type, name, multiline, rows, required, className, }: TextFieldProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { intentCls, toIntent } from "./intent.js";
|
|
5
|
+
import { vcls } from "./variants.js";
|
|
6
|
+
/** Campo de texto con etiqueta, ayuda y estados. El foco y los estados usan `--acc` / colores semánticos. */
|
|
7
|
+
export default function TextField({ label = "Correo electrónico", placeholder = "tu@correo.com", defaultValue = "", hint = "", intent, state, variant = "glass", size = "md", prefix = "", type = "text", name, multiline = false, rows = 4, required = false, className = "", }) {
|
|
8
|
+
const [value, setValue] = useState(defaultValue);
|
|
9
|
+
const raw = intent ?? (state && state !== "default" ? state : undefined);
|
|
10
|
+
const i = raw ? toIntent(raw, "info") : undefined;
|
|
11
|
+
const invalid = i === "danger" || undefined;
|
|
12
|
+
return (_jsxs("label", { className: `ui-field ui-field--${size} ${i ? `ui-field--state ${intentCls(i)}` : ""} ${multiline ? "ui-field--multiline" : ""} ${className}`, children: [label && _jsx("span", { className: "ui-field__label", children: label }), _jsxs("span", { className: `ui-field__box ui-surface ${vcls(variant)}`, children: [prefix && _jsx("span", { className: "ui-field__prefix", "aria-hidden": true, children: prefix }), multiline ? (_jsx("textarea", { "aria-invalid": invalid, name: name, rows: rows, placeholder: placeholder, value: value, required: required, onChange: (e) => setValue(e.target.value) }, defaultValue)) : (_jsx("input", { "aria-invalid": invalid, type: type, name: name, placeholder: placeholder, value: value, required: required, onChange: (e) => setValue(e.target.value) }, defaultValue))] }), hint && _jsx("span", { className: "ui-field__hint", children: hint })] }));
|
|
13
|
+
}
|