@aleph-alpha/chat-kit 1.0.0 → 1.2.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 (133) hide show
  1. package/dist/{CkInput.vue_vue_type_script_setup_true_lang-744ZHsQU.js → CkInput.vue_vue_type_script_setup_true_lang-CRymGkQs.js} +24 -69
  2. package/dist/components/base/CkHistoryItem/CkHistoryItem.vue.d.ts +3 -4
  3. package/dist/components/base/CkHistoryItem/CkHistoryItem.vue.d.ts.map +1 -1
  4. package/dist/components/base/CkHistoryItem/types.d.ts +3 -2
  5. package/dist/components/base/CkHistoryItem/types.d.ts.map +1 -1
  6. package/dist/components/base/CkPromptStopButton/CkPromptStopButton.vue.d.ts.map +1 -1
  7. package/dist/components/index.js +1 -1
  8. package/dist/composables/index.d.ts +2 -0
  9. package/dist/composables/index.d.ts.map +1 -1
  10. package/dist/composables/index.js +3 -2
  11. package/dist/composables/useConversationSessions.d.ts +29 -0
  12. package/dist/composables/useConversationSessions.d.ts.map +1 -0
  13. package/dist/handlers/index.js +2 -2
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +7 -6
  17. package/dist/purify.es-md74IFlY.js +1081 -0
  18. package/dist/{useChatKitLabels-Dvi2FmjE.js → useChatKitLabels-BmWmMyqX.js} +75 -1152
  19. package/dist/{useConversations-Ca242D2P.js → useConversationSessions-ulFhtpQr.js} +50 -2
  20. package/dist/{useMessageHandler-uLHNac39.js → useMessageHandler-BmBdu2EW.js} +4 -1
  21. package/package.json +5 -5
  22. package/src/components/base/CkAgentMessage/CkAgentMessage.spec.ts +91 -0
  23. package/src/components/base/CkAgentMessage/CkAgentMessage.stories.ts +215 -0
  24. package/src/components/base/CkAgentMessage/CkAgentMessage.vue +25 -0
  25. package/src/components/base/CkAgentMessage/index.ts +2 -0
  26. package/src/components/base/CkAgentMessage/types.ts +11 -0
  27. package/src/components/base/CkConversationLayout/CkConversationLayout.spec.ts +129 -0
  28. package/src/components/base/CkConversationLayout/CkConversationLayout.stories.ts +251 -0
  29. package/src/components/base/CkConversationLayout/CkConversationLayout.vue +206 -0
  30. package/src/components/base/CkConversationLayout/index.ts +2 -0
  31. package/src/components/base/CkConversationLayout/types.ts +18 -0
  32. package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.spec.ts +89 -0
  33. package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.stories.ts +70 -0
  34. package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.vue +110 -0
  35. package/src/components/base/CkCopyMessageActionButton/index.ts +2 -0
  36. package/src/components/base/CkCopyMessageActionButton/types.ts +43 -0
  37. package/src/components/base/CkHistoryItem/CkHistoryItem.spec.ts +42 -0
  38. package/src/components/base/CkHistoryItem/CkHistoryItem.stories.ts +230 -0
  39. package/src/components/base/CkHistoryItem/CkHistoryItem.vue +285 -0
  40. package/src/components/base/CkHistoryItem/activeIndicator.ts +15 -0
  41. package/src/components/base/CkHistoryItem/index.ts +2 -0
  42. package/src/components/base/CkHistoryItem/types.ts +111 -0
  43. package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.spec.ts +26 -0
  44. package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.stories.ts +149 -0
  45. package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.vue +36 -0
  46. package/src/components/base/CkHistoryWrapper/index.ts +2 -0
  47. package/src/components/base/CkHistoryWrapper/types.ts +16 -0
  48. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.spec.ts +348 -0
  49. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.ts +382 -0
  50. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue +89 -0
  51. package/src/components/base/CkMarkdownRenderer/defaultMarkdownClasses.ts +19 -0
  52. package/src/components/base/CkMarkdownRenderer/index.ts +5 -0
  53. package/src/components/base/CkMarkdownRenderer/types.ts +53 -0
  54. package/src/components/base/CkMessageActionButton/CkMessageActionButton.spec.ts +40 -0
  55. package/src/components/base/CkMessageActionButton/CkMessageActionButton.stories.ts +56 -0
  56. package/src/components/base/CkMessageActionButton/CkMessageActionButton.vue +23 -0
  57. package/src/components/base/CkMessageActionButton/index.ts +2 -0
  58. package/src/components/base/CkMessageActionButton/types.ts +21 -0
  59. package/src/components/base/CkPromptActionButton/CkPromptActionButton.spec.ts +52 -0
  60. package/src/components/base/CkPromptActionButton/CkPromptActionButton.stories.ts +126 -0
  61. package/src/components/base/CkPromptActionButton/CkPromptActionButton.vue +25 -0
  62. package/src/components/base/CkPromptActionButton/index.ts +2 -0
  63. package/src/components/base/CkPromptActionButton/types.ts +32 -0
  64. package/src/components/base/CkPromptInput/CkPromptInput.spec.ts +181 -0
  65. package/src/components/base/CkPromptInput/CkPromptInput.stories.ts +224 -0
  66. package/src/components/base/CkPromptInput/CkPromptInput.vue +104 -0
  67. package/src/components/base/CkPromptInput/index.ts +2 -0
  68. package/src/components/base/CkPromptInput/types.ts +39 -0
  69. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.spec.ts +40 -0
  70. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.stories.ts +103 -0
  71. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue +16 -0
  72. package/src/components/base/CkPromptInputDisclaimer/index.ts +2 -0
  73. package/src/components/base/CkPromptInputDisclaimer/types.ts +17 -0
  74. package/src/components/base/CkPromptStopButton/CkPromptStopButton.spec.ts +64 -0
  75. package/src/components/base/CkPromptStopButton/CkPromptStopButton.stories.ts +81 -0
  76. package/src/components/base/CkPromptStopButton/CkPromptStopButton.vue +27 -0
  77. package/src/components/base/CkPromptStopButton/index.ts +2 -0
  78. package/src/components/base/CkPromptStopButton/types.ts +28 -0
  79. package/src/components/base/CkUserMessage/CkUserMessage.spec.ts +20 -0
  80. package/src/components/base/CkUserMessage/CkUserMessage.stories.ts +112 -0
  81. package/src/components/base/CkUserMessage/CkUserMessage.vue +19 -0
  82. package/src/components/base/CkUserMessage/index.ts +2 -0
  83. package/src/components/base/CkUserMessage/types.ts +14 -0
  84. package/src/components/base/icons/CkSendIcon.vue +22 -0
  85. package/src/components/base/icons/CkStopIcon.vue +18 -0
  86. package/src/components/base/icons/index.ts +2 -0
  87. package/src/components/base/index.ts +21 -0
  88. package/src/components/composed/CkConversation/CkConversation.spec.ts +439 -0
  89. package/src/components/composed/CkConversation/CkConversation.stories.ts +642 -0
  90. package/src/components/composed/CkConversation/CkConversation.vue +146 -0
  91. package/src/components/composed/CkConversation/index.ts +2 -0
  92. package/src/components/composed/CkConversation/types.ts +37 -0
  93. package/src/components/composed/CkHistory/CkHistory.spec.ts +327 -0
  94. package/src/components/composed/CkHistory/CkHistory.stories.ts +373 -0
  95. package/src/components/composed/CkHistory/CkHistory.vue +126 -0
  96. package/src/components/composed/CkHistory/index.ts +2 -0
  97. package/src/components/composed/CkHistory/types.ts +72 -0
  98. package/src/components/composed/CkInput/CkInput.spec.ts +191 -0
  99. package/src/components/composed/CkInput/CkInput.stories.ts +438 -0
  100. package/src/components/composed/CkInput/CkInput.vue +104 -0
  101. package/src/components/composed/CkInput/index.ts +2 -0
  102. package/src/components/composed/CkInput/types.ts +60 -0
  103. package/src/components/composed/index.ts +5 -0
  104. package/src/components/index.ts +34 -0
  105. package/src/composables/index.ts +15 -0
  106. package/src/composables/types.ts +36 -0
  107. package/src/composables/useChat.ts +18 -0
  108. package/src/composables/useConversationSessions.spec.ts +86 -0
  109. package/src/composables/useConversationSessions.ts +85 -0
  110. package/src/composables/useConversations.spec.ts +557 -0
  111. package/src/composables/useConversations.ts +321 -0
  112. package/src/handlers/index.ts +9 -0
  113. package/src/handlers/useChatKitLabels.spec.ts +290 -0
  114. package/src/handlers/useChatKitLabels.ts +96 -0
  115. package/src/handlers/useMarkdownParser.spec.ts +112 -0
  116. package/src/handlers/useMarkdownParser.ts +47 -0
  117. package/src/handlers/useMessageHandler.spec.ts +535 -0
  118. package/src/handlers/useMessageHandler.ts +138 -0
  119. package/src/helpers/conversationTree.spec.ts +423 -0
  120. package/src/helpers/conversationTree.ts +117 -0
  121. package/src/helpers/generateId.ts +5 -0
  122. package/src/helpers/index.ts +4 -0
  123. package/src/helpers/secondsToDate.ts +7 -0
  124. package/src/helpers/toError.ts +8 -0
  125. package/src/index.ts +86 -0
  126. package/src/services/ChatService.ts +60 -0
  127. package/src/services/MockChatService.ts +47 -0
  128. package/src/services/index.ts +12 -0
  129. package/src/services/types.ts +92 -0
  130. package/src/test-setup.ts +12 -0
  131. package/src/test-utils/createTextPlugin.ts +9 -0
  132. package/src/types.ts +47 -0
  133. package/dist/generateId-BHf0W4Rq.js +0 -6
