sqlite-hub 0.9.3 → 0.9.6

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 (48) hide show
  1. package/.github/workflows/ci.yml +36 -0
  2. package/README.md +2 -2
  3. package/bin/sqlite-hub.js +1 -1
  4. package/frontend/index.html +2 -158
  5. package/frontend/js/app.js +41 -22
  6. package/frontend/js/components/connectionCard.js +62 -87
  7. package/frontend/js/components/emptyState.js +20 -23
  8. package/frontend/js/components/modal.js +145 -195
  9. package/frontend/js/components/pageHeader.js +1 -1
  10. package/frontend/js/components/queryEditor.js +16 -30
  11. package/frontend/js/components/queryHistoryDetail.js +93 -164
  12. package/frontend/js/components/queryHistoryPanel.js +81 -99
  13. package/frontend/js/components/queryResults.js +3 -1
  14. package/frontend/js/components/rowEditorPanel.js +28 -31
  15. package/frontend/js/components/structureGraph.js +10 -9
  16. package/frontend/js/components/tableDesignerEditor.js +91 -116
  17. package/frontend/js/store.js +39 -3
  18. package/frontend/js/utils/dom.js +28 -0
  19. package/frontend/js/utils/tableDesigner.js +8 -2
  20. package/frontend/js/views/charts.js +23 -43
  21. package/frontend/js/views/data.js +116 -132
  22. package/frontend/js/views/mediaTagging.js +131 -164
  23. package/frontend/js/views/structure.js +52 -48
  24. package/frontend/styles/tailwind.css +80 -0
  25. package/frontend/styles/tailwind.generated.css +2 -0
  26. package/frontend/styles/tokens.css +3 -3
  27. package/package.json +19 -5
  28. package/server/routes/mediaTagging.js +2 -10
  29. package/server/routes/sql.js +35 -10
  30. package/server/server.js +24 -0
  31. package/server/services/sqlite/dataBrowserService.js +25 -5
  32. package/server/services/sqlite/exportService.js +4 -2
  33. package/server/services/sqlite/introspection.js +2 -2
  34. package/server/services/sqlite/mediaTaggingService.js +166 -53
  35. package/server/services/sqlite/structureService.js +2 -2
  36. package/server/services/sqlite/tableDesigner/sql.js +19 -3
  37. package/server/services/storage/appStateStore.js +227 -87
  38. package/server/utils/appPaths.js +55 -19
  39. package/server/utils/fileValidation.js +94 -8
  40. package/tailwind.config.cjs +73 -0
  41. package/tests/security-paths.test.js +84 -0
  42. package/tests/sql-identifier-safety.test.js +66 -0
  43. package/.npmingnore +0 -4
  44. package/changelog.md +0 -84
  45. package/docs/DESIGN_GUIDELINES.md +0 -36
  46. package/scripts/publish_brew.sh +0 -466
  47. package/scripts/publish_npm.sh +0 -241
  48. package/shortkeys.md +0 -5
@@ -0,0 +1,36 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - main
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ build-test-audit:
14
+ runs-on: ubuntu-latest
15
+
16
+ steps:
17
+ - name: Checkout
18
+ uses: actions/checkout@v4
19
+
20
+ - name: Setup Node
21
+ uses: actions/setup-node@v4
22
+ with:
23
+ node-version: 26
24
+ cache: npm
25
+
26
+ - name: Install dependencies
27
+ run: npm ci
28
+
29
+ - name: Build
30
+ run: npm run build
31
+
32
+ - name: Test
33
+ run: npm test
34
+
35
+ - name: Dependency audit
36
+ run: npm run audit
package/README.md CHANGED
@@ -66,13 +66,13 @@ brew install sqlite-hub
66
66
  ### NPM
67
67
 
68
68
  ```bash
69
- npm install -g sqlit-hub
69
+ npm install -g sqlite-hub
70
70
  ```
71
71
 
72
72
  ## alternative port
73
73
 
