|
22 | 22 | size?: number; |
23 | 23 | extension?: string; |
24 | 24 | error?: string; |
| 25 | + progress?: number; |
25 | 26 | status?: 'failed' | 'pending' | 'success'; |
26 | 27 | })[] = []; |
27 | 28 |
|
|
67 | 68 | <div transition:slide={{ axis: 'y', duration: 200 }}> |
68 | 69 | {#each files as file} |
69 | 70 | {@const fileSize = file?.size ? humanFileSize(file.size) : false} |
| 71 | + {@const hasProgress = typeof file?.progress === 'number' && file.progress > 0} |
| 72 | + {@const clampedProgress = hasProgress |
| 73 | + ? Math.round(Math.min(Math.max(file.progress ?? 0, 0), 100)) |
| 74 | + : 0} |
| 75 | + {@const isError = !!file?.error || file?.status === 'failed'} |
70 | 76 | <section> |
71 | | - <Stack |
72 | | - direction="row" |
73 | | - gap="s" |
74 | | - justifyContent="space-between" |
75 | | - alignItems="center" |
76 | | - > |
77 | | - <Stack direction="row" gap="s" style="overflow: hidden;"> |
78 | | - <Icon |
79 | | - icon={IconDocument} |
80 | | - color={file?.error |
81 | | - ? '--fgcolor-error' |
82 | | - : '--fgcolor-neutral-tertiary'} |
83 | | - /> |
84 | | - <Stack direction="row" gap="xxs" inline style="min-width: 0;"> |
85 | | - <Text truncate> |
86 | | - {file.name} |
87 | | - </Text> |
88 | | - {#if fileSize} |
89 | | - <Text color="--fgcolor-neutral-tertiary"> |
90 | | - <span style="white-space: nowrap"> |
91 | | - ({fileSize.value} |
92 | | - {fileSize.unit}) |
93 | | - </span> |
| 77 | + <Stack direction="column" gap="xs"> |
| 78 | + <Stack |
| 79 | + direction="row" |
| 80 | + gap="s" |
| 81 | + justifyContent="space-between" |
| 82 | + alignItems="center" |
| 83 | + > |
| 84 | + <Stack direction="row" gap="s" style="overflow: hidden;"> |
| 85 | + <Icon |
| 86 | + icon={IconDocument} |
| 87 | + color={file?.error |
| 88 | + ? '--fgcolor-error' |
| 89 | + : '--fgcolor-neutral-tertiary'} |
| 90 | + /> |
| 91 | + <Stack direction="row" gap="xxs" inline style="min-width: 0;"> |
| 92 | + <Text truncate> |
| 93 | + {file.name} |
94 | 94 | </Text> |
95 | | - {/if} |
| 95 | + {#if fileSize} |
| 96 | + <Text color="--fgcolor-neutral-tertiary"> |
| 97 | + <span style="white-space: nowrap"> |
| 98 | + ({fileSize.value} |
| 99 | + {fileSize.unit}) |
| 100 | + </span> |
| 101 | + </Text> |
| 102 | + {/if} |
| 103 | + </Stack> |
96 | 104 | </Stack> |
97 | | - </Stack> |
98 | | - <Stack direction="row" gap="xxs" inline> |
99 | | - {#if file?.status === 'success'} |
100 | | - <Button variant="text" icon size="s"> |
| 105 | + <Stack direction="row" gap="xxs" inline> |
| 106 | + {#if file?.status === 'success'} |
101 | 107 | <Icon icon={IconCheck} color="--fgcolor-success" size="s" /> |
102 | | - </Button> |
103 | | - {:else} |
104 | | - {#if file?.error} |
105 | | - <Stack inline justifyContent="center"> |
106 | | - <Badge |
107 | | - variant="secondary" |
108 | | - type="error" |
109 | | - content="Failed" |
110 | | - size="s" |
111 | | - /> |
112 | | - </Stack> |
113 | | - {:else if file?.status === 'pending'} |
114 | | - <Stack inline justifyContent="center"> |
115 | | - <Badge |
116 | | - variant="secondary" |
117 | | - type="warning" |
118 | | - content="Pending" |
| 108 | + {:else} |
| 109 | + {#if isError} |
| 110 | + <Stack inline justifyContent="center"> |
| 111 | + <Badge |
| 112 | + variant="secondary" |
| 113 | + type="error" |
| 114 | + content="Failed" |
| 115 | + size="s" |
| 116 | + /> |
| 117 | + </Stack> |
| 118 | + {:else if hasProgress} |
| 119 | + <Text color="--fgcolor-neutral-tertiary"> |
| 120 | + {clampedProgress}% |
| 121 | + </Text> |
| 122 | + {:else if file?.status === 'pending'} |
| 123 | + <Stack inline justifyContent="center"> |
| 124 | + <Badge |
| 125 | + variant="secondary" |
| 126 | + type="warning" |
| 127 | + content="Pending" |
| 128 | + size="s" |
| 129 | + /> |
| 130 | + </Stack> |
| 131 | + {/if} |
| 132 | + <Button |
| 133 | + variant="text" |
| 134 | + icon |
| 135 | + size="s" |
| 136 | + on:click={() => { |
| 137 | + dispatch('remove', file); |
| 138 | + }} |
| 139 | + > |
| 140 | + <Icon |
| 141 | + icon={IconX} |
| 142 | + color="--fgcolor-neutral-tertiary" |
119 | 143 | size="s" |
120 | 144 | /> |
121 | | - </Stack> |
| 145 | + </Button> |
122 | 146 | {/if} |
123 | | - <Button |
124 | | - variant="text" |
125 | | - icon |
126 | | - size="s" |
127 | | - on:click={() => { |
128 | | - dispatch('remove', file); |
129 | | - }} |
130 | | - > |
131 | | - <Icon |
132 | | - icon={IconX} |
133 | | - color="--fgcolor-neutral-tertiary" |
134 | | - size="s" |
135 | | - /> |
136 | | - </Button> |
137 | | - {/if} |
| 147 | + </Stack> |
138 | 148 | </Stack> |
| 149 | + {#if hasProgress && file?.status !== 'success'} |
| 150 | + <div |
| 151 | + class="upload-progress-bar" |
| 152 | + class:is-error={isError} |
| 153 | + role="progressbar" |
| 154 | + aria-valuenow={clampedProgress} |
| 155 | + aria-valuemin={0} |
| 156 | + aria-valuemax={100} |
| 157 | + aria-label="{file.name} upload progress" |
| 158 | + > |
| 159 | + <div |
| 160 | + class="upload-progress-bar-fill" |
| 161 | + style="width: {clampedProgress}%" |
| 162 | + /> |
| 163 | + </div> |
| 164 | + {/if} |
139 | 165 | </Stack> |
140 | 166 | </section> |
141 | 167 | {/each} |
|
171 | 197 | flex-shrink: 1; |
172 | 198 | } |
173 | 199 | } |
| 200 | +
|
| 201 | + .upload-progress-bar { |
| 202 | + height: 4px; |
| 203 | + width: 100%; |
| 204 | + border-radius: var(--border-radius-XS, 4px); |
| 205 | + background: var(--bgcolor-neutral-secondary, hsl(0 0% 90%)); |
| 206 | + overflow: hidden; |
| 207 | +
|
| 208 | + &-fill { |
| 209 | + height: 100%; |
| 210 | + border-radius: inherit; |
| 211 | + background: var(--bgcolor-accent); |
| 212 | + transition: width 0.3s ease; |
| 213 | + } |
| 214 | +
|
| 215 | + &.is-error &-fill { |
| 216 | + background: var(--bgcolor-error, hsl(0 70% 55%)); |
| 217 | + } |
| 218 | + } |
174 | 219 | </style> |
0 commit comments