@aleph-alpha/chat-kit 2.2.0 → 3.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 (102) hide show
  1. package/dist/{CkInput.vue_vue_type_script_setup_true_lang-CjfKRuZs.js → CkInput.vue_vue_type_script_setup_true_lang-CwH5M5WD.js} +263 -142
  2. package/dist/chat-kit.css +33 -6
  3. package/dist/components/base/CkDropZoneOverlay/CkDropZoneOverlay.vue.d.ts.map +1 -1
  4. package/dist/components/base/CkFileItemsList/CkFileItemsList.stories.d.ts +7 -0
  5. package/dist/components/base/CkFileItemsList/CkFileItemsList.stories.d.ts.map +1 -1
  6. package/dist/components/base/CkFileItemsList/CkFileItemsList.vue.d.ts +1 -0
  7. package/dist/components/base/CkFileItemsList/CkFileItemsList.vue.d.ts.map +1 -1
  8. package/dist/components/base/CkFileItemsList/types.d.ts +9 -0
  9. package/dist/components/base/CkFileItemsList/types.d.ts.map +1 -1
  10. package/dist/components/base/CkModelSelector/types.d.ts +1 -1
  11. package/dist/components/base/CkPromptInput/CkPromptInput.stories.d.ts +48 -5
  12. package/dist/components/base/CkPromptInput/CkPromptInput.stories.d.ts.map +1 -1
  13. package/dist/components/base/CkPromptInput/CkPromptInput.vue.d.ts +7 -6
  14. package/dist/components/base/CkPromptInput/CkPromptInput.vue.d.ts.map +1 -1
  15. package/dist/components/base/CkPromptInput/types.d.ts +11 -12
  16. package/dist/components/base/CkPromptInput/types.d.ts.map +1 -1
  17. package/dist/components/base/CkPromptMenuButton/CkPromptMenuButton.stories.d.ts +18 -0
  18. package/dist/components/base/CkPromptMenuButton/CkPromptMenuButton.stories.d.ts.map +1 -0
  19. package/dist/components/base/CkPromptMenuButton/CkPromptMenuButton.vue.d.ts +18 -0
  20. package/dist/components/base/CkPromptMenuButton/CkPromptMenuButton.vue.d.ts.map +1 -0
  21. package/dist/components/base/CkPromptMenuButton/index.d.ts +3 -0
  22. package/dist/components/base/CkPromptMenuButton/index.d.ts.map +1 -0
  23. package/dist/components/base/CkPromptMenuButton/types.d.ts +30 -0
  24. package/dist/components/base/CkPromptMenuButton/types.d.ts.map +1 -0
  25. package/dist/components/base/{CkPromptActionButton/CkPromptActionButton.stories.d.ts → CkPromptSendButton/CkPromptSendButton.stories.d.ts} +5 -5
  26. package/dist/components/base/CkPromptSendButton/CkPromptSendButton.stories.d.ts.map +1 -0
  27. package/dist/components/base/{CkPromptActionButton/CkPromptActionButton.vue.d.ts → CkPromptSendButton/CkPromptSendButton.vue.d.ts} +3 -3
  28. package/dist/components/base/CkPromptSendButton/CkPromptSendButton.vue.d.ts.map +1 -0
  29. package/dist/components/base/CkPromptSendButton/index.d.ts +3 -0
  30. package/dist/components/base/CkPromptSendButton/index.d.ts.map +1 -0
  31. package/dist/components/base/CkPromptSendButton/types.d.ts +34 -0
  32. package/dist/components/base/CkPromptSendButton/types.d.ts.map +1 -0
  33. package/dist/components/base/CkPromptStopButton/CkPromptStopButton.stories.d.ts +1 -1
  34. package/dist/components/base/CkPromptStopButton/CkPromptStopButton.vue.d.ts.map +1 -1
  35. package/dist/components/base/CkPromptStopButton/types.d.ts +6 -4
  36. package/dist/components/base/CkPromptStopButton/types.d.ts.map +1 -1
  37. package/dist/components/base/icons/CkSendIcon.vue.d.ts.map +1 -1
  38. package/dist/components/base/index.d.ts +3 -1
  39. package/dist/components/base/index.d.ts.map +1 -1
  40. package/dist/components/composed/CkHistory/CkHistory.vue.d.ts +1 -1
  41. package/dist/components/composed/CkInput/CkInput.stories.d.ts.map +1 -1
  42. package/dist/components/composed/CkInput/CkInput.vue.d.ts +9 -8
  43. package/dist/components/composed/CkInput/CkInput.vue.d.ts.map +1 -1
  44. package/dist/components/composed/CkInput/types.d.ts +2 -7
  45. package/dist/components/composed/CkInput/types.d.ts.map +1 -1
  46. package/dist/components/index.d.ts +2 -2
  47. package/dist/components/index.d.ts.map +1 -1
  48. package/dist/components/index.js +9 -8
  49. package/dist/composables/fileUpload/types.d.ts +1 -1
  50. package/dist/composables/fileUpload/types.d.ts.map +1 -1
  51. package/dist/composables/fileUpload/useFileUpload.d.ts.map +1 -1
  52. package/dist/composables/index.js +1 -1
  53. package/dist/{index-CfXEGjc7.js → index-THzZwfMU.js} +6 -4
  54. package/dist/index.d.ts +2 -2
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +11 -10
  57. package/package.json +1 -1
  58. package/src/components/base/CkDropZoneOverlay/CkDropZoneOverlay.vue +2 -7
  59. package/src/components/base/CkFileItemsList/CkFileItemsList.spec.ts +67 -0
  60. package/src/components/base/CkFileItemsList/CkFileItemsList.stories.ts +48 -0
  61. package/src/components/base/CkFileItemsList/CkFileItemsList.vue +78 -4
  62. package/src/components/base/CkFileItemsList/types.ts +9 -0
  63. package/src/components/base/CkModelSelector/types.ts +1 -1
  64. package/src/components/base/CkPromptInput/CkPromptInput.spec.ts +206 -11
  65. package/src/components/base/CkPromptInput/CkPromptInput.stories.ts +1068 -43
  66. package/src/components/base/CkPromptInput/CkPromptInput.vue +137 -28
  67. package/src/components/base/CkPromptInput/types.ts +11 -12
  68. package/src/components/base/CkPromptMenuButton/CkPromptMenuButton.spec.ts +62 -0
  69. package/src/components/base/CkPromptMenuButton/CkPromptMenuButton.stories.ts +75 -0
  70. package/src/components/base/CkPromptMenuButton/CkPromptMenuButton.vue +26 -0
  71. package/src/components/base/CkPromptMenuButton/index.ts +2 -0
  72. package/src/components/base/CkPromptMenuButton/types.ts +29 -0
  73. package/src/components/base/{CkPromptActionButton/CkPromptActionButton.spec.ts → CkPromptSendButton/CkPromptSendButton.spec.ts} +11 -11
  74. package/src/components/base/CkPromptSendButton/CkPromptSendButton.stories.ts +117 -0
  75. package/src/components/base/{CkPromptActionButton/CkPromptActionButton.vue → CkPromptSendButton/CkPromptSendButton.vue} +4 -4
  76. package/src/components/base/CkPromptSendButton/index.ts +2 -0
  77. package/src/components/base/CkPromptSendButton/types.ts +33 -0
  78. package/src/components/base/CkPromptStopButton/CkPromptStopButton.spec.ts +5 -5
  79. package/src/components/base/CkPromptStopButton/CkPromptStopButton.stories.ts +1 -1
  80. package/src/components/base/CkPromptStopButton/CkPromptStopButton.vue +3 -2
  81. package/src/components/base/CkPromptStopButton/types.ts +6 -4
  82. package/src/components/base/icons/CkSendIcon.vue +3 -16
  83. package/src/components/base/icons/CkStopIcon.vue +1 -1
  84. package/src/components/base/index.ts +3 -1
  85. package/src/components/composed/CkInput/CkInput.stories.ts +0 -4
  86. package/src/components/composed/CkInput/CkInput.vue +7 -5
  87. package/src/components/composed/CkInput/types.ts +2 -7
  88. package/src/components/index.ts +3 -1
  89. package/src/composables/fileUpload/types.ts +1 -1
  90. package/src/composables/fileUpload/useFileUpload.spec.ts +26 -0
  91. package/src/composables/fileUpload/useFileUpload.ts +12 -3
  92. package/src/composables/fileUpload/useFileUploadWrapper.ts +1 -1
  93. package/src/index.ts +3 -1
  94. package/dist/components/base/CkPromptActionButton/CkPromptActionButton.stories.d.ts.map +0 -1
  95. package/dist/components/base/CkPromptActionButton/CkPromptActionButton.vue.d.ts.map +0 -1
  96. package/dist/components/base/CkPromptActionButton/index.d.ts +0 -3
  97. package/dist/components/base/CkPromptActionButton/index.d.ts.map +0 -1
  98. package/dist/components/base/CkPromptActionButton/types.d.ts +0 -33
  99. package/dist/components/base/CkPromptActionButton/types.d.ts.map +0 -1
  100. package/src/components/base/CkPromptActionButton/CkPromptActionButton.stories.ts +0 -126
  101. package/src/components/base/CkPromptActionButton/index.ts +0 -2
  102. package/src/components/base/CkPromptActionButton/types.ts +0 -32
