@proj-airi/ui 0.9.0-beta.6 → 0.9.0-rc.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@proj-airi/ui",
3
3
  "type": "module",
4
- "version": "0.9.0-beta.6",
4
+ "version": "0.9.0-rc.1",
5
5
  "description": "A collection of UI components that used by Project AIRI",
6
6
  "author": {
7
7
  "name": "Moeru AI Project AIRI Team",
@@ -23,6 +23,7 @@
23
23
  "./*": "./*"
24
24
  },
25
25
  "dependencies": {
26
+ "@moeru/std": "0.1.0-beta.17",
26
27
  "@vueuse/core": "^14.2.1",
27
28
  "floating-vue": "^5.2.2",
28
29
  "reka-ui": "^2.9.2",
@@ -0,0 +1,38 @@
1
+ <script setup lang="ts">
2
+ import { InputFile } from '../input'
3
+
4
+ const props = defineProps<{
5
+ label?: string
6
+ description?: string
7
+ accept?: string
8
+ multiple?: boolean
9
+ placeholder?: string
10
+ }>()
11
+
12
+ const modelValue = defineModel<File[] | undefined>({ required: false })
13
+ </script>
14
+
15
+ <template>
16
+ <div class="max-w-full">
17
+ <label class="flex flex-col gap-4">
18
+ <div>
19
+ <div class="flex items-center gap-1 text-sm font-medium">
20
+ <slot name="label">
21
+ {{ props.label }}
22
+ </slot>
23
+ </div>
24
+ <div class="text-xs text-neutral-500 dark:text-neutral-400">
25
+ <slot name="description">
26
+ {{ props.description }}
27
+ </slot>
28
+ </div>
29
+ </div>
30
+ <InputFile
31
+ v-model="modelValue"
32
+ :accept="props.accept"
33
+ :multiple="props.multiple"
34
+ :placeholder="props.placeholder"
35
+ />
36
+ </label>
37
+ </div>
38
+ </template>
@@ -1,8 +1,10 @@
1
1
  <script
2
2
  setup
3
3
  lang="ts"
4
- generic="InputType extends 'number' | string, T = InputType extends 'number' ? (number | undefined) : ((string | undefined))"
4
+ generic="InputType extends 'number' | InputTypeHTMLAttribute | string, T = InputType extends 'number' ? (number | undefined) : ((string | undefined))"
5
5
  >
6
+ import type { InputTypeHTMLAttribute } from 'vue'
7
+
6
8
  import { Input } from '../input'
7
9
 
8
10
  const props = withDefaults(defineProps<{
@@ -0,0 +1,105 @@
1
+ <script setup lang="ts" generic="T extends AcceptableValue">
2
+ import type { AcceptableValue } from 'reka-ui'
3
+
4
+ import { Select } from '../select'
5
+
6
+ interface SelectOptionItem<T extends AcceptableValue> {
7
+ label: string
8
+ value: T
9
+ description?: string
10
+ disabled?: boolean
11
+ icon?: string
12
+ }
13
+
14
+ interface SelectOptionGroupItem<T extends AcceptableValue> {
15
+ groupLabel?: string
16
+ children?: SelectOptionItem<T>[]
17
+ }
18
+
19
+ const props = withDefaults(defineProps<{
20
+ label: string
21
+ description?: string
22
+ options?: SelectOptionItem<T>[] | SelectOptionGroupItem<T>[]
23
+ placeholder?: string
24
+ disabled?: boolean
25
+ layout?: 'horizontal' | 'vertical'
26
+ selectClass?: string | string[]
27
+ by?: string | ((a: T, b: T) => boolean)
28
+ contentMinWidth?: string | number
29
+ contentWidth?: string | number
30
+ shape?: 'rounded' | 'default'
31
+ variant?: 'blurry' | 'default'
32
+ }>(), {
33
+ layout: 'horizontal',
34
+ })
35
+
36
+ const modelValue = defineModel<T>({ required: false })
37
+ </script>
38
+
39
+ <template>
40
+ <label :class="['flex', 'flex-col', 'gap-4']">
41
+ <div
42
+ :class="[
43
+ 'items-center',
44
+ props.layout === 'horizontal' ? 'grid grid-cols-4 gap-2' : 'grid grid-rows-2 gap-2',
45
+ ]"
46
+ >
47
+ <div
48
+ :class="[
49
+ 'w-full',
50
+ props.layout === 'horizontal' ? 'col-span-2' : 'row-span-2',
51
+ ]"
52
+ >
53
+ <div :class="['flex', 'items-center', 'gap-1', 'break-words', 'text-sm', 'font-medium', 'text-left']">
54
+ <slot name="label">
55
+ {{ props.label }}
56
+ </slot>
57
+ </div>
58
+ <div :class="['break-words', 'text-xs', 'text-neutral-500', 'dark:text-neutral-400', 'text-left']">
59
+ <slot name="description">
60
+ {{ props.description }}
61
+ </slot>
62
+ </div>
63
+ </div>
64
+ <slot>
65
+ <Select
66
+ v-model="modelValue"
67
+ :options="props.options ?? []"
68
+ :placeholder="props.placeholder"
69
+ :disabled="props.disabled"
70
+ :by="props.by"
71
+ :content-min-width="props.contentMinWidth"
72
+ :content-width="props.contentWidth"
73
+ :shape="props.shape"
74
+ :variant="props.variant"
75
+ :class="[
76
+ ...(props.selectClass
77
+ ? (typeof props.selectClass === 'string' ? [props.selectClass] : props.selectClass)
78
+ : []),
79
+ props.layout === 'horizontal' ? 'col-span-2' : 'row-span-2',
80
+ ]"
81
+ >
82
+ <template
83
+ v-if="$slots.value"
84
+ #value="{ option, value, placeholder: slotPlaceholder }"
85
+ >
86
+ <slot
87
+ name="value"
88
+ v-bind="{ option, value, placeholder: slotPlaceholder }"
89
+ />
90
+ </template>
91
+
92
+ <template
93
+ v-if="$slots.option"
94
+ #option="{ option }"
95
+ >
96
+ <slot
97
+ name="option"
98
+ v-bind="{ option }"
99
+ />
100
+ </template>
101
+ </Select>
102
+ </slot>
103
+ </div>
104
+ </label>
105
+ </template>
@@ -1,7 +1,9 @@
1
1
  export { default as FieldCheckbox } from './field-checkbox.vue'
