@vincent99/vlib 0.1.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/LICENSE +178 -0
- package/README.md +107 -0
- package/bin/vlib.js +10 -0
- package/dist/AdminForm.vue_vue_type_style_index_0_lang-xCk1ywLq.js +753 -0
- package/dist/auth/middleware.d.ts +18 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +44 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/password.d.ts +10 -0
- package/dist/auth/password.d.ts.map +1 -0
- package/dist/auth/password.js +44 -0
- package/dist/auth/password.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +104 -0
- package/dist/cli.js.map +1 -0
- package/dist/components/AdminForm.vue.d.ts +7 -0
- package/dist/components/AdminTable.vue.d.ts +5 -0
- package/dist/components/AppLayout.vue.d.ts +36 -0
- package/dist/components/NavSidebar.vue.d.ts +11 -0
- package/dist/components/TableView.vue.d.ts +52 -0
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.js +8 -0
- package/dist/components/types.d.ts +25 -0
- package/dist/db/index.d.ts +12 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +84 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrate.d.ts +2 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +94 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +11 -0
- package/dist/router/index.d.ts +33 -0
- package/dist/router/index.js +62 -0
- package/dist/server/api/admin.d.ts +3 -0
- package/dist/server/api/admin.d.ts.map +1 -0
- package/dist/server/api/admin.js +184 -0
- package/dist/server/api/admin.js.map +1 -0
- package/dist/server/api/auth.d.ts +3 -0
- package/dist/server/api/auth.d.ts.map +1 -0
- package/dist/server/api/auth.js +66 -0
- package/dist/server/api/auth.js.map +1 -0
- package/dist/server/index.d.ts +17 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +47 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/vlib.css +1 -0
- package/package.json +91 -0
- package/src/components/AdminForm.vue +491 -0
- package/src/components/AdminTable.vue +269 -0
- package/src/components/AppLayout.vue +280 -0
- package/src/components/NavSidebar.vue +176 -0
- package/src/components/TableView.vue +379 -0
- package/src/components/index.ts +13 -0
- package/src/components/types.ts +28 -0
- package/templates/.env.example +4 -0
- package/templates/.prettierignore +3 -0
- package/templates/.prettierrc +6 -0
- package/templates/Dockerfile.ejs +31 -0
- package/templates/docker-compose.prod.yml.ejs +22 -0
- package/templates/docker-compose.yml.ejs +22 -0
- package/templates/eslint.config.mjs +42 -0
- package/templates/index.html.ejs +13 -0
- package/templates/package.json.ejs +44 -0
- package/templates/postcss.config.js.ejs +6 -0
- package/templates/schemas/001-initial.sql +35 -0
- package/templates/scripts/migrate.ts +13 -0
- package/templates/server/index.ts +13 -0
- package/templates/src/App.vue +8 -0
- package/templates/src/main.ts +6 -0
- package/templates/src/router.ts +26 -0
- package/templates/src/routes/_layout.vue +58 -0
- package/templates/src/routes/admin/_layout.vue +8 -0
- package/templates/src/routes/admin/index.vue +88 -0
- package/templates/src/routes/admin/tables/[table]/[id].vue +20 -0
- package/templates/src/routes/admin/tables/[table]/index.vue +10 -0
- package/templates/src/routes/admin/tables/[table]/new.vue +10 -0
- package/templates/src/routes/index.vue +34 -0
- package/templates/src/routes/login.vue +128 -0
- package/templates/src/stores/auth.ts +58 -0
- package/templates/src/styles/main.scss +98 -0
- package/templates/src/styles/variables.scss +7 -0
- package/templates/tailwind.config.js.ejs +27 -0
- package/templates/tsconfig.json.ejs +26 -0
- package/templates/tsconfig.server.json.ejs +17 -0
- package/templates/vite.config.ts.ejs +36 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createRouter, createWebHistory } from 'vue-router';
|
|
2
|
+
import { buildRoutes, createAuthGuard } from '@vincent99/vlib/router';
|
|
3
|
+
import { useAuthStore } from './stores/auth';
|
|
4
|
+
|
|
5
|
+
// Glob-import all route components
|
|
6
|
+
const pages = import.meta.glob('./routes/**/*.vue');
|
|
7
|
+
|
|
8
|
+
const router = createRouter({
|
|
9
|
+
history: createWebHistory(),
|
|
10
|
+
routes: buildRoutes(pages, {
|
|
11
|
+
publicPaths: ['/login'],
|
|
12
|
+
}),
|
|
13
|
+
scrollBehavior(_to, _from, savedPosition) {
|
|
14
|
+
return savedPosition ?? { top: 0 };
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Auth navigation guard
|
|
19
|
+
router.beforeEach(
|
|
20
|
+
createAuthGuard(() => {
|
|
21
|
+
const auth = useAuthStore();
|
|
22
|
+
return auth.isAuthenticated;
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export default router;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<AppLayout
|
|
3
|
+
:app-name="appName"
|
|
4
|
+
:user="auth.user.value"
|
|
5
|
+
:nav-items="navItems"
|
|
6
|
+
@logout="auth.logout()"
|
|
7
|
+
>
|
|
8
|
+
<RouterView />
|
|
9
|
+
</AppLayout>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
import { computed, onMounted } from 'vue';
|
|
14
|
+
import { AppLayout } from '@vincent99/vlib/components';
|
|
15
|
+
import type { NavItem } from '@vincent99/vlib/components';
|
|
16
|
+
import { useAuthStore } from '../stores/auth';
|
|
17
|
+
|
|
18
|
+
const appName = import.meta.env.VITE_APP_NAME || 'My App';
|
|
19
|
+
const auth = useAuthStore();
|
|
20
|
+
|
|
21
|
+
// Admin nav items loaded from the API at runtime
|
|
22
|
+
const adminTables = computed<NavItem[]>(() =>
|
|
23
|
+
tables.value.map((t) => ({
|
|
24
|
+
label: t.name,
|
|
25
|
+
to: `/admin/tables/${t.name}`,
|
|
26
|
+
}))
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const navItems = computed<NavItem[]>(() => [
|
|
30
|
+
{ label: 'Home', to: '/', icon: '🏠' },
|
|
31
|
+
{
|
|
32
|
+
label: 'Admin',
|
|
33
|
+
icon: '⚙️',
|
|
34
|
+
defaultOpen: true,
|
|
35
|
+
children: [
|
|
36
|
+
{ label: 'Dashboard', to: '/admin', icon: '📊' },
|
|
37
|
+
...adminTables.value,
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
import { ref } from 'vue';
|
|
43
|
+
const tables = ref<{ name: string }[]>([]);
|
|
44
|
+
|
|
45
|
+
onMounted(async () => {
|
|
46
|
+
await auth.init();
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch('/api/admin/tables');
|
|
49
|
+
if (res.ok) {
|
|
50
|
+
const data: Array<{ name: string; isJoinTable: boolean }> =
|
|
51
|
+
await res.json();
|
|
52
|
+
tables.value = data.filter((t) => !t.isJoinTable);
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
/* ignore */
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
</script>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="vl-page">
|
|
3
|
+
<h1 class="vl-page__title">Admin</h1>
|
|
4
|
+
<p class="vl-page__subtitle">Database tables</p>
|
|
5
|
+
|
|
6
|
+
<div v-if="loading" class="vl-admin-home__loading">Loading tables…</div>
|
|
7
|
+
|
|
8
|
+
<div v-else class="vl-admin-home__grid">
|
|
9
|
+
<RouterLink
|
|
10
|
+
v-for="table in tables"
|
|
11
|
+
:key="table.name"
|
|
12
|
+
:to="`/admin/tables/${table.name}`"
|
|
13
|
+
class="vl-admin-home__card"
|
|
14
|
+
>
|
|
15
|
+
<span class="vl-admin-home__card-name">{{ table.name }}</span>
|
|
16
|
+
<span class="vl-admin-home__card-arrow">→</span>
|
|
17
|
+
</RouterLink>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script setup lang="ts">
|
|
23
|
+
import { ref, onMounted } from 'vue';
|
|
24
|
+
|
|
25
|
+
interface TableInfo {
|
|
26
|
+
name: string;
|
|
27
|
+
isJoinTable: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const tables = ref<TableInfo[]>([]);
|
|
31
|
+
const loading = ref(true);
|
|
32
|
+
|
|
33
|
+
onMounted(async () => {
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch('/api/admin/tables');
|
|
36
|
+
if (res.ok) {
|
|
37
|
+
const data: TableInfo[] = await res.json();
|
|
38
|
+
tables.value = data.filter((t) => !t.isJoinTable);
|
|
39
|
+
}
|
|
40
|
+
} finally {
|
|
41
|
+
loading.value = false;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<style lang="scss">
|
|
47
|
+
.vl-admin-home {
|
|
48
|
+
&__loading {
|
|
49
|
+
color: var(--color-text-secondary);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
&__grid {
|
|
53
|
+
display: grid;
|
|
54
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
55
|
+
gap: var(--space-3);
|
|
56
|
+
margin-top: var(--space-4);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
&__card {
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
justify-content: space-between;
|
|
63
|
+
padding: var(--space-4);
|
|
64
|
+
background: white;
|
|
65
|
+
border: 1px solid var(--color-border);
|
|
66
|
+
border-radius: var(--radius-lg);
|
|
67
|
+
text-decoration: none;
|
|
68
|
+
color: var(--color-text);
|
|
69
|
+
font-weight: 500;
|
|
70
|
+
box-shadow: var(--shadow);
|
|
71
|
+
transition: all 0.15s;
|
|
72
|
+
|
|
73
|
+
&:hover {
|
|
74
|
+
border-color: var(--color-primary-light);
|
|
75
|
+
box-shadow: var(--shadow-lg);
|
|
76
|
+
transform: translateY(-1px);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
&-name {
|
|
80
|
+
text-transform: capitalize;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
&-arrow {
|
|
84
|
+
color: var(--color-primary);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<AdminForm :table-name="route.params.table as string" :row-ids="rowIds" />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup lang="ts">
|
|
6
|
+
import { computed } from 'vue';
|
|
7
|
+
import { useRoute } from 'vue-router';
|
|
8
|
+
import { AdminForm } from '@vincent99/vlib/components';
|
|
9
|
+
|
|
10
|
+
const route = useRoute();
|
|
11
|
+
|
|
12
|
+
// Support comma-separated IDs for multi-edit: /admin/tables/users/1,2,3
|
|
13
|
+
const rowIds = computed(() => {
|
|
14
|
+
const id = route.params.id as string;
|
|
15
|
+
return id.split(',').map((s) => {
|
|
16
|
+
const n = parseInt(s, 10);
|
|
17
|
+
return isNaN(n) ? s : n;
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
</script>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="vl-page">
|
|
3
|
+
<h1 class="vl-page__title">Welcome</h1>
|
|
4
|
+
<p class="vl-page__subtitle">
|
|
5
|
+
You are logged in as
|
|
6
|
+
<strong>{{
|
|
7
|
+
auth.user.value?.displayName || auth.user.value?.username
|
|
8
|
+
}}</strong
|
|
9
|
+
>.
|
|
10
|
+
</p>
|
|
11
|
+
</div>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup lang="ts">
|
|
15
|
+
import { useAuthStore } from '../stores/auth';
|
|
16
|
+
|
|
17
|
+
const auth = useAuthStore();
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<style lang="scss">
|
|
21
|
+
.vl-page {
|
|
22
|
+
&__title {
|
|
23
|
+
font-size: 2rem;
|
|
24
|
+
font-weight: 700;
|
|
25
|
+
color: var(--color-text);
|
|
26
|
+
margin-bottom: var(--space-2);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&__subtitle {
|
|
30
|
+
color: var(--color-text-secondary);
|
|
31
|
+
font-size: 1rem;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
</style>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="vl-login-page">
|
|
3
|
+
<div class="vl-login-card">
|
|
4
|
+
<div class="vl-login-card__logo">
|
|
5
|
+
<span class="vl-login-card__app-name">{{ appName }}</span>
|
|
6
|
+
</div>
|
|
7
|
+
<h1 class="vl-login-card__title">Sign in</h1>
|
|
8
|
+
|
|
9
|
+
<div v-if="error" class="vl-alert vl-alert--error">{{ error }}</div>
|
|
10
|
+
|
|
11
|
+
<form class="vl-login-form" @submit.prevent="handleLogin">
|
|
12
|
+
<div class="vl-form__field">
|
|
13
|
+
<label for="username" class="vl-form__label">Username</label>
|
|
14
|
+
<input
|
|
15
|
+
id="username"
|
|
16
|
+
v-model="username"
|
|
17
|
+
type="text"
|
|
18
|
+
class="vl-form__input"
|
|
19
|
+
autocomplete="username"
|
|
20
|
+
required
|
|
21
|
+
:disabled="loading"
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="vl-form__field">
|
|
26
|
+
<label for="password" class="vl-form__label">Password</label>
|
|
27
|
+
<input
|
|
28
|
+
id="password"
|
|
29
|
+
v-model="password"
|
|
30
|
+
type="password"
|
|
31
|
+
class="vl-form__input"
|
|
32
|
+
autocomplete="current-password"
|
|
33
|
+
required
|
|
34
|
+
:disabled="loading"
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<button
|
|
39
|
+
type="submit"
|
|
40
|
+
class="vl-btn vl-btn--primary vl-login-form__submit"
|
|
41
|
+
:disabled="loading"
|
|
42
|
+
>
|
|
43
|
+
{{ loading ? 'Signing in…' : 'Sign in' }}
|
|
44
|
+
</button>
|
|
45
|
+
</form>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<script setup lang="ts">
|
|
51
|
+
import { ref } from 'vue';
|
|
52
|
+
import { useRouter } from 'vue-router';
|
|
53
|
+
import { useAuthStore } from '../stores/auth';
|
|
54
|
+
|
|
55
|
+
const appName = import.meta.env.VITE_APP_NAME || 'My App';
|
|
56
|
+
const router = useRouter();
|
|
57
|
+
const auth = useAuthStore();
|
|
58
|
+
|
|
59
|
+
const username = ref('');
|
|
60
|
+
const password = ref('');
|
|
61
|
+
const loading = ref(false);
|
|
62
|
+
const error = ref<string | null>(null);
|
|
63
|
+
|
|
64
|
+
async function handleLogin() {
|
|
65
|
+
loading.value = true;
|
|
66
|
+
error.value = null;
|
|
67
|
+
const err = await auth.login(username.value, password.value);
|
|
68
|
+
if (err) {
|
|
69
|
+
error.value = err;
|
|
70
|
+
loading.value = false;
|
|
71
|
+
} else {
|
|
72
|
+
router.push('/');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<style lang="scss">
|
|
78
|
+
.vl-login-page {
|
|
79
|
+
min-height: 100vh;
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
justify-content: center;
|
|
83
|
+
background: var(--color-background);
|
|
84
|
+
padding: var(--space-4);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.vl-login-card {
|
|
88
|
+
background: white;
|
|
89
|
+
border-radius: var(--radius-lg);
|
|
90
|
+
box-shadow: var(--shadow-lg);
|
|
91
|
+
padding: var(--space-8);
|
|
92
|
+
width: 100%;
|
|
93
|
+
max-width: 400px;
|
|
94
|
+
|
|
95
|
+
&__logo {
|
|
96
|
+
text-align: center;
|
|
97
|
+
margin-bottom: var(--space-4);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
&__app-name {
|
|
101
|
+
font-size: 1.5rem;
|
|
102
|
+
font-weight: 800;
|
|
103
|
+
color: var(--color-primary);
|
|
104
|
+
letter-spacing: -0.02em;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
&__title {
|
|
108
|
+
font-size: 1.25rem;
|
|
109
|
+
font-weight: 700;
|
|
110
|
+
color: var(--color-text);
|
|
111
|
+
text-align: center;
|
|
112
|
+
margin-bottom: var(--space-6);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.vl-login-form {
|
|
117
|
+
display: flex;
|
|
118
|
+
flex-direction: column;
|
|
119
|
+
gap: var(--space-4);
|
|
120
|
+
|
|
121
|
+
&__submit {
|
|
122
|
+
width: 100%;
|
|
123
|
+
justify-content: center;
|
|
124
|
+
padding: var(--space-3);
|
|
125
|
+
font-size: 1rem;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
</style>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ref, computed } from 'vue';
|
|
2
|
+
import { useRouter } from 'vue-router';
|
|
3
|
+
|
|
4
|
+
interface User {
|
|
5
|
+
id: number;
|
|
6
|
+
username: string;
|
|
7
|
+
displayName: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Simple reactive auth store (no Pinia required)
|
|
11
|
+
const user = ref<User | null>(null);
|
|
12
|
+
const initialized = ref(false);
|
|
13
|
+
|
|
14
|
+
export function useAuthStore() {
|
|
15
|
+
const router = useRouter();
|
|
16
|
+
|
|
17
|
+
const isAuthenticated = computed(() => user.value !== null);
|
|
18
|
+
|
|
19
|
+
async function init() {
|
|
20
|
+
if (initialized.value) return;
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch('/api/auth/me');
|
|
23
|
+
if (res.ok) {
|
|
24
|
+
const data = await res.json();
|
|
25
|
+
user.value = data.user;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
/* not authenticated */
|
|
29
|
+
}
|
|
30
|
+
initialized.value = true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function login(
|
|
34
|
+
username: string,
|
|
35
|
+
password: string
|
|
36
|
+
): Promise<string | null> {
|
|
37
|
+
const res = await fetch('/api/auth/login', {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify({ username, password }),
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
return data.error || 'Login failed';
|
|
45
|
+
}
|
|
46
|
+
const data = await res.json();
|
|
47
|
+
user.value = data.user;
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function logout() {
|
|
52
|
+
await fetch('/api/auth/logout', { method: 'POST' });
|
|
53
|
+
user.value = null;
|
|
54
|
+
router.push('/login');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { user, isAuthenticated, initialized, init, login, logout };
|
|
58
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
// ── CSS Custom Properties (design tokens) ────────────────────────────────────
|
|
6
|
+
:root {
|
|
7
|
+
// Colors — blues
|
|
8
|
+
--color-primary: #1e40af; // Blue 800
|
|
9
|
+
--color-primary-light: #3b82f6; // Blue 500
|
|
10
|
+
--color-primary-dark: #1e3a5f; // Deep navy
|
|
11
|
+
--color-accent: #60a5fa; // Blue 400
|
|
12
|
+
|
|
13
|
+
// Surface colors
|
|
14
|
+
--color-background: #f0f4f8;
|
|
15
|
+
--color-surface: #ffffff;
|
|
16
|
+
--color-sidebar: #1e3a5f;
|
|
17
|
+
--color-header: #1e40af;
|
|
18
|
+
|
|
19
|
+
// Text
|
|
20
|
+
--color-text: #1e293b; // Slate 800
|
|
21
|
+
--color-text-secondary: #64748b; // Slate 500
|
|
22
|
+
--color-border: #e2e8f0; // Slate 200
|
|
23
|
+
|
|
24
|
+
// Status
|
|
25
|
+
--color-danger: #ef4444;
|
|
26
|
+
--color-success: #22c55e;
|
|
27
|
+
--color-warning: #f59e0b;
|
|
28
|
+
|
|
29
|
+
// Layout
|
|
30
|
+
--header-height: 60px;
|
|
31
|
+
--sidebar-width: 240px;
|
|
32
|
+
|
|
33
|
+
// Spacing scale (multiples of 4px)
|
|
34
|
+
--space-1: 4px;
|
|
35
|
+
--space-2: 8px;
|
|
36
|
+
--space-3: 12px;
|
|
37
|
+
--space-4: 16px;
|
|
38
|
+
--space-5: 20px;
|
|
39
|
+
--space-6: 24px;
|
|
40
|
+
--space-8: 32px;
|
|
41
|
+
--space-10: 40px;
|
|
42
|
+
--space-12: 48px;
|
|
43
|
+
|
|
44
|
+
// Border radius
|
|
45
|
+
--radius: 6px;
|
|
46
|
+
--radius-lg: 10px;
|
|
47
|
+
|
|
48
|
+
// Shadows
|
|
49
|
+
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
50
|
+
--shadow-lg: 0 4px 6px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.06);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Base reset / global styles ────────────────────────────────────────────────
|
|
54
|
+
*,
|
|
55
|
+
*::before,
|
|
56
|
+
*::after {
|
|
57
|
+
box-sizing: border-box;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
html,
|
|
61
|
+
body,
|
|
62
|
+
#app {
|
|
63
|
+
margin: 0;
|
|
64
|
+
padding: 0;
|
|
65
|
+
height: 100%;
|
|
66
|
+
font-family:
|
|
67
|
+
system-ui,
|
|
68
|
+
-apple-system,
|
|
69
|
+
'Segoe UI',
|
|
70
|
+
Roboto,
|
|
71
|
+
'Helvetica Neue',
|
|
72
|
+
sans-serif;
|
|
73
|
+
color: var(--color-text);
|
|
74
|
+
background: var(--color-background);
|
|
75
|
+
-webkit-font-smoothing: antialiased;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
h1,
|
|
79
|
+
h2,
|
|
80
|
+
h3,
|
|
81
|
+
h4,
|
|
82
|
+
h5,
|
|
83
|
+
h6 {
|
|
84
|
+
margin: 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
p {
|
|
88
|
+
margin: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
a {
|
|
92
|
+
color: var(--color-primary);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Responsive helpers ────────────────────────────────────────────────────────
|
|
96
|
+
// Small: ≤ 767px (portrait phone)
|
|
97
|
+
// Medium: 768–1023px (tablet)
|
|
98
|
+
// Large: ≥ 1024px (desktop)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Design tokens — imported into every component via vite.config.ts preprocessorOptions
|
|
2
|
+
|
|
3
|
+
// ── Breakpoints ───────────────────────────────────────────────────────────────
|
|
4
|
+
// Used as Sass variables in media queries (CSS custom props can't be in @media)
|
|
5
|
+
$bp-small: 390px; // Small: portrait phone (≤ 767px)
|
|
6
|
+
$bp-medium: 768px; // Medium: tablet (768px – 1023px)
|
|
7
|
+
$bp-large: 1024px; // Large: desktop (≥ 1024px)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
content: [
|
|
4
|
+
'./index.html',
|
|
5
|
+
'./src/**/*.{vue,js,ts,jsx,tsx}',
|
|
6
|
+
'./node_modules/@vincent99/vlib/src/components/**/*.vue',
|
|
7
|
+
],
|
|
8
|
+
theme: {
|
|
9
|
+
extend: {
|
|
10
|
+
colors: {
|
|
11
|
+
primary: {
|
|
12
|
+
50: '#eff6ff',
|
|
13
|
+
100: '#dbeafe',
|
|
14
|
+
200: '#bfdbfe',
|
|
15
|
+
300: '#93c5fd',
|
|
16
|
+
400: '#60a5fa',
|
|
17
|
+
500: '#3b82f6',
|
|
18
|
+
600: '#2563eb',
|
|
19
|
+
700: '#1d4ed8',
|
|
20
|
+
800: '#1e40af',
|
|
21
|
+
900: '#1e3a5f',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
plugins: [],
|
|
27
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"jsx": "preserve",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"allowSyntheticDefaultImports": true,
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"paths": {
|
|
16
|
+
"@/*": ["./src/*"]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"include": [
|
|
20
|
+
"src/**/*.ts",
|
|
21
|
+
"src/**/*.d.ts",
|
|
22
|
+
"src/**/*.tsx",
|
|
23
|
+
"src/**/*.vue"
|
|
24
|
+
],
|
|
25
|
+
"exclude": ["node_modules", "dist", "server", "scripts"]
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": ".",
|
|
9
|
+
"declaration": false,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"resolveJsonModule": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["server/**/*.ts", "scripts/**/*.ts"],
|
|
16
|
+
"exclude": ["node_modules", "dist", "src"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import vue from '@vitejs/plugin-vue'
|
|
3
|
+
import { resolve } from 'path'
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [vue()],
|
|
7
|
+
resolve: {
|
|
8
|
+
alias: {
|
|
9
|
+
'@': resolve(__dirname, 'src'),
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
css: {
|
|
13
|
+
preprocessorOptions: {
|
|
14
|
+
scss: {
|
|
15
|
+
// Inject breakpoint variables into every SCSS file.
|
|
16
|
+
// Uses loadPaths so the Sass compiler can resolve the module.
|
|
17
|
+
loadPaths: [resolve(__dirname, 'src/styles')],
|
|
18
|
+
additionalData: `@use "variables" as *;`,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
server: {
|
|
23
|
+
host: '0.0.0.0',
|
|
24
|
+
port: 5173,
|
|
25
|
+
proxy: {
|
|
26
|
+
'/api': {
|
|
27
|
+
target: 'http://localhost:3001',
|
|
28
|
+
changeOrigin: true,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
build: {
|
|
33
|
+
outDir: 'dist/public',
|
|
34
|
+
emptyOutDir: true,
|
|
35
|
+
},
|
|
36
|
+
})
|