@nuxt/ui 4.6.0 → 4.6.1

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.
package/README.md CHANGED
@@ -126,7 +126,7 @@ Thank you for considering contributing to Nuxt UI. Here are a few ways you can g
126
126
  - Suggestions: Have any thoughts to enhance Nuxt UI? We'd love to hear them! Check out the [contribution guide](https://ui.nuxt.com/docs/getting-started/contribution) to share your suggestions.
127
127
 
128
128
  > [!TIP]
129
- > We provide a [contributing skill](https://github.com/nuxt/ui/tree/v4/.claude/skills/contributing) for AI assistants to help you contribute to Nuxt UI. It will automatically guide you through component structure, theming patterns, testing conventions, and documentation guidelines when working in this repository.
129
+ > We provide contributing guidelines through [`AGENTS.md`](https://github.com/nuxt/ui/blob/v4/AGENTS.md) for AI assistants to help you contribute to Nuxt UI. It is automatically picked up by all AI coding agents and guides through component structure, theming patterns, testing conventions, and documentation guidelines.
130
130
 
131
131
  ## Local Development
132
132
 
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuxt/ui",
3
- "version": "4.6.0",
3
+ "version": "4.6.1",
4
4
  "docs": "https://ui.nuxt.com/docs/getting-started/installation/nuxt",
5
5
  "configKey": "ui",
6
6
  "compatibility": {
package/dist/module.mjs CHANGED
@@ -11,7 +11,7 @@ import 'pathe';
11
11
  import 'tinyglobby';
12
12
 
13
13
  const name = "@nuxt/ui";
14
- const version = "4.6.0";
14
+ const version = "4.6.1";
15
15
 
16
16
  const module$1 = defineNuxtModule({
17
17
  meta: {
@@ -97,6 +97,7 @@ const module$1 = defineNuxtModule({
97
97
  nuxt.options.ui = options;
98
98
  nuxt.options.alias["#ui"] = resolve("./runtime");
99
99
  nuxt.options.appConfig.ui = defu(nuxt.options.appConfig.ui || {}, getDefaultConfig(options.theme));
100
+ nuxt.options.build.transpile.push("reka-ui");
100
101
  nuxt.options.app.rootAttrs = nuxt.options.app.rootAttrs || {};
101
102
  nuxt.options.app.rootAttrs.class = [nuxt.options.app.rootAttrs.class, `${options.theme?.prefix ? options.theme.prefix + ":" : ""}isolate`].filter(Boolean).join(" ");
102
103
  nuxt.hook("vite:extend", async ({ config }) => {
@@ -44,7 +44,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...appConfig.ui?.chatMessage |
44
44
 
45
45
  <template>
46
46
  <Primitive :as="as" :data-role="role" data-slot="root" :class="ui.root({ class: [uiProp?.root, props.class] })">
47
- <div v-if="!!slots.files" data-slot="files" :class="ui.files({ class: uiProp?.files })">
47
+ <div v-if="!!slots.files && fileParts.length" data-slot="files" :class="ui.files({ class: uiProp?.files })">
48
48
  <slot name="files" :parts="fileParts" />
49
49
  </div>
50
50
 
@@ -29,6 +29,12 @@ const props = defineProps({
29
29
  });
30
30
  const slots = defineSlots();
31
31
  const getProxySlots = () => omit(slots, ["default", "indicator", "viewport"]);
32
+ const showIndicator = computed(() => {
33
+ if (props.status === "submitted") return true;
34
+ if (props.status !== "streaming") return false;
35
+ const lastMessage = props.messages?.[props.messages.length - 1];
36
+ return lastMessage?.role === "assistant" && !lastMessage.parts?.length;
37
+ });
32
38
  const appConfig = useAppConfig();
33
39
  const uiProp = useComponentUI("chatMessages", props);
34
40
  const userProps = toRef(() => defu(props.user, { side: "right", variant: "soft" }));
@@ -67,6 +73,13 @@ function scrollToBottom(smooth = true) {
67
73
  }
68
74
  }
69
75
  watchThrottled([() => props.messages, () => props.status], ([_, status]) => {
76
+ if (!props.messages?.length) {
77
+ showAutoScroll.value = false;
78
+ userScrolledUp.value = false;
79
+ lastScrollTop.value = 0;
80
+ messagesRefs.value.clear();
81
+ return;
82
+ }
70
83
  if (status !== "streaming") {
71
84
  return;
72
85
  }
@@ -197,7 +210,7 @@ onMounted(() => {
197
210
  </slot>
198
211
 
199
212
  <UChatMessage
200
- v-if="status === 'submitted'"
213
+ v-if="showIndicator"
201
214
  id="indicator"
202
215
  role="assistant"
203
216
  v-bind="{ ...assistantProps, actions: void 0, parts: [] }"
@@ -8,6 +8,7 @@ import { Primitive, useForwardProps } from "reka-ui";
8
8
  import { reactivePick } from "@vueuse/core";
9
9
  import { useAppConfig } from "#imports";
10
10
  import { useComponentUI } from "../composables/useComponentUI";
11
+ import { useIMEGuard } from "../composables/useIMEGuard";
11
12
  import { useLocale } from "../composables/useLocale";
12
13
  import { omit, transformUI } from "../utils";
13
14
  import { tv } from "../utils/tv";
@@ -54,6 +55,9 @@ function blur(e) {
54
55
  textareaRef.value?.textareaRef?.blur();
55
56
  emits("close", e);
56
57
  }
58
+ const { onKeydown: onEnter, onCompositionEnd } = useIMEGuard((event) => {
59
+ submit(event);
60
+ });
57
61
  defineExpose({
58
62
  textareaRef: toRef(() => textareaRef.value?.textareaRef)
59
63
  });
@@ -76,7 +80,8 @@ defineExpose({
76
80
  :ui="transformUI(omit(ui, ['root', 'body', 'header', 'footer']), uiProp)"
77
81
  data-slot="body"
78
82
  :class="ui.body({ class: uiProp?.body })"
79
- @keydown.enter.exact.prevent="submit"
83
+ @keydown.enter.exact="onEnter"
84
+ @compositionend="onCompositionEnd"
80
85
  @keydown.esc="blur"
81
86
  >
82
87
  <template v-for="(_, name) in getProxySlots()" #[name]="slotData">
@@ -151,17 +151,20 @@ function toggleOpen() {
151
151
  <div v-if="!!slots.header || mode !== 'drawer'" data-slot="header" :class="ui.header({ class: uiProp?.header, menu: true })">
152
152
  <ReuseToggleTemplate v-if="mode !== 'drawer' && toggleSide === 'left'" />
153
153
 
154
- <slot name="header" :collapsed="isCollapsed" :collapse="collapse" />
154
+ <slot name="header" :collapsed="false" :collapse="() => {
155
+ }" />
155
156
 
156
157
  <ReuseToggleTemplate v-if="mode !== 'drawer' && toggleSide === 'right'" />
157
158
  </div>
158
159
 
159
160
  <div data-slot="body" :class="ui.body({ class: uiProp?.body, menu: true })">
160
- <slot :collapsed="isCollapsed" :collapse="collapse" />
161
+ <slot :collapsed="false" :collapse="() => {
162
+ }" />
161
163
  </div>
162
164
 
163
165
  <div v-if="!!slots.footer" data-slot="footer" :class="ui.footer({ class: uiProp?.footer, menu: true })">
164
- <slot name="footer" :collapsed="isCollapsed" :collapse="collapse" />
166
+ <slot name="footer" :collapsed="false" :collapse="() => {
167
+ }" />
165
168
  </div>
166
169
  </slot>
167
170
  </template>
@@ -81,14 +81,16 @@ const ui = computed(() => tv({ extend: tv(theme), ...appConfig.ui?.drawer || {}
81
81
  <DrawerContent data-slot="content" :class="ui.content({ class: [!slots.default && props.class, uiProp?.content] })" v-bind="contentProps" v-on="contentEvents">
82
82
  <DrawerHandle v-if="handle" data-slot="handle" :class="ui.handle({ class: uiProp?.handle })" />
83
83
 
84
- <VisuallyHidden v-if="!!slots.content && (title || !!slots.title || (description || !!slots.description))">
85
- <DrawerTitle v-if="title || !!slots.title">
84
+ <VisuallyHidden v-if="!title && !slots.title || !description && !slots.description || !!slots.content">
85
+ <DrawerTitle v-if="!title && !slots.title" />
86
+ <DrawerTitle v-else-if="!!slots.content">
86
87
  <slot name="title">
87
88
  {{ title }}
88
89
  </slot>
89
90
  </DrawerTitle>
90
91
 
91
- <DrawerDescription v-if="description || !!slots.description">
92
+ <DrawerDescription v-if="!description && !slots.description" />
93
+ <DrawerDescription v-else-if="!!slots.content">
92
94
  <slot name="description">
93
95
  {{ description }}
94
96
  </slot>
@@ -11,10 +11,10 @@ const props = defineProps({
11
11
  size: { type: [String, Number], required: false },
12
12
  customize: { type: [Function, Boolean, null], required: false }
13
13
  });
14
- const iconProps = useForwardProps(reactivePick(props, "name", "mode", "size", "customize"));
14
+ const iconProps = useForwardProps(reactivePick(props, "mode", "size", "customize"));
15
15
  </script>
16
16
 
17
17
  <template>
18
- <Icon v-if="typeof name === 'string'" v-bind="iconProps" />
18
+ <Icon v-if="typeof name === 'string'" :name="name" v-bind="iconProps" />
19
19
  <component :is="name" v-else />
20
20
  </template>
@@ -74,14 +74,16 @@ const ui = computed(() => tv({ extend: tv(theme), ...appConfig.ui?.modal || {} }
74
74
  @after-leave="emits('after:leave')"
75
75
  v-on="contentEvents"
76
76
  >
77
- <VisuallyHidden v-if="!!slots.content && (title || !!slots.title || (description || !!slots.description))">
78
- <DialogTitle v-if="title || !!slots.title">
77
+ <VisuallyHidden v-if="!title && !slots.title || !description && !slots.description || !!slots.content">
78
+ <DialogTitle v-if="!title && !slots.title" />
79
+ <DialogTitle v-else-if="!!slots.content">
79
80
  <slot name="title">
80
81
  {{ title }}
81
82
  </slot>
82
83
  </DialogTitle>
83
84
 
84
- <DialogDescription v-if="description || !!slots.description">
85
+ <DialogDescription v-if="!description && !slots.description" />
86
+ <DialogDescription v-else-if="!!slots.content">
85
87
  <slot name="description">
86
88
  {{ description }}
87
89
  </slot>
@@ -79,14 +79,16 @@ const ui = computed(() => tv({ extend: tv(theme), ...appConfig.ui?.slideover ||
79
79
  @after-leave="emits('after:leave')"
80
80
  v-on="contentEvents"
81
81
  >
82
- <VisuallyHidden v-if="!!slots.content && (title || !!slots.title || (description || !!slots.description))">
83
- <DialogTitle v-if="title || !!slots.title">
82
+ <VisuallyHidden v-if="!title && !slots.title || !description && !slots.description || !!slots.content">
83
+ <DialogTitle v-if="!title && !slots.title" />
84
+ <DialogTitle v-else-if="!!slots.content">
84
85
  <slot name="title">
85
86
  {{ title }}
86
87
  </slot>
87
88
  </DialogTitle>
88
89
 
89
- <DialogDescription v-if="description || !!slots.description">
90
+ <DialogDescription v-if="!description && !slots.description" />
91
+ <DialogDescription v-else-if="!!slots.content">
90
92
  <slot name="description">
91
93
  {{ description }}
92
94
  </slot>
@@ -9,6 +9,7 @@ export * from './useFieldGroup';
9
9
  export * from './useFileUpload';
10
10
  export * from './useFilter';
11
11
  export * from './useFormField';
12
+ export * from './useIMEGuard';
12
13
  export * from './useKbd';
13
14
  export * from './useLocale';
14
15
  export * from './useOverlay';
@@ -9,6 +9,7 @@ export * from "./useFieldGroup.js";
9
9
  export * from "./useFileUpload.js";
10
10
  export * from "./useFilter.js";
11
11
  export * from "./useFormField.js";
12
+ export * from "./useIMEGuard.js";
12
13
  export * from "./useKbd.js";
13
14
  export * from "./useLocale.js";
14
15
  export * from "./useOverlay.js";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Prevents Enter-to-submit from firing during IME composition.
3
+ *
4
+ * Handles the Safari quirk where `compositionend` fires before `keydown`,
5
+ * making `event.isComposing` unreliable (https://bugs.webkit.org/show_bug.cgi?id=165004).
6
+ */
7
+ export declare function useIMEGuard(callback: (event: KeyboardEvent) => void): {
8
+ onKeydown: (event: KeyboardEvent) => void;
9
+ onCompositionEnd: () => void;
10
+ };
@@ -0,0 +1,22 @@
1
+ export function useIMEGuard(callback) {
2
+ let compositionJustEnded = false;
3
+ function isComposing(event) {
4
+ return event.isComposing || event.keyCode === 229;
5
+ }
6
+ function onKeydown(event) {
7
+ if (compositionJustEnded || isComposing(event)) {
8
+ return;
9
+ }
10
+ event.preventDefault();
11
+ callback(event);
12
+ }
13
+ let compositionEndTimer;
14
+ function onCompositionEnd() {
15
+ clearTimeout(compositionEndTimer);
16
+ compositionJustEnded = true;
17
+ compositionEndTimer = setTimeout(() => {
18
+ compositionJustEnded = false;
19
+ }, 50);
20
+ }
21
+ return { onKeydown, onCompositionEnd };
22
+ }
@@ -8,14 +8,10 @@ import type { UIMessage } from 'ai';
8
8
  */
9
9
  export declare function getTextFromMessage(message: UIMessage): string;
10
10
  /**
11
- * Checks if a reasoning part is currently being streamed.
12
- *
13
- * Returns `true` when the chat is streaming, the message is the last one,
14
- * and no parts of a different type follow the given part index.
11
+ * Checks if a text or reasoning part is currently being streamed.
15
12
  */
16
- export declare function isReasoningStreaming(message: UIMessage, partIndex: number, chat: {
17
- status: string;
18
- messages: UIMessage[];
13
+ export declare function isPartStreaming(part: {
14
+ state?: string;
19
15
  }): boolean;
20
16
  /**
21
17
  * Checks if a tool part is still streaming (hasn't reached a terminal state).
@@ -25,3 +21,12 @@ export declare function isReasoningStreaming(message: UIMessage, partIndex: numb
25
21
  export declare function isToolStreaming(part: {
26
22
  state: string;
27
23
  }): boolean;
24
+ /**
25
+ * Checks if a reasoning part is currently being streamed.
26
+ *
27
+ * @deprecated Use `isPartStreaming` instead.
28
+ */
29
+ export declare function isReasoningStreaming(message: UIMessage, partIndex: number, chat: {
30
+ status: string;
31
+ messages: UIMessage[];
32
+ }): boolean;
@@ -1,6 +1,12 @@
1
1
  export function getTextFromMessage(message) {
2
2
  return message.parts.filter((part) => part.type === "text").map((part) => part.text).join("");
3
3
  }
4
+ export function isPartStreaming(part) {
5
+ return part.state === "streaming";
6
+ }
7
+ export function isToolStreaming(part) {
8
+ return !["output-available", "output-error", "output-denied"].includes(part.state);
9
+ }
4
10
  export function isReasoningStreaming(message, partIndex, chat) {
5
11
  if (chat.status !== "streaming") return false;
6
12
  if (message.id !== chat.messages.at(-1)?.id) return false;
@@ -11,6 +17,3 @@ export function isReasoningStreaming(message, partIndex, chat) {
11
17
  }
12
18
  return true;
13
19
  }
14
- export function isToolStreaming(part) {
15
- return !["output-available", "output-error", "output-denied"].includes(part.state);
16
- }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@nuxt/ui",
3
3
  "description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
4
- "version": "4.6.0",
5
- "packageManager": "pnpm@10.32.1",
4
+ "version": "4.6.1",
5
+ "packageManager": "pnpm@10.33.0",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/nuxt/ui.git"
@@ -140,23 +140,23 @@
140
140
  "@tailwindcss/vite": "^4.2.2",
141
141
  "@tanstack/vue-table": "^8.21.3",
142
142
  "@tanstack/vue-virtual": "^3.13.23",
143
- "@tiptap/core": "^3.20.4",
144
- "@tiptap/extension-bubble-menu": "^3.20.4",
145
- "@tiptap/extension-code": "^3.20.4",
146
- "@tiptap/extension-collaboration": "^3.20.4",
147
- "@tiptap/extension-drag-handle": "^3.20.4",
148
- "@tiptap/extension-drag-handle-vue-3": "^3.20.4",
149
- "@tiptap/extension-floating-menu": "^3.20.4",
150
- "@tiptap/extension-horizontal-rule": "^3.20.4",
151
- "@tiptap/extension-image": "^3.20.4",
152
- "@tiptap/extension-mention": "^3.20.4",
153
- "@tiptap/extension-node-range": "^3.20.4",
154
- "@tiptap/extension-placeholder": "^3.20.4",
155
- "@tiptap/markdown": "^3.20.4",
156
- "@tiptap/pm": "^3.20.4",
157
- "@tiptap/starter-kit": "^3.20.4",
158
- "@tiptap/suggestion": "^3.20.4",
159
- "@tiptap/vue-3": "^3.20.4",
143
+ "@tiptap/core": "^3.21.0",
144
+ "@tiptap/extension-bubble-menu": "^3.21.0",
145
+ "@tiptap/extension-code": "^3.21.0",
146
+ "@tiptap/extension-collaboration": "^3.21.0",
147
+ "@tiptap/extension-drag-handle": "^3.21.0",
148
+ "@tiptap/extension-drag-handle-vue-3": "^3.21.0",
149
+ "@tiptap/extension-floating-menu": "^3.21.0",
150
+ "@tiptap/extension-horizontal-rule": "^3.21.0",
151
+ "@tiptap/extension-image": "^3.21.0",
152
+ "@tiptap/extension-mention": "^3.21.0",
153
+ "@tiptap/extension-node-range": "^3.21.0",
154
+ "@tiptap/extension-placeholder": "^3.21.0",
155
+ "@tiptap/markdown": "^3.21.0",
156
+ "@tiptap/pm": "^3.21.0",
157
+ "@tiptap/starter-kit": "^3.21.0",
158
+ "@tiptap/suggestion": "^3.21.0",
159
+ "@tiptap/vue-3": "^3.21.0",
160
160
  "@unhead/vue": "^2.1.12",
161
161
  "@vueuse/core": "^14.2.1",
162
162
  "@vueuse/integrations": "^14.2.1",
@@ -179,7 +179,7 @@
179
179
  "motion-v": "^2.2.0",
180
180
  "ohash": "^2.0.11",
181
181
  "pathe": "^2.0.3",
182
- "reka-ui": "2.9.2",
182
+ "reka-ui": "2.9.3",
183
183
  "scule": "^1.3.0",
184
184
  "tailwind-merge": "^3.5.0",
185
185
  "tailwind-variants": "^3.2.2",
@@ -188,7 +188,7 @@
188
188
  "ufo": "^1.6.3",
189
189
  "unplugin": "^3.0.0",
190
190
  "unplugin-auto-import": "^21.0.0",
191
- "unplugin-vue-components": "^31.1.0",
191
+ "unplugin-vue-components": "^32.0.0",
192
192
  "vaul-vue": "0.4.1",
193
193
  "vue-component-type-helpers": "^3.2.6"
194
194
  },
@@ -200,26 +200,26 @@
200
200
  "@tanstack/table-core": "^8.21.3",
201
201
  "@vitejs/plugin-vue": "^6.0.5",
202
202
  "@vue/test-utils": "^2.4.6",
203
- "ai": "^6.0.134",
203
+ "ai": "^6.0.141",
204
204
  "embla-carousel": "^8.6.0",
205
205
  "eslint": "^10.1.0",
206
- "happy-dom": "^20.8.4",
206
+ "happy-dom": "^20.8.9",
207
207
  "nuxt": "^4.4.2",
208
208
  "release-it": "^19.2.4",
209
209
  "unbuild": "^3.6.1",
210
- "vitest": "^4.1.0",
210
+ "vitest": "^4.1.2",
211
211
  "vitest-axe": "^0.1.0",
212
212
  "vitest-environment-nuxt": "^1.0.1",
213
213
  "vue": "^3.5.30",
214
214
  "vue-tsc": "^3.2.6"
215
215
  },
216
216
  "peerDependencies": {
217
- "@inertiajs/vue3": "^2.0.7",
217
+ "@inertiajs/vue3": "^2.0.7 || ^3.0.0",
218
218
  "@nuxt/content": "^3.0.0",
219
219
  "joi": "^18.0.0",
220
220
  "superstruct": "^2.0.0",
221
221
  "tailwindcss": "^4.0.0",
222
- "typescript": "^5.6.3",
222
+ "typescript": "^5.6.3 || ^6.0.0",
223
223
  "valibot": "^1.0.0",
224
224
  "vue-router": "^4.5.0 || ^5.0.0",
225
225
  "yup": "^1.7.0",