@reddb-io/ui 0.1.1 → 0.2.0
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.
- package/build/_app/immutable/assets/0.DQDLkSdR.css +1 -0
- package/build/_app/immutable/assets/Workspace.CvP5De7i.css +1 -0
- package/build/_app/immutable/chunks/65XjEwaF.js +367 -0
- package/build/_app/immutable/chunks/{Ov8N-vxt.js → BHBXmd1Y.js} +1 -1
- package/build/_app/immutable/chunks/BMc_qoms.js +973 -0
- package/build/_app/immutable/chunks/{ALqaDrNQ.js → C278awGv.js} +1 -1
- package/build/_app/immutable/chunks/CDKIdLnm.js +187 -0
- package/build/_app/immutable/chunks/{DXY_Rxa7.js → CXNPJgv1.js} +7 -7
- package/build/_app/immutable/chunks/{BMHKbvs8.js → CwxFwvow.js} +1 -1
- package/build/_app/immutable/chunks/{Bgso8cDH.js → DqvWKbSc.js} +1 -1
- package/build/_app/immutable/entry/app.CGfmRH-N.js +2 -0
- package/build/_app/immutable/entry/start._LhFJimm.js +1 -0
- package/build/_app/immutable/nodes/{0.CHPiFxDo.js → 0.BYhY6O5-.js} +1 -1
- package/build/_app/immutable/nodes/{1.BC_4YZ0h.js → 1.4XYGRMul.js} +1 -1
- package/build/_app/immutable/nodes/11.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/{10.B7DMKSNc.js → 12.DXZfNPNK.js} +5 -5
- package/build/_app/immutable/nodes/7.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/{7.BZX9Iird.js → 8.C7OOsMOJ.js} +1 -1
- package/build/_app/version.json +1 -1
- package/build/index.html +12 -12
- package/dist/embed/{RedUiEmbedRoot-BGoLxJf1.js → RedUiEmbedRoot-Dia9lxfb.js} +12622 -10614
- package/dist/embed/RedUiEmbedRoot-Dia9lxfb.js.map +1 -0
- package/dist/embed/index-Bq0TT7Ao.js +577 -0
- package/dist/embed/{index-B6ZQhCsu.js.map → index-Bq0TT7Ao.js.map} +1 -1
- package/dist/embed/{index-fad77a13.esm-BqPj0saH.js → index-fad77a13.esm-BMULHdcE.js} +22 -20
- package/dist/embed/{index-fad77a13.esm-BqPj0saH.js.map → index-fad77a13.esm-BMULHdcE.js.map} +1 -1
- package/dist/embed/index.js +1 -1
- package/dist/embed/sigma-edge-curve.esm-CAT5Xrom.js +536 -0
- package/dist/embed/sigma-edge-curve.esm-CAT5Xrom.js.map +1 -0
- package/dist/embed/{sigma-node-border.esm-CGtlgqmo.js → sigma-node-border.esm-Bnk4BRbw.js} +32 -32
- package/dist/embed/{sigma-node-border.esm-CGtlgqmo.js.map → sigma-node-border.esm-Bnk4BRbw.js.map} +1 -1
- package/dist/embed/{sigma.esm-y0qgY-OC.js → sigma.esm-C2pqg5JH.js} +3 -3
- package/dist/embed/{sigma.esm-y0qgY-OC.js.map → sigma.esm-C2pqg5JH.js.map} +1 -1
- package/package.json +5 -3
- package/src/lib/Appearance.svelte +90 -0
- package/src/lib/CommandPalette.svelte +2 -0
- package/src/lib/SettingsView.svelte +322 -0
- package/src/lib/Topbar.svelte +15 -1
- package/src/lib/Workspace.svelte +14 -1
- package/src/lib/index.ts +2 -0
- package/src/lib/renderers/GraphRenderer.svelte +448 -44
- package/src/lib/renderers/graph-bundle.ts +401 -0
- package/src/lib/renderers/graph-communities.ts +193 -0
- package/src/lib/renderers/graph-render.ts +48 -5
- package/src/lib/renderers/graph-sigma.ts +274 -8
- package/src/lib/router.svelte.ts +84 -57
- package/src/lib/settings-sections.ts +249 -0
- package/src/lib/skins/skins.svelte.ts +64 -0
- package/src/lib/skins/skins.ts +178 -0
- package/build/_app/immutable/assets/0.C63jY5jX.css +0 -1
- package/build/_app/immutable/assets/Workspace.BCzG1DsH.css +0 -1
- package/build/_app/immutable/chunks/BQHbwf54.js +0 -916
- package/build/_app/immutable/chunks/Bmgx1mfv.js +0 -310
- package/build/_app/immutable/entry/app.D_IVtpdv.js +0 -2
- package/build/_app/immutable/entry/start.3oC0UQ_Q.js +0 -1
- package/dist/embed/RedUiEmbedRoot-BGoLxJf1.js.map +0 -1
- package/dist/embed/index-B6ZQhCsu.js +0 -577
- /package/build/_app/immutable/assets/{10.Dblyk0Kr.css → 12.Dblyk0Kr.css} +0 -0
- /package/build/_app/immutable/nodes/{8.CAG5_cGP.js → 10.CAG5_cGP.js} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reddb-io/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "RedDB UI components and standalone web application.",
|
|
6
6
|
"type": "module",
|
|
@@ -45,8 +45,10 @@
|
|
|
45
45
|
"access": "public"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
+
"@aflsolutions/graphology-communities-leiden": "^1.1.1",
|
|
48
49
|
"@fontsource-variable/inter": "^5.2.8",
|
|
49
50
|
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
|
51
|
+
"@sigma/edge-curve": "^3.1.0",
|
|
50
52
|
"@sigma/node-border": "^3.0.0",
|
|
51
53
|
"@tauri-apps/api": "^2.5.0",
|
|
52
54
|
"@xyflow/svelte": "^1.2.0",
|
|
@@ -59,8 +61,8 @@
|
|
|
59
61
|
"sigma": "^3.0.3",
|
|
60
62
|
"tailwind-merge": "^3.6.0",
|
|
61
63
|
"tailwind-variants": "^2.1.0",
|
|
62
|
-
"@reddb-io/ui-
|
|
63
|
-
"@reddb-io/ui-
|
|
64
|
+
"@reddb-io/ui-kit": "0.2.0",
|
|
65
|
+
"@reddb-io/ui-mcp": "0.2.0"
|
|
64
66
|
},
|
|
65
67
|
"devDependencies": {
|
|
66
68
|
"@sveltejs/adapter-static": "^3.0.8",
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Appearance section (#75). Renders each registered skin as a live preview
|
|
3
|
+
// card — a mini-mockup painted with that skin's *own* derived tokens — and
|
|
4
|
+
// applies the skin on click. Selection persists across reloads via the skin
|
|
5
|
+
// store. Dark-only: there is no light/system switch here.
|
|
6
|
+
import { Check } from 'lucide-svelte'
|
|
7
|
+
import PageHeader from '$lib/PageHeader.svelte'
|
|
8
|
+
import { SKINS, skinToCssVars, type Skin } from '$lib/skins/skins'
|
|
9
|
+
import { skins } from '$lib/skins/skins.svelte'
|
|
10
|
+
|
|
11
|
+
// Each preview is painted with the skin's own custom properties so the card
|
|
12
|
+
// is a faithful, self-contained swatch of the palette — `color-mix` ramps
|
|
13
|
+
// resolve in the browser exactly as they would once applied.
|
|
14
|
+
function previewStyle(skin: Skin): string {
|
|
15
|
+
return Object.entries(skinToCssVars(skin))
|
|
16
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
17
|
+
.join('; ')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const selected = $derived(skins.current.name)
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<div class="h-full overflow-auto bg-bg-0 p-6 text-fg-1">
|
|
24
|
+
<PageHeader
|
|
25
|
+
eyebrow="Settings"
|
|
26
|
+
title="Appearance"
|
|
27
|
+
subtitle="Pick a skin. Surfaces are derived from a single seed; the accent stays surgical. Dark only."
|
|
28
|
+
/>
|
|
29
|
+
|
|
30
|
+
<div class="grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-4 max-w-4xl">
|
|
31
|
+
{#each SKINS as skin (skin.name)}
|
|
32
|
+
{@const active = skin.name === selected}
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
onclick={() => skins.select(skin.name)}
|
|
36
|
+
aria-pressed={active}
|
|
37
|
+
class="group text-left rounded-lg border bg-bg-1 p-3 transition-colors {active
|
|
38
|
+
? 'border-accent'
|
|
39
|
+
: 'border-line-2 hover:border-line-3'}"
|
|
40
|
+
>
|
|
41
|
+
<!-- Mini-mockup, painted in the skin's own tokens -->
|
|
42
|
+
<div
|
|
43
|
+
class="rounded-md overflow-hidden border border-line-1"
|
|
44
|
+
style={previewStyle(skin)}
|
|
45
|
+
>
|
|
46
|
+
<div class="bg-bg-0 p-3">
|
|
47
|
+
<!-- faux topbar -->
|
|
48
|
+
<div class="flex items-center gap-1.5 mb-2.5">
|
|
49
|
+
<span class="size-2 rounded-full" style="background: var(--color-accent)"></span>
|
|
50
|
+
<span class="h-1.5 w-12 rounded-full bg-bg-3"></span>
|
|
51
|
+
<span class="ml-auto h-1.5 w-6 rounded-full bg-bg-2"></span>
|
|
52
|
+
</div>
|
|
53
|
+
<!-- faux card with strips -->
|
|
54
|
+
<div class="rounded-md bg-bg-1 border border-line-2 p-2.5">
|
|
55
|
+
<div class="h-1.5 w-2/3 rounded-full bg-fg-2 mb-2"></div>
|
|
56
|
+
<div class="h-1.5 w-full rounded-full bg-bg-3 mb-1.5"></div>
|
|
57
|
+
<div class="h-1.5 w-4/5 rounded-full bg-bg-3 mb-2.5"></div>
|
|
58
|
+
<div class="flex items-center gap-1.5">
|
|
59
|
+
<span
|
|
60
|
+
class="h-4 w-12 rounded"
|
|
61
|
+
style="background: var(--color-accent)"
|
|
62
|
+
></span>
|
|
63
|
+
<span class="h-4 w-8 rounded bg-bg-2 border border-line-2"></span>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<!-- surface ramp strip: bg-0..bg-3 -->
|
|
68
|
+
<div class="flex h-2.5">
|
|
69
|
+
<span class="flex-1 bg-bg-0"></span>
|
|
70
|
+
<span class="flex-1 bg-bg-1"></span>
|
|
71
|
+
<span class="flex-1 bg-bg-2"></span>
|
|
72
|
+
<span class="flex-1 bg-bg-3"></span>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div class="flex items-center gap-2 mt-3">
|
|
77
|
+
<span class="type-h2 text-fg-0">{skin.label}</span>
|
|
78
|
+
{#if active}
|
|
79
|
+
<span
|
|
80
|
+
class="inline-flex items-center gap-1 rounded-full bg-accent-soft px-2 py-0.5 text-[10px] font-mono uppercase tracking-wider text-accent"
|
|
81
|
+
>
|
|
82
|
+
<Check class="size-3" /> Active
|
|
83
|
+
</span>
|
|
84
|
+
{/if}
|
|
85
|
+
</div>
|
|
86
|
+
<p class="type-caption m-0 mt-1">{skin.description}</p>
|
|
87
|
+
</button>
|
|
88
|
+
{/each}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
{ id: 'collections', group: 'navigate', label: 'Open collections', hint: '/collections', run: () => navigate({ view: 'collections' }) },
|
|
32
32
|
{ id: 'topology', group: 'navigate', label: 'Open cluster', hint: '/cluster', run: () => navigate({ view: 'cluster' }) },
|
|
33
33
|
{ id: 'security', group: 'navigate', label: 'Open security', hint: '/security', run: () => navigate({ view: 'security' }) },
|
|
34
|
+
{ id: 'settings', group: 'navigate', label: 'Open settings', hint: '/settings', run: () => navigate({ view: 'settings' }) },
|
|
35
|
+
{ id: 'appearance', group: 'navigate', label: 'Open appearance', hint: '/appearance', run: () => navigate({ view: 'appearance' }) },
|
|
34
36
|
|
|
35
37
|
{ id: 'new-query', group: 'data', label: 'New query', shortcut: '⌘T', run: () => { fire('red:new-query'); close() } },
|
|
36
38
|
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Settings view — a data-driven, permission-aware config inspector.
|
|
3
|
+
//
|
|
4
|
+
// The sections, their curated keys, and the label/description/enum copy all
|
|
5
|
+
// live in `settings-sections` as DATA. This view does three things: reads the
|
|
6
|
+
// *real* config from `connection.client` (no mocks/fixtures), flattens it to
|
|
7
|
+
// dotted keys, and renders each curated key through `resolveControl` as a
|
|
8
|
+
// `ListRow`. When the connection is unreachable it falls back to EmptyState.
|
|
9
|
+
//
|
|
10
|
+
// Permission-aware by default: a key whose source endpoint is denied or
|
|
11
|
+
// unsupported is not greyed out — its control is absent, replaced by a small
|
|
12
|
+
// chip naming the missing grant (with the server's reason in the tooltip).
|
|
13
|
+
import {
|
|
14
|
+
SplitView,
|
|
15
|
+
NavItem,
|
|
16
|
+
SectionHeading,
|
|
17
|
+
Pill,
|
|
18
|
+
ListRow,
|
|
19
|
+
Kbd,
|
|
20
|
+
} from '@reddb-io/ui-kit'
|
|
21
|
+
import {
|
|
22
|
+
Settings2,
|
|
23
|
+
Database,
|
|
24
|
+
Network,
|
|
25
|
+
Plug,
|
|
26
|
+
Shield,
|
|
27
|
+
Search,
|
|
28
|
+
RefreshCw,
|
|
29
|
+
Lock,
|
|
30
|
+
Unplug,
|
|
31
|
+
type Icon,
|
|
32
|
+
} from 'lucide-svelte'
|
|
33
|
+
import PageHeader from '$lib/PageHeader.svelte'
|
|
34
|
+
import EmptyState from '$lib/EmptyState.svelte'
|
|
35
|
+
import { connection } from '$lib/connections.svelte'
|
|
36
|
+
import { activity } from '$lib/activity.svelte'
|
|
37
|
+
import {
|
|
38
|
+
SECTIONS,
|
|
39
|
+
resolveSection,
|
|
40
|
+
resolveControl,
|
|
41
|
+
sourceForKey,
|
|
42
|
+
flattenConfig,
|
|
43
|
+
type ControlDescriptor,
|
|
44
|
+
type SettingsSource,
|
|
45
|
+
} from '$lib/settings-sections'
|
|
46
|
+
|
|
47
|
+
const icons: Record<string, typeof Icon> = {
|
|
48
|
+
settings: Settings2,
|
|
49
|
+
database: Database,
|
|
50
|
+
network: Network,
|
|
51
|
+
plug: Plug,
|
|
52
|
+
shield: Shield,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The grant a denied source is gated on — shown on the "absent" chip.
|
|
56
|
+
const GRANTS: Record<SettingsSource, string> = {
|
|
57
|
+
stats: 'stats:read',
|
|
58
|
+
cluster: 'cluster:status',
|
|
59
|
+
capabilities: 'capabilities',
|
|
60
|
+
session: 'auth:whoami',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let activeId = $state(SECTIONS[0].id)
|
|
64
|
+
const active = $derived(resolveSection(activeId))
|
|
65
|
+
|
|
66
|
+
// Flattened live config and, per source, the reason its grant is absent
|
|
67
|
+
// (null ⇒ available). Both are rebuilt atomically on every refresh.
|
|
68
|
+
let values = $state<Record<string, unknown>>({})
|
|
69
|
+
let denied = $state<Record<SettingsSource, string | null>>({
|
|
70
|
+
stats: null,
|
|
71
|
+
cluster: null,
|
|
72
|
+
capabilities: null,
|
|
73
|
+
session: null,
|
|
74
|
+
})
|
|
75
|
+
let loading = $state(false)
|
|
76
|
+
|
|
77
|
+
const connected = $derived(connection.connected)
|
|
78
|
+
const activeUrl = $derived(connection.active.url)
|
|
79
|
+
|
|
80
|
+
async function refresh() {
|
|
81
|
+
const client = connection.client
|
|
82
|
+
if (!client || !connected) {
|
|
83
|
+
values = {}
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
loading = true
|
|
87
|
+
const next: Record<string, unknown> = {}
|
|
88
|
+
const nextDenied: Record<SettingsSource, string | null> = {
|
|
89
|
+
stats: null,
|
|
90
|
+
cluster: null,
|
|
91
|
+
capabilities: null,
|
|
92
|
+
session: null,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// /capabilities is the negotiation itself — always available from the store.
|
|
96
|
+
Object.assign(next, flattenConfig({ capabilities: connection.capabilities }))
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const stats = await activity.track('settings · stats', () => client.stats())
|
|
100
|
+
Object.assign(next, flattenConfig(stats as unknown as Record<string, unknown>))
|
|
101
|
+
} catch (e) {
|
|
102
|
+
nextDenied.stats = (e as Error).message
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Cluster topology is capability-gated: if the server can't honour
|
|
106
|
+
// /cluster/status, those keys are absent + explained, never faked.
|
|
107
|
+
if (!connection.capabilities.clusterStatus) {
|
|
108
|
+
nextDenied.cluster = 'Server does not expose /cluster/status.'
|
|
109
|
+
} else {
|
|
110
|
+
try {
|
|
111
|
+
const cluster = await activity.track('settings · cluster', () =>
|
|
112
|
+
client.clusterStatus(),
|
|
113
|
+
)
|
|
114
|
+
Object.assign(next, flattenConfig(cluster as unknown as Record<string, unknown>))
|
|
115
|
+
} catch (e) {
|
|
116
|
+
nextDenied.cluster = (e as Error).message
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const session = await activity.track('settings · whoami', () => client.whoami())
|
|
122
|
+
Object.assign(
|
|
123
|
+
next,
|
|
124
|
+
flattenConfig({ session: session as unknown as Record<string, unknown> }),
|
|
125
|
+
)
|
|
126
|
+
} catch (e) {
|
|
127
|
+
nextDenied.session = (e as Error).message
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
values = next
|
|
131
|
+
denied = nextDenied
|
|
132
|
+
loading = false
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
$effect(() => {
|
|
136
|
+
void activeUrl
|
|
137
|
+
void connected
|
|
138
|
+
refresh()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
type RenderRow =
|
|
142
|
+
| { type: 'value'; key: string; label: string; control: ControlDescriptor; value: unknown }
|
|
143
|
+
| { type: 'denied'; key: string; label: string; reason: string; grant: string }
|
|
144
|
+
|
|
145
|
+
// Resolve the active section's curated keys to render rows. A key whose
|
|
146
|
+
// source is denied becomes a "denied" row (absent control + explanation); a
|
|
147
|
+
// key with a live value becomes a "value" row; a key the server simply does
|
|
148
|
+
// not report is dropped (graceful degradation — no empty placeholder).
|
|
149
|
+
const rows = $derived.by<RenderRow[]>(() => {
|
|
150
|
+
const out: RenderRow[] = []
|
|
151
|
+
for (const key of active.keys) {
|
|
152
|
+
const src = sourceForKey(key)
|
|
153
|
+
const reason = denied[src]
|
|
154
|
+
const label = resolveControl(key).label
|
|
155
|
+
if (reason) {
|
|
156
|
+
out.push({ type: 'denied', key, label, reason, grant: GRANTS[src] })
|
|
157
|
+
} else if (key in values) {
|
|
158
|
+
out.push({
|
|
159
|
+
type: 'value',
|
|
160
|
+
key,
|
|
161
|
+
label,
|
|
162
|
+
control: resolveControl(key, values[key]),
|
|
163
|
+
value: values[key],
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return out
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
function formatBytes(n: number): string {
|
|
171
|
+
if (!Number.isFinite(n)) return String(n)
|
|
172
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
173
|
+
let v = n
|
|
174
|
+
let i = 0
|
|
175
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
176
|
+
v /= 1024
|
|
177
|
+
i++
|
|
178
|
+
}
|
|
179
|
+
return `${i === 0 ? v : v.toFixed(1)} ${units[i]}`
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function formatValue(control: ControlDescriptor, value: unknown): string {
|
|
183
|
+
if (control.kind === 'number' && control.key.endsWith('_bytes')) {
|
|
184
|
+
return formatBytes(Number(value))
|
|
185
|
+
}
|
|
186
|
+
return String(value)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Reuse the global command palette as the search surface — the same ⌘K idiom
|
|
190
|
+
// the topbar uses, so Esc-to-close comes for free from the palette overlay.
|
|
191
|
+
function openPalette() {
|
|
192
|
+
window.dispatchEvent(
|
|
193
|
+
new KeyboardEvent('keydown', { key: 'k', metaKey: true, ctrlKey: true, bubbles: true }),
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
</script>
|
|
197
|
+
|
|
198
|
+
{#if !connected}
|
|
199
|
+
<div class="h-full overflow-auto bg-bg-0 p-6">
|
|
200
|
+
<PageHeader
|
|
201
|
+
eyebrow="Settings"
|
|
202
|
+
title="Settings"
|
|
203
|
+
subtitle="Live configuration for the active reddb connection."
|
|
204
|
+
/>
|
|
205
|
+
<EmptyState
|
|
206
|
+
icon={Unplug}
|
|
207
|
+
title="No active connection"
|
|
208
|
+
message="Connect to a reddb instance to inspect its live configuration."
|
|
209
|
+
hint="red serve"
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
{:else}
|
|
213
|
+
<SplitView searchShortcut={null}>
|
|
214
|
+
{#snippet search()}
|
|
215
|
+
<button
|
|
216
|
+
type="button"
|
|
217
|
+
onclick={openPalette}
|
|
218
|
+
class="flex h-7 w-full items-center gap-2 rounded-md border border-line-2 bg-bg-2 px-2.5 text-xs text-fg-2 transition-colors hover:border-line-3 hover:text-fg-1"
|
|
219
|
+
>
|
|
220
|
+
<Search class="size-3.5 text-fg-3" />
|
|
221
|
+
<span class="flex-1 text-left">Search settings</span>
|
|
222
|
+
<span class="inline-flex gap-0.5"><Kbd>⌘</Kbd><Kbd>K</Kbd></span>
|
|
223
|
+
</button>
|
|
224
|
+
{/snippet}
|
|
225
|
+
|
|
226
|
+
{#snippet nav()}
|
|
227
|
+
<nav class="grid gap-0.5 p-2">
|
|
228
|
+
{#each SECTIONS as section (section.id)}
|
|
229
|
+
{@const SectionIcon = icons[section.icon] ?? Settings2}
|
|
230
|
+
<NavItem
|
|
231
|
+
label={section.label}
|
|
232
|
+
active={section.id === active.id}
|
|
233
|
+
onclick={() => (activeId = section.id)}
|
|
234
|
+
>
|
|
235
|
+
{#snippet icon()}<SectionIcon class="size-3.5" />{/snippet}
|
|
236
|
+
</NavItem>
|
|
237
|
+
{/each}
|
|
238
|
+
</nav>
|
|
239
|
+
{/snippet}
|
|
240
|
+
|
|
241
|
+
{#snippet footer()}
|
|
242
|
+
<div class="px-3 py-2.5 text-[11px] text-fg-3">
|
|
243
|
+
Live from <span class="font-mono text-fg-2">{activeUrl}</span>.
|
|
244
|
+
</div>
|
|
245
|
+
{/snippet}
|
|
246
|
+
|
|
247
|
+
{#snippet content()}
|
|
248
|
+
{@const SectionIcon = icons[active.icon] ?? Settings2}
|
|
249
|
+
<div class="p-6">
|
|
250
|
+
<PageHeader eyebrow="Settings" title={active.label} subtitle={active.blurb}>
|
|
251
|
+
{#snippet actions()}
|
|
252
|
+
<button
|
|
253
|
+
type="button"
|
|
254
|
+
onclick={refresh}
|
|
255
|
+
disabled={loading}
|
|
256
|
+
class="inline-flex h-7 items-center gap-1.5 rounded-md border border-line-2 bg-bg-2 px-2.5 text-[11px] font-mono text-fg-1 hover:border-line-3 hover:text-fg-0 disabled:cursor-not-allowed disabled:opacity-40"
|
|
257
|
+
>
|
|
258
|
+
<RefreshCw class={loading ? 'size-3.5 animate-spin' : 'size-3.5'} />
|
|
259
|
+
refresh
|
|
260
|
+
</button>
|
|
261
|
+
{/snippet}
|
|
262
|
+
</PageHeader>
|
|
263
|
+
|
|
264
|
+
<section class="mb-6">
|
|
265
|
+
<SectionHeading title={active.label} class="mb-2">
|
|
266
|
+
{#snippet icon()}<SectionIcon class="size-3.5" />{/snippet}
|
|
267
|
+
{#snippet meta()}<Pill>{rows.length}</Pill>{/snippet}
|
|
268
|
+
</SectionHeading>
|
|
269
|
+
|
|
270
|
+
{#if rows.length === 0}
|
|
271
|
+
<div
|
|
272
|
+
class="rounded-lg border border-dashed border-line-1 bg-bg-1 px-3 py-6 text-center text-[12px] text-fg-3"
|
|
273
|
+
>
|
|
274
|
+
This server reports nothing for these keys.
|
|
275
|
+
</div>
|
|
276
|
+
{:else}
|
|
277
|
+
<div
|
|
278
|
+
class="divide-y divide-line-1 overflow-hidden rounded-lg border border-line-1 bg-bg-1"
|
|
279
|
+
>
|
|
280
|
+
{#each rows as row (row.key)}
|
|
281
|
+
{#if row.type === 'denied'}
|
|
282
|
+
<ListRow title={row.label} hint={row.key}>
|
|
283
|
+
{#snippet action()}
|
|
284
|
+
<span
|
|
285
|
+
class="inline-flex items-center gap-1.5 rounded-md border border-line-2 bg-bg-2 px-2 py-1 text-[11px] font-mono text-fg-3"
|
|
286
|
+
title={row.reason}
|
|
287
|
+
>
|
|
288
|
+
<Lock class="size-3" />
|
|
289
|
+
requires {row.grant}
|
|
290
|
+
</span>
|
|
291
|
+
{/snippet}
|
|
292
|
+
</ListRow>
|
|
293
|
+
{:else}
|
|
294
|
+
<ListRow
|
|
295
|
+
title={row.label}
|
|
296
|
+
description={row.control.description}
|
|
297
|
+
hint={row.key}
|
|
298
|
+
>
|
|
299
|
+
{#snippet action()}
|
|
300
|
+
{#if row.control.kind === 'boolean'}
|
|
301
|
+
<Pill tone={row.value ? 'accent' : 'muted'}>{row.value ? 'on' : 'off'}</Pill>
|
|
302
|
+
{:else if row.control.kind === 'enum'}
|
|
303
|
+
<span
|
|
304
|
+
class="font-mono text-[12px] text-accent"
|
|
305
|
+
title={`Options: ${row.control.options?.join(', ')}`}
|
|
306
|
+
>{row.value}</span>
|
|
307
|
+
{:else}
|
|
308
|
+
<span class="font-mono text-[12px] text-fg-1"
|
|
309
|
+
>{formatValue(row.control, row.value)}</span
|
|
310
|
+
>
|
|
311
|
+
{/if}
|
|
312
|
+
{/snippet}
|
|
313
|
+
</ListRow>
|
|
314
|
+
{/if}
|
|
315
|
+
{/each}
|
|
316
|
+
</div>
|
|
317
|
+
{/if}
|
|
318
|
+
</section>
|
|
319
|
+
</div>
|
|
320
|
+
{/snippet}
|
|
321
|
+
</SplitView>
|
|
322
|
+
{/if}
|
package/src/lib/Topbar.svelte
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Kbd } from '@reddb-io/ui-kit'
|
|
3
3
|
import ConnectDropdown from './ConnectDropdown.svelte'
|
|
4
4
|
import ActivityIndicator from './ActivityIndicator.svelte'
|
|
5
|
-
import { Search, Sun, Moon, Network, Database, FileCode2, Shield } from 'lucide-svelte'
|
|
5
|
+
import { Search, Sun, Moon, Network, Database, FileCode2, Shield, Settings2 } from 'lucide-svelte'
|
|
6
6
|
import { theme } from './theme.svelte'
|
|
7
7
|
import { useRouter } from './router.svelte'
|
|
8
8
|
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
const onCollections = $derived(router.view === 'collections')
|
|
19
19
|
const onCluster = $derived(router.view === 'cluster')
|
|
20
20
|
const onSecurity = $derived(router.view === 'security')
|
|
21
|
+
const onSettings = $derived(router.view === 'settings')
|
|
21
22
|
</script>
|
|
22
23
|
|
|
23
24
|
<header class="h-11 flex items-center justify-between px-3.5 bg-bg-1/80 backdrop-blur-md border-b border-line-1 relative z-10">
|
|
@@ -89,6 +90,19 @@
|
|
|
89
90
|
<Shield class="size-3.5" />
|
|
90
91
|
<span>Security</span>
|
|
91
92
|
</a>
|
|
93
|
+
<a
|
|
94
|
+
href={router.href({ view: 'settings' })}
|
|
95
|
+
onclick={(e) => router.go({ view: 'settings' }, e)}
|
|
96
|
+
title="Settings"
|
|
97
|
+
aria-current={onSettings ? 'page' : undefined}
|
|
98
|
+
class={[
|
|
99
|
+
'inline-flex items-center gap-1.5 h-7 px-2 rounded-md text-xs no-underline transition-colors',
|
|
100
|
+
onSettings ? 'bg-bg-2 text-fg-0' : 'text-fg-2 hover:text-fg-0 hover:bg-bg-2',
|
|
101
|
+
].join(' ')}
|
|
102
|
+
>
|
|
103
|
+
<Settings2 class="size-3.5" />
|
|
104
|
+
<span>Settings</span>
|
|
105
|
+
</a>
|
|
92
106
|
</nav>
|
|
93
107
|
</div>
|
|
94
108
|
|
package/src/lib/Workspace.svelte
CHANGED
|
@@ -15,9 +15,12 @@
|
|
|
15
15
|
import CollectionsView from './CollectionsView.svelte'
|
|
16
16
|
import ClusterView from './ClusterView.svelte'
|
|
17
17
|
import SecurityView from './SecurityView.svelte'
|
|
18
|
+
import SettingsView from './SettingsView.svelte'
|
|
19
|
+
import Appearance from './Appearance.svelte'
|
|
18
20
|
import { connection, setConnectionProvider } from './connections.svelte'
|
|
19
21
|
import { runQueryPreferStream, type ConnectionProvider } from '#reddb'
|
|
20
22
|
import { theme, type Theme } from './theme.svelte'
|
|
23
|
+
import { skins } from './skins/skins.svelte'
|
|
21
24
|
import { secureStore } from './secureStore.svelte'
|
|
22
25
|
import { pendingChanges, buildUpdateSql, type CommitOutcome } from './pending-changes.svelte'
|
|
23
26
|
import { queryTabs } from './query-tabs.svelte'
|
|
@@ -63,7 +66,7 @@
|
|
|
63
66
|
|
|
64
67
|
function navigateToBootRoute(route: string) {
|
|
65
68
|
const legacyView = route as View
|
|
66
|
-
if (legacyView === 'query' || legacyView === 'collections' || legacyView === 'cluster' || legacyView === 'security') {
|
|
69
|
+
if (legacyView === 'query' || legacyView === 'collections' || legacyView === 'cluster' || legacyView === 'security' || legacyView === 'settings' || legacyView === 'appearance') {
|
|
67
70
|
router.go({ view: legacyView }, undefined, true)
|
|
68
71
|
return
|
|
69
72
|
}
|
|
@@ -133,6 +136,12 @@
|
|
|
133
136
|
// svelte-ignore state_referenced_locally
|
|
134
137
|
theme.init({ target: themeTarget ?? null, persist: persistTheme, initial: initialTheme })
|
|
135
138
|
|
|
139
|
+
// Skin-as-data (#75): apply the persisted skin's seed-derived custom
|
|
140
|
+
// properties over the active theme. Scoped to the same target the theme
|
|
141
|
+
// uses, so an embedded host's shadow root is skinned, not the document.
|
|
142
|
+
// svelte-ignore state_referenced_locally
|
|
143
|
+
skins.init(themeTarget ?? null)
|
|
144
|
+
|
|
136
145
|
// Hard lock gate: while the secure store is locked, no automatic network
|
|
137
146
|
// traffic should fire — probing the configured URL would leak its existence
|
|
138
147
|
// and shape. Additionally (#21, acceptance #4) no traffic fires until a real
|
|
@@ -235,6 +244,10 @@
|
|
|
235
244
|
<ClusterView />
|
|
236
245
|
{:else if router.view === 'security'}
|
|
237
246
|
<SecurityView />
|
|
247
|
+
{:else if router.view === 'settings'}
|
|
248
|
+
<SettingsView />
|
|
249
|
+
{:else if router.view === 'appearance'}
|
|
250
|
+
<Appearance />
|
|
238
251
|
{:else}
|
|
239
252
|
<CollectionsView />
|
|
240
253
|
{/if}
|
package/src/lib/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { default as QueryEditor } from "./QueryEditor.svelte";
|
|
|
15
15
|
export { default as ResultsPane } from "./ResultsPane.svelte";
|
|
16
16
|
export { default as SchemaTree } from "./SchemaTree.svelte";
|
|
17
17
|
export { default as SecurityView } from "./SecurityView.svelte";
|
|
18
|
+
export { default as SettingsView } from "./SettingsView.svelte";
|
|
18
19
|
export { default as ShortcutOverlay } from "./ShortcutOverlay.svelte";
|
|
19
20
|
export { default as Splash } from "./Splash.svelte";
|
|
20
21
|
export { default as StatusBar } from "./StatusBar.svelte";
|
|
@@ -33,6 +34,7 @@ export type { Capability as CollectionCapability } from "./capability";
|
|
|
33
34
|
export * from "./collection-catalog";
|
|
34
35
|
export * from "./collection-pages";
|
|
35
36
|
export * from "./router.svelte";
|
|
37
|
+
export * from "./settings-sections";
|
|
36
38
|
export * from "./cn";
|
|
37
39
|
export * from "./connections.svelte";
|
|
38
40
|
export * from "./shortcuts";
|