super-svelte-skeleton 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/.env.example +17 -0
  2. package/.github/workflows/ninja_i18n.yml +23 -0
  3. package/.prettierignore +9 -0
  4. package/.prettierrc +12 -0
  5. package/.vscode/extensions.json +5 -0
  6. package/.vscode/launch.json +15 -0
  7. package/.vscode/settings.json +30 -0
  8. package/README.md +237 -0
  9. package/_gitignore +27 -0
  10. package/eslint.config.js +40 -0
  11. package/messages/ar.json +70 -0
  12. package/messages/en.json +70 -0
  13. package/messages/es.json +70 -0
  14. package/package.json +54 -0
  15. package/project.inlang/settings.json +15 -0
  16. package/src/app.css +8 -0
  17. package/src/app.d.ts +20 -0
  18. package/src/app.html +40 -0
  19. package/src/auto-imports.d.ts +35 -0
  20. package/src/hooks.client.ts +17 -0
  21. package/src/hooks.server.ts +73 -0
  22. package/src/hooks.ts +15 -0
  23. package/src/lib/entities/auth/api/endpoints.ts +11 -0
  24. package/src/lib/entities/auth/api/service.ts +35 -0
  25. package/src/lib/entities/auth/index.ts +9 -0
  26. package/src/lib/entities/auth/store.svelte.ts +50 -0
  27. package/src/lib/entities/auth/types.ts +33 -0
  28. package/src/lib/entities/user/api/endpoints.ts +6 -0
  29. package/src/lib/entities/user/api/service.ts +10 -0
  30. package/src/lib/entities/user/index.ts +2 -0
  31. package/src/lib/entities/user/types.ts +18 -0
  32. package/src/lib/features/theme-editor/constants.ts +33 -0
  33. package/src/lib/features/theme-editor/index.ts +3 -0
  34. package/src/lib/features/theme-editor/types.ts +10 -0
  35. package/src/lib/features/theme-editor/ui/CSSOutput.svelte +17 -0
  36. package/src/lib/features/theme-editor/ui/ColorCard.svelte +66 -0
  37. package/src/lib/features/theme-editor/ui/ThemeEditorWidget.svelte +319 -0
  38. package/src/lib/features/theme-editor/ui/ThemePreview.svelte +121 -0
  39. package/src/lib/features/theme-editor/ui/TypographySettings.svelte +73 -0
  40. package/src/lib/features/theme-editor/utils.ts +10 -0
  41. package/src/lib/shared/api/client.ts +47 -0
  42. package/src/lib/shared/api/index.ts +3 -0
  43. package/src/lib/shared/api/types.ts +25 -0
  44. package/src/lib/shared/config/api.ts +1 -0
  45. package/src/lib/shared/config/index.ts +2 -0
  46. package/src/lib/shared/config/routes.ts +18 -0
  47. package/src/lib/shared/i18n/index.ts +1 -0
  48. package/src/lib/shared/index.ts +2 -0
  49. package/src/lib/tailwind.config.ts +28 -0
  50. package/src/lib/widgets/topbar/Topbar.svelte +122 -0
  51. package/src/lib/widgets/topbar/constants.ts +16 -0
  52. package/src/lib/widgets/topbar/index.ts +2 -0
  53. package/src/params/integer.ts +5 -0
  54. package/src/routes/(app)/(admin)/+layout.server.ts +14 -0
  55. package/src/routes/(app)/(admin)/admin/+page.svelte +101 -0
  56. package/src/routes/(app)/+layout.server.ts +9 -0
  57. package/src/routes/(app)/+layout.svelte +12 -0
  58. package/src/routes/(app)/settings/+page.svelte +48 -0
  59. package/src/routes/(app)/theme/+page.svelte +5 -0
  60. package/src/routes/(auth)/forgot-password/+page.svelte +83 -0
  61. package/src/routes/(auth)/login/+page.server.ts +66 -0
  62. package/src/routes/(auth)/login/+page.svelte +156 -0
  63. package/src/routes/(auth)/logout/+page.server.ts +16 -0
  64. package/src/routes/(auth)/register/+page.svelte +167 -0
  65. package/src/routes/(auth)/reset-password/+page.svelte +127 -0
  66. package/src/routes/+error.svelte +95 -0
  67. package/src/routes/+layout.svelte +36 -0
  68. package/src/routes/+layout.ts +24 -0
  69. package/src/routes/+page.svelte +192 -0
  70. package/src/routes/+page.ts +3 -0
  71. package/static/config/config.local.json +3 -0
  72. package/static/config/config.prod.json +3 -0
  73. package/static/favicon.svg +1 -0
  74. package/static/logo.svg +7 -0
  75. package/static/profile.avif +0 -0
  76. package/static/smile.jpg +0 -0
  77. package/static/styles/theme-dark.css +30 -0
  78. package/static/styles/theme-light.css +28 -0
  79. package/stats.html +4950 -0
  80. package/svelte.config.js +78 -0
  81. package/tsconfig.json +46 -0
  82. package/vite.config.ts +51 -0
