@proj-airi/ui 0.9.0-beta.7 → 0.9.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.
- package/package.json +2 -1
- package/src/components/form/field/field-input-file.vue +38 -0
- package/src/components/form/field/index.ts +1 -0
- package/src/components/form/input/index.ts +1 -0
- package/src/components/form/input/input-file-card.vue +56 -0
- package/src/components/form/input/input-file.vue +81 -41
- package/src/components/form/select/select.vue +2 -0
- package/src/components/misc/button.vue +23 -9
- package/src/components/misc/container-error.vue +219 -0
- package/src/components/misc/index.ts +1 -0
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
|
|
4
|
+
"version": "0.9.0",
|
|
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,5 +1,6 @@
|
|
|
1
1
|
export { default as FieldCheckbox } from './field-checkbox.vue'
|
|
2
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'
|
|
@@ -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 {
|
|
2
|
+
import { useObjectUrl } from '@vueuse/core'
|
|
3
|
+
import { computed } from 'vue'
|
|
3
4
|
|
|
4
|
-
|
|
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
|
|
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
|
-
<
|
|
46
|
+
<label
|
|
16
47
|
:class="[
|
|
17
|
-
'
|
|
18
|
-
'border-
|
|
19
|
-
'transition-all duration-
|
|
20
|
-
'
|
|
21
|
-
'hover:
|
|
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
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
91
|
+
'h-full w-full object-cover',
|
|
41
92
|
]"
|
|
42
93
|
>
|
|
43
|
-
|
|
44
|
-
|
|
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>
|
|
@@ -40,6 +40,7 @@ const props = withDefaults(defineProps<{
|
|
|
40
40
|
contentWidth?: string | number
|
|
41
41
|
shape?: 'rounded' | 'default'
|
|
42
42
|
variant?: 'blurry' | 'default'
|
|
43
|
+
class?: string | string[]
|
|
43
44
|
}>(), {
|
|
44
45
|
placeholder: 'Select an option',
|
|
45
46
|
disabled: false,
|
|
@@ -112,6 +113,7 @@ function toCssSize(value?: string | number): string | undefined {
|
|
|
112
113
|
<SelectTrigger
|
|
113
114
|
:class="[
|
|
114
115
|
'group',
|
|
116
|
+
...Array.isArray(props.class) ? props.class : [props.class],
|
|
115
117
|
'w-full inline-flex items-center justify-between border px-3 leading-none h-9 gap-[5px] outline-none',
|
|
116
118
|
props.shape === 'rounded' ? 'rounded-full' : 'rounded-lg',
|
|
117
119
|
'text-sm text-neutral-700 dark:text-neutral-200 data-[placeholder]:text-neutral-400 dark:data-[placeholder]:text-neutral-500',
|
|
@@ -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
|
}
|
|
@@ -29,6 +30,7 @@ const props = withDefaults(defineProps<ButtonProps>(), {
|
|
|
29
30
|
disabled: false,
|
|
30
31
|
loading: false,
|
|
31
32
|
size: 'md',
|
|
33
|
+
shape: 'pill',
|
|
32
34
|
theme: 'default',
|
|
33
35
|
block: false,
|
|
34
36
|
})
|
|
@@ -44,7 +46,7 @@ const variantClasses: Record<ButtonVariant, Record<ButtonTheme, {
|
|
|
44
46
|
'primary': {
|
|
45
47
|
default: {
|
|
46
48
|
default: [
|
|
47
|
-
'rounded-
|
|
49
|
+
'rounded-lg',
|
|
48
50
|
'backdrop-blur-md',
|
|
49
51
|
'bg-primary-500/15 hover:bg-primary-500/20 active:bg-primary-500/30 dark:bg-primary-700/30 dark:hover:bg-primary-700/40 dark:active:bg-primary-700/30',
|
|
50
52
|
'focus:ring-primary-300/60 dark:focus:ring-primary-600/30',
|
|
@@ -57,7 +59,7 @@ const variantClasses: Record<ButtonVariant, Record<ButtonTheme, {
|
|
|
57
59
|
'secondary': {
|
|
58
60
|
default: {
|
|
59
61
|
default: [
|
|
60
|
-
'rounded-
|
|
62
|
+
'rounded-lg',
|
|
61
63
|
'backdrop-blur-md',
|
|
62
64
|
'bg-neutral-100/55 hover:bg-neutral-400/20 active:bg-neutral-400/30 dark:bg-neutral-700/60 dark:hover:bg-neutral-700/80 dark:active:bg-neutral-700/60',
|
|
63
65
|
'focus:ring-neutral-300/30 dark:focus:ring-neutral-600/60 dark:focus:ring-neutral-600/30',
|
|
@@ -70,7 +72,7 @@ const variantClasses: Record<ButtonVariant, Record<ButtonTheme, {
|
|
|
70
72
|
'secondary-muted': {
|
|
71
73
|
default: {
|
|
72
74
|
default: [
|
|
73
|
-
'rounded-
|
|
75
|
+
'rounded-lg',
|
|
74
76
|
'backdrop-blur-md',
|
|
75
77
|
'hover:bg-neutral-50/50 active:bg-neutral-50/90 hover:dark:bg-neutral-800/50 active:dark:bg-neutral-800/90',
|
|
76
78
|
'border-2 border-solid border-neutral-100/60 dark:border-neutral-800/30',
|
|
@@ -83,7 +85,7 @@ const variantClasses: Record<ButtonVariant, Record<ButtonTheme, {
|
|
|
83
85
|
'danger': {
|
|
84
86
|
default: {
|
|
85
87
|
default: [
|
|
86
|
-
'rounded-
|
|
88
|
+
'rounded-lg',
|
|
87
89
|
'backdrop-blur-md',
|
|
88
90
|
'bg-red-500/15 hover:bg-red-500/20 active:bg-red-500/30 dark:bg-red-700/30 dark:hover:bg-red-700/40 dark:active:bg-red-700/30',
|
|
89
91
|
'focus:ring-2 focus:ring-red-300/30 dark:focus:ring-red-600/60 dark:focus:ring-red-600/30',
|
|
@@ -95,7 +97,7 @@ const variantClasses: Record<ButtonVariant, Record<ButtonTheme, {
|
|
|
95
97
|
'caution': {
|
|
96
98
|
default: {
|
|
97
99
|
default: [
|
|
98
|
-
'rounded-
|
|
100
|
+
'rounded-lg',
|
|
99
101
|
'backdrop-blur-md',
|
|
100
102
|
'bg-amber-400/20 hover:bg-amber-400/25 active:bg-amber-400/35 dark:bg-amber-500/20 dark:hover:bg-amber-500/30 dark:active:bg-amber-500/35',
|
|
101
103
|
'focus:ring-2 focus:ring-amber-300/40 dark:focus:ring-amber-400/40',
|
|
@@ -127,9 +129,21 @@ const variantClasses: Record<ButtonVariant, Record<ButtonTheme, {
|
|
|
127
129
|
|
|
128
130
|
// Extract size styles for better organization
|
|
129
131
|
const sizeClasses: Record<ButtonSize, string> = {
|
|
130
|
-
sm:
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
sm: props.shape === 'pill'
|
|
133
|
+
? 'px-3 py-1.5 text-xs'
|
|
134
|
+
: props.shape === 'square'
|
|
135
|
+
? 'p-2 text-xs'
|
|
136
|
+
: 'px-4 py-2 text-sm',
|
|
137
|
+
md: props.shape === 'pill'
|
|
138
|
+
? 'px-4 py-2 text-sm'
|
|
139
|
+
: props.shape === 'square'
|
|
140
|
+
? 'p-3 text-sm'
|
|
141
|
+
: 'px-5 py-3 text-base',
|
|
142
|
+
lg: props.shape === 'pill'
|
|
143
|
+
? 'px-6 py-3 text-base'
|
|
144
|
+
: props.shape === 'square'
|
|
145
|
+
? 'p-4 text-base'
|
|
146
|
+
: 'px-6 py-3 text-base',
|
|
133
147
|
}
|
|
134
148
|
|
|
135
149
|
// Base classes that are always applied
|
|
@@ -138,7 +152,7 @@ const baseClasses = computed(() => {
|
|
|
138
152
|
const theme = variant[props.theme] || variant.default
|
|
139
153
|
|
|
140
154
|
return [
|
|
141
|
-
'
|
|
155
|
+
'font-medium outline-none',
|
|
142
156
|
'transition-all duration-200 ease-in-out',
|
|
143
157
|
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
144
158
|
'backdrop-blur-md',
|
|
@@ -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'
|