vaderjs-daisyui 0.0.1
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/Components/Actions/Button/index.tsx +63 -0
- package/Components/Actions/Dropdown/index.tsx +104 -0
- package/Components/Actions/Fab/index.tsx +76 -0
- package/Components/Actions/Modal/index.tsx +147 -0
- package/Components/Actions/Swap/index.tsx +52 -0
- package/Components/Actions/ThemeController/index.tsx +133 -0
- package/Components/Data/Display/Accordion/index.tsx +82 -0
- package/Components/Data/Display/Avatar/index.tsx +96 -0
- package/Components/Data/Display/Badge/index.tsx +46 -0
- package/Components/Data/Display/Card/index.tsx +72 -0
- package/Components/Data/Display/Carousel/index.tsx +72 -0
- package/Components/Data/Display/ChatBubble/index.tsx +57 -0
- package/Components/Data/Display/Collapse/index.tsx +60 -0
- package/Components/Data/Display/Countdown/index.tsx +97 -0
- package/Components/Data/Display/Diff/index.tsx +60 -0
- package/Components/Data/Display/Hover/Card/index.tsx +37 -0
- package/Components/Data/Display/Hover/Gallery/index.tsx +57 -0
- package/Components/Data/Display/Keyboard/index.tsx +31 -0
- package/Components/Data/Display/List/index.tsx +93 -0
- package/Components/Data/Display/Stat/index.tsx +114 -0
- package/Components/Data/Display/Table/index.tsx +33 -0
- package/Components/Data/Display/TextRotate/index.tsx +118 -0
- package/Components/Data/Display/Timeline/index.tsx +209 -0
- package/Components/Navigation/BreadCrumbs/index.tsx +201 -0
- package/Components/Navigation/Doc/index.tsx +394 -0
- package/Components/Navigation/Link/index.tsx +87 -0
- package/index.ts +130 -0
- package/package.json +15 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { component, createElement, VNode } from "vaderjs";
|
|
2
|
+
|
|
3
|
+
export type AvatarProps = {
|
|
4
|
+
src?: string;
|
|
5
|
+
size?: string; // w-8, w-12, w-16, w-24, etc.
|
|
6
|
+
rounded?: "none" | "sm" | "md" | "xl" | "full";
|
|
7
|
+
mask?: string; // mask-heart, mask-squircle, mask-hexagon-2
|
|
8
|
+
ring?: string; // ring-primary, ring-secondary, etc.
|
|
9
|
+
ringOffset?: string; // ring-offset-base-100, etc.
|
|
10
|
+
online?: boolean;
|
|
11
|
+
offline?: boolean;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type AvatarGroupProps = {
|
|
17
|
+
children: VNode[];
|
|
18
|
+
counter?: number; // show "+N" on last avatar
|
|
19
|
+
space?: string; // spacing e.g. -space-x-6
|
|
20
|
+
className?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const Avatar = component((props: AvatarProps) => {
|
|
24
|
+
const {
|
|
25
|
+
src,
|
|
26
|
+
size = "w-12",
|
|
27
|
+
rounded = "full",
|
|
28
|
+
mask,
|
|
29
|
+
ring,
|
|
30
|
+
ringOffset,
|
|
31
|
+
online,
|
|
32
|
+
offline,
|
|
33
|
+
placeholder,
|
|
34
|
+
className,
|
|
35
|
+
} = props;
|
|
36
|
+
|
|
37
|
+
const containerClasses = ["avatar"];
|
|
38
|
+
if (online) containerClasses.push("avatar-online");
|
|
39
|
+
if (offline) containerClasses.push("avatar-offline");
|
|
40
|
+
if (placeholder) containerClasses.push("avatar-placeholder");
|
|
41
|
+
if (className) containerClasses.push(className);
|
|
42
|
+
|
|
43
|
+
const avatarClasses = [
|
|
44
|
+
size,
|
|
45
|
+
rounded === "none" ? "" : `rounded-${rounded}`,
|
|
46
|
+
mask ? `mask ${mask}` : "",
|
|
47
|
+
ring ? `ring-2 ${ring}` : "",
|
|
48
|
+
ringOffset ? `ring-offset-2 ${ringOffset}` : "",
|
|
49
|
+
].filter(Boolean).join(" ");
|
|
50
|
+
|
|
51
|
+
const fontSize = size.includes("w-24")
|
|
52
|
+
? "text-3xl"
|
|
53
|
+
: size.includes("w-16")
|
|
54
|
+
? "text-xl"
|
|
55
|
+
: size.includes("w-12")
|
|
56
|
+
? "text-base"
|
|
57
|
+
: "text-xs";
|
|
58
|
+
|
|
59
|
+
return createElement(
|
|
60
|
+
"div",
|
|
61
|
+
{ className: containerClasses.join(" ") },
|
|
62
|
+
createElement(
|
|
63
|
+
"div",
|
|
64
|
+
{ className: avatarClasses },
|
|
65
|
+
placeholder
|
|
66
|
+
? createElement("span", { className: fontSize }, placeholder)
|
|
67
|
+
: src
|
|
68
|
+
? createElement("img", { src })
|
|
69
|
+
: null
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const AvatarGroup = component((props: AvatarGroupProps) => {
|
|
75
|
+
const { children, counter, space = "-space-x-6", className } = props;
|
|
76
|
+
const groupClasses = ["avatar-group", space];
|
|
77
|
+
if (className) groupClasses.push(className);
|
|
78
|
+
|
|
79
|
+
const renderedChildren = [...children];
|
|
80
|
+
if (counter) {
|
|
81
|
+
// Add last avatar as counter
|
|
82
|
+
renderedChildren.push(
|
|
83
|
+
createElement(
|
|
84
|
+
"div",
|
|
85
|
+
{ className: "avatar avatar-placeholder" },
|
|
86
|
+
createElement(
|
|
87
|
+
"div",
|
|
88
|
+
{ className: `bg-neutral text-neutral-content ${children[0]?.props?.size || "w-12"} rounded-full` },
|
|
89
|
+
`+${counter}`
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return createElement("div", { className: groupClasses.join(" ") }, ...renderedChildren);
|
|
96
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { component, createElement, VNode } from "vaderjs";
|
|
2
|
+
|
|
3
|
+
export type BadgeProps = {
|
|
4
|
+
children?: string | VNode;
|
|
5
|
+
size?: "xs" | "sm" | "md" | "lg" | "xl";
|
|
6
|
+
color?: "neutral" | "primary" | "secondary" | "accent" | "info" | "success" | "warning" | "error";
|
|
7
|
+
style?: "soft" | "outline" | "dash" | "ghost";
|
|
8
|
+
icon?: VNode; // optional SVG icon
|
|
9
|
+
className?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const Badge = component((props: BadgeProps) => {
|
|
13
|
+
const { children, size = "md", color = "neutral", style, icon, className } = props;
|
|
14
|
+
|
|
15
|
+
const classes = ["badge"];
|
|
16
|
+
if (size) classes.push(`badge-${size}`);
|
|
17
|
+
if (color) classes.push(`badge-${color}`);
|
|
18
|
+
if (style) classes.push(`badge-${style}`);
|
|
19
|
+
if (className) classes.push(className);
|
|
20
|
+
|
|
21
|
+
return createElement(
|
|
22
|
+
"span",
|
|
23
|
+
{ className: classes.join(" ") },
|
|
24
|
+
icon ? createElement("span", { className: "badge-icon mr-1" }, icon) : null,
|
|
25
|
+
children
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export type BadgeButtonProps = {
|
|
30
|
+
label: string | VNode;
|
|
31
|
+
badge?: BadgeProps;
|
|
32
|
+
className?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const BadgeButton = component((props: BadgeButtonProps) => {
|
|
36
|
+
const { label, badge, className } = props;
|
|
37
|
+
const btnClasses = ["btn"];
|
|
38
|
+
if (className) btnClasses.push(className);
|
|
39
|
+
|
|
40
|
+
return createElement(
|
|
41
|
+
"button",
|
|
42
|
+
{ className: btnClasses.join(" ") },
|
|
43
|
+
label,
|
|
44
|
+
badge ? createElement(Badge, badge) : null
|
|
45
|
+
);
|
|
46
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { component, createElement, VNode } from "vaderjs";
|
|
2
|
+
import { Badge, BadgeProps } from "../Badge";
|
|
3
|
+
|
|
4
|
+
export type CardProps = {
|
|
5
|
+
title?: string | VNode;
|
|
6
|
+
body?: string | VNode;
|
|
7
|
+
actions?: VNode | VNode[];
|
|
8
|
+
figure?: VNode;
|
|
9
|
+
size?: "xs" | "sm" | "md" | "lg" | "xl";
|
|
10
|
+
style?: "border" | "dash";
|
|
11
|
+
side?: boolean; // card-side
|
|
12
|
+
imageFull?: boolean; // image-full
|
|
13
|
+
center?: boolean; // center content
|
|
14
|
+
className?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const Card = component((props: CardProps) => {
|
|
18
|
+
const {
|
|
19
|
+
title,
|
|
20
|
+
body,
|
|
21
|
+
actions,
|
|
22
|
+
figure,
|
|
23
|
+
size = "md",
|
|
24
|
+
style,
|
|
25
|
+
side,
|
|
26
|
+
imageFull,
|
|
27
|
+
center,
|
|
28
|
+
className,
|
|
29
|
+
} = props;
|
|
30
|
+
|
|
31
|
+
const classes = ["card"];
|
|
32
|
+
if (size) classes.push(`card-${size}`);
|
|
33
|
+
if (style === "border") classes.push("card-border");
|
|
34
|
+
if (style === "dash") classes.push("card-dash");
|
|
35
|
+
if (side) classes.push("card-side");
|
|
36
|
+
if (imageFull) classes.push("image-full");
|
|
37
|
+
if (className) classes.push(className);
|
|
38
|
+
|
|
39
|
+
const bodyClasses = ["card-body"];
|
|
40
|
+
if (center) bodyClasses.push("items-center", "text-center");
|
|
41
|
+
|
|
42
|
+
return createElement(
|
|
43
|
+
"div",
|
|
44
|
+
{ className: classes.join(" ") },
|
|
45
|
+
figure ? figure : null,
|
|
46
|
+
createElement(
|
|
47
|
+
"div",
|
|
48
|
+
{ className: bodyClasses.join(" ") },
|
|
49
|
+
title ? (typeof title === "string" ? createElement("h2", { className: "card-title" }, title) : title) : null,
|
|
50
|
+
body ? (typeof body === "string" ? createElement("p", {}, body) : body) : null,
|
|
51
|
+
actions ? (Array.isArray(actions) ? actions.map(a => a) : actions) : null
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Optional helper for card with badge on title
|
|
57
|
+
export type CardWithBadgeProps = CardProps & {
|
|
58
|
+
badges?: BadgeProps[];
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const CardWithBadge = component((props: CardWithBadgeProps) => {
|
|
62
|
+
const { badges, title, ...cardProps } = props;
|
|
63
|
+
|
|
64
|
+
const titleVNode = createElement(
|
|
65
|
+
"h2",
|
|
66
|
+
{ className: "card-title flex items-center gap-2" },
|
|
67
|
+
title,
|
|
68
|
+
badges ? badges.map(b => createElement(Badge, b)) : null
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return createElement(Card, { ...cardProps, title: titleVNode });
|
|
72
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Carousel.tsx
|
|
2
|
+
import { component, createElement } from "vaderjs";
|
|
3
|
+
|
|
4
|
+
interface CarouselProps {
|
|
5
|
+
images: string[];
|
|
6
|
+
snap?: "start" | "center" | "end"; // snap position
|
|
7
|
+
width?: "full" | "half"; // width of items
|
|
8
|
+
vertical?: boolean; // vertical layout
|
|
9
|
+
containerWidth?: string; // optional width of carousel container, e.g., "w-96"
|
|
10
|
+
fullBleed?: boolean; // full-bleed layout
|
|
11
|
+
indicators?: boolean; // show indicator buttons
|
|
12
|
+
controls?: boolean; // show next/prev buttons
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const Carousel = component(({
|
|
16
|
+
images,
|
|
17
|
+
snap = "start",
|
|
18
|
+
width = "full",
|
|
19
|
+
vertical = false,
|
|
20
|
+
containerWidth = "w-64",
|
|
21
|
+
fullBleed = false,
|
|
22
|
+
indicators = false,
|
|
23
|
+
controls = false,
|
|
24
|
+
}: CarouselProps) => {
|
|
25
|
+
const snapClass = snap === "center" ? "carousel-center" : snap === "end" ? "carousel-end" : "carousel-start";
|
|
26
|
+
const orientationClass = vertical ? "carousel-vertical" : "";
|
|
27
|
+
const itemClass = width === "full" ? "w-full" : "w-1/2";
|
|
28
|
+
const containerClass = fullBleed
|
|
29
|
+
? "carousel carousel-center bg-neutral rounded-box max-w-md space-x-4 p-4"
|
|
30
|
+
: `carousel ${snapClass} ${orientationClass} ${containerWidth} rounded-box`;
|
|
31
|
+
|
|
32
|
+
const len = images.length;
|
|
33
|
+
|
|
34
|
+
// VaderJS requires returning VNode or VNode[], no fragments <>
|
|
35
|
+
const carouselItems = images.map((src, i) => {
|
|
36
|
+
const prev = (i - 1 + len) % len;
|
|
37
|
+
const next = (i + 1) % len;
|
|
38
|
+
|
|
39
|
+
return createElement("div", {
|
|
40
|
+
key: i,
|
|
41
|
+
id: controls ? `slide${i + 1}` : indicators ? `item${i + 1}` : undefined,
|
|
42
|
+
class: `carousel-item ${itemClass} ${vertical ? "h-full" : ""} relative`
|
|
43
|
+
},
|
|
44
|
+
createElement("img", {
|
|
45
|
+
src,
|
|
46
|
+
class: fullBleed ? "rounded-box" : "w-full",
|
|
47
|
+
alt: `Carousel ${i + 1}`
|
|
48
|
+
}),
|
|
49
|
+
controls ? createElement("div", {
|
|
50
|
+
class: "absolute left-5 right-5 top-1/2 flex -translate-y-1/2 transform justify-between"
|
|
51
|
+
},
|
|
52
|
+
createElement("a", { href: `#slide${prev + 1}`, class: "btn btn-circle" }, "❮"),
|
|
53
|
+
createElement("a", { href: `#slide${next + 1}`, class: "btn btn-circle" }, "❯")
|
|
54
|
+
) : null
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const indicatorButtons = indicators && !controls
|
|
59
|
+
? createElement("div", { class: "flex justify-center w-full py-2 gap-2" },
|
|
60
|
+
images.map((_, i) =>
|
|
61
|
+
createElement("a", { href: `#item${i + 1}`, class: "btn btn-xs", key: i }, `${i + 1}`)
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
: null;
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
createElement("div", { class: containerClass }, ...carouselItems),
|
|
68
|
+
indicatorButtons
|
|
69
|
+
];
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export default Carousel;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// ChatBubbles.ts
|
|
2
|
+
import { component, createElement } from "vaderjs";
|
|
3
|
+
|
|
4
|
+
export type ChatColor =
|
|
5
|
+
| "neutral"
|
|
6
|
+
| "primary"
|
|
7
|
+
| "secondary"
|
|
8
|
+
| "accent"
|
|
9
|
+
| "info"
|
|
10
|
+
| "success"
|
|
11
|
+
| "warning"
|
|
12
|
+
| "error";
|
|
13
|
+
|
|
14
|
+
export interface ChatMessage {
|
|
15
|
+
id: string | number;
|
|
16
|
+
author?: string;
|
|
17
|
+
avatar?: string;
|
|
18
|
+
time?: string;
|
|
19
|
+
content: string;
|
|
20
|
+
placement?: "start" | "end";
|
|
21
|
+
header?: string;
|
|
22
|
+
footer?: string;
|
|
23
|
+
color?: ChatColor;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ChatProps {
|
|
27
|
+
messages: ChatMessage[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ChatBubbles = component(({ messages }: ChatProps) => {
|
|
31
|
+
const renderMessage = (msg: ChatMessage) => {
|
|
32
|
+
const placementClass = msg.placement === "end" ? "chat-end" : "chat-start";
|
|
33
|
+
const bubbleColor = msg.color ? `chat-bubble-${msg.color}` : "";
|
|
34
|
+
|
|
35
|
+
return createElement("div", { class: `chat ${placementClass}`, key: msg.id }, [
|
|
36
|
+
msg.avatar
|
|
37
|
+
? createElement("div", { class: "chat-image avatar" },
|
|
38
|
+
createElement("div", { class: "w-10 rounded-full" },
|
|
39
|
+
createElement("img", { src: msg.avatar, alt: msg.author ?? "User" })
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
: null,
|
|
43
|
+
msg.header
|
|
44
|
+
? createElement("div", { class: "chat-header" }, [
|
|
45
|
+
msg.header,
|
|
46
|
+
msg.time ? createElement("time", { class: "text-xs opacity-50" }, msg.time) : null
|
|
47
|
+
])
|
|
48
|
+
: null,
|
|
49
|
+
createElement("div", { class: `chat-bubble ${bubbleColor}` }, msg.content),
|
|
50
|
+
msg.footer ? createElement("div", { class: "chat-footer opacity-50" }, msg.footer) : null
|
|
51
|
+
]);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return messages.map(renderMessage);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export default ChatBubbles;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Collapse.ts
|
|
2
|
+
import { component, createElement } from "vaderjs";
|
|
3
|
+
|
|
4
|
+
export interface CollapseItem {
|
|
5
|
+
id: string | number;
|
|
6
|
+
title: string;
|
|
7
|
+
content: string;
|
|
8
|
+
focus?: boolean; // true = focus-based collapse
|
|
9
|
+
checkbox?: boolean; // true = checkbox-controlled
|
|
10
|
+
details?: boolean; // true = use <details>/<summary>
|
|
11
|
+
open?: boolean; // force open
|
|
12
|
+
close?: boolean; // force close
|
|
13
|
+
arrow?: boolean; // arrow icon
|
|
14
|
+
plus?: boolean; // plus/minus icon
|
|
15
|
+
placementStart?: boolean; // move icon to start
|
|
16
|
+
customClasses?: string; // any additional Tailwind classes
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CollapseProps {
|
|
20
|
+
items: CollapseItem[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const Collapse = component(({ items }: CollapseProps) => {
|
|
24
|
+
const renderItem = (item: CollapseItem) => {
|
|
25
|
+
let baseClasses = "collapse";
|
|
26
|
+
if (item.focus) baseClasses += " focus:outline-none";
|
|
27
|
+
if (item.arrow) baseClasses += " collapse-arrow";
|
|
28
|
+
if (item.plus) baseClasses += " collapse-plus";
|
|
29
|
+
if (item.open) baseClasses += " collapse-open";
|
|
30
|
+
if (item.close) baseClasses += " collapse-close";
|
|
31
|
+
if (item.customClasses) baseClasses += ` ${item.customClasses}`;
|
|
32
|
+
|
|
33
|
+
const titleClasses = `collapse-title font-semibold${
|
|
34
|
+
item.placementStart ? " after:start-5 after:end-auto pe-4 ps-12" : ""
|
|
35
|
+
}`;
|
|
36
|
+
const contentClasses = "collapse-content text-sm";
|
|
37
|
+
|
|
38
|
+
if (item.details) {
|
|
39
|
+
// Using details/summary
|
|
40
|
+
return createElement(
|
|
41
|
+
"details",
|
|
42
|
+
{ class: baseClasses, key: item.id },
|
|
43
|
+
[
|
|
44
|
+
createElement("summary", { class: titleClasses }, item.title),
|
|
45
|
+
createElement("div", { class: contentClasses }, item.content)
|
|
46
|
+
]
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return createElement("div", { class: baseClasses, key: item.id, tabindex: item.focus ? 0 : undefined }, [
|
|
51
|
+
item.checkbox ? createElement("input", { type: "checkbox" }) : null,
|
|
52
|
+
createElement("div", { class: titleClasses }, item.title),
|
|
53
|
+
createElement("div", { class: contentClasses }, item.content)
|
|
54
|
+
]);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return items.map(renderItem);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export default Collapse;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Countdown.ts
|
|
2
|
+
import { component, createElement, useState, useEffect, useRef } from "vaderjs";
|
|
3
|
+
|
|
4
|
+
export interface CountdownUnit {
|
|
5
|
+
label?: string;
|
|
6
|
+
value: number;
|
|
7
|
+
max?: number; // e.g., 59 for seconds
|
|
8
|
+
digits?: number;
|
|
9
|
+
animation?: string;
|
|
10
|
+
containerClass?: string;
|
|
11
|
+
numberClass?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CountdownProps {
|
|
15
|
+
units: CountdownUnit[];
|
|
16
|
+
interval?: number; // ms per tick
|
|
17
|
+
loop?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const Countdown = component(({ units, interval = 1000, loop = true }: CountdownProps) => {
|
|
21
|
+
const [counts, setCounts] = useState(() => units.map(u => u.value));
|
|
22
|
+
|
|
23
|
+
// Refs to avoid stale closures in setInterval
|
|
24
|
+
const unitsRef = useRef(units);
|
|
25
|
+
const loopRef = useRef(loop);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
unitsRef.current = units;
|
|
29
|
+
loopRef.current = loop;
|
|
30
|
+
}, [units, loop]);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const timer = setInterval(() => {
|
|
34
|
+
setCounts(prev => {
|
|
35
|
+
const newCounts = [...prev];
|
|
36
|
+
const currentUnits = unitsRef.current;
|
|
37
|
+
const shouldLoop = loopRef.current;
|
|
38
|
+
|
|
39
|
+
let i = newCounts.length - 1;
|
|
40
|
+
let carry = true;
|
|
41
|
+
|
|
42
|
+
while (i >= 0 && carry) {
|
|
43
|
+
const max = currentUnits[i]?.max ?? (i === newCounts.length - 1 ? 59 : 99);
|
|
44
|
+
if (newCounts[i] > 0) {
|
|
45
|
+
newCounts[i]--;
|
|
46
|
+
carry = false;
|
|
47
|
+
} else {
|
|
48
|
+
newCounts[i] = max;
|
|
49
|
+
i--;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (carry && shouldLoop) {
|
|
54
|
+
// Reset all to initial values if looping
|
|
55
|
+
return currentUnits.map(u => u.value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (carry && !shouldLoop) {
|
|
59
|
+
// Stop at zero
|
|
60
|
+
clearInterval(timer);
|
|
61
|
+
return newCounts.map(() => 0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return newCounts;
|
|
65
|
+
});
|
|
66
|
+
}, interval);
|
|
67
|
+
|
|
68
|
+
return () => clearInterval(timer);
|
|
69
|
+
}, [interval]);
|
|
70
|
+
|
|
71
|
+
const renderUnit = (val: number, unit: CountdownUnit, idx: number) => {
|
|
72
|
+
const digits = unit.digits ?? 2;
|
|
73
|
+
const padded = String(val).padStart(digits, "0");
|
|
74
|
+
|
|
75
|
+
return createElement(
|
|
76
|
+
"div",
|
|
77
|
+
{ key: idx, class: `flex flex-col items-center ${unit.containerClass || ""}` },
|
|
78
|
+
[
|
|
79
|
+
createElement("span", {
|
|
80
|
+
class: `countdown font-mono ${unit.numberClass || ""} ${unit.animation || ""}`,
|
|
81
|
+
style: `--value:${val}; --digits:${digits};`,
|
|
82
|
+
"aria-live": "polite",
|
|
83
|
+
"aria-label": String(val)
|
|
84
|
+
}, padded),
|
|
85
|
+
unit.label ? createElement("span", { class: "text-sm mt-1" }, unit.label) : null
|
|
86
|
+
]
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return createElement(
|
|
91
|
+
"div",
|
|
92
|
+
{ class: "grid auto-cols-max grid-flow-col gap-5 text-center" },
|
|
93
|
+
counts.map((val, idx) => renderUnit(val, units[idx], idx))
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export default Countdown;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { component, createElement, useRef, useEffect } from "vaderjs";
|
|
2
|
+
|
|
3
|
+
export interface DiffProps {
|
|
4
|
+
item1: any;
|
|
5
|
+
item2: any;
|
|
6
|
+
className?: string;
|
|
7
|
+
initial?: number; // 0–100
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const Diff = component(
|
|
11
|
+
({ item1, item2, className = "diff rounded-field aspect-16/9", initial = 50 }: DiffProps) => {
|
|
12
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const el = containerRef.current;
|
|
16
|
+
if (!el) return;
|
|
17
|
+
el.style.setProperty("--pos", `${initial}%`);
|
|
18
|
+
}, [initial]);
|
|
19
|
+
|
|
20
|
+
return createElement(
|
|
21
|
+
"figure",
|
|
22
|
+
{
|
|
23
|
+
ref: containerRef,
|
|
24
|
+
class: className,
|
|
25
|
+
tabindex: 0,
|
|
26
|
+
},
|
|
27
|
+
[
|
|
28
|
+
createElement(
|
|
29
|
+
"div",
|
|
30
|
+
{ class: "diff-item-1", role: "img", tabindex: 0 },
|
|
31
|
+
item1
|
|
32
|
+
),
|
|
33
|
+
|
|
34
|
+
createElement(
|
|
35
|
+
"div",
|
|
36
|
+
{ class: "diff-item-2", role: "img" },
|
|
37
|
+
item2
|
|
38
|
+
),
|
|
39
|
+
|
|
40
|
+
// 🔑 THIS is the key
|
|
41
|
+
createElement("input", {
|
|
42
|
+
type: "range",
|
|
43
|
+
min: 0,
|
|
44
|
+
max: 100,
|
|
45
|
+
value: initial,
|
|
46
|
+
class: "diff-resizer",
|
|
47
|
+
"aria-label": "Resize comparison",
|
|
48
|
+
onInput: (e: any) => {
|
|
49
|
+
containerRef.current?.style.setProperty(
|
|
50
|
+
"--pos",
|
|
51
|
+
`${e.target.value}%`
|
|
52
|
+
);
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
]
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
export default Diff;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { component, createElement } from "vaderjs";
|
|
2
|
+
|
|
3
|
+
export interface Hover3DProps {
|
|
4
|
+
children: any;
|
|
5
|
+
className?: string;
|
|
6
|
+
as?: "div" | "a";
|
|
7
|
+
href?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const Hover3D = component(
|
|
11
|
+
({ children, className = "", as = "div", href }: Hover3DProps) => {
|
|
12
|
+
const Tag = as;
|
|
13
|
+
|
|
14
|
+
return createElement(
|
|
15
|
+
Tag,
|
|
16
|
+
{
|
|
17
|
+
class: `hover-3d ${className}`.trim(),
|
|
18
|
+
...(Tag === "a" && href ? { href } : {}),
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
// ✅ IMPORTANT: spread children
|
|
22
|
+
children,
|
|
23
|
+
|
|
24
|
+
// required 8 hover zones
|
|
25
|
+
createElement("div", {}),
|
|
26
|
+
createElement("div", {}),
|
|
27
|
+
createElement("div", {}),
|
|
28
|
+
createElement("div", {}),
|
|
29
|
+
createElement("div", {}),
|
|
30
|
+
createElement("div", {}),
|
|
31
|
+
createElement("div", {}),
|
|
32
|
+
createElement("div", {})
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export default Hover3D;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// HoverGallery.ts
|
|
2
|
+
import { component, createElement } from "vaderjs";
|
|
3
|
+
|
|
4
|
+
export interface HoverGalleryProps {
|
|
5
|
+
images: (string | JSX.Element)[];
|
|
6
|
+
className?: string;
|
|
7
|
+
as?: "figure" | "div";
|
|
8
|
+
altPrefix?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* HoverGallery
|
|
13
|
+
* DaisyUI hover-gallery component
|
|
14
|
+
*
|
|
15
|
+
* Rules:
|
|
16
|
+
* - Max 10 images (CSS limitation)
|
|
17
|
+
* - First image is default
|
|
18
|
+
* - Remaining images activate on horizontal hover zones
|
|
19
|
+
* - NO JS REQUIRED (CSS-driven)
|
|
20
|
+
*/
|
|
21
|
+
const HoverGallery = component(
|
|
22
|
+
({
|
|
23
|
+
images,
|
|
24
|
+
className = "",
|
|
25
|
+
as = "figure",
|
|
26
|
+
altPrefix = "Gallery image",
|
|
27
|
+
}: HoverGalleryProps) => {
|
|
28
|
+
const Tag = as;
|
|
29
|
+
|
|
30
|
+
const safeImages = images.slice(0, 10); // hard cap per DaisyUI
|
|
31
|
+
|
|
32
|
+
return createElement(
|
|
33
|
+
Tag,
|
|
34
|
+
{
|
|
35
|
+
class: `hover-gallery ${className}`.trim(),
|
|
36
|
+
role: "group",
|
|
37
|
+
"aria-label": "Hover image gallery",
|
|
38
|
+
},
|
|
39
|
+
safeImages.map((img, idx) => {
|
|
40
|
+
// string → <img src="..." />
|
|
41
|
+
if (typeof img === "string") {
|
|
42
|
+
return createElement("img", {
|
|
43
|
+
src: img,
|
|
44
|
+
alt: `${altPrefix} ${idx + 1}`,
|
|
45
|
+
loading: idx === 0 ? "eager" : "lazy",
|
|
46
|
+
draggable: false,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// JSX passed directly
|
|
51
|
+
return img;
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
export default HoverGallery;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Kbd.ts
|
|
2
|
+
import { component, createElement } from "vaderjs";
|
|
3
|
+
|
|
4
|
+
export type KbdSize = "xs" | "sm" | "md" | "lg" | "xl";
|
|
5
|
+
|
|
6
|
+
export interface KbdProps {
|
|
7
|
+
children: any;
|
|
8
|
+
size?: KbdSize;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Kbd
|
|
14
|
+
* DaisyUI keyboard key / shortcut component
|
|
15
|
+
*/
|
|
16
|
+
const Kbd = component(
|
|
17
|
+
({ children, size = "md", className = "" }: KbdProps) => {
|
|
18
|
+
const sizeClass =
|
|
19
|
+
size === "md" ? "" : `kbd-${size}`;
|
|
20
|
+
|
|
21
|
+
return createElement(
|
|
22
|
+
"kbd",
|
|
23
|
+
{
|
|
24
|
+
class: `kbd ${sizeClass} ${className}`.trim(),
|
|
25
|
+
},
|
|
26
|
+
children
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export default Kbd;
|