@alaarab/ogrid-vue 2.0.18 → 2.0.21

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.
@@ -216,6 +216,9 @@ export function createDataGridTable(ui) {
216
216
  backgroundColor: '#fff',
217
217
  willChange: 'scroll-position',
218
218
  };
219
+ if (p.rowHeight) {
220
+ wrapperStyle['--ogrid-row-height'] = `${p.rowHeight}px`;
221
+ }
219
222
  return h('div', { class: 'ogrid-outer-container' }, [
220
223
  // Scrollable wrapper
221
224
  h('div', {
@@ -0,0 +1,194 @@
1
+ import { defineComponent, ref, h, onMounted, nextTick, watch } from 'vue';
2
+ const editorWrapperStyle = {
3
+ width: '100%',
4
+ height: '100%',
5
+ display: 'flex',
6
+ alignItems: 'center',
7
+ padding: '0 2px',
8
+ boxSizing: 'border-box',
9
+ };
10
+ export function createInlineCellEditor(options) {
11
+ const { renderCheckbox, renderDatePicker } = options;
12
+ return defineComponent({
13
+ name: 'InlineCellEditor',
14
+ props: {
15
+ value: { default: undefined },
16
+ item: { type: Object, required: true },
17
+ column: { type: Object, required: true },
18
+ rowIndex: { type: Number, required: true },
19
+ editorType: { type: String, required: true },
20
+ onCommit: { type: Function, required: true },
21
+ onCancel: { type: Function, required: true },
22
+ },
23
+ setup(props) {
24
+ const inputRef = ref(null);
25
+ const selectWrapperRef = ref(null);
26
+ const selectDropdownRef = ref(null);
27
+ const localValue = ref(props.value);
28
+ const highlightedIndex = ref(0);
29
+ const positionDropdown = () => {
30
+ const wrapper = selectWrapperRef.value;
31
+ const dropdown = selectDropdownRef.value;
32
+ if (!wrapper || !dropdown)
33
+ return;
34
+ const rect = wrapper.getBoundingClientRect();
35
+ const maxH = 200;
36
+ const spaceBelow = window.innerHeight - rect.bottom;
37
+ const flipUp = spaceBelow < maxH && rect.top > spaceBelow;
38
+ dropdown.style.position = 'fixed';
39
+ dropdown.style.left = `${rect.left}px`;
40
+ dropdown.style.width = `${rect.width}px`;
41
+ dropdown.style.maxHeight = `${maxH}px`;
42
+ dropdown.style.zIndex = '9999';
43
+ dropdown.style.right = 'auto';
44
+ if (flipUp) {
45
+ dropdown.style.top = 'auto';
46
+ dropdown.style.bottom = `${window.innerHeight - rect.top}px`;
47
+ }
48
+ else {
49
+ dropdown.style.top = `${rect.bottom}px`;
50
+ }
51
+ };
52
+ onMounted(() => {
53
+ nextTick(() => {
54
+ if (selectWrapperRef.value) {
55
+ selectWrapperRef.value.focus();
56
+ positionDropdown();
57
+ return;
58
+ }
59
+ inputRef.value?.focus();
60
+ inputRef.value?.select();
61
+ });
62
+ });
63
+ // Sync local value when prop changes
64
+ watch(() => props.value, (v) => { localValue.value = v; });
65
+ // Initialize highlighted index to current value
66
+ const initHighlightedIndex = () => {
67
+ const values = props.column.cellEditorParams?.values ?? [];
68
+ const idx = values.findIndex((v) => String(v) === String(props.value));
69
+ highlightedIndex.value = Math.max(idx, 0);
70
+ };
71
+ initHighlightedIndex();
72
+ const scrollHighlightedIntoView = () => {
73
+ nextTick(() => {
74
+ const dropdown = selectDropdownRef.value;
75
+ if (!dropdown)
76
+ return;
77
+ const highlighted = dropdown.children[highlightedIndex.value];
78
+ highlighted?.scrollIntoView({ block: 'nearest' });
79
+ });
80
+ };
81
+ const getDisplayText = (value) => {
82
+ const formatValue = props.column.cellEditorParams?.formatValue;
83
+ if (formatValue)
84
+ return formatValue(value);
85
+ return value != null ? String(value) : '';
86
+ };
87
+ const handleSelectKeyDown = (e) => {
88
+ const values = props.column.cellEditorParams?.values ?? [];
89
+ switch (e.key) {
90
+ case 'ArrowDown':
91
+ e.preventDefault();
92
+ highlightedIndex.value = Math.min(highlightedIndex.value + 1, values.length - 1);
93
+ scrollHighlightedIntoView();
94
+ break;
95
+ case 'ArrowUp':
96
+ e.preventDefault();
97
+ highlightedIndex.value = Math.max(highlightedIndex.value - 1, 0);
98
+ scrollHighlightedIntoView();
99
+ break;
100
+ case 'Enter':
101
+ e.preventDefault();
102
+ e.stopPropagation();
103
+ if (values.length > 0 && highlightedIndex.value < values.length) {
104
+ props.onCommit(values[highlightedIndex.value]);
105
+ }
106
+ break;
107
+ case 'Tab':
108
+ e.preventDefault();
109
+ if (values.length > 0 && highlightedIndex.value < values.length) {
110
+ props.onCommit(values[highlightedIndex.value]);
111
+ }
112
+ break;
113
+ case 'Escape':
114
+ e.preventDefault();
115
+ e.stopPropagation();
116
+ props.onCancel();
117
+ break;
118
+ }
119
+ };
120
+ return () => {
121
+ if (props.editorType === 'checkbox') {
122
+ const checked = !!props.value;
123
+ return h('div', { style: { ...editorWrapperStyle, justifyContent: 'center' } }, renderCheckbox({
124
+ checked,
125
+ onChange: (c) => props.onCommit(c),
126
+ onCancel: props.onCancel,
127
+ }));
128
+ }
129
+ if (props.editorType === 'select') {
130
+ const values = props.column.cellEditorParams?.values ?? [];
131
+ return h('div', {
132
+ ref: (el) => { selectWrapperRef.value = el; },
133
+ tabindex: 0,
134
+ style: { ...editorWrapperStyle, position: 'relative' },
135
+ onKeydown: handleSelectKeyDown,
136
+ }, [
137
+ h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', cursor: 'pointer', fontSize: '13px', color: 'inherit' } }, [
138
+ h('span', getDisplayText(props.value)),
139
+ h('span', { style: { marginLeft: '4px', fontSize: '10px', opacity: '0.5' } }, '\u25BE'),
140
+ ]),
141
+ h('div', {
142
+ ref: (el) => { selectDropdownRef.value = el; },
143
+ role: 'listbox',
144
+ style: { position: 'absolute', top: '100%', left: '0', right: '0', maxHeight: '200px', overflowY: 'auto', background: 'var(--ogrid-bg, #fff)', border: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))', zIndex: '10', boxShadow: '0 4px 16px rgba(0,0,0,0.2)' },
145
+ }, values.map((v, i) => h('div', {
146
+ key: String(v),
147
+ role: 'option',
148
+ 'aria-selected': i === highlightedIndex.value,
149
+ onClick: () => props.onCommit(v),
150
+ style: { padding: '6px 8px', cursor: 'pointer', color: 'var(--ogrid-fg, #242424)', ...(i === highlightedIndex.value ? { background: 'var(--ogrid-bg-hover, #e8f0fe)' } : {}) },
151
+ }, getDisplayText(v)))),
152
+ ]);
153
+ }
154
+ if (props.editorType === 'date') {
155
+ let dateStr = '';
156
+ if (localValue.value) {
157
+ const d = new Date(String(localValue.value));
158
+ if (!Number.isNaN(d.getTime())) {
159
+ dateStr = d.toISOString().slice(0, 10);
160
+ }
161
+ }
162
+ return h('div', { style: editorWrapperStyle }, renderDatePicker({
163
+ value: dateStr,
164
+ onChange: (val) => props.onCommit(val),
165
+ onCancel: props.onCancel,
166
+ }));
167
+ }
168
+ // Default: text editor
169
+ return h('div', { style: editorWrapperStyle }, h('input', {
170
+ ref: (el) => { inputRef.value = el; },
171
+ type: 'text',
172
+ value: localValue.value != null ? String(localValue.value) : '',
173
+ style: { width: '100%', height: '100%', border: 'none', outline: 'none', padding: '0 4px', fontSize: 'inherit', boxSizing: 'border-box' },
174
+ onInput: (e) => { localValue.value = e.target.value; },
175
+ onKeydown: (e) => {
176
+ if (e.key === 'Enter') {
177
+ e.preventDefault();
178
+ props.onCommit(localValue.value);
179
+ }
180
+ if (e.key === 'Escape') {
181
+ e.preventDefault();
182
+ props.onCancel();
183
+ }
184
+ if (e.key === 'Tab') {
185
+ e.preventDefault();
186
+ props.onCommit(localValue.value);
187
+ }
188
+ },
189
+ onBlur: () => props.onCommit(localValue.value),
190
+ }));
191
+ };
192
+ },
193
+ });
194
+ }
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Shared OGrid factory for Vue UI packages.
3
+ *
4
+ * Both vue-vuetify and vue-primevue OGrid components are 100% identical —
5
+ * they only differ in which DataGridTable, ColumnChooser, and PaginationControls
6
+ * components they use. This factory extracts all shared logic into one place.
7
+ */
8
+ import { defineComponent, h, computed } from 'vue';
9
+ import { useOGrid, } from '../composables';
10
+ // --- SideBar constants and styles ---
11
+ const PANEL_WIDTH = 240;
12
+ const TAB_WIDTH = 36;
13
+ const PANEL_LABELS = {
14
+ columns: 'Columns',
15
+ filters: 'Filters',
16
+ };
17
+ const PANEL_ICONS = {
18
+ columns: '\u2261', // hamburger icon
19
+ filters: '\u2A65', // filter icon
20
+ };
21
+ /** Render the SideBar inline (tab strip + panel content). */
22
+ function renderSideBar(sb) {
23
+ const isOpen = sb.activePanel !== null;
24
+ const position = sb.position ?? 'right';
25
+ const tabStripStyle = {
26
+ display: 'flex',
27
+ flexDirection: 'column',
28
+ width: `${TAB_WIDTH}px`,
29
+ background: 'var(--ogrid-header-bg, #f5f5f5)',
30
+ ...(position === 'right'
31
+ ? { borderLeft: '1px solid var(--ogrid-border, #e0e0e0)' }
32
+ : { borderRight: '1px solid var(--ogrid-border, #e0e0e0)' }),
33
+ };
34
+ const tabStrip = h('div', { style: tabStripStyle, role: 'tablist', 'aria-label': 'Side bar tabs' }, sb.panels.map((panel) => h('button', {
35
+ key: panel,
36
+ role: 'tab',
37
+ 'aria-selected': sb.activePanel === panel,
38
+ 'aria-label': PANEL_LABELS[panel],
39
+ title: PANEL_LABELS[panel],
40
+ onClick: () => sb.onPanelChange(sb.activePanel === panel ? null : panel),
41
+ style: {
42
+ width: `${TAB_WIDTH}px`,
43
+ height: `${TAB_WIDTH}px`,
44
+ border: 'none',
45
+ cursor: 'pointer',
46
+ color: 'var(--ogrid-fg, #242424)',
47
+ fontSize: '14px',
48
+ display: 'flex',
49
+ alignItems: 'center',
50
+ justifyContent: 'center',
51
+ background: sb.activePanel === panel ? 'var(--ogrid-bg, #fff)' : 'transparent',
52
+ fontWeight: sb.activePanel === panel ? 'bold' : 'normal',
53
+ },
54
+ }, PANEL_ICONS[panel])));
55
+ let panelContent = null;
56
+ if (isOpen && sb.activePanel) {
57
+ const panelContainerStyle = {
58
+ width: `${PANEL_WIDTH}px`,
59
+ display: 'flex',
60
+ flexDirection: 'column',
61
+ overflow: 'hidden',
62
+ background: 'var(--ogrid-bg, #fff)',
63
+ color: 'var(--ogrid-fg, #242424)',
64
+ ...(position === 'right'
65
+ ? { borderLeft: '1px solid var(--ogrid-border, #e0e0e0)' }
66
+ : { borderRight: '1px solid var(--ogrid-border, #e0e0e0)' }),
67
+ };
68
+ const panelBodyChildren = [];
69
+ if (sb.activePanel === 'columns') {
70
+ const allVisible = sb.columns.every((c) => sb.visibleColumns.has(c.columnId));
71
+ // Select All / Clear All buttons
72
+ panelBodyChildren.push(h('div', { style: { display: 'flex', gap: '8px', marginBottom: '8px' } }, [
73
+ h('button', {
74
+ disabled: allVisible,
75
+ onClick: () => {
76
+ const next = new Set(sb.visibleColumns);
77
+ sb.columns.forEach((c) => next.add(c.columnId));
78
+ sb.onSetVisibleColumns(next);
79
+ },
80
+ style: { flex: '1', cursor: 'pointer', background: 'var(--ogrid-bg-subtle, #f3f2f1)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: '4px', padding: '4px 8px' },
81
+ }, 'Select All'),
82
+ h('button', {
83
+ onClick: () => {
84
+ const next = new Set();
85
+ sb.columns.forEach((c) => {
86
+ if (c.required && sb.visibleColumns.has(c.columnId))
87
+ next.add(c.columnId);
88
+ });
89
+ sb.onSetVisibleColumns(next);
90
+ },
91
+ style: { flex: '1', cursor: 'pointer', background: 'var(--ogrid-bg-subtle, #f3f2f1)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: '4px', padding: '4px 8px' },
92
+ }, 'Clear All'),
93
+ ]));
94
+ // Column checkboxes
95
+ sb.columns.forEach((col) => {
96
+ panelBodyChildren.push(h('label', { key: col.columnId, style: { display: 'flex', alignItems: 'center', gap: '6px', padding: '2px 0', cursor: 'pointer' } }, [
97
+ h('input', {
98
+ type: 'checkbox',
99
+ checked: sb.visibleColumns.has(col.columnId),
100
+ disabled: col.required,
101
+ onChange: (e) => sb.onVisibilityChange(col.columnId, e.target.checked),
102
+ }),
103
+ h('span', null, col.name),
104
+ ]));
105
+ });
106
+ }
107
+ if (sb.activePanel === 'filters') {
108
+ if (sb.filterableColumns.length === 0) {
109
+ panelBodyChildren.push(h('div', { style: { color: 'var(--ogrid-muted, #999)', fontStyle: 'italic' } }, 'No filterable columns'));
110
+ }
111
+ else {
112
+ sb.filterableColumns.forEach((col) => {
113
+ const filterKey = col.filterField;
114
+ const groupChildren = [
115
+ h('div', { style: { fontWeight: '500', marginBottom: '4px', fontSize: '13px' } }, col.name),
116
+ ];
117
+ if (col.filterType === 'text') {
118
+ const currentVal = sb.filters[filterKey]?.type === 'text' ? sb.filters[filterKey].value : '';
119
+ groupChildren.push(h('input', {
120
+ type: 'text',
121
+ value: currentVal,
122
+ onInput: (e) => {
123
+ const val = e.target.value;
124
+ sb.onFilterChange(filterKey, val ? { type: 'text', value: val } : undefined);
125
+ },
126
+ placeholder: `Filter ${col.name}...`,
127
+ 'aria-label': `Filter ${col.name}`,
128
+ style: { width: '100%', boxSizing: 'border-box', padding: '4px 6px', background: 'var(--ogrid-bg, #fff)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: '4px' },
129
+ }));
130
+ }
131
+ if (col.filterType === 'multiSelect') {
132
+ const options = sb.filterOptions[filterKey] ?? [];
133
+ const msChildren = options.map((opt) => {
134
+ const selected = sb.filters[filterKey]?.type === 'multiSelect' ? sb.filters[filterKey].value.includes(opt) : false;
135
+ return h('label', { key: opt, style: { display: 'flex', alignItems: 'center', gap: '4px', padding: '1px 0', cursor: 'pointer', fontSize: '13px' } }, [
136
+ h('input', {
137
+ type: 'checkbox',
138
+ checked: selected,
139
+ onChange: (e) => {
140
+ const current = sb.filters[filterKey]?.type === 'multiSelect' ? sb.filters[filterKey].value : [];
141
+ const next = e.target.checked
142
+ ? [...current, opt]
143
+ : current.filter((v) => v !== opt);
144
+ sb.onFilterChange(filterKey, next.length > 0 ? { type: 'multiSelect', value: next } : undefined);
145
+ },
146
+ }),
147
+ h('span', null, opt),
148
+ ]);
149
+ });
150
+ groupChildren.push(h('div', { style: { maxHeight: '120px', overflowY: 'auto' }, role: 'group', 'aria-label': `${col.name} options` }, msChildren));
151
+ }
152
+ if (col.filterType === 'date') {
153
+ const existingValue = sb.filters[filterKey]?.type === 'date' ? sb.filters[filterKey].value : { from: undefined, to: undefined };
154
+ groupChildren.push(h('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } }, [
155
+ h('label', { style: { display: 'flex', alignItems: 'center', gap: '4px', fontSize: '12px' } }, [
156
+ 'From:',
157
+ h('input', {
158
+ type: 'date',
159
+ value: existingValue.from ?? '',
160
+ onInput: (e) => {
161
+ const from = e.target.value || undefined;
162
+ const to = existingValue.to;
163
+ sb.onFilterChange(filterKey, from || to ? { type: 'date', value: { from, to } } : undefined);
164
+ },
165
+ 'aria-label': `${col.name} from date`,
166
+ style: { flex: '1', padding: '2px 4px', background: 'var(--ogrid-bg, #fff)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: '4px' },
167
+ }),
168
+ ]),
169
+ h('label', { style: { display: 'flex', alignItems: 'center', gap: '4px', fontSize: '12px' } }, [
170
+ 'To:',
171
+ h('input', {
172
+ type: 'date',
173
+ value: existingValue.to ?? '',
174
+ onInput: (e) => {
175
+ const to = e.target.value || undefined;
176
+ const from = existingValue.from;
177
+ sb.onFilterChange(filterKey, from || to ? { type: 'date', value: { from, to } } : undefined);
178
+ },
179
+ 'aria-label': `${col.name} to date`,
180
+ style: { flex: '1', padding: '2px 4px', background: 'var(--ogrid-bg, #fff)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: '4px' },
181
+ }),
182
+ ]),
183
+ ]));
184
+ }
185
+ panelBodyChildren.push(h('div', { key: col.columnId, style: { marginBottom: '12px' } }, groupChildren));
186
+ });
187
+ }
188
+ }
189
+ panelContent = h('div', { role: 'tabpanel', 'aria-label': PANEL_LABELS[sb.activePanel], style: panelContainerStyle }, [
190
+ // Panel header
191
+ h('div', {
192
+ style: {
193
+ display: 'flex',
194
+ justifyContent: 'space-between',
195
+ alignItems: 'center',
196
+ padding: '8px 12px',
197
+ borderBottom: '1px solid var(--ogrid-border, #e0e0e0)',
198
+ fontWeight: '600',
199
+ },
200
+ }, [
201
+ h('span', null, PANEL_LABELS[sb.activePanel]),
202
+ h('button', {
203
+ onClick: () => sb.onPanelChange(null),
204
+ style: { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: '16px', color: 'var(--ogrid-fg, #242424)' },
205
+ 'aria-label': 'Close panel',
206
+ }, '\u00D7'),
207
+ ]),
208
+ // Panel body
209
+ h('div', { style: { flex: '1', overflowY: 'auto', padding: '8px 12px' } }, panelBodyChildren),
210
+ ]);
211
+ }
212
+ const children = [];
213
+ if (position === 'left') {
214
+ children.push(tabStrip);
215
+ if (panelContent)
216
+ children.push(panelContent);
217
+ }
218
+ else {
219
+ if (panelContent)
220
+ children.push(panelContent);
221
+ children.push(tabStrip);
222
+ }
223
+ return h('div', {
224
+ style: { display: 'flex', flexDirection: 'row', flexShrink: '0' },
225
+ role: 'complementary',
226
+ 'aria-label': 'Side bar',
227
+ }, children);
228
+ }
229
+ /**
230
+ * Creates an OGrid component with framework-specific UI bindings.
231
+ * All orchestration logic, sidebar, toolbar, and layout are shared.
232
+ */
233
+ export function createOGrid(ui) {
234
+ return defineComponent({
235
+ name: 'OGrid',
236
+ props: {
237
+ gridProps: { type: Object, required: true },
238
+ },
239
+ setup(props, { expose }) {
240
+ const propsRef = computed(() => props.gridProps);
241
+ const { dataGridProps, pagination, columnChooser, layout, api } = useOGrid(propsRef);
242
+ // Expose the API for parent refs
243
+ expose({ api: api.value });
244
+ return () => {
245
+ const sideBar = layout.value.sideBarProps;
246
+ const hasSideBar = sideBar != null;
247
+ const sideBarPosition = sideBar?.position ?? 'right';
248
+ // Toolbar
249
+ const toolbarChildren = [];
250
+ if (layout.value.toolbar) {
251
+ toolbarChildren.push(layout.value.toolbar);
252
+ }
253
+ // ColumnChooser in toolbar
254
+ const toolbarEnd = columnChooser.value.placement === 'toolbar'
255
+ ? h(ui.ColumnChooser, {
256
+ columns: columnChooser.value.columns,
257
+ visibleColumns: columnChooser.value.visibleColumns,
258
+ onVisibilityChange: columnChooser.value.onVisibilityChange,
259
+ })
260
+ : null;
261
+ // Pagination
262
+ const paginationNode = h(ui.PaginationControls, {
263
+ currentPage: pagination.value.page,
264
+ pageSize: pagination.value.pageSize,
265
+ totalCount: pagination.value.displayTotalCount,
266
+ onPageChange: pagination.value.setPage,
267
+ onPageSizeChange: (size) => {
268
+ pagination.value.setPageSize(size);
269
+ pagination.value.setPage(1);
270
+ },
271
+ pageSizeOptions: pagination.value.pageSizeOptions,
272
+ entityLabelPlural: pagination.value.entityLabelPlural,
273
+ });
274
+ // Grid content area
275
+ const gridChild = h('div', {
276
+ style: { flex: '1', minWidth: '0', minHeight: '0', display: 'flex', flexDirection: 'column' },
277
+ }, [
278
+ h(ui.DataGridTable, {
279
+ gridProps: dataGridProps.value,
280
+ }),
281
+ ]);
282
+ // Main content area (sidebar + grid)
283
+ const mainAreaChildren = [];
284
+ if (hasSideBar && sideBarPosition === 'left') {
285
+ mainAreaChildren.push(renderSideBar(sideBar));
286
+ }
287
+ mainAreaChildren.push(gridChild);
288
+ if (hasSideBar && sideBarPosition !== 'left') {
289
+ mainAreaChildren.push(renderSideBar(sideBar));
290
+ }
291
+ return h('div', {
292
+ class: layout.value.className,
293
+ style: {
294
+ display: 'flex',
295
+ flexDirection: 'column',
296
+ border: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))',
297
+ borderRadius: '4px',
298
+ overflow: 'hidden',
299
+ },
300
+ }, [
301
+ // Toolbar strip
302
+ ...(toolbarChildren.length || toolbarEnd ? [
303
+ h('div', {
304
+ style: {
305
+ display: 'flex',
306
+ alignItems: 'center',
307
+ justifyContent: 'space-between',
308
+ padding: '8px 12px',
309
+ borderBottom: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))',
310
+ gap: '8px',
311
+ },
312
+ }, [
313
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flex: '1' } }, toolbarChildren),
314
+ ...(toolbarEnd ? [toolbarEnd] : []),
315
+ ]),
316
+ ] : []),
317
+ // Below toolbar strip
318
+ ...(layout.value.toolbarBelow ? [
319
+ h('div', {
320
+ style: { padding: '8px 12px', borderBottom: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))' },
321
+ }, [layout.value.toolbarBelow]),
322
+ ] : []),
323
+ // Main content area (sidebar + grid)
324
+ h('div', { style: { display: 'flex', flex: '1', minHeight: '0' } }, mainAreaChildren),
325
+ // Footer strip (pagination)
326
+ h('div', {
327
+ style: {
328
+ display: 'flex',
329
+ alignItems: 'center',
330
+ padding: '8px 0',
331
+ borderTop: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))',
332
+ },
333
+ }, [paginationNode]),
334
+ ]);
335
+ };
336
+ },
337
+ });
338
+ }
@@ -286,13 +286,25 @@ export function useCellSelection(params) {
286
286
  if (wasDrag)
287
287
  isDragging.value = false;
288
288
  };
