@reddb-io/ui 0.1.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 (169) hide show
  1. package/LICENSE +17 -0
  2. package/bin/red-ui.js +259 -0
  3. package/build/CNAME +1 -0
  4. package/build/_app/env.js +1 -0
  5. package/build/_app/immutable/assets/0.C63jY5jX.css +1 -0
  6. package/build/_app/immutable/assets/10.Dblyk0Kr.css +1 -0
  7. package/build/_app/immutable/assets/EmptyState.RXcH7ryE.css +1 -0
  8. package/build/_app/immutable/assets/Workspace.BCzG1DsH.css +1 -0
  9. package/build/_app/immutable/assets/inter-cyrillic-ext-wght-normal.BOeWTOD4.woff2 +0 -0
  10. package/build/_app/immutable/assets/inter-cyrillic-wght-normal.DqGufNeO.woff2 +0 -0
  11. package/build/_app/immutable/assets/inter-greek-ext-wght-normal.DlzME5K_.woff2 +0 -0
  12. package/build/_app/immutable/assets/inter-greek-wght-normal.CkhJZR-_.woff2 +0 -0
  13. package/build/_app/immutable/assets/inter-latin-ext-wght-normal.DO1Apj_S.woff2 +0 -0
  14. package/build/_app/immutable/assets/inter-latin-wght-normal.Dx4kXJAl.woff2 +0 -0
  15. package/build/_app/immutable/assets/inter-vietnamese-wght-normal.CBcvBZtf.woff2 +0 -0
  16. package/build/_app/immutable/assets/jetbrains-mono-cyrillic-wght-normal.D73BlboJ.woff2 +0 -0
  17. package/build/_app/immutable/assets/jetbrains-mono-greek-wght-normal.Bw9x6K1M.woff2 +0 -0
  18. package/build/_app/immutable/assets/jetbrains-mono-latin-ext-wght-normal.DBQx-q_a.woff2 +0 -0
  19. package/build/_app/immutable/assets/jetbrains-mono-latin-wght-normal.B9CIFXIH.woff2 +0 -0
  20. package/build/_app/immutable/assets/jetbrains-mono-vietnamese-wght-normal.Bt-aOZkq.woff2 +0 -0
  21. package/build/_app/immutable/chunks/3j4cZ58b.js +1 -0
  22. package/build/_app/immutable/chunks/69_IOA4Y.js +1 -0
  23. package/build/_app/immutable/chunks/ALqaDrNQ.js +2 -0
  24. package/build/_app/immutable/chunks/BGcIe2ep.js +2 -0
  25. package/build/_app/immutable/chunks/BMHKbvs8.js +1 -0
  26. package/build/_app/immutable/chunks/BQHbwf54.js +916 -0
  27. package/build/_app/immutable/chunks/Bgso8cDH.js +311 -0
  28. package/build/_app/immutable/chunks/Bmgx1mfv.js +310 -0
  29. package/build/_app/immutable/chunks/BrvvOVaJ.js +1 -0
  30. package/build/_app/immutable/chunks/Bzak7iHL.js +1 -0
  31. package/build/_app/immutable/chunks/D9CTkMjK.js +1 -0
  32. package/build/_app/immutable/chunks/DIeogL5L.js +1 -0
  33. package/build/_app/immutable/chunks/DXY_Rxa7.js +113 -0
  34. package/build/_app/immutable/chunks/DhEqZVGG.js +1 -0
  35. package/build/_app/immutable/chunks/Ov8N-vxt.js +1 -0
  36. package/build/_app/immutable/chunks/oiO4RLqp.js +1 -0
  37. package/build/_app/immutable/entry/app.D_IVtpdv.js +2 -0
  38. package/build/_app/immutable/entry/start.3oC0UQ_Q.js +1 -0
  39. package/build/_app/immutable/nodes/0.CHPiFxDo.js +1 -0
  40. package/build/_app/immutable/nodes/1.BC_4YZ0h.js +1 -0
  41. package/build/_app/immutable/nodes/10.B7DMKSNc.js +60 -0
  42. package/build/_app/immutable/nodes/2.CAG5_cGP.js +1 -0
  43. package/build/_app/immutable/nodes/3.CAG5_cGP.js +1 -0
  44. package/build/_app/immutable/nodes/4.CAG5_cGP.js +1 -0
  45. package/build/_app/immutable/nodes/5.CAG5_cGP.js +1 -0
  46. package/build/_app/immutable/nodes/6.CAG5_cGP.js +1 -0
  47. package/build/_app/immutable/nodes/7.BZX9Iird.js +134 -0
  48. package/build/_app/immutable/nodes/8.CAG5_cGP.js +1 -0
  49. package/build/_app/immutable/nodes/9.CAG5_cGP.js +1 -0
  50. package/build/_app/version.json +1 -0
  51. package/build/favicon.png +0 -0
  52. package/build/index.html +59 -0
  53. package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js +26562 -0
  54. package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js.map +1 -0
  55. package/dist/embed/bundle-mjs-UuVRz3O4.js +3047 -0
  56. package/dist/embed/bundle-mjs-UuVRz3O4.js.map +1 -0
  57. package/dist/embed/core-QuRYvMt8.js +118 -0
  58. package/dist/embed/core-QuRYvMt8.js.map +1 -0
  59. package/dist/embed/index-B6ZQhCsu.js +577 -0
  60. package/dist/embed/index-B6ZQhCsu.js.map +1 -0
  61. package/dist/embed/index-client-ItCEU64C.js +3087 -0
  62. package/dist/embed/index-client-ItCEU64C.js.map +1 -0
  63. package/dist/embed/index-fad77a13.esm-BqPj0saH.js +1318 -0
  64. package/dist/embed/index-fad77a13.esm-BqPj0saH.js.map +1 -0
  65. package/dist/embed/index.js +11 -0
  66. package/dist/embed/index.js.map +1 -0
  67. package/dist/embed/sigma-node-border.esm-CGtlgqmo.js +432 -0
  68. package/dist/embed/sigma-node-border.esm-CGtlgqmo.js.map +1 -0
  69. package/dist/embed/sigma.esm-y0qgY-OC.js +2355 -0
  70. package/dist/embed/sigma.esm-y0qgY-OC.js.map +1 -0
  71. package/dist/embed/ui.css +1 -0
  72. package/package.json +101 -0
  73. package/src/app.css +238 -0
  74. package/src/lib/ActivityIndicator.svelte +100 -0
  75. package/src/lib/ClusterNode.svelte +119 -0
  76. package/src/lib/ClusterView.svelte +541 -0
  77. package/src/lib/CollectionHistory.svelte +266 -0
  78. package/src/lib/CollectionsView.svelte +14 -0
  79. package/src/lib/CommandPalette.svelte +237 -0
  80. package/src/lib/ConnectDropdown.svelte +222 -0
  81. package/src/lib/EmptyState.svelte +27 -0
  82. package/src/lib/LiveChanges.svelte +195 -0
  83. package/src/lib/MasterPasswordDialog.svelte +168 -0
  84. package/src/lib/PageHeader.svelte +32 -0
  85. package/src/lib/PendingChangesPanel.svelte +153 -0
  86. package/src/lib/QueryEditor.svelte +122 -0
  87. package/src/lib/ResultsPane.svelte +747 -0
  88. package/src/lib/SchemaTree.svelte +193 -0
  89. package/src/lib/SecurityView.svelte +255 -0
  90. package/src/lib/ShortcutOverlay.svelte +104 -0
  91. package/src/lib/Splash.svelte +46 -0
  92. package/src/lib/StatusBar.svelte +163 -0
  93. package/src/lib/SubNav.svelte +62 -0
  94. package/src/lib/Topbar.svelte +119 -0
  95. package/src/lib/Workspace.svelte +247 -0
  96. package/src/lib/activity.svelte.ts +91 -0
  97. package/src/lib/build-info.ts +41 -0
  98. package/src/lib/capability.ts +221 -0
  99. package/src/lib/cn.ts +6 -0
  100. package/src/lib/collection-catalog.ts +116 -0
  101. package/src/lib/collection-pages.ts +33 -0
  102. package/src/lib/connection-history.ts +301 -0
  103. package/src/lib/connections.svelte.ts +493 -0
  104. package/src/lib/embed/RedUiEmbedRoot.svelte +31 -0
  105. package/src/lib/embed/index.ts +234 -0
  106. package/src/lib/graph-loader/contract.ts +336 -0
  107. package/src/lib/graph-loader/drilldown.ts +334 -0
  108. package/src/lib/graph-loader/index.ts +13 -0
  109. package/src/lib/index.ts +114 -0
  110. package/src/lib/pending-changes.svelte.ts +149 -0
  111. package/src/lib/query-tabs.svelte.ts +130 -0
  112. package/src/lib/reddb/boot-params.ts +254 -0
  113. package/src/lib/reddb/cdc-stream-client.ts +234 -0
  114. package/src/lib/reddb/client.ts +698 -0
  115. package/src/lib/reddb/connection-bootstrap.ts +260 -0
  116. package/src/lib/reddb/connection-provider.contract.ts +72 -0
  117. package/src/lib/reddb/connection-provider.ts +72 -0
  118. package/src/lib/reddb/index.ts +14 -0
  119. package/src/lib/reddb/injected-client-provider.ts +76 -0
  120. package/src/lib/reddb/local-url-provider.ts +245 -0
  121. package/src/lib/reddb/secure-store.ts +41 -0
  122. package/src/lib/reddb/server-capabilities.ts +71 -0
  123. package/src/lib/reddb/tauri-encrypted-store.ts +64 -0
  124. package/src/lib/reddb/transports.ts +89 -0
  125. package/src/lib/reddb/types.ts +32 -0
  126. package/src/lib/reddb/uri.ts +54 -0
  127. package/src/lib/reddb/web-encrypted-store.ts +201 -0
  128. package/src/lib/renderers/DiffRenderer.svelte +184 -0
  129. package/src/lib/renderers/DocumentRenderer.svelte +134 -0
  130. package/src/lib/renderers/GraphRenderer.svelte +1328 -0
  131. package/src/lib/renderers/HypertableRenderer.svelte +253 -0
  132. package/src/lib/renderers/JsonRenderer.svelte +30 -0
  133. package/src/lib/renderers/KvRenderer.svelte +150 -0
  134. package/src/lib/renderers/QueueRenderer.svelte +286 -0
  135. package/src/lib/renderers/StatsRenderer.svelte +123 -0
  136. package/src/lib/renderers/TableRenderer.svelte +440 -0
  137. package/src/lib/renderers/VectorRenderer.svelte +388 -0
  138. package/src/lib/renderers/diff-render.ts +122 -0
  139. package/src/lib/renderers/diff.ts +9 -0
  140. package/src/lib/renderers/document-render.ts +64 -0
  141. package/src/lib/renderers/document.ts +9 -0
  142. package/src/lib/renderers/graph-render.ts +631 -0
  143. package/src/lib/renderers/graph-sigma.ts +395 -0
  144. package/src/lib/renderers/graph.ts +9 -0
  145. package/src/lib/renderers/hypertable-render.ts +320 -0
  146. package/src/lib/renderers/hypertable.ts +9 -0
  147. package/src/lib/renderers/index.ts +42 -0
  148. package/src/lib/renderers/json-render.ts +89 -0
  149. package/src/lib/renderers/json.ts +10 -0
  150. package/src/lib/renderers/kv-render.ts +193 -0
  151. package/src/lib/renderers/kv.ts +9 -0
  152. package/src/lib/renderers/queue-render.ts +197 -0
  153. package/src/lib/renderers/queue.ts +9 -0
  154. package/src/lib/renderers/registry.ts +68 -0
  155. package/src/lib/renderers/stats-render.ts +73 -0
  156. package/src/lib/renderers/stats.ts +9 -0
  157. package/src/lib/renderers/table-render.ts +156 -0
  158. package/src/lib/renderers/table.ts +11 -0
  159. package/src/lib/renderers/types.ts +33 -0
  160. package/src/lib/renderers/vector-render.ts +154 -0
  161. package/src/lib/renderers/vector.ts +9 -0
  162. package/src/lib/router.svelte.ts +163 -0
  163. package/src/lib/secureStore.svelte.ts +148 -0
  164. package/src/lib/shadow-dom-spike/ShadowDomSpikeCore.svelte +198 -0
  165. package/src/lib/shadow-dom-spike/mount-shadow-dom-spike.ts +94 -0
  166. package/src/lib/shortcuts.ts +22 -0
  167. package/src/lib/surface.ts +80 -0
  168. package/src/lib/tabs.svelte.ts +115 -0
  169. package/src/lib/theme.svelte.ts +63 -0
