@cocoar/vue-ui 3.1.0 → 3.2.0-beta.11

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.
Files changed (180) hide show
  1. package/bin/cli.mjs +53 -0
  2. package/dist/components/checkbox/CoarCheckbox.vue.d.ts +1 -1
  3. package/dist/components/checkbox/CoarCheckbox.vue.d.ts.map +1 -1
  4. package/dist/components/code-block/CoarCodeBlock.vue.d.ts +1 -1
  5. package/dist/components/code-block/CoarCodeBlock.vue.d.ts.map +1 -1
  6. package/dist/components/data-list/CoarDataList.vue.d.ts +158 -0
  7. package/dist/components/data-list/CoarDataList.vue.d.ts.map +1 -0
  8. package/dist/components/data-list/CoarDataListToolbar.vue.d.ts +30 -0
  9. package/dist/components/data-list/CoarDataListToolbar.vue.d.ts.map +1 -0
  10. package/dist/components/data-list/data-list-builder.d.ts +288 -0
  11. package/dist/components/data-list/data-list-builder.d.ts.map +1 -0
  12. package/dist/components/data-list/index.d.ts +12 -0
  13. package/dist/components/data-list/index.d.ts.map +1 -0
  14. package/dist/components/data-list/internal/compare.d.ts +14 -0
  15. package/dist/components/data-list/internal/compare.d.ts.map +1 -0
  16. package/dist/components/data-list/internal/reorder-core.d.ts +46 -0
  17. package/dist/components/data-list/internal/reorder-core.d.ts.map +1 -0
  18. package/dist/components/data-list/internal/search.d.ts +16 -0
  19. package/dist/components/data-list/internal/search.d.ts.map +1 -0
  20. package/dist/components/data-list/internal/useDataListLines.d.ts +92 -0
  21. package/dist/components/data-list/internal/useDataListLines.d.ts.map +1 -0
  22. package/dist/components/data-list/internal/useDataListReorder.d.ts +68 -0
  23. package/dist/components/data-list/internal/useDataListReorder.d.ts.map +1 -0
  24. package/dist/components/data-list/internal/useSearchHighlight.d.ts +19 -0
  25. package/dist/components/data-list/internal/useSearchHighlight.d.ts.map +1 -0
  26. package/dist/components/data-list/types.d.ts +185 -0
  27. package/dist/components/data-list/types.d.ts.map +1 -0
  28. package/dist/components/data-list/useDataListModel.d.ts +79 -0
  29. package/dist/components/data-list/useDataListModel.d.ts.map +1 -0
  30. package/dist/components/date-time/_shared/maskito-config.d.ts +7 -2
  31. package/dist/components/date-time/_shared/maskito-config.d.ts.map +1 -1
  32. package/dist/components/date-time/_shared/time-helpers.d.ts +11 -4
  33. package/dist/components/date-time/_shared/time-helpers.d.ts.map +1 -1
  34. package/dist/components/date-time/_shared/use-date-picker-base.d.ts +1 -1
  35. package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts +1 -1
  36. package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts.map +1 -1
  37. package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts +1 -1
  38. package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts.map +1 -1
  39. package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts +1 -1
  40. package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts.map +1 -1
  41. package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts +3 -0
  42. package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts.map +1 -1
  43. package/dist/components/form-field/CoarFormField.vue.d.ts +1 -1
  44. package/dist/components/form-field/CoarFormField.vue.d.ts.map +1 -1
  45. package/dist/components/icon/CoarIcon.vue.d.ts +2 -2
  46. package/dist/components/icon/CoarIcon.vue.d.ts.map +1 -1
  47. package/dist/components/listbox/CoarListbox.vue.d.ts +7 -0
  48. package/dist/components/listbox/CoarListbox.vue.d.ts.map +1 -1
  49. package/dist/components/number-input/CoarNumberInput.vue.d.ts +1 -1
  50. package/dist/components/number-input/CoarNumberInput.vue.d.ts.map +1 -1
  51. package/dist/components/otp-input/CoarOtpInput.vue.d.ts +1 -1
  52. package/dist/components/otp-input/CoarOtpInput.vue.d.ts.map +1 -1
  53. package/dist/components/password-input/CoarPasswordInput.vue.d.ts +1 -1
  54. package/dist/components/password-input/CoarPasswordInput.vue.d.ts.map +1 -1
  55. package/dist/components/switch/CoarSwitch.vue.d.ts +1 -1
  56. package/dist/components/switch/CoarSwitch.vue.d.ts.map +1 -1
  57. package/dist/components/text-input/CoarTextInput.vue.d.ts +1 -1
  58. package/dist/components/text-input/CoarTextInput.vue.d.ts.map +1 -1
  59. package/dist/composables/dragRegistry.d.ts +12 -0
  60. package/dist/composables/dragRegistry.d.ts.map +1 -1
  61. package/dist/composables/index.d.ts +2 -2
  62. package/dist/composables/index.d.ts.map +1 -1
  63. package/dist/composables/useDragDrop.d.ts +62 -6
  64. package/dist/composables/useDragDrop.d.ts.map +1 -1
  65. package/dist/composables/useVirtualList.d.ts +21 -1
  66. package/dist/composables/useVirtualList.d.ts.map +1 -1
  67. package/dist/index.css +1 -1
  68. package/dist/index.d.ts +4 -2
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +4508 -2039
  71. package/package.json +9 -3
  72. package/skills/cocoar-vue-ui/SKILL.md +244 -0
  73. package/skills/cocoar-vue-ui/references/components/avatar.md +178 -0
  74. package/skills/cocoar-vue-ui/references/components/badge.md +290 -0
  75. package/skills/cocoar-vue-ui/references/components/breadcrumb.md +238 -0
  76. package/skills/cocoar-vue-ui/references/components/button.md +252 -0
  77. package/skills/cocoar-vue-ui/references/components/calendar/agenda-view.md +372 -0
  78. package/skills/cocoar-vue-ui/references/components/calendar/coar-calendar.md +1647 -0
  79. package/skills/cocoar-vue-ui/references/components/calendar/day-view.md +361 -0
  80. package/skills/cocoar-vue-ui/references/components/calendar/index.md +312 -0
  81. package/skills/cocoar-vue-ui/references/components/calendar/month-view.md +486 -0
  82. package/skills/cocoar-vue-ui/references/components/calendar/performance.md +186 -0
  83. package/skills/cocoar-vue-ui/references/components/calendar/timeline-view.md +301 -0
  84. package/skills/cocoar-vue-ui/references/components/calendar/week-view.md +382 -0
  85. package/skills/cocoar-vue-ui/references/components/calendar/work-week-view.md +145 -0
  86. package/skills/cocoar-vue-ui/references/components/calendar/year-view.md +63 -0
  87. package/skills/cocoar-vue-ui/references/components/card.md +151 -0
  88. package/skills/cocoar-vue-ui/references/components/checkbox-group.md +103 -0
  89. package/skills/cocoar-vue-ui/references/components/checkbox.md +279 -0
  90. package/skills/cocoar-vue-ui/references/components/code-block.md +241 -0
  91. package/skills/cocoar-vue-ui/references/components/context-menu.md +355 -0
  92. package/skills/cocoar-vue-ui/references/components/data-grid/checkbox.md +213 -0
  93. package/skills/cocoar-vue-ui/references/components/data-grid/date-columns.md +272 -0
  94. package/skills/cocoar-vue-ui/references/components/data-grid/editing.md +236 -0
  95. package/skills/cocoar-vue-ui/references/components/data-grid/multi-select.md +193 -0
  96. package/skills/cocoar-vue-ui/references/components/data-grid/number.md +147 -0
  97. package/skills/cocoar-vue-ui/references/components/data-grid/select.md +202 -0
  98. package/skills/cocoar-vue-ui/references/components/data-grid/text.md +127 -0
  99. package/skills/cocoar-vue-ui/references/components/data-grid.md +1225 -0
  100. package/skills/cocoar-vue-ui/references/components/data-list.md +2121 -0
  101. package/skills/cocoar-vue-ui/references/components/date-or-time-picker.md +200 -0
  102. package/skills/cocoar-vue-ui/references/components/date-picker.md +203 -0
  103. package/skills/cocoar-vue-ui/references/components/date-time-picker.md +154 -0
  104. package/skills/cocoar-vue-ui/references/components/date-views.md +212 -0
  105. package/skills/cocoar-vue-ui/references/components/dialog.md +148 -0
  106. package/skills/cocoar-vue-ui/references/components/divider.md +157 -0
  107. package/skills/cocoar-vue-ui/references/components/document-viewer/annotations.md +400 -0
  108. package/skills/cocoar-vue-ui/references/components/document-viewer/coar-document-viewer.md +327 -0
  109. package/skills/cocoar-vue-ui/references/components/document-viewer/index.md +234 -0
  110. package/skills/cocoar-vue-ui/references/components/document-viewer/toolbar.md +221 -0
  111. package/skills/cocoar-vue-ui/references/components/drag-drop.md +267 -0
  112. package/skills/cocoar-vue-ui/references/components/dual-listbox.md +449 -0
  113. package/skills/cocoar-vue-ui/references/components/file-explorer/asset-store.md +327 -0
  114. package/skills/cocoar-vue-ui/references/components/file-explorer/in-memory-store.md +176 -0
  115. package/skills/cocoar-vue-ui/references/components/file-explorer/index.md +311 -0
  116. package/skills/cocoar-vue-ui/references/components/file-explorer/use-file-explorer.md +300 -0
  117. package/skills/cocoar-vue-ui/references/components/form-field.md +1025 -0
  118. package/skills/cocoar-vue-ui/references/components/fragment-parser.md +298 -0
  119. package/skills/cocoar-vue-ui/references/components/link.md +235 -0
  120. package/skills/cocoar-vue-ui/references/components/listbox.md +575 -0
  121. package/skills/cocoar-vue-ui/references/components/map/editor.md +448 -0
  122. package/skills/cocoar-vue-ui/references/components/map/index.md +351 -0
  123. package/skills/cocoar-vue-ui/references/components/markdown-diagrams.md +210 -0
  124. package/skills/cocoar-vue-ui/references/components/markdown-editor.md +1478 -0
  125. package/skills/cocoar-vue-ui/references/components/markdown-embeds.md +387 -0
  126. package/skills/cocoar-vue-ui/references/components/markdown-form.md +447 -0
  127. package/skills/cocoar-vue-ui/references/components/markdown.md +276 -0
  128. package/skills/cocoar-vue-ui/references/components/menu.md +380 -0
  129. package/skills/cocoar-vue-ui/references/components/mermaid.md +172 -0
  130. package/skills/cocoar-vue-ui/references/components/navbar.md +147 -0
  131. package/skills/cocoar-vue-ui/references/components/note.md +96 -0
  132. package/skills/cocoar-vue-ui/references/components/notice.md +131 -0
  133. package/skills/cocoar-vue-ui/references/components/number-input.md +215 -0
  134. package/skills/cocoar-vue-ui/references/components/otp-input.md +324 -0
  135. package/skills/cocoar-vue-ui/references/components/page-builder/authoring-contract.md +271 -0
  136. package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-builder.md +476 -0
  137. package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-renderer.md +677 -0
  138. package/skills/cocoar-vue-ui/references/components/page-builder/custom-elements.md +441 -0
  139. package/skills/cocoar-vue-ui/references/components/page-builder/idp-integration.md +122 -0
  140. package/skills/cocoar-vue-ui/references/components/page-builder/index.md +573 -0
  141. package/skills/cocoar-vue-ui/references/components/pagination.md +150 -0
  142. package/skills/cocoar-vue-ui/references/components/panel-layout.md +372 -0
  143. package/skills/cocoar-vue-ui/references/components/password-input.md +175 -0
  144. package/skills/cocoar-vue-ui/references/components/popconfirm.md +186 -0
  145. package/skills/cocoar-vue-ui/references/components/popover.md +173 -0
  146. package/skills/cocoar-vue-ui/references/components/progress-bar.md +178 -0
  147. package/skills/cocoar-vue-ui/references/components/radio-group.md +225 -0
  148. package/skills/cocoar-vue-ui/references/components/script-editor.md +1271 -0
  149. package/skills/cocoar-vue-ui/references/components/segmented-control.md +238 -0
  150. package/skills/cocoar-vue-ui/references/components/select.md +463 -0
  151. package/skills/cocoar-vue-ui/references/components/sidebar.md +421 -0
  152. package/skills/cocoar-vue-ui/references/components/spinner.md +132 -0
  153. package/skills/cocoar-vue-ui/references/components/switch.md +195 -0
  154. package/skills/cocoar-vue-ui/references/components/table.md +170 -0
  155. package/skills/cocoar-vue-ui/references/components/tabs.md +382 -0
  156. package/skills/cocoar-vue-ui/references/components/tag.md +178 -0
  157. package/skills/cocoar-vue-ui/references/components/text-input.md +256 -0
  158. package/skills/cocoar-vue-ui/references/components/toast.md +160 -0
  159. package/skills/cocoar-vue-ui/references/components/tooltip.md +121 -0
  160. package/skills/cocoar-vue-ui/references/components/transitions.md +193 -0
  161. package/skills/cocoar-vue-ui/references/components/tree.md +2388 -0
  162. package/skills/cocoar-vue-ui/references/components/virtual-list.md +212 -0
  163. package/skills/cocoar-vue-ui/references/components/wizard.md +251 -0
  164. package/skills/cocoar-vue-ui/references/components/zoned-date-time-picker.md +177 -0
  165. package/skills/cocoar-vue-ui/references/foundations/colors.md +708 -0
  166. package/skills/cocoar-vue-ui/references/foundations/design-principles.md +115 -0
  167. package/skills/cocoar-vue-ui/references/foundations/icons.md +381 -0
  168. package/skills/cocoar-vue-ui/references/foundations/localization/formatting.md +530 -0
  169. package/skills/cocoar-vue-ui/references/foundations/localization/setup.md +86 -0
  170. package/skills/cocoar-vue-ui/references/foundations/localization/timezones.md +182 -0
  171. package/skills/cocoar-vue-ui/references/foundations/localization/translations.md +305 -0
  172. package/skills/cocoar-vue-ui/references/foundations/motion.md +549 -0
  173. package/skills/cocoar-vue-ui/references/foundations/spacing.md +330 -0
  174. package/skills/cocoar-vue-ui/references/foundations/theming.md +140 -0
  175. package/skills/cocoar-vue-ui/references/foundations/typography.md +206 -0
  176. package/skills/cocoar-vue-ui/references/guide/error-handling.md +162 -0
  177. package/skills/cocoar-vue-ui/references/guide/getting-started.md +116 -0
  178. package/skills/cocoar-vue-ui/references/guide/migration-page-builder-3.md +207 -0
  179. package/skills/cocoar-vue-ui/references/guide/migration.md +140 -0
  180. package/skills/cocoar-vue-ui/references/guide/theming.md +98 -0
