@tsparticles/template-login 4.1.3 → 4.2.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +3 -0
  3. package/package.json +4 -3
  4. package/template/angular/package.json +34 -0
  5. package/template/angular/src/app/app.component.css +151 -0
  6. package/template/angular/src/app/app.component.html +32 -0
  7. package/template/angular/src/app/app.component.ts +105 -0
  8. package/template/angular/src/app/app.module.ts +13 -0
  9. package/template/angular-confetti/package.json +36 -0
  10. package/template/angular-confetti/src/app/app.component.css +151 -0
  11. package/template/angular-confetti/src/app/app.component.html +32 -0
  12. package/template/angular-confetti/src/app/app.component.ts +105 -0
  13. package/template/angular-confetti/src/app/app.module.ts +13 -0
  14. package/template/angular-fireworks/package.json +36 -0
  15. package/template/angular-fireworks/src/app/app.component.css +151 -0
  16. package/template/angular-fireworks/src/app/app.component.html +32 -0
  17. package/template/angular-fireworks/src/app/app.component.ts +105 -0
  18. package/template/angular-fireworks/src/app/app.module.ts +13 -0
  19. package/template/astro/package.json +22 -0
  20. package/template/astro/src/pages/index.astro +173 -0
  21. package/template/ember/app/templates/application.hbs +61 -0
  22. package/template/ember/package.json +42 -0
  23. package/template/inferno/package.json +24 -0
  24. package/template/inferno/src/App.css +151 -0
  25. package/template/inferno/src/App.tsx +158 -0
  26. package/template/jquery/package.json +22 -0
  27. package/template/jquery/src/main.ts +110 -0
  28. package/template/jquery/src/style.css +151 -0
  29. package/template/lit/package.json +22 -0
  30. package/template/lit/src/my-app.ts +143 -0
  31. package/template/nextjs/package.json +25 -0
  32. package/template/nextjs/src/app/page.tsx +168 -0
  33. package/template/nextjs/src/app/providers.tsx +13 -0
  34. package/template/nuxt2/package.json +17 -0
  35. package/template/nuxt2/pages/index.vue +303 -0
  36. package/template/nuxt3/app.vue +288 -0
  37. package/template/nuxt3/package.json +21 -0
  38. package/template/nuxt4/app.vue +288 -0
  39. package/template/nuxt4/package.json +21 -0
  40. package/template/preact/package.json +23 -0
  41. package/template/preact/src/App.css +151 -0
  42. package/template/preact/src/App.tsx +140 -0
  43. package/template/qwik/package.json +22 -0
  44. package/template/qwik/src/App.tsx +139 -0
  45. package/template/react/package.json +19 -0
  46. package/template/react/src/App.css +151 -0
  47. package/template/react/src/App.tsx +140 -0
  48. package/template/riot/package.json +24 -0
  49. package/template/riot/src/app.riot +156 -0
  50. package/template/solid/package.json +18 -0
  51. package/template/solid/src/App.tsx +169 -0
  52. package/template/solid/src/index.css +151 -0
  53. package/template/solid/src/main.tsx +9 -0
  54. package/template/stencil/package.json +20 -0
  55. package/template/stencil/src/components/app-home/app-home.tsx +164 -0
  56. package/template/svelte/package.json +20 -0
  57. package/template/svelte/src/App.svelte +325 -0
  58. package/template/svelte/src/main.ts +12 -0
  59. package/template/vanilla/LICENSE +21 -0
  60. package/template/vanilla/src/main.ts +1 -1
  61. package/template/vue2/package.json +24 -0
  62. package/template/vue2/src/App.vue +287 -0
  63. package/template/vue3/package.json +19 -0
  64. package/template/vue3/src/App.vue +286 -0
  65. package/template/vue3/src/main.ts +14 -0
  66. package/template/webcomponents/package.json +21 -0
  67. package/template/webcomponents/src/main.ts +125 -0
  68. package/template/webcomponents/src/style.css +151 -0