2
- export { default as FieldCombobox } from './field-combobox.vue'
2
+ export { default as FieldCombobox } from './field-combobox-select.vue'
3
+ export { default as FieldInputFile } from './field-input-file.vue'
3
4
  export { default as FieldInput } from './field-input.vue'
4
5
  export { default as FieldKeyValues } from './field-key-values.vue'
5
6
  export { default as FieldRange } from './field-range.vue'
7
+ export { default as FieldSelect } from './field-select.vue'
6
8
  export { default as FieldTextArea } from './field-text-area.vue'
7
9
  export { default as FieldValues } from './field-values.vue'
@@ -1,4 +1,5 @@
1
1
  export { default as BasicInputFile } from './basic-input-file.vue'
2
+ export { default as InputFileCard } from './input-file-card.vue'
2
3
  export { default as InputFile } from './input-file.vue'
3
4
  export { default as InputKeyValue } from './input-key-value.vue'
4
5
  export { default as Input } from './input.vue'
@@ -0,0 +1,56 @@
1
+ <script setup lang="ts">
2
+ import { useSlots } from 'vue'
3
+
4
+ import BasicInputFile from './basic-input-file.vue'
5
+
6
+ defineProps<{
7
+ accept?: string
8
+ multiple?: boolean
9
+ }>()
10
+
11
+ const slots = useSlots()
12
+ </script>
13
+
14
+ <template>
15
+ <BasicInputFile
16
+ :class="[
17
+ 'min-h-[120px] flex flex-col cursor-pointer items-center justify-center rounded-xl p-6',
18
+ 'border-dashed border-2',
19
+ 'transition-all duration-300',
20
+ 'opacity-95',
21
+ 'hover:scale-100 hover:opacity-100 hover:shadow-md hover:dark:shadow-lg',
22
+ ]"
23
+ :is-not-dragging-classes="[
24
+ 'border-neutral-200 dark:border-neutral-700 hover:border-primary-300 dark:hover:border-primary-700',
25
+ 'bg-white/60 dark:bg-black/30 hover:bg-white/80 dark:hover:bg-black/40',
26
+ ]"
27
+ :is-dragging-classes="[
28
+ 'border-primary-400 dark:border-primary-600 hover:border-primary-300 dark:hover:border-primary-700',
29
+ 'bg-primary-50/5 dark:bg-primary-900/5',
30
+ ]"
31
+ :accept="accept"
32
+ :multiple="multiple"
33
+ >
34
+ <template #default="{ isDragging }">
35
+ <slot v-if="slots.default" :is-dragging="isDragging" />
36
+ <div
37
+ v-else
38
+ class="flex flex-col items-center"
39
+ :class="[
40
+ isDragging ? 'text-primary-500 dark:text-primary-400' : 'text-neutral-400 dark:text-neutral-500',
41
+ ]"
42
+ >
43
+ <div i-solar:upload-square-line-duotone mb-2 text-5xl />
44
+ <p font-medium text="center lg">
45
+ Upload
46
+ </p>
47
+ <p v-if="isDragging" text="center" text-sm>
48
+ Release to upload
49
+ </p>
50
+ <p v-else text="center" text-sm>
51
+ Click or drag and drop a file here
52
+ </p>
53
+ </div>
54
+ </template>
55
+ </BasicInputFile>
56
+ </template>
@@ -1,56 +1,96 @@
1
1
  <script setup lang="ts">