@@ -0,0 +1,440 @@
1
+ <script lang="ts">
2
+ import type { QueryResult } from '#reddb'
3
+ import { connection } from '$lib/connections.svelte'
4
+ import { activity } from '$lib/activity.svelte'
5
+ import { pendingChanges } from '$lib/pending-changes.svelte'
6
+ import { Columns3, Filter, Loader2, Play, RotateCw, X } from 'lucide-svelte'
7
+ import {
8
+ formatCell,
9
+ identityKind,
10
+ inferPkColumn,
11
+ isNull,
12
+ isSystemField,
13
+ visibleColumns,
14
+ visibleRows,
15
+ } from './table-render'
16
+
17
+ interface Props {
18
+ result: QueryResult
19
+ collection?: string
20
+ showSystem?: boolean
21
+ }
22
+
23
+ let { result, collection, showSystem = false }: Props = $props()
24
+
25
+ let overrideResult = $state<QueryResult | null>(null)
26
+ const activeResult = $derived(overrideResult ?? result)
27
+ const allRows = $derived(visibleRows(activeResult))
28
+ const columns = $derived(visibleColumns(activeResult, { showSystem }))
29
+ const pkCol = $derived(inferPkColumn(columns))
30
+ const queryableColumns = $derived(columns.filter((c) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(c)))
31
+
32
+ let selectOpen = $state(false)
33
+ let selectedColumns = $state<Record<string, boolean>>({})
34
+ let selectLimit = $state(200)
35
+ let selecting = $state(false)
36
+ let selectError = $state<string | null>(null)
37
+
38
+ $effect(() => {
39
+ const next = { ...selectedColumns }
40
+ let changed = false
41
+ for (const col of queryableColumns) {
42
+ if (next[col] === undefined) {
43
+ next[col] = true
44
+ changed = true
45
+ }
46
+ }
47
+ for (const col of Object.keys(next)) {
48
+ if (!queryableColumns.includes(col)) {
49
+ delete next[col]
50
+ changed = true
51
+ }
52
+ }
53
+ if (changed) selectedColumns = next
54
+ })
55
+
56
+ // ─── filters ────────────────────────────────────────────────────────────
57
+ // Per-column substring filter, case-insensitive. The toolbar shows a
58
+ // single-input "filter X" affordance with column picker; the header
59
+ // also lets the user click a column name to focus the filter on it.
60
+ let filters = $state<Record<string, string>>({})
61
+ let filtersOpen = $state(false)
62
+
63
+ const filteredRows = $derived.by(() => {
64
+ const active = Object.entries(filters).filter(([, v]) => v.trim().length > 0)
65
+ if (active.length === 0) return allRows
66
+ return allRows.filter((row) =>
67
+ active.every(([col, term]) => {
68
+ const v = row[col]
69
+ if (v === null || v === undefined) return false
70
+ return String(v).toLowerCase().includes(term.toLowerCase())
71
+ }),
72
+ )
73
+ })
74
+
75
+ const rows = $derived(filteredRows)
76
+ const activeFilterCount = $derived(Object.values(filters).filter((v) => v.trim().length > 0).length)
77
+
78
+ function clearFilter(col: string) {
79
+ const next = { ...filters }
80
+ delete next[col]
81
+ filters = next
82
+ }
83
+
84
+ function clearAllFilters() {
85
+ filters = {}
86
+ }
87
+
88
+ function safeCollectionName(): string | null {
89
+ if (!collection) return null
90
+ const safe = collection.replace(/[^A-Za-z0-9_./-]/g, '')
91
+ return safe.length > 0 ? safe : null
92
+ }
93
+
94
+ function selectedProjection(): string {
95
+ const cols = queryableColumns.filter((c) => selectedColumns[c])
96
+ return cols.length === 0 || cols.length === queryableColumns.length ? '*' : cols.join(', ')
97
+ }
98
+
99
+ function selectSql(): string | null {
100
+ const safe = safeCollectionName()
101
+ if (!safe) return null
102
+ return `SELECT ${selectedProjection()} FROM ${safe} LIMIT ${selectLimit}`
103
+ }
104
+
105
+ async function runSelect() {
106
+ const client = connection.client
107
+ const sql = selectSql()
108
+ if (!client || !sql) return
109
+ selecting = true
110
+ selectError = null
111
+ try {
112
+ const r = await activity.track(`${collection} · ${sql}`, () => client.query(sql))
113
+ if (!r.ok) {
114
+ selectError = r.error ?? 'query failed'
115
+ return
116
+ }
117
+ overrideResult = r
118
+ filters = {}
119
+ editing = null
120
+ } catch (e) {
121
+ selectError = (e as Error).message
122
+ } finally {
123
+ selecting = false
124
+ }
125
+ }
126
+
127
+ function resetSelect() {
128
+ overrideResult = null
129
+ filters = {}
130
+ selectError = null
131
+ }
132
+
133
+ let editing = $state<{ row: number; col: string } | null>(null)
134
+ let editValue = $state('')
135
+
136
+ function startEdit(row: number, col: string, value: unknown) {
137
+ // Read-only server (#23): cells aren't editable — the mutation entry point
138
+ // is absent rather than letting a doomed edit stage and fail at commit.
139
+ if (editing || !collection || connection.readOnly) return
140
+ editing = { row, col }
141
+ const staged = pendingChanges.find(collection, row, col)
142
+ editValue = staged ? staged.newValue : String(value ?? '')
143
+ }
144
+
145
+ function commit() {
146
+ if (!editing || !collection) return
147
+ const { row, col } = editing
148
+ const original = rows[row]?.[col]
149
+ const next = editValue
150
+ if (String(original ?? '') !== next) {
151
+ pendingChanges.stage(collection, row, col, original, next)
152
+ } else {
153
+ // Reverting to original value — drop any prior staged change for it.
154
+ const existing = pendingChanges.find(collection, row, col)
155
+ if (existing) pendingChanges.unstage(existing.id)
156
+ }
157
+ editing = null
158
+ }
159
+
160
+ function cancel() {
161
+ editing = null
162
+ }
163
+
164
+ function focusOnMount(node: HTMLInputElement) {
165
+ node.focus()
166
+ node.select()
167
+ }
168
+
169
+ function cellDisplay(row: number, col: string, v: unknown): { value: unknown; staged: boolean } {
170
+ if (!collection) return { value: v, staged: false }
171
+ const staged = pendingChanges.find(collection, row, col)
172
+ if (staged) return { value: staged.newValue, staged: true }
173
+ return { value: v, staged: false }
174
+ }
175
+ </script>
176
+
177
+ <div class="flex h-full flex-col text-fg-1">
178
+ {#if !activeResult.ok}
179
+ <div class="flex-1 grid place-items-center text-fg-3 text-[12px] font-mono p-6">
180
+ {activeResult.error ?? 'Query failed.'}
181
+ </div>
182
+ {:else}
183
+ <!-- Toolbar: SELECT + per-column filters -->
184
+ <div class="border-b border-line-1 px-3 py-1.5 flex items-center gap-2 text-[11px] font-mono bg-bg-1/40">
185
+ {#if collection}
186
+ <button
187
+ type="button"
188
+ onclick={() => (selectOpen = !selectOpen)}
189
+ aria-pressed={selectOpen}
190
+ title="Build a SELECT for this collection"
191
+ class={[
192
+ 'inline-flex items-center gap-1.5 h-6 px-2 rounded border transition-colors cursor-pointer',
193
+ selectOpen
194
+ ? 'border-accent/40 bg-accent/10 text-accent'
195
+ : 'border-line-1 text-fg-3 hover:text-fg-1 hover:border-line-2',
196
+ ].join(' ')}
197
+ >
198
+ <Columns3 class="size-3" />
199
+ <span>select</span>
200
+ </button>
201
+ <button
202
+ type="button"
203
+ onclick={runSelect}
204
+ disabled={selecting || !connection.connected}
205
+ class="inline-flex items-center gap-1 h-6 px-2 rounded border border-line-1 text-fg-3 hover:text-fg-1 hover:border-line-2 cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
206
+ title="Run the current SELECT against reddb"
207
+ >
208
+ {#if selecting}<Loader2 class="size-3 animate-spin" />{:else}<Play class="size-3" />{/if}
209
+ run
210
+ </button>
211
+ {#if overrideResult}
212
+ <button
213
+ type="button"
214
+ onclick={resetSelect}
215
+ class="inline-flex items-center gap-1 h-6 px-2 rounded border border-line-1 text-fg-3 hover:text-fg-1 hover:border-line-2 cursor-pointer"
216
+ title="Return to the original result"
217
+ >
218
+ <RotateCw class="size-3" />
219
+ reset
220
+ </button>
221
+ {/if}
222
+ <div class="h-4 w-px bg-line-1"></div>
223
+ {/if}
224
+ <button
225
+ type="button"
226
+ onclick={() => (filtersOpen = !filtersOpen)}
227
+ aria-pressed={filtersOpen}
228
+ title={filtersOpen ? 'Hide column filters' : 'Show column filters'}
229
+ class={[
230
+ 'inline-flex items-center gap-1.5 h-6 px-2 rounded border transition-colors cursor-pointer',
231
+ filtersOpen
232
+ ? 'border-accent/40 bg-accent/10 text-accent'
233
+ : 'border-line-1 text-fg-3 hover:text-fg-1 hover:border-line-2',
234
+ ].join(' ')}
235
+ >
236
+ <Filter class="size-3" />
237
+ <span>filters</span>
238
+ {#if activeFilterCount > 0}
239
+ <span class="text-[9px] px-1 rounded bg-accent text-white">{activeFilterCount}</span>
240
+ {/if}
241
+ </button>
242
+ {#if activeFilterCount > 0}
243
+ <button
244
+ type="button"
245
+ onclick={clearAllFilters}
246
+ class="inline-flex items-center gap-1 h-6 px-2 rounded border border-line-1 text-fg-3 hover:text-fg-1 hover:border-line-2 cursor-pointer"
247
+ title="Clear all filters"
248
+ >
249
+ <X class="size-3" />
250
+ clear
251
+ </button>
252
+ {/if}
253
+ <div class="ml-auto text-fg-3">
254
+ {rows.length.toLocaleString()}{#if activeFilterCount > 0} of {allRows.length.toLocaleString()}{/if} rows
255
+ </div>
256
+ </div>
257
+
258
+ {#if selectOpen && collection}
259
+ <div class="border-b border-line-1 bg-bg-1/20 px-3 py-2 text-[11px] font-mono">
260
+ <div class="mb-2 flex items-center gap-2">
261
+ <span class="text-fg-3">columns:</span>
262
+ <button
263
+ type="button"
264
+ class="rounded border border-line-1 px-1.5 py-0.5 text-fg-3 hover:text-fg-1 hover:border-line-2"
265
+ onclick={() => {
266
+ const next: Record<string, boolean> = {}
267
+ for (const col of queryableColumns) next[col] = true
268
+ selectedColumns = next
269
+ }}
270
+ >
271
+ all
272
+ </button>
273
+ <button
274
+ type="button"
275
+ class="rounded border border-line-1 px-1.5 py-0.5 text-fg-3 hover:text-fg-1 hover:border-line-2"
276
+ onclick={() => (selectedColumns = {})}
277
+ >
278
+ none
279
+ </button>
280
+ <label class="ml-auto inline-flex items-center gap-1.5 text-fg-3">
281
+ limit
282
+ <select
283
+ class="bg-bg-1 text-fg-1 border border-line-1 rounded px-1 py-0.5"
284
+ value={selectLimit}
285
+ onchange={(e) => (selectLimit = Number((e.currentTarget as HTMLSelectElement).value))}
286
+ >
287
+ <option value={50}>50</option>
288
+ <option value={100}>100</option>
289
+ <option value={200}>200</option>
290
+ <option value={500}>500</option>
291
+ <option value={1000}>1k</option>
292
+ </select>
293
+ </label>
294
+ </div>
295
+ <div class="flex flex-wrap gap-1">
296
+ {#each queryableColumns as col (col)}
297
+ <label
298
+ class={[
299
+ 'inline-flex h-5 items-center gap-1 rounded border px-1.5 cursor-pointer',
300
+ selectedColumns[col] ? 'border-accent/40 bg-accent/10 text-accent' : 'border-line-1 text-fg-3 hover:text-fg-1',
301
+ ].join(' ')}
302
+ >
303
+ <input
304
+ type="checkbox"
305
+ class="sr-only"
306
+ checked={selectedColumns[col] ?? false}
307
+ onchange={(e) => (selectedColumns = { ...selectedColumns, [col]: (e.currentTarget as HTMLInputElement).checked })}
308
+ />
309
+ {col}
310
+ </label>
311
+ {/each}
312
+ </div>
313
+ <div class="mt-2 flex items-center gap-2 text-fg-3">
314
+ <code class="min-w-0 flex-1 truncate rounded border border-line-1 bg-bg-0 px-2 py-1 text-fg-1">{selectSql() ?? 'No collection selected'}</code>
315
+ {#if selectError}<span class="text-warn">{selectError}</span>{/if}
316
+ </div>
317
+ </div>
318
+ {/if}
319
+
320
+ {#if !activeResult.result.records.length}
321
+ <div class="flex-1 grid place-items-center text-fg-3 text-[12px] font-mono p-6">
322
+ Empty result.
323
+ </div>
324
+ {:else}
325
+
326
+ <div class="flex-1 overflow-auto">
327
+ <table class="w-full border-collapse text-[12px] font-mono">
328
+ <thead class="sticky top-0 bg-bg-1 z-10">
329
+ <tr class="border-b border-line-1 text-fg-3">
330
+ <th class="px-2 py-1.5 text-right w-10 font-normal">#</th>
331
+ {#each columns as c}
332
+ {@const kind = identityKind(c)}
333
+ {@const sys = isSystemField(c)}
334
+ <th
335
+ class={[
336
+ 'px-2 py-1.5 text-left font-normal whitespace-nowrap',
337
+ kind === 'rid' ? 'bg-accent/5 text-fg-0' : sys ? 'bg-bg-1 text-fg-2' : kind === 'id' ? 'text-fg-1' : '',
338
+ ].join(' ')}
339
+ >
340
+ <span class={kind || sys ? 'font-semibold' : ''}>{c}</span>
341
+ {#if kind === 'rid'}
342
+ <span class="ml-1 text-[9px] px-1 py-0.5 rounded bg-accent text-white uppercase tracking-wider font-semibold" title="System-assigned row id — reddb's canonical address">sys</span>
343
+ {:else if sys}
344
+ <span class="ml-1 text-[9px] px-1 py-0.5 rounded border border-line-2 text-fg-3 uppercase tracking-wider" title="Reserved system field — written by reddb">sys</span>
345
+ {:else if kind === 'id'}
346
+ <span class="ml-1 text-[9px] px-1 py-0.5 rounded border border-line-2 text-fg-2 uppercase tracking-wider" title="User-defined identifier">pk</span>
347
+ {:else if c === pkCol}
348
+ <span class="ml-1 text-[10px] text-fg-3 uppercase tracking-wide" title="Inferred primary key">pk</span>
349
+ {/if}
350
+ </th>
351
+ {/each}
352
+ </tr>
353
+ {#if filtersOpen}
354
+ <tr class="border-b border-line-1 bg-bg-0">
355
+ <th class="px-2 py-1"></th>
356
+ {#each columns as c}
357
+ <th class="px-1 py-1">
358
+ <div class="relative">
359
+ <input
360
+ type="text"
361
+ bind:value={filters[c]}
362
+ placeholder="filter {c}…"
363
+ class="w-full h-6 pl-2 pr-5 bg-bg-1 text-fg-1 border border-line-1 rounded text-[11px] font-mono outline-none focus:border-accent placeholder:text-fg-3"
364
+ />
365
+ {#if filters[c]?.length}
366
+ <button
367
+ type="button"
368
+ onclick={() => clearFilter(c)}
369
+ aria-label="Clear filter for {c}"
370
+ class="absolute right-1 top-1/2 -translate-y-1/2 text-fg-3 hover:text-fg-1 cursor-pointer"
371
+ >
372
+ <X class="size-3" />
373
+ </button>
374
+ {/if}
375
+ </div>
376
+ </th>
377
+ {/each}
378
+ </tr>
379
+ {/if}
380
+ </thead>
381
+ <tbody>
382
+ {#each rows as row, i (i)}
383
+ <tr class="border-b border-line-1/60 hover:bg-bg-1/40">
384
+ <td class="px-2 py-1 text-right text-fg-3 select-none">{i + 1}</td>
385
+ {#each columns as c}
386
+ {@const v = row[c]}
387
+ {@const display = cellDisplay(i, c, v)}
388
+ {@const isEditing = editing?.row === i && editing.col === c}
389
+ {@const kind = identityKind(c)}
390
+ {@const sys = isSystemField(c)}
391
+ <td
392
+ class={[
393
+ 'px-2 py-1 max-w-[260px] truncate align-top',
394
+ kind === 'rid'
395
+ ? 'bg-accent/5 text-accent font-semibold'
396
+ : sys
397
+ ? 'bg-bg-1 text-fg-2 font-mono'
398
+ : kind === 'id'
399
+ ? 'text-fg-0'
400
+ : '',
401
+ display.staged && !isEditing ? 'bg-warn/15 text-fg-1' : '',
402
+ ].join(' ')}
403
+ ondblclick={() => startEdit(i, c, v)}
404
+ title={display.staged ? 'Staged change — open Pending Changes to commit' : undefined}
405
+ >
406
+ {#if isEditing}
407
+ <input
408
+ class="w-full bg-bg-2 text-fg-1 border border-accent/60 rounded px-1 py-0.5 outline-none"
409
+ bind:value={editValue}
410
+ onblur={commit}
411
+ use:focusOnMount
412
+ onkeydown={(e) => {
413
+ if (e.key === 'Enter') commit()
414
+ else if (e.key === 'Escape') cancel()
415
+ }}
416
+ />
417
+ {:else if isNull(display.value)}
418
+ <span class="text-fg-3 italic">NULL</span>
419
+ {:else}
420
+ <span class="block truncate">{formatCell(display.value)}</span>
421
+ {/if}
422
+ </td>
423
+ {/each}
424
+ </tr>
425
+ {/each}
426
+ </tbody>
427
+ </table>
428
+ </div>
429
+ <div class="border-t border-line-1 px-3 py-1.5 text-[11px] font-mono text-fg-3 flex items-center gap-3">
430
+ <span>
431
+ {rows.length} of {activeResult.record_count} rows
432
+ {#if activeFilterCount > 0}· filtered{/if}
433
+ {#if collection}· <span class="text-fg-2">{collection}</span>{/if}
434
+ </span>
435
+ <span>· double-click a cell to stage edit</span>
436
+ {#if pkCol}<span>· pk <code class="text-fg-2">{pkCol}</code></span>{/if}
437
+ </div>
438
+ {/if}
439
+ {/if}
440
+ </div>