falak-app-duo 1.2.0 → 1.3.1

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 (39) hide show
  1. package/README.md +2 -2
  2. package/bin/falak-app-duo +12 -8
  3. package/package.json +1 -1
  4. package/templates/default/index.html +20 -0
  5. package/templates/default/package.json +3 -0
  6. package/templates/default/src/App.vue +23 -6
  7. package/templates/default/src/assets/main.css +92 -19
  8. package/templates/default/src/components/AppBackdrop.vue +2 -2
  9. package/templates/default/src/components/AuthLayout.vue +11 -11
  10. package/templates/default/src/components/ToggleSwitch.vue +1 -1
  11. package/templates/default/src/components/settings/AIAgentDialog.vue +19 -30
  12. package/templates/default/src/components/settings/NotificationsDialog.vue +10 -11
  13. package/templates/default/src/components/settings/SecurityDialog.vue +17 -17
  14. package/templates/default/src/components/settings/SettingsDialog.vue +5 -5
  15. package/templates/default/src/components/settings/TelegramDialog.vue +14 -38
  16. package/templates/default/src/components/settings/TwoFactorDialog.vue +228 -86
  17. package/templates/default/src/data/locations.js +10 -0
  18. package/templates/default/src/i18n/index.js +11 -7
  19. package/templates/default/src/i18n/locales/ar.json +28 -8
  20. package/templates/default/src/i18n/locales/de.json +28 -8
  21. package/templates/default/src/i18n/locales/en.json +28 -8
  22. package/templates/default/src/i18n/locales/es.json +29 -9
  23. package/templates/default/src/i18n/locales/fr.json +28 -8
  24. package/templates/default/src/i18n/locales/it.json +29 -9
  25. package/templates/default/src/i18n/locales/ru.json +28 -8
  26. package/templates/default/src/main.js +28 -13
  27. package/templates/default/src/services/api.js +5 -3
  28. package/templates/default/src/services/demoApi.js +35 -25
  29. package/templates/default/src/storage.js +86 -0
  30. package/templates/default/src/stores/auth.js +6 -5
  31. package/templates/default/src/theme.js +35 -0
  32. package/templates/default/src/utils/crypto.js +3 -2
  33. package/templates/default/src/views/ForgotPassword.vue +3 -3
  34. package/templates/default/src/views/Home.vue +17 -17
  35. package/templates/default/src/views/Login.vue +32 -31
  36. package/templates/default/src/views/Profile.vue +117 -68
  37. package/templates/default/src/views/Register.vue +8 -8
  38. package/templates/default/src/views/SettingsView.vue +7 -7
  39. package/templates/default/tailwind.config.js +47 -0
package/README.md CHANGED
@@ -13,7 +13,7 @@ integrations on the frontend side: **Ably** (realtime) and **Brevo**
13
13
  | Ably (realtime) | **New** — via Laravel Echo, authorized through the same API-key + user-token headers as the rest of the app |
14
14
  | Brevo (email/SMS) | Kept — calls go through Laravel, key never ships to the browser |
15
15
  | Laravel API auth | `X-API-Key` (app) + `Authorization: Bearer <token>` (user) |
16
- | AES-256 | Kept — encrypts the cached user token in localStorage |
16
+ | AES-256 | Kept — encrypts the cached user token in IndexedDB |
17
17
  | Arabic/RTL | Kept — baked in via vue-i18n + postcss-rtlcss (opt-out: `--no-rtl` or the RTL prompt) |
18
18
 
19
19
  ## Install & run
@@ -43,7 +43,7 @@ Every request from `src/services/api.js` carries two headers:
43
43
  - `X-API-Key` — identifies this client app, set once per environment in `.env`.
44
44
  - `Authorization: Bearer <user token>` — identifies the logged-in user, issued
45
45
  by your Laravel `/auth/login` endpoint and stored AES-256 encrypted in
46
- `localStorage` (see `src/utils/crypto.js` and `src/stores/auth.js`).
46
+ IndexedDB (see `src/utils/crypto.js`, `src/storage.js` and `src/stores/auth.js`).
47
47
 
48
48
  ## Ably (realtime)
49
49
 
