@carefully-built/cli 0.1.0 → 0.1.2
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 +148 -7
- package/dist/index.mjs +71 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/registry/ui/avatar/manifest.json +33 -0
- package/registry/ui/avatar/primitives/avatar.tsx +64 -0
- package/registry/ui/avatar/utils/cn.ts +6 -0
- package/registry/ui/button/manifest.json +24 -5
- package/registry/ui/button/utils/cn.ts +6 -0
- package/registry/ui/calendar/manifest.json +35 -0
- package/registry/ui/calendar/primitives/button.tsx +89 -0
- package/registry/ui/calendar/primitives/calendar.tsx +68 -0
- package/registry/ui/calendar/utils/cn.ts +6 -0
- package/registry/ui/card/manifest.json +36 -0
- package/registry/ui/card/primitives/card.tsx +80 -0
- package/registry/ui/card/utils/cn.ts +6 -0
- package/registry/ui/chip/manifest.json +36 -0
- package/registry/ui/chip/primitives/chip-utils.ts +10 -0
- package/registry/ui/chip/primitives/chip.tsx +74 -0
- package/registry/ui/chip/utils/cn.ts +6 -0
- package/registry/ui/chip-utils/manifest.json +33 -0
- package/registry/ui/chip-utils/primitives/chip-utils.ts +10 -0
- package/registry/ui/chip-utils/utils/cn.ts +6 -0
- package/registry/ui/date-display/manifest.json +33 -0
- package/registry/ui/date-display/utils/cn.ts +6 -0
- package/registry/ui/date-display/utils/date-display.ts +61 -0
- package/registry/ui/dialog/manifest.json +43 -0
- package/registry/ui/dialog/primitives/button.tsx +89 -0
- package/registry/ui/dialog/primitives/dialog.tsx +147 -0
- package/registry/ui/dialog/utils/cn.ts +6 -0
- package/registry/ui/display-date/manifest.json +36 -0
- package/registry/ui/display-date/primitives/display-date.tsx +20 -0
- package/registry/ui/display-date/utils/cn.ts +6 -0
- package/registry/ui/display-date/utils/date-display.ts +61 -0
- package/registry/ui/drawer/manifest.json +37 -0
- package/registry/ui/drawer/primitives/drawer.tsx +99 -0
- package/registry/ui/drawer/utils/cn.ts +6 -0
- package/registry/ui/dropdown-menu/manifest.json +37 -0
- package/registry/ui/dropdown-menu/primitives/dropdown-menu.tsx +140 -0
- package/registry/ui/dropdown-menu/utils/cn.ts +6 -0
- package/registry/ui/empty-state/empty-state/collection-empty-state.ts +29 -0
- package/registry/ui/empty-state/empty-state/empty-state-card.tsx +72 -0
- package/registry/ui/empty-state/empty-state/index.ts +8 -0
- package/registry/ui/empty-state/empty-state/initial-empty-state.tsx +36 -0
- package/registry/ui/empty-state/empty-state/no-results-state.tsx +20 -0
- package/registry/ui/empty-state/manifest.json +63 -0
- package/registry/ui/empty-state/primitives/button.tsx +89 -0
- package/registry/ui/empty-state/primitives/card.tsx +80 -0
- package/registry/ui/empty-state/utils/cn.ts +6 -0
- package/registry/ui/error-page/error-page/error-code.tsx +16 -0
- package/registry/ui/error-page/error-page/error-page-content.ts +75 -0
- package/registry/ui/error-page/error-page/index.ts +19 -0
- package/registry/ui/error-page/error-page/posthog-error-capture.ts +83 -0
- package/registry/ui/error-page/error-page/saas-error-page.tsx +146 -0
- package/registry/ui/error-page/manifest.json +64 -0
- package/registry/ui/error-page/primitives/button.tsx +89 -0
- package/registry/ui/error-page/utils/cn.ts +6 -0
- package/registry/ui/field-detail-row/manifest.json +32 -0
- package/registry/ui/field-detail-row/primitives/field-detail-row.tsx +28 -0
- package/registry/ui/field-detail-row/utils/cn.ts +6 -0
- package/registry/ui/file-dropzone/manifest.json +35 -0
- package/registry/ui/file-dropzone/primitives/button.tsx +89 -0
- package/registry/ui/file-dropzone/primitives/file-dropzone.tsx +236 -0
- package/registry/ui/file-dropzone/utils/cn.ts +6 -0
- package/registry/ui/help-info-button/manifest.json +72 -0
- package/registry/ui/help-info-button/overlays/responsive-sheet.footer.tsx +88 -0
- package/registry/ui/help-info-button/overlays/responsive-sheet.layouts.tsx +207 -0
- package/registry/ui/help-info-button/overlays/responsive-sheet.shortcuts.ts +103 -0
- package/registry/ui/help-info-button/overlays/responsive-sheet.tsx +132 -0
- package/registry/ui/help-info-button/primitives/button.tsx +89 -0
- package/registry/ui/help-info-button/primitives/drawer.tsx +99 -0
- package/registry/ui/help-info-button/primitives/help-info-button.tsx +63 -0
- package/registry/ui/help-info-button/primitives/keyboard-shortcut-hint.tsx +40 -0
- package/registry/ui/help-info-button/primitives/sheet.tsx +103 -0
- package/registry/ui/help-info-button/primitives/tooltip.tsx +57 -0
- package/registry/ui/help-info-button/utils/cn.ts +6 -0
- package/registry/ui/help-info-button/utils/use-media-query.ts +28 -0
- package/registry/ui/input/manifest.json +31 -0
- package/registry/ui/input/primitives/input.tsx +19 -0
- package/registry/ui/input/utils/cn.ts +6 -0
- package/registry/ui/keyboard-shortcut-hint/manifest.json +32 -0
- package/registry/ui/keyboard-shortcut-hint/primitives/keyboard-shortcut-hint.tsx +40 -0
- package/registry/ui/keyboard-shortcut-hint/utils/cn.ts +6 -0
- package/registry/ui/label/manifest.json +31 -0
- package/registry/ui/label/primitives/label.tsx +21 -0
- package/registry/ui/label/utils/cn.ts +6 -0
- package/registry/ui/pagination/manifest.json +36 -0
- package/registry/ui/pagination/primitives/button.tsx +89 -0
- package/registry/ui/pagination/primitives/pagination.tsx +143 -0
- package/registry/ui/pagination/utils/cn.ts +6 -0
- package/registry/ui/popover/manifest.json +33 -0
- package/registry/ui/popover/primitives/popover.tsx +46 -0
- package/registry/ui/popover/utils/cn.ts +6 -0
- package/registry/ui/responsive-sheet/manifest.json +66 -0
- package/registry/ui/responsive-sheet/overlays/responsive-sheet.footer.tsx +88 -0
- package/registry/ui/responsive-sheet/overlays/responsive-sheet.layouts.tsx +207 -0
- package/registry/ui/responsive-sheet/overlays/responsive-sheet.shortcuts.ts +103 -0
- package/registry/ui/responsive-sheet/overlays/responsive-sheet.tsx +132 -0
- package/registry/ui/responsive-sheet/primitives/button.tsx +89 -0
- package/registry/ui/responsive-sheet/primitives/drawer.tsx +99 -0
- package/registry/ui/responsive-sheet/primitives/keyboard-shortcut-hint.tsx +40 -0
- package/registry/ui/responsive-sheet/primitives/sheet.tsx +103 -0
- package/registry/ui/responsive-sheet/utils/cn.ts +6 -0
- package/registry/ui/responsive-sheet/utils/use-media-query.ts +28 -0
- package/registry/ui/responsive-sheet.footer/manifest.json +40 -0
- package/registry/ui/responsive-sheet.footer/overlays/responsive-sheet.footer.tsx +88 -0
- package/registry/ui/responsive-sheet.footer/primitives/button.tsx +89 -0
- package/registry/ui/responsive-sheet.footer/primitives/keyboard-shortcut-hint.tsx +40 -0
- package/registry/ui/responsive-sheet.footer/utils/cn.ts +6 -0
- package/registry/ui/responsive-sheet.shortcuts/manifest.json +34 -0
- package/registry/ui/responsive-sheet.shortcuts/overlays/responsive-sheet.shortcuts.ts +103 -0
- package/registry/ui/responsive-sheet.shortcuts/utils/cn.ts +6 -0
- package/registry/ui/scroll-fade-area/manifest.json +31 -0
- package/registry/ui/scroll-fade-area/primitives/scroll-fade-area.tsx +295 -0
- package/registry/ui/scroll-fade-area/utils/cn.ts +6 -0
- package/registry/ui/search/manifest.json +35 -0
- package/registry/ui/search/utils/cn.ts +6 -0
- package/registry/ui/search/utils/search.ts +227 -0
- package/registry/ui/searchable-select/manifest.json +48 -0
- package/registry/ui/searchable-select/primitives/input.tsx +19 -0
- package/registry/ui/searchable-select/search/searchable-select-position.ts +95 -0
- package/registry/ui/searchable-select/search/searchable-select.tsx +431 -0
- package/registry/ui/searchable-select/utils/cn.ts +6 -0
- package/registry/ui/searchable-select/utils/search.ts +227 -0
- package/registry/ui/searchable-select-position/manifest.json +32 -0
- package/registry/ui/searchable-select-position/search/searchable-select-position.ts +95 -0
- package/registry/ui/searchable-select-position/utils/cn.ts +6 -0
- package/registry/ui/segmented-toggle/manifest.json +41 -0
- package/registry/ui/segmented-toggle/primitives/scroll-fade-area.tsx +295 -0
- package/registry/ui/segmented-toggle/primitives/segmented-toggle.tsx +106 -0
- package/registry/ui/segmented-toggle/primitives/tabs.tsx +97 -0
- package/registry/ui/segmented-toggle/utils/cn.ts +6 -0
- package/registry/ui/select/manifest.json +37 -0
- package/registry/ui/select/primitives/select.tsx +142 -0
- package/registry/ui/select/utils/cn.ts +6 -0
- package/registry/ui/sheet/manifest.json +39 -0
- package/registry/ui/sheet/primitives/button.tsx +89 -0
- package/registry/ui/sheet/primitives/sheet.tsx +103 -0
- package/registry/ui/sheet/utils/cn.ts +6 -0
- package/registry/ui/skeleton/manifest.json +31 -0
- package/registry/ui/skeleton/primitives/skeleton.tsx +13 -0
- package/registry/ui/skeleton/utils/cn.ts +6 -0
- package/registry/ui/smart-table/manifest.json +115 -0
- package/registry/ui/smart-table/primitives/button.tsx +89 -0
- package/registry/ui/smart-table/primitives/card.tsx +80 -0
- package/registry/ui/smart-table/primitives/display-date.tsx +20 -0
- package/registry/ui/smart-table/primitives/pagination.tsx +143 -0
- package/registry/ui/smart-table/primitives/skeleton.tsx +13 -0
- package/registry/ui/smart-table/primitives/table.tsx +92 -0
- package/registry/ui/smart-table/primitives/tooltip.tsx +57 -0
- package/registry/ui/smart-table/smart-table/DesktopView.tsx +343 -0
- package/registry/ui/smart-table/smart-table/MobileView.tsx +170 -0
- package/registry/ui/smart-table/smart-table/SmartTable.tsx +85 -0
- package/registry/ui/smart-table/smart-table/SmartTableActions.tsx +71 -0
- package/registry/ui/smart-table/smart-table/TruncatedContent.tsx +147 -0
- package/registry/ui/smart-table/smart-table/index.ts +15 -0
- package/registry/ui/smart-table/smart-table/sorting.ts +148 -0
- package/registry/ui/smart-table/smart-table/truncated-content.utils.ts +22 -0
- package/registry/ui/smart-table/smart-table/types.ts +95 -0
- package/registry/ui/smart-table/smart-table/utils.ts +150 -0
- package/registry/ui/smart-table/utils/cn.ts +6 -0
- package/registry/ui/smart-table/utils/date-display.ts +61 -0
- package/registry/ui/smart-table/utils/use-media-query.ts +28 -0
- package/registry/ui/switch/manifest.json +31 -0
- package/registry/ui/switch/primitives/switch.tsx +31 -0
- package/registry/ui/switch/utils/cn.ts +6 -0
- package/registry/ui/table/manifest.json +38 -0
- package/registry/ui/table/primitives/table.tsx +92 -0
- package/registry/ui/table/utils/cn.ts +6 -0
- package/registry/ui/table-toolbar/manifest.json +93 -0
- package/registry/ui/table-toolbar/overlays/responsive-sheet.footer.tsx +88 -0
- package/registry/ui/table-toolbar/overlays/responsive-sheet.layouts.tsx +207 -0
- package/registry/ui/table-toolbar/overlays/responsive-sheet.shortcuts.ts +103 -0
- package/registry/ui/table-toolbar/overlays/responsive-sheet.tsx +132 -0
- package/registry/ui/table-toolbar/primitives/button.tsx +89 -0
- package/registry/ui/table-toolbar/primitives/drawer.tsx +99 -0
- package/registry/ui/table-toolbar/primitives/input.tsx +19 -0
- package/registry/ui/table-toolbar/primitives/keyboard-shortcut-hint.tsx +40 -0
- package/registry/ui/table-toolbar/primitives/sheet.tsx +103 -0
- package/registry/ui/table-toolbar/search/searchable-select-position.ts +95 -0
- package/registry/ui/table-toolbar/search/searchable-select.tsx +431 -0
- package/registry/ui/table-toolbar/table-toolbar/index.ts +9 -0
- package/registry/ui/table-toolbar/table-toolbar/table-toolbar.tsx +552 -0
- package/registry/ui/table-toolbar/utils/cn.ts +6 -0
- package/registry/ui/table-toolbar/utils/search.ts +227 -0
- package/registry/ui/table-toolbar/utils/use-media-query.ts +28 -0
- package/registry/ui/tabs/manifest.json +40 -0
- package/registry/ui/tabs/primitives/scroll-fade-area.tsx +295 -0
- package/registry/ui/tabs/primitives/tabs.tsx +97 -0
- package/registry/ui/tabs/utils/cn.ts +6 -0
- package/registry/ui/textarea/manifest.json +31 -0
- package/registry/ui/textarea/primitives/textarea.tsx +18 -0
- package/registry/ui/textarea/utils/cn.ts +6 -0
- package/registry/ui/tooltip/manifest.json +34 -0
- package/registry/ui/tooltip/primitives/tooltip.tsx +57 -0
- package/registry/ui/tooltip/utils/cn.ts +6 -0
- package/registry/ui/use-media-query/manifest.json +32 -0
- package/registry/ui/use-media-query/utils/cn.ts +6 -0
- package/registry/ui/use-media-query/utils/use-media-query.ts +28 -0
- package/registry/ui/user-picker/manifest.json +52 -0
- package/registry/ui/user-picker/primitives/avatar.tsx +64 -0
- package/registry/ui/user-picker/primitives/button.tsx +89 -0
- package/registry/ui/user-picker/primitives/input.tsx +19 -0
- package/registry/ui/user-picker/primitives/popover.tsx +46 -0
- package/registry/ui/user-picker/primitives/user-picker-utils.ts +113 -0
- package/registry/ui/user-picker/primitives/user-picker.tsx +226 -0
- package/registry/ui/user-picker/utils/cn.ts +6 -0
- package/registry/ui/user-picker-utils/manifest.json +38 -0
- package/registry/ui/user-picker-utils/primitives/user-picker-utils.ts +113 -0
- package/registry/ui/user-picker-utils/utils/cn.ts +6 -0
- package/registry/ui/button/cn.ts +0 -6
- /package/registry/ui/button/{button.tsx → primitives/button.tsx} +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getTruncatedContentAlignmentClass,
|
|
5
|
+
shouldRenderTooltipTrigger,
|
|
6
|
+
} from '@/components/ui/smart-table/truncated-content.utils';
|
|
7
|
+
|
|
8
|
+
import { useEffect, useRef, useState } from 'react';
|
|
9
|
+
|
|
10
|
+
import type { ReactNode } from 'react';
|
|
11
|
+
|
|
12
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
13
|
+
|
|
14
|
+
interface TruncatedContentProps {
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
tooltip?: string | null;
|
|
17
|
+
align?: 'left' | 'right' | 'center';
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function TruncatedContent({
|
|
22
|
+
children,
|
|
23
|
+
tooltip,
|
|
24
|
+
align = 'left',
|
|
25
|
+
className = '',
|
|
26
|
+
}: TruncatedContentProps): React.ReactElement {
|
|
27
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
28
|
+
const [isOverflowing, setIsOverflowing] = useState(false);
|
|
29
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
30
|
+
|
|
31
|
+
const updateOverflowState = (): boolean => {
|
|
32
|
+
const element = contentRef.current;
|
|
33
|
+
if (!element) {
|
|
34
|
+
setIsOverflowing(false);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const overflowing = element.scrollWidth > element.clientWidth + 1;
|
|
39
|
+
setIsOverflowing(overflowing);
|
|
40
|
+
return overflowing;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const element = contentRef.current;
|
|
45
|
+
if (!element) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
updateOverflowState();
|
|
50
|
+
|
|
51
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
52
|
+
updateOverflowState();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
resizeObserver.observe(element);
|
|
56
|
+
window.addEventListener('resize', updateOverflowState);
|
|
57
|
+
|
|
58
|
+
return () => {
|
|
59
|
+
resizeObserver.disconnect();
|
|
60
|
+
window.removeEventListener('resize', updateOverflowState);
|
|
61
|
+
};
|
|
62
|
+
}, [children, className, align, tooltip]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!isOverflowing && isOpen) {
|
|
66
|
+
setIsOpen(false);
|
|
67
|
+
}
|
|
68
|
+
}, [isOpen, isOverflowing]);
|
|
69
|
+
|
|
70
|
+
const alignmentClass = getTruncatedContentAlignmentClass(align);
|
|
71
|
+
const showTooltipTrigger = shouldRenderTooltipTrigger({
|
|
72
|
+
tooltip,
|
|
73
|
+
isOverflowing,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const content = (
|
|
77
|
+
<div
|
|
78
|
+
ref={contentRef}
|
|
79
|
+
className={[
|
|
80
|
+
'block min-w-0 w-full truncate',
|
|
81
|
+
alignmentClass,
|
|
82
|
+
className,
|
|
83
|
+
]
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.join(' ')}
|
|
86
|
+
>
|
|
87
|
+
{children}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (!showTooltipTrigger) {
|
|
92
|
+
return content;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<Tooltip
|
|
97
|
+
delayDuration={100}
|
|
98
|
+
open={isOverflowing ? isOpen : false}
|
|
99
|
+
onOpenChange={(nextOpen) => {
|
|
100
|
+
if (!nextOpen) {
|
|
101
|
+
setIsOpen(false);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setIsOpen(updateOverflowState());
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<TooltipTrigger asChild>
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
className={[
|
|
112
|
+
'block min-w-0 w-full overflow-hidden bg-transparent p-0 text-inherit outline-none',
|
|
113
|
+
alignmentClass,
|
|
114
|
+
isOverflowing ? 'cursor-help' : 'cursor-default',
|
|
115
|
+
]
|
|
116
|
+
.filter(Boolean)
|
|
117
|
+
.join(' ')}
|
|
118
|
+
onMouseEnter={() => {
|
|
119
|
+
updateOverflowState();
|
|
120
|
+
}}
|
|
121
|
+
onFocus={() => {
|
|
122
|
+
updateOverflowState();
|
|
123
|
+
}}
|
|
124
|
+
onClick={(event) => {
|
|
125
|
+
event.stopPropagation();
|
|
126
|
+
const overflowing = updateOverflowState();
|
|
127
|
+
if (overflowing) {
|
|
128
|
+
setIsOpen((previous) => !previous);
|
|
129
|
+
}
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
{content}
|
|
133
|
+
</button>
|
|
134
|
+
</TooltipTrigger>
|
|
135
|
+
<TooltipContent
|
|
136
|
+
side="top"
|
|
137
|
+
align={align === 'right' ? 'end' : align === 'center' ? 'center' : 'start'}
|
|
138
|
+
sideOffset={6}
|
|
139
|
+
collisionPadding={12}
|
|
140
|
+
avoidCollisions
|
|
141
|
+
className="w-[min(20rem,calc(100vw-1.5rem))] max-w-[calc(100vw-1.5rem)] whitespace-normal break-words text-left"
|
|
142
|
+
>
|
|
143
|
+
{tooltip}
|
|
144
|
+
</TooltipContent>
|
|
145
|
+
</Tooltip>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { SmartTable } from "@/components/ui/smart-table/SmartTable";
|
|
2
|
+
export { SmartTableActions } from "@/components/ui/smart-table/SmartTableActions";
|
|
3
|
+
export { TruncatedContent } from "@/components/ui/smart-table/TruncatedContent";
|
|
4
|
+
export type {
|
|
5
|
+
Column,
|
|
6
|
+
ColumnAlign,
|
|
7
|
+
SortDirection,
|
|
8
|
+
SortState,
|
|
9
|
+
SortValue,
|
|
10
|
+
ActionType,
|
|
11
|
+
ActionHandlers,
|
|
12
|
+
SmartTableProps,
|
|
13
|
+
PaginationConfig,
|
|
14
|
+
} from "@/components/ui/smart-table/types";
|
|
15
|
+
export { useTableSorting } from "@/components/ui/smart-table/sorting";
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { getColumnValue } from '@/components/ui/smart-table/utils';
|
|
6
|
+
|
|
7
|
+
import type { Column, SortDirection, SortState, SortValue } from '@/components/ui/smart-table/types';
|
|
8
|
+
|
|
9
|
+
interface UseTableSortingOptions<T> {
|
|
10
|
+
readonly data: readonly T[];
|
|
11
|
+
readonly columns: readonly Column<T>[];
|
|
12
|
+
readonly initialSortState?: SortState;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UseTableSortingResult<T> {
|
|
16
|
+
readonly sortedData: T[];
|
|
17
|
+
readonly sortState: SortState;
|
|
18
|
+
readonly setSortState: (state: SortState) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getColumnSortKey<T>(column: Column<T>): string | null {
|
|
22
|
+
if (column.sortable === false) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (column.sortKey) {
|
|
27
|
+
return column.sortKey;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return typeof column.accessor === 'string' ? column.accessor : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getNextSortState(currentState: SortState, key: string): SortState {
|
|
34
|
+
if (currentState?.key !== key) {
|
|
35
|
+
return { key, direction: 'asc' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (currentState.direction === 'asc') {
|
|
39
|
+
return { key, direction: 'desc' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeSortValue(value: SortValue): string | number | boolean | null {
|
|
46
|
+
if (value === null || value === undefined || value === '') {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (value instanceof Date) {
|
|
51
|
+
return value.getTime();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function compareSortValues(left: SortValue, right: SortValue): number {
|
|
58
|
+
const leftValue = normalizeSortValue(left);
|
|
59
|
+
const rightValue = normalizeSortValue(right);
|
|
60
|
+
|
|
61
|
+
if (leftValue === null && rightValue === null) {
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (leftValue === null) {
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (rightValue === null) {
|
|
70
|
+
return -1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (typeof leftValue === 'number' && typeof rightValue === 'number') {
|
|
74
|
+
return leftValue - rightValue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof leftValue === 'boolean' && typeof rightValue === 'boolean') {
|
|
78
|
+
return Number(leftValue) - Number(rightValue);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return String(leftValue).localeCompare(String(rightValue), 'it', {
|
|
82
|
+
numeric: true,
|
|
83
|
+
sensitivity: 'base',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getSortValue<T>(column: Column<T>, row: T): SortValue {
|
|
88
|
+
if (column.sortAccessor) {
|
|
89
|
+
return column.sortAccessor(row);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return getColumnValue(column, row) as SortValue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getSortColumn<T>(
|
|
96
|
+
columns: readonly Column<T>[],
|
|
97
|
+
sortState: SortState,
|
|
98
|
+
): Column<T> | null {
|
|
99
|
+
if (!sortState) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return columns.find((column) => getColumnSortKey(column) === sortState.key) ?? null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function applyDirection(value: number, direction: SortDirection): number {
|
|
107
|
+
return direction === 'asc' ? value : -value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function sortTableData<T>(
|
|
111
|
+
data: readonly T[],
|
|
112
|
+
columns: readonly Column<T>[],
|
|
113
|
+
sortState: SortState,
|
|
114
|
+
): T[] {
|
|
115
|
+
const sortColumn = getSortColumn(columns, sortState);
|
|
116
|
+
|
|
117
|
+
if (!sortColumn || !sortState) {
|
|
118
|
+
return [...data];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return data
|
|
122
|
+
.map((row, index) => ({ row, index }))
|
|
123
|
+
.sort((left, right) => {
|
|
124
|
+
const compared = compareSortValues(
|
|
125
|
+
getSortValue(sortColumn, left.row),
|
|
126
|
+
getSortValue(sortColumn, right.row),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return compared === 0
|
|
130
|
+
? left.index - right.index
|
|
131
|
+
: applyDirection(compared, sortState.direction);
|
|
132
|
+
})
|
|
133
|
+
.map(({ row }) => row);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function useTableSorting<T>({
|
|
137
|
+
data,
|
|
138
|
+
columns,
|
|
139
|
+
initialSortState = null,
|
|
140
|
+
}: UseTableSortingOptions<T>): UseTableSortingResult<T> {
|
|
141
|
+
const [sortState, setSortState] = useState(initialSortState);
|
|
142
|
+
const sortedData = useMemo(
|
|
143
|
+
() => sortTableData(data, columns, sortState),
|
|
144
|
+
[columns, data, sortState],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return { sortedData, sortState, setSortState };
|
|
148
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ColumnAlign } from '@/components/ui/smart-table/types';
|
|
2
|
+
|
|
3
|
+
export function getTruncatedContentAlignmentClass(
|
|
4
|
+
align?: ColumnAlign
|
|
5
|
+
): string {
|
|
6
|
+
if (align === 'right') {
|
|
7
|
+
return 'text-right';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (align === 'center') {
|
|
11
|
+
return 'text-center';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return 'text-left';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function shouldRenderTooltipTrigger(options: {
|
|
18
|
+
tooltip?: string | null;
|
|
19
|
+
isOverflowing: boolean;
|
|
20
|
+
}): boolean {
|
|
21
|
+
return Boolean(options.tooltip) && options.isOverflowing;
|
|
22
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type ColumnAlign = 'left' | 'center' | 'right';
|
|
4
|
+
export type SortDirection = 'asc' | 'desc';
|
|
5
|
+
export type SortValue = string | number | Date | boolean | null | undefined;
|
|
6
|
+
export type SortState = { key: string; direction: SortDirection } | null;
|
|
7
|
+
|
|
8
|
+
export interface Column<T> {
|
|
9
|
+
/** Column header text */
|
|
10
|
+
header: string;
|
|
11
|
+
/** Optional label override for mobile cards */
|
|
12
|
+
mobileLabel?: string;
|
|
13
|
+
/** Key to access data (supports nested paths like 'user.name') */
|
|
14
|
+
accessor?: keyof T | string;
|
|
15
|
+
/** Column width */
|
|
16
|
+
width?: string | number;
|
|
17
|
+
/** Text alignment */
|
|
18
|
+
align?: ColumnAlign;
|
|
19
|
+
/** Custom render function */
|
|
20
|
+
render?: (value: unknown, row: T) => ReactNode;
|
|
21
|
+
/** Enable header sorting for this column (defaults to true when an accessor or sortKey exists) */
|
|
22
|
+
sortable?: boolean;
|
|
23
|
+
/** Stable sort key when the accessor is not a string */
|
|
24
|
+
sortKey?: string;
|
|
25
|
+
/** Custom value used for sorting */
|
|
26
|
+
sortAccessor?: (row: T) => SortValue;
|
|
27
|
+
/** Enable text truncation wrapper in table cells (default: true) */
|
|
28
|
+
truncate?: boolean;
|
|
29
|
+
/** Hide this column on mobile cards */
|
|
30
|
+
hideOnMobile?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ActionType = 'view' | 'edit' | 'delete';
|
|
34
|
+
|
|
35
|
+
export interface ActionHandlers<T> {
|
|
36
|
+
onView?: (item: T) => void;
|
|
37
|
+
onEdit?: (item: T) => void;
|
|
38
|
+
onDelete?: (item: T) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PaginationConfig {
|
|
42
|
+
/** Current page (1-indexed) */
|
|
43
|
+
currentPage: number;
|
|
44
|
+
/** Total number of pages */
|
|
45
|
+
totalPages: number;
|
|
46
|
+
/** Total number of items */
|
|
47
|
+
totalItems: number;
|
|
48
|
+
/** Items per page */
|
|
49
|
+
pageSize: number;
|
|
50
|
+
/** Start index (0-indexed) */
|
|
51
|
+
startIndex: number;
|
|
52
|
+
/** End index (exclusive) */
|
|
53
|
+
endIndex: number;
|
|
54
|
+
/** Go to specific page */
|
|
55
|
+
onPageChange: (page: number) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface SmartTableProps<T> {
|
|
59
|
+
/** Data array to display */
|
|
60
|
+
data: T[];
|
|
61
|
+
/** Column definitions */
|
|
62
|
+
columns: Column<T>[];
|
|
63
|
+
/** Loading state - shows skeletons */
|
|
64
|
+
isLoading: boolean;
|
|
65
|
+
/** Number of skeleton rows to show when loading */
|
|
66
|
+
skeletonRows?: number;
|
|
67
|
+
/** Actions to show (will render action buttons) */
|
|
68
|
+
actions?: ActionType[];
|
|
69
|
+
/** Action handlers */
|
|
70
|
+
actionHandlers?: ActionHandlers<T>;
|
|
71
|
+
/** Custom actions renderer (overrides actions prop) */
|
|
72
|
+
renderActions?: (item: T) => ReactNode;
|
|
73
|
+
/** Message when no data */
|
|
74
|
+
noDataMessage?: string;
|
|
75
|
+
/** Custom content when no data */
|
|
76
|
+
noDataContent?: ReactNode;
|
|
77
|
+
/** Function to get unique key for each row */
|
|
78
|
+
getRowKey?: (item: T) => string | number;
|
|
79
|
+
/** Make rows clickable */
|
|
80
|
+
onRowClick?: (item: T) => void;
|
|
81
|
+
/** Custom mobile card renderer */
|
|
82
|
+
renderMobileCard?: (item: T) => ReactNode;
|
|
83
|
+
/** Enable pagination */
|
|
84
|
+
pagination?: PaginationConfig;
|
|
85
|
+
/** Enable sticky header with scrollable body */
|
|
86
|
+
stickyHeader?: boolean;
|
|
87
|
+
/** Max height for scrollable table (default: 'calc(100vh - 300px)') */
|
|
88
|
+
maxHeight?: string;
|
|
89
|
+
/** Fill remaining height of parent container (requires parent with flex and height set) */
|
|
90
|
+
fullHeight?: boolean;
|
|
91
|
+
/** Active sort state for sortable columns */
|
|
92
|
+
sortState?: SortState;
|
|
93
|
+
/** Called when a sortable column header is selected */
|
|
94
|
+
onSortChange?: (state: SortState) => void;
|
|
95
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createElement } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { Column } from '@/components/ui/smart-table/types';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import { DisplayDate } from '@/components/ui/display-date';
|
|
8
|
+
import { formatDisplayDate } from '@/components/ui/date-display';
|
|
9
|
+
|
|
10
|
+
function formatListEntry(value: unknown): string {
|
|
11
|
+
const formatted = formatValue(value);
|
|
12
|
+
|
|
13
|
+
if (typeof formatted === 'string' || typeof formatted === 'number') {
|
|
14
|
+
return String(formatted);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return '—';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get nested value from object using dot notation
|
|
22
|
+
* e.g., getNestedValue(obj, 'user.profile.name')
|
|
23
|
+
*/
|
|
24
|
+
function getNestedValue(obj: unknown, path: string): unknown {
|
|
25
|
+
return path.split('.').reduce((acc, key) => {
|
|
26
|
+
if (acc && typeof acc === 'object' && key in acc) {
|
|
27
|
+
return (acc as Record<string, unknown>)[key];
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}, obj);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Format a value for display
|
|
35
|
+
*/
|
|
36
|
+
function formatValue(value: unknown): ReactNode {
|
|
37
|
+
if (value === null || value === undefined) {
|
|
38
|
+
return '—';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof value === 'boolean') {
|
|
42
|
+
return value ? 'Si' : 'No';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (value instanceof Date) {
|
|
46
|
+
return createElement(DisplayDate, { value });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (typeof value === 'number') {
|
|
50
|
+
return value.toLocaleString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof value === 'string') {
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
if (value.length === 0) {
|
|
59
|
+
return '—';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return value.map((entry) => formatListEntry(entry)).join(', ');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof value === 'object') {
|
|
66
|
+
const record = value as Record<string, unknown>;
|
|
67
|
+
|
|
68
|
+
for (const key of ['label', 'name', 'title']) {
|
|
69
|
+
const nestedValue = record[key];
|
|
70
|
+
if (typeof nestedValue === 'string' || typeof nestedValue === 'number') {
|
|
71
|
+
return String(nestedValue);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return '[Object]';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatValueAsText(value: unknown): string | null {
|
|
80
|
+
if (value === null || value === undefined) {
|
|
81
|
+
return '—';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (typeof value === 'boolean') {
|
|
85
|
+
return value ? 'Si' : 'No';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (value instanceof Date) {
|
|
89
|
+
return formatDisplayDate(value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (typeof value === 'number') {
|
|
93
|
+
return value.toLocaleString();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (typeof value === 'string') {
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (Array.isArray(value)) {
|
|
101
|
+
if (value.length === 0) {
|
|
102
|
+
return '—';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return value.map((entry) => formatValueAsText(entry) ?? '—').join(', ');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (typeof value === 'object') {
|
|
109
|
+
const record = value as Record<string, unknown>;
|
|
110
|
+
|
|
111
|
+
for (const key of ['label', 'name', 'title']) {
|
|
112
|
+
const nestedValue = record[key];
|
|
113
|
+
if (typeof nestedValue === 'string' || typeof nestedValue === 'number') {
|
|
114
|
+
return String(nestedValue);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getColumnValue<T>(column: Column<T>, item: T): unknown {
|
|
123
|
+
if (!column.accessor) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (typeof column.accessor === 'string' && column.accessor.includes('.')) {
|
|
128
|
+
return getNestedValue(item, column.accessor);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return (item as Record<string, unknown>)[column.accessor as string];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function renderColumnValue<T>(column: Column<T>, item: T): ReactNode {
|
|
135
|
+
const value = getColumnValue(column, item);
|
|
136
|
+
|
|
137
|
+
if (column.render) {
|
|
138
|
+
return column.render(value, item);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return formatValue(value);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function getColumnTooltipText<T>(column: Column<T>, item: T): string | null {
|
|
145
|
+
if (column.render) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return formatValueAsText(getColumnValue(column, item));
|
|
150
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type DateDisplayValue = Date | number | string;
|
|
2
|
+
|
|
3
|
+
function getDate(value: DateDisplayValue): Date {
|
|
4
|
+
return value instanceof Date ? value : new Date(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function startOfDay(date: Date): Date {
|
|
8
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getDayDifference(from: Date, to: Date): number {
|
|
12
|
+
const millisecondsPerDay = 24 * 60 * 60 * 1000;
|
|
13
|
+
return Math.round((startOfDay(to).getTime() - startOfDay(from).getTime()) / millisecondsPerDay);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function capitalizeMonthLabel(value: string): string {
|
|
17
|
+
return value
|
|
18
|
+
.split(' ')
|
|
19
|
+
.map((part, index) => {
|
|
20
|
+
if (index !== 1 || part.length === 0) {
|
|
21
|
+
return part;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const [firstCharacter = '', ...restCharacters] = part;
|
|
25
|
+
return `${firstCharacter.toUpperCase()}${restCharacters.join('')}`;
|
|
26
|
+
})
|
|
27
|
+
.join(' ');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatAbsoluteDate(value: DateDisplayValue): string {
|
|
31
|
+
const date = getDate(value);
|
|
32
|
+
const currentYear = new Date().getFullYear();
|
|
33
|
+
const includesYear = date.getFullYear() !== currentYear;
|
|
34
|
+
|
|
35
|
+
return capitalizeMonthLabel(
|
|
36
|
+
new Intl.DateTimeFormat('it-IT', {
|
|
37
|
+
day: 'numeric',
|
|
38
|
+
month: 'short',
|
|
39
|
+
...(includesYear ? { year: 'numeric' } : {}),
|
|
40
|
+
}).format(date)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function formatDisplayDate(value: DateDisplayValue): string {
|
|
45
|
+
const date = getDate(value);
|
|
46
|
+
const dayDifference = getDayDifference(date, new Date());
|
|
47
|
+
|
|
48
|
+
if (dayDifference === 0) {
|
|
49
|
+
return 'Oggi';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (dayDifference === 1) {
|
|
53
|
+
return 'Ieri';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (dayDifference >= 2 && dayDifference <= 10) {
|
|
57
|
+
return `${String(dayDifference)} giorni fa`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return formatAbsoluteDate(date);
|
|
61
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
export function useMediaQuery(query: string, defaultValue = false): boolean {
|
|
6
|
+
const [matches, setMatches] = useState(defaultValue);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const mediaQuery = window.matchMedia(query);
|
|
10
|
+
|
|
11
|
+
const handleChange = (): void => {
|
|
12
|
+
setMatches(mediaQuery.matches);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
handleChange();
|
|
16
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
17
|
+
|
|
18
|
+
return () => {
|
|
19
|
+
mediaQuery.removeEventListener('change', handleChange);
|
|
20
|
+
};
|
|
21
|
+
}, [query]);
|
|
22
|
+
|
|
23
|
+
return matches;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useIsMobile(maxWidth = 767): boolean {
|
|
27
|
+
return useMediaQuery(`(max-width: ${String(maxWidth)}px)`);
|
|
28
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "switch",
|
|
3
|
+
"description": "Editable source registry entry for switch.",
|
|
4
|
+
"importPath": "@carefully-built/ui",
|
|
5
|
+
"exports": [
|
|
6
|
+
"Switch"
|
|
7
|
+
],
|
|
8
|
+
"dependencies": [
|
|
9
|
+
"class-variance-authority",
|
|
10
|
+
"clsx",
|
|
11
|
+
"tailwind-merge"
|
|
12
|
+
],
|
|
13
|
+
"peerDependencies": [
|
|
14
|
+
"react",
|
|
15
|
+
"react-dom",
|
|
16
|
+
"radix-ui",
|
|
17
|
+
"lucide-react",
|
|
18
|
+
"react-day-picker",
|
|
19
|
+
"vaul"
|
|
20
|
+
],
|
|
21
|
+
"files": [
|
|
22
|
+
{
|
|
23
|
+
"source": "primitives/switch.tsx",
|
|
24
|
+
"target": "components/ui/switch.tsx"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"source": "utils/cn.ts",
|
|
28
|
+
"target": "lib/utils.ts"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|