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

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 (179) hide show
  1. package/dist/components/checkbox/CoarCheckbox.vue.d.ts +1 -1
  2. package/dist/components/checkbox/CoarCheckbox.vue.d.ts.map +1 -1
  3. package/dist/components/code-block/CoarCodeBlock.vue.d.ts +1 -1
  4. package/dist/components/code-block/CoarCodeBlock.vue.d.ts.map +1 -1
  5. package/dist/components/data-list/CoarDataList.vue.d.ts +158 -0
  6. package/dist/components/data-list/CoarDataList.vue.d.ts.map +1 -0
  7. package/dist/components/data-list/CoarDataListToolbar.vue.d.ts +30 -0
  8. package/dist/components/data-list/CoarDataListToolbar.vue.d.ts.map +1 -0
  9. package/dist/components/data-list/data-list-builder.d.ts +288 -0
  10. package/dist/components/data-list/data-list-builder.d.ts.map +1 -0
  11. package/dist/components/data-list/index.d.ts +12 -0
  12. package/dist/components/data-list/index.d.ts.map +1 -0
  13. package/dist/components/data-list/internal/compare.d.ts +14 -0
  14. package/dist/components/data-list/internal/compare.d.ts.map +1 -0
  15. package/dist/components/data-list/internal/reorder-core.d.ts +46 -0
  16. package/dist/components/data-list/internal/reorder-core.d.ts.map +1 -0
  17. package/dist/components/data-list/internal/search.d.ts +16 -0
  18. package/dist/components/data-list/internal/search.d.ts.map +1 -0
  19. package/dist/components/data-list/internal/useDataListLines.d.ts +92 -0
  20. package/dist/components/data-list/internal/useDataListLines.d.ts.map +1 -0
  21. package/dist/components/data-list/internal/useDataListReorder.d.ts +68 -0
  22. package/dist/components/data-list/internal/useDataListReorder.d.ts.map +1 -0
  23. package/dist/components/data-list/internal/useSearchHighlight.d.ts +19 -0
  24. package/dist/components/data-list/internal/useSearchHighlight.d.ts.map +1 -0
  25. package/dist/components/data-list/types.d.ts +185 -0
  26. package/dist/components/data-list/types.d.ts.map +1 -0
  27. package/dist/components/data-list/useDataListModel.d.ts +79 -0
  28. package/dist/components/data-list/useDataListModel.d.ts.map +1 -0
  29. package/dist/components/date-time/_shared/maskito-config.d.ts +7 -2
  30. package/dist/components/date-time/_shared/maskito-config.d.ts.map +1 -1
  31. package/dist/components/date-time/_shared/time-helpers.d.ts +11 -4
  32. package/dist/components/date-time/_shared/time-helpers.d.ts.map +1 -1
  33. package/dist/components/date-time/_shared/use-date-picker-base.d.ts +1 -1
  34. package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts +1 -1
  35. package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts.map +1 -1
  36. package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts +1 -1
  37. package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts.map +1 -1
  38. package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts +1 -1
  39. package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts.map +1 -1
  40. package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts +3 -0
  41. package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts.map +1 -1
  42. package/dist/components/form-field/CoarFormField.vue.d.ts +1 -1
  43. package/dist/components/form-field/CoarFormField.vue.d.ts.map +1 -1
  44. package/dist/components/icon/CoarIcon.vue.d.ts +2 -2
  45. package/dist/components/icon/CoarIcon.vue.d.ts.map +1 -1
  46. package/dist/components/listbox/CoarListbox.vue.d.ts +7 -0
  47. package/dist/components/listbox/CoarListbox.vue.d.ts.map +1 -1
  48. package/dist/components/number-input/CoarNumberInput.vue.d.ts +1 -1
  49. package/dist/components/number-input/CoarNumberInput.vue.d.ts.map +1 -1
  50. package/dist/components/otp-input/CoarOtpInput.vue.d.ts +1 -1
  51. package/dist/components/otp-input/CoarOtpInput.vue.d.ts.map +1 -1
  52. package/dist/components/password-input/CoarPasswordInput.vue.d.ts +1 -1
  53. package/dist/components/password-input/CoarPasswordInput.vue.d.ts.map +1 -1
  54. package/dist/components/switch/CoarSwitch.vue.d.ts +1 -1
  55. package/dist/components/switch/CoarSwitch.vue.d.ts.map +1 -1
  56. package/dist/components/text-input/CoarTextInput.vue.d.ts +1 -1
  57. package/dist/components/text-input/CoarTextInput.vue.d.ts.map +1 -1
  58. package/dist/composables/dragRegistry.d.ts +12 -0
  59. package/dist/composables/dragRegistry.d.ts.map +1 -1
  60. package/dist/composables/index.d.ts +2 -2
  61. package/dist/composables/index.d.ts.map +1 -1
  62. package/dist/composables/useDragDrop.d.ts +62 -6
  63. package/dist/composables/useDragDrop.d.ts.map +1 -1
  64. package/dist/composables/useVirtualList.d.ts +21 -1
  65. package/dist/composables/useVirtualList.d.ts.map +1 -1
  66. package/dist/index.css +1 -1
  67. package/dist/index.d.ts +4 -2
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +4508 -2039
  70. package/package.json +5 -3
  71. package/skills/cocoar-vue-ui/SKILL.md +244 -0
  72. package/skills/cocoar-vue-ui/references/components/avatar.md +178 -0
  73. package/skills/cocoar-vue-ui/references/components/badge.md +290 -0
  74. package/skills/cocoar-vue-ui/references/components/breadcrumb.md +238 -0
  75. package/skills/cocoar-vue-ui/references/components/button.md +252 -0
  76. package/skills/cocoar-vue-ui/references/components/calendar/agenda-view.md +353 -0
  77. package/skills/cocoar-vue-ui/references/components/calendar/coar-calendar.md +1645 -0
  78. package/skills/cocoar-vue-ui/references/components/calendar/day-view.md +361 -0
  79. package/skills/cocoar-vue-ui/references/components/calendar/index.md +312 -0
  80. package/skills/cocoar-vue-ui/references/components/calendar/month-view.md +475 -0
  81. package/skills/cocoar-vue-ui/references/components/calendar/performance.md +186 -0
  82. package/skills/cocoar-vue-ui/references/components/calendar/timeline-view.md +301 -0
  83. package/skills/cocoar-vue-ui/references/components/calendar/week-view.md +382 -0
  84. package/skills/cocoar-vue-ui/references/components/calendar/work-week-view.md +145 -0
  85. package/skills/cocoar-vue-ui/references/components/calendar/year-view.md +63 -0
  86. package/skills/cocoar-vue-ui/references/components/card.md +151 -0
  87. package/skills/cocoar-vue-ui/references/components/checkbox-group.md +103 -0
  88. package/skills/cocoar-vue-ui/references/components/checkbox.md +279 -0
  89. package/skills/cocoar-vue-ui/references/components/code-block.md +241 -0
  90. package/skills/cocoar-vue-ui/references/components/context-menu.md +355 -0
  91. package/skills/cocoar-vue-ui/references/components/data-grid/checkbox.md +213 -0
  92. package/skills/cocoar-vue-ui/references/components/data-grid/date-columns.md +272 -0
  93. package/skills/cocoar-vue-ui/references/components/data-grid/editing.md +236 -0
  94. package/skills/cocoar-vue-ui/references/components/data-grid/multi-select.md +193 -0
  95. package/skills/cocoar-vue-ui/references/components/data-grid/number.md +147 -0
  96. package/skills/cocoar-vue-ui/references/components/data-grid/select.md +202 -0
  97. package/skills/cocoar-vue-ui/references/components/data-grid/text.md +127 -0
  98. package/skills/cocoar-vue-ui/references/components/data-grid.md +1225 -0
  99. package/skills/cocoar-vue-ui/references/components/data-list.md +2121 -0
  100. package/skills/cocoar-vue-ui/references/components/date-or-time-picker.md +200 -0
  101. package/skills/cocoar-vue-ui/references/components/date-picker.md +203 -0
  102. package/skills/cocoar-vue-ui/references/components/date-time-picker.md +154 -0
  103. package/skills/cocoar-vue-ui/references/components/date-views.md +212 -0
  104. package/skills/cocoar-vue-ui/references/components/dialog.md +148 -0
  105. package/skills/cocoar-vue-ui/references/components/divider.md +157 -0
  106. package/skills/cocoar-vue-ui/references/components/document-viewer/annotations.md +400 -0
  107. package/skills/cocoar-vue-ui/references/components/document-viewer/coar-document-viewer.md +327 -0
  108. package/skills/cocoar-vue-ui/references/components/document-viewer/index.md +234 -0
  109. package/skills/cocoar-vue-ui/references/components/document-viewer/toolbar.md +221 -0
  110. package/skills/cocoar-vue-ui/references/components/drag-drop.md +267 -0
  111. package/skills/cocoar-vue-ui/references/components/dual-listbox.md +449 -0
  112. package/skills/cocoar-vue-ui/references/components/file-explorer/asset-store.md +327 -0
  113. package/skills/cocoar-vue-ui/references/components/file-explorer/in-memory-store.md +176 -0
  114. package/skills/cocoar-vue-ui/references/components/file-explorer/index.md +311 -0
  115. package/skills/cocoar-vue-ui/references/components/file-explorer/use-file-explorer.md +300 -0
  116. package/skills/cocoar-vue-ui/references/components/form-field.md +1025 -0
  117. package/skills/cocoar-vue-ui/references/components/fragment-parser.md +298 -0
  118. package/skills/cocoar-vue-ui/references/components/link.md +235 -0
  119. package/skills/cocoar-vue-ui/references/components/listbox.md +575 -0
  120. package/skills/cocoar-vue-ui/references/components/map/editor.md +448 -0
  121. package/skills/cocoar-vue-ui/references/components/map/index.md +351 -0
  122. package/skills/cocoar-vue-ui/references/components/markdown-diagrams.md +210 -0
  123. package/skills/cocoar-vue-ui/references/components/markdown-editor.md +1478 -0
  124. package/skills/cocoar-vue-ui/references/components/markdown-embeds.md +387 -0
  125. package/skills/cocoar-vue-ui/references/components/markdown-form.md +447 -0
  126. package/skills/cocoar-vue-ui/references/components/markdown.md +276 -0
  127. package/skills/cocoar-vue-ui/references/components/menu.md +380 -0
  128. package/skills/cocoar-vue-ui/references/components/mermaid.md +172 -0
  129. package/skills/cocoar-vue-ui/references/components/navbar.md +147 -0
  130. package/skills/cocoar-vue-ui/references/components/note.md +96 -0
  131. package/skills/cocoar-vue-ui/references/components/notice.md +131 -0
  132. package/skills/cocoar-vue-ui/references/components/number-input.md +215 -0
  133. package/skills/cocoar-vue-ui/references/components/otp-input.md +324 -0
  134. package/skills/cocoar-vue-ui/references/components/page-builder/authoring-contract.md +271 -0
  135. package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-builder.md +476 -0
  136. package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-renderer.md +677 -0
  137. package/skills/cocoar-vue-ui/references/components/page-builder/custom-elements.md +441 -0
  138. package/skills/cocoar-vue-ui/references/components/page-builder/idp-integration.md +122 -0
  139. package/skills/cocoar-vue-ui/references/components/page-builder/index.md +573 -0
  140. package/skills/cocoar-vue-ui/references/components/pagination.md +150 -0
  141. package/skills/cocoar-vue-ui/references/components/panel-layout.md +372 -0
  142. package/skills/cocoar-vue-ui/references/components/password-input.md +175 -0
  143. package/skills/cocoar-vue-ui/references/components/popconfirm.md +186 -0
  144. package/skills/cocoar-vue-ui/references/components/popover.md +173 -0
  145. package/skills/cocoar-vue-ui/references/components/progress-bar.md +178 -0
  146. package/skills/cocoar-vue-ui/references/components/radio-group.md +225 -0
  147. package/skills/cocoar-vue-ui/references/components/script-editor.md +1271 -0
  148. package/skills/cocoar-vue-ui/references/components/segmented-control.md +238 -0
  149. package/skills/cocoar-vue-ui/references/components/select.md +463 -0
  150. package/skills/cocoar-vue-ui/references/components/sidebar.md +421 -0
  151. package/skills/cocoar-vue-ui/references/components/spinner.md +132 -0
  152. package/skills/cocoar-vue-ui/references/components/switch.md +195 -0
  153. package/skills/cocoar-vue-ui/references/components/table.md +170 -0
  154. package/skills/cocoar-vue-ui/references/components/tabs.md +382 -0
  155. package/skills/cocoar-vue-ui/references/components/tag.md +178 -0
  156. package/skills/cocoar-vue-ui/references/components/text-input.md +256 -0
  157. package/skills/cocoar-vue-ui/references/components/toast.md +160 -0
  158. package/skills/cocoar-vue-ui/references/components/tooltip.md +121 -0
  159. package/skills/cocoar-vue-ui/references/components/transitions.md +193 -0
  160. package/skills/cocoar-vue-ui/references/components/tree.md +2388 -0
  161. package/skills/cocoar-vue-ui/references/components/virtual-list.md +212 -0
  162. package/skills/cocoar-vue-ui/references/components/wizard.md +251 -0
  163. package/skills/cocoar-vue-ui/references/components/zoned-date-time-picker.md +177 -0
  164. package/skills/cocoar-vue-ui/references/foundations/colors.md +708 -0
  165. package/skills/cocoar-vue-ui/references/foundations/design-principles.md +115 -0
  166. package/skills/cocoar-vue-ui/references/foundations/icons.md +381 -0
  167. package/skills/cocoar-vue-ui/references/foundations/localization/formatting.md +530 -0
  168. package/skills/cocoar-vue-ui/references/foundations/localization/setup.md +86 -0
  169. package/skills/cocoar-vue-ui/references/foundations/localization/timezones.md +182 -0
  170. package/skills/cocoar-vue-ui/references/foundations/localization/translations.md +305 -0
  171. package/skills/cocoar-vue-ui/references/foundations/motion.md +549 -0
  172. package/skills/cocoar-vue-ui/references/foundations/spacing.md +330 -0
  173. package/skills/cocoar-vue-ui/references/foundations/theming.md +140 -0
  174. package/skills/cocoar-vue-ui/references/foundations/typography.md +206 -0
  175. package/skills/cocoar-vue-ui/references/guide/error-handling.md +162 -0
  176. package/skills/cocoar-vue-ui/references/guide/getting-started.md +119 -0
  177. package/skills/cocoar-vue-ui/references/guide/migration-page-builder-3.md +207 -0
  178. package/skills/cocoar-vue-ui/references/guide/migration.md +140 -0
  179. package/skills/cocoar-vue-ui/references/guide/theming.md +98 -0