74
74
  ```bash
75
- sqlit-hub --port:4174
75
+ sqlite-hub --port:4174
76
76
  ```
77
77
 
78
78
  ## CLI Interface
package/bin/sqlite-hub.js CHANGED
@@ -334,7 +334,7 @@ async function main() {
334
334
  } else if (stmt.kind === 'mutation') {
335
335
  console.log(`Changes: ${stmt.changes}`);
336
336
  if (stmt.lastInsertRowid) {
337
- console.log(`Last insert rowid: ${stmt.lastInsertRowid}`);
337
+ console.log('Last insert rowid:', stmt.lastInsertRowid);
338
338
  }
339
339
  }
340
340
  });
@@ -6,164 +6,8 @@
6
6
  <title>SQLite Hub</title>
7
7
  <link href="/assets/images/logo_raw.png" rel="icon" type="image/png" />
8
8
  <link href="/assets/images/logo_raw.png" rel="shortcut icon" type="image/png" />
9
- <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
10
- <link
11
- href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700;800;900&family=Inter:wght@300;400;500;600;700;800&family=Roboto+Mono:wght@400;500;700&display=swap"
12
- rel="stylesheet"
13
- />
14
- <link
15
- href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
16
- rel="stylesheet"
17
- />
18
- <script id="tailwind-config">
19
- tailwind.config = {
20
- darkMode: "class",
21
- theme: {
22
- extend: {
23
- colors: {
24
- "on-secondary-fixed": "#1c1b1b",
25
- background: "rgb(var(--rgb-background) / <alpha-value>)",
26
- "inverse-primary": "#6a5f00",
27
- "primary-fixed": "#fde403",
28
- tertiary: "#fbfffe",
29
- outline: "#979177",
30
- "on-secondary": "#313030",
31
- "error-container": "#93000a",
32
- "on-primary-fixed-variant": "#504700",
33
- "on-tertiary-container": "#006f72",
34
- "on-tertiary-fixed-variant": "#004f51",
35
- "secondary-fixed-dim": "#c8c6c5",
36
- "tertiary-fixed-dim": "#00dce1",
37
- "on-background": "rgb(var(--rgb-on-surface) / <alpha-value>)",
38
- "surface-tint": "#dec800",
39
- "primary-fixed-dim": "#dec800",
40
- "on-surface": "rgb(var(--rgb-on-surface) / <alpha-value>)",
41
- "surface-container-low": "rgb(var(--rgb-surface-low) / <alpha-value>)",
42
- "on-tertiary": "#003738",
43
- "surface-container": "rgb(var(--rgb-surface-container) / <alpha-value>)",
44
- "on-surface-variant": "rgb(var(--rgb-on-surface-variant) / <alpha-value>)",
45
- "on-primary-container": "#706400",
46
- "on-error": "#690005",
47
- "surface-container-high": "rgb(var(--rgb-surface-high) / <alpha-value>)",
48
- "surface-container-highest": "rgb(var(--rgb-surface-highest) / <alpha-value>)",
49
- error: "rgb(var(--rgb-error) / <alpha-value>)",
50
- primary: "#fffeff",
51
- "outline-variant": "rgb(var(--rgb-outline) / <alpha-value>)",
52
- "on-error-container": "#ffdad6",
53
- "on-secondary-fixed-variant": "#474746",
54
- "surface-container-lowest": "rgb(var(--rgb-surface-lowest) / <alpha-value>)",
55
- "inverse-surface": "#e5e2e1",
56
- surface: "rgb(var(--rgb-surface) / <alpha-value>)",
57
- "tertiary-container": "#04faff",
58
- "on-tertiary-fixed": "#002021",
59
- "tertiary-fixed": "#2dfaff",
60
- "surface-variant": "#353534",
61
- "on-primary": "#373100",
62
- "primary-container": "rgb(var(--rgb-primary) / <alpha-value>)",
63
- "inverse-on-surface": "#313030",
64
- secondary: "#c8c6c5",
65
- "on-secondary-container": "#b7b5b4",
66
- "on-primary-fixed": "#201c00",
67
- "secondary-container": "#474746",
68
- "surface-dim": "rgb(var(--rgb-surface) / <alpha-value>)",
69
- "surface-bright": "rgb(var(--rgb-surface-bright) / <alpha-value>)",
70
- "secondary-fixed": "#e5e2e1",
71
- },
72
- fontFamily: {
73
- headline: ["Space Grotesk"],
74
- body: ["Inter"],
75
- label: ["Inter"],
76
- mono: ["Roboto Mono"],
77
- },
78
- borderRadius: {
79
- DEFAULT: "0px",
80
- lg: "0px",
81
- xl: "0px",
82
- full: "9999px",
83
- },
84
- },
85
- },
86
- };
87
- </script>
88
- <style type="text/tailwindcss">
89
- @layer components {
90
- .signature-button {
91
- @apply inline-flex items-center justify-center gap-2 h-[var(--control-height)] min-h-[var(--control-height)] border border-primary-container bg-primary-container px-[var(--control-padding-inline)] font-headline text-xs font-bold uppercase tracking-[0.16em] text-on-primary transition-all;
92
- box-shadow: 0 0 18px -10px rgb(var(--rgb-primary) / 0.7);
93
- clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%);
94
- }
95
-
96
- .signature-button:hover {
97
- @apply -translate-y-px bg-primary-fixed text-on-primary;
98
- border-color: rgb(var(--rgb-primary) / 0.55);
99
- box-shadow: 0 0 24px -8px rgb(var(--rgb-primary) / 0.92);
100
- filter: brightness(1.03);
101
- }
102
-
103
- .signature-button:disabled,
104
- .signature-button[aria-disabled="true"] {
105
- @apply cursor-not-allowed opacity-45;
106
- box-shadow: none;
107
- filter: none;
108
- transform: none;
109
- }
110
-
111
- .signature-button .material-symbols-outlined {
112
- @apply text-base;
113
- }
114
-
115
- .standard-button {
116
- @apply inline-flex items-center justify-center gap-2 h-[var(--control-height)] min-h-[var(--control-height)] border border-outline-variant/20 bg-surface-container px-[var(--control-padding-inline)] font-mono text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface transition-colors;
117
- }
118
-
119
- .standard-button:hover {
120
- @apply bg-surface-container-highest text-primary-container;
121
- border-color: var(--primary-alpha-24);
122
- }
123
-
124
- .standard-button.is-active {
125
- @apply bg-surface-container-high text-primary-container;
126
- border-color: var(--primary-alpha-30);
127
- }
128
-
129
- .standard-button:disabled,
130
- .standard-button[aria-disabled="true"] {
131
- @apply cursor-not-allowed opacity-40;
132
- }
133
-
134
- .standard-button .material-symbols-outlined {
135
- @apply text-base;
136
- }
137
-
138
- .standard-checkbox {
139
- @apply inline-flex w-full items-center gap-3 min-h-[var(--control-height)] border border-outline-variant/10 bg-surface-container-lowest px-[var(--control-padding-inline)] text-sm text-on-surface;
140
- }
141
-
142
- .standard-checkbox input[type="checkbox"] {
143
- @apply m-0 rounded-none border-outline bg-surface-container-lowest text-primary-container focus:ring-primary-container;
144
- accent-color: var(--color-primary-container);
145
- }
146
-
147
- .standard-checkbox.is-disabled,
148
- .standard-checkbox:has(input:disabled) {
149
- @apply opacity-45;
150
- }
151
-
152
- .delete-button {
153
- @apply inline-flex items-center justify-center gap-2 h-[var(--control-height)] min-h-[var(--control-height)] border border-error/25 bg-error-container/10 px-[var(--control-padding-inline)] font-mono text-[10px] font-bold uppercase tracking-[0.16em] text-error transition-all;
154
- }
155
-
156
- .delete-button:hover {
157
- @apply -translate-y-px border-error bg-error-container/20 text-error;
158
- }
159
-
160
- .delete-button:disabled,
161
- .delete-button[aria-disabled="true"] {
162
- @apply cursor-not-allowed opacity-45;
163
- transform: none;
164
- }
165
- }
166
- </style>
9
+ <link href="/vendor/material-symbols/outlined.css" rel="stylesheet" />
10
+ <link href="styles/tailwind.generated.css" rel="stylesheet" />
167
11
  <link href="styles/tokens.css" rel="stylesheet" />
