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.
- package/README.md +2 -2
- package/bin/falak-app-duo +12 -8
- package/package.json +1 -1
- package/templates/default/index.html +20 -0
- package/templates/default/package.json +3 -0
- package/templates/default/src/App.vue +23 -6
- package/templates/default/src/assets/main.css +92 -19
- package/templates/default/src/components/AppBackdrop.vue +2 -2
- package/templates/default/src/components/AuthLayout.vue +11 -11
- package/templates/default/src/components/ToggleSwitch.vue +1 -1
- package/templates/default/src/components/settings/AIAgentDialog.vue +19 -30
- package/templates/default/src/components/settings/NotificationsDialog.vue +10 -11
- package/templates/default/src/components/settings/SecurityDialog.vue +17 -17
- package/templates/default/src/components/settings/SettingsDialog.vue +5 -5
- package/templates/default/src/components/settings/TelegramDialog.vue +14 -38
- package/templates/default/src/components/settings/TwoFactorDialog.vue +228 -86
- package/templates/default/src/data/locations.js +10 -0
- package/templates/default/src/i18n/index.js +11 -7
- package/templates/default/src/i18n/locales/ar.json +28 -8
- package/templates/default/src/i18n/locales/de.json +28 -8
- package/templates/default/src/i18n/locales/en.json +28 -8
- package/templates/default/src/i18n/locales/es.json +29 -9
- package/templates/default/src/i18n/locales/fr.json +28 -8
- package/templates/default/src/i18n/locales/it.json +29 -9
- package/templates/default/src/i18n/locales/ru.json +28 -8
- package/templates/default/src/main.js +28 -13
- package/templates/default/src/services/api.js +5 -3
- package/templates/default/src/services/demoApi.js +35 -25
- package/templates/default/src/storage.js +86 -0
- package/templates/default/src/stores/auth.js +6 -5
- package/templates/default/src/theme.js +35 -0
- package/templates/default/src/utils/crypto.js +3 -2
- package/templates/default/src/views/ForgotPassword.vue +3 -3
- package/templates/default/src/views/Home.vue +17 -17
- package/templates/default/src/views/Login.vue +32 -31
- package/templates/default/src/views/Profile.vue +117 -68
- package/templates/default/src/views/Register.vue +8 -8
- package/templates/default/src/views/SettingsView.vue +7 -7
- 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
|
|
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
|
-
|
|
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(
|
|
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("
|
|
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
|
|
249
|
-
lines.push("
|
|
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.
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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:
|
|
106
|
+
background-color: rgb(var(--c-page));
|
|
31
107
|
}
|
|
32
108
|
body {
|
|
33
|
-
color: rgb(
|
|
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:
|
|
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:
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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(
|
|
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-
|
|
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%,
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
88
|
-
<p class="mt-0.5 text-xs text-
|
|
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-
|
|
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-
|
|
107
|
-
<p v-if="subtitle" class="mt-2 text-sm text-
|
|
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 />
|
|
@@ -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-
|
|
67
|
-
<p class="text-xs text-
|
|
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
|
-
<
|
|
79
|
-
<
|
|
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="
|
|
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-
|
|
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-
|
|
105
|
-
</
|
|
106
|
-
</
|
|
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-
|
|
122
|
-
<p class="mt-1 text-sm text-
|
|
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-
|
|
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-
|
|
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-
|
|
144
|
-
<span class="mt-0.5 flex items-center gap-1 text-[10px] text-
|
|
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-
|
|
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="
|
|
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-
|
|
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-
|
|
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" />
|