2
- import { useSlots } from 'vue'
2
+ import { useObjectUrl } from '@vueuse/core'
3
+ import { computed } from 'vue'
3
4
 
4
- import BasicInputFile from './basic-input-file.vue'
5
-
6
- defineProps<{
5
+ const props = withDefaults(defineProps<{
7
6
  accept?: string
8
7
  multiple?: boolean
9
- }>()
8
+ placeholder?: string
9
+ }>(), {
10
+ placeholder: 'Choose file',
11
+ multiple: false,
12
+ })
13
+
14
+ const modelValue = defineModel<File[] | undefined>({ default: undefined })
15
+
16
+ const fileNames = computed(() => {
17
+ const files = modelValue.value ?? []
18
+ if (!files.length)
19
+ return props.placeholder
20
+ return files.map(file => file.name).join(', ')
21
+ })
22
+
23
+ const previewImageFile = computed(() => {
24
+ const files = modelValue.value ?? []
25
+ return files.find(file => file.type.startsWith('image/'))
26
+ })
27
+
28
+ const previewUrl = useObjectUrl(previewImageFile)
29
+
30
+ function onFileChange(event: Event) {
31
+ const input = event.target as HTMLInputElement
32
+ if (!input.files) {
33
+ modelValue.value = undefined
34
+ return
35
+ }
10
36
 
11
- const slots = useSlots()
37
+ const files = Array.from(input.files)
38
+ modelValue.value = files.length ? files : undefined
39
+
40
+ // Allow re-selecting the same file.
41
+ input.value = ''
42
+ }
12
43
  </script>
13
44
 
14
45
  <template>
15
- <BasicInputFile
46
+ <label
16
47
  :class="[
17
- 'min-h-[120px] flex flex-col cursor-pointer items-center justify-center rounded-xl p-6',
18
- 'border-dashed border-2',
19
- 'transition-all duration-300',
20
- 'opacity-95',
21
- 'hover:scale-100 hover:opacity-100 hover:shadow-md hover:dark:shadow-lg',
22
- ]"
23
- :is-not-dragging-classes="[
24
- 'border-neutral-200 dark:border-neutral-700 hover:border-primary-300 dark:hover:border-primary-700',
25
- 'bg-white/60 dark:bg-black/30 hover:bg-white/80 dark:hover:bg-black/40',
26
- ]"
27
- :is-dragging-classes="[
28
- 'border-primary-400 dark:border-primary-600 hover:border-primary-300 dark:hover:border-primary-700',
29
- 'bg-primary-50/5 dark:bg-primary-900/5',
48
+ 'w-full flex cursor-pointer items-center gap-2',
49
+ 'rounded-lg border-2 border-solid border-neutral-100 bg-neutral-50 px-2 py-1 shadow-sm',
50
+ 'transition-all duration-200 ease-in-out',
51
+ 'dark:border-neutral-900 dark:bg-neutral-950',
52
+ 'hover:border-primary-300/70 dark:hover:border-primary-700/70',
30
53
  ]"
