@aleph-alpha/chat-kit 3.3.0 → 5.0.0

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 (189) hide show
  1. package/dist/{CkInput.vue_vue_type_script_setup_true_lang-BAcBFs6I.js → CkHistory.vue_vue_type_script_setup_true_lang-Bp9hXdH6.js} +452 -348
  2. package/dist/adapters/responses-api.d.ts +11 -5
  3. package/dist/adapters/responses-api.d.ts.map +1 -1
  4. package/dist/adapters/responses-api.js +9 -3
  5. package/dist/chat-kit.css +36 -14
  6. package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts +14 -13
  7. package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts.map +1 -1
  8. package/dist/components/base/CkAgentMessage/CkAgentMessage.vue.d.ts +8 -7
  9. package/dist/components/base/CkAgentMessage/CkAgentMessage.vue.d.ts.map +1 -1
  10. package/dist/components/base/CkAgentMessage/types.d.ts +16 -5
  11. package/dist/components/base/CkAgentMessage/types.d.ts.map +1 -1
  12. package/dist/components/base/CkAgentMessageHeader/CkAgentMessageHeader.stories.d.ts +16 -0
  13. package/dist/components/base/CkAgentMessageHeader/CkAgentMessageHeader.stories.d.ts.map +1 -0
  14. package/dist/components/base/CkAgentMessageHeader/CkAgentMessageHeader.vue.d.ts +21 -0
  15. package/dist/components/base/CkAgentMessageHeader/CkAgentMessageHeader.vue.d.ts.map +1 -0
  16. package/dist/components/base/CkAgentMessageHeader/index.d.ts +3 -0
  17. package/dist/components/base/CkAgentMessageHeader/index.d.ts.map +1 -0
  18. package/dist/components/base/CkAgentMessageHeader/types.d.ts +22 -0
  19. package/dist/components/base/CkAgentMessageHeader/types.d.ts.map +1 -0
  20. package/dist/components/base/CkCitation/CkCitation.stories.d.ts +6 -0
  21. package/dist/components/base/CkCitation/CkCitation.stories.d.ts.map +1 -1
  22. package/dist/components/base/CkCitation/CkCitation.vue.d.ts.map +1 -1
  23. package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts +5 -4
  24. package/dist/components/base/CkCitationCard/CkCitationCard.stories.d.ts.map +1 -1
  25. package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts.map +1 -1
  26. package/dist/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.stories.d.ts → CkCopyMessageButton/CkCopyMessageButton.stories.d.ts} +3 -3
  27. package/dist/components/base/CkCopyMessageButton/CkCopyMessageButton.stories.d.ts.map +1 -0
  28. package/dist/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.vue.d.ts → CkCopyMessageButton/CkCopyMessageButton.vue.d.ts} +4 -4
  29. package/dist/components/base/CkCopyMessageButton/CkCopyMessageButton.vue.d.ts.map +1 -0
  30. package/dist/components/base/CkCopyMessageButton/index.d.ts +3 -0
  31. package/dist/components/base/CkCopyMessageButton/index.d.ts.map +1 -0
  32. package/dist/components/base/{CkCopyMessageActionButton → CkCopyMessageButton}/types.d.ts +3 -3
  33. package/dist/components/base/CkCopyMessageButton/types.d.ts.map +1 -0
  34. package/dist/components/base/CkFeedbackButton/types.d.ts +1 -1
  35. package/dist/components/base/CkFeedbackDialog/types.d.ts +1 -1
  36. package/dist/components/base/CkFileItemsList/CkFileItemsList.stories.d.ts +15 -4
  37. package/dist/components/base/CkFileItemsList/CkFileItemsList.stories.d.ts.map +1 -1
  38. package/dist/components/base/CkFileItemsList/CkFileItemsList.vue.d.ts +2 -1
  39. package/dist/components/base/CkFileItemsList/CkFileItemsList.vue.d.ts.map +1 -1
  40. package/dist/components/base/CkFileItemsList/types.d.ts +17 -4
  41. package/dist/components/base/CkFileItemsList/types.d.ts.map +1 -1
  42. package/dist/components/base/CkHeaderActivityButton/CkHeaderActivityButton.stories.d.ts +19 -0
  43. package/dist/components/base/CkHeaderActivityButton/CkHeaderActivityButton.stories.d.ts.map +1 -0
  44. package/dist/components/base/CkHeaderActivityButton/CkHeaderActivityButton.vue.d.ts +22 -0
  45. package/dist/components/base/CkHeaderActivityButton/CkHeaderActivityButton.vue.d.ts.map +1 -0
  46. package/dist/components/base/CkHeaderActivityButton/index.d.ts +3 -0
  47. package/dist/components/base/CkHeaderActivityButton/index.d.ts.map +1 -0
  48. package/dist/components/base/CkHeaderActivityButton/types.d.ts +36 -0
  49. package/dist/components/base/CkHeaderActivityButton/types.d.ts.map +1 -0
  50. package/dist/components/base/{CkMessageActionButton/CkMessageActionButton.stories.d.ts → CkMessageButton/CkMessageButton.stories.d.ts} +3 -3
  51. package/dist/components/base/CkMessageButton/CkMessageButton.stories.d.ts.map +1 -0
  52. package/dist/components/base/{CkMessageActionButton/CkMessageActionButton.vue.d.ts → CkMessageButton/CkMessageButton.vue.d.ts} +3 -3
  53. package/dist/components/base/CkMessageButton/CkMessageButton.vue.d.ts.map +1 -0
  54. package/dist/components/base/CkMessageButton/index.d.ts +3 -0
  55. package/dist/components/base/CkMessageButton/index.d.ts.map +1 -0
  56. package/dist/components/base/{CkMessageActionButton → CkMessageButton}/types.d.ts +2 -2
  57. package/dist/components/base/CkMessageButton/types.d.ts.map +1 -0
  58. package/dist/components/base/CkPromptInput/CkPromptInput.stories.d.ts +6 -6
  59. package/dist/components/base/CkPromptInput/CkPromptInput.vue.d.ts.map +1 -1
  60. package/dist/components/base/CkPromptInput/types.d.ts +4 -4
  61. package/dist/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue.d.ts.map +1 -1
  62. package/dist/components/base/CkPromptInputDisclaimer/types.d.ts +1 -1
  63. package/dist/components/base/CkPromptSendButton/CkPromptSendButton.vue.d.ts +1 -1
  64. package/dist/components/base/CkPromptSendButton/types.d.ts +1 -1
  65. package/dist/components/base/CkPromptStopButton/types.d.ts +1 -1
  66. package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.stories.d.ts +16 -0
  67. package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.stories.d.ts.map +1 -0
  68. package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.vue.d.ts +12 -0
  69. package/dist/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.vue.d.ts.map +1 -0
  70. package/dist/components/base/CkRegenerateMessageButton/index.d.ts +3 -0
  71. package/dist/components/base/{CkCopyMessageActionButton → CkRegenerateMessageButton}/index.d.ts.map +1 -1
  72. package/dist/components/base/CkRegenerateMessageButton/types.d.ts +27 -0
  73. package/dist/components/base/CkRegenerateMessageButton/types.d.ts.map +1 -0
  74. package/dist/components/base/index.d.ts +7 -2
  75. package/dist/components/base/index.d.ts.map +1 -1
  76. package/dist/components/composed/CkConversation/CkConversation.stories.d.ts +1 -1
  77. package/dist/components/composed/CkConversation/CkConversation.vue.d.ts +13 -1
  78. package/dist/components/composed/CkConversation/CkConversation.vue.d.ts.map +1 -1
  79. package/dist/components/composed/CkConversation/types.d.ts +1 -1
  80. package/dist/components/composed/index.d.ts +0 -1
  81. package/dist/components/composed/index.d.ts.map +1 -1
  82. package/dist/components/index.d.ts +3 -3
  83. package/dist/components/index.d.ts.map +1 -1
  84. package/dist/components/index.js +26 -24
  85. package/dist/composables/index.js +1 -1
  86. package/dist/handlers/index.js +2 -2
  87. package/dist/handlers/useChatKitLabels.d.ts +2 -0
  88. package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
  89. package/dist/handlers/useMessageHandler.d.ts +37 -0
  90. package/dist/handlers/useMessageHandler.d.ts.map +1 -1
  91. package/dist/helpers/conversationTree.d.ts +15 -0
  92. package/dist/helpers/conversationTree.d.ts.map +1 -1
  93. package/dist/{index-C6njJ8EB.js → index-CyrVnbS9.js} +1 -1
  94. package/dist/index.d.ts +2 -2
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +30 -28
  97. package/dist/{useChatKitLabels-BM2I7-9o.js → useChatKitLabels-zMFV7aEz.js} +1 -0
  98. package/dist/{useMessageHandler-CMdy-lm0.js → useMessageHandler-N50Yj-NU.js} +45 -7
  99. package/package.json +3 -3
  100. package/src/adapters/responses-api.spec.ts +52 -5
  101. package/src/adapters/responses-api.ts +26 -7
  102. package/src/components/base/CkAgentMessage/CkAgentMessage.spec.ts +12 -17
  103. package/src/components/base/CkAgentMessage/CkAgentMessage.stories.ts +124 -113
  104. package/src/components/base/CkAgentMessage/CkAgentMessage.vue +18 -16
  105. package/src/components/base/CkAgentMessage/types.ts +16 -5
  106. package/src/components/base/CkAgentMessageHeader/CkAgentMessageHeader.spec.ts +64 -0
  107. package/src/components/base/CkAgentMessageHeader/CkAgentMessageHeader.stories.ts +90 -0
  108. package/src/components/base/CkAgentMessageHeader/CkAgentMessageHeader.vue +47 -0
  109. package/src/components/base/CkAgentMessageHeader/index.ts +2 -0
  110. package/src/components/base/CkAgentMessageHeader/types.ts +21 -0
  111. package/src/components/base/CkCitation/CkCitation.stories.ts +12 -0
  112. package/src/components/base/CkCitation/CkCitation.vue +34 -9
  113. package/src/components/base/CkCitationCard/CkCitationCard.stories.ts +5 -4
  114. package/src/components/base/CkCitationCard/CkCitationCard.vue +2 -2
  115. package/src/components/base/CkConversationLayout/CkConversationLayout.vue +1 -1
  116. package/src/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.spec.ts → CkCopyMessageButton/CkCopyMessageButton.spec.ts} +6 -6
  117. package/src/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.stories.ts → CkCopyMessageButton/CkCopyMessageButton.stories.ts} +8 -8
  118. package/src/components/base/{CkCopyMessageActionButton/CkCopyMessageActionButton.vue → CkCopyMessageButton/CkCopyMessageButton.vue} +6 -6
  119. package/src/components/base/CkCopyMessageButton/index.ts +2 -0
  120. package/src/components/base/{CkCopyMessageActionButton → CkCopyMessageButton}/types.ts +3 -3
  121. package/src/components/base/CkFeedbackButton/CkFeedbackButton.vue +5 -5
  122. package/src/components/base/CkFeedbackButton/types.ts +1 -1
  123. package/src/components/base/CkFeedbackDialog/types.ts +1 -1
  124. package/src/components/base/CkFileItemsList/CkFileItemsList.spec.ts +88 -13
  125. package/src/components/base/CkFileItemsList/CkFileItemsList.stories.ts +61 -13
  126. package/src/components/base/CkFileItemsList/CkFileItemsList.vue +153 -110
  127. package/src/components/base/CkFileItemsList/types.ts +17 -4
  128. package/src/components/base/CkHeaderActivityButton/CkHeaderActivityButton.spec.ts +50 -0
  129. package/src/components/base/CkHeaderActivityButton/CkHeaderActivityButton.stories.ts +102 -0
  130. package/src/components/base/CkHeaderActivityButton/CkHeaderActivityButton.vue +39 -0
  131. package/src/components/base/CkHeaderActivityButton/index.ts +2 -0
  132. package/src/components/base/CkHeaderActivityButton/types.ts +36 -0
  133. package/src/components/base/{CkMessageActionButton/CkMessageActionButton.spec.ts → CkMessageButton/CkMessageButton.spec.ts} +6 -6
  134. package/src/components/base/{CkMessageActionButton/CkMessageActionButton.stories.ts → CkMessageButton/CkMessageButton.stories.ts} +10 -10
  135. package/src/components/base/{CkMessageActionButton/CkMessageActionButton.vue → CkMessageButton/CkMessageButton.vue} +3 -3
  136. package/src/components/base/CkMessageButton/index.ts +2 -0
  137. package/src/components/base/{CkMessageActionButton → CkMessageButton}/types.ts +2 -2
  138. package/src/components/base/CkPromptInput/CkPromptInput.spec.ts +88 -0
  139. package/src/components/base/CkPromptInput/CkPromptInput.stories.ts +6 -6
  140. package/src/components/base/CkPromptInput/CkPromptInput.vue +63 -12
  141. package/src/components/base/CkPromptInput/types.ts +4 -4
  142. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.spec.ts +1 -1
  143. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue +1 -1
  144. package/src/components/base/CkPromptInputDisclaimer/types.ts +1 -1
  145. package/src/components/base/CkPromptSendButton/types.ts +1 -1
  146. package/src/components/base/CkPromptStopButton/types.ts +1 -1
  147. package/src/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.spec.ts +49 -0
  148. package/src/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.stories.ts +63 -0
  149. package/src/components/base/CkRegenerateMessageButton/CkRegenerateMessageButton.vue +43 -0
  150. package/src/components/base/CkRegenerateMessageButton/index.ts +2 -0
  151. package/src/components/base/CkRegenerateMessageButton/types.ts +27 -0
  152. package/src/components/base/index.ts +10 -2
  153. package/src/components/composed/CkConversation/CkConversation.spec.ts +81 -6
  154. package/src/components/composed/CkConversation/CkConversation.stories.ts +6 -6
  155. package/src/components/composed/CkConversation/CkConversation.vue +80 -37
  156. package/src/components/composed/CkConversation/types.ts +1 -1
  157. package/src/components/composed/index.ts +0 -1
  158. package/src/components/index.ts +9 -3
  159. package/src/handlers/useChatKitLabels.spec.ts +1 -0
  160. package/src/handlers/useChatKitLabels.ts +3 -0
  161. package/src/handlers/useMessageHandler.spec.ts +79 -0
  162. package/src/handlers/useMessageHandler.ts +50 -2
  163. package/src/helpers/conversationTree.spec.ts +120 -0
  164. package/src/helpers/conversationTree.ts +57 -8
  165. package/src/index.ts +8 -3
  166. package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.stories.d.ts.map +0 -1
  167. package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.vue.d.ts.map +0 -1
  168. package/dist/components/base/CkCopyMessageActionButton/index.d.ts +0 -3
  169. package/dist/components/base/CkCopyMessageActionButton/types.d.ts.map +0 -1
  170. package/dist/components/base/CkMessageActionButton/CkMessageActionButton.stories.d.ts.map +0 -1
  171. package/dist/components/base/CkMessageActionButton/CkMessageActionButton.vue.d.ts.map +0 -1
  172. package/dist/components/base/CkMessageActionButton/index.d.ts +0 -3
  173. package/dist/components/base/CkMessageActionButton/index.d.ts.map +0 -1
  174. package/dist/components/base/CkMessageActionButton/types.d.ts.map +0 -1
  175. package/dist/components/composed/CkInput/CkInput.stories.d.ts +0 -43
  176. package/dist/components/composed/CkInput/CkInput.stories.d.ts.map +0 -1
  177. package/dist/components/composed/CkInput/CkInput.vue.d.ts +0 -50
  178. package/dist/components/composed/CkInput/CkInput.vue.d.ts.map +0 -1
  179. package/dist/components/composed/CkInput/index.d.ts +0 -3
  180. package/dist/components/composed/CkInput/index.d.ts.map +0 -1
  181. package/dist/components/composed/CkInput/types.d.ts +0 -55
  182. package/dist/components/composed/CkInput/types.d.ts.map +0 -1
  183. package/src/components/base/CkCopyMessageActionButton/index.ts +0 -2
  184. package/src/components/base/CkMessageActionButton/index.ts +0 -2
  185. package/src/components/composed/CkInput/CkInput.spec.ts +0 -191
  186. package/src/components/composed/CkInput/CkInput.stories.ts +0 -434
  187. package/src/components/composed/CkInput/CkInput.vue +0 -106
  188. package/src/components/composed/CkInput/index.ts +0 -2
  189. package/src/components/composed/CkInput/types.ts +0 -55
