@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,253 @@
1
+ <script lang="ts">
2
+ import type { QueryResult } from '#reddb'
3
+ import { Activity, BarChart3, LineChart, X } from 'lucide-svelte'
4
+ import {
5
+ ALL_METRICS,
6
+ bucketGeometry,
7
+ bucketSeries,
8
+ chartGeometry,
9
+ extractSeries,
10
+ type BucketPoint,
11
+ } from './hypertable-render'
12
+
13
+ interface Props {
14
+ result: QueryResult
15
+ collection?: string
16
+ showSystem?: boolean
17
+ }
18
+
19
+ let { result, collection }: Props = $props()
20
+
21
+ let metric = $state<string | undefined>(ALL_METRICS)
22
+ let chartMode = $state<'line' | 'rate'>('rate')
23
+ let bucketMs = $state(60_000)
24
+ let selectedBucket = $state<BucketPoint | null>(null)
25
+
26
+ const series = $derived(extractSeries(result, metric))
27
+ const lineGeom = $derived(series && chartMode === 'line' ? chartGeometry(series) : null)
28
+ const buckets = $derived(series ? bucketSeries(series, bucketMs) : [])
29
+ const barGeom = $derived(series && chartMode === 'rate' ? bucketGeometry(buckets) : null)
30
+
31
+ // ─── filter rows by selected bucket ─────────────────────────────────────
32
+ const visiblePoints = $derived.by(() => {
33
+ if (!series) return []
34
+ if (!selectedBucket) return series.points
35
+ const t0 = selectedBucket.t
36
+ const t1 = t0 + bucketMs
37
+ return series.points.filter((p) => p.t >= t0 && p.t < t1)
38
+ })
39
+
40
+ function fmtT(t: number): string {
41
+ try {
42
+ return new Date(t).toISOString().replace('T', ' ').slice(0, 19)
43
+ } catch {
44
+ return String(t)
45
+ }
46
+ }
47
+
48
+ function renderLogValue(v: unknown): string {
49
+ if (v === null || v === undefined) return ''
50
+ if (typeof v === 'object') return JSON.stringify(v)
51
+ return String(v)
52
+ }
53
+
54
+ function logPayload(p: { raw: Record<string, unknown> }): string {
55
+ const parts: string[] = []
56
+ const metricName = series?.metricNameColumn ? p.raw[series.metricNameColumn] : undefined
57
+ if (metricName !== undefined) parts.push(String(metricName))
58
+ const tags = p.raw.tags
59
+ if (tags && typeof tags === 'object') {
60
+ const tagText = Object.entries(tags as Record<string, unknown>)
61
+ .map(([k, v]) => `${k}=${renderLogValue(v)}`)
62
+ .join(' ')
63
+ if (tagText) parts.push(tagText)
64
+ }
65
+ const event = p.raw.event ?? p.raw.action ?? p.raw.message ?? p.raw.payload
66
+ if (event !== undefined) parts.push(renderLogValue(event))
67
+ return parts.join(' · ')
68
+ }
69
+
70
+ function bucketLabel(opt: number): string {
71
+ if (opt === 1000) return '1s'
72
+ if (opt === 10_000) return '10s'
73
+ if (opt === 60_000) return '1min'
74
+ if (opt === 300_000) return '5min'
75
+ if (opt === 600_000) return '10min'
76
+ if (opt === 3_600_000) return '1hr'
77
+ return `${opt}ms`
78
+ }
79
+
80
+ function metricLabel(m: string): string {
81
+ return m === ALL_METRICS ? 'all writes' : m
82
+ }
83
+
84
+ function activeMetricLabel(): string {
85
+ if (!series) return ''
86
+ if (!metric || metric === ALL_METRICS) return 'all writes'
87
+ return metric
88
+ }
89
+ </script>
90
+
91
+ <div class="flex h-full flex-col text-fg-1">
92
+ {#if !series}
93
+ <div class="flex-1 grid place-items-center text-fg-3 text-[12px] font-mono p-6">
94
+ No timeseries shape (need a time column and a numeric metric).
95
+ </div>
96
+ {:else}
97
+ <!-- Toolbar -->
98
+ <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">
99
+ <div class="inline-flex border border-line-2 rounded overflow-hidden">
100
+ <button
101
+ type="button"
102
+ class={[
103
+ 'inline-flex items-center gap-1 px-2 py-1 cursor-pointer transition-colors',
104
+ chartMode === 'rate' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0',
105
+ ].join(' ')}
106
+ onclick={() => (chartMode = 'rate')}
107
+ aria-pressed={chartMode === 'rate'}
108
+ >
109
+ <BarChart3 class="size-3" />
110
+ Writes/{bucketLabel(bucketMs)}
111
+ </button>
112
+ <button
113
+ type="button"
114
+ class={[
115
+ 'inline-flex items-center gap-1 px-2 py-1 cursor-pointer transition-colors border-l border-line-2',
116
+ chartMode === 'line' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0',
117
+ ].join(' ')}
118
+ onclick={() => (chartMode = 'line')}
119
+ aria-pressed={chartMode === 'line'}
120
+ >
121
+ <LineChart class="size-3" />
122
+ Value
123
+ </button>
124
+ </div>
125
+
126
+ <label class="inline-flex items-center gap-1.5 text-fg-3">
127
+ <Activity class="size-3" />
128
+ metric:
129
+ <select
130
+ class="bg-bg-1 text-fg-1 border border-line-1 rounded px-1.5 py-0.5"
131
+ bind:value={metric}
132
+ >
133
+ {#each series.metrics as m (m)}
134
+ <option value={m}>{metricLabel(m)}</option>
135
+ {/each}
136
+ </select>
137
+ </label>
138
+
139
+ {#if chartMode === 'rate'}
140
+ <label class="inline-flex items-center gap-1.5 text-fg-3">
141
+ bucket:
142
+ <select
143
+ class="bg-bg-1 text-fg-1 border border-line-1 rounded px-1.5 py-0.5"
144
+ value={bucketMs}
145
+ onchange={(e) => { bucketMs = Number((e.currentTarget as HTMLSelectElement).value); selectedBucket = null }}
146
+ >
147
+ <option value={1000}>1s</option>
148
+ <option value={10_000}>10s</option>
149
+ <option value={60_000}>1min</option>
150
+ <option value={300_000}>5min</option>
151
+ <option value={600_000}>10min</option>
152
+ <option value={3_600_000}>1hr</option>
153
+ </select>
154
+ </label>
155
+ {/if}
156
+
157
+ {#if selectedBucket}
158
+ <button
159
+ type="button"
160
+ onclick={() => (selectedBucket = null)}
161
+ class="inline-flex items-center gap-1 px-2 py-1 rounded border border-line-1 text-fg-3 hover:text-fg-1 cursor-pointer"
162
+ title="Show all rows again"
163
+ >
164
+ <X class="size-3" />
165
+ clear bucket
166
+ </button>
167
+ {/if}
168
+
169
+ <div class="ml-auto text-fg-3">
170
+ <span class="text-fg-2">{activeMetricLabel()}</span>
171
+ · {visiblePoints.length.toLocaleString()}{#if selectedBucket} of {series.points.length.toLocaleString()}{/if} inserts
172
+ {#if collection}· <span class="text-fg-2">{collection}</span>{/if}
173
+ </div>
174
+ </div>
175
+
176
+ <!-- Chart -->
177
+ <div class="px-3 pt-2">
178
+ {#if chartMode === 'rate' && barGeom}
179
+ <svg
180
+ viewBox={`0 0 ${barGeom.width} ${barGeom.height}`}
181
+ class="block w-full h-[180px]"
182
+ aria-label="Writes per bucket"
183
+ >
184
+ {#each barGeom.bars as bar (bar.bucket.t)}
185
+ <rect
186
+ x={bar.x}
187
+ y={bar.y}
188
+ width={bar.w}
189
+ height={bar.h}
190
+ class="cursor-pointer transition-opacity"
191
+ fill="var(--color-accent)"
192
+ opacity={!selectedBucket || selectedBucket.t === bar.bucket.t ? 0.9 : 0.3}
193
+ role="button"
194
+ tabindex="0"
195
+ aria-label="Bucket starting at {fmtT(bar.bucket.t)} with {bar.bucket.count} writes"
196
+ onclick={() => (selectedBucket = selectedBucket?.t === bar.bucket.t ? null : bar.bucket)}
197
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectedBucket = selectedBucket?.t === bar.bucket.t ? null : bar.bucket } }}
198
+ >
199
+ <title>{fmtT(bar.bucket.t)} — {bar.bucket.count} writes (sum {bar.bucket.sum.toFixed(2)})</title>
200
+ </rect>
201
+ {/each}
202
+ <text x={barGeom.padding} y={barGeom.height - 2} class="text-[9px] fill-fg-3 font-mono">
203
+ {fmtT(barGeom.bars[0]?.bucket.t ?? 0)}
204
+ </text>
205
+ <text x={barGeom.width - barGeom.padding} y={barGeom.height - 2} text-anchor="end" class="text-[9px] fill-fg-3 font-mono">
206
+ peak {barGeom.maxCount}/{bucketLabel(bucketMs)}
207
+ </text>
208
+ </svg>
209
+ {:else if lineGeom}
210
+ <svg
211
+ viewBox={`0 0 ${lineGeom.width} ${lineGeom.height}`}
212
+ class="block w-full h-[180px] text-accent"
213
+ aria-label="Timeseries chart"
214
+ >
215
+ <path d={lineGeom.path} fill="none" stroke="currentColor" stroke-width="1.5" />
216
+ </svg>
217
+ {/if}
218
+ </div>
219
+
220
+ <!-- Event log -->
221
+ <div class="flex-1 min-h-0 overflow-auto">
222
+ <table class="w-full border-collapse text-[12px] font-mono">
223
+ <thead class="sticky top-0 bg-bg-1 z-10">
224
+ <tr class="border-b border-line-1 text-fg-3">
225
+ <th class="px-2 py-1.5 text-left font-normal">time</th>
226
+ {#if series.metricNameColumn}
227
+ <th class="px-2 py-1.5 text-left font-normal">metric</th>
228
+ {/if}
229
+ <th class="px-2 py-1.5 text-right font-normal">value</th>
230
+ <th class="px-2 py-1.5 text-left font-normal">log</th>
231
+ </tr>
232
+ </thead>
233
+ <tbody>
234
+ {#each visiblePoints as p, i (i)}
235
+ <tr class="border-b border-line-1/60 hover:bg-bg-1/40">
236
+ <td class="px-2 py-1">{fmtT(p.t)}</td>
237
+ {#if series.metricNameColumn}
238
+ <td class="px-2 py-1 text-fg-2">{String(p.raw[series.metricNameColumn] ?? '')}</td>
239
+ {/if}
240
+ <td class="px-2 py-1 text-right text-accent">{p.v}</td>
241
+ <td class="px-2 py-1 text-fg-2">{logPayload(p)}</td>
242
+ </tr>
243
+ {/each}
244
+ </tbody>
245
+ </table>
246
+ </div>
247
+
248
+ <div class="border-t border-line-1 px-3 py-1.5 text-[11px] font-mono text-fg-3">
249
+ {visiblePoints.length} inserts{collection ? ' · ' : ''}{collection ?? ''}
250
+ {#if selectedBucket}· bucket {fmtT(selectedBucket.t)} (+{bucketLabel(bucketMs)}){/if}
251
+ </div>
252
+ {/if}
253
+ </div>
@@ -0,0 +1,30 @@
1
+ <script lang="ts">
2
+ import type { QueryResult } from '#reddb'
3
+ import { highlightJson, prettyJson, projectJson } from './json-render'
4
+
5
+ interface Props {
6
+ result: QueryResult
7
+ collection?: string
8
+ }
9
+
10
+ let { result, collection }: Props = $props()
11
+ const pretty = $derived(prettyJson(projectJson(result)))
12
+ const html = $derived(highlightJson(pretty))
13
+ </script>
14
+
15
+ <div class="flex h-full flex-col text-fg-1">
16
+ <div class="flex-1 min-h-0 overflow-auto">
17
+ <pre class="json text-[12px] font-mono p-3 leading-relaxed">{@html html}</pre>
18
+ </div>
19
+ <div class="border-t border-line-1 px-3 py-1.5 text-[11px] font-mono text-fg-3">
20
+ {result.record_count} records · raw shape{collection ? ' · ' : ''}{collection ?? ''}
21
+ </div>
22
+ </div>
23
+
24
+ <style>
25
+ .json :global(.k) { color: var(--color-accent); }
26
+ .json :global(.s) { color: var(--color-fg-1); }
27
+ .json :global(.n) { color: #7dd3fc; }
28
+ .json :global(.b) { color: #fbbf24; }
29
+ .json :global(.z) { color: var(--color-fg-3); font-style: italic; }
30
+ </style>
@@ -0,0 +1,150 @@
1
+ <script lang="ts">
2
+ import type { QueryResult } from '#reddb'
3
+ import { Braces, ListTree, Search, X } from 'lucide-svelte'
4
+ import { buildTree, extractKv, formatValue, materializeKvJson, type KvNode } from './kv-render'
5
+
6
+ interface Props {
7
+ result: QueryResult
8
+ collection?: string
9
+ showSystem?: boolean
10
+ }
11
+
12
+ let { result, collection }: Props = $props()
13
+
14
+ // ─── state ──────────────────────────────────────────────────────────────
15
+ let prefix = $state('')
16
+ let viewMode = $state<'tree' | 'json'>('tree')
17
+ let selected = $state<KvNode | null>(null)
18
+
19
+ // ─── data ───────────────────────────────────────────────────────────────
20
+ const entries = $derived(extractKv(result))
21
+ const filteredEntries = $derived(
22
+ prefix.trim() ? entries.filter((e) => e.key.startsWith(prefix.trim())) : entries,
23
+ )
24
+ const tree = $derived(buildTree(filteredEntries))
25
+ const jsonObject = $derived(materializeKvJson(entries, prefix.trim()))
26
+
27
+ function preview(v: unknown): string {
28
+ const s = formatValue(v)
29
+ return s.length > 60 ? s.slice(0, 59) + '…' : s
30
+ }
31
+ </script>
32
+
33
+ <div class="flex h-full flex-col text-fg-1">
34
+ {#if entries.length === 0}
35
+ <div class="flex-1 grid place-items-center text-fg-3 text-[12px] font-mono p-6">
36
+ No KV shape — expected <code>key</code> and <code>value</code> columns
37
+ (also accepts <code>*_key</code>/<code>*_value</code>).
38
+ </div>
39
+ {:else}
40
+ <!-- Toolbar -->
41
+ <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">
42
+ <div class="inline-flex border border-line-2 rounded overflow-hidden">
43
+ <button
44
+ type="button"
45
+ class={[
46
+ 'inline-flex items-center gap-1 px-2 py-1 cursor-pointer transition-colors',
47
+ viewMode === 'tree' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0',
48
+ ].join(' ')}
49
+ onclick={() => (viewMode = 'tree')}
50
+ aria-pressed={viewMode === 'tree'}
51
+ >
52
+ <ListTree class="size-3" />
53
+ Tree
54
+ </button>
55
+ <button
56
+ type="button"
57
+ class={[
58
+ 'inline-flex items-center gap-1 px-2 py-1 cursor-pointer transition-colors border-l border-line-2',
59
+ viewMode === 'json' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0',
60
+ ].join(' ')}
61
+ onclick={() => (viewMode = 'json')}
62
+ aria-pressed={viewMode === 'json'}
63
+ >
64
+ <Braces class="size-3" />
65
+ JSON
66
+ </button>
67
+ </div>
68
+
69
+ <div class="inline-flex items-center gap-1.5 border border-line-1 rounded px-2 py-1 bg-bg-0">
70
+ <Search class="size-3 text-fg-3" />
71
+ <input
72
+ type="text"
73
+ bind:value={prefix}
74
+ placeholder="key prefix…"
75
+ class="bg-transparent text-fg-1 outline-none w-[220px] placeholder:text-fg-3"
76
+ />
77
+ {#if prefix}
78
+ <button
79
+ type="button"
80
+ onclick={() => (prefix = '')}
81
+ class="text-fg-3 hover:text-fg-1 cursor-pointer"
82
+ aria-label="Clear prefix"
83
+ >
84
+ <X class="size-3" />
85
+ </button>
86
+ {/if}
87
+ </div>
88
+
89
+ <div class="ml-auto text-fg-3">
90
+ {filteredEntries.length.toLocaleString()}{#if prefix} of {entries.length.toLocaleString()}{/if} keys
91
+ {#if collection}· <span class="text-fg-2">{collection}</span>{/if}
92
+ </div>
93
+ </div>
94
+
95
+ <!-- Body -->
96
+ {#if viewMode === 'tree'}
97
+ <div class="flex-1 grid grid-cols-[280px_1fr] min-h-0">
98
+ <ul class="overflow-auto border-r border-line-1 text-[12px] font-mono py-1">
99
+ {#if tree.children.length === 0}
100
+ <li class="px-3 py-2 text-fg-3">No keys match prefix.</li>
101
+ {:else}
102
+ {#each tree.children as node (node.name)}
103
+ {@render renderNode(node, 0)}
104
+ {/each}
105
+ {/if}
106
+ </ul>
107
+ <div class="overflow-auto p-3 text-[12px] font-mono">
108
+ {#if selected}
109
+ <div class="text-fg-3 mb-2">{selected.fullKey ?? selected.name}</div>
110
+ <pre class="text-fg-1 whitespace-pre-wrap break-all">{formatValue(selected.value)}</pre>
111
+ {:else}
112
+ <div class="text-fg-3">Click a key to inspect.</div>
113
+ {/if}
114
+ </div>
115
+ </div>
116
+ {:else}
117
+ <div class="flex-1 overflow-auto p-3 text-[12px] font-mono">
118
+ {#if Object.keys(jsonObject).length === 0}
119
+ <div class="text-fg-3">No keys match prefix.</div>
120
+ {:else}
121
+ <pre class="whitespace-pre text-fg-1">{JSON.stringify(jsonObject, null, 2)}</pre>
122
+ {/if}
123
+ </div>
124
+ {/if}
125
+ {/if}
126
+ </div>
127
+
128
+ {#snippet renderNode(node: KvNode, depth: number)}
129
+ <li>
130
+ <button
131
+ type="button"
132
+ class="w-full text-left px-2 py-0.5 hover:bg-bg-1/60"
133
+ class:text-accent={selected === node}
134
+ style:padding-left={`${8 + depth * 12}px`}
135
+ onclick={() => (selected = node)}
136
+ >
137
+ <span class="text-fg-1">{node.name}</span>
138
+ {#if node.fullKey !== null}
139
+ <span class="text-fg-3 ml-2">{preview(node.value)}</span>
140
+ {/if}
141
+ </button>
142
+ {#if node.children.length > 0}
143
+ <ul>
144
+ {#each node.children as child (child.name)}
145
+ {@render renderNode(child, depth + 1)}
146
+ {/each}
147
+ </ul>
148
+ {/if}
149
+ </li>
150
+ {/snippet}