@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,266 @@
1
+ <script lang="ts">
2
+ import type { CollectionVcsState, QueryResult, RedClient, VcsCommit, VcsDiff } from '#reddb'
3
+ import { GitCommitHorizontal, GitCompareArrows, LoaderCircle, RadioTower, RefreshCw, ShieldCheck } from 'lucide-svelte'
4
+ import DiffRenderer from '$lib/renderers/DiffRenderer.svelte'
5
+ import LiveChanges from '$lib/LiveChanges.svelte'
6
+
7
+ interface Props {
8
+ client?: RedClient
9
+ collection: string
10
+ }
11
+
12
+ let { client, collection }: Props = $props()
13
+
14
+ let mode = $state<'commits' | 'diff' | 'live'>('commits')
15
+ let vcs = $state<CollectionVcsState | null>(null)
16
+ let commits = $state<VcsCommit[]>([])
17
+ let from = $state('')
18
+ let to = $state('')
19
+ let diff = $state<VcsDiff | null>(null)
20
+ let loading = $state(false)
21
+ let diffLoading = $state(false)
22
+ let error = $state<string | null>(null)
23
+ let loadedKey = ''
24
+
25
+ const canDiff = $derived(from.length > 0 && to.length > 0 && from !== to)
26
+
27
+ function shortHash(hash: string): string {
28
+ return hash.slice(0, 10)
29
+ }
30
+
31
+ function formatTime(ms: number | undefined): string {
32
+ if (!ms) return '-'
33
+ return new Date(ms).toISOString().replace('T', ' ').replace('Z', '')
34
+ }
35
+
36
+ function diffResult(d: VcsDiff): QueryResult {
37
+ return {
38
+ ok: true,
39
+ query: `/repo/commits/${d.from}/diff/${d.to}?collection=${collection}`,
40
+ capability: 'diff',
41
+ record_count: d.entries.length,
42
+ result: {
43
+ columns: ['collection', 'entity_id', 'change', 'before', 'after'],
44
+ records: d.entries.map((entry, index) => ({
45
+ values: {
46
+ id: `${entry.collection}:${entry.entity_id}:${index}`,
47
+ collection: entry.collection,
48
+ entity_id: entry.entity_id,
49
+ change: entry.change,
50
+ before: entry.before ?? null,
51
+ after: entry.after ?? null,
52
+ },
53
+ })),
54
+ },
55
+ }
56
+ }
57
+
58
+ async function load() {
59
+ if (!client) return
60
+ loading = true
61
+ error = null
62
+ diff = null
63
+ try {
64
+ const [vcsState, log] = await Promise.all([
65
+ client.collectionVcs(collection),
66
+ client.commits({ limit: 40 }),
67
+ ])
68
+ vcs = vcsState
69
+ commits = log
70
+ if (log.length >= 2) {
71
+ to = log[0].hash
72
+ from = log[1].hash
73
+ } else if (log.length === 1) {
74
+ to = log[0].hash
75
+ from = ''
76
+ } else {
77
+ to = ''
78
+ from = ''
79
+ }
80
+ } catch (e) {
81
+ error = (e as Error).message
82
+ } finally {
83
+ loading = false
84
+ }
85
+ }
86
+
87
+ async function enableVersioning() {
88
+ if (!client) return
89
+ loading = true
90
+ error = null
91
+ try {
92
+ vcs = await client.setCollectionVcs(collection, true)
93
+ commits = await client.commits({ limit: 40 })
94
+ } catch (e) {
95
+ error = (e as Error).message
96
+ } finally {
97
+ loading = false
98
+ }
99
+ }
100
+
101
+ async function runDiff() {
102
+ if (!client || !canDiff) return
103
+ diffLoading = true
104
+ error = null
105
+ try {
106
+ diff = await client.commitDiff(from, to, { collection })
107
+ mode = 'diff'
108
+ } catch (e) {
109
+ error = (e as Error).message
110
+ } finally {
111
+ diffLoading = false
112
+ }
113
+ }
114
+
115
+ $effect(() => {
116
+ const key = `${client?.baseUrl ?? 'none'}:${collection}`
117
+ if (key === loadedKey) return
118
+ loadedKey = key
119
+ void load()
120
+ })
121
+ </script>
122
+
123
+ <div class="h-full min-h-0 flex flex-col bg-bg-0">
124
+ <div class="border-b border-line-1 px-3 py-1.5 flex items-center gap-2 font-mono text-[11px]">
125
+ <div class="inline-flex overflow-hidden rounded border border-line-2">
126
+ <button
127
+ type="button"
128
+ class={['inline-flex h-7 items-center gap-1 px-2', mode === 'commits' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
129
+ onclick={() => (mode = 'commits')}
130
+ >
131
+ <GitCommitHorizontal class="size-3" />
132
+ commits
133
+ </button>
134
+ <button
135
+ type="button"
136
+ class={['inline-flex h-7 items-center gap-1 border-l border-line-2 px-2', mode === 'diff' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
137
+ onclick={() => (mode = 'diff')}
138
+ >
139
+ <GitCompareArrows class="size-3" />
140
+ diff
141
+ </button>
142
+ <button
143
+ type="button"
144
+ class={['inline-flex h-7 items-center gap-1 border-l border-line-2 px-2', mode === 'live' ? 'bg-accent text-white' : 'bg-bg-1 text-fg-2 hover:text-fg-0'].join(' ')}
145
+ onclick={() => (mode = 'live')}
146
+ >
147
+ <RadioTower class="size-3" />
148
+ live
149
+ </button>
150
+ </div>
151
+
152
+ <span class={vcs?.versioned ? 'text-ok' : 'text-fg-3'}>
153
+ {vcs?.versioned ? 'versioned' : 'not versioned'}
154
+ </span>
155
+ <span class="text-fg-3">{collection}</span>
156
+
157
+ <button
158
+ type="button"
159
+ class="ml-auto inline-flex h-7 items-center gap-1 rounded border border-line-1 px-2 text-fg-3 hover:bg-bg-2 hover:text-fg-1"
160
+ onclick={() => void load()}
161
+ disabled={loading}
162
+ >
163
+ {#if loading}
164
+ <LoaderCircle class="size-3 animate-spin" />
165
+ {:else}
166
+ <RefreshCw class="size-3" />
167
+ {/if}
168
+ refresh
169
+ </button>
170
+ </div>
171
+
172
+ {#if error}
173
+ <div class="border-b border-line-1 px-3 py-1.5 font-mono text-[11px] text-warn">{error}</div>
174
+ {/if}
175
+
176
+ {#if !client}
177
+ <div class="p-4 font-mono text-[12px] text-fg-3">No active RedDB connection.</div>
178
+ {:else if loading && !vcs}
179
+ <div class="p-4 font-mono text-[12px] text-fg-3">Loading version history…</div>
180
+ {:else if vcs && !vcs.versioned}
181
+ <div class="p-4 font-mono text-[12px] text-fg-3">
182
+ <div class="mb-2 text-fg-1">This collection is a normal collection with versioning disabled.</div>
183
+ <button
184
+ type="button"
185
+ class="inline-flex h-7 items-center gap-1 rounded border border-accent/40 bg-accent px-2 text-white hover:bg-accent/90"
186
+ onclick={() => void enableVersioning()}
187
+ >
188
+ <ShieldCheck class="size-3" />
189
+ enable versioning
190
+ </button>
191
+ </div>
192
+ {:else if mode === 'live'}
193
+ <div class="flex-1 min-h-0">
194
+ <LiveChanges {collection} />
195
+ </div>
196
+ {:else if mode === 'diff'}
197
+ <div class="border-b border-line-1 px-3 py-1.5 flex flex-wrap items-center gap-2 font-mono text-[11px]">
198
+ <label class="inline-flex items-center gap-1 text-fg-3">
199
+ <span>from</span>
200
+ <select bind:value={from} class="h-7 rounded border border-line-1 bg-bg-0 px-2 text-fg-1">
201
+ <option value="">select commit</option>
202
+ {#each commits as commit (commit.hash)}
203
+ <option value={commit.hash}>{shortHash(commit.hash)} · {commit.message}</option>
204
+ {/each}
205
+ </select>
206
+ </label>
207
+ <label class="inline-flex items-center gap-1 text-fg-3">
208
+ <span>to</span>
209
+ <select bind:value={to} class="h-7 rounded border border-line-1 bg-bg-0 px-2 text-fg-1">
210
+ <option value="">select commit</option>
211
+ {#each commits as commit (commit.hash)}
212
+ <option value={commit.hash}>{shortHash(commit.hash)} · {commit.message}</option>
213
+ {/each}
214
+ </select>
215
+ </label>
216
+ <button
217
+ type="button"
218
+ class="inline-flex h-7 items-center gap-1 rounded border border-line-1 px-2 text-fg-2 hover:bg-bg-2 disabled:text-fg-3"
219
+ disabled={!canDiff || diffLoading}
220
+ onclick={() => void runDiff()}
221
+ >
222
+ {#if diffLoading}<LoaderCircle class="size-3 animate-spin" />{:else}<GitCompareArrows class="size-3" />{/if}
223
+ run diff
224
+ </button>
225
+ {#if diff}
226
+ <span class="ml-auto text-fg-3">
227
+ +{diff.added} -{diff.removed} ~{diff.modified}
228
+ </span>
229
+ {/if}
230
+ </div>
231
+ <div class="flex-1 min-h-0">
232
+ {#if diff}
233
+ <DiffRenderer result={diffResult(diff)} {collection} />
234
+ {:else}
235
+ <div class="p-4 font-mono text-[12px] text-fg-3">Choose two commits to compare this collection.</div>
236
+ {/if}
237
+ </div>
238
+ {:else}
239
+ <div class="flex-1 overflow-auto">
240
+ {#if commits.length === 0}
241
+ <div class="p-4 font-mono text-[12px] text-fg-3">No commits yet.</div>
242
+ {:else}
243
+ <table class="w-full border-collapse font-mono text-[12px]">
244
+ <thead class="sticky top-0 bg-bg-0 text-fg-3">
245
+ <tr class="border-b border-line-1">
246
+ <th class="px-3 py-1.5 text-left font-normal">commit</th>
247
+ <th class="px-3 py-1.5 text-left font-normal">message</th>
248
+ <th class="px-3 py-1.5 text-left font-normal">author</th>
249
+ <th class="px-3 py-1.5 text-left font-normal">time</th>
250
+ </tr>
251
+ </thead>
252
+ <tbody>
253
+ {#each commits as commit (commit.hash)}
254
+ <tr class="border-b border-line-1/60 hover:bg-bg-1/50">
255
+ <td class="px-3 py-1.5 text-accent">{shortHash(commit.hash)}</td>
256
+ <td class="px-3 py-1.5 text-fg-1">{commit.message}</td>
257
+ <td class="px-3 py-1.5 text-fg-3">{commit.author?.name ?? '-'}</td>
258
+ <td class="px-3 py-1.5 text-fg-3">{formatTime(commit.timestamp_ms)}</td>
259
+ </tr>
260
+ {/each}
261
+ </tbody>
262
+ </table>
263
+ {/if}
264
+ </div>
265
+ {/if}
266
+ </div>
@@ -0,0 +1,14 @@
1
+ <script lang="ts">
2
+ import SchemaTree from './SchemaTree.svelte'
3
+ import ResultsPane from './ResultsPane.svelte'
4
+ </script>
5
+
6
+ <div class="h-full grid grid-cols-[260px_1fr] bg-bg-0 text-fg-1">
7
+ <aside class="row-start-1 col-start-1 border-r border-line-1 bg-bg-0 overflow-y-auto">
8
+ <SchemaTree />
9
+ </aside>
10
+
11
+ <section class="row-start-1 col-start-2 bg-bg-0 overflow-hidden">
12
+ <ResultsPane />
13
+ </section>
14
+ </div>
@@ -0,0 +1,237 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte'
3
+ import { Kbd } from '@reddb-io/ui-kit'
4
+ import { useRouter, type RouteTarget } from './router.svelte'
5
+ import { theme } from './theme.svelte'
6
+
7
+ const router = useRouter()
8
+
9
+ type Group = 'navigate' | 'data' | 'actions' | 'view'
10
+
11
+ interface Command {
12
+ id: string
13
+ group: Group
14
+ label: string
15
+ hint?: string
16
+ shortcut?: string
17
+ run: () => void
18
+ }
19
+
20
+ let open = $state(false)
21
+ let query = $state('')
22
+ let active = $state(0)
23
+ let inputEl: HTMLInputElement | undefined = $state()
24
+
25
+ function fire(name: string) {
26
+ window.dispatchEvent(new CustomEvent(name))
27
+ }
28
+
29
+ const commands: Command[] = [
30
+ { id: 'query', group: 'navigate', label: 'Open query', hint: '/query', run: () => navigate({ view: 'query' }) },
31
+ { id: 'collections', group: 'navigate', label: 'Open collections', hint: '/collections', run: () => navigate({ view: 'collections' }) },
32
+ { id: 'topology', group: 'navigate', label: 'Open cluster', hint: '/cluster', run: () => navigate({ view: 'cluster' }) },
33
+ { id: 'security', group: 'navigate', label: 'Open security', hint: '/security', run: () => navigate({ view: 'security' }) },
34
+
35
+ { id: 'new-query', group: 'data', label: 'New query', shortcut: '⌘T', run: () => { fire('red:new-query'); close() } },
36
+
37
+ { id: 'open-connect', group: 'actions', label: 'Open Connect dropdown', shortcut: '⌘⇧C', run: () => { fire('red:open-connect'); close() } },
38
+ { id: 'apply-pending', group: 'actions', label: 'Apply pending changes', run: () => { fire('red:apply-pending'); close() } },
39
+ { id: 'discard-pending', group: 'actions', label: 'Discard pending changes', run: () => { fire('red:discard-pending'); close() } },
40
+
41
+ { id: 'toggle-theme', group: 'view', label: 'Toggle theme (light / dark)', hint: 'theme', run: () => { theme.toggle(); close() } },
42
+ { id: 'show-shortcuts', group: 'view', label: 'Show keyboard shortcuts', shortcut: '?', run: () => { close(); fire('red:open-shortcuts') } },
43
+ ]
44
+
45
+ const filtered = $derived(
46
+ query.trim() === ''
47
+ ? commands
48
+ : commands.filter((c) => c.label.toLowerCase().includes(query.toLowerCase()))
49
+ )
50
+
51
+ function navigate(target: RouteTarget) {
52
+ router.go(target)
53
+ close()
54
+ }
55
+
56
+ function close() {
57
+ open = false
58
+ query = ''
59
+ active = 0
60
+ }
61
+
62
+ function isTypingTarget(el: EventTarget | null): boolean {
63
+ if (!(el instanceof HTMLElement)) return false
64
+ if (el.isContentEditable) return true
65
+ const tag = el.tagName
66
+ return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT'
67
+ }
68
+
69
+ function onKey(e: KeyboardEvent) {
70
+ const mod = e.metaKey || e.ctrlKey
71
+ const k = e.key.toLowerCase()
72
+
73
+ if (mod && k === 'k') {
74
+ e.preventDefault()
75
+ open = !open
76
+ if (open) setTimeout(() => inputEl?.focus(), 0)
77
+ return
78
+ }
79
+
80
+ // Global accelerators wired here so they fire regardless of focus.
81
+ if (mod && e.shiftKey && k === 'c') {
82
+ e.preventDefault()
83
+ fire('red:open-connect')
84
+ return
85
+ }
86
+ if (mod && !e.shiftKey && k === 't' && !isTypingTarget(e.target)) {
87
+ e.preventDefault()
88
+ fire('red:new-query')
89
+ return
90
+ }
91
+
92
+ if (!open) return
93
+ if (e.key === 'Escape') { close(); return }
94
+ if (e.key === 'ArrowDown') { e.preventDefault(); active = Math.min(active + 1, filtered.length - 1) }
95
+ if (e.key === 'ArrowUp') { e.preventDefault(); active = Math.max(active - 1, 0) }
96
+ if (e.key === 'Enter') {
97
+ e.preventDefault()
98
+ filtered[active]?.run()
99
+ }
100
+ }
101
+
102
+ onMount(() => {
103
+ window.addEventListener('keydown', onKey)
104
+ const onOpen = () => {
105
+ open = true
106
+ setTimeout(() => inputEl?.focus(), 0)
107
+ }
108
+ window.addEventListener('red:open-palette', onOpen)
109
+ return () => {
110
+ window.removeEventListener('keydown', onKey)
111
+ window.removeEventListener('red:open-palette', onOpen)
112
+ }
113
+ })
114
+
115
+ const groupOrder: Group[] = ['navigate', 'data', 'actions', 'view']
116
+ const groupLabel: Record<Group, string> = {
117
+ navigate: 'Navigate',
118
+ data: 'Data',
119
+ actions: 'Actions',
120
+ view: 'View',
121
+ }
122
+ </script>
123
+
124
+ {#if open}
125
+ <div class="scrim" onclick={close} onkeydown={null} role="presentation"></div>
126
+ <div class="palette" role="dialog" aria-modal="true">
127
+ <div class="input-wrap">
128
+ <span class="search-icon">⌕</span>
129
+ <input
130
+ bind:this={inputEl}
131
+ bind:value={query}
132
+ oninput={() => (active = 0)}
133
+ placeholder="Search or run a command…"
134
+ spellcheck="false"
135
+ />
136
+ <Kbd>esc</Kbd>
137
+ </div>
138
+
139
+ <div class="list">
140
+ {#each groupOrder as g}
141
+ {@const items = filtered.filter((c) => c.group === g)}
142
+ {#if items.length}
143
+ <div class="group-label">{groupLabel[g]}</div>
144
+ {#each items as cmd}
145
+ {@const idx = filtered.indexOf(cmd)}
146
+ <button
147
+ class="row"
148
+ class:active={idx === active}
149
+ onmouseenter={() => (active = idx)}
150
+ onclick={() => cmd.run()}
151
+ >
152
+ <span class="label">{cmd.label}</span>
153
+ {#if cmd.hint}<span class="hint">{cmd.hint}</span>{/if}
154
+ {#if cmd.shortcut}<Kbd>{cmd.shortcut}</Kbd>{/if}
155
+ </button>
156
+ {/each}
157
+ {/if}
158
+ {/each}
159
+ {#if filtered.length === 0}
160
+ <div class="empty">No commands match "{query}"</div>
161
+ {/if}
162
+ </div>
163
+
164
+ <footer>
165
+ <span><Kbd>↑</Kbd> <Kbd>↓</Kbd> navigate</span>
166
+ <span><Kbd>↵</Kbd> run</span>
167
+ <span><Kbd>⌘</Kbd><Kbd>K</Kbd> toggle</span>
168
+ </footer>
169
+ </div>
170
+ {/if}
171
+
172
+ <style>
173
+ .scrim {
174
+ position: fixed; inset: 0; z-index: 100;
175
+ background: rgba(0, 0, 0, 0.5);
176
+ backdrop-filter: blur(4px);
177
+ animation: fade 140ms var(--ease-out);
178
+ }
179
+ .palette {
180
+ position: fixed; z-index: 101;
181
+ top: 18%; left: 50%; transform: translateX(-50%);
182
+ width: min(640px, 92vw);
183
+ background: var(--bg-1);
184
+ border: 1px solid var(--line-2);
185
+ border-radius: var(--r-lg);
186
+ box-shadow: var(--shadow-lg);
187
+ overflow: hidden;
188
+ animation: pop 180ms var(--ease-snap);
189
+ }
190
+ @keyframes fade { from { opacity: 0 } to { opacity: 1 } }
191
+ @keyframes pop { from { opacity: 0; transform: translate(-50%, -8px) scale(0.98); } to { opacity: 1; transform: translateX(-50%) scale(1); } }
192
+
193
+ .input-wrap {
194
+ display: flex; align-items: center; gap: 10px;
195
+ padding: 12px 14px;
196
+ border-bottom: 1px solid var(--line-1);
197
+ }
198
+ .search-icon { color: var(--fg-3); font-size: 18px; }
199
+ .input-wrap input {
200
+ flex: 1; background: transparent; border: 0; outline: 0;
201
+ color: var(--fg-0); font-size: 14px;
202
+ }
203
+ .input-wrap input::placeholder { color: var(--fg-3); }
204
+
205
+ .list { max-height: 360px; overflow-y: auto; padding: 6px; }
206
+ .group-label {
207
+ padding: 10px 8px 4px;
208
+ font-family: var(--font-mono);
209
+ font-size: 10px;
210
+ text-transform: uppercase;
211
+ letter-spacing: 0.08em;
212
+ color: var(--fg-3);
213
+ }
214
+ .row {
215
+ display: flex; align-items: center; gap: 10px;
216
+ width: 100%; padding: 8px 10px;
217
+ background: transparent; border: 0; cursor: pointer;
218
+ border-radius: var(--r-md);
219
+ text-align: left;
220
+ color: var(--fg-1);
221
+ font-size: 13px;
222
+ }
223
+ .row.active { background: var(--bg-2); color: var(--fg-0); }
224
+ .label { flex: 1; }
225
+ .hint { color: var(--fg-3); font-family: var(--font-mono); font-size: 11px; }
226
+
227
+ .empty { padding: 24px; text-align: center; color: var(--fg-3); font-size: 13px; }
228
+
229
+ footer {
230
+ display: flex; gap: 14px; justify-content: flex-end;
231
+ padding: 8px 14px;
232
+ border-top: 1px solid var(--line-1);
233
+ background: var(--bg-0);
234
+ font-size: 11px;
235
+ color: var(--fg-3);
236
+ }
237
+ </style>