@alaarab/ogrid-vue-vuetify 2.0.11 → 2.0.13

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.
@@ -60,16 +60,16 @@ export const ColumnHeaderMenu = defineComponent({
60
60
  }, {
61
61
  default: () => h(VList, { density: 'compact', 'aria-label': 'Column options' }, () => {
62
62
  const children = [];
63
- items.value.forEach((item, index) => {
64
- // Add divider before item if needed (but not at the start)
65
- if (item.divider && index > 0) {
66
- children.push(h(VDivider, { key: `divider-${item.id}` }));
67
- }
63
+ items.value.forEach((item) => {
68
64
  children.push(h(VListItem, {
69
65
  key: item.id,
70
66
  disabled: item.disabled,
71
67
  onClick: () => { getHandler(item.id)(); },
72
68
  }, () => item.label));
69
+ // Add divider after item to separate sections
70
+ if (item.divider) {
71
+ children.push(h(VDivider, { key: `divider-${item.id}` }));
72
+ }
73
73
  });
74
74
  return children;
75
75
  }),
@@ -1,4 +1,55 @@
1
- /* OGrid Vue Vuetify DataGridTable styles */
1
+ /* OGrid Vue Vuetify - DataGridTable styles */
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
+ }
2
53
 
3
54
  /* Remove focus outline from scrollable wrapper (keyboard nav is handled via cell outlines) */
4
55
  [role="region"][tabindex="0"] {
@@ -12,18 +63,18 @@
12
63
 
13
64
  /* Cut range highlighting */
14
65
  .ogrid-cell-cut {
15
- background: var(--ogrid-bg-hover, rgba(0, 0, 0, 0.04)) !important;
66
+ background: var(--ogrid-hover-bg) !important;
16
67
  opacity: 0.7;
17
68
  }
18
69
 
19
70
  /* Drag-range highlight applied via DOM attributes during drag (bypasses Vue for performance) */
20
71
  [data-drag-range] {
21
- background: var(--ogrid-bg-range, rgba(33, 115, 70, 0.12)) !important;
72
+ background: var(--ogrid-range-bg, rgba(33, 115, 70, 0.12)) !important;
22
73
  }
23
74
 
24
75
  /* Anchor cell during drag: white/transparent background (like Excel) */
25
76
  [data-drag-anchor] {
26
- background: var(--ogrid-bg, #fff) !important;
77
+ background: var(--ogrid-bg) !important;
27
78
  }
28
79
 
29
80
  /* Accessibility: Focus visible styles */
@@ -49,3 +100,299 @@
49
100
  outline-offset: -2px;
50
101
  z-index: 3;
51
102
  }
103
+
104
+ /* === Layout === */
105
+
106
+ .ogrid-outer-container {
107
+ position: relative;
108
+ flex: 1;
109
+ min-height: 0;
110
+ display: flex;
111
+ flex-direction: column;
112
+ background-color: var(--ogrid-bg);
113
+ color: var(--ogrid-fg);
114
+ }
115
+
116
+ .ogrid-scroll-wrapper {
117
+ display: flex;
118
+ flex-direction: column;
119
+ min-height: 100%;
120
+ }
121
+
122
+ .ogrid-table-container {
123
+ position: relative;
124
+ }
125
+
126
+ .ogrid-table-container--loading {
127
+ opacity: 0.6;
128
+ }
129
+
130
+ .ogrid-table {
131
+ width: 100%;
132
+ border-collapse: collapse;
133
+ font-size: 0.875rem;
134
+ background-color: var(--ogrid-bg);
135
+ color: var(--ogrid-fg);
136
+ }
137
+
138
+ /* === Header === */
139
+
140
+ .ogrid-thead {
141
+ z-index: 8;
142
+ background-color: var(--ogrid-header-bg);
143
+ }
144
+
145
+ .ogrid-header-row {
146
+ background-color: var(--ogrid-header-bg);
147
+ }
148
+
149
+ .ogrid-header-cell {
150
+ font-weight: 600;
151
+ position: sticky;
152
+ top: 0;
153
+ background-color: var(--ogrid-header-bg);
154
+ z-index: 8;
155
+ }
156
+
157
+ .ogrid-header-cell--pinned-left {
158
+ z-index: 10;
159
+ will-change: transform;
160
+ }
161
+
162
+ .ogrid-header-cell--pinned-right {
163
+ z-index: 10;
164
+ will-change: transform;
165
+ }
166
+
167
+ .ogrid-header-content {
168
+ display: flex;
169
+ align-items: center;
170
+ width: 100%;
171
+ }
172
+
173
+ .ogrid-column-group-header {
174
+ text-align: center;
175
+ font-weight: 600;
176
+ border-bottom: 2px solid var(--ogrid-border);
177
+ padding: 6px;
178
+ }
179
+
180
+ .ogrid-column-menu-btn {
181
+ background: none;
182
+ border: none;
183
+ cursor: pointer;
184
+ padding: 4px 6px;
185
+ font-size: 16px;
186
+ color: var(--ogrid-fg-muted);
187
+ line-height: 1;
188
+ flex-shrink: 0;
189
+ border-radius: 4px;
190
+ display: inline-flex;
191
+ align-items: center;
192
+ justify-content: center;
193
+ min-width: 24px;
194
+ height: 24px;
195
+ transition: background-color 0.15s;
196
+ }
197
+
198
+ .ogrid-column-menu-btn:hover {
199
+ background: var(--ogrid-hover-bg, rgba(0, 0, 0, 0.04));
200
+ color: var(--ogrid-fg);
201
+ }
202
+
203
+ /* === Checkbox column === */
204
+
205
+ .ogrid-checkbox-header,
206
+ .ogrid-checkbox-cell {
207
+ text-align: center;
208
+ padding: 4px;
209
+ }
210
+
211
+ .ogrid-checkbox-cell {
212
+ padding: 0;
213
+ }
214
+
215
+ .ogrid-checkbox-wrapper {
216
+ display: flex;
217
+ align-items: center;
218
+ justify-content: center;
219
+ }
220
+
221
+ .ogrid-checkbox-spacer {
222
+ padding: 0;
223
+ }
224
+
225
+ /* === Row numbers === */
226
+
227
+ .ogrid-row-number-header {
228
+ text-align: center;
229
+ font-weight: 600;
230
+ background-color: var(--ogrid-header-bg);
231
+ color: var(--ogrid-fg-secondary);
232
+ }
233
+
234
+ .ogrid-row-number-spacer {
235
+ background-color: var(--ogrid-header-bg);
236
+ }
237
+
238
+ .ogrid-row-number-cell {
239
+ text-align: center;
240
+ font-weight: 600;
241
+ font-variant-numeric: tabular-nums;
242
+ background-color: var(--ogrid-header-bg);
243
+ color: var(--ogrid-fg-secondary);
244
+ }
245
+
246
+ /* === Data cells === */
247
+
248
+ .ogrid-data-cell {
249
+ position: relative;
250
+ padding: 0;
251
+ height: 1px;
252
+ }
253
+
254
+ .ogrid-data-cell--pinned-left {
255
+ position: sticky;
256
+ z-index: 6;
257
+ background-color: var(--ogrid-bg);
258
+ will-change: transform;
259
+ }
260
+
261
+ .ogrid-data-cell--pinned-right {
262
+ position: sticky;
263
+ z-index: 6;
264
+ background-color: var(--ogrid-bg);
265
+ will-change: transform;
266
+ }
267
+
268
+ .ogrid-cell-content {
269
+ width: 100%;
270
+ height: 100%;
271
+ display: flex;
272
+ align-items: center;
273
+ min-width: 0;
274
+ padding: 6px 10px;
275
+ box-sizing: border-box;
276
+ overflow: hidden;
277
+ text-overflow: ellipsis;
278
+ white-space: nowrap;
279
+ user-select: none;
280
+ outline: none;
281
+ }
282
+
283
+ .ogrid-cell-content--numeric {
284
+ justify-content: flex-end;
285
+ text-align: right;
286
+ }
287
+
288
+ .ogrid-cell-content--boolean {
289
+ justify-content: center;
290
+ text-align: center;
291
+ }
292
+
293
+ .ogrid-cell-content--editable {
294
+ cursor: cell;
295
+ }
296
+
297
+ .ogrid-cell-content--active {
298
+ outline: 2px solid var(--ogrid-selection, #217346);
299
+ outline-offset: -1px;
300
+ z-index: 2;
301
+ position: relative;
302
+ overflow: visible;
303
+ }
304
+
305
+ /* === Fill handle === */
306
+
307
+ .ogrid-fill-handle {
308
+ position: absolute;
309
+ right: -3px;
310
+ bottom: -3px;
311
+ width: 7px;
312
+ height: 7px;
313
+ background-color: var(--ogrid-selection, #217346);
314
+ border: 1px solid var(--ogrid-bg);
315
+ border-radius: 1px;
316
+ cursor: crosshair;
317
+ pointer-events: auto;
318
+ z-index: 3;
319
+ }
320
+
321
+ /* === Resize handle === */
322
+
323
+ .ogrid-resize-handle {
324
+ position: absolute;
325
+ top: 0;
326
+ right: -3px;
327
+ bottom: 0;
328
+ width: 8px;
329
+ cursor: col-resize;
330
+ user-select: none;
331
+ }
332
+
333
+ /* === Drop indicator === */
334
+
335
+ .ogrid-drop-indicator {
336
+ position: absolute;
337
+ top: 0;
338
+ bottom: 0;
339
+ width: 3px;
340
+ background: var(--ogrid-primary, #217346);
341
+ pointer-events: none;
342
+ z-index: 100;
343
+ }
344
+
345
+ /* === Empty state === */
346
+
347
+ .ogrid-empty-state {
348
+ padding: 32px 16px;
349
+ text-align: center;
350
+ border-top: 1px solid var(--ogrid-border);
351
+ background-color: var(--ogrid-header-bg);
352
+ }
353
+
354
+ .ogrid-empty-state-title {
355
+ font-size: 1.25rem;
356
+ font-weight: 600;
357
+ margin-bottom: 8px;
358
+ }
359
+
360
+ .ogrid-empty-state-message {
361
+ font-size: 0.875rem;
362
+ color: var(--ogrid-fg-secondary);
363
+ }
364
+
365
+ /* === Loading overlay === */
366
+
367
+ .ogrid-loading-overlay {
368
+ position: absolute;
369
+ inset: 0;
370
+ z-index: 2;
371
+ display: flex;
372
+ align-items: center;
373
+ justify-content: center;
374
+ background-color: var(--ogrid-loading-overlay);
375
+ }
376
+
377
+ .ogrid-loading-inner {
378
+ display: flex;
379
+ flex-direction: column;
380
+ align-items: center;
381
+ gap: 8px;
382
+ padding: 16px;
383
+ background-color: var(--ogrid-bg);
384
+ border: 1px solid var(--ogrid-border);
385
+ border-radius: 4px;
386
+ }
387
+
388
+ .ogrid-loading-message {
389
+ font-size: 0.875rem;
390
+ color: var(--ogrid-fg-secondary);
391
+ }
392
+
393
+ /* === Popover editor anchor === */
394
+
395
+ .ogrid-popover-anchor {
396
+ min-height: 100%;
397
+ min-width: 40px;
398
+ }
@@ -1,609 +1,28 @@
1
- import { defineComponent, computed, h, Teleport } from 'vue';
2
- import { VCheckbox, VProgressCircular, VBtn } from 'vuetify/components';
3
- import { useDataGridTableSetup, getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, buildHeaderRows, CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, StatusBar, } from '@alaarab/ogrid-vue';
1
+ import { h } from 'vue';
2
+ import { VCheckbox, VProgressCircular } from 'vuetify/components';
3
+ import { createDataGridTable } from '@alaarab/ogrid-vue';
4
4
  import { ColumnHeaderFilter } from '../ColumnHeaderFilter';
5
5
  import { ColumnHeaderMenu } from '../ColumnHeaderMenu/ColumnHeaderMenu';
6
6
  import { InlineCellEditor } from './InlineCellEditor';
7
7
  import { GridContextMenu } from './GridContextMenu';
8
+ import { renderEmptyState } from './EmptyState';
8
9
  import './DataGridTable.css';
9
- const NOOP = () => { };
10
- export const DataGridTable = defineComponent({
11
- name: 'DataGridTable',
12
- props: {
13
- // Pass all IOGridDataGridProps as a single prop object for simplicity with generics
14
- gridProps: { type: Object, required: true },
15
- },
16
- setup(props) {
17
- const propsRef = computed(() => props.gridProps);
18
- const { wrapperRef, tableContainerRef, tableRef, lastMouseShift, state, columnReorder: { isDragging: isReorderDragging, dropIndicatorX, handleHeaderMouseDown: handleReorderMouseDown }, virtualScroll: { containerRef: vsContainerRef, visibleRange, totalHeight: _totalHeight, scrollToRow: _scrollToRow }, virtualScrollEnabled, columnResize: { handleResizeStart, getColumnWidth }, } = useDataGridTableSetup({ props: propsRef });
19
- return () => {
20
- /**
21
- * Vue Performance: Fine-grained reactivity vs React memoization
22
- *
23
- * This render function destructures all state properties upfront (lines 43-63),
24
- * which differs from React's GridRow memoization pattern. However, benchmarking
25
- * shows excellent performance (4.74ms for selection change with 1000 rows).
26
- *
27
- * Why Vue doesn't need React-style memoization:
28
- * - Vue tracks which specific reactive properties are accessed during render
29
- * - When `activeCell` changes, Vue only re-runs code paths that access it
30
- * - React re-runs the entire render function unless explicitly memoized
31
- *
32
- * The over-dereferencing pattern here is intentional for code clarity.
33
- * Vue's reactivity system compensates for it automatically.
34
- *
35
- * Benchmark results (1000 rows): Initial: 19.07ms, Selection change: 4.74ms
36
- */
37
- const p = props.gridProps;
38
- const layout = state.layout.value;
39
- const rowSel = state.rowSelection.value;
40
- const editing = state.editing.value;
41
- const interaction = state.interaction.value;
42
- const ctxMenu = state.contextMenu.value;
43
- const viewModels = state.viewModels.value;
44
- const pinning = state.pinning.value;
45
- const { headerMenu } = pinning;
46
- const { visibleCols, hasCheckboxCol, hasRowNumbersCol, colOffset: _colOffset, containerWidth, minTableWidth, desiredTableWidth, } = layout;
47
- const currentPage = props.gridProps.currentPage ?? 1;
48
- const pageSize = props.gridProps.pageSize ?? 25;
49
- const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * pageSize : 0;
50
- const { selectedRowIds, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
51
- const { editingCell: _editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
52
- const { setActiveCell, handleCellMouseDown, handleSelectAllCells, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange: _cutRange, copyRange: _copyRange, canUndo, canRedo, onUndo, onRedo, isDragging: _isDragging, } = interaction;
53
- const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
54
- const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError: _onCellError } = viewModels;
55
- const items = p.items;
56
- const getRowId = p.getRowId;
57
- const layoutMode = p.layoutMode ?? 'fill';
58
- const rowSelection = p.rowSelection ?? 'none';
59
- const freezeRows = p.freezeRows;
60
- const freezeCols = p.freezeCols;
61
- const suppressHorizontalScroll = p.suppressHorizontalScroll;
62
- const isLoading = p.isLoading ?? false;
63
- const loadingMessage = p.loadingMessage ?? 'Loading\u2026';
64
- const ariaLabel = p['aria-label'];
65
- const ariaLabelledBy = p['aria-labelledby'];
66
- const fitToContent = layoutMode === 'content';
67
- const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
68
- const headerRows = buildHeaderRows(p.columns, p.visibleColumns);
69
- const editCallbacks = { commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit };
70
- const interactionHandlers = { handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu };
71
- const handleSingleRowClick = (e) => {
72
- if (rowSelection !== 'single')
73
- return;
74
- const tr = e.currentTarget;
75
- const rowId = tr.dataset.rowId;
76
- if (!rowId)
77
- return;
78
- rowSel.updateSelection(selectedRowIds.has(rowId) ? new Set() : new Set([rowId]));
79
- };
80
- // Render a cell's content
81
- const renderCellContent = (item, col, rowIndex, colIdx) => {
82
- const descriptor = getCellRenderDescriptor(item, col, rowIndex, colIdx, cellDescriptorInput);
83
- const _rowId = getRowId(item);
84
- if (descriptor.mode === 'editing-inline') {
85
- const editorProps = buildInlineEditorProps(item, col, descriptor, editCallbacks);
86
- return h(InlineCellEditor, {
87
- value: editorProps.value,
88
- item: editorProps.item,
89
- column: editorProps.column,
90
- rowIndex: editorProps.rowIndex,
91
- editorType: editorProps.editorType,
92
- onCommit: editorProps.onCommit,
93
- onCancel: editorProps.onCancel,
94
- });
95
- }
96
- if (descriptor.mode === 'editing-popover' && typeof col.cellEditor === 'function') {
97
- const editorProps = buildPopoverEditorProps(item, col, descriptor, pendingEditorValue, editCallbacks);
98
- // For Vue, custom editors are components
99
- const CustomEditor = col.cellEditor;
100
- return h('div', [
101
- h('div', {
102
- ref: (el) => { if (el)
103
- setPopoverAnchorEl(el); },
104
- style: { minHeight: '100%', minWidth: '40px' },
105
- 'aria-hidden': 'true',
106
- }),
107
- // Render custom editor inline (Vue doesn't have Popover built-in to Vuetify in this pattern)
108
- popoverAnchorEl
109
- ? h(CustomEditor, editorProps)
110
- : null,
111
- ]);
112
- }
113
- // Display mode
114
- const content = resolveCellDisplayContent(col, item, descriptor.displayValue);
115
- const cellStyle = resolveCellStyle(col, item);
116
- const interactionProps = getCellInteractionProps(descriptor, col.columnId, interactionHandlers);
117
- // Compute cell CSS classes based on state
118
- const _cellClasses = ['ogrid-cell'];
119
- const cellInlineStyle = {
120
- width: '100%',
121
- height: '100%',
122
- display: 'flex',
123
- alignItems: 'center',
124
- minWidth: '0',
125
- padding: '6px 10px',
126
- boxSizing: 'border-box',
127
- overflow: 'hidden',
128
- textOverflow: 'ellipsis',
129
- whiteSpace: 'nowrap',
130
- userSelect: 'none',
131
- outline: 'none',
132
- };
133
- if (col.type === 'numeric') {
134
- cellInlineStyle.justifyContent = 'flex-end';
135
- cellInlineStyle.textAlign = 'right';
136
- }
137
- else if (col.type === 'boolean') {
138
- cellInlineStyle.justifyContent = 'center';
139
- cellInlineStyle.textAlign = 'center';
140
- }
141
- if (descriptor.canEditAny) {
142
- cellInlineStyle.cursor = 'cell';
143
- }
144
- if (descriptor.isActive && !descriptor.isInRange) {
145
- cellInlineStyle.outline = '2px solid var(--ogrid-selection, #217346)';
146
- cellInlineStyle.outlineOffset = '-1px';
147
- cellInlineStyle.zIndex = '2';
148
- cellInlineStyle.position = 'relative';
149
- cellInlineStyle.overflow = 'visible';
150
- }
151
- // Apply range/cut highlighting via CSS classes + data attributes for proper precedence
152
- let highlightClass = '';
153
- if (descriptor.isInRange) {
154
- highlightClass = 'ogrid-cell-in-range';
155
- }
156
- if (descriptor.isInCutRange) {
157
- highlightClass = 'ogrid-cell-cut';
158
- }
159
- const styledContent = cellStyle
160
- ? h('span', { style: cellStyle }, content)
161
- : content;
162
- return h('div', {
163
- ...interactionProps,
164
- class: highlightClass || undefined,
165
- style: cellInlineStyle,
166
- }, [
167
- styledContent,
168
- ...(descriptor.canEditAny && descriptor.isSelectionEndCell ? [
169
- h('div', {
170
- onMousedown: handleFillHandleMouseDown,
171
- 'aria-label': 'Fill handle',
172
- style: {
173
- position: 'absolute',
174
- right: '-3px',
175
- bottom: '-3px',
176
- width: '7px',
177
- height: '7px',
178
- backgroundColor: 'var(--ogrid-selection, #217346)',
179
- border: '1px solid var(--ogrid-bg, #fff)',
180
- borderRadius: '1px',
181
- cursor: 'crosshair',
182
- pointerEvents: 'auto',
183
- zIndex: '3',
184
- },
185
- }),
186
- ] : []),
187
- ]);
188
- };
189
- // Compute pinning offsets
190
- const columnWidthsMap = {};
191
- visibleCols.forEach((col) => {
192
- columnWidthsMap[col.columnId] = getColumnWidth(col);
193
- });
194
- const leftOffsets = pinning.computeLeftOffsets(visibleCols, columnWidthsMap, DEFAULT_MIN_COLUMN_WIDTH, hasCheckboxCol, CHECKBOX_COLUMN_WIDTH);
195
- const rightOffsets = pinning.computeRightOffsets(visibleCols, columnWidthsMap, DEFAULT_MIN_COLUMN_WIDTH);
196
- // Build column layouts
197
- const columnLayouts = visibleCols.map((col, colIdx) => {
198
- const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
199
- const isPinnedLeft = col.pinned === 'left';
200
- const isPinnedRight = col.pinned === 'right';
201
- const columnWidth = getColumnWidth(col);
202
- const tdStyle = {
203
- position: 'relative',
204
- padding: '0',
205
- height: '1px',
206
- };
207
- if (isPinnedLeft || (isFreezeCol && colIdx === 0)) {
208
- tdStyle.position = 'sticky';
209
- tdStyle.left = `${leftOffsets[col.columnId] ?? 0}px`;
210
- tdStyle.zIndex = '6';
211
- tdStyle.backgroundColor = '#fff';
212
- tdStyle.willChange = 'transform';
213
- }
214
- else if (isPinnedRight) {
215
- tdStyle.position = 'sticky';
216
- tdStyle.right = `${rightOffsets[col.columnId] ?? 0}px`;
217
- tdStyle.zIndex = '6';
218
- tdStyle.backgroundColor = '#fff';
219
- tdStyle.willChange = 'transform';
220
- }
221
- return { col, tdStyle, minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH, width: columnWidth, maxWidth: columnWidth };
222
- });
223
- // Build header cell styles
224
- const getHeaderStyle = (col, colIdx) => {
225
- const isFreezeCol = freezeCols != null && freezeCols >= 1 && colIdx < freezeCols;
226
- const isPinnedLeft = col.pinned === 'left';
227
- const isPinnedRight = col.pinned === 'right';
228
- const base = { fontWeight: '600', position: 'sticky', top: '0', backgroundColor: 'rgba(0,0,0,0.04)', zIndex: '8' };
229
- if (isPinnedLeft || (isFreezeCol && colIdx === 0)) {
230
- const leftOffset = leftOffsets[col.columnId] ?? 0;
231
- return { ...base, position: 'sticky', left: `${leftOffset}px`, top: '0', zIndex: '10', backgroundColor: 'rgba(0,0,0,0.04)', willChange: 'transform' };
232
- }
233
- if (isPinnedRight) {
234
- const rightOffset = rightOffsets[col.columnId] ?? 0;
235
- return { ...base, position: 'sticky', right: `${rightOffset}px`, top: '0', zIndex: '10', backgroundColor: 'rgba(0,0,0,0.04)', willChange: 'transform' };
236
- }
237
- return base;
238
- };
239
- const wrapperStyle = {
240
- position: 'relative',
241
- flex: '1',
242
- minHeight: '0',
243
- width: fitToContent ? 'fit-content' : '100%',
244
- maxWidth: '100%',
245
- overflowX: suppressHorizontalScroll ? 'hidden' : allowOverflowX ? 'auto' : 'hidden',
246
- overflowY: 'auto',
247
- backgroundColor: '#fff',
248
- willChange: 'scroll-position',
249
- };
250
- return h('div', { style: { position: 'relative', flex: '1', minHeight: '0', display: 'flex', flexDirection: 'column' } }, [
251
- // Scrollable wrapper
252
- h('div', {
253
- ref: (el) => { wrapperRef.value = el; vsContainerRef.value = el; },
254
- tabindex: 0,
255
- role: 'region',
256
- 'aria-label': ariaLabel ?? (ariaLabelledBy ? undefined : 'Data grid'),
257
- 'aria-labelledby': ariaLabelledBy,
258
- onMousedown: (e) => { lastMouseShift.value = e.shiftKey; },
259
- onKeydown: handleGridKeyDown,
260
- onContextmenu: (e) => e.preventDefault(),
261
- 'data-overflow-x': allowOverflowX ? 'true' : 'false',
262
- style: wrapperStyle,
263
- }, [
264
- h('div', { style: { display: 'flex', flexDirection: 'column', minHeight: '100%' } }, [
265
- h('div', { style: { minWidth: allowOverflowX ? `${minTableWidth}px` : undefined } }, [
266
- h('div', {
267
- ref: (el) => { tableContainerRef.value = el; },
268
- style: isLoading && items.length > 0
269
- ? { position: 'relative', opacity: '0.6' }
270
- : { position: 'relative', opacity: '1' },
271
- }, [
272
- // Drop indicator for column reorder
273
- ...(isReorderDragging.value && dropIndicatorX.value !== null ? [
274
- h('div', {
275
- style: {
276
- position: 'absolute',
277
- top: '0',
278
- bottom: '0',
279
- width: '3px',
280
- background: 'var(--ogrid-primary, #217346)',
281
- pointerEvents: 'none',
282
- zIndex: '100',
283
- left: `${dropIndicatorX.value}px`,
284
- },
285
- }),
286
- ] : []),
287
- // Table
288
- h('table', {
289
- ref: (el) => { tableRef.value = el; },
290
- style: {
291
- width: '100%',
292
- borderCollapse: 'collapse',
293
- minWidth: `${minTableWidth}px`,
294
- fontSize: '0.875rem',
295
- },
296
- 'data-freeze-rows': freezeRows != null && freezeRows >= 1 ? freezeRows : undefined,
297
- 'data-freeze-cols': freezeCols != null && freezeCols >= 1 ? freezeCols : undefined,
298
- }, [
299
- // Header
300
- h('thead', { style: { zIndex: '8', backgroundColor: 'rgba(0,0,0,0.04)' } }, headerRows.map((row, rowIdx) => h('tr', { key: rowIdx, style: { backgroundColor: 'rgba(0,0,0,0.04)' } }, [
301
- // Checkbox header cell (last leaf row only)
302
- ...(rowIdx === headerRows.length - 1 && hasCheckboxCol ? [
303
- h('th', {
304
- style: {
305
- width: `${CHECKBOX_COLUMN_WIDTH}px`,
306
- minWidth: `${CHECKBOX_COLUMN_WIDTH}px`,
307
- maxWidth: `${CHECKBOX_COLUMN_WIDTH}px`,
308
- textAlign: 'center',
309
- padding: '4px',
310
- },
311
- }, h(VCheckbox, {
312
- modelValue: allSelected,
313
- indeterminate: someSelected,
314
- hideDetails: true,
315
- density: 'compact',
316
- 'aria-label': 'Select all rows',
317
- 'onUpdate:modelValue': (c) => handleSelectAll(!!c),
318
- })),
319
- ] : []),
320
- // Empty placeholder for checkbox in first group row
321
- ...(rowIdx === 0 && rowIdx < headerRows.length - 1 && hasCheckboxCol ? [
322
- h('th', {
323
- rowSpan: headerRows.length - 1,
324
- style: { width: `${CHECKBOX_COLUMN_WIDTH}px`, minWidth: `${CHECKBOX_COLUMN_WIDTH}px`, padding: '0' },
325
- }),
326
- ] : []),
327
- // Row numbers header cell (last leaf row only)
328
- ...(rowIdx === headerRows.length - 1 && hasRowNumbersCol ? [
329
- h('th', {
330
- style: {
331
- width: `${ROW_NUMBER_COLUMN_WIDTH}px`,
332
- minWidth: `${ROW_NUMBER_COLUMN_WIDTH}px`,
333
- maxWidth: `${ROW_NUMBER_COLUMN_WIDTH}px`,
334
- textAlign: 'center',
335
- fontWeight: '600',
336
- padding: '6px',
337
- backgroundColor: 'rgba(0,0,0,0.04)',
338
- color: 'rgba(0,0,0,0.6)',
339
- },
340
- }, '#'),
341
- ] : []),
342
- // Empty placeholder for row numbers in first group row
343
- ...(rowIdx === 0 && rowIdx < headerRows.length - 1 && hasRowNumbersCol ? [
344
- h('th', {
345
- rowSpan: headerRows.length - 1,
346
- style: { width: `${ROW_NUMBER_COLUMN_WIDTH}px`, minWidth: `${ROW_NUMBER_COLUMN_WIDTH}px`, padding: '0' },
347
- }),
348
- ] : []),
349
- // Header cells
350
- ...row.map((cell, cellIdx) => {
351
- if (cell.isGroup) {
352
- return h('th', {
353
- key: cellIdx,
354
- colSpan: cell.colSpan,
355
- scope: 'colgroup',
356
- style: { textAlign: 'center', fontWeight: '600', borderBottom: '2px solid rgba(0,0,0,0.12)', padding: '6px' },
357
- }, cell.label);
358
- }
359
- const col = cell.columnDef;
360
- const colIdx = visibleCols.indexOf(col);
361
- const columnWidth = getColumnWidth(col);
362
- const headerStyle = getHeaderStyle(col, colIdx);
363
- return h('th', {
364
- key: col.columnId,
365
- scope: 'col',
366
- 'data-column-id': col.columnId,
367
- rowSpan: headerRows.length > 1 ? headerRows.length - rowIdx : undefined,
368
- style: {
369
- ...headerStyle,
370
- cursor: isReorderDragging.value ? 'grabbing' : 'grab',
371
- minWidth: `${col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH}px`,
372
- width: `${columnWidth}px`,
373
- maxWidth: `${columnWidth}px`,
374
- },
375
- onMousedown: (e) => handleReorderMouseDown(col.columnId, e),
376
- }, [
377
- h('div', { style: { display: 'flex', alignItems: 'center', width: '100%' } }, [
378
- h(ColumnHeaderFilter, getHeaderFilterConfig(col, headerFilterInput)),
379
- h('button', {
380
- onClick: (e) => {
381
- e.stopPropagation();
382
- headerMenu.open(col.columnId, e.currentTarget);
383
- },
384
- 'aria-label': 'Column options',
385
- title: 'Column options',
386
- style: {
387
- background: 'none',
388
- border: 'none',
389
- cursor: 'pointer',
390
- padding: '2px 4px',
391
- fontSize: '14px',
392
- color: 'rgba(0,0,0,0.5)',
393
- lineHeight: '1',
394
- flexShrink: '0',
395
- },
396
- }, '\u22EE'),
397
- ]),
398
- h('div', {
399
- onMousedown: (e) => { e.stopPropagation(); handleResizeStart(e, col); },
400
- style: {
401
- position: 'absolute',
402
- top: '0',
403
- right: '-3px',
404
- bottom: '0',
405
- width: '8px',
406
- cursor: 'col-resize',
407
- userSelect: 'none',
408
- },
409
- }),
410
- ]);
411
- }),
412
- ]))),
413
- // Body
414
- ...(!showEmptyInGrid ? [
415
- h('tbody', {}, (() => {
416
- const vsEnabled = virtualScrollEnabled.value;
417
- const vr = visibleRange.value;
418
- const startIdx = vsEnabled ? vr.startIndex : 0;
419
- const endIdx = vsEnabled ? Math.min(vr.endIndex, items.length - 1) : items.length - 1;
420
- const rows = [];
421
- // Top spacer for virtual scrolling
422
- if (vsEnabled && vr.offsetTop > 0) {
423
- rows.push(h('tr', { key: '__vs-top', style: { height: `${vr.offsetTop}px` } }));
424
- }
425
- for (let rowIndex = startIdx; rowIndex <= endIdx; rowIndex++) {
426
- const item = items[rowIndex];
427
- if (!item)
428
- continue;
429
- const rowIdStr = getRowId(item);
430
- const isSelected = selectedRowIds.has(rowIdStr);
431
- rows.push(h('tr', {
432
- key: rowIdStr,
433
- 'data-row-id': rowIdStr,
434
- onClick: handleSingleRowClick,
435
- style: { cursor: rowSelection === 'single' ? 'pointer' : undefined },
436
- }, [
437
- // Checkbox cell
438
- ...(hasCheckboxCol ? [
439
- h('td', {
440
- style: {
441
- width: `${CHECKBOX_COLUMN_WIDTH}px`,
442
- minWidth: `${CHECKBOX_COLUMN_WIDTH}px`,
443
- maxWidth: `${CHECKBOX_COLUMN_WIDTH}px`,
444
- textAlign: 'center',
445
- padding: '0',
446
- },
447
- }, h('div', {
448
- 'data-row-index': rowIndex,
449
- 'data-col-index': 0,
450
- onClick: (e) => e.stopPropagation(),
451
- style: { display: 'flex', alignItems: 'center', justifyContent: 'center' },
452
- }, h(VCheckbox, {
453
- modelValue: isSelected,
454
- hideDetails: true,
455
- density: 'compact',
456
- 'aria-label': `Select row ${rowIndex + 1}`,
457
- 'onUpdate:modelValue': (checked) => handleRowCheckboxChange(rowIdStr, checked, rowIndex, lastMouseShift.value),
458
- }))),
459
- ] : []),
460
- // Row numbers cell
461
- ...(hasRowNumbersCol ? [
462
- h('td', {
463
- style: {
464
- width: `${ROW_NUMBER_COLUMN_WIDTH}px`,
465
- minWidth: `${ROW_NUMBER_COLUMN_WIDTH}px`,
466
- maxWidth: `${ROW_NUMBER_COLUMN_WIDTH}px`,
467
- textAlign: 'center',
468
- fontWeight: '600',
469
- fontVariantNumeric: 'tabular-nums',
470
- padding: '6px',
471
- backgroundColor: 'rgba(0,0,0,0.04)',
472
- color: 'rgba(0,0,0,0.6)',
473
- },
474
- }, String(rowNumberOffset + rowIndex + 1)),
475
- ] : []),
476
- // Data cells
477
- ...columnLayouts.map((cl, colIdx) => h('td', {
478
- key: cl.col.columnId,
479
- style: {
480
- ...cl.tdStyle,
481
- minWidth: `${cl.minWidth}px`,
482
- width: `${cl.width}px`,
483
- maxWidth: `${cl.maxWidth}px`,
484
- },
485
- }, [renderCellContent(item, cl.col, rowIndex, colIdx)])),
486
- ]));
487
- }
488
- // Bottom spacer for virtual scrolling
489
- if (vsEnabled && vr.offsetBottom > 0) {
490
- rows.push(h('tr', { key: '__vs-bottom', style: { height: `${vr.offsetBottom}px` } }));
491
- }
492
- return rows;
493
- })()),
494
- ] : []),
495
- ]),
496
- // Empty state
497
- ...(showEmptyInGrid && p.emptyState ? [
498
- h('div', {
499
- style: {
500
- padding: '32px 16px',
501
- textAlign: 'center',
502
- borderTop: '1px solid rgba(0,0,0,0.12)',
503
- backgroundColor: 'rgba(0,0,0,0.04)',
504
- },
505
- }, p.emptyState.render
506
- ? [p.emptyState.render()]
507
- : [
508
- h('div', { style: { fontSize: '1.25rem', fontWeight: '600', marginBottom: '8px' } }, 'No results found'),
509
- h('div', { style: { fontSize: '0.875rem', color: 'rgba(0,0,0,0.6)' } }, p.emptyState.message != null
510
- ? String(p.emptyState.message)
511
- : p.emptyState.hasActiveFilters
512
- ? [
513
- 'No items match your current filters. Try adjusting your search or ',
514
- h(VBtn, {
515
- variant: 'text',
516
- size: 'small',
517
- onClick: p.emptyState.onClearAll,
518
- }, () => 'clear all filters'),
519
- ' to see all items.',
520
- ]
521
- : 'There are no items available at this time.'),
522
- ]),
523
- ] : []),
524
- ]),
525
- ]),
526
- ]),
527
- ]),
528
- // Context menu (teleported to body)
529
- ...(menuPosition ? [
530
- h(Teleport, { to: 'body' }, h(GridContextMenu, {
531
- x: menuPosition.x,
532
- y: menuPosition.y,
533
- hasSelection: hasCellSelection,
534
- canUndo,
535
- canRedo,
536
- onUndo: onUndo ?? NOOP,
537
- onRedo: onRedo ?? NOOP,
538
- onCopy: handleCopy,
539
- onCut: handleCut,
540
- onPaste: () => { void handlePaste(); },
541
- onSelectAll: handleSelectAllCells,
542
- onClose: closeContextMenu,
543
- })),
544
- ] : []),
545
- // Column header menu
546
- h(ColumnHeaderMenu, {
547
- isOpen: headerMenu.isOpen,
548
- anchorElement: headerMenu.anchorElement,
549
- onClose: headerMenu.close,
550
- onPinLeft: headerMenu.handlePinLeft,
551
- onPinRight: headerMenu.handlePinRight,
552
- onUnpin: headerMenu.handleUnpin,
553
- onSortAsc: headerMenu.handleSortAsc,
554
- onSortDesc: headerMenu.handleSortDesc,
555
- onClearSort: headerMenu.handleClearSort,
556
- onAutosizeThis: headerMenu.handleAutosizeThis,
557
- onAutosizeAll: headerMenu.handleAutosizeAll,
558
- canPinLeft: headerMenu.canPinLeft,
559
- canPinRight: headerMenu.canPinRight,
560
- canUnpin: headerMenu.canUnpin,
561
- currentSort: headerMenu.currentSort,
562
- isSortable: headerMenu.isSortable,
563
- isResizable: headerMenu.isResizable,
564
- }),
565
- // Status bar
566
- ...(statusBarConfig ? [
567
- h(StatusBar, {
568
- totalCount: statusBarConfig.totalCount,
569
- filteredCount: statusBarConfig.filteredCount,
570
- selectedCount: statusBarConfig.selectedCount ?? selectedRowIds.size,
571
- selectedCellCount: selectionRange
572
- ? (Math.abs(selectionRange.endRow - selectionRange.startRow) + 1) * (Math.abs(selectionRange.endCol - selectionRange.startCol) + 1)
573
- : undefined,
574
- aggregation: statusBarConfig.aggregation,
575
- suppressRowCount: statusBarConfig.suppressRowCount,
576
- }),
577
- ] : []),
578
- // Loading overlay
579
- ...(isLoading ? [
580
- h('div', {
581
- style: {
582
- position: 'absolute',
583
- inset: '0',
584
- zIndex: '2',
585
- display: 'flex',
586
- alignItems: 'center',
587
- justifyContent: 'center',
588
- backgroundColor: 'rgba(255,255,255,0.7)',
589
- },
590
- }, h('div', {
591
- style: {
592
- display: 'flex',
593
- flexDirection: 'column',
594
- alignItems: 'center',
595
- gap: '8px',
596
- padding: '16px',
597
- backgroundColor: '#fff',
598
- border: '1px solid rgba(0,0,0,0.12)',
599
- borderRadius: '4px',
600
- },
601
- }, [
602
- h(VProgressCircular, { size: 24, indeterminate: true }),
603
- h('span', { style: { fontSize: '0.875rem', color: 'rgba(0,0,0,0.6)' } }, loadingMessage),
604
- ])),
605
- ] : []),
606
- ]);
607
- };
608
- },
10
+ export const DataGridTable = createDataGridTable({
11
+ renderCheckbox: ({ modelValue, indeterminate, ariaLabel, onChange }) => h(VCheckbox, {
12
+ modelValue,
13
+ indeterminate,
14
+ hideDetails: true,
15
+ density: 'compact',
16
+ 'aria-label': ariaLabel,
17
+ 'onUpdate:modelValue': (c) => onChange(!!c),
18
+ }),
19
+ renderSpinner: (message) => h('div', { class: 'ogrid-loading-inner' }, [
20
+ h(VProgressCircular, { size: 24, indeterminate: true }),
21
+ h('span', { class: 'ogrid-loading-message' }, message),
22
+ ]),
23
+ ColumnHeaderFilter,
24
+ ColumnHeaderMenu,
25
+ InlineCellEditor,
26
+ GridContextMenu,
27
+ renderEmptyState: (emptyState) => renderEmptyState({ emptyState }),
609
28
  });
@@ -0,0 +1,22 @@
1
+ import { h } from 'vue';
2
+ import { VBtn } from 'vuetify/components';
3
+ export function renderEmptyState({ emptyState }) {
4
+ return h('div', { class: 'ogrid-empty-state' }, emptyState.render
5
+ ? [emptyState.render()]
6
+ : [
7
+ h('div', { class: 'ogrid-empty-state-title' }, 'No results found'),
8
+ h('div', { class: 'ogrid-empty-state-message' }, emptyState.message != null
9
+ ? String(emptyState.message)
10
+ : emptyState.hasActiveFilters
11
+ ? [
12
+ 'No items match your current filters. Try adjusting your search or ',
13
+ h(VBtn, {
14
+ variant: 'text',
15
+ size: 'small',
16
+ onClick: emptyState.onClearAll,
17
+ }, () => 'clear all filters'),
18
+ ' to see all items.',
19
+ ]
20
+ : 'There are no items available at this time.'),
21
+ ]);
22
+ }
@@ -284,7 +284,7 @@ export const OGrid = defineComponent({
284
284
  style: {
285
285
  display: 'flex',
286
286
  flexDirection: 'column',
287
- border: '1px solid rgba(0,0,0,0.12)',
287
+ border: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))',
288
288
  borderRadius: '4px',
289
289
  overflow: 'hidden',
290
290
  },
@@ -297,7 +297,7 @@ export const OGrid = defineComponent({
297
297
  alignItems: 'center',
298
298
  justifyContent: 'space-between',
299
299
  padding: '8px 12px',
300
- borderBottom: '1px solid rgba(0,0,0,0.12)',
300
+ borderBottom: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))',
301
301
  gap: '8px',
302
302
  },
303
303
  }, [
@@ -308,7 +308,7 @@ export const OGrid = defineComponent({
308
308
  // Below toolbar strip
309
309
  ...(layout.value.toolbarBelow ? [
310
310
  h('div', {
311
- style: { padding: '8px 12px', borderBottom: '1px solid rgba(0,0,0,0.12)' },
311
+ style: { padding: '8px 12px', borderBottom: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))' },
312
312
  }, [layout.value.toolbarBelow]),
313
313
  ] : []),
314
314
  // Main content area (sidebar + grid)
@@ -319,7 +319,7 @@ export const OGrid = defineComponent({
319
319
  display: 'flex',
320
320
  alignItems: 'center',
321
321
  padding: '8px 0',
322
- borderTop: '1px solid rgba(0,0,0,0.12)',
322
+ borderTop: '1px solid var(--ogrid-border, rgba(0,0,0,0.12))',
323
323
  },
324
324
  }, [paginationNode]),
325
325
  ]);
@@ -37,7 +37,7 @@ export const PaginationControls = defineComponent({
37
37
  }, [
38
38
  // Summary text
39
39
  h('span', {
40
- style: { fontSize: '0.875rem', color: 'rgba(0,0,0,0.6)' },
40
+ style: { fontSize: '0.875rem', color: 'var(--ogrid-fg-secondary, rgba(0,0,0,0.6))' },
41
41
  }, `Showing ${startItem} to ${endItem} of ${props.totalCount.toLocaleString()} ${label}`),
42
42
  // Page buttons
43
43
  h('div', { style: { display: 'flex', alignItems: 'center', gap: '4px' } }, [
@@ -68,7 +68,7 @@ export const PaginationControls = defineComponent({
68
68
  style: { minWidth: '32px' },
69
69
  onClick: () => props.onPageChange(1),
70
70
  }, () => '1'),
71
- h('span', { style: { margin: '0 4px', color: 'rgba(0,0,0,0.6)' }, 'aria-hidden': 'true' }, '\u2026'),
71
+ h('span', { style: { margin: '0 4px', color: 'var(--ogrid-fg-secondary, rgba(0,0,0,0.6))' }, 'aria-hidden': 'true' }, '\u2026'),
72
72
  ] : []),
73
73
  // Page numbers
74
74
  ...pageNumbers.map((pageNum) => h(VBtn, {
@@ -83,7 +83,7 @@ export const PaginationControls = defineComponent({
83
83
  }, () => String(pageNum))),
84
84
  // End ellipsis
85
85
  ...(showEndEllipsis ? [
86
- h('span', { style: { margin: '0 4px', color: 'rgba(0,0,0,0.6)' }, 'aria-hidden': 'true' }, '\u2026'),
86
+ h('span', { style: { margin: '0 4px', color: 'var(--ogrid-fg-secondary, rgba(0,0,0,0.6))' }, 'aria-hidden': 'true' }, '\u2026'),
87
87
  h(VBtn, {
88
88
  size: 'small',
89
89
  variant: 'outlined',
@@ -113,7 +113,7 @@ export const PaginationControls = defineComponent({
113
113
  ]),
114
114
  // Page size selector
115
115
  h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } }, [
116
- h('span', { style: { fontSize: '0.875rem', color: 'rgba(0,0,0,0.6)' } }, 'Rows'),
116
+ h('span', { style: { fontSize: '0.875rem', color: 'var(--ogrid-fg-secondary, rgba(0,0,0,0.6))' } }, 'Rows'),
117
117
  h(VSelect, {
118
118
  modelValue: props.pageSize,
119
119
  items: v.pageSizeOptions,
@@ -1,16 +1,14 @@
1
- import { type PropType, type VNode } from 'vue';
2
- import { type IOGridDataGridProps } from '@alaarab/ogrid-vue';
3
1
  import './DataGridTable.css';
4
2
  export declare const DataGridTable: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
5
3
  gridProps: {
6
- type: PropType<IOGridDataGridProps<unknown>>;
4
+ type: import("vue").PropType<import("@alaarab/ogrid-vue").IOGridDataGridProps<unknown>>;
7
5
  required: true;
8
6
  };
9
- }>, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
7
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
10
8
  [key: string]: any;
11
9
  }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
12
10
  gridProps: {
13
- type: PropType<IOGridDataGridProps<unknown>>;
11
+ type: import("vue").PropType<import("@alaarab/ogrid-vue").IOGridDataGridProps<unknown>>;
14
12
  required: true;
15
13
  };
16
14
  }>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,11 @@
1
+ import { type VNode } from 'vue';
2
+ interface EmptyStateProps {
3
+ emptyState: {
4
+ render?: () => unknown;
5
+ message?: string | null;
6
+ hasActiveFilters?: boolean;
7
+ onClearAll?: () => void;
8
+ };
9
+ }
10
+ export declare function renderEmptyState({ emptyState }: EmptyStateProps): VNode;
11
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-vue-vuetify",
3
- "version": "2.0.11",
3
+ "version": "2.0.13",
4
4
  "description": "OGrid Vuetify – Vuetify-based data grid with sorting, filtering, pagination, column chooser, and CSV export.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -37,7 +37,7 @@
37
37
  "node": ">=18"
38
38
  },
39
39
  "dependencies": {
40
- "@alaarab/ogrid-vue": "2.0.11"
40
+ "@alaarab/ogrid-vue": "2.0.13"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "vue": "^3.3.0",