@kreativa/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/README.md +88 -0
- package/dist/index.d.mts +316 -0
- package/dist/index.d.ts +316 -0
- package/dist/index.js +1222 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1178 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +54 -0
- package/tailwind.config.js +56 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1178 @@
|
|
|
1
|
+
// src/components/Layout.tsx
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
function Layout({ children, sidebar }) {
|
|
4
|
+
return /* @__PURE__ */ jsxs("div", { className: "min-h-screen flex", children: [
|
|
5
|
+
sidebar && /* @__PURE__ */ jsx("aside", { className: "w-64 bg-primary-600 text-white flex flex-col", children: sidebar }),
|
|
6
|
+
/* @__PURE__ */ jsx("main", { className: "flex-1 overflow-auto bg-cream-50", children: /* @__PURE__ */ jsx("div", { className: "p-6", children }) })
|
|
7
|
+
] });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/components/Sidebar.tsx
|
|
11
|
+
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
12
|
+
function Sidebar({ header, navigation, footer }) {
|
|
13
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
14
|
+
header && /* @__PURE__ */ jsx2("div", { className: "p-4 border-b border-primary-500", children: header }),
|
|
15
|
+
/* @__PURE__ */ jsx2("nav", { className: "flex-1 p-4 overflow-y-auto", children: navigation }),
|
|
16
|
+
footer && /* @__PURE__ */ jsx2("div", { className: "p-4 border-t border-primary-500", children: footer })
|
|
17
|
+
] });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/components/ServiceSwitcher.tsx
|
|
21
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
22
|
+
function ServiceSwitcher({ services, currentServiceId, onServiceChange }) {
|
|
23
|
+
if (services.length <= 1) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return /* @__PURE__ */ jsx3("div", { className: "mt-2", children: /* @__PURE__ */ jsx3(
|
|
27
|
+
"select",
|
|
28
|
+
{
|
|
29
|
+
value: currentServiceId,
|
|
30
|
+
onChange: (e) => onServiceChange(e.target.value),
|
|
31
|
+
className: "w-full px-3 py-2 rounded-lg text-sm appearance-none cursor-pointer bg-primary-500 text-white border border-primary-400 focus:outline-none focus:ring-2 focus:ring-primary-300",
|
|
32
|
+
children: services.map((service) => /* @__PURE__ */ jsx3("option", { value: service.id, children: service.name }, service.id))
|
|
33
|
+
}
|
|
34
|
+
) });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/components/Button.tsx
|
|
38
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
39
|
+
var variantClasses = {
|
|
40
|
+
primary: "bg-pink-500 hover:bg-pink-600 text-white",
|
|
41
|
+
secondary: "bg-primary-500 hover:bg-primary-600 text-white",
|
|
42
|
+
danger: "bg-red-500 hover:bg-red-600 text-white",
|
|
43
|
+
ghost: "bg-transparent hover:bg-primary-100 text-primary-600"
|
|
44
|
+
};
|
|
45
|
+
var sizeClasses = {
|
|
46
|
+
sm: "px-3 py-1.5 text-sm",
|
|
47
|
+
md: "px-4 py-2 text-base",
|
|
48
|
+
lg: "px-6 py-3 text-lg"
|
|
49
|
+
};
|
|
50
|
+
function Button({
|
|
51
|
+
variant = "primary",
|
|
52
|
+
size = "md",
|
|
53
|
+
className = "",
|
|
54
|
+
children,
|
|
55
|
+
disabled,
|
|
56
|
+
...props
|
|
57
|
+
}) {
|
|
58
|
+
return /* @__PURE__ */ jsx4(
|
|
59
|
+
"button",
|
|
60
|
+
{
|
|
61
|
+
className: `
|
|
62
|
+
inline-flex items-center justify-center rounded-lg font-medium
|
|
63
|
+
transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2
|
|
64
|
+
disabled:opacity-50 disabled:cursor-not-allowed
|
|
65
|
+
${variantClasses[variant]}
|
|
66
|
+
${sizeClasses[size]}
|
|
67
|
+
${className}
|
|
68
|
+
`,
|
|
69
|
+
disabled,
|
|
70
|
+
...props,
|
|
71
|
+
children
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/components/Input.tsx
|
|
77
|
+
import { forwardRef } from "react";
|
|
78
|
+
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
79
|
+
var Input = forwardRef(
|
|
80
|
+
({ label, error, className = "", ...props }, ref) => {
|
|
81
|
+
return /* @__PURE__ */ jsxs3("div", { className: "w-full", children: [
|
|
82
|
+
label && /* @__PURE__ */ jsx5("label", { className: "block text-sm font-medium text-gray-700 mb-1", children: label }),
|
|
83
|
+
/* @__PURE__ */ jsx5(
|
|
84
|
+
"input",
|
|
85
|
+
{
|
|
86
|
+
ref,
|
|
87
|
+
className: `
|
|
88
|
+
w-full px-3 py-2 border rounded-lg
|
|
89
|
+
focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500
|
|
90
|
+
disabled:bg-gray-100 disabled:cursor-not-allowed
|
|
91
|
+
${error ? "border-red-500" : "border-gray-300"}
|
|
92
|
+
${className}
|
|
93
|
+
`,
|
|
94
|
+
...props
|
|
95
|
+
}
|
|
96
|
+
),
|
|
97
|
+
error && /* @__PURE__ */ jsx5("p", { className: "mt-1 text-sm text-red-500", children: error })
|
|
98
|
+
] });
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
Input.displayName = "Input";
|
|
102
|
+
|
|
103
|
+
// src/components/Card.tsx
|
|
104
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
105
|
+
var paddingClasses = {
|
|
106
|
+
none: "",
|
|
107
|
+
sm: "p-3",
|
|
108
|
+
md: "p-4",
|
|
109
|
+
lg: "p-6"
|
|
110
|
+
};
|
|
111
|
+
function Card({ children, className = "", padding = "md" }) {
|
|
112
|
+
return /* @__PURE__ */ jsx6(
|
|
113
|
+
"div",
|
|
114
|
+
{
|
|
115
|
+
className: `
|
|
116
|
+
bg-white rounded-lg shadow-sm border border-gray-200
|
|
117
|
+
${paddingClasses[padding]}
|
|
118
|
+
${className}
|
|
119
|
+
`,
|
|
120
|
+
children
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/components/LoadingSpinner.tsx
|
|
126
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
127
|
+
var sizeClasses2 = {
|
|
128
|
+
sm: "w-4 h-4",
|
|
129
|
+
md: "w-8 h-8",
|
|
130
|
+
lg: "w-12 h-12"
|
|
131
|
+
};
|
|
132
|
+
function LoadingSpinner({ size = "md", fullScreen = false }) {
|
|
133
|
+
const spinner = /* @__PURE__ */ jsx7(
|
|
134
|
+
"div",
|
|
135
|
+
{
|
|
136
|
+
className: `
|
|
137
|
+
animate-spin rounded-full border-2 border-primary-200 border-t-primary-600
|
|
138
|
+
${sizeClasses2[size]}
|
|
139
|
+
`
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
if (fullScreen) {
|
|
143
|
+
return /* @__PURE__ */ jsx7("div", { className: "min-h-screen flex items-center justify-center", children: spinner });
|
|
144
|
+
}
|
|
145
|
+
return spinner;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/components/EmptyState.tsx
|
|
149
|
+
import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
150
|
+
function EmptyState({ icon, title, description, action }) {
|
|
151
|
+
return /* @__PURE__ */ jsxs4("div", { className: "text-center py-16", children: [
|
|
152
|
+
icon && /* @__PURE__ */ jsx8("div", { className: "w-16 h-16 mx-auto mb-4 rounded-full bg-gray-100 flex items-center justify-center", children: icon }),
|
|
153
|
+
/* @__PURE__ */ jsx8("h3", { className: "text-lg font-medium text-gray-800 mb-1", children: title }),
|
|
154
|
+
description && /* @__PURE__ */ jsx8("p", { className: "text-gray-500 mb-4", children: description }),
|
|
155
|
+
action
|
|
156
|
+
] });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/components/Modal.tsx
|
|
160
|
+
import { useEffect } from "react";
|
|
161
|
+
import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
162
|
+
var maxWidthClasses = {
|
|
163
|
+
sm: "max-w-sm",
|
|
164
|
+
md: "max-w-md",
|
|
165
|
+
lg: "max-w-lg",
|
|
166
|
+
xl: "max-w-xl",
|
|
167
|
+
"2xl": "max-w-2xl"
|
|
168
|
+
};
|
|
169
|
+
function Modal({ isOpen, onClose, title, children, maxWidth = "xl" }) {
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
const handleEscape = (e) => {
|
|
172
|
+
if (e.key === "Escape") {
|
|
173
|
+
onClose();
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
if (isOpen) {
|
|
177
|
+
document.addEventListener("keydown", handleEscape);
|
|
178
|
+
document.body.style.overflow = "hidden";
|
|
179
|
+
}
|
|
180
|
+
return () => {
|
|
181
|
+
document.removeEventListener("keydown", handleEscape);
|
|
182
|
+
document.body.style.overflow = "";
|
|
183
|
+
};
|
|
184
|
+
}, [isOpen, onClose]);
|
|
185
|
+
if (!isOpen) return null;
|
|
186
|
+
return /* @__PURE__ */ jsx9(
|
|
187
|
+
"div",
|
|
188
|
+
{
|
|
189
|
+
className: "fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4",
|
|
190
|
+
onClick: (e) => {
|
|
191
|
+
if (e.target === e.currentTarget) {
|
|
192
|
+
onClose();
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
children: /* @__PURE__ */ jsxs5(
|
|
196
|
+
"div",
|
|
197
|
+
{
|
|
198
|
+
className: `bg-white rounded-2xl shadow-2xl w-full ${maxWidthClasses[maxWidth]} p-6 max-h-[90vh] overflow-y-auto`,
|
|
199
|
+
children: [
|
|
200
|
+
/* @__PURE__ */ jsx9("h2", { className: "text-xl font-bold text-gray-800 mb-6", children: title }),
|
|
201
|
+
children
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/components/FormInput.tsx
|
|
210
|
+
import { forwardRef as forwardRef2 } from "react";
|
|
211
|
+
import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
212
|
+
var FormInput = forwardRef2(
|
|
213
|
+
({ label, helpText, error, className = "", ...props }, ref) => {
|
|
214
|
+
return /* @__PURE__ */ jsxs6("div", { children: [
|
|
215
|
+
label && /* @__PURE__ */ jsx10("label", { className: "block text-sm font-medium text-gray-700 mb-2", children: label }),
|
|
216
|
+
/* @__PURE__ */ jsx10(
|
|
217
|
+
"input",
|
|
218
|
+
{
|
|
219
|
+
ref,
|
|
220
|
+
className: `w-full px-4 py-3 rounded-xl border border-gray-200 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent ${error ? "border-red-300 focus:ring-red-500" : ""} ${className}`,
|
|
221
|
+
...props
|
|
222
|
+
}
|
|
223
|
+
),
|
|
224
|
+
helpText && !error && /* @__PURE__ */ jsx10("p", { className: "mt-1 text-xs text-gray-500", children: helpText }),
|
|
225
|
+
error && /* @__PURE__ */ jsx10("p", { className: "mt-1 text-xs text-red-600", children: error })
|
|
226
|
+
] });
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
FormInput.displayName = "FormInput";
|
|
230
|
+
|
|
231
|
+
// src/components/FormTextarea.tsx
|
|
232
|
+
import { forwardRef as forwardRef3 } from "react";
|
|
233
|
+
import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
234
|
+
var FormTextarea = forwardRef3(
|
|
235
|
+
({ label, helpText, error, className = "", ...props }, ref) => {
|
|
236
|
+
return /* @__PURE__ */ jsxs7("div", { children: [
|
|
237
|
+
label && /* @__PURE__ */ jsx11("label", { className: "block text-sm font-medium text-gray-700 mb-2", children: label }),
|
|
238
|
+
/* @__PURE__ */ jsx11(
|
|
239
|
+
"textarea",
|
|
240
|
+
{
|
|
241
|
+
ref,
|
|
242
|
+
className: `w-full px-4 py-3 rounded-xl border border-gray-200 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent resize-y ${error ? "border-red-300 focus:ring-red-500" : ""} ${className}`,
|
|
243
|
+
...props
|
|
244
|
+
}
|
|
245
|
+
),
|
|
246
|
+
helpText && !error && /* @__PURE__ */ jsx11("p", { className: "mt-1 text-xs text-gray-500", children: helpText }),
|
|
247
|
+
error && /* @__PURE__ */ jsx11("p", { className: "mt-1 text-xs text-red-600", children: error })
|
|
248
|
+
] });
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
FormTextarea.displayName = "FormTextarea";
|
|
252
|
+
|
|
253
|
+
// src/components/FormButtonGroup.tsx
|
|
254
|
+
import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
255
|
+
function FormButtonGroup({
|
|
256
|
+
cancelText = "Cancel",
|
|
257
|
+
submitText = "Save",
|
|
258
|
+
loadingText = "Saving...",
|
|
259
|
+
isLoading = false,
|
|
260
|
+
isDisabled = false,
|
|
261
|
+
onCancel,
|
|
262
|
+
onSubmit,
|
|
263
|
+
submitContent
|
|
264
|
+
}) {
|
|
265
|
+
return /* @__PURE__ */ jsxs8("div", { className: "flex gap-3 pt-2", children: [
|
|
266
|
+
/* @__PURE__ */ jsx12(
|
|
267
|
+
"button",
|
|
268
|
+
{
|
|
269
|
+
type: "button",
|
|
270
|
+
onClick: onCancel,
|
|
271
|
+
className: "flex-1 py-3 px-4 bg-gray-100 text-gray-700 font-medium rounded-xl hover:bg-gray-200 transition-colors",
|
|
272
|
+
children: cancelText
|
|
273
|
+
}
|
|
274
|
+
),
|
|
275
|
+
/* @__PURE__ */ jsx12(
|
|
276
|
+
"button",
|
|
277
|
+
{
|
|
278
|
+
type: onSubmit ? "button" : "submit",
|
|
279
|
+
onClick: onSubmit,
|
|
280
|
+
disabled: isLoading || isDisabled,
|
|
281
|
+
className: "flex-1 py-3 px-4 bg-primary-600 text-white font-medium rounded-xl hover:bg-primary-700 transition-colors disabled:opacity-50",
|
|
282
|
+
children: submitContent ?? (isLoading ? loadingText : submitText)
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
] });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/components/Table.tsx
|
|
289
|
+
import { useState, useMemo } from "react";
|
|
290
|
+
import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
291
|
+
function Table({
|
|
292
|
+
data,
|
|
293
|
+
columns,
|
|
294
|
+
getRowKey,
|
|
295
|
+
onRowClick,
|
|
296
|
+
loading = false,
|
|
297
|
+
emptyMessage = "No data available",
|
|
298
|
+
className = ""
|
|
299
|
+
}) {
|
|
300
|
+
const [sortKey, setSortKey] = useState(null);
|
|
301
|
+
const [sortDirection, setSortDirection] = useState(null);
|
|
302
|
+
const handleHeaderClick = (column) => {
|
|
303
|
+
if (!column.sortable) return;
|
|
304
|
+
if (sortKey === column.key) {
|
|
305
|
+
if (sortDirection === "asc") {
|
|
306
|
+
setSortDirection("desc");
|
|
307
|
+
} else if (sortDirection === "desc") {
|
|
308
|
+
setSortKey(null);
|
|
309
|
+
setSortDirection(null);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
setSortKey(column.key);
|
|
313
|
+
setSortDirection("asc");
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
const sortedData = useMemo(() => {
|
|
317
|
+
if (!sortKey || !sortDirection) return data;
|
|
318
|
+
const column = columns.find((c) => c.key === sortKey);
|
|
319
|
+
if (!column) return data;
|
|
320
|
+
const sorted = [...data].sort((a, b) => {
|
|
321
|
+
if (column.sortFn) {
|
|
322
|
+
return column.sortFn(a, b);
|
|
323
|
+
}
|
|
324
|
+
const aVal = String(column.render(a) ?? "");
|
|
325
|
+
const bVal = String(column.render(b) ?? "");
|
|
326
|
+
return aVal.localeCompare(bVal);
|
|
327
|
+
});
|
|
328
|
+
return sortDirection === "desc" ? sorted.reverse() : sorted;
|
|
329
|
+
}, [data, columns, sortKey, sortDirection]);
|
|
330
|
+
const getSortIcon = (column) => {
|
|
331
|
+
if (!column.sortable) return null;
|
|
332
|
+
if (sortKey !== column.key) {
|
|
333
|
+
return /* @__PURE__ */ jsx13("svg", { className: "w-4 h-4 text-gray-400", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx13("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" }) });
|
|
334
|
+
}
|
|
335
|
+
if (sortDirection === "asc") {
|
|
336
|
+
return /* @__PURE__ */ jsx13("svg", { className: "w-4 h-4 text-primary-600", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx13("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M5 15l7-7 7 7" }) });
|
|
337
|
+
}
|
|
338
|
+
return /* @__PURE__ */ jsx13("svg", { className: "w-4 h-4 text-primary-600", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx13("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" }) });
|
|
339
|
+
};
|
|
340
|
+
const alignmentClasses = {
|
|
341
|
+
left: "text-left",
|
|
342
|
+
center: "text-center",
|
|
343
|
+
right: "text-right"
|
|
344
|
+
};
|
|
345
|
+
return /* @__PURE__ */ jsx13("div", { className: `overflow-x-auto ${className}`, children: /* @__PURE__ */ jsxs9("table", { className: "w-full border-collapse", children: [
|
|
346
|
+
/* @__PURE__ */ jsx13("thead", { children: /* @__PURE__ */ jsx13("tr", { className: "bg-gray-50 border-b border-gray-200", children: columns.map((column) => /* @__PURE__ */ jsx13(
|
|
347
|
+
"th",
|
|
348
|
+
{
|
|
349
|
+
className: `
|
|
350
|
+
px-4 py-3 text-sm font-semibold text-gray-700
|
|
351
|
+
${alignmentClasses[column.align ?? "left"]}
|
|
352
|
+
${column.sortable ? "cursor-pointer select-none hover:bg-gray-100" : ""}
|
|
353
|
+
`,
|
|
354
|
+
style: { width: column.width },
|
|
355
|
+
onClick: () => handleHeaderClick(column),
|
|
356
|
+
children: /* @__PURE__ */ jsxs9("div", { className: `flex items-center gap-1 ${column.align === "right" ? "justify-end" : column.align === "center" ? "justify-center" : ""}`, children: [
|
|
357
|
+
/* @__PURE__ */ jsx13("span", { children: column.header }),
|
|
358
|
+
getSortIcon(column)
|
|
359
|
+
] })
|
|
360
|
+
},
|
|
361
|
+
column.key
|
|
362
|
+
)) }) }),
|
|
363
|
+
/* @__PURE__ */ jsx13("tbody", { children: loading ? /* @__PURE__ */ jsx13("tr", { children: /* @__PURE__ */ jsx13("td", { colSpan: columns.length, className: "px-4 py-8 text-center text-gray-500", children: /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-center gap-2", children: [
|
|
364
|
+
/* @__PURE__ */ jsx13("div", { className: "w-5 h-5 border-2 border-primary-200 border-t-primary-600 rounded-full animate-spin" }),
|
|
365
|
+
/* @__PURE__ */ jsx13("span", { children: "Loading..." })
|
|
366
|
+
] }) }) }) : sortedData.length === 0 ? /* @__PURE__ */ jsx13("tr", { children: /* @__PURE__ */ jsx13("td", { colSpan: columns.length, className: "px-4 py-8 text-center text-gray-500", children: emptyMessage }) }) : sortedData.map((item) => /* @__PURE__ */ jsx13(
|
|
367
|
+
"tr",
|
|
368
|
+
{
|
|
369
|
+
className: `
|
|
370
|
+
border-b border-gray-100 hover:bg-gray-50 transition-colors
|
|
371
|
+
${onRowClick ? "cursor-pointer" : ""}
|
|
372
|
+
`,
|
|
373
|
+
onClick: () => onRowClick?.(item),
|
|
374
|
+
children: columns.map((column) => /* @__PURE__ */ jsx13(
|
|
375
|
+
"td",
|
|
376
|
+
{
|
|
377
|
+
className: `px-4 py-3 text-sm text-gray-800 ${alignmentClasses[column.align ?? "left"]}`,
|
|
378
|
+
style: { width: column.width },
|
|
379
|
+
children: column.render(item)
|
|
380
|
+
},
|
|
381
|
+
column.key
|
|
382
|
+
))
|
|
383
|
+
},
|
|
384
|
+
getRowKey(item)
|
|
385
|
+
)) })
|
|
386
|
+
] }) });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/components/Select.tsx
|
|
390
|
+
import { useState as useState2, useRef, useEffect as useEffect2, useMemo as useMemo2 } from "react";
|
|
391
|
+
import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
392
|
+
function Select({
|
|
393
|
+
options,
|
|
394
|
+
value,
|
|
395
|
+
onChange,
|
|
396
|
+
placeholder = "Select...",
|
|
397
|
+
label,
|
|
398
|
+
error,
|
|
399
|
+
searchable = true,
|
|
400
|
+
multiple = false,
|
|
401
|
+
disabled = false,
|
|
402
|
+
className = ""
|
|
403
|
+
}) {
|
|
404
|
+
const [isOpen, setIsOpen] = useState2(false);
|
|
405
|
+
const [searchTerm, setSearchTerm] = useState2("");
|
|
406
|
+
const [highlightedIndex, setHighlightedIndex] = useState2(0);
|
|
407
|
+
const containerRef = useRef(null);
|
|
408
|
+
const inputRef = useRef(null);
|
|
409
|
+
const filteredOptions = useMemo2(() => {
|
|
410
|
+
if (!searchTerm) return options;
|
|
411
|
+
const term = searchTerm.toLowerCase();
|
|
412
|
+
return options.filter((opt) => opt.label.toLowerCase().includes(term));
|
|
413
|
+
}, [options, searchTerm]);
|
|
414
|
+
const selectedOptions = useMemo2(() => {
|
|
415
|
+
if (value === null) return [];
|
|
416
|
+
const values = Array.isArray(value) ? value : [value];
|
|
417
|
+
return options.filter((opt) => values.includes(opt.value));
|
|
418
|
+
}, [options, value]);
|
|
419
|
+
const displayText = useMemo2(() => {
|
|
420
|
+
if (selectedOptions.length === 0) return "";
|
|
421
|
+
if (multiple) {
|
|
422
|
+
return selectedOptions.map((opt) => opt.label).join(", ");
|
|
423
|
+
}
|
|
424
|
+
return selectedOptions[0]?.label ?? "";
|
|
425
|
+
}, [selectedOptions, multiple]);
|
|
426
|
+
useEffect2(() => {
|
|
427
|
+
const handleClickOutside = (e) => {
|
|
428
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
429
|
+
setIsOpen(false);
|
|
430
|
+
setSearchTerm("");
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
434
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
435
|
+
}, []);
|
|
436
|
+
useEffect2(() => {
|
|
437
|
+
setHighlightedIndex(0);
|
|
438
|
+
}, [filteredOptions]);
|
|
439
|
+
const handleSelect = (option) => {
|
|
440
|
+
if (option.disabled) return;
|
|
441
|
+
if (multiple) {
|
|
442
|
+
const currentValues = Array.isArray(value) ? value : value ? [value] : [];
|
|
443
|
+
const isSelected2 = currentValues.includes(option.value);
|
|
444
|
+
if (isSelected2) {
|
|
445
|
+
const newValues = currentValues.filter((v) => v !== option.value);
|
|
446
|
+
onChange(newValues.length > 0 ? newValues : null);
|
|
447
|
+
} else {
|
|
448
|
+
onChange([...currentValues, option.value]);
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
onChange(option.value);
|
|
452
|
+
setIsOpen(false);
|
|
453
|
+
setSearchTerm("");
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
const handleKeyDown = (e) => {
|
|
457
|
+
if (disabled) return;
|
|
458
|
+
switch (e.key) {
|
|
459
|
+
case "ArrowDown":
|
|
460
|
+
e.preventDefault();
|
|
461
|
+
if (!isOpen) {
|
|
462
|
+
setIsOpen(true);
|
|
463
|
+
} else {
|
|
464
|
+
setHighlightedIndex((prev) => Math.min(prev + 1, filteredOptions.length - 1));
|
|
465
|
+
}
|
|
466
|
+
break;
|
|
467
|
+
case "ArrowUp":
|
|
468
|
+
e.preventDefault();
|
|
469
|
+
setHighlightedIndex((prev) => Math.max(prev - 1, 0));
|
|
470
|
+
break;
|
|
471
|
+
case "Enter":
|
|
472
|
+
e.preventDefault();
|
|
473
|
+
if (isOpen && filteredOptions[highlightedIndex]) {
|
|
474
|
+
handleSelect(filteredOptions[highlightedIndex]);
|
|
475
|
+
} else {
|
|
476
|
+
setIsOpen(true);
|
|
477
|
+
}
|
|
478
|
+
break;
|
|
479
|
+
case "Escape":
|
|
480
|
+
setIsOpen(false);
|
|
481
|
+
setSearchTerm("");
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
const isSelected = (option) => {
|
|
486
|
+
if (value === null) return false;
|
|
487
|
+
const values = Array.isArray(value) ? value : [value];
|
|
488
|
+
return values.includes(option.value);
|
|
489
|
+
};
|
|
490
|
+
return /* @__PURE__ */ jsxs10("div", { ref: containerRef, className: `relative ${className}`, children: [
|
|
491
|
+
label && /* @__PURE__ */ jsx14("label", { className: "block text-sm font-medium text-gray-700 mb-2", children: label }),
|
|
492
|
+
/* @__PURE__ */ jsx14(
|
|
493
|
+
"div",
|
|
494
|
+
{
|
|
495
|
+
className: `
|
|
496
|
+
relative w-full px-4 py-3 rounded-xl border bg-white
|
|
497
|
+
${disabled ? "bg-gray-100 cursor-not-allowed" : "cursor-pointer"}
|
|
498
|
+
${error ? "border-red-300" : isOpen ? "border-primary-500 ring-2 ring-primary-100" : "border-gray-200"}
|
|
499
|
+
transition-all
|
|
500
|
+
`,
|
|
501
|
+
onClick: () => {
|
|
502
|
+
if (!disabled) {
|
|
503
|
+
setIsOpen(!isOpen);
|
|
504
|
+
if (!isOpen && searchable) {
|
|
505
|
+
setTimeout(() => inputRef.current?.focus(), 0);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
onKeyDown: handleKeyDown,
|
|
510
|
+
tabIndex: disabled ? -1 : 0,
|
|
511
|
+
children: /* @__PURE__ */ jsxs10("div", { className: "flex items-center justify-between", children: [
|
|
512
|
+
searchable && isOpen ? /* @__PURE__ */ jsx14(
|
|
513
|
+
"input",
|
|
514
|
+
{
|
|
515
|
+
ref: inputRef,
|
|
516
|
+
type: "text",
|
|
517
|
+
className: "w-full outline-none bg-transparent",
|
|
518
|
+
placeholder: displayText || placeholder,
|
|
519
|
+
value: searchTerm,
|
|
520
|
+
onChange: (e) => setSearchTerm(e.target.value),
|
|
521
|
+
onClick: (e) => e.stopPropagation()
|
|
522
|
+
}
|
|
523
|
+
) : /* @__PURE__ */ jsx14("span", { className: displayText ? "text-gray-800" : "text-gray-400", children: displayText || placeholder }),
|
|
524
|
+
/* @__PURE__ */ jsx14(
|
|
525
|
+
"svg",
|
|
526
|
+
{
|
|
527
|
+
className: `w-5 h-5 text-gray-400 transition-transform ${isOpen ? "rotate-180" : ""}`,
|
|
528
|
+
fill: "none",
|
|
529
|
+
stroke: "currentColor",
|
|
530
|
+
viewBox: "0 0 24 24",
|
|
531
|
+
children: /* @__PURE__ */ jsx14("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" })
|
|
532
|
+
}
|
|
533
|
+
)
|
|
534
|
+
] })
|
|
535
|
+
}
|
|
536
|
+
),
|
|
537
|
+
error && /* @__PURE__ */ jsx14("p", { className: "mt-1 text-xs text-red-600", children: error }),
|
|
538
|
+
isOpen && /* @__PURE__ */ jsx14("div", { className: "absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-xl shadow-lg max-h-60 overflow-auto", children: filteredOptions.length === 0 ? /* @__PURE__ */ jsx14("div", { className: "px-4 py-3 text-sm text-gray-500", children: "No options found" }) : filteredOptions.map((option, index) => /* @__PURE__ */ jsxs10(
|
|
539
|
+
"div",
|
|
540
|
+
{
|
|
541
|
+
className: `
|
|
542
|
+
px-4 py-3 text-sm cursor-pointer flex items-center gap-2
|
|
543
|
+
${option.disabled ? "text-gray-400 cursor-not-allowed" : "text-gray-800"}
|
|
544
|
+
${highlightedIndex === index ? "bg-primary-50" : "hover:bg-gray-50"}
|
|
545
|
+
${isSelected(option) ? "bg-primary-50 font-medium" : ""}
|
|
546
|
+
`,
|
|
547
|
+
onClick: (e) => {
|
|
548
|
+
e.stopPropagation();
|
|
549
|
+
handleSelect(option);
|
|
550
|
+
},
|
|
551
|
+
onMouseEnter: () => setHighlightedIndex(index),
|
|
552
|
+
children: [
|
|
553
|
+
multiple && /* @__PURE__ */ jsx14(
|
|
554
|
+
"div",
|
|
555
|
+
{
|
|
556
|
+
className: `
|
|
557
|
+
w-4 h-4 border rounded flex items-center justify-center
|
|
558
|
+
${isSelected(option) ? "bg-primary-600 border-primary-600" : "border-gray-300"}
|
|
559
|
+
`,
|
|
560
|
+
children: isSelected(option) && /* @__PURE__ */ jsx14("svg", { className: "w-3 h-3 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx14("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 3, d: "M5 13l4 4L19 7" }) })
|
|
561
|
+
}
|
|
562
|
+
),
|
|
563
|
+
option.icon,
|
|
564
|
+
/* @__PURE__ */ jsx14("span", { children: option.label })
|
|
565
|
+
]
|
|
566
|
+
},
|
|
567
|
+
String(option.value)
|
|
568
|
+
)) })
|
|
569
|
+
] });
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/components/Tabs.tsx
|
|
573
|
+
import { useState as useState3 } from "react";
|
|
574
|
+
import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
575
|
+
function Tabs({
|
|
576
|
+
tabs,
|
|
577
|
+
activeTab: controlledActiveTab,
|
|
578
|
+
onTabChange,
|
|
579
|
+
defaultTab,
|
|
580
|
+
variant = "default",
|
|
581
|
+
renderContent = true,
|
|
582
|
+
className = ""
|
|
583
|
+
}) {
|
|
584
|
+
const isControlled = controlledActiveTab !== void 0;
|
|
585
|
+
const [internalActiveTab, setInternalActiveTab] = useState3(
|
|
586
|
+
defaultTab || tabs.find((t) => !t.disabled)?.id || tabs[0]?.id
|
|
587
|
+
);
|
|
588
|
+
const activeTab = isControlled ? controlledActiveTab : internalActiveTab;
|
|
589
|
+
const handleTabClick = (tab) => {
|
|
590
|
+
if (tab.disabled) return;
|
|
591
|
+
if (!isControlled) {
|
|
592
|
+
setInternalActiveTab(tab.id);
|
|
593
|
+
}
|
|
594
|
+
onTabChange?.(tab.id);
|
|
595
|
+
};
|
|
596
|
+
const handleKeyDown = (e, index) => {
|
|
597
|
+
const enabledTabs = tabs.filter((t) => !t.disabled);
|
|
598
|
+
const currentEnabledIndex = enabledTabs.findIndex((t) => t.id === tabs[index].id);
|
|
599
|
+
let newTab;
|
|
600
|
+
switch (e.key) {
|
|
601
|
+
case "ArrowLeft":
|
|
602
|
+
e.preventDefault();
|
|
603
|
+
newTab = enabledTabs[currentEnabledIndex - 1] || enabledTabs[enabledTabs.length - 1];
|
|
604
|
+
break;
|
|
605
|
+
case "ArrowRight":
|
|
606
|
+
e.preventDefault();
|
|
607
|
+
newTab = enabledTabs[currentEnabledIndex + 1] || enabledTabs[0];
|
|
608
|
+
break;
|
|
609
|
+
case "Home":
|
|
610
|
+
e.preventDefault();
|
|
611
|
+
newTab = enabledTabs[0];
|
|
612
|
+
break;
|
|
613
|
+
case "End":
|
|
614
|
+
e.preventDefault();
|
|
615
|
+
newTab = enabledTabs[enabledTabs.length - 1];
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
if (newTab) {
|
|
619
|
+
handleTabClick(newTab);
|
|
620
|
+
const button = document.querySelector(`[data-tab-id="${newTab.id}"]`);
|
|
621
|
+
button?.focus();
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
const activeContent = tabs.find((t) => t.id === activeTab)?.content;
|
|
625
|
+
const variantStyles = {
|
|
626
|
+
default: {
|
|
627
|
+
container: "border-b border-gray-200",
|
|
628
|
+
tab: (isActive, isDisabled) => `
|
|
629
|
+
px-4 py-3 text-sm font-medium border-b-2 -mb-px transition-colors
|
|
630
|
+
${isDisabled ? "text-gray-300 cursor-not-allowed" : "cursor-pointer"}
|
|
631
|
+
${isActive ? "border-primary-500 text-primary-600" : isDisabled ? "border-transparent" : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"}
|
|
632
|
+
`
|
|
633
|
+
},
|
|
634
|
+
pills: {
|
|
635
|
+
container: "bg-gray-100 rounded-lg p-1 inline-flex",
|
|
636
|
+
tab: (isActive, isDisabled) => `
|
|
637
|
+
px-4 py-2 text-sm font-medium rounded-md transition-colors
|
|
638
|
+
${isDisabled ? "text-gray-400 cursor-not-allowed" : "cursor-pointer"}
|
|
639
|
+
${isActive ? "bg-white text-gray-900 shadow-sm" : isDisabled ? "" : "text-gray-500 hover:text-gray-700"}
|
|
640
|
+
`
|
|
641
|
+
},
|
|
642
|
+
underline: {
|
|
643
|
+
container: "",
|
|
644
|
+
tab: (isActive, isDisabled) => `
|
|
645
|
+
px-4 py-2 text-sm font-medium transition-colors relative
|
|
646
|
+
${isDisabled ? "text-gray-300 cursor-not-allowed" : "cursor-pointer"}
|
|
647
|
+
${isActive ? "text-primary-600" : isDisabled ? "" : "text-gray-500 hover:text-gray-700"}
|
|
648
|
+
${isActive ? "after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-primary-500" : ""}
|
|
649
|
+
`
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
const styles = variantStyles[variant];
|
|
653
|
+
return /* @__PURE__ */ jsxs11("div", { className, children: [
|
|
654
|
+
/* @__PURE__ */ jsx15("div", { className: `flex ${styles.container}`, role: "tablist", children: tabs.map((tab, index) => {
|
|
655
|
+
const isActive = tab.id === activeTab;
|
|
656
|
+
const isDisabled = tab.disabled ?? false;
|
|
657
|
+
return /* @__PURE__ */ jsx15(
|
|
658
|
+
"button",
|
|
659
|
+
{
|
|
660
|
+
type: "button",
|
|
661
|
+
role: "tab",
|
|
662
|
+
"data-tab-id": tab.id,
|
|
663
|
+
"aria-selected": isActive,
|
|
664
|
+
"aria-disabled": isDisabled,
|
|
665
|
+
tabIndex: isActive ? 0 : -1,
|
|
666
|
+
className: styles.tab(isActive, isDisabled),
|
|
667
|
+
onClick: () => handleTabClick(tab),
|
|
668
|
+
onKeyDown: (e) => handleKeyDown(e, index),
|
|
669
|
+
children: /* @__PURE__ */ jsxs11("span", { className: "flex items-center gap-2", children: [
|
|
670
|
+
tab.icon,
|
|
671
|
+
tab.label
|
|
672
|
+
] })
|
|
673
|
+
},
|
|
674
|
+
tab.id
|
|
675
|
+
);
|
|
676
|
+
}) }),
|
|
677
|
+
renderContent && activeContent && /* @__PURE__ */ jsx15("div", { className: "pt-4", role: "tabpanel", children: activeContent })
|
|
678
|
+
] });
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/components/DatePicker.tsx
|
|
682
|
+
import { useState as useState4, useRef as useRef2, useEffect as useEffect3, useMemo as useMemo3 } from "react";
|
|
683
|
+
import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
684
|
+
var WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
685
|
+
var MONTHS = [
|
|
686
|
+
"January",
|
|
687
|
+
"February",
|
|
688
|
+
"March",
|
|
689
|
+
"April",
|
|
690
|
+
"May",
|
|
691
|
+
"June",
|
|
692
|
+
"July",
|
|
693
|
+
"August",
|
|
694
|
+
"September",
|
|
695
|
+
"October",
|
|
696
|
+
"November",
|
|
697
|
+
"December"
|
|
698
|
+
];
|
|
699
|
+
var PRESET_RANGES = [
|
|
700
|
+
{
|
|
701
|
+
label: "Today",
|
|
702
|
+
getValue: () => {
|
|
703
|
+
const today = /* @__PURE__ */ new Date();
|
|
704
|
+
today.setHours(0, 0, 0, 0);
|
|
705
|
+
return { start: today, end: today };
|
|
706
|
+
}
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
label: "Yesterday",
|
|
710
|
+
getValue: () => {
|
|
711
|
+
const yesterday = /* @__PURE__ */ new Date();
|
|
712
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
713
|
+
yesterday.setHours(0, 0, 0, 0);
|
|
714
|
+
return { start: yesterday, end: yesterday };
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
label: "Last 7 days",
|
|
719
|
+
getValue: () => {
|
|
720
|
+
const end = /* @__PURE__ */ new Date();
|
|
721
|
+
end.setHours(0, 0, 0, 0);
|
|
722
|
+
const start = /* @__PURE__ */ new Date();
|
|
723
|
+
start.setDate(start.getDate() - 6);
|
|
724
|
+
start.setHours(0, 0, 0, 0);
|
|
725
|
+
return { start, end };
|
|
726
|
+
}
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
label: "Last 30 days",
|
|
730
|
+
getValue: () => {
|
|
731
|
+
const end = /* @__PURE__ */ new Date();
|
|
732
|
+
end.setHours(0, 0, 0, 0);
|
|
733
|
+
const start = /* @__PURE__ */ new Date();
|
|
734
|
+
start.setDate(start.getDate() - 29);
|
|
735
|
+
start.setHours(0, 0, 0, 0);
|
|
736
|
+
return { start, end };
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
label: "This month",
|
|
741
|
+
getValue: () => {
|
|
742
|
+
const start = /* @__PURE__ */ new Date();
|
|
743
|
+
start.setDate(1);
|
|
744
|
+
start.setHours(0, 0, 0, 0);
|
|
745
|
+
const end = /* @__PURE__ */ new Date();
|
|
746
|
+
end.setHours(0, 0, 0, 0);
|
|
747
|
+
return { start, end };
|
|
748
|
+
}
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
label: "Last month",
|
|
752
|
+
getValue: () => {
|
|
753
|
+
const start = /* @__PURE__ */ new Date();
|
|
754
|
+
start.setMonth(start.getMonth() - 1);
|
|
755
|
+
start.setDate(1);
|
|
756
|
+
start.setHours(0, 0, 0, 0);
|
|
757
|
+
const end = new Date(start.getFullYear(), start.getMonth() + 1, 0);
|
|
758
|
+
end.setHours(0, 0, 0, 0);
|
|
759
|
+
return { start, end };
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
];
|
|
763
|
+
function formatDate(date) {
|
|
764
|
+
return date.toLocaleDateString("sv-SE");
|
|
765
|
+
}
|
|
766
|
+
function isSameDay(a, b) {
|
|
767
|
+
return a.getDate() === b.getDate() && a.getMonth() === b.getMonth() && a.getFullYear() === b.getFullYear();
|
|
768
|
+
}
|
|
769
|
+
function isInRange(date, range) {
|
|
770
|
+
if (!range.start || !range.end) return false;
|
|
771
|
+
const d = date.getTime();
|
|
772
|
+
return d >= range.start.getTime() && d <= range.end.getTime();
|
|
773
|
+
}
|
|
774
|
+
function DatePicker({
|
|
775
|
+
value,
|
|
776
|
+
onChange,
|
|
777
|
+
range = false,
|
|
778
|
+
label,
|
|
779
|
+
placeholder = "Select date",
|
|
780
|
+
error,
|
|
781
|
+
minDate,
|
|
782
|
+
maxDate,
|
|
783
|
+
disabled = false,
|
|
784
|
+
className = ""
|
|
785
|
+
}) {
|
|
786
|
+
const [isOpen, setIsOpen] = useState4(false);
|
|
787
|
+
const [viewDate, setViewDate] = useState4(/* @__PURE__ */ new Date());
|
|
788
|
+
const [hoverDate, setHoverDate] = useState4(null);
|
|
789
|
+
const containerRef = useRef2(null);
|
|
790
|
+
const [selectingEnd, setSelectingEnd] = useState4(false);
|
|
791
|
+
useEffect3(() => {
|
|
792
|
+
const handleClickOutside = (e) => {
|
|
793
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
794
|
+
setIsOpen(false);
|
|
795
|
+
setSelectingEnd(false);
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
799
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
800
|
+
}, []);
|
|
801
|
+
const displayText = useMemo3(() => {
|
|
802
|
+
if (!value) return "";
|
|
803
|
+
if (range) {
|
|
804
|
+
const rangeValue = value;
|
|
805
|
+
if (rangeValue.start && rangeValue.end) {
|
|
806
|
+
if (isSameDay(rangeValue.start, rangeValue.end)) {
|
|
807
|
+
return formatDate(rangeValue.start);
|
|
808
|
+
}
|
|
809
|
+
return `${formatDate(rangeValue.start)} - ${formatDate(rangeValue.end)}`;
|
|
810
|
+
}
|
|
811
|
+
if (rangeValue.start) {
|
|
812
|
+
return `${formatDate(rangeValue.start)} - ...`;
|
|
813
|
+
}
|
|
814
|
+
return "";
|
|
815
|
+
}
|
|
816
|
+
return formatDate(value);
|
|
817
|
+
}, [value, range]);
|
|
818
|
+
const calendarDays = useMemo3(() => {
|
|
819
|
+
const year = viewDate.getFullYear();
|
|
820
|
+
const month = viewDate.getMonth();
|
|
821
|
+
const firstDay = new Date(year, month, 1);
|
|
822
|
+
const lastDay = new Date(year, month + 1, 0);
|
|
823
|
+
let startDayOfWeek = firstDay.getDay() - 1;
|
|
824
|
+
if (startDayOfWeek < 0) startDayOfWeek = 6;
|
|
825
|
+
const days = [];
|
|
826
|
+
for (let i = 0; i < startDayOfWeek; i++) {
|
|
827
|
+
days.push(null);
|
|
828
|
+
}
|
|
829
|
+
for (let i = 1; i <= lastDay.getDate(); i++) {
|
|
830
|
+
days.push(new Date(year, month, i));
|
|
831
|
+
}
|
|
832
|
+
return days;
|
|
833
|
+
}, [viewDate]);
|
|
834
|
+
const handleDateClick = (date) => {
|
|
835
|
+
if (isDateDisabled(date)) return;
|
|
836
|
+
if (range) {
|
|
837
|
+
const rangeValue = value || { start: null, end: null };
|
|
838
|
+
if (!selectingEnd || !rangeValue.start) {
|
|
839
|
+
onChange({ start: date, end: null });
|
|
840
|
+
setSelectingEnd(true);
|
|
841
|
+
} else {
|
|
842
|
+
if (date < rangeValue.start) {
|
|
843
|
+
onChange({ start: date, end: rangeValue.start });
|
|
844
|
+
} else {
|
|
845
|
+
onChange({ start: rangeValue.start, end: date });
|
|
846
|
+
}
|
|
847
|
+
setSelectingEnd(false);
|
|
848
|
+
setIsOpen(false);
|
|
849
|
+
}
|
|
850
|
+
} else {
|
|
851
|
+
onChange(date);
|
|
852
|
+
setIsOpen(false);
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
const handlePresetClick = (preset) => {
|
|
856
|
+
const rangeVal = preset.getValue();
|
|
857
|
+
onChange(rangeVal);
|
|
858
|
+
setIsOpen(false);
|
|
859
|
+
setSelectingEnd(false);
|
|
860
|
+
};
|
|
861
|
+
const isDateDisabled = (date) => {
|
|
862
|
+
if (minDate && date < minDate) return true;
|
|
863
|
+
if (maxDate && date > maxDate) return true;
|
|
864
|
+
return false;
|
|
865
|
+
};
|
|
866
|
+
const isDateSelected = (date) => {
|
|
867
|
+
if (!value) return false;
|
|
868
|
+
if (range) {
|
|
869
|
+
const rangeValue = value;
|
|
870
|
+
if (rangeValue.start && isSameDay(date, rangeValue.start)) return true;
|
|
871
|
+
if (rangeValue.end && isSameDay(date, rangeValue.end)) return true;
|
|
872
|
+
return false;
|
|
873
|
+
}
|
|
874
|
+
return isSameDay(date, value);
|
|
875
|
+
};
|
|
876
|
+
const isDateInRange = (date) => {
|
|
877
|
+
if (!range || !value) return false;
|
|
878
|
+
const rangeValue = value;
|
|
879
|
+
if (selectingEnd && rangeValue.start && hoverDate) {
|
|
880
|
+
const previewRange = {
|
|
881
|
+
start: rangeValue.start,
|
|
882
|
+
end: hoverDate > rangeValue.start ? hoverDate : rangeValue.start
|
|
883
|
+
};
|
|
884
|
+
if (hoverDate < rangeValue.start) {
|
|
885
|
+
previewRange.start = hoverDate;
|
|
886
|
+
previewRange.end = rangeValue.start;
|
|
887
|
+
}
|
|
888
|
+
return isInRange(date, previewRange);
|
|
889
|
+
}
|
|
890
|
+
if (!rangeValue.start || !rangeValue.end) return false;
|
|
891
|
+
return isInRange(date, rangeValue);
|
|
892
|
+
};
|
|
893
|
+
const goToPrevMonth = () => {
|
|
894
|
+
setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() - 1, 1));
|
|
895
|
+
};
|
|
896
|
+
const goToNextMonth = () => {
|
|
897
|
+
setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() + 1, 1));
|
|
898
|
+
};
|
|
899
|
+
const today = /* @__PURE__ */ new Date();
|
|
900
|
+
today.setHours(0, 0, 0, 0);
|
|
901
|
+
return /* @__PURE__ */ jsxs12("div", { ref: containerRef, className: `relative ${className}`, children: [
|
|
902
|
+
label && /* @__PURE__ */ jsx16("label", { className: "block text-sm font-medium text-gray-700 mb-2", children: label }),
|
|
903
|
+
/* @__PURE__ */ jsx16(
|
|
904
|
+
"div",
|
|
905
|
+
{
|
|
906
|
+
className: `
|
|
907
|
+
relative w-full px-4 py-3 rounded-xl border bg-white
|
|
908
|
+
${disabled ? "bg-gray-100 cursor-not-allowed" : "cursor-pointer"}
|
|
909
|
+
${error ? "border-red-300" : isOpen ? "border-primary-500 ring-2 ring-primary-100" : "border-gray-200"}
|
|
910
|
+
transition-all
|
|
911
|
+
`,
|
|
912
|
+
onClick: () => !disabled && setIsOpen(!isOpen),
|
|
913
|
+
children: /* @__PURE__ */ jsxs12("div", { className: "flex items-center justify-between", children: [
|
|
914
|
+
/* @__PURE__ */ jsx16("span", { className: displayText ? "text-gray-800" : "text-gray-400", children: displayText || placeholder }),
|
|
915
|
+
/* @__PURE__ */ jsx16("svg", { className: "w-5 h-5 text-gray-400", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx16("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" }) })
|
|
916
|
+
] })
|
|
917
|
+
}
|
|
918
|
+
),
|
|
919
|
+
error && /* @__PURE__ */ jsx16("p", { className: "mt-1 text-xs text-red-600", children: error }),
|
|
920
|
+
isOpen && /* @__PURE__ */ jsx16("div", { className: "absolute z-50 mt-1 bg-white border border-gray-200 rounded-xl shadow-lg p-4", children: /* @__PURE__ */ jsxs12("div", { className: range ? "flex gap-4" : "", children: [
|
|
921
|
+
range && /* @__PURE__ */ jsx16("div", { className: "border-r border-gray-100 pr-4 space-y-1", children: PRESET_RANGES.map((preset) => /* @__PURE__ */ jsx16(
|
|
922
|
+
"button",
|
|
923
|
+
{
|
|
924
|
+
type: "button",
|
|
925
|
+
className: "block w-full text-left px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded-lg",
|
|
926
|
+
onClick: () => handlePresetClick(preset),
|
|
927
|
+
children: preset.label
|
|
928
|
+
},
|
|
929
|
+
preset.label
|
|
930
|
+
)) }),
|
|
931
|
+
/* @__PURE__ */ jsxs12("div", { children: [
|
|
932
|
+
/* @__PURE__ */ jsxs12("div", { className: "flex items-center justify-between mb-4", children: [
|
|
933
|
+
/* @__PURE__ */ jsx16(
|
|
934
|
+
"button",
|
|
935
|
+
{
|
|
936
|
+
type: "button",
|
|
937
|
+
className: "p-1 hover:bg-gray-100 rounded",
|
|
938
|
+
onClick: goToPrevMonth,
|
|
939
|
+
children: /* @__PURE__ */ jsx16("svg", { className: "w-5 h-5 text-gray-600", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx16("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 19l-7-7 7-7" }) })
|
|
940
|
+
}
|
|
941
|
+
),
|
|
942
|
+
/* @__PURE__ */ jsxs12("span", { className: "text-sm font-semibold text-gray-800", children: [
|
|
943
|
+
MONTHS[viewDate.getMonth()],
|
|
944
|
+
" ",
|
|
945
|
+
viewDate.getFullYear()
|
|
946
|
+
] }),
|
|
947
|
+
/* @__PURE__ */ jsx16(
|
|
948
|
+
"button",
|
|
949
|
+
{
|
|
950
|
+
type: "button",
|
|
951
|
+
className: "p-1 hover:bg-gray-100 rounded",
|
|
952
|
+
onClick: goToNextMonth,
|
|
953
|
+
children: /* @__PURE__ */ jsx16("svg", { className: "w-5 h-5 text-gray-600", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx16("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) })
|
|
954
|
+
}
|
|
955
|
+
)
|
|
956
|
+
] }),
|
|
957
|
+
/* @__PURE__ */ jsx16("div", { className: "grid grid-cols-7 gap-1 mb-2", children: WEEKDAYS.map((day) => /* @__PURE__ */ jsx16("div", { className: "text-center text-xs font-medium text-gray-500 py-1", children: day }, day)) }),
|
|
958
|
+
/* @__PURE__ */ jsx16("div", { className: "grid grid-cols-7 gap-1", children: calendarDays.map((date, index) => {
|
|
959
|
+
if (!date) {
|
|
960
|
+
return /* @__PURE__ */ jsx16("div", { className: "w-9 h-9" }, `empty-${index}`);
|
|
961
|
+
}
|
|
962
|
+
const isDisabled = isDateDisabled(date);
|
|
963
|
+
const isSelected = isDateSelected(date);
|
|
964
|
+
const inRange = isDateInRange(date);
|
|
965
|
+
const isToday = isSameDay(date, today);
|
|
966
|
+
return /* @__PURE__ */ jsx16(
|
|
967
|
+
"button",
|
|
968
|
+
{
|
|
969
|
+
type: "button",
|
|
970
|
+
className: `
|
|
971
|
+
w-9 h-9 text-sm rounded-lg transition-colors
|
|
972
|
+
${isDisabled ? "text-gray-300 cursor-not-allowed" : "hover:bg-gray-100"}
|
|
973
|
+
${isSelected ? "bg-primary-600 text-white hover:bg-primary-700" : ""}
|
|
974
|
+
${inRange && !isSelected ? "bg-primary-100" : ""}
|
|
975
|
+
${isToday && !isSelected ? "ring-1 ring-primary-400" : ""}
|
|
976
|
+
`,
|
|
977
|
+
onClick: () => handleDateClick(date),
|
|
978
|
+
onMouseEnter: () => setHoverDate(date),
|
|
979
|
+
onMouseLeave: () => setHoverDate(null),
|
|
980
|
+
disabled: isDisabled,
|
|
981
|
+
children: date.getDate()
|
|
982
|
+
},
|
|
983
|
+
date.toISOString()
|
|
984
|
+
);
|
|
985
|
+
}) })
|
|
986
|
+
] })
|
|
987
|
+
] }) })
|
|
988
|
+
] });
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// src/components/Timer.tsx
|
|
992
|
+
import { useState as useState5, useEffect as useEffect4, useCallback, useRef as useRef3 } from "react";
|
|
993
|
+
import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
994
|
+
function formatTime(totalSeconds) {
|
|
995
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
996
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
997
|
+
const seconds = totalSeconds % 60;
|
|
998
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
999
|
+
if (hours > 0) {
|
|
1000
|
+
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
|
|
1001
|
+
}
|
|
1002
|
+
return `${pad(minutes)}:${pad(seconds)}`;
|
|
1003
|
+
}
|
|
1004
|
+
function Timer({
|
|
1005
|
+
initialSeconds = 0,
|
|
1006
|
+
onStart,
|
|
1007
|
+
onStop,
|
|
1008
|
+
onReset,
|
|
1009
|
+
onTick,
|
|
1010
|
+
showReset = true,
|
|
1011
|
+
size = "md",
|
|
1012
|
+
className = "",
|
|
1013
|
+
isRunning: controlledIsRunning,
|
|
1014
|
+
elapsedSeconds: controlledElapsedSeconds
|
|
1015
|
+
}) {
|
|
1016
|
+
const isControlled = controlledIsRunning !== void 0;
|
|
1017
|
+
const [internalIsRunning, setInternalIsRunning] = useState5(false);
|
|
1018
|
+
const [internalElapsedSeconds, setInternalElapsedSeconds] = useState5(initialSeconds);
|
|
1019
|
+
const isRunning = isControlled ? controlledIsRunning : internalIsRunning;
|
|
1020
|
+
const elapsedSeconds = isControlled ? controlledElapsedSeconds ?? 0 : internalElapsedSeconds;
|
|
1021
|
+
const intervalRef = useRef3(null);
|
|
1022
|
+
const startTimeRef = useRef3(null);
|
|
1023
|
+
useEffect4(() => {
|
|
1024
|
+
return () => {
|
|
1025
|
+
if (intervalRef.current) {
|
|
1026
|
+
clearInterval(intervalRef.current);
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
}, []);
|
|
1030
|
+
useEffect4(() => {
|
|
1031
|
+
if (isControlled) return;
|
|
1032
|
+
if (isRunning) {
|
|
1033
|
+
intervalRef.current = setInterval(() => {
|
|
1034
|
+
setInternalElapsedSeconds((prev) => {
|
|
1035
|
+
const newValue = prev + 1;
|
|
1036
|
+
onTick?.(newValue);
|
|
1037
|
+
return newValue;
|
|
1038
|
+
});
|
|
1039
|
+
}, 1e3);
|
|
1040
|
+
} else {
|
|
1041
|
+
if (intervalRef.current) {
|
|
1042
|
+
clearInterval(intervalRef.current);
|
|
1043
|
+
intervalRef.current = null;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return () => {
|
|
1047
|
+
if (intervalRef.current) {
|
|
1048
|
+
clearInterval(intervalRef.current);
|
|
1049
|
+
intervalRef.current = null;
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
}, [isRunning, isControlled, onTick]);
|
|
1053
|
+
const handleStart = useCallback(() => {
|
|
1054
|
+
const now = /* @__PURE__ */ new Date();
|
|
1055
|
+
startTimeRef.current = now;
|
|
1056
|
+
if (!isControlled) {
|
|
1057
|
+
setInternalIsRunning(true);
|
|
1058
|
+
}
|
|
1059
|
+
onStart?.(now);
|
|
1060
|
+
}, [isControlled, onStart]);
|
|
1061
|
+
const handleStop = useCallback(() => {
|
|
1062
|
+
if (!isControlled) {
|
|
1063
|
+
setInternalIsRunning(false);
|
|
1064
|
+
}
|
|
1065
|
+
onStop?.(elapsedSeconds);
|
|
1066
|
+
}, [isControlled, elapsedSeconds, onStop]);
|
|
1067
|
+
const handleReset = useCallback(() => {
|
|
1068
|
+
if (!isControlled) {
|
|
1069
|
+
setInternalIsRunning(false);
|
|
1070
|
+
setInternalElapsedSeconds(0);
|
|
1071
|
+
}
|
|
1072
|
+
startTimeRef.current = null;
|
|
1073
|
+
onReset?.();
|
|
1074
|
+
}, [isControlled, onReset]);
|
|
1075
|
+
const handleToggle = useCallback(() => {
|
|
1076
|
+
if (isRunning) {
|
|
1077
|
+
handleStop();
|
|
1078
|
+
} else {
|
|
1079
|
+
handleStart();
|
|
1080
|
+
}
|
|
1081
|
+
}, [isRunning, handleStart, handleStop]);
|
|
1082
|
+
const sizeClasses3 = {
|
|
1083
|
+
sm: {
|
|
1084
|
+
container: "gap-2",
|
|
1085
|
+
time: "text-2xl",
|
|
1086
|
+
button: "w-10 h-10",
|
|
1087
|
+
icon: "w-4 h-4"
|
|
1088
|
+
},
|
|
1089
|
+
md: {
|
|
1090
|
+
container: "gap-3",
|
|
1091
|
+
time: "text-4xl",
|
|
1092
|
+
button: "w-12 h-12",
|
|
1093
|
+
icon: "w-5 h-5"
|
|
1094
|
+
},
|
|
1095
|
+
lg: {
|
|
1096
|
+
container: "gap-4",
|
|
1097
|
+
time: "text-5xl",
|
|
1098
|
+
button: "w-14 h-14",
|
|
1099
|
+
icon: "w-6 h-6"
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
const styles = sizeClasses3[size];
|
|
1103
|
+
return /* @__PURE__ */ jsxs13("div", { className: `flex items-center ${styles.container} ${className}`, children: [
|
|
1104
|
+
/* @__PURE__ */ jsx17("div", { className: `font-mono font-bold text-gray-800 ${styles.time} tabular-nums`, children: formatTime(elapsedSeconds) }),
|
|
1105
|
+
/* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-2", children: [
|
|
1106
|
+
/* @__PURE__ */ jsx17(
|
|
1107
|
+
"button",
|
|
1108
|
+
{
|
|
1109
|
+
type: "button",
|
|
1110
|
+
onClick: handleToggle,
|
|
1111
|
+
className: `
|
|
1112
|
+
${styles.button} rounded-full flex items-center justify-center
|
|
1113
|
+
${isRunning ? "bg-amber-500 hover:bg-amber-600 text-white" : "bg-green-500 hover:bg-green-600 text-white"}
|
|
1114
|
+
transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2
|
|
1115
|
+
${isRunning ? "focus:ring-amber-500" : "focus:ring-green-500"}
|
|
1116
|
+
`,
|
|
1117
|
+
title: isRunning ? "Pause" : "Start",
|
|
1118
|
+
children: isRunning ? (
|
|
1119
|
+
// Pause icon
|
|
1120
|
+
/* @__PURE__ */ jsx17("svg", { className: styles.icon, fill: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx17("path", { d: "M6 4h4v16H6V4zm8 0h4v16h-4V4z" }) })
|
|
1121
|
+
) : (
|
|
1122
|
+
// Play icon
|
|
1123
|
+
/* @__PURE__ */ jsx17("svg", { className: styles.icon, fill: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx17("path", { d: "M8 5v14l11-7z" }) })
|
|
1124
|
+
)
|
|
1125
|
+
}
|
|
1126
|
+
),
|
|
1127
|
+
elapsedSeconds > 0 && /* @__PURE__ */ jsx17(
|
|
1128
|
+
"button",
|
|
1129
|
+
{
|
|
1130
|
+
type: "button",
|
|
1131
|
+
onClick: handleStop,
|
|
1132
|
+
className: `
|
|
1133
|
+
${styles.button} rounded-full flex items-center justify-center
|
|
1134
|
+
bg-red-500 hover:bg-red-600 text-white
|
|
1135
|
+
transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500
|
|
1136
|
+
`,
|
|
1137
|
+
title: "Stop",
|
|
1138
|
+
children: /* @__PURE__ */ jsx17("svg", { className: styles.icon, fill: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx17("path", { d: "M6 6h12v12H6z" }) })
|
|
1139
|
+
}
|
|
1140
|
+
),
|
|
1141
|
+
showReset && elapsedSeconds > 0 && !isRunning && /* @__PURE__ */ jsx17(
|
|
1142
|
+
"button",
|
|
1143
|
+
{
|
|
1144
|
+
type: "button",
|
|
1145
|
+
onClick: handleReset,
|
|
1146
|
+
className: `
|
|
1147
|
+
${styles.button} rounded-full flex items-center justify-center
|
|
1148
|
+
bg-gray-200 hover:bg-gray-300 text-gray-700
|
|
1149
|
+
transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-400
|
|
1150
|
+
`,
|
|
1151
|
+
title: "Reset",
|
|
1152
|
+
children: /* @__PURE__ */ jsx17("svg", { className: styles.icon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx17("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" }) })
|
|
1153
|
+
}
|
|
1154
|
+
)
|
|
1155
|
+
] })
|
|
1156
|
+
] });
|
|
1157
|
+
}
|
|
1158
|
+
export {
|
|
1159
|
+
Button,
|
|
1160
|
+
Card,
|
|
1161
|
+
DatePicker,
|
|
1162
|
+
EmptyState,
|
|
1163
|
+
FormButtonGroup,
|
|
1164
|
+
FormInput,
|
|
1165
|
+
FormTextarea,
|
|
1166
|
+
Input,
|
|
1167
|
+
Layout,
|
|
1168
|
+
LoadingSpinner,
|
|
1169
|
+
Modal,
|
|
1170
|
+
Select,
|
|
1171
|
+
ServiceSwitcher,
|
|
1172
|
+
Sidebar,
|
|
1173
|
+
Table,
|
|
1174
|
+
Tabs,
|
|
1175
|
+
Timer,
|
|
1176
|
+
formatTime
|
|
1177
|
+
};
|
|
1178
|
+
//# sourceMappingURL=index.mjs.map
|