@@ -1,14 +1,31 @@
1
1
  <script setup lang="ts">
2
2
  import type { CkPromptInputProps } from './types';
3
- import { computed, nextTick, useId, useTemplateRef, watch } from 'vue';
3
+ import {
4
+ computed,
5
+ nextTick,
6
+ onMounted,
7
+ onUpdated,
8
+ ref,
9
+ useId,
10
+ useSlots,
11
+ useTemplateRef,
12
+ watch,
13
+ } from 'vue';
4
14
 
5
15
  defineOptions({ name: 'CkPromptInput' });
6
16
 
17
+ // Pixel height of one rendered line — must match the textarea's `leading-6`
18
+ // (24 px). Used to derive how many lines the current content occupies.
19
+ const LINE_HEIGHT = 24;
20
+
21
+ // Cap the textarea at 6 visible lines; content beyond that scrolls inside.
22
+ const MAX_LINES = 6;
23
+ const MAX_HEIGHT = LINE_HEIGHT * MAX_LINES;
24
+
7
25
  const props = withDefaults(defineProps<CkPromptInputProps>(), {
8
26
  modelValue: '',
9
27
  placeholder: undefined,
10
28
  disabled: false,
11
- maxHeight: 200,
12
29
  ariaLabel: undefined,
13
30
  });
