sqlite-hub 0.16.0 → 0.17.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.
@@ -218,6 +218,428 @@
218
218
  min-height: 0;
219
219
  }
220
220
 
221
+ .documents-view-surface {
222
+ height: 100%;
223
+ min-height: 0;
224
+ overflow: hidden;
225
+ }
226
+
227
+ .documents-view-frame {
228
+ display: flex;
229
+ flex-direction: column;
230
+ height: 100%;
231
+ min-height: 0;
232
+ overflow: hidden;
233
+ }
234
+
235
+ .documents-view {
236
+ background: var(--color-surface);
237
+ border: 1px solid var(--border-medium);
238
+ display: flex;
239
+ flex: 1;
240
+ height: 100%;
241
+ min-height: 0;
242
+ overflow: hidden;
243
+ }
244
+
245
+ .documents-view__sidebar {
246
+ background: var(--color-surface-low);
247
+ border-right: 1px solid var(--border-medium);
248
+ display: flex;
249
+ flex: 0 0 clamp(17rem, 23vw, 21rem);
250
+ flex-direction: column;
251
+ min-height: 100%;
252
+ overflow: hidden;
253
+ width: clamp(17rem, 23vw, 21rem);
254
+ }
255
+
256
+ .documents-view__sidebar-header {
257
+ align-items: center;
258
+ border-bottom: 1px solid var(--border-medium);
259
+ display: flex;
260
+ justify-content: space-between;
261
+ padding: var(--spacing-5);
262
+ }
263
+
264
+ .documents-view__sidebar-body {
265
+ display: flex;
266
+ flex: 1;
267
+ flex-direction: column;
268
+ gap: var(--spacing-2);
269
+ min-height: 0;
270
+ overflow: hidden;
271
+ padding: var(--spacing-3);
272
+ }
273
+
274
+ .documents-list-item {
275
+ align-items: flex-start;
276
+ background: var(--color-surface-lowest);
277
+ border: 1px solid rgb(var(--rgb-outline) / 0.12);
278
+ color: var(--color-on-surface);
279
+ display: flex;
280
+ gap: var(--spacing-3);
281
+ min-width: 0;
282
+ padding: var(--spacing-3);
283
+ text-align: left;
284
+ transition:
285
+ background-color var(--transition-fast),
286
+ border-color var(--transition-fast),
287
+ color var(--transition-fast);
288
+ }
289
+
290
+ .documents-list-item:hover,
291
+ .documents-list-item.is-selected {
292
+ background: var(--color-surface-container);
293
+ border-color: var(--primary-alpha-24);
294
+ }
295
+
296
+ .documents-list-item.is-selected {
297
+ box-shadow: inset 2px 0 0 var(--color-primary-container);
298
+ }
299
+
300
+ .documents-list-item__icon {
301
+ color: var(--color-primary-container);
302
+ font-size: 1.1rem;
303
+ line-height: 1.25;
304
+ }
305
+
306
+ .documents-list-item__body {
307
+ display: block;
308
+ min-width: 0;
309
+ }
310
+
311
+ .documents-list-item__title {
312
+ display: block;
313
+ font-family: var(--font-family-mono);
314
+ font-size: 0.75rem;
315
+ font-weight: 700;
316
+ overflow: hidden;
317
+ text-overflow: ellipsis;
318
+ white-space: nowrap;
319
+ }
320
+
321
+ .documents-list-item__meta,
322
+ .documents-list-empty {
323
+ color: rgb(var(--rgb-on-surface-variant) / 0.55);
324
+ display: block;
325
+ font-family: var(--font-family-mono);
326
+ font-size: 0.625rem;
327
+ margin-top: 0.25rem;
328
+ text-transform: uppercase;
329
+ }
330
+
331
+ .documents-list-empty {
332
+ padding: var(--spacing-4);
333
+ }
334
+
335
+ .documents-view__detail {
336
+ display: flex;
337
+ flex: 1;
338
+ flex-direction: column;
339
+ min-height: 0;
340
+ min-width: 0;
341
+ overflow: hidden;
342
+ }
343
+
344
+ .documents-toolbar {
345
+ align-items: center;
346
+ border-bottom: 1px solid var(--border-medium);
347
+ display: flex;
348
+ flex-wrap: wrap;
349
+ gap: var(--spacing-4);
350
+ justify-content: space-between;
351
+ padding: var(--spacing-4);
352
+ }
353
+
354
+ .documents-filename-field {
355
+ display: flex;
356
+ flex-direction: column;
357
+ padding: var(--spacing-4);
358
+ padding-bottom: 0;
359
+ max-width: 32rem;
360
+ min-width: min(100%, 14rem);
361
+ }
362
+
363
+ .documents-filename-field span {
364
+ color: rgb(var(--rgb-on-surface-variant) / 0.6);
365
+ font-family: var(--font-family-mono);
366
+ font-size: 0.625rem;
367
+ font-weight: 700;
368
+ letter-spacing: 0.18em;
369
+ text-transform: uppercase;
370
+ }
371
+
372
+ .documents-filename-field .control-input {
373
+ background: var(--color-surface-lowest);
374
+ border: 1px solid rgb(var(--rgb-outline) / 0.18);
375
+ color: var(--color-on-surface);
376
+ height: var(--control-height);
377
+ padding: 0 var(--spacing-3);
378
+ }
379
+
380
+ .documents-filename-field .control-input:focus {
381
+ border-color: var(--color-primary-container);
382
+ }
383
+
384
+ .documents-filename-field .control-input:disabled {
385
+ color: rgb(var(--rgb-on-surface-variant) / 0.72);
386
+ opacity: 1;
387
+ }
388
+
389
+ .documents-toolbar__actions {
390
+ display: flex;
391
+ flex-wrap: wrap;
392
+ gap: var(--spacing-2);
393
+ justify-content: flex-end;
394
+ }
395
+
396
+ .documents-workspace {
397
+ display: grid;
398
+ flex: 1;
399
+ grid-template-columns: 1fr;
400
+ min-height: 0;
401
+ overflow: hidden;
402
+ }
403
+
404
+ .documents-workspace--split {
405
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
406
+ }
407
+
408
+ .documents-pane {
409
+ display: flex;
410
+ flex-direction: column;
411
+ min-height: 0;
412
+ min-width: 0;
413
+ overflow: hidden;
414
+ }
415
+
416
+ .documents-pane + .documents-pane {
417
+ border-left: 1px solid var(--border-medium);
418
+ }
419
+
420
+ .documents-pane__header {
421
+ align-items: center;
422
+ background: var(--color-surface-low);
423
+ border-bottom: 1px solid var(--border-medium);
424
+ color: rgb(var(--rgb-on-surface-variant) / 0.62);
425
+ display: flex;
426
+ font-family: var(--font-family-mono);
427
+ font-size: 0.625rem;
428
+ font-weight: 700;
429
+ justify-content: space-between;
430
+ letter-spacing: 0.18em;
431
+ padding: var(--spacing-3) var(--spacing-4);
432
+ text-transform: uppercase;
433
+ }
434
+
435
+ .documents-editor-input {
436
+ background: var(--color-surface-lowest);
437
+ border: 0;
438
+ color: var(--color-on-surface);
439
+ flex: 1;
440
+ font-family: var(--font-family-mono);
441
+ font-size: 0.875rem;
442
+ height: 100%;
443
+ line-height: 1.7;
444
+ min-height: 0;
445
+ outline: none;
446
+ overflow-y: auto;
447
+ padding: var(--spacing-5);
448
+ resize: none;
449
+ }
450
+
451
+ .documents-editor-input:focus {
452
+ box-shadow: inset 0 0 0 1px var(--color-primary-container);
453
+ }
454
+
455
+ .document-markdown-preview {
456
+ background: var(--color-surface-lowest);
457
+ color: var(--color-on-surface);
458
+ flex: 1;
459
+ font-size: 0.9375rem;
460
+ line-height: 1.7;
461
+ min-height: 0;
462
+ overflow-y: auto;
463
+ padding: var(--spacing-6);
464
+ }
465
+
466
+ .document-markdown-preview > * + * {
467
+ margin-top: 1rem;
468
+ }
469
+
470
+ .document-markdown-preview h1,
471
+ .document-markdown-preview h2,
472
+ .document-markdown-preview h3 {
473
+ color: var(--color-primary-container);
474
+ font-family: var(--font-family-headline);
475
+ font-weight: 900;
476
+ letter-spacing: 0;
477
+ line-height: 1.1;
478
+ text-transform: uppercase;
479
+ }
480
+
481
+ .document-markdown-preview h1 {
482
+ font-size: 2.25rem;
483
+ }
484
+
485
+ .document-markdown-preview h2 {
486
+ font-size: 1.5rem;
487
+ }
488
+
489
+ .document-markdown-preview h3 {
490
+ font-size: 1.15rem;
491
+ }
492
+
493
+ .document-markdown-preview a {
494
+ color: var(--color-primary-container);
495
+ text-decoration: underline;
496
+ text-underline-offset: 0.18em;
497
+ }
498
+
499
+ .document-markdown-preview code {
500
+ background: var(--color-surface-container);
501
+ color: var(--color-primary-container);
502
+ font-family: var(--font-family-mono);
503
+ font-size: 0.85em;
504
+ padding: 0.1rem 0.25rem;
505
+ }
506
+
507
+ .document-markdown-preview pre {
508
+ background: var(--color-surface-low);
509
+ border: 1px solid rgb(var(--rgb-outline) / 0.12);
510
+ overflow-x: auto;
511
+ padding: var(--spacing-4);
512
+ }
513
+
514
+ .document-markdown-preview pre code {
515
+ background: transparent;
516
+ color: inherit;
517
+ padding: 0;
518
+ }
519
+
520
+ .document-markdown-preview blockquote {
521
+ border-left: 2px solid var(--color-primary-container);
522
+ color: rgb(var(--rgb-on-surface) / 0.72);
523
+ padding-left: var(--spacing-4);
524
+ }
525
+
526
+ .document-markdown-preview table {
527
+ border-collapse: collapse;
528
+ width: 100%;
529
+ }
530
+
531
+ .document-markdown-preview th,
532
+ .document-markdown-preview td {
533
+ border: 1px solid rgb(var(--rgb-outline) / 0.16);
534
+ padding: 0.5rem 0.75rem;
535
+ text-align: left;
536
+ }
537
+
538
+ .document-markdown-preview ul,
539
+ .document-markdown-preview ol {
540
+ padding-left: 1.5rem;
541
+ }
542
+
543
+ .document-markdown-preview ul {
544
+ list-style: disc outside;
545
+ }
546
+
547
+ .document-markdown-preview ol {
548
+ list-style: decimal outside;
549
+ }
550
+
551
+ .document-markdown-preview li + li {
552
+ margin-top: 0.35rem;
553
+ }
554
+
555
+ .document-markdown-task-list {
556
+ display: flex;
557
+ flex-direction: column;
558
+ gap: var(--spacing-2);
559
+ list-style: none;
560
+ padding-left: 0;
561
+ }
562
+
563
+ .document-markdown-task-label {
564
+ align-items: flex-start;
565
+ cursor: pointer;
566
+ display: flex;
567
+ gap: var(--spacing-3);
568
+ }
569
+
570
+ .document-markdown-task-checkbox {
571
+ accent-color: var(--color-primary-container);
572
+ margin-top: 0.35rem;
573
+ }
574
+
575
+ .document-markdown-task-item.is-checked .document-markdown-task-text {
576
+ color: rgb(var(--rgb-on-surface-variant) / 0.64);
577
+ text-decoration: line-through;
578
+ }
579
+
580
+ .document-markdown-empty,
581
+ .documents-empty-state {
582
+ color: rgb(var(--rgb-on-surface-variant) / 0.55);
583
+ }
584
+
585
+ .documents-empty-state {
586
+ align-items: center;
587
+ display: flex;
588
+ flex: 1;
589
+ flex-direction: column;
590
+ justify-content: center;
591
+ padding: var(--spacing-8);
592
+ text-align: center;
593
+ }
594
+
595
+ .documents-empty-state .material-symbols-outlined {
596
+ color: rgb(var(--rgb-on-surface-variant) / 0.28);
597
+ font-size: 3.5rem;
598
+ margin-bottom: var(--spacing-3);
599
+ }
600
+
601
+ .documents-error {
602
+ background: rgb(var(--rgb-error) / 0.08);
603
+ border-bottom: 1px solid rgb(var(--rgb-error) / 0.28);
604
+ color: var(--color-error);
605
+ font-size: 0.8125rem;
606
+ padding: var(--spacing-3) var(--spacing-4);
607
+ }
608
+
609
+ @media (max-width: 980px) {
610
+ .documents-view {
611
+ min-height: 0;
612
+ }
613
+
614
+ .documents-view,
615
+ .documents-workspace--split {
616
+ grid-template-columns: 1fr;
617
+ }
618
+
619
+ .documents-view {
620
+ flex-direction: column;
621
+ }
622
+
623
+ .documents-view__sidebar {
624
+ border-bottom: 1px solid var(--border-medium);
625
+ border-right: 0;
626
+ flex: 0 0 auto;
627
+ max-height: 18rem;
628
+ min-height: 18rem;
629
+ width: 100%;
630
+ }
631
+
632
+ .documents-pane + .documents-pane {
633
+ border-left: 0;
634
+ border-top: 1px solid var(--border-medium);
635
+ }
636
+
637
+ .documents-editor-input,
638
+ .document-markdown-preview {
639
+ min-height: 0;
640
+ }
641
+ }
642
+
221
643
  .structure-sidebar__section + .structure-sidebar__section {
222
644
  margin-top: 1.5rem;
223
645
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-hub",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "SQLite-only local management app backend and SPA shell",
5
5
  "main": "server/server.js",
6
6
  "bin": {
@@ -26,6 +26,7 @@
26
26
  "express": "^5.2.1",
27
27
  "express-rate-limit": "^8.5.2",
28
28
  "helmet": "^8.2.0",
29
+ "marked": "^18.0.5",
29
30
  "material-symbols": "^0.44.9"
30
31
  },
31
32
  "devDependencies": {
@@ -0,0 +1,163 @@
1
+ const express = require("express");
2
+ const path = require("node:path");
3
+ const { DatabaseRequiredError, route, successResponse } = require("../utils/errors");
4
+
5
+ function getActiveDatabaseKey(connectionManager) {
6
+ return connectionManager.getActiveConnection()?.id ?? null;
7
+ }
8
+
9
+ function requireActiveDatabaseKey(connectionManager) {
10
+ const databaseKey = getActiveDatabaseKey(connectionManager);
11
+
12
+ if (!databaseKey) {
13
+ throw new DatabaseRequiredError();
14
+ }
15
+
16
+ return databaseKey;
17
+ }
18
+
19
+ function resolveActiveDatabaseDocumentName(connection) {
20
+ const label = String(connection?.label ?? "").trim();
21
+
22
+ if (label) {
23
+ return label;
24
+ }
25
+
26
+ const basename = path.basename(String(connection?.path ?? "").trim());
27
+ return basename || "Database";
28
+ }
29
+
30
+ function buildInitialDocumentPayload(connection) {
31
+ const databaseName = resolveActiveDatabaseDocumentName(connection);
32
+
33
+ return {
34
+ title: databaseName,
35
+ filename: databaseName,
36
+ content: `# ${databaseName}\n`,
37
+ };
38
+ }
39
+
40
+ function pickDocumentPatch(body = {}) {
41
+ const patch = {};
42
+
43
+ for (const field of ["title", "filename", "content"]) {
44
+ if (Object.prototype.hasOwnProperty.call(body, field)) {
45
+ patch[field] = body[field];
46
+ }
47
+ }
48
+
49
+ return patch;
50
+ }
51
+
52
+ function ensureDatabaseDocuments({ appStateStore, connectionManager, databaseKey }) {
53
+ let items = appStateStore.listDatabaseDocuments(databaseKey);
54
+
55
+ if (items.length > 0) {
56
+ return items;
57
+ }
58
+
59
+ appStateStore.createDatabaseDocument(
60
+ databaseKey,
61
+ buildInitialDocumentPayload(connectionManager.getActiveConnection())
62
+ );
63
+
64
+ return appStateStore.listDatabaseDocuments(databaseKey);
65
+ }
66
+
67
+ function createDocumentsRouter({ appStateStore, connectionManager }) {
68
+ const router = express.Router();
69
+
70
+ router.get(
71
+ "/",
72
+ route((req, res) => {
73
+ const databaseKey = requireActiveDatabaseKey(connectionManager);
74
+
75
+ res.json(
76
+ successResponse({
77
+ data: {
78
+ items: ensureDatabaseDocuments({ appStateStore, connectionManager, databaseKey }),
79
+ },
80
+ metadata: { databaseKey },
81
+ })
82
+ );
83
+ })
84
+ );
85
+
86
+ router.post(
87
+ "/",
88
+ route((req, res) => {
89
+ const databaseKey = requireActiveDatabaseKey(connectionManager);
90
+ const document = appStateStore.createDatabaseDocument(databaseKey, {
91
+ title: req.body?.title,
92
+ filename: req.body?.filename,
93
+ content: req.body?.content,
94
+ });
95
+
96
+ res.status(201).json(
97
+ successResponse({
98
+ message: "Document created.",
99
+ data: document,
100
+ metadata: { databaseKey },
101
+ })
102
+ );
103
+ })
104
+ );
105
+
106
+ router.get(
107
+ "/:documentId",
108
+ route((req, res) => {
109
+ const databaseKey = requireActiveDatabaseKey(connectionManager);
110
+
111
+ res.json(
112
+ successResponse({
113
+ data: appStateStore.getDatabaseDocument(databaseKey, req.params.documentId),
114
+ metadata: { databaseKey },
115
+ })
116
+ );
117
+ })
118
+ );
119
+
120
+ router.patch(
121
+ "/:documentId",
122
+ route((req, res) => {
123
+ const databaseKey = requireActiveDatabaseKey(connectionManager);
124
+
125
+ res.json(
126
+ successResponse({
127
+ message: "Document saved.",
128
+ data: appStateStore.updateDatabaseDocument(
129
+ databaseKey,
130
+ req.params.documentId,
131
+ pickDocumentPatch(req.body)
132
+ ),
133
+ metadata: { databaseKey },
134
+ })
135
+ );
136
+ })
137
+ );
138
+
139
+ router.delete(
140
+ "/:documentId",
141
+ route((req, res) => {
142
+ const databaseKey = requireActiveDatabaseKey(connectionManager);
143
+
144
+ res.json(
145
+ successResponse({
146
+ message: "Document deleted.",
147
+ data: appStateStore.deleteDatabaseDocument(databaseKey, req.params.documentId),
148
+ metadata: { databaseKey },
149
+ })
150
+ );
151
+ })
152
+ );
153
+
154
+ return router;
155
+ }
156
+
157
+ module.exports = {
158
+ buildInitialDocumentPayload,
159
+ createDocumentsRouter,
160
+ ensureDatabaseDocuments,
161
+ pickDocumentPatch,
162
+ resolveActiveDatabaseDocumentName,
163
+ };
@@ -1,4 +1,5 @@
1
1
  const express = require("express");
2
+ const Database = require("better-sqlite3");
2
3
  const fs = require("node:fs");
3
4
  const path = require("node:path");
4
5
  const { route, successResponse } = require("../utils/errors");
@@ -9,6 +10,23 @@ function readAppVersion() {
9
10
  return packageJson.version ?? "0.0.0";
10
11
  }
11
12
 
13
+ function readSqliteVersion() {
14
+ const db = new Database(":memory:");
15
+
16
+ try {
17
+ return db.prepare("SELECT sqlite_version() AS version").get().version ?? "unknown";
18
+ } finally {
19
+ db.close();
20
+ }
21
+ }
22
+
23
+ function readSettingsMetadata() {
24
+ return {
25
+ appVersion: readAppVersion(),
26
+ sqliteVersion: readSqliteVersion(),
27
+ };
28
+ }
29
+
12
30
  function createSettingsRouter({ appStateStore }) {
13
31
  const router = express.Router();
14
32
 
@@ -18,9 +36,7 @@ function createSettingsRouter({ appStateStore }) {
18
36
  res.json(
19
37
  successResponse({
20
38
  data: appStateStore.getSettings(),
21
- metadata: {
22
- appVersion: readAppVersion(),
23
- },
39
+ metadata: readSettingsMetadata(),
24
40
  })
25
41
  );
26
42
  })
@@ -34,9 +50,7 @@ function createSettingsRouter({ appStateStore }) {
34
50
  successResponse({
35
51
  message: "Settings updated.",
36
52
  data: settings,
37
- metadata: {
38
- appVersion: readAppVersion(),
39
- },
53
+ metadata: readSettingsMetadata(),
40
54
  })
41
55
  );
42
56
  })