package/bin/falak-app-duo CHANGED
@@ -215,6 +215,7 @@ function buildI18nModule(locales, defaultLocale) {
215
215
  const ordered = ["ar", "en", "es", "de", "fr", "ru", "it"].filter((c) => locales.includes(c));
216
216
  const lines = [];
217
217
  lines.push('import { createI18n } from "vue-i18n";');
218
+ lines.push('import storage from "@/storage";');
218
219
  for (const code of ordered) {
219
220
  lines.push(`import ${code} from "./locales/${code}.json";`);
220
221
  }
@@ -226,27 +227,30 @@ function buildI18nModule(locales, defaultLocale) {
226
227
  lines.push("];");
227
228
  lines.push("");
228
229
  lines.push('const STORAGE_KEY = "falak_locale";');
229
- lines.push("const stored = localStorage.getItem(STORAGE_KEY);");
230
- lines.push(
231
- `const savedLocale = SUPPORTED_LOCALES.some((l) => l.code === stored) ? stored : "${defaultLocale}";`
232
- );
233
230
  lines.push("");
234
231
  lines.push("const i18n = createI18n({");
235
232
  lines.push(" legacy: false,");
236
- lines.push(" locale: savedLocale,");
233
+ lines.push(` locale: "${defaultLocale}",`);
237
234
  lines.push(' fallbackLocale: "en",');
238
235
  lines.push(` messages: { ${ordered.join(", ")} },`);
239
236
  lines.push("});");
240
237
  lines.push("");
241
238
  lines.push("export function setLocale(locale) {");
242
239
  lines.push(" i18n.global.locale.value = locale;");
243
- lines.push(" localStorage.setItem(STORAGE_KEY, locale);");
240
+ lines.push(" storage.set(STORAGE_KEY, locale);");
244
241
  lines.push(" document.documentElement.lang = locale;");
245
242
  lines.push(' document.documentElement.dir = locale === "ar" ? "rtl" : "ltr";');
246
243
  lines.push("}");
247
244
  lines.push("");
248
- lines.push("// Apply on load");
249
- lines.push("setLocale(savedLocale);");
245
+ lines.push("// Apply the persisted locale (falling back to the default) once storage is");
246
+ lines.push("// ready. main.js awaits this before mounting so the first paint is correct.");
247
+ lines.push("export async function initLocale() {");
248
+ lines.push(" const stored = await storage.get(STORAGE_KEY);");
249
+ lines.push(
250
+ ` const saved = SUPPORTED_LOCALES.some((l) => l.code === stored) ? stored : "${defaultLocale}";`
251
+ );
252
+ lines.push(" setLocale(saved);");
253
+ lines.push("}");
250
254
  lines.push("");
251
255
  lines.push("export default i18n;");
252
256
  lines.push("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "falak-app-duo",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
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": {
@@ -4,6 +4,26 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Falak App</title>
7
+ <style>
8
+ /* The theme is resolved from IndexedDB (async) before the app mounts;
9
+ keep the page hidden until then so the first paint is never the wrong
10
+ theme. main.js removes this class once the theme is applied. */
11
+ html.theme-loading {
12
+ visibility: hidden;
13
+ }
14
+ </style>
15
+ <script>
16
+ // Runs before first paint: hide the page until main.js has hydrated the
17
+ // IndexedDB store and applied the theme (see src/main.js).
18
+ (function () {
19
+ var el = document.documentElement;
20
+ el.classList.add("theme-loading");
21
+ // Safety net: never leave the page hidden if the app fails to boot.
22
+ setTimeout(function () {
23
+ el.classList.remove("theme-loading");
24
+ }, 3000);
25
+ })();
26
+ </script>
7
27
  </head>
8
28
  <body>
9
29
  <div id="app"></div>
@@ -13,10 +13,13 @@
13
13
  "ably": "^2.5.0",
14
14
  "axios": "^1.7.7",
15
15
  "crypto-js": "^4.2.0",
16
+ "idb": "^8.0.0",
16
17
  "laravel-echo": "^1.16.1",
18
+ "otplib": "^12.0.1",
17
19
  "pinia": "^2.2.2",
18
20
  "primeicons": "^7.0.0",
19
21
  "primevue": "^4.5.5",
22
+ "qrcode": "^1.5.4",
20
23
  "vue": "^3.5.0",
21
24
  "vue-i18n": "^9.14.0",
22
25
  "vue-router": "^4.4.5"
@@ -1,9 +1,10 @@
1
1
  <script setup>
2
- import { computed } from "vue";
2
+ import { computed, ref } from "vue";
3
3
  import { useI18n } from "vue-i18n";
4
4
  import { useRoute, useRouter, RouterLink, RouterView } from "vue-router";
5
5
  import { setLocale, SUPPORTED_LOCALES } from "@/i18n";
6
6
  import { useAuthStore } from "@/stores/auth";
7
+ import { getTheme, toggleTheme } from "@/theme";
7
8
  import AppBackdrop from "@/components/AppBackdrop.vue";
8
9
 
9
10
  const { t, locale } = useI18n();
@@ -11,6 +12,7 @@ const locales = SUPPORTED_LOCALES;
11
12
  const auth = useAuthStore();
12
13
  const route = useRoute();
13
14
  const router = useRouter();
15
+ const theme = ref(getTheme());
14
16
 
15
17
  // Auth pages hide the app chrome and go full-screen.
16
18
  const authPageNames = ["login", "register", "forgot-password"];
@@ -34,7 +36,7 @@ async function logout() {
34
36
  <div class="relative z-10 flex min-h-screen flex-col">
35
37
  <header
36
38
  v-if="!isAuthPage"
37
- class="sticky top-0 z-30 border-b border-white/10 bg-[#0b0f19]/70 backdrop-blur-xl"
39
+ class="sticky top-0 z-30 border-b border-edge bg-page/70 backdrop-blur-xl"
38
40
  >
39
41
  <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
42
  <RouterLink to="/" class="group flex items-center gap-2.5">
@@ -43,14 +45,14 @@ async function logout() {
43
45
  >
44
46
  F
45
47
  </span>
46
- <span class="text-lg font-bold text-white">{{ t("app.name") }}</span>
48
+ <span class="text-lg font-bold text-ink">{{ t("app.name") }}</span>
47
49
  </RouterLink>
48
50
 
49
51
  <div class="flex flex-1 flex-wrap items-center justify-end gap-1.5 text-sm">
50
52
  <RouterLink
51
53
  to="/"
52
54
  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'"
55
+ :class="route.name === 'home' ? 'bg-emerald-500/15 text-emerald-200 ring-1 ring-emerald-300/30' : 'text-ink2 hover:bg-glass hover:text-ink'"
54
56
  >
55
57
  {{ t("nav.home") }}
56
58
  </RouterLink>
@@ -60,7 +62,7 @@ async function logout() {
60
62
  :key="link.to"
61
63
  :to="link.to"
62
64
  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'"
65
+ :class="route.path === link.to ? 'bg-emerald-500/15 text-emerald-200 ring-1 ring-emerald-300/30' : 'text-ink2 hover:bg-glass hover:text-ink'"
64
66
  >
65
67
  {{ link.label }}
66
68
  </RouterLink>
@@ -68,6 +70,21 @@ async function logout() {
68
70
  </div>
69
71
 
70
72
  <div class="flex items-center gap-2.5">
73
+ <button
74
+ type="button"
75
+ :title="t(theme === 'light' ? 'theme.light' : 'theme.dark')"
76
+ :aria-label="t(theme === 'light' ? 'theme.light' : 'theme.dark')"
77
+ class="grid h-9 w-9 place-items-center rounded-full border border-edge2 bg-glass text-ink2 transition hover:bg-glass3 hover:text-ink"
78
+ @click="theme = toggleTheme()"
79
+ >
80
+ <svg v-if="theme === 'light'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4.5 w-4.5">
81
+ <circle cx="12" cy="12" r="4" />
82
+ <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" />
83
+ </svg>
84
+ <svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4.5 w-4.5">
85
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79Z" />
86
+ </svg>
87
+ </button>
71
88
  <RouterLink
72
89
  v-if="!auth.userToken"
73
90
  to="/login"
@@ -82,7 +99,7 @@ async function logout() {
82
99
  v-if="locales.length > 1"
83
100
  :value="locale"
84
101
  @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"
102
+ class="rounded-full border border-edge2 bg-glass px-2.5 py-1.5 text-sm font-medium text-ink2 outline-none transition hover:bg-glass3 hover:text-ink"
86
103
  >
87
104
  <option v-for="l in locales" :key="l.code" :value="l.code">{{ l.label }}</option>
88
105
  </select>
@@ -2,6 +2,82 @@
2
2
  @tailwind components;
3
3
  @tailwind utilities;
4
4
 
5
+ /* ============================================================
6
+ Theme tokens
7
+ Dark is the default (no class on <html>); light is applied by
8
+ toggling `.light` on <html> (see src/theme.js + the `theme-loading`
9
+ guard in index.html). Components use ONLY these semantic tokens — never
10
+ hardcoded dark-palette utilities — so a single var flip re-themes
11
+ the whole app. Keep `html { }` (dark) and `html.light { }` in sync.
12
+ ============================================================ */
13
+
14
+ :root {
15
+ --c-page: 11 15 25; /* #0b0f19 body/backdrop base */
16
+ --c-page-2: 13 21 32; /* #0d1520 raised surface */
17
+ --glass-a: 0.05;
18
+ --glass-a-2: 0.08;
19
+ --glass-a-3: 0.1;
20
+ --c-ink: 243 244 246; /* gray-100 primary text */
21
+ --c-ink-2: 229 231 235; /* gray-200 secondary text */
22
+ --c-ink-3: 156 163 175; /* gray-400 muted text */
23
+ --c-mute: 107 114 128; /* gray-500 faint text */
24
+ --c-edge: rgba(255, 255, 255, 0.1);
25
+ --c-edge-2: rgba(255, 255, 255, 0.15);
26
+ --c-edge-3: rgba(255, 255, 255, 0.2);
27
+ /* Pale "soft accent" text tones (glow-y on dark, deepened on light) */
28
+ --c-em-200: 167 243 208; /* emerald-200 */
29
+ --c-em-300: 110 231 183; /* emerald-300 */
30
+ --c-teal-200: 153 246 228; /* teal-200 */
31
+ --c-teal-300: 94 234 212; /* teal-300 */
32
+ --c-cyan-200: 165 243 252; /* cyan-200 */
33
+ --c-cyan-300: 103 232 249; /* cyan-300 */
34
+ --c-sky-200: 186 230 253; /* sky-200 */
35
+ --c-amber-200: 254 240 138; /* amber-200 */
36
+ --c-amber-300: 252 211 77; /* amber-300 */
37
+ --c-orange-300: 253 186 116; /* orange-300 */
38
+ --c-rose-200: 254 205 211; /* rose-200 */
39
+ --c-rose-300: 253 164 175; /* rose-300 */
40
+ --c-rose-400: 251 113 133; /* rose-400 */
41
+ --glass-shine: rgba(255, 255, 255, 0.08);
42
+ --ripple: rgba(255, 255, 255, 0.4);
43
+ --grid-dot: rgba(255, 255, 255, 0.06);
44
+ --vignette: rgba(5, 8, 15, 0.55);
45
+ --scroll-thumb: rgba(255, 255, 255, 0.14);
46
+ }
47
+
48
+ html.light {
49
+ --c-page: 248 250 252; /* slate-50 */
50
+ --c-page-2: 226 232 240; /* slate-200 */
51
+ --glass-a: 0.72;
52
+ --glass-a-2: 0.8;
53
+ --glass-a-3: 0.85;
54
+ --c-ink: 30 41 59; /* slate-800 */
55
+ --c-ink-2: 51 65 85; /* slate-700 */
56
+ --c-ink-3: 71 85 105; /* slate-600 */
57
+ --c-mute: 100 116 139; /* slate-500 */
58
+ --c-edge: rgba(15, 23, 42, 0.08);
59
+ --c-edge-2: rgba(15, 23, 42, 0.14);
60
+ --c-edge-3: rgba(15, 23, 42, 0.2);
61
+ --c-em-200: 6 95 70; /* emerald-800 */
62
+ --c-em-300: 4 120 87; /* emerald-700 */
63
+ --c-teal-200: 19 78 74; /* teal-800 */
64
+ --c-teal-300: 15 118 110; /* teal-700 */
65
+ --c-cyan-200: 21 94 117; /* cyan-800 */
66
+ --c-cyan-300: 14 116 144; /* cyan-700 */
67
+ --c-sky-200: 20 83 136; /* sky-800 */
68
+ --c-amber-200: 146 64 14; /* amber-800 */
69
+ --c-amber-300: 180 83 9; /* amber-700 */
70
+ --c-orange-300: 194 65 12; /* orange-700 */
71
+ --c-rose-200: 159 18 57; /* rose-800 */
72
+ --c-rose-300: 190 18 60; /* rose-700 */
73
+ --c-rose-400: 225 29 72; /* rose-600 */
74
+ --glass-shine: rgba(255, 255, 255, 0.35);
75
+ --ripple: rgba(2, 6, 23, 0.14);
76
+ --grid-dot: rgba(2, 6, 23, 0.09);
77
+ --vignette: rgba(100, 116, 139, 0.18);
78
+ --scroll-thumb: rgba(2, 6, 23, 0.22);
79
+ }
80
+
5
81
  /* Registered once for the animated gradient-border effect (Tailwind v3 has no
6
82
  @property in config, so it lives here in plain CSS next to the keyframes). */
7
83
  @property --tw-border-angle {
@@ -27,10 +103,10 @@
27
103
  html,
28
104
  body {
29
105
  font-family: "Almarai", "IBM Plex Sans Arabic", sans-serif;
30
- background-color: #0b0f19;
106
+ background-color: rgb(var(--c-page));
31
107
  }
32
108
  body {
33
- color: rgb(243 244 246 / 1);
109
+ color: rgb(var(--c-ink));
34
110
  }
35
111
  ::selection {
36
112
  background: rgb(16 185 129 / 0.35);
@@ -38,7 +114,7 @@
38
114
  }
39
115
  * {
40
116
  scrollbar-width: thin;
41
- scrollbar-color: rgb(255 255 255 / 0.14) transparent;
117
+ scrollbar-color: var(--scroll-thumb) transparent;
42
118
  }
43
119
  *::-webkit-scrollbar {
44
120
  width: 8px;
@@ -46,17 +122,17 @@
46
122
  }
47
123
  *::-webkit-scrollbar-thumb {
48
124
  border-radius: 9999px;
49
- background: rgb(255 255 255 / 0.14);
125
+ background: var(--scroll-thumb);
50
126
  }
51
127
  }
52
128
 
53
129
  @layer components {
54
130
  /* Reusable glass surfaces — the app's core visual language */
55
131
  .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;
132
+ @apply relative rounded-2xl border border-edge bg-glass shadow-2xl shadow-black/30 backdrop-blur-xl backdrop-saturate-150;
57
133
  }
58
134
  .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;
135
+ @apply transition-all duration-300 ease-out hover:-translate-y-1 hover:border-edge3 hover:bg-glass2 hover:shadow-emerald-500/10;
60
136
  }
61
137
 
62
138
  /* Fine hairline top highlight so glass reads as "lit" from above */
@@ -68,16 +144,12 @@
68
144
  position: absolute;
69
145
  inset: 0;
70
146
  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
- );
147
+ background: linear-gradient(180deg, var(--glass-shine) 0%, rgba(0, 0, 0, 0) 32%);
76
148
  pointer-events: none;
77
149
  }
78
150
 
79
151
  .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;
152
+ @apply w-full rounded-xl border border-edge bg-glass px-4 py-2.5 text-sm text-ink placeholder-mute outline-none ring-1 ring-transparent transition focus:border-emerald-400/50 focus:bg-glass2 focus:ring-2 focus:ring-emerald-400/20 caret-emerald-500;
81
153
  }
82
154
  .glass-select {
83
155
  @apply glass-input appearance-none pe-10;
@@ -88,11 +160,11 @@
88
160
  }
89
161
 
90
162
  .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;
163
+ @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-edge transition-all duration-200 hover:brightness-110 hover:shadow-emerald-400/40 hover:ring-edge3 animate-gradient-pan motion-reduce:animate-none disabled:cursor-not-allowed disabled:opacity-50 active:translate-y-px;
92
164
  }
93
165
 
94
166
  .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;
167
+ @apply inline-flex items-center justify-center gap-2 rounded-xl border border-edge2 bg-glass px-4 py-2.5 text-sm font-semibold text-ink2 shadow-sm backdrop-blur transition-all duration-200 hover:bg-glass3 hover:text-ink disabled:cursor-not-allowed disabled:opacity-50;
96
168
  }
97
169
 
98
170
  /* Bright brand accent button — CTA on auth/settings pages */
@@ -105,16 +177,17 @@
105
177
  @apply relative isolate overflow-hidden;
106
178
  }
107
179
  .ripple {
108
- @apply pointer-events-none absolute rounded-full bg-white/40;
180
+ @apply pointer-events-none absolute rounded-full;
181
+ background: var(--ripple);
109
182
  transform: translate(-50%, -50%) scale(0);
110
183
  animation: ripple-expand 0.6s ease-out forwards;
111
184
  }
112
185
 
113
186
  .field-label {
114
- @apply mb-1.5 block text-sm font-medium text-gray-300;
187
+ @apply mb-1.5 block text-sm font-medium text-ink;
115
188
  }
116
189
  .field-hint {
117
- @apply mt-1 block text-xs text-gray-500;
190
+ @apply mt-1 block text-xs text-mute;
118
191
  }
119
192
 
120
193
  /* Two-stop gradient headline text (2 stops only — 3+ reads muddy at text size) */
@@ -127,7 +200,7 @@
127
200
 
128
201
  /* Small pill / chip */
129
202
  .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;
203
+ @apply inline-flex items-center gap-1.5 rounded-full border border-edge bg-glass px-3 py-1 text-xs font-medium text-ink2;
131
204
  }
