@bagelink/vue 0.0.378 → 0.0.390

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.
@@ -1,57 +1,58 @@
1
1
  <template>
2
- <div class="bgl_vid">
2
+ <div class="bgl_vid" :class="{ vid_empty: !src }">
3
3
  <iframe
4
4
  v-if="embedType"
5
5
  :src="videoUrl"
6
6
  :style="{ aspectRatio }"
7
7
  frameborder="0"
8
8
  allowfullscreen
9
- title="Video"
10
- />
9
+ title="Video" />
11
10
  <video
12
- v-else
11
+ v-else-if="src"
13
12
  :src="src"
14
13
  :autoplay="autoplay"
15
14
  :muted="mute"
16
15
  :loop="loop"
17
16
  :style="{ aspectRatio }"
18
17
  :controls="controls"
19
- playsinline
20
- />
18
+ playsinline />
21
19
  </div>
22
20
  </template>
23
21
 
24
22
  <script setup lang="ts">
25
23
  type Props = {
26
- src: string;
27
- autoplay: boolean;
28
- mute: boolean;
29
- aspectRatio: string;
30
- controls: boolean;
31
- loop: boolean;
24
+ src?: string;
25
+ autoplay?: boolean;
26
+ mute?: boolean;
27
+ aspectRatio?: string;
28
+ controls?: boolean;
29
+ loop?: boolean;
32
30
  };
33
31
 
34
32
  const props = defineProps<Props>();
35
33
 
36
- const aspectRatio = $computed(() => props.aspectRatio.replace(':', '/'));
34
+ const aspectRatio = $computed(
35
+ () => props.aspectRatio?.replace(':', '/') || '16/9'
36
+ );
37
37
 
38
38
  const embedType = $computed<'YouTube' | 'Vimeo' | null>(() => {
39
39
  const youtubeRegex = /youtube\.com|youtu\.be/;
40
- if (youtubeRegex.test(props.src)) return 'YouTube';
40
+ if (youtubeRegex.test(props.src || '')) return 'YouTube';
41
41
  const vimeoRegex = /vimeo\.com/;
42
- if (vimeoRegex.test(props.src)) return 'Vimeo';
42
+ if (vimeoRegex.test(props.src || '')) return 'Vimeo';
43
43
  return null;
44
44
  });
45
45
 
