@alaarab/ogrid-vue-vuetify 2.0.6 → 2.0.8

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.
@@ -5,7 +5,6 @@ import { TextFilterPopover } from './TextFilterPopover';
5
5
  import { MultiSelectFilterPopover } from './MultiSelectFilterPopover';
6
6
  import { PeopleFilterPopover } from './PeopleFilterPopover';
7
7
  // Vuetify component types don't align with h() overloads; cast to Component
8
- /* eslint-disable @typescript-eslint/no-explicit-any */
9
8
  const _VBtn = VBtn;
10
9
  const _VIcon = VIcon;
11
10
  const _VMenu = VMenu;
@@ -1,12 +1,10 @@
1
1
  import { defineComponent, h } from 'vue';
2
2
  import { VBtn, VTextField, VCheckbox, VProgressCircular, VDivider } from 'vuetify/components';
3
- /* eslint-disable @typescript-eslint/no-explicit-any */
4
3
  const _VBtn = VBtn;
5
4
  const _VTextField = VTextField;
6
5
  const _VCheckbox = VCheckbox;
7
6
  const _VProgressCircular = VProgressCircular;
8
7
  const _VDivider = VDivider;
9
- /* eslint-enable @typescript-eslint/no-explicit-any */
10
8
  export const MultiSelectFilterPopover = defineComponent({
11
9
  name: 'MultiSelectFilterPopover',
12
10
  props: {
@@ -1,6 +1,5 @@
1
1
  import { defineComponent, h } from 'vue';
2
2
  import { VBtn, VTextField, VProgressCircular, VAvatar, VIcon, VDivider } from 'vuetify/components';
3
- /* eslint-disable @typescript-eslint/no-explicit-any */
4
3
  const _VBtn = VBtn;
5
4
  const _VTextField = VTextField;
6
5
  const _VProgressCircular = VProgressCircular;
@@ -1,9 +1,7 @@
1
1
  import { defineComponent, h } from 'vue';
2
2
  import { VBtn, VTextField } from 'vuetify/components';
3
- /* eslint-disable @typescript-eslint/no-explicit-any */
4
3
  const _VBtn = VBtn;
5
4
  const _VTextField = VTextField;
6
- /* eslint-enable @typescript-eslint/no-explicit-any */
7
5
  export const TextFilterPopover = defineComponent({
8
6
  name: 'TextFilterPopover',
9
7
  props: {
@@ -0,0 +1,55 @@
1
+ import { defineComponent, h } from 'vue';
2
+ import { VMenu, VList, VListItem } from 'vuetify/components';
3
+ import { COLUMN_HEADER_MENU_ITEMS } from '@alaarab/ogrid-vue';
4
+ export const ColumnHeaderMenu = defineComponent({
5
+ name: 'ColumnHeaderMenu',
6
+ props: {
7
+ isOpen: { type: Boolean, required: true },
8
+ anchorElement: { type: Object, default: null },
9
+ onClose: { type: Function, required: true },
10
+ onPinLeft: { type: Function, required: true },
11
+ onPinRight: { type: Function, required: true },
12
+ onUnpin: { type: Function, required: true },
13
+ canPinLeft: { type: Boolean, required: true },
14
+ canPinRight: { type: Boolean, required: true },
15
+ canUnpin: { type: Boolean, required: true },
16
+ },
17
+ setup(props) {
18
+ const handleOpenChange = (open) => {
19
+ if (!open) {
20
+ props.onClose();
21
+ }
22
+ };
23
+ const items = COLUMN_HEADER_MENU_ITEMS;
24
+ const getDisabled = (index) => {
25
+ if (index === 0)
26
+ return !props.canPinLeft;
27
+ if (index === 1)
28
+ return !props.canPinRight;
29
+ if (index === 2)
30
+ return !props.canUnpin;
31
+ return false;
32
+ };
33
+ const getHandler = (index) => {
34
+ if (index === 0)
35
+ return props.onPinLeft;
36
+ if (index === 1)
37
+ return props.onPinRight;
38
+ if (index === 2)
39
+ return props.onUnpin;
40
+ return () => { };
41
+ };
42
+ return () => h(VMenu, {
43
+ modelValue: props.isOpen,
44
+ 'onUpdate:modelValue': handleOpenChange,
45
+ activator: props.anchorElement || undefined,
46
+ location: 'bottom start',
47
+ }, {
48
+ default: () => h(VList, { density: 'compact', 'aria-label': 'Column options' }, () => items.map((item, index) => h(VListItem, {
49
+ key: item.id,
50
+ disabled: getDisabled(index),
51
+ onClick: () => { getHandler(index)(); },
52
+ }, () => item.label))),
53
+ });
54
+ },
55
+ });
@@ -1,9 +1,9 @@
1
1
  import { defineComponent, computed, h, Teleport } from 'vue';
2
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, } from '@alaarab/ogrid-vue';
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';
4
4
  import { ColumnHeaderFilter } from '../ColumnHeaderFilter';
5
+ import { ColumnHeaderMenu } from '../ColumnHeaderMenu/ColumnHeaderMenu';
5
6
  import { InlineCellEditor } from './InlineCellEditor';
6
- import { StatusBar } from './StatusBar';
7
7
  import { GridContextMenu } from './GridContextMenu';
8
8
  const NOOP = () => { };
9
9
  export const DataGridTable = defineComponent({
@@ -14,7 +14,7 @@ export const DataGridTable = defineComponent({
14
14
  },
15
15
  setup(props) {
16
16
  const propsRef = computed(() => props.gridProps);
17
- const { wrapperRef, tableContainerRef, tableRef, lastMouseShift, state, columnReorder: { isDragging: isReorderDragging, dropIndicatorX, handleHeaderMouseDown: handleReorderMouseDown }, virtualScroll: { containerRef: vsContainerRef, visibleRange, totalHeight, scrollToRow }, virtualScrollEnabled, columnResize: { handleResizeStart, getColumnWidth }, } = useDataGridTableSetup({ props: propsRef });
17
+ 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 });
18
18
  return () => {
19
19
  /**
20
20
  * Vue Performance: Fine-grained reactivity vs React memoization
@@ -40,15 +40,17 @@ export const DataGridTable = defineComponent({
40
40
  const interaction = state.interaction.value;
41
41
  const ctxMenu = state.contextMenu.value;
42
42
  const viewModels = state.viewModels.value;
43
- const { visibleCols, hasCheckboxCol, hasRowNumbersCol, colOffset, containerWidth, minTableWidth, desiredTableWidth, } = layout;
43
+ const pinning = state.pinning.value;
44
+ const { headerMenu } = pinning;
45
+ const { visibleCols, hasCheckboxCol, hasRowNumbersCol, colOffset: _colOffset, containerWidth, minTableWidth, desiredTableWidth, } = layout;
44
46
  const currentPage = props.gridProps.currentPage ?? 1;
45
47
  const pageSize = props.gridProps.pageSize ?? 25;
46
48
  const rowNumberOffset = hasRowNumbersCol ? (currentPage - 1) * pageSize : 0;
47
49
  const { selectedRowIds, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
48
- const { editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
49
- const { setActiveCell, handleCellMouseDown, handleSelectAllCells, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange, copyRange, canUndo, canRedo, onUndo, onRedo, isDragging, } = interaction;
50
+ const { editingCell: _editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
51
+ const { setActiveCell, handleCellMouseDown, handleSelectAllCells, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange: _cutRange, copyRange: _copyRange, canUndo, canRedo, onUndo, onRedo, isDragging: _isDragging, } = interaction;
50
52
  const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
51
- const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError } = viewModels;
53
+ const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError: _onCellError } = viewModels;
52
54
  const items = p.items;
53
55
  const getRowId = p.getRowId;
54
56
  const layoutMode = p.layoutMode ?? 'fill';
@@ -77,7 +79,7 @@ export const DataGridTable = defineComponent({
77
79
  // Render a cell's content
78
80
  const renderCellContent = (item, col, rowIndex, colIdx) => {
79
81
  const descriptor = getCellRenderDescriptor(item, col, rowIndex, colIdx, cellDescriptorInput);
80
- const rowId = getRowId(item);
82
+ const _rowId = getRowId(item);
81
83
  if (descriptor.mode === 'editing-inline') {
82
84
  const editorProps = buildInlineEditorProps(item, col, descriptor, editCallbacks);
83
85
  return h(InlineCellEditor, {
@@ -112,7 +114,7 @@ export const DataGridTable = defineComponent({
112
114
  const cellStyle = resolveCellStyle(col, item);
113
115
  const interactionProps = getCellInteractionProps(descriptor, col.columnId, interactionHandlers);
114
116
  // Compute cell CSS classes based on state
115
- const cellClasses = ['ogrid-cell'];
117
+ const _cellClasses = ['ogrid-cell'];
116
118
  const cellInlineStyle = {
117
119
  width: '100%',
118
120
  height: '100%',
@@ -361,7 +363,27 @@ export const DataGridTable = defineComponent({
361
363
  },
362
364
  onMousedown: (e) => handleReorderMouseDown(col.columnId, e),
363
365
  }, [
364
- h(ColumnHeaderFilter, getHeaderFilterConfig(col, headerFilterInput)),
366
+ h('div', { style: { display: 'flex', alignItems: 'center', width: '100%' } }, [
367
+ h(ColumnHeaderFilter, getHeaderFilterConfig(col, headerFilterInput)),
368
+ h('button', {
369
+ onClick: (e) => {
370
+ e.stopPropagation();
371
+ headerMenu.open(col.columnId, e.currentTarget);
372
+ },
373
+ 'aria-label': 'Column options',
374
+ title: 'Column options',
375
+ style: {
376
+ background: 'none',
377
+ border: 'none',
378
+ cursor: 'pointer',
379
+ padding: '2px 4px',
380
+ fontSize: '14px',
381
+ color: 'rgba(0,0,0,0.5)',
382
+ lineHeight: '1',
383
+ flexShrink: '0',
384
+ },
385
+ }, '\u22EE'),
386
+ ]),
365
387
  h('div', {
366
388
  onMousedown: (e) => { e.stopPropagation(); handleResizeStart(e, col); },
367
389
  style: {
@@ -509,6 +531,18 @@ export const DataGridTable = defineComponent({
509
531
  onClose: closeContextMenu,
510
532
  })),
511
533
  ] : []),
534
+ // Column header menu
535
+ h(ColumnHeaderMenu, {
536
+ isOpen: headerMenu.isOpen,
537
+ anchorElement: headerMenu.anchorElement,
538
+ onClose: headerMenu.close,
539
+ onPinLeft: headerMenu.handlePinLeft,
540
+ onPinRight: headerMenu.handlePinRight,
541
+ onUnpin: headerMenu.handleUnpin,
542
+ canPinLeft: headerMenu.canPinLeft,
543
+ canPinRight: headerMenu.canPinRight,
544
+ canUnpin: headerMenu.canUnpin,
545
+ }),
512
546
  // Status bar
513
547
  ...(statusBarConfig ? [
514
548
  h(StatusBar, {
@@ -3,6 +3,225 @@ import { useOGrid, } from '@alaarab/ogrid-vue';
3
3
  import { DataGridTable } from '../DataGridTable/DataGridTable';
4
4
  import { ColumnChooser } from '../ColumnChooser/ColumnChooser';
5
5
  import { PaginationControls } from '../PaginationControls/PaginationControls';
6
+ // --- SideBar constants and styles ---
7
+ const PANEL_WIDTH = 240;
8
+ const TAB_WIDTH = 36;
9
+ const PANEL_LABELS = {
10
+ columns: 'Columns',
11
+ filters: 'Filters',
12
+ };
13
+ const PANEL_ICONS = {
14
+ columns: '\u2261', // hamburger icon
15
+ filters: '\u2A65', // filter icon
16
+ };
17
+ /** Render the SideBar inline (tab strip + panel content). */
18
+ function renderSideBar(sb) {
19
+ const isOpen = sb.activePanel !== null;
20
+ const position = sb.position ?? 'right';
21
+ const tabStripStyle = {
22
+ display: 'flex',
23
+ flexDirection: 'column',
24
+ width: `${TAB_WIDTH}px`,
25
+ background: 'var(--ogrid-header-bg, #f5f5f5)',
26
+ ...(position === 'right'
27
+ ? { borderLeft: '1px solid var(--ogrid-border, #e0e0e0)' }
28
+ : { borderRight: '1px solid var(--ogrid-border, #e0e0e0)' }),
29
+ };
30
+ const tabStrip = h('div', { style: tabStripStyle, role: 'tablist', 'aria-label': 'Side bar tabs' }, sb.panels.map((panel) => h('button', {
31
+ key: panel,
32
+ role: 'tab',
33
+ 'aria-selected': sb.activePanel === panel,
34
+ 'aria-label': PANEL_LABELS[panel],
35
+ title: PANEL_LABELS[panel],
36
+ onClick: () => sb.onPanelChange(sb.activePanel === panel ? null : panel),
37
+ style: {
38
+ width: `${TAB_WIDTH}px`,
39
+ height: `${TAB_WIDTH}px`,
40
+ border: 'none',
41
+ cursor: 'pointer',
42
+ color: 'var(--ogrid-fg, #242424)',
43
+ fontSize: '14px',
44
+ display: 'flex',
45
+ alignItems: 'center',
46
+ justifyContent: 'center',
47
+ background: sb.activePanel === panel ? 'var(--ogrid-bg, #fff)' : 'transparent',
48
+ fontWeight: sb.activePanel === panel ? 'bold' : 'normal',
49
+ },
50
+ }, PANEL_ICONS[panel])));
51
+ let panelContent = null;
52
+ if (isOpen && sb.activePanel) {
53
+ const panelContainerStyle = {
54
+ width: `${PANEL_WIDTH}px`,
55
+ display: 'flex',
56
+ flexDirection: 'column',
57
+ overflow: 'hidden',
58
+ background: 'var(--ogrid-bg, #fff)',
59
+ color: 'var(--ogrid-fg, #242424)',
60
+ ...(position === 'right'
61
+ ? { borderLeft: '1px solid var(--ogrid-border, #e0e0e0)' }
62
+ : { borderRight: '1px solid var(--ogrid-border, #e0e0e0)' }),
63
+ };
64
+ const panelBodyChildren = [];
65
+ if (sb.activePanel === 'columns') {
66
+ const allVisible = sb.columns.every((c) => sb.visibleColumns.has(c.columnId));
67
+ // Select All / Clear All buttons
68
+ panelBodyChildren.push(h('div', { style: { display: 'flex', gap: '8px', marginBottom: '8px' } }, [
69
+ h('button', {
70
+ disabled: allVisible,
71
+ onClick: () => {
72
+ const next = new Set(sb.visibleColumns);
73
+ sb.columns.forEach((c) => next.add(c.columnId));
74
+ sb.onSetVisibleColumns(next);
75
+ },
76
+ 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' },
77
+ }, 'Select All'),
78
+ h('button', {
79
+ onClick: () => {
80
+ const next = new Set();
81
+ sb.columns.forEach((c) => {
82
+ if (c.required && sb.visibleColumns.has(c.columnId))
83
+ next.add(c.columnId);
84
+ });
85
+ sb.onSetVisibleColumns(next);
86
+ },
87
+ 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' },
88
+ }, 'Clear All'),
89
+ ]));
90
+ // Column checkboxes
91
+ sb.columns.forEach((col) => {
92
+ panelBodyChildren.push(h('label', { key: col.columnId, style: { display: 'flex', alignItems: 'center', gap: '6px', padding: '2px 0', cursor: 'pointer' } }, [
93
+ h('input', {
94
+ type: 'checkbox',
95
+ checked: sb.visibleColumns.has(col.columnId),
96
+ disabled: col.required,
97
+ onChange: (e) => sb.onVisibilityChange(col.columnId, e.target.checked),
98
+ }),
99
+ h('span', null, col.name),
100
+ ]));
101
+ });
102
+ }
103
+ if (sb.activePanel === 'filters') {
104
+ if (sb.filterableColumns.length === 0) {
105
+ panelBodyChildren.push(h('div', { style: { color: 'var(--ogrid-muted, #999)', fontStyle: 'italic' } }, 'No filterable columns'));
106
+ }
107
+ else {
108
+ sb.filterableColumns.forEach((col) => {
109
+ const filterKey = col.filterField;
110
+ const groupChildren = [
111
+ h('div', { style: { fontWeight: '500', marginBottom: '4px', fontSize: '13px' } }, col.name),
112
+ ];
113
+ if (col.filterType === 'text') {
114
+ const currentVal = sb.filters[filterKey]?.type === 'text' ? sb.filters[filterKey].value : '';
115
+ groupChildren.push(h('input', {
116
+ type: 'text',
117
+ value: currentVal,
118
+ onInput: (e) => {
119
+ const val = e.target.value;
120
+ sb.onFilterChange(filterKey, val ? { type: 'text', value: val } : undefined);
121
+ },
122
+ placeholder: `Filter ${col.name}...`,
123
+ 'aria-label': `Filter ${col.name}`,
124
+ 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' },
125
+ }));
126
+ }
127
+ if (col.filterType === 'multiSelect') {
128
+ const options = sb.filterOptions[filterKey] ?? [];
129
+ const msChildren = options.map((opt) => {
130
+ const selected = sb.filters[filterKey]?.type === 'multiSelect' ? sb.filters[filterKey].value.includes(opt) : false;
131
+ return h('label', { key: opt, style: { display: 'flex', alignItems: 'center', gap: '4px', padding: '1px 0', cursor: 'pointer', fontSize: '13px' } }, [
132
+ h('input', {
133
+ type: 'checkbox',
134
+ checked: selected,
135
+ onChange: (e) => {
136
+ const current = sb.filters[filterKey]?.type === 'multiSelect' ? sb.filters[filterKey].value : [];
137
+ const next = e.target.checked
138
+ ? [...current, opt]
139
+ : current.filter((v) => v !== opt);
140
+ sb.onFilterChange(filterKey, next.length > 0 ? { type: 'multiSelect', value: next } : undefined);
141
+ },
142
+ }),
143
+ h('span', null, opt),
144
+ ]);
145
+ });
146
+ groupChildren.push(h('div', { style: { maxHeight: '120px', overflowY: 'auto' }, role: 'group', 'aria-label': `${col.name} options` }, msChildren));
147
+ }
148
+ if (col.filterType === 'date') {
149
+ const existingValue = sb.filters[filterKey]?.type === 'date' ? sb.filters[filterKey].value : { from: undefined, to: undefined };
150
+ groupChildren.push(h('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } }, [
151
+ h('label', { style: { display: 'flex', alignItems: 'center', gap: '4px', fontSize: '12px' } }, [
152
+ 'From:',
153
+ h('input', {
154
+ type: 'date',
155
+ value: existingValue.from ?? '',
156
+ onInput: (e) => {
157
+ const from = e.target.value || undefined;
158
+ const to = existingValue.to;
159
+ sb.onFilterChange(filterKey, from || to ? { type: 'date', value: { from, to } } : undefined);
160
+ },
161
+ 'aria-label': `${col.name} from date`,
162
+ 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' },
163
+ }),
164
+ ]),
165
+ h('label', { style: { display: 'flex', alignItems: 'center', gap: '4px', fontSize: '12px' } }, [
166
+ 'To:',
167
+ h('input', {
168
+ type: 'date',
169
+ value: existingValue.to ?? '',
170
+ onInput: (e) => {
171
+ const to = e.target.value || undefined;
172
+ const from = existingValue.from;
173
+ sb.onFilterChange(filterKey, from || to ? { type: 'date', value: { from, to } } : undefined);
174
+ },
175
+ 'aria-label': `${col.name} to date`,
176
+ 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' },
177
+ }),
178
+ ]),
179
+ ]));
180
+ }
181
+ panelBodyChildren.push(h('div', { key: col.columnId, style: { marginBottom: '12px' } }, groupChildren));
182
+ });
183
+ }
184
+ }
185
+ panelContent = h('div', { role: 'tabpanel', 'aria-label': PANEL_LABELS[sb.activePanel], style: panelContainerStyle }, [
186
+ // Panel header
187
+ h('div', {
188
+ style: {
189
+ display: 'flex',
190
+ justifyContent: 'space-between',
191
+ alignItems: 'center',
192
+ padding: '8px 12px',
193
+ borderBottom: '1px solid var(--ogrid-border, #e0e0e0)',
194
+ fontWeight: '600',
195
+ },
196
+ }, [
197
+ h('span', null, PANEL_LABELS[sb.activePanel]),
198
+ h('button', {
199
+ onClick: () => sb.onPanelChange(null),
200
+ style: { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: '16px', color: 'var(--ogrid-fg, #242424)' },
201
+ 'aria-label': 'Close panel',
202
+ }, '\u00D7'),
203
+ ]),
204
+ // Panel body
205
+ h('div', { style: { flex: '1', overflowY: 'auto', padding: '8px 12px' } }, panelBodyChildren),
206
+ ]);
207
+ }
208
+ const children = [];
209
+ if (position === 'left') {
210
+ children.push(tabStrip);
211
+ if (panelContent)
212
+ children.push(panelContent);
213
+ }
214
+ else {
215
+ if (panelContent)
216
+ children.push(panelContent);
217
+ children.push(tabStrip);
218
+ }
219
+ return h('div', {
220
+ style: { display: 'flex', flexDirection: 'row', flexShrink: '0' },
221
+ role: 'complementary',
222
+ 'aria-label': 'Side bar',
223
+ }, children);
224
+ }
6
225
  export const OGrid = defineComponent({
7
226
  name: 'OGrid',
8
227
  props: {
@@ -15,6 +234,8 @@ export const OGrid = defineComponent({
15
234
  expose({ api: api.value });
16
235
  return () => {
17
236
  const sideBar = layout.value.sideBarProps;
237
+ const hasSideBar = sideBar != null;
238
+ const sideBarPosition = sideBar?.position ?? 'right';
18
239
  // Toolbar
19
240
  const toolbarChildren = [];
20
241
  if (layout.value.toolbar) {
@@ -41,6 +262,23 @@ export const OGrid = defineComponent({
41
262
  pageSizeOptions: pagination.value.pageSizeOptions,
42
263
  entityLabelPlural: pagination.value.entityLabelPlural,
43
264
  });
265
+ // Grid content area
266
+ const gridChild = h('div', {
267
+ style: { flex: '1', minWidth: '0', minHeight: '0', display: 'flex', flexDirection: 'column' },
268
+ }, [
269
+ h(DataGridTable, {
270
+ gridProps: dataGridProps.value,
271
+ }),
272
+ ]);
273
+ // Main content area (sidebar + grid)
274
+ const mainAreaChildren = [];
275
+ if (hasSideBar && sideBarPosition === 'left') {
276
+ mainAreaChildren.push(renderSideBar(sideBar));
277
+ }
278
+ mainAreaChildren.push(gridChild);
279
+ if (hasSideBar && sideBarPosition !== 'left') {
280
+ mainAreaChildren.push(renderSideBar(sideBar));
281
+ }
44
282
  return h('div', {
45
283
  class: layout.value.className,
46
284
  style: {
@@ -74,12 +312,7 @@ export const OGrid = defineComponent({
74
312
  }, [layout.value.toolbarBelow]),
75
313
  ] : []),
76
314
  // Main content area (sidebar + grid)
77
- h('div', { style: { display: 'flex', flex: '1', minHeight: '0' } }, [
78
- // DataGridTable
79
- h(DataGridTable, {
80
- gridProps: dataGridProps.value,
81
- }),
82
- ]),
315
+ h('div', { style: { display: 'flex', flex: '1', minHeight: '0' } }, mainAreaChildren),
83
316
  // Footer strip (pagination)
84
317
  h('div', {
85
318
  style: {
package/dist/esm/index.js CHANGED
@@ -6,3 +6,8 @@ export { DataGridTable } from './DataGridTable/DataGridTable';
6
6
  export { ColumnHeaderFilter } from './ColumnHeaderFilter/ColumnHeaderFilter';
7
7
  export { ColumnChooser } from './ColumnChooser/ColumnChooser';
8
8
  export { PaginationControls } from './PaginationControls/PaginationControls';
9
+ export { ColumnHeaderMenu } from './ColumnHeaderMenu/ColumnHeaderMenu';
10
+ // Re-export shared components from base
11
+ export { StatusBar, MarchingAntsOverlay } from '@alaarab/ogrid-vue';
12
+ export { GridContextMenu } from './DataGridTable/GridContextMenu';
13
+ export { InlineCellEditor } from './DataGridTable/InlineCellEditor';
@@ -0,0 +1,91 @@
1
+ import { type PropType } from 'vue';
2
+ export interface ColumnHeaderMenuProps {
3
+ isOpen: boolean;
4
+ anchorElement: HTMLElement | null;
5
+ onClose: () => void;
6
+ onPinLeft: () => void;
7
+ onPinRight: () => void;
8
+ onUnpin: () => void;
9
+ canPinLeft: boolean;
10
+ canPinRight: boolean;
11
+ canUnpin: boolean;
12
+ }
13
+ export declare const ColumnHeaderMenu: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
14
+ isOpen: {
15
+ type: BooleanConstructor;
16
+ required: true;
17
+ };
18
+ anchorElement: {
19
+ type: PropType<HTMLElement | null>;
20
+ default: null;
21
+ };
22
+ onClose: {
23
+ type: PropType<() => void>;
24
+ required: true;
25
+ };
26
+ onPinLeft: {
27
+ type: PropType<() => void>;
28
+ required: true;
29
+ };
30
+ onPinRight: {
31
+ type: PropType<() => void>;
32
+ required: true;
33
+ };
34
+ onUnpin: {
35
+ type: PropType<() => void>;
36
+ required: true;
37
+ };
38
+ canPinLeft: {
39
+ type: BooleanConstructor;
40
+ required: true;
41
+ };
42
+ canPinRight: {
43
+ type: BooleanConstructor;
44
+ required: true;
45
+ };
46
+ canUnpin: {
47
+ type: BooleanConstructor;
48
+ required: true;
49
+ };
50
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
51
+ [key: string]: any;
52
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
53
+ isOpen: {
54
+ type: BooleanConstructor;
55
+ required: true;
56
+ };
57
+ anchorElement: {
58
+ type: PropType<HTMLElement | null>;
59
+ default: null;
60
+ };
61
+ onClose: {
62
+ type: PropType<() => void>;
63
+ required: true;
64
+ };
65
+ onPinLeft: {
66
+ type: PropType<() => void>;
67
+ required: true;
68
+ };
69
+ onPinRight: {
70
+ type: PropType<() => void>;
71
+ required: true;
72
+ };
73
+ onUnpin: {
74
+ type: PropType<() => void>;
75
+ required: true;
76
+ };
77
+ canPinLeft: {
78
+ type: BooleanConstructor;
79
+ required: true;
80
+ };
81
+ canPinRight: {
82
+ type: BooleanConstructor;
83
+ required: true;
84
+ };
85
+ canUnpin: {
86
+ type: BooleanConstructor;
87
+ required: true;
88
+ };
89
+ }>> & Readonly<{}>, {
90
+ anchorElement: HTMLElement | null;
91
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -1,11 +1,11 @@
1
- import { type PropType } from 'vue';
1
+ import { type PropType, type VNode } from 'vue';
2
2
  import { type IOGridProps } from '@alaarab/ogrid-vue';
3
3
  export declare const OGrid: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
4
4
  gridProps: {
5
5
  type: PropType<IOGridProps<unknown>>;
6
6
  required: true;
7
7
  };
8
- }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
8
+ }>, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
9
9
  [key: string]: any;
10
10
  }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
11
11
  gridProps: {
@@ -7,3 +7,8 @@ export { ColumnChooser } from './ColumnChooser/ColumnChooser';
7
7
  export type { IColumnChooserProps } from './ColumnChooser/ColumnChooser';
8
8
  export { PaginationControls } from './PaginationControls/PaginationControls';
9
9
  export type { IPaginationControlsProps } from './PaginationControls/PaginationControls';
10
+ export { ColumnHeaderMenu } from './ColumnHeaderMenu/ColumnHeaderMenu';
11
+ export type { ColumnHeaderMenuProps } from './ColumnHeaderMenu/ColumnHeaderMenu';
12
+ export { StatusBar, MarchingAntsOverlay } from '@alaarab/ogrid-vue';
13
+ export { GridContextMenu } from './DataGridTable/GridContextMenu';
14
+ export { InlineCellEditor } from './DataGridTable/InlineCellEditor';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-vue-vuetify",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
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",
@@ -14,28 +14,47 @@
14
14
  },
15
15
  "scripts": {
16
16
  "build": "rimraf dist && tsc -p tsconfig.build.json",
17
- "test": "jest --passWithNoTests"
17
+ "test": "jest --passWithNoTests",
18
+ "storybook": "storybook dev -p 6011 --no-open",
19
+ "build-storybook": "storybook build"
18
20
  },
19
- "keywords": ["ogrid", "vue", "vuetify", "datatable", "typescript", "grid"],
21
+ "keywords": [
22
+ "ogrid",
23
+ "vue",
24
+ "vuetify",
25
+ "datatable",
26
+ "typescript",
27
+ "grid"
28
+ ],
20
29
  "author": "Ala Arab",
21
30
  "license": "MIT",
22
- "files": ["dist", "README.md", "LICENSE"],
23
- "engines": { "node": ">=18" },
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
24
39
  "dependencies": {
25
- "@alaarab/ogrid-vue": "2.0.6"
40
+ "@alaarab/ogrid-vue": "2.0.7"
26
41
  },
27
42
  "peerDependencies": {
28
43
  "vue": "^3.3.0",
29
44
  "vuetify": "^3.0.0"
30
45
  },
31
46
  "devDependencies": {
32
- "vue": "^3.5.28",
33
- "vuetify": "^3.11.8",
47
+ "@storybook/vue3-vite": "^10.2.8",
34
48
  "@vitejs/plugin-vue": "^6.0.4",
49
+ "storybook": "^10.2.8",
50
+ "typescript": "^5.9.3",
35
51
  "vite": "^7.3.1",
36
52
  "vite-plugin-dts": "^4.5.4",
37
- "typescript": "^5.9.3"
53
+ "vue": "^3.5.28",
54
+ "vuetify": "^3.11.8"
38
55
  },
39
56
  "sideEffects": false,
40
- "publishConfig": { "access": "public" }
57
+ "publishConfig": {
58
+ "access": "public"
59
+ }
41
60
  }
@@ -1,45 +0,0 @@
1
- import { defineComponent, h } from 'vue';
2
- import { getStatusBarParts } from '@alaarab/ogrid-vue';
3
- export const StatusBar = defineComponent({
4
- name: 'StatusBar',
5
- props: {
6
- totalCount: { type: Number, required: true },
7
- filteredCount: { type: Number, default: undefined },
8
- selectedCount: { type: Number, default: undefined },
9
- selectedCellCount: { type: Number, default: undefined },
10
- aggregation: { type: Object, default: undefined },
11
- suppressRowCount: { type: Boolean, default: false },
12
- },
13
- setup(props) {
14
- return () => {
15
- const parts = getStatusBarParts(props);
16
- return h('div', {
17
- role: 'status',
18
- 'aria-live': 'polite',
19
- style: {
20
- marginTop: 'auto',
21
- padding: '6px 12px',
22
- borderTop: '1px solid rgba(0,0,0,0.12)',
23
- backgroundColor: 'rgba(0,0,0,0.04)',
24
- display: 'flex',
25
- alignItems: 'center',
26
- gap: '16px',
27
- fontSize: '0.875rem',
28
- },
29
- }, parts.map((p, i) => h('span', {
30
- key: p.key,
31
- style: {
32
- display: 'inline-flex',
33
- alignItems: 'center',
34
- gap: '4px',
35
- ...(i < parts.length - 1
36
- ? { marginRight: '16px', borderRight: '1px solid rgba(0,0,0,0.12)', paddingRight: '16px' }
37
- : {}),
38
- },
39
- }, [
40
- h('span', { style: { color: 'rgba(0,0,0,0.6)' } }, p.label),
41
- h('span', { style: { fontWeight: '600' } }, p.value.toLocaleString()),
42
- ])));
43
- };
44
- },
45
- });
@@ -1,80 +0,0 @@
1
- import { type PropType } from 'vue';
2
- export interface StatusBarProps {
3
- totalCount: number;
4
- filteredCount?: number;
5
- selectedCount?: number;
6
- selectedCellCount?: number;
7
- aggregation?: {
8
- sum: number;
9
- avg: number;
10
- min: number;
11
- max: number;
12
- count: number;
13
- } | null;
14
- suppressRowCount?: boolean;
15
- }
16
- export declare const StatusBar: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
17
- totalCount: {
18
- type: NumberConstructor;
19
- required: true;
20
- };
21
- filteredCount: {
22
- type: NumberConstructor;
23
- default: undefined;
24
- };
25
- selectedCount: {
26
- type: NumberConstructor;
27
- default: undefined;
28
- };
29
- selectedCellCount: {
30
- type: NumberConstructor;
31
- default: undefined;
32
- };
33
- aggregation: {
34
- type: PropType<StatusBarProps["aggregation"]>;
35
- default: undefined;
36
- };
37
- suppressRowCount: {
38
- type: BooleanConstructor;
39
- default: boolean;
40
- };
41
- }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
42
- [key: string]: any;
43
- }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
44
- totalCount: {
45
- type: NumberConstructor;
46
- required: true;
47
- };
48
- filteredCount: {
49
- type: NumberConstructor;
50
- default: undefined;
51
- };
52
- selectedCount: {
53
- type: NumberConstructor;
54
- default: undefined;
55
- };
56
- selectedCellCount: {
57
- type: NumberConstructor;
58
- default: undefined;
59
- };
60
- aggregation: {
61
- type: PropType<StatusBarProps["aggregation"]>;
62
- default: undefined;
63
- };
64
- suppressRowCount: {
65
- type: BooleanConstructor;
66
- default: boolean;
67
- };
68
- }>> & Readonly<{}>, {
69
- aggregation: {
70
- sum: number;
71
- avg: number;
72
- min: number;
73
- max: number;
74
- count: number;
75
- } | null | undefined;
76
- filteredCount: number;
77
- selectedCount: number;
78
- selectedCellCount: number;
79
- suppressRowCount: boolean;
80
- }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;