@alaarab/ogrid-vue-radix 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # @alaarab/ogrid-vue-radix
2
+
3
+ Lightweight Vue 3 data grid with minimal dependencies. Built with Headless UI Vue for a clean, accessible interface.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @alaarab/ogrid-vue-radix
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```vue
14
+ <script setup lang="ts">
15
+ import { ref } from 'vue';
16
+ import { OGrid } from '@alaarab/ogrid-vue-radix';
17
+
18
+ const columns = ref([
19
+ { columnId: 'id', name: 'ID', type: 'numeric' },
20
+ { columnId: 'name', name: 'Name', type: 'text', editable: true },
21
+ { columnId: 'email', name: 'Email', type: 'text', editable: true },
22
+ { columnId: 'age', name: 'Age', type: 'numeric', editable: true }
23
+ ]);
24
+
25
+ const data = ref([
26
+ { id: 1, name: 'Alice', email: 'alice@example.com', age: 28 },
27
+ { id: 2, name: 'Bob', email: 'bob@example.com', age: 32 },
28
+ { id: 3, name: 'Charlie', email: 'charlie@example.com', age: 25 }
29
+ ]);
30
+ </script>
31
+
32
+ <template>
33
+ <OGrid
34
+ :columns="columns"
35
+ :data="data"
36
+ :getRowId="(row) => row.id"
37
+ :editable="true"
38
+ :cellSelection="true"
39
+ />
40
+ </template>
41
+ ```
42
+
43
+ ## Why OGrid Vue Radix?
44
+
45
+ **Lightweight alternative** to Vuetify and PrimeVue data grids:
46
+ - 📦 Minimal bundle size (~80KB gzipped)
47
+ - 🎨 Built with Headless UI Vue - no large component library required
48
+ - 🚀 Same powerful features as OGrid Vue Vuetify/PrimeVue
49
+ - 💅 Clean, modern design with CSS variables for easy customization
50
+
51
+ ## Features
52
+
53
+ - ✅ Sorting, filtering, and pagination
54
+ - ✅ Cell editing (inline text, select, checkbox, date)
55
+ - ✅ Cell and row selection
56
+ - ✅ Column resizing and reordering
57
+ - ✅ Column pinning (sticky left/right)
58
+ - ✅ Virtual scrolling for large datasets
59
+ - ✅ CSV export
60
+ - ✅ Keyboard navigation
61
+ - ✅ Context menu (copy, cut, paste)
62
+ - ✅ Undo/redo
63
+ - ✅ Server-side data sources
64
+ - ✅ Column groups (multi-level headers)
65
+ - ✅ Sidebar panels (columns, filters)
66
+ - ✅ TypeScript support
67
+
68
+ ## Documentation
69
+
70
+ Full documentation: **[ogrid.dev](https://ogrid.dev)**
71
+ - [Getting Started](https://ogrid.dev/docs/quick-start)
72
+ - [API Reference](https://ogrid.dev/docs/api/props)
73
+ - [Examples](https://ogrid.dev/docs/examples)
74
+
75
+ ## Customization
76
+
77
+ Customize appearance with CSS variables:
78
+
79
+ ```css
80
+ :root {
81
+ --ogrid-border: #e0e0e0;
82
+ --ogrid-bg: #ffffff;
83
+ --ogrid-header-bg: #f5f5f5;
84
+ --ogrid-primary: #0066cc;
85
+ --ogrid-active-cell-border: #0066cc;
86
+ --ogrid-selected-cell-bg: #e3f2fd;
87
+ }
88
+ ```
89
+
90
+ ## Architecture
91
+
92
+ OGrid Vue Radix is part of the OGrid framework family:
93
+ - **@alaarab/ogrid-core** - Pure TypeScript types and utilities
94
+ - **@alaarab/ogrid-vue** - Vue 3 composables and headless components
95
+ - **@alaarab/ogrid-vue-radix** - Headless UI implementation (this package)
96
+ - **@alaarab/ogrid-vue-vuetify** - Vuetify 3 implementation
97
+ - **@alaarab/ogrid-vue-primevue** - PrimeVue 4 implementation
98
+
99
+ All Vue packages share the same headless core and expose identical APIs.
100
+
101
+ ## License
102
+
103
+ MIT © Ala Arab
@@ -0,0 +1,139 @@
1
+ .container {
2
+ position: relative;
3
+ display: inline-flex;
4
+ }
5
+
6
+ .trigger-button {
7
+ display: inline-flex;
8
+ align-items: center;
9
+ gap: 6px;
10
+ padding: 6px 12px;
11
+ border: 1px solid var(--ogrid-border, #ccc);
12
+ border-radius: 6px;
13
+ background: var(--ogrid-bg, #fff);
14
+ cursor: pointer;
15
+ font-size: 13px;
16
+ font-weight: 600;
17
+ color: var(--ogrid-fg, #333);
18
+ transition: background 0.15s, border-color 0.15s;
19
+ }
20
+ .trigger-button:hover {
21
+ background: var(--ogrid-bg-hover, #f5f5f5);
22
+ border-color: var(--ogrid-border-hover, #999);
23
+ }
24
+ .trigger-button[aria-expanded=true] {
25
+ border-color: var(--ogrid-primary, #0066cc);
26
+ }
27
+
28
+ .button-icon {
29
+ font-size: 16px;
30
+ line-height: 1;
31
+ }
32
+
33
+ .chevron {
34
+ font-size: 12px;
35
+ color: var(--ogrid-muted, #888);
36
+ }
37
+
38
+ .dropdown {
39
+ position: absolute;
40
+ right: 0;
41
+ top: calc(100% + 4px);
42
+ min-width: 220px;
43
+ background: var(--ogrid-bg, #fff);
44
+ border-radius: 6px;
45
+ box-shadow: var(--ogrid-shadow, 0 4px 16px rgba(0, 0, 0, 0.12));
46
+ border: 1px solid var(--ogrid-border, #e0e0e0);
47
+ display: flex;
48
+ flex-direction: column;
49
+ padding: 0;
50
+ z-index: 50;
51
+ }
52
+
53
+ .header {
54
+ padding: 8px 12px;
55
+ border-bottom: 1px solid var(--ogrid-border, #e5e5e5);
56
+ font-weight: 600;
57
+ font-size: 13px;
58
+ color: var(--ogrid-fg, #333);
59
+ background: var(--ogrid-bg-subtle, #f5f5f5);
60
+ }
61
+
62
+ .options-list {
63
+ max-height: 320px;
64
+ overflow-y: auto;
65
+ padding: 0;
66
+ }
67
+
68
+ .option-item {
69
+ padding: 4px 12px;
70
+ display: flex;
71
+ align-items: center;
72
+ min-height: 32px;
73
+ gap: 8px;
74
+ }
75
+ .option-item:hover {
76
+ background: var(--ogrid-bg-hover, #f0f0f0);
77
+ }
78
+
79
+ .checkbox-input {
80
+ width: 16px;
81
+ height: 16px;
82
+ border: 1px solid var(--ogrid-border, #888);
83
+ border-radius: 3px;
84
+ background: var(--ogrid-bg, #fff);
85
+ cursor: pointer;
86
+ flex-shrink: 0;
87
+ }
88
+ .checkbox-input:checked {
89
+ background: var(--ogrid-primary, #0066cc);
90
+ border-color: var(--ogrid-primary, #0066cc);
91
+ }
92
+ .checkbox-input:disabled {
93
+ opacity: 0.5;
94
+ cursor: not-allowed;
95
+ }
96
+
97
+ .checkbox-label {
98
+ cursor: pointer;
99
+ font-size: 13px;
100
+ color: var(--ogrid-fg, #333);
101
+ user-select: none;
102
+ }
103
+
104
+ .actions {
105
+ display: flex;
106
+ justify-content: flex-end;
107
+ gap: 8px;
108
+ padding: 8px 12px;
109
+ border-top: 1px solid var(--ogrid-border, #e5e5e5);
110
+ background: var(--ogrid-bg-subtle, #f5f5f5);
111
+ }
112
+
113
+ .clear-button {
114
+ padding: 6px 12px;
115
+ border: 1px solid var(--ogrid-border, #ccc);
116
+ border-radius: 4px;
117
+ background: var(--ogrid-bg, #fff);
118
+ color: var(--ogrid-muted, #666);
119
+ font-size: 12px;
120
+ cursor: pointer;
121
+ }
122
+ .clear-button:hover {
123
+ background: var(--ogrid-bg-hover, #f5f5f5);
124
+ color: var(--ogrid-fg, #333);
125
+ }
126
+
127
+ .select-all-button {
128
+ padding: 6px 16px;
129
+ border: none;
130
+ border-radius: 4px;
131
+ background: var(--ogrid-primary, #0066cc);
132
+ color: var(--ogrid-primary-fg, #fff);
133
+ font-size: 12px;
134
+ font-weight: 600;
135
+ cursor: pointer;
136
+ }
137
+ .select-all-button:hover {
138
+ background: var(--ogrid-primary-hover, #0052a3);
139
+ }
@@ -0,0 +1,245 @@
1
+ .column-header {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: 4px;
5
+ width: 100%;
6
+ max-width: 100%;
7
+ min-width: 0;
8
+ position: relative;
9
+ box-sizing: border-box;
10
+ overflow: hidden;
11
+ }
12
+
13
+ .header-content {
14
+ display: flex;
15
+ align-items: center;
16
+ flex: 1;
17
+ min-width: 0;
18
+ overflow: hidden;
19
+ }
20
+
21
+ .column-name {
22
+ display: block;
23
+ min-width: 0;
24
+ max-width: 100%;
25
+ overflow: hidden;
26
+ text-overflow: ellipsis;
27
+ white-space: nowrap;
28
+ font-weight: 600;
29
+ font-size: 14px;
30
+ color: var(--ogrid-fg, #242424);
31
+ }
32
+
33
+ .header-actions {
34
+ display: flex;
35
+ align-items: center;
36
+ gap: 2px;
37
+ margin-left: auto;
38
+ flex-shrink: 0;
39
+ }
40
+
41
+ .sort-icon {
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ width: 24px;
46
+ height: 24px;
47
+ padding: 4px;
48
+ border: none;
49
+ border-radius: 4px;
50
+ background: transparent;
51
+ color: var(--ogrid-muted, #616161);
52
+ cursor: pointer;
53
+ flex-shrink: 0;
54
+ font-size: 14px;
55
+ }
56
+ .sort-icon:hover {
57
+ background: var(--ogrid-bg-hover, #f5f5f5);
58
+ color: var(--ogrid-fg, #424242);
59
+ }
60
+ .sort-icon.sort-active {
61
+ background: var(--ogrid-bg-selected, #e0e0e0);
62
+ color: var(--ogrid-fg, #333);
63
+ }
64
+
65
+ .filter-icon {
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ width: 24px;
70
+ height: 24px;
71
+ padding: 4px;
72
+ border: none;
73
+ border-radius: 4px;
74
+ background: transparent;
75
+ color: var(--ogrid-muted, #616161);
76
+ cursor: pointer;
77
+ flex-shrink: 0;
78
+ position: relative;
79
+ font-size: 14px;
80
+ }
81
+ .filter-icon:hover {
82
+ background: var(--ogrid-bg-hover, #f5f5f5);
83
+ color: var(--ogrid-fg, #424242);
84
+ }
85
+ .filter-icon.filter-active {
86
+ background: var(--ogrid-bg-selected, #e0e0e0);
87
+ color: var(--ogrid-primary, #0066cc);
88
+ }
89
+ .filter-icon.filter-open {
90
+ background: var(--ogrid-bg-selected, #e8e8e8);
91
+ }
92
+
93
+ .filter-badge {
94
+ position: absolute;
95
+ top: 2px;
96
+ right: 2px;
97
+ width: 6px;
98
+ height: 6px;
99
+ background: var(--ogrid-primary, #0066cc);
100
+ border-radius: 50%;
101
+ border: 1px solid var(--ogrid-bg, #fff);
102
+ }
103
+
104
+ .popover-content {
105
+ position: absolute;
106
+ top: calc(100% + 4px);
107
+ right: 0;
108
+ z-index: 1000;
109
+ min-width: 280px;
110
+ max-width: 320px;
111
+ background: var(--ogrid-bg, #fff);
112
+ border: 1px solid var(--ogrid-border, #d1d1d1);
113
+ border-radius: 8px;
114
+ box-shadow: var(--ogrid-shadow, 0 4px 16px rgba(0, 0, 0, 0.12));
115
+ overflow: hidden;
116
+ }
117
+
118
+ .popover-header {
119
+ padding: 10px 14px;
120
+ font-size: 12px;
121
+ font-weight: 600;
122
+ color: var(--ogrid-muted, #616161);
123
+ border-bottom: 1px solid var(--ogrid-border, #e0e0e0);
124
+ background: var(--ogrid-bg-subtle, #fafafa);
125
+ }
126
+
127
+ .popover-search {
128
+ padding: 10px 12px;
129
+ border-bottom: 1px solid var(--ogrid-border, #e0e0e0);
130
+ }
131
+
132
+ .search-input {
133
+ width: 100%;
134
+ padding: 6px 10px;
135
+ border: 1px solid var(--ogrid-border, #d1d1d1);
136
+ border-radius: 4px;
137
+ font-size: 14px;
138
+ box-sizing: border-box;
139
+ }
140
+ .search-input:focus {
141
+ outline: none;
142
+ border-color: var(--ogrid-primary, #0066cc);
143
+ }
144
+
145
+ .result-count {
146
+ margin-top: 6px;
147
+ font-size: 11px;
148
+ color: var(--ogrid-muted, #616161);
149
+ }
150
+
151
+ .select-all-row {
152
+ display: flex;
153
+ gap: 8px;
154
+ padding: 6px 12px;
155
+ border-bottom: 1px solid var(--ogrid-border, #e0e0e0);
156
+ background: var(--ogrid-bg-subtle, #fafafa);
157
+ }
158
+
159
+ .select-all-button {
160
+ background: none;
161
+ border: none;
162
+ color: var(--ogrid-primary, #0066cc);
163
+ font-size: 12px;
164
+ font-weight: 500;
165
+ cursor: pointer;
166
+ padding: 4px 8px;
167
+ border-radius: 4px;
168
+ }
169
+ .select-all-button:hover {
170
+ background: var(--ogrid-bg-hover, #e8f4fc);
171
+ }
172
+
173
+ .popover-options {
174
+ overflow-y: auto;
175
+ max-height: 250px;
176
+ padding: 6px 0;
177
+ }
178
+
179
+ .popover-option {
180
+ padding: 4px 12px;
181
+ display: flex;
182
+ align-items: center;
183
+ gap: 8px;
184
+ }
185
+ .popover-option:hover {
186
+ background: var(--ogrid-bg-hover, #f5f5f5);
187
+ }
188
+
189
+ .filter-checkbox {
190
+ width: 16px;
191
+ height: 16px;
192
+ border: 1px solid var(--ogrid-border, #888);
193
+ border-radius: 3px;
194
+ cursor: pointer;
195
+ flex-shrink: 0;
196
+ }
197
+
198
+ .popover-actions {
199
+ display: flex;
200
+ justify-content: flex-end;
201
+ gap: 8px;
202
+ padding: 8px 12px;
203
+ border-top: 1px solid var(--ogrid-border, #e0e0e0);
204
+ background: var(--ogrid-bg-subtle, #f5f5f5);
205
+ }
206
+
207
+ .clear-button {
208
+ padding: 6px 12px;
209
+ border: 1px solid var(--ogrid-border, #ccc);
210
+ border-radius: 4px;
211
+ background: var(--ogrid-bg, #fff);
212
+ color: var(--ogrid-muted, #666);
213
+ font-size: 12px;
214
+ cursor: pointer;
215
+ }
216
+ .clear-button:hover {
217
+ background: var(--ogrid-bg-hover, #f5f5f5);
218
+ color: var(--ogrid-fg, #333);
219
+ }
220
+ .clear-button:disabled {
221
+ opacity: 0.5;
222
+ cursor: not-allowed;
223
+ }
224
+
225
+ .apply-button {
226
+ padding: 6px 16px;
227
+ border: none;
228
+ border-radius: 4px;
229
+ background: var(--ogrid-primary, #0066cc);
230
+ color: var(--ogrid-primary-fg, #fff);
231
+ font-size: 12px;
232
+ font-weight: 600;
233
+ cursor: pointer;
234
+ }
235
+ .apply-button:hover {
236
+ background: var(--ogrid-primary-hover, #0052a3);
237
+ }
238
+
239
+ .loading-container,
240
+ .no-results {
241
+ padding: 20px;
242
+ text-align: center;
243
+ font-size: 13px;
244
+ color: var(--ogrid-muted, #666);
245
+ }
@@ -0,0 +1,63 @@
1
+ .ogrid-table-wrapper {
2
+ position: relative;
3
+ flex: 1;
4
+ min-height: 0;
5
+ display: flex;
6
+ flex-direction: column;
7
+ }
8
+
9
+ .ogrid-loading {
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ padding: 48px;
14
+ color: var(--ogrid-muted, #888);
15
+ font-size: 14px;
16
+ }
17
+
18
+ .ogrid-table-container {
19
+ flex: 1;
20
+ overflow: auto;
21
+ position: relative;
22
+ }
23
+
24
+ .ogrid-table {
25
+ width: 100%;
26
+ border-collapse: collapse;
27
+ font-size: 14px;
28
+ background: var(--ogrid-bg, #fff);
29
+ }
30
+ .ogrid-table th {
31
+ position: sticky;
32
+ top: 0;
33
+ background: var(--ogrid-header-bg, #f5f5f5);
34
+ border-bottom: 1px solid var(--ogrid-border, #e0e0e0);
35
+ padding: 8px 12px;
36
+ text-align: left;
37
+ font-weight: 600;
38
+ z-index: 2;
39
+ }
40
+ .ogrid-table th.sortable {
41
+ cursor: pointer;
42
+ user-select: none;
43
+ }
44
+ .ogrid-table th.sortable:hover {
45
+ background: var(--ogrid-header-hover-bg, #f0f0f0);
46
+ }
47
+ .ogrid-table td {
48
+ padding: 8px 12px;
49
+ border-bottom: 1px solid var(--ogrid-border, #e0e0e0);
50
+ }
51
+ .ogrid-table td.active-cell {
52
+ outline: 2px solid var(--ogrid-active-border, #0078d4);
53
+ outline-offset: -2px;
54
+ }
55
+ .ogrid-table td.editable {
56
+ cursor: cell;
57
+ }
58
+ .ogrid-table tr:hover {
59
+ background: var(--ogrid-hover-bg, #f9f9f9);
60
+ }
61
+ .ogrid-table tr.selected-row {
62
+ background: var(--ogrid-selected-bg, #e3f2fd);
63
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * MarchingAntsOverlay — Renders range overlays on top of the grid:
3
+ *
4
+ * 1. **Selection range**: solid green border around the current selection
5
+ * 2. **Copy/Cut range**: animated dashed border (marching ants) like Excel
6
+ *
7
+ * Uses SVG rects positioned via cell data-attribute measurements.
8
+ */
9
+ import { defineComponent, ref, computed, watch, onMounted, onUnmounted, h } from 'vue';
10
+ // Inject the @keyframes rule once into <head> (deduplicates across multiple OGrid instances)
11
+ function ensureKeyframes() {
12
+ if (typeof document === 'undefined')
13
+ return;
14
+ if (document.getElementById('ogrid-marching-ants-keyframes'))
15
+ return;
16
+ const style = document.createElement('style');
17
+ style.id = 'ogrid-marching-ants-keyframes';
18
+ style.textContent =
19
+ '@keyframes ogrid-marching-ants{to{stroke-dashoffset:-8}}';
20
+ document.head.appendChild(style);
21
+ }
22
+ /** Measure the bounding rect of a range within a container. */
23
+ function measureRange(container, range, colOffset) {
24
+ const startGlobalCol = range.startCol + colOffset;
25
+ const endGlobalCol = range.endCol + colOffset;
26
+ const topLeft = container.querySelector(`[data-row-index="${range.startRow}"][data-col-index="${startGlobalCol}"]`);
27
+ const bottomRight = container.querySelector(`[data-row-index="${range.endRow}"][data-col-index="${endGlobalCol}"]`);
28
+ if (!topLeft || !bottomRight)
29
+ return null;
30
+ const cRect = container.getBoundingClientRect();
31
+ const tlRect = topLeft.getBoundingClientRect();
32
+ const brRect = bottomRight.getBoundingClientRect();
33
+ return {
34
+ top: tlRect.top - cRect.top,
35
+ left: tlRect.left - cRect.left,
36
+ width: brRect.right - tlRect.left,
37
+ height: brRect.bottom - tlRect.top,
38
+ };
39
+ }
40
+ export const MarchingAntsOverlay = defineComponent({
41
+ name: 'MarchingAntsOverlay',
42
+ props: {
43
+ /** Ref to the positioned container that wraps the table (must have position: relative) */
44
+ containerRef: { type: Object, required: true },
45
+ /** Current selection range — solid green border */
46
+ selectionRange: { type: Object, default: null },
47
+ /** Copy range — animated dashed border */
48
+ copyRange: { type: Object, default: null },
49
+ /** Cut range — animated dashed border */
50
+ cutRange: { type: Object, default: null },
51
+ /** Column offset — 1 when checkbox column is present, else 0 */
52
+ colOffset: { type: Number, required: true },
53
+ },
54
+ setup(props) {
55
+ const selRect = ref(null);
56
+ const clipRect = ref(null);
57
+ let rafId = 0;
58
+ let ro;
59
+ const clipRange = computed(() => props.copyRange ?? props.cutRange);
60
+ const measureAll = () => {
61
+ const container = props.containerRef.value;
62
+ if (!container) {
63
+ selRect.value = null;
64
+ clipRect.value = null;
65
+ return;
66
+ }
67
+ selRect.value = props.selectionRange ? measureRange(container, props.selectionRange, props.colOffset) : null;
68
+ clipRect.value = clipRange.value ? measureRange(container, clipRange.value, props.colOffset) : null;
69
+ };
70
+ // Inject keyframes on mount
71
+ onMounted(() => {
72
+ ensureKeyframes();
73
+ });
74
+ // Measure when any range changes; re-measure on resize
75
+ watch([() => props.selectionRange, clipRange, () => props.containerRef.value], () => {
76
+ if (!props.selectionRange && !clipRange.value) {
77
+ selRect.value = null;
78
+ clipRect.value = null;
79
+ return;
80
+ }
81
+ // Delay one frame so cells are rendered
82
+ rafId = requestAnimationFrame(measureAll);
83
+ const container = props.containerRef.value;
84
+ if (container) {
85
+ ro?.disconnect();
86
+ ro = new ResizeObserver(measureAll);
87
+ ro.observe(container);
88
+ }
89
+ }, { immediate: true });
90
+ onUnmounted(() => {
91
+ cancelAnimationFrame(rafId);
92
+ ro?.disconnect();
93
+ });
94
+ const clipRangeMatchesSel = computed(() => {
95
+ const sel = props.selectionRange;
96
+ const clip = clipRange.value;
97
+ return (sel != null &&
98
+ clip != null &&
99
+ sel.startRow === clip.startRow &&
100
+ sel.startCol === clip.startCol &&
101
+ sel.endRow === clip.endRow &&
102
+ sel.endCol === clip.endCol);
103
+ });
104
+ return () => {
105
+ if (!selRect.value && !clipRect.value)
106
+ return null;
107
+ return h('div', { style: { position: 'relative' } }, [
108
+ // Selection range: solid green border (hidden when clipboard range overlaps)
109
+ selRect.value && !clipRangeMatchesSel.value ? h('svg', {
110
+ style: {
111
+ position: 'absolute',
112
+ top: `${selRect.value.top}px`,
113
+ left: `${selRect.value.left}px`,
114
+ width: `${selRect.value.width}px`,
115
+ height: `${selRect.value.height}px`,
116
+ pointerEvents: 'none',
117
+ zIndex: 4,
118
+ overflow: 'visible',
119
+ },
120
+ 'aria-hidden': 'true',
121
+ }, [
122
+ h('rect', {
123
+ x: 1,
124
+ y: 1,
125
+ width: Math.max(0, selRect.value.width - 2),
126
+ height: Math.max(0, selRect.value.height - 2),
127
+ fill: 'none',
128
+ stroke: 'var(--ogrid-selection, #217346)',
129
+ 'stroke-width': 2,
130
+ }),
131
+ ]) : null,
132
+ // Copy/Cut range: animated marching ants
133
+ clipRect.value ? h('svg', {
134
+ style: {
135
+ position: 'absolute',
136
+ top: `${clipRect.value.top}px`,
137
+ left: `${clipRect.value.left}px`,
138
+ width: `${clipRect.value.width}px`,
139
+ height: `${clipRect.value.height}px`,
140
+ pointerEvents: 'none',
141
+ zIndex: 5,
142
+ overflow: 'visible',
143
+ },
144
+ 'aria-hidden': 'true',
145
+ }, [
146
+ h('rect', {
147
+ x: 1,
148
+ y: 1,
149
+ width: Math.max(0, clipRect.value.width - 2),
150
+ height: Math.max(0, clipRect.value.height - 2),
151
+ fill: 'none',
152
+ stroke: 'var(--ogrid-selection, #217346)',
153
+ 'stroke-width': 2,
154
+ 'stroke-dasharray': '4 4',
155
+ style: {
156
+ animation: 'ogrid-marching-ants 0.5s linear infinite',
157
+ },
158
+ }),
159
+ ]) : null,
160
+ ]);
161
+ };
162
+ },
163
+ });
@@ -0,0 +1,110 @@
1
+ .pagination {
2
+ display: flex;
3
+ flex-wrap: wrap;
4
+ align-items: center;
5
+ justify-content: space-between;
6
+ gap: 14px 24px;
7
+ width: 100%;
8
+ min-width: 0;
9
+ box-sizing: border-box;
10
+ padding: 0;
11
+ }
12
+
13
+ .pagination-info {
14
+ font-size: 13px;
15
+ color: var(--ogrid-muted, #606060);
16
+ flex-shrink: 0;
17
+ font-variant-numeric: tabular-nums;
18
+ }
19
+
20
+ .pagination-controls {
21
+ display: flex;
22
+ align-items: center;
23
+ gap: 4px;
24
+ flex-wrap: wrap;
25
+ flex: 1 1 auto;
26
+ justify-content: center;
27
+ min-width: 0;
28
+ }
29
+
30
+ .nav-btn {
31
+ min-width: 28px;
32
+ min-height: 28px;
33
+ padding: 4px;
34
+ border: 1px solid var(--ogrid-border, #ccc);
35
+ border-radius: 50%;
36
+ background: var(--ogrid-bg, #fff);
37
+ color: var(--ogrid-fg, #333);
38
+ cursor: pointer;
39
+ display: inline-flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ font-size: 14px;
43
+ }
44
+ .nav-btn:hover:not(:disabled) {
45
+ background: var(--ogrid-bg-hover, #f5f5f5);
46
+ border-color: var(--ogrid-border-hover, #999);
47
+ }
48
+ .nav-btn:disabled {
49
+ opacity: 0.5;
50
+ cursor: not-allowed;
51
+ }
52
+
53
+ .page-numbers {
54
+ display: inline-flex;
55
+ align-items: center;
56
+ gap: 4px;
57
+ margin: 0 8px;
58
+ }
59
+
60
+ .page-btn {
61
+ min-width: 28px;
62
+ min-height: 28px;
63
+ padding: 4px 8px;
64
+ border: 1px solid var(--ogrid-border, #ccc);
65
+ border-radius: 4px;
66
+ background: var(--ogrid-bg, #fff);
67
+ color: var(--ogrid-fg, #333);
68
+ cursor: pointer;
69
+ font-size: 13px;
70
+ font-variant-numeric: tabular-nums;
71
+ }
72
+ .page-btn:hover {
73
+ background: var(--ogrid-bg-hover, #f5f5f5);
74
+ }
75
+ .page-btn.active {
76
+ background: var(--ogrid-primary, #0066cc);
77
+ border-color: var(--ogrid-primary, #0066cc);
78
+ color: var(--ogrid-primary-fg, #fff);
79
+ }
80
+
81
+ .ellipsis {
82
+ display: inline-flex;
83
+ align-items: center;
84
+ justify-content: center;
85
+ min-width: 24px;
86
+ font-size: 12px;
87
+ color: var(--ogrid-muted, #888);
88
+ user-select: none;
89
+ }
90
+
91
+ .page-size-selector {
92
+ display: inline-flex;
93
+ align-items: center;
94
+ gap: 8px;
95
+ flex-shrink: 0;
96
+ }
97
+ .page-size-selector .page-size-label {
98
+ font-size: 13px;
99
+ color: var(--ogrid-muted, #606060);
100
+ user-select: none;
101
+ white-space: nowrap;
102
+ }
103
+ .page-size-selector .page-size-select {
104
+ min-width: 72px;
105
+ padding: 4px 8px;
106
+ border: 1px solid var(--ogrid-border, #ccc);
107
+ border-radius: 4px;
108
+ background: var(--ogrid-bg, #fff);
109
+ font-size: 13px;
110
+ }
@@ -0,0 +1,13 @@
1
+ // Main components
2
+ export { default as OGrid } from './OGrid/OGrid.vue';
3
+ export { default as DataGridTable } from './DataGridTable/DataGridTable.vue';
4
+ export { default as ColumnChooser } from './ColumnChooser/ColumnChooser.vue';
5
+ export { default as ColumnHeaderFilter } from './ColumnHeaderFilter/ColumnHeaderFilter.vue';
6
+ export { default as PaginationControls } from './PaginationControls/PaginationControls.vue';
7
+ // DataGridTable sub-components
8
+ export { default as StatusBar } from './DataGridTable/StatusBar.vue';
9
+ export { default as GridContextMenu } from './DataGridTable/GridContextMenu.vue';
10
+ export { default as MarchingAntsOverlay } from './DataGridTable/MarchingAntsOverlay.vue';
11
+ export { default as InlineCellEditor } from './DataGridTable/InlineCellEditor.vue';
12
+ // Re-export everything from @alaarab/ogrid-vue (which re-exports from @alaarab/ogrid-core)
13
+ export * from '@alaarab/ogrid-vue';
@@ -0,0 +1,69 @@
1
+ /**
2
+ * MarchingAntsOverlay — Renders range overlays on top of the grid:
3
+ *
4
+ * 1. **Selection range**: solid green border around the current selection
5
+ * 2. **Copy/Cut range**: animated dashed border (marching ants) like Excel
6
+ *
7
+ * Uses SVG rects positioned via cell data-attribute measurements.
8
+ */
9
+ import { type PropType, type Ref } from 'vue';
10
+ import type { ISelectionRange } from '@alaarab/ogrid-vue';
11
+ export declare const MarchingAntsOverlay: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
12
+ /** Ref to the positioned container that wraps the table (must have position: relative) */
13
+ containerRef: {
14
+ type: PropType<Ref<HTMLElement | null>>;
15
+ required: true;
16
+ };
17
+ /** Current selection range — solid green border */
18
+ selectionRange: {
19
+ type: PropType<ISelectionRange | null>;
20
+ default: null;
21
+ };
22
+ /** Copy range — animated dashed border */
23
+ copyRange: {
24
+ type: PropType<ISelectionRange | null>;
25
+ default: null;
26
+ };
27
+ /** Cut range — animated dashed border */
28
+ cutRange: {
29
+ type: PropType<ISelectionRange | null>;
30
+ default: null;
31
+ };
32
+ /** Column offset — 1 when checkbox column is present, else 0 */
33
+ colOffset: {
34
+ type: NumberConstructor;
35
+ required: true;
36
+ };
37
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
38
+ [key: string]: any;
39
+ }> | null, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
40
+ /** Ref to the positioned container that wraps the table (must have position: relative) */
41
+ containerRef: {
42
+ type: PropType<Ref<HTMLElement | null>>;
43
+ required: true;
44
+ };
45
+ /** Current selection range — solid green border */
46
+ selectionRange: {
47
+ type: PropType<ISelectionRange | null>;
48
+ default: null;
49
+ };
50
+ /** Copy range — animated dashed border */
51
+ copyRange: {
52
+ type: PropType<ISelectionRange | null>;
53
+ default: null;
54
+ };
55
+ /** Cut range — animated dashed border */
56
+ cutRange: {
57
+ type: PropType<ISelectionRange | null>;
58
+ default: null;
59
+ };
60
+ /** Column offset — 1 when checkbox column is present, else 0 */
61
+ colOffset: {
62
+ type: NumberConstructor;
63
+ required: true;
64
+ };
65
+ }>> & Readonly<{}>, {
66
+ selectionRange: ISelectionRange | null;
67
+ copyRange: ISelectionRange | null;
68
+ cutRange: ISelectionRange | null;
69
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,10 @@
1
+ export { default as OGrid } from './OGrid/OGrid.vue';
2
+ export { default as DataGridTable } from './DataGridTable/DataGridTable.vue';
3
+ export { default as ColumnChooser } from './ColumnChooser/ColumnChooser.vue';
4
+ export { default as ColumnHeaderFilter } from './ColumnHeaderFilter/ColumnHeaderFilter.vue';
5
+ export { default as PaginationControls } from './PaginationControls/PaginationControls.vue';
6
+ export { default as StatusBar } from './DataGridTable/StatusBar.vue';
7
+ export { default as GridContextMenu } from './DataGridTable/GridContextMenu.vue';
8
+ export { default as MarchingAntsOverlay } from './DataGridTable/MarchingAntsOverlay.vue';
9
+ export { default as InlineCellEditor } from './DataGridTable/InlineCellEditor.vue';
10
+ export * from '@alaarab/ogrid-vue';
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@alaarab/ogrid-vue-radix",
3
+ "version": "2.0.4",
4
+ "description": "OGrid Vue Radix – Lightweight data grid with sorting, filtering, pagination, column chooser, and CSV export. Built with Headless UI Vue.",
5
+ "main": "dist/esm/index.js",
6
+ "module": "dist/esm/index.js",
7
+ "types": "dist/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/types/index.d.ts",
11
+ "import": "./dist/esm/index.js",
12
+ "require": "./dist/esm/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "rimraf dist && tsc -p tsconfig.build.json && node scripts/compile-styles.js",
17
+ "test": "jest"
18
+ },
19
+ "keywords": [
20
+ "ogrid",
21
+ "headlessui",
22
+ "vue",
23
+ "datatable",
24
+ "typescript",
25
+ "grid",
26
+ "headless"
27
+ ],
28
+ "author": "Ala Arab",
29
+ "license": "MIT",
30
+ "files": [
31
+ "dist",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "sideEffects": [
36
+ "**/*.css"
37
+ ],
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
41
+ "dependencies": {
42
+ "@alaarab/ogrid-vue": "2.0.4",
43
+ "@headlessui/vue": "^1.7.0"
44
+ },
45
+ "peerDependencies": {
46
+ "vue": "^3.3.0"
47
+ },
48
+ "devDependencies": {
49
+ "vue": "^3.5.28",
50
+ "@vitejs/plugin-vue": "^6.0.4",
51
+ "vite": "^7.0.0",
52
+ "vite-plugin-dts": "^4.5.0",
53
+ "typescript": "^5.7.3",
54
+ "sass": "^1.83.4",
55
+ "@types/jest": "^29.5.0",
56
+ "jest": "^29.7.0",
57
+ "jest-environment-jsdom": "^29.7.0",
58
+ "ts-jest": "^29.2.0"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
62
+ }
63
+ }