14
31
 
@@ -19,21 +36,74 @@ const emit = defineEmits<{
19
36
 
20
37
  const textareaId = useId();
21
38
  const textareaRef = useTemplateRef<HTMLTextAreaElement>('textareaRef');
39
+ const slots = useSlots();
40
+
41
+ // Latches to `true` once content spans 2+ lines (newline OR soft-wrap) and
42
+ // stays there until the field is emptied. Without the latch, switching to the
43
+ // wider two-row layout would let the text fit on one line again, flip us
44
+ // back, and re-wrap — an infinite toggle loop.
45
+ const isMultiLine = ref(false);
22
46
 
23
47
  const isEmpty = computed(() => props.modelValue.trim() === '');
24
48
  const isSubmitDisabled = computed(() => props.disabled || isEmpty.value);
49
+ // `useSlots()` is reactive for *static* slot provision (the parent always
50
+ // passes `#toolbar-top` and merely changes its inner content) but NOT when
51
+ // the parent flips slot presence via `<template v-if="…" #toolbar-top>`.
52
+ // That parent-side v-if is our actual use case (file-pill row appears once
53
+ // `files.length > 0`), so sync the boolean via the post-render hook —
54
+ // onUpdated fires after every parent re-render, after slots are reconciled.
55
+ const hasToolbarTop = ref(typeof slots['toolbar-top'] === 'function');
56
+ function syncSlotState(): void {
57
+ const next = typeof slots['toolbar-top'] === 'function';
58
+ if (next !== hasToolbarTop.value) hasToolbarTop.value = next;
59
+ }
60
+ onMounted(syncSlotState);
61
+ onUpdated(syncSlotState);
62
+
63
+ // Compact form uses the larger 48 px corner radius; once a second row appears
64
+ // (multi-line text OR toolbar-top filled with chips/attachments), the radius
65
+ // drops to 24 px per Solea spec.
66
+ const hasMultipleRows = computed(
67
+ () => isMultiLine.value || hasToolbarTop.value,
68
+ );
69
+
70
+ const gridAreas = computed(() => {
71
+ const top = hasToolbarTop.value ? '"top top top" ' : '';
72
+ return isMultiLine.value
73
+ ? `${top}"input input input" "left . right"`
74
+ : `${top}"left input right"`;
75
+ });
25
76
 
26
77
  function adjustHeight(): void {
27
78
  const el = textareaRef.value;
28
79
  if (!el) return;
80
+
81
+ if (props.modelValue === '') {
82
+ isMultiLine.value = false;
83
+ el.style.height = '';
84
+ return;
85
+ }
86
+
87
+ // Collapse to natural content height first so the next scrollHeight read
88
+ // reflects what the content needs, not what the inline height is forcing.
29
89
  el.style.height = 'auto';
30
- el.style.height = `${Math.min(el.scrollHeight, props.maxHeight)}px`;
90
+ const measured = el.scrollHeight;
91
+ el.style.height = `${Math.min(measured, MAX_HEIGHT)}px`;
92
+
93
+ // Latch only on a *real* second line: either explicit newline OR scrollHeight
94
+ // ≥ 2 × line-height. Using `Math.round` on a borderline 36 px (sub-pixel
95
+ // emoji/CJK rendering on Chromium/Linux) would falsely latch a single line.
96
+ const hasExplicitNewline = props.modelValue.includes('\n');
97
+ if (
98
+ !isMultiLine.value &&
99
+ (hasExplicitNewline || measured >= LINE_HEIGHT * 2)
100
+ ) {
101
+ isMultiLine.value = true;
102
+ }
31
103
  }
32
104
 
33
105
  function onInput(event: Event): void {
34
- const value = (event.target as HTMLTextAreaElement).value;
35
- emit('update:modelValue', value);
36
- adjustHeight();
106
+ emit('update:modelValue', (event.target as HTMLTextAreaElement).value);
37
107
  }
38
108
 
39
109
  function submit(): void {
@@ -56,10 +126,9 @@ function focus(): void {
56
126
 
57
127
  watch(
58
128
  () => props.modelValue,
59
- () => {
60
- nextTick(adjustHeight);
61
- },
129
+ () => nextTick(adjustHeight),
62
130
  );
131
+ onMounted(adjustHeight);
63
132
 
64
133
  defineExpose({ focus, submit });
65
134
  </script>
@@ -67,38 +136,78 @@ defineExpose({ focus, submit });
67
136
  <template>
68
137
  <div class="flex flex-col gap-2">
69
138
  <form
70
- class="border-core-border-default bg-core-bg-primary [&:has(textarea:focus)]:ring-core-border-focus flex flex-col gap-3 rounded-xl border px-4 pt-3 pb-3 shadow-sm [&:has(textarea:focus)]:ring-2"
139
+ :class="[
140
+ 'ck-prompt-form border-border-surface-default bg-background-input-strong [&:has(textarea:focus)]:border-border-surface-focus-ring [&:has(textarea:focus)]:ring-border-surface-focus-ring/40 min-h-17 grid items-center gap-x-0 gap-y-5 border p-4 transition-[border-radius] duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] [&:has(textarea:focus)]:ring-2',
141
+ hasMultipleRows ? 'rounded-2xl' : 'rounded-3xl',
142
+ ]"
143
+ :style="{
144
+ gridTemplateColumns: 'auto 1fr auto',
145
+ gridTemplateAreas: gridAreas,
146
+ }"
71
147
  @submit.prevent="submit"
72
148
  >
73
- <label v-if="ariaLabel" :for="textareaId" class="sr-only">{{
74
- ariaLabel
75
- }}</label>
149
+ <div v-if="hasToolbarTop" style="grid-area: top">
150
+ <slot name="toolbar-top" />
151
+ </div>
152
+ <label v-if="ariaLabel" :for="textareaId" class="sr-only">
153
+ {{ ariaLabel }}
154
+ </label>
155
+ <div :class="[!isMultiLine && 'pr-2']" style="grid-area: left">
156
+ <slot name="toolbar-left" />
157
+ </div>
76
158
  <textarea
77
159
  :id="textareaId"
78
160
  ref="textareaRef"
79
- rows="2"
161
+ rows="1"
80
162
  :value="modelValue"
81
163
  :placeholder="placeholder"
82
164
  :disabled="disabled"
83
- class="text-core-content-primary placeholder-core-content-placeholder min-h-10 w-full resize-none bg-transparent text-sm leading-5 focus:outline-none"
165
+ style="grid-area: input"
166
+ :class="[
167
+ 'ck-prompt-textarea text-content-on-surface-primary placeholder-content-on-surface-muted w-full min-w-0 resize-none overflow-y-auto bg-transparent py-0 font-sans text-base font-normal leading-6 max-h-36 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60',
168
+ isMultiLine ? 'pl-3 pr-3' : 'px-0',
169
+ ]"
84
170
  @input="onInput"
