falak-app-duo 1.1.2 → 1.1.3

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 (34) hide show
  1. package/bin/falak-app-duo +118 -54
  2. package/package.json +2 -5
  3. package/templates/default/src/App.vue +86 -24
  4. package/templates/default/src/assets/main.css +142 -3
  5. package/templates/default/src/components/AppBackdrop.vue +43 -0
  6. package/templates/default/src/components/AuthLayout.vue +115 -0
  7. package/templates/default/src/components/GlassCard.vue +36 -0
  8. package/templates/default/src/components/SocialIcon.vue +46 -0
  9. package/templates/default/src/components/ToggleSwitch.vue +31 -0
  10. package/templates/default/src/components/settings/AIAgentDialog.vue +213 -0
  11. package/templates/default/src/components/settings/NotificationsDialog.vue +122 -0
  12. package/templates/default/src/components/settings/SecurityDialog.vue +231 -0
  13. package/templates/default/src/components/settings/SettingsDialog.vue +115 -0
  14. package/templates/default/src/components/settings/TelegramDialog.vue +156 -0
  15. package/templates/default/src/components/settings/TwoFactorDialog.vue +192 -0
  16. package/templates/default/src/composables/useRipple.js +25 -0
  17. package/templates/default/src/data/locations.js +59 -0
  18. package/templates/default/src/i18n/index.js +19 -3
  19. package/templates/default/src/i18n/locales/ar.json +328 -5
  20. package/templates/default/src/i18n/locales/de.json +341 -0
  21. package/templates/default/src/i18n/locales/en.json +328 -5
  22. package/templates/default/src/i18n/locales/es.json +341 -0
  23. package/templates/default/src/i18n/locales/fr.json +341 -0
  24. package/templates/default/src/i18n/locales/it.json +341 -0
  25. package/templates/default/src/i18n/locales/ru.json +341 -0
  26. package/templates/default/src/router/index.js +25 -1
  27. package/templates/default/src/stores/auth.js +79 -3
  28. package/templates/default/src/views/ForgotPassword.vue +111 -0
  29. package/templates/default/src/views/Home.vue +182 -6
  30. package/templates/default/src/views/Login.vue +206 -24
  31. package/templates/default/src/views/Profile.vue +323 -0
  32. package/templates/default/src/views/Register.vue +235 -0
  33. package/templates/default/src/views/SettingsView.vue +200 -0
  34. package/templates/default/tailwind.config.js +75 -1
package/bin/falak-app-duo CHANGED
@@ -13,13 +13,76 @@ import gradient from "gradient-string";
13
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
14
  const TEMPLATE_DIR = path.join(__dirname, "..", "templates", "default");
15
15
 