@@ -0,0 +1,1225 @@
1
+ <!-- Generated from apps/docs/components/data-grid.md by apps/docs/scripts/sync-skill.mjs. Do not edit; edit the docs page. -->
2
+
3
+ # Data Grid
4
+
5
+ A powerful data grid built on AG Grid with Cocoar theming. Configure columns, sorting, selection, and cell renderers through a fluent builder API — no raw AG Grid config needed.
6
+
7
+ > **Info: Separate Package**
8
+ >
9
+ > The Data Grid depends on AG Grid. Install it separately:
10
+ ```bash
11
+ pnpm add @cocoar/vue-data-grid ag-grid-community ag-grid-vue3
12
+ ```
13
+
14
+ ```ts
15
+ import { CoarDataGrid, CoarGridBuilder } from '@cocoar/vue-data-grid';
16
+ ```
17
+
18
+ ## Basic Usage
19
+
20
+ Define columns with `.field()`, `.header()`, and `.flex()` / `.width()`. Pass row data with `.rowData()`.
21
+
22
+ **Demo — `data-grid/demos/GridBasic.vue`**
23
+
24
+ ```vue
25
+ <template>
26
+ <div style="height: 350px;">
27
+ <CoarDataGrid :builder="builder" />
28
+ </div>
29
+ </template>
30
+
31
+ <script setup lang="ts">
32
+ import { CoarDataGrid, CoarGridBuilder } from '@cocoar/vue-data-grid';
33
+
34
+ interface User {
35
+ name: string;
36
+ email: string;
37
+ role: string;
38
+ }
39
+
40
+ const data: User[] = [
41
+ { name: 'Alice Johnson', email: 'alice@example.com', role: 'Engineer' },
42
+ { name: 'Bob Smith', email: 'bob@example.com', role: 'Designer' },
43
+ { name: 'Carol Williams', email: 'carol@example.com', role: 'Manager' },
44
+ { name: 'David Brown', email: 'david@example.com', role: 'Engineer' },
45
+ { name: 'Eve Davis', email: 'eve@example.com', role: 'Designer' },
46
+ ];
47
+
48
+ const builder = CoarGridBuilder.create<User>()
49
+ .columns([
50
+ (col) => col.field('name').header('Name').flex(1),
51
+ (col) => col.field('email').header('Email').flex(1),
52
+ (col) => col.field('role').header('Role').width(120),
53
+ ])
54
+ .rowData(data);
55
+ </script>
56
+ ```
57
+
58
+ ## Appearance
59
+
60
+ Add a border or elevation shadow to the grid. Toggle the checkboxes to see the effect.
61
+
62
+ **Demo — `data-grid/demos/GridAppearance.vue`**
63
+
64
+ ```vue
65
+ <template>
66
+ <div style="display: flex; flex-direction: column; gap: 16px;">
67
+ <div style="display: flex; gap: 16px; align-items: center; flex-wrap: wrap;">
68
+ <label style="display: flex; align-items: center; gap: 6px; font-size: 0.9em;">
69
+ <input type="checkbox" v-model="showTitle" /> Title (left)
70
+ </label>
71
+ <label style="display: flex; align-items: center; gap: 6px; font-size: 0.9em;">
72
+ <input type="checkbox" v-model="showSearch" /> Search
73
+ </label>
74
+ <label style="display: flex; align-items: center; gap: 6px; font-size: 0.9em;">
75
+ <input type="checkbox" v-model="showActions" /> Actions (right)
76
+ </label>
77
+ <label style="display: flex; align-items: center; gap: 6px; font-size: 0.9em;">
78
+ <input type="checkbox" v-model="bordered" /> Bordered
79
+ </label>
80
+ <label style="display: flex; align-items: center; gap: 6px; font-size: 0.9em;">
81
+ <input type="checkbox" v-model="elevated" /> Elevated
82
+ </label>
83
+ </div>
84
+ <div style="height: 300px;">
85
+ <CoarDataGrid
86
+ :builder="builder"
87
+ :show-search="showSearch"
88
+ :bordered="bordered"
89
+ :elevated="elevated"
90
+ search-placeholder="Search users..."
91
+ >
92
+ <template v-if="showTitle" #toolbar-left>
93
+ <span style="font-weight: 600; white-space: nowrap;">User List</span>
94
+ </template>
95
+ <template v-if="showActions" #toolbar-right>
96
+ <CoarButton size="s" variant="secondary">Export</CoarButton>
97
+ <CoarButton size="s">Add User</CoarButton>
98
+ </template>
99
+ </CoarDataGrid>
100
+ </div>
101
+ </div>
102
+ </template>
103
+
104
+ <script setup lang="ts">
105
+ import { ref } from 'vue';
106
+ import { CoarDataGrid, CoarGridBuilder } from '@cocoar/vue-data-grid';
107
+ import { CoarButton } from '@cocoar/vue-ui';
108
+
109
+ const showTitle = ref(false);
110
+ const showSearch = ref(true);
111
+ const showActions = ref(true);
112
+ const bordered = ref(true);
113
+ const elevated = ref(false);
114
+
115
+ interface User {
116
+ name: string;
117
+ email: string;
118
+ role: string;
119
+ }
120
+
121
+ const data: User[] = [
122
+ { name: 'Alice Johnson', email: 'alice@example.com', role: 'Engineer' },
123
+ { name: 'Bob Smith', email: 'bob@example.com', role: 'Designer' },
124
+ { name: 'Carol Williams', email: 'carol@example.com', role: 'Manager' },
125
+ { name: 'David Brown', email: 'david@example.com', role: 'Engineer' },
126
+ { name: 'Eve Davis', email: 'eve@example.com', role: 'Designer' },
127
+ ];
128
+
129
+ const builder = CoarGridBuilder.create<User>()
130
+ .columns([
131
+ (col) => col.field('name').header('Name').flex(1),
132
+ (col) => col.field('email').header('Email').flex(1),
133
+ (col) => col.field('role').header('Role').width(120),
134
+ ])
135
+ .rowData(data);
136
+ </script>
137
+ ```
138
+
139
+ ### Dark mode
140
+
141
+ Dark styles ship with the package. The theme maps AG Grid's variables onto the semantic `--coar-*` tokens (with hardcoded fallbacks), so the grid follows your design-system theme in both modes. Dark values activate via the `.dark-mode` class — on `<html>` or any ancestor (the Cocoar convention, same as `@cocoar/vue-ui`), or directly on the grid element. `[data-theme="dark"]` is **not** a trigger here.
142
+
143
+ ## Column Types
144
+
145
+ Built-in renderers for dates, numbers, currency, tags, and icons — no custom cell components needed. Date, number, and currency columns are locale-aware and update reactively when the locale changes. Try the locale switcher in the nav bar.
146
+
147
+ **Demo — `data-grid/demos/GridColumnTypes.vue`**
148
+
149
+ ```vue
150
+ <template>
151
+ <div style="height: 350px;">
152
+ <CoarDataGrid :builder="builder" />
153
+ </div>
154
+ </template>
155
+
156
+ <script setup lang="ts">
157
+ import { CoarDataGrid, CoarGridBuilder } from '@cocoar/vue-data-grid';
158
+
159
+ interface Employee {
160
+ name: string;
161
+ joinDate: string;
162
+ salary: number;
163
+ hours: number;
164
+ status: 'active' | 'inactive' | 'pending';
165
+ icon: string;
166
+ }
167
+
168
+ const data: Employee[] = [
169
+ { name: 'Alice Johnson', joinDate: '2022-03-15', salary: 95000, hours: 1420.5, status: 'active', icon: 'user' },
170
+ { name: 'Bob Smith', joinDate: '2021-07-01', salary: 85000, hours: 1105, status: 'active', icon: 'palette' },
171
+ { name: 'Carol Williams', joinDate: '2020-01-20', salary: 110000, hours: 2250.75, status: 'inactive', icon: 'users' },
172
+ { name: 'David Brown', joinDate: '2023-06-10', salary: 90000, hours: 870.25, status: 'pending', icon: 'user' },
173
+ { name: 'Eve Davis', joinDate: '2022-11-05', salary: 88000, hours: 1680, status: 'active', icon: 'palette' },
174
+ ];
175
+
176
+ const builder = CoarGridBuilder.create<Employee>()
177
+ .columns([
178
+ (col) => col.field('name').header('Name').flex(1).sortable(),
179
+ (col) => col.date('joinDate').header('Joined').width(150),
180
+ (col) => col.currency('salary', { currencyCode: 'EUR' }).header('Salary').width(140),
181
+ (col) => col.number('hours', { decimals: 1 }).header('Hours').width(110),
182
+ (col) => col.tag('status', {
183
+ variantMap: { active: 'success', inactive: 'error', pending: 'warning' },
184
+ }).header('Status').width(130),
185
+ (col) => col.icon('icon', { size: 's' }).header('Type').fixedWidth(60),
186
+ ])
187
+ .rowData(data);
188
+ </script>
189
+ ```
190
+
191
+ | Method | Description |
192
+ |--------|-------------|
193
+ | `.field(name)` | Plain text column |
194
+ | `.date(field, config?)` | Locale-aware date display |
195
+ | `.number(field, config?)` | Locale-aware number display |
196
+ | `.currency(field, config?)` | Locale-aware currency display |
197
+ | `.tag(field, config)` | Renders a `CoarTag` with variant mapping or custom colors |
198
+ | `.icon(field, config?)` | Renders a `CoarIcon` |
199
+ | `.wrap(inner)` | Wraps any column builder with left/right decoration slots |
200
+
201
+ ## Wrapper Column
202
+
203
+ Decorate any column with left and/or right slots — perfect for status indicators, action icons, or inline badges. The inner column keeps all its behavior (sort, filter, edit, `valueFormatter`, custom `cellRenderer`, …); only rendering gets an extra frame around it.
204
+
205
+ **Demo — `data-grid/demos/GridWrapperColumn.vue`**
206
+
207
+ ```vue
208
+ <template>
209
+ <div style="height: 420px;">
210
+ <CoarDataGrid :builder="builder" />
211
+ </div>
212
+ </template>
213
+
214
+ <script setup lang="ts">
215
+ import { ref, defineComponent, h } from 'vue';
216
+ import { CoarDataGrid, CoarGridBuilder } from '@cocoar/vue-data-grid';
217
+ import { CoarBadge, CoarTag, CoarIcon } from '@cocoar/vue-ui';
218
+
219
+ interface Message {
220
+ id: number;
221
+ subject: string;
222
+ sender: string;
223
+ starred: boolean;
224
+ unread: number;
225
+ isCritical: boolean;
226
+ awaitingFeedback: boolean;
227
+ priority: 'low' | 'normal' | 'high';
228
+ }
229
+
230
+ const rows = ref<Message[]>([
231
+ { id: 1, subject: 'Q4 budget review', sender: 'Alice', starred: true, unread: 3, isCritical: true, awaitingFeedback: false, priority: 'high' },
232
+ { id: 2, subject: 'Design handoff — checkout', sender: 'Bob', starred: false, unread: 0, isCritical: false, awaitingFeedback: true, priority: 'normal' },
233
+ { id: 3, subject: 'Release notes draft', sender: 'Carol', starred: false, unread: 12, isCritical: false, awaitingFeedback: false, priority: 'low' },
234
+ { id: 4, subject: 'Offsite logistics', sender: 'David', starred: true, unread: 1, isCritical: true, awaitingFeedback: true, priority: 'high' },
235
+ { id: 5, subject: 'Customer escalation #42', sender: 'Eve', starred: false, unread: 0, isCritical: false, awaitingFeedback: false, priority: 'normal' },
236
+ ]);
237
+
238
+ function toggleStar(row: Message): void {
239
+ const target = rows.value.find((r) => r.id === row.id);
240
+ if (target) target.starred = !target.starred;
241
+ }
242
+
243
+ // A single Vue component that receives the whole row and decides internally
244
+ // whether to render an icon, a tag, or nothing — demonstrates the implicit `row` prop.
245
+ const PriorityIndicator = defineComponent({
246
+ props: { row: { type: Object as () => Message, required: true } },
247
+ setup(props) {
248
+ return () => {
249
+ if (props.row.priority === 'high') {
250
+ return h(CoarTag, { variant: 'error', size: 's' }, () => 'HIGH');
251
+ }
252
+ if (props.row.priority === 'low') {
253
+ return h(CoarIcon, { name: 'arrow-down', source: 'coar-builtin', size: 's', color: '#9ca3af' });
254
+ }
255
+ return null; // 'normal' — render nothing
256
+ };
257
+ },
258
+ });
259
+
260
+ const builder = CoarGridBuilder.create<Message>()
261
+ .columns([
262
+ // Wrapper with a single left item (star toggle) and a single right item (badge).
263
+ // Subject is editable — double-click opens the editor. Slots disappear automatically
264
+ // in edit mode because AG Grid swaps the whole cellRenderer for the cellEditor.
265
+ (col) => col
266
+ .wrap(col.field('subject').header('Subject').flex(1).sortable().option('editable', true))
267
+ .left({
268
+ icon: 'star',
269
+ source: 'coar-builtin',
270
+ color: (r) => (r.starred ? '#f5a623' : '#cbd5e1'),
271
+ tooltip: (r) => (r.starred ? 'Unstar' : 'Star'),
272
+ onClick: (r) => toggleStar(r),
273
+ })
274
+ .right({
275
+ component: CoarBadge,
276
+ params: (r) => ({ content: String(r.unread) }),
277
+ show: (r) => r.unread > 0,
278
+ }),
279
+
280
+ (col) => col.field('sender').header('From').width(130).sortable(),
281
+
282
+ // Wrapper with TWO right-side icons + a component that decides what to render
283
+ // based on the full row. Each item has its own show() gate.
284
+ (col) => col
285
+ .wrap(col.field('priority').header('Priority').width(200).sortable())
286
+ .right([
287
+ // 1) Critical-flag icon — only visible when isCritical = true
288
+ {
289
+ icon: 'circle-alert',
290
+ source: 'coar-builtin',
291
+ color: '#dc2626',
292
+ tooltip: 'Critical',
293
+ show: (r) => r.isCritical,
294
+ },
295
+ // 2) Awaiting-feedback icon — only visible when awaitingFeedback = true
296
+ {
297
+ icon: 'message-circle',
298
+ source: 'coar-builtin',
299
+ color: '#3b82f6',
300
+ tooltip: 'Awaiting feedback',
301
+ show: (r) => r.awaitingFeedback,
302
+ },
303
+ // 3) A component that receives the whole row and renders tag OR icon OR nothing
304
+ { component: PriorityIndicator },
305
+ ]),
306
+ ])
307
+ .rowDataRef(rows)
308
+ .rowId((params) => String(params.data.id));
309
+ </script>
310
+ ```
311
+
312
+ Each slot accepts one of three shapes:
313
+
314
+ ```ts
315
+ // 1) Icon shorthand
316
+ .left({
317
+ icon: (row) => row.starred ? 'star' : 'star-outline',
318
+ color: (row) => row.starred ? '#f5a623' : '#ccc',
319
+ tooltip: (row) => row.starred ? 'Unstar' : 'Star',
320
+ onClick: (row, event) => toggleStar(row),
321
+ show: (row) => row.visible, // optional v-if gate
322
+ })
323
+
324
+ // 2) Any Vue component
325
+ // The component automatically receives `row: TData` as a prop —
326
+ // use `params(row)` to add or override props.
327
+ .right({
328
+ component: CoarBadge,
329
+ params: (row) => ({ content: String(row.unread) }),
330
+ show: (row) => row.unread > 0,
331
+ })
332
+
333
+ // 3) Plain text
334
+ .right({ text: (row) => row.suffix })
335
+ ```
336
+
337
+ ### Multiple items per slot
338
+
339
+ Pass an array to stack several items in the same slot — each with its own `show()` gate, `onClick`, and tooltip. Items are rendered in order with a small gap.
340
+
341
+ ```ts
342
+ .right([
343
+ { icon: 'circle-alert', color: '#dc2626', show: (r) => r.isCritical },
344
+ { icon: 'message-circle', color: '#3b82f6', show: (r) => r.awaitingFeedback },
345
+ { component: PriorityIndicator }, // receives `row` automatically
346
+ ])
347
+ ```
348
+
349
+ ### Row-aware components
350
+
351
+ Every component slot automatically receives `row: TData` as a prop. This lets a single component decide what to render — icon, tag, or nothing — based on the full row:
352
+
353
+ ```ts
354
+ const PriorityIndicator = defineComponent({
355
+ props: { row: { type: Object as () => Message, required: true } },
356
+ setup(props) {
357
+ return () => {
358
+ if (props.row.priority === 'high') return h(CoarTag, { variant: 'error' }, () => 'HIGH');
359
+ if (props.row.priority === 'low') return h(CoarIcon, { name: 'arrow-down' });
360
+ return null;
361
+ };
362
+ },
363
+ });
364
+ ```
365
+
366
+ Slot `onClick` handlers automatically call `event.stopPropagation()` so they don't trigger row-click or cell-click events on the grid.
367
+
368
+ ## Row Selection
369
+
370
+ Toggle between single-click and multi-select with checkboxes.
371
+
372
+ **Demo — `data-grid/demos/GridSelection.vue`**
373
+
374
+ ```vue
375
+ <template>
376
+ <div>
377
+ <div style="display: flex; gap: 12px; margin-bottom: 12px;">
378
+ <label style="display: flex; align-items: center; gap: 4px; font-size: 13px; cursor: pointer;">
379
+ <input type="radio" value="single" v-model="mode" /> Single
380
+ </label>
381
+ <label style="display: flex; align-items: center; gap: 4px; font-size: 13px; cursor: pointer;">
382
+ <input type="radio" value="multiple" v-model="mode" /> Multi (Checkboxes)
383
+ </label>
384
+ </div>
385
+ <div style="height: 300px;">
386
+ <CoarDataGrid :key="mode" :builder="builders[mode]" />
387
+ </div>
388
+ </div>
389
+ </template>
390
+
391
+ <script setup lang="ts">
392
+ import { ref } from 'vue';
393
+ import { CoarDataGrid, CoarGridBuilder } from '@cocoar/vue-data-grid';
394
+
395
+ interface User {
396
+ name: string;
397
+ email: string;
398
+ role: string;
399
+ }
400
+
401
+ const data: User[] = [
402
+ { name: 'Alice Johnson', email: 'alice@example.com', role: 'Engineer' },
403
+ { name: 'Bob Smith', email: 'bob@example.com', role: 'Designer' },
404
+ { name: 'Carol Williams', email: 'carol@example.com', role: 'Manager' },
405
+ { name: 'David Brown', email: 'david@example.com', role: 'Engineer' },
406
+ { name: 'Eve Davis', email: 'eve@example.com', role: 'Designer' },
407
+ ];
408
+
409
+ const cols = [
410
+ (col: any) => col.field('name').header('Name').flex(1),
411
+ (col: any) => col.field('email').header('Email').flex(1),
412
+ (col: any) => col.field('role').header('Role').width(120),
413
+ ];
414
+
415
+ const mode = ref<'single' | 'multiple'>('single');
416
+
417
+ const builders = {
418
+ single: CoarGridBuilder.create<User>().columns(cols).rowData(data).rowSelection('single'),
419
+ multiple: CoarGridBuilder.create<User>().columns(cols).rowData(data).rowSelection('multiple', { checkboxes: true }),
420
+ };
421
+ </script>
422
+ ```
423
+
424
+ ## Reactive Data
425
+
426
+ Bind a `ref` with `.rowDataRef()` and the grid updates automatically when your data changes.
427
+
428
+ **Demo — `data-grid/demos/GridReactive.vue`**
429
+
430
+ ```vue
431
+ <template>
432
+ <div>
433
+ <div style="display: flex; gap: 8px; margin-bottom: 12px;">
434
+ <CoarButton size="s" @click="addRow">Add Row</CoarButton>
435
+ <CoarButton size="s" variant="secondary" @click="reset">Reset</CoarButton>
436
+ <span style="font-size: 13px; color: var(--coar-text-neutral-secondary); align-self: center;">
437
+ {{ data.length }} rows
438
+ </span>
439
+ </div>
440
+ <div style="height: 300px;">
441
+ <CoarDataGrid :builder="builder" />
442
+ </div>
443
+ </div>
444
+ </template>
445
+
446
+ <script setup lang="ts">
447
+ import { ref } from 'vue';
448
+ import { CoarButton } from '@cocoar/vue-ui';
449
+ import { CoarDataGrid, CoarGridBuilder } from '@cocoar/vue-data-grid';
450
+
451
+ interface User {
452
+ id: number;
453
+ name: string;
454
+ email: string;
455
+ role: string;
456
+ }
457
+
458
+ const initialData: User[] = [
459
+ { id: 1, name: 'Alice Johnson', email: 'alice@example.com', role: 'Engineer' },
460
+ { id: 2, name: 'Bob Smith', email: 'bob@example.com', role: 'Designer' },
461
+ { id: 3, name: 'Carol Williams', email: 'carol@example.com', role: 'Manager' },
462
+ ];
463
+
464
+ const allRows: User[] = [
465
+ ...initialData,
466
+ { id: 4, name: 'David Brown', email: 'david@example.com', role: 'Engineer' },
467
+ { id: 5, name: 'Eve Davis', email: 'eve@example.com', role: 'Designer' },
468
+ { id: 6, name: 'Frank Miller', email: 'frank@example.com', role: 'Engineer' },
469
+ { id: 7, name: 'Grace Wilson', email: 'grace@example.com', role: 'Manager' },
470
+ { id: 8, name: 'Henry Taylor', email: 'henry@example.com', role: 'Designer' },
471
+ ];
472
+
473
+ const data = ref<User[]>([...initialData]);
474
+
475
+ const builder = CoarGridBuilder.create<User>()
476
+ .columns([
477
+ (col) => col.field('name').header('Name').flex(1),
478
+ (col) => col.field('email').header('Email').flex(1),
479
+ (col) => col.field('role').header('Role').width(120),
480
+ ])
481
+ .rowDataRef(data);
482
+
483
+ function addRow() {
484
+ const next = allRows[data.value.length % allRows.length];
485
+ if (next) {
486
+ data.value = [...data.value, { ...next, id: Date.now() }];
487
+ }
488
+ }
489
+
490
+ function reset() {
491
+ data.value = [...initialData];
492
+ }
493
+ </script>
494
+ ```
495
+
496
+ ## Search (Quick Filter)
497
+
498
+ Enable the built-in search bar with `show-search`. It wires the search input to the builder's quick filter automatically.
499
+
500
+ **Demo — `data-grid/demos/GridSearchPanel.vue`**
501
+
502
+ ```vue
503
+ <template>
504
+ <div style="height: 400px;">
505
+ <CoarDataGrid
506
+ :builder="builder"
507
+ show-search
508
+ search-placeholder="Search users..."
509
+ >
510
+ <template #toolbar-right>
511
+ <CoarButton size="s" variant="secondary" @click="addUser">Add User</CoarButton>
512
+ </template>
513
+ </CoarDataGrid>
514
+ </div>
515
+ </template>
516
+
517
+ <script setup lang="ts">
518
+ import { ref } from 'vue';
519
+ import { CoarDataGrid, CoarGridBuilder } from '@cocoar/vue-data-grid';
520
+ import { CoarButton } from '@cocoar/vue-ui';
521
+
522
+ interface User {
523
+ name: string;
524
+ email: string;
525
+ role: string;
526
+ department: string;
527
+ }
528
+
529
+ const users = ref<User[]>([
530
+ { name: 'Alice Johnson', email: 'alice@example.com', role: 'Engineer', department: 'Platform' },
531
+ { name: 'Bob Smith', email: 'bob@example.com', role: 'Designer', department: 'Product' },
532
+ { name: 'Carol Williams', email: 'carol@example.com', role: 'Manager', department: 'Platform' },
533
+ { name: 'David Brown', email: 'david@example.com', role: 'Engineer', department: 'Mobile' },
534
+ { name: 'Eve Davis', email: 'eve@example.com', role: 'Designer', department: 'Product' },
535
+ { name: 'Frank Miller', email: 'frank@example.com', role: 'Engineer', department: 'Mobile' },
536
+ { name: 'Grace Wilson', email: 'grace@example.com', role: 'Manager', department: 'Product' },
537
+ { name: 'Henry Taylor', email: 'henry@example.com', role: 'Designer', department: 'Platform' },
538
+ ]);
539
+
540
+ const builder = CoarGridBuilder.create<User>()
541
+ .columns([
542
+ (col) => col.field('name').header('Name').flex(1).sortable(),
543
+ (col) => col.field('email').header('Email').flex(1),
544
+ (col) => col.field('role').header('Role').width(120),
545
+ (col) => col.field('department').header('Department').width(130),
546
+ ])
547
+ .rowDataRef(users)
548
+ .searchHighlight();
549
+
550
+ let counter = 0;
551
+ function addUser() {
552
+ counter++;
553
+ users.value = [
554
+ ...users.value,
555
+ { name: `New User ${counter}`, email: `new${counter}@example.com`, role: 'Engineer', department: 'Platform' },
556
+ ];
557
+ }
558
+ </script>
559
+ ```
560
+
561
+ ### Custom Layout
562
+
563
+ Use `CoarDataGridSearch` and `CoarDataGrid` separately for full layout control. Connect them via `builder.quickFilterText(ref)`.
564
+
565
+ **Demo — `data-grid/demos/GridSearchCustom.vue`**
566
+
567
+ ```vue
568
+ <template>
569
+ <div style="height: 400px; display: flex; flex-direction: column; gap: 8px;">
570
+ <h4 style="margin: 0;">Custom Layout</h4>
571
+ <CoarDataGridSearch v-model="search" placeholder="Filter by name or tags...">
572
+ <CoarButton size="s" variant="secondary" @click="showAll = !showAll">
573
+ {{ showAll ? 'Active Only' : 'Show All' }}
574
+ </CoarButton>
575
+ </CoarDataGridSearch>
576
+ <CoarDataGrid :builder="builder" />
577
+ </div>
578
+ </template>
579
+
580
+ <script setup lang="ts">
581
+ import { ref } from 'vue';
582
+ import { CoarDataGrid, CoarDataGridSearch, CoarGridBuilder } from '@cocoar/vue-data-grid';
583
+ import { CoarButton } from '@cocoar/vue-ui';
584
+
585
+ interface Task {
586
+ title: string;
587
+ status: string;
588
+ tags: string[];
589
+ assignee: string;
590
+ }
591
+
592
+ const tasks: Task[] = [
593
+ { title: 'Fix login bug', status: 'active', tags: ['bug', 'urgent'], assignee: 'Alice' },
594
+ { title: 'Add dark mode', status: 'active', tags: ['feature', 'ui'], assignee: 'Bob' },
595
+ { title: 'Update docs', status: 'done', tags: ['docs'], assignee: 'Carol' },
596
+ { title: 'Refactor API', status: 'active', tags: ['refactor', 'backend'], assignee: 'David' },
597
+ { title: 'Fix typo in header', status: 'done', tags: ['bug'], assignee: 'Eve' },
598
+ { title: 'Add search to grid', status: 'active', tags: ['feature', 'ui'], assignee: 'Frank' },
599
+ ];
600
+
601
+ const search = ref('');
602
+ const showAll = ref(true);
603
+
604
+ const builder = CoarGridBuilder.create<Task>()
605
+ .columns([
606
+ (col) => col.field('title').header('Title').flex(1),
607
+ (col) => col.field('status').header('Status').width(100),
608
+ (col) => col.field('tags').header('Tags').flex(1)
609
+ .valueFormatter((p) => p.value?.join(', ') ?? '')
610
+ .quickFilter((tags) => tags.join(' ')),
611
+ (col) => col.field('assignee').header('Assignee').width(120),
612
+ ])
613
+ .rowData(tasks)
614
+ .quickFilterText(search)
615
+ .externalFilter((node) => showAll.value || node.data?.status === 'active')
616
+ .updateExternalFilterWhen(showAll);
617
+ </script>
618
+ ```
619
+
620
+ ### Per-Column Configuration
621
+
622
+ Control how each column participates in quick filtering:
623
+
624
+ ```ts
625
+ builder.columns([
626
+ // Default: searches by String(value)
627
+ (col) => col.field('name').header('Name'),
628
+
629
+ // Custom text extraction (e.g., for arrays or objects)
630
+ (col) => col.field('tags').quickFilter((tags) => tags.map(t => t.label).join(' ')),
631
+
632
+ // Exclude from search
633
+ (col) => col.field('id').quickFilter(false),
634
+ ]);
635
+ ```
636
+
637
+ ### Custom Filter Function
638
+
639
+ Override the default per-column matching with a fully custom filter:
640
+
641
+ ```ts
642
+ builder.quickFilterFn((searchValue, data) => {
643
+ // searchValue is already lowercased and trimmed
644
+ return data.name.toLowerCase().includes(searchValue)
645
+ || data.email.toLowerCase().includes(searchValue);
646
+ });
647
+ ```
648
+
649
+ ### Search Highlighting
650
+
651
+ Enable text highlighting in grid cells using the CSS Custom Highlight API. Matching text is underlined without modifying the DOM.
652
+
653
+ ```ts
654
+ builder
655
+ .quickFilterText(searchRef)
656
+ .searchHighlight()
657
+ ```
658
+
659
+ The highlight style can be customized via CSS:
660
+
661
+ ```css
662
+ ::highlight(coar-search) {
663
+ text-decoration: underline;
664
+ text-decoration-color: #0066cc;
665
+ }
666
+ ```
667
+
668
+ ## I18n Headers
669
+
670
+ Column headers support runtime language switching via `@cocoar/vue-localization`. Pass a fallback text and an optional translation key:
671
+
672
+ ```ts
673
+ builder.columns([
674
+ // Static header
675
+ (col) => col.field('name').header('Name'),
676
+
677
+ // With i18n — falls back to 'Name' if no translation found
678
+ (col) => col.field('name').header('Name', 'todo.grid.header.title'),
679
+ ])
680
+ ```
681
+
682
+ If `@cocoar/vue-localization` is not installed, the fallback text is always shown. Headers update automatically when the language changes at runtime.
683
+
684
+ ## Auto Size
685
+
686
+ Control how columns are sized initially:
687
+
688
+ ```ts
689
+ // Columns fill the grid width (most common)
690
+ builder.autoSize('fitGridWidth')
691
+
692
+ // Columns fit their content
693
+ builder.autoSize('fitCellContents')
694
+ ```
695
+
696
+ ## Tree Drag & Drop
697
+
698
+ Move rows between parents via drag & drop. Use `.rowDrag()` on the tree column, `.rowDragHighlight()` for visual feedback, and `.onRowDragEnd()` to handle the reparenting.
699
+
700
+ **Demo — `data-grid/demos/GridTreeDrag.vue`**
701
+
702
+ ```vue
703
+ <template>
704
+ <div style="height: 450px; display: flex; flex-direction: column; gap: 8px;">
705
+ <label style="display: flex; align-items: center; gap: 6px; font-size: 0.9em;">
706
+ <input type="checkbox" v-model="maxTwoLevels" />
707
+ Limit to 2 levels (no nesting into children)
708
+ </label>
709
+ <CoarDataGrid :builder="builder" />
710
+ <div v-if="lastAction" style="font-size: 0.85em; color: #666;">
711
+ {{ lastAction }}
712
+ </div>
713
+ </div>
714
+ </template>
715
+
716
+ <script setup lang="ts">
717
+ import { ref } from 'vue';
718
+ import { CoarDataGrid, CoarGridBuilder } from '@cocoar/vue-data-grid';
719
+
720
+ const maxTwoLevels = ref(false);
721
+
722
+ interface Task {
723
+ id: string;
724
+ title: string;
725
+ status: string;
726
+ children?: Task[];
727
+ }
728
+
729
+ const tasks = ref<Task[]>([
730
+ {
731
+ id: '1', title: 'Frontend', status: 'active', children: [
732
+ { id: '1-1', title: 'Login page', status: 'done' },
733
+ { id: '1-2', title: 'Dashboard', status: 'active' },
734
+ ],
735
+ },
736
+ {
737
+ id: '2', title: 'Backend', status: 'active', children: [
738
+ { id: '2-1', title: 'Auth API', status: 'done' },
739
+ { id: '2-2', title: 'User API', status: 'active' },
740
+ { id: '2-3', title: 'Settings API', status: 'active' },
741
+ ],
742
+ },
743
+ {
744
+ id: '3', title: 'Testing', status: 'active', children: [
745
+ { id: '3-1', title: 'Unit tests', status: 'active' },
746
+ ],
747
+ },
748
+ ]);
749
+
750
+ const openRows = ref(['1', '2', '3']);
751
+ const lastAction = ref('');
752
+
753
+ function findAndRemove(items: Task[], id: string): Task | undefined {
754
+ for (let i = 0; i < items.length; i++) {
755
+ if (items[i].id === id) return items.splice(i, 1)[0];
756
+ if (items[i].children) {
757
+ const found = findAndRemove(items[i].children!, id);
758
+ if (found) return found;
759
+ }
760
+ }
761
+ return undefined;
762
+ }
763
+
764
+ function findById(items: Task[], id: string): Task | undefined {
765
+ for (const item of items) {
766
+ if (item.id === id) return item;
767
+ if (item.children) {
768
+ const found = findById(item.children, id);
769
+ if (found) return found;
770
+ }
771
+ }
772
+ return undefined;
773
+ }
774
+
775
+ function isDescendantOf(items: Task[], ancestorId: string, targetId: string): boolean {
776
+ const ancestor = findById(items, ancestorId);
777
+ if (!ancestor?.children) return false;
778
+ return !!findById(ancestor.children, targetId);
779
+ }
780
+
781
+ const builder = CoarGridBuilder.create<Task>()
782
+ .columns([
783
+ (col) => col.tree('title').header('Task').flex(1).rowDrag(),
784
+ (col) => col.field('status').header('Status').width(100),
785
+ ])
786
+ .treeData({
787
+ children: (row) => row.children ?? [],
788
+ rowId: (row) => row.id,
789
+ })
790
+ .openRows(openRows)
791
+ .rowDataRef(tasks)
792
+ .rowDragHighlight({
793
+ canDrop: (dragged, target) => {
794
+ if (dragged.id === target.id) return false;
795
+ if (isDescendantOf(tasks.value, dragged.id, target.id)) return false;
796
+ // When "max 2 levels" is on, only allow drop on root items
797
+ if (maxTwoLevels.value) {
798
+ const targetMeta = builder.getTreeMeta(target.id);
799
+ if (targetMeta && targetMeta.depth > 0) return false;
800
+ }
801
+ return true;
802
+ },
803
+ })
804
+ .onRowDragEnd((event) => {
805
+ const dragged = event.node.data;
806
+ const target = event.overNode?.data;
807
+ if (!dragged) return;
808
+
809
+ const clone = JSON.parse(JSON.stringify(tasks.value)) as Task[];
810
+ const movedItem = findAndRemove(clone, dragged.id);
811
+ if (!movedItem) return;
812
+
813
+ if (!target) {
814
+ clone.push(movedItem);
815
+ tasks.value = clone;
816
+ lastAction.value = `Moved "${movedItem.title}" to root level`;
817
+ return;
818
+ }
819
+
820
+ if (dragged.id === target.id) return;
821
+ if (isDescendantOf(tasks.value, dragged.id, target.id)) return;
822
+ if (maxTwoLevels.value) {
823
+ const targetMeta = builder.getTreeMeta(target.id);
824
+ if (targetMeta && targetMeta.depth > 0) return;
825
+ }
826
+
827
+ const targetItem = findById(clone, target.id);
828
+ if (!targetItem) return;
829
+
830
+ if (!targetItem.children) targetItem.children = [];
831
+ targetItem.children.push(movedItem);
832
+
833
+ if (!openRows.value.includes(target.id)) {
834
+ openRows.value = [...openRows.value, target.id];
835
+ }
836
+
837
+ tasks.value = clone;
838
+ lastAction.value = `Moved "${movedItem.title}" into "${targetItem.title}"`;
839
+ });
840
+ </script>
841
+ ```
842
+
843
+ ```ts
844
+ builder
845
+ .treeData({ children: (r) => r.children ?? [], rowId: (r) => r.id })
846
+ .openRows(openRows)
847
+ .rowDragHighlight()
848
+ .onRowDragEnd((event) => {
849
+ const dragged = event.node.data;
850
+ const target = event.overNode?.data;
851
+ if (!dragged || !target) return;
852
+ // API call or store mutation to reparent
853
+ api.moveInto(dragged.id, target.id);
854
+ });
855
+ ```
856
+
857
+ ## Tree Data
858
+
859
+ Display hierarchical data with expand/collapse. Use `treeData()` with nested children arrays and `openRows()` to control expansion. The `tree()` column type renders indentation, chevron toggle, and child count.
860
+
861
+ Search automatically expands matching branches — a parent stays visible when any descendant matches.
862
+
863
+ **Demo — `data-grid/demos/GridTreeData.vue`**
864
+
865
+ ```vue
866
+ <template>
867
+ <div style="height: 450px;">
868
+ <CoarDataGrid
869
+ :builder="builder"
870
+ show-search
871
+ search-placeholder="Search files..."
872
+ />
873
+ </div>
874
+ </template>
875
+
876
+ <script setup lang="ts">
877
+ import { ref } from 'vue';
878
+ import { CoarDataGrid, CoarGridBuilder } from '@cocoar/vue-data-grid';
879
+
880
+ interface FileNode {
881
+ id: string;
882
+ name: string;
883
+ type: 'folder' | 'file';
884
+ size?: string;
885
+ modified: string;
886
+ children?: FileNode[];
887
+ }
888
+
889
+ const files: FileNode[] = [
890
+ {
891
+ id: '1', name: 'src', type: 'folder', modified: '2024-03-15', children: [
892
+ {
893
+ id: '1-1', name: 'components', type: 'folder', modified: '2024-03-14', children: [
894
+ { id: '1-1-1', name: 'Button.vue', type: 'file', size: '2.4 KB', modified: '2024-03-10' },
895
+ { id: '1-1-2', name: 'Input.vue', type: 'file', size: '3.1 KB', modified: '2024-03-12' },
896
+ { id: '1-1-3', name: 'Dialog.vue', type: 'file', size: '5.8 KB', modified: '2024-03-14' },
897
+ ],
898
+ },
899
+ {
900
+ id: '1-2', name: 'utils', type: 'folder', modified: '2024-03-13', children: [
901
+ { id: '1-2-1', name: 'format.ts', type: 'file', size: '1.2 KB', modified: '2024-03-13' },
902
+ { id: '1-2-2', name: 'validate.ts', type: 'file', size: '0.8 KB', modified: '2024-03-11' },
903
+ ],
904
+ },
905
+ { id: '1-3', name: 'App.vue', type: 'file', size: '1.5 KB', modified: '2024-03-15' },
906
+ { id: '1-4', name: 'main.ts', type: 'file', size: '0.3 KB', modified: '2024-03-01' },
907
+ ],
908
+ },
909
+ {
910
+ id: '2', name: 'public', type: 'folder', modified: '2024-02-20', children: [
911
+ { id: '2-1', name: 'favicon.ico', type: 'file', size: '4.2 KB', modified: '2024-01-15' },
912
+ { id: '2-2', name: 'index.html', type: 'file', size: '0.5 KB', modified: '2024-02-20' },
913
+ ],
914
+ },
915
+ { id: '3', name: 'package.json', type: 'file', size: '1.1 KB', modified: '2024-03-15' },
916
+ { id: '4', name: 'tsconfig.json', type: 'file', size: '0.4 KB', modified: '2024-01-10' },
917
+ { id: '5', name: 'README.md', type: 'file', size: '2.0 KB', modified: '2024-03-05' },
918
+ ];
919
+
920
+ const openRows = ref<string[]>(['1']);
921
+
922
+ const builder = CoarGridBuilder.create<FileNode>()
923
+ .columns([
924
+ (col) => col.tree('name').header('Name').flex(2),
925
+ (col) => col.field('type').header('Type').width(90),
926
+ (col) => col.field('size').header('Size').width(100),
927
+ (col) => col.field('modified').header('Modified').width(130),
928
+ ])
929
+ .treeData({
930
+ children: (row) => row.children ?? [],
931
+ rowId: (row) => row.id,
932
+ })
933
+ .openRows(openRows)
934
+ .rowData(files)
935
+ .searchHighlight();
936
+ </script>
937
+ ```
938
+
939
+ ```ts
940
+ builder
941
+ .treeData({
942
+ children: (row) => row.children ?? [],
943
+ rowId: (row) => row.id,
944
+ })
945
+ .openRows(openRowsRef)
946
+ .columns([
947
+ (col) => col.tree('name').header('Name').flex(1), // tree column
948
+ (col) => col.field('size').header('Size').width(100),
949
+ ])
950
+ ```
951
+
952
+ ## Row Drag & Drop
953
+
954
+ Reorder rows via drag & drop. Use `.rowDrag()` on a column to show the drag handle, and `.rowDragManaged()` on the builder. Dragging is automatically disabled when a column sort is active.
955
+
956
+ **Demo — `data-grid/demos/GridRowDrag.vue`**
957
+
958
+ ```vue
959
+ <template>
960
+ <div style="height: 350px; display: flex; flex-direction: column; gap: 8px;">
961
+ <CoarDataGrid :builder="builder" />
962
+ <div v-if="lastOrder" style="font-size: 0.85em; color: #666;">
963
+ Order: {{ lastOrder }}
964
+ </div>
965
+ </div>
966
+ </template>
967
+
968
+ <script setup lang="ts">
969
+ import { ref } from 'vue';
970
+ import { CoarDataGrid, CoarGridBuilder } from '@cocoar/vue-data-grid';
971
+
972
+ interface Step {
973
+ id: number;
974
+ title: string;
975
+ priority: string;
976
+ }
977
+
978
+ const steps = ref<Step[]>([
979
+ { id: 1, title: 'Gather requirements', priority: 'High' },
980
+ { id: 2, title: 'Create wireframes', priority: 'Medium' },
981
+ { id: 3, title: 'Implement prototype', priority: 'High' },
982
+ { id: 4, title: 'User testing', priority: 'Medium' },
983
+ { id: 5, title: 'Final review', priority: 'Low' },
984
+ ]);
985
+
986
+ const lastOrder = ref('');
987
+
988
+ const builder = CoarGridBuilder.create<Step>()
989
+ .columns([
990
+ (col) => col.field('title').header('Step').flex(1).rowDrag(),
991
+ (col) => col.field('priority').header('Priority').width(120).sortable(),
992
+ ])
993
+ .rowDataRef(steps)
994
+ .rowId((p) => String(p.data.id))
995
+ .rowDragManaged()
996
+ .onRowDragEnd(() => {
997
+ const newOrder = builder.getDisplayedRowData();
998
+ lastOrder.value = newOrder.map((s) => s.title).join(' → ');
999
+ // In a real app: store.updateOrder(newOrder) or api.reorder(newOrder)
1000
+ });
1001
+ </script>
1002
+ ```
1003
+
1004
+ ```ts
1005
+ builder
1006
+ .columns([
1007
+ (col) => col.field('name').rowDrag().flex(1),
1008
+ ])
1009
+ .rowDragManaged()
1010
+ .onRowDragEnd(() => {
1011
+ const newOrder = builder.getDisplayedRowData();
1012
+ store.updateOrder(newOrder); // persist new order
1013
+ });
1014
+ ```
1015
+
1016
+ ## Sorting
1017
+
1018
+ Make columns sortable and set a default sort order.
1019
+
1020
+ ```ts
1021
+ const builder = CoarGridBuilder.create<Row>()
1022
+ .columns([
1023
+ (col) => col.field('name').header('Name').flex(1).sortable(),
1024
+ (col) => col.number('salary').header('Salary').width(120).sortable(),
1025
+ ])
1026
+ .rowData(data)
1027
+ .defaultSort('name', 'asc');
1028
+ ```
1029
+
1030
+ ## Column Persistence
1031
+
1032
+ Persist column widths, order, visibility, and sort in IndexedDB with `.persistColumnState(key)`.
1033
+
1034
+ **Width buckets:** The grid container width is rounded to buckets (default: 100px). Each bucket gets its own saved column layout, so different container sizes — switching monitors, collapsing a sidebar — each keep their own column widths. When no exact bucket exists, the nearest saved state is applied.
1035
+
1036
+ **Live sync:** Multiple grids with the same key synchronize column changes instantly. Resize, reorder, or hide a column in one grid and all others update immediately. Useful for comparison views with different filters on the same data structure.
1037
+
1038
+ Try it below — resize a column in Team A and watch Team B follow.
1039
+
1040
+ **Demo — `data-grid/demos/GridPersistence.vue`**
1041
+
1042
+ ```vue
1043
+ <template>
1044
+ <div style="display: flex; flex-direction: column; gap: 16px;">
1045
+ <div>
1046
+ <CoarButton size="s" @click="builder.resetPersistedStates()">Reset columns</CoarButton>
1047
+ </div>
1048
+
1049
+ <div style="height: 250px;">
1050
+ <CoarDataGrid :builder="builder" bordered>
1051
+ <template #toolbar-left>
1052
+ <span style="font-weight: 600;">Team A</span>
1053
+ </template>
1054
+ </CoarDataGrid>
1055
+ </div>
1056
+
1057
+ <div style="height: 250px;">
1058
+ <CoarDataGrid :builder="builder2" bordered>
1059
+ <template #toolbar-left>
1060
+ <span style="font-weight: 600;">Team B</span>
1061
+ </template>
1062
+ </CoarDataGrid>
1063
+ </div>
1064
+ </div>
1065
+ </template>
1066
+
1067
+ <script setup lang="ts">
1068
+ import { CoarDataGrid, CoarGridBuilder } from '@cocoar/vue-data-grid';
1069
+ import { CoarButton } from '@cocoar/vue-ui';
1070
+
1071
+ interface User {
1072
+ name: string;
1073
+ email: string;
1074
+ role: string;
1075
+ department: string;
1076
+ }
1077
+
1078
+ const teamA: User[] = [
1079
+ { name: 'Alice Johnson', email: 'alice@example.com', role: 'Engineer', department: 'Platform' },
1080
+ { name: 'Bob Smith', email: 'bob@example.com', role: 'Designer', department: 'Platform' },
1081
+ { name: 'Carol Williams', email: 'carol@example.com', role: 'Manager', department: 'Platform' },
1082
+ ];
1083
+
1084
+ const teamB: User[] = [
1085
+ { name: 'David Brown', email: 'david@example.com', role: 'Engineer', department: 'Product' },
1086
+ { name: 'Eve Davis', email: 'eve@example.com', role: 'Designer', department: 'Product' },
1087
+ { name: 'Frank Miller', email: 'frank@example.com', role: 'Manager', department: 'Product' },
1088
+ ];
1089
+
1090
+ const columns = [
1091
+ (col: any) => col.field('name').header('Name').flex(1),
1092
+ (col: any) => col.field('email').header('Email').flex(1),
1093
+ (col: any) => col.field('role').header('Role').width(120),
1094
+ (col: any) => col.field('department').header('Dept').width(120),
1095
+ ];
1096
+
1097
+ const builder = CoarGridBuilder.create<User>()
1098
+ .persistColumnState('docs-persistence-demo')
1099
+ .columns(columns)
1100
+ .rowData(teamA);
1101
+
1102
+ const builder2 = CoarGridBuilder.create<User>()
1103
+ .persistColumnState('docs-persistence-demo')
1104
+ .columns(columns)
1105
+ .rowData(teamB);
1106
+ </script>
1107
+ ```
1108
+
1109
+ ```ts
1110
+ const builder = CoarGridBuilder.create<User>()
1111
+ .persistColumnState('my-users-grid')
1112
+ .columns([...])
1113
+
1114
+ // Optional: custom bucket size and debounce
1115
+ .persistColumnState('my-grid', { bucketSize: 200, debounceMs: 1000 })
1116
+
1117
+ // Reset current bucket
1118
+ builder.resetPersistedState()
1119
+
1120
+ // Reset all buckets
1121
+ builder.resetPersistedStates()
1122
+ ```
1123
+
1124
+ ### Cleanup
1125
+
1126
+ Persisted entries are timestamped on every read and write. Call `cleanupColumnStates()` once at application startup to remove stale entries and prevent unbounded growth:
1127
+
1128
+ ```ts
1129
+ // main.ts
1130
+ import { cleanupColumnStates } from '@cocoar/vue-data-grid';
1131
+
1132
+ cleanupColumnStates(180); // Remove entries older than 6 months
1133
+ ```
1134
+
1135
+ ## API
1136
+
1137
+ ### CoarDataGrid Props
1138
+
1139
+ | Prop | Type | Default | Description |
1140
+ |------|------|---------|-------------|
1141
+ | `builder` | `CoarGridBuilder<T>` | — | Grid configuration builder (required) |
1142
+ | `theme` | `Theme` | `cocoarTheme` | AG Grid theme override |
1143
+ | `showSearch` | `boolean` | `false` | Show the search bar in the toolbar |
1144
+ | `searchPlaceholder` | `string` | `'Search...'` | Placeholder for the search input |
1145
+ | `searchSize` | `'xs' \| 's' \| 'm' \| 'l'` | `'m'` | Search input size |
1146
+ | `search` | `string` | `''` | Search text (`v-model:search`) |
1147
+ | `bordered` | `boolean` | `false` | Show a border around the grid |
1148
+ | `elevated` | `boolean` | `false` | Add elevation shadow |
1149
+
1150
+ ### CoarDataGrid Slots
1151
+
1152
+ | Slot | Description |
1153
+ |------|-------------|
1154
+ | `toolbar-left` | Content on the left side of the toolbar (e.g., title, icon) |
1155
+ | `toolbar-right` | Content on the right side of the toolbar (e.g., buttons, actions) |
1156
+
1157
+ The toolbar appears automatically when `showSearch` is enabled or any `toolbar-*` slot is used. The search input fills available space (`flex: 1`). When search is disabled, a spacer pushes `toolbar-right` to the far right.
1158
+
1159
+ ```vue
1160
+ <!-- Search + actions -->
1161
+ <CoarDataGrid :builder="builder" show-search bordered>
1162
+ <template #toolbar-left>
1163
+ <span style="font-weight: 600;">Users</span>
1164
+ </template>
1165
+ <template #toolbar-right>
1166
+ <CoarButton size="s">Add User</CoarButton>
1167
+ </template>
1168
+ </CoarDataGrid>
1169
+
1170
+ <!-- Only toolbar actions, no search -->
1171
+ <CoarDataGrid :builder="builder">
1172
+ <template #toolbar-right>
1173
+ <CoarButton size="s">Export</CoarButton>
1174
+ </template>
1175
+ </CoarDataGrid>
1176
+ ```
1177
+
1178
+ ### CoarGridBuilder Methods
1179
+
1180
+ | Method | Parameters | Description |
1181
+ |--------|-----------|-------------|
1182
+ | `.columns(defs)` | `ColumnDefFn<T>[]` | Define column configuration |
1183
+ | `.rowData(data)` | `T[]` | Set static row data |
1184
+ | `.rowDataRef(ref)` | `Ref<T[]>` | Bind reactive row data |
1185
+ | `.quickFilterText(ref)` | `Ref<string>` | Bind search text for quick filtering |
1186
+ | `.quickFilterFn(fn)` | `(search, data) => boolean` | Custom filter function override |
1187
+ | `.searchHighlight()` | — | Highlight matching text via CSS Custom Highlight API |
1188
+ | `.rowDragManaged()` | — | Enable managed drag & drop reordering |
1189
+ | `.onRowDragEnd(fn)` | `(event) => void` | Handle drag end, persist new order |
1190
+ | `.rowDragHighlight(opts?)` | `{ canDrop? }` | Visual drop target feedback with validation |
1191
+ | `.getDisplayedRowData()` | — | Get row data in current display order |
1192
+ | `.getTreeMeta(rowId)` | `string` | Get tree node depth, children info |
1193
+ | `.treeData(config)` | `TreeDataConfig<T>` | Enable tree mode with nested children |
1194
+ | `.openRows(ref)` | `Ref<string[]>` | Reactive ref of expanded row IDs |
1195
+ | `.autoSize(strategy)` | `'fitGridWidth' \| 'fitCellContents'` | Column auto-sizing strategy |
1196
+ | `.rowSelection(mode, opts?)` | `'single' \| 'multiple'` | Enable row selection |
1197
+ | `.defaultSort(field, dir)` | `string, 'asc' \| 'desc'` | Set default sort column |
1198
+ | `.persistColumnState(key, opts?)` | `string, ColumnPersistenceOptions?` | Persist column state in IndexedDB with width-based buckets |
1199
+ | `.resetPersistedState(bucket?)` | `number?` | Reset persisted state for a specific bucket (defaults to current) |
1200
+ | `.resetPersistedStates()` | — | Reset all persisted column states (all buckets) |
1201
+ | `.rowClassRules(rules)` | `RowClassRules<T>` | Conditional row CSS classes |
1202
+
1203
+ ### Standalone Functions
1204
+
1205
+ | Function | Parameters | Description |
1206
+ |----------|-----------|-------------|
1207
+ | `cleanupColumnStates(maxAgeDays)` | `number` | Remove persisted column states older than `maxAgeDays`. Call at app startup. |
1208
+
1209
+ ### CoarGridColumnBuilder Methods
1210
+
1211
+ | Method | Parameters | Description |
1212
+ |--------|-----------|-------------|
1213
+ | `.field(name)` | `keyof T` | Set column data field |
1214
+ | `.header(text, i18nKey?)` | `string, string?` | Set header text with optional i18n key |
1215
+ | `.flex(value)` | `number` | Flexible column width |
1216
+ | `.width(px)` | `number` | Fixed column width |
1217
+ | `.fixedWidth(px)` | `number` | Non-resizable fixed width |
1218
+ | `.sortable()` | — | Enable column sorting |
1219
+ | `.quickFilter(fn)` | `boolean \| (value, data) => string` | Configure quick filter for column |
1220
+ | `.date(field, config?)` | `keyof T, DateCellRendererConfig?` | Locale-aware date cell renderer |
1221
+ | `.number(field, config?)` | `keyof T, NumberCellRendererConfig?` | Locale-aware number cell renderer |
1222
+ | `.currency(field, config?)` | `keyof T, CurrencyCellRendererConfig?` | Locale-aware currency cell renderer |
1223
+ | `.tag(field, config)` | `keyof T, TagConfig` | Tag cell renderer |
1224
+ | `.icon(field, config?)` | `keyof T, IconConfig?` | Icon cell renderer |
1225
+ | `.tree(field, config?)` | `keyof T, TreeCellRendererConfig?` | Tree column with expand/collapse |