admindb 1.0.1

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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +325 -0
  3. package/dist/app.d.ts +36 -0
  4. package/dist/app.js +186 -0
  5. package/dist/app.js.map +1 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +61 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/config.d.ts +17 -0
  10. package/dist/config.js +23 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/csv.d.ts +15 -0
  13. package/dist/csv.js +80 -0
  14. package/dist/csv.js.map +1 -0
  15. package/dist/db/database.d.ts +225 -0
  16. package/dist/db/database.js +497 -0
  17. package/dist/db/database.js.map +1 -0
  18. package/dist/db/export.d.ts +12 -0
  19. package/dist/db/export.js +37 -0
  20. package/dist/db/export.js.map +1 -0
  21. package/dist/db/manager.d.ts +62 -0
  22. package/dist/db/manager.js +206 -0
  23. package/dist/db/manager.js.map +1 -0
  24. package/dist/index.d.ts +16 -0
  25. package/dist/index.js +24 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/logger.d.ts +10 -0
  28. package/dist/logger.js +25 -0
  29. package/dist/logger.js.map +1 -0
  30. package/dist/public/css/app.css +1 -0
  31. package/dist/public/css/input.css +179 -0
  32. package/dist/public/js/api.js +47 -0
  33. package/dist/public/js/app.js +120 -0
  34. package/dist/public/js/browse.js +123 -0
  35. package/dist/public/js/databases.js +45 -0
  36. package/dist/public/js/designer.js +205 -0
  37. package/dist/public/js/forms.js +244 -0
  38. package/dist/public/js/query.js +232 -0
  39. package/dist/public/js/schema.js +314 -0
  40. package/dist/public/js/triggers.js +155 -0
  41. package/dist/public/js/views.js +176 -0
  42. package/dist/routes/api.d.ts +9 -0
  43. package/dist/routes/api.js +622 -0
  44. package/dist/routes/api.js.map +1 -0
  45. package/dist/routes/databases.d.ts +19 -0
  46. package/dist/routes/databases.js +99 -0
  47. package/dist/routes/databases.js.map +1 -0
  48. package/dist/routes/pages.d.ts +9 -0
  49. package/dist/routes/pages.js +253 -0
  50. package/dist/routes/pages.js.map +1 -0
  51. package/dist/sql/classifier.d.ts +19 -0
  52. package/dist/sql/classifier.js +37 -0
  53. package/dist/sql/classifier.js.map +1 -0
  54. package/dist/sql/generator.d.ts +99 -0
  55. package/dist/sql/generator.js +259 -0
  56. package/dist/sql/generator.js.map +1 -0
  57. package/dist/test/classifier.test.d.ts +1 -0
  58. package/dist/test/classifier.test.js +44 -0
  59. package/dist/test/classifier.test.js.map +1 -0
  60. package/dist/test/csv.test.d.ts +1 -0
  61. package/dist/test/csv.test.js +64 -0
  62. package/dist/test/csv.test.js.map +1 -0
  63. package/dist/test/database.test.d.ts +1 -0
  64. package/dist/test/database.test.js +224 -0
  65. package/dist/test/database.test.js.map +1 -0
  66. package/dist/test/generator.test.d.ts +1 -0
  67. package/dist/test/generator.test.js +164 -0
  68. package/dist/test/generator.test.js.map +1 -0
  69. package/dist/test/manager.test.d.ts +1 -0
  70. package/dist/test/manager.test.js +146 -0
  71. package/dist/test/manager.test.js.map +1 -0
  72. package/dist/util.d.ts +29 -0
  73. package/dist/util.js +127 -0
  74. package/dist/util.js.map +1 -0
  75. package/dist/views/layouts/main.hbs +49 -0
  76. package/dist/views/pages/databases.hbs +102 -0
  77. package/dist/views/pages/designer.hbs +81 -0
  78. package/dist/views/pages/error.hbs +12 -0
  79. package/dist/views/pages/form.hbs +62 -0
  80. package/dist/views/pages/home.hbs +123 -0
  81. package/dist/views/pages/query.hbs +94 -0
  82. package/dist/views/pages/schema.hbs +290 -0
  83. package/dist/views/pages/table.hbs +203 -0
  84. package/dist/views/pages/triggers.hbs +162 -0
  85. package/dist/views/pages/views.hbs +134 -0
  86. package/dist/views/partials/navbar.hbs +50 -0
  87. package/dist/views/partials/sidebar.hbs +128 -0
  88. package/package.json +62 -0