168
12
  <link href="styles/base.css" rel="stylesheet" />
169
13
  <link href="styles/utilities.css" rel="stylesheet" />
@@ -122,11 +122,15 @@ import { renderOverviewView } from './views/overview.js';
122
122
  import { renderSettingsView } from './views/settings.js';
123
123
  import { renderStructureView } from './views/structure.js';
124
124
  import { renderTableDesignerView } from './views/tableDesigner.js';
125
+ import {
126
+ replaceChildrenFromRenderedMarkup,
127
+ replaceElementFromRenderedMarkup,
128
+ } from './utils/dom.js';
125
129
  import { highlightSql } from './utils/format.js';
126
130
 
127
131
  const appRoot = document.querySelector('#app');
128
132
 
129
- appRoot.innerHTML = renderAppShell();
133
+ replaceChildrenFromRenderedMarkup(appRoot, renderAppShell());
130
134
 
131
135
  const shellRefs = {
132
136
  shell: document.querySelector('.app-shell'),
@@ -186,7 +190,7 @@ function syncQueryEditorHighlight(textarea) {
186
190
  return;
187
191
  }
188
192
 
189
- highlightNode.innerHTML = renderQueryHighlightMarkup(textarea.value);
193
+ replaceChildrenFromRenderedMarkup(highlightNode, renderQueryHighlightMarkup(textarea.value));
190
194
  }