16
+ const LANGUAGE_CHOICES = [
17
+ { title: "العربية (Arabic) \u2014 the RTL default", value: "ar", selected: true },
18
+ { title: "Español (Spanish)", value: "es" },
19
+ { title: "Deutsch (German)", value: "de" },
20
+ { title: "Français (French)", value: "fr" },
21
+ { title: "Русский (Russian)", value: "ru" },
22
+ { title: "Italiano (Italian)", value: "it" },
23
+ ];
24
+
25
+ const LOCALE_LABELS = {
26
+ en: "English",
27
+ ar: "العربية",
28
+ es: "Español",
29
+ de: "Deutsch",
30
+ fr: "Français",
31
+ ru: "Русский",
32
+ it: "Italiano",
33
+ };
34
+
35
+ const EXTRA_LOCALE_FILES = ["ar.json", "de.json", "es.json", "fr.json", "it.json", "ru.json"];
36
+
37
+ function buildI18nModule(locales, defaultLocale) {
38
+ const ordered = ["ar", "en", "es", "de", "fr", "ru", "it"].filter((c) => locales.includes(c));
39
+ const lines = [];
40
+ lines.push('import { createI18n } from "vue-i18n";');
41
+ for (const code of ordered) {
42
+ lines.push(`import ${code} from "./locales/${code}.json";`);
43
+ }
44
+ lines.push("");
45
+ lines.push("export const SUPPORTED_LOCALES = [");
46
+ for (const code of ordered) {
47
+ lines.push(` { code: "${code}", label: "${LOCALE_LABELS[code]}" },`);
48
+ }
49
+ lines.push("];");
50
+ lines.push("");
51
+ lines.push('const STORAGE_KEY = "falak_locale";');
52
+ lines.push("const stored = localStorage.getItem(STORAGE_KEY);");
53
+ lines.push(
54
+ `const savedLocale = SUPPORTED_LOCALES.some((l) => l.code === stored) ? stored : "${defaultLocale}";`
55
+ );
56
+ lines.push("");
57
+ lines.push("const i18n = createI18n({");
58
+ lines.push(" legacy: false,");
59
+ lines.push(" locale: savedLocale,");
60
+ lines.push(' fallbackLocale: "en",');
61
+ lines.push(` messages: { ${ordered.join(", ")} },`);
62
+ lines.push("});");
63
+ lines.push("");
64
+ lines.push("export function setLocale(locale) {");
65
+ lines.push(" i18n.global.locale.value = locale;");
66
+ lines.push(" localStorage.setItem(STORAGE_KEY, locale);");
67
+ lines.push(" document.documentElement.lang = locale;");
68
+ lines.push(' document.documentElement.dir = locale === "ar" ? "rtl" : "ltr";');
69
+ lines.push("}");
70
+ lines.push("");
71
+ lines.push("// Apply on load");
72
+ lines.push("setLocale(savedLocale);");
73
+ lines.push("");
74
+ lines.push("export default i18n;");
75
+ lines.push("");
76
+ return lines.join("\n");
77
+ }
78
+
16
79
  const program = new Command();
17
80
  program
18
81
  .name("falak-app")
19
82
  .description("Scaffold a Vue 3 app with Ably, Brevo, AES-256 and a Laravel API client baked in.")
20
83
  .argument("[project-name]", "Directory to create the project in")
21
84
  .option("-y, --yes", "Skip prompts and use defaults (all services enabled)")
22
- .option("--no-rtl", "Disable Arabic/RTL default (English/LTR) instead")
85
+ .option("--no-rtl", "Exclude Arabic/RTL (English or your chosen LTR languages only)")
23
86
  .parse(process.argv);
24
87
 
25
88
  const [argProjectName] = program.args;
@@ -48,7 +111,7 @@ async function printBanner() {
48
111
  const art = (await figlet("Falak Duo", { font: "ANSI Shadow" }).catch(() => null)) || "Falak Duo";
49
112
  console.log("\n" + gradient("#a78bfa", "#22d3ee", "#2dd4bf")(art));
50
113
  console.log(
51
- chalk.gray(" Vue 3 boilerplate \u2014 Ably \u00b7 Brevo \u00b7 AES-256 \u00b7 Arabic/RTL")
114
+ chalk.gray(" Vue 3 boilerplate \u2014 Ably \u00b7 Brevo \u00b7 AES-256 \u00b7 Multilingual / RTL")
52
115
  );
53
116
  console.log("\n");
54
117
  }
