@kala-ui/react-app 0.1.0-beta.1 → 0.1.0-beta.11
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/CHANGELOG.md +73 -0
- package/dist/components/app-shell/app-shell.d.ts +8 -46
- package/dist/components/app-shell/app-shell.js +41 -42
- package/dist/components/app-shell/app-shell.types.d.ts +45 -0
- package/dist/components/app-shell/app-shell.types.js +2 -0
- package/dist/components/app-shell/index.d.ts +1 -0
- package/dist/components/charts/area-chart.d.ts +1 -1
- package/dist/components/charts/area-chart.js +4 -3
- package/dist/components/charts/bar-chart.d.ts +1 -1
- package/dist/components/charts/bar-chart.js +4 -3
- package/dist/components/charts/chart-skeleton.d.ts +0 -21
- package/dist/components/charts/chart-skeleton.js +2 -1
- package/dist/components/charts/chart-utils.d.ts +56 -0
- package/dist/components/charts/chart-utils.js +135 -0
- package/dist/components/charts/chart.d.ts +3 -1
- package/dist/components/charts/chart.js +13 -3
- package/dist/components/charts/chart.types.d.ts +3 -0
- package/dist/components/charts/donut-chart.d.ts +1 -1
- package/dist/components/charts/donut-chart.js +4 -3
- package/dist/components/charts/index.d.ts +1 -1
- package/dist/components/charts/index.js +1 -1
- package/dist/components/charts/line-chart.d.ts +1 -1
- package/dist/components/charts/line-chart.js +4 -3
- package/dist/components/charts/radial-bar-chart.d.ts +1 -1
- package/dist/components/charts/radial-bar-chart.js +4 -3
- package/dist/components/data-table/data-table.js +4 -2
- package/dist/components/data-table/data-table.types.d.ts +2 -0
- package/dist/components/data-table/index.d.ts +1 -1
- package/dist/components/data-table/index.js +1 -1
- package/dist/components/data-table/use-table-state.d.ts +29 -0
- package/dist/components/data-table/use-table-state.js +312 -0
- package/dist/components/dnd/dnd.d.ts +16 -98
- package/dist/components/dnd/dnd.js +30 -2
- package/dist/components/dnd/dnd.types.d.ts +92 -0
- package/dist/components/dnd/dnd.types.js +7 -0
- package/dist/components/dnd/index.d.ts +2 -1
- package/dist/components/dnd/index.js +1 -1
- package/dist/components/footer/footer.d.ts +2 -37
- package/dist/components/footer/footer.js +18 -2
- package/dist/components/footer/footer.types.d.ts +25 -0
- package/dist/components/footer/footer.types.js +2 -0
- package/dist/components/footer/index.d.ts +1 -1
- package/dist/components/header/header.js +5 -11
- package/dist/components/metric-card/index.d.ts +2 -1
- package/dist/components/metric-card/metric-card-skeleton.js +2 -1
- package/dist/components/metric-card/metric-card.d.ts +2 -14
- package/dist/components/metric-card/metric-card.js +32 -28
- package/dist/components/metric-card/metric-card.types.d.ts +3 -0
- package/dist/components/nav-link/index.d.ts +2 -1
- package/dist/components/nav-link/index.js +1 -1
- package/dist/components/nav-link/nav-link.d.ts +2 -25
- package/dist/components/nav-link/nav-link.js +43 -5
- package/dist/components/nav-link/nav-link.types.d.ts +23 -0
- package/dist/components/nav-link/nav-link.types.js +2 -0
- package/dist/components/navigation/index.d.ts +1 -1
- package/dist/components/navigation/navigation.d.ts +1 -24
- package/dist/components/navigation/navigation.js +7 -17
- package/dist/components/navigation/navigation.types.d.ts +15 -0
- package/dist/components/navigation/navigation.types.js +2 -0
- package/dist/components/session-card/index.d.ts +2 -1
- package/dist/components/session-card/index.js +1 -1
- package/dist/components/session-card/session-card-skeleton.d.ts +3 -1
- package/dist/components/session-card/session-card-skeleton.js +3 -2
- package/dist/components/session-card/session-card.d.ts +3 -26
- package/dist/components/session-card/session-card.js +23 -8
- package/dist/components/session-card/session-card.types.d.ts +26 -0
- package/dist/components/session-card/session-card.types.js +2 -0
- package/dist/components/sidebar/sidebar.js +5 -11
- package/dist/components/social-login-button/index.d.ts +3 -1
- package/dist/components/social-login-button/index.js +2 -1
- package/dist/components/social-login-button/social-login-button.d.ts +2 -20
- package/dist/components/social-login-button/social-login-button.js +9 -11
- package/dist/components/social-login-button/social-login-button.types.d.ts +28 -0
- package/dist/components/social-login-button/social-login-button.types.js +2 -0
- package/dist/components/social-login-button/social-login-buttons.d.ts +4 -0
- package/dist/components/social-login-button/social-login-buttons.js +18 -0
- package/dist/components/user-menu-dropdown/index.d.ts +2 -1
- package/dist/components/user-menu-dropdown/index.js +1 -1
- package/dist/components/user-menu-dropdown/user-menu-dropdown.d.ts +2 -31
- package/dist/components/user-menu-dropdown/user-menu-dropdown.js +14 -2
- package/dist/components/user-menu-dropdown/user-menu-dropdown.types.d.ts +16 -0
- package/dist/components/user-menu-dropdown/user-menu-dropdown.types.js +2 -0
- package/dist/config/app-shell.d.ts +13 -0
- package/dist/config/app-shell.js +20 -0
- package/dist/config/charts.d.ts +23 -0
- package/dist/config/charts.js +23 -0
- package/dist/config/footer.d.ts +16 -0
- package/dist/config/footer.js +16 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.js +7 -0
- package/dist/config/metric-card.d.ts +36 -0
- package/dist/config/metric-card.js +44 -0
- package/dist/config/session-card.d.ts +32 -0
- package/dist/config/session-card.js +32 -0
- package/dist/config/social-login-button.d.ts +15 -0
- package/dist/config/social-login-button.js +15 -0
- package/dist/config/user-menu-dropdown.d.ts +10 -0
- package/dist/config/user-menu-dropdown.js +10 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/styles/app.css +18 -53
- package/package.json +24 -28
|
@@ -4,8 +4,11 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
4
4
|
* Base Chart Component
|
|
5
5
|
* React component for ApexCharts with built-in loading and empty states
|
|
6
6
|
*/
|
|
7
|
+
import { useSlotStyles } from "@kala-ui/react/kala-provider";
|
|
8
|
+
import { applySlot } from "@kala-ui/react/lib/slot-styles";
|
|
7
9
|
import { cn } from "@kala-ui/react/lib/utils";
|
|
8
10
|
import { lazy, Suspense } from "react";
|
|
11
|
+
import { chartStyles } from "../../config/charts.js";
|
|
9
12
|
import { ChartSkeleton } from "./chart-skeleton.js";
|
|
10
13
|
// apexcharts touches `window` at module scope, so importing it statically
|
|
11
14
|
// crashes any SSR framework on the first render — and the root barrel
|
|
@@ -24,7 +27,10 @@ function getTooltipTheme() {
|
|
|
24
27
|
* Base Chart Component - use this as the foundation for all charts
|
|
25
28
|
* Provides consistent styling, loading skeleton, and empty state
|
|
26
29
|
*/
|
|
27
|
-
export function Chart({ className, isLoading, isEmpty, emptyMessage = "No data available", skeletonConfig, skeleton, ...props }) {
|
|
30
|
+
export function Chart({ className, isLoading, isEmpty, emptyMessage = "No data available", skeletonConfig, skeleton, slotStyles: slotStylesRaw, ...props }) {
|
|
31
|
+
const slotStyles = useSlotStyles("charts", slotStylesRaw);
|
|
32
|
+
const { "data-kala-component": _marker, ...chartProps } = props;
|
|
33
|
+
const markerProps = { "data-kala-component": _marker };
|
|
28
34
|
// Render skeleton if loading
|
|
29
35
|
if (isLoading) {
|
|
30
36
|
if (skeleton) {
|
|
@@ -32,6 +38,9 @@ export function Chart({ className, isLoading, isEmpty, emptyMessage = "No data a
|
|
|
32
38
|
}
|
|
33
39
|
return (_jsx(ChartSkeleton, { "data-kala-component": "charts-chart", ...(skeletonConfig || {}), className: className }));
|
|
34
40
|
}
|
|
41
|
+
const emptyPart = applySlot(chartStyles.empty, slotStyles?.empty);
|
|
42
|
+
const emptyIcon = applySlot(chartStyles.emptyIcon, slotStyles?.emptyIcon);
|
|
43
|
+
const emptyText = applySlot(chartStyles.emptyText, slotStyles?.emptyText);
|
|
35
44
|
// Render empty state when no data
|
|
36
45
|
if (isEmpty) {
|
|
37
46
|
// Height may arrive as a number (px) or a CSS length string ("20rem",
|
|
@@ -40,9 +49,10 @@ export function Chart({ className, isLoading, isEmpty, emptyMessage = "No data a
|
|
|
40
49
|
const height = typeof configuredHeight === "number"
|
|
41
50
|
? `${configuredHeight}px`
|
|
42
51
|
: configuredHeight;
|
|
43
|
-
return (_jsxs("div", { "data-kala-component": "charts-chart", className: cn(
|
|
52
|
+
return (_jsxs("div", { "data-kala-component": "charts-chart", className: cn(emptyPart.className, className), style: { height, ...emptyPart.style }, children: [_jsxs("svg", { className: emptyIcon.className, fill: "none", role: "img", "aria-label": "Chart empty state", stroke: "currentColor", viewBox: "0 0 24 24", children: [_jsx("title", { children: "No chart data" }), _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M3 13h2v8H3zM9 9h2v12H9zM15 5h2v16h-2zM21 1h2v20h-2z" })] }), _jsx("p", { className: emptyText.className, children: emptyMessage })] }));
|
|
44
53
|
}
|
|
45
|
-
|
|
54
|
+
const root = applySlot(cn(chartStyles.root, className), slotStyles?.root);
|
|
55
|
+
return (_jsx("div", { "data-kala-component": _marker ?? "charts-chart", className: root.className, style: root.style, children: _jsx(Suspense, { fallback: skeleton ?? (_jsx(ChartSkeleton, { ...(skeletonConfig || {}), className: className })), children: _jsx(ReactApexChart, { ...chartProps }) }) }));
|
|
46
56
|
}
|
|
47
57
|
export { ChartSkeleton } from "./chart-skeleton.js";
|
|
48
58
|
export { getTooltipTheme };
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Chart Component Types
|
|
3
3
|
* Type definitions for ApexCharts-based chart components
|
|
4
4
|
*/
|
|
5
|
+
import type { SlotStyles } from "@kala-ui/react/lib/slot-styles";
|
|
5
6
|
import type { ApexOptions } from "apexcharts";
|
|
6
7
|
/**
|
|
7
8
|
* Base chart props shared across all chart types
|
|
@@ -27,6 +28,8 @@ export interface BaseChartProps {
|
|
|
27
28
|
isLoading?: boolean;
|
|
28
29
|
/** Message shown when data is empty (default: "No data available") */
|
|
29
30
|
emptyMessage?: string;
|
|
31
|
+
/** Per-part overrides: root, empty, emptyIcon, emptyText, titleBlock, title, subtitle. */
|
|
32
|
+
slotStyles?: SlotStyles;
|
|
30
33
|
}
|
|
31
34
|
/**
|
|
32
35
|
* Series data point for single value charts
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* Circular charts for showing proportions and percentages
|
|
4
4
|
*/
|
|
5
5
|
import type { DonutChartProps } from "./chart.types";
|
|
6
|
-
export declare function DonutChart({ series, labels, colors, height, width, className, title, subtitle, animations, donut, donutSize, dataLabels, legend, isLoading, emptyMessage, options: customOptions, }: DonutChartProps): import("react").JSX.Element;
|
|
6
|
+
export declare function DonutChart({ series, labels, colors, height, width, className, title, subtitle, animations, donut, donutSize, dataLabels, legend, slotStyles, isLoading, emptyMessage, options: customOptions, }: DonutChartProps): import("react").JSX.Element;
|
|
7
7
|
export declare function PieChart(props: DonutChartProps): import("react").JSX.Element;
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
8
|
import { cn } from "@kala-ui/react/lib/utils";
|
|
9
9
|
import { useMemo } from "react";
|
|
10
|
+
import { chartStyles } from "../../config/charts.js";
|
|
10
11
|
import { Chart, getTooltipTheme } from "./chart.js";
|
|
11
12
|
import { useThemeAwareChart } from "./use-theme-aware-chart.js";
|
|
12
|
-
import { getDefaultChartOptions } from "./utils.js";
|
|
13
|
-
export function DonutChart({ series, labels, colors, height = 350, width = "100%", className, title, subtitle, animations = true, donut = true, donutSize = 65, dataLabels = true, legend = true, isLoading, emptyMessage, options: customOptions, }) {
|
|
13
|
+
import { getDefaultChartOptions } from "./chart-utils.js";
|
|
14
|
+
export function DonutChart({ series, labels, colors, height = 350, width = "100%", className, title, subtitle, animations = true, donut = true, donutSize = 65, dataLabels = true, legend = true, slotStyles, isLoading, emptyMessage, options: customOptions, }) {
|
|
14
15
|
const { colors: themeColors } = useThemeAwareChart();
|
|
15
16
|
const isEmpty = !isLoading && (series.length === 0 || labels.length === 0);
|
|
16
17
|
const chartOptions = useMemo(() => {
|
|
@@ -138,7 +139,7 @@ export function DonutChart({ series, labels, colors, height = 350, width = "100%
|
|
|
138
139
|
subtitle,
|
|
139
140
|
customOptions,
|
|
140
141
|
]);
|
|
141
|
-
return (_jsx(Chart, { "data-kala-component": "charts-donut-chart", className: cn(
|
|
142
|
+
return (_jsx(Chart, { "data-kala-component": "charts-donut-chart", className: cn(chartStyles.root, className), slotStyles: slotStyles, options: chartOptions, series: series, type: donut ? "donut" : "pie", height: height, width: width, isLoading: isLoading, isEmpty: isEmpty, emptyMessage: emptyMessage }));
|
|
142
143
|
}
|
|
143
144
|
// Convenience alias for Pie charts
|
|
144
145
|
export function PieChart(props) {
|
|
@@ -12,4 +12,4 @@ export { LineChart } from "./line-chart.js";
|
|
|
12
12
|
export { RadialBarChart } from "./radial-bar-chart.js";
|
|
13
13
|
export * from "./theme-utils.js";
|
|
14
14
|
export { useThemeAwareChart } from "./use-theme-aware-chart.js";
|
|
15
|
-
export * from "./utils.js";
|
|
15
|
+
export * from "./chart-utils.js";
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* Clean line charts for trends and time series data
|
|
4
4
|
*/
|
|
5
5
|
import type { LineChartProps } from "./chart.types";
|
|
6
|
-
export declare function LineChart({ series, categories, colors, height, width, className, title, subtitle, toolbar, animations, curve, strokeWidth, markers, yAxisLabel, isLoading, emptyMessage, options: customOptions, }: LineChartProps): import("react").JSX.Element;
|
|
6
|
+
export declare function LineChart({ series, categories, colors, height, width, className, title, subtitle, toolbar, animations, curve, strokeWidth, markers, yAxisLabel, slotStyles, isLoading, emptyMessage, options: customOptions, }: LineChartProps): import("react").JSX.Element;
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
8
|
import { cn } from "@kala-ui/react/lib/utils";
|
|
9
9
|
import { useMemo } from "react";
|
|
10
|
+
import { chartStyles } from "../../config/charts.js";
|
|
10
11
|
import { Chart, getTooltipTheme } from "./chart.js";
|
|
11
12
|
import { useThemeAwareChart } from "./use-theme-aware-chart.js";
|
|
12
|
-
import { getDefaultChartOptions } from "./utils.js";
|
|
13
|
+
import { getDefaultChartOptions } from "./chart-utils.js";
|
|
13
14
|
/**
|
|
14
15
|
* Check if series data is empty (no series or all series have empty data arrays)
|
|
15
16
|
*/
|
|
@@ -18,7 +19,7 @@ function isSeriesEmpty(series) {
|
|
|
18
19
|
return true;
|
|
19
20
|
return series.every((s) => !s.data || (Array.isArray(s.data) && s.data.length === 0));
|
|
20
21
|
}
|
|
21
|
-
export function LineChart({ series, categories, colors, height = 350, width = "100%", className, title, subtitle, toolbar = false, animations = true, curve = "smooth", strokeWidth = 2, markers = true, yAxisLabel, isLoading, emptyMessage, options: customOptions, }) {
|
|
22
|
+
export function LineChart({ series, categories, colors, height = 350, width = "100%", className, title, subtitle, toolbar = false, animations = true, curve = "smooth", strokeWidth = 2, markers = true, yAxisLabel, slotStyles, isLoading, emptyMessage, options: customOptions, }) {
|
|
22
23
|
const { colors: themeColors } = useThemeAwareChart();
|
|
23
24
|
const isEmpty = !isLoading && isSeriesEmpty(series);
|
|
24
25
|
const chartOptions = useMemo(() => {
|
|
@@ -154,5 +155,5 @@ export function LineChart({ series, categories, colors, height = 350, width = "1
|
|
|
154
155
|
subtitle,
|
|
155
156
|
customOptions,
|
|
156
157
|
]);
|
|
157
|
-
return (_jsx(Chart, { "data-kala-component": "charts-line-chart", className: cn(
|
|
158
|
+
return (_jsx(Chart, { "data-kala-component": "charts-line-chart", className: cn(chartStyles.root, className), slotStyles: slotStyles, options: chartOptions, series: series, type: "line", height: height, width: width, isLoading: isLoading, isEmpty: isEmpty, emptyMessage: emptyMessage }));
|
|
158
159
|
}
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* Circular progress bars for showing percentages and completion rates
|
|
4
4
|
*/
|
|
5
5
|
import type { RadialBarChartProps } from "./chart.types";
|
|
6
|
-
export declare function RadialBarChart({ series, labels, colors, height, width, className, title, subtitle, animations, dataLabels, hollowSize, isLoading, emptyMessage, options: customOptions, }: RadialBarChartProps): import("react").JSX.Element;
|
|
6
|
+
export declare function RadialBarChart({ series, labels, colors, height, width, className, title, subtitle, animations, dataLabels, hollowSize, slotStyles, isLoading, emptyMessage, options: customOptions, }: RadialBarChartProps): import("react").JSX.Element;
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
8
|
import { cn } from "@kala-ui/react/lib/utils";
|
|
9
9
|
import { useMemo } from "react";
|
|
10
|
+
import { chartStyles } from "../../config/charts.js";
|
|
10
11
|
import { Chart, getTooltipTheme } from "./chart.js";
|
|
11
12
|
import { useThemeAwareChart } from "./use-theme-aware-chart.js";
|
|
12
|
-
import { getDefaultChartOptions } from "./utils.js";
|
|
13
|
-
export function RadialBarChart({ series, labels, colors, height = 350, width = "100%", className, title, subtitle, animations = true, dataLabels = true, hollowSize = "50%", isLoading, emptyMessage, options: customOptions, }) {
|
|
13
|
+
import { getDefaultChartOptions } from "./chart-utils.js";
|
|
14
|
+
export function RadialBarChart({ series, labels, colors, height = 350, width = "100%", className, title, subtitle, animations = true, dataLabels = true, hollowSize = "50%", slotStyles, isLoading, emptyMessage, options: customOptions, }) {
|
|
14
15
|
const { colors: themeColors } = useThemeAwareChart();
|
|
15
16
|
const isEmpty = !isLoading && (series.length === 0 || labels.length === 0);
|
|
16
17
|
const chartOptions = useMemo(() => {
|
|
@@ -111,5 +112,5 @@ export function RadialBarChart({ series, labels, colors, height = 350, width = "
|
|
|
111
112
|
subtitle,
|
|
112
113
|
customOptions,
|
|
113
114
|
]);
|
|
114
|
-
return (_jsx(Chart, { "data-kala-component": "charts-radial-bar-chart", className: cn(
|
|
115
|
+
return (_jsx(Chart, { "data-kala-component": "charts-radial-bar-chart", className: cn(chartStyles.root, className), slotStyles: slotStyles, options: chartOptions, series: series, type: "radialBar", height: height, width: width, isLoading: isLoading, isEmpty: isEmpty, emptyMessage: emptyMessage }));
|
|
115
116
|
}
|
|
@@ -37,7 +37,7 @@ import { DataTableHeaderRow } from "./data-table-header-row.js";
|
|
|
37
37
|
import { DataTableSkeleton } from "./data-table-skeleton.js";
|
|
38
38
|
import { DataTableToolbar } from "./data-table-toolbar.js";
|
|
39
39
|
import { PaginationNav } from "./pagination-nav.js";
|
|
40
|
-
import { useTableState } from "./
|
|
40
|
+
import { useTableState } from "./use-table-state.js";
|
|
41
41
|
/**
|
|
42
42
|
* DataTable Component
|
|
43
43
|
*/
|
|
@@ -230,7 +230,9 @@ export function DataTable({ data, columns, searchable, pagination, defaultSort,
|
|
|
230
230
|
: "There is no data to display at the moment.",
|
|
231
231
|
...emptyState,
|
|
232
232
|
};
|
|
233
|
-
return (_jsxs(Stack, { "data-kala-component": "data-table", className: cn(stickyFooter ? "h-full min-h-0" : "", className), "aria-label": ariaLabel, role: ariaLabel ? "region" : undefined, gap: 0, children: [_jsx(DataTableToolbar, { searchConfig: searchConfig, searchQuery: searchQuery, onSearchChange: setSearchQuery, bulkActions: bulkActions, selectedRows: selectedRows, selectedIdsCount: selectedIds.size, toolbarContent: toolbarContent, hasData: data.length > 0, className: stickyFooter ? "shrink-0" : undefined }), filterableColumns && (_jsx("div", { className: stickyFooter ? "shrink-0" : undefined, children: _jsx(DataTableFilterChips, { filterConfigs: filterConfigs, filterableColumns: filterableColumns, onSetFilter: setFilter, onRemoveFilter: removeFilter, onClearFilters: clearFilters }) })), displayData.length === 0 && !filterConfigs.length && !searchQuery ? (_jsx(EmptyState, { title: emptyConfig.title, description: emptyConfig.description, ...(emptyState?.icon && { icon: emptyState.icon }), ...(emptyState?.action && { action: emptyState.action })
|
|
233
|
+
return (_jsxs(Stack, { "data-kala-component": "data-table", className: cn(stickyFooter ? "h-full min-h-0" : "", className), "aria-label": ariaLabel, role: ariaLabel ? "region" : undefined, gap: 0, children: [_jsx(DataTableToolbar, { searchConfig: searchConfig, searchQuery: searchQuery, onSearchChange: setSearchQuery, bulkActions: bulkActions, selectedRows: selectedRows, selectedIdsCount: selectedIds.size, toolbarContent: toolbarContent, hasData: data.length > 0, className: stickyFooter ? "shrink-0" : undefined }), filterableColumns && (_jsx("div", { className: stickyFooter ? "shrink-0" : undefined, children: _jsx(DataTableFilterChips, { filterConfigs: filterConfigs, filterableColumns: filterableColumns, onSetFilter: setFilter, onRemoveFilter: removeFilter, onClearFilters: clearFilters }) })), displayData.length === 0 && !filterConfigs.length && !searchQuery ? (_jsx(EmptyState, { title: emptyConfig.title, description: emptyConfig.description, ...(emptyState?.icon && { icon: emptyState.icon }), ...(emptyState?.action && { action: emptyState.action }), ...(emptyState?.slotStyles && {
|
|
234
|
+
slotStyles: emptyState.slotStyles,
|
|
235
|
+
}) })) : (_jsxs(_Fragment, { children: [_jsxs(Box, { className: cn("border relative overflow-hidden kala-surface-card", stickyFooter && paginationConfig && displayData.length > 0
|
|
234
236
|
? "flex-1 min-h-0"
|
|
235
237
|
: "", bordered && "border-2", paginationConfig && displayData.length > 0
|
|
236
238
|
? "rounded-t-lg border-b-0"
|
|
@@ -121,6 +121,8 @@ export interface EmptyStateConfig {
|
|
|
121
121
|
label: string;
|
|
122
122
|
onClick: () => void;
|
|
123
123
|
};
|
|
124
|
+
/** Per-part overrides forwarded to the embedded EmptyState */
|
|
125
|
+
slotStyles?: EmptyStateProps["slotStyles"];
|
|
124
126
|
}
|
|
125
127
|
/**
|
|
126
128
|
* Loading state configuration - DEPRECATED: Use DataTableSkeletonConfig
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { DataTable, DataTableSkeleton } from "./data-table";
|
|
5
5
|
export type { BulkAction, ColumnDef, DataTableProps, DataTableSkeletonConfig, EmptyStateConfig, FilterableColumn, FilterConfig, FilterOperator, LoadingConfig, PaginationConfig, SearchConfig, SelectionConfig, SortConfig, SortDirection, TableState, } from "./data-table.types";
|
|
6
|
-
export { useTableState } from "./
|
|
6
|
+
export { useTableState } from "./use-table-state";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useTableState Hook
|
|
3
|
+
* Manages table state including sorting, filtering, pagination, and search
|
|
4
|
+
*/
|
|
5
|
+
import type { ColumnDef, FilterConfig, SearchConfig, SortConfig, TableState } from "./data-table.types";
|
|
6
|
+
interface UseTableStateOptions<TData> {
|
|
7
|
+
data: TData[];
|
|
8
|
+
columns: ColumnDef<TData>[];
|
|
9
|
+
defaultSort?: SortConfig<TData>;
|
|
10
|
+
defaultFilters?: FilterConfig<TData>[];
|
|
11
|
+
searchConfig?: SearchConfig<TData> | boolean;
|
|
12
|
+
pageSize?: number;
|
|
13
|
+
total?: number;
|
|
14
|
+
/** Controlled page number (1-indexed); omitted = uncontrolled */
|
|
15
|
+
page?: number;
|
|
16
|
+
/** Callback when sort changes */
|
|
17
|
+
onSortChange?: (sort: SortConfig<TData> | null) => void;
|
|
18
|
+
/** Callback when filters change */
|
|
19
|
+
onFilterChange?: (filters: FilterConfig<TData>[]) => void;
|
|
20
|
+
/** Callback when search query changes */
|
|
21
|
+
onSearchChange?: (query: string) => void;
|
|
22
|
+
/** Callback when pagination changes */
|
|
23
|
+
onPaginationChange?: (page: number, pageSize: number) => void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Hook to manage table state (sorting, filtering, search, pagination)
|
|
27
|
+
*/
|
|
28
|
+
export declare function useTableState<TData>({ data, columns, defaultSort, defaultFilters, searchConfig, pageSize: initialPageSize, total: manualTotal, page, onSortChange, onFilterChange, onSearchChange, onPaginationChange, }: UseTableStateOptions<TData>): TableState<TData>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* useTableState Hook
|
|
4
|
+
* Manages table state including sorting, filtering, pagination, and search
|
|
5
|
+
*/
|
|
6
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
|
+
/**
|
|
8
|
+
* Hook to manage table state (sorting, filtering, search, pagination)
|
|
9
|
+
*/
|
|
10
|
+
export function useTableState({ data, columns, defaultSort, defaultFilters = [], searchConfig, pageSize: initialPageSize = 10, total: manualTotal, page, onSortChange, onFilterChange, onSearchChange, onPaginationChange, }) {
|
|
11
|
+
// State
|
|
12
|
+
const [sortConfig, setSortConfig] = useState(defaultSort ?? null);
|
|
13
|
+
const [filterConfigs, setFilterConfigs] = useState(defaultFilters);
|
|
14
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
15
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
16
|
+
const [pageSize, setPageSize] = useState(initialPageSize);
|
|
17
|
+
// Controlled page: mirror prop changes into internal state during render
|
|
18
|
+
// (adjust-state-on-prop-change). The hook keeps working uncontrolled when
|
|
19
|
+
// no `page` is supplied.
|
|
20
|
+
const prevControlledPageRef = useRef(undefined);
|
|
21
|
+
if (page !== undefined &&
|
|
22
|
+
page !== prevControlledPageRef.current &&
|
|
23
|
+
page !== currentPage) {
|
|
24
|
+
prevControlledPageRef.current = page;
|
|
25
|
+
setCurrentPage(page);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
prevControlledPageRef.current = page;
|
|
29
|
+
}
|
|
30
|
+
// Sync pageSize when initialPageSize changes (e.g. data loads and pagination is false)
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
setPageSize(initialPageSize);
|
|
33
|
+
}, [initialPageSize]);
|
|
34
|
+
// Get search keys
|
|
35
|
+
const searchKeys = useMemo(() => {
|
|
36
|
+
if (!searchConfig)
|
|
37
|
+
return [];
|
|
38
|
+
const defaultKeys = columns
|
|
39
|
+
.filter((col) => col.accessorKey)
|
|
40
|
+
.map((col) => col.accessorKey);
|
|
41
|
+
if (typeof searchConfig === "boolean") {
|
|
42
|
+
return defaultKeys;
|
|
43
|
+
}
|
|
44
|
+
return searchConfig.searchKeys ?? defaultKeys;
|
|
45
|
+
}, [searchConfig, columns]);
|
|
46
|
+
// Filter data based on search query
|
|
47
|
+
// When onSearchChange is provided, data is already server-filtered — skip client-side filtering
|
|
48
|
+
const searchedData = useMemo(() => {
|
|
49
|
+
if (onSearchChange)
|
|
50
|
+
return data;
|
|
51
|
+
if (!searchQuery.trim() || searchKeys.length === 0)
|
|
52
|
+
return data;
|
|
53
|
+
const query = searchQuery.toLowerCase();
|
|
54
|
+
return data.filter((row) => {
|
|
55
|
+
return searchKeys.some((key) => {
|
|
56
|
+
const value = row[key];
|
|
57
|
+
if (value === null || value === undefined)
|
|
58
|
+
return false;
|
|
59
|
+
return String(value).toLowerCase().includes(query);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}, [data, searchQuery, searchKeys, onSearchChange]);
|
|
63
|
+
// Apply filters
|
|
64
|
+
const filteredData = useMemo(() => {
|
|
65
|
+
if (filterConfigs.length === 0)
|
|
66
|
+
return searchedData;
|
|
67
|
+
return searchedData.filter((row) => {
|
|
68
|
+
return filterConfigs.every((filter) => {
|
|
69
|
+
const value = row[filter.key];
|
|
70
|
+
const filterValue = filter.value;
|
|
71
|
+
switch (filter.operator) {
|
|
72
|
+
case "equals":
|
|
73
|
+
if (typeof value === "boolean") {
|
|
74
|
+
return value === (filterValue === "true" || filterValue === true);
|
|
75
|
+
}
|
|
76
|
+
return value === filterValue;
|
|
77
|
+
case "contains":
|
|
78
|
+
return String(value)
|
|
79
|
+
.toLowerCase()
|
|
80
|
+
.includes(String(filterValue).toLowerCase());
|
|
81
|
+
case "startsWith":
|
|
82
|
+
return String(value)
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.startsWith(String(filterValue).toLowerCase());
|
|
85
|
+
case "endsWith":
|
|
86
|
+
return String(value)
|
|
87
|
+
.toLowerCase()
|
|
88
|
+
.endsWith(String(filterValue).toLowerCase());
|
|
89
|
+
case "in":
|
|
90
|
+
if (typeof value === "boolean") {
|
|
91
|
+
const strVal = String(value);
|
|
92
|
+
return (Array.isArray(filterValue) &&
|
|
93
|
+
filterValue.some((v) => v === strVal || v === value));
|
|
94
|
+
}
|
|
95
|
+
return Array.isArray(filterValue) && filterValue.includes(value);
|
|
96
|
+
default:
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}, [searchedData, filterConfigs]);
|
|
102
|
+
// Apply sorting
|
|
103
|
+
const sortedData = useMemo(() => {
|
|
104
|
+
if (!sortConfig)
|
|
105
|
+
return filteredData;
|
|
106
|
+
const { key, direction } = sortConfig;
|
|
107
|
+
const column = columns.find((col) => col.accessorKey === key);
|
|
108
|
+
return [...filteredData].sort((a, b) => {
|
|
109
|
+
// Use custom sort function if provided
|
|
110
|
+
if (column?.sortFn) {
|
|
111
|
+
return column.sortFn(a, b, direction);
|
|
112
|
+
}
|
|
113
|
+
// Default sort
|
|
114
|
+
const aValue = a[key];
|
|
115
|
+
const bValue = b[key];
|
|
116
|
+
if (aValue === bValue)
|
|
117
|
+
return 0;
|
|
118
|
+
if (aValue === null || aValue === undefined)
|
|
119
|
+
return 1;
|
|
120
|
+
if (bValue === null || bValue === undefined)
|
|
121
|
+
return -1;
|
|
122
|
+
// Handle different types
|
|
123
|
+
if (typeof aValue === "string" && typeof bValue === "string") {
|
|
124
|
+
const result = aValue.localeCompare(bValue);
|
|
125
|
+
return direction === "asc" ? result : -result;
|
|
126
|
+
}
|
|
127
|
+
if (typeof aValue === "number" && typeof bValue === "number") {
|
|
128
|
+
return direction === "asc" ? aValue - bValue : bValue - aValue;
|
|
129
|
+
}
|
|
130
|
+
if (aValue instanceof Date && bValue instanceof Date) {
|
|
131
|
+
return direction === "asc"
|
|
132
|
+
? aValue.getTime() - bValue.getTime()
|
|
133
|
+
: bValue.getTime() - aValue.getTime();
|
|
134
|
+
}
|
|
135
|
+
// Fallback to string comparison
|
|
136
|
+
const result = String(aValue).localeCompare(String(bValue));
|
|
137
|
+
return direction === "asc" ? result : -result;
|
|
138
|
+
});
|
|
139
|
+
}, [filteredData, sortConfig, columns]);
|
|
140
|
+
// Pagination
|
|
141
|
+
const totalPages = Math.ceil((manualTotal ?? sortedData.length) / pageSize);
|
|
142
|
+
const hasNextPage = currentPage < totalPages;
|
|
143
|
+
const hasPreviousPage = currentPage > 1;
|
|
144
|
+
const pageData = useMemo(() => {
|
|
145
|
+
// If manualTotal AND onPaginationChange are provided, assume server-side pagination
|
|
146
|
+
// Otherwise, do client-side pagination
|
|
147
|
+
if (manualTotal !== undefined && onPaginationChange !== undefined) {
|
|
148
|
+
return sortedData;
|
|
149
|
+
}
|
|
150
|
+
const startIndex = (currentPage - 1) * pageSize;
|
|
151
|
+
const endIndex = startIndex + pageSize;
|
|
152
|
+
return sortedData.slice(startIndex, endIndex);
|
|
153
|
+
}, [sortedData, currentPage, pageSize, manualTotal, onPaginationChange]);
|
|
154
|
+
// Toggle sort
|
|
155
|
+
const toggleSort = useCallback((key) => {
|
|
156
|
+
setSortConfig((prev) => {
|
|
157
|
+
let newSort;
|
|
158
|
+
if (!prev || prev.key !== key) {
|
|
159
|
+
newSort = { key, direction: "asc" };
|
|
160
|
+
}
|
|
161
|
+
else if (prev.direction === "asc") {
|
|
162
|
+
newSort = { key, direction: "desc" };
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
newSort = null; // Clear sort
|
|
166
|
+
}
|
|
167
|
+
return newSort;
|
|
168
|
+
});
|
|
169
|
+
setCurrentPage(1); // Reset to first page when sorting changes
|
|
170
|
+
}, []);
|
|
171
|
+
// Notify parent of sort changes after render. Calling the callback directly
|
|
172
|
+
// inside useEffect is safe — the setState that triggered this effect has
|
|
173
|
+
// already committed; the callback runs in the passive-effect phase, not
|
|
174
|
+
// during render. (The prior queueMicrotask deferral broke act() in tests:
|
|
175
|
+
// the microtask fired outside act()'s flush window, so sync assertions saw
|
|
176
|
+
// zero calls.)
|
|
177
|
+
const prevSortRef = useRef(defaultSort ?? null);
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (sortConfig !== prevSortRef.current) {
|
|
180
|
+
prevSortRef.current = sortConfig;
|
|
181
|
+
onSortChange?.(sortConfig);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
// Notify parent of filter changes after render.
|
|
185
|
+
const prevFilterRef = useRef(defaultFilters);
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
if (filterConfigs !== prevFilterRef.current) {
|
|
188
|
+
prevFilterRef.current = filterConfigs;
|
|
189
|
+
onFilterChange?.(filterConfigs);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
// Extract debounce ms from searchConfig
|
|
193
|
+
const debounceMs = useMemo(() => {
|
|
194
|
+
if (!searchConfig || typeof searchConfig === "boolean")
|
|
195
|
+
return 0;
|
|
196
|
+
return searchConfig.debounce ?? 0;
|
|
197
|
+
}, [searchConfig]);
|
|
198
|
+
// Notify parent of search changes after render. When a debounce is
|
|
199
|
+
// configured, the parent (server-side search) is notified only after the
|
|
200
|
+
// debounce window elapses — firing on every keystroke would hammer the
|
|
201
|
+
// server. The local searchQuery state updates immediately so the local
|
|
202
|
+
// (client-side) filtering stays responsive; only the parent callback waits.
|
|
203
|
+
// The effect depends on [searchQuery] only: re-running it on unrelated
|
|
204
|
+
// re-renders would clear the pending timer and silently swallow the
|
|
205
|
+
// callback, so the latest callback is read through a ref instead.
|
|
206
|
+
const prevSearchRef = useRef("");
|
|
207
|
+
const onSearchChangeRef = useRef(onSearchChange);
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
onSearchChangeRef.current = onSearchChange;
|
|
210
|
+
});
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (searchQuery === prevSearchRef.current)
|
|
213
|
+
return;
|
|
214
|
+
prevSearchRef.current = searchQuery;
|
|
215
|
+
const callback = onSearchChangeRef.current;
|
|
216
|
+
if (!callback)
|
|
217
|
+
return;
|
|
218
|
+
if (debounceMs <= 0) {
|
|
219
|
+
callback(searchQuery);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const timer = setTimeout(() => callback(searchQuery), debounceMs);
|
|
223
|
+
return () => clearTimeout(timer);
|
|
224
|
+
}, [searchQuery, debounceMs]);
|
|
225
|
+
// Notify parent of pagination changes after render.
|
|
226
|
+
const prevPaginationRef = useRef({ page: 1, pageSize: initialPageSize });
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
const prev = prevPaginationRef.current;
|
|
229
|
+
if (currentPage !== prev.page || pageSize !== prev.pageSize) {
|
|
230
|
+
prevPaginationRef.current = { page: currentPage, pageSize };
|
|
231
|
+
onPaginationChange?.(currentPage, pageSize);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
// Filter management
|
|
235
|
+
const setFilter = useCallback((filter) => {
|
|
236
|
+
setFilterConfigs((prev) => {
|
|
237
|
+
const filtered = prev.filter((f) => f.key !== filter.key);
|
|
238
|
+
return [...filtered, filter];
|
|
239
|
+
});
|
|
240
|
+
setCurrentPage(1);
|
|
241
|
+
}, []);
|
|
242
|
+
const removeFilter = useCallback((key) => {
|
|
243
|
+
setFilterConfigs((prev) => prev.filter((f) => f.key !== key));
|
|
244
|
+
setCurrentPage(1);
|
|
245
|
+
}, []);
|
|
246
|
+
const clearFilters = useCallback(() => {
|
|
247
|
+
setFilterConfigs([]);
|
|
248
|
+
setCurrentPage(1);
|
|
249
|
+
}, []);
|
|
250
|
+
const searchDebounceRef = useRef(null);
|
|
251
|
+
// Search management
|
|
252
|
+
const handleSetSearchQuery = useCallback((query) => {
|
|
253
|
+
setSearchQuery(query);
|
|
254
|
+
if (searchDebounceRef.current)
|
|
255
|
+
clearTimeout(searchDebounceRef.current);
|
|
256
|
+
if (debounceMs > 0) {
|
|
257
|
+
searchDebounceRef.current = setTimeout(() => {
|
|
258
|
+
setCurrentPage(1);
|
|
259
|
+
}, debounceMs);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
setCurrentPage(1);
|
|
263
|
+
}
|
|
264
|
+
}, [debounceMs]);
|
|
265
|
+
// Set current page and notify server-side listener
|
|
266
|
+
const handleSetCurrentPage = useCallback((pageOrUpdater) => {
|
|
267
|
+
setCurrentPage((prev) => {
|
|
268
|
+
const newPage = typeof pageOrUpdater === "function"
|
|
269
|
+
? pageOrUpdater(prev)
|
|
270
|
+
: pageOrUpdater;
|
|
271
|
+
return newPage;
|
|
272
|
+
});
|
|
273
|
+
}, []);
|
|
274
|
+
// Pagination navigation
|
|
275
|
+
const nextPage = useCallback(() => {
|
|
276
|
+
if (hasNextPage) {
|
|
277
|
+
handleSetCurrentPage((prev) => prev + 1);
|
|
278
|
+
}
|
|
279
|
+
}, [hasNextPage, handleSetCurrentPage]);
|
|
280
|
+
const previousPage = useCallback(() => {
|
|
281
|
+
if (hasPreviousPage) {
|
|
282
|
+
handleSetCurrentPage((prev) => prev - 1);
|
|
283
|
+
}
|
|
284
|
+
}, [hasPreviousPage, handleSetCurrentPage]);
|
|
285
|
+
// Page size change — reset page without direct callback
|
|
286
|
+
const handleSetPageSize = useCallback((size) => {
|
|
287
|
+
setPageSize(size);
|
|
288
|
+
setCurrentPage(1);
|
|
289
|
+
}, []);
|
|
290
|
+
return {
|
|
291
|
+
processedData: sortedData,
|
|
292
|
+
pageData,
|
|
293
|
+
sortConfig,
|
|
294
|
+
setSortConfig,
|
|
295
|
+
toggleSort,
|
|
296
|
+
filterConfigs,
|
|
297
|
+
setFilter,
|
|
298
|
+
removeFilter,
|
|
299
|
+
clearFilters,
|
|
300
|
+
searchQuery,
|
|
301
|
+
setSearchQuery: handleSetSearchQuery,
|
|
302
|
+
currentPage,
|
|
303
|
+
setCurrentPage: handleSetCurrentPage,
|
|
304
|
+
pageSize,
|
|
305
|
+
setPageSize: handleSetPageSize,
|
|
306
|
+
totalPages,
|
|
307
|
+
hasNextPage,
|
|
308
|
+
hasPreviousPage,
|
|
309
|
+
nextPage,
|
|
310
|
+
previousPage,
|
|
311
|
+
};
|
|
312
|
+
}
|