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.
- package/.env.example +17 -0
- package/.github/workflows/ninja_i18n.yml +23 -0
- package/.prettierignore +9 -0
- package/.prettierrc +12 -0
- package/.vscode/extensions.json +5 -0
- package/.vscode/launch.json +15 -0
- package/.vscode/settings.json +30 -0
- package/README.md +237 -0
- package/_gitignore +27 -0
- package/eslint.config.js +40 -0
- package/messages/ar.json +70 -0
- package/messages/en.json +70 -0
- package/messages/es.json +70 -0
- package/package.json +54 -0
- package/project.inlang/settings.json +15 -0
- package/src/app.css +8 -0
- package/src/app.d.ts +20 -0
- package/src/app.html +40 -0
- package/src/auto-imports.d.ts +35 -0
- package/src/hooks.client.ts +17 -0
- package/src/hooks.server.ts +73 -0
- package/src/hooks.ts +15 -0
- package/src/lib/entities/auth/api/endpoints.ts +11 -0
- package/src/lib/entities/auth/api/service.ts +35 -0
- package/src/lib/entities/auth/index.ts +9 -0
- package/src/lib/entities/auth/store.svelte.ts +50 -0
- package/src/lib/entities/auth/types.ts +33 -0
- package/src/lib/entities/user/api/endpoints.ts +6 -0
- package/src/lib/entities/user/api/service.ts +10 -0
- package/src/lib/entities/user/index.ts +2 -0
- package/src/lib/entities/user/types.ts +18 -0
- package/src/lib/features/theme-editor/constants.ts +33 -0
- package/src/lib/features/theme-editor/index.ts +3 -0
- package/src/lib/features/theme-editor/types.ts +10 -0
- package/src/lib/features/theme-editor/ui/CSSOutput.svelte +17 -0
- package/src/lib/features/theme-editor/ui/ColorCard.svelte +66 -0
- package/src/lib/features/theme-editor/ui/ThemeEditorWidget.svelte +319 -0
- package/src/lib/features/theme-editor/ui/ThemePreview.svelte +121 -0
- package/src/lib/features/theme-editor/ui/TypographySettings.svelte +73 -0
- package/src/lib/features/theme-editor/utils.ts +10 -0
- package/src/lib/shared/api/client.ts +47 -0
- package/src/lib/shared/api/index.ts +3 -0
- package/src/lib/shared/api/types.ts +25 -0
- package/src/lib/shared/config/api.ts +1 -0
- package/src/lib/shared/config/index.ts +2 -0
- package/src/lib/shared/config/routes.ts +18 -0
- package/src/lib/shared/i18n/index.ts +1 -0
- package/src/lib/shared/index.ts +2 -0
- package/src/lib/tailwind.config.ts +28 -0
- package/src/lib/widgets/topbar/Topbar.svelte +122 -0
- package/src/lib/widgets/topbar/constants.ts +16 -0
- package/src/lib/widgets/topbar/index.ts +2 -0
- package/src/params/integer.ts +5 -0
- package/src/routes/(app)/(admin)/+layout.server.ts +14 -0
- package/src/routes/(app)/(admin)/admin/+page.svelte +101 -0
- package/src/routes/(app)/+layout.server.ts +9 -0
- package/src/routes/(app)/+layout.svelte +12 -0
- package/src/routes/(app)/settings/+page.svelte +48 -0
- package/src/routes/(app)/theme/+page.svelte +5 -0
- package/src/routes/(auth)/forgot-password/+page.svelte +83 -0
- package/src/routes/(auth)/login/+page.server.ts +66 -0
- package/src/routes/(auth)/login/+page.svelte +156 -0
- package/src/routes/(auth)/logout/+page.server.ts +16 -0
- package/src/routes/(auth)/register/+page.svelte +167 -0
- package/src/routes/(auth)/reset-password/+page.svelte +127 -0
- package/src/routes/+error.svelte +95 -0
- package/src/routes/+layout.svelte +36 -0
- package/src/routes/+layout.ts +24 -0
- package/src/routes/+page.svelte +192 -0
- package/src/routes/+page.ts +3 -0
- package/static/config/config.local.json +3 -0
- package/static/config/config.prod.json +3 -0
- package/static/favicon.svg +1 -0
- package/static/logo.svg +7 -0
- package/static/profile.avif +0 -0
- package/static/smile.jpg +0 -0
- package/static/styles/theme-dark.css +30 -0
- package/static/styles/theme-light.css +28 -0
- package/stats.html +4950 -0
- package/svelte.config.js +78 -0
- package/tsconfig.json +46 -0
- package/vite.config.ts +51 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
ArrowClockwise,
|
|
4
|
+
CodeSlash,
|
|
5
|
+
Copy,
|
|
6
|
+
Download,
|
|
7
|
+
Eye,
|
|
8
|
+
Save,
|
|
9
|
+
ArrowDownUp,
|
|
10
|
+
} from 'svelte-bootstrap-icons';
|
|
11
|
+
import { onMount } from 'svelte';
|
|
12
|
+
import { SvelteMap } from 'svelte/reactivity';
|
|
13
|
+
import { fade } from 'svelte/transition';
|
|
14
|
+
import axios from 'axios';
|
|
15
|
+
import { snackStore } from '@aryagg/ui-kit';
|
|
16
|
+
import { Tabs } from '@aryagg/ui-kit';
|
|
17
|
+
import { ETheme, THEMES_TABS } from '@aryagg/types';
|
|
18
|
+
import { deepClone } from '@aryagg/utils';
|
|
19
|
+
import ThemePreview from './ThemePreview.svelte';
|
|
20
|
+
import TypographySettings from './TypographySettings.svelte';
|
|
21
|
+
import ColorCard from './ColorCard.svelte';
|
|
22
|
+
import CSSOutput from './CSSOutput.svelte';
|
|
23
|
+
import { THEME_FILES } from '../constants';
|
|
24
|
+
import type { ThemeEntry, Tab } from '../types';
|
|
25
|
+
import { extractCSSVariables } from '../utils';
|
|
26
|
+
|
|
27
|
+
const DEFAULT_FONT = "'Inter', sans-serif";
|
|
28
|
+
|
|
29
|
+
let selectedTheme = $state<ETheme>(ETheme.LIGHT);
|
|
30
|
+
let variables = $state<ThemeEntry[]>([]);
|
|
31
|
+
let clonedVariables = $state<ThemeEntry[]>([]);
|
|
32
|
+
let rightTab = $state<Tab>('preview');
|
|
33
|
+
let copiedCss = $state(false);
|
|
34
|
+
let copiedVar = $state<string | null>(null);
|
|
35
|
+
let headingFontFamily = $state(DEFAULT_FONT);
|
|
36
|
+
let bodyFontFamily = $state(DEFAULT_FONT);
|
|
37
|
+
let savedHeadingFont = $state(DEFAULT_FONT);
|
|
38
|
+
let savedBodyFont = $state(DEFAULT_FONT);
|
|
39
|
+
let fileInput: HTMLInputElement;
|
|
40
|
+
|
|
41
|
+
const rightTabs = [
|
|
42
|
+
{ id: 'preview', label: 'App Preview', icon: Eye },
|
|
43
|
+
{ id: 'css', label: 'CSS Output', icon: CodeSlash },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
let currentTheme = $derived(variables.find((v) => v.value === selectedTheme));
|
|
47
|
+
|
|
48
|
+
let allVars = $derived.by(() => {
|
|
49
|
+
const colors = currentTheme?.colors;
|
|
50
|
+
if (!colors) return [] as { varName: string; hex: string }[];
|
|
51
|
+
return Object.entries(colors).map(([varName, hex]) => ({ varName, hex: hex as string }));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let groupedVars = $derived.by(() => {
|
|
55
|
+
const colors = currentTheme?.colors;
|
|
56
|
+
if (!colors) return [] as [string, { varName: string; hex: string }[]][];
|
|
57
|
+
const map = new SvelteMap<string, { varName: string; hex: string }[]>();
|
|
58
|
+
for (const [varName, hex] of Object.entries(colors)) {
|
|
59
|
+
const seg = varName.replace(/^--/, '').split('-')[0] ?? 'other';
|
|
60
|
+
const label = seg.charAt(0).toUpperCase() + seg.slice(1);
|
|
61
|
+
if (!map.has(label)) map.set(label, []);
|
|
62
|
+
map.get(label)!.push({ varName, hex: hex as string });
|
|
63
|
+
}
|
|
64
|
+
return [...map.entries()];
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let cssText = $derived.by(() => {
|
|
68
|
+
if (!currentTheme) return '';
|
|
69
|
+
const lines = Object.entries(currentTheme.colors).map(([k, v]) => ` ${k}: ${v};`).join('\n');
|
|
70
|
+
return `:root {\n${lines}\n}`;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
let isModified = $derived.by(() => {
|
|
74
|
+
const orig = clonedVariables.find((v) => v.value === selectedTheme);
|
|
75
|
+
const curr = variables.find((v) => v.value === selectedTheme);
|
|
76
|
+
if (!orig || !curr) return false;
|
|
77
|
+
return (
|
|
78
|
+
JSON.stringify(orig.colors) !== JSON.stringify(curr.colors) ||
|
|
79
|
+
headingFontFamily !== savedHeadingFont ||
|
|
80
|
+
bodyFontFamily !== savedBodyFont
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
onMount(() => {
|
|
85
|
+
const themeLink = document.getElementById('theme-style') as HTMLLinkElement | null;
|
|
86
|
+
if (!themeLink) return;
|
|
87
|
+
const observer = new MutationObserver(() => {
|
|
88
|
+
selectedTheme = themeLink.href.includes('dark') ? ETheme.DARK : ETheme.LIGHT;
|
|
89
|
+
});
|
|
90
|
+
observer.observe(themeLink, { attributes: true, attributeFilter: ['href'] });
|
|
91
|
+
return () => observer.disconnect();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
onMount(() => loadThemes());
|
|
95
|
+
|
|
96
|
+
async function loadThemes() {
|
|
97
|
+
await Promise.all(
|
|
98
|
+
THEME_FILES.map(async (theme) => {
|
|
99
|
+
const { data } = await axios.get(theme.href);
|
|
100
|
+
variables.push({ ...theme, colors: extractCSSVariables(data) });
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
clonedVariables = deepClone(variables);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function updateColor(varName: string, hex: string) {
|
|
107
|
+
variables = variables.map((v) =>
|
|
108
|
+
v.value === selectedTheme ? { ...v, colors: { ...v.colors, [varName]: hex } } : v,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function handleHexInput(varName: string, e: Event) {
|
|
113
|
+
const input = e.target as HTMLInputElement;
|
|
114
|
+
let val = input.value.trim();
|
|
115
|
+
if (!val.startsWith('#')) val = '#' + val;
|
|
116
|
+
if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(val)) {
|
|
117
|
+
updateColor(varName, val.toLowerCase());
|
|
118
|
+
} else {
|
|
119
|
+
const entry = variables.find((v) => v.value === selectedTheme);
|
|
120
|
+
if (entry) input.value = entry.colors[varName] as string;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function copyHex(varName: string, hex: string) {
|
|
125
|
+
await navigator.clipboard.writeText(hex);
|
|
126
|
+
copiedVar = varName;
|
|
127
|
+
setTimeout(() => (copiedVar = null), 1500);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function copyCss() {
|
|
131
|
+
await navigator.clipboard.writeText(cssText);
|
|
132
|
+
copiedCss = true;
|
|
133
|
+
setTimeout(() => (copiedCss = false), 2000);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function exportCss() {
|
|
137
|
+
const blob = new Blob([cssText], { type: 'text/css' });
|
|
138
|
+
const url = URL.createObjectURL(blob);
|
|
139
|
+
const a = document.createElement('a');
|
|
140
|
+
a.href = url;
|
|
141
|
+
a.download = `theme-${selectedTheme}.css`;
|
|
142
|
+
a.click();
|
|
143
|
+
URL.revokeObjectURL(url);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function handleImport(e: Event) {
|
|
147
|
+
const file = (e.target as HTMLInputElement).files?.[0];
|
|
148
|
+
if (!file) return;
|
|
149
|
+
const reader = new FileReader();
|
|
150
|
+
reader.onload = (ev) => {
|
|
151
|
+
const parsed = extractCSSVariables(ev.target?.result as string);
|
|
152
|
+
const entry = variables.find((v) => v.value === selectedTheme);
|
|
153
|
+
if (entry) {
|
|
154
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
155
|
+
entry.colors[k] = v;
|
|
156
|
+
updateColor(k, v);
|
|
157
|
+
}
|
|
158
|
+
variables = [...variables];
|
|
159
|
+
snackStore.showSuccess('Theme imported successfully');
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
reader.readAsText(file);
|
|
163
|
+
(e.target as HTMLInputElement).value = '';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function saveTheme() {
|
|
167
|
+
const theme = variables.find((v) => v.value === selectedTheme);
|
|
168
|
+
if (!theme) return;
|
|
169
|
+
for (const [k, v] of Object.entries(theme.colors)) {
|
|
170
|
+
document.documentElement.style.setProperty(k, v as string);
|
|
171
|
+
}
|
|
172
|
+
document.documentElement.style.setProperty('--font-heading', headingFontFamily);
|
|
173
|
+
document.documentElement.style.setProperty('--font-family', bodyFontFamily);
|
|
174
|
+
document.documentElement.style.setProperty('--font-body', bodyFontFamily);
|
|
175
|
+
clonedVariables = deepClone(variables);
|
|
176
|
+
savedHeadingFont = headingFontFamily;
|
|
177
|
+
savedBodyFont = bodyFontFamily;
|
|
178
|
+
snackStore.showSuccess('Theme applied to page');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function resetTheme() {
|
|
182
|
+
const original = clonedVariables.find((v) => v.value === selectedTheme);
|
|
183
|
+
if (!original) return;
|
|
184
|
+
variables = variables.map((v) => (v.value === selectedTheme ? deepClone(original) : v));
|
|
185
|
+
headingFontFamily = savedHeadingFont;
|
|
186
|
+
bodyFontFamily = savedBodyFont;
|
|
187
|
+
snackStore.showInfo('Theme reset to saved state');
|
|
188
|
+
}
|
|
189
|
+
</script>
|
|
190
|
+
|
|
191
|
+
<svelte:head>
|
|
192
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
193
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
|
194
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300..700&family=Instrument+Serif&family=JetBrains+Mono:wght@400..700&display=swap" rel="stylesheet" />
|
|
195
|
+
</svelte:head>
|
|
196
|
+
|
|
197
|
+
<input bind:this={fileInput} type="file" accept=".css" class="hidden" onchange={handleImport} />
|
|
198
|
+
|
|
199
|
+
<div class="bg-surface-secondary flex size-full flex-col overflow-hidden">
|
|
200
|
+
<!-- ── Page Title Strip ── -->
|
|
201
|
+
<div class="bg-surface-secondary shrink-0 px-10 py-2.5">
|
|
202
|
+
<div class="flex items-center gap-4">
|
|
203
|
+
<div>
|
|
204
|
+
<h1 class="text-3xl font-bold text-primary tracking-widest">Theme Studio</h1>
|
|
205
|
+
<p class="text-tertiary text-xs leading-tight pl-2">Design tokens · CSS variables · Live preview</p>
|
|
206
|
+
</div>
|
|
207
|
+
<div class="ml-auto flex items-center gap-1.5">
|
|
208
|
+
<button onclick={() => fileInput.click()} class="btn btn-sm">
|
|
209
|
+
<ArrowDownUp width="13" /> Import
|
|
210
|
+
</button>
|
|
211
|
+
<button onclick={copyCss} class="btn btn-sm {copiedCss ? 'border-success/30 text-success' : ''}">
|
|
212
|
+
<Copy width="13" /> {copiedCss ? 'Copied!' : 'Copy CSS'}
|
|
213
|
+
</button>
|
|
214
|
+
<button onclick={exportCss} class="btn btn-sm">
|
|
215
|
+
<Download width="13" /> Export
|
|
216
|
+
</button>
|
|
217
|
+
<div class="bg-border-primary/50 mx-1 h-5 w-px shrink-0"></div>
|
|
218
|
+
<button onclick={resetTheme} disabled={!isModified} class="btn btn-sm">
|
|
219
|
+
<ArrowClockwise width="13" /> Reset
|
|
220
|
+
</button>
|
|
221
|
+
<button onclick={saveTheme} disabled={!isModified} class="btn btn-primary btn-sm">
|
|
222
|
+
<Save width="13" /> Save & Apply
|
|
223
|
+
</button>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<!-- ── Main Grid ── -->
|
|
229
|
+
<div class="grid min-h-0 flex-1 grid-cols-1 gap-2.5 overflow-hidden px-10 pt-2.5 pb-4 lg:grid-cols-5">
|
|
230
|
+
|
|
231
|
+
<!-- Typography panel -->
|
|
232
|
+
<TypographySettings bind:headingFontFamily bind:bodyFontFamily />
|
|
233
|
+
|
|
234
|
+
<!-- LEFT: Variable editor -->
|
|
235
|
+
<div class="border-border-primary bg-surface-primary flex h-full flex-col overflow-hidden rounded-2xl border shadow-sm lg:col-span-2">
|
|
236
|
+
<div class="border-border-primary/60 flex shrink-0 items-center gap-3 px-4 py-3">
|
|
237
|
+
<div class="min-w-0 flex-1">
|
|
238
|
+
<p class="text-sm font-semibold">CSS Variables</p>
|
|
239
|
+
<p class="text-tertiary text-[11px]">
|
|
240
|
+
{allVars.length} tokens · click swatch to edit
|
|
241
|
+
{#if isModified}<span class="text-warning font-medium"> · unsaved</span>{/if}
|
|
242
|
+
</p>
|
|
243
|
+
</div>
|
|
244
|
+
<div class="bg-surface-secondary flex shrink-0 gap-0.5 rounded-xl p-0.5 text-xs">
|
|
245
|
+
<Tabs tabs={THEMES_TABS} bind:active={selectedTheme} />
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<div class="min-h-0 flex-1 overflow-y-auto p-4 pt-0">
|
|
250
|
+
{#if groupedVars.length === 0}
|
|
251
|
+
<div class="flex flex-col items-center justify-center gap-3 py-16">
|
|
252
|
+
<div class="border-border-primary border-t-accent size-8 animate-spin rounded-full border-2"></div>
|
|
253
|
+
<p class="text-tertiary text-xs">Loading variables…</p>
|
|
254
|
+
</div>
|
|
255
|
+
{:else}
|
|
256
|
+
<div class="flex flex-col gap-6">
|
|
257
|
+
{#each groupedVars as [group, entries] (group)}
|
|
258
|
+
<div class="card bg-surface-secondary/30!">
|
|
259
|
+
<div class="mb-3 flex items-center gap-2">
|
|
260
|
+
<p class="text-tertiary shrink-0 text-[10px] font-bold tracking-widest uppercase">{group}</p>
|
|
261
|
+
<div class="from-border-primary/50 h-px flex-1 bg-linear-to-r to-transparent"></div>
|
|
262
|
+
<span class="bg-surface-secondary text-tertiary rounded-full px-1.5 py-0.5 text-[9px] font-medium">{entries.length}</span>
|
|
263
|
+
</div>
|
|
264
|
+
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3 xl:grid-cols-4">
|
|
265
|
+
{#each entries as entry (entry.varName)}
|
|
266
|
+
<ColorCard
|
|
267
|
+
varName={entry.varName}
|
|
268
|
+
hex={entry.hex}
|
|
269
|
+
copied={copiedVar === entry.varName}
|
|
270
|
+
onColorChange={(hex) => updateColor(entry.varName, hex)}
|
|
271
|
+
onHexInput={(e) => handleHexInput(entry.varName, e)}
|
|
272
|
+
onCopy={() => copyHex(entry.varName, entry.hex)}
|
|
273
|
+
/>
|
|
274
|
+
{/each}
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
{/each}
|
|
278
|
+
</div>
|
|
279
|
+
{/if}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<!-- RIGHT: Preview / CSS -->
|
|
284
|
+
<div class="border-border-primary bg-surface-secondary flex h-full flex-col overflow-hidden rounded-2xl border shadow-sm lg:col-span-2">
|
|
285
|
+
<div class="border-border-primary/60 flex shrink-0 items-center gap-2 px-4 py-3">
|
|
286
|
+
<div class="bg-surface-secondary flex gap-0.5 rounded-xl p-0.5 text-xs">
|
|
287
|
+
<Tabs tabs={rightTabs} bind:active={rightTab} />
|
|
288
|
+
</div>
|
|
289
|
+
{#if rightTab === 'css'}
|
|
290
|
+
<div class="ml-auto flex items-center gap-1.5">
|
|
291
|
+
<button
|
|
292
|
+
onclick={copyCss}
|
|
293
|
+
class="flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs font-medium transition-all
|
|
294
|
+
{copiedCss ? 'border-success/30 bg-success/10 text-success' : 'border-border-primary text-secondary hover:border-primary/30 hover:text-primary'}"
|
|
295
|
+
>
|
|
296
|
+
<Copy /> {copiedCss ? 'Copied!' : 'Copy'}
|
|
297
|
+
</button>
|
|
298
|
+
<button
|
|
299
|
+
onclick={exportCss}
|
|
300
|
+
class="border-border-primary text-secondary hover:border-primary/30 hover:text-primary flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors"
|
|
301
|
+
>
|
|
302
|
+
<Download /> Export
|
|
303
|
+
</button>
|
|
304
|
+
</div>
|
|
305
|
+
{/if}
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
<div class="min-h-0 flex-1 overflow-auto">
|
|
309
|
+
{#if rightTab === 'css'}
|
|
310
|
+
<div in:fade={{ duration: 150 }}>
|
|
311
|
+
<CSSOutput {currentTheme} />
|
|
312
|
+
</div>
|
|
313
|
+
{:else}
|
|
314
|
+
<ThemePreview colors={currentTheme?.colors} />
|
|
315
|
+
{/if}
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { IGenericObject } from '@aryagg/types';
|
|
3
|
+
|
|
4
|
+
let { colors }: { colors: IGenericObject | undefined } = $props();
|
|
5
|
+
|
|
6
|
+
let styleVars = $derived(
|
|
7
|
+
colors
|
|
8
|
+
? Object.entries(colors)
|
|
9
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
10
|
+
.join(';')
|
|
11
|
+
: '',
|
|
12
|
+
);
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div
|
|
16
|
+
style={styleVars}
|
|
17
|
+
class="bg-surface-secondary text-primary flex h-full flex-col gap-3 overflow-y-auto p-4 pt-0 text-sm"
|
|
18
|
+
>
|
|
19
|
+
<!-- Metric Card -->
|
|
20
|
+
<div class="card bg-surface-secondary/30!">
|
|
21
|
+
<p class="text-tertiary mb-4 text-[10px] font-bold tracking-widest uppercase">Card</p>
|
|
22
|
+
<div class="flex flex-col gap-4">
|
|
23
|
+
<div class="flex items-start justify-between gap-3">
|
|
24
|
+
<div>
|
|
25
|
+
<p class="text-tertiary mb-1 text-xs">Total Revenue</p>
|
|
26
|
+
<p class="text-[26px] font-bold leading-none tracking-tight">$48,295</p>
|
|
27
|
+
<p class="text-success mt-1.5 text-xs font-medium">↑ 8.2% this month</p>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="bg-accent text-on-accent flex size-9 shrink-0 items-center justify-center rounded-xl text-sm font-bold">$</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div>
|
|
32
|
+
<div class="mb-1.5 flex items-center justify-between">
|
|
33
|
+
<p class="text-tertiary text-[10px]">Goal progress</p>
|
|
34
|
+
<p class="text-primary text-[10px] font-semibold">72%</p>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="bg-surface-secondary h-1.5 overflow-hidden rounded-full">
|
|
37
|
+
<div class="bg-accent h-full w-[72%] rounded-full"></div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- Form Controls -->
|
|
44
|
+
<div class="card bg-surface-secondary/30!">
|
|
45
|
+
<p class="text-tertiary mb-3 text-[10px] font-bold tracking-widest uppercase">Form Controls</p>
|
|
46
|
+
<div class="mb-3 grid grid-cols-2 gap-3">
|
|
47
|
+
<div class="form-group"><label for="pv-name">Full name *</label><input id="pv-name" type="text" placeholder="Alex Bennett" /></div>
|
|
48
|
+
<div class="form-group">
|
|
49
|
+
<label for="pv-email">Email</label>
|
|
50
|
+
<input id="pv-email" type="email" data-state="success" placeholder="alex@example.com" />
|
|
51
|
+
<small class="text-success pl-0.5">Looks good!</small>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="form-group">
|
|
54
|
+
<label for="pv-err">Error state</label>
|
|
55
|
+
<input id="pv-err" type="text" data-state="error" value="bad@value" />
|
|
56
|
+
<small class="text-error pl-0.5">Invalid format</small>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="form-group">
|
|
59
|
+
<label for="pv-role">Role</label>
|
|
60
|
+
<select id="pv-role"><option>Designer</option><option>Developer</option><option>Manager</option></select>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="flex flex-wrap gap-x-5 gap-y-2">
|
|
63
|
+
<label class="flex cursor-pointer items-center gap-1.5 text-xs" style="text-transform:none;letter-spacing:normal;color:var(--text-primary)">
|
|
64
|
+
<input type="checkbox" checked /> Remember me
|
|
65
|
+
</label>
|
|
66
|
+
<label class="flex cursor-pointer items-center gap-1.5 text-xs" style="text-transform:none;letter-spacing:normal;color:var(--text-primary)">
|
|
67
|
+
<input type="checkbox" disabled /> Disabled
|
|
68
|
+
</label>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="flex flex-wrap gap-x-5 gap-y-2">
|
|
71
|
+
<label class="flex cursor-pointer items-center gap-1.5 text-xs" style="text-transform:none;letter-spacing:normal;color:var(--text-primary)">
|
|
72
|
+
<input type="radio" name="pv-r" checked /> Option A
|
|
73
|
+
</label>
|
|
74
|
+
<label class="flex cursor-pointer items-center gap-1.5 text-xs" style="text-transform:none;letter-spacing:normal;color:var(--text-primary)">
|
|
75
|
+
<input type="radio" name="pv-r" /> Option B
|
|
76
|
+
</label>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- Buttons -->
|
|
82
|
+
<div class="card bg-surface-secondary/30!">
|
|
83
|
+
<p class="text-tertiary mb-3 text-[10px] font-bold tracking-widest uppercase">Buttons</p>
|
|
84
|
+
<div class="flex flex-wrap gap-2">
|
|
85
|
+
<button class="btn btn-primary btn-sm">Primary</button>
|
|
86
|
+
<button class="btn btn-secondary btn-sm">Secondary</button>
|
|
87
|
+
<button class="btn btn-muted btn-sm">Muted</button>
|
|
88
|
+
<button class="btn btn-ghost btn-sm">Ghost</button>
|
|
89
|
+
<button class="btn btn-outline btn-sm">Outline</button>
|
|
90
|
+
<button class="btn btn-success btn-sm">Success</button>
|
|
91
|
+
<button class="btn btn-danger btn-sm">Danger</button>
|
|
92
|
+
<button class="btn btn-info btn-sm">Info</button>
|
|
93
|
+
<button class="btn btn-primary btn-sm" disabled>Disabled</button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<!-- Badges -->
|
|
98
|
+
<div class="card bg-surface-secondary/30!">
|
|
99
|
+
<p class="text-tertiary mb-3 text-[10px] font-bold tracking-widest uppercase">Badges</p>
|
|
100
|
+
<div class="flex flex-wrap gap-2">
|
|
101
|
+
<span data-badge="success"><i></i>Active</span>
|
|
102
|
+
<span data-badge="warning"><i></i>Pending</span>
|
|
103
|
+
<span data-badge="error"><i></i>Failed</span>
|
|
104
|
+
<span data-badge="info"><i></i>Info</span>
|
|
105
|
+
<span data-badge="accent">Accent</span>
|
|
106
|
+
<span data-badge="outline">Draft</span>
|
|
107
|
+
<span data-badge>Archived</span>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<!-- Alerts -->
|
|
112
|
+
<div class="card bg-surface-secondary/30!">
|
|
113
|
+
<p class="text-tertiary mb-3 text-[10px] font-bold tracking-widest uppercase">Alerts</p>
|
|
114
|
+
<div class="grid grid-cols-2 gap-2">
|
|
115
|
+
<div data-alert="success"><i>✓</i> Saved successfully.</div>
|
|
116
|
+
<div data-alert="warning"><i>⚠</i> Low contrast detected.</div>
|
|
117
|
+
<div data-alert="error"><i>✕</i> Failed to apply theme.</div>
|
|
118
|
+
<div data-alert="info"><i>ℹ</i> 2 variables have no on-color.</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
type FontPreset = { label: string; family: string };
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
headingFontFamily = $bindable("'Inter', sans-serif"),
|
|
6
|
+
bodyFontFamily = $bindable("'Inter', sans-serif"),
|
|
7
|
+
}: { headingFontFamily?: string; bodyFontFamily?: string } = $props();
|
|
8
|
+
|
|
9
|
+
const FONT_PRESETS: FontPreset[] = [
|
|
10
|
+
{ label: 'Inter', family: "'Inter', sans-serif" },
|
|
11
|
+
{ label: 'Instrument Serif', family: "'Instrument Serif', Georgia, serif" },
|
|
12
|
+
{ label: 'JetBrains Mono', family: "'JetBrains Mono', monospace" },
|
|
13
|
+
{ label: 'System', family: 'system-ui, -apple-system, sans-serif' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
let selectedHeading = $derived(FONT_PRESETS.find((f) => f.family === headingFontFamily)?.label ?? 'Inter');
|
|
17
|
+
let selectedBody = $derived(FONT_PRESETS.find((f) => f.family === bodyFontFamily)?.label ?? 'Inter');
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<div class="border-border-primary bg-surface-primary flex h-full flex-col overflow-hidden rounded-2xl border shadow-sm">
|
|
21
|
+
<div class="border-border-primary/60 shrink-0 px-4 py-3">
|
|
22
|
+
<p class="text-sm font-semibold">Typography</p>
|
|
23
|
+
<p class="text-tertiary text-[11px]">Preview here · apply via Save & Apply</p>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4 pt-0">
|
|
27
|
+
<!-- Heading Font -->
|
|
28
|
+
<div class="card bg-surface-secondary/30!">
|
|
29
|
+
<p class="text-tertiary mb-2.5 text-[10px] font-bold tracking-widest uppercase">Heading Font</p>
|
|
30
|
+
<div class="grid grid-cols-2 gap-2">
|
|
31
|
+
{#each FONT_PRESETS as f (f.label)}
|
|
32
|
+
<button
|
|
33
|
+
onclick={() => (headingFontFamily = f.family)}
|
|
34
|
+
class="flex flex-col items-start gap-1.5 rounded-xl border p-3 text-left transition-all duration-150
|
|
35
|
+
{selectedHeading === f.label ? 'border-accent/40 bg-accent/5 shadow-sm' : 'border-border-primary/60 hover:border-primary/20 hover:bg-surface-primary/60'}"
|
|
36
|
+
>
|
|
37
|
+
<span class="text-[22px] leading-none font-semibold" style="font-family:{f.family}">Aa</span>
|
|
38
|
+
<span class="w-full truncate text-[9px] leading-none tracking-widest uppercase {selectedHeading === f.label ? 'text-accent font-semibold' : 'text-tertiary'}">{f.label}</span>
|
|
39
|
+
</button>
|
|
40
|
+
{/each}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<!-- Body Font -->
|
|
45
|
+
<div class="card bg-surface-secondary/30!">
|
|
46
|
+
<p class="text-tertiary mb-2.5 text-[10px] font-bold tracking-widest uppercase">Body Font</p>
|
|
47
|
+
<div class="grid grid-cols-2 gap-2">
|
|
48
|
+
{#each FONT_PRESETS as f (f.label)}
|
|
49
|
+
<button
|
|
50
|
+
onclick={() => (bodyFontFamily = f.family)}
|
|
51
|
+
class="flex flex-col items-start gap-1.5 rounded-xl border p-3 text-left transition-all duration-150
|
|
52
|
+
{selectedBody === f.label ? 'border-accent/40 bg-accent/5 shadow-sm' : 'border-border-primary/60 hover:border-primary/20 hover:bg-surface-primary/60'}"
|
|
53
|
+
>
|
|
54
|
+
<span class="text-[22px] leading-none" style="font-family:{f.family}">Aa</span>
|
|
55
|
+
<span class="w-full truncate text-[9px] leading-none tracking-widest uppercase {selectedBody === f.label ? 'text-accent font-semibold' : 'text-tertiary'}">{f.label}</span>
|
|
56
|
+
</button>
|
|
57
|
+
{/each}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<!-- Live preview -->
|
|
62
|
+
<div class="card bg-surface-secondary/30!">
|
|
63
|
+
<p class="text-tertiary mb-2.5 text-[10px] font-bold tracking-widest uppercase">Preview</p>
|
|
64
|
+
<div class="border-border-primary/60 bg-surface-primary flex flex-col gap-1 rounded-xl border px-4 py-3">
|
|
65
|
+
<p class="text-2xl leading-tight font-bold" style="font-family:{headingFontFamily}">Heading One</p>
|
|
66
|
+
<p class="text-secondary text-lg font-semibold" style="font-family:{headingFontFamily}">Heading Two</p>
|
|
67
|
+
<p class="text-sm font-medium" style="font-family:{bodyFontFamily}">Body text — medium weight</p>
|
|
68
|
+
<p class="text-secondary text-xs leading-relaxed" style="font-family:{bodyFontFamily}">Regular body text.</p>
|
|
69
|
+
<p class="text-tertiary text-xs" style="font-family:{bodyFontFamily}">Caption · tertiary · metadata</p>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function extractCSSVariables(css: string): Record<string, string> {
|
|
2
|
+
const cleaned = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
3
|
+
const vars: Record<string, string> = {};
|
|
4
|
+
const regex = /(--[\w-]+)\s*:\s*([^;]+);/g;
|
|
5
|
+
let m: RegExpExecArray | null;
|
|
6
|
+
while ((m = regex.exec(cleaned)) !== null) {
|
|
7
|
+
if (m[1] && m[2]) vars[m[1].trim()] = m[2].trim();
|
|
8
|
+
}
|
|
9
|
+
return vars;
|
|
10
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Configured axios instance — import httpClient instead of creating new instances.
|
|
2
|
+
// Intentionally has NO UI imports (no snackStore). UI side-effects belong in the
|
|
3
|
+
// entity service layer, not in transport infrastructure.
|
|
4
|
+
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
import { env } from '$env/dynamic/public';
|
|
7
|
+
import { getItem, removeItem } from '@aryagg/utils';
|
|
8
|
+
import { EStorageKey } from '@aryagg/types';
|
|
9
|
+
import { API_TIMEOUT } from './types';
|
|
10
|
+
|
|
11
|
+
let _token: string | null = null;
|
|
12
|
+
|
|
13
|
+
/** Call this from the auth entity after login/logout to keep the in-memory token in sync. */
|
|
14
|
+
export function setAuthToken(token: string | null) {
|
|
15
|
+
_token = token;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const httpClient = axios.create({
|
|
19
|
+
baseURL: env.PUBLIC_API_URL,
|
|
20
|
+
timeout: API_TIMEOUT,
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Attach Bearer token on every request.
|
|
25
|
+
// Prefers the in-memory mirror; falls back to localStorage for the edge case where
|
|
26
|
+
// an API call fires before the auth entity has synced the store (e.g. page reload).
|
|
27
|
+
httpClient.interceptors.request.use((config) => {
|
|
28
|
+
let token = _token;
|
|
29
|
+
if (!token && typeof localStorage !== 'undefined') {
|
|
30
|
+
const raw = getItem(EStorageKey.AUTH_USER);
|
|
31
|
+
token = raw ? (JSON.parse(raw) as { token?: string }).token ?? null : null;
|
|
32
|
+
}
|
|
33
|
+
if (token) config.headers['kuki'] = token;
|
|
34
|
+
return config;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// On 401: clear stale credentials silently.
|
|
38
|
+
// The entity service layer decides whether to show a toast.
|
|
39
|
+
httpClient.interceptors.response.use(
|
|
40
|
+
(response) => response,
|
|
41
|
+
(error) => {
|
|
42
|
+
if (error?.response?.status === 401) {
|
|
43
|
+
removeItem(EStorageKey.AUTH_USER);
|
|
44
|
+
}
|
|
45
|
+
return Promise.reject(error);
|
|
46
|
+
},
|
|
47
|
+
);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export enum HttpStatus {
|
|
2
|
+
OK = 200,
|
|
3
|
+
CREATED = 201,
|
|
4
|
+
BAD_REQUEST = 400,
|
|
5
|
+
UNAUTHORIZED = 401,
|
|
6
|
+
FORBIDDEN = 403,
|
|
7
|
+
NOT_FOUND = 404,
|
|
8
|
+
INTERNAL_SERVER_ERROR = 500,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export enum HttpMethod {
|
|
12
|
+
GET = 'GET',
|
|
13
|
+
POST = 'POST',
|
|
14
|
+
PUT = 'PUT',
|
|
15
|
+
PATCH = 'PATCH',
|
|
16
|
+
DELETE = 'DELETE',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const API_TIMEOUT = 30_000;
|
|
20
|
+
|
|
21
|
+
export interface IApiResult<T> {
|
|
22
|
+
ok: boolean;
|
|
23
|
+
data: T | null;
|
|
24
|
+
error: string | null;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const AUTH_COOKIE_NAME = 'auth_session';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { resolve } from '$app/paths';
|
|
2
|
+
|
|
3
|
+
// ── Auth routes ───────────────────────────────────────
|
|
4
|
+
export const LOGIN_ROUTE = resolve('/login', {});
|
|
5
|
+
export const REGISTER_ROUTE = resolve('/register', {});
|
|
6
|
+
export const LOGOUT_ROUTE = resolve('/logout', {});
|
|
7
|
+
export const FORGOT_PASSWORD_ROUTE = resolve('/forgot-password', {});
|
|
8
|
+
export const RESET_PASSWORD_ROUTE = resolve('/reset-password', {});
|
|
9
|
+
|
|
10
|
+
// ── App routes ────────────────────────────────────────
|
|
11
|
+
export const DASHBOARD_ROUTE = resolve('/', {});
|
|
12
|
+
export const PROFILE_ROUTE = resolve('/profile', {});
|
|
13
|
+
export const SETTINGS_ROUTE = resolve('/settings', {});
|
|
14
|
+
export const THEME_ROUTE = resolve('/theme', {});
|
|
15
|
+
|
|
16
|
+
// ── Admin routes ──────────────────────────────────────
|
|
17
|
+
export const ADMIN_ROUTE = resolve('/admin', {});
|
|
18
|
+
export const ADMIN_USERS_ROUTE = resolve('/admin/users', {});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getLocale, setLocale, locales, getTextDirection } from '$paraglide/runtime';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Project-level Tailwind config.
|
|
2
|
+
// Extends the shared @aryagg/theme palette with app-specific colors.
|
|
3
|
+
// Note: no 'content' needed — scanning is handled by @source in app.css since v4 ignores it.
|
|
4
|
+
|
|
5
|
+
import type { Config } from 'tailwindcss';
|
|
6
|
+
|
|
7
|
+
const config: Config = {
|
|
8
|
+
// Dark mode is toggled by adding/removing .dark on <html>
|
|
9
|
+
darkMode: 'class',
|
|
10
|
+
|
|
11
|
+
theme: {
|
|
12
|
+
extend: {
|
|
13
|
+
colors: {
|
|
14
|
+
// Add your project colors here — they sit alongside
|
|
15
|
+
// the theme colors (accent, surface, primary, etc.)
|
|
16
|
+
brand: {
|
|
17
|
+
DEFAULT: '#6366f1',
|
|
18
|
+
light: '#818cf8',
|
|
19
|
+
dark: '#4f46e5',
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
plugins: []
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default config;
|