@@ -1,5 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
- import { ref } from 'vue';
2
+ import { computed, ref } from 'vue';
3
3
  import CkFileItemsList from './CkFileItemsList.vue';
4
4
  import type { CkFileEntry } from './types';
5
5
 
@@ -12,12 +12,13 @@ const meta: Meta<typeof CkFileItemsList> = {
12
12
  docs: {
13
13
  description: {
14
14
  component:
15
- 'Compact horizontal row of file attachments. Renders up to `maxInline` `UiFileItem`s side-by-side; once that threshold is exceeded, the full list collapses into a single popover trigger.',
15
+ 'Compact horizontal row of file attachments. Renders as many `UiFileItem`s inline as fit the available width — each kept between `minItemWidth` and `maxItemWidth` wide — and collapses the full list into a single popover trigger once they no longer fit.',
16
16
  },
17
17
  },
18
18
  },
19
19
  argTypes: {
20
- maxInline: { control: { type: 'number', min: 1, max: 6, step: 1 } },
20
+ minItemWidth: { control: { type: 'number', min: 80, max: 320, step: 8 } },
21
+ maxItemWidth: { control: { type: 'number', min: 120, max: 480, step: 8 } },
21
22
  size: { control: { type: 'inline-radio' }, options: ['default', 'small'] },
22
23
  isRemovable: { control: 'boolean' },
23
24
  disabled: { control: 'boolean' },
@@ -97,15 +98,15 @@ function handleRemove(id: CkFileEntry['id']) {
97
98
  </template>`;
98
99
 
99
100
  /**
100
- * Up to `maxInline` (default `3`) files are rendered inline as a horizontal
101
- * row of `UiFileItem`s.
101
+ * As many files as fit the available width are rendered inline as a horizontal
102
+ * row of `UiFileItem`s (each kept between `minItemWidth` and `maxItemWidth`).
102
103
  */
103
104
  export const Inline: Story = {
104
105
  render: () => ({
105
106
  components: { CkFileItemsList },
106
107
  setup: () => ({ files: baseFiles }),
107
108
  template: `
108
- <div class="bg-background-surface-primary p-4 w-[36rem]">
109
+ <div class="bg-background-surface-primary p-4 w-[44rem]">
109
110
  <CkFileItemsList :files="files" />
110
111
  </div>
111
112
  `,
@@ -114,15 +115,15 @@ export const Inline: Story = {
114
115
  };
115
116
 
116
117
  /**
117
- * When the list overflows `maxInline`, the component collapses to a single
118
- * popover trigger. Click it to reveal the full vertical list of files.
118
+ * When the files no longer fit the available width, the component collapses to
119
+ * a single popover trigger. Click it to reveal the full vertical list of files.
119
120
  */
120
121
  export const Collapsed: Story = {
121
122
  render: () => ({
122
123
  components: { CkFileItemsList },
123
124
  setup: () => ({ files: overflowFiles }),
124
125
  template: `
125
- <div class="bg-background-surface-primary p-4 w-[36rem]">
126
+ <div class="bg-background-surface-primary p-4 w-[44rem]">
126
127
  <CkFileItemsList :files="files" />
127
128
  </div>
128
129
  `,
@@ -193,7 +194,7 @@ export const Removable: Story = {
193
194
  return { files, handleRemove };
194
195
  },
195
196
  template: `
196
- <div class="bg-background-surface-primary p-4 w-[36rem]">
197
+ <div class="bg-background-surface-primary p-4 w-[44rem]">
197
198
  <CkFileItemsList :files="files" is-removable @remove="handleRemove" />
198
199
  </div>
199
200
  `,
@@ -232,7 +233,7 @@ export const CustomItem: Story = {
232
233
  components: { CkFileItemsList },
233
234
  setup: () => ({ files: baseFiles }),
234
235
  template: `
235
- <div class="bg-background-surface-primary p-4 w-[36rem]">
236
+ <div class="bg-background-surface-primary p-4 w-[44rem]">
236
237
  <CkFileItemsList :files="files">
237
238
  <template #item="{ file }">
238
239
  <a
@@ -283,7 +284,7 @@ export const Clickable: Story = {
283
284
  return { files: overflowFiles, openFile, lastClicked };
284
285
  },
285
286
  template: `
286
- <div class="bg-background-surface-primary p-4 w-[36rem]">
287
+ <div class="bg-background-surface-primary p-4 w-[44rem]">
287
288
  <CkFileItemsList
288
289
  :files="files"
289
290
  list-label="Attached files"
@@ -335,7 +336,7 @@ export const Disabled: Story = {
335
336
  ] as CkFileEntry[],
336
337
  }),
337
338
  template: `
338
- <div class="flex flex-col gap-6 bg-background-surface-primary p-4 w-[36rem]">
339
+ <div class="flex flex-col gap-6 bg-background-surface-primary p-4 w-[44rem]">
339
340
  <div>
340
341
  <p class="label-12-medium text-content-on-surface-muted mb-2">
341
342
  Whole list disabled
@@ -353,3 +354,50 @@ export const Disabled: Story = {
353
354
  }),
354
355
  parameters: { docs: { source: { code: disabledTemplateSource } } },
355
356
  };
