unifyedx-storybook-syncfusion 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,219 @@
1
+ ---
2
+ name: storybook-syncfusion-components
3
+ description: Use CustomDataGrid, CustomGridToolbar and CustomDatePicker from unifyedx-storybook-syncfusion instead of building data tables, grid toolbars or Syncfusion date/time pickers by hand. Covers install, licence registration, CSS, the server-controlled grid state contract and the grid API ref.
4
+ paths:
5
+ - "**/*.jsx"
6
+ - "**/*.tsx"
7
+ - "**/*.js"
8
+ - "**/*.ts"
9
+ ---
10
+
11
+ # UnifyedX Syncfusion Components (`unifyedx-storybook-syncfusion`)
12
+
13
+ Package version: 0.4.x | Last updated: 2026-09-15
14
+
15
+ > **Install this skill** in a consuming app by copying
16
+ > `node_modules/unifyedx-storybook-syncfusion/claude-skill/SKILL.md` to
17
+ > `.claude/skills/storybook-syncfusion-components/SKILL.md`.
18
+ > Pair it with the `storybook-components` skill from `unifyedx-storybook-new`.
19
+
20
+ ## 1. When to use this package — and when not to
21
+
22
+ This package holds the **only** Syncfusion-backed UnifyedX components. It was split
23
+ out of `unifyedx-storybook-new` in 0.4.0 so that apps without a data grid do not
24
+ download the Syncfusion runtime (~1.3 GB installed) or its ~1.9 MB stylesheet.
25
+
26
+ | Need | Use |
27
+ |---|---|
28
+ | A server-paged data table | `CustomDataGrid` (this package) |
29
+ | Title / search / filter / export bar above that table | `CustomGridToolbar` (this package) |
30
+ | Date **range**, **datetime** or **datetime range** picker | `CustomDatePicker` (this package) |
31
+ | A plain date or time field that returns a `Date` | `DateField` / `TimeField` from **`unifyedx-storybook-new`** — do NOT pull in Syncfusion for this |
32
+ | A numeric range slider | `RangeSlider` from **`unifyedx-storybook-new`** |
33
+ | Page numbers without a grid | `Pagination` from **`unifyedx-storybook-new`** |
34
+
35
+ **Rule:** only add this package if the app renders `CustomDataGrid`, `CustomGridToolbar`
36
+ or a range/datetime `CustomDatePicker`. Otherwise use the core package alone.
37
+
38
+ ## 2. Install & setup
39
+
40
+ ```bash
41
+ npm i unifyedx-storybook-syncfusion unifyedx-storybook-new \
42
+ @syncfusion/ej2-base@32.1.19 @syncfusion/ej2-react-base@32.1.19 \
43
+ @syncfusion/ej2-react-grids@32.1.19 @syncfusion/ej2-react-calendars@32.1.19 \
44
+ @syncfusion/ej2-react-inputs@32.1.19
45
+ ```
46
+
47
+ All Syncfusion packages are **peer dependencies pinned to 32.1.19**. Declare them in
48
+ the app's own `package.json` — do not rely on hoisting. Mixing Syncfusion majors on
49
+ one page breaks `ej2-base`.
50
+
51
+ ```jsx
52
+ // main.jsx — once, before rendering
53
+ import 'unifyedx-storybook-new/style.css';
54
+ import 'unifyedx-storybook-syncfusion/syncfusion.css'; // Syncfusion theme + component styles
55
+ import { registerSyncfusionLicense } from 'unifyedx-storybook-syncfusion';
56
+
57
+ registerSyncfusionLicense(import.meta.env.VITE_APP_SYNCFUSION_LICENSE_KEY);
58
+ ```
59
+
60
+ - `registerSyncfusionLicense` is idempotent; calling it twice is harmless.
61
+ - Import it from **this** package so the licence registers on the same `ej2-base` instance the components use.
62
+ - Never hardcode the licence key in source; read it from env.
63
+
64
+ ## 3. Imports
65
+
66
+ ```jsx
67
+ import { CustomDataGrid, CustomGridToolbar, CustomDatePicker } from 'unifyedx-storybook-syncfusion';
68
+ import { UnifyedCoreButton, GenericFilter, notify } from 'unifyedx-storybook-new';
69
+ ```
70
+
71
+ These three were removed from `unifyedx-storybook-new` in 0.4.0 — importing them from there fails.
72
+
73
+ ## 4. CustomDataGrid
74
+
75
+ A lean, **server-controlled** Syncfusion grid. It cancels client-side data operations:
76
+ the parent fetches one page and passes `data` + `totalCount`.
77
+
78
+ ```
79
+ data=[] totalCount={0} columns=[] uniqueId="grid" height="600"
80
+ pageIndex={1} (1-based) pageSize={20}
81
+ allowPaging={true} allowSorting={true} allowReordering={true} allowResizing={true}
82
+ allowFiltering={false} allowGrouping={false} allowExcelExport={false} allowPdfExport={false}
83
+ showColumnChooser={false} enableSearchApi={false} enableCheckbox={false} selectionSettings
84
+ onGridStateChange(state, rawArgs) onRowSelected onRowDeselected onRowDoubleClick
85
+ allowEditing={false} editSettings onCellSave
86
+ apiRef tableId gridProps={} ariaLabel ariaLabelledBy ariaDescribedBy
87
+ ```
88
+
89
+ ### State contract — `onGridStateChange(state)`
90
+
91
+ ```js
92
+ {
93
+ reason: "paging" | "sorting" | "searching" | "filtering" | "grouping" | "reorder" | "columnstate" | "other",
94
+ page: { index, size, skip, take }, // index is 1-based; skip/take ready for the API
95
+ sort: [{ field, dir: "asc" | "desc" }],
96
+ search: "",
97
+ filters: [],
98
+ rawEvent, // the original Syncfusion args
99
+ }
100
+ ```
101
+
102
+ ```jsx
103
+ const [rows, setRows] = useState([]);
104
+ const [total, setTotal] = useState(0);
105
+ const apiRef = useRef(null);
106
+
107
+ const load = useCallback(async ({ page, sort, search }) => {
108
+ const res = await axiosGet(`/api/users?skip=${page.skip}&take=${page.take}` +
109
+ `&sort=${sort[0]?.field ?? ''}&dir=${sort[0]?.dir ?? ''}&q=${encodeURIComponent(search)}`);
110
+ setRows(res.data.items); setTotal(res.data.total);
111
+ }, []);
112
+
113
+ <CustomDataGrid
114
+ data={rows}
115
+ totalCount={total}
116
+ columns={columns}
117
+ apiRef={apiRef}
118
+ tableId="users"
119
+ onGridStateChange={load}
120
+ />
121
+ ```
122
+
123
+ **Never** pass the full dataset for client paging — fetch per page.
124
+
125
+ ### Columns
126
+
127
+ Syncfusion column objects: `{ field, headerText, width, textAlign, template, allowSorting, allowEditing, isPrimaryKey, freeze }`.
128
+ A column whose `headerText` is `"Action"`/`"Actions"` gets the Freeze service injected automatically (use `freeze: "Right"` to pin it).
129
+ Mark one column `isPrimaryKey` when `enableCheckbox` / `persistSelection` is on, or Syncfusion logs *PRIMARY KEY MISSING*.
130
+
131
+ ### `apiRef` methods
132
+
133
+ ```
134
+ getInstance() refresh() getSelectedRecords() getSelectedRowIndexes() clearSelection()
135
+ selectRow(i) selectRows([i]) deselectByPrimaryKey(k) deselectByPrimaryKeys([k])
136
+ search(text) // needs enableSearchApi
137
+ print()
138
+ excelExport(opts) // needs allowExcelExport
139
+ pdfExport(opts) // needs allowPdfExport
140
+ openColumnChooser(x, y) // needs showColumnChooser
141
+ getColumns() getFilteredRecords() autoFitColumns(fields)
142
+ ```
143
+ Calling a gated method without its `allow*` / `show*` prop logs a warning and does nothing.
144
+
145
+ ### Inline editing
146
+
147
+ `allowEditing`, `editSettings` and `onCellSave` are wired to the grid (fixed in 0.4.0 —
148
+ before that they were accepted but ignored). Defaults: `mode: "Batch"`, no add/delete,
149
+ no confirm dialogs. Override any default via `editSettings`.
150
+
151
+ ```js
152
+ onCellSave({ field, columnName, value, previousValue, rowData, cancel(), originalEvent })
153
+ // call cancel() to reject the edit (e.g. validation failed)
154
+ ```
155
+
156
+ ### State persistence
157
+
158
+ Pass a stable `tableId` (or omit it to derive one from the route; `false`/`null` opts out).
159
+ Page, page size and sort restore through `useTablePreferences` / `useRestoredGridState`
160
+ from `unifyedx-storybook-new`. Use the **same** `tableId` on `CustomGridToolbar` and
161
+ `GenericFilter` so search and filters restore alongside.
162
+
163
+ ## 5. CustomGridToolbar
164
+
165
+ ```
166
+ heading="Data Grid" subheading="" totalCount={0} apiRef (or legacy gridRef) tableId
167
+ showSearch={true} showDelete={true} showColumnChooser={true}
168
+ showFilter={false} showPrint={false} showExcel={false} showPdf={false} showRefresh={false} showAdd={false}
169
+ onAdd addBtnText="Add" onFilterOpen filterCount
170
+ iSelectedRecords={0} selectedRecords searchPlaceholder="Search..."
171
+ handleSearchChange handleDeleteClick handleRefreshClick
172
+ excelFileName pdfFileName actionButtons=[]
173
+ ```
174
+
175
+ `actionButtons` entries by `type`:
176
+ - `"button"` (default) — a `UnifyedCoreButton`, or an `IconButton` when `iconOnly: true` and `icon` are set
177
+ - `"segmented"` — `{ id, value, options:[{key,label}], onChange, ariaLabel }` renders a `SegmentedControl`
178
+ - `"multioptions"` — a `UnifyedCoreButton` with a dropdown menu
179
+ - `"custom"` — `{ id, render: () => <YourNode/> }`
180
+
181
+ ```jsx
182
+ <CustomGridToolbar
183
+ heading="Users" totalCount={total} apiRef={apiRef} tableId="users"
184
+ showAdd addBtnText="Invite" onAdd={openInvite}
185
+ showFilter filterCount={countActiveFilters(filters)} onFilterOpen={() => setFilterOpen(true)}
186
+ showExcel excelFileName="users.xlsx"
187
+ />
188
+ <CustomDataGrid apiRef={apiRef} tableId="users" allowExcelExport ... />
189
+ ```
190
+ Export/print/column-chooser buttons call the grid through `apiRef`, so the grid must have the matching `allow*` prop.
191
+
192
+ ## 6. CustomDatePicker
193
+
194
+ ```
195
+ mode="date" | "daterange" | "time" | "datetime" | "datetimerange"
196
+ value onChange min max format placeholder step={30} (minutes)
197
+ label required errorMessage disabled readOnly showClearButton={true}
198
+ strictMode={false} typeable={true} openOnFocus={false} weekNumber firstDayOfWeek
199
+ width cssClass id name ariaLabel ariaLabelledBy ariaDescribedBy
200
+ daterange: startDate endDate separator="-" presets startPlaceholder endPlaceholder
201
+ datetimerange: startValue endValue onStartChange onEndChange
202
+ events: onOpen onClose onFocus onBlur onNavigated onRenderDayCell onCleared
203
+ ```
204
+
205
+ - Single modes: `onChange({ value: Date, event })`.
206
+ - `daterange`: `onChange({ startDate, endDate, ... })`.
207
+ - `datetimerange`: separate `onStartChange` / `onEndChange`, each `{ value }`.
208
+
209
+ **Prefer the core package** for `mode="date"` and `mode="time"`: `DateField` and
210
+ `TimeField` in `unifyedx-storybook-new` emit the identical `{ value: Date }` and need
211
+ no Syncfusion. Reach for `CustomDatePicker` only for range and datetime modes.
212
+
213
+ ## 7. Gotchas
214
+
215
+ 1. **CSS order**: import `unifyedx-storybook-syncfusion/syncfusion.css` once. It already contains the Syncfusion theme followed by the component overrides — do not import individual `@syncfusion/*/styles` files too, or the overrides lose.
216
+ 2. **One `unifyedx-storybook-new` instance**: this package imports core as a peer. If `npm ls unifyedx-storybook-new` shows two copies, hooks break with *Invalid hook call*. Keep both packages on the same version.
217
+ 3. **Peers must be explicit**: several older apps imported `CustomDataGrid` without declaring `@syncfusion/ej2-react-grids` and worked only via hoisting. Declare every peer in section 2.
218
+ 4. **Migrating from ≤0.3.x**: change the import specifier from `unifyedx-storybook-new` to `unifyedx-storybook-syncfusion`, add the package + peers, and add the `syncfusion.css` import. Nothing else changes.
219
+ 5. **Accessibility**: pass `label` or `ariaLabel` to every picker and `ariaLabel` to the grid; UnifyedX targets WCAG 2.0 AAA.
@@ -0,0 +1,2 @@
1
+ export function CustomDataGrid(props: any): React.JSX.Element;
2
+ import React from "react";
@@ -0,0 +1,107 @@
1
+ /**
2
+ * CustomDatePicker — a unified Syncfusion-based date/time picker.
3
+ *
4
+ * Modes:
5
+ * - "date" → single date
6
+ * - "daterange" → start + end date
7
+ * - "time" → time only
8
+ * - "datetime" → single date + time
9
+ * - "datetimerange" → start + end date-time (two DateTimePickers)
10
+ */
11
+ export function CustomDatePicker({ mode, value, onChange, min, max, format, placeholder, disabled, readOnly, showClearButton, floatLabelType, strictMode, typeable, openOnFocus, cssClass, width, step, weekNumber, firstDayOfWeek, label, labelId, ariaLabel, ariaLabelledBy, ariaDescribedBy, required, errorMessage, startDate, endDate, separator, presets, startPlaceholder, endPlaceholder, startValue, endValue, onStartChange, onEndChange, onOpen, onClose, onFocus, onBlur, onNavigated, onRenderDayCell, onCleared, id, name, htmlAttributes, ...rest }: {
12
+ [x: string]: any;
13
+ mode?: string;
14
+ value: any;
15
+ onChange: any;
16
+ min: any;
17
+ max: any;
18
+ format: any;
19
+ placeholder: any;
20
+ disabled?: boolean;
21
+ readOnly?: boolean;
22
+ showClearButton?: boolean;
23
+ floatLabelType?: string;
24
+ strictMode?: boolean;
25
+ typeable?: boolean;
26
+ openOnFocus?: boolean;
27
+ cssClass?: string;
28
+ width: any;
29
+ step?: number;
30
+ weekNumber?: boolean;
31
+ firstDayOfWeek: any;
32
+ label: any;
33
+ labelId: any;
34
+ ariaLabel: any;
35
+ ariaLabelledBy: any;
36
+ ariaDescribedBy: any;
37
+ required?: boolean;
38
+ errorMessage: any;
39
+ startDate: any;
40
+ endDate: any;
41
+ separator?: string;
42
+ presets: any;
43
+ startPlaceholder: any;
44
+ endPlaceholder: any;
45
+ startValue: any;
46
+ endValue: any;
47
+ onStartChange: any;
48
+ onEndChange: any;
49
+ onOpen: any;
50
+ onClose: any;
51
+ onFocus: any;
52
+ onBlur: any;
53
+ onNavigated: any;
54
+ onRenderDayCell: any;
55
+ onCleared: any;
56
+ id: any;
57
+ name: any;
58
+ htmlAttributes: any;
59
+ }): React.JSX.Element;
60
+ export namespace CustomDatePicker {
61
+ namespace propTypes {
62
+ let mode: any;
63
+ let value: any;
64
+ let onChange: any;
65
+ let min: any;
66
+ let max: any;
67
+ let format: any;
68
+ let placeholder: any;
69
+ let disabled: any;
70
+ let readOnly: any;
71
+ let showClearButton: any;
72
+ let floatLabelType: any;
73
+ let strictMode: any;
74
+ let typeable: any;
75
+ let openOnFocus: any;
76
+ let cssClass: any;
77
+ let width: any;
78
+ let step: any;
79
+ let weekNumber: any;
80
+ let firstDayOfWeek: any;
81
+ let label: any;
82
+ let labelId: any;
83
+ let required: any;
84
+ let errorMessage: any;
85
+ let startDate: any;
86
+ let endDate: any;
87
+ let separator: any;
88
+ let presets: any;
89
+ let startPlaceholder: any;
90
+ let endPlaceholder: any;
91
+ let startValue: any;
92
+ let endValue: any;
93
+ let onStartChange: any;
94
+ let onEndChange: any;
95
+ let onOpen: any;
96
+ let onClose: any;
97
+ let onFocus: any;
98
+ let onBlur: any;
99
+ let onNavigated: any;
100
+ let onRenderDayCell: any;
101
+ let onCleared: any;
102
+ let id: any;
103
+ let name: any;
104
+ let htmlAttributes: any;
105
+ }
106
+ }
107
+ import React from "react";
@@ -0,0 +1,35 @@
1
+ export function CustomGridToolbar({ heading, subheading, totalCount, gridRef, apiRef, onAdd, onFilterOpen, addBtnText, showSearch, showDelete, showColumnChooser, showFilter, showPrint, showExcel, showPdf, showRefresh, showAdd, iSelectedRecords, selectedRecords, searchPlaceholder, handleRefreshClick, handleSearchChange, handleDeleteClick, excelFileName, pdfFileName, searchIconSize, searchIconClass, inputStyle, filterCount, actionButtons, tableId: tableIdProp, ...props }: {
2
+ [x: string]: any;
3
+ heading?: string;
4
+ subheading?: string;
5
+ totalCount?: number;
6
+ gridRef: any;
7
+ apiRef: any;
8
+ onAdd: any;
9
+ onFilterOpen: any;
10
+ addBtnText?: string;
11
+ showSearch?: boolean;
12
+ showDelete?: boolean;
13
+ showColumnChooser?: boolean;
14
+ showFilter?: boolean;
15
+ showPrint?: boolean;
16
+ showExcel?: boolean;
17
+ showPdf?: boolean;
18
+ showRefresh?: boolean;
19
+ showAdd?: boolean;
20
+ iSelectedRecords?: number;
21
+ selectedRecords: any;
22
+ searchPlaceholder?: string;
23
+ handleRefreshClick?: () => void;
24
+ handleSearchChange?: () => void;
25
+ handleDeleteClick?: () => void;
26
+ excelFileName: any;
27
+ pdfFileName: any;
28
+ searchIconSize: any;
29
+ searchIconClass: any;
30
+ inputStyle: any;
31
+ filterCount: any;
32
+ actionButtons?: any[];
33
+ tableId: any;
34
+ }): React.JSX.Element;
35
+ import React from "react";
@@ -0,0 +1,4 @@
1
+ export * from "./components/CustomDataGrid/CustomDataGrid";
2
+ export * from "./components/CustomGridToolbar/CustomGridToolbar";
3
+ export * from "./components/CustomDatePicker/CustomDatePicker";
4
+ export * from "./utils/registerSyncfusionLicense";