@carefully-built/crud 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 ADDED
@@ -0,0 +1,70 @@
1
+ # Carefully Built CRUD
2
+
3
+ Config-driven CRUD table helpers for Carefully Built SaaS apps.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @carefully-built/crud @carefully-built/ui
9
+ ```
10
+
11
+ ## What It Includes
12
+
13
+ - `useCrudTableState`: search, select filters, sorting, pagination, and empty-state derivation for table CRUD screens.
14
+ - `CrudTableView`: shared toolbar plus `SmartTable` rendering with consistent empty-state and pagination wiring.
15
+ - `CrudListTable`: direct CRUD table wrapper for pages that already own their toolbar/filter layout.
16
+ - `CrudResourceSheet`: responsive create/edit sheet for resource forms, with confirm/cancel footer wiring.
17
+ - CRUD types for resource configs, filters, and empty-state values.
18
+
19
+ ## Basic Usage
20
+
21
+ ```tsx
22
+ const table = useCrudTableState({
23
+ data: items,
24
+ columns,
25
+ searchFields: ["name", "description"],
26
+ filters: [
27
+ { key: "status", config: STATUS_FILTER },
28
+ { key: "priority", config: PRIORITY_FILTER },
29
+ ],
30
+ });
31
+
32
+ <CrudTableView
33
+ state={table}
34
+ columns={columns}
35
+ isLoading={isLoading}
36
+ searchPlaceholder="Search..."
37
+ actions={["edit", "delete"]}
38
+ actionHandlers={actionHandlers}
39
+ />;
40
+ ```
41
+
42
+ Keep domain actions, mutations, and labels inside the consuming app. This package owns the repeated table mechanics.
43
+
44
+ ## Sheet Usage
45
+
46
+ Use `CrudResourceSheet` for create/edit forms instead of wiring `ResponsiveSheet` repeatedly in each app.
47
+
48
+ ```tsx
49
+ <CrudResourceSheet
50
+ open={isOpen}
51
+ onOpenChange={setIsOpen}
52
+ title="Edit contact"
53
+ description="Update the contact details."
54
+ formId="contact-form"
55
+ confirmLabel="Save"
56
+ confirmLoading={isSaving}
57
+ classes={{
58
+ body: 'gap-4',
59
+ footer: 'border-t',
60
+ }}
61
+ >
62
+ <ContactForm id="contact-form" />
63
+ </CrudResourceSheet>
64
+ ```
65
+
66
+ `className`, `contentClassName`, `footerClassName`, and `classes` are optional. Omit them to keep the default kit style.
67
+
68
+ ## Component Docs
69
+
70
+ - [CRUD Table](./docs/crud-table.md)
@@ -0,0 +1,211 @@
1
+ import { ActionHandlers, ActionType, Column, FilterConfig, ResponsiveSheetClassNames, SortState } from "@carefully-built/ui";
2
+ import { ReactNode } from "react";
3
+
4
+ //#region src/pagination.d.ts
5
+ interface CrudPaginationState {
6
+ readonly currentPage: number;
7
+ readonly totalPages: number;
8
+ readonly totalItems: number;
9
+ readonly pageSize: number;
10
+ readonly startIndex: number;
11
+ readonly endIndex: number;
12
+ readonly onPageChange: (page: number) => void;
13
+ }
14
+ //#endregion
15
+ //#region src/types.d.ts
16
+ type CrudEmptyState = "initial" | "no-results";
17
+ interface CrudFilterDefinition<TItem> {
18
+ readonly key: Extract<keyof TItem, string>;
19
+ readonly config: FilterConfig;
20
+ readonly allowAll?: boolean;
21
+ readonly clearable?: boolean;
22
+ }
23
+ interface UseCrudTableStateOptions<TItem> {
24
+ readonly data: readonly TItem[];
25
+ readonly columns: readonly Column<TItem>[];
26
+ readonly searchFields?: readonly Extract<keyof TItem, string>[];
27
+ readonly filters?: readonly CrudFilterDefinition<TItem>[];
28
+ readonly pageSize?: number;
29
+ readonly initialSortState?: SortState;
30
+ }
31
+ interface CrudTableState<TItem> {
32
+ readonly filteredData: TItem[];
33
+ readonly sortedData: TItem[];
34
+ readonly paginatedData: TItem[];
35
+ readonly search: string;
36
+ readonly setSearch: (value: string) => void;
37
+ readonly filters: Record<string, string>;
38
+ readonly setFilter: (key: string, value: string) => void;
39
+ readonly clearAll: () => void;
40
+ readonly getDraftFilterResultCount: (draftValues: Record<string, string>) => number;
41
+ readonly hasSearch: boolean;
42
+ readonly hasFilters: boolean;
43
+ readonly emptyState: CrudEmptyState;
44
+ readonly sortState: SortState;
45
+ readonly setSortState: (state: SortState) => void;
46
+ readonly pagination: CrudPaginationState;
47
+ }
48
+ interface CrudTableViewProps<TItem> {
49
+ readonly state: CrudTableState<TItem>;
50
+ readonly columns: readonly Column<TItem>[];
51
+ readonly isLoading: boolean;
52
+ readonly searchPlaceholder?: string;
53
+ readonly filters?: readonly CrudFilterDefinition<TItem>[];
54
+ readonly actions?: readonly ActionType[];
55
+ readonly actionHandlers?: ActionHandlers<TItem>;
56
+ readonly renderActions?: (item: TItem) => ReactNode;
57
+ readonly noDataMessage?: string;
58
+ readonly initialEmptyContent?: ReactNode;
59
+ readonly noResultsContent?: ReactNode;
60
+ readonly getRowKey?: (item: TItem) => string | number;
61
+ readonly onRowClick?: (item: TItem) => void;
62
+ readonly renderMobileCard?: (item: TItem) => ReactNode;
63
+ readonly stickyHeader?: boolean;
64
+ readonly fullHeight?: boolean;
65
+ readonly maxHeight?: string;
66
+ }
67
+ interface CrudDataTableProps<TItem> {
68
+ readonly data: readonly TItem[];
69
+ readonly columns: readonly Column<TItem>[];
70
+ readonly isLoading: boolean;
71
+ readonly actions?: readonly ActionType[];
72
+ readonly actionHandlers?: ActionHandlers<TItem>;
73
+ readonly renderActions?: (item: TItem) => ReactNode;
74
+ readonly noDataMessage?: string;
75
+ readonly noDataContent?: ReactNode;
76
+ readonly getRowKey?: (item: TItem) => string | number;
77
+ readonly onRowClick?: (item: TItem) => void;
78
+ readonly renderMobileCard?: (item: TItem) => ReactNode;
79
+ readonly stickyHeader?: boolean;
80
+ readonly fullHeight?: boolean;
81
+ readonly maxHeight?: string;
82
+ readonly sortState?: SortState;
83
+ readonly onSortChange?: (state: SortState) => void;
84
+ readonly pagination?: CrudPaginationState;
85
+ }
86
+ //#endregion
87
+ //#region src/crud-data-table.d.ts
88
+ declare function CrudDataTable<TItem extends object>({
89
+ actions,
90
+ actionHandlers,
91
+ columns,
92
+ data,
93
+ fullHeight,
94
+ getRowKey,
95
+ isLoading,
96
+ maxHeight,
97
+ noDataContent,
98
+ noDataMessage,
99
+ onRowClick,
100
+ pagination,
101
+ renderActions,
102
+ renderMobileCard,
103
+ sortState,
104
+ stickyHeader,
105
+ onSortChange
106
+ }: CrudDataTableProps<TItem>): React.ReactElement;
107
+ //#endregion
108
+ //#region src/crud-list-table.d.ts
109
+ type CrudListTableProps<TItem extends object> = CrudDataTableProps<TItem>;
110
+ declare function CrudListTable<TItem extends object>(props: CrudListTableProps<TItem>): React.ReactElement;
111
+ //#endregion
112
+ //#region src/crud-resource-sheet.d.ts
113
+ interface CrudResourceSheetProps {
114
+ readonly open: boolean;
115
+ readonly onOpenChange: (open: boolean) => void;
116
+ readonly title: ReactNode;
117
+ readonly children: ReactNode;
118
+ readonly formId?: string;
119
+ readonly description?: ReactNode;
120
+ readonly onCancel?: () => void;
121
+ readonly cancelLabel?: ReactNode;
122
+ readonly onConfirm?: () => void;
123
+ readonly confirmLabel?: ReactNode;
124
+ readonly confirmDisabled?: boolean;
125
+ readonly confirmLoading?: boolean;
126
+ readonly confirmCloseWhenDirty?: boolean;
127
+ readonly width?: number;
128
+ readonly className?: string;
129
+ readonly contentClassName?: string;
130
+ readonly footerClassName?: string;
131
+ readonly classes?: ResponsiveSheetClassNames;
132
+ }
133
+ declare function CrudResourceSheet({
134
+ children,
135
+ formId,
136
+ onConfirm,
137
+ confirmCloseWhenDirty: _confirmCloseWhenDirty,
138
+ ...sheetProps
139
+ }: CrudResourceSheetProps): React.ReactElement;
140
+ //#endregion
141
+ //#region src/crud-table-view.d.ts
142
+ declare function CrudTableView<TItem extends object>({
143
+ state,
144
+ columns,
145
+ isLoading,
146
+ searchPlaceholder,
147
+ filters,
148
+ actions,
149
+ actionHandlers,
150
+ renderActions,
151
+ noDataMessage,
152
+ initialEmptyContent,
153
+ noResultsContent,
154
+ getRowKey,
155
+ onRowClick,
156
+ renderMobileCard,
157
+ stickyHeader,
158
+ fullHeight,
159
+ maxHeight
160
+ }: CrudTableViewProps<TItem>): React.ReactElement;
161
+ //#endregion
162
+ //#region src/use-crud-table-state.d.ts
163
+ declare function useCrudTableState<TItem extends object>({
164
+ data,
165
+ columns,
166
+ searchFields,
167
+ filters: filterDefinitions,
168
+ pageSize,
169
+ initialSortState
170
+ }: UseCrudTableStateOptions<TItem>): CrudTableState<TItem>;
171
+ //#endregion
172
+ //#region src/use-url-string-filters.d.ts
173
+ interface UrlStringFilterDefinition<TKey extends string = string> {
174
+ readonly key: TKey;
175
+ readonly param?: string;
176
+ readonly defaultValue?: string;
177
+ readonly clearValue?: string;
178
+ }
179
+ type UrlStringFilterValues<TDefinitions extends readonly UrlStringFilterDefinition[]> = { readonly [TDefinition in TDefinitions[number] as TDefinition["key"]]: string };
180
+ interface UrlStringFiltersState<TDefinitions extends readonly UrlStringFilterDefinition[]> {
181
+ readonly values: UrlStringFilterValues<TDefinitions>;
182
+ readonly setValue: (key: TDefinitions[number]["key"], value: string) => void;
183
+ readonly clear: () => void;
184
+ readonly getDraftValues: (draftValues: Record<string, string>) => UrlStringFilterValues<TDefinitions>;
185
+ }
186
+ declare function useUrlStringFilters<const TDefinitions extends readonly UrlStringFilterDefinition[]>(definitions: TDefinitions): UrlStringFiltersState<TDefinitions>;
187
+ //#endregion
188
+ //#region src/use-url-pagination.d.ts
189
+ interface UseUrlPaginationOptions {
190
+ readonly totalItems: number;
191
+ readonly pageSize?: number;
192
+ readonly pageParam?: string;
193
+ }
194
+ interface UrlPaginationState extends CrudPaginationState {
195
+ readonly hasPrevPage: boolean;
196
+ readonly hasNextPage: boolean;
197
+ readonly goToPage: (page: number) => void;
198
+ readonly nextPage: () => void;
199
+ readonly prevPage: () => void;
200
+ readonly firstPage: () => void;
201
+ readonly lastPage: () => void;
202
+ readonly paginate: <T>(data: readonly T[]) => T[];
203
+ }
204
+ declare function useUrlPagination({
205
+ totalItems,
206
+ pageSize,
207
+ pageParam
208
+ }: UseUrlPaginationOptions): UrlPaginationState;
209
+ //#endregion
210
+ export { CrudDataTable, type CrudDataTableProps, type CrudEmptyState, type CrudFilterDefinition, CrudListTable, type CrudListTableProps, type CrudPaginationState, CrudResourceSheet, type CrudResourceSheetProps, type CrudTableState, CrudTableView, type CrudTableViewProps, type UrlPaginationState, type UrlStringFilterDefinition, type UrlStringFiltersState, type UseCrudTableStateOptions, type UseUrlPaginationOptions, useCrudTableState, useUrlPagination, useUrlStringFilters };
211
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/pagination.ts","../src/types.ts","../src/crud-data-table.tsx","../src/crud-list-table.tsx","../src/crud-resource-sheet.tsx","../src/crud-table-view.tsx","../src/use-crud-table-state.ts","../src/use-url-string-filters.ts","../src/use-url-pagination.ts"],"sourcesContent":[],"mappings":";;;;UAAiB,mBAAA;;;;EAAA,SAAA,QAAA,EAAA,MAAmB;;;;ACKpC;;;KAAY,cAAA;ADLK,UCOA,oBDPmB,CAAA,KAAA,CAAA,CAAA;gBCQpB,cAAc;mBACX;;EAJP,SAAA,SAAc,CAAA,EAAA,OAAA;AAE1B;AAC8B,UAMb,wBANa,CAAA,KAAA,CAAA,CAAA;EAAd,SAAA,IAAA,EAAA,SAOU,KAPV,EAAA;EACG,SAAA,OAAA,EAAA,SAOU,MAPV,CAOiB,KAPjB,CAAA,EAAA;EAAY,SAAA,YAAA,CAAA,EAAA,SAQI,OARJ,CAAA,MAQkB,KARlB,EAAA,MAAA,CAAA,EAAA;EAKd,SAAA,OAAA,CAAA,EAAA,SAIa,oBAJW,CAIU,KAJV,CAAA,EAAA;EACf,SAAA,QAAA,CAAA,EAAA,MAAA;EACU,SAAA,gBAAA,CAAA,EAIN,SAJM;;AACa,UAMhC,cANgC,CAAA,KAAA,CAAA,CAAA;EAAd,SAAA,YAAA,EAOV,KAPU,EAAA;EACgB,SAAA,UAAA,EAO5B,KAP4B,EAAA;EAArB,SAAA,aAAA,EAQJ,KARI,EAAA;EAEA,SAAA,MAAA,EAAA,MAAA;EAAS,SAAA,SAAA,EAAA,CAAA,KAAA,EAAA,MAAA,EAAA,GAAA,IAAA;EAGtB,SAAA,OAAA,EAMG,MANW,CAAA,MAAA,EAAA,MAAA,CAAA;EACN,SAAA,SAAA,EAAA,CAAA,GAAA,EAAA,MAAA,EAAA,KAAA,EAAA,MAAA,EAAA,GAAA,IAAA;EACF,SAAA,QAAA,EAAA,GAAA,GAAA,IAAA;EACG,SAAA,yBAAA,EAAA,CAAA,WAAA,EAOT,MAPS,CAAA,MAAA,EAAA,MAAA,CAAA,EAAA,GAAA,MAAA;EAGN,SAAA,SAAA,EAAA,OAAA;EAIH,SAAA,UAAA,EAAA,OAAA;EAIM,SAAA,UAAA,EAAA,cAAA;EACD,SAAA,SAAA,EAAA,SAAA;EACW,SAAA,YAAA,EAAA,CAAA,KAAA,EAAA,SAAA,EAAA,GAAA,IAAA;EACV,SAAA,UAAA,EAAA,mBAAA;;AAGN,UAAA,kBAAkB,CAAA,KAAA,CAAA,CAAA;EACF,SAAA,KAAA,EAAf,cAAe,CAAA,KAAA,CAAA;EAAf,SAAA,OAAA,EAAA,SACW,MADX,CACkB,KADlB,CAAA,EAAA;EACkB,SAAA,SAAA,EAAA,OAAA;EAAP,SAAA,iBAAA,CAAA,EAAA,MAAA;EAGsB,SAAA,OAAA,CAAA,EAAA,SAArB,oBAAqB,CAAA,KAAA,CAAA,EAAA;EAArB,SAAA,OAAA,CAAA,EAAA,SACA,UADA,EAAA;EACA,SAAA,cAAA,CAAA,EACF,cADE,CACa,KADb,CAAA;EACa,SAAA,aAAA,CAAA,EAAA,CAAA,IAAA,EACT,KADS,EAAA,GACC,SADD;EAAf,SAAA,aAAA,CAAA,EAAA,MAAA;EACM,SAAA,mBAAA,CAAA,EAED,SAFC;EAAU,SAAA,gBAAA,CAAA,EAGd,SAHc;EAEX,SAAA,SAAA,CAAA,EAAA,CAAA,IAAA,EAEH,KAFG,EAAA,GAAA,MAAA,GAAA,MAAA;EACH,SAAA,UAAA,CAAA,EAAA,CAAA,IAAA,EAEC,KAFD,EAAA,GAAA,IAAA;EACA,SAAA,gBAAA,CAAA,EAAA,CAAA,IAAA,EAEO,KAFP,EAAA,GAEiB,SAFjB;EACC,SAAA,YAAA,CAAA,EAAA,OAAA;EACM,SAAA,UAAA,CAAA,EAAA,OAAA;EAAU,SAAA,SAAA,CAAA,EAAA,MAAA;;AAM9B,UAAA,kBAAkB,CAAA,KAAA,CAAA,CAAA;EACT,SAAA,IAAA,EAAA,SAAA,KAAA,EAAA;EACU,SAAA,OAAA,EAAA,SAAP,MAAO,CAAA,KAAA,CAAA,EAAA;EAAP,SAAA,SAAA,EAAA,OAAA;EAEC,SAAA,OAAA,CAAA,EAAA,SAAA,UAAA,EAAA;EACa,SAAA,cAAA,CAAA,EAAf,cAAe,CAAA,KAAA,CAAA;EAAf,SAAA,aAAA,CAAA,EAAA,CAAA,IAAA,EACM,KADN,EAAA,GACgB,SADhB;EACM,SAAA,aAAA,CAAA,EAAA,MAAA;EAAU,SAAA,aAAA,CAAA,EAEjB,SAFiB;EAEjB,SAAA,SAAA,CAAA,EAAA,CAAA,IAAA,EACG,KADH,EAAA,GAAA,MAAA,GAAA,MAAA;EACG,SAAA,UAAA,CAAA,EAAA,CAAA,IAAA,EACC,KADD,EAAA,GAAA,IAAA;EACC,SAAA,gBAAA,CAAA,EAAA,CAAA,IAAA,EACM,KADN,EAAA,GACgB,SADhB;EACM,SAAA,YAAA,CAAA,EAAA,OAAA;EAAU,SAAA,UAAA,CAAA,EAAA,OAAA;EAIxB,SAAA,SAAA,CAAA,EAAA,MAAA;EACW,SAAA,SAAA,CAAA,EADX,SACW;EACV,SAAA,YAAA,CAAA,EAAA,CAAA,KAAA,EADU,SACV,EAAA,GAAA,IAAA;EAAmB,SAAA,UAAA,CAAA,EAAnB,mBAAmB;;;;iBCxE3B;;;;;;;;;;;;;;;;;;GAkBb,mBAAmB,SAAS,KAAA,CAAM;;;KCrBzB,2CAA2C,mBAAmB;iBAE1D,2CACP,mBAAmB,SACzB,KAAA,CAAM;;;UCFQ,sBAAA;;EJPA,SAAA,YAAA,EAAmB,CAAA,IAAA,EAAA,OAAA,EAAA,GAAA,IAAA;kBIUlB;qBACG;;EHNT,SAAA,WAAc,CAAA,EGQD,SHRC;EAET,SAAA,QAAA,CAAA,EAAA,GAAoB,GAAA,IAAA;EACP,SAAA,WAAA,CAAA,EGOL,SHPK;EAAd,SAAA,SAAA,CAAA,EAAA,GAAA,GAAA,IAAA;EACG,SAAA,YAAA,CAAA,EGQO,SHRP;EAAY,SAAA,eAAA,CAAA,EAAA,OAAA;EAKd,SAAA,cAAA,CAAA,EAAA,OAAwB;EACf,SAAA,qBAAA,CAAA,EAAA,OAAA;EACU,SAAA,KAAA,CAAA,EAAA,MAAA;EAAP,SAAA,SAAA,CAAA,EAAA,MAAA;EACoB,SAAA,gBAAA,CAAA,EAAA,MAAA;EAAd,SAAA,eAAA,CAAA,EAAA,MAAA;EACgB,SAAA,OAAA,CAAA,EGO9B,yBHP8B;;AAErB,iBGQd,iBAAA,CHRc;EAAA,QAAA;EAAA,MAAA;EAAA,SAAA;EAAA,qBAAA,EGYL,sBHZK;EAAA,GAAA;AAAA,CAAA,EGc3B,sBHd2B,CAAA,EGcF,KAAA,CAAM,YHdJ;;;iBIdd;;;;;;;;;;;;;;;;;;GAkBb,mBAAmB,SAAS,KAAA,CAAM;;;iBC2BrB;;;;WAIL;;;GAGR,yBAAyB,SAAS,eAAe;;;UCrDnC;gBACD;;;EPNC,SAAA,UAAA,CAAA,EAAmB,MAAA;;KOY/B,oDAAoD,0DAC9B,wBAAwB,6BNRnD;AAEiB,UMSA,qBNToB,CAAA,qBAAA,SMUL,yBNVK,EAAA,CAAA,CAAA;EACP,SAAA,MAAA,EMWX,qBNXW,CMWW,YNXX,CAAA;EAAd,SAAA,QAAA,EAAA,CAAA,GAAA,EMYW,YNZX,CAAA,MAAA,CAAA,CAAA,KAAA,CAAA,EAAA,KAAA,EAAA,MAAA,EAAA,GAAA,IAAA;EACG,SAAA,KAAA,EAAA,GAAA,GAAA,IAAA;EAAY,SAAA,cAAA,EAAA,CAAA,WAAA,EMcd,MNdc,CAAA,MAAA,EAAA,MAAA,CAAA,EAAA,GMexB,qBNfwB,CMeF,YNfE,CAAA;AAK/B;AAC0B,iBMkCV,mBNlCU,CAAA,2BAAA,SMmCY,yBNnCZ,EAAA,CAAA,CAAA,WAAA,EMoCX,YNpCW,CAAA,EMoCI,qBNpCJ,CMoC0B,YNpC1B,CAAA;;;UORT,uBAAA;;;ERPA,SAAA,SAAA,CAAA,EAAA,MAAmB;;UQanB,kBAAA,SAA2B;;EPRhC,SAAA,WAAc,EAAA,OAAA;EAET,SAAA,QAAA,EAAA,CAAA,IAAoB,EAAA,MAAA,EAAA,GAAA,IAAA;EACP,SAAA,QAAA,EAAA,GAAA,GAAA,IAAA;EAAd,SAAA,QAAA,EAAA,GAAA,GAAA,IAAA;EACG,SAAA,SAAA,EAAA,GAAA,GAAA,IAAA;EAAY,SAAA,QAAA,EAAA,GAAA,GAAA,IAAA;EAKd,SAAA,QAAA,EAAA,CAAA,CAAA,CAAA,CAAA,IAAwB,EAAA,SOOD,CPPC,EAAA,EAAA,GOOO,CPPP,EAAA;;AAEL,iBOQpB,gBAAA,CPRoB;EAAA,UAAA;EAAA,QAAA;EAAA;AAAA,CAAA,EOYjC,uBPZiC,CAAA,EOYP,kBPZO"}
package/dist/index.mjs ADDED
@@ -0,0 +1,322 @@
1
+ import { ResponsiveSheet, SmartTable, TableToolbar, useTableSorting } from "@carefully-built/ui";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useMemo, useState } from "react";
4
+ import { parseAsInteger, parseAsString, useQueryState, useQueryStates } from "nuqs";
5
+
6
+ //#region src/crud-data-table.tsx
7
+ function CrudDataTable({ actions, actionHandlers, columns, data, fullHeight = true, getRowKey, isLoading, maxHeight, noDataContent, noDataMessage, onRowClick, pagination, renderActions, renderMobileCard, sortState, stickyHeader = true, onSortChange }) {
8
+ return /* @__PURE__ */ jsx(SmartTable, {
9
+ data: [...data],
10
+ columns: [...columns],
11
+ isLoading,
12
+ actions: actions ? [...actions] : void 0,
13
+ actionHandlers,
14
+ renderActions,
15
+ noDataMessage,
16
+ noDataContent,
17
+ getRowKey,
18
+ onRowClick,
19
+ renderMobileCard,
20
+ stickyHeader,
21
+ fullHeight,
22
+ maxHeight,
23
+ sortState,
24
+ onSortChange,
25
+ pagination
26
+ });
27
+ }
28
+
29
+ //#endregion
30
+ //#region src/crud-list-table.tsx
31
+ function CrudListTable(props) {
32
+ return /* @__PURE__ */ jsx(CrudDataTable, { ...props });
33
+ }
34
+
35
+ //#endregion
36
+ //#region src/crud-resource-sheet.tsx
37
+ function CrudResourceSheet({ children, formId, onConfirm, confirmCloseWhenDirty: _confirmCloseWhenDirty, ...sheetProps }) {
38
+ const submitForm = () => {
39
+ const form = formId ? document.getElementById(formId) : null;
40
+ if (form instanceof HTMLFormElement) {
41
+ form.requestSubmit();
42
+ return;
43
+ }
44
+ onConfirm?.();
45
+ };
46
+ return /* @__PURE__ */ jsx(ResponsiveSheet, {
47
+ ...sheetProps,
48
+ onConfirm: formId || onConfirm ? submitForm : void 0,
49
+ children
50
+ });
51
+ }
52
+
53
+ //#endregion
54
+ //#region src/crud-table-view.tsx
55
+ function CrudTableView({ state, columns, isLoading, searchPlaceholder = "Search...", filters = [], actions, actionHandlers, renderActions, noDataMessage, initialEmptyContent, noResultsContent, getRowKey, onRowClick, renderMobileCard, stickyHeader = true, fullHeight = true, maxHeight }) {
56
+ const emptyContent = state.emptyState === "no-results" ? noResultsContent : initialEmptyContent;
57
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
58
+ className: "shrink-0",
59
+ children: /* @__PURE__ */ jsx(TableToolbar, {
60
+ search: {
61
+ value: state.search,
62
+ onChange: state.setSearch,
63
+ placeholder: searchPlaceholder
64
+ },
65
+ filters: filters.map((filter) => ({
66
+ config: filter.config,
67
+ value: state.filters[filter.key] ?? "all",
68
+ onChange: (value) => {
69
+ state.setFilter(filter.key, value);
70
+ },
71
+ allowAll: filter.allowAll,
72
+ clearable: filter.clearable
73
+ })),
74
+ onClearAll: state.clearAll,
75
+ getDraftResultCount: state.getDraftFilterResultCount
76
+ })
77
+ }), /* @__PURE__ */ jsx(SmartTable, {
78
+ data: state.paginatedData,
79
+ columns: [...columns],
80
+ isLoading,
81
+ actions: actions ? [...actions] : void 0,
82
+ actionHandlers,
83
+ renderActions,
84
+ noDataMessage,
85
+ noDataContent: emptyContent,
86
+ getRowKey,
87
+ onRowClick,
88
+ renderMobileCard,
89
+ stickyHeader,
90
+ fullHeight,
91
+ maxHeight,
92
+ sortState: state.sortState,
93
+ onSortChange: state.setSortState,
94
+ pagination: state.pagination
95
+ })] });
96
+ }
97
+
98
+ //#endregion
99
+ //#region src/search.ts
100
+ function normalizeSearchText(value) {
101
+ return value.toLocaleLowerCase().normalize("NFD").replace(/\p{Diacritic}/gu, "").trim();
102
+ }
103
+ function buildCrudSearchText(values) {
104
+ return normalizeSearchText(values.filter((value) => typeof value === "string" || typeof value === "number").map(String).join(" "));
105
+ }
106
+ function matchesCrudSearch(searchText, query) {
107
+ const normalizedQuery = normalizeSearchText(query);
108
+ if (!normalizedQuery) return true;
109
+ return normalizedQuery.split(/\s+/).every((token) => searchText.includes(token));
110
+ }
111
+
112
+ //#endregion
113
+ //#region src/pagination.ts
114
+ function getValidPage(page, totalPages) {
115
+ return Math.min(Math.max(1, page), totalPages);
116
+ }
117
+ function paginateCrudData(data, currentPage, pageSize) {
118
+ const startIndex = (currentPage - 1) * pageSize;
119
+ return data.slice(startIndex, startIndex + pageSize);
120
+ }
121
+
122
+ //#endregion
123
+ //#region src/use-crud-table-state.ts
124
+ function buildInitialFilters(filters) {
125
+ return Object.fromEntries(filters.map((filter) => [filter.key, "all"]));
126
+ }
127
+ function itemMatchesFilters(item, filters) {
128
+ const record = item;
129
+ return Object.entries(filters).every(([key, filterValue]) => {
130
+ if (!filterValue || filterValue === "all") return true;
131
+ return record[key] === filterValue;
132
+ });
133
+ }
134
+ function filterCrudData(data, search, filters, searchFields) {
135
+ return data.filter((item) => {
136
+ if (!itemMatchesFilters(item, filters)) return false;
137
+ const record = item;
138
+ return matchesCrudSearch(buildCrudSearchText(searchFields.map((field) => record[field])), search);
139
+ });
140
+ }
141
+ function useCrudTableState({ data, columns, searchFields = [], filters: filterDefinitions = [], pageSize = 20, initialSortState = null }) {
142
+ const [search, setSearch] = useState("");
143
+ const [filters, setFilters] = useState(() => buildInitialFilters(filterDefinitions));
144
+ const [currentPage, setCurrentPage] = useState(1);
145
+ const filteredData = useMemo(() => filterCrudData(data, search, filters, searchFields), [
146
+ data,
147
+ filters,
148
+ search,
149
+ searchFields
150
+ ]);
151
+ const { sortedData, sortState, setSortState } = useTableSorting({
152
+ data: filteredData,
153
+ columns,
154
+ initialSortState
155
+ });
156
+ const totalPages = Math.max(1, Math.ceil(filteredData.length / pageSize));
157
+ const validCurrentPage = getValidPage(currentPage, totalPages);
158
+ const startIndex = (validCurrentPage - 1) * pageSize;
159
+ const endIndex = Math.min(startIndex + pageSize, filteredData.length);
160
+ const paginatedData = useMemo(() => paginateCrudData(sortedData, validCurrentPage, pageSize), [
161
+ pageSize,
162
+ sortedData,
163
+ validCurrentPage
164
+ ]);
165
+ const setFilter = useCallback((key, value) => {
166
+ setFilters((currentFilters) => ({
167
+ ...currentFilters,
168
+ [key]: value
169
+ }));
170
+ setCurrentPage(1);
171
+ }, []);
172
+ const updateSearch = useCallback((value) => {
173
+ setSearch(value);
174
+ setCurrentPage(1);
175
+ }, []);
176
+ const clearAll = useCallback(() => {
177
+ setSearch("");
178
+ setFilters(buildInitialFilters(filterDefinitions));
179
+ setCurrentPage(1);
180
+ }, [filterDefinitions]);
181
+ const getDraftFilterResultCount = useCallback((draftValues) => filterCrudData(data, search, {
182
+ ...filters,
183
+ ...draftValues
184
+ }, searchFields).length, [
185
+ data,
186
+ filters,
187
+ search,
188
+ searchFields
189
+ ]);
190
+ const hasSearch = search.trim().length > 0;
191
+ const hasFilters = Object.values(filters).some((value) => value && value !== "all");
192
+ return {
193
+ filteredData,
194
+ sortedData,
195
+ paginatedData,
196
+ search,
197
+ setSearch: updateSearch,
198
+ filters,
199
+ setFilter,
200
+ clearAll,
201
+ getDraftFilterResultCount,
202
+ hasSearch,
203
+ hasFilters,
204
+ emptyState: hasSearch || hasFilters ? "no-results" : "initial",
205
+ sortState,
206
+ setSortState,
207
+ pagination: {
208
+ currentPage: validCurrentPage,
209
+ totalPages,
210
+ totalItems: filteredData.length,
211
+ pageSize,
212
+ startIndex,
213
+ endIndex,
214
+ onPageChange: setCurrentPage
215
+ }
216
+ };
217
+ }
218
+
219
+ //#endregion
220
+ //#region src/use-url-string-filters.ts
221
+ function buildParserMap(definitions) {
222
+ return Object.fromEntries(definitions.map((definition) => [definition.param ?? definition.key, parseAsString.withDefault(definition.defaultValue ?? "all")]));
223
+ }
224
+ function normalizeFilterValue(value, definition) {
225
+ if (value === (definition.clearValue ?? definition.defaultValue ?? "all")) return null;
226
+ return value.length > 0 ? value : null;
227
+ }
228
+ function useUrlStringFilters(definitions) {
229
+ const [params, setParams] = useQueryStates(useMemo(() => buildParserMap(definitions), [definitions]));
230
+ const values = useMemo(() => Object.fromEntries(definitions.map((definition) => {
231
+ const param = definition.param ?? definition.key;
232
+ return [definition.key, params[param] ?? definition.defaultValue ?? "all"];
233
+ })), [definitions, params]);
234
+ return {
235
+ values,
236
+ setValue: useCallback((key, value) => {
237
+ const definition = definitions.find((item) => item.key === key);
238
+ if (!definition) return;
239
+ setParams({ [definition.param ?? definition.key]: normalizeFilterValue(value, definition) });
240
+ }, [definitions, setParams]),
241
+ clear: useCallback(() => {
242
+ setParams(Object.fromEntries(definitions.map((definition) => [definition.param ?? definition.key, null])));
243
+ }, [definitions, setParams]),
244
+ getDraftValues: useCallback((draftValues) => ({
245
+ ...values,
246
+ ...draftValues
247
+ }), [values])
248
+ };
249
+ }
250
+
251
+ //#endregion
252
+ //#region src/use-url-pagination.ts
253
+ function useUrlPagination({ totalItems, pageSize = 20, pageParam = "page" }) {
254
+ const [page, setPage] = useQueryState(pageParam, parseAsInteger.withDefault(1));
255
+ const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
256
+ const currentPage = Math.min(Math.max(1, page), totalPages);
257
+ const startIndex = (currentPage - 1) * pageSize;
258
+ const endIndex = Math.min(startIndex + pageSize, totalItems);
259
+ const hasPrevPage = currentPage > 1;
260
+ const hasNextPage = currentPage < totalPages;
261
+ const goToPage = useCallback((newPage) => {
262
+ const validPage = Math.min(Math.max(1, newPage), totalPages);
263
+ setPage(validPage === 1 ? null : validPage);
264
+ }, [setPage, totalPages]);
265
+ const nextPage = useCallback(() => {
266
+ if (hasNextPage) goToPage(currentPage + 1);
267
+ }, [
268
+ currentPage,
269
+ goToPage,
270
+ hasNextPage
271
+ ]);
272
+ const prevPage = useCallback(() => {
273
+ if (hasPrevPage) goToPage(currentPage - 1);
274
+ }, [
275
+ currentPage,
276
+ goToPage,
277
+ hasPrevPage
278
+ ]);
279
+ const firstPage = useCallback(() => {
280
+ goToPage(1);
281
+ }, [goToPage]);
282
+ const lastPage = useCallback(() => {
283
+ goToPage(totalPages);
284
+ }, [goToPage, totalPages]);
285
+ const paginate = useCallback((data) => data.slice(startIndex, endIndex), [endIndex, startIndex]);
286
+ return useMemo(() => ({
287
+ currentPage,
288
+ pageSize,
289
+ totalPages,
290
+ totalItems,
291
+ startIndex,
292
+ endIndex,
293
+ hasPrevPage,
294
+ hasNextPage,
295
+ goToPage,
296
+ nextPage,
297
+ prevPage,
298
+ firstPage,
299
+ lastPage,
300
+ paginate,
301
+ onPageChange: goToPage
302
+ }), [
303
+ currentPage,
304
+ endIndex,
305
+ firstPage,
306
+ goToPage,
307
+ hasNextPage,
308
+ hasPrevPage,
309
+ lastPage,
310
+ nextPage,
311
+ pageSize,
312
+ paginate,
313
+ prevPage,
314
+ startIndex,
315
+ totalItems,
316
+ totalPages
317
+ ]);
318
+ }
319
+
320
+ //#endregion
321
+ export { CrudDataTable, CrudListTable, CrudResourceSheet, CrudTableView, useCrudTableState, useUrlPagination, useUrlStringFilters };
322
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/crud-data-table.tsx","../src/crud-list-table.tsx","../src/crud-resource-sheet.tsx","../src/crud-table-view.tsx","../src/search.ts","../src/pagination.ts","../src/use-crud-table-state.ts","../src/use-url-string-filters.ts","../src/use-url-pagination.ts"],"sourcesContent":["\"use client\";\n\nimport { SmartTable } from \"@carefully-built/ui\";\n\nimport type {\n CrudDataTableProps,\n} from \"./types\";\n\nexport function CrudDataTable<TItem extends object>({\n actions,\n actionHandlers,\n columns,\n data,\n fullHeight = true,\n getRowKey,\n isLoading,\n maxHeight,\n noDataContent,\n noDataMessage,\n onRowClick,\n pagination,\n renderActions,\n renderMobileCard,\n sortState,\n stickyHeader = true,\n onSortChange,\n}: CrudDataTableProps<TItem>): React.ReactElement {\n return (\n <SmartTable\n data={[...data]}\n columns={[...columns]}\n isLoading={isLoading}\n actions={actions ? [...actions] : undefined}\n actionHandlers={actionHandlers}\n renderActions={renderActions}\n noDataMessage={noDataMessage}\n noDataContent={noDataContent}\n getRowKey={getRowKey}\n onRowClick={onRowClick}\n renderMobileCard={renderMobileCard}\n stickyHeader={stickyHeader}\n fullHeight={fullHeight}\n maxHeight={maxHeight}\n sortState={sortState}\n onSortChange={onSortChange}\n pagination={pagination}\n />\n );\n}\n","\"use client\";\n\nimport { CrudDataTable } from \"./crud-data-table\";\nimport type { CrudDataTableProps } from \"./types\";\n\nexport type CrudListTableProps<TItem extends object> = CrudDataTableProps<TItem>;\n\nexport function CrudListTable<TItem extends object>(\n props: CrudListTableProps<TItem>,\n): React.ReactElement {\n return <CrudDataTable {...props} />;\n}\n","\"use client\";\n\nimport type { ReactNode } from \"react\";\n\nimport { ResponsiveSheet } from \"@carefully-built/ui\";\nimport type { ResponsiveSheetClassNames } from \"@carefully-built/ui\";\n\nexport interface CrudResourceSheetProps {\n readonly open: boolean;\n readonly onOpenChange: (open: boolean) => void;\n readonly title: ReactNode;\n readonly children: ReactNode;\n readonly formId?: string;\n readonly description?: ReactNode;\n readonly onCancel?: () => void;\n readonly cancelLabel?: ReactNode;\n readonly onConfirm?: () => void;\n readonly confirmLabel?: ReactNode;\n readonly confirmDisabled?: boolean;\n readonly confirmLoading?: boolean;\n readonly confirmCloseWhenDirty?: boolean;\n readonly width?: number;\n readonly className?: string;\n readonly contentClassName?: string;\n readonly footerClassName?: string;\n readonly classes?: ResponsiveSheetClassNames;\n}\n\nexport function CrudResourceSheet({\n children,\n formId,\n onConfirm,\n confirmCloseWhenDirty: _confirmCloseWhenDirty,\n ...sheetProps\n}: CrudResourceSheetProps): React.ReactElement {\n const submitForm = (): void => {\n const form = formId ? document.getElementById(formId) : null;\n\n if (form instanceof HTMLFormElement) {\n form.requestSubmit();\n return;\n }\n\n onConfirm?.();\n };\n\n return (\n <ResponsiveSheet\n {...sheetProps}\n onConfirm={formId || onConfirm ? submitForm : undefined}\n >\n {children}\n </ResponsiveSheet>\n );\n}\n","\"use client\";\n\nimport type { CrudTableViewProps } from \"./types\";\n\nimport { SmartTable, TableToolbar } from \"@carefully-built/ui\";\n\nexport function CrudTableView<TItem extends object>({\n state,\n columns,\n isLoading,\n searchPlaceholder = \"Search...\",\n filters = [],\n actions,\n actionHandlers,\n renderActions,\n noDataMessage,\n initialEmptyContent,\n noResultsContent,\n getRowKey,\n onRowClick,\n renderMobileCard,\n stickyHeader = true,\n fullHeight = true,\n maxHeight,\n}: CrudTableViewProps<TItem>): React.ReactElement {\n const emptyContent =\n state.emptyState === \"no-results\" ? noResultsContent : initialEmptyContent;\n\n return (\n <>\n <div className=\"shrink-0\">\n <TableToolbar\n search={{\n value: state.search,\n onChange: state.setSearch,\n placeholder: searchPlaceholder,\n }}\n filters={filters.map((filter) => ({\n config: filter.config,\n value: state.filters[filter.key] ?? \"all\",\n onChange: (value) => {\n state.setFilter(filter.key, value);\n },\n allowAll: filter.allowAll,\n clearable: filter.clearable,\n }))}\n onClearAll={state.clearAll}\n getDraftResultCount={state.getDraftFilterResultCount}\n />\n </div>\n\n <SmartTable\n data={state.paginatedData}\n columns={[...columns]}\n isLoading={isLoading}\n actions={actions ? [...actions] : undefined}\n actionHandlers={actionHandlers}\n renderActions={renderActions}\n noDataMessage={noDataMessage}\n noDataContent={emptyContent}\n getRowKey={getRowKey}\n onRowClick={onRowClick}\n renderMobileCard={renderMobileCard}\n stickyHeader={stickyHeader}\n fullHeight={fullHeight}\n maxHeight={maxHeight}\n sortState={state.sortState}\n onSortChange={state.setSortState}\n pagination={state.pagination}\n />\n </>\n );\n}\n","function normalizeSearchText(value: string): string {\n return value\n .toLocaleLowerCase()\n .normalize(\"NFD\")\n .replace(/\\p{Diacritic}/gu, \"\")\n .trim();\n}\n\nexport function buildCrudSearchText(values: readonly unknown[]): string {\n return normalizeSearchText(\n values\n .filter(\n (value): value is string | number =>\n typeof value === \"string\" || typeof value === \"number\",\n )\n .map(String)\n .join(\" \"),\n );\n}\n\nexport function matchesCrudSearch(searchText: string, query: string): boolean {\n const normalizedQuery = normalizeSearchText(query);\n\n if (!normalizedQuery) {\n return true;\n }\n\n return normalizedQuery\n .split(/\\s+/)\n .every((token) => searchText.includes(token));\n}\n","export interface CrudPaginationState {\n readonly currentPage: number;\n readonly totalPages: number;\n readonly totalItems: number;\n readonly pageSize: number;\n readonly startIndex: number;\n readonly endIndex: number;\n readonly onPageChange: (page: number) => void;\n}\n\nexport function getValidPage(page: number, totalPages: number): number {\n return Math.min(Math.max(1, page), totalPages);\n}\n\nexport function paginateCrudData<T>(\n data: readonly T[],\n currentPage: number,\n pageSize: number,\n): T[] {\n const startIndex = (currentPage - 1) * pageSize;\n return data.slice(startIndex, startIndex + pageSize);\n}\n","\"use client\";\n\nimport { useCallback, useMemo, useState } from \"react\";\n\nimport type { UseCrudTableStateOptions, CrudTableState } from \"./types\";\n\nimport { useTableSorting } from \"@carefully-built/ui\";\n\nimport { buildCrudSearchText, matchesCrudSearch } from \"./search\";\nimport { getValidPage, paginateCrudData } from \"./pagination\";\n\nfunction buildInitialFilters<TItem>(\n filters: readonly { readonly key: Extract<keyof TItem, string> }[],\n): Record<string, string> {\n return Object.fromEntries(filters.map((filter) => [filter.key, \"all\"]));\n}\n\nfunction itemMatchesFilters<TItem extends object>(\n item: TItem,\n filters: Record<string, string>,\n): boolean {\n const record = item as Record<string, unknown>;\n\n return Object.entries(filters).every(([key, filterValue]) => {\n if (!filterValue || filterValue === \"all\") {\n return true;\n }\n\n return record[key] === filterValue;\n });\n}\n\nfunction filterCrudData<TItem extends object>(\n data: readonly TItem[],\n search: string,\n filters: Record<string, string>,\n searchFields: readonly Extract<keyof TItem, string>[],\n): TItem[] {\n return data.filter((item) => {\n if (!itemMatchesFilters(item, filters)) {\n return false;\n }\n\n const record = item as Record<string, unknown>;\n const searchText = buildCrudSearchText(\n searchFields.map((field) => record[field]),\n );\n return matchesCrudSearch(searchText, search);\n });\n}\n\nexport function useCrudTableState<TItem extends object>({\n data,\n columns,\n searchFields = [],\n filters: filterDefinitions = [],\n pageSize = 20,\n initialSortState = null,\n}: UseCrudTableStateOptions<TItem>): CrudTableState<TItem> {\n const [search, setSearch] = useState(\"\");\n const [filters, setFilters] = useState<Record<string, string>>(() =>\n buildInitialFilters(filterDefinitions),\n );\n const [currentPage, setCurrentPage] = useState(1);\n\n const filteredData = useMemo(\n () => filterCrudData(data, search, filters, searchFields),\n [data, filters, search, searchFields],\n );\n const { sortedData, sortState, setSortState } = useTableSorting({\n data: filteredData,\n columns,\n initialSortState,\n });\n\n const totalPages = Math.max(1, Math.ceil(filteredData.length / pageSize));\n const validCurrentPage = getValidPage(currentPage, totalPages);\n const startIndex = (validCurrentPage - 1) * pageSize;\n const endIndex = Math.min(startIndex + pageSize, filteredData.length);\n const paginatedData = useMemo(\n () => paginateCrudData(sortedData, validCurrentPage, pageSize),\n [pageSize, sortedData, validCurrentPage],\n );\n\n const setFilter = useCallback((key: string, value: string) => {\n setFilters((currentFilters) => ({\n ...currentFilters,\n [key]: value,\n }));\n setCurrentPage(1);\n }, []);\n\n const updateSearch = useCallback((value: string) => {\n setSearch(value);\n setCurrentPage(1);\n }, []);\n\n const clearAll = useCallback(() => {\n setSearch(\"\");\n setFilters(buildInitialFilters(filterDefinitions));\n setCurrentPage(1);\n }, [filterDefinitions]);\n\n const getDraftFilterResultCount = useCallback(\n (draftValues: Record<string, string>) =>\n filterCrudData(\n data,\n search,\n {\n ...filters,\n ...draftValues,\n },\n searchFields,\n ).length,\n [data, filters, search, searchFields],\n );\n\n const hasSearch = search.trim().length > 0;\n const hasFilters = Object.values(filters).some(\n (value) => value && value !== \"all\",\n );\n\n return {\n filteredData,\n sortedData,\n paginatedData,\n search,\n setSearch: updateSearch,\n filters,\n setFilter,\n clearAll,\n getDraftFilterResultCount,\n hasSearch,\n hasFilters,\n emptyState: hasSearch || hasFilters ? \"no-results\" : \"initial\",\n sortState,\n setSortState,\n pagination: {\n currentPage: validCurrentPage,\n totalPages,\n totalItems: filteredData.length,\n pageSize,\n startIndex,\n endIndex,\n onPageChange: setCurrentPage,\n },\n };\n}\n","\"use client\";\n\nimport { parseAsString, useQueryStates } from \"nuqs\";\nimport { useCallback, useMemo } from \"react\";\n\nexport interface UrlStringFilterDefinition<TKey extends string = string> {\n readonly key: TKey;\n readonly param?: string;\n readonly defaultValue?: string;\n readonly clearValue?: string;\n}\n\ntype UrlStringFilterValues<TDefinitions extends readonly UrlStringFilterDefinition[]> = {\n readonly [TDefinition in TDefinitions[number] as TDefinition[\"key\"]]: string;\n};\n\nexport interface UrlStringFiltersState<\n TDefinitions extends readonly UrlStringFilterDefinition[],\n> {\n readonly values: UrlStringFilterValues<TDefinitions>;\n readonly setValue: (key: TDefinitions[number][\"key\"], value: string) => void;\n readonly clear: () => void;\n readonly getDraftValues: (\n draftValues: Record<string, string>,\n ) => UrlStringFilterValues<TDefinitions>;\n}\n\nfunction buildParserMap(definitions: readonly UrlStringFilterDefinition[]) {\n return Object.fromEntries(\n definitions.map((definition) => [\n definition.param ?? definition.key,\n parseAsString.withDefault(definition.defaultValue ?? \"all\"),\n ]),\n );\n}\n\nfunction normalizeFilterValue(\n value: string,\n definition: UrlStringFilterDefinition,\n): string | null {\n const clearValue = definition.clearValue ?? definition.defaultValue ?? \"all\";\n\n if (value === clearValue) {\n return null;\n }\n\n return value.length > 0 ? value : null;\n}\n\nexport function useUrlStringFilters<\n const TDefinitions extends readonly UrlStringFilterDefinition[],\n>(definitions: TDefinitions): UrlStringFiltersState<TDefinitions> {\n const parserMap = useMemo(() => buildParserMap(definitions), [definitions]);\n const [params, setParams] = useQueryStates(parserMap);\n\n const values = useMemo(\n () =>\n Object.fromEntries(\n definitions.map((definition) => {\n const param = definition.param ?? definition.key;\n return [definition.key, params[param] ?? definition.defaultValue ?? \"all\"];\n }),\n ) as UrlStringFilterValues<TDefinitions>,\n [definitions, params],\n );\n\n const setValue = useCallback(\n (key: TDefinitions[number][\"key\"], value: string) => {\n const definition = definitions.find((item) => item.key === key);\n\n if (!definition) {\n return;\n }\n\n void setParams({\n [definition.param ?? definition.key]: normalizeFilterValue(value, definition),\n });\n },\n [definitions, setParams],\n );\n\n const clear = useCallback(() => {\n void setParams(\n Object.fromEntries(\n definitions.map((definition) => [definition.param ?? definition.key, null]),\n ),\n );\n }, [definitions, setParams]);\n\n const getDraftValues = useCallback(\n (draftValues: Record<string, string>) =>\n ({\n ...values,\n ...draftValues,\n }) as UrlStringFilterValues<TDefinitions>,\n [values],\n );\n\n return {\n values,\n setValue,\n clear,\n getDraftValues,\n };\n}\n","\"use client\";\n\nimport { parseAsInteger, useQueryState } from \"nuqs\";\nimport { useCallback, useMemo } from \"react\";\n\nimport type { CrudPaginationState } from \"./pagination\";\n\nexport interface UseUrlPaginationOptions {\n readonly totalItems: number;\n readonly pageSize?: number;\n readonly pageParam?: string;\n}\n\nexport interface UrlPaginationState extends CrudPaginationState {\n readonly hasPrevPage: boolean;\n readonly hasNextPage: boolean;\n readonly goToPage: (page: number) => void;\n readonly nextPage: () => void;\n readonly prevPage: () => void;\n readonly firstPage: () => void;\n readonly lastPage: () => void;\n readonly paginate: <T>(data: readonly T[]) => T[];\n}\n\nexport function useUrlPagination({\n totalItems,\n pageSize = 20,\n pageParam = \"page\",\n}: UseUrlPaginationOptions): UrlPaginationState {\n const [page, setPage] = useQueryState(\n pageParam,\n parseAsInteger.withDefault(1),\n );\n\n const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));\n const currentPage = Math.min(Math.max(1, page), totalPages);\n const startIndex = (currentPage - 1) * pageSize;\n const endIndex = Math.min(startIndex + pageSize, totalItems);\n const hasPrevPage = currentPage > 1;\n const hasNextPage = currentPage < totalPages;\n\n const goToPage = useCallback(\n (newPage: number) => {\n const validPage = Math.min(Math.max(1, newPage), totalPages);\n void setPage(validPage === 1 ? null : validPage);\n },\n [setPage, totalPages],\n );\n\n const nextPage = useCallback(() => {\n if (hasNextPage) {\n goToPage(currentPage + 1);\n }\n }, [currentPage, goToPage, hasNextPage]);\n\n const prevPage = useCallback(() => {\n if (hasPrevPage) {\n goToPage(currentPage - 1);\n }\n }, [currentPage, goToPage, hasPrevPage]);\n\n const firstPage = useCallback(() => {\n goToPage(1);\n }, [goToPage]);\n\n const lastPage = useCallback(() => {\n goToPage(totalPages);\n }, [goToPage, totalPages]);\n\n const paginate = useCallback(\n <T,>(data: readonly T[]): T[] => data.slice(startIndex, endIndex),\n [endIndex, startIndex],\n );\n\n return useMemo(\n () => ({\n currentPage,\n pageSize,\n totalPages,\n totalItems,\n startIndex,\n endIndex,\n hasPrevPage,\n hasNextPage,\n goToPage,\n nextPage,\n prevPage,\n firstPage,\n lastPage,\n paginate,\n onPageChange: goToPage,\n }),\n [\n currentPage,\n endIndex,\n firstPage,\n goToPage,\n hasNextPage,\n hasPrevPage,\n lastPage,\n nextPage,\n pageSize,\n paginate,\n prevPage,\n startIndex,\n totalItems,\n totalPages,\n ],\n );\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@carefully-built/crud",
3
+ "version": "0.1.2",
4
+ "description": "Config-driven CRUD table and form helpers for Carefully Built SaaS apps.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Alessandro Dodi",
8
+ "homepage": "https://github.com/AlessandroDodi/carefully-built-saas-kit#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/AlessandroDodi/carefully-built-saas-kit.git",
12
+ "directory": "packages/crud"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/AlessandroDodi/carefully-built-saas-kit/issues"
16
+ },
17
+ "keywords": [
18
+ "react",
19
+ "crud",
20
+ "table",
21
+ "saas",
22
+ "convex"
23
+ ],
24
+ "sideEffects": false,
25
+ "main": "./dist/index.mjs",
26
+ "module": "./dist/index.mjs",
27
+ "types": "./dist/index.d.mts",
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.mts",
34
+ "import": "./dist/index.mjs",
35
+ "default": "./dist/index.mjs"
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "scripts": {
40
+ "build": "tsdown src/index.ts --format esm --dts",
41
+ "prepublishOnly": "bun run typecheck && bun run build",
42
+ "typecheck": "tsc --noEmit"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "peerDependencies": {
48
+ "@carefully-built/ui": ">=0.1.4",
49
+ "nuqs": ">=2.0.0",
50
+ "react": ">=18.2.0 || >=19.0.0",
51
+ "react-dom": ">=18.2.0 || >=19.0.0"
52
+ },
53
+ "peerDependenciesMeta": {
54
+ "@carefully-built/ui": {
55
+ "optional": true
56
+ },
57
+ "nuqs": {
58
+ "optional": true
59
+ }
60
+ },
61
+ "devDependencies": {
62
+ "@carefully-built/ui": "workspace:*",
63
+ "nuqs": "^2.8.8",
64
+ "react": "^19.2.3",
65
+ "react-dom": "^19.2.3"
66
+ }
67
+ }