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
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "super-svelte-skeleton",
3
+ "version": "0.0.3",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite dev",
7
+ "build": "vite build",
8
+ "preview": "vite preview",
9
+ "prepare": "svelte-kit sync || echo ''",
10
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
11
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
12
+ "lint": "prettier --check . && eslint .",
13
+ "format": "prettier --write .",
14
+ "machine-translate": "inlang machine translate --project project.inlang"
15
+ },
16
+ "devDependencies": {
17
+ "@aryagg/theme": "^0.0.1",
18
+ "@aryagg/types": "^1.1.0",
19
+ "@eslint/compat": "^2.0.2",
20
+ "@eslint/js": "^10.0.1",
21
+ "@fontsource/fira-mono": "^5.2.7",
22
+ "@inlang/cli": "^3.1.9",
23
+ "@inlang/paraglide-js": "^2.16.0",
24
+ "@inlang/plugin-m-function-matcher": "^2.2.6",
25
+ "@inlang/plugin-message-format": "^4.4.0",
26
+ "@neoconfetti/svelte": "^2.2.2",
27
+ "@sveltejs/kit": "^2.53.4",
28
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
29
+ "@tailwindcss/vite": "^4.2.1",
30
+ "@types/node": "^25",
31
+ "eslint": "^10.0.2",
32
+ "eslint-config-prettier": "^10.1.8",
33
+ "eslint-plugin-svelte": "^3.15.0",
34
+ "globals": "^17.4.0",
35
+ "prettier": "^3.8.3",
36
+ "prettier-plugin-svelte": "^3.5.2",
37
+ "prettier-plugin-tailwindcss": "^0.7.4",
38
+ "svelte": "^5.53.7",
39
+ "svelte-check": "^4.4.4",
40
+ "tailwindcss": "^4.2.1",
41
+ "typescript": "^5.9.3",
42
+ "typescript-eslint": "^8.56.1",
43
+ "vite": "^7.3.1"
44
+ },
45
+ "dependencies": {
46
+ "@aryagg/ui-kit": "^0.2.0",
47
+ "@aryagg/utils": "^1.1.0",
48
+ "@inlang/paraglide-sveltekit": "^0.16.1",
49
+ "@sveltejs/adapter-node": "^5.5.4",
50
+ "axios": "^1.13.6",
51
+ "svelte-bootstrap-icons": "^3.3.0",
52
+ "unplugin-auto-import": "^21.0.0"
53
+ }
54
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "modules": [
3
+ "node_modules/@inlang/plugin-message-format/dist/index.js",
4
+ "node_modules/@inlang/plugin-m-function-matcher/dist/index.js"
5
+ ],
6
+ "plugin.inlang.messageFormat": {
7
+ "pathPattern": "./messages/{locale}.json"
8
+ },
9
+ "baseLocale": "en",
10
+ "locales": [
11
+ "en",
12
+ "es",
13
+ "ar"
14
+ ]
15
+ }
package/src/app.css ADDED
@@ -0,0 +1,8 @@
1
+ /* 1. Shared design system — imports Tailwind base + theme colors + global styles */
2
+ @import "@aryagg/theme";
3
+
4
+ /* 2. Scan ui-kit for Tailwind classes (node_modules are skipped by default in v4) */
5
+ @source "../node_modules/@aryagg/ui-kit/dist";
6
+
7
+ /* 3. Project config — adds your custom colors/plugins on top of the theme */
8
+ @config "./lib/tailwind.config.ts";
package/src/app.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ // See https://svelte.dev/docs/kit/types#app.d.ts
2
+ // for information about these interfaces
3
+ /// <reference path="../.svelte-kit/ambient.d.ts" />
4
+ declare global {
5
+ namespace App {
6
+ /** Error shape returned to error pages */
7
+ interface Error {
8
+ message: string;
9
+ code?: string;
10
+ }
11
+ /** Server-side locals set in hooks.server.ts */
12
+ interface Locals {
13
+ user: IAuthUser | null;
14
+ isAuthenticated: boolean;
15
+ }
16
+
17
+ }
18
+ }
19
+
20
+ export { };
package/src/app.html ADDED
@@ -0,0 +1,40 @@
1
+ <!doctype html>
2
+
3
+ <html lang="%paraglide.lang%" dir="%paraglide.dir%">
4
+
5
+ <head>
6
+ <meta charset="utf-8" />
7
+ <link rel="icon" href="%sveltekit.assets%/favicon.svg?v=2" />
8
+
9
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
10
+
11
+ <title>%sveltekit.env.PUBLIC_SITE_NAME%</title>
12
+
13
+ <meta name="description" content="%sveltekit.env.PUBLIC_SITE_DESCRIPTION%" />
14
+
15
+ <meta name="theme-color" content="#3b82f6" />
16
+ <meta name="color-scheme" content="light dark" />
17
+
18
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
19
+
20
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
21
+
22
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
23
+ rel="stylesheet" />
24
+
25
+ %sveltekit.head%
26
+
27
+ <link id="theme-style" rel="stylesheet" href="styles/theme-light.css" />
28
+
29
+ <script>
30
+ if (localStorage.getItem('theme') === 'dark') {
31
+ document.getElementById('theme-style').href = 'styles/theme-dark.css';
32
+ }
33
+ </script>
34
+ </head>
35
+
36
+ <body data-sveltekit-preload-data="hover">
37
+ <div style="display: contents">%sveltekit.body%</div>
38
+ </body>
39
+
40
+ </html>
@@ -0,0 +1,35 @@
1
+ /* eslint-disable */
2
+ /* prettier-ignore */
3
+ // @ts-nocheck
4
+ // noinspection JSUnusedGlobalSymbols
5
+ // Generated by unplugin-auto-import
6
+ // biome-ignore lint: disable
7
+ export {}
8
+ declare global {
9
+ const afterUpdate: typeof import('svelte').afterUpdate
10
+ const beforeUpdate: typeof import('svelte').beforeUpdate
11
+ const blur: typeof import('svelte/transition').blur
12
+ const createEventDispatcher: typeof import('svelte').createEventDispatcher
13
+ const crossfade: typeof import('svelte/transition').crossfade
14
+ const derived: typeof import('svelte/store').derived
15
+ const draw: typeof import('svelte/transition').draw
16
+ const fade: typeof import('svelte/transition').fade
17
+ const fly: typeof import('svelte/transition').fly
18
+ const get: typeof import('svelte/store').get
19
+ const getAllContexts: typeof import('svelte').getAllContexts
20
+ const getContext: typeof import('svelte').getContext
21
+ const goto: typeof import('$app/navigation').goto
22
+ const hasContext: typeof import('svelte').hasContext
23
+ const invalidate: typeof import('$app/navigation').invalidate
24
+ const invalidateAll: typeof import('$app/navigation').invalidateAll
25
+ const m: typeof import('$lib/paraglide/messages')
26
+ const onDestroy: typeof import('svelte').onDestroy
27
+ const onMount: typeof import('svelte').onMount
28
+ const readable: typeof import('svelte/store').readable
29
+ const redirect: typeof import('@sveltejs/kit').redirect
30
+ const scale: typeof import('svelte/transition').scale
31
+ const setContext: typeof import('svelte').setContext
32
+ const slide: typeof import('svelte/transition').slide
33
+ const tick: typeof import('svelte').tick
34
+ const writable: typeof import('svelte/store').writable
35
+ }
@@ -0,0 +1,17 @@
1
+ // SvelteKit client hooks - run in the browser
2
+ // Handle client-side errors and navigation events
3
+
4
+ import type { HandleClientError } from '@sveltejs/kit';
5
+ /**
6
+ * Global client-side error handler
7
+ * Catches unhandled errors in load functions and components
8
+ */
9
+ export const handleError: HandleClientError = ({ error, event }) => {
10
+ // In production, send to error tracking
11
+ console.error('Client error:', error, 'URL:', event.url.pathname);
12
+
13
+ return {
14
+ message: 'Something went wrong. Please refresh and try again.',
15
+ code: 'CLIENT_HOOK_ERROR'
16
+ };
17
+ };
@@ -0,0 +1,73 @@
1
+ // src/hooks.server.ts
2
+ import type { Handle, HandleFetch, HandleServerError } from '@sveltejs/kit';
3
+ import { paraglideMiddleware } from '$lib/paraglide/server';
4
+ import { getTextDirection } from '$lib/paraglide/runtime';
5
+ import { sequence } from '@sveltejs/kit/hooks';
6
+ import { AUTH_COOKIE_NAME } from '$shared/config';
7
+ import { env } from '$env/dynamic/private';
8
+
9
+ /* Paraglide: inject locale + direction into HTML */
10
+ const handleParaglide: Handle = ({ event, resolve }) =>
11
+ paraglideMiddleware(event.request, ({ request, locale }) => {
12
+ event.request = request;
13
+ return resolve(event, {
14
+ transformPageChunk: ({ html }) =>
15
+ html
16
+ .replace('%paraglide.lang%', locale)
17
+ .replace('%paraglide.dir%', getTextDirection(locale))
18
+ });
19
+ });
20
+
21
+ /* Auth: block access if no session cookie */
22
+ const authHandle: Handle = async ({ event, resolve }) => {
23
+ const sessionCookie = event.cookies.get(AUTH_COOKIE_NAME);
24
+ if(sessionCookie) {
25
+ try {
26
+ const session = JSON.parse(atob(sessionCookie));
27
+ if(session?.user) {
28
+ event.locals.user = session.user;
29
+ event.locals.isAuthenticated = true;
30
+ }
31
+
32
+ } catch(err) {
33
+ console.error(err);
34
+ event.cookies.delete(AUTH_COOKIE_NAME, { path: '/' });
35
+ }
36
+ } else {
37
+ event.locals.user = null;
38
+ event.locals.isAuthenticated = false;
39
+ }
40
+ return resolve(event);
41
+ };
42
+
43
+ /* Combine middleware in order */
44
+ export const handle = sequence(handleParaglide, authHandle);
45
+
46
+ /* Add headers to all server-side fetch calls, runs before every server‑side fetch */
47
+ export const handleFetch: HandleFetch = async ({ request, fetch }) => {
48
+ request.headers.set('X-API-Key', env.API_KEY ?? '');
49
+ return fetch(request);
50
+ };
51
+
52
+ /* Log all server errors */
53
+ export const handleError: HandleServerError = ({ error, event }) => {
54
+ console.error('🔥 SERVER ERROR:', error, 'URL:', event.url.pathname);
55
+
56
+ return {
57
+ message: 'Something went wrong on the server.'
58
+ };
59
+ };
60
+
61
+
62
+ // import { redirect } from '@sveltejs/kit';
63
+
64
+ // export function handle({ event, resolve }) {
65
+ // // Force all routes to lowercase so routing becomes case‑insensitive
66
+ // const lower = event.url.pathname.toLowerCase();
67
+
68
+ // if (event.url.pathname !== lower) {
69
+ // return redirect(301, lower + event.url.search);
70
+ // }
71
+
72
+ // return resolve(event);
73
+ // }
package/src/hooks.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { RequestEvent } from '@sveltejs/kit';
2
+ import { deLocalizeUrl } from '$lib/paraglide/runtime';
3
+
4
+ export const reroute = (request: RequestEvent) => deLocalizeUrl(request.url).pathname;
5
+ // If you export a function named reroute, SvelteKit will automatically run it on every request before routing.
6
+ // Rerouting helper for Paraglide i18n.
7
+ // This removes the locale prefix from the URL (e.g. /en/about → /about)
8
+ // so SvelteKit always receives a clean, non‑localized path.
9
+ // Paraglide adds this to ensure routing works normally even when URLs contain language codes.
10
+
11
+
12
+ // 1. handle - Runs before every request; used for middleware logic like auth, i18n, headers, etc.
13
+ // 2. reroute - Runs before routing; lets you rewrite URLs (e.g., remove /en from /en/about).
14
+ // 3. handleFetch - Runs on every fetch() call; lets you modify outgoing requests (add tokens, headers).
15
+ // 4. handleError - Runs whenever an error occurs; used for logging or sending errors to monitoring tools.
@@ -0,0 +1,11 @@
1
+ const BASE = '/auth/';
2
+
3
+ export const AUTH = {
4
+ LOGIN: `${BASE}login`,
5
+ LOGOUT: `${BASE}logout`,
6
+ REGISTER: `${BASE}register`,
7
+ REFRESH: `${BASE}refresh`,
8
+ ME: `${BASE}me`,
9
+ FORGOT_PASSWORD: `${BASE}forgot-password`,
10
+ RESET_PASSWORD: `${BASE}reset-password`,
11
+ };
@@ -0,0 +1,35 @@
1
+ // Auth service — domain layer that owns the HTTP + UI side-effect decision.
2
+ // snackStore lives here (not in the infra layer below it).
3
+
4
+ import { snackStore } from '@aryagg/ui-kit';
5
+ import { AUTH } from './endpoints';
6
+ import type { LoginPayload, RegisterPayload, ResetPayload, AuthToken, IAuthUser } from '../types';
7
+ import { handleApiResponse } from '@aryagg/utils';
8
+ import { httpClient } from '$shared';
9
+
10
+ async function callWithToast<T>(
11
+ promise: ReturnType<typeof handleApiResponse<T>>,
12
+ ): Promise<Awaited<ReturnType<typeof handleApiResponse<T>>>> {
13
+ const result = await promise;
14
+ if (!result.isSuccess) snackStore.showError(result.message!);
15
+ return result;
16
+ }
17
+
18
+ export const authApi = {
19
+ login: (body: LoginPayload) =>
20
+ callWithToast(handleApiResponse<AuthToken>(httpClient.post(AUTH.LOGIN, body), 'Login failed.')),
21
+
22
+ register: (body: RegisterPayload) =>
23
+ callWithToast(handleApiResponse<AuthToken>(httpClient.post(AUTH.REGISTER, body), 'Registration failed.')),
24
+
25
+ forgotPassword: (email: string) =>
26
+ callWithToast(handleApiResponse<void>(httpClient.post(AUTH.FORGOT_PASSWORD, { email }), 'Request failed.')),
27
+
28
+ resetPassword: (body: ResetPayload) =>
29
+ callWithToast(handleApiResponse<void>(httpClient.post(AUTH.RESET_PASSWORD, body), 'Reset failed.')),
30
+
31
+ me: () => handleApiResponse<IAuthUser>(httpClient.get(AUTH.ME), 'Could not load profile.'),
32
+ logout: () => handleApiResponse<void>(httpClient.post(AUTH.LOGOUT, {}), 'Logout failed.'),
33
+ refresh: (body: { refreshToken: string }) =>
34
+ handleApiResponse<AuthToken>(httpClient.post(AUTH.REFRESH, body), 'Token refresh failed.'),
35
+ };
@@ -0,0 +1,9 @@
1
+ export { authStore } from './store.svelte';
2
+ export { authApi } from './api/service';
3
+ export type {
4
+ LoginPayload,
5
+ RegisterPayload,
6
+ ResetPayload,
7
+ AuthToken,
8
+ IAuthUser,
9
+ } from './types';
@@ -0,0 +1,50 @@
1
+ import { EUserRole } from '@aryagg/types';
2
+ import type { IAuthUser } from './types';
3
+
4
+ let _authState = $state<IAuthUser | null>(null);
5
+
6
+ const isAdmin = $derived(
7
+ _authState?.role === EUserRole.ADMIN ||
8
+ _authState?.role === EUserRole.SUPER_ADMIN,
9
+ );
10
+
11
+ const isSuperAdmin = $derived(_authState?.role === EUserRole.SUPER_ADMIN);
12
+
13
+ const displayName = $derived(_authState?.name ?? _authState?.email ?? '');
14
+
15
+ function setUser(user: IAuthUser) {
16
+ _authState = user;
17
+ }
18
+
19
+ function clearUser() {
20
+ _authState = null;
21
+ }
22
+
23
+ function hasRole(role: EUserRole): boolean {
24
+ if (!_authState) return false;
25
+ const hierarchy = [
26
+ EUserRole.GUEST,
27
+ EUserRole.USER,
28
+ EUserRole.MANAGER,
29
+ EUserRole.ADMIN,
30
+ EUserRole.SUPER_ADMIN,
31
+ ];
32
+ return hierarchy.indexOf(_authState.role as EUserRole) >= hierarchy.indexOf(role);
33
+ }
34
+
35
+ function hasPermission(roles: EUserRole[]): boolean {
36
+ return roles.some((role) => hasRole(role));
37
+ }
38
+
39
+ export const authStore = {
40
+ get state() { return _authState; },
41
+ get user() { return _authState; },
42
+ get isAuthenticated() { return !!_authState; },
43
+ get isAdmin() { return isAdmin; },
44
+ get isSuperAdmin() { return isSuperAdmin; },
45
+ get displayName() { return displayName; },
46
+ setUser,
47
+ clearUser,
48
+ hasRole,
49
+ hasPermission,
50
+ };
@@ -0,0 +1,33 @@
1
+ // ── Request payloads ──────────────────────────────────────────────────────────
2
+
3
+ export interface LoginPayload {
4
+ email: string;
5
+ password: string;
6
+ }
7
+
8
+ export interface RegisterPayload {
9
+ name: string;
10
+ email: string;
11
+ password: string;
12
+ }
13
+
14
+ export interface ResetPayload {
15
+ token: string;
16
+ password: string;
17
+ }
18
+
19
+ // ── Response shapes ───────────────────────────────────────────────────────────
20
+
21
+ export interface AuthToken {
22
+ accessToken: string;
23
+ refreshToken: string;
24
+ expiresIn: number;
25
+ }
26
+
27
+ export interface IAuthUser {
28
+ id: string;
29
+ name: string;
30
+ email: string;
31
+ role: string;
32
+ avatar?: string;
33
+ }
@@ -0,0 +1,6 @@
1
+ const BASE = '/users/';
2
+
3
+ export const USER = {
4
+ LIST: BASE,
5
+ BY_ID: `${BASE}:id`,
6
+ };
@@ -0,0 +1,10 @@
1
+ import { httpClient, handleApiResponse, buildUrl } from '$shared/api';
2
+ import { USER } from './endpoints';
3
+ import type { User, UpdateUserPayload } from '../types';
4
+
5
+ export const userApi = {
6
+ list: () => handleApiResponse<User[]>(httpClient.get(USER.LIST), 'Failed to load users.'),
7
+ get: (id: string) => handleApiResponse<User>(httpClient.get(buildUrl(USER.BY_ID, { id })), 'Failed to load user.'),
8
+ update: (id: string, body: UpdateUserPayload) => handleApiResponse<User>(httpClient.put(buildUrl(USER.BY_ID, { id }), body), 'Failed to update user.'),
9
+ remove: (id: string) => handleApiResponse<void>(httpClient.delete(buildUrl(USER.BY_ID, { id })), 'Failed to delete user.'),
10
+ };
@@ -0,0 +1,2 @@
1
+ export { userApi } from './api/service';
2
+ export type { User, UpdateUserPayload } from './types';
@@ -0,0 +1,18 @@
1
+ // ── Request payloads ──────────────────────────────────────────────────────────
2
+
3
+ export interface UpdateUserPayload {
4
+ name?: string;
5
+ email?: string;
6
+ }
7
+
8
+ // ── Response shapes ───────────────────────────────────────────────────────────
9
+
10
+ export interface User {
11
+ id: string;
12
+ name: string;
13
+ email: string;
14
+ role: string;
15
+ avatar?: string;
16
+ createdAt: string;
17
+ updatedAt: string;
18
+ }
@@ -0,0 +1,33 @@
1
+ import { PUBLIC_BASE_PATH } from '$env/static/public';
2
+ import { ETheme, EInputType, type IFormField } from '@aryagg/types';
3
+
4
+ export const THEME_FILES = [
5
+ { name: 'Light', href: `${PUBLIC_BASE_PATH}/theme-light.css`, value: ETheme.LIGHT },
6
+ { name: 'Dark', href: `${PUBLIC_BASE_PATH}/theme-dark.css`, value: ETheme.DARK },
7
+ ];
8
+
9
+ export const THEME_INPUTS: IFormField[] = [
10
+ {
11
+ id: 'site-title', key: 'title', label: 'Site title',
12
+ placeholder: 'Theme Studio', type: EInputType.TEXT, value: 'Theme Studio',
13
+ required: true, helperText: 'Shown in the browser tab and shared links',
14
+ attributes: { maxlength: 60 },
15
+ },
16
+ {
17
+ id: 'meta-description', key: 'metaDescription', label: 'Meta description',
18
+ placeholder: 'Design tokens · CSS variables · Live preview',
19
+ type: EInputType.TEXTAREA, value: 'Design tokens · CSS variables · Live preview',
20
+ required: true, helperText: 'Used for SEO and link previews',
21
+ attributes: { rows: 3, maxlength: 160 },
22
+ },
23
+ {
24
+ id: 'logo', key: 'logo', label: 'Logo', type: EInputType.FILE,
25
+ required: false, helperText: 'Recommended 256×256 PNG', placeholder: 'Upload logo',
26
+ attributes: { accept: '.png,.jpg,.jpeg,.svg,.webp' }, multiple: false,
27
+ },
28
+ {
29
+ id: 'favicon', key: 'favicon', label: 'Favicon', type: EInputType.FILE,
30
+ required: false, helperText: '32×32 PNG or ICO', placeholder: 'Upload favicon',
31
+ attributes: { accept: '.png,.ico' }, multiple: false,
32
+ },
33
+ ];
@@ -0,0 +1,3 @@
1
+ export { default as ThemeEditorWidget } from './ui/ThemeEditorWidget.svelte';
2
+ export { THEME_FILES, THEME_INPUTS } from './constants';
3
+ export type { ThemeEntry, Tab } from './types';
@@ -0,0 +1,10 @@
1
+ import type { ETheme, IGenericObject } from '@aryagg/types';
2
+
3
+ export interface ThemeEntry {
4
+ name: string;
5
+ value: ETheme;
6
+ href: string;
7
+ colors: IGenericObject;
8
+ }
9
+
10
+ export type Tab = 'css' | 'preview';
@@ -0,0 +1,17 @@
1
+ <script lang="ts">
2
+ import type { ThemeEntry } from '../types';
3
+
4
+ let { currentTheme }: { currentTheme: ThemeEntry | undefined } = $props();
5
+ </script>
6
+
7
+ <div class="mt-4 overflow-auto rounded-xl bg-surface-2 p-4 font-mono text-[11px] leading-relaxed text-primary">
8
+ <code>
9
+ {#if currentTheme}
10
+ <span class="text-accent font-semibold">:root</span> &#123;<br />
11
+ {#each Object.entries(currentTheme.colors) as [k, v] (k)}
12
+ <span class="text-info pl-4">{k}</span><span class="text-tertiary">: </span><span class="text-success">{v}</span><span class="text-tertiary">;</span><br />
13
+ {/each}
14
+ &#125;
15
+ {/if}
16
+ </code>
17
+ </div>
@@ -0,0 +1,66 @@
1
+ <script lang="ts">
2
+ import { fade } from 'svelte/transition';
3
+ import { Copy } from 'svelte-bootstrap-icons';
4
+
5
+ let {
6
+ varName,
7
+ hex,
8
+ copied = false,
9
+ onColorChange,
10
+ onHexInput,
11
+ onCopy,
12
+ }: {
13
+ varName: string;
14
+ hex: string;
15
+ copied?: boolean;
16
+ onColorChange: (hex: string) => void;
17
+ onHexInput: (e: Event) => void;
18
+ onCopy: () => void;
19
+ } = $props();
20
+ </script>
21
+
22
+ <div
23
+ class="group border-border-primary/60 bg-surface-secondary hover:border-accent/30 flex flex-col overflow-hidden rounded-xl border transition-all duration-200 hover:shadow-md"
24
+ in:fade={{ duration: 80 }}
25
+ >
26
+ <!-- Swatch -->
27
+ <div class="relative h-16 w-full cursor-pointer overflow-hidden">
28
+ <div class="absolute inset-0" style="background:{hex}"></div>
29
+ <input
30
+ type="color"
31
+ class="absolute inset-0 h-full w-full cursor-pointer opacity-0"
32
+ value={hex.startsWith('#') && hex.length <= 7 ? hex : '#000000'}
33
+ oninput={(e) => onColorChange(e.currentTarget.value)}
34
+ />
35
+ <div
36
+ class="pointer-events-none absolute inset-0 flex items-center justify-center opacity-0 transition-all duration-150 group-hover:opacity-100"
37
+ style="background:rgba(0,0,0,0.18)"
38
+ >
39
+ <span class="rounded-full px-2 py-0.5 text-[9px] font-semibold text-white" style="background:rgba(0,0,0,0.45);backdrop-filter:blur(4px)">Edit</span>
40
+ </div>
41
+ </div>
42
+
43
+ <!-- Info -->
44
+ <div class="flex flex-col gap-1.5 p-2">
45
+ <p class="text-secondary truncate font-mono text-[9.5px] leading-none">{varName.replace('--', '')}</p>
46
+ <div class="flex items-center gap-1">
47
+ <input
48
+ type="text"
49
+ class="text-tertiary bg-surface-secondary/60 border-border-primary/40 focus:border-accent/60 focus:text-primary min-w-0 flex-1 rounded-lg border px-1.5 py-0.5 font-mono text-[10px] transition-colors focus:outline-none"
50
+ value={hex}
51
+ onchange={onHexInput}
52
+ />
53
+ <button
54
+ class="shrink-0 rounded-lg border p-1 transition-all
55
+ {copied ? 'border-success/30 bg-success/10 text-success' : 'border-border-primary/60 text-tertiary hover:border-primary/30 hover:text-primary'}"
56
+ onclick={onCopy}
57
+ >
58
+ {#if copied}
59
+ <span class="px-0.5 text-[9px] font-bold">✓</span>
60
+ {:else}
61
+ <Copy width="10" />
62
+ {/if}
63
+ </button>
64
+ </div>
65
+ </div>
66
+ </div>