uibee 2.9.4 → 2.10.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.
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import type { Column } from 'uibee/components';
3
+ type BodyProps = {
4
+ list: object[];
5
+ columns: Column[];
6
+ menuItems?: (data: object, id: string) => React.ReactNode;
7
+ redirectPath?: string | {
8
+ path: string;
9
+ key?: string;
10
+ };
11
+ variant?: 'default' | 'minimal';
12
+ idKey?: string;
13
+ };
14
+ export default function Body({ list, columns, menuItems, redirectPath, variant, idKey }: BodyProps): import("react/jsx-runtime").JSX.Element;
15
+ export {};
@@ -0,0 +1,116 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { EllipsisVertical } from 'lucide-react';
3
+ import { useRouter } from 'next/navigation';
4
+ import { useState, useRef, useEffect } from 'react';
5
+ import useClickOutside from '../../hooks/useClickOutside';
6
+ import Menu from './menu';
7
+ import { formatValue } from './format';
8
+ export default function Body({ list, columns, menuItems, redirectPath, variant = 'default', idKey }) {
9
+ const [openMenuId, setOpenMenuId] = useState(null);
10
+ const [anchor, setAnchor] = useState(null);
11
+ const router = useRouter();
12
+ const menuRef = useRef(null);
13
+ const tbodyRef = useRef(null);
14
+ const menuWasOpenOnMouseDown = useRef(false);
15
+ useClickOutside(menuRef, () => setOpenMenuId(null));
16
+ useEffect(() => {
17
+ const el = tbodyRef.current;
18
+ if (!el)
19
+ return;
20
+ const close = () => setOpenMenuId(null);
21
+ el.addEventListener('scroll', close);
22
+ return () => el.removeEventListener('scroll', close);
23
+ }, []);
24
+ return (_jsx("tbody", { ref: tbodyRef, className: `
25
+ divide-login-600 block overflow-y-auto flex-1 min-h-0
26
+ ${variant === 'default' ? 'bg-login-500/50 divide-y' : 'bg-transparent divide-y'}
27
+ `, children: list.map((item, index) => {
28
+ const itemRecord = item;
29
+ let id = '';
30
+ if (idKey && itemRecord[idKey] !== undefined) {
31
+ id = String(itemRecord[idKey]);
32
+ }
33
+ else if (itemRecord['id'] !== undefined) {
34
+ id = String(itemRecord['id']);
35
+ }
36
+ else {
37
+ const firstKey = columns[0]?.key || Object.keys(itemRecord)[0];
38
+ id = String(itemRecord[firstKey]);
39
+ }
40
+ const redirectConfig = (typeof redirectPath === 'object' && redirectPath) ? redirectPath : { path: redirectPath };
41
+ const redirectId = redirectConfig.key ? String(itemRecord[redirectConfig.key]) : id;
42
+ const menuButtonColors = variant === 'minimal'
43
+ ? {
44
+ active: 'bg-login-600 text-login-100',
45
+ inactive: 'hover:bg-login-600 text-login-200 hover:text-login-100'
46
+ }
47
+ : {
48
+ active: 'bg-login-400 text-login-100',
49
+ inactive: 'hover:bg-login-400 text-login-200 hover:text-login-100'
50
+ };
51
+ const buttonClass = openMenuId === id ? menuButtonColors.active : menuButtonColors.inactive;
52
+ return (_jsxs("tr", { className: `
53
+ flex w-full group/row transition-colors
54
+ ${redirectConfig.path ? 'cursor-pointer' : ''}
55
+ ${variant === 'default' && redirectConfig.path ? 'hover:bg-login-600/30' : ''}
56
+ ${variant === 'minimal' ? 'hover:bg-white/5 border-b border-login-600/50 last:border-0' : ''}
57
+ `, onMouseDown: () => {
58
+ menuWasOpenOnMouseDown.current = openMenuId !== null;
59
+ }, onClick: () => {
60
+ if (menuWasOpenOnMouseDown.current) {
61
+ menuWasOpenOnMouseDown.current = false;
62
+ return;
63
+ }
64
+ if (redirectConfig.path) {
65
+ if (redirectConfig.path.includes('?')) {
66
+ router.push(`${redirectConfig.path}${redirectId}`);
67
+ }
68
+ else {
69
+ router.push(`${redirectConfig.path}/${redirectId}`);
70
+ }
71
+ }
72
+ }, onContextMenu: (e) => {
73
+ e.preventDefault();
74
+ setAnchor({ top: e.clientY, right: window.innerWidth - e.clientX });
75
+ setOpenMenuId(id);
76
+ }, children: [columns.map((col) => {
77
+ const val = itemRecord[col.key];
78
+ const value = val;
79
+ let badgeClass = '';
80
+ if (col.highlight) {
81
+ const highlightKey = String(value);
82
+ const colorName = col.highlight[highlightKey] || col.highlight.default;
83
+ if (colorName) {
84
+ switch (colorName) {
85
+ case 'green':
86
+ badgeClass = 'bg-green-500/20 text-green-400';
87
+ break;
88
+ case 'yellow':
89
+ badgeClass = 'bg-yellow-500/20 text-yellow-400';
90
+ break;
91
+ case 'red':
92
+ badgeClass = 'bg-red-500/20 text-red-400';
93
+ break;
94
+ case 'blue':
95
+ badgeClass = 'bg-blue-500/20 text-blue-400';
96
+ break;
97
+ case 'gray':
98
+ badgeClass = 'bg-gray-500/20 text-gray-400';
99
+ break;
100
+ }
101
+ badgeClass += ' px-2 py-1 rounded';
102
+ }
103
+ }
104
+ return (_jsx("td", { className: `
105
+ flex-1 px-6 py-4 whitespace-nowrap text-sm min-w-40 flex items-center text-login-100
106
+ ${variant === 'minimal' ? 'py-3' : ''}
107
+ `, children: _jsx("div", { className: 'relative', children: _jsx("h1", { className: badgeClass, children: formatValue(col.key, value) }) }) }, col.key));
108
+ }), menuItems && (_jsx("td", { className: 'shrink-0 w-16 flex flex-row justify-end p-2 px-4\n whitespace-nowrap text-right text-sm font-medium', children: _jsxs("div", { className: 'relative', children: [_jsx("button", { type: 'button', className: `p-1.5 rounded flex items-start justify-center transition-colors ${buttonClass}`, onMouseDown: (e) => e.nativeEvent.stopImmediatePropagation(), onClick: (e) => {
109
+ e.stopPropagation();
110
+ const rect = e.currentTarget.getBoundingClientRect();
111
+ const coords = { top: rect.bottom + 4, right: window.innerWidth - rect.right };
112
+ setAnchor(openMenuId === id ? null : coords);
113
+ setOpenMenuId(openMenuId === id ? null : id);
114
+ }, children: _jsx("span", { className: 'text-xl leading-none select-none', children: _jsx(EllipsisVertical, { className: 'h-5 w-5' }) }) }), openMenuId === id && anchor && (_jsx(Menu, { ref: menuRef, anchor: anchor, onClose: () => setOpenMenuId(null), children: menuItems?.(item, id) }))] }) }))] }, index));
115
+ }) }));
116
+ }
@@ -0,0 +1 @@
1
+ export declare function formatValue(key: string, value: string | number): string | number;
@@ -0,0 +1,27 @@
1
+ const nullableTimeKeys = ['date', 'last_sent', 'time'];
2
+ const ISODateTimeReg = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
3
+ const ISODateReg = /^\d{4}-\d{2}-\d{2}$/;
4
+ const ISOTimeReg = /^\d{2}:\d{2}(:\d{2})?$/;
5
+ const OsloTZ = { timeZone: 'Europe/Oslo', second: undefined };
6
+ const OsloTime = { ...OsloTZ, hour: '2-digit', minute: '2-digit' };
7
+ const OsloDateTime = { ...OsloTime, year: 'numeric', month: '2-digit', day: '2-digit' };
8
+ export function formatValue(key, value) {
9
+ if (nullableTimeKeys.includes(key) && !value) {
10
+ return 'Never';
11
+ }
12
+ if (typeof value === 'string') {
13
+ if (ISODateTimeReg.test(value)) {
14
+ return new Date(value).toLocaleString('nb-NO', OsloDateTime);
15
+ }
16
+ if (ISODateReg.test(value)) {
17
+ return new Date(value).toLocaleDateString('nb-NO', OsloTZ);
18
+ }
19
+ if (ISOTimeReg.test(value)) {
20
+ return new Date(`1970-01-01T${value}`).toLocaleTimeString('nb-NO', OsloTime);
21
+ }
22
+ }
23
+ if (key.includes('capacity')) {
24
+ return value === 0 ? 'Unlimited' : value;
25
+ }
26
+ return value;
27
+ }
@@ -0,0 +1,8 @@
1
+ import type { Column } from 'uibee/components';
2
+ type HeaderProps = {
3
+ columns: Column[];
4
+ hideMenu?: boolean;
5
+ variant?: 'default' | 'minimal';
6
+ };
7
+ export default function Header({ columns, hideMenu, variant }: HeaderProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ChevronDown, ChevronUp } from 'lucide-react';
3
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
4
+ import { useEffect, useState } from 'react';
5
+ export default function Header({ columns, hideMenu, variant = 'default' }) {
6
+ const [column, setColumn] = useState(columns[0]?.key || '');
7
+ const [order, setOrder] = useState('asc');
8
+ const router = useRouter();
9
+ const pathname = usePathname();
10
+ const searchParams = useSearchParams();
11
+ useEffect(() => {
12
+ const params = new URLSearchParams(searchParams.toString());
13
+ if (searchParams.get('order') !== order ||
14
+ searchParams.get('column') !== column) {
15
+ params.set('order', order);
16
+ params.set('column', column);
17
+ params.set('page', '1');
18
+ router.push(`${pathname}?${params.toString()}`);
19
+ }
20
+ }, [order, column, pathname, router, searchParams]);
21
+ function handleChange(key) {
22
+ setColumn(key);
23
+ setOrder((prev) => (key === column && prev === 'asc' ? 'desc' : 'asc'));
24
+ }
25
+ return (_jsx("thead", { className: `
26
+ block w-full
27
+ ${variant === 'default' ? 'bg-login-700' : 'bg-transparent border-b border-login-600'}
28
+ `, children: _jsxs("tr", { className: 'flex w-full divide-x divide-transparent', children: [columns.map((col) => {
29
+ const key = col.key;
30
+ const value = col.label || (key.length < 3
31
+ ? key.toUpperCase()
32
+ : `${key[0].toUpperCase()}${key
33
+ .slice(1)
34
+ .replaceAll('_', ' ')}`);
35
+ return (_jsx("th", { className: `
36
+ flex-1 px-6 py-3 text-xs font-medium uppercase tracking-wider text-left
37
+ ${variant === 'default' ? 'text-login-200' : 'text-login-100'}
38
+ `, children: _jsxs("button", { className: 'flex flex-row items-center gap-2 group uppercase', onClick: () => handleChange(key), children: [value, _jsx("span", { className: 'flex flex-col', children: column === key ? (order === 'asc' ? (_jsx(ChevronUp, { className: 'h-4 w-4' })) : (_jsx(ChevronDown, { className: 'h-4 w-4' }))) : (_jsx(ChevronUp, { className: 'h-4 w-4 stroke-login-200 opacity-0 group-hover:opacity-100' })) })] }) }, key));
39
+ }), !hideMenu && _jsx("th", { className: 'shrink-0 w-16 px-6 py-3' })] }) }));
40
+ }
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ export default function Menu({ ref, children, anchor, onClose }: {
3
+ ref: React.RefObject<HTMLDivElement | null>;
4
+ children: React.ReactNode;
5
+ anchor: {
6
+ top: number;
7
+ right: number;
8
+ };
9
+ onClose?: () => void;
10
+ }): React.ReactPortal;
11
+ export declare function MenuButton({ icon, text, hotKey, onClick, className, }: {
12
+ icon: React.ReactNode;
13
+ text: string;
14
+ hotKey?: string;
15
+ onClick: () => void;
16
+ className?: string;
17
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React, { createContext, useContext, useEffect } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+ const MenuContext = createContext({});
6
+ export default function Menu({ ref, children, anchor, onClose }) {
7
+ return createPortal(_jsx("div", { ref: ref, style: { top: anchor.top, right: anchor.right }, className: 'fixed bg-login-500 border border-login-600 rounded-lg shadow-lg z-9999 w-44', children: _jsx(MenuContext.Provider, { value: { onClose }, children: children }) }), document.body);
8
+ }
9
+ export function MenuButton({ icon, text, hotKey, onClick, className, }) {
10
+ const { onClose } = useContext(MenuContext);
11
+ useEffect(() => {
12
+ if (!hotKey)
13
+ return;
14
+ function handleKeyDown(e) {
15
+ if (e.key.toLowerCase() === hotKey.toLowerCase()) {
16
+ e.preventDefault();
17
+ onClick();
18
+ onClose?.();
19
+ }
20
+ }
21
+ window.addEventListener('keydown', handleKeyDown);
22
+ return () => window.removeEventListener('keydown', handleKeyDown);
23
+ }, [hotKey, onClick, onClose]);
24
+ return (_jsxs("button", { onClick: () => {
25
+ onClick();
26
+ onClose?.();
27
+ }, className: `flex items-center justify-between w-full px-3 py-2 text-sm hover:bg-login-600 cursor-pointer
28
+ ${className || ''}
29
+ `, children: [_jsxs("div", { className: 'flex items-center', children: [React.cloneElement(icon, { className: 'w-4 h-4 mr-2' }), text] }), _jsx("span", { className: 'text-xs opacity-50 font-mono', children: hotKey })] }));
30
+ }
@@ -0,0 +1,6 @@
1
+ type PaginationProps = {
2
+ pageSize?: number;
3
+ totalRows?: number;
4
+ };
5
+ export default function Pagination({ pageSize, totalRows, }: PaginationProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
4
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
5
+ import { useEffect, useState } from 'react';
6
+ export default function Pagination({ pageSize = 10, totalRows, }) {
7
+ const router = useRouter();
8
+ const pathname = usePathname();
9
+ const searchParams = useSearchParams();
10
+ const rawPage = parseInt(searchParams.get('page') || '1', 10);
11
+ const initialPage = Math.max(1, Number.isNaN(rawPage) ? 1 : rawPage);
12
+ const [current, setCurrent] = useState(initialPage);
13
+ useEffect(() => {
14
+ const raw = parseInt(searchParams.get('page') || '1', 10);
15
+ const p = Math.max(1, Number.isNaN(raw) ? 1 : raw);
16
+ const computedTotalPages = Math.max(1, Math.ceil(totalRows !== undefined && pageSize > 0
17
+ ? totalRows / pageSize
18
+ : 1));
19
+ setCurrent(Math.max(1, Math.min(computedTotalPages, p)));
20
+ }, [searchParams, totalRows, pageSize]);
21
+ function updateQuery(p) {
22
+ const params = new URLSearchParams(searchParams.toString());
23
+ params.set('page', String(p));
24
+ router.push(`${pathname}?${params.toString()}`);
25
+ }
26
+ function goPrevious() {
27
+ if (current <= 1)
28
+ return;
29
+ const next = current - 1;
30
+ setCurrent(next);
31
+ updateQuery(next);
32
+ }
33
+ const totalPages = Math.max(1, Math.ceil(totalRows !== undefined && pageSize > 0
34
+ ? totalRows / pageSize
35
+ : 1));
36
+ function goNext() {
37
+ if (current >= totalPages)
38
+ return;
39
+ const next = current + 1;
40
+ setCurrent(next);
41
+ updateQuery(next);
42
+ }
43
+ function setPage(p) {
44
+ if (p === current)
45
+ return;
46
+ setCurrent(p);
47
+ updateQuery(p);
48
+ }
49
+ function getPages(curr, total) {
50
+ const delta = 2;
51
+ const left = Math.max(1, curr - delta);
52
+ const right = Math.min(total, curr + delta);
53
+ const pages = [];
54
+ if (left > 1) {
55
+ pages.push(1);
56
+ if (left > 2)
57
+ pages.push('...');
58
+ }
59
+ for (let i = left; i <= right; i++)
60
+ pages.push(i);
61
+ if (right < total) {
62
+ if (right < total - 1)
63
+ pages.push('...');
64
+ pages.push(total);
65
+ }
66
+ return pages;
67
+ }
68
+ const pages = getPages(current, totalPages);
69
+ const start = Math.max(1, (current - 1) * pageSize + 1);
70
+ const end = Math.min(current * pageSize, totalRows !== undefined ? totalRows : current * pageSize);
71
+ return (_jsxs("div", { className: 'flex items-center justify-between w-full pt-4', children: [_jsx("div", { className: 'text-sm /70', children: totalRows !== undefined ? (totalRows === 0 ? (_jsx("span", { children: "Showing 0 results" })) : (_jsxs("span", { children: ["Showing ", start, " to ", end, " of ", totalRows, " results"] }))) : null }), _jsxs("div", { className: 'flex items-center gap-3', children: [_jsx("button", { type: 'button', onClick: goPrevious, disabled: current <= 1, className: `
72
+ flex items-center gap-2 p-1 rounded-lg
73
+ bg-login-50/5 hover:bg-login-500 disabled:opacity-50
74
+ border-[0.10rem] border-login-200 text-sm
75
+ `, children: _jsx(ChevronLeft, { className: 'h-5 w-5' }) }), _jsx("nav", { className: 'flex items-center gap-1', "aria-label": 'Pagination', children: pages.map((p, i) => typeof p === 'string' ? (_jsx("span", { className: 'px-3 py-1 text-sm', children: p }, `e-${i}`)) : (_jsx("button", { type: 'button', onClick: () => setPage(p), "aria-current": p === current ? 'page' : undefined, className: `
76
+ px-3 py-1 rounded-lg text-sm
77
+ border-[0.10rem] ${p === current
78
+ ? 'bg-login-50/5 border-login-50'
79
+ : `bg-login-50/0 border-login-200
80
+ 'hover:bg-login-400`}
81
+ `, children: p }, p))) }), _jsx("button", { type: 'button', onClick: goNext, disabled: current >= totalPages, className: `
82
+ flex items-center gap-2 p-1 rounded-lg bg-login-50/5
83
+ hover:bg-login-500 disabled:opacity-50 select-none
84
+ border-[0.10rem] border-login-200 text-sm
85
+ `, children: _jsx(ChevronRight, { className: 'h-5 w-5' }) })] })] }));
86
+ }
@@ -0,0 +1,14 @@
1
+ import type { Column } from 'uibee/components';
2
+ type TableProps = {
3
+ data: object[];
4
+ columns: Column[];
5
+ menuItems?: (data: object, id: string) => React.ReactNode;
6
+ redirectPath?: string | {
7
+ path: string;
8
+ key?: string;
9
+ };
10
+ variant?: 'default' | 'minimal';
11
+ idKey?: string;
12
+ };
13
+ export default function Table({ data, columns, menuItems, redirectPath, variant, idKey }: TableProps): import("react/jsx-runtime").JSX.Element;
14
+ export {};
@@ -0,0 +1,14 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import Body from './body';
4
+ import Header from './header';
5
+ export default function Table({ data, columns, menuItems, redirectPath, variant = 'default', idKey }) {
6
+ if (data.length === 0) {
7
+ return _jsx("div", { className: 'p-4 text-center text-login-200', children: "No data found" });
8
+ }
9
+ return (_jsx("div", { className: `
10
+ flex-1 flex flex-col min-h-0 overflow-x-auto h-full w-full
11
+ ${variant === 'default' ? 'bg-login-500/50 rounded-lg shadow border border-login-600' : ''}
12
+ ${variant === 'minimal' ? 'bg-transparent' : ''}
13
+ `, children: _jsxs("table", { className: 'min-w-full w-max divide-y divide-login-600 flex flex-col flex-1 min-h-0', children: [_jsx(Header, { columns: columns, hideMenu: !menuItems, variant: variant }), _jsx(Body, { list: data, columns: columns, menuItems: menuItems, redirectPath: redirectPath, variant: variant, idKey: idKey })] }) }));
14
+ }