@@ -60,8 +123,8 @@ async function main() {
60
123
  ? {
61
124
  projectName: argProjectName || "falak-app-duo",
62
125
  services: ["ably", "brevo"],
126
+ languages: ["ar"],
63
127
  apiBaseUrl: "http://localhost:8000/api",
64
- rtl: opts.rtl,
65
128
  }
66
129
  : await prompts(
67
130
  [
@@ -81,27 +144,34 @@ async function main() {
81
144
  ],
82
145
  hint: "Space to toggle, Enter to confirm",
83
146
  },
147
+ {
148
+ type: "multiselect",
149
+ name: "languages",
150
+ message: "Which languages should the app ship with? (English is always included)",
151
+ choices: LANGUAGE_CHOICES.map((c) =>
152
+ c.value === "ar" && !opts.rtl ? { ...c, selected: false } : c
153
+ ),
154
+ hint: "Space to toggle, Enter to confirm",
155
+ },
84
156
  {
85
157
  type: "text",
86
158
  name: "apiBaseUrl",
87
159
  message: "Laravel API base URL:",
88
160
  initial: "http://localhost:8000/api",
89
161
  },
90
- ...(opts.rtl
91
- ? [
92
- {
93
- type: "confirm",
94
- name: "rtl",
95
- message: "Enable Arabic / RTL by default?",
96
- initial: true,
97
- },
98
- ]
99
- : []),
100
162
  ],
101
163
  { onCancel: () => process.exit(1) }
102
164
  );
103
165
 
104
- const rtlEnabled = opts.yes ? answers.rtl : opts.rtl ? answers.rtl : false;
166
+ // The set of locales the app ships: English is always bundled, plus the
167
+ // languages the user picked. `--no-rtl` forces Arabic out (LTR only).
168
+ let locales = ["en", ...(answers.languages || [])];
169
+ locales = [...new Set(locales)];
170
+ if (!opts.rtl) {
171
+ locales = locales.filter((l) => l !== "ar");
172
+ }
173
+ const hasRtl = locales.includes("ar");
174
+ const defaultLocale = hasRtl ? "ar" : "en";
105
175
 
106
176
  const projectName = argProjectName || answers.projectName;
107
177
  const services = answers.services || [];
@@ -138,49 +208,39 @@ async function main() {
138
208
  }
139
209
  }
140
210
 
