diff --git a/src/components/ui/sheet/Sheet.vue b/src/components/ui/sheet/Sheet.vue
new file mode 100644
index 000000000..ecdbffe0c
--- /dev/null
+++ b/src/components/ui/sheet/Sheet.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/sheet/SheetClose.vue b/src/components/ui/sheet/SheetClose.vue
new file mode 100644
index 000000000..cc1574196
--- /dev/null
+++ b/src/components/ui/sheet/SheetClose.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/sheet/SheetContent.vue b/src/components/ui/sheet/SheetContent.vue
new file mode 100644
index 000000000..b581b2d91
--- /dev/null
+++ b/src/components/ui/sheet/SheetContent.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/ui/sheet/SheetDescription.vue b/src/components/ui/sheet/SheetDescription.vue
new file mode 100644
index 000000000..9a8eb2f24
--- /dev/null
+++ b/src/components/ui/sheet/SheetDescription.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/sheet/SheetFooter.vue b/src/components/ui/sheet/SheetFooter.vue
new file mode 100644
index 000000000..7ac7948ab
--- /dev/null
+++ b/src/components/ui/sheet/SheetFooter.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/sheet/SheetHeader.vue b/src/components/ui/sheet/SheetHeader.vue
new file mode 100644
index 000000000..bad371dd5
--- /dev/null
+++ b/src/components/ui/sheet/SheetHeader.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/sheet/SheetTitle.vue b/src/components/ui/sheet/SheetTitle.vue
new file mode 100644
index 000000000..ce9aece81
--- /dev/null
+++ b/src/components/ui/sheet/SheetTitle.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/sheet/SheetTrigger.vue b/src/components/ui/sheet/SheetTrigger.vue
new file mode 100644
index 000000000..a57258ef7
--- /dev/null
+++ b/src/components/ui/sheet/SheetTrigger.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/sheet/index.ts b/src/components/ui/sheet/index.ts
new file mode 100644
index 000000000..b94a071f2
--- /dev/null
+++ b/src/components/ui/sheet/index.ts
@@ -0,0 +1,8 @@
+export { default as Sheet } from "./Sheet.vue";
+export { default as SheetClose } from "./SheetClose.vue";
+export { default as SheetContent } from "./SheetContent.vue";
+export { default as SheetDescription } from "./SheetDescription.vue";
+export { default as SheetFooter } from "./SheetFooter.vue";
+export { default as SheetHeader } from "./SheetHeader.vue";
+export { default as SheetTitle } from "./SheetTitle.vue";
+export { default as SheetTrigger } from "./SheetTrigger.vue";
diff --git a/src/lib/api.ts b/src/lib/api.ts
index 89d2ee42c..dd1b91369 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -111,6 +111,7 @@ export const redisDeleteKey = forward("redisDeleteKey");
export const redisHashSet = forward("redisHashSet");
export const redisHashDel = forward("redisHashDel");
export const redisListPush = forward("redisListPush");
+export const redisListSet = forward("redisListSet");
export const redisListRemove = forward("redisListRemove");
export const redisSetAdd = forward("redisSetAdd");
export const redisSetRemove = forward("redisSetRemove");
diff --git a/src/lib/http.ts b/src/lib/http.ts
index fa9212076..cc1ca579c 100644
--- a/src/lib/http.ts
+++ b/src/lib/http.ts
@@ -597,6 +597,16 @@ export async function redisListPush(connectionId: string, db: number, keyRaw: st
return post("/api/redis/list-push", { connectionId, db, keyRaw, value });
}
+export async function redisListSet(
+ connectionId: string,
+ db: number,
+ keyRaw: string,
+ index: number,
+ value: string,
+): Promise {
+ return post("/api/redis/list-set", { connectionId, db, keyRaw, index, value });
+}
+
export async function redisListRemove(connectionId: string, db: number, keyRaw: string, index: number): Promise {
return post("/api/redis/list-remove", { connectionId, db, keyRaw, index });
}
diff --git a/src/lib/redisValuePresentation.ts b/src/lib/redisValuePresentation.ts
index fdcad551b..d51174c77 100644
--- a/src/lib/redisValuePresentation.ts
+++ b/src/lib/redisValuePresentation.ts
@@ -5,6 +5,23 @@ export interface RedisMemberDetail {
format: RedisMemberDetailFormat;
}
+export type RedisMemberDetailKind = "list" | "set" | "hash" | "zset" | "stream";
+
+export const REDIS_MEMBER_DETAIL_SHEET_MIN_WIDTH = 360;
+export const REDIS_MEMBER_DETAIL_SHEET_MAX_WIDTH = 900;
+
+export function canEditRedisMemberDetail(kind: RedisMemberDetailKind): boolean {
+ return kind !== "stream";
+}
+
+export function clampRedisMemberDetailSheetWidth(width: number, viewportWidth: number): number {
+ const viewportMax = Math.max(REDIS_MEMBER_DETAIL_SHEET_MIN_WIDTH, viewportWidth - 32);
+ return Math.min(
+ Math.min(REDIS_MEMBER_DETAIL_SHEET_MAX_WIDTH, viewportMax),
+ Math.max(REDIS_MEMBER_DETAIL_SHEET_MIN_WIDTH, width),
+ );
+}
+
export function formatRedisMemberDetail(value: unknown): RedisMemberDetail {
if (typeof value === "string") {
const trimmed = value.trim();
@@ -24,3 +41,22 @@ export function formatRedisMemberDetail(value: unknown): RedisMemberDetail {
return { text: String(value), format: "text" };
}
}
+
+export function getRedisMemberSelectionKey(title: string, value: unknown): string {
+ return `${title}\n${formatRedisMemberDetail(value).text}`;
+}
+
+export function highlightRedisJsonDetail(json: string): string {
+ const escaped = json.replace(/&/g, "&").replace(//g, ">");
+
+ return escaped.replace(
+ /("(?:\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g,
+ (match) => {
+ let cls = "json-number";
+ if (match.startsWith('"')) cls = match.endsWith(":") ? "json-key" : "json-string";
+ else if (match === "true" || match === "false") cls = "json-boolean";
+ else if (match === "null") cls = "json-null";
+ return `${match}`;
+ },
+ );
+}
diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts
index 370e75324..a066ed94a 100644
--- a/src/lib/tauri.ts
+++ b/src/lib/tauri.ts
@@ -429,6 +429,16 @@ export async function redisListPush(connectionId: string, db: number, keyRaw: st
return invoke("redis_list_push", { connectionId, db, keyRaw, value });
}
+export async function redisListSet(
+ connectionId: string,
+ db: number,
+ keyRaw: string,
+ index: number,
+ value: string,
+): Promise {
+ return invoke("redis_list_set", { connectionId, db, keyRaw, index, value });
+}
+
export async function redisListRemove(connectionId: string, db: number, keyRaw: string, index: number): Promise {
return invoke("redis_list_remove", { connectionId, db, keyRaw, index });
}
diff --git a/tests/redisValuePresentation.test.ts b/tests/redisValuePresentation.test.ts
index 54c4cd928..92fbadb81 100644
--- a/tests/redisValuePresentation.test.ts
+++ b/tests/redisValuePresentation.test.ts
@@ -1,6 +1,12 @@
import { strict as assert } from "node:assert";
import test from "node:test";
-import { formatRedisMemberDetail } from "../src/lib/redisValuePresentation.ts";
+import {
+ canEditRedisMemberDetail,
+ clampRedisMemberDetailSheetWidth,
+ formatRedisMemberDetail,
+ getRedisMemberSelectionKey,
+ highlightRedisJsonDetail,
+} from "../src/lib/redisValuePresentation.ts";
test("formats JSON object strings for Redis member details", () => {
const detail = formatRedisMemberDetail('{"id":1,"name":"Ada","tags":["dbx","redis"]}');
@@ -22,3 +28,35 @@ test("formats non-string Redis member values as JSON", () => {
assert.equal(detail.format, "json");
assert.equal(detail.text, '{\n "field": "name",\n "value": "{\\"nested\\":true}"\n}');
});
+
+test("builds stable Redis member selection keys from title and formatted value", () => {
+ const key = getRedisMemberSelectionKey("#2", '{"id":240,"kind":"json"}');
+
+ assert.equal(key, '#2\n{\n "id": 240,\n "kind": "json"\n}');
+});
+
+test("highlights formatted Redis JSON detail safely", () => {
+ const html = highlightRedisJsonDetail('{"id":240,"name":"