31
- :accept="accept"
32
- :multiple="multiple"
33
54
  >
34
- <template #default="{ isDragging }">
35
- <slot v-if="slots.default" :is-dragging="isDragging" />
36
- <div
37
- v-else
38
- class="flex flex-col items-center"
55
+ <input
56
+ type="file"
57
+ :accept="accept"
58
+ :multiple="multiple"
59
+ :class="[
60
+ 'hidden',
61
+ ]"
62
+ @change="onFileChange"
63
+ >
64
+
65
+ <div
66
+ :class="[
67
+ 'i-solar:upload-square-line-duotone h-5 w-5 shrink-0 text-neutral-500 dark:text-neutral-400',
68
+ ]"
69
+ />
70
+
71
+ <div
72
+ :class="[
73
+ 'min-w-0 flex-1 truncate text-sm text-neutral-600 dark:text-neutral-300',
74
+ ]"
75
+ :title="fileNames"
76
+ >
77
+ {{ fileNames }}
78
+ </div>
79
+
80
+ <div
81
+ v-if="previewUrl"
82
+ :class="[
83
+ 'h-8 w-8 shrink-0 overflow-hidden rounded-md border border-neutral-200 bg-white',
84
+ 'dark:border-neutral-700 dark:bg-neutral-900',
85
+ ]"
86
+ >
87
+ <img
88
+ :src="previewUrl"
89
+ alt="Preview"
39
90
  :class="[
40
- isDragging ? 'text-primary-500 dark:text-primary-400' : 'text-neutral-400 dark:text-neutral-500',
91
+ 'h-full w-full object-cover',
41
92
  ]"
42
93
  >
43
- <div i-solar:upload-square-line-duotone mb-2 text-5xl />
44
- <p font-medium text="center lg">
45
- Upload
46
- </p>
47
- <p v-if="isDragging" text="center" text-sm>
48
- Release to upload
49
- </p>
50
- <p v-else text="center" text-sm>
51
- Click or drag and drop a file here
52
- </p>
53
- </div>
54
- </template>
55
- </BasicInputFile>
94
+ </div>
95
+ </label>
56
96
  </template>
@@ -1,8 +1,10 @@
1
1
  <script
2
2
  setup
3
3
  lang="ts"
4
- generic="InputType extends 'number' | string, T = InputType extends 'number' ? (number | undefined) : ((string | undefined))"
4
+ generic="InputType extends 'number' | InputTypeHTMLAttribute | string, T = InputType extends 'number' ? (number | undefined) : ((string | undefined))"
5
5
  >
6
+ import type { InputTypeHTMLAttribute } from 'vue'
7
+
6
8
  // Define button variants for better type safety and maintainability
7
9
  type InputVariant = 'primary' | 'secondary' | 'primary-dimmed'
8
10
 
@@ -71,7 +71,6 @@ https://toughengineer.github.io/demo/slider-styler*/
71
71
  min-height: var(--height);
72
72
  appearance: none;
73
73
  background: transparent;
74
- border-radius: 4px;
75
74
  transition: background-color 0.2s ease;
76
75
 
77
76
  --thumb-width: var(--height);
@@ -84,7 +83,7 @@ https://toughengineer.github.io/demo/slider-styler*/
84
83
  --track-height: calc(var(--height) - var(--track-value-padding) * 2);
85
84
  --track-box-shadow: 0 0 12px -2px rgb(0 0 0 / 22%);
86
85
  --track-border: none;
87
- --track-border-radius: 10px;
86
+ --track-border-radius: 16px;
88
87
  --track-background: rgba(0, 0, 0, 0.4);
89
88
 
90
89
  --track-value-background: rgb(255, 255, 255);