@@ -0,0 +1,449 @@
1
+ <!-- Generated from apps/docs/components/dual-listbox.md by apps/docs/scripts/sync-skill.mjs. Do not edit; edit the docs page. -->
2
+
3
+ # Dual Listbox
4
+
5
+ Two [`CoarListbox`](./listbox.md) columns side-by-side with move buttons — the classic pattern for moving items between "available" and "selected" sets. All the search, grouping, and custom-render features of `CoarListbox` carry over automatically.
6
+
7
+ ```ts
8
+ import { CoarDualListbox } from '@cocoar/vue-ui';
9
+ import type { CoarListboxOption } from '@cocoar/vue-ui';
10
+ ```
11
+
12
+ ## Basic
13
+
14
+ `v-model` holds the currently-selected values (right column). Everything else from `options` lives in the left column.
15
+
16
+ **Demo — `dual-listbox/demos/BasicDualListbox.vue`**
17
+
18
+ ```vue
19
+ <template>
20
+ <div style="height: 320px;">
21
+ <CoarDualListbox
22
+ v-model="selected"
23
+ :options="options"
24
+ available-label="Available"
25
+ selected-label="Selected"
26
+ />
27
+ <p style="margin-top: 8px; font-size: 13px; color: #64748b;">
28
+ Selected: {{ selected.join(', ') || 'none' }}
29
+ </p>
30
+ </div>
31
+ </template>
32
+
33
+ <script setup lang="ts">
34
+ import { ref } from 'vue';
35
+ import { CoarDualListbox } from '@cocoar/vue-ui';
36
+ import type { CoarListboxOption } from '@cocoar/vue-ui';
37
+
38
+ const options: CoarListboxOption<string>[] = [
39
+ { value: 'read', label: 'Read' },
40
+ { value: 'write', label: 'Write' },
41
+ { value: 'delete', label: 'Delete' },
42
+ { value: 'admin', label: 'Admin' },
43
+ { value: 'publish', label: 'Publish' },
44
+ { value: 'review', label: 'Review' },
45
+ ];
46
+
47
+ const selected = ref<string[]>(['read']);
48
+ </script>
49
+ ```
50
+
51
+ ## Grouped
52
+
53
+ Group headings appear in both columns. Search and sorting apply per column.
54
+
55
+ **Demo — `dual-listbox/demos/GroupedDualListbox.vue`**
56
+
57
+ ```vue
58
+ <template>
59
+ <div style="height: 360px;">
60
+ <CoarDualListbox
61
+ v-model="selected"
62
+ :options="options"
63
+ available-label="Roles"
64
+ selected-label="Assigned"
65
+ :search-fields="['label', 'group']"
66
+ />
67
+ </div>
68
+ </template>
69
+
70
+ <script setup lang="ts">
71
+ import { ref } from 'vue';
72
+ import { CoarDualListbox } from '@cocoar/vue-ui';
73
+ import type { CoarListboxOption } from '@cocoar/vue-ui';
74
+
75
+ const options: CoarListboxOption<string>[] = [
76
+ { value: 'admin', label: 'Admin', group: 'System' },
77
+ { value: 'auditor', label: 'Auditor', group: 'System' },
78
+ { value: 'editor', label: 'Editor', group: 'Content' },
79
+ { value: 'publisher', label: 'Publisher', group: 'Content' },
80
+ { value: 'reviewer', label: 'Reviewer', group: 'Content' },
81
+ { value: 'viewer', label: 'Viewer', group: 'Content' },
82
+ { value: 'billing', label: 'Billing', group: 'Finance' },
83
+ { value: 'reporting', label: 'Reporting', group: 'Finance' },
84
+ ];
85
+
86
+ const selected = ref<string[]>(['viewer']);
87
+ </script>
88
+ ```
89
+
90
+ ## Custom item components
91
+
92
+ Pass `itemComponents` and they're used on both sides. The component receives a `side` prop (`'available' | 'selected'`) so the same renderer can adapt per column if needed.
93
+
94
+ **Demo — `dual-listbox/demos/CustomComponentDualListbox.vue`**
95
+
96
+ ```vue
97
+ <template>
98
+ <div style="height: 360px;">
99
+ <CoarDualListbox
100
+ v-model="selectedIds"
101
+ :options="items"
102
+ :item-components="{ user: UserItem }"
103
+ :compare-with="byId"
104
+ available-label="Everyone"
105
+ selected-label="Group members"
106
+ :search-by="(i) => `${i.label} ${(i.value as UserRow).email}`"
107
+ />
108
+ <p style="margin-top: 8px; font-size: 13px; color: #64748b;">
109
+ Group: {{ selectedIds.map(u => u.name).join(', ') || '—' }}
110
+ </p>
111
+ </div>
112
+ </template>
113
+
114
+ <script setup lang="ts">
115
+ import { ref } from 'vue';
116
+ import { CoarDualListbox } from '@cocoar/vue-ui';
117
+ import type { CoarListboxOption } from '@cocoar/vue-ui';
118
+ import UserItem from '../../listbox/demos/UserItem.vue';
119
+
120
+ interface UserRow { id: string; name: string; email: string; role: string }
121
+
122
+ const users: UserRow[] = [
123
+ { id: '1', name: 'Alice Müller', email: 'alice@x.com', role: 'Admin' },
124
+ { id: '2', name: 'Bob Meier', email: 'bob@x.com', role: 'Editor' },
125
+ { id: '3', name: 'Clara Schmid', email: 'clara@x.com', role: 'Viewer' },
126
+ { id: '4', name: 'Dan Roth', email: 'dan@x.com', role: 'Editor' },
127
+ { id: '5', name: 'Eva Horn', email: 'eva@x.com', role: 'Admin' },
128
+ ];
129
+
130
+ const items: CoarListboxOption<UserRow>[] = users.map(u => ({
131
+ value: u,
132
+ label: u.name,
133
+ kind: 'user',
134
+ }));
135
+
136
+ // Compare users by id — protects against reactivity-induced reference changes.
137
+ const byId = (a: UserRow, b: UserRow) => a.id === b.id;
138
+
139
+ const selectedIds = ref<UserRow[]>([users[0]]);
140
+ </script>
141
+ ```
142
+
143
+ ## Inline remove button (right side)
144
+
145
+ A common pattern: render an × button inside each selected item, and clicking it moves the user back to the available column. Since the right column is just `options ∩ modelValue`, "moving back" is simply *dropping the id from v-model* — the item re-appears on the left automatically.
146
+
147
+ **Demo — `dual-listbox/demos/RemoveBackToAvailable.vue`**
148
+
149
+ ```vue
150
+ <template>
151
+ <div style="height: 360px;">
152
+ <CoarDualListbox
153
+ v-model="selectedIds"
154
+ :options="items"
155
+ :item-components="{ user: UserCard }"
156
+ :compare-with="byId"
157
+ available-label="Everyone"
158
+ selected-label="Group members"
159
+ :search-by="(i) => `${i.label} ${(i.value as UserRow).email}`"
160
+ @item-remove="onItemRemove"
161
+ />
162
+ <p style="margin-top: 8px; font-size: 13px; color: #64748b;">
163
+ Group ({{ selectedIds.length }}): {{ selectedIds.map(u => u.name).join(', ') || '—' }}
164
+ </p>
165
+ </div>
166
+ </template>
167
+
168
+ <script setup lang="ts">
169
+ import { ref } from 'vue';
170
+ import { CoarDualListbox } from '@cocoar/vue-ui';
171
+ import type { CoarListboxOption } from '@cocoar/vue-ui';
172
+ import UserCard from './UserCard.vue';
173
+
174
+ interface UserRow { id: string; name: string; email: string; role: string }
175
+
176
+ const users: UserRow[] = [
177
+ { id: '1', name: 'Alice Müller', email: 'alice@x.com', role: 'Admin' },
178
+ { id: '2', name: 'Bob Meier', email: 'bob@x.com', role: 'Editor' },
179
+ { id: '3', name: 'Clara Schmid', email: 'clara@x.com', role: 'Viewer' },
180
+ { id: '4', name: 'Dan Roth', email: 'dan@x.com', role: 'Editor' },
181
+ { id: '5', name: 'Eva Horn', email: 'eva@x.com', role: 'Admin' },
182
+ ];
183
+
184
+ const items: CoarListboxOption<UserRow>[] = users.map((u) => ({
185
+ value: u, label: u.name, kind: 'user',
186
+ }));
187
+
188
+ const byId = (a: UserRow, b: UserRow) => a.id === b.id;
189
+
190
+ const selectedIds = ref<UserRow[]>([users[0], users[2]]);
191
+
192
+ // When a user clicks × on the right side, it's a "move back to available"
193
+ // — just drop the item from v-model and DualListbox re-computes the columns.
194
+ function onItemRemove(payload: { item: CoarListboxOption<UserRow>; side: 'available' | 'selected' }) {
195
+ if (payload.side !== 'selected') return;
196
+ selectedIds.value = selectedIds.value.filter((u) => !byId(u, payload.item.value));
197
+ }
198
+ </script>
199
+ ```
200
+
201
+ The renderer uses the `side` prop to show the × only on the selected side; clicking it calls `api.remove()`, which bubbles up as `item-remove` with `side: 'selected'`, and the parent filters v-model:
202
+
203
+ ```vue
204
+ <!-- UserCard.vue -->
205
+ <button v-if="side === 'selected'" @click.stop="api.remove()">×</button>
206
+ ```
207
+
208
+ ```vue
209
+ <!-- Parent -->
210
+ <CoarDualListbox v-model="selected" :options="..." @item-remove="onRemove" />
211
+
212
+ <script setup lang="ts">
213
+ function onRemove({ item, side }) {
214
+ if (side !== 'selected') return
215
+ selected.value = selected.value.filter(u => u.id !== item.value.id)
216
+ }
217
+ </script>
218
+ ```
219
+
220
+ See [`CoarListboxItemApi`](./listbox.md#item-api-inline-actions) for the full api surface.
221
+
222
+ ## Drag & drop
223
+
224
+ Set the `drag-drop` prop and items become draggable between the two columns. A unique drag group is wired up internally so the two sides exchange items with each other but not with unrelated listboxes on the page.
225
+
226
+ **Demo — `dual-listbox/demos/DragDropDualListbox.vue`**
227
+
228
+ ```vue
229
+ <template>
230
+ <div style="height: 320px;">
231
+ <CoarDualListbox
232
+ v-model="selected"
233
+ :options="options"
234
+ available-label="Roles"
235
+ selected-label="Assigned"
236
+ drag-drop
237
+ />
238
+ <p style="margin-top: 8px; font-size: 13px; color: #64748b;">
239
+ Tip: drag from one column to the other. Or Ctrl/Shift-click to pick several first, then drag.
240
+ </p>
241
+ </div>
242
+ </template>
243
+
244
+ <script setup lang="ts">
245
+ import { ref } from 'vue';
246
+ import { CoarDualListbox } from '@cocoar/vue-ui';
247
+ import type { CoarListboxOption } from '@cocoar/vue-ui';
248
+
249
+ const options: CoarListboxOption<string>[] = [
250
+ { value: 'admin', label: 'Admin' },
251
+ { value: 'editor', label: 'Editor' },
252
+ { value: 'publisher', label: 'Publisher' },
253
+ { value: 'reviewer', label: 'Reviewer' },
254
+ { value: 'viewer', label: 'Viewer' },
255
+ { value: 'billing', label: 'Billing' },
256
+ { value: 'reporting', label: 'Reporting' },
257
+ ];
258
+
259
+ const selected = ref<string[]>(['viewer']);
260
+ </script>
261
+ ```
262
+
263
+ Selection-aware: if the dragged item is part of a multi-highlight, the whole highlighted set moves at once.
264
+
265
+ For a standalone two-listbox setup (or three+ lists), see the same feature on [`CoarListbox`](./listbox.md#drag-drop-between-lists).
266
+
267
+ ## Virtual scrolling (large datasets)
268
+
269
+ For directories with thousands of entries, enable `virtual` — only the rows in and around the viewport are rendered. Group headings scroll naturally in this mode (they are not sticky).
270
+
271
+ The demo below loads 10,000 synthetic principals (users, groups, service accounts) client-side. Scroll, search, highlight, drag, and the inline × all still work.
272
+
273
+ **Demo — `dual-listbox/demos/VirtualLargeList.vue`**
274
+
275
+ ```vue
276
+ <template>
277
+ <div style="height: 440px;">
278
+ <CoarDualListbox
279
+ v-model="assigned"
280
+ :options="principals"
281
+ :item-components="{ principal: PrincipalRow }"
282
+ :compare-with="byId"
283
+ :search-by="(i) => `${i.label} ${(i.value as Principal).sub} ${(i.value as Principal).kind}`"
284
+ available-label="Directory"
285
+ selected-label="Assigned"
286
+ virtual
287
+ :item-height="44"
288
+ :group-heading-height="28"
289
+ drag-drop
290
+ @item-remove="onRemove"
291
+ />
292
+ <p style="margin-top: 8px; font-size: 13px; color: #64748b;">
293
+ Directory: {{ principals.length.toLocaleString() }} principals. Assigned: {{ assigned.length }}.
294
+ </p>
295
+ </div>
296
+ </template>
297
+
298
+ <script setup lang="ts">
299
+ import { computed, ref } from 'vue';
300
+ import { CoarDualListbox } from '@cocoar/vue-ui';
301
+ import type { CoarListboxOption } from '@cocoar/vue-ui';
302
+ import PrincipalRow, { type Principal } from './PrincipalRow.vue';
303
+
304
+ // Generate 10k synthetic IPrincipals: 8k users, 1.5k groups, 500 system users.
305
+ function makePrincipals(): Principal[] {
306
+ const out: Principal[] = [];
307
+ const firstNames = ['Alex', 'Sam', 'Jordan', 'Taylor', 'Morgan', 'Riley', 'Casey', 'Quinn', 'Avery', 'Rowan', 'Maya', 'Leo', 'Nora', 'Ivo', 'Yara', 'Milo', 'Luna', 'Finn', 'Zoe', 'Theo'];
308
+ const lastNames = ['Müller', 'Schmid', 'Meier', 'Weber', 'Keller', 'Huber', 'Fischer', 'Baumann', 'Steiner', 'Brunner', 'Roth', 'Zürcher', 'Frei', 'Hofer', 'Widmer', 'Kaufmann', 'Bosshart', 'Stucki', 'Graf', 'Winkler'];
309
+ for (let i = 0; i < 8000; i++) {
310
+ const fn = firstNames[i % firstNames.length];
311
+ const ln = lastNames[(i * 7) % lastNames.length];
312
+ out.push({
313
+ id: `u${i}`,
314
+ kind: 'user',
315
+ name: `${fn} ${ln}`,
316
+ sub: `${fn.toLowerCase()}.${ln.toLowerCase()}@corp.example`,
317
+ });
318
+ }
319
+ const teams = ['Engineering', 'Design', 'Product', 'Sales', 'Marketing', 'Finance', 'Legal', 'HR', 'Support', 'Operations'];
320
+ for (let i = 0; i < 1500; i++) {
321
+ const t = teams[i % teams.length];
322
+ out.push({
323
+ id: `g${i}`,
324
+ kind: 'group',
325
+ name: `${t} #${Math.floor(i / teams.length) + 1}`,
326
+ sub: `${(8 + (i % 24))} members`,
327
+ });
328
+ }
329
+ for (let i = 0; i < 500; i++) {
330
+ out.push({
331
+ id: `s${i}`,
332
+ kind: 'system',
333
+ name: `svc-${String(i).padStart(4, '0')}`,
334
+ sub: 'service account',
335
+ });
336
+ }
337
+ return out;
338
+ }
339
+
340
+ const rawPrincipals = makePrincipals();
341
+
342
+ const principals = computed<CoarListboxOption<Principal>[]>(() =>
343
+ rawPrincipals.map((p) => ({
344
+ value: p,
345
+ label: p.name,
346
+ kind: 'principal',
347
+ })),
348
+ );
349
+
350
+ const byId = (a: Principal, b: Principal) => a.id === b.id;
351
+
352
+ const assigned = ref<Principal[]>([rawPrincipals[3], rawPrincipals[42], rawPrincipals[8005]]);
353
+
354
+ function onRemove(payload: { item: CoarListboxOption<Principal>; side: 'available' | 'selected' }) {
355
+ if (payload.side !== 'selected') return;
356
+ assigned.value = assigned.value.filter((p) => !byId(p, payload.item.value));
357
+ }
358
+ </script>
359
+ ```
360
+
361
+ ```vue
362
+ <CoarDualListbox
363
+ v-model="assigned"
364
+ :options="principals"
365
+ virtual
366
+ :item-height="44"
367
+ :group-heading-height="28"
368
+ />
369
+ ```
370
+
371
+ - Set `item-height` to the pixel height of a row — must be fixed, but you can pick whatever matches your custom item component.
372
+ - `overscan` controls how many extra rows are rendered above/below the viewport (default 5). Raise it if you see blank flashes on fast scrolls.
373
+ - Virtual mode uses the [`useVirtualList`](./virtual-list.md) composable, which is also exported for your own components.
374
+
375
+ ## Interaction
376
+
377
+ - **Click**: highlight a single item
378
+ - **Ctrl / ⌘ + Click**: add/remove from highlight
379
+ - **Shift + Click**: range select
380
+ - **Double-click / Enter**: move that single item across
381
+ - **→ / ← buttons**: move all currently-highlighted items
382
+ - **≫ / ≪ buttons**: move all currently-visible items (respects the column's search filter). Hide with `hideMoveAll`.
383
+
384
+ ## Slots
385
+
386
+ Shared slots apply to both columns; side-specific slots override only one side.
387
+
388
+ | Slot | Applies to | Scope |
389
+ |------|------------|-------|
390
+ | `item` | both | `{ item, highlighted, selectable, side }` |
391
+ | `item-<kind>` | both | `{ item, highlighted, selectable, side }` |
392
+ | `group-heading` | both | `{ group, items }` |
393
+ | `header-available` | left | `{ label, count, total }` |
394
+ | `header-selected` | right | `{ label, count, total }` |
395
+ | `empty-available` | left | — |
396
+ | `empty-selected` | right | — |
397
+ | `actions` | center | `{ moveRight, moveLeft, moveAllRight, moveAllLeft, canMoveRight, canMoveLeft, canMoveAllRight, canMoveAllLeft }` |
398
+
399
+ ## API
400
+
401
+ ### Props
402
+
403
+ | Prop | Type | Default | Description |
404
+ |------|------|---------|-------------|
405
+ | `v-model` | `T[]` | `[]` | Selected values (right column contents, in order) |
406
+ | `options` | `CoarListboxOption<T>[]` | `[]` | All items — split into columns by `modelValue` membership |
407
+ | `availableLabel` | `string` | `'Available'` | Left column header |
408
+ | `selectedLabel` | `string` | `'Selected'` | Right column header |
409
+ | `height` | `string` | *flex* | Fixed list height for both columns |
410
+ | `disabled` / `readonly` | `boolean` | `false` | Standard form states |
411
+ | `hideSearch` | `boolean` | `false` | Hide the search input in both columns |
412
+ | `searchPlaceholder` | `string` | `'Search…'` | Placeholder for both search inputs |
413
+ | `searchFields` / `searchBy` / `filterWith` | — | — | Search config, forwarded to both columns — see [Listbox docs](./listbox.md#flexible-search) |
414
+ | `sortGroups` / `sortOptions` / `hideGroupHeadings` | — | — | Grouping / sorting config, forwarded to both columns |
415
+ | `itemComponents` / `kindBy` | — | — | Custom rendering, forwarded to both columns |
416
+ | `compareWith` | `(a, b) => boolean` | `===` | Value equality |
417
+ | `hideMoveAll` | `boolean` | `false` | Hide the ≫ / ≪ buttons |
418
+ | `hideCounts` | `boolean` | `false` | Hide count badges in headers |
419
+ | `emptyAvailable` | `string` | `'No items'` | Empty state for left column |
420
+ | `emptySelected` | `string` | `'None selected'` | Empty state for right column |
421
+ | `sortSelectedBySource` | `boolean` | `false` | When `true`, the right column is re-sorted by the order of items in `options` after every move. Default keeps the order in which the user moved items across |
422
+ | `dragDrop` | `boolean` | `false` | Enable drag-and-drop between the two columns |
423
+ | `dragEngine` | `'native' \| 'pointer' \| 'auto'` | `'native'` | HTML5 drag events, or Pointer Events for touch — see [Drag & Drop → Engines](./drag-drop.md#engines) |
424
+ | `canDrag` | `(item) => boolean` | — | Per-item source permission, applied to both columns. See [Listbox docs](./listbox.md#per-item-validation) |
425
+ | `canDrop` | `(payload) => boolean` | — | Runtime drop validation, applied to both columns |
426
+ | `virtual` | `boolean` | `false` | Enable virtual scrolling — render only rows in/near the viewport |
427
+ | `itemHeight` | `number` | `32` | Row height in pixels (used only when `virtual` is on) |
428
+ | `groupHeadingHeight` | `number` | `28` | Heading height when `virtual` is on |
429
+ | `overscan` | `number` | `5` | Extra rows rendered above/below the viewport |
430
+
431
+ ### Events
432
+
433
+ | Event | Payload | When |
434
+ |-------|---------|------|
435
+ | `update:modelValue` | `T[]` | Selection changed |
436
+ | `move` | `{ direction: 'right' \| 'left', values: T[] }` | Any move action (button or double-click) |
437
+ | `item-remove` | `{ item, side }` | A custom renderer called `api.remove()` in either column |
438
+ | `item-action` | `{ item, name, payload?, side }` | A custom renderer called `api.action(...)` in either column |
439
+
440
+ ### Exposed methods
441
+
442
+ ```ts
443
+ const dual = useTemplateRef('dual')
444
+ dual.value?.moveRight()
445
+ dual.value?.moveLeft()
446
+ dual.value?.moveAllRight()
447
+ dual.value?.moveAllLeft()
448
+ dual.value?.clearHighlight()
449
+ ```