@@ -0,0 +1,143 @@
1
+ import { LitElement, html } from "lit";
2
+ import { customElement, state } from "lit/decorators.js";
3
+ import type { ISourceOptions } from "@tsparticles/engine";
4
+
5
+ const options: ISourceOptions = {
6
+ background: { color: { value: "transparent" } },
7
+ fpsLimit: 60,
8
+ particles: {
9
+ number: { value: 60, density: { enable: true } },
10
+ color: { value: ["#6c5ce7", "#a29bfe", "#fd79a8", "#00cec9"] },
11
+ shape: { type: "circle" },
12
+ opacity: { value: 0.5, random: true },
13
+ size: { value: { min: 1, max: 3 }, random: true },
14
+ move: { enable: true, speed: 1, direction: "none", random: true, straight: false, outModes: { default: "out" } },
15
+ },
16
+ interactivity: {
17
+ events: { onHover: { enable: true, mode: "bubble" } },
18
+ modes: { bubble: { distance: 200, size: 6, duration: 2, opacity: 0.8 } },
19
+ },
20
+ detectRetina: true,
21
+ };
22
+
23
+ function validateEmail(val: string): boolean {
24
+ return /^[^\s@]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/.test(val);
25
+ }
26
+
27
+ @customElement("my-app")
28
+ export class MyApp extends LitElement {
29
+ @state() private isLogin = true;
30
+ @state() private theme = localStorage.getItem("theme") || "dark";
31
+ @state() private email = "";
32
+ @state() private password = "";
33
+ @state() private confirm = "";
34
+ @state() private emailErr = "";
35
+ @state() private passErr = "";
36
+ @state() private confirmErr = "";
37
+
38
+ connectedCallback(): void {
39
+ super.connectedCallback();
40
+ document.documentElement.setAttribute("data-theme", this.theme);
41
+ }
42
+
43
+ private setTheme(t: string): void {
44
+ this.theme = t;
45
+ document.documentElement.setAttribute("data-theme", t);
46
+ localStorage.setItem("theme", t);
47
+ }
48
+
49
+ private toggleTheme(): void {
50
+ this.setTheme(this.theme === "dark" ? "light" : "dark");
51
+ }
52
+
53
+ private handleSubmit(e: Event): void {
54
+ e.preventDefault();
55
+ let valid = true;
56
+
57
+ if (!this.email || !validateEmail(this.email)) {
58
+ this.emailErr = "Please enter a valid email address";
59
+ valid = false;
60
+ } else {
61
+ this.emailErr = "";
62
+ }
63
+
64
+ if (!this.password || this.password.length < 6) {
65
+ this.passErr = "Password must be at least 6 characters";
66
+ valid = false;
67
+ } else {
68
+ this.passErr = "";
69
+ }
70
+
71
+ if (!this.isLogin) {
72
+ if (this.password !== this.confirm) {
73
+ this.confirmErr = "Passwords do not match";
74
+ valid = false;
75
+ } else {
76
+ this.confirmErr = "";
77
+ }
78
+ }
79
+
80
+ if (!valid) return;
81
+
82
+ if (this.isLogin) {
83
+ alert("Logged in successfully! (demo)");
84
+ } else {
85
+ alert("Account created successfully! (demo)");
86
+ this.isLogin = true;
87
+ }
88
+
89
+ this.email = "";
90
+ this.password = "";
91
+ this.confirm = "";
92
+ }
93
+
94
+ private toggleMode(e: Event): void {
95
+ e.preventDefault();
96
+ this.isLogin = !this.isLogin;
97
+ this.emailErr = "";
98
+ this.passErr = "";
99
+ this.confirmErr = "";
100
+ }
101
+
102
+ render() {
103
+ return html`
104
+ <lit-particles id="tsparticles" .options=${options}></lit-particles>
105
+ <div class="auth-container">
106
+ <div class="auth-card">
107
+ <div class="auth-header">
108
+ <h1>${this.isLogin ? "Login" : "Register"}</h1>
109
+ <button class="theme-btn" @click=${this.toggleTheme} aria-label="Toggle theme">${this.theme === "dark" ? "🌙" : "☀️"}</button>
110
+ </div>
111
+ <form @submit=${this.handleSubmit} novalidate>
112
+ <div class="form-group">
113
+ <label for="email">Email</label>
114
+ <input id="email" type="email" placeholder="you@example.com" .value=${this.email} @input=${(e: InputEvent) => { this.email = (e.target as HTMLInputElement).value; }} required />
115
+ <span class="error">${this.emailErr}</span>
116
+ </div>
117
+ <div class="form-group">
118
+ <label for="password">Password</label>
119
+ <input id="password" type="password" placeholder="Enter password" .value=${this.password} @input=${(e: InputEvent) => { this.password = (e.target as HTMLInputElement).value; }} required />
120
+ <span class="error">${this.passErr}</span>
121
+ </div>
122
+ ${!this.isLogin ? html`
123
+ <div class="form-group">
124
+ <label for="confirmPassword">Confirm Password</label>
125
+ <input id="confirmPassword" type="password" placeholder="Confirm password" .value=${this.confirm} @input=${(e: InputEvent) => { this.confirm = (e.target as HTMLInputElement).value; }} />
126
+ <span class="error">${this.confirmErr}</span>
127
+ </div>
128
+ ` : ""}
129
+ <button type="submit">${this.isLogin ? "Sign In" : "Create Account"}</button>
130
+ </form>
131
+ <p class="toggle-text">
132
+ <span>${this.isLogin ? "Don't have an account?" : "Already have an account?"}</span>
133
+ <a href="#" @click=${this.toggleMode}>${this.isLogin ? "Register" : "Login"}</a>
134
+ </p>
135
+ </div>
136
+ </div>
137
+ `;
138
+ }
139
+
140
+ createRenderRoot() {
141
+ return this;
142
+ }
143
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "preview": "next start"
9
+ },
10
+ "dependencies": {
11
+ "next": "^15.3.2",
12
+ "react": "^19.2.5",
13
+ "react-dom": "^19.2.5",
14
+ "@tsparticles/nextjs": "^4.1.3",
15
+ "@tsparticles/slim": "^4.1.3",
16
+ "@tsparticles/configs": "^4.1.3",
17
+ "@tsparticles/engine": "^4.1.3"
18
+ },
19
+ "devDependencies": {
20
+ "typescript": "^6.0.3",
21
+ "@types/node": "^22.15.3",
22
+ "@types/react": "^19.1.4",
23
+ "@types/react-dom": "^19.1.3"
24
+ }
25
+ }
@@ -0,0 +1,168 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { NextParticles } from "@tsparticles/nextjs";
5
+ import type { ISourceOptions } from "@tsparticles/engine";
6
+
7
+ const options: ISourceOptions = {
8
+ background: { color: { value: "transparent" } },
9
+ fpsLimit: 60,
10
+ particles: {
11
+ number: { value: 60, density: { enable: true } },
12
+ color: { value: ["#6c5ce7", "#a29bfe", "#fd79a8", "#00cec9"] },
13
+ shape: { type: "circle" },
14
+ opacity: { value: 0.5, random: true },
15
+ size: { value: { min: 1, max: 3 }, random: true },
16
+ move: { enable: true, speed: 1, direction: "none", random: true, straight: false, outModes: { default: "out" } },
17
+ },
18
+ interactivity: {
19
+ events: { onHover: { enable: true, mode: "bubble" } },
20
+ modes: { bubble: { distance: 200, size: 6, duration: 2, opacity: 0.8 } },
21
+ },
22
+ detectRetina: true,
23
+ };
24
+
25
+ function validateEmail(email: string): boolean {
26
+ return /^[^\s@]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/.test(email);
27
+ }
28
+
29
+ export default function Home() {
30
+ const [theme, setTheme] = useState("dark");
31
+ const [isLogin, setIsLogin] = useState(true);
32
+ const [email, setEmail] = useState("");
33
+ const [password, setPassword] = useState("");
34
+ const [confirm, setConfirm] = useState("");
35
+ const [emailErr, setEmailErr] = useState("");
36
+ const [passErr, setPassErr] = useState("");
37
+ const [confirmErr, setConfirmErr] = useState("");
38
+
39
+ useEffect(() => {
40
+ const saved = localStorage.getItem("theme") || "dark";
41
+ setTheme(saved);
42
+ document.documentElement.setAttribute("data-theme", saved);
43
+ }, []);
44
+
45
+ useEffect(() => {
46
+ document.documentElement.setAttribute("data-theme", theme);
47
+ localStorage.setItem("theme", theme);
48
+ }, [theme]);
49
+
50
+ function toggleTheme() {
51
+ setTheme((t) => (t === "dark" ? "light" : "dark"));
52
+ }
53
+
54
+ function handleSubmit(e: React.FormEvent) {
55
+ e.preventDefault();
56
+ let valid = true;
57
+
58
+ if (!email || !validateEmail(email)) {
59
+ setEmailErr("Please enter a valid email address");
60
+ valid = false;
61
+ } else {
62
+ setEmailErr("");
63
+ }
64
+
65
+ if (!password || password.length < 6) {
66
+ setPassErr("Password must be at least 6 characters");
67
+ valid = false;
68
+ } else {
69
+ setPassErr("");
70
+ }
71
+
72
+ if (!isLogin) {
73
+ if (password !== confirm) {
74
+ setConfirmErr("Passwords do not match");
75
+ valid = false;
76
+ } else {
77
+ setConfirmErr("");
78
+ }
79
+ }
80
+
81
+ if (!valid) return;
82
+
83
+ if (isLogin) {
84
+ alert("Logged in successfully! (demo)");
85
+ } else {
86
+ alert("Account created successfully! (demo)");
87
+ setIsLogin(true);
88
+ }
89
+
90
+ setEmail("");
91
+ setPassword("");
92
+ setConfirm("");
93
+ }
94
+
95
+ function toggleMode(e: React.MouseEvent) {
96
+ e.preventDefault();
97
+ setIsLogin((m) => !m);
98
+ setEmailErr("");
99
+ setPassErr("");
100
+ setConfirmErr("");
101
+ }
102
+
103
+ return (
104
+ <html lang="en">
105
+ <head>
106
+ <title>{{projectName}}</title>
107
+ <style>{`
108
+ :root { --bg: #0f0f1a; --card-bg: rgba(255, 255, 255, 0.05); --text: #fff; --border: rgba(255, 255, 255, 0.1); --input-bg: rgba(255, 255, 255, 0.08); --accent: #6c5ce7; --accent-hover: #a29bfe; --error: #e74c3c; }
109
+ [data-theme="light"] { --bg: #f0f0f5; --card-bg: rgba(255, 255, 255, 0.8); --text: #1a1a2e; --border: rgba(0, 0, 0, 0.1); --input-bg: rgba(0, 0, 0, 0.05); }
110
+ * { box-sizing: border-box; }
111
+ body { margin: 0; font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; background: var(--bg); color: var(--text); transition: background 0.3s, color 0.3s; }
112
+ .auth-container { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10; width: 100%; max-width: 400px; padding: 1rem; }
113
+ .auth-card { background: var(--card-bg); backdrop-filter: blur(10px); border: 1px solid var(--border); border-radius: 16px; padding: 2rem; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); }
114
+ .auth-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; }
115
+ .auth-header h1 { margin: 0; font-size: 1.8em; }
116
+ .theme-btn { background: none; border: 1px solid var(--border); border-radius: 8px; padding: 0.4em 0.6em; font-size: 1.2em; cursor: pointer; transition: background 0.2s; }
117
+ .theme-btn:hover { background: var(--input-bg); }
118
+ .form-group { margin-bottom: 1rem; }
119
+ label { display: block; margin-bottom: 0.3em; font-size: 0.9em; opacity: 0.8; }
120
+ input { width: 100%; padding: 0.7em 1em; font-size: 1em; border: 1px solid var(--border); border-radius: 8px; background: var(--input-bg); color: var(--text); outline: none; transition: border-color 0.2s; }
121
+ input:focus { border-color: var(--accent); }
122
+ input::placeholder { color: var(--text); opacity: 0.4; }
123
+ .error { display: block; margin-top: 0.3em; font-size: 0.8em; color: var(--error); min-height: 1em; }
124
+ button[type="submit"] { width: 100%; padding: 0.8em; font-size: 1em; font-weight: 600; border: none; border-radius: 8px; background: var(--accent); color: #fff; cursor: pointer; transition: background 0.2s; margin-top: 0.5rem; }
125
+ button[type="submit"]:hover { background: var(--accent-hover); }
126
+ .toggle-text { text-align: center; margin-top: 1.5rem; font-size: 0.9em; opacity: 0.7; }
127
+ .toggle-text a { color: var(--accent); text-decoration: none; margin-left: 0.3em; }
128
+ .toggle-text a:hover { text-decoration: underline; }
129
+ `}</style>
130
+ </head>
131
+ <body>
132
+ <NextParticles id="tsparticles" options={options} />
133
+ <div className="auth-container">
134
+ <div className="auth-card">
135
+ <div className="auth-header">
136
+ <h1>{isLogin ? "Login" : "Register"}</h1>
137
+ <button className="theme-btn" onClick={toggleTheme} aria-label="Toggle theme">{theme === "dark" ? "🌙" : "☀️"}</button>
138
+ </div>
139
+ <form onSubmit={handleSubmit} noValidate>
140
+ <div className="form-group">
141
+ <label htmlFor="email">Email</label>
142
+ <input id="email" type="email" placeholder="you@example.com" value={email} onChange={(e) => setEmail(e.target.value)} required />
143
+ <span className="error">{emailErr}</span>
144
+ </div>
145
+ <div className="form-group">
146
+ <label htmlFor="password">Password</label>
147
+ <input id="password" type="password" placeholder="Enter password" value={password} onChange={(e) => setPassword(e.target.value)} required />
148
+ <span className="error">{passErr}</span>
149
+ </div>
150
+ {!isLogin && (
151
+ <div className="form-group">
152
+ <label htmlFor="confirmPassword">Confirm Password</label>
153
+ <input id="confirmPassword" type="password" placeholder="Confirm password" value={confirm} onChange={(e) => setConfirm(e.target.value)} />
154
+ <span className="error">{confirmErr}</span>
155
+ </div>
156
+ )}
157
+ <button type="submit">{isLogin ? "Sign In" : "Create Account"}</button>
158
+ </form>
159
+ <p className="toggle-text">
160
+ <span>{isLogin ? "Don't have an account?" : "Already have an account?"}</span>
161
+ <a href="#" onClick={toggleMode}>{isLogin ? "Register" : "Login"}</a>
162
+ </p>
163
+ </div>
164
+ </div>
165
+ </body>
166
+ </html>
167
+ );
168
+ }
@@ -0,0 +1,13 @@
1
+ "use client";
2
+
3
+ import { NextParticlesProvider } from "@tsparticles/nextjs";
4
+ import { loadSlim } from "@tsparticles/slim";
5
+ import type { Engine } from "@tsparticles/engine";
6
+
7
+ export default function Providers({ children }: { children: React.ReactNode }) {
8
+ return (
9
+ <NextParticlesProvider init={async (engine: Engine): Promise<void> => { await loadSlim(engine); }}>
10
+ {children}
11
+ </NextParticlesProvider>
12
+ );
13
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "scripts": {
6
+ "dev": "nuxt",
7
+ "build": "nuxt build",
8
+ "preview": "nuxt start"
9
+ },
10
+ "dependencies": {
11
+ "nuxt": "^2.18.1",
12
+ "@tsparticles/nuxt2": "^4.1.3",
13
+ "@tsparticles/slim": "^4.1.3",
14
+ "@tsparticles/configs": "^4.1.3",
15
+ "@tsparticles/engine": "^4.1.3"
16
+ }
17
+ }
@@ -0,0 +1,303 @@
1
+ <template>
2
+ <div>
3
+ <Particles id="tsparticles" :options="options" />
4
+ <div class="auth-container">
5
+ <div class="auth-card">
6
+ <div class="auth-header">
7
+ <h1>{{ isLogin ? 'Login' : 'Register' }}</h1>
8
+ <button class="theme-btn" @click="toggleTheme" aria-label="Toggle theme">{{ theme === 'dark' ? '🌙' : '☀️' }}</button>
9
+ </div>
10
+ <form @submit="handleSubmit" novalidate>
11
+ <div class="form-group">
12
+ <label for="email">Email</label>
13
+ <input id="email" v-model="email" type="email" placeholder="you@example.com" required />
14
+ <span class="error">{{ emailErr }}</span>
15
+ </div>
16
+ <div class="form-group">
17
+ <label for="password">Password</label>
18
+ <input id="password" v-model="password" type="password" placeholder="Enter password" required />
19
+ <span class="error">{{ passErr }}</span>
20
+ </div>
21
+ <div v-if="!isLogin" class="form-group">
22
+ <label for="confirmPassword">Confirm Password</label>
23
+ <input id="confirmPassword" v-model="confirm" type="password" placeholder="Confirm password" />
24
+ <span class="error">{{ confirmErr }}</span>
25
+ </div>
26
+ <button type="submit">{{ isLogin ? 'Sign In' : 'Create Account' }}</button>
27
+ </form>
28
+ <p class="toggle-text">
29
+ <span>{{ isLogin ? "Don't have an account?" : 'Already have an account?' }}</span>
30
+ <a href="#" @click="toggleMode">{{ isLogin ? 'Register' : 'Login' }}</a>
31
+ </p>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </template>
36
+
37
+ <script>
38
+ import { loadSlim } from "@tsparticles/slim";
39
+
40
+ function validateEmail(val) {
41
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val);
42
+ }
43
+
44
+ export default {
45
+ name: "HomePage",
46
+ data() {
47
+ return {
48
+ isLogin: true,
49
+ theme: (typeof localStorage !== "undefined" ? localStorage.getItem("theme") : null) || "dark",
50
+ email: "",
51
+ password: "",
52
+ confirm: "",
53
+ emailErr: "",
54
+ passErr: "",
55
+ confirmErr: "",
56
+ options: {
57
+ background: { color: { value: "transparent" } },
58
+ fpsLimit: 60,
59
+ particles: {
60
+ number: { value: 60, density: { enable: true } },
61
+ color: { value: ["#6c5ce7", "#a29bfe", "#fd79a8", "#00cec9"] },
62
+ shape: { type: "circle" },
63
+ opacity: { value: 0.5, random: true },
64
+ size: { value: { min: 1, max: 3 }, random: true },
65
+ move: { enable: true, speed: 1, direction: "none", random: true, straight: false, outModes: { default: "out" } },
66
+ },
67
+ interactivity: {
68
+ events: { onHover: { enable: true, mode: "bubble" } },
69
+ modes: { bubble: { distance: 200, size: 6, duration: 2, opacity: 0.8 } },
70
+ },
71
+ detectRetina: true,
72
+ },
73
+ };
74
+ },
75
+ mounted() {
76
+ document.documentElement.setAttribute("data-theme", this.theme);
77
+ },
78
+ methods: {
79
+ toggleTheme() {
80
+ this.theme = this.theme === "dark" ? "light" : "dark";
81
+ document.documentElement.setAttribute("data-theme", this.theme);
82
+ localStorage.setItem("theme", this.theme);
83
+ },
84
+ handleSubmit(e) {
85
+ e.preventDefault();
86
+ let valid = true;
87
+
88
+ if (!this.email || !validateEmail(this.email)) {
89
+ this.emailErr = "Please enter a valid email address";
90
+ valid = false;
91
+ } else {
92
+ this.emailErr = "";
93
+ }
94
+
95
+ if (!this.password || this.password.length < 6) {
96
+ this.passErr = "Password must be at least 6 characters";
97
+ valid = false;
98
+ } else {
99
+ this.passErr = "";
100
+ }
101
+
102
+ if (!this.isLogin) {
103
+ if (this.password !== this.confirm) {
104
+ this.confirmErr = "Passwords do not match";
105
+ valid = false;
106
+ } else {
107
+ this.confirmErr = "";
108
+ }
109
+ }
110
+
111
+ if (!valid) return;
112
+
113
+ if (this.isLogin) {
114
+ alert("Logged in successfully! (demo)");
115
+ } else {
116
+ alert("Account created successfully! (demo)");
117
+ this.isLogin = true;
118
+ }
119
+
120
+ this.email = "";
121
+ this.password = "";
122
+ this.confirm = "";
123
+ },
124
+ toggleMode(e) {
125
+ e.preventDefault();
126
+ this.isLogin = !this.isLogin;
127
+ this.emailErr = "";
128
+ this.passErr = "";
129
+ this.confirmErr = "";
130
+ },
131
+ },
132
+ head() {
133
+ return {
134
+ script: [
135
+ {
136
+ innerHTML: `
137
+ (async function() {
138
+ const { loadSlim } = await import("@tsparticles/slim");
139
+ const { tsParticles } = await import("@tsparticles/engine");
140
+ await loadSlim(tsParticles);
141
+ })();
142
+ `,
143
+ },
144
+ ],
145
+ __dangerouslyDisableSanitizers: ["script"],
146
+ };
147
+ },
148
+ };
149
+ </script>
150
+
151
+ <style>
152
+ :root {
153
+ --bg: #0f0f1a;
154
+ --card-bg: rgba(255, 255, 255, 0.05);
155
+ --text: #fff;
156
+ --border: rgba(255, 255, 255, 0.1);
157
+ --input-bg: rgba(255, 255, 255, 0.08);
158
+ --accent: #6c5ce7;
159
+ --accent-hover: #a29bfe;
160
+ --error: #e74c3c;
161
+ }
162
+
163
+ [data-theme="light"] {
164
+ --bg: #f0f0f5;
165
+ --card-bg: rgba(255, 255, 255, 0.8);
166
+ --text: #1a1a2e;
167
+ --border: rgba(0, 0, 0, 0.1);
168
+ --input-bg: rgba(0, 0, 0, 0.05);
169
+ }
170
+
171
+ * {
172
+ box-sizing: border-box;
173
+ }
174
+
175
+ body {
176
+ margin: 0;
177
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
178
+ background: var(--bg);
179
+ color: var(--text);
180
+ transition: background 0.3s, color 0.3s;
181
+ }
182
+
183
+ .auth-container {
184
+ position: absolute;
185
+ top: 50%;
186
+ left: 50%;
187
+ transform: translate(-50%, -50%);
188
+ z-index: 10;
189
+ width: 100%;
190
+ max-width: 400px;
191
+ padding: 1rem;
192
+ }
193
+
194
+ .auth-card {
195
+ background: var(--card-bg);
196
+ backdrop-filter: blur(10px);
197
+ border: 1px solid var(--border);
198
+ border-radius: 16px;
199
+ padding: 2rem;
200
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
201
+ }
202
+
203
+ .auth-header {
204
+ display: flex;
205
+ justify-content: space-between;
206
+ align-items: center;
207
+ margin-bottom: 1.5rem;
208
+ }
209
+
210
+ .auth-header h1 {
211
+ margin: 0;
212
+ font-size: 1.8em;
213
+ }
214
+
215
+ .theme-btn {
216
+ background: none;
217
+ border: 1px solid var(--border);
218
+ border-radius: 8px;
219
+ padding: 0.4em 0.6em;
220
+ font-size: 1.2em;
221
+ cursor: pointer;
222
+ transition: background 0.2s;
223
+ }
224
+
225
+ .theme-btn:hover {
226
+ background: var(--input-bg);
227
+ }
228
+
229
+ .form-group {
230
+ margin-bottom: 1rem;
231
+ }
232
+
233
+ label {
234
+ display: block;
235
+ margin-bottom: 0.3em;
236
+ font-size: 0.9em;
237
+ opacity: 0.8;
238
+ }
239
+
240
+ input {
241
+ width: 100%;
242
+ padding: 0.7em 1em;
243
+ font-size: 1em;
244
+ border: 1px solid var(--border);
245
+ border-radius: 8px;
246
+ background: var(--input-bg);
247
+ color: var(--text);
248
+ outline: none;
249
+ transition: border-color 0.2s;
250
+ }
251
+
252
+ input:focus {
253
+ border-color: var(--accent);
254
+ }
255
+
256
+ input::placeholder {
257
+ color: var(--text);
258
+ opacity: 0.4;
259
+ }
260
+
261
+ .error {
262
+ display: block;
263
+ margin-top: 0.3em;
264
+ font-size: 0.8em;
265
+ color: var(--error);
266
+ min-height: 1em;
267
+ }
268
+
269
+ button[type="submit"] {
270
+ width: 100%;
271
+ padding: 0.8em;
272
+ font-size: 1em;
273
+ font-weight: 600;
274
+ border: none;
275
+ border-radius: 8px;
276
+ background: var(--accent);
277
+ color: #fff;
278
+ cursor: pointer;
279
+ transition: background 0.2s;
280
+ margin-top: 0.5rem;
281
+ }
282
+
283
+ button[type="submit"]:hover {
284
+ background: var(--accent-hover);
285
+ }
286
+
287
+ .toggle-text {
288
+ text-align: center;
289
+ margin-top: 1.5rem;
290
+ font-size: 0.9em;
291
+ opacity: 0.7;
292
+ }
293
+
294
+ .toggle-text a {
295
+ color: var(--accent);
296
+ text-decoration: none;
297
+ margin-left: 0.3em;
298
+ }
299
+
300
+ .toggle-text a:hover {
301
+ text-decoration: underline;
302
+ }
303
+ </style>