85
171
  @keydown="onKeydown"
86
172
  />
87
- <div class="flex items-center justify-between">
88
- <div v-if="$slots['toolbar-left']">
89
- <slot name="toolbar-left" />
90
- </div>
91
- <div v-else />
92
- <div class="ml-auto">
93
- <slot
94
- name="toolbar-right"
95
- :submit="submit"
96
- :disabled="isSubmitDisabled"
97
- :is-empty="isEmpty"
98
- />
99
- </div>
173
+ <div :class="[!isMultiLine && 'pl-4']" style="grid-area: right">
174
+ <slot
175
+ name="toolbar-right"
176
+ :submit="submit"
177
+ :disabled="isSubmitDisabled"
178
+ :is-empty="isEmpty"
179
+ />
100
180
  </div>
101
181
  </form>
102
182
  <slot name="footer" />
103
183
  </div>
104
184
  </template>
185
+
186
+ <style scoped>
187
+ /* Solea two-layer drop shadow — warm grey + subtle orange tint. Matches the
188
+ `shadow/md/{1,2}` tokens from the design system; the generic `shadow-md`
189
+ utility from preset-wind4 uses a cold black-based shadow instead. */
190
+ .ck-prompt-form {
191
+ box-shadow:
192
+ 0 4px 6px -2px rgba(112, 95, 90, 0.1),
193
+ 0 6px 14px -2px rgba(248, 94, 43, 0.05);
194
+ }
195
+
196
+ /* Slim styled scrollbar for the multi-line textarea — matches Figma's 4 px
197
+ muted-default track. Firefox uses the native `scrollbar-*` properties;
198
+ Chromium/WebKit needs the `::-webkit-scrollbar` pseudo-elements. */
199
+ .ck-prompt-textarea {
200
+ scrollbar-width: thin;
201
+ scrollbar-color: rgba(166, 141, 133, 0.3) transparent;
202
+ }
203
+ .ck-prompt-textarea::-webkit-scrollbar {
204
+ width: 4px;
205
+ }
206
+ .ck-prompt-textarea::-webkit-scrollbar-track {
207
+ background: transparent;
208
+ }
209
+ .ck-prompt-textarea::-webkit-scrollbar-thumb {
210
+ background: rgba(166, 141, 133, 0.3);
211
+ border-radius: 9999px;
212
+ }
213
+ </style>
@@ -1,13 +1,17 @@
1
1
  /**
2
- * Low-level prompt input: a single auto-growing textarea with left/right toolbar
3
- * slots, a footer slot, and keyboard submission (Enter to submit, Shift+Enter
4
- * for newline). Use when you need full control over the prompt layout; for a
5
- * turnkey chat input use `CkInput` instead.
2
+ * Low-level prompt input: an auto-growing textarea (1–6 lines, then internal
3
+ * scroll) with `toolbar-top` / `toolbar-left` / `toolbar-right` / `footer`
4
+ * slots and keyboard submission (Enter to submit, Shift+Enter for newline).
5
+ * The form layout adapts between a compact single-row variant (large 3xl
6
+ * radius) and a stacked multi-row variant (2xl radius) automatically — driven
7
+ * by content height and whether `toolbar-top` is filled. Use when you need
8
+ * full control over the prompt layout; for a turnkey chat input use `CkInput`
9
+ * instead.
6
10
  *
7
11
  * @category Form Inputs
8
- * @useCases raw chat prompt textarea, custom prompt toolbar, auto-growing textarea, compose box, keyboard submit
9
- * @keywords prompt, input, textarea, chat, auto-grow, compose, submit, v-model
10
- * @related CkInput, CkPromptActionButton, CkPromptInputDisclaimer
12
+ * @useCases raw chat prompt textarea, custom prompt toolbar, auto-growing compose box, file-attachment chip row, multi-line chat input, keyboard submit
13
+ * @keywords prompt, input, textarea, chat, compose, submit, v-model, auto-grow, multi-line, toolbar
14
+ * @related CkInput, CkPromptSendButton, CkPromptStopButton, CkPromptMenuButton, CkPromptInputDisclaimer, CkFileItemsList
11
15
  */