@@ -0,0 +1,16 @@
1
+ import type { Actions } from '@sveltejs/kit';
2
+ import { redirect } from '@sveltejs/kit';
3
+ import { AUTH_COOKIE_NAME, LOGIN_ROUTE } from '$shared/config';
4
+
5
+ export const actions: Actions = {
6
+ default: async ({ cookies }) => {
7
+ cookies.delete(AUTH_COOKIE_NAME, { path: '/' });
8
+ throw redirect(302, LOGIN_ROUTE);
9
+ }
10
+ };
11
+
12
+ // GET /logout — also works for simple <a href="/logout"> links
13
+ export async function load({ cookies }) {
14
+ cookies.delete(AUTH_COOKIE_NAME, { path: '/' });
15
+ throw redirect(302, LOGIN_ROUTE);
16
+ }
@@ -0,0 +1,167 @@
1
+ <script lang="ts">
2
+ import * as m from '$lib/paraglide/messages';
3
+ import { Eye, EyeSlash } from 'svelte-bootstrap-icons';
4
+ import { resolve } from '$app/paths';
5
+
6
+ let name = $state('');
7
+ let email = $state('');
8
+ let password = $state('');
9
+ let confirm = $state('');
10
+ let showPassword = $state(false);
11
+ let showConfirm = $state(false);
12
+
13
+ const handleSubmit = (e: Event) => {
14
+ e.preventDefault();
15
+ // TODO: wire up registration logic
16
+ };
17
+ </script>
18
+
19
+ <svelte:head>
20
+ <title>{m.register_title()}</title>
21
+ </svelte:head>
22
+
23
+ <div class="min-h-screen bg-surface-tertiary flex items-center justify-center px-4 py-12">
24
+
25
+ <!-- Ambient blobs -->
26
+ <div class="fixed inset-0 -z-10 pointer-events-none overflow-hidden" aria-hidden="true">
27
+ <div class="absolute w-96 h-96 rounded-full bg-accent/5 -top-32 -right-32 blur-3xl"></div>
28
+ <div class="absolute w-80 h-80 rounded-full bg-warning/5 -bottom-24 -left-24 blur-3xl"></div>
29
+ </div>
30
+
31
+ <div class="w-full max-w-md">
32
+
33
+ <!-- Logo -->
34
+ <a href={resolve("/",{})} class="flex items-center justify-center gap-2 mb-8">
35
+ <span class="text-accent text-xl leading-none">✦</span>
36
+ <span class="font-semibold text-content-primary tracking-wide text-lg">{m.app_name()}</span>
37
+ </a>
38
+
39
+ <!-- Card -->
40
+ <div class="bg-surface-primary rounded-2xl shadow-xl border p-8">
41
+
42
+ <!-- Heading -->
43
+ <div class="mb-8 text-center">
44
+ <h1 class="text-2xl font-bold text-content-primary mb-1">{m.register_title()}</h1>
45
+ <p class="text-sm text-content-secondary">{m.register_subtitle()}</p>
46
+ </div>
47
+
48
+ <form onsubmit={handleSubmit} class="flex flex-col gap-5">
49
+
50
+ <!-- Full Name -->
51
+ <div class="flex flex-col gap-1.5">
52
+ <label for="name" class="text-sm font-medium text-content-primary">
53
+ {m.register_name_label()}
54
+ </label>
55
+ <input
56
+ id="name"
57
+ type="text"
58
+ bind:value={name}
59
+ placeholder={m.register_name_placeholder()}
60
+ required
61
+ autocomplete="name"
62
+ class="w-full px-4 py-2.5 rounded-lg border bg-surface-secondary text-content-primary placeholder:text-content-tertiary focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent transition"
63
+ />
64
+ </div>
65
+
66
+ <!-- Email -->
67
+ <div class="flex flex-col gap-1.5">
68
+ <label for="email" class="text-sm font-medium text-content-primary">
69
+ {m.register_email_label()}
70
+ </label>
71
+ <input
72
+ id="email"
73
+ type="email"
74
+ bind:value={email}
75
+ placeholder={m.register_email_placeholder()}
76
+ required
77
+ autocomplete="email"
78
+ class="w-full px-4 py-2.5 rounded-lg border bg-surface-secondary text-content-primary placeholder:text-content-tertiary focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent transition"
79
+ />
80
+ </div>
81
+
82
+ <!-- Password -->
83
+ <div class="flex flex-col gap-1.5">
84
+ <label for="password" class="text-sm font-medium text-content-primary">
85
+ {m.register_password_label()}
86
+ </label>
87
+ <div class="relative">
88
+ <input
89
+ id="password"
90
+ type={showPassword ? 'text' : 'password'}
91
+ bind:value={password}
92
+ placeholder={m.register_password_placeholder()}
93
+ required
94
+ autocomplete="new-password"
95
+ class="w-full px-4 py-2.5 pr-11 rounded-lg border bg-surface-secondary text-content-primary placeholder:text-content-tertiary focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent transition"
96
+ />
97
+ <button
98
+ type="button"
99
+ onclick={() => showPassword = !showPassword}
100
+ class="absolute right-3 top-1/2 -translate-y-1/2 text-content-tertiary hover:text-content-primary transition"
101
+ aria-label="Toggle password visibility"
102
+ >
103
+ {#if showPassword}
104
+ <EyeSlash width={16} height={16} />
105
+ {:else}
106
+ <Eye width={16} height={16} />
107
+ {/if}
108
+ </button>
109
+ </div>
110
+ </div>
111
+
112
+ <!-- Confirm Password -->
113
+ <div class="flex flex-col gap-1.5">
114
+ <label for="confirm" class="text-sm font-medium text-content-primary">
115
+ {m.register_confirm_label()}
116
+ </label>
117
+ <div class="relative">
118
+ <input
119
+ id="confirm"
120
+ type={showConfirm ? 'text' : 'password'}
121
+ bind:value={confirm}
122
+ placeholder={m.register_confirm_placeholder()}
123
+ required
124
+ autocomplete="new-password"
125
+ class="w-full px-4 py-2.5 pr-11 rounded-lg border bg-surface-secondary text-content-primary placeholder:text-content-tertiary focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent transition"
126
+ />
127
+ <button
128
+ type="button"
129
+ onclick={() => showConfirm = !showConfirm}
130
+ class="absolute right-3 top-1/2 -translate-y-1/2 text-content-tertiary hover:text-content-primary transition"
131
+ aria-label="Toggle confirm password visibility"
132
+ >
133
+ {#if showConfirm}
134
+ <EyeSlash width={16} height={16} />
135
+ {:else}
136
+ <Eye width={16} height={16} />
137
+ {/if}
138
+ </button>
139
+ </div>
140
+ </div>
141
+
142
+ <!-- Submit -->
143
+ <button type="submit" class="btn btn-primary w-full py-3 text-base font-semibold mt-1">
144
+ {m.register_submit()}
145
+ </button>
146
+
147
+ </form>
148
+
149
+ <!-- Divider -->
150
+ <div class="relative my-6">
151
+ <div class="absolute inset-0 flex items-center">
152
+ <div class="w-full border-t border-border-primary"></div>
153
+ </div>
154
+ <div class="relative flex justify-center text-xs">
155
+ <span class="px-3 bg-surface-primary text-content-tertiary">{m.divider_text()}</span>
156
+ </div>
157
+ </div>
158
+
159
+ <!-- Sign in link -->
160
+ <p class="text-center text-sm text-content-secondary">
161
+ {m.register_have_account()}
162
+ <a href={resolve("/login",{})} class="text-accent font-medium hover:underline ml-1">{m.register_sign_in()}</a>
163
+ </p>
164
+
165
+ </div>
166
+ </div>
167
+ </div>
@@ -0,0 +1,127 @@
1
+ <script lang="ts">
2
+ import * as m from '$lib/paraglide/messages';
3
+ import { page } from '$app/state';
4
+ import { Eye, EyeSlash, ExclamationCircleFill } from 'svelte-bootstrap-icons';
5
+ import { resolve } from '$app/paths';
6
+
7
+ // Token comes from ?token=... query param (emailed link)
8
+ const token = $derived(page.url.searchParams.get('token') ?? '');
9
+
10
+ let password = $state('');
11
+ let confirm = $state('');
12
+ let showPassword = $state(false);
13
+ let showConfirm = $state(false);
14
+ let error = $state('');
15
+
16
+ const handleSubmit = async (e: Event) => {
17
+ e.preventDefault();
18
+ error = '';
19
+ if (!token) {
20
+ error = 'Invalid or missing reset link.';
21
+ return;
22
+ }
23
+ if (password !== confirm) {
24
+ error = 'Passwords do not match.';
25
+ return;
26
+ }
27
+ // TODO: call authApi.resetPassword({ token, password, confirmPassword: confirm })
28
+ };
29
+ </script>
30
+
31
+ <svelte:head>
32
+ <title>{m.reset_title()}</title>
33
+ </svelte:head>
34
+
35
+ <div class="min-h-screen bg-surface-tertiary flex items-center justify-center px-4 py-12">
36
+
37
+ <div class="fixed inset-0 -z-10 pointer-events-none overflow-hidden" aria-hidden="true">
38
+ <div class="absolute w-80 h-80 rounded-full bg-accent/5 -bottom-24 -left-24 blur-3xl"></div>
39
+ </div>
40
+
41
+ <div class="w-full max-w-md">
42
+
43
+ <a href={resolve("/",{})} class="flex items-center justify-center gap-2 mb-8">
44
+ <span class="text-accent text-xl leading-none">✦</span>
45
+ <span class="font-semibold text-content-primary tracking-wide text-lg">{m.app_name()}</span>
46
+ </a>
47
+
48
+ <div class="bg-surface-primary rounded-2xl shadow-xl border p-8">
49
+
50
+ <div class="mb-8 text-center">
51
+ <h1 class="text-2xl font-bold text-content-primary mb-1">{m.reset_title()}</h1>
52
+ <p class="text-sm text-content-secondary">{m.reset_subtitle()}</p>
53
+ </div>
54
+
55
+ <form onsubmit={handleSubmit} class="flex flex-col gap-5">
56
+
57
+ <!-- New password -->
58
+ <div class="flex flex-col gap-1.5">
59
+ <label for="password" class="text-sm font-medium text-content-primary">
60
+ {m.reset_new_label()}
61
+ </label>
62
+ <div class="relative">
63
+ <input
64
+ id="password"
65
+ type={showPassword ? 'text' : 'password'}
66
+ bind:value={password}
67
+ placeholder={m.reset_new_placeholder()}
68
+ required
69
+ autocomplete="new-password"
70
+ class="w-full px-4 py-2.5 pr-11 rounded-lg border bg-surface-secondary text-content-primary placeholder:text-content-tertiary focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent transition"
71
+ />
72
+ <button type="button" onclick={() => showPassword = !showPassword}
73
+ class="absolute right-3 top-1/2 -translate-y-1/2 text-content-tertiary hover:text-content-primary transition"
74
+ aria-label="Toggle visibility">
75
+ {#if showPassword}
76
+ <EyeSlash width={16} height={16} />
77
+ {:else}
78
+ <Eye width={16} height={16} />
79
+ {/if}
80
+ </button>
81
+ </div>
82
+ </div>
83
+
84
+ <!-- Confirm password -->
85
+ <div class="flex flex-col gap-1.5">
86
+ <label for="confirm" class="text-sm font-medium text-content-primary">
87
+ {m.reset_confirm_label()}
88
+ </label>
89
+ <div class="relative">
90
+ <input
91
+ id="confirm"
92
+ type={showConfirm ? 'text' : 'password'}
93
+ bind:value={confirm}
94
+ placeholder={m.reset_confirm_placeholder()}
95
+ required
96
+ autocomplete="new-password"
97
+ class="w-full px-4 py-2.5 pr-11 rounded-lg border bg-surface-secondary text-content-primary placeholder:text-content-tertiary focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent transition"
98
+ />
99
+ <button type="button" onclick={() => showConfirm = !showConfirm}
100
+ class="absolute right-3 top-1/2 -translate-y-1/2 text-content-tertiary hover:text-content-primary transition"
101
+ aria-label="Toggle visibility">
102
+ {#if showConfirm}
103
+ <EyeSlash width={16} height={16} />
104
+ {:else}
105
+ <Eye width={16} height={16} />
106
+ {/if}
107
+ </button>
108
+ </div>
109
+ </div>
110
+
111
+ <!-- Inline error -->
112
+ {#if error}
113
+ <p class="text-xs text-error flex items-center gap-1.5">
114
+ <ExclamationCircleFill width={12} height={12} />
115
+ {error}
116
+ </p>
117
+ {/if}
118
+
119
+ <button type="submit" class="btn btn-primary w-full py-3 text-base font-semibold mt-1">
120
+ {m.reset_submit()}
121
+ </button>
122
+
123
+ </form>
124
+
125
+ </div>
126
+ </div>
127
+ </div>
@@ -0,0 +1,95 @@
1
+ <script lang="ts">
2
+ import { page } from '$app/state';
3
+ import { HttpStatus } from '$shared/api';
4
+ import { resolve } from '$app/paths';
5
+ import { LightbulbOff } from 'svelte-bootstrap-icons';
6
+
7
+ const status = $derived(page.status);
8
+ const message = $derived(page.error?.message ?? 'An unexpected error occurred');
9
+
10
+ const title = $derived(
11
+ status === HttpStatus.NOT_FOUND
12
+ ? "LOOKS LIKE YOU'RE LOST"
13
+ : status === HttpStatus.FORBIDDEN
14
+ ? "ACCESS DENIED"
15
+ : status === HttpStatus.INTERNAL_SERVER_ERROR
16
+ ? "SOMETHING BROKE"
17
+ : "SOMETHING WENT WRONG"
18
+ );
19
+
20
+ const hint = $derived(
21
+ status === HttpStatus.NOT_FOUND
22
+ ? "The page you are looking for is not available!"
23
+ : status === HttpStatus.FORBIDDEN
24
+ ? "You don't have permission to view this resource."
25
+ : status === HttpStatus.INTERNAL_SERVER_ERROR
26
+ ? "Our servers ran into an issue. Please try again in a moment."
27
+ : message
28
+ );
29
+ </script>
30
+
31
+ <svelte:head>
32
+ <title>Error {status}</title>
33
+ </svelte:head>
34
+
35
+ <main class="flex min-h-screen flex-col bg-primary">
36
+ <!-- Main content -->
37
+ <div class="flex flex-1 items-center justify-center px-8 pb-16">
38
+ <div class="flex flex-col items-center gap-16 md:flex-row md:gap-24">
39
+
40
+ <!-- Left: Icon in circle -->
41
+ <div class="shrink-0">
42
+ <div class="flex h-72 w-72 items-center justify-center rounded-full bg-error">
43
+ <div class="text-error opacity-90 animate-bounce duration-300" >
44
+ <LightbulbOff width={140} height={140} />
45
+ </div>
46
+ </div>
47
+ </div>
48
+
49
+ <!-- Right: Text content -->
50
+ <div class="flex flex-col gap-4 items-center ">
51
+ <!-- Big status number -->
52
+ <p
53
+ class="leading-none text-error select-none text-[clamp(6rem,15vw,11rem)] "
54
+ >
55
+ {status}
56
+ </p>
57
+
58
+ <!-- Title -->
59
+ <h1 class="text-lg font-bold uppercase tracking-widest text-content-secondary">
60
+ {title}
61
+ </h1>
62
+
63
+ <!-- Hint -->
64
+ <p class="font-light italic" >
65
+ {hint}
66
+ </p>
67
+
68
+ <!-- CTA -->
69
+ <div class="mt-10 flex items-center gap-8">
70
+ <a
71
+ href={resolve("/", {})}
72
+ class="btn btn-primary"
73
+ >
74
+ GO TO HOME
75
+ <span class="inline-block transition-transform group-hover:translate-x-1">→</span>
76
+ </a>
77
+
78
+ <button
79
+ onclick={() => history.back()}
80
+ class="btn btn-secondary"
81
+ >
82
+ Go back
83
+ </button>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </main>
89
+
90
+ <style>
91
+ @keyframes float {
92
+ 0%, 100% { transform: translateY(0px); }
93
+ 50% { transform: translateY(-12px); }
94
+ }
95
+ </style>
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import '../app.css';
3
+ import { updated } from '$app/state';
4
+ import { beforeNavigate } from '$app/navigation';
5
+ import { snackStore, loaderStore, configStore } from '@aryagg/ui-kit';
6
+ import { SnackBar, Loader } from '@aryagg/ui-kit';
7
+ import { ESnackType } from '@aryagg/types';
8
+
9
+ let { children, data } = $props();
10
+
11
+
12
+ $effect(() => {
13
+ // Set synchronously so children have config available on first render (SSR + CSR)
14
+ if (data.configError) {
15
+ snackStore.show({ type: ESnackType.DANGER, message: data.configError });
16
+ }
17
+ if (data.config) {
18
+ configStore.set(data.config);
19
+ }
20
+ });
21
+
22
+ // When a new build is deployed, force a full-page reload on the next navigation
23
+ // so the user always runs the latest code without a manual refresh.
24
+ beforeNavigate(({ willUnload, to }) => {
25
+ if (updated.current && !willUnload && to?.url) {
26
+ location.href = to.url.href;
27
+ }
28
+ });
29
+ </script>
30
+
31
+ {#if snackStore.current}<SnackBar />{/if}
32
+ {#if loaderStore.isVisible}<Loader />{/if}
33
+
34
+ <div class="h-screen w-screen">
35
+ {@render children()}
36
+ </div>
@@ -0,0 +1,24 @@
1
+ import { PUBLIC_CONFIG_ENV, PUBLIC_BASE_PATH } from "$env/static/public";
2
+ import type { LayoutLoad } from "./$types";
3
+
4
+ export const load: LayoutLoad = async ({ fetch }) => {
5
+ if (!PUBLIC_CONFIG_ENV) {
6
+ console.error("PUBLIC_CONFIG_ENV is not set");
7
+ return { config: null, configError: "Configuration environment missing" };
8
+ }
9
+
10
+ try {
11
+ const res = await fetch(`${PUBLIC_BASE_PATH}/config/config.${PUBLIC_CONFIG_ENV}.json`);
12
+
13
+ if (!res.ok) {
14
+ throw new Error(`Failed to load config.${PUBLIC_CONFIG_ENV}.json`);
15
+ }
16
+
17
+ const config = await res.json();
18
+ return { config, configError: null };
19
+
20
+ } catch (err) {
21
+ console.error(err);
22
+ return { config: null, configError: "Failed to load configuration" };
23
+ }
24
+ };
@@ -0,0 +1,192 @@
1
+ <script lang="ts">
2
+ import * as m from '$lib/paraglide/messages';
3
+ import { Topbar } from '$widgets/topbar';
4
+ import {
5
+ ArrowRight,
6
+ LightningCharge,
7
+ ShieldLock,
8
+ Sun,
9
+ Globe2,
10
+ Grid3x3Gap,
11
+ CodeSlash,
12
+ } from 'svelte-bootstrap-icons';
13
+ import { resolve } from '$app/paths';
14
+
15
+ const features = [
16
+ {
17
+ title: 'Authentication',
18
+ desc: 'Login, register, forgot & reset password flows with server-side session management.',
19
+ tag: 'Ready',
20
+ href: resolve('/register', {}),
21
+ icon: ShieldLock,
22
+ },
23
+ {
24
+ title: 'Theming System',
25
+ desc: 'Light and dark themes using CSS variables with persistent user preference storage.',
26
+ tag: 'Built-in',
27
+ href: null,
28
+ icon: Sun,
29
+ },
30
+ {
31
+ title: 'Internationalization',
32
+ desc: '8 languages out of the box with Paraglide. RTL-aware layout support included.',
33
+ tag: '8 Languages',
34
+ href: null,
35
+ icon: Globe2,
36
+ },
37
+ {
38
+ title: 'Component Library',
39
+ desc: 'Over 20 pre-built UI components ready to use and customise across your app.',
40
+ tag: '20+ Items',
41
+ href: null,
42
+ icon: Grid3x3Gap,
43
+ },
44
+ {
45
+ title: 'TypeScript',
46
+ desc: 'Strict TypeScript throughout — full type safety from routes to components.',
47
+ tag: 'Strict Mode',
48
+ href: null,
49
+ icon: CodeSlash,
50
+ },
51
+ ];
52
+ </script>
53
+
54
+ <svelte:head>
55
+ <title>{m.home_title()}</title>
56
+ <meta name="description" content={m.home_description()} />
57
+ </svelte:head>
58
+
59
+ <div class="min-h-screen flex flex-col bg-surface-primary text-primary overflow-x-hidden">
60
+
61
+ <Topbar />
62
+
63
+
64
+ <!-- HERO -->
65
+ <section class="flex-1 flex flex-col items-center justify-center px-6 py-20 text-center">
66
+
67
+ <!-- Logo mark -->
68
+ <div class="relative mb-10">
69
+ <div class="absolute inset-0 rounded-full bg-accent/15 blur-3xl scale-[2.5] pointer-events-none"></div>
70
+ <div class="relative w-20 h-20 rounded-3xl bg-accent flex items-center justify-center shadow-2xl rotate-12">
71
+ <span class="-rotate-12 text-on-accent">
72
+ <LightningCharge width={36} height={36} />
73
+ </span>
74
+ </div>
75
+ </div>
76
+
77
+ <!-- Eyebrow badge -->
78
+ <div class="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-accent/10 border border-accent/20 text-accent text-xs font-semibold tracking-[0.15em] uppercase mb-7">
79
+ <span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
80
+ {m.hero_eyebrow()}
81
+ </div>
82
+
83
+ <!-- Heading -->
84
+ <h1 class="text-5xl md:text-7xl font-bold leading-[1.06] tracking-tight text-primary mb-5 max-w-2xl">
85
+ {m.home_welcome()}
86
+ </h1>
87
+
88
+ <!-- Subtitle -->
89
+ <p class="text-secondary text-lg md:text-xl leading-relaxed max-w-md mb-10">
90
+ {m.home_subtitle()}
91
+ </p>
92
+
93
+ <!-- CTAs -->
94
+ <div class="flex flex-wrap items-center justify-center gap-3 mb-14">
95
+ <a
96
+ href={resolve('/register', {})}
97
+ class="no-underline inline-flex items-center gap-2 px-7 py-3.5 rounded-xl bg-accent text-on-accent font-semibold text-base transition-all duration-200 shadow-lg hover:opacity-90 hover:-translate-y-0.5"
98
+ >
99
+ {m.home_register_btn()}
100
+ <ArrowRight width={15} height={15} />
101
+ </a>
102
+ <a
103
+ href={resolve('/login', {})}
104
+ class="no-underline inline-flex items-center gap-2 px-7 py-3.5 rounded-xl border border-accent/40 text-accent hover:bg-accent/10 font-semibold text-base transition-all duration-200 hover:-translate-y-0.5"
105
+ >
106
+ {m.home_login_btn()}
107
+ </a>
108
+ </div>
109
+
110
+ <!-- Stats -->
111
+ <div class="flex items-center gap-6 sm:gap-10">
112
+ <div>
113
+ <p class="text-2xl font-bold text-primary">{m.hero_stat1_value()}</p>
114
+ <p class="text-[11px] text-tertiary uppercase tracking-widest mt-1">{m.hero_stat1_label()}</p>
115
+ </div>
116
+ <div class="w-px h-8 bg-border-primary"></div>
117
+ <div>
118
+ <p class="text-2xl font-bold text-primary">{m.hero_stat2_value()}</p>
119
+ <p class="text-[11px] text-tertiary uppercase tracking-widest mt-1">{m.hero_stat2_label()}</p>
120
+ </div>
121
+ <div class="w-px h-8 bg-border-primary"></div>
122
+ <div>
123
+ <p class="text-2xl font-bold text-primary">{m.hero_stat3_value()}</p>
124
+ <p class="text-[11px] text-tertiary uppercase tracking-widest mt-1">{m.hero_stat3_label()}</p>
125
+ </div>
126
+ </div>
127
+ </section>
128
+
129
+ <!-- FEATURES GRID -->
130
+ <section class="w-full max-w-5xl mx-auto px-6 pb-24">
131
+
132
+ <!-- Section divider label -->
133
+ <div class="flex items-center gap-4 mb-8">
134
+ <span class="h-px flex-1 bg-linear-to-r from-transparent to-accent/30"></span>
135
+ <span class="section-label text-accent">What's Included</span>
136
+ <span class="h-px flex-1 bg-linear-to-l from-transparent to-accent/30"></span>
137
+ </div>
138
+
139
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
140
+ {#each features as f, i (i)}
141
+ <div class="group flex items-start gap-4 p-5 rounded-2xl bg-surface-secondary border border-border-primary hover:border-accent/40 hover:bg-accent/3 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg">
142
+
143
+ <!-- Icon bubble -->
144
+ <div class="flex-none w-10 h-10 rounded-xl bg-accent/10 group-hover:bg-accent/20 flex items-center justify-center transition-colors shrink-0 text-accent">
145
+ <svelte:component this={f.icon} width={20} height={20} />
146
+ </div>
147
+
148
+ <!-- Text -->
149
+ <div class="flex-1 min-w-0">
150
+ <div class="flex items-center justify-between gap-2 mb-1.5">
151
+ <h3 class="text-sm font-semibold text-primary">{f.title}</h3>
152
+ <span class="flex-none text-[10px] font-semibold px-1.5 py-0.5 rounded-md bg-accent/10 text-accent">{f.tag}</span>
153
+ </div>
154
+ <p class="text-xs text-secondary leading-relaxed">{f.desc}</p>
155
+ {#if f.href}
156
+ <a href={f.href} class="no-underline inline-flex items-center gap-1 mt-3 text-xs font-medium text-accent hover:gap-2 transition-all">
157
+ Get started <ArrowRight width={11} height={11} />
158
+ </a>
159
+ {/if}
160
+ </div>
161
+ </div>
162
+ {/each}
163
+ </div>
164
+ </section>
165
+
166
+ <!-- MARQUEE STRIP — unchanged -->
167
+ <div class="sticky flex-none bottom-0 z-40 border-t border-border-primary bg-surface-primary/80 backdrop-blur-sm overflow-hidden py-3">
168
+ <div class="flex gap-10 w-max marquee-track items-center">
169
+ {#each [0,1,2,3,4,5,6,7] as i (i)}
170
+ <span class="text-tertiary text-xs tracking-[0.18em] uppercase whitespace-nowrap">{m.marquee_1()}</span>
171
+ <span class="text-accent text-xs">✦</span>
172
+ <span class="text-tertiary text-xs tracking-[0.18em] uppercase whitespace-nowrap">{m.marquee_2()}</span>
173
+ <span class="text-accent text-xs">◈</span>
174
+ <span class="text-tertiary text-xs tracking-[0.18em] uppercase whitespace-nowrap">{m.marquee_3()}</span>
175
+ <span class="text-accent text-xs">✦</span>
176
+ <span class="text-tertiary text-xs tracking-[0.18em] uppercase whitespace-nowrap">{m.marquee_4()}</span>
177
+ <span class="text-accent text-xs">◈</span>
178
+ {/each}
179
+ </div>
180
+ </div>
181
+
182
+ </div>
183
+
184
+ <style>
185
+ .marquee-track {
186
+ animation: marquee 28s linear infinite;
187
+ }
188
+ @keyframes marquee {
189
+ from { transform: translateX(0); }
190
+ to { transform: translateX(-50%); }
191
+ }
192
+ </style>
@@ -0,0 +1,3 @@
1
+ // since there's no dynamic data here, we can prerender
2
+ // it so that it gets served as a static asset in production
3
+ export const prerender = true;
@@ -0,0 +1,3 @@
1
+ {
2
+ "name": "local"
3
+ }