@@ -0,0 +1,285 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, useAttrs, useSlots, watch } from 'vue';
3
+ import {
4
+ Primitive,
5
+ UiIcon,
6
+ UiPopover,
7
+ UiPopoverContent,
8
+ UiPopoverTrigger,
9
+ UiTooltip,
10
+ } from '@aleph-alpha/ui-library';
11
+ import { CHAT_HISTORY_ACTIVE_INDICATOR_CLASS } from './activeIndicator';
12
+ import type { CkHistoryItemEmits, CkHistoryItemOwnProps } from './types';
13
+
14
+ defineOptions({
15
+ name: 'CkHistoryItem',
16
+ inheritAttrs: false,
17
+ });
18
+
19
+ const emit = defineEmits<CkHistoryItemEmits>();
20
+
21
+ const props = withDefaults(defineProps<CkHistoryItemOwnProps>(), {
22
+ isActive: false,
23
+ showAction: true,
24
+ showActionOnHover: true,
25
+ actionTooltip: '',
26
+ actionAriaLabel: 'History item action',
27
+ actionPopover: false,
28
+ actionPopoverSide: 'right',
29
+ actionPopoverAlign: 'start',
30
+ actionPopoverSideOffset: 8,
31
+ });
32
+
33
+ const attrs = useAttrs();
34
+ const slots = useSlots();
35
+
36
+ const hasMeta = computed(() =>
37
+ Boolean(props.metaStart || props.metaEnd || slots.meta),
38
+ );
39
+
40
+ /**
41
+ * When true, pointer/focus is in the action column. The row-level hover
42
+ * fill is suppressed so the icon's own hover fill + outlined border
43
+ * don't stack on top of it.
44
+ */
45
+ const pointerOnAction = ref(false);
46
+
47
+ function onActionPointerEnter() {
48
+ pointerOnAction.value = true;
49
+ }
50
+
51
+ function onActionPointerLeave() {
52
+ pointerOnAction.value = false;
53
+ }
54
+
55
+ function onActionFocusIn() {
56
+ pointerOnAction.value = true;
57
+ }
58
+
59
+ function onActionFocusOut() {
60
+ pointerOnAction.value = false;
61
+ }
62
+
63
+ watch(
64
+ () => props.showAction,
65
+ (show) => {
66
+ if (!show) {
67
+ pointerOnAction.value = false;
68
+ }
69
+ },
70
+ );
71
+
72
+ const containerClass = computed(() => [
73
+ 'group/ck-history-item relative flex w-full min-w-0 items-stretch gap-2 rounded-md border border-transparent px-2 py-1.5 transition-colors',
74
+ hasMeta.value ? 'h-12' : 'h-10',
75
+ // Whole-row focus ring: first tab lands on the main row control; second tab on the action button — both are inside this container.
76
+ // `:has(:focus-visible)` instead of `:focus-within` so mouse clicks on the row don't flash a keyboard focus ring.
77
+ 'has-[:focus-visible]:border-border-surface-focus-ring has-[:focus-visible]:ring-[3px] has-[:focus-visible]:ring-ring/50',
78
+ // Row hover/active fill. Hover is suppressed while the
79
+ // pointer/focus is on the action column so the icon's own fill does not stack on top.
80
+ (props.isActive || !pointerOnAction.value) &&
81
+ 'hover:bg-background-surface-muted-subtle',
82
+ props.isActive && 'bg-background-surface-muted-subtle',
83
+ CHAT_HISTORY_ACTIVE_INDICATOR_CLASS,
84
+ ]);
85
+
86
+ const itemClass = computed(() => [
87
+ 'text-left outline-hidden transition-colors',
88
+ 'body-14-regular text-content-on-surface-primary',
89
+ '!hover:bg-transparent !active:bg-transparent',
90
+ // Suppress a second ring on the primary control; the row ring is on the outer container.
91
+ 'focus-visible:ring-0 focus-visible:outline-none',
92
+ 'data-[active=true]:text-content-on-surface-primary',
93
+ // Stretched-link: extend the primary control's hit area to cover the full row
94
+ // container (including its `px-2 py-1.5` padding and the gap before the action
95
+ // column) so clicks anywhere on the row activate the item. The action button
96
+ // stays interactive because it comes later in the DOM and is `relative`.
97
+ 'before:content-[""] before:absolute before:inset-0 before:z-0',
98
+ 'flex min-w-0 flex-1 items-center',
99
+ attrs.class as unknown,
100
+ ]);
101
+
102
+ /**
103
+ * Wrapper column that owns the action button and the `pointerenter` /
104
+ * `pointerleave` hit area driving `pointerOnAction`. Uses `self-stretch` so
105
+ * the full row height reports pointer-enter on the column (not just the
106
+ * 32×32 button), and — when `showActionOnHover` is true — hides the inner
107
+ * button with `opacity-0` until the row is hovered or keyboard focus lands
108
+ * anywhere inside it.
109
+ */
110
+ const actionColumnClass = computed(() => [
111
+ 'ml-auto flex shrink-0 items-center self-stretch',
112
+ props.showActionOnHover
113
+ ? '[&_button]:opacity-0 [&_button]:transition-opacity group-hover/ck-history-item:[&_button]:opacity-100 group-focus-within/ck-history-item:[&_button]:opacity-100'
114
+ : '',
115
+ ]);
116
+
117
+ /**
118
+ * Action control: same hover fill as the row, plus the
119
+ * `border/button/outlined` token so the icon picks up a visible outline on
120
+ * hover/focus (mirrors the outline button variant). Applied only while
121
+ * pointer/focus is on the action column so the row tint does not stack
122
+ * underneath the icon.
123
+ */
124
+ const actionButtonClass = computed(() => [
125
+ 'relative z-10 inline-flex size-8 shrink-0 items-center justify-center rounded-sm border border-transparent text-content-on-surface-primary outline-hidden transition-colors',
126
+ '[&_svg]:size-4 [&_svg]:shrink-0',
127
+ 'focus-visible:ring-0 focus-visible:outline-none',
128
+ pointerOnAction.value &&
129
+ 'bg-background-surface-muted-subtle border-border-button-outlined',
130
+ ]);
131
+
132
+ // Popover panel surface is `background/surface/secondary` to match the
133
+ // look of the original sidebar chat-history item popover. This is
134
+ // intentional for visual parity and can be overridden via
135
+ // `actionPopoverContentClass`.
136
+ const actionPopoverPanelClass = computed(() => [
137
+ 'w-auto min-w-48 max-w-sm p-0 bg-background-surface-secondary',
138
+ props.actionPopoverContentClass,
139
+ ]);
140
+
141
+ const passthroughAttrs = computed(() => {
142
+ const {
143
+ class: _class,
144
+ as: _as,
145
+ asChild: _asChild,
146
+ 'as-child': _asChildAttr,
147
+ ...rest
148
+ } = attrs as Record<string, unknown>;
149
+ return rest;
150
+ });
151
+
152
+ const primitiveAs = computed(() => attrs.as ?? 'button');
153
+ const primitiveAsChild = computed(
154
+ () =>
155
+ attrs.asChild === true ||
156
+ attrs.asChild === '' ||
157
+ attrs['as-child'] === true ||
158
+ attrs['as-child'] === '',
159
+ );
160
+
161
+ function onActionClick(event: MouseEvent) {
162
+ event.stopPropagation();
163
+ const el = event.currentTarget;
164
+ if (el instanceof HTMLElement && el.focus) {
165
+ queueMicrotask(() => el.focus({ preventScroll: true }));
166
+ }
167
+ if (!props.actionPopover) {
168
+ // Public kebab-case event preserved for API parity.
169
+
170
+ emit('action-click', event);
171
+ }
172
+ }
173
+ </script>
174
+
175
+ <template>
176
+ <div :data-active="props.isActive" :class="containerClass">
177
+ <Primitive
178
+ v-bind="passthroughAttrs"
179
+ :as="primitiveAs"
180
+ :as-child="primitiveAsChild"
181
+ :data-active="props.isActive"
182
+ :class="itemClass"
183
+ >
184
+ <div class="min-w-0 flex-1">
185
+ <div class="truncate body-14-regular text-content-on-surface-primary">
186
+ {{ props.title }}
187
+ </div>
188
+
189
+ <div
190
+ v-if="props.metaStart || props.metaEnd || $slots.meta"
191
+ class="mt-0.5 flex min-w-0 items-center gap-1 label-12-medium text-content-on-surface-muted"
192
+ >
193
+ <slot name="meta">
194
+ <span v-if="props.metaStart" class="truncate">{{
195
+ props.metaStart
196
+ }}</span>
197
+ <span v-if="props.metaStart && props.metaEnd" aria-hidden="true"
198
+ >|</span
199
+ >
200
+ <span v-if="props.metaEnd" class="truncate">{{
201
+ props.metaEnd
202
+ }}</span>
203
+ </slot>
204
+ </div>
205
+ </div>
206
+ </Primitive>
207
+
208
+ <div
209
+ v-if="props.showAction"
210
+ :class="actionColumnClass"
211
+ @pointerenter="onActionPointerEnter"
212
+ @pointerleave="onActionPointerLeave"
213
+ >
214
+ <UiPopover v-if="props.actionPopover">
215
+ <UiPopoverTrigger as-child>
216
+ <button
217
+ type="button"
218
+ :aria-label="props.actionAriaLabel"
219
+ :aria-haspopup="'dialog'"
220
+ :class="actionButtonClass"
221
+ @focusin="onActionFocusIn"
222
+ @focusout="onActionFocusOut"
223
+ @click.stop="onActionClick"
224
+ >
225
+ <component
226
+ :is="props.actionIcon"
227
+ v-if="props.actionIcon"
228
+ :size="16"
229
+ aria-hidden="true"
230
+ />
231
+ <UiIcon v-else name="more-vertical" :size="16" aria-hidden="true" />
232
+ </button>
233
+ </UiPopoverTrigger>
234
+ <UiPopoverContent
235
+ :side="props.actionPopoverSide"
236
+ :align="props.actionPopoverAlign"
237
+ :side-offset="props.actionPopoverSideOffset"
238
+ :class="actionPopoverPanelClass"
239
+ >
240
+ <slot name="action-popover" />
241
+ </UiPopoverContent>
242
+ </UiPopover>
243
+
244
+ <UiTooltip
245
+ v-else-if="props.actionTooltip"
246
+ :content="props.actionTooltip"
247
+ side="right"
248
+ >
249
+ <button
250
+ type="button"
251
+ :aria-label="props.actionAriaLabel"
252
+ :class="actionButtonClass"
253
+ @focusin="onActionFocusIn"
254
+ @focusout="onActionFocusOut"
255
+ @click.stop="onActionClick"
256
+ >
257
+ <component
258
+ :is="props.actionIcon"
259
+ v-if="props.actionIcon"
260
+ :size="16"
261
+ aria-hidden="true"
262
+ />
263
+ <UiIcon v-else name="more-vertical" :size="16" aria-hidden="true" />
264
+ </button>
265
+ </UiTooltip>
266
+ <button
267
+ v-else
268
+ type="button"
269
+ :aria-label="props.actionAriaLabel"
270
+ :class="actionButtonClass"
271
+ @focusin="onActionFocusIn"
272
+ @focusout="onActionFocusOut"
273
+ @click.stop="onActionClick"
274
+ >
275
+ <component
276
+ :is="props.actionIcon"
277
+ v-if="props.actionIcon"
278
+ :size="16"
279
+ aria-hidden="true"
280
+ />
281
+ <UiIcon v-else name="more-vertical" :size="16" aria-hidden="true" />
282
+ </button>
283
+ </div>
284
+ </div>
285
+ </template>
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Shared class string for the left-edge active indicator used by a chat
3
+ * history row.
4
+ *
5
+ * The `::before` pseudo-element is always mounted so CSS has something to
6
+ * transition. At rest it's painted from `scale-x-0` + `opacity-0` with its
7
+ * transform anchored on the left edge (`origin-left`); when the row's
8
+ * `data-active="true"` flips on, the bar paints in from left to right while
9
+ * fading in, using a 200ms linear curve.
10
+ *
11
+ * Kept as a string constant (rather than generated via `cn`) so UnoCSS's
12
+ * static scanner picks up every utility class for atomic CSS generation.
13
+ */
14
+ export const CHAT_HISTORY_ACTIVE_INDICATOR_CLASS =
15
+ 'before:absolute before:z-10 before:top-1/2 before:left-0 before:h-1/2 before:w-0.5 before:-translate-y-1/2 before:origin-left before:scale-x-0 before:rounded-full before:bg-background-accent-default before:opacity-0 before:transition-[transform,opacity] before:duration-200 before:ease-linear before:content-[""] data-[active=true]:before:scale-x-100 data-[active=true]:before:opacity-100';
@@ -0,0 +1,2 @@
1
+ export { default as CkHistoryItem } from './CkHistoryItem.vue';
2
+ export type * from './types';
@@ -0,0 +1,111 @@
1
+ import type { Component, HTMLAttributes } from 'vue';
2
+ import type { PrimitiveProps } from '@aleph-alpha/ui-library';
3
+
4
+ /**
5
+ * A single chat history entry with title, optional meta line, active
6
+ * indicator, and a trailing action control (plain button or popover).
7
+ * Sidebar-agnostic: can be used anywhere, not only inside a sidebar.
8
+ *
9
+ * @category Actions
10
+ * @useCases chat history row, thread list entry, recent conversation item
11
+ * @keywords chat, history, item, row, conversation, thread
12
+ * @related CkHistoryWrapper
13
+ */
14
+ export type CkHistoryItemProps = PrimitiveProps & CkHistoryItemOwnProps;
15
+
16
+ export interface CkHistoryItemOwnProps {
17
+ /**
18
+ * Main title of the history item.
19
+ */
20
+ title: string;
21
+
22
+ /**
23
+ * First metadata label shown under the title.
24
+ */
25
+ metaStart?: string;
26
+
27
+ /**
28
+ * Second metadata label shown under the title.
29
+ */
30
+ metaEnd?: string;
31
+
32
+ /**
33
+ * Marks the item as active (shows the left-edge rail indicator and
34
+ * background tint).
35
+ * @default false
36
+ */
37
+ isActive?: boolean;
38
+
39
+ /**
40
+ * Whether to render the end action button.
41
+ * @default true
42
+ */
43
+ showAction?: boolean;
44
+
45
+ /**
46
+ * Whether the end action should only appear on hover/focus.
47
+ * @default true
48
+ */
49
+ showActionOnHover?: boolean;
50
+
51
+ /**
52
+ * Tooltip for the end action button.
53
+ */
54
+ actionTooltip?: string;
55
+
56
+ /**
57
+ * Accessible label for the end action button.
58
+ * @default 'History item action'
59
+ */
60
+ actionAriaLabel?: string;
61
+
62
+ /**
63
+ * Custom action icon rendered at the end of the row. Defaults to a
64
+ * `more-vertical` icon.
65
+ */
66
+ actionIcon?: Component;
67
+
68
+ /**
69
+ * When true, the action control opens a popover (use `#action-popover`
70
+ * slot for the body). Tooltip on the action is not used in this mode;
71
+ * set `actionAriaLabel` for accessibility.
72
+ * @default false
73
+ */
74
+ actionPopover?: boolean;
75
+
76
+ /**
77
+ * Popover side relative to the action trigger.
78
+ * @default 'right'
79
+ */
80
+ actionPopoverSide?: 'top' | 'right' | 'bottom' | 'left';
81
+
82
+ /**
83
+ * Popover alignment relative to the trigger.
84
+ * @default 'start'
85
+ */
86
+ actionPopoverAlign?: 'start' | 'center' | 'end';
87
+
88
+ /**
89
+ * Offset between trigger and popover content.
90
+ * @default 8
91
+ */
92
+ actionPopoverSideOffset?: number;
93
+
94
+ /**
95
+ * Extra classes for the popover panel (merged with pattern defaults).
96
+ */
97
+ actionPopoverContentClass?: HTMLAttributes['class'];
98
+ }
99
+
100
+ /**
101
+ * Emitted events for `CkHistoryItem`.
102
+ */
103
+ export interface CkHistoryItemEmits {
104
+ /**
105
+ * Fired when the end action button is clicked.
106
+ * Not emitted when `actionPopover` is `true` — in that mode the click
107
+ * opens the popover and consumers react to interactions inside the
108
+ * `#action-popover` slot.
109
+ */
110
+ 'action-click': [event: MouseEvent];
111
+ }
@@ -0,0 +1,26 @@
1
+ import { render } from '@testing-library/vue';
2
+ import { describe, expect, it } from 'vitest';
3
+ import CkHistoryWrapper from './CkHistoryWrapper.vue';
4
+
5
+ describe('CkHistoryWrapper', () => {
6
+ it('sets data-slot and applies gap class by default', () => {
7
+ const { container } = render(CkHistoryWrapper, {
8
+ slots: { default: '<span>x</span>' },
9
+ });
10
+
11
+ const root = container.querySelector('[data-slot="chat-history"]');
12
+ expect(root).toBeTruthy();
13
+ expect(root?.className).toMatch(/gap-1/);
14
+ });
15
+
16
+ it('omits gap class when withGap is false', () => {
17
+ const { container } = render(CkHistoryWrapper, {
18
+ props: { withGap: false },
19
+ slots: { default: '<span>y</span>' },
20
+ });
21
+
22
+ const root = container.querySelector('[data-slot="chat-history"]');
23
+ expect(root).toBeTruthy();
24
+ expect(root?.className).not.toMatch(/gap-1/);
25
+ });
26
+ });
@@ -0,0 +1,149 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import { ref } from 'vue';
3
+ import CkHistoryWrapper from './CkHistoryWrapper.vue';
4
+ import { CkHistoryItem } from '../CkHistoryItem';
5
+
6
+ const meta: Meta<typeof CkHistoryWrapper> = {
7
+ title: 'Base/CkHistoryWrapper',
8
+ component: CkHistoryWrapper,
9
+ tags: ['autodocs'],
10
+ parameters: {
11
+ layout: 'padded',
12
+ docs: {
13
+ description: {
14
+ component:
15
+ 'Vertical stack for chat history rows (`CkHistoryItem`). Sidebar-agnostic — use anywhere in the app. Toggle **`withGap`** to add spacing between items.',
16
+ },
17
+ },
18
+ },
19
+ argTypes: {
20
+ withGap: {
21
+ control: 'boolean',
22
+ },
23
+ },
24
+ };
25
+
26
+ export default meta;
27
+
28
+ type Story = StoryObj<typeof CkHistoryWrapper>;
29
+
30
+ const defaultTemplateSource = `<script setup lang="ts">
31
+ import { CkHistoryWrapper, CkHistoryItem } from '@aleph-alpha/chat-kit';
32
+ </script>
33
+
34
+ <template>
35
+ <CkHistoryWrapper>
36
+ <CkHistoryItem title="Project Proposal Overview" meta-start="Today" meta-end="Innsbruck" />
37
+ <CkHistoryItem title="Project Proposal Overview" meta-start="Today" meta-end="Innsbruck" is-active />
38
+ <CkHistoryItem title="Project Proposal Overview" meta-start="Today" meta-end="Innsbruck" />
39
+ <CkHistoryItem title="Project Proposal Overview" meta-start="Today" meta-end="Innsbruck" />
40
+ </CkHistoryWrapper>
41
+ </template>`;
42
+
43
+ const withoutGapTemplateSource = `<script setup lang="ts">
44
+ import { CkHistoryWrapper, CkHistoryItem } from '@aleph-alpha/chat-kit';
45
+ </script>
46
+
47
+ <template>
48
+ <CkHistoryWrapper :with-gap="false">
49
+ <CkHistoryItem title="Thread A" meta-start="Today" />
50
+ <CkHistoryItem title="Thread B" meta-start="Yesterday" />
51
+ <CkHistoryItem title="Thread C" meta-start="Last week" />
52
+ </CkHistoryWrapper>
53
+ </template>`;
54
+
55
+ /**
56
+ * Default wrapper with the small vertical gap (`gap-1`) applied between
57
+ * child rows. Use as the container for any list of `CkHistoryItem`s.
58
+ */
59
+ export const Default: Story = {
60
+ render: () => ({
61
+ components: { CkHistoryWrapper, CkHistoryItem },
62
+ setup() {
63
+ const actionCount = ref(0);
64
+ const items = [
65
+ {
66
+ title: 'Project Proposal Overview',
67
+ metaStart: 'Today',
68
+ metaEnd: 'Innsbruck',
69
+ },
70
+ {
71
+ title: 'Project Proposal Overview',
72
+ metaStart: 'Today',
73
+ metaEnd: 'Innsbruck',
74
+ isActive: true,
75
+ },
76
+ {
77
+ title: 'Project Proposal Overview',
78
+ metaStart: 'Today',
79
+ metaEnd: 'Innsbruck',
80
+ },
81
+ {
82
+ title: 'Project Proposal Overview',
83
+ metaStart: 'Today',
84
+ metaEnd: 'Innsbruck',
85
+ },
86
+ ];
87
+
88
+ function handleActionClick() {
89
+ actionCount.value += 1;
90
+ }
91
+
92
+ return { actionCount, handleActionClick, items };
93
+ },
94
+ template: `
95
+ <div class="bg-background-surface-primary p-4 w-[22rem]">
96
+ <CkHistoryWrapper>
97
+ <CkHistoryItem
98
+ v-for="(item, index) in items"
99
+ :key="index"
100
+ :title="item.title"
101
+ :meta-start="item.metaStart"
102
+ :meta-end="item.metaEnd"
103
+ :is-active="item.isActive"
104
+ action-tooltip="More actions"
105
+ @action-click="handleActionClick"
106
+ />
107
+ </CkHistoryWrapper>
108
+ <p class="label-12-regular mt-3 text-content-on-surface-muted">
109
+ Action clicks: {{ actionCount }}
110
+ </p>
111
+ </div>
112
+ `,
113
+ }),
114
+ parameters: { docs: { source: { code: defaultTemplateSource } } },
115
+ };
116
+
117
+ /**
118
+ * Disables the inter-item gap (`with-gap="false"`). Use for dense lists
119
+ * where items should sit flush against each other.
120
+ */
121
+ export const WithoutGap: Story = {
122
+ args: {
123
+ withGap: false,
124
+ },
125
+ render: (args) => ({
126
+ components: { CkHistoryWrapper, CkHistoryItem },
127
+ setup() {
128
+ const items = [
129
+ { title: 'Thread A', metaStart: 'Today' },
130
+ { title: 'Thread B', metaStart: 'Yesterday' },
131
+ { title: 'Thread C', metaStart: 'Last week' },
132
+ ];
133
+ return { args, items };
134
+ },
135
+ template: `
136
+ <div class="bg-background-surface-primary p-4 w-[22rem]">
137
+ <CkHistoryWrapper v-bind="args">
138
+ <CkHistoryItem
139
+ v-for="(item, index) in items"
140
+ :key="index"
141
+ :title="item.title"
142
+ :meta-start="item.metaStart"
143
+ />
144
+ </CkHistoryWrapper>
145
+ </div>
146
+ `,
147
+ }),
148
+ parameters: { docs: { source: { code: withoutGapTemplateSource } } },
149
+ };
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ import { computed, useAttrs } from 'vue';
3
+ import type { CkHistoryWrapperProps } from './types';
4
+
5
+ defineOptions({
6
+ name: 'CkHistoryWrapper',
7
+ inheritAttrs: false,
8
+ });
9
+
10
+ const props = withDefaults(defineProps<CkHistoryWrapperProps>(), {
11
+ withGap: true,
12
+ });
13
+
14
+ const attrs = useAttrs();
15
+
16
+ const baseClass = 'flex w-full min-w-0 flex-col p-2';
17
+
18
+ const rootClass = computed(() => {
19
+ const consumerClass =
20
+ typeof attrs.class === 'string' ? attrs.class : undefined;
21
+ return [baseClass, props.withGap ? 'gap-1' : undefined, consumerClass]
22
+ .filter(Boolean)
23
+ .join(' ');
24
+ });
25
+
26
+ const passthroughAttrs = computed(() => {
27
+ const { class: _class, ...rest } = attrs as Record<string, unknown>;
28
+ return rest;
29
+ });
30
+ </script>
31
+
32
+ <template>
33
+ <div v-bind="passthroughAttrs" data-slot="chat-history" :class="rootClass">
34
+ <slot />
35
+ </div>
36
+ </template>
@@ -0,0 +1,2 @@
1
+ export { default as CkHistoryWrapper } from './CkHistoryWrapper.vue';
2
+ export type * from './types';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Vertical stack for chat history rows (`CkHistoryItem`). Sidebar-agnostic:
3
+ * can be used anywhere in the app, not only inside a sidebar.
4
+ *
5
+ * @category Layout
6
+ * @useCases chat history list, thread list container, recent conversations stack
7
+ * @keywords chat, history, list, stack, container
8
+ * @related CkHistoryItem
9
+ */
10
+ export interface CkHistoryWrapperProps {
11
+ /**
12
+ * When true, applies a small vertical gap (`gap-1`) between child items.
13
+ * @default true
14
+ */
15
+ withGap?: boolean;
16
+ }