357
+
358
+ /**
359
+ * The inline count adapts to the available width. There is no hard cap on the
360
+ * number of inline items — as many fit are kept, each between `minItemWidth`
361
+ * and `maxItemWidth` wide. As the wrapper narrows, fewer items stay inline
362
+ * until the whole list collapses into the popover trigger.
363
+ *
364
+ * Drag the bottom-right corner of the box to resize it and watch the list
365
+ * react. (The other stories use a fixed-width wrapper, so resizing the browser
366
+ * window alone won't change their layout.)
367
+ */
368
+ export const Responsive: Story = {
369
+ args: {
370
+ minItemWidth: 200,
371
+ maxItemWidth: 256,
372
+ fileCount: 3,
373
+ } as never,
374
+ argTypes: {
375
+ fileCount: { control: { type: 'number', min: 1, max: 5, step: 1 } },
376
+ } as never,
377
+ render: (args) => ({
378
+ components: { CkFileItemsList },
379
+ setup() {
380
+ const argsWithCount = args as typeof args & { fileCount?: number };
381
+ const files = computed(() =>
382
+ overflowFiles.slice(
383
+ 0,
384
+ Math.min(5, Math.max(1, argsWithCount.fileCount ?? 5)),
385
+ ),
386
+ );
387
+ const listArgs = computed(() => {
388
+ const rest = { ...argsWithCount };
389
+ delete rest.fileCount;
390
+ return rest;
391
+ });
392
+ return { args: listArgs, files };
393
+ },
394
+ template: `
395
+ <div
396
+ class="resize-x overflow-auto bg-background-surface-primary p-4"
397
+ style="resize: horizontal; overflow: auto; width: 44rem; min-width: 12rem; max-width: 100%;"
398
+ >
399
+ <CkFileItemsList v-bind="args" :files="files" />
400
+ </div>
401
+ `,
402
+ }),
403
+ };
@@ -8,7 +8,7 @@ import {
8
8
  UiProgress,
9
9
  type UiFileItemSize,
10
10
  } from '@aleph-alpha/ui-library';