141
- // RTL off: strip Arabic/RTL out of the copied app — English/LTR only.
142
- // The template is RTL by default, so this must also remove the locale
143
- // toggle and the Arabic bundle or the app would still "support RTL".
144
- if (!rtlEnabled) {
145
- const htmlPath = path.join(targetDir, "index.html");
146
- const html = await fs.readFile(htmlPath, "utf-8");
147
- await fs.writeFile(htmlPath, html.replace('<html lang="ar" dir="rtl">', '<html lang="en" dir="ltr">'));
148
-
149
- const i18nPath = path.join(targetDir, "src", "i18n", "index.js");
150
- await fs.writeFile(
151
- i18nPath,
152
- [
153
- 'import { createI18n } from "vue-i18n";',
154
- 'import en from "./locales/en.json";',
155
- "",
156
- "export default createI18n({",
157
- ' legacy: false,',
158
- ' locale: "en",',
159
- ' fallbackLocale: "en",',
160
- " messages: { en },",
161
- "});",
162
- "",
163
- ].join("\n")
164
- );
165
- await fs.remove(path.join(targetDir, "src", "i18n", "locales", "ar.json"));
211
+ // Wire i18n to exactly the selected languages: rewrite index.html lang/dir,
212
+ // regenerate src/i18n/index.js, and drop unselected locale bundles. The
213
+ // template ships Arabic+RTL by default; --no-rtl (or deselecting Arabic)
214
+ // yields an LTR-only app.
215
+ const htmlPath = path.join(targetDir, "index.html");
216
+ const html = await fs.readFile(htmlPath, "utf-8");
217
+ const htmlTag = hasRtl ? '<html lang="ar" dir="rtl">' : '<html lang="en" dir="ltr">';
218
+ await fs.writeFile(htmlPath, html.replace('<html lang="ar" dir="rtl">', htmlTag));
219
+
220
+ const i18nPath = path.join(targetDir, "src", "i18n", "index.js");
221
+ await fs.writeFile(i18nPath, buildI18nModule(locales, defaultLocale));
222
+
223
+ for (const file of EXTRA_LOCALE_FILES) {
224
+ if (!locales.includes(file.replace(".json", ""))) {
225
+ await fs.remove(path.join(targetDir, "src", "i18n", "locales", file));
226
+ }
227
+ }
166
228
 
229
+ // English-only app: strip the language switcher (and its setup) from App.vue.
230
+ if (locales.length === 1) {
167
231
  const appPath = path.join(targetDir, "src", "App.vue");
168
232
  let app = await fs.readFile(appPath, "utf-8");
169
- app = app.replace('import { setLocale } from "@/i18n";\n', "");
233
+ app = app.replace('import { setLocale, SUPPORTED_LOCALES } from "@/i18n";\n', "");
170
234
  app = app.replace("const { t, locale } = useI18n();", "const { t } = useI18n();");
171
- app = app.replace(
172
- 'function toggleLocale() {\n setLocale(locale.value === "ar" ? "en" : "ar");\n}\n',
173
- ""
174
- );
175
- app = app.replace(
176
- /\n <button @click="toggleLocale" class="text-sm px-2 py-1 border rounded">\n \{\{ locale === "ar" \? "EN" : "AR" \}\}\n <\/button>/,
177
- ""
178
- );
235
+ app = app.replace("const locales = SUPPORTED_LOCALES;\n", "");
236
+ app = app.replace(/\n\s*<select\s*v-if="locales\.length > 1"[\s\S]*?<\/select>/, "");
179
237
  await fs.writeFile(appPath, app);
180
-
181
- console.log(chalk.gray(" RTL disabled \u2014 English/LTR only, no Arabic or AR/EN toggle."));
182
238
  }
183
239
 
240
+ console.log(
241
+ chalk.gray(` Languages wired: ${locales.map((l) => LOCALE_LABELS[l]).join(", ")}${hasRtl ? " (RTL default)" : ""}`)
242
+ );
243
+
184
244
  // Generate a real .env from .env.example with a fresh encryption key
185
245
  const envExamplePath = path.join(targetDir, ".env.example");
186
246
  let envContent = await fs.readFile(envExamplePath, "utf-8");
@@ -235,7 +295,11 @@ async function main() {
235
295
  await runInProject(targetDir, "npm", ["run", "dev"]).catch(() => {});
236
296
  }
237
297
 
238
- main().catch((err) => {
239
- console.error(chalk.red(err));
240
- process.exit(1);
241
- });
298
+ if (process.argv[1] && path.resolve(fileURLToPath(import.meta.url)) === path.resolve(process.argv[1])) {
299
+ main().catch((err) => {
300
+ console.error(chalk.red(err));
301
+ process.exit(1);
302
+ });
303
+ }
304
+
305
+ export { buildI18nModule };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "falak-app-duo",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "Vue 3 boilerplate scaffolder — Ably (realtime), Brevo, AES-256, Arabic/RTL, and a Laravel API client (API key + user token auth). No Firebase, no Groq/Paymob on the frontend (Groq is backend-only; Paymob dropped).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,8 +32,5 @@
32
32
  "ably",
33
33
  "realtime"
34
34
  ],
35
- "license": "MIT",
36
- "publishConfig": {
37
- "access": "public"
38
- }
35
+ "license": "MIT"
39
36
  }
@@ -1,36 +1,98 @@
1
1
  <script setup>
2
+ import { computed } from "vue";
2
3
  import { useI18n } from "vue-i18n";
3
- import { setLocale } from "@/i18n";
4
+ import { useRoute, useRouter, RouterLink, RouterView } from "vue-router";
5
+ import { setLocale, SUPPORTED_LOCALES } from "@/i18n";
4
6
  import { useAuthStore } from "@/stores/auth";
5
- import { RouterLink, RouterView } from "vue-router";
7
+ import AppBackdrop from "@/components/AppBackdrop.vue";
6
8
 
7
9
  const { t, locale } = useI18n();
