@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.
Files changed (92) hide show
  1. package/LICENSE +178 -0
  2. package/README.md +107 -0
  3. package/bin/vlib.js +10 -0
  4. package/dist/AdminForm.vue_vue_type_style_index_0_lang-xCk1ywLq.js +753 -0
  5. package/dist/auth/middleware.d.ts +18 -0
  6. package/dist/auth/middleware.d.ts.map +1 -0
  7. package/dist/auth/middleware.js +44 -0
  8. package/dist/auth/middleware.js.map +1 -0
  9. package/dist/auth/password.d.ts +10 -0
  10. package/dist/auth/password.d.ts.map +1 -0
  11. package/dist/auth/password.js +44 -0
  12. package/dist/auth/password.js.map +1 -0
  13. package/dist/cli.d.ts +3 -0
  14. package/dist/cli.d.ts.map +1 -0
  15. package/dist/cli.js +104 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/components/AdminForm.vue.d.ts +7 -0
  18. package/dist/components/AdminTable.vue.d.ts +5 -0
  19. package/dist/components/AppLayout.vue.d.ts +36 -0
  20. package/dist/components/NavSidebar.vue.d.ts +11 -0
  21. package/dist/components/TableView.vue.d.ts +52 -0
  22. package/dist/components/index.d.ts +6 -0
  23. package/dist/components/index.js +8 -0
  24. package/dist/components/types.d.ts +25 -0
  25. package/dist/db/index.d.ts +12 -0
  26. package/dist/db/index.d.ts.map +1 -0
  27. package/dist/db/index.js +84 -0
  28. package/dist/db/index.js.map +1 -0
  29. package/dist/db/migrate.d.ts +2 -0
  30. package/dist/db/migrate.d.ts.map +1 -0
  31. package/dist/db/migrate.js +94 -0
  32. package/dist/db/migrate.js.map +1 -0
  33. package/dist/index.d.ts +3 -0
  34. package/dist/index.js +11 -0
  35. package/dist/router/index.d.ts +33 -0
  36. package/dist/router/index.js +62 -0
  37. package/dist/server/api/admin.d.ts +3 -0
  38. package/dist/server/api/admin.d.ts.map +1 -0
  39. package/dist/server/api/admin.js +184 -0
  40. package/dist/server/api/admin.js.map +1 -0
  41. package/dist/server/api/auth.d.ts +3 -0
  42. package/dist/server/api/auth.d.ts.map +1 -0
  43. package/dist/server/api/auth.js +66 -0
  44. package/dist/server/api/auth.js.map +1 -0
  45. package/dist/server/index.d.ts +17 -0
  46. package/dist/server/index.d.ts.map +1 -0
  47. package/dist/server/index.js +47 -0
  48. package/dist/server/index.js.map +1 -0
  49. package/dist/types.d.ts +53 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +3 -0
  52. package/dist/types.js.map +1 -0
  53. package/dist/vlib.css +1 -0
  54. package/package.json +91 -0
  55. package/src/components/AdminForm.vue +491 -0
  56. package/src/components/AdminTable.vue +269 -0
  57. package/src/components/AppLayout.vue +280 -0
  58. package/src/components/NavSidebar.vue +176 -0
  59. package/src/components/TableView.vue +379 -0
  60. package/src/components/index.ts +13 -0
  61. package/src/components/types.ts +28 -0
  62. package/templates/.env.example +4 -0
  63. package/templates/.prettierignore +3 -0
  64. package/templates/.prettierrc +6 -0
  65. package/templates/Dockerfile.ejs +31 -0
  66. package/templates/docker-compose.prod.yml.ejs +22 -0
  67. package/templates/docker-compose.yml.ejs +22 -0
  68. package/templates/eslint.config.mjs +42 -0
  69. package/templates/index.html.ejs +13 -0
  70. package/templates/package.json.ejs +44 -0
  71. package/templates/postcss.config.js.ejs +6 -0
  72. package/templates/schemas/001-initial.sql +35 -0
  73. package/templates/scripts/migrate.ts +13 -0
  74. package/templates/server/index.ts +13 -0
  75. package/templates/src/App.vue +8 -0
  76. package/templates/src/main.ts +6 -0
  77. package/templates/src/router.ts +26 -0
  78. package/templates/src/routes/_layout.vue +58 -0
  79. package/templates/src/routes/admin/_layout.vue +8 -0
  80. package/templates/src/routes/admin/index.vue +88 -0
  81. package/templates/src/routes/admin/tables/[table]/[id].vue +20 -0
  82. package/templates/src/routes/admin/tables/[table]/index.vue +10 -0
  83. package/templates/src/routes/admin/tables/[table]/new.vue +10 -0
  84. package/templates/src/routes/index.vue +34 -0
  85. package/templates/src/routes/login.vue +128 -0
  86. package/templates/src/stores/auth.ts +58 -0
  87. package/templates/src/styles/main.scss +98 -0
  88. package/templates/src/styles/variables.scss +7 -0
  89. package/templates/tailwind.config.js.ejs +27 -0
  90. package/templates/tsconfig.json.ejs +26 -0
  91. package/templates/tsconfig.server.json.ejs +17 -0
  92. package/templates/vite.config.ts.ejs +36 -0