11
- import { computed, ref } from 'vue';
11
+ import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
12
12
  import CkFileItemRow from './CkFileItemRow.vue';
13
13
  import type {
14
14
  CkFileEntry,
@@ -21,7 +21,8 @@ defineOptions({
21
21
  });
22
22
 
23
23
  const props = withDefaults(defineProps<CkFileItemsListProps>(), {
24
- maxInline: 3,
24
+ minItemWidth: 200,
25
+ maxItemWidth: 256,
25
26
  size: 'small',
26
27
  isRemovable: false,
27
28
  removeLabel: 'Remove file',
@@ -48,7 +49,42 @@ defineSlots<{
48
49
  }) => unknown;
49
50
  }>();
50
51
 
51
- const isCollapsed = computed(() => props.files.length > props.maxInline);
52
+ // Gap between inline items (`gap-2` => 0.5rem => 8px).
53
+ const ITEM_GAP = 8;
54
+
55
+ const rootRef = ref<HTMLElement | null>(null);
56
+ const containerWidth = ref(0);
57
+ let resizeObserver: ResizeObserver | undefined;
58
+
59
+ onMounted(() => {
60
+ const el = rootRef.value;
61
+ if (!el) return;
62
+ containerWidth.value = el.clientWidth;
63
+ if (typeof ResizeObserver !== 'undefined') {
64
+ resizeObserver = new ResizeObserver((entries) => {
65
+ for (const entry of entries) {
66
+ containerWidth.value = entry.contentRect.width;
67
+ }
68
+ });
69
+ resizeObserver.observe(el);
70
+ }
71
+ });
72
+
73
+ onBeforeUnmount(() => resizeObserver?.disconnect());
74
+
75
+ // Number of items that fit at the current width while each stays at least
76
+ // `minItemWidth` wide. There is no upper cap — as many items as fit are kept
77
+ // inline. Falls back to showing every file before the first measurement (SSR /
78
+ // initial render / environments without ResizeObserver) so the list never
79
+ // flashes collapsed; the observer corrects it on mount.
80
+ const inlineCapacity = computed(() => {
81
+ const width = containerWidth.value;
82
+ if (!width) return props.files.length;
83
+ const fit = Math.floor((width + ITEM_GAP) / (props.minItemWidth + ITEM_GAP));
84
+ return Math.max(1, fit);
85
+ });
86
+
87
+ const isCollapsed = computed(() => props.files.length > inlineCapacity.value);
52
88
  const isOpen = ref(false);
53
89
 
54
90
  function isRowDisabled(file: CkFileEntry): boolean {
@@ -120,123 +156,130 @@ function handleRowClick(file: CkFileEntry, event: MouseEvent) {
120
156
  </script>
121
157
 
122
158
  <template>
123
- <ul
124
- v-if="!isCollapsed"
125
- data-slot="file-items-list"
126
- class="m-0 flex w-full min-w-0 list-none flex-wrap items-center gap-2 p-0"
127
- :aria-label="listLabel"
128
- >
129
- <li
130
- v-for="file in files"
131
- :key="file.id"
132
- class="min-w-0 max-w-64 flex-1 basis-0"
133
- @click="handleRowClick(file, $event)"
159
+ <div ref="rootRef" class="w-full min-w-0">
160
+ <ul
161
+ v-if="!isCollapsed"
162
+ data-slot="file-items-list"
163
+ class="m-0 flex w-full min-w-0 list-none flex-wrap items-center gap-2 p-0"
164
+ :aria-label="listLabel"
134
165
  >
135
- <CkFileItemRow
136
- :file="file"
137
- :size="size"
138
- :is-removable="isRemovable"
139
- :remove-label="removeLabel"
140
- :disabled="isRowDisabled(file)"
141
- @remove="handleRemove(file)"
142
- >
143
- <template #default="bindings">
144
- <slot name="item" v-bind="bindings" />
145
- </template>
146
- </CkFileItemRow>
147
- </li>
148
- </ul>
149
-
150
- <UiPopover v-else v-model:open="isOpen">
151
- <UiPopoverTrigger as-child>
152
- <button
153
- type="button"
154
- data-slot="file-items-list-trigger"
155
- :disabled="disabled"
156
- class="relative flex w-full max-w-64 min-w-0 items-center gap-3 rounded-xl bg-background-surface-muted-subtle text-left transition-colors hover:bg-background-surface-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-border-focus disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-background-surface-muted-subtle"
157
- :class="size === 'small' ? 'p-2' : 'p-3'"
158
- :aria-label="resolvedTriggerAriaLabel"
166
+ <li
167
+ v-for="file in files"
168
+ :key="file.id"
169
+ class="flex-1 basis-0"
170
+ :style="{
171
+ minWidth: `${minItemWidth}px`,
172
+ maxWidth: `${maxItemWidth}px`,
173
+ }"
174
+ @click="handleRowClick(file, $event)"
159
175
  >
160
- <UiFileType
161
- v-if="showStatus && hasFailures && !uploadInProgress"
162
- :error="true"
163
- />
164
- <UiFileType v-else icon="paperclip" tone="default" />
165
-
166
- <span
167
- v-if="!showStatus"
168
- class="min-w-0 flex-1 truncate text-content-on-surface-primary"
169
- :class="size === 'small' ? 'label-12-medium' : 'body-14-medium'"
176
+ <CkFileItemRow
177
+ :file="file"
178
+ :size="size"
179
+ :is-removable="isRemovable"
180
+ :remove-label="removeLabel"
181
+ :disabled="isRowDisabled(file)"
182
+ @remove="handleRemove(file)"
170
183
  >
171
- {{ resolvedTriggerLabel }}
172
- </span>
173
- <div v-else class="flex min-w-0 flex-1 flex-col items-start gap-0.5">
184
+ <template #default="bindings">
185
+ <slot name="item" v-bind="bindings" />
186
+ </template>
187
+ </CkFileItemRow>
188
+ </li>
189
+ </ul>
190
+
191
+ <UiPopover v-else v-model:open="isOpen">
192
+ <UiPopoverTrigger as-child>
193
+ <button
194
+ type="button"
195
+ data-slot="file-items-list-trigger"
196
+ :disabled="disabled"
197
+ class="relative flex w-full max-w-64 min-w-0 items-center gap-3 rounded-xl bg-background-surface-muted-subtle text-left transition-colors hover:bg-background-surface-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-border-focus disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-background-surface-muted-subtle"
198
+ :class="size === 'small' ? 'p-2' : 'p-3'"
199
+ :aria-label="resolvedTriggerAriaLabel"
200
+ >
201
+ <UiFileType
202
+ v-if="showStatus && hasFailures && !uploadInProgress"
203
+ :error="true"
204
+ />
205
+ <UiFileType v-else icon="paperclip" tone="default" />
206
+
174
207
  <span
175
- class="w-full truncate text-content-on-surface-primary"
208
+ v-if="!showStatus"
209
+ class="min-w-0 flex-1 truncate text-content-on-surface-primary"
176
210
  :class="size === 'small' ? 'label-12-medium' : 'body-14-medium'"
177
211
  >
178
212
  {{ resolvedTriggerLabel }}
179
213
  </span>
180
- <UiProgress
181
- v-if="uploadInProgress"
182
- :model-value="aggregateProgress"
183
- class="h-1 w-full"
184
- :aria-label="`${resolvedTriggerLabel} upload progress`"
214
+ <div v-else class="flex min-w-0 flex-1 flex-col items-start gap-0.5">
215
+ <span
216
+ class="w-full truncate text-content-on-surface-primary"
217
+ :class="size === 'small' ? 'label-12-medium' : 'body-14-medium'"
218
+ >
219
+ {{ resolvedTriggerLabel }}
220
+ </span>
221
+ <UiProgress
222
+ v-if="uploadInProgress"
223
+ :model-value="aggregateProgress"
224
+ class="h-1 w-full"
225
+ :aria-label="`${resolvedTriggerLabel} upload progress`"
226
+ />
227
+ <span
228
+ v-else-if="hasFailures"
229
+ class="label-12-medium w-full truncate text-content-on-status-error-inverse"
230
+ >
231
+ {{ failedStatusLabel }}
232
+ </span>
233
+ <span
234
+ v-else-if="triggerLabel"
235
+ class="label-12-medium w-full truncate text-content-on-surface-muted"
236
+ >
237
+ {{ countStatusLabel }}
238
+ </span>
239
+ </div>
240
+
241
+ <UiIcon
242
+ name="chevron-down"
243
+ :size="16"
244
+ aria-hidden="true"
245
+ class="shrink-0 text-content-on-surface-muted transition-transform"
246
+ :class="isOpen && 'rotate-180'"
185
247
  />
186
- <span
187
- v-else-if="hasFailures"
188
- class="label-12-medium w-full truncate text-content-on-status-error-inverse"
189
- >
190
- {{ failedStatusLabel }}
191
- </span>
192
- <span
193
- v-else-if="triggerLabel"
194
- class="label-12-medium w-full truncate text-content-on-surface-muted"
195
- >
196
- {{ countStatusLabel }}
197
- </span>
198
- </div>
199
-
200
- <UiIcon
201
- name="chevron-down"
202
- :size="16"
203
- aria-hidden="true"
204
- class="shrink-0 text-content-on-surface-muted transition-transform"
205
- :class="isOpen && 'rotate-180'"
206
- />
207
- </button>
208
- </UiPopoverTrigger>
209
-
210
- <UiPopoverContent
211
- align="start"
212
- :side-offset="4"
213
- :class="['w-64 p-2', popoverClass]"
214
- >
215
- <ul
216
- data-slot="file-items-list-collapsed"
217
- class="m-0 flex list-none flex-col gap-2 p-0"
218
- :aria-label="listLabel"
248
+ </button>
249
+ </UiPopoverTrigger>
250
+
251
+ <UiPopoverContent
252
+ align="start"
253
+ :side-offset="4"
254
+ :class="['w-64 p-2', popoverClass]"
219
255
  >
220
- <li
221
- v-for="file in files"
222
- :key="file.id"
223
- class="min-w-0 max-w-64"
224
- @click="handleRowClick(file, $event)"
256
+ <ul
257
+ data-slot="file-items-list-collapsed"
258
+ class="m-0 flex list-none flex-col gap-2 p-0"
259
+ :aria-label="listLabel"
225
260
  >
226
- <CkFileItemRow
227
- :file="file"
228
- :size="size"
229
- :is-removable="isRemovable"
230
- :remove-label="removeLabel"
231
- :disabled="isRowDisabled(file)"
232
- @remove="handleRemove(file)"
261
+ <li
262
+ v-for="file in files"
263
+ :key="file.id"
264
+ class="min-w-0"
265
+ :style="{ maxWidth: `${maxItemWidth}px` }"
266
+ @click="handleRowClick(file, $event)"
233
267
  >
234
- <template #default="bindings">
235
- <slot name="item" v-bind="bindings" />
236
- </template>
237
- </CkFileItemRow>
238
- </li>
239
- </ul>
240
- </UiPopoverContent>
241
- </UiPopover>
268
+ <CkFileItemRow
269
+ :file="file"
270
+ :size="size"
271
+ :is-removable="isRemovable"
272
+ :remove-label="removeLabel"
273
+ :disabled="isRowDisabled(file)"
274
+ @remove="handleRemove(file)"
275
+ >
276
+ <template #default="bindings">
277
+ <slot name="item" v-bind="bindings" />
278
+ </template>
279
+ </CkFileItemRow>
280
+ </li>
281
+ </ul>
282
+ </UiPopoverContent>
283
+ </UiPopover>
284
+ </div>
242
285
  </template>
@@ -25,8 +25,9 @@ export interface CkFileEntry {
25
25
  }
26
26
 
27
27
  /**
28
- * Horizontal list of file attachments; collapses to a popover once the
29
- * count exceeds `maxInline`.
28
+ * Horizontal list of file attachments. Renders as many items inline as fit the
29
+ * available width — each kept between `minItemWidth` and `maxItemWidth` wide —
30
+ * and collapses into a popover once they no longer fit.
30
31
  *
31
32
  * @category Form Inputs
32
33
  * @useCases user message attachments, prompt input previews, chat file row
@@ -36,8 +37,20 @@ export interface CkFileEntry {
36
37
  export interface CkFileItemsListProps {
37
38
  /** Files to render. Order is preserved. */
38
39
  files: CkFileEntry[];
39
- /** @default 3 */
40
- maxInline?: number;
40
+ /**
41
+ * Minimum width (px) an inline item may occupy. As the wrapper narrows, fewer
42
+ * items stay inline (each kept at least this wide) until the list collapses
43
+ * into the popover. Lower values keep more items inline on narrow widths.
44
+ * @default 200
45
+ */
46
+ minItemWidth?: number;
47
+ /**
48
+ * Maximum width (px) an inline item may grow to. Items never stretch beyond
49
+ * this width; any extra horizontal space is left unused. There is no cap on
50
+ * the number of inline items as long as they fit the wrapper.
51
+ * @default 256
52
+ */
53
+ maxItemWidth?: number;
41
54
  /** @default 'small' */
42
55
  size?: UiFileItemSize;
43
56
  /** Show a remove control on every row. @default false */
@@ -0,0 +1,50 @@
1
+ import CkHeaderActivityButton from './CkHeaderActivityButton.vue';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen } from '@testing-library/vue';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ describe('CkHeaderActivityButton', () => {
7
+ it('renders the slot content as the label', () => {
8
+ render(CkHeaderActivityButton, {
9
+ slots: { default: '1 tool, 42 sources' },
10
+ });
11
+
12
+ expect(
13
+ screen.getByRole('button', { name: /1 tool, 42 sources/ }),
14
+ ).toBeInTheDocument();
15
+ });
16
+
17
+ it('uses the explicit ariaLabel when provided', () => {
18
+ render(CkHeaderActivityButton, {
19
+ props: { ariaLabel: 'Show activity' },
20
+ slots: { default: '1 tool' },
21
+ });
22
+
23
+ expect(
24
+ screen.getByRole('button', { name: 'Show activity' }),
25
+ ).toBeInTheDocument();
26
+ });
27
+
28
+ it('emits click when pressed', async () => {
29
+ const user = userEvent.setup();
30
+ const { emitted } = render(CkHeaderActivityButton, {
31
+ slots: { default: 'Activity' },
32
+ });
33
+
34
+ await user.click(screen.getByRole('button'));
35
+
36
+ expect(emitted('click')).toHaveLength(1);
37
+ });
38
+
39
+ it('does not emit click when disabled', async () => {
40
+ const user = userEvent.setup();
41
+ const { emitted } = render(CkHeaderActivityButton, {
42
+ props: { disabled: true },
43
+ slots: { default: 'Activity' },
44
+ });
45
+
46
+ await user.click(screen.getByRole('button'));
47
+
48
+ expect(emitted('click')).toBeUndefined();
49
+ });
50
+ });
@@ -0,0 +1,102 @@
1
+ import CkHeaderActivityButton from './CkHeaderActivityButton.vue';
2
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
3
+
4
+ const meta: Meta<typeof CkHeaderActivityButton> = {
5
+ title: 'Base/CkHeaderActivityButton',
6
+ component: CkHeaderActivityButton,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ disabled: {
10
+ control: 'boolean',
11
+ description: 'Disables the button and prevents the `click` event.',
12
+ },
13
+ ariaLabel: {
14
+ control: 'text',
15
+ description:
16
+ 'Accessible label. Falls back to the label text when omitted.',
17
+ },
18
+ icon: {
19
+ control: 'text',
20
+ description:
21
+ "Trailing Lucide icon name. Set to '' to hide. Defaults to 'arrow-right'.",
22
+ },
23
+ },
24
+ decorators: [
25
+ () => ({
26
+ template: '<div style="padding: 24px;"><story /></div>',
27
+ }),
28
+ ],
29
+ };
30
+
31
+ export default meta;
32
+ type Story = StoryObj<typeof meta>;
33
+
34
+ const defaultTemplateSource = `<script setup lang="ts">
35
+ import { CkHeaderActivityButton } from '@aleph-alpha/chat-kit';
36
+
37
+ function openActivity() {
38
+ // open the activity / chain-of-thought panel
39
+ }
40
+ </script>
41
+
42
+ <template>
43
+ <CkHeaderActivityButton @click="openActivity">
44
+ 1 tool, 42 sources
45
+ </CkHeaderActivityButton>
46
+ </template>`;
47
+
48
+ /**
49
+ * Default activity button: a short label followed by the trailing arrow icon.
50
+ * It emits a generic `click` and performs no action on its own.
51
+ */
52
+ export const Default: Story = {
53
+ render: (args) => ({
54
+ components: { CkHeaderActivityButton },
55
+ setup() {
56
+ return { args };
57
+ },
58
+ template: `
59
+ <CkHeaderActivityButton v-bind="args">
60
+ 1 tool, 42 sources
61
+ </CkHeaderActivityButton>
62
+ `,
63
+ }),
64
+ args: { disabled: false, icon: 'arrow-right' },
65
+ parameters: { docs: { source: { code: defaultTemplateSource } } },
66
+ };
67
+
68
+ /**
69
+ * Without a trailing icon — pass an empty `icon` to render the label only.
70
+ */
71
+ export const WithoutIcon: Story = {
72
+ render: (args) => ({
73
+ components: { CkHeaderActivityButton },
74
+ setup() {
75
+ return { args };
76
+ },
77
+ template: `
78
+ <CkHeaderActivityButton v-bind="args">
79
+ 3 sources
80
+ </CkHeaderActivityButton>
81
+ `,
82
+ }),
83
+ args: { icon: '' },
84
+ };
85
+
86
+ /**
87
+ * Disabled state: the button is non-interactive and does not emit `click`.
88
+ */
89
+ export const Disabled: Story = {
90
+ render: (args) => ({
91
+ components: { CkHeaderActivityButton },
92
+ setup() {
93
+ return { args };
94
+ },
95
+ template: `
96
+ <CkHeaderActivityButton v-bind="args">
97
+ 1 tool, 42 sources
98
+ </CkHeaderActivityButton>
99
+ `,
100
+ }),
101
+ args: { disabled: true },
102
+ };
@@ -0,0 +1,39 @@
1
+ <script setup lang="ts">
2
+ import type { CkHeaderActivityButtonProps } from './types';
3
+ import { UiButton, UiIcon } from '@aleph-alpha/ui-library';
4
+
5
+ defineOptions({
6
+ name: 'CkHeaderActivityButton',
7
+ });
8
+
9
+ const props = withDefaults(defineProps<CkHeaderActivityButtonProps>(), {
10
+ disabled: false,
11
+ icon: 'arrow-right',
12
+ });
13
+
14
+ const emit = defineEmits<{
15
+ click: [];
16
+ }>();
17
+
18
+ function handleClick() {
19
+ if (props.disabled) {
20
+ return;
21
+ }
22
+
23
+ emit('click');
24
+ }
25
+ </script>
26
+
27
+ <template>
28
+ <UiButton
29
+ v-bind="{ ariaLabel }"
30
+ :disabled="disabled"
31
+ variant="ghost"
32
+ size="sm"
33
+ class="label-12-medium text-content-on-surface-muted gap-1 rounded-[3px] px-2 py-1"
34
+ @click="handleClick"
35
+ >
36
+ <slot />
37
+ <UiIcon v-if="icon" :name="icon" :size="12" aria-hidden="true" />
38
+ </UiButton>
39
+ </template>
@@ -0,0 +1,2 @@
1
+ export { default as CkHeaderActivityButton } from './CkHeaderActivityButton.vue';
2
+ export type * from './types';