@holmdigital/components 1.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/README.md +46 -0
- package/dist/Button/Button.js +117 -0
- package/dist/Button/Button.mjs +6 -0
- package/dist/Checkbox/Checkbox.js +82 -0
- package/dist/Checkbox/Checkbox.mjs +6 -0
- package/dist/Dialog/Dialog.js +129 -0
- package/dist/Dialog/Dialog.mjs +6 -0
- package/dist/FormField/FormField.js +110 -0
- package/dist/FormField/FormField.mjs +6 -0
- package/dist/Heading/Heading.js +48 -0
- package/dist/Heading/Heading.mjs +6 -0
- package/dist/Modal/Modal.js +146 -0
- package/dist/Modal/Modal.mjs +7 -0
- package/dist/NavigationMenu/NavigationMenu.js +141 -0
- package/dist/NavigationMenu/NavigationMenu.mjs +6 -0
- package/dist/RadioGroup/RadioGroup.js +103 -0
- package/dist/RadioGroup/RadioGroup.mjs +6 -0
- package/dist/Select/Select.js +157 -0
- package/dist/Select/Select.mjs +12 -0
- package/dist/SkipLink/SkipLink.js +59 -0
- package/dist/SkipLink/SkipLink.mjs +6 -0
- package/dist/Switch/Switch.js +82 -0
- package/dist/Switch/Switch.mjs +6 -0
- package/dist/Toast/Toast.js +123 -0
- package/dist/Toast/Toast.mjs +8 -0
- package/dist/Tooltip/Tooltip.js +121 -0
- package/dist/Tooltip/Tooltip.mjs +12 -0
- package/dist/chunk-2MJRKHPL.mjs +98 -0
- package/dist/chunk-5RKBS475.mjs +58 -0
- package/dist/chunk-C5M6C7KT.mjs +84 -0
- package/dist/chunk-GK4BYT56.mjs +117 -0
- package/dist/chunk-HALLFO25.mjs +22 -0
- package/dist/chunk-LZ42XDDI.mjs +105 -0
- package/dist/chunk-MKKQLWGK.mjs +35 -0
- package/dist/chunk-NDYRGXQ6.mjs +93 -0
- package/dist/chunk-NOE5QKC2.mjs +58 -0
- package/dist/chunk-PLT5CAFO.mjs +86 -0
- package/dist/chunk-V2JYAFB7.mjs +130 -0
- package/dist/chunk-W4ZHBRFT.mjs +14 -0
- package/dist/chunk-YMSNGQN6.mjs +79 -0
- package/dist/index.js +1256 -0
- package/dist/index.mjs +308 -0
- package/package.json +113 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// src/NavigationMenu/NavigationMenu.tsx
|
|
2
|
+
import { useState, useRef, useEffect, forwardRef } from "react";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
var NavigationMenu = forwardRef(
|
|
5
|
+
({ items, className, "aria-label": ariaLabel = "Main Navigation" }, ref) => {
|
|
6
|
+
return /* @__PURE__ */ jsx(
|
|
7
|
+
"nav",
|
|
8
|
+
{
|
|
9
|
+
ref,
|
|
10
|
+
className: `flex items-center ${className || ""}`,
|
|
11
|
+
"aria-label": ariaLabel,
|
|
12
|
+
children: /* @__PURE__ */ jsx("ul", { className: "flex flex-wrap gap-2 m-0 p-0 list-none", children: items.map((item, index) => /* @__PURE__ */ jsx(MenuItem, { item }, index)) })
|
|
13
|
+
}
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
);
|
|
17
|
+
NavigationMenu.displayName = "NavigationMenu";
|
|
18
|
+
var MenuItem = ({ item }) => {
|
|
19
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
20
|
+
const containerRef = useRef(null);
|
|
21
|
+
const timeoutRef = useRef();
|
|
22
|
+
const hasChildren = item.children && item.children.length > 0;
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const handleClickOutside = (event) => {
|
|
25
|
+
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
|
26
|
+
setIsOpen(false);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
if (isOpen) {
|
|
30
|
+
document.addEventListener("click", handleClickOutside);
|
|
31
|
+
}
|
|
32
|
+
return () => document.removeEventListener("click", handleClickOutside);
|
|
33
|
+
}, [isOpen]);
|
|
34
|
+
const handleKeyDown = (e) => {
|
|
35
|
+
if (e.key === "Escape" && isOpen) {
|
|
36
|
+
setIsOpen(false);
|
|
37
|
+
const trigger = containerRef.current?.querySelector("button");
|
|
38
|
+
trigger?.focus();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const handleMouseEnter = () => {
|
|
42
|
+
clearTimeout(timeoutRef.current);
|
|
43
|
+
setIsOpen(true);
|
|
44
|
+
};
|
|
45
|
+
const handleMouseLeave = () => {
|
|
46
|
+
timeoutRef.current = setTimeout(() => setIsOpen(false), 200);
|
|
47
|
+
};
|
|
48
|
+
return /* @__PURE__ */ jsx(
|
|
49
|
+
"li",
|
|
50
|
+
{
|
|
51
|
+
ref: containerRef,
|
|
52
|
+
className: "relative group",
|
|
53
|
+
onKeyDown: handleKeyDown,
|
|
54
|
+
onMouseEnter: hasChildren ? handleMouseEnter : void 0,
|
|
55
|
+
onMouseLeave: hasChildren ? handleMouseLeave : void 0,
|
|
56
|
+
children: hasChildren ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
57
|
+
/* @__PURE__ */ jsxs(
|
|
58
|
+
"button",
|
|
59
|
+
{
|
|
60
|
+
onClick: () => setIsOpen(!isOpen),
|
|
61
|
+
"aria-expanded": isOpen,
|
|
62
|
+
"aria-haspopup": "true",
|
|
63
|
+
className: `
|
|
64
|
+
flex items-center gap-1 px-4 py-2 rounded-md
|
|
65
|
+
text-slate-700 font-medium hover:bg-slate-100 focus:bg-slate-100
|
|
66
|
+
transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500
|
|
67
|
+
`,
|
|
68
|
+
children: [
|
|
69
|
+
item.label,
|
|
70
|
+
/* @__PURE__ */ jsx(
|
|
71
|
+
"svg",
|
|
72
|
+
{
|
|
73
|
+
className: `w-4 h-4 transition-transform ${isOpen ? "rotate-180" : ""}`,
|
|
74
|
+
fill: "none",
|
|
75
|
+
viewBox: "0 0 24 24",
|
|
76
|
+
stroke: "currentColor",
|
|
77
|
+
children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" })
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
),
|
|
83
|
+
/* @__PURE__ */ jsx(
|
|
84
|
+
"ul",
|
|
85
|
+
{
|
|
86
|
+
className: `
|
|
87
|
+
absolute top-full left-0 mt-1 min-w-[200px]
|
|
88
|
+
bg-white border border-slate-200 rounded-lg shadow-xl
|
|
89
|
+
py-2 z-50
|
|
90
|
+
transform origin-top transition-all duration-200
|
|
91
|
+
${isOpen ? "opacity-100 scale-100 visible" : "opacity-0 scale-95 invisible"}
|
|
92
|
+
`,
|
|
93
|
+
children: item.children?.map((child, idx) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
|
|
94
|
+
"a",
|
|
95
|
+
{
|
|
96
|
+
href: child.href,
|
|
97
|
+
className: "block px-4 py-2 text-slate-700 hover:bg-slate-50 hover:text-primary-600 focus:bg-slate-50 focus:text-primary-600 focus:outline-none",
|
|
98
|
+
children: child.label
|
|
99
|
+
}
|
|
100
|
+
) }, idx))
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
] }) : /* @__PURE__ */ jsx(
|
|
104
|
+
"a",
|
|
105
|
+
{
|
|
106
|
+
href: item.href,
|
|
107
|
+
className: "block px-4 py-2 text-slate-700 font-medium rounded-md hover:bg-slate-100 focus:bg-slate-100 transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500",
|
|
108
|
+
children: item.label
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export {
|
|
116
|
+
NavigationMenu
|
|
117
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Dialog
|
|
3
|
+
} from "./chunk-LZ42XDDI.mjs";
|
|
4
|
+
|
|
5
|
+
// src/Modal/Modal.tsx
|
|
6
|
+
import { forwardRef } from "react";
|
|
7
|
+
import { jsx } from "react/jsx-runtime";
|
|
8
|
+
var Modal = forwardRef((props, ref) => {
|
|
9
|
+
return /* @__PURE__ */ jsx(
|
|
10
|
+
Dialog,
|
|
11
|
+
{
|
|
12
|
+
ref,
|
|
13
|
+
...props,
|
|
14
|
+
className: `max-w-2xl ${props.className || ""}`
|
|
15
|
+
}
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
Modal.displayName = "Modal";
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
Modal
|
|
22
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// src/Dialog/Dialog.tsx
|
|
2
|
+
import { useRef, useEffect, forwardRef, useImperativeHandle } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
var CloseIcon = () => /* @__PURE__ */ jsxs(
|
|
5
|
+
"svg",
|
|
6
|
+
{
|
|
7
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
8
|
+
width: "24",
|
|
9
|
+
height: "24",
|
|
10
|
+
viewBox: "0 0 24 24",
|
|
11
|
+
fill: "none",
|
|
12
|
+
stroke: "currentColor",
|
|
13
|
+
strokeWidth: "2",
|
|
14
|
+
strokeLinecap: "round",
|
|
15
|
+
strokeLinejoin: "round",
|
|
16
|
+
className: "h-5 w-5",
|
|
17
|
+
children: [
|
|
18
|
+
/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }),
|
|
19
|
+
/* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
var Dialog = forwardRef(
|
|
24
|
+
({ isOpen, onClose, title, children, variant = "default", description, className, ...props }, ref) => {
|
|
25
|
+
const dialogRef = useRef(null);
|
|
26
|
+
useImperativeHandle(ref, () => dialogRef.current);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const dialog = dialogRef.current;
|
|
29
|
+
if (!dialog) return;
|
|
30
|
+
if (isOpen) {
|
|
31
|
+
if (!dialog.open) {
|
|
32
|
+
dialog.showModal();
|
|
33
|
+
document.body.style.overflow = "hidden";
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
if (dialog.open) {
|
|
37
|
+
dialog.close();
|
|
38
|
+
document.body.style.overflow = "";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}, [isOpen]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const dialog = dialogRef.current;
|
|
44
|
+
if (!dialog) return;
|
|
45
|
+
const handleClose = () => {
|
|
46
|
+
onClose();
|
|
47
|
+
document.body.style.overflow = "";
|
|
48
|
+
};
|
|
49
|
+
dialog.addEventListener("close", handleClose);
|
|
50
|
+
const handleBackdropClick = (e) => {
|
|
51
|
+
const rect = dialog.getBoundingClientRect();
|
|
52
|
+
const isInDialog = rect.top <= e.clientY && e.clientY <= rect.top + rect.height && rect.left <= e.clientX && e.clientX <= rect.left + rect.width;
|
|
53
|
+
if (!isInDialog) {
|
|
54
|
+
dialog.close();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
dialog.addEventListener("click", handleBackdropClick);
|
|
58
|
+
return () => {
|
|
59
|
+
dialog.removeEventListener("close", handleClose);
|
|
60
|
+
dialog.removeEventListener("click", handleBackdropClick);
|
|
61
|
+
};
|
|
62
|
+
}, [onClose]);
|
|
63
|
+
return /* @__PURE__ */ jsxs(
|
|
64
|
+
"dialog",
|
|
65
|
+
{
|
|
66
|
+
ref: dialogRef,
|
|
67
|
+
className: `
|
|
68
|
+
backdrop:bg-slate-900/50 backdrop:backdrop-blur-sm
|
|
69
|
+
open:animate-in open:fade-in-0 open:zoom-in-95
|
|
70
|
+
bg-white rounded-xl shadow-2xl ring-1 ring-slate-900/5
|
|
71
|
+
w-full max-w-lg p-0
|
|
72
|
+
${className || ""}
|
|
73
|
+
`,
|
|
74
|
+
"aria-labelledby": "dialog-title",
|
|
75
|
+
"aria-describedby": description ? "dialog-desc" : void 0,
|
|
76
|
+
...props,
|
|
77
|
+
children: [
|
|
78
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-slate-100", children: [
|
|
79
|
+
/* @__PURE__ */ jsx("h2", { id: "dialog-title", className: `text-lg font-semibold ${variant === "alert" ? "text-red-600" : "text-slate-900"}`, children: title }),
|
|
80
|
+
/* @__PURE__ */ jsx(
|
|
81
|
+
"button",
|
|
82
|
+
{
|
|
83
|
+
onClick: () => {
|
|
84
|
+
dialogRef.current?.close();
|
|
85
|
+
},
|
|
86
|
+
className: "p-1 rounded-md text-slate-400 hover:text-slate-500 hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-primary-500 transition-colors",
|
|
87
|
+
"aria-label": "Close dialog",
|
|
88
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
] }),
|
|
92
|
+
/* @__PURE__ */ jsxs("div", { className: "px-6 py-4", children: [
|
|
93
|
+
description && /* @__PURE__ */ jsx("p", { id: "dialog-desc", className: "text-sm text-slate-500 mb-4", children: description }),
|
|
94
|
+
children
|
|
95
|
+
] })
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
Dialog.displayName = "Dialog";
|
|
102
|
+
|
|
103
|
+
export {
|
|
104
|
+
Dialog
|
|
105
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/SkipLink/SkipLink.tsx
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
var SkipLink = forwardRef(
|
|
5
|
+
({ targetId = "main", className, style, children, ...props }, ref) => {
|
|
6
|
+
return /* @__PURE__ */ jsx(
|
|
7
|
+
"a",
|
|
8
|
+
{
|
|
9
|
+
ref,
|
|
10
|
+
href: `#${targetId}`,
|
|
11
|
+
className: `
|
|
12
|
+
fixed top-4 left-4 z-50
|
|
13
|
+
px-4 py-3
|
|
14
|
+
bg-white text-slate-900 font-medium
|
|
15
|
+
rounded-md shadow-lg ring-2 ring-slate-900
|
|
16
|
+
transition-transform duration-200
|
|
17
|
+
-translate-y-[150%] focus:translate-y-0
|
|
18
|
+
${className || ""}
|
|
19
|
+
`,
|
|
20
|
+
style: {
|
|
21
|
+
// Ensure it stays on top of everything
|
|
22
|
+
zIndex: 9999,
|
|
23
|
+
...style
|
|
24
|
+
},
|
|
25
|
+
...props,
|
|
26
|
+
children: children || "Hoppa till huvudinneh\xE5ll"
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
SkipLink.displayName = "SkipLink";
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
SkipLink
|
|
35
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// src/Button/Button.tsx
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
var Button = forwardRef(
|
|
5
|
+
({
|
|
6
|
+
children,
|
|
7
|
+
variant = "primary",
|
|
8
|
+
size = "medium",
|
|
9
|
+
isLoading,
|
|
10
|
+
disabled,
|
|
11
|
+
className = "",
|
|
12
|
+
...props
|
|
13
|
+
}, ref) => {
|
|
14
|
+
const baseStyles = {
|
|
15
|
+
display: "inline-flex",
|
|
16
|
+
alignItems: "center",
|
|
17
|
+
justifyContent: "center",
|
|
18
|
+
borderRadius: "4px",
|
|
19
|
+
border: "none",
|
|
20
|
+
cursor: disabled || isLoading ? "not-allowed" : "pointer",
|
|
21
|
+
fontFamily: "inherit",
|
|
22
|
+
fontWeight: "600",
|
|
23
|
+
transition: "all 0.2s ease",
|
|
24
|
+
// Garantera synlig fokusindikator (WCAG 2.4.7)
|
|
25
|
+
outlineOffset: "2px"
|
|
26
|
+
};
|
|
27
|
+
const variants = {
|
|
28
|
+
primary: {
|
|
29
|
+
background: "#0056b3",
|
|
30
|
+
// AA Large, AAA Normal mot vit text
|
|
31
|
+
color: "#ffffff"
|
|
32
|
+
},
|
|
33
|
+
secondary: {
|
|
34
|
+
background: "#f8f9fa",
|
|
35
|
+
color: "#212529",
|
|
36
|
+
border: "1px solid #dee2e6"
|
|
37
|
+
},
|
|
38
|
+
danger: {
|
|
39
|
+
background: "#dc3545",
|
|
40
|
+
color: "#ffffff"
|
|
41
|
+
},
|
|
42
|
+
ghost: {
|
|
43
|
+
background: "transparent",
|
|
44
|
+
color: "#0056b3"
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const sizes = {
|
|
48
|
+
small: {
|
|
49
|
+
padding: "0.25rem 0.5rem",
|
|
50
|
+
fontSize: "0.875rem",
|
|
51
|
+
minHeight: "32px"
|
|
52
|
+
// OBS: Kan bryta mot 44px om inte hanteras med margin
|
|
53
|
+
},
|
|
54
|
+
medium: {
|
|
55
|
+
padding: "0.5rem 1rem",
|
|
56
|
+
fontSize: "1rem",
|
|
57
|
+
minHeight: "44px"
|
|
58
|
+
// Touch target safe
|
|
59
|
+
},
|
|
60
|
+
large: {
|
|
61
|
+
padding: "0.75rem 1.5rem",
|
|
62
|
+
fontSize: "1.25rem",
|
|
63
|
+
minHeight: "56px"
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const style = {
|
|
67
|
+
...baseStyles,
|
|
68
|
+
...variants[variant],
|
|
69
|
+
...sizes[size],
|
|
70
|
+
opacity: disabled || isLoading ? 0.65 : 1
|
|
71
|
+
};
|
|
72
|
+
return /* @__PURE__ */ jsxs(
|
|
73
|
+
"button",
|
|
74
|
+
{
|
|
75
|
+
ref,
|
|
76
|
+
style,
|
|
77
|
+
disabled: disabled || isLoading,
|
|
78
|
+
"aria-busy": isLoading,
|
|
79
|
+
tabIndex: props.tabIndex,
|
|
80
|
+
...props,
|
|
81
|
+
children: [
|
|
82
|
+
isLoading ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { marginRight: "8px" }, children: "\u23F3" }) : null,
|
|
83
|
+
children
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
Button.displayName = "Button";
|
|
90
|
+
|
|
91
|
+
export {
|
|
92
|
+
Button
|
|
93
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/Checkbox/Checkbox.tsx
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { Check } from "lucide-react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
var Checkbox = forwardRef(
|
|
6
|
+
({ className = "", checked, onCheckedChange, onChange, label, disabled, id, ...props }, ref) => {
|
|
7
|
+
const handleChange = (e) => {
|
|
8
|
+
onCheckedChange?.(e.target.checked);
|
|
9
|
+
onChange?.(e);
|
|
10
|
+
};
|
|
11
|
+
const generatedId = id || `checkbox-${Math.random().toString(36).substr(2, 9)}`;
|
|
12
|
+
return /* @__PURE__ */ jsxs("div", { className: `flex items-start ${className}`, children: [
|
|
13
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center h-5", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
14
|
+
/* @__PURE__ */ jsx(
|
|
15
|
+
"input",
|
|
16
|
+
{
|
|
17
|
+
id: generatedId,
|
|
18
|
+
type: "checkbox",
|
|
19
|
+
ref,
|
|
20
|
+
className: "peer sr-only",
|
|
21
|
+
checked,
|
|
22
|
+
onChange: handleChange,
|
|
23
|
+
disabled,
|
|
24
|
+
...props
|
|
25
|
+
}
|
|
26
|
+
),
|
|
27
|
+
/* @__PURE__ */ jsx(
|
|
28
|
+
"div",
|
|
29
|
+
{
|
|
30
|
+
className: `
|
|
31
|
+
h-5 w-5 rounded border border-slate-300 bg-white shadow-sm transition-all
|
|
32
|
+
peer-focus:ring-2 peer-focus:ring-primary-500 peer-focus:ring-offset-2
|
|
33
|
+
peer-checked:bg-primary-600 peer-checked:border-primary-600
|
|
34
|
+
peer-disabled:cursor-not-allowed peer-disabled:opacity-50
|
|
35
|
+
hover:border-primary-400
|
|
36
|
+
flex items-center justify-center
|
|
37
|
+
`,
|
|
38
|
+
"aria-hidden": "true",
|
|
39
|
+
children: /* @__PURE__ */ jsx(Check, { className: `h-3.5 w-3.5 text-white transition-opacity ${checked ? "opacity-100" : "opacity-0"}`, strokeWidth: 3 })
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
] }) }),
|
|
43
|
+
/* @__PURE__ */ jsx(
|
|
44
|
+
"label",
|
|
45
|
+
{
|
|
46
|
+
htmlFor: generatedId,
|
|
47
|
+
className: `ml-3 text-sm font-medium ${disabled ? "text-slate-400" : "text-slate-700"} cursor-pointer select-none`,
|
|
48
|
+
children: label
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
] });
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
Checkbox.displayName = "Checkbox";
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
Checkbox
|
|
58
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// src/FormField/FormField.tsx
|
|
2
|
+
import { forwardRef, useId } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
var FormField = forwardRef(
|
|
5
|
+
({
|
|
6
|
+
label,
|
|
7
|
+
error,
|
|
8
|
+
helpText,
|
|
9
|
+
required,
|
|
10
|
+
id,
|
|
11
|
+
className = "",
|
|
12
|
+
style,
|
|
13
|
+
...props
|
|
14
|
+
}, ref) => {
|
|
15
|
+
const generatedId = useId();
|
|
16
|
+
const inputId = id || `input-${generatedId}`;
|
|
17
|
+
const helpTextId = `help-${generatedId}`;
|
|
18
|
+
const errorId = `error-${generatedId}`;
|
|
19
|
+
const describedBy = [
|
|
20
|
+
helpText ? helpTextId : null,
|
|
21
|
+
error ? errorId : null
|
|
22
|
+
].filter(Boolean).join(" ");
|
|
23
|
+
const containerStyle = {
|
|
24
|
+
display: "flex",
|
|
25
|
+
flexDirection: "column",
|
|
26
|
+
marginBottom: "1rem",
|
|
27
|
+
fontFamily: "system-ui, sans-serif",
|
|
28
|
+
...style
|
|
29
|
+
};
|
|
30
|
+
const labelStyle = {
|
|
31
|
+
marginBottom: "0.5rem",
|
|
32
|
+
fontWeight: "600",
|
|
33
|
+
color: "#333"
|
|
34
|
+
};
|
|
35
|
+
const inputStyle = {
|
|
36
|
+
padding: "0.5rem",
|
|
37
|
+
borderRadius: "4px",
|
|
38
|
+
border: error ? "2px solid #dc3545" : "1px solid #ced4da",
|
|
39
|
+
fontSize: "1rem",
|
|
40
|
+
minHeight: "44px"
|
|
41
|
+
// Touch target
|
|
42
|
+
};
|
|
43
|
+
const errorStyle = {
|
|
44
|
+
color: "#dc3545",
|
|
45
|
+
fontSize: "0.875rem",
|
|
46
|
+
marginTop: "0.25rem",
|
|
47
|
+
display: "flex",
|
|
48
|
+
alignItems: "center"
|
|
49
|
+
};
|
|
50
|
+
const helpStyle = {
|
|
51
|
+
color: "#6c757d",
|
|
52
|
+
fontSize: "0.875rem",
|
|
53
|
+
marginTop: "0.25rem"
|
|
54
|
+
};
|
|
55
|
+
return /* @__PURE__ */ jsxs("div", { style: containerStyle, className, children: [
|
|
56
|
+
/* @__PURE__ */ jsxs("label", { htmlFor: inputId, style: labelStyle, children: [
|
|
57
|
+
label,
|
|
58
|
+
required && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { color: "#dc3545", marginLeft: "4px" }, children: "*" }),
|
|
59
|
+
required && /* @__PURE__ */ jsx("span", { className: "sr-only", children: " (obligatoriskt)" })
|
|
60
|
+
] }),
|
|
61
|
+
/* @__PURE__ */ jsx(
|
|
62
|
+
"input",
|
|
63
|
+
{
|
|
64
|
+
ref,
|
|
65
|
+
id: inputId,
|
|
66
|
+
"aria-invalid": !!error,
|
|
67
|
+
"aria-describedby": describedBy || void 0,
|
|
68
|
+
"aria-required": required,
|
|
69
|
+
required,
|
|
70
|
+
style: inputStyle,
|
|
71
|
+
...props
|
|
72
|
+
}
|
|
73
|
+
),
|
|
74
|
+
error && /* @__PURE__ */ jsxs("div", { id: errorId, style: errorStyle, role: "alert", children: [
|
|
75
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { marginRight: "4px" }, children: "\u26A0\uFE0F" }),
|
|
76
|
+
error
|
|
77
|
+
] }),
|
|
78
|
+
helpText && /* @__PURE__ */ jsx("div", { id: helpTextId, style: helpStyle, children: helpText })
|
|
79
|
+
] });
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
FormField.displayName = "FormField";
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
FormField
|
|
86
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// src/Select/Select.tsx
|
|
2
|
+
import { useState, useRef, useEffect, createContext, useContext } from "react";
|
|
3
|
+
import { ChevronDown, Check } from "lucide-react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
var SelectContext = createContext(void 0);
|
|
6
|
+
var Select = ({ value, onChange, children }) => {
|
|
7
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
8
|
+
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
9
|
+
const optionsRef = useRef([]);
|
|
10
|
+
const containerRef = useRef(null);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const handleClickOutside = (event) => {
|
|
13
|
+
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
|
14
|
+
setIsOpen(false);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
18
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
19
|
+
}, []);
|
|
20
|
+
const handleKeyDown = (e) => {
|
|
21
|
+
if (!isOpen) {
|
|
22
|
+
if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
setIsOpen(true);
|
|
25
|
+
setHighlightedIndex(0);
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
switch (e.key) {
|
|
30
|
+
case "ArrowDown":
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
setHighlightedIndex((prev) => Math.min(prev + 1, optionsRef.current.length - 1));
|
|
33
|
+
break;
|
|
34
|
+
case "ArrowUp":
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
setHighlightedIndex((prev) => Math.max(prev - 1, 0));
|
|
37
|
+
break;
|
|
38
|
+
case "Enter":
|
|
39
|
+
case " ":
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
if (highlightedIndex >= 0 && highlightedIndex < optionsRef.current.length) {
|
|
42
|
+
const option = optionsRef.current[highlightedIndex];
|
|
43
|
+
if (option) {
|
|
44
|
+
option.click();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
case "Escape":
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
setIsOpen(false);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
return /* @__PURE__ */ jsx(SelectContext.Provider, { value: { value, onChange, isOpen, setIsOpen, highlightedIndex, setHighlightedIndex, optionsRef }, children: /* @__PURE__ */ jsx(
|
|
55
|
+
"div",
|
|
56
|
+
{
|
|
57
|
+
ref: containerRef,
|
|
58
|
+
className: "relative inline-block text-left w-full",
|
|
59
|
+
onKeyDown: handleKeyDown,
|
|
60
|
+
children
|
|
61
|
+
}
|
|
62
|
+
) });
|
|
63
|
+
};
|
|
64
|
+
var SelectTrigger = ({ children, className = "", placeholder = "Select..." }) => {
|
|
65
|
+
const context = useContext(SelectContext);
|
|
66
|
+
if (!context) throw new Error("SelectTrigger must be used within Select");
|
|
67
|
+
return /* @__PURE__ */ jsxs(
|
|
68
|
+
"button",
|
|
69
|
+
{
|
|
70
|
+
type: "button",
|
|
71
|
+
onClick: () => context.setIsOpen(!context.isOpen),
|
|
72
|
+
"aria-haspopup": "listbox",
|
|
73
|
+
"aria-expanded": context.isOpen,
|
|
74
|
+
className: `flex items-center justify-between w-full px-4 py-2 text-sm bg-white border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent ${className}`,
|
|
75
|
+
children: [
|
|
76
|
+
/* @__PURE__ */ jsx("span", { className: context.value ? "text-slate-900" : "text-slate-500", children: children || placeholder }),
|
|
77
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: "w-4 h-4 ml-2 opacity-50" })
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
var SelectContent = ({ children }) => {
|
|
83
|
+
const context = useContext(SelectContext);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (context && context.isOpen) {
|
|
86
|
+
context.optionsRef.current = [];
|
|
87
|
+
}
|
|
88
|
+
}, [context?.isOpen]);
|
|
89
|
+
if (!context || !context.isOpen) return null;
|
|
90
|
+
return /* @__PURE__ */ jsx(
|
|
91
|
+
"div",
|
|
92
|
+
{
|
|
93
|
+
className: "absolute z-10 w-full mt-1 bg-white border border-slate-200 rounded-md shadow-lg max-h-60 overflow-auto focus:outline-none",
|
|
94
|
+
role: "listbox",
|
|
95
|
+
children
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
var SelectItem = ({ value, children }) => {
|
|
100
|
+
const context = useContext(SelectContext);
|
|
101
|
+
if (!context) throw new Error("SelectItem must be used within Select");
|
|
102
|
+
const isSelected = context.value === value;
|
|
103
|
+
const index = context.optionsRef.current.length;
|
|
104
|
+
return /* @__PURE__ */ jsxs(
|
|
105
|
+
"div",
|
|
106
|
+
{
|
|
107
|
+
ref: (el) => {
|
|
108
|
+
context.optionsRef.current[index] = el;
|
|
109
|
+
},
|
|
110
|
+
role: "option",
|
|
111
|
+
"aria-selected": isSelected,
|
|
112
|
+
onClick: () => {
|
|
113
|
+
context.onChange(value);
|
|
114
|
+
context.setIsOpen(false);
|
|
115
|
+
},
|
|
116
|
+
className: `flex items-center justify-between px-4 py-2 text-sm cursor-pointer ${context.highlightedIndex === index ? "bg-slate-100" : ""} ${isSelected ? "bg-slate-50 text-slate-900 font-medium" : "text-slate-700"}`,
|
|
117
|
+
children: [
|
|
118
|
+
children,
|
|
119
|
+
isSelected && /* @__PURE__ */ jsx(Check, { className: "w-4 h-4 text-slate-900" })
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export {
|
|
126
|
+
Select,
|
|
127
|
+
SelectTrigger,
|
|
128
|
+
SelectContent,
|
|
129
|
+
SelectItem
|
|
130
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/Heading/Heading.tsx
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
var Heading = React.forwardRef(
|
|
5
|
+
({ level, children, className, ...props }, ref) => {
|
|
6
|
+
const Tag = `h${level}`;
|
|
7
|
+
return /* @__PURE__ */ jsx(Tag, { ref, className, ...props, children });
|
|
8
|
+
}
|
|
9
|
+
);
|
|
10
|
+
Heading.displayName = "Heading";
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
Heading
|
|
14
|
+
};
|