v-uixy 1.1.3 → 1.3.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/commands/add.js +61 -8
- package/commands/init.js +25 -21
- package/commands/list.js +2 -3
- package/commands/remove.js +13 -6
- package/package.json +1 -1
- package/templates/components/Modal/Modal.component.vue +29 -23
- package/templates/components/PieChart/PieChart.component.vue +218 -0
- package/templates/components/PieChart/PieChart.types.ts +10 -0
- package/templates/components/PieChart/index.ts +4 -0
- package/templates/components/Sheet/Sheet.component.vue +27 -25
- package/templates/components.json +6 -0
- package/templates/nuxt.config.ts +16 -4
- package/templates/scripts/generateIconTypes.script.js +2 -2
- package/templates/tailwind.config.ts +1 -8
package/commands/add.js
CHANGED
|
@@ -10,12 +10,20 @@ const templatesRoot = path.resolve(
|
|
|
10
10
|
"../templates"
|
|
11
11
|
);
|
|
12
12
|
|
|
13
|
-
const componentsDir = path.join(projectRoot, "components/ui");
|
|
14
|
-
const composablesDir = path.join(projectRoot, "composables");
|
|
15
|
-
const componentIndexPath = path.join(componentsDir, "index.ts");
|
|
16
|
-
const composablesIndexPath = path.join(composablesDir, "index.ts");
|
|
17
13
|
const registryPath = path.join(templatesRoot, "components.json");
|
|
18
14
|
|
|
15
|
+
async function getProjectPaths() {
|
|
16
|
+
const componentsDir = path.join(projectRoot, "app/components/ui");
|
|
17
|
+
const composablesDir = path.join(projectRoot, "app/composables");
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
componentsDir,
|
|
21
|
+
composablesDir,
|
|
22
|
+
componentIndexPath: path.join(componentsDir, "index.ts"),
|
|
23
|
+
composablesIndexPath: path.join(composablesDir, "index.ts"),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
async function loadRegistry() {
|
|
20
28
|
const exists = await fs.pathExists(registryPath);
|
|
21
29
|
if (!exists) {
|
|
@@ -52,19 +60,21 @@ async function resolveDeps(name, registry, resolved = new Set()) {
|
|
|
52
60
|
|
|
53
61
|
async function copyComponent(name) {
|
|
54
62
|
const src = path.join(templatesRoot, `components/${name}`);
|
|
63
|
+
const { componentsDir } = await getProjectPaths();
|
|
55
64
|
const dest = path.join(componentsDir, name);
|
|
56
65
|
|
|
57
66
|
if (!(await fs.pathExists(src))) {
|
|
58
67
|
console.log(chalk.yellow(`⚠️ No template found for component "${name}"`));
|
|
59
68
|
return;
|
|
60
69
|
}
|
|
61
|
-
|
|
70
|
+
await fs.ensureDir(componentsDir);
|
|
62
71
|
await fs.copy(src, dest, { overwrite: true });
|
|
63
|
-
console.log(chalk.green(`✔ Copied ${name} to components/ui/${name}`));
|
|
72
|
+
console.log(chalk.green(`✔ Copied ${name} to app/components/ui/${name}`));
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
async function copyComposable(name) {
|
|
67
76
|
const src = path.join(templatesRoot, `composables/${name}.ts`);
|
|
77
|
+
const { composablesDir } = await getProjectPaths();
|
|
68
78
|
const dest = path.join(composablesDir, `${name}.ts`);
|
|
69
79
|
|
|
70
80
|
if (!(await fs.pathExists(src))) {
|
|
@@ -80,6 +90,7 @@ async function copyComposable(name) {
|
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
async function getExistingIndexComponents() {
|
|
93
|
+
const { componentIndexPath } = await getProjectPaths();
|
|
83
94
|
if (!(await fs.pathExists(componentIndexPath))) return [];
|
|
84
95
|
|
|
85
96
|
const content = await fs.readFile(componentIndexPath, "utf8");
|
|
@@ -92,6 +103,8 @@ async function getExistingIndexComponents() {
|
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
async function updateIndex(newComponents) {
|
|
106
|
+
const { componentIndexPath, componentsDir } = await getProjectPaths();
|
|
107
|
+
await fs.ensureDir(componentsDir);
|
|
95
108
|
await fs.ensureFile(componentIndexPath);
|
|
96
109
|
|
|
97
110
|
const existing = await getExistingIndexComponents();
|
|
@@ -107,18 +120,20 @@ async function updateIndex(newComponents) {
|
|
|
107
120
|
|
|
108
121
|
const lines = merged.map((name) => `export * from "./${name}";`);
|
|
109
122
|
await fs.writeFile(componentIndexPath, lines.join("\n") + "\n");
|
|
110
|
-
console.log(chalk.green(`🔗 Updated components/ui/index.ts`));
|
|
123
|
+
console.log(chalk.green(`🔗 Updated app/components/ui/index.ts`));
|
|
111
124
|
}
|
|
112
125
|
|
|
113
126
|
async function updateComposablesIndex(name) {
|
|
127
|
+
const { composablesIndexPath, composablesDir } = await getProjectPaths();
|
|
114
128
|
const line = `export * from "./${name}";`;
|
|
129
|
+
await fs.ensureDir(composablesDir);
|
|
115
130
|
await fs.ensureFile(composablesIndexPath);
|
|
116
131
|
const content = await fs.readFile(composablesIndexPath, "utf8");
|
|
117
132
|
const lines = content.split("\n").map((line) => line.trim());
|
|
118
133
|
|
|
119
134
|
if (!lines.includes(line)) {
|
|
120
135
|
await fs.appendFile(composablesIndexPath, line + "\n");
|
|
121
|
-
console.log(`🔗 Linked composable "${name}" in composables/index.ts`);
|
|
136
|
+
console.log(`🔗 Linked composable "${name}" in app/composables/index.ts`);
|
|
122
137
|
} else {
|
|
123
138
|
console.log(`ℹ️ Composable "${name}" already linked`);
|
|
124
139
|
}
|
|
@@ -162,10 +177,48 @@ async function installPackages(componentNames, registry) {
|
|
|
162
177
|
}
|
|
163
178
|
}
|
|
164
179
|
|
|
180
|
+
async function ensureTypeScriptInstalled() {
|
|
181
|
+
const packageJsonPath = path.join(projectRoot, "package.json");
|
|
182
|
+
if (!(await fs.pathExists(packageJsonPath))) return;
|
|
183
|
+
|
|
184
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
185
|
+
const hasTS = Boolean(
|
|
186
|
+
packageJson.dependencies?.typescript ||
|
|
187
|
+
packageJson.devDependencies?.typescript
|
|
188
|
+
);
|
|
189
|
+
if (hasTS) return;
|
|
190
|
+
|
|
191
|
+
console.log(chalk.blue("📦 Installing dev dependency: typescript"));
|
|
192
|
+
try {
|
|
193
|
+
await execa("npm", ["install", "-D", "typescript"], { stdio: "inherit" });
|
|
194
|
+
console.log(chalk.green("✔ Installed typescript"));
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.error(chalk.red("✖ Failed to install typescript"));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function ensureTsconfig() {
|
|
201
|
+
const tsconfigDest = path.join(projectRoot, "tsconfig.json");
|
|
202
|
+
if (await fs.pathExists(tsconfigDest)) return;
|
|
203
|
+
|
|
204
|
+
const tsconfigSrc = path.join(templatesRoot, "tsconfig.json");
|
|
205
|
+
if (!(await fs.pathExists(tsconfigSrc))) return;
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await fs.copy(tsconfigSrc, tsconfigDest);
|
|
209
|
+
console.log(chalk.green("✔ Added tsconfig.json"));
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.error(chalk.red("✖ Failed to add tsconfig.json"));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
165
215
|
export default async function add(componentName) {
|
|
166
216
|
const registry = await loadRegistry();
|
|
167
217
|
let componentsToAdd = new Set();
|
|
168
218
|
|
|
219
|
+
await ensureTypeScriptInstalled();
|
|
220
|
+
await ensureTsconfig();
|
|
221
|
+
|
|
169
222
|
if (componentName.toLowerCase() === "all") {
|
|
170
223
|
componentsToAdd = new Set(registry.map((c) => c.name));
|
|
171
224
|
} else {
|
package/commands/init.js
CHANGED
|
@@ -13,19 +13,22 @@ const root = process.cwd();
|
|
|
13
13
|
const templateRoot = path.resolve(__dirname, "../templates");
|
|
14
14
|
|
|
15
15
|
const filesToCopy = [
|
|
16
|
-
"components.json",
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
16
|
+
{ src: "components.json", dest: "components.json" },
|
|
17
|
+
{
|
|
18
|
+
src: "scripts/generateIconTypes.script.js",
|
|
19
|
+
dest: "scripts/generateIconTypes.script.js",
|
|
20
|
+
},
|
|
21
|
+
{ src: "assets/css/main.css", dest: "app/assets/css/main.css" },
|
|
22
|
+
{ src: "tailwind.config.ts", dest: "tailwind.config.ts" },
|
|
23
|
+
{ src: "nuxt.config.ts", dest: "nuxt.config.ts" },
|
|
24
|
+
{ src: "tsconfig.json", dest: "tsconfig.json" },
|
|
25
|
+
{ src: "types/styles.d.ts", dest: "types/styles.d.ts" },
|
|
26
|
+
{ src: "types/icons.ts", dest: "app/types/icons.ts" },
|
|
27
|
+
{ src: "utils/styles.ts", dest: "app/utils/styles.ts" },
|
|
28
|
+
{ src: "utils/twMerge.ts", dest: "app/utils/twMerge.ts" },
|
|
26
29
|
];
|
|
27
30
|
|
|
28
|
-
const dirsToCopy = ["assets/icons"];
|
|
31
|
+
const dirsToCopy = [{ src: "assets/icons", dest: "app/assets/icons" }];
|
|
29
32
|
|
|
30
33
|
async function installPackages() {
|
|
31
34
|
const packages = [
|
|
@@ -35,6 +38,7 @@ async function installPackages() {
|
|
|
35
38
|
"vite-svg-loader",
|
|
36
39
|
"tailwind-merge",
|
|
37
40
|
"@tailwindcss/vite",
|
|
41
|
+
"typescript",
|
|
38
42
|
];
|
|
39
43
|
|
|
40
44
|
console.log(chalk.blue("\n📦 Installing dependencies..."));
|
|
@@ -50,9 +54,9 @@ async function installPackages() {
|
|
|
50
54
|
export default async function init(options = {}) {
|
|
51
55
|
console.log(chalk.cyan("🧩 Initializing v-uixy..."));
|
|
52
56
|
|
|
53
|
-
for (const
|
|
54
|
-
const src = path.join(templateRoot,
|
|
55
|
-
const dest = path.join(root,
|
|
57
|
+
for (const { src: srcRel, dest: destRel } of filesToCopy) {
|
|
58
|
+
const src = path.join(templateRoot, srcRel);
|
|
59
|
+
const dest = path.join(root, destRel);
|
|
56
60
|
|
|
57
61
|
const exists = await fs.pathExists(dest);
|
|
58
62
|
if (exists) {
|
|
@@ -60,7 +64,7 @@ export default async function init(options = {}) {
|
|
|
60
64
|
{
|
|
61
65
|
type: "confirm",
|
|
62
66
|
name: "overwrite",
|
|
63
|
-
message: `${chalk.yellow(
|
|
67
|
+
message: `${chalk.yellow(destRel)} already exists. Overwrite?`,
|
|
64
68
|
default: false,
|
|
65
69
|
},
|
|
66
70
|
]);
|
|
@@ -73,12 +77,12 @@ export default async function init(options = {}) {
|
|
|
73
77
|
|
|
74
78
|
await fs.ensureDir(path.dirname(dest));
|
|
75
79
|
await fs.copy(src, dest);
|
|
76
|
-
console.log(chalk.green(`✔ Created ${
|
|
80
|
+
console.log(chalk.green(`✔ Created ${destRel}`));
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
for (const
|
|
80
|
-
const src = path.join(templateRoot,
|
|
81
|
-
const dest = path.join(root,
|
|
83
|
+
for (const { src: srcRel, dest: destRel } of dirsToCopy) {
|
|
84
|
+
const src = path.join(templateRoot, srcRel);
|
|
85
|
+
const dest = path.join(root, destRel);
|
|
82
86
|
|
|
83
87
|
const exists = await fs.pathExists(dest);
|
|
84
88
|
if (exists) {
|
|
@@ -87,7 +91,7 @@ export default async function init(options = {}) {
|
|
|
87
91
|
type: "confirm",
|
|
88
92
|
name: "overwrite",
|
|
89
93
|
message: `${chalk.yellow(
|
|
90
|
-
|
|
94
|
+
destRel
|
|
91
95
|
)} already exists. Overwrite entire directory?`,
|
|
92
96
|
default: false,
|
|
93
97
|
},
|
|
@@ -102,7 +106,7 @@ export default async function init(options = {}) {
|
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
await fs.copy(src, dest);
|
|
105
|
-
console.log(chalk.green(`✔ Copied directory ${
|
|
109
|
+
console.log(chalk.green(`✔ Copied directory ${destRel}`));
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
if (!options.withoutPackages) {
|
package/commands/list.js
CHANGED
|
@@ -4,11 +4,11 @@ import chalk from "chalk";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
|
|
6
6
|
const projectRoot = process.cwd();
|
|
7
|
-
const componentsDir = path.join(projectRoot, "components/ui");
|
|
8
7
|
const templatesRoot = path.resolve(
|
|
9
8
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
10
9
|
"../templates"
|
|
11
10
|
);
|
|
11
|
+
const componentsDir = path.join(projectRoot, "app/components/ui");
|
|
12
12
|
|
|
13
13
|
async function loadRegistry() {
|
|
14
14
|
const registryPath = path.join(templatesRoot, "components.json");
|
|
@@ -28,8 +28,7 @@ async function listComponents() {
|
|
|
28
28
|
const results = [];
|
|
29
29
|
|
|
30
30
|
for (const { name } of registry) {
|
|
31
|
-
const
|
|
32
|
-
const isInstalled = await fs.pathExists(componentPath);
|
|
31
|
+
const isInstalled = await fs.pathExists(path.join(componentsDir, name));
|
|
33
32
|
|
|
34
33
|
results.push({
|
|
35
34
|
name,
|
package/commands/remove.js
CHANGED
|
@@ -9,7 +9,7 @@ const templatesRoot = path.resolve(
|
|
|
9
9
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
10
10
|
"../templates"
|
|
11
11
|
);
|
|
12
|
-
const componentsDir = path.join(projectRoot, "components/ui");
|
|
12
|
+
const componentsDir = path.join(projectRoot, "app/components/ui");
|
|
13
13
|
const componentIndexPath = path.join(componentsDir, "index.ts");
|
|
14
14
|
|
|
15
15
|
async function loadRegistry() {
|
|
@@ -34,8 +34,15 @@ async function removeIndexExport(name) {
|
|
|
34
34
|
.filter((l) => l.trim() !== line)
|
|
35
35
|
.join("\n");
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
if (newContent !== content) {
|
|
38
|
+
await fs.writeFile(componentIndexPath, newContent);
|
|
39
|
+
console.log(
|
|
40
|
+
`🧹 Removed export from ${path.relative(
|
|
41
|
+
projectRoot,
|
|
42
|
+
componentIndexPath
|
|
43
|
+
)} for "${name}"`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
async function removeComponent(name) {
|
|
@@ -60,11 +67,11 @@ async function removeComponent(name) {
|
|
|
60
67
|
|
|
61
68
|
await fs.remove(pathToRemove);
|
|
62
69
|
await removeIndexExport(name);
|
|
63
|
-
console.log(
|
|
70
|
+
console.log(
|
|
71
|
+
chalk.green(`🗑️ Removed component "${name}" from app/components/ui`)
|
|
72
|
+
);
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
export default async function remove(componentName) {
|
|
67
|
-
const registry = await loadRegistry();
|
|
68
|
-
|
|
69
76
|
await removeComponent(componentName);
|
|
70
77
|
}
|
package/package.json
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
v-if="open"
|
|
6
|
-
:initial="{ opacity: 0 }"
|
|
7
|
-
:animate="{ opacity: 1 }"
|
|
8
|
-
:exit="{ opacity: 0 }"
|
|
9
|
-
:transition="{ duration: 0.15, ease: 'easeInOut' }"
|
|
10
|
-
:class="modalStyles($attrs.class as string)"
|
|
11
|
-
data-modal
|
|
12
|
-
@click.self="handleClick"
|
|
13
|
-
@keydown{esc}="handleClick"
|
|
14
|
-
>
|
|
2
|
+
<client-only>
|
|
3
|
+
<teleport to="body">
|
|
4
|
+
<animate-presence>
|
|
15
5
|
<motion.div
|
|
16
|
-
|
|
17
|
-
:
|
|
18
|
-
:
|
|
6
|
+
v-if="open"
|
|
7
|
+
:initial="{ opacity: 0 }"
|
|
8
|
+
:animate="{ opacity: 1 }"
|
|
9
|
+
:exit="{ opacity: 0 }"
|
|
19
10
|
:transition="{ duration: 0.15, ease: 'easeInOut' }"
|
|
11
|
+
:class="modalStyles($attrs.class as string)"
|
|
12
|
+
data-modal
|
|
13
|
+
@click.self="handleClick"
|
|
14
|
+
@keydown{esc}="handleClick"
|
|
20
15
|
>
|
|
21
|
-
<
|
|
16
|
+
<motion.div
|
|
17
|
+
:initial="{ scale: 0.8 }"
|
|
18
|
+
:animate="{ scale: 1 }"
|
|
19
|
+
:exit="{ scale: 0.8 }"
|
|
20
|
+
:transition="{ duration: 0.15, ease: 'easeInOut' }"
|
|
21
|
+
>
|
|
22
|
+
<slot />
|
|
23
|
+
</motion.div>
|
|
22
24
|
</motion.div>
|
|
23
|
-
</
|
|
24
|
-
</
|
|
25
|
-
</
|
|
25
|
+
</animate-presence>
|
|
26
|
+
</teleport>
|
|
27
|
+
</client-only>
|
|
26
28
|
</template>
|
|
27
29
|
|
|
28
30
|
<script setup lang="ts">
|
|
@@ -43,10 +45,14 @@
|
|
|
43
45
|
};
|
|
44
46
|
|
|
45
47
|
const toggleSiblingsInert = (enable?: boolean) => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
if (typeof document === "undefined") return;
|
|
49
|
+
|
|
50
|
+
const modalEl = document.querySelector(
|
|
51
|
+
"[data-modal]"
|
|
52
|
+
) as HTMLElement | null;
|
|
48
53
|
|
|
49
|
-
|
|
54
|
+
Array.from(document.body.children).forEach((el) => {
|
|
55
|
+
if (modalEl && (el === modalEl || el.contains(modalEl))) return;
|
|
50
56
|
|
|
51
57
|
if (enable) {
|
|
52
58
|
el.setAttribute("inert", "");
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div ref="wrapperRef" class="relative w-full aspect-square">
|
|
3
|
+
<svg
|
|
4
|
+
ref="svgRef"
|
|
5
|
+
width="100%"
|
|
6
|
+
height="100%"
|
|
7
|
+
:viewBox="`0 0 ${size} ${size}`"
|
|
8
|
+
role="img"
|
|
9
|
+
aria-label="Pie chart"
|
|
10
|
+
class="block select-none pointer-events-none"
|
|
11
|
+
>
|
|
12
|
+
<motion.g
|
|
13
|
+
:initial="{ transform: `rotate(0, ${center}, ${center})` }"
|
|
14
|
+
:animate="{ transform: `rotate(0, ${center}, ${center})` }"
|
|
15
|
+
:transition="groupTransition"
|
|
16
|
+
>
|
|
17
|
+
<circle
|
|
18
|
+
:cx="center"
|
|
19
|
+
:cy="center"
|
|
20
|
+
:r="radius"
|
|
21
|
+
fill="none"
|
|
22
|
+
class="stroke-gray-300 dark:stroke-gray-800"
|
|
23
|
+
:stroke-width="thickness"
|
|
24
|
+
vector-effect="non-scaling-stroke"
|
|
25
|
+
shape-rendering="geometricPrecision"
|
|
26
|
+
/>
|
|
27
|
+
<template v-if="isReady">
|
|
28
|
+
<motion.circle
|
|
29
|
+
v-for="seg in segmentsTimed"
|
|
30
|
+
:key="seg.key"
|
|
31
|
+
:initial="{
|
|
32
|
+
strokeDasharray: `0 ${circumference}`,
|
|
33
|
+
strokeDashoffset: seg.offset,
|
|
34
|
+
opacity: 0.8,
|
|
35
|
+
}"
|
|
36
|
+
:animate="{
|
|
37
|
+
strokeDasharray: `${seg.length} ${circumference}`,
|
|
38
|
+
strokeDashoffset: seg.offset,
|
|
39
|
+
opacity: 1,
|
|
40
|
+
}"
|
|
41
|
+
:transition="transitionFor(seg)"
|
|
42
|
+
:cx="center"
|
|
43
|
+
:cy="center"
|
|
44
|
+
:r="radius"
|
|
45
|
+
fill="none"
|
|
46
|
+
:stroke="seg.color"
|
|
47
|
+
:stroke-width="thickness - 2"
|
|
48
|
+
stroke-linecap="butt"
|
|
49
|
+
vector-effect="non-scaling-stroke"
|
|
50
|
+
shape-rendering="geometricPrecision"
|
|
51
|
+
/>
|
|
52
|
+
</template>
|
|
53
|
+
</motion.g>
|
|
54
|
+
</svg>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<script setup lang="ts">
|
|
59
|
+
import { computed, onMounted, onUnmounted, ref, toRefs, watch } from "vue";
|
|
60
|
+
import { motion } from "motion-v";
|
|
61
|
+
import type { UixyPieChartProps } from "./PieChart.types";
|
|
62
|
+
|
|
63
|
+
type SegBase = { key: string; length: number; offset: number; color: string };
|
|
64
|
+
type SegTimed = SegBase & { dur: number; delay: number };
|
|
65
|
+
|
|
66
|
+
const INITIAL_TOTAL_DURATION = 1.2;
|
|
67
|
+
const UPDATE_DURATION = 0.45;
|
|
68
|
+
const OVERLAP = 0.02;
|
|
69
|
+
const SIZE_EPSILON = 0.5;
|
|
70
|
+
const EASING = "linear";
|
|
71
|
+
|
|
72
|
+
const props = withDefaults(defineProps<UixyPieChartProps>(), {
|
|
73
|
+
thickness: 18,
|
|
74
|
+
animate: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const wrapperRef = ref<HTMLElement | null>(null);
|
|
78
|
+
const svgRef = ref<SVGSVGElement | null>(null);
|
|
79
|
+
const measuredSize = ref<number>(180);
|
|
80
|
+
const roRef = ref<ResizeObserver | null>(null);
|
|
81
|
+
const isReady = ref(false);
|
|
82
|
+
const initialTimer = ref<ReturnType<typeof setTimeout> | null>(null);
|
|
83
|
+
|
|
84
|
+
const size = computed(() => measuredSize.value);
|
|
85
|
+
const center = computed(() => measuredSize.value / 2);
|
|
86
|
+
const radius = computed(() =>
|
|
87
|
+
Math.max(1, measuredSize.value / 2 - props.thickness / 2 - 2)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const circumference = computed(() => 2 * Math.PI * radius.value);
|
|
91
|
+
|
|
92
|
+
const groupTransition = computed(() =>
|
|
93
|
+
!isReady.value || isInitial.value
|
|
94
|
+
? { duration: 0 }
|
|
95
|
+
: { duration: 0.35, easing: EASING }
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const total = computed(() =>
|
|
99
|
+
props.data.reduce((acc, d) => acc + Math.max(0, d.value), 0)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const segmentsBase = computed<SegBase[]>(() => {
|
|
103
|
+
const totalVal = total.value || 1;
|
|
104
|
+
let acc = 0;
|
|
105
|
+
const list: SegBase[] = [];
|
|
106
|
+
|
|
107
|
+
props.data.forEach((d, i) => {
|
|
108
|
+
const frac = Math.max(0, d.value) / totalVal;
|
|
109
|
+
const baseLen = frac * circumference.value;
|
|
110
|
+
const drawLen = Math.max(0, baseLen);
|
|
111
|
+
|
|
112
|
+
if (drawLen > 0) {
|
|
113
|
+
list.push({
|
|
114
|
+
key: `seg-${i}-${d.color ?? "c"}`,
|
|
115
|
+
length: drawLen,
|
|
116
|
+
offset: -acc,
|
|
117
|
+
color: d.color || "#999999",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
acc += baseLen;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return list;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const segmentsTimed = computed<SegTimed[]>(() => {
|
|
128
|
+
const totalDur = Math.max(0.01, INITIAL_TOTAL_DURATION);
|
|
129
|
+
let running = 0;
|
|
130
|
+
|
|
131
|
+
return segmentsBase.value.map((s) => {
|
|
132
|
+
const dur = (s.length / Math.max(1e-6, circumference.value)) * totalDur;
|
|
133
|
+
const delay = Math.max(0, running - OVERLAP);
|
|
134
|
+
const segT: SegTimed = { ...s, dur, delay };
|
|
135
|
+
running += dur;
|
|
136
|
+
return segT;
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const isInitial = ref(true);
|
|
141
|
+
|
|
142
|
+
watch(
|
|
143
|
+
() => ({ ready: isReady.value }),
|
|
144
|
+
({ ready }) => {
|
|
145
|
+
if (!ready) return;
|
|
146
|
+
|
|
147
|
+
if (initialTimer.value) return;
|
|
148
|
+
|
|
149
|
+
const totalDur = INITIAL_TOTAL_DURATION;
|
|
150
|
+
|
|
151
|
+
initialTimer.value = setTimeout(() => {
|
|
152
|
+
isInitial.value = false;
|
|
153
|
+
if (initialTimer.value) {
|
|
154
|
+
clearTimeout(initialTimer.value);
|
|
155
|
+
initialTimer.value = null;
|
|
156
|
+
}
|
|
157
|
+
}, Math.ceil(totalDur * 1000) + 16);
|
|
158
|
+
},
|
|
159
|
+
{ immediate: true }
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
onMounted(() => {
|
|
163
|
+
startResizeObserver();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
onUnmounted(() => {
|
|
167
|
+
stopResizeObserver();
|
|
168
|
+
|
|
169
|
+
if (initialTimer.value) {
|
|
170
|
+
clearTimeout(initialTimer.value);
|
|
171
|
+
initialTimer.value = null;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const transitionFor = (seg: SegTimed) => {
|
|
176
|
+
if (isInitial.value)
|
|
177
|
+
return { delay: seg.delay, duration: seg.dur, easing: EASING } as const;
|
|
178
|
+
|
|
179
|
+
return { delay: 0, duration: UPDATE_DURATION, easing: EASING };
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const measure = () => {
|
|
183
|
+
const el = wrapperRef.value ?? svgRef.value;
|
|
184
|
+
|
|
185
|
+
if (!el) return;
|
|
186
|
+
|
|
187
|
+
const rect = el.getBoundingClientRect();
|
|
188
|
+
const next = Math.max(1, Math.min(rect.width || 0, rect.height || 0));
|
|
189
|
+
|
|
190
|
+
if (Math.abs(next - measuredSize.value) > SIZE_EPSILON)
|
|
191
|
+
measuredSize.value = next;
|
|
192
|
+
|
|
193
|
+
if (!isReady.value && next > 1) isReady.value = true;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const startResizeObserver = () => {
|
|
197
|
+
if (roRef.value) return;
|
|
198
|
+
|
|
199
|
+
roRef.value = new ResizeObserver(measure);
|
|
200
|
+
|
|
201
|
+
const el = wrapperRef.value ?? svgRef.value;
|
|
202
|
+
|
|
203
|
+
if (el) roRef.value.observe(el);
|
|
204
|
+
|
|
205
|
+
requestAnimationFrame(measure);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const stopResizeObserver = () => {
|
|
209
|
+
if (!roRef.value) return;
|
|
210
|
+
|
|
211
|
+
const el = wrapperRef.value ?? svgRef.value;
|
|
212
|
+
|
|
213
|
+
if (el) roRef.value.unobserve(el);
|
|
214
|
+
|
|
215
|
+
roRef.value.disconnect();
|
|
216
|
+
roRef.value = null;
|
|
217
|
+
};
|
|
218
|
+
</script>
|
|
@@ -1,31 +1,33 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
2
|
+
<client-only>
|
|
3
|
+
<teleport to="body">
|
|
4
|
+
<animate-presence>
|
|
5
|
+
<template v-if="open">
|
|
6
|
+
<motion.div
|
|
7
|
+
:initial="{ opacity: 0 }"
|
|
8
|
+
:animate="{ opacity: 1 }"
|
|
9
|
+
:exit="{ opacity: 0 }"
|
|
10
|
+
:transition="{ ease: 'easeInOut', duration: 0.15 }"
|
|
11
|
+
@click.self="handleClick"
|
|
12
|
+
class="fixed left-0 top-0 z-20 size-full bg-white/5 backdrop-blur-[2px] dark:bg-gray-900/5"
|
|
13
|
+
/>
|
|
14
|
+
<motion.div
|
|
15
|
+
initial="initial"
|
|
16
|
+
animate="animate"
|
|
17
|
+
exit="exit"
|
|
18
|
+
:variants="ANIMATIONS[props.direction ?? 'right']"
|
|
19
|
+
:transition="{ ease: 'easeInOut', duration: 0.3 }"
|
|
20
|
+
:class="
|
|
20
21
|
sheetStyles({ direction: props.direction ?? 'right' }, $attrs.class as string)
|
|
21
22
|
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
v-bind="filteredAttrs"
|
|
24
|
+
>
|
|
25
|
+
<slot />
|
|
26
|
+
</motion.div>
|
|
27
|
+
</template>
|
|
28
|
+
</animate-presence>
|
|
29
|
+
</teleport>
|
|
30
|
+
</client-only>
|
|
29
31
|
</template>
|
|
30
32
|
|
|
31
33
|
<script setup lang="ts">
|
package/templates/nuxt.config.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
import svgLoader from "vite-svg-loader";
|
|
2
2
|
import tailwindcss from "@tailwindcss/vite";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
|
-
import { dirname,
|
|
4
|
+
import { dirname, resolve } from "path";
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
7
8
|
|
|
8
9
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
9
10
|
export default defineNuxtConfig({
|
|
10
|
-
|
|
11
|
+
modules: ["motion-v/nuxt"],
|
|
12
|
+
compatibilityDate: "2025-10-15",
|
|
11
13
|
devtools: { enabled: true },
|
|
12
|
-
css: [
|
|
14
|
+
css: ["~/assets/css/main.css"],
|
|
13
15
|
vite: {
|
|
14
16
|
plugins: [svgLoader(), tailwindcss()],
|
|
17
|
+
resolve: {
|
|
18
|
+
alias: {
|
|
19
|
+
"~/utils": resolve(__dirname, "app/utils"),
|
|
20
|
+
"@/utils": resolve(__dirname, "app/utils"),
|
|
21
|
+
"~/assets": resolve(__dirname, "app/assets"),
|
|
22
|
+
"@/assets": resolve(__dirname, "app/assets"),
|
|
23
|
+
"~/types": resolve(__dirname, "app/types"),
|
|
24
|
+
"@/types": resolve(__dirname, "app/types"),
|
|
25
|
+
},
|
|
26
|
+
},
|
|
15
27
|
},
|
|
16
28
|
});
|
|
@@ -5,8 +5,8 @@ import { fileURLToPath } from "url";
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = path.dirname(__filename);
|
|
7
7
|
|
|
8
|
-
const svgDir = path.join(__dirname, "../assets/icons");
|
|
9
|
-
const outputFile = path.join(__dirname, "../types/icons.ts");
|
|
8
|
+
const svgDir = path.join(__dirname, "../app/assets/icons");
|
|
9
|
+
const outputFile = path.join(__dirname, "../app/types/icons.ts");
|
|
10
10
|
|
|
11
11
|
function formatKey(name) {
|
|
12
12
|
return name
|
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
/** @type {import('tailwindcss').Config} */
|
|
2
2
|
export default {
|
|
3
|
-
content: [
|
|
4
|
-
"./components/**/*.{js,vue,ts}",
|
|
5
|
-
"./layouts/**/*.vue",
|
|
6
|
-
"./pages/**/*.vue",
|
|
7
|
-
"./plugins/**/*.{js,ts}",
|
|
8
|
-
"./app.vue",
|
|
9
|
-
"./error.vue",
|
|
10
|
-
],
|
|
3
|
+
content: ["./app/**/*.{js,ts,vue}", "./error.vue"],
|
|
11
4
|
darkMode: "class",
|
|
12
5
|
};
|