12
16
  export interface CkPromptInputProps {
13
17
  /**
@@ -22,11 +26,6 @@ export interface CkPromptInputProps {
22
26
  * @default false
23
27
  */
24
28
  disabled?: boolean;
25
- /**
26
- * Maximum height (in px) before the textarea becomes scrollable.
27
- * @default 200
28
- */
29
- maxHeight?: number;
30
29
  /** Accessible label for the textarea, hidden visually but read by assistive tech. */
31
30
  ariaLabel?: string;
32
31
  }
@@ -0,0 +1,62 @@
1
+ import CkPromptMenuButton from './CkPromptMenuButton.vue';
2
+ import { render, screen } from '@testing-library/vue';
3
+
4
+ describe('CkPromptMenuButton', () => {
5
+ it('renders a default plus icon when the slot is empty', () => {
6
+ const { container } = render(CkPromptMenuButton, {
7
+ props: { ariaLabel: 'Open prompt menu' },
8
+ });
9
+
10
+ // Lucide's icon component renders an <svg> — assert at least one is present.
11
+ expect(container.querySelector('svg')).toBeInTheDocument();
12
+ });
13
+
14
+ it('renders icon slot content when provided', () => {
15
+ render(CkPromptMenuButton, {
16
+ props: { ariaLabel: 'Open prompt menu' },
17
+ slots: { default: '<svg data-testid="icon"></svg>' },
18
+ });
19
+
20
+ expect(screen.getByTestId('icon')).toBeInTheDocument();
21
+ });
22
+
23
+ it('passes ariaLabel to the underlying button', () => {
24
+ render(CkPromptMenuButton, {
25
+ props: { ariaLabel: 'Open prompt menu' },
26
+ });
27
+
28
+ expect(
29
+ screen.getByRole('button', { name: 'Open prompt menu' }),
30
+ ).toBeInTheDocument();
31
+ });
32
+
33
+ it('passes disabled state to the underlying button', () => {
34
+ render(CkPromptMenuButton, {
35
+ props: { ariaLabel: 'Open prompt menu', disabled: true },
36
+ });
37
+
38
+ expect(
39
+ screen.getByRole('button', { name: 'Open prompt menu' }),
40
+ ).toBeDisabled();
41
+ });
42
+
43
+ it('defaults type to button', () => {
44
+ render(CkPromptMenuButton, {
45
+ props: { ariaLabel: 'Open prompt menu' },
46
+ });
47
+
48
+ expect(
49
+ screen.getByRole('button', { name: 'Open prompt menu' }),
50
+ ).toHaveAttribute('type', 'button');
51
+ });
52
+
53
+ it('renders as a pill (rounded-full)', () => {
54
+ render(CkPromptMenuButton, {
55
+ props: { ariaLabel: 'Open prompt menu' },
56
+ });
57
+
58
+ expect(
59
+ screen.getByRole('button', { name: 'Open prompt menu' }).className,
60
+ ).toContain('!rounded-full');
61
+ });
62
+ });
@@ -0,0 +1,75 @@
1
+ import CkPromptMenuButton from './CkPromptMenuButton.vue';
2
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
3
+
4
+ const meta: Meta<typeof CkPromptMenuButton> = {
5
+ title: 'Base/CkPromptMenuButton',
6
+ component: CkPromptMenuButton,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ ariaLabel: {
10
+ control: 'text',
11
+ description:
12
+ 'Accessible label for the button. Required because the button is icon-only.',
13
+ },
14
+ disabled: {
15
+ control: 'boolean',
16
+ description: 'Whether the button is disabled.',
17
+ },
18
+ type: {
19
+ control: 'radio',
20
+ options: ['button', 'submit'],
21
+ description: 'Native button type. Use `submit` inside a <form>.',
22
+ },
23
+ },
24
+ decorators: [
25
+ () => ({
26
+ template:
27
+ '<div style="max-width: 640px; margin: 0 auto; padding: 24px;"><story /></div>',
28
+ }),
29
+ ],
30
+ };
31
+
32
+ export default meta;
33
+ type Story = StoryObj<typeof meta>;
34
+
35
+ const defaultTemplateSource = `<script setup lang="ts">
36
+ import { CkPromptMenuButton } from '@aleph-alpha/chat-kit';
37
+ </script>
38
+
39
+ <template>
40
+ <CkPromptMenuButton aria-label="Open prompt menu" />
41
+ </template>`;
42
+
43
+ const disabledTemplateSource = `<script setup lang="ts">
44
+ import { CkPromptMenuButton } from '@aleph-alpha/chat-kit';
45
+ </script>
46
+
47
+ <template>
48
+ <CkPromptMenuButton aria-label="Open prompt menu" disabled />
49
+ </template>`;
50
+
51
+ /**
52
+ * Default menu-trigger button — ghost variant with the built-in plus icon.
53
+ * Drop into a `CkPromptInput` left toolbar slot, wrap with `UiDropdownMenu`
54
+ * to expose the actual prompt actions (attach files, attach folders, toggle
55
+ * tools, etc.).
56
+ */
57
+ export const Default: Story = {
58
+ render: () => ({
59
+ components: { CkPromptMenuButton },
60
+ template: `<CkPromptMenuButton aria-label="Open prompt menu" />`,
61
+ }),
62
+ parameters: { docs: { source: { code: defaultTemplateSource } } },
63
+ };
64
+
65
+ /**
66
+ * Disabled state — use while the menu's actions are unavailable (e.g. while
67
+ * the previous request is still in flight).
68
+ */
69
+ export const Disabled: Story = {
70
+ render: () => ({
71
+ components: { CkPromptMenuButton },
72
+ template: `<CkPromptMenuButton aria-label="Open prompt menu" disabled />`,
73
+ }),
74
+ parameters: { docs: { source: { code: disabledTemplateSource } } },
75
+ };
@@ -0,0 +1,26 @@
1
+ <script setup lang="ts">
2
+ import type { CkPromptMenuButtonProps } from './types';
3
+ import { UiButton, UiIcon } from '@aleph-alpha/ui-library';
4
+
5
+ defineOptions({ name: 'CkPromptMenuButton' });
6
+
7
+ withDefaults(defineProps<CkPromptMenuButtonProps>(), {
8
+ disabled: false,
9
+ type: 'button',
10
+ });
11
+ </script>
12
+
13
+ <template>
14
+ <UiButton
15
+ v-bind="{ ariaLabel }"
16
+ variant="ghost"
17
+ size="icon"
18
+ :type="type"
19
+ :disabled="disabled"
20
+ class="!rounded-full"
21
+ >
22
+ <slot>
23
+ <UiIcon name="plus" :size="20" />
24
+ </slot>
25
+ </UiButton>
26
+ </template>
@@ -0,0 +1,2 @@
1
+ export { default as CkPromptMenuButton } from './CkPromptMenuButton.vue';
2
+ export type * from './types';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Icon-only menu trigger styled for the chat prompt area (ghost variant,
3
+ * pill-shaped). Sits in `CkPromptInput`'s `toolbar-left` slot as the entry
4
+ * point to a dropdown of prompt actions — attach files, attach folders,
5
+ * toggle tools, switch length, etc. Defaults to a `plus` icon when the slot
6
+ * is empty; pass any icon to override.
7
+ *
8
+ * @category Actions
9
+ * @useCases open prompt menu, attach files, add resources, toggle tools, prompt input toolbar
10
+ * @keywords prompt, menu, plus, add, button, icon, ghost, dropdown trigger, tools
11
+ * @related CkPromptInput, CkPromptSendButton, CkPromptStopButton, CkDropZoneOverlay, CkFileItemsList
12
+ */
13
+ export interface CkPromptMenuButtonProps {
14
+ /**
15
+ * Accessible label for the button. **Required** because the button only
16
+ * renders an icon.
17
+ */
18
+ ariaLabel: string;
19
+ /**
20
+ * Whether the button is disabled.
21
+ * @default false
22
+ */
23
+ disabled?: boolean;
24
+ /**
25
+ * Native button `type`.
26
+ * @default 'button'
27
+ */
28
+ type?: 'button' | 'submit';
29
+ }
@@ -1,9 +1,9 @@
1
- import CkPromptActionButton from './CkPromptActionButton.vue';
1
+ import CkPromptSendButton from './CkPromptSendButton.vue';
2
2
  import { render, screen } from '@testing-library/vue';
