falak-app-duo 1.1.5 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/falak-app-duo +14 -1
- package/package.json +1 -1
- package/templates/default/.env.example +8 -1
- package/templates/default/index.html +19 -0
- package/templates/default/package.json +2 -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/locales/ar.json +35 -9
- package/templates/default/src/i18n/locales/de.json +35 -9
- package/templates/default/src/i18n/locales/en.json +35 -9
- package/templates/default/src/i18n/locales/es.json +36 -10
- package/templates/default/src/i18n/locales/fr.json +35 -9
- package/templates/default/src/i18n/locales/it.json +36 -10
- package/templates/default/src/i18n/locales/ru.json +35 -9
- package/templates/default/src/main.js +3 -0
- package/templates/default/src/services/dataClient.js +22 -0
- package/templates/default/src/services/demoApi.js +387 -0
- package/templates/default/src/stores/auth.js +18 -18
- package/templates/default/src/theme.js +31 -0
- package/templates/default/src/views/ForgotPassword.vue +3 -3
- package/templates/default/src/views/Home.vue +64 -14
- package/templates/default/src/views/Login.vue +41 -16
- 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/bin/falak-app-duo
CHANGED
|
@@ -540,6 +540,11 @@ async function main() {
|
|
|
540
540
|
: "1 (normal demo user)"
|
|
541
541
|
: "none";
|
|
542
542
|
|
|
543
|
+
// Data mode: demo apps are wired fully to the bundled demo data (auth +
|
|
544
|
+
// profile/settings + entity lists) and need no backend; everything else
|
|
545
|
+
// goes straight to the Laravel API.
|
|
546
|
+
const dataMode = demoAccounts || (demoEntities && demoEntities.length) ? "demo" : "live";
|
|
547
|
+
|
|
543
548
|
console.log(
|
|
544
549
|
chalk.gray(` Languages wired: ${locales.map((l) => LOCALE_LABELS[l]).join(", ")}${hasRtl ? " (RTL default)" : ""}`)
|
|
545
550
|
);
|
|
@@ -548,11 +553,13 @@ async function main() {
|
|
|
548
553
|
` Roles: ${roles && roles.length ? roles.join(", ") : "none"} | Demo accounts: ${demoAccountsSummary} | Demo data: ${demoEntities && demoEntities.length ? demoEntities.join(", ") : "none"}`
|
|
549
554
|
)
|
|
550
555
|
);
|
|
556
|
+
console.log(chalk.gray(` Data mode: ${dataMode}${dataMode === "demo" ? " (no backend needed)" : " (Laravel API)"}`));
|
|
551
557
|
|
|
552
558
|
// Generate a real .env from .env.example with a fresh encryption key
|
|
553
559
|
const envExamplePath = path.join(targetDir, ".env.example");
|
|
554
560
|
let envContent = await fs.readFile(envExamplePath, "utf-8");
|
|
555
561
|
envContent = envContent
|
|
562
|
+
.replace("__DATA_MODE__", dataMode)
|
|
556
563
|
.replace("__API_BASE_URL__", answers.apiBaseUrl || "http://localhost:8000/api")
|
|
557
564
|
.replace("__AES_KEY__", randomHex(32)) // 256-bit key, hex-encoded
|
|
558
565
|
.replace("__AES_IV__", randomHex(16)); // 128-bit IV, hex-encoded
|
|
@@ -561,9 +568,15 @@ async function main() {
|
|
|
561
568
|
console.log(chalk.green(`\n Scaffolded ./${projectName} successfully.\n`));
|
|
562
569
|
console.log(
|
|
563
570
|
chalk.gray(
|
|
564
|
-
" Fill in VITE_API_KEY, VITE_ABLY_PUBLIC_KEY, Brevo backend routes, and review .env before shipping
|
|
571
|
+
" Fill in VITE_API_KEY, VITE_ABLY_PUBLIC_KEY, Brevo backend routes, and review .env before shipping."
|
|
565
572
|
)
|
|
566
573
|
);
|
|
574
|
+
if (dataMode === "demo") {
|
|
575
|
+
console.log(chalk.gray(" Data mode: demo — the app runs on bundled demo data (no API keys needed)."));
|
|
576
|
+
console.log(chalk.gray(" Flip VITE_DATA_MODE=live in .env to wire the app to your Laravel backend.\n"));
|
|
577
|
+
} else {
|
|
578
|
+
console.log(chalk.gray("\n Data mode: live — the app calls your Laravel backend.\n"));
|
|
579
|
+
}
|
|
567
580
|
|
|
568
581
|
const runChoice = opts.yes || hasFeatureFlags
|
|
569
582
|
? "install"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "falak-app-duo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
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": {
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
# --- Data source ---
|
|
2
|
+
# demo: the app runs entirely on bundled demo data (auth, profile, settings and
|
|
3
|
+
# entity lists) with no backend required.
|
|
4
|
+
# live: every request goes to the Laravel API below.
|
|
5
|
+
# The scaffolder sets this to "demo" when demo accounts/demo data were selected.
|
|
6
|
+
VITE_DATA_MODE=__DATA_MODE__
|
|
7
|
+
|
|
1
8
|
# --- Laravel API (Falak Bridge or your own backend) ---
|
|
2
|
-
# Base URL of your Laravel API
|
|
9
|
+
# Base URL of your Laravel API (not used in demo mode)
|
|
3
10
|
VITE_API_BASE_URL=__API_BASE_URL__
|
|
4
11
|
# Static API key issued to this client app (sent as X-API-Key on every request)
|
|
5
12
|
VITE_API_KEY=replace-with-your-app-api-key
|
|
@@ -4,6 +4,25 @@
|
|
|
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
|
+
<script>
|
|
8
|
+
// FOUC guard: paint with the right theme before the app mounts.
|
|
9
|
+
// Mirrors src/theme.js.getTheme() — keep in sync.
|
|
10
|
+
(function () {
|
|
11
|
+
var s;
|
|
12
|
+
try {
|
|
13
|
+
s = localStorage.getItem("falak_theme");
|
|
14
|
+
} catch (e) {}
|
|
15
|
+
if (s === "light") {
|
|
16
|
+
document.documentElement.classList.add("light");
|
|
17
|
+
} else if (s !== "dark") {
|
|
18
|
+
try {
|
|
19
|
+
if (window.matchMedia("(prefers-color-scheme: light)").matches) {
|
|
20
|
+
document.documentElement.classList.add("light");
|
|
21
|
+
}
|
|
22
|
+
} catch (e) {}
|
|
23
|
+
}
|
|
24
|
+
})();
|
|
25
|
+
</script>
|
|
7
26
|
</head>
|
|
8
27
|
<body>
|
|
9
28
|
<div id="app"></div>
|
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
"axios": "^1.7.7",
|
|
15
15
|
"crypto-js": "^4.2.0",
|
|
16
16
|
"laravel-echo": "^1.16.1",
|
|
17
|
+
"otplib": "^12.0.1",
|
|
17
18
|
"pinia": "^2.2.2",
|
|
18
19
|
"primeicons": "^7.0.0",
|
|
19
20
|
"primevue": "^4.5.5",
|
|
21
|
+
"qrcode": "^1.5.4",
|
|
20
22
|
"vue": "^3.5.0",
|
|
21
23
|
"vue-i18n": "^9.14.0",
|
|
22
24
|
"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 FOUC guard
|
|
9
|
+
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" />
|