sqlite-hub 0.6.0 → 0.8.7

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 (44) hide show
  1. package/README.md +107 -0
  2. package/bin/sqlite-hub.js +285 -4
  3. package/changelog.md +21 -0
  4. package/docs/DESIGN_GUIDELINES.md +36 -0
  5. package/frontend/assets/mockups/sql_editor_croped.png +0 -0
  6. package/frontend/index.html +80 -0
  7. package/frontend/js/api.js +34 -0
  8. package/frontend/js/app.js +188 -10
  9. package/frontend/js/components/connectionCard.js +3 -4
  10. package/frontend/js/components/emptyState.js +4 -4
  11. package/frontend/js/components/modal.js +341 -24
  12. package/frontend/js/components/queryChartRenderer.js +125 -0
  13. package/frontend/js/components/queryEditor.js +33 -6
  14. package/frontend/js/components/queryHistoryDetail.js +16 -7
  15. package/frontend/js/components/queryHistoryPanel.js +18 -7
  16. package/frontend/js/components/rowEditorPanel.js +59 -54
  17. package/frontend/js/components/sidebar.js +32 -32
  18. package/frontend/js/components/structureGraph.js +54 -122
  19. package/frontend/js/components/tableDesignerEditor.js +9 -8
  20. package/frontend/js/components/tableDesignerSidebar.js +52 -48
  21. package/frontend/js/components/tableDesignerSqlPreview.js +60 -28
  22. package/frontend/js/lib/queryChartOptions.js +283 -0
  23. package/frontend/js/lib/queryCharts.js +560 -0
  24. package/frontend/js/router.js +8 -0
  25. package/frontend/js/store.js +641 -0
  26. package/frontend/js/views/charts.js +499 -0
  27. package/frontend/js/views/connections.js +5 -17
  28. package/frontend/js/views/data.js +40 -27
  29. package/frontend/js/views/editor.js +19 -13
  30. package/frontend/js/views/overview.js +149 -10
  31. package/frontend/js/views/settings.js +2 -2
  32. package/frontend/js/views/structure.js +105 -59
  33. package/frontend/js/views/tableDesigner.js +7 -2
  34. package/frontend/styles/base.css +62 -0
  35. package/frontend/styles/components.css +68 -118
  36. package/frontend/styles/structure-graph.css +19 -82
  37. package/frontend/styles/tokens.css +2 -0
  38. package/frontend/styles/views.css +293 -66
  39. package/package.json +2 -1
  40. package/server/routes/charts.js +153 -0
  41. package/server/server.js +6 -0
  42. package/server/services/sqlite/overviewService.js +68 -0
  43. package/server/services/storage/appStateStore.js +400 -1
  44. package/server/services/storage/queryHistoryChartUtils.js +145 -0
