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,63 @@
|
|
|
1
|
+
import { component, createElement, VNode } from "vaderjs";
|
|
2
|
+
|
|
3
|
+
export type ButtonProps = {
|
|
4
|
+
color?: "neutral" | "primary" | "secondary" | "accent" | "info" | "success" | "warning" | "error";
|
|
5
|
+
style?: "outline" | "dash" | "soft" | "ghost" | "link";
|
|
6
|
+
size?: "xs" | "sm" | "md" | "lg" | "xl";
|
|
7
|
+
modifier?: "wide" | "block" | "square" | "circle";
|
|
8
|
+
active?: boolean;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
className?: string;
|
|
11
|
+
onClick?: (e: MouseEvent) => void;
|
|
12
|
+
children?: VNode | VNode[] | string;
|
|
13
|
+
ariaLabel?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const Button = component<ButtonProps>((props): VNode => {
|
|
17
|
+
const {
|
|
18
|
+
color,
|
|
19
|
+
style,
|
|
20
|
+
size = "md",
|
|
21
|
+
modifier,
|
|
22
|
+
active,
|
|
23
|
+
disabled,
|
|
24
|
+
className,
|
|
25
|
+
onClick,
|
|
26
|
+
children,
|
|
27
|
+
ariaLabel,
|
|
28
|
+
...rest
|
|
29
|
+
} = props;
|
|
30
|
+
|
|
31
|
+
// Build class string
|
|
32
|
+
const classes = ["btn"];
|
|
33
|
+
if (color) classes.push(`btn-${color}`);
|
|
34
|
+
if (style) classes.push(`btn-${style}`);
|
|
35
|
+
if (size) classes.push(`btn-${size}`);
|
|
36
|
+
if (modifier) classes.push(`btn-${modifier}`);
|
|
37
|
+
if (active) classes.push("btn-active");
|
|
38
|
+
if (disabled) classes.push("btn-disabled");
|
|
39
|
+
if (className) classes.push(className);
|
|
40
|
+
|
|
41
|
+
// Convert camelCase props to proper DOM attributes
|
|
42
|
+
const domProps: Record<string, any> = {
|
|
43
|
+
class: classes.join(" "),
|
|
44
|
+
onClick: disabled ? undefined : onClick,
|
|
45
|
+
disabled,
|
|
46
|
+
...rest,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Add accessibility attributes with proper names
|
|
50
|
+
if (disabled) domProps["aria-disabled"] = "true";
|
|
51
|
+
if (active) domProps["aria-pressed"] = "true";
|
|
52
|
+
if (ariaLabel) domProps["aria-label"] = ariaLabel;
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
// Always return a VNode
|
|
56
|
+
return createElement(
|
|
57
|
+
"button",
|
|
58
|
+
domProps,
|
|
59
|
+
children
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export default Button;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { component, createElement, useState, useRef, useOnClickOutside, useEffect } from "vaderjs";
|
|
2
|
+
|
|
3
|
+
export type DropdownProps = {
|
|
4
|
+
method?: "details" | "popover" | "focus";
|
|
5
|
+
placement?: "start" | "center" | "end" | "top" | "bottom" | "left" | "right";
|
|
6
|
+
hover?: boolean;
|
|
7
|
+
open?: boolean;
|
|
8
|
+
close?: boolean;
|
|
9
|
+
buttonClass?: string;
|
|
10
|
+
contentClass?: string;
|
|
11
|
+
buttonContent: any;
|
|
12
|
+
children: any;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const Dropdown = component<DropdownProps>((props) => {
|
|
16
|
+
const {
|
|
17
|
+
method = "details",
|
|
18
|
+
placement = "bottom",
|
|
19
|
+
hover = false,
|
|
20
|
+
open,
|
|
21
|
+
close,
|
|
22
|
+
buttonClass = "btn",
|
|
23
|
+
contentClass = "menu dropdown-content bg-base-100 rounded-box shadow-md p-2",
|
|
24
|
+
buttonContent,
|
|
25
|
+
children,
|
|
26
|
+
} = props;
|
|
27
|
+
|
|
28
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
29
|
+
|
|
30
|
+
// Create ref objects
|
|
31
|
+
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
|
32
|
+
const contentRef = useRef<HTMLDivElement | null>(null);
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
36
|
+
if (!isOpen) return;
|
|
37
|
+
if (e.key === "Escape") {
|
|
38
|
+
setIsOpen(false);
|
|
39
|
+
buttonRef.current?.focus();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
console.log(contentRef)
|
|
45
|
+
if (!contentRef.current) return; // Only attach if element exists
|
|
46
|
+
|
|
47
|
+
const listener = (event: MouseEvent) => {
|
|
48
|
+
console.log(event)
|
|
49
|
+
if (!contentRef.current?.contains(event.target as Node)) {
|
|
50
|
+
setIsOpen(false);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
document.addEventListener("mousedown", listener, true);
|
|
55
|
+
return () => document.removeEventListener("mousedown", listener, true);
|
|
56
|
+
}, [isOpen]); // re-run when isOpen changes
|
|
57
|
+
|
|
58
|
+
if (method === "details") {
|
|
59
|
+
return createElement(
|
|
60
|
+
"details",
|
|
61
|
+
{
|
|
62
|
+
class: `dropdown ${placement ? `dropdown-${placement}` : ""} ${hover ? "dropdown-hover" : ""} ${open ? "dropdown-open" : ""} ${close ? "dropdown-close" : ""}`,
|
|
63
|
+
},
|
|
64
|
+
createElement("summary", { class: buttonClass }, buttonContent),
|
|
65
|
+
createElement(
|
|
66
|
+
"div",
|
|
67
|
+
{ class: contentClass, role: "menu", tabIndex: -1 },
|
|
68
|
+
children
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Popover or focus methods
|
|
74
|
+
return createElement(
|
|
75
|
+
"fragment",
|
|
76
|
+
null,
|
|
77
|
+
createElement(
|
|
78
|
+
"button",
|
|
79
|
+
{
|
|
80
|
+
// Pass ref as a property, not an attribute
|
|
81
|
+
ref: buttonRef,
|
|
82
|
+
class: buttonClass,
|
|
83
|
+
"aria-haspopup": "menu",
|
|
84
|
+
"aria-expanded": isOpen,
|
|
85
|
+
onClick: () => setIsOpen(!isOpen),
|
|
86
|
+
},
|
|
87
|
+
buttonContent
|
|
88
|
+
),
|
|
89
|
+
isOpen &&
|
|
90
|
+
createElement(
|
|
91
|
+
"div",
|
|
92
|
+
{
|
|
93
|
+
// Pass ref as a property
|
|
94
|
+
ref: contentRef,
|
|
95
|
+
class: contentClass,
|
|
96
|
+
role: "menu",
|
|
97
|
+
tabIndex: -1
|
|
98
|
+
},
|
|
99
|
+
children
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export default Dropdown;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Fab.tsx
|
|
2
|
+
import { component, createElement, useState, useRef, useEffect } from "vaderjs";
|
|
3
|
+
|
|
4
|
+
interface FabProps {
|
|
5
|
+
mainIcon: any;
|
|
6
|
+
children?: any;
|
|
7
|
+
position?: "top-left" | "top-right" | "bottom-left" | "bottom-right" | "center";
|
|
8
|
+
direction?: "up" | "down" | "left" | "right";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface FabItemProps {
|
|
12
|
+
icon?: any;
|
|
13
|
+
label?: string;
|
|
14
|
+
onClick?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const FabItem = component((props: FabItemProps) => {
|
|
18
|
+
const { icon, label, onClick } = props;
|
|
19
|
+
|
|
20
|
+
return createElement(
|
|
21
|
+
"div",
|
|
22
|
+
{ className: "flex items-center gap-2" }, // wrapper div for DaisyUI
|
|
23
|
+
label && createElement("span", { className: "fab-item-label" }, label), // label outside button
|
|
24
|
+
createElement(
|
|
25
|
+
"button",
|
|
26
|
+
{
|
|
27
|
+
type: "button",
|
|
28
|
+
className: "btn btn-lg btn-circle fab-item",
|
|
29
|
+
onClick,
|
|
30
|
+
"aria-label": label,
|
|
31
|
+
},
|
|
32
|
+
icon // icon inside button
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
export const Fab = component((props: FabProps) => {
|
|
40
|
+
const { mainIcon, children, position = "bottom-right", direction = "up" } = props;
|
|
41
|
+
const [open, setOpen] = useState(false);
|
|
42
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
43
|
+
|
|
44
|
+
const toggleOpen = () => setOpen(!open);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
48
|
+
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
49
|
+
setOpen(false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
53
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
return createElement(
|
|
57
|
+
"div",
|
|
58
|
+
{ ref: containerRef, className: `fab ${position} ${direction}` },
|
|
59
|
+
// Main button
|
|
60
|
+
createElement(
|
|
61
|
+
"div",
|
|
62
|
+
{
|
|
63
|
+
tabIndex: 0,
|
|
64
|
+
role: "button",
|
|
65
|
+
className: "btn btn-lg btn-circle fab-main-btn",
|
|
66
|
+
onClick: toggleOpen,
|
|
67
|
+
},
|
|
68
|
+
mainIcon
|
|
69
|
+
),
|
|
70
|
+
// Children (DaisyUI handles visibility/animation)
|
|
71
|
+
children
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
export default Fab;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useState, useEffect, component, createElement, VNode } from "vaderjs";
|
|
2
|
+
import Button from "../Button";
|
|
3
|
+
|
|
4
|
+
export function useModal(initialOpen = false) {
|
|
5
|
+
const [isOpen, setIsOpen] = useState(initialOpen);
|
|
6
|
+
const open = () => setIsOpen(true);
|
|
7
|
+
const close = () => setIsOpen(false);
|
|
8
|
+
const toggle = () => setIsOpen(!isOpen);
|
|
9
|
+
return { isOpen, open, close, toggle };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ModalProps {
|
|
13
|
+
isOpen: boolean;
|
|
14
|
+
onClose?: () => void;
|
|
15
|
+
title?: string;
|
|
16
|
+
children?: VNode | VNode[] | string;
|
|
17
|
+
size?: string; // DaisyUI sizes: w-11/12 max-w-md etc.
|
|
18
|
+
placement?: "top" | "middle" | "bottom";
|
|
19
|
+
horizontal?: "start" | "center" | "end"; // horizontal alignment
|
|
20
|
+
backdrop?: boolean; // show backdrop or not
|
|
21
|
+
openClass?: string; // extra classes when modal is open
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ModalActionProps {
|
|
25
|
+
children: VNode | VNode[] | string;
|
|
26
|
+
onClick?: () => void;
|
|
27
|
+
closeModal?: boolean;
|
|
28
|
+
close?: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Utility to get focusable elements inside the modal
|
|
32
|
+
function getFocusableElements(container: HTMLElement): HTMLElement[] {
|
|
33
|
+
return Array.from(
|
|
34
|
+
container.querySelectorAll(
|
|
35
|
+
'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
|
|
36
|
+
)
|
|
37
|
+
) as HTMLElement[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const Modal = component((props: ModalProps) => {
|
|
41
|
+
const {
|
|
42
|
+
isOpen,
|
|
43
|
+
onClose,
|
|
44
|
+
title,
|
|
45
|
+
children,
|
|
46
|
+
size = "w-11/12 max-w-md",
|
|
47
|
+
placement = "middle",
|
|
48
|
+
horizontal = "center",
|
|
49
|
+
backdrop = true,
|
|
50
|
+
openClass = "",
|
|
51
|
+
} = props;
|
|
52
|
+
|
|
53
|
+
let modalRef: HTMLDivElement | null = null;
|
|
54
|
+
|
|
55
|
+
const handleClose = () => {
|
|
56
|
+
if (onClose) onClose();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// ESC key & focus trap
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!isOpen) return;
|
|
62
|
+
|
|
63
|
+
const handleKey = (e: KeyboardEvent) => {
|
|
64
|
+
if (e.key === "Escape") handleClose();
|
|
65
|
+
if (e.key === "Tab" && modalRef) {
|
|
66
|
+
const focusables = getFocusableElements(modalRef);
|
|
67
|
+
if (focusables.length === 0) return;
|
|
68
|
+
const first = focusables[0];
|
|
69
|
+
const last = focusables[focusables.length - 1];
|
|
70
|
+
|
|
71
|
+
if (e.shiftKey) {
|
|
72
|
+
if (document.activeElement === first) {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
last.focus();
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
if (document.activeElement === last) {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
first.focus();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
document.addEventListener("keydown", handleKey);
|
|
86
|
+
return () => document.removeEventListener("keydown", handleKey);
|
|
87
|
+
}, [isOpen]);
|
|
88
|
+
|
|
89
|
+
// Autofocus first element
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (isOpen && modalRef) {
|
|
92
|
+
const focusables = getFocusableElements(modalRef);
|
|
93
|
+
if (focusables.length > 0) focusables[0].focus();
|
|
94
|
+
}
|
|
95
|
+
}, [isOpen]);
|
|
96
|
+
|
|
97
|
+
if (!isOpen) return null;
|
|
98
|
+
|
|
99
|
+
// DaisyUI placement classes
|
|
100
|
+
const placementClass =
|
|
101
|
+
placement === "top"
|
|
102
|
+
? "modal-top"
|
|
103
|
+
: placement === "bottom"
|
|
104
|
+
? "modal-bottom"
|
|
105
|
+
: "modal-middle"; // default
|
|
106
|
+
|
|
107
|
+
const horizontalClass =
|
|
108
|
+
horizontal === "start"
|
|
109
|
+
? "modal-start"
|
|
110
|
+
: horizontal === "end"
|
|
111
|
+
? "modal-end"
|
|
112
|
+
: "modal-center"; // center default
|
|
113
|
+
|
|
114
|
+
return createElement(
|
|
115
|
+
"div",
|
|
116
|
+
{
|
|
117
|
+
className: `modal modal-open ${placementClass} ${openClass}`,
|
|
118
|
+
"aria-modal": "true",
|
|
119
|
+
role: "dialog",
|
|
120
|
+
},
|
|
121
|
+
backdrop &&
|
|
122
|
+
createElement("div", {
|
|
123
|
+
className: "modal-backdrop",
|
|
124
|
+
onClick: handleClose,
|
|
125
|
+
}),
|
|
126
|
+
createElement(
|
|
127
|
+
"div",
|
|
128
|
+
{
|
|
129
|
+
className: `modal-box ${size} relative`,
|
|
130
|
+
ref: (el: HTMLDivElement) => (modalRef = el),
|
|
131
|
+
},
|
|
132
|
+
title && createElement("h3", { className: "font-bold text-lg" }, title),
|
|
133
|
+
children
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
export const ModalAction = component((props: ModalActionProps) => {
|
|
139
|
+
const { children, onClick, closeModal, close } = props;
|
|
140
|
+
|
|
141
|
+
const handleClick = () => {
|
|
142
|
+
if (onClick) onClick();
|
|
143
|
+
if (closeModal && close) close();
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return createElement(Button, { onClick: handleClick }, children);
|
|
147
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useState, component, createElement, VNode } from "vaderjs";
|
|
2
|
+
|
|
3
|
+
export type SwapProps = {
|
|
4
|
+
on?: VNode | VNode[] | string;
|
|
5
|
+
off?: VNode | VNode[] | string;
|
|
6
|
+
indeterminate?: VNode | VNode[] | string;
|
|
7
|
+
active?: boolean; // Controlled toggle
|
|
8
|
+
rotate?: boolean;
|
|
9
|
+
flip?: boolean;
|
|
10
|
+
className?: string;
|
|
11
|
+
onChange?: (active: boolean) => void;
|
|
12
|
+
clickable?: boolean; // If true, label click toggles state
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const Swap = component((props: SwapProps) => {
|
|
16
|
+
const { on, off, indeterminate, active, rotate, flip, className, onChange, clickable } = props;
|
|
17
|
+
|
|
18
|
+
const [internalActive, setInternalActive] = useState(active ?? false);
|
|
19
|
+
const isControlled = active !== undefined;
|
|
20
|
+
const currentActive = isControlled ? active : internalActive;
|
|
21
|
+
|
|
22
|
+
const handleClick = () => {
|
|
23
|
+
if (!clickable) return;
|
|
24
|
+
const newState = !currentActive;
|
|
25
|
+
if (!isControlled) setInternalActive(newState);
|
|
26
|
+
if (onChange) onChange(newState);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const swapClasses = ["swap"];
|
|
30
|
+
if (rotate) swapClasses.push("swap-rotate");
|
|
31
|
+
if (flip) swapClasses.push("swap-flip");
|
|
32
|
+
if (currentActive) swapClasses.push("swap-active");
|
|
33
|
+
if (className) swapClasses.push(className);
|
|
34
|
+
|
|
35
|
+
return createElement(
|
|
36
|
+
"label",
|
|
37
|
+
{ className: swapClasses.join(" "), onClick: handleClick },
|
|
38
|
+
// Only render checkbox if not clickable or for accessibility
|
|
39
|
+
!clickable && createElement("input", {
|
|
40
|
+
type: "checkbox",
|
|
41
|
+
checked: currentActive,
|
|
42
|
+
onChange: (e: Event) => {
|
|
43
|
+
const checked = (e.target as HTMLInputElement).checked;
|
|
44
|
+
if (!isControlled) setInternalActive(checked);
|
|
45
|
+
if (onChange) onChange(checked);
|
|
46
|
+
}
|
|
47
|
+
}),
|
|
48
|
+
on && createElement("div", { className: "swap-on" }, on),
|
|
49
|
+
off && createElement("div", { className: "swap-off" }, off),
|
|
50
|
+
indeterminate && createElement("div", { className: "swap-indeterminate" }, indeterminate)
|
|
51
|
+
);
|
|
52
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { component, createElement, useState, VNode } from "vaderjs";
|
|
2
|
+
|
|
3
|
+
export type ThemeOption = {
|
|
4
|
+
value: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
icon?: VNode; // optional icon
|
|
7
|
+
ariaLabel?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ThemeControllerProps = {
|
|
11
|
+
type?: "checkbox" | "radio" | "toggle" | "swap" | "buttons" | "dropdown"; // type of input
|
|
12
|
+
value?: string | boolean; // initial value
|
|
13
|
+
options?: ThemeOption[]; // for radios, buttons, dropdown
|
|
14
|
+
className?: string;
|
|
15
|
+
direction?: "vertical" | "horizontal"; // for buttons/radios
|
|
16
|
+
onChange?: (value: string | boolean) => void;
|
|
17
|
+
swapRotate?: boolean;
|
|
18
|
+
swapFlip?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const ThemeController = component((props: ThemeControllerProps) => {
|
|
22
|
+
const {
|
|
23
|
+
type = "checkbox",
|
|
24
|
+
value,
|
|
25
|
+
options = [],
|
|
26
|
+
className,
|
|
27
|
+
direction = "horizontal",
|
|
28
|
+
onChange,
|
|
29
|
+
swapRotate,
|
|
30
|
+
swapFlip,
|
|
31
|
+
} = props;
|
|
32
|
+
|
|
33
|
+
const [state, setState] = useState(value ?? (type === "checkbox" || type === "toggle" || type === "swap" ? false : ""));
|
|
34
|
+
|
|
35
|
+
const handleChange = (val: string | boolean) => {
|
|
36
|
+
setState(val);
|
|
37
|
+
if (onChange) onChange(val);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Checkbox / Toggle / Swap
|
|
41
|
+
if (type === "checkbox" || type === "toggle") {
|
|
42
|
+
return createElement("input", {
|
|
43
|
+
type: "checkbox",
|
|
44
|
+
className: `theme-controller ${type} ${className || ""}`,
|
|
45
|
+
checked: state as boolean,
|
|
46
|
+
onChange: (e: Event) => handleChange((e.target as HTMLInputElement).checked),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (type === "swap") {
|
|
51
|
+
// swap-on / swap-off
|
|
52
|
+
const [checked, setChecked] = useState(state as boolean);
|
|
53
|
+
return createElement(
|
|
54
|
+
"label",
|
|
55
|
+
{
|
|
56
|
+
className: `swap ${swapRotate ? "swap-rotate" : ""} ${swapFlip ? "swap-flip" : ""} ${checked ? "swap-active" : ""} ${className || ""}`,
|
|
57
|
+
onClick: () => handleChange(!checked),
|
|
58
|
+
},
|
|
59
|
+
createElement("input", {
|
|
60
|
+
type: "checkbox",
|
|
61
|
+
className: "theme-controller hidden",
|
|
62
|
+
checked,
|
|
63
|
+
onChange: (e: Event) => handleChange((e.target as HTMLInputElement).checked),
|
|
64
|
+
}),
|
|
65
|
+
options[0]?.icon && createElement("div", { className: "swap-off" }, options[0].icon),
|
|
66
|
+
options[1]?.icon && createElement("div", { className: "swap-on" }, options[1].icon)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Radio / Buttons / Dropdown
|
|
71
|
+
if (type === "radio" || type === "buttons") {
|
|
72
|
+
const containerClass = type === "buttons" ? `join ${direction === "vertical" ? "join-vertical" : ""}` : "";
|
|
73
|
+
return createElement(
|
|
74
|
+
"div",
|
|
75
|
+
{ className: `${containerClass} ${className || ""}` },
|
|
76
|
+
options.map(opt =>
|
|
77
|
+
createElement("input", {
|
|
78
|
+
type: "radio",
|
|
79
|
+
name: type === "radio" ? "theme-radios" : "theme-buttons",
|
|
80
|
+
className: `theme-controller ${type === "buttons" ? "btn join-item" : "radio"} ${opt?.ariaLabel ? "" : ""}`,
|
|
81
|
+
value: opt.value,
|
|
82
|
+
checked: state === opt.value,
|
|
83
|
+
"aria-label": opt.ariaLabel ?? opt.label ?? opt.value,
|
|
84
|
+
onChange: () => handleChange(opt.value),
|
|
85
|
+
})
|
|
86
|
+
)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (type === "dropdown") {
|
|
91
|
+
return createElement(
|
|
92
|
+
"div",
|
|
93
|
+
{ className: `dropdown ${className || ""}` },
|
|
94
|
+
createElement(
|
|
95
|
+
"div",
|
|
96
|
+
{ tabIndex: 0, role: "button", className: "btn m-1" },
|
|
97
|
+
"Theme",
|
|
98
|
+
createElement(
|
|
99
|
+
"svg",
|
|
100
|
+
{
|
|
101
|
+
width: "12px",
|
|
102
|
+
height: "12px",
|
|
103
|
+
className: "inline-block h-2 w-2 fill-current opacity-60",
|
|
104
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
105
|
+
viewBox: "0 0 2048 2048",
|
|
106
|
+
},
|
|
107
|
+
createElement("path", { d: "M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z" })
|
|
108
|
+
)
|
|
109
|
+
),
|
|
110
|
+
createElement(
|
|
111
|
+
"ul",
|
|
112
|
+
{ tabIndex: -1, className: "dropdown-content bg-base-300 rounded-box z-1 w-52 p-2 shadow-2xl" },
|
|
113
|
+
options.map(opt =>
|
|
114
|
+
createElement(
|
|
115
|
+
"li",
|
|
116
|
+
{},
|
|
117
|
+
createElement("input", {
|
|
118
|
+
type: "radio",
|
|
119
|
+
name: "theme-dropdown",
|
|
120
|
+
className: "theme-controller w-full btn btn-sm btn-block btn-ghost justify-start",
|
|
121
|
+
value: opt.value,
|
|
122
|
+
checked: state === opt.value,
|
|
123
|
+
"aria-label": opt.ariaLabel ?? opt.label ?? opt.value,
|
|
124
|
+
onChange: () => handleChange(opt.value),
|
|
125
|
+
})
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { component, createElement, VNode } from "vaderjs";
|
|
2
|
+
|
|
3
|
+
export type AccordionItem = {
|
|
4
|
+
title: string | VNode;
|
|
5
|
+
content: string | VNode;
|
|
6
|
+
value?: string; // for radio inputs
|
|
7
|
+
open?: boolean; // for details
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type AccordionProps = {
|
|
11
|
+
items: AccordionItem[];
|
|
12
|
+
type?: "radio" | "details"; // radio inputs or details elements
|
|
13
|
+
name?: string; // radio group name
|
|
14
|
+
arrow?: boolean; // add arrow icon
|
|
15
|
+
plus?: boolean; // add plus/minus icon
|
|
16
|
+
join?: boolean; // join items for seamless borders
|
|
17
|
+
direction?: "vertical" | "horizontal"; // join direction
|
|
18
|
+
className?: string; // additional container class
|
|
19
|
+
border?: boolean; // add border to items
|
|
20
|
+
onChange?: (value: string) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const Accordion = component((props: AccordionProps) => {
|
|
24
|
+
const {
|
|
25
|
+
items,
|
|
26
|
+
type = "radio",
|
|
27
|
+
name = "accordion",
|
|
28
|
+
arrow,
|
|
29
|
+
plus,
|
|
30
|
+
join,
|
|
31
|
+
direction = "vertical",
|
|
32
|
+
className,
|
|
33
|
+
border = true,
|
|
34
|
+
onChange,
|
|
35
|
+
} = props;
|
|
36
|
+
|
|
37
|
+
const containerClass = join ? `join ${direction === "vertical" ? "join-vertical" : ""}` : "";
|
|
38
|
+
|
|
39
|
+
return createElement(
|
|
40
|
+
"div",
|
|
41
|
+
{ className: `${containerClass} ${className || ""}` },
|
|
42
|
+
items.map((item, index) => {
|
|
43
|
+
const itemClasses = ["collapse"];
|
|
44
|
+
if (arrow) itemClasses.push("collapse-arrow");
|
|
45
|
+
if (plus) itemClasses.push("collapse-plus");
|
|
46
|
+
if (border) itemClasses.push("border border-base-300");
|
|
47
|
+
if (join) itemClasses.push("join-item");
|
|
48
|
+
|
|
49
|
+
if (type === "details" && item.open) itemClasses.push("collapse-open");
|
|
50
|
+
|
|
51
|
+
const handleRadioChange = () => {
|
|
52
|
+
if (onChange && item.value) onChange(item.value);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (type === "details") {
|
|
56
|
+
return createElement(
|
|
57
|
+
"details",
|
|
58
|
+
{
|
|
59
|
+
className: itemClasses.join(" "),
|
|
60
|
+
open: !!item.open,
|
|
61
|
+
},
|
|
62
|
+
createElement("summary", { className: "collapse-title font-semibold" }, item.title),
|
|
63
|
+
createElement("div", { className: "collapse-content text-sm" }, item.content)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// radio type
|
|
68
|
+
return createElement(
|
|
69
|
+
"div",
|
|
70
|
+
{ className: itemClasses.join(" ") },
|
|
71
|
+
createElement("input", {
|
|
72
|
+
type: "radio",
|
|
73
|
+
name,
|
|
74
|
+
checked: index === 0,
|
|
75
|
+
onChange: handleRadioChange,
|
|
76
|
+
}),
|
|
77
|
+
createElement("div", { className: "collapse-title font-semibold" }, item.title),
|
|
78
|
+
createElement("div", { className: "collapse-content text-sm" }, item.content)
|
|
79
|
+
);
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
});
|