289
+ let isUnmounted = false;
290
+ const onMoveSafe = (e) => {
291
+ if (isUnmounted)
292
+ return;
293
+ onMove(e);
294
+ };
295
+ const onUpSafe = () => {
296
+ if (isUnmounted)
297
+ return;
298
+ onUp();
299
+ };
289
300
  onMounted(() => {
290
- window.addEventListener('mousemove', onMove, true);
291
- window.addEventListener('mouseup', onUp, true);
301
+ window.addEventListener('mousemove', onMoveSafe, true);
302
+ window.addEventListener('mouseup', onUpSafe, true);
292
303
  });
293
304
  onUnmounted(() => {
294
- window.removeEventListener('mousemove', onMove, true);
295
- window.removeEventListener('mouseup', onUp, true);
305
+ isUnmounted = true;
306
+ window.removeEventListener('mousemove', onMoveSafe, true);
307
+ window.removeEventListener('mouseup', onUpSafe, true);
296
308
  if (rafId)
297
309
  cancelAnimationFrame(rafId);
298
310
  stopAutoScroll();
@@ -1,4 +1,4 @@
1
- import { ref, computed, watch, nextTick } from 'vue';
1
+ import { ref, shallowRef, computed, watch, nextTick, triggerRef } from 'vue';
2
2
  import { flattenColumns, getDataGridStatusBarConfig, parseValue, computeAggregations, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH } from '@alaarab/ogrid-core';
3
3
  import { useRowSelection } from './useRowSelection';
4
4
  import { useCellEditing } from './useCellEditing';
@@ -26,7 +26,7 @@ export function useDataGridState(params) {
26
26
  const { props, wrapperRef } = params;
27
27
  const items = computed(() => props.value.items);
28
28
  const columnsProp = computed(() => props.value.columns);
29
- const getRowId = computed(() => props.value.getRowId).value; // getRowId is stable
29
+ const getRowId = props.value.getRowId; // stable function reference, no reactivity needed
30
30
  const visibleColumnsProp = computed(() => props.value.visibleColumns);
31
31
  const columnOrderProp = computed(() => props.value.columnOrder);
32
32
  const rowSelectionProp = computed(() => props.value.rowSelection ?? 'none');
@@ -86,12 +86,14 @@ export function useDataGridState(params) {
86
86
  const hasRowNumbersCol = computed(() => !!props.value.showRowNumbers);
87
87
  const specialColsCount = computed(() => (hasCheckboxCol.value ? 1 : 0) + (hasRowNumbersCol.value ? 1 : 0));
88
88
  const totalColCount = computed(() => visibleColumnCount.value + specialColsCount.value);
89
- const colOffset = computed(() => specialColsCount.value).value; // stable once computed
90
- const rowIndexByRowId = computed(() => {
91
- const m = new Map();
92
- items.value.forEach((item, idx) => m.set(getRowId(item), idx));
93
- return m;
94
- });
89
+ const colOffset = specialColsCount.value; // snapshot: checkbox/rowNumbers cols are fixed at setup
90
+ const rowIndexByRowId = shallowRef(new Map());
91
+ watch(items, (newItems) => {
92
+ const m = rowIndexByRowId.value;
93
+ m.clear();
94
+ newItems.forEach((item, idx) => m.set(getRowId(item), idx));
95
+ triggerRef(rowIndexByRowId);
96
+ }, { immediate: true });
95
97
  const rowSelectionResult = useRowSelection({
96
98
  items,
97
99
  getRowId,
@@ -1,4 +1,4 @@
1
- import { ref, computed, watch } from 'vue';
1
+ import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
2
2
  import { mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, flattenColumns, processClientSideData, computeNextSortState, } from '@alaarab/ogrid-core';
3
3
  import { useFilterOptions } from './useFilterOptions';
4
4
  import { useSideBarState } from './useSideBarState';
@@ -173,8 +173,9 @@ export function useOGrid(props) {
173
173
  const serverTotalCount = ref(0);
174
174
  const loading = ref(true);
175
175
  let fetchId = 0;
176
+ let isDestroyed = false;
176
177
  const refreshCounter = ref(0);
177
- watch([isServerSide, () => dataProps.value.dataSource, page, pageSize, () => sort.value.field, () => sort.value.direction, filters, refreshCounter], () => {
178
+ const doFetch = () => {
178
179
  if (!isServerSide.value || !dataProps.value.dataSource) {
179
180
  if (!isServerSide.value)
180
181
  loading.value = false;
@@ -190,23 +191,34 @@ export function useOGrid(props) {
190
191
  filters: filters.value,
191
192
  })
192
193
  .then((res) => {
193
- if (id !== fetchId)
194
+ if (id !== fetchId || isDestroyed)
194
195
  return;
195
196
  serverItems.value = res.items;
196
197
  serverTotalCount.value = res.totalCount;
197
198
  })
198
199
  .catch((err) => {
199
- if (id !== fetchId)
200
+ if (id !== fetchId || isDestroyed)
200
201
  return;
201
202
  callbacks.value.onError?.(err);
202
203
  serverItems.value = [];
203
204
  serverTotalCount.value = 0;
204
205
  })
205
206
  .finally(() => {
206
- if (id === fetchId)
207
+ if (id === fetchId && !isDestroyed)
207
208
  loading.value = false;
208
209
  });
209
- }, { immediate: true });
210
+ };
211
+ // Initial fetch on mount
212
+ onMounted(() => {
213
+ doFetch();
214
+ });
215
+ // Subsequent fetches on page/sort/filter changes (no immediate — onMounted handles initial)
216
+ watch([() => dataProps.value.dataSource, page, pageSize, () => sort.value.field, () => sort.value.direction, filters, refreshCounter], () => {
217
+ doFetch();
218
+ });
219
+ onUnmounted(() => {
220
+ isDestroyed = true;
221
+ });
210
222
  const displayItems = computed(() => isClientSide.value && clientItemsAndTotal.value
211
223
  ? clientItemsAndTotal.value.items
212
224
  : serverItems.value);
@@ -339,6 +351,7 @@ export function useOGrid(props) {
339
351
  suppressHorizontalScroll: p.suppressHorizontalScroll,
340
352
  columnReorder: p.columnReorder,
341
353
  virtualScroll: p.virtualScroll,
354
+ rowHeight: p.rowHeight,
342
355
  density: p.density ?? 'normal',
343
356
  'aria-label': p['aria-label'],
344
357
  'aria-labelledby': p['aria-labelledby'],
@@ -1,10 +1,10 @@
1
- import { ref, computed } from 'vue';
1
+ import { shallowRef, computed } from 'vue';
2
2
  /**
3
3
  * Manages row selection state for single or multiple selection modes with shift-click range support.
4
4
  */
5
5
  export function useRowSelection(params) {
6
6
  const { items, getRowId, rowSelection, controlledSelectedRows, onSelectionChange, } = params;
7
- const internalSelectedRows = ref(new Set());
7
+ const internalSelectedRows = shallowRef(new Set());
8
8
  let lastClickedRow = -1;
9
9
  const selectedRowIds = computed(() => {
10
10
  const controlled = controlledSelectedRows.value;
package/dist/esm/index.js CHANGED
@@ -10,3 +10,7 @@ export { useOGrid, useDataGridState, useActiveCell, useCellEditing, useCellSelec
10
10
  export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from './utils';
11
11
  // DataGridTable factory (for UI packages)
12
12
  export { createDataGridTable } from './components/createDataGridTable';
13
+ // InlineCellEditor factory (for UI packages)
14
+ export { createInlineCellEditor } from './components/createInlineCellEditor';
15
+ // OGrid factory (for UI packages)
16
+ export { createOGrid } from './components/createOGrid';
@@ -0,0 +1,344 @@
1
+ /* OGrid Shared Layout Styles — consumed by vue-vuetify and vue-primevue */
2
+
3
+ /* Remove focus outline from scrollable wrapper (keyboard nav is handled via cell outlines) */
4
+ [role="region"][tabindex="0"] {
5
+ outline: none !important;
6
+ }
7
+
8
+ /* Cell selection highlighting */
9
+ .ogrid-cell-in-range {
10
+ background: var(--ogrid-bg-range, rgba(33, 115, 70, 0.12)) !important;
11
+ }
12
+
13
+ /* Cut range highlighting */
14
+ .ogrid-cell-cut {
15
+ background: var(--ogrid-hover-bg) !important;
16
+ opacity: 0.7;
17
+ }
18
+
19
+ /* Drag-range highlight applied via DOM attributes during drag (bypasses Vue for performance) */
20
+ [data-drag-range] {
21
+ background: var(--ogrid-range-bg, rgba(33, 115, 70, 0.12)) !important;
22
+ }
23
+
24
+ /* Anchor cell during drag: white/transparent background (like Excel) */
25
+ [data-drag-anchor] {
26
+ background: var(--ogrid-bg) !important;
27
+ }
28
+
29
+ /* === Layout === */
30
+
31
+ .ogrid-outer-container {
32
+ position: relative;
33
+ flex: 1;
34
+ min-height: 0;
35
+ display: flex;
36
+ flex-direction: column;
37
+ background-color: var(--ogrid-bg);
38
+ color: var(--ogrid-fg);
39
+ }
40
+
41
+ .ogrid-scroll-wrapper {
42
+ display: flex;
43
+ flex-direction: column;
44
+ min-height: 100%;
45
+ }
46
+
47
+ .ogrid-table-container {
48
+ position: relative;
49
+ }
50
+
51
+ .ogrid-table-container--loading {
52
+ opacity: 0.6;
53
+ }
54
+
55
+ .ogrid-table {
56
+ width: 100%;
57
+ border-collapse: collapse;
58
+ font-size: 0.875rem;
59
+ background-color: var(--ogrid-bg);
60
+ color: var(--ogrid-fg);
61
+ }
62
+
63
+ .ogrid-table tbody tr {
64
+ height: var(--ogrid-row-height, auto);
65
+ }
66
+
67
+ /* === Header === */
68
+
69
+ .ogrid-thead {
70
+ z-index: 8;
71
+ background-color: var(--ogrid-header-bg);
72
+ }
73
+
74
+ .ogrid-header-row {
75
+ background-color: var(--ogrid-header-bg);
76
+ }
77
+
78
+ .ogrid-header-cell {
79
+ font-weight: 600;
80
+ position: sticky;
81
+ top: 0;
82
+ background-color: var(--ogrid-header-bg);
83
+ z-index: 8;
84
+ }
85
+
86
+ .ogrid-header-cell--pinned-left {
87
+ z-index: 10;
88
+ will-change: transform;
89
+ }
90
+
91
+ .ogrid-header-cell--pinned-right {
92
+ z-index: 10;
93
+ will-change: transform;
94
+ }
95
+
96
+ .ogrid-header-content {
97
+ display: flex;
98
+ align-items: center;
99
+ width: 100%;
100
+ }
101
+
102
+ .ogrid-column-group-header {
103
+ text-align: center;
104
+ font-weight: 600;
105
+ border-bottom: 2px solid var(--ogrid-border);
106
+ padding: 6px;
107
+ }
108
+
109
+ .ogrid-column-menu-btn {
110
+ background: none;
111
+ border: none;
112
+ cursor: pointer;
113
+ padding: 4px 6px;
114
+ font-size: 16px;
115
+ color: var(--ogrid-fg-muted);
116
+ line-height: 1;
117
+ flex-shrink: 0;
118
+ border-radius: 4px;
119
+ display: inline-flex;
120
+ align-items: center;
121
+ justify-content: center;
122
+ min-width: 24px;
123
+ height: 24px;
124
+ transition: background-color 0.15s;
125
+ }
126
+
127
+ .ogrid-column-menu-btn:hover {
128
+ background: var(--ogrid-hover-bg, rgba(0, 0, 0, 0.04));
129
+ color: var(--ogrid-fg);
130
+ }
131
+
132
+ /* === Checkbox column === */
133
+
134
+ .ogrid-checkbox-header,
135
+ .ogrid-checkbox-cell {
136
+ text-align: center;
137
+ padding: 4px;
138
+ }
139
+
140
+ .ogrid-checkbox-cell {
141
+ padding: 0;
142
+ }
143
+
144
+ .ogrid-checkbox-wrapper {
145
+ display: flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ }
149
+
150
+ .ogrid-checkbox-spacer {
151
+ padding: 0;
152
+ }
153
+
154
+ /* === Row numbers === */
155
+
156
+ .ogrid-row-number-header {
157
+ text-align: center;
158
+ font-weight: 600;
159
+ background-color: var(--ogrid-header-bg);
160
+ color: var(--ogrid-fg-secondary);
161
+ }
162
+
163
+ .ogrid-row-number-spacer {
164
+ background-color: var(--ogrid-header-bg);
165
+ }
166
+
167
+ .ogrid-row-number-cell {
168
+ text-align: center;
169
+ font-weight: 600;
170
+ font-variant-numeric: tabular-nums;
171
+ background-color: var(--ogrid-header-bg);
172
+ color: var(--ogrid-fg-secondary);
173
+ }
174
+
175
+ /* === Data cells === */
176
+
177
+ .ogrid-data-cell {
178
+ position: relative;
179
+ padding: 0;
180
+ height: 1px;
181
+ }
182
+
183
+ .ogrid-data-cell--pinned-left {
184
+ position: sticky;
185
+ z-index: 6;
186
+ background-color: var(--ogrid-bg);
187
+ will-change: transform;
188
+ }
189
+
190
+ .ogrid-data-cell--pinned-right {
191
+ position: sticky;
192
+ z-index: 6;
193
+ background-color: var(--ogrid-bg);
194
+ will-change: transform;
195
+ }
196
+
197
+ .ogrid-cell-content {
198
+ width: 100%;
199
+ height: 100%;
200
+ display: flex;
201
+ align-items: center;
202
+ min-width: 0;
203
+ padding: 6px 10px;
204
+ box-sizing: border-box;
205
+ overflow: hidden;
206
+ text-overflow: ellipsis;
207
+ white-space: nowrap;
208
+ user-select: none;
209
+ outline: none;
210
+ }
211
+
212
+ .ogrid-cell-content--numeric {
213
+ justify-content: flex-end;
214
+ text-align: right;
215
+ }
216
+
217
+ .ogrid-cell-content--boolean {
218
+ justify-content: center;
219
+ text-align: center;
220
+ }
221
+
222
+ .ogrid-cell-content--editable {
223
+ cursor: cell;
224
+ }
225
+
226
+ .ogrid-cell-content--active {
227
+ outline: 2px solid var(--ogrid-selection, #217346);
228
+ outline-offset: -1px;
229
+ z-index: 2;
230
+ position: relative;
231
+ overflow: visible;
232
+ }
233
+
234
+ /* === Editing cell wrapper === */
235
+
236
+ .ogrid-editing-cell {
237
+ width: 100%;
238
+ height: 100%;
239
+ display: flex;
240
+ align-items: center;
241
+ box-sizing: border-box;
242
+ outline: 2px solid var(--ogrid-selection-color, #217346);
243
+ outline-offset: -1px;
244
+ z-index: 2;
245
+ position: relative;
246
+ background: var(--ogrid-bg, #fff);
247
+ overflow: visible;
248
+ padding: 0;
249
+ }
250
+
251
+ /* === Fill handle === */
252
+
253
+ .ogrid-fill-handle {
254
+ position: absolute;
255
+ right: -3px;
256
+ bottom: -3px;
257
+ width: 7px;
258
+ height: 7px;
259
+ background-color: var(--ogrid-selection, #217346);
260
+ border: 1px solid var(--ogrid-bg);
261
+ border-radius: 1px;
262
+ cursor: crosshair;
263
+ pointer-events: auto;
264
+ z-index: 3;
265
+ }
266
+
267
+ /* === Resize handle === */
268
+
269
+ .ogrid-resize-handle {
270
+ position: absolute;
271
+ top: 0;
272
+ right: -3px;
273
+ bottom: 0;
274
+ width: 8px;
275
+ cursor: col-resize;
276
+ user-select: none;
277
+ }
278
+
279
+ /* === Drop indicator === */
280
+
281
+ .ogrid-drop-indicator {
282
+ position: absolute;
283
+ top: 0;
284
+ bottom: 0;
285
+ width: 3px;
286
+ background: var(--ogrid-primary, #217346);
287
+ pointer-events: none;
288
+ z-index: 100;
289
+ }
290
+
291
+ /* === Empty state === */
292
+
293
+ .ogrid-empty-state {
294
+ padding: 32px 16px;
295
+ text-align: center;
296
+ border-top: 1px solid var(--ogrid-border);
297
+ background-color: var(--ogrid-header-bg);
298
+ }
299
+
300
+ .ogrid-empty-state-title {
301
+ font-size: 1.25rem;
302
+ font-weight: 600;
303
+ margin-bottom: 8px;
304
+ }
305
+
306
+ .ogrid-empty-state-message {
307
+ font-size: 0.875rem;
308
+ color: var(--ogrid-fg-secondary);
309
+ }
310
+
311
+ /* === Loading overlay === */
312
+
313
+ .ogrid-loading-overlay {
314
+ position: absolute;
315
+ inset: 0;
316
+ z-index: 2;
317
+ display: flex;
318
+ align-items: center;
319
+ justify-content: center;
320
+ background-color: var(--ogrid-loading-overlay);
321
+ }
322
+
323
+ .ogrid-loading-inner {
324
+ display: flex;
325
+ flex-direction: column;
326
+ align-items: center;
327
+ gap: 8px;
328
+ padding: 16px;
329
+ background-color: var(--ogrid-bg);
330
+ border: 1px solid var(--ogrid-border);
331
+ border-radius: 4px;
332
+ }
333
+
334
+ .ogrid-loading-message {
335
+ font-size: 0.875rem;
336
+ color: var(--ogrid-fg-secondary);
337
+ }
338
+
339
+ /* === Popover editor anchor === */
340
+
341
+ .ogrid-popover-anchor {
342
+ min-height: 100%;
343
+ min-width: 40px;
344
+ }
@@ -0,0 +1,52 @@
1
+ /* OGrid Shared Theme Variables — consumed by vue-vuetify and vue-primevue */
2
+
3
+ /* ─── OGrid Theme Variables ─── */
4
+ :root {
5
+ --ogrid-bg: #ffffff;
6
+ --ogrid-fg: rgba(0, 0, 0, 0.87);
7
+ --ogrid-fg-secondary: rgba(0, 0, 0, 0.6);
8
+ --ogrid-fg-muted: rgba(0, 0, 0, 0.5);
9
+ --ogrid-border: rgba(0, 0, 0, 0.12);
10
+ --ogrid-header-bg: rgba(0, 0, 0, 0.04);
11
+ --ogrid-hover-bg: rgba(0, 0, 0, 0.04);
12
+ --ogrid-selected-row-bg: #e6f0fb;
13
+ --ogrid-active-cell-bg: rgba(0, 0, 0, 0.02);
14
+ --ogrid-range-bg: rgba(33, 115, 70, 0.12);
15
+ --ogrid-accent: #0078d4;
16
+ --ogrid-selection-color: #217346;
17
+ --ogrid-loading-overlay: rgba(255, 255, 255, 0.7);
18
+ }
19
+
20
+ @media (prefers-color-scheme: dark) {
21
+ :root:not([data-theme="light"]) {
22
+ --ogrid-bg: #1e1e1e;
23
+ --ogrid-fg: rgba(255, 255, 255, 0.87);
24
+ --ogrid-fg-secondary: rgba(255, 255, 255, 0.6);
25
+ --ogrid-fg-muted: rgba(255, 255, 255, 0.5);
26
+ --ogrid-border: rgba(255, 255, 255, 0.12);
27
+ --ogrid-header-bg: rgba(255, 255, 255, 0.06);
28
+ --ogrid-hover-bg: rgba(255, 255, 255, 0.08);
29
+ --ogrid-selected-row-bg: #1a3a5c;
30
+ --ogrid-active-cell-bg: rgba(255, 255, 255, 0.06);
31
+ --ogrid-range-bg: rgba(46, 160, 67, 0.15);
32
+ --ogrid-accent: #4da6ff;
33
+ --ogrid-selection-color: #2ea043;
34
+ --ogrid-loading-overlay: rgba(0, 0, 0, 0.7);
35
+ }
36
+ }
37
+
38
+ [data-theme="dark"] {
39
+ --ogrid-bg: #1e1e1e;
40
+ --ogrid-fg: rgba(255, 255, 255, 0.87);
41
+ --ogrid-fg-secondary: rgba(255, 255, 255, 0.6);
42
+ --ogrid-fg-muted: rgba(255, 255, 255, 0.5);
43
+ --ogrid-border: rgba(255, 255, 255, 0.12);
44
+ --ogrid-header-bg: rgba(255, 255, 255, 0.06);
45
+ --ogrid-hover-bg: rgba(255, 255, 255, 0.08);
46
+ --ogrid-selected-row-bg: #1a3a5c;
47
+ --ogrid-active-cell-bg: rgba(255, 255, 255, 0.06);
48
+ --ogrid-range-bg: rgba(46, 160, 67, 0.15);
49
+ --ogrid-accent: #4da6ff;
50
+ --ogrid-selection-color: #2ea043;
51
+ --ogrid-loading-overlay: rgba(0, 0, 0, 0.7);
52
+ }
@@ -0,0 +1,75 @@
1
+ import { type PropType, type VNode } from 'vue';
2
+ import type { IColumnDef } from '../types';
3
+ export interface CreateInlineCellEditorOptions {
4
+ renderCheckbox: (props: {
5
+ checked: boolean;
6
+ onChange: (val: boolean) => void;
7
+ onCancel: () => void;
8
+ }) => VNode;
9
+ renderDatePicker: (props: {
10
+ value: string;
11
+ onChange: (val: string) => void;
12
+ onCancel: () => void;
13
+ }) => VNode;
14
+ }
15
+ export declare function createInlineCellEditor(options: CreateInlineCellEditorOptions): import("vue").DefineComponent<import("vue").ExtractPropTypes<{
16
+ value: {
17
+ default: undefined;
18
+ };
19
+ item: {
20
+ type: ObjectConstructor;
21
+ required: true;
22
+ };
23
+ column: {
24
+ type: PropType<IColumnDef>;
25
+ required: true;
26
+ };
27
+ rowIndex: {
28
+ type: NumberConstructor;
29
+ required: true;
30
+ };
31
+ editorType: {
32
+ type: PropType<"text" | "select" | "checkbox" | "richSelect" | "date">;
33
+ required: true;
34
+ };
35
+ onCommit: {
36
+ type: PropType<(value: unknown) => void>;
37
+ required: true;
38
+ };
39
+ onCancel: {
40
+ type: PropType<() => void>;
41
+ required: true;
42
+ };
43
+ }>, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
44
+ [key: string]: any;
45
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
46
+ value: {
47
+ default: undefined;
48
+ };
49
+ item: {
50
+ type: ObjectConstructor;
51
+ required: true;
52
+ };
53
+ column: {
54
+ type: PropType<IColumnDef>;
55
+ required: true;
56
+ };
57
+ rowIndex: {
58
+ type: NumberConstructor;
59
+ required: true;
60
+ };
61
+ editorType: {
62
+ type: PropType<"text" | "select" | "checkbox" | "richSelect" | "date">;
63
+ required: true;
64
+ };
65
+ onCommit: {
66
+ type: PropType<(value: unknown) => void>;
67
+ required: true;
68
+ };
69
+ onCancel: {
70
+ type: PropType<() => void>;
71
+ required: true;
72
+ };
73
+ }>> & Readonly<{}>, {
74
+ value: undefined;
75
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Shared OGrid factory for Vue UI packages.
3
+ *
4
+ * Both vue-vuetify and vue-primevue OGrid components are 100% identical —
5
+ * they only differ in which DataGridTable, ColumnChooser, and PaginationControls
6
+ * components they use. This factory extracts all shared logic into one place.
7
+ */
8
+ import { type PropType, type VNode, type Component } from 'vue';
9
+ import type { IOGridProps } from '../types';
10
+ /** Framework-specific component bindings passed by each UI package */
11
+ export interface IOGridUIBindings {
12
+ /** Package-local DataGridTable component */
13
+ DataGridTable: Component;
14
+ /** Package-local ColumnChooser component */
15
+ ColumnChooser: Component;
16
+ /** Package-local PaginationControls component */
17
+ PaginationControls: Component;
18
+ }
19
+ /**
20
+ * Creates an OGrid component with framework-specific UI bindings.
21
+ * All orchestration logic, sidebar, toolbar, and layout are shared.
22
+ */
23
+ export declare function createOGrid(ui: IOGridUIBindings): import("vue").DefineComponent<import("vue").ExtractPropTypes<{
24
+ gridProps: {
25
+ type: PropType<IOGridProps<unknown>>;
26
+ required: true;
27
+ };
28
+ }>, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
29
+ [key: string]: any;
30
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
31
+ gridProps: {
32
+ type: PropType<IOGridProps<unknown>>;
33
+ required: true;
34
+ };
35
+ }>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -9,4 +9,6 @@ export type { UseOGridResult, UseOGridPagination, UseOGridColumnChooser, UseOGri
9
9
  export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from './utils';
10
10
  export type { HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, CellInteractionHandlers, CellInteractionProps, } from './utils';
11
11
  export { createDataGridTable, type IDataGridTableUIBindings } from './components/createDataGridTable';
12
+ export { createInlineCellEditor, type CreateInlineCellEditorOptions } from './components/createInlineCellEditor';
13
+ export { createOGrid, type IOGridUIBindings } from './components/createOGrid';
12
14
  export type { SideBarProps, SideBarFilterColumn } from './components/SideBar';
@@ -77,6 +77,8 @@ interface IOGridBaseProps<T> {
77
77
  columnReorder?: boolean;
78
78
  /** Virtual scrolling configuration. Set `enabled: true` with a fixed `rowHeight` to virtualize large datasets. */
79
79
  virtualScroll?: IVirtualScrollConfig;
80
+ /** Fixed row height in pixels. Overrides default row height (36px). */
81
+ rowHeight?: number;
80
82
  /** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
81
83
  density?: 'compact' | 'normal' | 'comfortable';
82
84
  'aria-label'?: string;
@@ -157,6 +159,8 @@ export interface IOGridDataGridProps<T> {
157
159
  columnReorder?: boolean;
158
160
  /** Virtual scrolling configuration. */
159
161
  virtualScroll?: IVirtualScrollConfig;
162
+ /** Fixed row height in pixels. Overrides default row height (36px). */
163
+ rowHeight?: number;
160
164
  /** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
161
165
  density?: 'compact' | 'normal' | 'comfortable';
162
166
  'aria-label'?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-vue",
3
- "version": "2.0.18",
3
+ "version": "2.0.21",
4
4
  "description": "OGrid Vue – Vue 3 composables, headless components, and utilities for OGrid data grids.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -10,10 +10,11 @@
10
10
  "types": "./dist/types/index.d.ts",
11
11
  "import": "./dist/esm/index.js",
12
12
  "require": "./dist/esm/index.js"
13
- }
13
+ },
14
+ "./styles/*": "./dist/esm/styles/*"
14
15
  },
15
16
  "scripts": {
16
- "build": "rimraf dist && tsc -p tsconfig.build.json",
17
+ "build": "rimraf dist && tsc -p tsconfig.build.json && node scripts/copy-css.js",
17
18
  "test": "jest --passWithNoTests"
18
19
  },
19
20
  "keywords": [
@@ -35,7 +36,7 @@
35
36
  "node": ">=18"
36
37
  },
37
38
  "dependencies": {
38
- "@alaarab/ogrid-core": "2.0.18"
39
+ "@alaarab/ogrid-core": "2.0.21"
39
40
  },
40
41
  "peerDependencies": {
41
42
  "vue": "^3.3.0"
@@ -47,7 +48,9 @@
47
48
  "vite-plugin-dts": "^4.5.4",
48
49
  "typescript": "^5.9.3"
49
50
  },
50
- "sideEffects": false,
51
+ "sideEffects": [
52
+ "**/*.css"
53
+ ],
51
54
  "publishConfig": {
52
55
  "access": "public"
53
56
  }