@@ -67,6 +67,7 @@ function renderTableList(state) {
67
67
 
68
68
  function renderWorkspaceHeader(state) {
69
69
  const table = state.dataBrowser.table;
70
+ const tablesVisible = state.dataBrowser.tablesVisible !== false;
70
71
 
71
72
  return `
72
73
  <header class="border-b border-outline-variant/10 bg-surface-container px-6 py-5">
@@ -91,19 +92,24 @@ function renderWorkspaceHeader(state) {
91
92
  <div class="flex items-center gap-3">
92
93
  ${
93
94
  table
94
- ? `
95
+ ? `<button
96
+ class="standard-button"
97
+ data-action="toggle-data-tables"
98
+ type="button"
99
+ >
100
+ ${tablesVisible ? '<span class="material-symbols-outlined text-sm">visibility_off</span> Hide Tables' : 'Show Tables'}
101
+ </button>
95
102
  <button
96
- class="border border-outline-variant/20 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface hover:bg-surface-container-highest"
103
+ class="standard-button"
97
104
  data-action="export-data-csv"
98
105
  type="button"
99
106
  >
100
107
  ${state.dataBrowser.exportLoading ? 'Exporting...' : 'Export CSV'}
101
- </button>
102
- `
108
+ </button>`
103
109
  : ''
104
110
  }
105
111
  <button
106
- class="border border-outline-variant/20 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface hover:bg-surface-container-highest"
112
+ class="standard-button"
107
113
  data-action="refresh-view"
108
114
  type="button"
109
115
  >
@@ -113,11 +119,12 @@ function renderWorkspaceHeader(state) {
113
119
  table
114
120
  ? `
115
121
  <button
116
- class="border border-outline-variant/20 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface hover:bg-surface-container-highest"
122
+ class="standard-button"
117
123
  data-action="navigate"
118
124
  data-to="/structure"
119
125
  type="button"
120
126
  >
127
+ <span class="material-symbols-outlined">account_tree</span>
121
128
  Open Structure
122
129
  </button>
123
130
  `
@@ -233,10 +240,10 @@ function renderTableSearchBar(table, state, activeColumn, filteredRowCount) {
233
240
 
234
241
  return `
235
242
  <div class="flex flex-wrap items-center gap-3 border-b border-outline-variant/10 bg-surface-container-low px-6 py-4">
236
- <label class="flex min-w-[18rem] flex-1 items-center gap-3 border border-outline-variant/20 bg-surface-container-lowest px-4 py-3">
243
+ <label class="control-shell flex min-w-[18rem] flex-1 items-center gap-3 border border-outline-variant/20 bg-surface-container-lowest px-3">
237
244
  <span class="material-symbols-outlined text-base text-on-surface-variant/55">search</span>
238
245
  <input
239
- class="min-w-0 flex-1 bg-transparent text-sm text-on-surface outline-none placeholder:text-on-surface-variant/40"
246
+ class="control-input control-input--ghost min-w-0 flex-1 text-sm text-on-surface outline-none placeholder:text-on-surface-variant/40"
240
247
  data-bind="data-search-query"
241
248
  placeholder="Filter current page..."
242
249
  type="search"
@@ -244,7 +251,7 @@ function renderTableSearchBar(table, state, activeColumn, filteredRowCount) {
244
251
  />
245
252
  </label>
246
253
  <select
247
- class="min-w-[14rem] border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 font-mono text-xs tracking-[0.04em] text-on-surface outline-none"
254
+ class="control-select min-w-[14rem] border border-outline-variant/20 bg-surface-container-lowest font-mono text-xs tracking-[0.04em] text-on-surface outline-none"
248
255
  data-bind="data-search-column"
249
256
  >
250
257
  ${columns
@@ -384,10 +391,8 @@ function renderTableSurface(state) {
384
391
  .map(
385
392
  pageSize => `
386
393
  <button
387
- class="border px-3 py-2 text-[10px] font-bold uppercase tracking-[0.16em] transition-colors ${
388
- pageSize === (table.limit ?? state.dataBrowser.pageSize)
389
- ? 'border-primary-container/30 bg-surface-container-high text-primary-container'
390
- : 'border-outline-variant/20 text-on-surface hover:bg-surface-container-highest'
394
+ class="standard-button ${
395
+ pageSize === (table.limit ?? state.dataBrowser.pageSize) ? 'is-active' : ''
391
396
  }"
392
397
  data-action="set-data-page-size"
393
398
  data-page-size="${pageSize}"
@@ -402,7 +407,7 @@ function renderTableSurface(state) {
402
407
  </div>
403
408
  <div class="flex items-center gap-2">
404
409
  <button
405
- class="border border-outline-variant/20 px-3 py-2 text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface transition-colors hover:bg-surface-container-highest disabled:cursor-default disabled:opacity-30"
410
+ class="standard-button"
406
411
  data-action="set-data-page"
407
412
  data-page="${page - 1}"
408
413
  type="button"
@@ -414,7 +419,7 @@ function renderTableSurface(state) {
414
419
  page ${escapeHtml(formatNumber(page))} / ${escapeHtml(formatNumber(pageCount))}
415
420
  </div>
416
421
  <button
417
- class="border border-outline-variant/20 px-3 py-2 text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface transition-colors hover:bg-surface-container-highest disabled:cursor-default disabled:opacity-30"
422
+ class="standard-button"
418
423
  data-action="set-data-page"
419
424
  data-page="${page + 1}"
420
425
  type="button"
@@ -505,21 +510,29 @@ function renderDataRowEditorPanel(state) {
505
510
  }
506
511
 
507
512
  export function renderDataView(state) {
513
+ const tablesVisible = state.dataBrowser.tablesVisible !== false;
514
+
508
515
  return {
509
516
  main: `
510
517
  <section class="view-surface min-h-full bg-surface-container flex h-full min-h-0 flex-col overflow-hidden">
511
- <div class="grid h-full min-h-0 grid-cols-1 md:grid-cols-[18rem_minmax(0,1fr)]">
512
- <aside class="flex min-h-0 flex-col border-r border-outline-variant/10 bg-surface-low">
513
- <div class="border-b border-outline-variant/10 px-6 py-5">
514
- <div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
515
- Tables
516
- </div>
517
- <div class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
518
- total ${escapeHtml(formatNumber(state.dataBrowser.tables.length))}
519
- </div>
520
- </div>
521
- ${renderTableList(state)}
522
- </aside>
518
+ <div class="grid h-full min-h-0 grid-cols-1 ${tablesVisible ? 'md:grid-cols-[18rem_minmax(0,1fr)]' : ''}">
519
+ ${
520
+ tablesVisible
521
+ ? `
522
+ <aside class="flex min-h-0 flex-col border-r border-outline-variant/10 bg-surface-low">
523
+ <div class="border-b border-outline-variant/10 px-6 py-5">
524
+ <div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
525
+ Tables
526
+ </div>
527
+ <div class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
528
+ total ${escapeHtml(formatNumber(state.dataBrowser.tables.length))}
529
+ </div>
530
+ </div>
531
+ ${renderTableList(state)}
532
+ </aside>
533
+ `
534
+ : ''
535
+ }
523
536
  <section class="flex min-h-0 flex-col overflow-hidden">
524
537
  ${renderWorkspaceHeader(state)}
525
538
  ${renderWorkspaceError(state)}
@@ -236,7 +236,7 @@ function renderResultsSurface(state, isResultsRoute) {
236
236
 
237
237
  export function renderEditorView(state, { isResultsRoute = false } = {}) {
238
238
  const connection = getCurrentConnection(state);
239
- const editorSectionClass = "min-h-[27.5%]";
239
+ const editorSectionClass = state.editor.editorPanelVisible ? "min-h-[27.5%]" : "flex-none";
240
240
  const resultsSectionClass = "flex-1";
241
241
 
242
242
  return {
@@ -252,6 +252,8 @@ export function renderEditorView(state, { isResultsRoute = false } = {}) {
252
252
  exporting: state.editor.exportLoading,
253
253
  historyLoading: state.editor.historyLoading,
254
254
  historyTotal: state.editor.historyTotal,
255
+ editorVisible: state.editor.editorPanelVisible,
256
+ historyVisible: state.editor.historyPanelVisible,
255
257
  title: connection?.label ?? "SQLite Query Workspace",
256
258
  })}
257
259
  </section>
@@ -259,18 +261,22 @@ export function renderEditorView(state, { isResultsRoute = false } = {}) {
259
261
  ${renderResultsSurface(state, isResultsRoute)}
260
262
  </section>
261
263
  </div>
262
- ${renderQueryHistoryPanel({
263
- items: state.editor.history,
264
- loading: state.editor.historyLoading,
265
- loadingMore: state.editor.historyLoadingMore,
266
- error: state.editor.historyError,
267
- activeTab: state.editor.historyTab,
268
- search: state.editor.historySearchInput,
269
- total: state.editor.historyTotal,
270
- hasMore: state.editor.historyHasMore,
271
- activeHistoryId: state.editor.historyActiveId,
272
- selectedHistoryId: state.editor.historySelectedId,
273
- })}
264
+ ${
265
+ state.editor.historyPanelVisible
266
+ ? renderQueryHistoryPanel({
267
+ items: state.editor.history,
268
+ loading: state.editor.historyLoading,
269
+ loadingMore: state.editor.historyLoadingMore,
270
+ error: state.editor.historyError,
271
+ activeTab: state.editor.historyTab,
272
+ search: state.editor.historySearchInput,
273
+ total: state.editor.historyTotal,
274
+ hasMore: state.editor.historyHasMore,
275
+ activeHistoryId: state.editor.historyActiveId,
276
+ selectedHistoryId: state.editor.historySelectedId,
277
+ })
278
+ : ""
279
+ }
274
280
  </section>
275
281
  `,
276
282
  panel:
@@ -58,7 +58,7 @@ function renderTopTables(overview) {
58
58
  );
59
59
 
60
60
  return `
61
- <section class="shell-section xl:col-span-2">
61
+ <section class="shell-section">
62
62
  <div class="flex items-center justify-between bg-surface-container-highest px-4 py-2">
63
63
  <span class="text-[10px] font-bold uppercase tracking-[0.25em]">Top Tables</span>
64
64
  <span class="material-symbols-outlined text-xs text-on-surface-variant">table_rows</span>
@@ -131,7 +131,7 @@ function renderOperationalSurface(overview) {
131
131
  ? `
132
132
  <div class="border-t border-outline-variant/10 px-4 py-3">
133
133
  <button
134
- class="toolbar-button border border-outline-variant/20 bg-surface-container px-3 py-1.5 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
134
+ class="standard-button"
135
135
  data-action="open-overview-in-finder"
136
136
  type="button"
137
137
  >
@@ -145,17 +145,154 @@ function renderOperationalSurface(overview) {
145
145
  `;
146
146
  }
147
147
 
148
+ function pluralize(count, singular, plural = `${singular}s`) {
149
+ return Number(count) === 1 ? singular : plural;
150
+ }
151
+
152
+ function getSchemaMapNarrative(preview) {
153
+ const relationshipCount = Number(preview.relationshipCount ?? 0);
154
+ const fkClusters = Number(preview.fkClusters ?? 0);
155
+ const isolatedTables = Number(preview.isolatedTables ?? 0);
156
+ const connectedTables = Math.max(0, Number(preview.tableCount ?? 0) - isolatedTables);
157
+
158
+ if (relationshipCount === 0 || fkClusters === 0) {
159
+ return {
160
+ title: "Mostly standalone schema",
161
+ body:
162
+ "There are no meaningful foreign-key groups yet. The Structure view will read more like a list of isolated tables than a connected graph.",
163
+ };
164
+ }
165
+
166
+ if (isolatedTables === 0) {
167
+ return {
168
+ title: "Connected schema",
169
+ body: `Foreign keys tie the whole schema together in ${formatNumber(
170
+ fkClusters
171
+ )} ${pluralize(fkClusters, "cluster")}. Structure is useful here because dependencies and joins are visible at a glance.`,
172
+ };
173
+ }
174
+
175
+ return {
176
+ title: "One connected core, several islands",
177
+ body: `${formatNumber(connectedTables)} ${pluralize(
178
+ connectedTables,
179
+ "table"
180
+ )} are connected through foreign keys, while ${formatNumber(
181
+ isolatedTables
182
+ )} ${pluralize(
183
+ isolatedTables,
184
+ "table"
185
+ )} stand alone. Structure helps most when you want to inspect the connected core and ignore the isolated tables.`,
186
+ };
187
+ }
188
+
189
+ function renderSchemaMapPreview(overview) {
190
+ const preview = overview.schemaMap ?? {
191
+ tableCount: overview.counts?.tables ?? 0,
192
+ indexCount: overview.counts?.indexes ?? 0,
193
+ relationshipCount: 0,
194
+ fkClusters: 0,
195
+ isolatedTables: 0,
196
+ };
197
+ const narrative = getSchemaMapNarrative(preview);
198
+ const stats = [
199
+ { label: "FK Links", value: formatNumber(preview.relationshipCount) },
200
+ { label: "FK Clusters", value: formatNumber(preview.fkClusters) },
201
+ { label: "Isolated Tables", value: formatNumber(preview.isolatedTables) },
202
+ ];
203
+
204
+ return `
205
+ <section class="shell-section overflow-hidden bg-[radial-gradient(circle_at_top_left,rgba(45,250,255,0.12),transparent_34%),radial-gradient(circle_at_bottom_right,rgba(252,227,0,0.12),transparent_42%),linear-gradient(135deg,#1b1b19,#101111)]">
206
+ <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest/60 px-4 py-2">
207
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em]">Schema Map</span>
208
+ <span class="material-symbols-outlined text-xs text-on-surface-variant">account_tree</span>
209
+ </div>
210
+ <div class="space-y-5 p-4">
211
+ <div class="flex flex-col gap-5 xl:flex-row xl:items-end xl:justify-between">
212
+ <div>
213
+ <div class="font-headline text-xl font-black uppercase tracking-tight text-primary-container">
214
+ ${escapeHtml(narrative.title)}
215
+ </div>
216
+ <p class="mt-2 max-w-3xl text-sm leading-6 text-on-surface-variant/70">
217
+ ${escapeHtml(narrative.body)}
218
+ </p>
219
+ </div>
220
+ <div class="flex justify-start xl:justify-end">
221
+ <button
222
+ class="standard-button"
223
+ data-action="navigate"
224
+ data-to="/structure"
225
+ type="button"
226
+ >
227
+ <span class="material-symbols-outlined text-sm">account_tree</span>
228
+ Open Structure
229
+ </button>
230
+ </div>
231
+ </div>
232
+ <div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
233
+ ${stats
234
+ .map(
235
+ (stat) => `
236
+ <div class="border border-outline-variant/10 bg-surface-container-lowest/70 px-3 py-3">
237
+ <div class="text-[9px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
238
+ ${escapeHtml(stat.label)}
239
+ </div>
240
+ <div class="mt-2 font-headline text-2xl font-black uppercase tracking-tight text-on-surface">
241
+ ${escapeHtml(stat.value)}
242
+ </div>
243
+ </div>
244
+ `
245
+ )
246
+ .join("")}
247
+ </div>
248
+ <div class="border border-outline-variant/10 bg-surface-container-lowest/70 px-4 py-4">
249
+ <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/70">
250
+ Why It Helps
251
+ </div>
252
+ <div class="mt-3 grid gap-3 md:grid-cols-3">
253
+ <div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
254
+ <div class="text-[9px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
255
+ FK Links
256
+ </div>
257
+ <div class="mt-2 text-sm leading-6 text-on-surface-variant/70">
258
+ Shows how many actual relationships exist between tables.
259
+ </div>
260
+ </div>
261
+ <div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
262
+ <div class="text-[9px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
263
+ FK Clusters
264
+ </div>
265
+ <div class="mt-2 text-sm leading-6 text-on-surface-variant/70">
266
+ Tells you whether the schema is one connected area or split into separate groups.
267
+ </div>
268
+ </div>
269
+ <div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
270
+ <div class="text-[9px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
271
+ Isolated Tables
272
+ </div>
273
+ <div class="mt-2 text-sm leading-6 text-on-surface-variant/70">
274
+ Highlights how many tables have no FK links and can usually be ignored during relationship analysis.
275
+ </div>
276
+ </div>
277
+ </div>
278
+ </div>
279
+ </div>
280
+ </section>
281
+ `;
282
+ }
283
+
148
284
  function renderIntegrityCard(overview, readOnly) {
149
285
  return `
150
- <section class="relative overflow-hidden border border-outline-variant/10 bg-[radial-gradient(circle_at_top_right,rgba(252,227,0,0.14),transparent_30%),linear-gradient(135deg,#1c1b1b,#0e0e0e)]">
151
- <div class="pointer-events-none absolute inset-0 bg-[linear-gradient(to_top,rgba(32,31,31,0.95),rgba(32,31,31,0.2))]"></div>
286
+ <section class="shell-section relative overflow-hidden bg-[radial-gradient(circle_at_top_right,rgba(252,227,0,0.14),transparent_30%),linear-gradient(135deg,#1c1b1b,#0e0e0e)]">
287
+ <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest/60 px-4 py-2">
288
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em]">Integrity Status</span>
289
+ <span class="material-symbols-outlined text-xs text-on-surface-variant">security</span>
290
+ </div>
291
+ <div class="pointer-events-none absolute inset-x-0 bottom-0 top-10 bg-[linear-gradient(to_top,rgba(32,31,31,0.95),rgba(32,31,31,0.2))]"></div>
152
292
  <div class="relative flex min-h-[220px] flex-col justify-end p-6">
153
293
  <div class="mb-6 flex h-16 w-16 items-center justify-center border border-primary-container/20 bg-primary-container/10">
154
294
  <span class="material-symbols-outlined text-4xl text-primary-container">security</span>
155
295
  </div>
156
- <div class="text-[10px] font-black uppercase tracking-[0.25em] text-primary-container">
157
- INTEGRITY_STATUS
158
- </div>
159
296
  <div class="mt-2 flex flex-wrap items-center gap-2">
160
297
  ${renderStatusBadge(overview.sqlite?.integrityCheck ?? "unknown", "success")}
161
298
  ${renderStatusBadge(
@@ -190,10 +327,9 @@ export function renderOverviewView(state) {
190
327
  : "System Registry: NO_ACTIVE_DATABASE",
191
328
  actions: `
192
329
  <button
193
- class="toolbar-button toolbar-button--primary bg-primary-container px-4 py-2 font-headline text-xs font-bold uppercase tracking-widest text-on-primary clipped-corner"
330
+ class="signature-button"
194
331
  data-action="navigate"
195
332
  data-to="/editor"
196
- style="--clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%);"
197
333
  type="button"
198
334
  >
199
335
  <span class="material-symbols-outlined text-sm">terminal</span>
@@ -218,7 +354,10 @@ export function renderOverviewView(state) {
218
354
  ? `
219
355
  ${renderOverviewMetrics(overview)}
220
356
  <div class="grid grid-cols-1 gap-6 xl:grid-cols-3">
221
- ${renderTopTables(overview)}
357
+ <div class="space-y-6 xl:col-span-2">
358
+ ${renderTopTables(overview)}
359
+ ${renderSchemaMapPreview(overview)}
360
+ </div>
222
361
  <div class="space-y-6">
223
362
  ${renderOperationalSurface(overview)}
224
363
  ${renderIntegrityCard(overview, readOnly)}
@@ -56,7 +56,7 @@ function renderSettingsContent(state) {
56
56
  Copyright Oliver Jessner
57
57
  </div>
58
58
  <a
59
- class="inline-flex items-center gap-2 border border-outline-variant/20 bg-surface-container-low px-4 py-3 text-xs font-bold uppercase tracking-[0.2em] text-primary-container transition-colors hover:bg-surface-container-highest"
59
+ class="standard-button"
60
60
  href="https://oliverjessner.at"
61
61
  rel="noreferrer"
62
62
  target="_blank"
@@ -77,7 +77,7 @@ function renderSettingsContent(state) {
77
77
  Open Source Code
78
78
  </div>
79
79
  <a
80
- class="inline-flex items-center gap-2 border border-outline-variant/20 bg-surface-container-low px-4 py-3 text-xs font-bold uppercase tracking-[0.2em] text-primary-container transition-colors hover:bg-surface-container-highest"
80
+ class="standard-button"
81
81
  href="https://github.com/oliverjessner/sqlite-hub"
82
82
  rel="noreferrer"
83
83
  target="_blank"