10
+ const locales = SUPPORTED_LOCALES;
8
11
  const auth = useAuthStore();
12
+ const route = useRoute();
13
+ const router = useRouter();
9
14
 
10
- function toggleLocale() {
11
- setLocale(locale.value === "ar" ? "en" : "ar");
15
+ // Auth pages hide the app chrome and go full-screen.
16
+ const authPageNames = ["login", "register", "forgot-password"];
17
+ const isAuthPage = computed(() => authPageNames.includes(route.name));
18
+
19
+ const links = computed(() => [
20
+ { to: "/profile", label: t("nav.profile") },
21
+ { to: "/settings", label: t("nav.settings") },
22
+ ]);
23
+
24
+ async function logout() {
25
+ await auth.logout();
26
+ router.push({ name: "home" });
12
27
  }
13
28
  </script>
14
29
 
15
30
  <template>
16
- <div class="min-h-screen bg-gray-50 text-gray-900">
17
- <nav class="flex items-center justify-between px-6 py-4 bg-white shadow-sm">
18
- <span class="font-bold text-lg">{{ t("app.name") }}</span>
19
- <div class="flex items-center gap-4">
20
- <RouterLink to="/" class="hover:underline">{{ t("nav.home") }}</RouterLink>
21
- <RouterLink v-if="!auth.userToken" to="/login" class="hover:underline">{{
22
- t("nav.login")
23
- }}</RouterLink>
24
- <button v-else @click="auth.logout()" class="text-sm text-red-600 hover:underline">
25
- {{ t("nav.login") === "Login" ? "Logout" : "خروج" }}
26
- </button>
27
- <button @click="toggleLocale" class="text-sm px-2 py-1 border rounded">
28
- {{ locale === "ar" ? "EN" : "AR" }}
29
- </button>
30
- </div>
31
- </nav>
32
- <main class="p-6">
33
- <RouterView />
34
- </main>
31
+ <div class="relative min-h-screen">
32
+ <AppBackdrop />
33
+
34
+ <div class="relative z-10 flex min-h-screen flex-col">
35
+ <header
36
+ v-if="!isAuthPage"
37
+ class="sticky top-0 z-30 border-b border-white/10 bg-[#0b0f19]/70 backdrop-blur-xl"
38
+ >
39
+ <nav class="mx-auto flex w-full max-w-6xl flex-wrap items-center justify-between gap-x-6 gap-y-3 px-4 py-3 sm:px-6">
40
+ <RouterLink to="/" class="group flex items-center gap-2.5">
41
+ <span
42
+ class="grid h-9 w-9 place-items-center rounded-xl bg-gradient-to-br from-emerald-400 to-teal-500 text-base font-black text-emerald-950 shadow-lg shadow-emerald-500/20 transition group-hover:shadow-emerald-400/40"
43
+ >
44
+ F
45
+ </span>
46
+ <span class="text-lg font-bold text-white">{{ t("app.name") }}</span>
47
+ </RouterLink>
48
+
49
+ <div class="flex flex-1 flex-wrap items-center justify-end gap-1.5 text-sm">
50
+ <RouterLink
51
+ to="/"
52
+ class="rounded-full px-3 py-1.5 font-medium transition"
53
+ :class="route.name === 'home' ? 'bg-emerald-500/15 text-emerald-200 ring-1 ring-emerald-300/30' : 'text-gray-300 hover:bg-white/5 hover:text-white'"
54
+ >
55
+ {{ t("nav.home") }}
56
+ </RouterLink>
57
+ <template v-if="auth.userToken">
58
+ <RouterLink
59
+ v-for="link in links"
60
+ :key="link.to"
61
+ :to="link.to"
62
+ class="rounded-full px-3 py-1.5 font-medium transition"
63
+ :class="route.path === link.to ? 'bg-emerald-500/15 text-emerald-200 ring-1 ring-emerald-300/30' : 'text-gray-300 hover:bg-white/5 hover:text-white'"
64
+ >
65
+ {{ link.label }}
66
+ </RouterLink>
67
+ </template>
68
+ </div>
69
+
70
+ <div class="flex items-center gap-2.5">
71
+ <RouterLink
72
+ v-if="!auth.userToken"
73
+ to="/login"
74
+ class="btn-primary !px-4 !py-2"
75
+ >
76
+ {{ t("nav.login") }}
77
+ </RouterLink>
78
+ <button v-else @click="logout" class="btn-ghost !px-4 !py-2 !text-rose-300 hover:!bg-rose-500/10 hover:!text-rose-200">
79
+ {{ t("nav.logout") }}
80
+ </button>
81
+ <select
82
+ v-if="locales.length > 1"
83
+ :value="locale"
84
+ @change="setLocale($event.target.value)"
85
+ class="rounded-full border border-white/15 bg-white/5 px-2.5 py-1.5 text-sm font-medium text-gray-300 outline-none transition hover:bg-white/10 hover:text-white"
86
+ >
87
+ <option v-for="l in locales" :key="l.code" :value="l.code">{{ l.label }}</option>
88
+ </select>
89
+ </div>
90
+ </nav>
91
+ </header>
92
+
93
+ <main class="flex-1">
94
+ <RouterView />
95
+ </main>
96
+ </div>
35
97
  </div>
36
- </template>
98
+ </template>
@@ -2,7 +2,146 @@
2
2
  @tailwind components;
3
3
  @tailwind utilities;
4
4
 
5
- html,
6
- body {
7
- font-family: "Almarai", "IBM Plex Sans Arabic", sans-serif;
5
+ /* Registered once for the animated gradient-border effect (Tailwind v3 has no
6
+ @property in config, so it lives here in plain CSS next to the keyframes). */
7
+ @property --tw-border-angle {
8
+ syntax: "<angle>";
9
+ initial-value: 0deg;
10
+ inherits: false;
8
11
  }
12
+
13
+ @keyframes border-spin {
14
+ to {
15
+ --tw-border-angle: 360deg;
16
+ }
17
+ }
18
+
19
+ @keyframes ripple-expand {
20
+ to {
21
+ transform: translate(-50%, -50%) scale(20);
22
+ opacity: 0;
23
+ }
24
+ }
25
+
26
+ @layer base {
27
+ html,
28
+ body {
29
+ font-family: "Almarai", "IBM Plex Sans Arabic", sans-serif;
30
+ background-color: #0b0f19;
31
+ }
32
+ body {
33
+ color: rgb(243 244 246 / 1);
34
+ }
35
+ ::selection {
36
+ background: rgb(16 185 129 / 0.35);
37
+ color: #fff;
38
+ }
39
+ * {
40
+ scrollbar-width: thin;
41
+ scrollbar-color: rgb(255 255 255 / 0.14) transparent;
42
+ }
43
+ *::-webkit-scrollbar {
44
+ width: 8px;
45
+ height: 8px;
46
+ }
47
+ *::-webkit-scrollbar-thumb {
48
+ border-radius: 9999px;
49
+ background: rgb(255 255 255 / 0.14);
50
+ }
51
+ }
52
+
53
+ @layer components {
54
+ /* Reusable glass surfaces — the app's core visual language */
55
+ .glass-card {
56
+ @apply relative rounded-2xl border border-white/10 bg-white/[0.05] shadow-2xl shadow-black/30 backdrop-blur-xl backdrop-saturate-150;
57
+ }
58
+ .glass-card-hover {
59
+ @apply transition-all duration-300 ease-out hover:-translate-y-1 hover:border-white/20 hover:bg-white/[0.08] hover:shadow-emerald-500/10;
60
+ }
61
+
62
+ /* Fine hairline top highlight so glass reads as "lit" from above */
63
+ .glass-highlight {
64
+ @apply relative overflow-hidden;
65
+ }
66
+ .glass-highlight::before {
67
+ content: "";
68
+ position: absolute;
69
+ inset: 0;
70
+ border-radius: inherit;
71
+ background: linear-gradient(
72
+ 180deg,
73
+ rgb(255 255 255 / 0.08) 0%,
74
+ rgb(255 255 255 / 0) 32%
75
+ );
76
+ pointer-events: none;
77
+ }
78
+
79
+ .glass-input {
80
+ @apply w-full rounded-xl border border-white/10 bg-white/[0.05] px-4 py-2.5 text-sm text-gray-100 placeholder-gray-500 outline-none ring-1 ring-transparent transition focus:border-emerald-400/50 focus:bg-white/[0.08] focus:ring-2 focus:ring-emerald-400/20;
81
+ }
82
+ .glass-select {
83
+ @apply glass-input appearance-none pe-10;
84
+ background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Cpath d='M5 8l5 5 5-5' stroke='%239ca3af' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
85
+ background-repeat: no-repeat;
86
+ background-position: right 0.9rem center;
87
+ background-size: 1.1rem;
88
+ }
89
+
90
+ .btn-primary {
91
+ @apply inline-flex items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-emerald-500 via-teal-500 to-cyan-500 bg-[length:200%_200%] px-4 py-2.5 text-sm font-semibold text-white shadow-lg shadow-emerald-500/25 ring-1 ring-white/10 transition-all duration-200 hover:brightness-110 hover:shadow-emerald-400/40 hover:ring-white/20 animate-gradient-pan motion-reduce:animate-none disabled:cursor-not-allowed disabled:opacity-50 active:translate-y-px;
92
+ }
93
+
94
+ .btn-ghost {
95
+ @apply inline-flex items-center justify-center gap-2 rounded-xl border border-white/15 bg-white/5 px-4 py-2.5 text-sm font-semibold text-gray-200 shadow-sm backdrop-blur transition-all duration-200 hover:bg-white/10 hover:text-white disabled:cursor-not-allowed disabled:opacity-50;
96
+ }
97
+
98
+ /* Bright brand accent button — CTA on auth/settings pages */
99
+ .btn-accent {
100
+ @apply inline-flex items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-amber-400 via-orange-400 to-rose-400 bg-[length:200%_200%] px-4 py-2.5 text-sm font-semibold text-amber-950 shadow-lg shadow-amber-500/25 transition-all duration-200 hover:brightness-110 hover:shadow-amber-400/40 animate-gradient-pan motion-reduce:animate-none disabled:cursor-not-allowed disabled:opacity-50 active:translate-y-px;
101
+ }
102
+
103
+ /* Ripple-ready fill button (JS spawns the span, classes live here) */
104
+ .btn-ripple {
105
+ @apply relative isolate overflow-hidden;
106
+ }
107
+ .ripple {
108
+ @apply pointer-events-none absolute rounded-full bg-white/40;
109
+ transform: translate(-50%, -50%) scale(0);
110
+ animation: ripple-expand 0.6s ease-out forwards;
111
+ }
112
+
113
+ .field-label {
114
+ @apply mb-1.5 block text-sm font-medium text-gray-300;
115
+ }
116
+ .field-hint {
117
+ @apply mt-1 block text-xs text-gray-500;
118
+ }
119
+
120
+ /* Two-stop gradient headline text (2 stops only — 3+ reads muddy at text size) */
121
+ .text-gradient {
122
+ @apply bg-gradient-to-r from-emerald-300 via-teal-200 to-cyan-300 bg-clip-text text-transparent;
123
+ }
124
+ .text-gradient-warm {
125
+ @apply bg-gradient-to-r from-amber-200 via-orange-300 to-rose-300 bg-clip-text text-transparent;
126
+ }
127
+
128
+ /* Small pill / chip */
129
+ .chip {
130
+ @apply inline-flex items-center gap-1.5 rounded-full border border-white/10 bg-white/5 px-3 py-1 text-xs font-medium text-gray-300;
131
+ }
132
+
133
+ /* Section heading with gradient marker bar */
134
+ .section-head {
135
+ @apply flex items-center gap-3;
136
+ }
137
+ .section-head::before {
138
+ content: "";
139
+ @apply h-5 w-1 rounded-full bg-gradient-to-b from-emerald-400 to-cyan-400;
140
+ }
141
+
142
+ /* Fine dotted grid used by the backdrop and hero */
143
+ .bg-grid {
144
+ background-image: radial-gradient(rgb(255 255 255 / 0.06) 1px, transparent 1px);
145
+ background-size: 26px 26px;
146
+ }
147
+ }
@@ -0,0 +1,43 @@
1
+ <script setup>
2
+ // Full-screen dynamic background: dark base + animated aurora mesh of blurred
3
+ // blobs + a fine dotted grid + film grain. Palette deliberately avoids
4
+ // purple/blue gradients (emerald / teal / cyan / rose / amber).
5
+ defineOptions({ name: "AppBackdrop" });
6
+ </script>
7
+
8
+ <template>
9
+ <div
10
+ aria-hidden="true"
11
+ class="pointer-events-none fixed inset-0 -z-10 overflow-hidden bg-[#0b0f19]"
12
+ >
13
+ <!-- Aurora mesh base -->
14
+ <div
15
+ class="absolute inset-0 bg-[radial-gradient(circle_at_16%_10%,rgba(16,185,129,0.18),transparent_42%),radial-gradient(circle_at_84%_16%,rgba(251,113,133,0.13),transparent_40%),radial-gradient(circle_at_48%_88%,rgba(251,191,36,0.11),transparent_45%),radial-gradient(circle_at_76%_60%,rgba(45,212,191,0.14),transparent_42%),radial-gradient(circle_at_6%_70%,rgba(34,211,238,0.08),transparent_45%)]"
16
+ ></div>
17
+
18
+ <!-- Dotted grid, faint -->
19
+ <div class="absolute inset-0 bg-grid opacity-70 [mask-image:radial-gradient(ellipse_at_center,black_30%,transparent_75%)]"></div>
20
+
21
+ <!-- Morphing blobs -->
22
+ <div
23
+ class="absolute -start-32 -top-32 h-[34rem] w-[34rem] rounded-full bg-emerald-500/25 blur-[130px] animate-mesh-drift-a motion-reduce:animate-none"
24
+ ></div>
25
+ <div
26
+ class="absolute -end-24 top-1/3 h-[28rem] w-[28rem] rounded-full bg-rose-500/20 blur-[120px] animate-mesh-drift-b motion-reduce:animate-none"
27
+ ></div>
28
+ <div
29
+ class="absolute -bottom-40 start-1/4 h-[36rem] w-[36rem] rounded-full bg-amber-400/15 blur-[140px] animate-mesh-drift-a motion-reduce:animate-none"
30
+ style="animation-delay: -7s"
31
+ ></div>
32
+ <div
33
+ class="absolute start-[42%] top-12 h-[22rem] w-[22rem] rounded-full bg-teal-400/15 blur-[110px] animate-blob-morph motion-reduce:animate-none"
34
+ ></div>
35
+ <div
36
+ class="absolute start-[10%] top-1/2 hidden h-[18rem] w-[18rem] rounded-full bg-cyan-400/10 blur-[100px] animate-blob-morph motion-reduce:animate-none lg:block"
37
+ style="animation-delay: -4s"
38
+ ></div>
39
+
40
+ <!-- Vignette so content keeps contrast -->
41
+ <div class="absolute inset-0 bg-[radial-gradient(ellipse_at_center,transparent_55%,rgba(5,8,15,0.55))]"></div>
42
+ </div>
43
+ </template>