package/dist/util.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /** Small shared helpers used across the server. */
2
+ export declare function errorMessage(err: unknown): string;
3
+ /** Row filters keyed by column name. */
4
+ export interface RowFilters {
5
+ [column: string]: string;
6
+ }
7
+ /** Parse the `f` query parameter (URL-encoded JSON object) into row filters. */
8
+ export declare function parseFilters(raw: unknown): RowFilters;
9
+ /** Encode filters back into a `f=…` query string (empty when no filters). */
10
+ export declare function filtersToQS(filters: RowFilters | undefined): string;
11
+ /** Internal tables (kept out of user-facing pickers) start with an underscore. */
12
+ export declare function isInternalTable(name: string): boolean;
13
+ /** Encode primary-key values into a single URL path segment (comma-joined, URL-encoded). */
14
+ export declare function encodePk(values: unknown[]): string;
15
+ /** Decode a primary-key path segment back into individual values. */
16
+ export declare function decodePk(encoded: string): string[];
17
+ /** Normalize a single cell for JSON / display (BLOB → 0x-hex string, undefined → null). */
18
+ export declare function normalizeCell(value: unknown): unknown;
19
+ /** Normalize every cell in a row. */
20
+ export declare function normalizeRow(row: Record<string, unknown>): Record<string, unknown>;
21
+ /**
22
+ * Coerce a raw form value into a value suitable for SQLite binding.
23
+ * - empty string / null / undefined → null ("not set")
24
+ * - `0` stays valid
25
+ * - BOOLEAN → 1/0
26
+ * - INTEGER/REAL → number when numeric
27
+ * - BLOB → Buffer (0x-hex or raw text)
28
+ */
29
+ export declare function coerceFormValue(raw: unknown, columnType: string): unknown;
package/dist/util.js ADDED
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ /** Small shared helpers used across the server. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.errorMessage = errorMessage;
5
+ exports.parseFilters = parseFilters;
6
+ exports.filtersToQS = filtersToQS;
7
+ exports.isInternalTable = isInternalTable;
8
+ exports.encodePk = encodePk;
9
+ exports.decodePk = decodePk;
10
+ exports.normalizeCell = normalizeCell;
11
+ exports.normalizeRow = normalizeRow;
12
+ exports.coerceFormValue = coerceFormValue;
13
+ function errorMessage(err) {
14
+ if (err instanceof Error)
15
+ return err.message;
16
+ return String(err);
17
+ }
18
+ /** Parse the `f` query parameter (URL-encoded JSON object) into row filters. */
19
+ function parseFilters(raw) {
20
+ const out = {};
21
+ const add = (obj) => {
22
+ if (obj && typeof obj === 'object') {
23
+ for (const [k, v] of Object.entries(obj)) {
24
+ if (v !== undefined && v !== null && v !== '')
25
+ out[k] = String(v);
26
+ }
27
+ }
28
+ };
29
+ if (typeof raw === 'string' && raw) {
30
+ try {
31
+ add(JSON.parse(raw));
32
+ }
33
+ catch {
34
+ return {};
35
+ }
36
+ }
37
+ else {
38
+ add(raw);
39
+ }
40
+ return out;
41
+ }
42
+ /** Encode filters back into a `f=…` query string (empty when no filters). */
43
+ function filtersToQS(filters) {
44
+ const out = {};
45
+ for (const [k, v] of Object.entries(filters ?? {})) {
46
+ if (v !== undefined && v !== null && String(v).trim() !== '')
47
+ out[k] = String(v).trim();
48
+ }
49
+ const keys = Object.keys(out);
50
+ if (!keys.length)
51
+ return '';
52
+ return 'f=' + encodeURIComponent(JSON.stringify(out));
53
+ }
54
+ /** Internal tables (kept out of user-facing pickers) start with an underscore. */
55
+ function isInternalTable(name) {
56
+ return name.startsWith('_');
57
+ }
58
+ /** Encode primary-key values into a single URL path segment (comma-joined, URL-encoded). */
59
+ function encodePk(values) {
60
+ return values.map((v) => encodeURIComponent(v == null ? '' : String(v))).join(',');
61
+ }
62
+ /** Decode a primary-key path segment back into individual values. */
63
+ function decodePk(encoded) {
64
+ return String(encoded)
65
+ .split(',')
66
+ .map((s) => {
67
+ try {
68
+ return decodeURIComponent(s);
69
+ }
70
+ catch {
71
+ return s;
72
+ }
73
+ });
74
+ }
75
+ /** Normalize a single cell for JSON / display (BLOB → 0x-hex string, undefined → null). */
76
+ function normalizeCell(value) {
77
+ if (value instanceof Uint8Array) {
78
+ return `0x${Buffer.from(value).toString('hex')}`;
79
+ }
80
+ if (value === undefined)
81
+ return null;
82
+ return value;
83
+ }
84
+ /** Normalize every cell in a row. */
85
+ function normalizeRow(row) {
86
+ const out = {};
87
+ for (const [k, v] of Object.entries(row ?? {}))
88
+ out[k] = normalizeCell(v);
89
+ return out;
90
+ }
91
+ /**
92
+ * Coerce a raw form value into a value suitable for SQLite binding.
93
+ * - empty string / null / undefined → null ("not set")
94
+ * - `0` stays valid
95
+ * - BOOLEAN → 1/0
96
+ * - INTEGER/REAL → number when numeric
97
+ * - BLOB → Buffer (0x-hex or raw text)
98
+ */
99
+ function coerceFormValue(raw, columnType) {
100
+ const type = (columnType || '').toUpperCase();
101
+ if (raw === undefined || raw === null)
102
+ return null;
103
+ if (type === 'BOOLEAN') {
104
+ const s = String(raw).trim().toLowerCase();
105
+ if (s === '')
106
+ return null;
107
+ return s === 'true' || s === '1' || s === 'on' || s === 'yes' || s === 'checked' ? 1 : 0;
108
+ }
109
+ if (type.startsWith('INTEGER') || type === 'REAL') {
110
+ const s = String(raw).trim();
111
+ if (s === '')
112
+ return null;
113
+ const n = Number(s);
114
+ return Number.isFinite(n) ? n : s;
115
+ }
116
+ if (type === 'BLOB') {
117
+ const s = String(raw);
118
+ if (/^0x[0-9a-f]*$/i.test(s))
119
+ return Buffer.from(s.slice(2), 'hex');
120
+ return Buffer.from(s, 'utf8');
121
+ }
122
+ const s = String(raw);
123
+ if (s === '')
124
+ return null;
125
+ return s;
126
+ }
127
+ //# sourceMappingURL=util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="admindb">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>{{title}} · AdminDB</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
10
+ <link rel="stylesheet" href="{{basePath}}/css/app.css" />
11
+ <script>
12
+ window.APP = { basePath: {{{json basePath}}}, readonly: {{{json readonly}}} };
13
+ // Apply the saved theme before first paint to avoid a flash.
14
+ (function () {
15
+ try {
16
+ var t = localStorage.getItem('admindb-theme');
17
+ if (t === 'dark') document.documentElement.setAttribute('data-theme', 'admindbDark');
18
+ } catch (e) {}
19
+ })();
20
+ </script>
21
+ <script src="{{basePath}}/js/api.js" defer></script>
22
+ <script src="{{basePath}}/js/app.js" defer></script>
23
+ </head>
24
+ <body class="min-h-screen bg-base-200 text-base-content">
25
+ {{#if readonly}}
26
+ <div class="flex items-center justify-center gap-2 bg-warning/15 px-4 py-2 text-center text-sm font-semibold text-warning">
27
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
28
+ Read-only mode — this database is open without write access. All writes are disabled.
29
+ </div>
30
+ {{/if}}
31
+ <div class="drawer lg:drawer-open">
32
+ <input id="sidebar-toggle" type="checkbox" class="drawer-toggle" />
33
+ <div class="drawer-content flex min-h-screen flex-col">
34
+ {{> navbar}}
35
+ <main class="mx-auto w-full max-w-[1500px] flex-1 px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
36
+ {{{body}}}
37
+ </main>
38
+ <footer class="border-t border-base-300 px-6 py-4 text-center text-xs text-base-content/40">
39
+ AdminDB · a browser tool for SQLite databases
40
+ </footer>
41
+ </div>
42
+ <div class="drawer-side z-40">
43
+ <label for="sidebar-toggle" aria-label="close sidebar" class="drawer-overlay"></label>
44
+ {{> sidebar}}
45
+ </div>
46
+ </div>
47
+ <div id="toast-container" class="pointer-events-none fixed inset-x-0 top-4 z-50 flex flex-col items-center gap-2 px-4"></div>
48
+ </body>
49
+ </html>
@@ -0,0 +1,102 @@
1
+ <div class="space-y-6">
2
+ <!-- Header -->
3
+ <div>
4
+ <p class="text-sm font-semibold uppercase tracking-wider text-primary">Manager</p>
5
+ <h1 class="mt-1 text-3xl font-extrabold tracking-tight">Databases</h1>
6
+ <p class="mt-1.5 text-sm text-base-content/55">Open a database to browse and edit it, or create a new one.</p>
7
+ </div>
8
+
9
+ <!-- New database -->
10
+ <div class="app-card p-5">
11
+ <div class="flex flex-wrap items-end gap-3">
12
+ <div class="w-full max-w-sm">
13
+ <label class="field-label" for="new-db-name">New database name</label>
14
+ <input id="new-db-name" class="field-input font-mono" placeholder="e.g. analytics" autocomplete="off" spellcheck="false" />
15
+ </div>
16
+ {{#unless readonly}}
17
+ <button id="create-db" type="button" class="btn btn-primary btn-sm mb-0.5 gap-1.5">
18
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" /></svg>
19
+ Create
20
+ </button>
21
+ {{else}}
22
+ <span class="mb-1 text-sm font-medium text-warning">Read-only — creating databases is disabled.</span>
23
+ {{/unless}}
24
+ </div>
25
+ <p class="mt-2 text-xs text-base-content/40">
26
+ New databases are created in <code class="font-mono">{{dbDir}}</code>. Files already there are picked up
27
+ automatically, and any explicitly configured database paths appear here too.
28
+ </p>
29
+ </div>
30
+
31
+ <!-- List -->
32
+ <div>
33
+ <div class="mb-3 flex items-center justify-between">
34
+ <h2 class="text-lg font-bold">Available databases</h2>
35
+ <span class="text-sm text-base-content/45">{{databases.length}} total</span>
36
+ </div>
37
+
38
+ {{#if databases.length}}
39
+ <div class="app-card overflow-hidden">
40
+ <div class="overflow-x-auto">
41
+ <table class="table table-sm">
42
+ <thead>
43
+ <tr class="bg-base-200/70 text-xs uppercase tracking-wide text-base-content/50">
44
+ <th class="font-semibold">Name</th>
45
+ <th class="text-right font-semibold">Tables</th>
46
+ <th class="text-right font-semibold">Size</th>
47
+ <th class="font-semibold">Modified</th>
48
+ <th class="text-right font-semibold">Actions</th>
49
+ </tr>
50
+ </thead>
51
+ <tbody>
52
+ {{#each databases}}
53
+ <tr class="group hover:bg-primary/[0.04]">
54
+ <td>
55
+ <a href="{{../basePath}}/{{this.id}}" class="inline-flex items-center gap-2.5">
56
+ <span class="grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-primary/10 text-primary transition-colors group-hover:bg-primary/20">
57
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 7v10c0 1.1.9 2 2 2h12a2 2 0 002-2V7M4 7c0-1.1.9-2 2-2h12a2 2 0 012 2M4 7c0 1.1.9 2 2 2h12a2 2 0 002-2" /></svg>
58
+ </span>
59
+ <span class="min-w-0">
60
+ <span class="block truncate font-mono text-[13.5px] font-semibold">{{this.name}}</span>
61
+ <span class="block max-w-[220px] truncate text-[11px] text-base-content/40" title="{{this.path}}">{{this.path}}</span>
62
+ </span>
63
+ </a>
64
+ </td>
65
+ <td class="text-right tabular-nums text-base-content/60">{{this.tables}}</td>
66
+ <td class="text-right tabular-nums text-base-content/60">{{this.sizeLabel}}</td>
67
+ <td class="text-[13px] text-base-content/55">{{this.modifiedLabel}}</td>
68
+ <td class="whitespace-nowrap text-right">
69
+ <div class="row-actions">
70
+ <a href="{{../basePath}}/{{this.id}}" class="btn btn-ghost btn-xs gap-1 text-base-content/70 hover:text-primary">
71
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13 7l5 5m0 0l-5 5m5-5H6" /></svg>
72
+ Open
73
+ </a>
74
+ {{#unless ../readonly}}
75
+ <button type="button" class="btn btn-ghost btn-xs gap-1 text-error js-delete-db" data-id="{{this.id}}">
76
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
77
+ Delete
78
+ </button>
79
+ {{/unless}}
80
+ </div>
81
+ </td>
82
+ </tr>
83
+ {{/each}}
84
+ </tbody>
85
+ </table>
86
+ </div>
87
+ </div>
88
+ {{else}}
89
+ <div class="empty-state">
90
+ <span class="empty-icon">
91
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 7v10c0 1.1.9 2 2 2h12a2 2 0 002-2V7M4 7c0-1.1.9-2 2-2h12a2 2 0 012 2M4 7c0 1.1.9 2 2 2h12a2 2 0 002-2" /></svg>
92
+ </span>
93
+ <p class="text-sm font-medium text-base-content/70">No databases yet</p>
94
+ <p class="max-w-sm text-sm text-base-content/45">
95
+ Create one above, or drop <code class="font-mono">.db</code> files into
96
+ <code class="font-mono">{{dbDir}}</code> and refresh.
97
+ </p>
98
+ </div>
99
+ {{/if}}
100
+ </div>
101
+ </div>
102
+ <script src="{{basePath}}/js/databases.js" defer></script>
@@ -0,0 +1,81 @@
1
+ <div class="space-y-6">
2
+ <!-- Header -->
3
+ <div>
4
+ <p class="text-sm font-semibold uppercase tracking-wider text-primary">Designer</p>
5
+ <h1 class="mt-1 text-3xl font-extrabold tracking-tight">Create table</h1>
6
+ <p class="mt-1.5 text-sm text-base-content/55">Define columns visually — the SQL preview updates live as you type.</p>
7
+ </div>
8
+
9
+ <div class="grid gap-6 lg:grid-cols-[1fr_380px]">
10
+ <!-- Left: editor -->
11
+ <div class="space-y-5">
12
+ <div class="app-card p-5">
13
+ <label class="field-label">
14
+ Table name
15
+ <span class="chip chip-type">required</span>
16
+ </label>
17
+ <input id="table-name" type="text" class="field-input font-mono" placeholder="e.g. users" autocomplete="off" spellcheck="false" />
18
+ </div>
19
+
20
+ <div class="app-card">
21
+ <div class="flex flex-wrap items-center justify-between gap-2 border-b border-base-200 px-5 py-4">
22
+ <h2 class="text-sm font-semibold uppercase tracking-wide text-base-content/45">Columns</h2>
23
+ <button id="add-column" type="button" class="btn btn-outline btn-sm gap-1.5">
24
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" /></svg>
25
+ Add column
26
+ </button>
27
+ </div>
28
+ <div id="columns" class="space-y-3 p-4"></div>
29
+ <div id="designer-error" class="hidden px-4 pb-4"></div>
30
+ <div class="flex flex-wrap gap-2 border-t border-base-200 px-5 py-4">
31
+ {{#unless readonly}}
32
+ <button id="create-table" type="button" class="btn btn-primary gap-1.5">
33
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" /></svg>
34
+ Create table
35
+ </button>
36
+ {{else}}
37
+ <span class="text-sm font-medium text-warning">Read-only — table creation is disabled.</span>
38
+ {{/unless}}
39
+ <button id="copy-sql" type="button" class="btn btn-outline gap-1.5" title="Copy the preview SQL">
40
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3" /></svg>
41
+ Copy SQL
42
+ </button>
43
+ </div>
44
+ </div>
45
+ </div>
46
+
47
+ <!-- Right: live preview + tips -->
48
+ <aside class="space-y-4 self-start lg:sticky lg:top-24">
49
+ <div class="app-card overflow-hidden">
50
+ <div class="flex items-center justify-between border-b border-base-200 bg-base-200/40 px-4 py-3">
51
+ <h2 class="flex items-center gap-2 text-sm font-semibold text-base-content/60">
52
+ <span class="grid h-5 w-5 place-items-center rounded-md bg-primary/10 text-primary">
53
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
54
+ </span>
55
+ SQL preview
56
+ </h2>
57
+ <span id="preview-status" class="text-[11px] font-medium text-base-content/40">waiting…</span>
58
+ </div>
59
+ <div class="p-4">
60
+ <pre id="sql-preview" class="code-panel hidden"><code id="preview-code"></code></pre>
61
+ <div id="preview-placeholder" class="rounded-xl border border-dashed border-base-300 px-4 py-8 text-center text-sm text-base-content/35">
62
+ Your <code class="font-mono">CREATE TABLE</code> statement will appear here as you build it.
63
+ </div>
64
+ </div>
65
+ </div>
66
+
67
+ <div class="rounded-2xl border border-base-300/70 bg-base-100 p-4">
68
+ <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wider text-base-content/45">Type guide</h3>
69
+ <ul class="space-y-1.5 text-[13px] text-base-content/60">
70
+ <li class="flex items-center justify-between gap-2"><span class="font-mono">TEXT</span><span class="text-base-content/40">Free-form text</span></li>
71
+ <li class="flex items-center justify-between gap-2"><span class="font-mono">INTEGER</span><span class="text-base-content/40">Whole numbers</span></li>
72
+ <li class="flex items-center justify-between gap-2"><span class="font-mono">REAL</span><span class="text-base-content/40">Decimal numbers</span></li>
73
+ <li class="flex items-center justify-between gap-2"><span class="font-mono">BLOB</span><span class="text-base-content/40">Binary data</span></li>
74
+ <li class="flex items-center justify-between gap-2"><span class="font-mono">BOOLEAN</span><span class="text-base-content/40">Stored as 0/1</span></li>
75
+ <li class="flex items-center justify-between gap-2"><span class="font-mono">DATE</span><span class="text-base-content/40">Datetime + now default</span></li>
76
+ </ul>
77
+ </div>
78
+ </aside>
79
+ </div>
80
+ </div>
81
+ <script src="{{basePath}}/js/designer.js" defer></script>
@@ -0,0 +1,12 @@
1
+ <div class="mx-auto mt-16 max-w-md space-y-6 text-center">
2
+ <div class="mx-auto grid h-20 w-20 place-items-center rounded-3xl bg-error/10 text-error">
3
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
4
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
5
+ </svg>
6
+ </div>
7
+ <div>
8
+ <h1 class="text-5xl font-extrabold tracking-tight text-error">{{status}}</h1>
9
+ <p class="mt-2 text-base-content/60">{{error}}</p>
10
+ </div>
11
+ <a href="{{basePath}}/" class="btn btn-primary">Back to home</a>
12
+ </div>
@@ -0,0 +1,62 @@
1
+ <div class="mx-auto max-w-3xl space-y-5">
2
+ <!-- Header -->
3
+ <div class="flex items-center gap-3">
4
+ <a href="{{basePath}}/tables/{{table}}" class="btn btn-circle btn-ghost btn-sm" aria-label="Back to table">
5
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18" /></svg>
6
+ </a>
7
+ <div>
8
+ <h1 class="text-2xl font-bold tracking-tight">
9
+ {{#if (eq mode 'edit')}}Edit row{{else}}New row{{/if}}
10
+ <span class="text-base-content/40">in</span> <span class="font-mono text-primary">{{table}}</span>
11
+ </h1>
12
+ <p class="text-sm text-base-content/55">{{infoSummary}}</p>
13
+ </div>
14
+ </div>
15
+
16
+ <div class="app-card">
17
+ <div class="border-b border-base-200 px-6 py-4">
18
+ <h2 class="text-sm font-semibold uppercase tracking-wide text-base-content/45">Row fields</h2>
19
+ </div>
20
+ <div class="p-6">
21
+ <div id="row-fields" class="grid grid-cols-1 gap-x-6 gap-y-5 md:grid-cols-2"></div>
22
+ <div id="form-error" class="hidden"></div>
23
+ <div class="mt-6 flex flex-wrap items-center gap-2 border-t border-base-200 pt-5">
24
+ {{#unless readonly}}
25
+ <button id="row-submit" type="button" class="btn btn-primary">
26
+ {{#if (eq mode 'edit')}}Update row{{else}}Insert row{{/if}}
27
+ </button>
28
+ <button id="row-generate" type="button" class="btn btn-outline gap-1.5">
29
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /></svg>
30
+ Get query
31
+ </button>
32
+ {{else}}
33
+ <span class="text-sm font-medium text-warning">This database is read-only — rows cannot be {{#if (eq mode 'edit')}}updated{{else}}inserted{{/if}}.</span>
34
+ {{/unless}}
35
+ <a href="{{basePath}}/tables/{{table}}" class="btn btn-ghost">Cancel</a>
36
+ </div>
37
+ <p class="mt-3 text-xs text-base-content/40">
38
+ Columns with defaults are pre-filled from the schema. Empty fields are treated as "not set" (DB defaults
39
+ apply). <code class="font-mono">0</code> is a valid value. "Get query" only previews the SQL — it never executes.
40
+ </p>
41
+ </div>
42
+ </div>
43
+
44
+ <div id="generated-sql" class="hidden">
45
+ <div class="app-card">
46
+ <div class="flex items-center justify-between border-b border-base-200 px-6 py-4">
47
+ <h2 class="flex items-center gap-2 text-sm font-semibold uppercase tracking-wide text-base-content/45">
48
+ <span class="grid h-6 w-6 place-items-center rounded-md bg-primary/10 text-primary">
49
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /></svg>
50
+ </span>
51
+ Generated SQL
52
+ </h2>
53
+ <button id="copy-sql" type="button" class="btn btn-ghost btn-xs gap-1">Copy</button>
54
+ </div>
55
+ <div class="p-4">
56
+ <pre class="code-panel"><code id="generated-sql-code"></code></pre>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ <script id="form-config" type="application/json">{{{json config}}}</script>
62
+ <script src="{{basePath}}/js/forms.js" defer></script>
@@ -0,0 +1,123 @@
1
+ <div class="space-y-8">
2
+ <!-- Header -->
3
+ <div class="flex flex-wrap items-end justify-between gap-4">
4
+ <div>
5
+ <p class="text-sm font-semibold uppercase tracking-wider text-primary">Overview</p>
6
+ <h1 class="mt-1 text-3xl font-extrabold tracking-tight">Database</h1>
7
+ <p class="mt-1.5 text-sm text-base-content/55">Browse, edit, and query the connected SQLite database from your browser.</p>
8
+ </div>
9
+ <div class="flex gap-2">
10
+ <a href="{{basePath}}/query" class="btn btn-ghost btn-sm gap-1.5">
11
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
12
+ Query
13
+ </a>
14
+ {{#unless readonly}}
15
+ <a href="{{basePath}}/designer" class="btn btn-primary btn-sm gap-1.5">
16
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" /></svg>
17
+ New table
18
+ </a>
19
+ {{/unless}}
20
+ </div>
21
+ </div>
22
+
23
+ <!-- Stat cards -->
24
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
25
+ <div class="stat-card">
26
+ <span class="stat-icon bg-primary/10 text-primary">
27
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>
28
+ </span>
29
+ <div>
30
+ <div class="text-2xl font-extrabold tabular-nums">{{tables.length}}</div>
31
+ <div class="text-[13px] text-base-content/50">Tables</div>
32
+ </div>
33
+ </div>
34
+ <div class="stat-card">
35
+ <span class="stat-icon bg-secondary/10 text-secondary">
36
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
37
+ </span>
38
+ <div>
39
+ <div class="text-2xl font-extrabold tabular-nums">{{totalRows}}</div>
40
+ <div class="text-[13px] text-base-content/50">Total rows</div>
41
+ </div>
42
+ </div>
43
+ <div class="stat-card">
44
+ <span class="stat-icon bg-accent/10 text-accent">
45
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" /></svg>
46
+ </span>
47
+ <div>
48
+ <div class="text-2xl font-extrabold tabular-nums">{{savedQueries}}</div>
49
+ <div class="text-[13px] text-base-content/50">Saved queries</div>
50
+ </div>
51
+ </div>
52
+ <div class="stat-card">
53
+ <span class="stat-icon bg-warning/10 text-warning">
54
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 7v10c0 1.1.9 2 2 2h12a2 2 0 002-2V7M4 7c0-1.1.9-2 2-2h12a2 2 0 012 2M4 7c0 1.1.9 2 2 2h12a2 2 0 002-2" /></svg>
55
+ </span>
56
+ <div>
57
+ <div class="truncate font-mono text-lg font-bold" title="{{dbPath}}">{{dbPath}}</div>
58
+ <div class="text-[13px] text-base-content/50">Database file</div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+
63
+ <!-- Tables -->
64
+ <div>
65
+ <div class="mb-3 flex items-center justify-between">
66
+ <h2 class="text-lg font-bold">Tables</h2>
67
+ <span class="text-sm text-base-content/45">{{tables.length}} total</span>
68
+ </div>
69
+
70
+ {{#if tables.length}}
71
+ <div class="app-card overflow-hidden">
72
+ <div class="overflow-x-auto">
73
+ <table class="table table-sm">
74
+ <thead>
75
+ <tr class="bg-base-200/70 text-xs uppercase tracking-wide text-base-content/50">
76
+ <th class="font-semibold">Table</th>
77
+ <th class="text-right font-semibold">Columns</th>
78
+ <th class="text-right font-semibold">Rows</th>
79
+ <th class="text-right font-semibold">Actions</th>
80
+ </tr>
81
+ </thead>
82
+ <tbody>
83
+ {{#each tables}}
84
+ <tr class="group hover:bg-base-200/50">
85
+ <td>
86
+ <a href="{{../basePath}}/tables/{{this}}" class="inline-flex items-center gap-2.5 font-medium">
87
+ <span class="grid h-8 w-8 place-items-center rounded-lg bg-primary/10 text-primary transition-colors group-hover:bg-primary/20">
88
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>
89
+ </span>
90
+ <span class="font-mono text-[13.5px]">{{this}}</span>
91
+ </a>
92
+ </td>
93
+ <td class="text-right tabular-nums text-base-content/60">{{lookup ../cols this}}</td>
94
+ <td class="text-right tabular-nums text-base-content/60">{{lookup ../counts this}}</td>
95
+ <td class="text-right">
96
+ <a href="{{../basePath}}/tables/{{this}}" class="btn btn-ghost btn-xs gap-1 text-base-content/70 hover:text-primary">
97
+ Browse
98
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13 7l5 5m0 0l-5 5m5-5H6" /></svg>
99
+ </a>
100
+ </td>
101
+ </tr>
102
+ {{/each}}
103
+ </tbody>
104
+ </table>
105
+ </div>
106
+ </div>
107
+ {{else}}
108
+ <div class="empty-state">
109
+ <span class="empty-icon">
110
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>
111
+ </span>
112
+ <p class="text-sm font-medium text-base-content/70">No tables yet</p>
113
+ <p class="max-w-sm text-sm text-base-content/45">Create one with the table designer, or run a SQL statement in the query editor to get started.</p>
114
+ <div class="mt-2 flex gap-2">
115
+ {{#unless readonly}}
116
+ <a href="{{basePath}}/designer" class="btn btn-primary btn-sm">New table</a>
117
+ {{/unless}}
118
+ <a href="{{basePath}}/query" class="btn btn-outline btn-sm">Query editor</a>
119
+ </div>
120
+ </div>
121
+ {{/if}}
122
+ </div>
123
+ </div>