132
205
 
133
206
  /* Section heading with gradient marker bar */
@@ -141,7 +214,7 @@
141
214
 
142
215
  /* Fine dotted grid used by the backdrop and hero */
143
216
  .bg-grid {
144
- background-image: radial-gradient(rgb(255 255 255 / 0.06) 1px, transparent 1px);
217
+ background-image: radial-gradient(var(--grid-dot) 1px, transparent 1px);
145
218
  background-size: 26px 26px;
146
219
  }
147
220
  }
@@ -8,7 +8,7 @@ defineOptions({ name: "AppBackdrop" });
8
8
  <template>
9
9
  <div
10
10
  aria-hidden="true"
11
- class="pointer-events-none fixed inset-0 -z-10 overflow-hidden bg-[#0b0f19]"
11
+ class="pointer-events-none fixed inset-0 -z-10 overflow-hidden bg-page"
12
12
  >
13
13
  <!-- Aurora mesh base -->
14
14
  <div
@@ -38,6 +38,6 @@ defineOptions({ name: "AppBackdrop" });
38
38
  ></div>
39
39
 
40
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>
41
+ <div class="absolute inset-0 bg-[radial-gradient(ellipse_at_center,transparent_55%,var(--vignette))]"></div>
42
42
  </div>
43
43
  </template>
