@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,575 @@
1
+ <!-- Generated from apps/docs/components/listbox.md by apps/docs/scripts/sync-skill.mjs. Do not edit; edit the docs page. -->
2
+
3
+ # Listbox
4
+
5
+ A single-column list of selectable items with grouping, search, custom renderers, and multi-select highlighting. Use it as a form control (v-model of highlighted values), as a display-only roster, or as the foundation for [`CoarDualListbox`](./dual-listbox.md).
6
+
7
+ ```ts
8
+ import { CoarListbox } from '@cocoar/vue-ui';
9
+ import type { CoarListboxOption } from '@cocoar/vue-ui';
10
+ ```
11
+
12
+ ## Basic
13
+
14
+ Multi-select highlight via click, `Ctrl+Click`, `Shift+Click`, and keyboard (arrows, `Space`, `Ctrl+A`). Double-click (or `Enter`) emits `item-activate` — wire this up to move items, open a detail view, etc.
15
+
16
+ **Demo — `listbox/demos/BasicListbox.vue`**
17
+
18
+ ```vue
19
+ <template>
20
+ <div style="max-width: 320px; height: 260px;">
21
+ <CoarListbox
22
+ v-model="highlighted"
23
+ :options="options"
24
+ label="Fruit"
25
+ show-count
26
+ searchable
27
+ />
28
+ <p style="margin-top: 8px; font-size: 13px; color: #64748b;">
29
+ Highlighted: {{ highlighted.join(', ') || 'none' }}
30
+ </p>
31
+ </div>
32
+ </template>
33
+
34
+ <script setup lang="ts">
35
+ import { ref } from 'vue';
36
+ import { CoarListbox } from '@cocoar/vue-ui';
37
+ import type { CoarListboxOption } from '@cocoar/vue-ui';
38
+
39
+ const options: CoarListboxOption<string>[] = [
40
+ { value: 'apple', label: 'Apple' },
41
+ { value: 'banana', label: 'Banana' },
42
+ { value: 'cherry', label: 'Cherry' },
43
+ { value: 'date', label: 'Date' },
44
+ { value: 'elderberry', label: 'Elderberry' },
45
+ { value: 'fig', label: 'Fig' },
46
+ ];
47
+
48
+ const highlighted = ref<string[]>([]);
49
+ </script>
50
+ ```
51
+
52
+ ## Display only
53
+
54
+ Set `display-only` to render a static roster — for example, "current group members". Clicks don't highlight, keyboard navigation is off, ARIA role drops to `list`, but search and grouping still work.
55
+
56
+ **Demo — `listbox/demos/DisplayOnlyListbox.vue`**
57
+
58
+ ```vue
59
+ <template>
60
+ <div style="max-width: 320px; height: 240px;">
61
+ <CoarListbox
62
+ :options="members"
63
+ display-only
64
+ label="Team members"
65
+ show-count
66
+ :searchable="members.length > 5"
67
+ />
68
+ </div>
69
+ </template>
70
+
71
+ <script setup lang="ts">
72
+ import { CoarListbox } from '@cocoar/vue-ui';
73
+ import type { CoarListboxOption } from '@cocoar/vue-ui';
74
+
75
+ const members: CoarListboxOption<string>[] = [
76
+ { value: 'alice', label: 'Alice Müller', subtitle: 'Engineer · Zürich', icon: 'user' },
77
+ { value: 'bob', label: 'Bob Meier', subtitle: 'Designer · Basel', icon: 'user' },
78
+ { value: 'clara', label: 'Clara Schmid', subtitle: 'PM · Bern', icon: 'user' },
79
+ { value: 'dan', label: 'Dan Roth', subtitle: 'Engineer · Remote', icon: 'user' },
80
+ ];
81
+ </script>
82
+ ```
83
+
84
+ ## Grouped
85
+
86
+ Items with a `group` field render under sticky headings. Group order is controlled by `sortGroups` ('asc' by default); item order within a group by `sortOptions` ('none' by default).
87
+
88
+ **Demo — `listbox/demos/GroupedListbox.vue`**
89
+
90
+ ```vue
91
+ <template>
92
+ <div style="max-width: 320px; height: 300px;">
93
+ <CoarListbox
94
+ v-model="highlighted"
95
+ :options="options"
96
+ label="Items"
97
+ searchable
98
+ :search-fields="['label', 'group']"
99
+ />
100
+ </div>
101
+ </template>
102
+
103
+ <script setup lang="ts">
104
+ import { ref } from 'vue';
105
+ import { CoarListbox } from '@cocoar/vue-ui';
106
+ import type { CoarListboxOption } from '@cocoar/vue-ui';
107
+
108
+ const options: CoarListboxOption<string>[] = [
109
+ { value: 'apple', label: 'Apple', group: 'Fruit' },
110
+ { value: 'banana', label: 'Banana', group: 'Fruit' },
111
+ { value: 'cherry', label: 'Cherry', group: 'Fruit' },
112
+ { value: 'carrot', label: 'Carrot', group: 'Vegetable' },
113
+ { value: 'potato', label: 'Potato', group: 'Vegetable' },
114
+ { value: 'broccoli', label: 'Broccoli', group: 'Vegetable' },
115
+ ];
116
+
117
+ const highlighted = ref<string[]>([]);
118
+ </script>
119
+ ```
120
+
121
+ ## Custom item components
122
+
123
+ For per-kind polymorphic rendering, register Vue components via `itemComponents`. The component resolution order is:
124
+
125
+ 1. `itemComponents[item.kind]` if provided
126
+ 2. `#item-<kind>` slot if present
127
+ 3. `#item` slot if present
128
+ 4. Built-in `icon + label + subtitle` layout
129
+
130
+ Each renderer receives `{ item, highlighted, selectable, side }` as props / slot scope. Set `kindBy` if `kind` lives on a different field than `item.kind`.
131
+
132
+ **Demo — `listbox/demos/CustomItemComponent.vue`**
133
+
134
+ ```vue
135
+ <template>
136
+ <div style="max-width: 360px; height: 320px;">
137
+ <CoarListbox
138
+ v-model="highlighted"
139
+ :options="items"
140
+ :item-components="{ user: UserItem, invite: InviteItem }"
141
+ label="Mixed entries"
142
+ searchable
143
+ :search-by="(i) => `${i.label} ${(i.value as any).email ?? ''}`"
144
+ />
145
+ </div>
146
+ </template>
147
+
148
+ <script setup lang="ts">
149
+ import { ref } from 'vue';
150
+ import { CoarListbox } from '@cocoar/vue-ui';
151
+ import type { CoarListboxOption } from '@cocoar/vue-ui';
152
+ import UserItem from './UserItem.vue';
153
+ import InviteItem from './InviteItem.vue';
154
+
155
+ interface UserRow { id: string; name: string; email: string; role: string }
156
+ interface InviteRow { id: string; email: string; expiresInDays: number }
157
+
158
+ const items: CoarListboxOption<UserRow | InviteRow>[] = [
159
+ { value: { id: '1', name: 'Alice Müller', email: 'alice@x.com', role: 'Admin' }, label: 'Alice Müller', kind: 'user' },
160
+ { value: { id: '2', name: 'Bob Meier', email: 'bob@x.com', role: 'Editor' }, label: 'Bob Meier', kind: 'user' },
161
+ { value: { id: 'inv-1', email: 'carol@new.com', expiresInDays: 3 }, label: 'carol@new.com', kind: 'invite' },
162
+ { value: { id: 'inv-2', email: 'dan@new.com', expiresInDays: 7 }, label: 'dan@new.com', kind: 'invite' },
163
+ ];
164
+
165
+ const highlighted = ref<unknown[]>([]);
166
+ </script>
167
+ ```
168
+
169
+ ```ts
170
+ // UserItem.vue
171
+ const props = defineProps<{
172
+ item: CoarListboxOption<UserRow>
173
+ highlighted: boolean
174
+ selectable: boolean
175
+ }>()
176
+ ```
177
+
178
+ ## Flexible search
179
+
180
+ Three levels of control, from simplest to most powerful — set only one:
181
+
182
+ ```ts
183
+ // (a) Extend the built-in search to additional fields
184
+ searchFields: ['label', 'subtitle', 'group']
185
+
186
+ // (b) Provide the searchable text for each item
187
+ searchBy: (item) => `${item.label} ${item.value.email}`
188
+
189
+ // (c) Full control — arbitrary matching logic
190
+ filterWith: (item, query) => fuse.search(query).includes(item)
191
+ ```
192
+
193
+ `filterWith` wins over `searchBy` wins over `searchFields`. Items can also carry a `searchText` field to override the default per-item.
194
+
195
+ ## Item API (inline actions)
196
+
197
+ Every item renderer — whether a component from `itemComponents` or the `#item` / `#item-<kind>` slot — receives a scoped `api` handle. Use it to trigger listbox behavior from inside the row: remove it from a trash button, toggle highlight from a checkbox, fire a custom action from a context menu.
198
+
199
+ **Demo — `listbox/demos/RemovableItems.vue`**
200
+
201
+ ```vue
202
+ <template>
203
+ <div style="max-width: 400px; height: 280px;">
204
+ <CoarListbox
205
+ :options="rows"
206
+ :item-components="{ tag: TagRow }"
207
+ label="Keywords"
208
+ show-count
209
+ @item-remove="remove"
210
+ @item-action="onAction"
211
+ />
212
+ <p style="margin-top: 8px; font-size: 13px; color: #64748b;">
213
+ Last action: {{ lastAction || '—' }}
214
+ </p>
215
+ </div>
216
+ </template>
217
+
218
+ <script setup lang="ts">
219
+ import { ref } from 'vue';
220
+ import { CoarListbox } from '@cocoar/vue-ui';
221
+ import type { CoarListboxOption } from '@cocoar/vue-ui';
222
+ import TagRow from './TagRow.vue';
223
+
224
+ const rows = ref<CoarListboxOption<string>[]>([
225
+ { value: 'vue', label: 'vue', kind: 'tag' },
226
+ { value: 'typescript', label: 'typescript', kind: 'tag' },
227
+ { value: 'design-system', label: 'design-system', kind: 'tag' },
228
+ { value: 'dnd', label: 'drag-and-drop', kind: 'tag' },
229
+ ]);
230
+
231
+ const lastAction = ref('');
232
+
233
+ function remove(p: { item: CoarListboxOption<string> }) {
234
+ rows.value = rows.value.filter((r) => r.value !== p.item.value);
235
+ lastAction.value = `removed ${p.item.label}`;
236
+ }
237
+
238
+ function onAction(p: { item: CoarListboxOption<string>; name: string }) {
239
+ lastAction.value = `${p.name} on ${p.item.label}`;
240
+ }
241
+ </script>
242
+ ```
243
+
244
+ The listbox emits the resulting event — `item-remove` or `item-action` with the name + payload — so the parent stays in charge of the data:
245
+
246
+ ```ts
247
+ interface CoarListboxItemApi<T> {
248
+ item: CoarListboxOption<T>
249
+ highlighted: boolean
250
+ highlight(): void
251
+ unhighlight(): void
252
+ toggleHighlight(): void
253
+ activate(): void // emits `item-activate`
254
+ remove(): void // emits `item-remove` → parent updates `options`
255
+ action(name: string, payload?: unknown): void // emits `item-action`
256
+ }
257
+ ```
258
+
259
+ Inside custom components:
260
+
261
+ ```vue
262
+ <script setup lang="ts">
263
+ import type { CoarListboxOption, CoarListboxItemApi } from '@cocoar/vue-ui'
264
+ defineProps<{
265
+ item: CoarListboxOption<Row>
266
+ api: CoarListboxItemApi<Row>
267
+ }>()
268
+ </script>
269
+
270
+ <template>
271
+ <div class="row">
272
+ <span>{{ item.label }}</span>
273
+ <button @click.stop="api.remove()">×</button>
274
+ </div>
275
+ </template>
276
+ ```
277
+
278
+ Use `@click.stop` on inline buttons so the click doesn't also trigger the row's click/highlight handler.
279
+
280
+ ## Drag & drop between lists
281
+
282
+ Set `draggable` on lists items can leave, `droppable` on lists items can enter, and a shared `drag-group` name to link them. Use it for any layout — source/destination (one-way) or bidirectional peers (this Kanban-style example uses three lists):
283
+
284
+ **Demo — `listbox/demos/DragDropTwoLists.vue`**
285
+
286
+ ```vue
287
+ <template>
288
+ <div style="display: flex; gap: 16px; height: 300px;">
289
+ <CoarListbox
290
+ :options="waiting"
291
+ label="Backlog"
292
+ show-count
293
+ draggable
294
+ droppable
295
+ drag-group="tasks"
296
+ @items-add="receive('waiting', $event)"
297
+ @items-remove="remove('waiting', $event)"
298
+ />
299
+ <CoarListbox
300
+ :options="doing"
301
+ label="In progress"
302
+ show-count
303
+ draggable
304
+ droppable
305
+ drag-group="tasks"
306
+ @items-add="receive('doing', $event)"
307
+ @items-remove="remove('doing', $event)"
308
+ />
309
+ <CoarListbox
310
+ :options="done"
311
+ label="Done"
312
+ show-count
313
+ draggable
314
+ droppable
315
+ drag-group="tasks"
316
+ @items-add="receive('done', $event)"
317
+ @items-remove="remove('done', $event)"
318
+ />
319
+ </div>
320
+ </template>
321
+
322
+ <script setup lang="ts">
323
+ import { ref } from 'vue';
324
+ import { CoarListbox } from '@cocoar/vue-ui';
325
+ import type { CoarListboxOption } from '@cocoar/vue-ui';
326
+
327
+ type Lane = 'waiting' | 'doing' | 'done';
328
+
329
+ const waiting = ref<CoarListboxOption<string>[]>([
330
+ { value: 't1', label: 'Design migration plan' },
331
+ { value: 't2', label: 'Spec compare-with API' },
332
+ { value: 't3', label: 'Audit stale PRs' },
333
+ ]);
334
+ const doing = ref<CoarListboxOption<string>[]>([
335
+ { value: 't4', label: 'Ship script-editor v2' },
336
+ ]);
337
+ const done = ref<CoarListboxOption<string>[]>([
338
+ { value: 't5', label: 'Upgrade to Vue 3.5' },
339
+ ]);
340
+
341
+ const laneRef = { waiting, doing, done };
342
+
343
+ function receive(to: Lane, payload: { items: CoarListboxOption<string>[] }) {
344
+ laneRef[to].value = [...laneRef[to].value, ...payload.items];
345
+ }
346
+
347
+ function remove(from: Lane, payload: { items: CoarListboxOption<string>[] }) {
348
+ const toRemove = new Set(payload.items.map((i) => i.value));
349
+ laneRef[from].value = laneRef[from].value.filter((o) => !toRemove.has(o.value));
350
+ }
351
+ </script>
352
+ ```
353
+
354
+ - **Events:** the source emits `items-remove`, the target emits `items-add`. The parent updates each list's `options` accordingly. Both fire synchronously on drop — no flicker.
355
+ - **Selection-aware:** if the dragged item is part of the multi-highlight, the whole highlighted set is carried along.
356
+ - **Groups:** lists with different `drag-group` values reject each other's drops. No group at all = self-drops only.
357
+ - For a built-in two-column experience, see [`CoarDualListbox`'s `drag-drop` prop](./dual-listbox.md#drag-drop).
358
+ - The drag logic comes from the [`useDragDrop`](./drag-drop.md) composable — use it directly if you need the same semantics in a custom component that isn't a listbox.
359
+
360
+ ### Directional flows
361
+
362
+ When `drag-group` is too coarse — e.g. *box1 can go to box2 or box3, but nothing can return to box1* — each list gets a stable `drag-id` and the targets whitelist sources via `drag-accept`:
363
+
364
+ **Demo — `listbox/demos/DirectionalDnd.vue`**
365
+
366
+ ```vue
367
+ <template>
368
+ <div style="display: flex; gap: 16px; height: 280px;">
369
+ <CoarListbox
370
+ :options="backlog"
371
+ label="Backlog"
372
+ show-count
373
+ draggable
374
+ drag-group="tickets"
375
+ drag-id="backlog"
376
+ @items-remove="remove('backlog', $event)"
377
+ />
378
+ <CoarListbox
379
+ :options="inReview"
380
+ label="In review"
381
+ show-count
382
+ draggable
383
+ droppable
384
+ drag-group="tickets"
385
+ drag-id="review"
386
+ :drag-accept="['backlog']"
387
+ @items-add="receive('inReview', $event)"
388
+ @items-remove="remove('inReview', $event)"
389
+ />
390
+ <CoarListbox
391
+ :options="done"
392
+ label="Done"
393
+ show-count
394
+ droppable
395
+ drag-group="tickets"
396
+ drag-id="done"
397
+ :drag-accept="['backlog', 'review']"
398
+ @items-add="receive('done', $event)"
399
+ />
400
+ </div>
401
+ <p style="margin-top: 8px; font-size: 13px; color: #64748b;">
402
+ Backlog → In review, Backlog → Done, In review → Done. No drops back onto Backlog (it's not droppable), and Done is a dead-end (not draggable).
403
+ </p>
404
+ </template>
405
+
406
+ <script setup lang="ts">
407
+ import { ref } from 'vue';
408
+ import { CoarListbox } from '@cocoar/vue-ui';
409
+ import type { CoarListboxOption } from '@cocoar/vue-ui';
410
+
411
+ type Lane = 'backlog' | 'inReview' | 'done';
412
+
413
+ const backlog = ref<CoarListboxOption<string>[]>([
414
+ { value: 't1', label: 'Spec the drag API' },
415
+ { value: 't2', label: 'Audit stale PRs' },
416
+ { value: 't3', label: 'Upgrade Vue minor' },
417
+ ]);
418
+ const inReview = ref<CoarListboxOption<string>[]>([
419
+ { value: 't4', label: 'Tokens v2' },
420
+ ]);
421
+ const done = ref<CoarListboxOption<string>[]>([
422
+ { value: 't5', label: 'Script editor v1' },
423
+ ]);
424
+
425
+ const lanes = { backlog, inReview, done };
426
+
427
+ function receive(to: Lane, payload: { items: CoarListboxOption<string>[] }) {
428
+ lanes[to].value = [...lanes[to].value, ...payload.items];
429
+ }
430
+
431
+ function remove(from: Lane, payload: { items: CoarListboxOption<string>[] }) {
432
+ const r = new Set(payload.items.map((i) => i.value));
433
+ lanes[from].value = lanes[from].value.filter((o) => !r.has(o.value));
434
+ }
435
+ </script>
436
+ ```
437
+
438
+ ```vue
439
+ <CoarListbox drag-id="box1" draggable drag-group="flow" /> <!-- source only -->
440
+ <CoarListbox drag-id="box2" draggable droppable drag-group="flow"
441
+ :drag-accept="['box1']" /> <!-- only box1 → box2 -->
442
+ <CoarListbox drag-id="box3" droppable drag-group="flow"
443
+ :drag-accept="['box1', 'box2']" /> <!-- box1 + box2 → box3, dead-end -->
444
+ ```
445
+
446
+ ### Per-item validation
447
+
448
+ `can-drag` controls which items can leave the source (applies per-item, also trims the multi-highlight payload). `can-drop` validates incoming drops at runtime — the cursor shows "not allowed" when it refuses:
449
+
450
+ ```vue
451
+ <!-- Only users can be dragged; groups stay put. -->
452
+ <CoarListbox :options="principals" draggable :can-drag="p => p.value.kind === 'user'" />
453
+
454
+ <!-- Admin group caps at 5 members, no bulk drops exceeding that. -->
455
+ <CoarListbox
456
+ v-model="admins"
457
+ :options="admins"
458
+ droppable
459
+ :can-drop="p => admins.length + p.items.length <= 5"
460
+ />
461
+ ```
462
+
463
+ ## Virtual scrolling
464
+
465
+ For very long lists (thousands of items), enable `virtual` — only the rows in/near the viewport are rendered. Group headings scroll naturally in this mode (they are not sticky; the non-virtual mode keeps sticky headings). Search, keyboard nav, highlight, drag & drop, and custom item components all still work.
466
+
467
+ ```vue
468
+ <CoarListbox
469
+ :options="tenThousand"
470
+ virtual
471
+ :item-height="32"
472
+ :group-heading-height="28"
473
+ :overscan="5"
474
+ />
475
+ ```
476
+
477
+ All items must have the same height (`item-height`). Headings can use a different height (`group-heading-height`). For a concrete example with 10k entries see the [DualListbox virtual demo](./dual-listbox.md#virtual-scrolling-large-datasets).
478
+
479
+ Virtual scrolling is built on top of the exported [`useVirtualList`](./virtual-list.md) composable — use it standalone in your own components.
480
+
481
+ ## Options Format
482
+
483
+ ```ts
484
+ interface CoarListboxOption<T> {
485
+ value: T;
486
+ label: string; // required — used for default rendering, search, a11y
487
+ kind?: string; // drives itemComponents / #item-<kind>
488
+ group?: string;
489
+ icon?: string; // default renderer only
490
+ subtitle?: string; // default renderer only
491
+ tooltip?: string; // native `title` attribute
492
+ disabled?: boolean;
493
+ searchText?: string; // per-item override for default search
494
+ }
495
+ ```
496
+
497
+ ## Slots
498
+
499
+ | Slot | Scope | Purpose |
500
+ |------|-------|---------|
501
+ | `header` | `{ label, count, total }` | Replace the entire header row |
502
+ | `search` | `{ query, update }` | Replace the search input |
503
+ | `item` | `{ item, highlighted, selectable, side?, api }` | Render every item. `api` exposes `remove()`, `toggleHighlight()`, `activate()`, `action(name, payload?)` |
504
+ | `item-<kind>` | `{ item, highlighted, selectable, side?, api }` | Render items whose `kind` matches |
505
+ | `group-heading` | `{ group, items }` | Replace the group heading |
506
+ | `empty` | — | Replace the empty state |
507
+ | `footer` | — | Add a footer below the list |
508
+
509
+ ## API
510
+
511
+ ### Props
512
+
513
+ | Prop | Type | Default | Description |
514
+ |------|------|---------|-------------|
515
+ | `v-model` | `T[]` | `[]` | Currently highlighted values |
516
+ | `options` | `CoarListboxOption<T>[]` | `[]` | Items to display |
517
+ | `label` | `string` | `''` | Header label |
518
+ | `showCount` | `boolean` | `false` | Show a count badge in the header |
519
+ | `showHeader` | `boolean` | *auto* | Override header visibility |
520
+ | `height` | `string` | *flex* | Fixed list height (e.g. `'280px'`); otherwise the list fills its parent |
521
+ | `displayOnly` | `boolean` | `false` | Disable click/keyboard interaction — render a static list. Downgrades ARIA role to `list` |
522
+ | `disabled` | `boolean` | `false` | Dim and ignore input |
523
+ | `readonly` | `boolean` | `false` | Keep appearance but ignore input |
524
+ | `searchable` | `boolean` | `false` | Render a search input above the list |
525
+ | `searchPlaceholder` | `string` | `'Search…'` | Search input placeholder |
526
+ | `searchFields` | `('label' \| 'subtitle' \| 'group')[]` | `['label']` | Fields searched by default |
527
+ | `searchBy` | `(item) => string` | — | Override the searchable text per item |
528
+ | `filterWith` | `(item, query) => boolean` | — | Full control over matching |
529
+ | `sortGroups` | `'asc' \| 'desc' \| 'none' \| (a,b) => number` | `'asc'` | Group order |
530
+ | `sortOptions` | `'asc' \| 'desc' \| 'none' \| (a,b) => number` | `'none'` | Item order |
531
+ | `hideGroupHeadings` | `boolean` | `false` | Omit group labels even when items have a `group` |
532
+ | `itemComponents` | `Record<string, Component>` | `{}` | Map of `kind` → renderer component |
533
+ | `kindBy` | `(item) => string` | `(i) => i.kind ?? ''` | Derive kind from a custom field |
534
+ | `compareWith` | `(a: T, b: T) => boolean` | `===` | Equality for values |
535
+ | `emptyText` | `string` | `'No items'` | Fallback empty state text |
536
+ | `draggable` | `boolean` | `false` | Allow items to be dragged out of this list |
537
+ | `droppable` | `boolean` | `false` | Accept drops from compatible listboxes |
538
+ | `dragEngine` | `'native' \| 'pointer' \| 'auto'` | `'native'` | HTML5 drag events, or Pointer Events for touch — see [Drag & Drop → Engines](./drag-drop.md#engines) |
539
+ | `dragGroup` | `string` | — | Shared name linking lists that can exchange items |
540
+ | `dragId` | `string` | auto | Stable identifier — pair with other lists' `dragAccept` for directional flow |
541
+ | `dragAccept` | `string[]` | — | Whitelist of source `dragId`s this list accepts. Unset = accept any source in the same `dragGroup`; empty array = accept nothing |
542
+ | `canDrag` | `(item) => boolean` | — | Per-item source permission. Items returning `false` are not draggable |
543
+ | `canDrop` | `(payload) => boolean` | — | Runtime drop validation; `payload` is `{ items, fromId, fromGroup, fromSelf }` |
544
+ | `virtual` | `boolean` | `false` | Enable virtual scrolling — only rows in/near the viewport are rendered |
545
+ | `itemHeight` | `number` | `32` | Row height in pixels (used only when `virtual` is on) |
546
+ | `groupHeadingHeight` | `number` | `28` | Heading height when `virtual` is on |
547
+ | `overscan` | `number` | `5` | Extra rows rendered above/below the viewport |
548
+
549
+ ### Events
550
+
551
+ | Event | Payload | When |
552
+ |-------|---------|------|
553
+ | `update:modelValue` | `T[]` | Highlight changed |
554
+ | `item-click` | `{ item, event }` | Single click on an item |
555
+ | `item-dblclick` | `{ item, event }` | Double click on an item |
556
+ | `item-activate` | `{ item }` | Double-click or `Enter` on a highlighted item — idiomatic "move this / open this" hook |
557
+ | `item-remove` | `{ item }` | A custom renderer called `api.remove()` — parent should drop the item from `options` |
558
+ | `item-action` | `{ item, name, payload? }` | A custom renderer called `api.action(name, payload?)` — escape hatch for any custom inline operation |
559
+ | `drag-start` | `{ items }` | Drag has started on this list. `items` includes the whole highlighted set when the grabbed item was part of it, else just the one item |
560
+ | `drag-end` | `{ items, dropped }` | Drag ended — `dropped: true` if a target consumed it |
561
+ | `items-add` | `{ items, insertIndex, fromGroup, fromSelf }` | Drop accepted here. Parent should add `items` to its source of truth. `insertIndex` is the position of the item dropped on (or `null` if dropped in empty space) |
562
+ | `items-remove` | `{ items, toGroup }` | The dragged items were consumed by a target. Parent should remove them from this list's source of truth |
563
+
564
+ ### Exposed methods
565
+
566
+ Accessible via template ref:
567
+
568
+ ```ts
569
+ const box = useTemplateRef<CoarListboxExposed<string>>('box')
570
+ box.value?.clearHighlight()
571
+ box.value?.highlightAll()
572
+ box.value?.focus()
573
+ box.value?.clearSearch()
574
+ box.value?.visibleItems // current filtered + sorted items
575
+ ```