191
195
 
192
196
  function syncQueryEditorScroll(textarea) {
@@ -258,12 +262,20 @@ function syncMediaTaggingCurrentMediaUi(button, detailsVisible) {
258
262
  const nextVisible = Boolean(detailsVisible);
259
263
  const expandedLabel = button.dataset.expandedLabel || 'Shrink Media Viewer';
260
264
  const collapsedLabel = button.dataset.collapsedLabel || 'Show Media Viewer';
261
- const expandedMarkup = `<span class="material-symbols-outlined">visibility_off</span> ${expandedLabel}`;
262
-
263
265
  preview.classList.toggle('media-tagging-preview--meta-hidden', !nextVisible);
264
266
  button.dataset.nextValue = nextVisible ? 'false' : 'true';
265
267
  button.setAttribute('aria-expanded', nextVisible ? 'true' : 'false');
266
- button.innerHTML = nextVisible ? expandedMarkup : collapsedLabel;
268
+
269
+ if (nextVisible) {
270
+ const icon = document.createElement('span');
271
+
272
+ icon.className = 'material-symbols-outlined';
273
+ icon.textContent = 'visibility_off';
274
+ button.replaceChildren(icon, document.createTextNode(` ${expandedLabel}`));
275
+ } else {
276
+ button.textContent = collapsedLabel;
277
+ }
278
+
267
279
  return true;
268
280
  }
269
281
 
@@ -350,7 +362,7 @@ function syncDataRowSelectionUi(selectedRowIndex = null) {
350
362
  const panelMarkup = renderDataRowEditorPanel(getState());
351
363
  const panelOpen = Boolean(panelMarkup);
352
364
 
353
- shellRefs.panel.innerHTML = panelMarkup;
365
+ replaceChildrenFromRenderedMarkup(shellRefs.panel, panelMarkup);
354
366
  shellRefs.shell.classList.toggle('panel-open', panelOpen);
355
367
  lastRenderedPanelMarkup = panelMarkup;
356
368
  lastRenderedPanelOpen = panelOpen;
@@ -367,14 +379,21 @@ function syncQueryHistoryUi(historyId) {
367
379
 
368
380
  const historyItem = state.editor.history.find(entry => Number(entry.id) === numericId) ?? state.editor.historyDetail ?? null;
369
381
  const listItemNode = shellRefs.view.querySelector(
370
- `[data-action="select-query-history-item"][data-history-id="${String(numericId)}"]`,
382
+ [
383
+ '[data-action="select-query-history-item"][data-history-id="',
384
+ String(numericId),
385
+ '"]',
386
+ ].join(''),
371
387
  )?.closest('.query-history-item');
372
388
 
373
389
  if (historyItem && listItemNode instanceof HTMLElement) {
374
- listItemNode.outerHTML = renderQueryHistoryListItem(
375
- historyItem,
376
- state.editor.historyActiveId,
377
- state.editor.historySelectedId,
390
+ replaceElementFromRenderedMarkup(
391
+ listItemNode,
392
+ renderQueryHistoryListItem(
393
+ historyItem,
394
+ state.editor.historyActiveId,
395
+ state.editor.historySelectedId,
396
+ ),
378
397
  );
379
398
  }
380
399
 
@@ -387,7 +406,7 @@ function syncQueryHistoryUi(historyId) {
387
406
  });
388
407
  const panelOpen = Boolean(panelMarkup);
389
408
 
390
- shellRefs.panel.innerHTML = panelMarkup;
409
+ replaceChildrenFromRenderedMarkup(shellRefs.panel, panelMarkup);
391
410
  shellRefs.shell.classList.toggle('panel-open', panelOpen);
392
411
  lastRenderedPanelMarkup = panelMarkup;
393
412
  lastRenderedPanelOpen = panelOpen;
@@ -410,7 +429,7 @@ function syncQueryHistorySelectionUi(selectedHistoryId = null) {
410
429
  }
411
430
 
412
431
  if (selectedHistoryId === null) {
413
- shellRefs.panel.innerHTML = '';
432
+ shellRefs.panel.replaceChildren();
414
433
  shellRefs.shell.classList.remove('panel-open');
415
434
  lastRenderedPanelMarkup = '';
416
435
  lastRenderedPanelOpen = false;
@@ -596,7 +615,7 @@ function patchChartsDetailUi(state) {
596
615
  return false;
597
616
  }
598
617
 
599
- detailNode.innerHTML = renderChartsDetail(state);
618
+ replaceChildrenFromRenderedMarkup(detailNode, renderChartsDetail(state));
600
619
  syncChartsHistorySelectionUi(state);
601
620
  return true;
602
621
  }
@@ -972,7 +991,7 @@ function renderApp(state) {
972
991
  !lockedRouteChanged;
973
992
 
974
993
  if (shellMarkupUnchanged && toastMarkup !== lastRenderedToastMarkup) {
975
- shellRefs.toast.innerHTML = toastMarkup;
994
+ replaceChildrenFromRenderedMarkup(shellRefs.toast, toastMarkup);
976
995
  lastRenderedToastMarkup = toastMarkup;
977
996
  return;
978
997
  }
@@ -1009,7 +1028,7 @@ function renderApp(state) {
1009
1028
  }
1010
1029
 
1011
1030
  if (topNavChanged) {
1012
- shellRefs.topNav.innerHTML = topNavMarkup;
1031
+ replaceChildrenFromRenderedMarkup(shellRefs.topNav, topNavMarkup);
1013
1032
  }
1014
1033
 
1015
1034
  if (sidebarChanged) {
@@ -1019,16 +1038,16 @@ function renderApp(state) {
1019
1038
  syncSidebarActiveRoute(state.route.name);
1020
1039
 
1021
1040
  if (!sidebarSynced) {
1022
- shellRefs.sidebar.innerHTML = sidebarMarkup;
1041
+ replaceChildrenFromRenderedMarkup(shellRefs.sidebar, sidebarMarkup);
1023
1042
  }
1024
1043
  }
1025
1044
 
1026
1045
  if (statusBarChanged) {
1027
- shellRefs.statusBar.innerHTML = statusBarMarkup;
1046
+ replaceChildrenFromRenderedMarkup(shellRefs.statusBar, statusBarMarkup);
1028
1047
  }
1029
1048
 
1030
1049
  if (mainChanged && !mainPatched) {
1031
- shellRefs.view.innerHTML = main;
1050
+ replaceChildrenFromRenderedMarkup(shellRefs.view, main);
1032
1051
  }
1033
1052
 
1034
1053
  if (mainChanged || lockedRouteChanged) {
@@ -1036,15 +1055,15 @@ function renderApp(state) {
1036
1055
  }
1037
1056
 
1038
1057
  if (panelChanged) {
1039
- shellRefs.panel.innerHTML = panel;
1058
+ replaceChildrenFromRenderedMarkup(shellRefs.panel, panel);
1040
1059
  }
1041
1060
 
1042
1061
  if (modalChanged) {
1043
- shellRefs.modal.innerHTML = modalMarkup;
1062
+ replaceChildrenFromRenderedMarkup(shellRefs.modal, modalMarkup);
1044
1063
  }
1045
1064
 
1046
1065
  if (toastMarkup !== lastRenderedToastMarkup) {
1047
- shellRefs.toast.innerHTML = toastMarkup;
1066
+ replaceChildrenFromRenderedMarkup(shellRefs.toast, toastMarkup);
1048
1067
  }
1049
1068
 
1050
1069
  if (panelChanged || panelOpenChanged) {
@@ -13,92 +13,67 @@ export function renderConnectionCard(connection, activeConnectionId) {
13
13
  : Boolean(connection.isActive);
14
14
  const clipPath = "polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%)";
15
15
  const primaryActionLabel = isActive ? "Open Overview" : "Set Active";
16
+ const connectionId = escapeHtml(connection.id);
17
+ const logoMarkup = renderConnectionLogo(connection, {
18
+ containerClass: [
19
+ "clipped-corner flex h-10 w-10 items-center justify-center overflow-hidden transition-colors",
20
+ isActive ? "bg-primary-container" : "bg-surface-container-highest",
21
+ ].join(" "),
22
+ containerAttributes: ['style="--clip-path: ', clipPath, ';"'].join(""),
23
+ imageClassName: "h-full w-full object-cover",
24
+ iconClassName: isActive ? "text-on-primary" : "text-outline-variant",
25
+ });
16
26
 
17
- return `
18
- <article
19
- class="connection-card clipped-corner ${isActive ? "is-active" : ""}"
20
- style="--clip-path: ${clipPath};"
21
- >
22
- <div class="flex-1 p-6">
23
- <div class="mb-6 flex items-start justify-between">
24
- ${renderConnectionLogo(connection, {
25
- containerClass: `clipped-corner flex h-10 w-10 items-center justify-center overflow-hidden transition-colors ${
26
- isActive ? "bg-primary-container" : "bg-surface-container-highest"
27
- }`,
28
- containerAttributes: `style="--clip-path: ${clipPath};"`,
29
- imageClassName: "h-full w-full object-cover",
30
- iconClassName: isActive ? "text-on-primary" : "text-outline-variant",
31
- })}
32
- <div class="flex items-center gap-2">
33
- ${renderStatusBadge(isActive ? "ACTIVE" : "RECENT", isActive ? "primary" : "muted")}
34
- ${connection.readOnly ? renderStatusBadge("READ_ONLY", "alert") : ""}
35
- </div>
36
- </div>
37
- <h3 class="mb-1 font-headline text-xl font-bold uppercase ${
38
- isActive ? "text-[#FCE300]" : "text-on-surface"
39
- }">
40
- ${escapeHtml(connection.label)}
41
- </h3>
42
- <p
43
- class="block overflow-hidden text-ellipsis whitespace-nowrap font-mono text-[10px] text-outline-variant"
44
- title="${escapeHtml(connection.path)}"
45
- >
46
- ${escapeHtml(truncateMiddle(connection.path, 68))}
47
- </p>
48
- <div class="mt-8 grid grid-cols-2 gap-4">
49
- <div>
50
- <div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Allocation</div>
51
- <div class="text-xs font-bold text-on-surface">${escapeHtml(
52
- formatBytes(connection.sizeBytes)
53
- )}</div>
54
- </div>
55
- <div>
56
- <div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Last Modified</div>
57
- <div class="text-xs font-bold text-on-surface">${escapeHtml(
58
- formatDateTime(connection.lastModifiedAt)
59
- )}</div>
60
- </div>
61
- <div>
62
- <div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Last Opened</div>
63
- <div class="text-xs font-bold text-on-surface">${escapeHtml(
64
- formatDateTime(connection.lastOpenedAt)
65
- )}</div>
66
- </div>
67
- <div>
68
- <div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Mode</div>
69
- <div class="text-xs font-bold text-on-surface">${connection.readOnly ? "Read only" : "Read / Write"}</div>
70
- </div>
71
- </div>
72
- </div>
73
- <div class="border-t border-outline-variant/10 bg-surface-container-low px-4 py-3">
74
- <div class="grid grid-cols-[minmax(0,1fr)_5.1rem_5.8rem] gap-2">
75
- <button
76
- class="signature-button"
77
- data-action="select-connection"
78
- data-connection-id="${escapeHtml(connection.id)}"
79
- type="button"
80
- title="${primaryActionLabel}"
81
- >
82
- ${primaryActionLabel}
83
- </button>
84
- <button
85
- class="standard-button"
86
- data-action="edit-connection"
87
- data-connection-id="${escapeHtml(connection.id)}"
88
- type="button"
89
- >
90
- Edit
91
- </button>
92
- <button
93
- class="delete-button"
94
- data-action="remove-connection"
95
- data-connection-id="${escapeHtml(connection.id)}"
96
- type="button"
97
- >
98
- Remove
99
- </button>
100
- </div>
101
- </div>
102
- </article>
103
- `;
27
+ return [
28
+ '<article class="connection-card clipped-corner ',
29
+ isActive ? "is-active" : "",
30
+ '" style="--clip-path: ',
31
+ clipPath,
32
+ ';">',
33
+ '<div class="flex-1 p-6"><div class="mb-6 flex items-start justify-between">',
34
+ logoMarkup,
35
+ '<div class="flex items-center gap-2">',
36
+ renderStatusBadge(isActive ? "ACTIVE" : "RECENT", isActive ? "primary" : "muted"),
37
+ connection.readOnly ? renderStatusBadge("READ_ONLY", "alert") : "",
38
+ "</div></div>",
39
+ '<h3 class="mb-1 font-headline text-xl font-bold uppercase ',
40
+ isActive ? "text-[#FCE300]" : "text-on-surface",
41
+ '">',
42
+ escapeHtml(connection.label),
43
+ "</h3>",
44
+ '<p class="block overflow-hidden text-ellipsis whitespace-nowrap font-mono text-[10px] text-outline-variant" title="',
45
+ escapeHtml(connection.path),
46
+ '">',
47
+ escapeHtml(truncateMiddle(connection.path, 68)),
48
+ "</p>",
49
+ '<div class="mt-8 grid grid-cols-2 gap-4">',
50
+ '<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Allocation</div><div class="text-xs font-bold text-on-surface">',
51
+ escapeHtml(formatBytes(connection.sizeBytes)),
52
+ "</div></div>",
53
+ '<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Last Modified</div><div class="text-xs font-bold text-on-surface">',
54
+ escapeHtml(formatDateTime(connection.lastModifiedAt)),
55
+ "</div></div>",
56
+ '<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Last Opened</div><div class="text-xs font-bold text-on-surface">',
57
+ escapeHtml(formatDateTime(connection.lastOpenedAt)),
58
+ "</div></div>",
59
+ '<div><div class="mb-1 text-[9px] font-mono uppercase text-outline-variant">Mode</div><div class="text-xs font-bold text-on-surface">',
60
+ connection.readOnly ? "Read only" : "Read / Write",
61
+ "</div></div></div></div>",
62
+ '<div class="border-t border-outline-variant/10 bg-surface-container-low px-4 py-3">',
63
+ '<div class="grid grid-cols-[minmax(0,1fr)_5.1rem_5.8rem] gap-2">',
64
+ '<button class="signature-button" data-action="select-connection" data-connection-id="',
65
+ connectionId,
66
+ '" type="button" title="',
67
+ primaryActionLabel,
68
+ '">',
69
+ primaryActionLabel,
70
+ "</button>",
71
+ '<button class="standard-button" data-action="edit-connection" data-connection-id="',
72
+ connectionId,
73
+ '" type="button">Edit</button>',
74
+ '<button class="delete-button" data-action="remove-connection" data-connection-id="',
75
+ connectionId,
76
+ '" type="button">Remove</button>',
77
+ "</div></div></article>",
78
+ ].join("");
104
79
  }
@@ -6,6 +6,25 @@ import {
6
6
  } from "../utils/format.js";
7
7
  import { renderConnectionLogo } from "./connectionLogo.js";
8
8
 
9
+ function renderRecentConnectionButton(connection) {
10
+ return [
11
+ '<button class="control-button flex items-center gap-2 border border-outline-variant/15 bg-surface-container-low px-4 text-left text-on-surface transition-colors hover:border-primary-container/30 hover:bg-surface-container-high" data-action="select-connection" data-connection-id="',
12
+ escapeHtml(connection.id),
13
+ '" type="button">',
14
+ renderConnectionLogo(connection, {
15
+ containerClass:
16
+ "flex h-8 w-8 items-center justify-center overflow-hidden bg-surface-container-highest",
17
+ imageClassName: "h-full w-full object-cover",
18
+ iconClassName: "text-sm text-primary-container",
19
+ }),
20
+ '<span class="min-w-0"><span class="block truncate font-mono text-xs">',
21
+ escapeHtml(connection.label),
22
+ '</span><span class="block truncate text-[10px] text-on-surface-variant/45">',
23
+ escapeHtml(truncateMiddle(connection.path, 34)),
24
+ "</span></span></button>",
25
+ ].join("");
26
+ }
27
+
9
28
  function renderRecentConnections(recentConnections = []) {
10
29
  if (!recentConnections.length) {
11
30
  return `
@@ -19,29 +38,7 @@ function renderRecentConnections(recentConnections = []) {
19
38
  <div class="flex flex-wrap justify-center gap-4">
20
39
  ${recentConnections
21
40
  .slice(0, 4)
22
- .map(
23
- (connection) => `
24
- <button
25
- class="control-button flex items-center gap-2 border border-outline-variant/15 bg-surface-container-low px-4 text-left text-on-surface transition-colors hover:border-primary-container/30 hover:bg-surface-container-high"
26
- data-action="select-connection"
27
- data-connection-id="${escapeHtml(connection.id)}"
28
- type="button"
29
- >
30
- ${renderConnectionLogo(connection, {
31
- containerClass:
32
- "flex h-8 w-8 items-center justify-center overflow-hidden bg-surface-container-highest",
33
- imageClassName: "h-full w-full object-cover",
34
- iconClassName: "text-sm text-primary-container",
35
- })}
36
- <span class="min-w-0">
37
- <span class="block truncate font-mono text-xs">${escapeHtml(connection.label)}</span>
38
- <span class="block truncate text-[10px] text-on-surface-variant/45">${escapeHtml(
39
- truncateMiddle(connection.path, 34)
40
- )}</span>
41
- </span>
42
- </button>
43
- `
44
- )
41
+ .map((connection) => renderRecentConnectionButton(connection))
45
42
  .join("")}
46
43
  </div>
47
44
  `;