@@ -0,0 +1,379 @@
1
+ <template>
2
+ <div class="vl-table-view">
3
+ <!-- Toolbar -->
4
+ <div class="vl-table-view__toolbar">
5
+ <div class="vl-table-view__toolbar-left">
6
+ <slot name="toolbar-left">
7
+ <span v-if="selectedIds.size > 0" class="vl-table-view__selected-count">
8
+ {{ selectedIds.size }} selected
9
+ </span>
10
+ </slot>
11
+ </div>
12
+ <div class="vl-table-view__toolbar-right">
13
+ <!-- Bulk actions -->
14
+ <template v-if="selectedIds.size > 0 && actions.length > 0">
15
+ <button
16
+ v-for="action in actions"
17
+ :key="action.key"
18
+ class="vl-btn"
19
+ :class="[`vl-btn--${action.variant || 'default'}`]"
20
+ @click="handleAction(action)"
21
+ >
22
+ {{ action.label }}
23
+ </button>
24
+ </template>
25
+ <slot name="toolbar-right" />
26
+ </div>
27
+ </div>
28
+
29
+ <!-- Table -->
30
+ <div class="vl-table-view__scroll">
31
+ <table class="vl-table">
32
+ <thead>
33
+ <tr>
34
+ <!-- Select-all checkbox -->
35
+ <th class="vl-table__th vl-table__th--check">
36
+ <input
37
+ type="checkbox"
38
+ :checked="allSelected"
39
+ :indeterminate="someSelected && !allSelected"
40
+ aria-label="Select all"
41
+ @change="toggleAll"
42
+ />
43
+ </th>
44
+ <th
45
+ v-for="col in headers"
46
+ :key="col.key"
47
+ class="vl-table__th"
48
+ :class="{ 'vl-table__th--sortable': col.sortable !== false }"
49
+ @click="col.sortable !== false && toggleSort(col.key)"
50
+ >
51
+ {{ col.label }}
52
+ <span v-if="sortKey === col.key" class="vl-table__sort-icon">
53
+ {{ sortDir === 'asc' ? '↑' : '↓' }}
54
+ </span>
55
+ </th>
56
+ <th v-if="$slots['row-actions']" class="vl-table__th vl-table__th--actions">Actions</th>
57
+ </tr>
58
+ </thead>
59
+ <tbody>
60
+ <tr
61
+ v-for="row in rows"
62
+ :key="getRowId(row)"
63
+ class="vl-table__tr"
64
+ :class="{ 'vl-table__tr--selected': selectedIds.has(getRowId(row)) }"
65
+ >
66
+ <td class="vl-table__td vl-table__td--check">
67
+ <input
68
+ type="checkbox"
69
+ :checked="selectedIds.has(getRowId(row))"
70
+ :aria-label="`Select row ${getRowId(row)}`"
71
+ @change="toggleRow(getRowId(row))"
72
+ />
73
+ </td>
74
+ <td
75
+ v-for="col in headers"
76
+ :key="col.key"
77
+ class="vl-table__td"
78
+ >
79
+ <slot :name="`cell-${col.key}`" :row="row" :value="(row as Record<string, unknown>)[col.key]">
80
+ {{ formatCell((row as Record<string, unknown>)[col.key]) }}
81
+ </slot>
82
+ </td>
83
+ <td v-if="$slots['row-actions']" class="vl-table__td vl-table__td--actions">
84
+ <slot :id="getRowId(row)" name="row-actions" :row="row" />
85
+ </td>
86
+ </tr>
87
+ <tr v-if="rows.length === 0">
88
+ <td :colspan="headers.length + 2" class="vl-table__empty">
89
+ No rows found.
90
+ </td>
91
+ </tr>
92
+ </tbody>
93
+ </table>
94
+ </div>
95
+
96
+ <!-- Pagination -->
97
+ <div v-if="total > pageSize" class="vl-table-view__pagination">
98
+ <button
99
+ class="vl-btn vl-btn--sm"
100
+ :disabled="page <= 1"
101
+ @click="$emit('update:page', page - 1)"
102
+ >← Prev</button>
103
+ <span class="vl-table-view__pagination-info">
104
+ Page {{ page }} of {{ totalPages }} &nbsp;({{ total }} total)
105
+ </span>
106
+ <button
107
+ class="vl-btn vl-btn--sm"
108
+ :disabled="page >= totalPages"
109
+ @click="$emit('update:page', page + 1)"
110
+ >Next →</button>
111
+ </div>
112
+ </div>
113
+ </template>
114
+
115
+ <script setup lang="ts">
116
+ import { ref, computed } from 'vue'
117
+ import type { TableHeader, TableAction } from './types.js'
118
+
119
+ export type { TableHeader, TableAction }
120
+
121
+ const props = withDefaults(
122
+ defineProps<{
123
+ rows: Record<string, unknown>[]
124
+ headers: TableHeader[]
125
+ idKey?: string
126
+ total?: number
127
+ page?: number
128
+ pageSize?: number
129
+ actions?: TableAction[]
130
+ }>(),
131
+ {
132
+ idKey: 'id',
133
+ total: 0,
134
+ page: 1,
135
+ pageSize: 50,
136
+ actions: () => [],
137
+ }
138
+ )
139
+
140
+ const emit = defineEmits<{
141
+ 'update:page': [page: number]
142
+ 'action': [key: string, ids: (string | number)[]]
143
+ 'sort': [key: string, dir: 'asc' | 'desc']
144
+ }>()
145
+
146
+ const selectedIds = ref(new Set<string | number>())
147
+ const sortKey = ref<string | null>(null)
148
+ const sortDir = ref<'asc' | 'desc'>('asc')
149
+
150
+ const totalPages = computed(() => Math.ceil((props.total || props.rows.length) / props.pageSize))
151
+
152
+ const allSelected = computed(
153
+ () => props.rows.length > 0 && props.rows.every((r) => selectedIds.value.has(getRowId(r)))
154
+ )
155
+ const someSelected = computed(() => props.rows.some((r) => selectedIds.value.has(getRowId(r))))
156
+
157
+ function getRowId(row: Record<string, unknown>): string | number {
158
+ return row[props.idKey] as string | number
159
+ }
160
+
161
+ function toggleAll() {
162
+ if (allSelected.value) {
163
+ props.rows.forEach((r) => selectedIds.value.delete(getRowId(r)))
164
+ } else {
165
+ props.rows.forEach((r) => selectedIds.value.add(getRowId(r)))
166
+ }
167
+ }
168
+
169
+ function toggleRow(id: string | number) {
170
+ if (selectedIds.value.has(id)) {selectedIds.value.delete(id)}
171
+ else {selectedIds.value.add(id)}
172
+ }
173
+
174
+ function handleAction(action: TableAction) {
175
+ emit('action', action.key, Array.from(selectedIds.value))
176
+ selectedIds.value.clear()
177
+ }
178
+
179
+ function toggleSort(key: string) {
180
+ if (sortKey.value === key) {
181
+ sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc'
182
+ } else {
183
+ sortKey.value = key
184
+ sortDir.value = 'asc'
185
+ }
186
+ emit('sort', sortKey.value!, sortDir.value)
187
+ }
188
+
189
+ function formatCell(value: unknown): string {
190
+ if (value === null || value === undefined) {return ''}
191
+ if (typeof value === 'object') {return JSON.stringify(value)}
192
+ const str = String(value)
193
+ return str.length > 120 ? str.slice(0, 120) + '…' : str
194
+ }
195
+
196
+ // Expose selected IDs for parent use
197
+ defineExpose({ selectedIds })
198
+ </script>
199
+
200
+ <style lang="scss">
201
+ .vl-table-view {
202
+ background: white;
203
+ border-radius: var(--radius-lg);
204
+ box-shadow: var(--shadow);
205
+ overflow: hidden;
206
+
207
+ &__toolbar {
208
+ display: flex;
209
+ align-items: center;
210
+ justify-content: space-between;
211
+ padding: var(--space-3) var(--space-4);
212
+ border-bottom: 1px solid var(--color-border);
213
+ gap: var(--space-3);
214
+ flex-wrap: wrap;
215
+
216
+ &-left,
217
+ &-right {
218
+ display: flex;
219
+ align-items: center;
220
+ gap: var(--space-2);
221
+ }
222
+ }
223
+
224
+ &__selected-count {
225
+ font-size: 0.875rem;
226
+ color: var(--color-primary);
227
+ font-weight: 500;
228
+ }
229
+
230
+ &__scroll {
231
+ overflow-x: auto;
232
+ }
233
+
234
+ &__pagination {
235
+ display: flex;
236
+ align-items: center;
237
+ gap: var(--space-3);
238
+ justify-content: center;
239
+ padding: var(--space-3) var(--space-4);
240
+ border-top: 1px solid var(--color-border);
241
+ font-size: 0.875rem;
242
+ color: var(--color-text-secondary);
243
+ }
244
+ }
245
+
246
+ .vl-table {
247
+ width: 100%;
248
+ border-collapse: collapse;
249
+ font-size: 0.875rem;
250
+
251
+ &__th {
252
+ padding: var(--space-2) var(--space-3);
253
+ text-align: left;
254
+ font-weight: 600;
255
+ color: var(--color-text-secondary);
256
+ border-bottom: 2px solid var(--color-border);
257
+ white-space: nowrap;
258
+ background: var(--color-background);
259
+
260
+ &--check,
261
+ &--actions {
262
+ width: 1%;
263
+ white-space: nowrap;
264
+ }
265
+
266
+ &--sortable {
267
+ cursor: pointer;
268
+ user-select: none;
269
+
270
+ &:hover {
271
+ color: var(--color-primary);
272
+ }
273
+ }
274
+ }
275
+
276
+ &__td {
277
+ padding: var(--space-2) var(--space-3);
278
+ border-bottom: 1px solid var(--color-border);
279
+ color: var(--color-text);
280
+ max-width: 300px;
281
+ overflow: hidden;
282
+ text-overflow: ellipsis;
283
+ white-space: nowrap;
284
+
285
+ &--check,
286
+ &--actions {
287
+ width: 1%;
288
+ white-space: nowrap;
289
+ }
290
+
291
+ &--actions {
292
+ display: flex;
293
+ gap: var(--space-1);
294
+ }
295
+ }
296
+
297
+ &__tr {
298
+ transition: background 0.1s;
299
+
300
+ &:hover {
301
+ background: var(--color-background);
302
+ }
303
+
304
+ &--selected {
305
+ background: color-mix(in srgb, var(--color-primary) 8%, white);
306
+
307
+ &:hover {
308
+ background: color-mix(in srgb, var(--color-primary) 12%, white);
309
+ }
310
+ }
311
+ }
312
+
313
+ &__sort-icon {
314
+ margin-left: var(--space-1);
315
+ color: var(--color-primary);
316
+ }
317
+
318
+ &__empty {
319
+ padding: var(--space-8) var(--space-4);
320
+ text-align: center;
321
+ color: var(--color-text-secondary);
322
+ font-style: italic;
323
+ }
324
+ }
325
+
326
+ // Button styles (shared)
327
+ .vl-btn {
328
+ display: inline-flex;
329
+ align-items: center;
330
+ gap: var(--space-1);
331
+ padding: var(--space-2) var(--space-3);
332
+ border-radius: var(--radius);
333
+ border: 1px solid var(--color-border);
334
+ background: white;
335
+ color: var(--color-text);
336
+ font-size: 0.875rem;
337
+ cursor: pointer;
338
+ transition: all 0.15s;
339
+ white-space: nowrap;
340
+
341
+ &:hover:not(:disabled) {
342
+ background: var(--color-background);
343
+ border-color: var(--color-primary-light);
344
+ color: var(--color-primary);
345
+ }
346
+
347
+ &:disabled {
348
+ opacity: 0.45;
349
+ cursor: not-allowed;
350
+ }
351
+
352
+ &--primary {
353
+ background: var(--color-primary);
354
+ border-color: var(--color-primary);
355
+ color: white;
356
+
357
+ &:hover:not(:disabled) {
358
+ background: var(--color-primary-dark);
359
+ border-color: var(--color-primary-dark);
360
+ color: white;
361
+ }
362
+ }
363
+
364
+ &--danger {
365
+ color: var(--color-danger);
366
+ border-color: var(--color-danger);
367
+
368
+ &:hover:not(:disabled) {
369
+ background: var(--color-danger);
370
+ color: white;
371
+ }
372
+ }
373
+
374
+ &--sm {
375
+ padding: var(--space-1) var(--space-2);
376
+ font-size: 0.8rem;
377
+ }
378
+ }
379
+ </style>
@@ -0,0 +1,13 @@
1
+ export { default as AppLayout } from './AppLayout.vue';
2
+ export { default as NavSidebar } from './NavSidebar.vue';
3
+ export { default as TableView } from './TableView.vue';
4
+ export { default as AdminTable } from './AdminTable.vue';
5
+ export { default as AdminForm } from './AdminForm.vue';
6
+
7
+ // Type exports from companion .ts (importable without Vue processing)
8
+ export type {
9
+ AppLayoutProps,
10
+ NavItem,
11
+ TableHeader,
12
+ TableAction,
13
+ } from './types.js';
@@ -0,0 +1,28 @@
1
+ // Component prop interfaces — defined here so they can be imported from both
2
+ // Vue files and TypeScript-only contexts.
3
+
4
+ export interface AppLayoutProps {
5
+ appName?: string;
6
+ user?: { username: string; displayName?: string | null } | null;
7
+ navItems?: NavItem[];
8
+ }
9
+
10
+ export interface NavItem {
11
+ label: string;
12
+ to?: string;
13
+ icon?: string;
14
+ children?: NavItem[];
15
+ defaultOpen?: boolean;
16
+ }
17
+
18
+ export interface TableHeader {
19
+ key: string;
20
+ label: string;
21
+ sortable?: boolean;
22
+ }
23
+
24
+ export interface TableAction {
25
+ key: string;
26
+ label: string;
27
+ variant?: 'default' | 'danger' | 'primary';
28
+ }
@@ -0,0 +1,4 @@
1
+ NODE_ENV=development
2
+ PORT=3001
3
+ DB_PATH=./data/app.db
4
+ SESSION_SECRET=change-me-in-production
@@ -0,0 +1,3 @@
1
+ dist/
2
+ node_modules/
3
+ *.db
@@ -0,0 +1,6 @@
1
+ {
2
+ "trailingComma": "es5",
3
+ "tabWidth": 2,
4
+ "semi": true,
5
+ "singleQuote": true
6
+ }
@@ -0,0 +1,31 @@
1
+ # ── Build stage ──────────────────────────────────────────────
2
+ FROM node:22-alpine AS builder
3
+ WORKDIR /app
4
+
5
+ # Install dependencies
6
+ COPY package.json yarn.lock* .yarnrc.yml* ./
7
+ RUN yarn install --frozen-lockfile
8
+
9
+ # Copy source and build
10
+ COPY . .
11
+ RUN yarn build
12
+
13
+ # ── Production stage ──────────────────────────────────────────
14
+ FROM node:22-alpine AS production
15
+ WORKDIR /app
16
+ ENV NODE_ENV=production
17
+
18
+ # Only install production deps
19
+ COPY package.json yarn.lock* .yarnrc.yml* ./
20
+ RUN yarn install --frozen-lockfile --production
21
+
22
+ # Copy compiled server and built frontend
23
+ COPY --from=builder /app/dist ./dist
24
+ COPY --from=builder /app/schemas ./schemas
25
+
26
+ # Data directory for SQLite
27
+ VOLUME ["/data"]
28
+
29
+ EXPOSE 3000
30
+
31
+ CMD ["sh", "-c", "node dist/scripts/migrate.js && node dist/server/index.js"]
@@ -0,0 +1,22 @@
1
+ name: <%= name.toLowerCase().replace(/\s+/g, '-') %>-prod
2
+
3
+ services:
4
+ app:
5
+ build:
6
+ context: .
7
+ dockerfile: Dockerfile
8
+ ports:
9
+ - "3000:3000"
10
+ volumes:
11
+ - ./data:/data
12
+ environment:
13
+ - NODE_ENV=production
14
+ - DB_PATH=/data/app.db
15
+ - PORT=3000
16
+ restart: unless-stopped
17
+ healthcheck:
18
+ test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/auth/me"]
19
+ interval: 30s
20
+ timeout: 10s
21
+ retries: 3
22
+ start_period: 10s
@@ -0,0 +1,22 @@
1
+ name: <%= name.toLowerCase().replace(/\s+/g, '-') %>
2
+
3
+ services:
4
+ app:
5
+ image: node:22-alpine
6
+ working_dir: /app
7
+ volumes:
8
+ - .:/app
9
+ - app_node_modules:/app/node_modules
10
+ - ./data:/data
11
+ ports:
12
+ - "5173:5173"
13
+ - "3001:3001"
14
+ environment:
15
+ - NODE_ENV=development
16
+ - DB_PATH=/data/app.db
17
+ - PORT=3001
18
+ command: sh -c "yarn install && yarn migrate && yarn dev"
19
+ restart: unless-stopped
20
+
21
+ volumes:
22
+ app_node_modules:
@@ -0,0 +1,42 @@
1
+ // @ts-check
2
+
3
+ import eslint from '@eslint/js';
4
+ import { defineConfig } from 'eslint/config';
5
+ import tseslint from 'typescript-eslint';
6
+ import pluginVue from 'eslint-plugin-vue';
7
+ import vueTs from '@vue/eslint-config-typescript';
8
+
9
+ export default defineConfig(
10
+ { ignores: ['dist/', 'node_modules/'] },
11
+ eslint.configs.recommended,
12
+ tseslint.configs.recommended,
13
+ pluginVue.configs['flat/recommended'],
14
+ vueTs(),
15
+ {
16
+ rules: {
17
+ curly: ['error', 'all'],
18
+ 'vue/multi-word-component-names': 'off',
19
+ 'vue/valid-template-root': 'off',
20
+ '@typescript-eslint/no-explicit-any': 'off',
21
+ // Defer HTML formatting to prettier
22
+ 'vue/max-attributes-per-line': 'off',
23
+ 'vue/singleline-html-element-content-newline': 'off',
24
+ 'vue/multiline-html-element-content-newline': 'off',
25
+ 'vue/html-self-closing': 'off',
26
+ 'vue/html-indent': 'off',
27
+ 'vue/html-closing-bracket-newline': 'off',
28
+ '@typescript-eslint/no-unused-vars': [
29
+ 'error',
30
+ {
31
+ args: 'all',
32
+ argsIgnorePattern: '^_',
33
+ caughtErrors: 'all',
34
+ caughtErrorsIgnorePattern: '^_',
35
+ destructuredArrayIgnorePattern: '^_',
36
+ varsIgnorePattern: '^_',
37
+ ignoreRestSiblings: true,
38
+ },
39
+ ],
40
+ },
41
+ }
42
+ );
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title><%= name %></title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "<%= name.toLowerCase().replace(/\s+/g, '-') %>",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "concurrently -n vite,server -c cyan,green \"vite\" \"tsx watch server/index.ts\"",
8
+ "dev:vite": "vite",
9
+ "dev:server": "tsx watch server/index.ts",
10
+ "build": "vue-tsc && vite build && tsc -p tsconfig.server.json",
11
+ "start": "node dist/server/index.js",
12
+ "migrate": "tsx scripts/migrate.ts",
13
+ "lint": "eslint .",
14
+ "lint:fix": "eslint --fix .",
15
+ "format": "prettier --write .",
16
+ "format:check": "prettier --check .",
17
+ "docker:dev": "docker compose up",
18
+ "docker:prod": "docker compose -f docker-compose.prod.yml up --build"
19
+ },
20
+ "dependencies": {
21
+ "@vincent99/vlib": "^0.1.0",
22
+ "vue": "^3.5.13",
23
+ "vue-router": "^4.5.0"
24
+ },
25
+ "devDependencies": {
26
+ "@eslint/js": "^9.18.0",
27
+ "@types/node": "^22.10.0",
28
+ "@vitejs/plugin-vue": "^5.2.1",
29
+ "@vue/eslint-config-typescript": "^14.2.0",
30
+ "autoprefixer": "^10.4.20",
31
+ "concurrently": "^9.1.2",
32
+ "eslint": "^9.18.0",
33
+ "eslint-plugin-vue": "^9.32.0",
34
+ "postcss": "^8.5.1",
35
+ "prettier": "^3.4.2",
36
+ "sass": "^1.83.4",
37
+ "tailwindcss": "^3.4.17",
38
+ "tsx": "^4.19.2",
39
+ "typescript": "^5.7.2",
40
+ "typescript-eslint": "^8.20.0",
41
+ "vite": "^6.0.7",
42
+ "vue-tsc": "^2.2.0"
43
+ }
44
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
@@ -0,0 +1,35 @@
1
+ -- Schema version 1: initial tables
2
+ -- Applied by the migration runner. DO NOT edit this file after first application.
3
+ -- To make changes, create 002-<description>.sql
4
+
5
+ -- Key-value settings store
6
+ CREATE TABLE IF NOT EXISTS settings (
7
+ key TEXT PRIMARY KEY NOT NULL,
8
+ value TEXT NOT NULL DEFAULT 'null' -- stored as JSON
9
+ );
10
+
11
+ -- schema_version is managed by the migration runner; initialize at 0 here,
12
+ -- the runner will update it to 1 after this file is applied.
13
+ INSERT OR IGNORE INTO settings (key, value) VALUES ('schema_version', '0');
14
+
15
+ -- Users
16
+ CREATE TABLE IF NOT EXISTS users (
17
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
18
+ username TEXT UNIQUE NOT NULL,
19
+ password TEXT NOT NULL, -- pbkdf2:<iters>:<salt>:<hash>
20
+ displayName TEXT,
21
+ preferences TEXT NOT NULL DEFAULT '{}' -- JSON object
22
+ );
23
+
24
+ -- Sessions
25
+ CREATE TABLE IF NOT EXISTS sessions (
26
+ id TEXT PRIMARY KEY NOT NULL, -- UUID v4
27
+ userId INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
28
+ created TEXT NOT NULL, -- ISO-8601
29
+ expires TEXT NOT NULL, -- ISO-8601
30
+ userAgent TEXT NOT NULL DEFAULT '',
31
+ ip TEXT NOT NULL DEFAULT ''
32
+ );
33
+
34
+ CREATE INDEX IF NOT EXISTS idx_sessions_userId ON sessions(userId);
35
+ CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires);
@@ -0,0 +1,13 @@
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import { runMigrations } from '@vincent99/vlib/server';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ const dbPath = process.env.DB_PATH ?? path.resolve(__dirname, '../data/app.db');
8
+ const schemasDir = path.resolve(__dirname, '../schemas');
9
+
10
+ runMigrations(dbPath, schemasDir).catch((err) => {
11
+ console.error('Migration failed:', err);
12
+ process.exit(1);
13
+ });
@@ -0,0 +1,13 @@
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import { startServer } from '@vincent99/vlib/server';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const isProd = process.env.NODE_ENV === 'production';
7
+
8
+ startServer({
9
+ dbPath: process.env.DB_PATH ?? path.resolve(__dirname, '../data/app.db'),
10
+ schemasDir: path.resolve(__dirname, '../schemas'),
11
+ staticDir: isProd ? path.resolve(__dirname, '../dist/public') : undefined,
12
+ port: process.env.PORT ? parseInt(process.env.PORT, 10) : undefined,
13
+ });