@@ -99,7 +98,7 @@ https://toughengineer.github.io/demo/slider-styler*/
99
98
  --thumb-background: rgb(238, 238, 238);
100
99
 
101
100
  --track-border: none;
102
- --track-background: rgba(99, 99, 99, 0.7);
101
+ --track-background: rgba(64, 64, 64, 0.7);
103
102
  --track-box-shadow: 0 0 12px -2px rgb(0 0 0 / 22%);
104
103
 
105
104
  --track-value-background: rgb(238, 238, 238);
@@ -112,7 +112,7 @@ function toCssSize(value?: string | number): string | undefined {
112
112
  <SelectTrigger
113
113
  :class="[
114
114
  'group',
115
- 'w-full inline-flex items-center justify-between border px-1 leading-none h-fit gap-[5px] outline-none',
115
+ 'w-full inline-flex items-center justify-between border px-3 leading-none h-9 gap-[5px] outline-none',
116
116
  props.shape === 'rounded' ? 'rounded-full' : 'rounded-lg',
117
117
  'text-sm text-neutral-700 dark:text-neutral-200 data-[placeholder]:text-neutral-400 dark:data-[placeholder]:text-neutral-500',
118
118
  props.variant === 'default' ? 'bg-white dark:bg-neutral-900 disabled:bg-neutral-100 hover:bg-neutral-50 dark:disabled:bg-neutral-900 dark:hover:bg-neutral-700' : '',
@@ -144,7 +144,6 @@ function toCssSize(value?: string | number): string | undefined {
144
144
  <SelectValue
145
145
  v-else
146
146
  v-model="modelValue"
147
- :placeholder="props.placeholder"
148
147
  />
149
148
  </div>
150
149
  <SelectIcon as-child>
@@ -19,6 +19,7 @@ interface ButtonProps {
19
19
  loading?: boolean // Loading state
20
20
  variant?: ButtonVariant // Button style variant
21
21
  size?: ButtonSize // Button size variant
22
+ shape?: 'rounded' | 'pill' | 'square' // Button shape
22
23
  theme?: ButtonTheme // Button theme
23
24
  block?: boolean // Full width button
24
25
  }
@@ -127,9 +128,21 @@ const variantClasses: Record<ButtonVariant, Record<ButtonTheme, {
127
128
 
128
129
  // Extract size styles for better organization
129
130
  const sizeClasses: Record<ButtonSize, string> = {
130
- sm: 'px-3 py-1.5 text-xs',
131
- md: 'px-4 py-2 text-sm',
132
- lg: 'px-6 py-3 text-base',
131
+ sm: props.shape === 'pill'
132
+ ? 'px-3 py-1.5 text-xs'
133
+ : props.shape === 'square'
134
+ ? 'p-2 text-xs'
135
+ : 'px-4 py-2 text-sm',
136
+ md: props.shape === 'pill'
137
+ ? 'px-4 py-2 text-sm'
138
+ : props.shape === 'square'
139
+ ? 'p-3 text-sm'
140
+ : 'px-5 py-3 text-base',
141
+ lg: props.shape === 'pill'
142
+ ? 'px-6 py-3 text-base'
143
+ : props.shape === 'square'
144
+ ? 'p-4 text-base'
145
+ : 'px-6 py-3 text-base',
133
146
  }
134
147
 
135
148
  // Base classes that are always applied
@@ -0,0 +1,219 @@
1
+ <script setup lang="ts">
2
+ import { errorCauseFrom, errorMessageFrom, errorNameFrom, errorStackFrom } from '@moeru/std'
3
+ import { ScrollAreaCorner, ScrollAreaRoot, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport } from 'reka-ui'
4
+ import { computed, shallowRef } from 'vue'
5
+
6
+ import Button from './button.vue'
7
+
8
+ type HeightPreset = 'sm' | 'md' | 'lg' | 'xl' | 'auto'
9
+
10
+ interface ContainerErrorProps {
11
+ error?: unknown
12
+ message?: string
13
+ stack?: string
14
+ includeStack?: boolean
15
+ showCopyButton?: boolean
16
+ showFeedbackButton?: boolean
17
+ copyButtonLabel?: string
18
+ copiedButtonLabel?: string
19
+ feedbackButtonLabel?: string
20
+ heightPreset?: HeightPreset
21
+ }
22
+
23
+ const props = withDefaults(defineProps<ContainerErrorProps>(), {
24
+ includeStack: true,
25
+ showCopyButton: true,
26
+ showFeedbackButton: true,
27
+ copyButtonLabel: 'Copy',
28
+ copiedButtonLabel: 'Copied',
29
+ feedbackButtonLabel: 'Feedback',
30
+ heightPreset: 'md',
31
+ })
32
+
33
+ const emit = defineEmits<{
34
+ (e: 'copy', content: string): void
35
+ (e: 'feedback'): void
36
+ }>()
37
+
38
+ const copied = shallowRef(false)
39
+
40
+ const heightPresetClasses: Record<HeightPreset, string[]> = {
41
+ sm: ['h-32'],
42
+ md: ['h-36'],
43
+ lg: ['h-42'],
44
+ xl: ['h-48'],
45
+ auto: [],
46
+ }
47
+
48
+ function indentLines(content: string, indent = 2): string {
49
+ const spaces = ' '.repeat(indent)
50
+ return content
51
+ .split('\n')
52
+ .map(line => `${spaces}${line}`)
53
+ .join('\n')
54
+ }
55
+
56
+ const resolvedErrorName = computed(() => {
57
+ return (errorNameFrom(props.error) ?? '').trim()
58
+ })
59
+
60
+ const resolvedMessage = computed(() => {
61
+ const fromProp = props.message?.trim()
62
+ if (fromProp)
63
+ return fromProp
64
+
65
+ const normalizedMessage = errorMessageFrom(props.error)
66
+ if (normalizedMessage)
67
+ return normalizedMessage.trim()
68
+
69
+ return props.error == null ? '' : String(props.error).trim()
70
+ })
71
+
72
+ const resolvedStack = computed(() => {
73
+ const fromProp = props.stack?.trim()
74
+ if (fromProp) {
75
+ const index = fromProp.indexOf(resolvedMessage.value)
76
+ if (index >= 0)
77
+ return fromProp.slice(index + resolvedMessage.value.length).trim()
78
+ }
79
+
80
+ if (!props.includeStack)
81
+ return ''
82
+
83
+ const resolved = (errorStackFrom(props.error) ?? '').trim()
84
+ const index = resolved.indexOf(resolvedMessage.value)
85
+ if (index >= 0)
86
+ return resolved.slice(index + resolvedMessage.value.length).trim()
87
+
88
+ return resolved
89
+ })
90
+
91
+ const resolvedCause = computed(() => {
92
+ if (props.error == null)
93
+ return ''
94
+
95
+ const cause = errorCauseFrom(props.error)
96
+ if (cause == null)
97
+ return ''
98
+
99
+ if (cause instanceof Error) {
100
+ const name = errorNameFrom(cause) ?? cause.name ?? 'Error'
101
+ const message = (errorMessageFrom(cause) ?? cause.message ?? '').trim()
102
+ const stack = (errorStackFrom(cause) ?? cause.stack ?? '').trim()
103
+
104
+ const header = message ? `${name}: ${message}` : name
105
+ if (!stack)
106
+ return header
107
+
108
+ return `${header}\n${indentLines(stack, 2)}`
109
+ }
110
+
111
+ return String(cause).trim()
112
+ })
113
+
114
+ const panelContent = computed(() => {
115
+ const sections: string[] = []
116
+
117
+ if (resolvedErrorName.value || resolvedMessage.value) {
118
+ const header = resolvedErrorName.value
119
+ ? (resolvedMessage.value ? `${resolvedErrorName.value}: ${resolvedMessage.value}` : resolvedErrorName.value)
120
+ : resolvedMessage.value
121
+ if (header)
122
+ sections.push(header)
123
+ }
124
+
125
+ if (resolvedStack.value)
126
+ sections.push(`Stack:\n${resolvedStack.value}`)
127
+
128
+ if (resolvedCause.value)
129
+ sections.push(`Cause:\n${resolvedCause.value}`)
130
+
131
+ return sections.join('\n\n')
132
+ })
133
+
134
+ async function copyContent() {
135
+ if (!panelContent.value)
136
+ return
137
+
138
+ emit('copy', panelContent.value)
139
+
140
+ try {
141
+ const clipboard = globalThis.navigator?.clipboard
142
+ if (!clipboard)
143
+ return
144
+
145
+ await clipboard.writeText(panelContent.value)
146
+ copied.value = true
147
+ globalThis.setTimeout(() => {
148
+ copied.value = false
149
+ }, 1200)
150
+ }
151
+ catch {
152
+ // Ignore clipboard failures and still keep emitted copy payload.
153
+ }
154
+ }
155
+ </script>
156
+
157
+ <template>
158
+ <div
159
+ :class="[
160
+ 'relative w-full rounded-xl border-2 border-red-200/70 bg-red-50/60 backdrop-blur-md p-1',
161
+ 'dark:border-red-900/50 dark:bg-red-950/25',
162
+ ]"
163
+ >
164
+ <div :class="['absolute right-4 -translate-x-full top-2 z-10']">
165
+ <Button
166
+ v-if="showCopyButton"
167
+ size="sm"
168
+ variant="secondary"
169
+ shape="square"
170
+ icon="i-solar:copy-line-duotone"
171
+ :title="copied ? copiedButtonLabel : copyButtonLabel"
172
+ :aria-label="copied ? copiedButtonLabel : copyButtonLabel"
173
+ @click="copyContent"
174
+ />
175
+ </div>
176
+
177
+ <div :class="['absolute right-2 top-2 z-10']">
178
+ <Button
179
+ v-if="showFeedbackButton"
180
+ size="sm"
181
+ variant="secondary"
182
+ shape="square"
183
+ icon="i-solar:square-share-line-line-duotone"
184
+ :title="feedbackButtonLabel"
185
+ :aria-label="feedbackButtonLabel"
186
+ @click="emit('feedback')"
187
+ />
188
+ </div>
189
+
190
+ <ScrollAreaRoot
191
+ type="auto"
192
+ :class="[
193
+ 'relative w-full overflow-hidden rounded-xl',
194
+ 'bg-neutral-50/80 dark:bg-neutral-950/80',
195
+ ...heightPresetClasses[heightPreset],
196
+ ]"
197
+ >
198
+ <ScrollAreaViewport :class="['h-full w-full']">
199
+ <div :class="['flex flex-col gap-2 p-3 text-xs']">
200
+ <div v-if="resolvedErrorName || resolvedMessage" :class="['font-mono font-semibold text-red-700 leading-relaxed dark:text-red-300']">
201
+ {{ resolvedErrorName || 'Error' }}
202
+ <span v-if="resolvedMessage">
203
+ : {{ resolvedMessage }}
204
+ </span>
205
+ </div>
206
+ <pre v-if="resolvedStack" :class="['whitespace-pre-wrap break-words text-neutral-700 leading-relaxed dark:text-neutral-200']"> {{ resolvedStack }}</pre>
207
+ <pre v-if="resolvedCause" :class="['whitespace-pre-wrap break-words text-neutral-700 leading-relaxed dark:text-neutral-200']">{{ `Cause:\n${resolvedCause}` }}</pre>
208
+ <div v-if="!panelContent" :class="['text-neutral-600 dark:text-neutral-300']">
209
+ No error details available.
210
+ </div>
211
+ </div>
212
+ </ScrollAreaViewport>
213
+ <ScrollAreaScrollbar orientation="vertical" :class="['w-2 p-0.5']">
214
+ <ScrollAreaThumb :class="['rounded-full bg-neutral-300/80 dark:bg-neutral-700/80']" />
215
+ </ScrollAreaScrollbar>
216
+ <ScrollAreaCorner />
217
+ </ScrollAreaRoot>
218
+ </div>
219
+ </template>
@@ -1,4 +1,5 @@
1
1
  export { default as Button } from './button.vue'
2
2
  export { default as Callout } from './callout.vue'
3
+ export { default as ContainerError } from './container-error.vue'
3
4
  export { default as DoubleCheckButton } from './double-check-button.vue'
4
5
  export { default as Progress } from './progress.vue'