@@ -47,4 +61,6 @@ function createSettingsRouter({ appStateStore }) {
47
61
 
48
62
  module.exports = {
49
63
  createSettingsRouter,
64
+ readSettingsMetadata,
65
+ readSqliteVersion,
50
66
  };
package/server/server.js CHANGED
@@ -25,6 +25,7 @@ const { createTableDesignerRouter } = require("./routes/tableDesigner");
25
25
  const { createMediaTaggingRouter } = require("./routes/mediaTagging");
26
26
  const { createSettingsRouter } = require("./routes/settings");
27
27
  const { createExportRouter } = require("./routes/export");
28
+ const { createDocumentsRouter } = require("./routes/documents");
28
29
 
29
30
  const PACKAGE_ROOT = path.resolve(__dirname, "..");
30
31
  const FRONTEND_ROOT = path.join(PACKAGE_ROOT, "frontend");
@@ -107,6 +108,7 @@ app.use("/api/table-designer", createTableDesignerRouter({ tableDesignerService
107
108
  app.use("/api/media-tagging", createMediaTaggingRouter({ mediaTaggingService }));
108
109
  app.use("/api/settings", createSettingsRouter({ appStateStore }));
109
110
  app.use("/api/export", createExportRouter({ exportService }));
111
+ app.use("/api/documents", createDocumentsRouter({ appStateStore, connectionManager }));
110
112
 
111
113
  // auth: public favicon response; it exposes no application data.
112
114
  app.get("/favicon.ico", (req, res) => {
@@ -143,6 +145,10 @@ app.use(
143
145
  "/vendor/material-symbols",
144
146
  express.static(path.resolve(__dirname, "..", "node_modules", "material-symbols"))
145
147
  );
148
+ app.use(
149
+ "/vendor/marked",
150
+ express.static(path.resolve(__dirname, "..", "node_modules", "marked"))
151
+ );
146
152
  app.use(express.static(FRONTEND_ROOT));
147
153
  app.use("/db_logos", express.static(path.join(APP_STATE_DIRECTORY, "db_logos")));
148
154
  app.use(errorMiddleware);