hamzus-ui 0.0.12 → 0.0.14
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/index.d.ts +8 -0
- package/index.js +8 -2
- package/package.json +1 -1
- package/src/components/hamzus-ui/Checkboxes/Checkbox/Checkbox.svelte +4 -1
- package/src/components/hamzus-ui/Code/Code.svelte +2 -2
- package/src/components/hamzus-ui/CopyCode/CopyCode.svelte +1 -1
- package/src/components/hamzus-ui/Form/Form.svelte +217 -0
- package/src/components/hamzus-ui/Form/clientCookie.js +18 -0
- package/src/components/hamzus-ui/IconButton/IconButton_default.svelte +2 -1
- package/src/components/hamzus-ui/Input/Input.svelte +2 -1
- package/src/components/hamzus-ui/ProgressCircle/ProgressCircle.svelte +167 -0
- package/src/styles/global.css +2 -2
package/index.d.ts
CHANGED
|
@@ -15,13 +15,21 @@ export { default as Kbd } from "./src/components/hamzus-ui/Kbd/Kbd.svelte"
|
|
|
15
15
|
export { default as Dialog } from "./src/components/hamzus-ui/Dialog/Dialog.svelte"
|
|
16
16
|
|
|
17
17
|
// form
|
|
18
|
+
export { default as Form } from "./src/components/hamzus-ui/Form/Form.svelte"
|
|
18
19
|
export { default as Input } from "./src/components/hamzus-ui/Input/Input.svelte"
|
|
19
20
|
export { default as DatePicker } from "./src/components/hamzus-ui/DatePicker/DatePicker.svelte"
|
|
20
21
|
export { default as TextArea } from "./src/components/hamzus-ui/TextArea/TextArea.svelte"
|
|
21
22
|
export { default as Checkbox } from "./src/components/hamzus-ui/Checkboxes/Checkbox/Checkbox.svelte"
|
|
22
23
|
export { default as Switch } from "./src/components/hamzus-ui/Swicth/Swicth.svelte"
|
|
23
24
|
|
|
25
|
+
// data
|
|
26
|
+
export { default as ProgressCircle } from "./src/components/hamzus-ui/ProgressCircle/ProgressCircle.svelte"
|
|
27
|
+
|
|
24
28
|
|
|
25
29
|
export * as DropdownMenu from "./src/components/hamzus-ui/DropdownMenu";
|
|
26
30
|
export * as Popover from "./src/components/hamzus-ui/Popover";
|
|
27
31
|
export * as Tooltip from "./src/components/hamzus-ui/AdvancedTooltip";
|
|
32
|
+
|
|
33
|
+
// utils
|
|
34
|
+
export { getCookie as getCookie } from "./src/utils/clientCookie.js"
|
|
35
|
+
export { sendRequest as sendRequest } from "./src/utils/request.js"
|
package/index.js
CHANGED
|
@@ -11,13 +11,19 @@ export { default as Kbd } from "./src/components/hamzus-ui/KBD/KBD.svelte"
|
|
|
11
11
|
// dialog
|
|
12
12
|
export { default as Dialog } from "./src/components/hamzus-ui/Dialog/Dialog.svelte"
|
|
13
13
|
// form
|
|
14
|
+
export { default as Form } from "./src/components/hamzus-ui/Form/Form.svelte"
|
|
14
15
|
export { default as Input } from "./src/components/hamzus-ui/Input/Input.svelte"
|
|
15
16
|
export { default as DatePicker } from "./src/components/hamzus-ui/DatePicker/DatePicker.svelte"
|
|
16
17
|
export { default as TextArea } from "./src/components/hamzus-ui/TextArea/TextArea.svelte"
|
|
17
18
|
export { default as Checkbox } from "./src/components/hamzus-ui/Checkboxes/Checkbox/Checkbox.svelte"
|
|
18
19
|
export { default as Switch } from "./src/components/hamzus-ui/Swicth/Swicth.svelte"
|
|
19
|
-
|
|
20
|
+
// data
|
|
21
|
+
export { default as ProgressCircle } from "./src/components/hamzus-ui/ProgressCircle/ProgressCircle.svelte"
|
|
20
22
|
|
|
21
23
|
export * as DropdownMenu from "./src/components/hamzus-ui/DropdownMenu"
|
|
22
24
|
export * as Popover from "./src/components/hamzus-ui/Popover"
|
|
23
|
-
export * as Tooltip from "./src/components/hamzus-ui/AdvancedTooltip"
|
|
25
|
+
export * as Tooltip from "./src/components/hamzus-ui/AdvancedTooltip"
|
|
26
|
+
|
|
27
|
+
// utils
|
|
28
|
+
export { getCookie as getCookie } from "./src/utils/clientCookie.js"
|
|
29
|
+
export { sendRequest as sendRequest } from "./src/utils/request.js"
|
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
export let label = '';
|
|
5
5
|
export let disabled = false;
|
|
6
6
|
export let fullWidth = false;
|
|
7
|
+
export let spaceBetween = false;
|
|
7
8
|
export let checked = false;
|
|
8
9
|
export let direction = 'ltr';
|
|
9
10
|
export let onChange = null;
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
}
|
|
23
24
|
</script>
|
|
24
25
|
|
|
25
|
-
<label class="checkbox-label {disabled ? 'disabled' : ''} {fullWidth ? 'full-width' : ''}">
|
|
26
|
+
<label class="checkbox-label {disabled ? 'disabled' : ''} {fullWidth ? 'full-width' : ''} {spaceBetween ? 'space-between' : ''}">
|
|
26
27
|
{#if label && direction === 'ltr'}
|
|
27
28
|
<h4>{label}</h4>
|
|
28
29
|
{/if}
|
|
@@ -54,6 +55,8 @@
|
|
|
54
55
|
}
|
|
55
56
|
.checkbox-label.full-width {
|
|
56
57
|
width: 100%;
|
|
58
|
+
}
|
|
59
|
+
.checkbox-label.space-between {
|
|
57
60
|
justify-content: space-between;
|
|
58
61
|
}
|
|
59
62
|
.checkbox-box {
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
function lintCode(text) {
|
|
15
15
|
// text = text.replace(/ /g, '\t');
|
|
16
|
-
text = text.replace(/"([^"]*)":/g, '<span>"$1"</span> <span class="syntax">:</span>');
|
|
17
|
-
text = text.replace(/(,)\n/g, '<span class="syntax">$1</span>\n');
|
|
16
|
+
// text = text.replace(/"([^"]*)":/g, '<span>"$1"</span> <span class="syntax">:</span>');
|
|
17
|
+
// text = text.replace(/(,)\n/g, '<span class="syntax">$1</span>\n');
|
|
18
18
|
|
|
19
19
|
let tempText = text.split('\n');
|
|
20
20
|
lines = tempText;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
|
|
4
|
+
export let afterSubmit = () => {};
|
|
5
|
+
export let url = undefined;
|
|
6
|
+
export let path = '/';
|
|
7
|
+
export let sending = false;
|
|
8
|
+
export let style = '';
|
|
9
|
+
export let avoidAutoMessage = false;
|
|
10
|
+
export let successMessage = null;
|
|
11
|
+
export let errorMessage = null;
|
|
12
|
+
export let includeToken = true;
|
|
13
|
+
export let appendData = undefined
|
|
14
|
+
export let alterData = undefined
|
|
15
|
+
export let preventQuit = true
|
|
16
|
+
|
|
17
|
+
import { getCookie } from './clientCookie';
|
|
18
|
+
import { beforeNavigate } from '$app/navigation';
|
|
19
|
+
import { config } from '../../../hamzus.config';
|
|
20
|
+
|
|
21
|
+
let form;
|
|
22
|
+
let isFormDirty = false;
|
|
23
|
+
|
|
24
|
+
onMount(() => {
|
|
25
|
+
window.addEventListener('beforeunload', handlePreventExit);
|
|
26
|
+
|
|
27
|
+
return () => {
|
|
28
|
+
window.removeEventListener('beforeunload', handlePreventExit);
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
function handleLinkClick(event) {
|
|
32
|
+
if (isFormDirty && preventQuit) {
|
|
33
|
+
const confirmLeave = confirm(
|
|
34
|
+
'Vos modifications pourraient être perdues. Êtes-vous sûr de vouloir quitter cette page ?'
|
|
35
|
+
);
|
|
36
|
+
if (!confirmLeave) {
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function handleSetFormDirty() {
|
|
42
|
+
isFormDirty = true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function handlePreventExit(event) {
|
|
46
|
+
if (isFormDirty && preventQuit) {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
// Certains navigateurs exigent que la propriété returnValue soit définie
|
|
49
|
+
event.returnValue =
|
|
50
|
+
'Vos modifications pourraient être perdues. Êtes-vous sûr de vouloir quitter cette page ?';
|
|
51
|
+
return 'Vos modifications pourraient être perdues. Êtes-vous sûr de vouloir quitter cette page ?';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
beforeNavigate(({ cancel }) => {
|
|
56
|
+
if (isFormDirty && preventQuit) {
|
|
57
|
+
let confiramtion = confirm("Vos modifications pourraient être perdues. Êtes-vous sûr de vouloir quitter cette page ?")
|
|
58
|
+
if (!confiramtion) {
|
|
59
|
+
cancel();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
function handleSubmit(event) {
|
|
65
|
+
if (sending) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
sending = true;
|
|
70
|
+
|
|
71
|
+
let formData = new FormData(event.target);
|
|
72
|
+
|
|
73
|
+
// inclure le token
|
|
74
|
+
if (includeToken) {
|
|
75
|
+
const token = getCookie(config['sessionTokenName']);
|
|
76
|
+
formData.append(config['sessionTokenName'], token);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// inclure les données
|
|
80
|
+
if (appendData !== undefined) {
|
|
81
|
+
let moreData = appendData();
|
|
82
|
+
|
|
83
|
+
for (const key in moreData) {
|
|
84
|
+
formData.append(key, moreData[key])
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// alterer si possible les donné
|
|
89
|
+
if (alterData !== undefined) {
|
|
90
|
+
const alteredData = alterData(Object.fromEntries(formData.entries())) // passer les données du formulaire acutelle en parametre
|
|
91
|
+
|
|
92
|
+
formData = new FormData();
|
|
93
|
+
|
|
94
|
+
for (const key in alteredData) {
|
|
95
|
+
formData.append(key, alteredData[key])
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
// Création de la requête XHR
|
|
101
|
+
var scriptPath = (url || config['url']) + path;
|
|
102
|
+
const xhr = new XMLHttpRequest();
|
|
103
|
+
xhr.open('POST', scriptPath, true);
|
|
104
|
+
|
|
105
|
+
xhr.onload = function () {
|
|
106
|
+
sending = false;
|
|
107
|
+
|
|
108
|
+
if (form.querySelectorAll(`input.invalid`).length > 0) {
|
|
109
|
+
const inputs = form.querySelectorAll(`input.invalid`);
|
|
110
|
+
for (const input of inputs) {
|
|
111
|
+
input.classList.remove('invalid');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const response = JSON.parse(xhr.responseText);
|
|
117
|
+
if (response.statusType == 'success') {
|
|
118
|
+
// desaactiver le invcalid
|
|
119
|
+
isFormDirty = false
|
|
120
|
+
errorMessage = null;
|
|
121
|
+
if (!avoidAutoMessage) {
|
|
122
|
+
successMessage = response.message;
|
|
123
|
+
setInterval(() => {
|
|
124
|
+
successMessage = null;
|
|
125
|
+
}, 5000);
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
successMessage = null;
|
|
129
|
+
if (!avoidAutoMessage) {
|
|
130
|
+
errorMessage = response.message;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// verifier si il y a des input manquant
|
|
134
|
+
if (response.requiredField && response.requiredField.length > 0) {
|
|
135
|
+
for (const inputName of response.requiredField) {
|
|
136
|
+
if (form.querySelector(`input[name="${inputName}"]`)) {
|
|
137
|
+
const input = form.querySelector(`input[name="${inputName}"]`);
|
|
138
|
+
input.classList.add('invalid');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (response.invalidField && response.invalidField.length > 0) {
|
|
143
|
+
for (const inputName of response.invalidField) {
|
|
144
|
+
if (form.querySelector(`input[name="${inputName}"]`)) {
|
|
145
|
+
const input = form.querySelector(`input[name="${inputName}"]`);
|
|
146
|
+
input.classList.add('invalid');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error(error);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (afterSubmit) {
|
|
157
|
+
afterSubmit(JSON.parse(xhr.responseText));
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
xhr.onerror = function () {
|
|
162
|
+
sending = false
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Envoi des données sous forme JSON
|
|
166
|
+
xhr.send(formData);
|
|
167
|
+
}
|
|
168
|
+
</script>
|
|
169
|
+
|
|
170
|
+
<form
|
|
171
|
+
{style}
|
|
172
|
+
on:input={handleSetFormDirty}
|
|
173
|
+
bind:this={form}
|
|
174
|
+
method="POST"
|
|
175
|
+
on:submit|preventDefault={handleSubmit}
|
|
176
|
+
>
|
|
177
|
+
<slot></slot>
|
|
178
|
+
<div class="buttons">
|
|
179
|
+
<slot name="button" />
|
|
180
|
+
</div>
|
|
181
|
+
<slot name="down-form" />
|
|
182
|
+
{#if successMessage}
|
|
183
|
+
<div class="success p">{successMessage}</div>
|
|
184
|
+
{/if}
|
|
185
|
+
{#if errorMessage}
|
|
186
|
+
<div class="error p">{errorMessage}</div>
|
|
187
|
+
{/if}
|
|
188
|
+
</form>
|
|
189
|
+
|
|
190
|
+
<style>
|
|
191
|
+
form {
|
|
192
|
+
display: flex;
|
|
193
|
+
flex-direction: column;
|
|
194
|
+
gap: 1rem;
|
|
195
|
+
}
|
|
196
|
+
.success {
|
|
197
|
+
width: 100%;
|
|
198
|
+
padding: 12px;
|
|
199
|
+
background-color: var(--green-b);
|
|
200
|
+
color: var(--green);
|
|
201
|
+
border-radius: 12px;
|
|
202
|
+
white-space: pre-wrap;
|
|
203
|
+
}
|
|
204
|
+
.error {
|
|
205
|
+
width: 100%;
|
|
206
|
+
padding: 12px;
|
|
207
|
+
background-color: var(--red-b);
|
|
208
|
+
color: var(--red);
|
|
209
|
+
border-radius: 12px;
|
|
210
|
+
white-space: pre-wrap;
|
|
211
|
+
}
|
|
212
|
+
.buttons {
|
|
213
|
+
display: flex;
|
|
214
|
+
width: 100%;
|
|
215
|
+
justify-content: end;
|
|
216
|
+
}
|
|
217
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
export function getCookie(cookieName) {
|
|
3
|
+
|
|
4
|
+
var cookies = document.cookie.split(';');
|
|
5
|
+
|
|
6
|
+
for (var i = 0; i < cookies.length; i++) {
|
|
7
|
+
var cookie = cookies[i].trim();
|
|
8
|
+
|
|
9
|
+
// Vérifie si le nom du cookie correspond à celui recherché
|
|
10
|
+
if (cookie.indexOf(cookieName + '=') === 0) {
|
|
11
|
+
// Retourne la valeur du cookie
|
|
12
|
+
return cookie.substring(cookieName.length + 1);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Retourne null si le cookie n'est pas trouvé
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
export let desabled = false
|
|
7
7
|
export let onClick = undefined
|
|
8
8
|
export let avoidRipple = false;
|
|
9
|
+
export let type = "button"
|
|
9
10
|
// local var
|
|
10
11
|
let button
|
|
11
12
|
// functions
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
}
|
|
41
42
|
</script>
|
|
42
43
|
|
|
43
|
-
<button bind:this={button} on:click={handleClick} class="button h4" class:desabled class:loading {...$$restProps}>
|
|
44
|
+
<button {type} bind:this={button} on:click={handleClick} class="button h4" class:desabled class:loading {...$$restProps}>
|
|
44
45
|
<slot/>
|
|
45
46
|
{#if loading}
|
|
46
47
|
<div class="loader">
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
export let hasChanged = false;
|
|
17
17
|
export let disabled = false;
|
|
18
18
|
export let required = false;
|
|
19
|
+
export let fullWidth = false;
|
|
19
20
|
export let type = '';
|
|
20
21
|
export let style = '';
|
|
21
22
|
export let placeholder = '';
|
|
@@ -88,7 +89,7 @@
|
|
|
88
89
|
|
|
89
90
|
<div
|
|
90
91
|
class="input-container {variant == 'default' ? variant : ''} {className}"
|
|
91
|
-
{style}
|
|
92
|
+
style={(fullWidth ? "width:100%;" : "") + style}
|
|
92
93
|
{...$$restProps}
|
|
93
94
|
>
|
|
94
95
|
<label
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
|
|
4
|
+
export let size = 100;
|
|
5
|
+
export let thickness = 10;
|
|
6
|
+
export let progress = 100; // 0–100
|
|
7
|
+
export let gapPixels = 4; // Écart visuel minimal
|
|
8
|
+
export let color = null;
|
|
9
|
+
export let colorPercentBased = {
|
|
10
|
+
0: 'var(--red)',
|
|
11
|
+
40: 'var(--orange)',
|
|
12
|
+
60: 'var(--green)'
|
|
13
|
+
};
|
|
14
|
+
export let bgColor = 'var(--bg-2)';
|
|
15
|
+
export let showLabel = false;
|
|
16
|
+
export let animated = true;
|
|
17
|
+
export let linecap = 'round';
|
|
18
|
+
|
|
19
|
+
const r = (size - thickness) / 2;
|
|
20
|
+
const cx = size / 2;
|
|
21
|
+
const cy = size / 2;
|
|
22
|
+
const circumference = 2 * Math.PI * r;
|
|
23
|
+
|
|
24
|
+
let dynamicColor = color === null;
|
|
25
|
+
let copyColor = dynamicColor
|
|
26
|
+
? animated
|
|
27
|
+
? colorPercentBased[0]
|
|
28
|
+
: getCorrectColor(progress)
|
|
29
|
+
: color;
|
|
30
|
+
let copyProgress = animated ? 0 : progress === 100 ? 99.99 : progress;
|
|
31
|
+
|
|
32
|
+
// Fonction ease-out (courbe cubic)
|
|
33
|
+
function easeOutCubic(t) {
|
|
34
|
+
return 1 - Math.pow(1 - t, 3);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
onMount(() => {
|
|
38
|
+
if (!animated) return;
|
|
39
|
+
|
|
40
|
+
const duration = 800; // en ms
|
|
41
|
+
const start = performance.now();
|
|
42
|
+
|
|
43
|
+
function animate(now) {
|
|
44
|
+
const elapsed = now - start;
|
|
45
|
+
const t = Math.min(elapsed / duration, 1); // clamp entre 0 et 1
|
|
46
|
+
|
|
47
|
+
const eased = easeOutCubic(t);
|
|
48
|
+
|
|
49
|
+
let newValue = Math.round(progress * eased);
|
|
50
|
+
|
|
51
|
+
copyProgress = newValue === 100 ? 99.99 : newValue;
|
|
52
|
+
|
|
53
|
+
// determiner la couleur si il y a dynamic color
|
|
54
|
+
if (dynamicColor) {
|
|
55
|
+
let finalColor = colorPercentBased[0];
|
|
56
|
+
for (const step in colorPercentBased) {
|
|
57
|
+
const color = colorPercentBased[step];
|
|
58
|
+
|
|
59
|
+
if (copyProgress > step) {
|
|
60
|
+
finalColor = color;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
copyColor = finalColor;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (t < 1) {
|
|
71
|
+
requestAnimationFrame(animate);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
requestAnimationFrame(animate);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
function getCorrectColor(progress) {
|
|
79
|
+
let finalColor = colorPercentBased[0];
|
|
80
|
+
for (const step in colorPercentBased) {
|
|
81
|
+
const color = colorPercentBased[step];
|
|
82
|
+
|
|
83
|
+
if (progress > step) {
|
|
84
|
+
finalColor = color;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return finalColor;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Progrès en angle
|
|
95
|
+
$: progressAngle = (copyProgress / 100) * 360;
|
|
96
|
+
|
|
97
|
+
// ➕ Compensation du stroke-linecap (ajoute une demi-thickness de chaque côté de l'arc)
|
|
98
|
+
const capVisualOverlap = thickness; // total débordement = thickness (2 * thickness/2)
|
|
99
|
+
|
|
100
|
+
$: totalGapPx = gapPixels + capVisualOverlap;
|
|
101
|
+
$: gapAngle = (totalGapPx / circumference) * 360;
|
|
102
|
+
|
|
103
|
+
$: startAngleProgress = -90;
|
|
104
|
+
$: endAngleProgress = startAngleProgress + progressAngle;
|
|
105
|
+
|
|
106
|
+
$: startAngleBg = endAngleProgress + gapAngle;
|
|
107
|
+
$: endAngleBg = 270 - gapAngle;
|
|
108
|
+
|
|
109
|
+
function polarToCartesian(cx, cy, r, angleDeg) {
|
|
110
|
+
const angleRad = (angleDeg * Math.PI) / 180;
|
|
111
|
+
return {
|
|
112
|
+
x: cx + r * Math.cos(angleRad),
|
|
113
|
+
y: cy + r * Math.sin(angleRad)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function describeArc(cx, cy, r, startAngle, endAngle) {
|
|
118
|
+
const start = polarToCartesian(cx, cy, r, endAngle);
|
|
119
|
+
const end = polarToCartesian(cx, cy, r, startAngle);
|
|
120
|
+
const largeArcFlag = endAngle - startAngle > 180 ? 1 : 0;
|
|
121
|
+
|
|
122
|
+
return ['M', start.x, start.y, 'A', r, r, 0, largeArcFlag, 0, end.x, end.y].join(' ');
|
|
123
|
+
}
|
|
124
|
+
</script>
|
|
125
|
+
|
|
126
|
+
<svg class="progress-circle" width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
|
|
127
|
+
<!-- Arc de progression -->
|
|
128
|
+
<path
|
|
129
|
+
d={describeArc(cx, cy, r, startAngleProgress, endAngleProgress)}
|
|
130
|
+
stroke={copyColor}
|
|
131
|
+
stroke-width={thickness}
|
|
132
|
+
fill="none"
|
|
133
|
+
stroke-linecap={linecap}
|
|
134
|
+
/>
|
|
135
|
+
|
|
136
|
+
{#if showLabel}
|
|
137
|
+
<text
|
|
138
|
+
x="50%"
|
|
139
|
+
y="50%"
|
|
140
|
+
text-anchor="middle"
|
|
141
|
+
dominant-baseline="central"
|
|
142
|
+
fill={copyColor}
|
|
143
|
+
style="font-size: {size / 3.5}px;"
|
|
144
|
+
class="h4">{Math.round(copyProgress)}%</text
|
|
145
|
+
>
|
|
146
|
+
{/if}
|
|
147
|
+
|
|
148
|
+
<!-- Arc de fond, après le progress + gap -->
|
|
149
|
+
{#if copyProgress < 96}
|
|
150
|
+
<path
|
|
151
|
+
d={describeArc(cx, cy, r, startAngleBg, endAngleBg)}
|
|
152
|
+
stroke={bgColor}
|
|
153
|
+
stroke-width={thickness}
|
|
154
|
+
fill="none"
|
|
155
|
+
stroke-linecap={linecap}
|
|
156
|
+
/>
|
|
157
|
+
{/if}
|
|
158
|
+
</svg>
|
|
159
|
+
|
|
160
|
+
<style>
|
|
161
|
+
.progress-circle path {
|
|
162
|
+
transition: stroke 0.3s ease-out;
|
|
163
|
+
}
|
|
164
|
+
.progress-circle text {
|
|
165
|
+
transition: color 0.3s ease-out;
|
|
166
|
+
}
|
|
167
|
+
</style>
|