@@ -33,7 +33,7 @@ const highlights = computed(() => [
33
33
  <template>
34
34
  <div class="grid min-h-screen lg:grid-cols-2">
35
35
  <!-- Brand panel (large screens) -->
36
- <aside class="relative hidden overflow-hidden border-e border-white/10 lg:flex lg:flex-col lg:justify-between lg:p-14">
36
+ <aside class="relative hidden overflow-hidden border-e border-edge lg:flex lg:flex-col lg:justify-between lg:p-14">
37
37
  <div class="absolute -start-24 -top-24 h-80 w-80 rounded-full bg-emerald-500/20 blur-[100px] animate-float motion-reduce:animate-none"></div>
38
38
  <div class="absolute -end-20 bottom-10 h-72 w-72 rounded-full bg-rose-500/15 blur-[100px] animate-float motion-reduce:animate-none" style="animation-delay: -3.5s"></div>
39
39
 
@@ -41,7 +41,7 @@ const highlights = computed(() => [
41
41
  <span class="grid h-11 w-11 place-items-center rounded-2xl bg-gradient-to-br from-emerald-400 to-teal-500 text-xl font-black text-emerald-950 shadow-lg shadow-emerald-500/30">
42
42
  F
43
43
  </span>
44
- <span class="text-xl font-bold text-white">{{ t("app.name") }}</span>
44
+ <span class="text-xl font-bold text-ink">{{ t("app.name") }}</span>
45
45
  </div>
46
46
 
47
47
  <div class="relative">
@@ -53,11 +53,11 @@ const highlights = computed(() => [
53
53
  <span class="text-xs font-medium tracking-wide text-emerald-300 uppercase">{{ t("auth.brand.badge") }}</span>
54
54
  </div>
55
55
 
56
- <h1 class="text-4xl font-extrabold leading-tight tracking-tight text-white">
56
+ <h1 class="text-4xl font-extrabold leading-tight tracking-tight text-ink">
57
57
  {{ t("auth.brand.headline1") }}
58
58
  <span class="text-gradient">{{ t("auth.brand.headline2") }}</span>
59
59
  </h1>
60
- <p class="mt-4 max-w-md text-sm leading-relaxed text-gray-400">
60
+ <p class="mt-4 max-w-md text-sm leading-relaxed text-mute">
61
61
  {{ t("auth.brand.tagline") }}
62
62
  </p>
63
63
 
@@ -65,9 +65,9 @@ const highlights = computed(() => [
65
65
  <li
66
66
  v-for="h in highlights"
67
67
  :key="h.title"
68
- class="flex items-start gap-3 rounded-xl border border-white/10 bg-white/[0.03] p-3.5 backdrop-blur"
68
+ class="flex items-start gap-3 rounded-xl border border-edge bg-glass p-3.5 backdrop-blur"
69
69
  >
70
- <span class="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-gradient-to-br from-emerald-400/20 to-teal-500/20 text-emerald-200 ring-1 ring-white/10">
70
+ <span class="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-gradient-to-br from-emerald-400/20 to-teal-500/20 text-emerald-200 ring-1 ring-edge">
71
71
  <svg v-if="h.icon === 'realtime'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
72
72
  <circle cx="12" cy="12" r="9" />
73
73
  <path d="M12 3a9 9 0 0 1 9 9" opacity=".4" />
@@ -84,14 +84,14 @@ const highlights = computed(() => [
84
84
  </svg>
85
85
  </span>
86
86
  <div>
87
- <p class="text-sm font-semibold text-white">{{ h.title }}</p>
88
- <p class="mt-0.5 text-xs text-gray-500">{{ h.desc }}</p>
87
+ <p class="text-sm font-semibold text-ink">{{ h.title }}</p>
88
+ <p class="mt-0.5 text-xs text-mute">{{ h.desc }}</p>
89
89
  </div>
90
90
  </li>
91
91
  </ul>
92
92
  </div>
93
93
 
94
- <p class="relative text-xs text-gray-500">
94
+ <p class="relative text-xs text-mute">
95
95
  {{ t("auth.brand.footer") }}
96
96
  </p>
97
97
  </aside>
@@ -103,8 +103,8 @@ const highlights = computed(() => [
103
103
  <div class="mx-auto mb-5 grid h-12 w-12 place-items-center rounded-2xl bg-gradient-to-br from-emerald-400 to-teal-500 text-2xl font-black text-emerald-950 shadow-lg shadow-emerald-500/30 lg:hidden">
104
104
  F
105
105
  </div>
106
- <h1 class="text-2xl font-bold text-white">{{ title }}</h1>
107
- <p v-if="subtitle" class="mt-2 text-sm text-gray-400">{{ subtitle }}</p>
106
+ <h1 class="text-2xl font-bold text-ink">{{ title }}</h1>
107
+ <p v-if="subtitle" class="mt-2 text-sm text-mute">{{ subtitle }}</p>
108
108
  </div>
109
109
  <GlassCard spotlight class="p-6 sm:p-8">
110
110
  <slot />
@@ -19,7 +19,7 @@ function toggle() {
19
19
  :class="
20
20
  modelValue
21
21
  ? 'border-emerald-300/40 bg-gradient-to-r from-emerald-500 to-teal-500'
22
- : 'border-white/15 bg-white/10'
22
+ : 'border-edge2 bg-glass'
23
23
  "
24
24
  @click="toggle"
25
25
  >
@@ -23,7 +23,6 @@ const form = ref({
23
23
  model: user?.ai_agent?.model ?? "llama-3.3-70b-versatile",
24
24
  temperature: user?.ai_agent?.temperature ?? 0.7,
25
25
  reasoning: user?.ai_agent?.reasoning ?? false,
26
- system_prompt: user?.ai_agent?.system_prompt ?? "",
27
26
  });
28
27
 
29
28
  const models = [
@@ -63,8 +62,8 @@ async function save() {
63
62
  AI
64
63
  </span>
65
64
  <div>
66
- <h2 class="text-base font-semibold text-white">{{ t("aiAgent.key.title") }}</h2>
67
- <p class="text-xs text-gray-500">{{ t("aiAgent.key.subtitle") }}</p>
65
+ <h2 class="text-base font-semibold text-ink">{{ t("aiAgent.key.title") }}</h2>
66
+ <p class="text-xs text-mute">{{ t("aiAgent.key.subtitle") }}</p>
68
67
  </div>
69
68
  <span class="ms-auto inline-flex items-center gap-1.5 rounded-full bg-emerald-500/15 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-wide text-emerald-300">
70
69
  <span class="relative inline-flex h-1.5 w-1.5">
@@ -75,16 +74,16 @@ async function save() {
75
74
  </span>
76
75
  </div>
77
76
 
78
- <ol class="mt-5 flex flex-col gap-3">
79
- <li
77
+ <div class="mt-5 flex flex-col gap-3 rounded-xl border border-edge bg-glass p-4">
78
+ <div
80
79
  v-for="(step, i) in steps"
81
80
  :key="step.num"
82
- class="group flex items-start gap-3.5 rounded-xl border border-white/10 bg-white/[0.03] p-4 transition hover:border-emerald-300/25 hover:bg-white/[0.05]"
81
+ class="flex items-start gap-3.5"
83
82
  >
84
83
  <span class="relative grid h-8 w-8 shrink-0 place-items-center rounded-full bg-gradient-to-br from-emerald-500/20 to-teal-500/20 font-bold text-emerald-300 ring-1 ring-emerald-300/30">
85
84
  {{ step.num }}
86
85
  </span>
87
- <span class="grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-white/5 text-gray-400 ring-1 ring-white/10 transition group-hover:text-emerald-300">
86
+ <span class="grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-glass text-mute ring-1 ring-edge">
88
87
  <svg v-if="step.icon === 'account'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4">
89
88
  <circle cx="12" cy="8" r="4" />
90
89
  <path d="M4 21a8 8 0 0 1 16 0" />
@@ -101,9 +100,9 @@ async function save() {
101
100
  <path d="M12 16v4M8 20h8" />
102
101
  </svg>
103
102
  </span>
104
- <span class="flex-1 pt-0.5 text-sm leading-relaxed text-gray-300">{{ t(`aiAgent.key.steps.${step.num}`) }}</span>
105
- </li>
106
- </ol>
103
+ <span class="flex-1 pt-0.5 text-sm leading-relaxed text-ink2">{{ t(`aiAgent.key.steps.${step.num}`) }}</span>
104
+ </div>
105
+ </div>
107
106
 
108
107
  <div class="mt-4 flex items-start gap-2.5 rounded-xl border border-amber-300/20 bg-amber-500/10 p-3.5 text-xs leading-relaxed text-amber-200">
109
108
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mt-0.5 h-4 w-4 shrink-0">
@@ -118,8 +117,8 @@ async function save() {
118
117
  <GlassCard spotlight class="p-4 sm:p-6">
119
118
  <div class="flex items-center justify-between gap-4">
120
119
  <div>
121
- <h2 class="text-base font-semibold text-white">{{ t("aiAgent.settings.title") }}</h2>
122
- <p class="mt-1 text-sm text-gray-400">{{ t("aiAgent.settings.hint") }}</p>
120
+ <h2 class="text-base font-semibold text-ink">{{ t("aiAgent.settings.title") }}</h2>
121
+ <p class="mt-1 text-sm text-mute">{{ t("aiAgent.settings.hint") }}</p>
123
122
  </div>
124
123
  <ToggleSwitch v-model="form.enabled" />
125
124
  </div>
@@ -133,15 +132,15 @@ async function save() {
133
132
  :key="m.id"
134
133
  type="button"
135
134
  class="flex items-center gap-3 rounded-xl border px-3.5 py-3 text-start transition"
136
- :class="form.model === m.id ? 'border-emerald-300/50 bg-emerald-500/15 shadow-lg shadow-emerald-500/10' : 'border-white/10 bg-white/[0.03] hover:bg-white/[0.07]'"
135
+ :class="form.model === m.id ? 'border-emerald-300/50 bg-emerald-500/15 shadow-lg shadow-emerald-500/10' : 'border-edge bg-glass hover:bg-glass2'"
137
136
  @click="form.model = m.id"
138
137
  >
139
- <span class="grid h-7 w-7 shrink-0 place-items-center rounded-lg text-[10px] font-black" :class="form.model === m.id ? 'bg-emerald-400/20 text-emerald-300' : 'bg-white/5 text-gray-500'">
138
+ <span class="grid h-7 w-7 shrink-0 place-items-center rounded-lg text-[10px] font-black" :class="form.model === m.id ? 'bg-emerald-400/20 text-emerald-300' : 'bg-glass text-mute'">
140
139
  {{ m.tag.slice(0, 2) }}
141
140
  </span>
142
141
  <span class="min-w-0">
143
- <span class="block truncate font-mono text-xs font-medium text-gray-200">{{ m.id }}</span>
144
- <span class="mt-0.5 flex items-center gap-1 text-[10px] text-gray-500">
142
+ <span class="block truncate font-mono text-xs font-medium text-ink2">{{ m.id }}</span>
143
+ <span class="mt-0.5 flex items-center gap-1 text-[10px] text-mute">
145
144
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-3 w-3">
146
145
  <path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z" />
147
146
  </svg>
@@ -168,26 +167,16 @@ async function save() {
168
167
  step="0.1"
169
168
  class="mt-2 w-full accent-emerald-500"
170
169
  />
171
- <div class="flex justify-between text-xs text-gray-500">
170
+ <div class="flex justify-between text-xs text-mute">
172
171
  <span>{{ t("aiAgent.temperatureLow") }}</span>
173
172
  <span>{{ t("aiAgent.temperatureHigh") }}</span>
174
173
  </div>
175
174
  </label>
176
175
 
177
- <label class="block">
178
- <span class="field-label">{{ t("aiAgent.systemPrompt") }}</span>
179
- <textarea
180
- v-model="form.system_prompt"
181
- rows="4"
182
- class="glass-input resize-y font-mono text-xs"
183
- :placeholder="t('aiAgent.systemPromptPlaceholder')"
184
- ></textarea>
185
- </label>
186
-
187
- <label class="flex cursor-pointer items-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] p-4">
176
+ <label class="flex cursor-pointer items-center gap-3 rounded-xl border border-edge bg-glass p-4">
188
177
  <ToggleSwitch v-model="form.reasoning" />
189
- <span class="text-sm text-gray-300">{{ t("aiAgent.reasoning") }}</span>
190
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="ms-auto h-4 w-4 text-gray-500">
178
+ <span class="text-sm text-ink2">{{ t("aiAgent.reasoning") }}</span>
179
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="ms-auto h-4 w-4 text-mute">
191
180
  <circle cx="12" cy="12" r="10" />
192
181
  <path d="M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3" />
193
182
  <path d="M12 17h.01" />