3
3
 
4
- describe('CkPromptActionButton', () => {
4
+ describe('CkPromptSendButton', () => {
5
5
  it('renders icon slot content', () => {
6
- render(CkPromptActionButton, {
6
+ render(CkPromptSendButton, {
7
7
  props: { ariaLabel: 'Send' },
8
8
  slots: { default: '<svg data-testid="icon"></svg>' },
9
9
  });
@@ -12,7 +12,7 @@ describe('CkPromptActionButton', () => {
12
12
  });
13
13
 
14
14
  it('passes ariaLabel to the underlying button', () => {
15
- render(CkPromptActionButton, {
15
+ render(CkPromptSendButton, {
16
16
  props: { ariaLabel: 'Send message' },
17
17
  });
18
18
 
@@ -22,7 +22,7 @@ describe('CkPromptActionButton', () => {
22
22
  });
23
23
 
24
24
  it('passes disabled state to the underlying button', () => {
25
- render(CkPromptActionButton, {
25
+ render(CkPromptSendButton, {
26
26
  props: { ariaLabel: 'Send', disabled: true },
27
27
  });
28
28
 
@@ -30,7 +30,7 @@ describe('CkPromptActionButton', () => {
30
30
  });
31
31
 
32
32
  it('defaults type to button', () => {
33
- render(CkPromptActionButton, {
33
+ render(CkPromptSendButton, {
34
34
  props: { ariaLabel: 'Send' },
35
35
  });
36
36
 
@@ -40,13 +40,13 @@ describe('CkPromptActionButton', () => {
40
40
  );
41
41
  });
42
42
 
43
- it('applies brand styling classes', () => {
44
- render(CkPromptActionButton, {
43
+ it('renders as a pill (rounded-full)', () => {
44
+ render(CkPromptSendButton, {
45
45
  props: { ariaLabel: 'Send' },
46
46
  });
47
47
 
48
- const button = screen.getByRole('button', { name: 'Send' });
49
- expect(button.className).toContain('!bg-brand-bg-brand');
50
- expect(button.className).toContain('!rounded-md');
48
+ expect(screen.getByRole('button', { name: 'Send' }).className).toContain(
49
+ '!rounded-full',
50
+ );
51
51
  });
52
52
  });
@@ -0,0 +1,117 @@
1
+ import CkPromptSendButton from './CkPromptSendButton.vue';
2
+ import CkSendIcon from '../icons/CkSendIcon.vue';
3
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
4
+
5
+ const meta: Meta<typeof CkPromptSendButton> = {
6
+ title: 'Base/CkPromptSendButton',
7
+ component: CkPromptSendButton,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ ariaLabel: {
11
+ control: 'text',
12
+ description:
13
+ 'Accessible label for the button. Required because the button is icon-only.',
14
+ },
15
+ disabled: {
16
+ control: 'boolean',
17
+ description: 'Whether the button is disabled.',
18
+ },
19
+ loading: {
20
+ control: 'boolean',
21
+ description:
22
+ 'Shows a spinner instead of the slotted icon and blocks clicks.',
23
+ },
24
+ type: {
25
+ control: 'radio',
26
+ options: ['button', 'submit'],
27
+ description: 'Native button type. Use `submit` inside a <form>.',
28
+ },
29
+ },
30
+ decorators: [
31
+ () => ({
32
+ template:
33
+ '<div style="max-width: 640px; margin: 0 auto; padding: 24px;"><story /></div>',
34
+ }),
35
+ ],
36
+ };
37
+
38
+ export default meta;
39
+ type Story = StoryObj<typeof meta>;
40
+
41
+ const defaultTemplateSource = `<script setup lang="ts">
42
+ import { CkPromptSendButton, CkSendIcon } from '@aleph-alpha/chat-kit';
43
+ </script>
44
+
45
+ <template>
46
+ <CkPromptSendButton aria-label="Send message">
47
+ <CkSendIcon />
48
+ </CkPromptSendButton>
49
+ </template>`;
50
+
51
+ const disabledTemplateSource = `<script setup lang="ts">
52
+ import { CkPromptSendButton, CkSendIcon } from '@aleph-alpha/chat-kit';
53
+ </script>
54
+
55
+ <template>
56
+ <CkPromptSendButton aria-label="Send message" disabled>
57
+ <CkSendIcon />
58
+ </CkPromptSendButton>
59
+ </template>`;
60
+
61
+ const loadingTemplateSource = `<script setup lang="ts">
62
+ import { CkPromptSendButton, CkSendIcon } from '@aleph-alpha/chat-kit';
63
+ </script>
64
+
65
+ <template>
66
+ <CkPromptSendButton aria-label="Send message" loading>
67
+ <CkSendIcon />
68
+ </CkPromptSendButton>
69
+ </template>`;
70
+
71
+ /**
72
+ * Default send button. Drop into a `CkPromptInput`'s `toolbar-right` slot to
73
+ * submit the prompt.
74
+ */
75
+ export const Default: Story = {
76
+ render: () => ({
77
+ components: { CkPromptSendButton, CkSendIcon },
78
+ template: `
79
+ <CkPromptSendButton aria-label="Send message">
80
+ <CkSendIcon />
81
+ </CkPromptSendButton>
82
+ `,
83
+ }),
84
+ parameters: { docs: { source: { code: defaultTemplateSource } } },
85
+ };
86
+
87
+ /**
88
+ * Disabled state — use when the input is empty or a submit is otherwise
89
+ * blocked (e.g. rate-limited).
90
+ */
91
+ export const Disabled: Story = {
92
+ render: () => ({
93
+ components: { CkPromptSendButton, CkSendIcon },
94
+ template: `
95
+ <CkPromptSendButton aria-label="Send message" disabled>
96
+ <CkSendIcon />
97
+ </CkPromptSendButton>
98
+ `,
99
+ }),
100
+ parameters: { docs: { source: { code: disabledTemplateSource } } },
101
+ };
102
+
103
+ /**
104
+ * Loading state — shows a spinner and blocks clicks. Use while the previous
105
+ * message is still streaming or the request is in flight.
106
+ */
107
+ export const Loading: Story = {
108
+ render: () => ({
109
+ components: { CkPromptSendButton, CkSendIcon },
110
+ template: `
111
+ <CkPromptSendButton aria-label="Send message" loading>
112
+ <CkSendIcon />
113
+ </CkPromptSendButton>
114
+ `,
115
+ }),
116
+ parameters: { docs: { source: { code: loadingTemplateSource } } },
117
+ };
@@ -1,10 +1,10 @@
1
1
  <script setup lang="ts">
2
- import type { CkPromptActionButtonProps } from './types';
2
+ import type { CkPromptSendButtonProps } from './types';
3
3
  import { UiButton } from '@aleph-alpha/ui-library';
4
4
 
5
- defineOptions({ name: 'CkPromptActionButton' });
5
+ defineOptions({ name: 'CkPromptSendButton' });
6
6
 
7
- withDefaults(defineProps<CkPromptActionButtonProps>(), {
7
+ withDefaults(defineProps<CkPromptSendButtonProps>(), {
8
8
  disabled: false,
9
9
  loading: false,
10
10
  type: 'button',
@@ -18,7 +18,7 @@ withDefaults(defineProps<CkPromptActionButtonProps>(), {
18
18
  :type="type"
19
19
  :disabled="disabled"
20
20
  :loading="loading"
21
- class="!bg-brand-bg-brand hover:!bg-brand-bg-brand-hover !text-brand-content-default !rounded-md"
21
+ class="!rounded-full"
22
22
  >
23
23
  <slot />
24
24
  </UiButton>
@@ -0,0 +1,2 @@
1
+ export { default as CkPromptSendButton } from './CkPromptSendButton.vue';
2
+ export type * from './types';