46
46
  const videoUrl = $computed(() => {
47
47
  if (embedType) {
48
48
  if (embedType === 'YouTube') {
49
- const videoId = props.src.split(/v=|youtu\.be\//)?.[1]?.split('&')?.[0];
49
+ const videoId = props.src?.split(/v=|youtu\.be\//)?.[1]?.split('&')?.[0];
50
50
  return `https://www.youtube.com/embed/${videoId}`;
51
51
  }
52
52
  if (embedType === 'Vimeo') {
53
- const vimeoRegex = /vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/)?(\d+)(?:$|\/|\?)/;
54
- const videoId = props.src.match(vimeoRegex)?.[3];
53
+ const vimeoRegex =
54
+ /vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/)?(\d+)(?:$|\/|\?)/;
55
+ const videoId = props.src?.match(vimeoRegex)?.[3];
55
56
  return `https://player.vimeo.com/video/${videoId}`;
56
57
  }
57
58
  }
@@ -64,8 +65,13 @@ console.log('Video URL:', videoUrl);
64
65
  <style scoped>
65
66
  .bgl_vid iframe,
66
67
  .bgl_vid video {
67
- width: 100%;
68
- height: auto;
69
- display: block
68
+ width: 100%;
69
+ height: auto;
70
+ display: block;
71
+ }
72
+
73
+ .bgl_vid.vid_empty {
74
+ padding-top: 56.25%;
75
+ background: #f0f0f0;
70
76
  }
71
77
  </style>
@@ -1,17 +1,27 @@
1
1
  <template>
2
2
  <div
3
- class="bg-dark" :class="{ 'is-side': side, 'is-active': isVisible, 'bg-lignt': false }"
4
- @click="() => (dismissable ? closeModal() : '')" @keydown.esc="closeModal"
5
- >
6
- <Card class="modal" @click.stop>
3
+ class="bg-dark"
4
+ :class="{ 'is-side': side, 'is-active': isVisible, 'bg-lignt': false }"
5
+ @click="() => (dismissable ? closeModal() : '')"
6
+ @keydown.esc="closeModal">
7
+ <Card class="modal" @click.stop :style="{ ...maxWidth }">
7
8
  <header class="tool-bar">
8
9
  <slot name="toolbar" />
9
- <Btn :style="{ float: side ? 'left' : 'right' }" flat icon="close" @click="closeModal" />
10
+ <Btn
11
+ :style="{ float: side ? 'left' : 'right' }"
12
+ flat
13
+ icon="close"
14
+ @click="closeModal" />
10
15
  <Title class="modal-title" tag="h3" v-if="title" :label="title" />
11
16
  </header>
12
17
  <slot />
13
18
  <footer class="modal-footer mt-3">
14
- <Btn v-for="(action, i) in actions" :key="i" @click="closeModal" color="gray" v-bind="action" />
19
+ <Btn
20
+ v-for="(action, i) in actions"
21
+ :key="i"
22
+ @click="closeModal"
23
+ color="gray"
24
+ v-bind="action" />
15
25
  <slot name="footer" />
16
26
  </footer>
17
27
  </Card>
@@ -20,26 +30,36 @@
20
30
 
21
31
  <script lang="ts" setup>
22
32
  import { onMounted, onUnmounted, watch } from 'vue';
23
- import {
24
- type BtnOptions, Btn, useEscape, Title, Card,
25
- } from '@bagelink/vue';
33
+ import { type BtnOptions, Btn, useEscape, Title, Card } from '@bagelink/vue';
26
34
  import '../styles/modal.css';
27
35
 
28
36
  const props = defineProps<{
29
- side?: boolean;
30
- title?: string;
31
- dismissable?: boolean;
32
- actions?: BtnOptions[];
33
- visible?: boolean;
37
+ side?: boolean;
38
+ title?: string;
39
+ width?: string;
40
+ dismissable?: boolean;
41
+ actions?: BtnOptions[];
42
+ visible?: boolean;
34
43
  }>();
35
44
 
36
45
  let isVisible = $ref<boolean>(false);
37
46
 
38
- watch(() => props.visible, (val) => {
39
- if (val === isVisible || val === undefined) return;
40
- if (val) openModal();
41
- else closeModal();
42
- }, { immediate: true });
47
+ watch(
48
+ () => props.visible,
49
+ (val) => {
50
+ if (val === isVisible || val === undefined) return;
51
+ if (val) openModal();
52
+ else closeModal();
53
+ },
54
+ { immediate: true }
55
+ );
56
+
57
+ const maxWidth = $computed(() => {
58
+ const { width } = props;
59
+ if (width?.match(/px|em|rem|vw|vh|%/)) return { 'max-width': width };
60
+ if (width?.match(/\d+/)) return { 'max-width': `${width}px` };
61
+ return { 'max-width': '720px' };
62
+ });
43
63
 
44
64
  const emit = defineEmits(['update:visible']);
45
65
 
@@ -50,10 +70,11 @@ const closeModal = () => {
50
70
 
51
71
  defineExpose({ closeModal });
52
72
 
53
- const escapeKeyClose = (e: KeyboardEvent) => props?.dismissable && useEscape(e, () => closeModal());
73
+ const escapeKeyClose = (e: KeyboardEvent) =>
74
+ props?.dismissable && useEscape(e, () => closeModal());
54
75
 
55
76
  function openModal() {
56
- setTimeout(() => isVisible = true, 1);
77
+ setTimeout(() => (isVisible = true), 1);
57
78
  }
58
79
 
59
80
  onMounted(() => document.addEventListener('keydown', escapeKeyClose));
@@ -65,35 +86,35 @@ onUnmounted(() => {
65
86
 
66
87
  <style>
67
88
  .modal-title {
68
- text-align: center;
69
- font-weight: 600;
70
- font-size: 20px;
71
- margin-top: 0.5rem;
72
- margin-bottom: 0 !important;
73
- width: 100%;
74
- -webkit-padding-end: 40px;
75
- padding-inline-end: 40px;
76
- line-height: 2;
77
- display: -webkit-box;
78
- max-width: 100%;
79
- -webkit-line-clamp: 1;
80
- -webkit-box-orient: vertical;
81
- overflow: hidden;
82
- text-overflow: ellipsis;
89
+ text-align: center;
90
+ font-weight: 600;
91
+ font-size: 20px;
92
+ margin-top: 0.5rem;
93
+ margin-bottom: 0 !important;
94
+ width: 100%;
95
+ -webkit-padding-end: 40px;
96
+ padding-inline-end: 40px;
97
+ line-height: 2;
98
+ display: -webkit-box;
99
+ max-width: 100%;
100
+ -webkit-line-clamp: 1;
101
+ -webkit-box-orient: vertical;
102
+ overflow: hidden;
103
+ text-overflow: ellipsis;
83
104
  }
84
105
 
85
106
  .modal-footer {
86
- gap: 1rem;
87
- display: flex;
88
- justify-content: space-between;
89
- align-items: center;
107
+ gap: 1rem;
108
+ display: flex;
109
+ justify-content: space-between;
110
+ align-items: center;
90
111
  }
91
112
 
92
- .modal-footer>div {
93
- gap: 1rem;
94
- display: flex;
95
- justify-content: space-between;
96
- align-items: center;
97
- gap: 0;
113
+ .modal-footer > div {
114
+ gap: 1rem;
115
+ display: flex;
116
+ justify-content: space-between;
117
+ align-items: center;
118
+ gap: 0;
98
119
  }
99
120
  </style>
@@ -1,13 +1,27 @@
1
1
  <template>
2
2
  <Modal
3
- @onUpdate:isModalVisible="props['onUpdate:isModalVisible']" :side="side" ref="modal" :dismissable="dismissable"
4
- :title="title"
5
- >
6
- <BagelForm @submit="runSubmit" ref="form" v-model="formData" :schema="computedFormSchema" />
3
+ @onUpdate:isModalVisible="props['onUpdate:isModalVisible']"
4
+ :side
5
+ ref="modal"
6
+ :width
7
+ :dismissable
8
+ :title="title">
9
+ <BagelForm
10
+ @submit="runSubmit"
11
+ ref="form"
12
+ v-model="formData"
13
+ :schema="computedFormSchema" />
7
14
  <template #footer v-if="onDelete || onSubmit">
8
15
  <div>
9
16
  <Btn thin flat value="Cancel" @click="closeModal" />
10
- <Btn thin icon="delete" v-if="onDelete" flat value="Delete" @click="runDelete" color="red" />
17
+ <Btn
18
+ thin
19
+ icon="delete"
20
+ v-if="onDelete"
21
+ flat
22
+ value="Delete"
23
+ @click="runDelete"
24
+ color="red" />
11
25
  </div>
12
26
  <Btn value="Submit" @click="runSubmit" />
13
27
  </template>
@@ -16,28 +30,33 @@
16
30
 
17
31
  <script lang="ts" setup>
18
32
  import {
19
- Modal, Btn, BagelForm, type BglFormSchemaT, type BtnOptions, useBagel,
33
+ Modal,
34
+ Btn,
35
+ BagelForm,
36
+ type BglFormSchemaT,
37
+ type BtnOptions,
38
+ useBagel,
20
39
  } from '@bagelink/vue';
21
40
 
22
41
  const bagel = useBagel();
23
42
 
24
43
  const props = defineProps<{
25
- side?: boolean;
26
- title?: string;
27
- dismissable?: boolean;
28
- actions?: BtnOptions[];
29
- schema: BglFormSchemaT | (() => BglFormSchemaT);
44
+ 'side'?: boolean;
45
+ 'title'?: string;
46
+ 'width'?: string;
47
+ 'dismissable'?: boolean;
48
+ 'actions'?: BtnOptions[];
49
+ 'schema': BglFormSchemaT | (() => BglFormSchemaT);
30
50
  // eslint-disable-next-line no-unused-vars
31
- onSubmit?: ((formData: any) => Promise<void>);
51
+ 'onSubmit'?: (formData: any) => Promise<void>;
32
52
  // eslint-disable-next-line no-unused-vars
33
- onDelete?: ((id: string) => void);
53
+ 'onDelete'?: (id: string) => void;
34
54
  // eslint-disable-next-line no-unused-vars
35
- 'onUpdate:isModalVisible'?: ((visible: boolean) => void);
55
+ 'onUpdate:isModalVisible'?: (visible: boolean) => void;
36
56
  // eslint-disable-next-line no-unused-vars
37
- onError?: ((err: any) => void);
38
-
39
- modelValue?: Record<string, any>;
57
+ 'onError'?: (err: any) => void;
40
58
 
59
+ 'modelValue'?: Record<string, any>;
41
60
  }>();
42
61
 
43
62
  const modal = $ref<InstanceType<typeof Modal>>();
@@ -47,7 +66,9 @@ const computedFormSchema = $computed(() => {
47
66
  return props.schema;
48
67
  });
49
68
 
50
- const formData = defineModel<Record<string, any>>('modelValue', { default: {} });
69
+ const formData = defineModel<Record<string, any>>('modelValue', {
70
+ default: {},
71
+ });
51
72
  const form = $ref<InstanceType<typeof BagelForm>>();
52
73
  const closeModal = () => modal?.closeModal();
53
74
 
@@ -79,17 +79,24 @@ type QueueFile = {
79
79
  uploaded?: boolean;
80
80
  };
81
81
 
82
+ type StrKey = keyof StorageFile;
83
+
84
+ type FSValue = string[] | string | number;
85
+
82
86
  const props = defineProps<{
83
87
  label: string;
84
88
  multiple?: boolean;
85
89
  files?: StorageFile | StorageFile[];
86
90
  deleteEndpoint?: string;
91
+ bindkey?: StrKey;
87
92
  }>();
88
93
 
89
- const file_ids = defineModel<string[] | string>('modelValue');
94
+ const bindKey: StrKey = props.bindkey || 'id';
95
+
96
+ const file_bindkeys = defineModel<FSValue>('modelValue');
90
97
  const storageFiles = $ref<StorageFile[]>([]);
91
98
 
92
- const compareIds = (v1?: string[] | string, v2?: string[] | string) =>
99
+ const compareIds = (v1?: FSValue, v2?: FSValue) =>
93
100
  [v1].flat().join(',') === [v2].flat().join(',');
94
101
 
95
102
  watch(
@@ -103,10 +110,14 @@ watch(
103
110
  watch(
104
111
  () => storageFiles,
105
112
  (newFiles) => {
106
- let idValue: string[] | string = '';
107
- if (props.multiple) idValue = newFiles.map((f) => f.id || '');
108
- else idValue = newFiles[0]?.id || '';
109
- if (!compareIds(file_ids?.value, idValue)) file_ids.value = idValue;
113
+ let idValue: FSValue;
114
+ if (props.multiple) idValue = newFiles.map((f) => f[bindKey]) as FSValue;
115
+ else {
116
+ idValue = (newFiles[0]?.[bindKey] as string) || '';
117
+ }
118
+ if (!compareIds(file_bindkeys?.value, idValue)) {
119
+ file_bindkeys.value = idValue;
120
+ }
110
121
  },
111
122
  { deep: true }
112
123
  );
@@ -0,0 +1,296 @@
1
+ <template>
2
+ <div class="bagel-input">
3
+ <label>
4
+ {{ label }}
5
+ </label>
6
+ <div
7
+ @click="browse"
8
+ @dragover="dragover"
9
+ @drop="drop"
10
+ @dragleave="dragleave"
11
+ class="fileUploadWrap"
12
+ :class="{
13
+ fileDropZone: !storageFiles.length && !fileQueue.length,
14
+ dragover: isDragOver,
15
+ }">
16
+ <template v-for="file in storageFiles" :key="file.id">
17
+ <div class="imagePreviewWrap" v-if="!multiple">
18
+ <img class="preview" :src="file.url" alt="" />
19
+ </div>
20
+ <div class="previewName">
21
+ <img
22
+ height="60"
23
+ v-if="multiple"
24
+ class="preview"
25
+ :src="file.url"
26
+ alt="" />
27
+ <p class="no-margin">
28
+ {{ file.name }}
29
+ </p>
30
+ <Btn
31
+ thin
32
+ @click.stop="removeFile(file)"
33
+ flat
34
+ icon="delete"
35
+ color="red" />
36
+ </div>
37
+ </template>
38
+ <template v-for="fileQ in fileQueue" :key="fileQ.file">
39
+ <div class="imagePreviewWrap" v-if="!multiple">
40
+ <img class="preview" :src="fileToUrl(fileQ.file)" alt="" />
41
+ </div>
42
+ <div class="previewName">
43
+ <img
44
+ height="60"
45
+ v-if="multiple"
46
+ class="preview"
47
+ :src="fileToUrl(fileQ.file)"
48
+ alt="" />
49
+ <p class="no-margin">
50
+ {{ fileQ.name }}
51
+ </p>
52
+ <div
53
+ class="pie"
54
+ :style="`--p:${fileQ.progress}`"
55
+ style="--b: 2px"
56
+ :class="{ complete: fileQ.progress === 100 }">
57
+ <span class="progress" v-if="fileQ.progress < 100">
58
+ {{ `${fileQ.progress.toFixed(0)}` }}
59
+ </span>
60
+ <MaterialIcon class="success" icon="check" />
61
+ </div>
62
+ </div>
63
+ </template>
64
+ </div>
65
+ </div>
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ import { watch } from 'vue';
70
+ import { Btn, type StorageFile, useBagel, MaterialIcon } from '@bagelink/vue';
71
+
72
+ const bagel = useBagel();
73
+
74
+ type QueueFile = {
75
+ file: File;
76
+ progress: number;
77
+ uploading?: boolean;
78
+ name?: string;
79
+ uploaded?: boolean;
80
+ };
81
+
82
+ const props = defineProps<{
83
+ label: string;
84
+ multiple?: boolean;
85
+ files?: StorageFile | StorageFile[];
86
+ deleteEndpoint?: string;
87
+ }>();
88
+
89
+ const fileURLs = defineModel<string[] | string>('modelValue');
90
+ const storageFiles = $ref<StorageFile[]>([]);
91
+
92
+ const compareIds = (v1?: string[] | string, v2?: string[] | string) =>
93
+ [v1].flat().join(',') === [v2].flat().join(',');
94
+
95
+ watch(
96
+ () => props.files,
97
+ (newFiles) => {
98
+ if (newFiles) storageFiles.push(...[newFiles].flat());
99
+ },
100
+ { immediate: true }
101
+ );
102
+
103
+ watch(
104
+ () => storageFiles,
105
+ (newFiles) => {
106
+ let idValue: string[] | string = '';
107
+ if (props.multiple) idValue = newFiles.map((f) => f.url || '');
108
+ else idValue = newFiles[0]?.url || '';
109
+ if (!compareIds(fileURLs?.value, idValue)) fileURLs.value = idValue;
110
+ },
111
+ { deep: true }
112
+ );
113
+
114
+ const fileQueue = $ref<QueueFile[]>([]);
115
+ let isDragOver = $ref(false);
116
+
117
+ const preventDefault = (e: Event) => {
118
+ e.preventDefault();
119
+ e.stopPropagation();
120
+ };
121
+
122
+ const fileToUrl = (file: File) => URL.createObjectURL(file);
123
+ const removeFile = (file: StorageFile) => {
124
+ const index = storageFiles.indexOf(file);
125
+ storageFiles.splice(index, 1);
126
+
127
+ if (props.deleteEndpoint) {
128
+ void bagel.delete(`${props.deleteEndpoint}/${file.id}`);
129
+ }
130
+ };
131
+
132
+ const flushQueue = () => {
133
+ fileQueue.forEach(async (file, i) => {
134
+ if (!props.multiple) storageFiles.splice(0, 1);
135
+ const serverFile = await bagel.uploadFile<StorageFile>(file.file, {
136
+ onUploadProgress: (e: any) => (file.progress = e.progress * 100 - 1),
137
+ });
138
+ storageFiles.push(serverFile);
139
+ fileQueue.splice(i, 1);
140
+ });
141
+ };
142
+
143
+ const browse = () => {
144
+ const input = document.createElement('input');
145
+ input.type = 'file';
146
+ input.multiple = props.multiple;
147
+ input.onchange = (e: Event) => {
148
+ const target = e?.target as HTMLInputElement;
149
+ if (target?.files) {
150
+ Array.from(target.files).forEach((file: File) =>
151
+ fileQueue.push({ name: file.name, file, progress: 0 })
152
+ );
153
+ }
154
+ flushQueue();
155
+ };
156
+ input.click();
157
+ };
158
+
159
+ const dragleave = (e: DragEvent) => {
160
+ preventDefault(e);
161
+ if (e.dataTransfer) isDragOver = false;
162
+ else isDragOver = false;
163
+ };
164
+
165
+ const dragover = (e: DragEvent) => {
166
+ preventDefault(e);
167
+ if (e.dataTransfer) isDragOver = true;
168
+ else isDragOver = false;
169
+ };
170
+
171
+ const drop = (e: DragEvent) => {
172
+ preventDefault(e);
173
+ if (e.dataTransfer) {
174
+ Array.from(e.dataTransfer.files).forEach((file: File) =>
175
+ fileQueue.push({ name: file.name, file, progress: 0 })
176
+ );
177
+ }
178
+ isDragOver = false;
179
+ flushQueue();
180
+ };
181
+ </script>
182
+
183
+ <style scoped>
184
+ .bagel-input .fileUploadWrap {
185
+ outline: 1px solid var(--border-color);
186
+ border-radius: var(--input-border-radius);
187
+ text-align: center;
188
+ cursor: pointer;
189
+ transition: var(--bgl-transition);
190
+ position: relative;
191
+ height: 132px;
192
+ font-size: var(--input-font-size);
193
+ }
194
+
195
+ .previewName {
196
+ padding: 0.5rem;
197
+ text-align: start;
198
+ color: var(--input-color);
199
+ display: grid;
200
+ grid-template-columns: 1fr 22px;
201
+ align-items: center;
202
+ padding-inline: 14px;
203
+ }
204
+
205
+ .previewName p {
206
+ overflow: hidden;
207
+ text-overflow: ellipsis;
208
+ white-space: nowrap;
209
+ }
210
+
211
+ .imagePreviewWrap {
212
+ background: var(--input-bg);
213
+ border-radius: var(--input-border-radius);
214
+ height: 90px;
215
+ padding: 5px;
216
+ }
217
+
218
+ img.preview {
219
+ height: 80px;
220
+ border-radius: var(--input-border-radius);
221
+ object-fit: contain;
222
+ }
223
+
224
+ .fileUploadWrap.dragover,
225
+ .fileUploadWrap:hover {
226
+ box-shadow: inset 0 0 10px #00000012;
227
+ }
228
+
229
+ .bagel-input .fileUploadWrap.fileDropZone {
230
+ background: var(--input-bg);
231
+ display: flex;
232
+ align-items: center;
233
+ justify-content: center;
234
+ color: var(--bgl-gray);
235
+ }
236
+
237
+ .bagel-input .fileUploadWrap.fileDropZone:after {
238
+ content: 'Drop files here or click to upload';
239
+ }
240
+
241
+ .pie {
242
+ width: 30px;
243
+ height: 30px;
244
+ position: relative;
245
+ display: flex;
246
+ align-items: center;
247
+ justify-content: center;
248
+ }
249
+
250
+ .pie:before {
251
+ content: '';
252
+ position: absolute;
253
+ border-radius: 50%;
254
+ inset: 0;
255
+ transition: all 0.2s ease-in-out;
256
+ background: conic-gradient(var(--bgl-primary) calc(var(--p) * 1%), #0000 0);
257
+ -webkit-mask: radial-gradient(
258
+ farthest-side,
259
+ #0000 calc(99% - var(--b)),
260
+ #000 calc(100% - var(--b))
261
+ );
262
+ mask: radial-gradient(
263
+ farthest-side,
264
+ #0000 calc(99% - var(--b)),
265
+ #000 calc(100% - var(--b))
266
+ );
267
+ }
268
+
269
+ .pie .success {
270
+ transform: scale(0);
271
+ opacity: 0;
272
+ transition: all 0.3s ease-in-out;
273
+ }
274
+
275
+ .pie .progress {
276
+ position: absolute;
277
+ font-size: 10px;
278
+ }
279
+
280
+ .pie.complete .progress {
281
+ display: none;
282
+ }
283
+
284
+ .pie.complete .success {
285
+ transform: scale(1.3);
286
+ opacity: 1;
287
+ }
288
+
289
+ .pie.complete:before {
290
+ background: conic-gradient(var(--bgl-green) calc(var(--p) * 1%), #0000 0);
291
+ }
292
+
293
+ .pie.complete {
294
+ color: var(--bgl-green);
295
+ }
296
+ </style>