biblius 1 тиждень тому
батько
коміт
63aa152848

+ 10 - 0
package-lock.json

@@ -16,6 +16,7 @@
         "@replit/codemirror-vim": "^6.3.0",
         "@tailwindcss/vite": "^4.1.17",
         "@tauri-apps/api": "^2",
+        "@tauri-apps/plugin-fs": "^2.4.5",
         "@tauri-apps/plugin-global-shortcut": "^2.3.1",
         "@tauri-apps/plugin-log": "^2.7.1",
         "@tauri-apps/plugin-opener": "^2",
@@ -1711,6 +1712,15 @@
         "node": ">= 10"
       }
     },
+    "node_modules/@tauri-apps/plugin-fs": {
+      "version": "2.4.5",
+      "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.4.5.tgz",
+      "integrity": "sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA==",
+      "license": "MIT OR Apache-2.0",
+      "dependencies": {
+        "@tauri-apps/api": "^2.8.0"
+      }
+    },
     "node_modules/@tauri-apps/plugin-global-shortcut": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-global-shortcut/-/plugin-global-shortcut-2.3.1.tgz",

+ 1 - 0
package.json

@@ -20,6 +20,7 @@
     "@replit/codemirror-vim": "^6.3.0",
     "@tailwindcss/vite": "^4.1.17",
     "@tauri-apps/api": "^2",
+    "@tauri-apps/plugin-fs": "^2.4.5",
     "@tauri-apps/plugin-global-shortcut": "^2.3.1",
     "@tauri-apps/plugin-log": "^2.7.1",
     "@tauri-apps/plugin-opener": "^2",

+ 23 - 0
src-tauri/Cargo.lock

@@ -3676,6 +3676,7 @@ dependencies = [
  "sqlx",
  "tauri",
  "tauri-build",
+ "tauri-plugin-fs",
  "tauri-plugin-global-shortcut",
  "tauri-plugin-log",
  "tauri-plugin-opener",
@@ -4781,6 +4782,28 @@ dependencies = [
  "walkdir",
 ]
 
+[[package]]
+name = "tauri-plugin-fs"
+version = "2.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804"
+dependencies = [
+ "anyhow",
+ "dunce",
+ "glob",
+ "percent-encoding",
+ "schemars 0.8.22",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "tauri",
+ "tauri-plugin",
+ "tauri-utils",
+ "thiserror 2.0.17",
+ "toml 0.9.8",
+ "url",
+]
+
 [[package]]
 name = "tauri-plugin-global-shortcut"
 version = "2.3.1"

+ 1 - 0
src-tauri/Cargo.toml

@@ -44,6 +44,7 @@ reqwest = { version = "0.12.15", features = [
 tauri-plugin-log = "2"
 tauri-plugin-store = "2"
 base64 = "0.22.1"
+tauri-plugin-fs = "2"
 
 [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
 tauri-plugin-global-shortcut = "2"

+ 2 - 2
src-tauri/src/db.rs

@@ -189,7 +189,7 @@ pub async fn insert_request_body(
     entry_id: i64,
     body: RequestBody,
 ) -> AppResult<EntryRequestBody> {
-    Ok(sqlx::query_as!(EntryRequestBody, r#"INSERT INTO request_bodies(request_id, content_type, body) VALUES (?, ?, ?) RETURNING id, content_type AS "content_type: _", body"#, entry_id, body.ty, body.content).fetch_one(&db).await?)
+    Ok(sqlx::query_as!(EntryRequestBody, r#"INSERT INTO request_bodies(request_id, content_type, body) VALUES (?, ?, ?) RETURNING id, content_type AS "content_type: _", body"#, entry_id, body.ty, body.path).fetch_one(&db).await?)
 }
 
 pub async fn update_request_body(
@@ -202,7 +202,7 @@ pub async fn update_request_body(
             sqlx::query!(
                 "UPDATE request_bodies SET content_type = ?, body = ? WHERE id = ?",
                 body.ty,
-                body.content,
+                body.path,
                 id,
             )
             .execute(&db)

+ 3 - 0
src-tauri/src/error.rs

@@ -9,6 +9,9 @@ pub enum AppError {
     #[error("{0}")]
     MimeFromStr(#[from] mime::FromStrError),
 
+    #[error("{0}")]
+    IO(#[from] std::io::Error),
+
     #[error("{0}")]
     SerdeJson(#[from] serde_json::Error),
     #[error("{0}")]

+ 1 - 0
src-tauri/src/lib.rs

@@ -17,6 +17,7 @@ mod workspace;
 #[cfg_attr(mobile, tauri::mobile_entry_point)]
 pub fn run() {
     tauri::Builder::default()
+        .plugin(tauri_plugin_fs::init())
         // .plugin(tauri_plugin_global_shortcut::Builder::new().build())
         .plugin(tauri_plugin_store::Builder::new().build())
         .plugin(

+ 32 - 8
src-tauri/src/request.rs

@@ -45,20 +45,20 @@ pub async fn send(client: reqwest::Client, req: HttpRequestParameters) -> AppRes
                 ContentType::Text => insert_ct_if_missing(&mut headers, "text/plain"),
                 ContentType::Json => {
                     insert_ct_if_missing(&mut headers, "application/json");
-                    serde_json::from_str::<serde_json::Value>(&body.content)?;
+                    serde_json::from_str::<serde_json::Value>(&body.path)?;
                 }
                 ContentType::Xml => {
                     insert_ct_if_missing(&mut headers, "application/xml");
-                    roxmltree::Document::parse(&body.content)?;
+                    roxmltree::Document::parse(&body.path)?;
                 }
                 ContentType::FormUrlEncoded => {
-                    serde_urlencoded::from_str::<Vec<(String, String)>>(&body.content)
+                    serde_urlencoded::from_str::<Vec<(&str, &str)>>(&body.path)
                         .map_err(|e| AppError::SerdeUrl(e.to_string()))?;
                 }
                 // Handled by reqwest
                 ContentType::FormData => {}
             };
-            Some(Body::from(body.content))
+            Some(Body::from(body.path))
         }
         None => None,
     };
@@ -66,10 +66,10 @@ pub async fn send(client: reqwest::Client, req: HttpRequestParameters) -> AppRes
     let mut req = client.request(method, url).headers(headers);
 
     if let Some(body) = body {
-        req = req.body(body)
+        req = req.body(body);
     }
 
-    let req = req.build().unwrap();
+    let req = req.build()?;
 
     dbg!(&req);
 
@@ -95,6 +95,30 @@ pub async fn send(client: reqwest::Client, req: HttpRequestParameters) -> AppRes
     Ok(res)
 }
 
+/// Load the request body into a vec, validating it beforehand to ensure no syntactic errors are
+/// present in bodies that need valid syntax.
+pub async fn get_valid_request_body(path: &str, ty: ContentType) -> AppResult<String> {
+    let body = tokio::fs::read_to_string(path).await?;
+
+    match ty {
+        ContentType::Text => {}
+        ContentType::Json => {
+            serde_json::from_str::<serde_json::Value>(&body)?;
+        }
+        ContentType::Xml => {
+            roxmltree::Document::parse(&body)?;
+        }
+        ContentType::FormUrlEncoded => {
+            serde_urlencoded::from_str::<Vec<(&str, &str)>>(&body)
+                .map_err(|e| AppError::SerdeUrl(e.to_string()))?;
+        }
+        // Handled by reqwest
+        ContentType::FormData => {}
+    };
+
+    Ok(body)
+}
+
 #[derive(Debug, Serialize)]
 pub struct WorkspaceRequest {
     /// Workspace entry representing this request.
@@ -234,7 +258,7 @@ impl TryFrom<WorkspaceRequest> for HttpRequestParameters {
             method,
             headers,
             body: value.body.map(|body| RequestBody {
-                content: body.body,
+                path: body.body,
                 ty: body.content_type,
             }),
         })
@@ -365,7 +389,7 @@ pub struct RequestHeaderUpdate {
 
 #[derive(Debug, Serialize, Deserialize)]
 pub struct RequestBody {
-    pub content: String,
+    pub path: String,
     pub ty: ContentType,
 }
 

+ 146 - 0
src/lib/components/Header.svelte

@@ -0,0 +1,146 @@
+<script lang="ts">
+  import * as Select from "$lib/components/ui/select";
+  import { Button } from "$lib/components/ui/button";
+  import {
+    state as _state,
+    createEnvironment,
+    selectEnvironment,
+  } from "$lib/state.svelte";
+  import {
+    loadWorkspace,
+    createWorkspace,
+    selectWorkspace,
+    selectEntry,
+  } from "$lib/state.svelte";
+  import { Input } from "$lib/components/ui/input";
+  import type { Workspace } from "$lib/types";
+
+  let { workspaces = $bindable() }: { workspaces: Workspace[] } = $props();
+
+  let addWorkspaceInput = $state("");
+  let addEnvInput = $state("");
+
+  const referenceChain = $derived.by(() => {
+    const parents = [];
+
+    let parent = _state.entry?.parent_id;
+
+    while (parent != null) {
+      parents.push(_state.indexes[parent]);
+      parent = _state.indexes[parent].parent_id;
+    }
+
+    return parents.reverse();
+  });
+</script>
+
+<header class="flex items-center w-full border-b pb-4 gap-3 h-8 text">
+  <div class="flex items-center text-sm">
+    <p class="pr-2 opacity-75">Workspace</p>
+    <!-- Workspace dropdown -->
+    <Select.Root type="single" value={_state.workspace?.id.toString()}>
+      <Select.Trigger size="sm">
+        {_state.workspace?.name || "Select workspace"}
+      </Select.Trigger>
+
+      <Select.Content align="start">
+        <Select.Label>New workspace</Select.Label>
+        <form
+          class="flex w-full max-w-sm items-center gap-2 p-1"
+          onsubmit={(e) => {
+            e.preventDefault();
+            if (addWorkspaceInput.length > 0) {
+              createWorkspace(addWorkspaceInput).then(selectWorkspace);
+              addWorkspaceInput = "";
+            }
+          }}
+        >
+          <Input
+            type="text"
+            placeholder="My workspace"
+            bind:value={addWorkspaceInput}
+          />
+          <Button type="submit" variant="outline">+</Button>
+        </form>
+
+        <Select.Separator />
+
+        {#each workspaces as ws}
+          <Select.Item
+            onclick={() => {
+              loadWorkspace(ws);
+            }}
+            value={ws.id.toString()}>{ws.name}</Select.Item
+          >
+        {/each}
+      </Select.Content>
+    </Select.Root>
+  </div>
+
+  <!-- ABSOLUTE PATH TO ENTRY -->
+
+  {#if _state.entry != null}
+    <div class="h-8 flex mx-auto items-center opacity-75">
+      {#each referenceChain as ref}
+        <Button
+          class="p-0 h-fit cursor-pointer text-sm"
+          onclick={() => selectEntry(ref.id)}
+          variant="ghost"
+        >
+          {ref.name || ref.type + "(" + ref.id + ")"}
+        </Button>
+        <p class="pl-1 pr-1">/</p>
+      {/each}
+      <Button
+        class="p-0 h-fit cursor-pointer text-sm"
+        onclick={() => selectEntry(_state.entry!!.id)}
+        variant="ghost"
+      >
+        {_state.entry.name || _state.entry.type + "(" + _state.entry.id + ")"}
+      </Button>
+    </div>
+  {/if}
+
+  <div class="flex items-center text-sm">
+    <p class="text-center mr-2 opacity-75">Environment</p>
+    <Select.Root type="single" value={_state.environment?.id.toString() ?? "-"}>
+      <Select.Trigger size="sm">
+        {_state.environment?.name || "-"}
+      </Select.Trigger>
+
+      <Select.Content align="end">
+        <Select.Label>New environment</Select.Label>
+        <form
+          class="flex w-full max-w-sm items-center gap-2 p-1"
+          onsubmit={(e) => {
+            e.preventDefault();
+            if (addEnvInput.length > 0 && _state.workspace) {
+              createEnvironment(_state.workspace.id, addEnvInput);
+              addEnvInput = "";
+            }
+          }}
+        >
+          <Input
+            type="text"
+            placeholder="My environment"
+            bind:value={addEnvInput}
+          />
+          <Button type="submit" variant="outline">+</Button>
+        </form>
+
+        <Select.Separator />
+
+        <Select.Item value="-" onclick={() => selectEnvironment(null)}
+          >-</Select.Item
+        >
+
+        {#each _state.environments as env}
+          <Select.Item
+            value={env.id.toString()}
+            onclick={() => selectEnvironment(env.id)}>{env.name}</Select.Item
+          >
+        {/each}
+      </Select.Content>
+    </Select.Root>
+  </div>
+</header>

+ 33 - 115
src/lib/components/Sidebar.svelte

@@ -18,91 +18,32 @@
   } from "$lib/state.svelte";
   import SidebarEntry from "./SidebarEntry.svelte";
   import { getSetting } from "$lib/settings.svelte";
+  import { Plus } from "@lucide/svelte";
 
   let { onSelect } = $props();
-
-  let workspaces: Workspace[] = $state([]);
-
-  let addWorkspaceInput = $state("");
-
-  onMount(async () => {
-    workspaces = await listWorkspaces();
-    const lastEntry = await getSetting("lastEntry");
-    if (lastEntry) {
-      const ws = workspaces.find((w) => w.id === lastEntry.workspace_id);
-      if (!ws) {
-        console.error("workspace for last entry not found", lastEntry);
-        return;
-      }
-      await loadWorkspace(ws);
-      selectEntry(lastEntry.id);
-    }
-  });
 </script>
 
-<Sidebar.Root fixed variant="sidebar">
-  <Sidebar.Header>
-    <Sidebar.Menu>
-      <Sidebar.MenuItem class="flex flex-nowrap items-center">
-        <!-- Workspace dropdown -->
-        <DropdownMenu.Root>
-          <DropdownMenu.Trigger>
-            {#snippet child({ props })}
-              <div class="flex w-full">
-                <!-- Workspace name -->
-                <Sidebar.MenuButton
-                  {...props}
-                  variant="outline"
-                  class="h-full justify-center"
-                >
-                  {_state.workspace?.name || "Select workspace"}
-                </Sidebar.MenuButton>
-              </div>
-            {/snippet}
-          </DropdownMenu.Trigger>
-
-          <DropdownMenu.Content align="start">
-            <DropdownMenu.Label>New workspace</DropdownMenu.Label>
-            <form
-              class="flex w-full max-w-sm items-center gap-2"
-              onsubmit={(e) => {
-                e.preventDefault();
-                if (addWorkspaceInput.length > 0) {
-                  createWorkspace(addWorkspaceInput).then(selectWorkspace);
-                  addWorkspaceInput = "";
-                }
-              }}
-            >
-              <Input
-                type="text"
-                placeholder="Add workspace..."
-                bind:value={addWorkspaceInput}
-              />
-              <Button type="submit" variant="outline">+</Button>
-            </form>
-
-            <DropdownMenu.Separator />
-            {#each workspaces as ws}
-              <DropdownMenu.Item
-                disabled={ws.name === _state.workspace?.name}
-                onSelect={() => {
-                  loadWorkspace(ws);
-                }}>{ws.name}</DropdownMenu.Item
-              >
-            {/each}
-          </DropdownMenu.Content>
-        </DropdownMenu.Root>
-
-        <!-- Workspace actions -->
+<Sidebar.Header>
+  <Sidebar.Menu>
+    <Sidebar.MenuItem class="flex flex-nowrap items-center">
+      <!-- Workspace actions -->
 
+      <div class="mx-auto">
         <DropdownMenu.Root>
           <DropdownMenu.Trigger>
             {#snippet child({ props })}
-              <Button {...props} variant="outline">...</Button>
+              <Button
+                {...props}
+                class="rounded-xl"
+                variant="outline"
+                size="icon"
+              >
+                <Plus />
+              </Button>
             {/snippet}
           </DropdownMenu.Trigger>
 
-          <DropdownMenu.Content align="start">
+          <DropdownMenu.Content align="center">
             <DropdownMenu.Group>
               <DropdownMenu.Item onclick={() => createCollection()}
                 >New collection</DropdownMenu.Item
@@ -112,48 +53,25 @@
                 >New request</DropdownMenu.Item
               >
             </DropdownMenu.Group>
-
-            <DropdownMenu.Separator />
-
-            <DropdownMenu.Group>
-              <DropdownMenu.Sub>
-                <DropdownMenu.SubTrigger>Environment</DropdownMenu.SubTrigger>
-                <DropdownMenu.SubContent>
-                  <DropdownMenu.Item onSelect={() => selectEnvironment(null)}
-                    >- {_state.environment === null
-                      ? " ✓"
-                      : ""}</DropdownMenu.Item
-                  >
-                  {#each _state.environments as env}
-                    <DropdownMenu.Item
-                      onSelect={() => selectEnvironment(env.id)}
-                      >{env.name}{_state.environment?.id === env.id
-                        ? " ✓"
-                        : ""}</DropdownMenu.Item
-                    >
-                  {/each}
-                </DropdownMenu.SubContent>
-              </DropdownMenu.Sub>
-            </DropdownMenu.Group>
           </DropdownMenu.Content>
         </DropdownMenu.Root>
-      </Sidebar.MenuItem>
-    </Sidebar.Menu>
-  </Sidebar.Header>
+      </div>
+    </Sidebar.MenuItem>
+  </Sidebar.Menu>
+</Sidebar.Header>
 
-  <Sidebar.Content>
-    {#if _state.workspace}
-      <!-- Workspace entries -->
+<Sidebar.Content>
+  {#if _state.workspace}
+    <!-- Workspace entries -->
 
-      <Sidebar.Group>
-        <Sidebar.GroupContent>
-          <Sidebar.Menu>
-            {#each _state.roots as root}
-              <SidebarEntry id={root} level={0} {onSelect} />
-            {/each}
-          </Sidebar.Menu>
-        </Sidebar.GroupContent>
-      </Sidebar.Group>
-    {/if}
-  </Sidebar.Content>
-</Sidebar.Root>
+    <Sidebar.Group>
+      <Sidebar.GroupContent>
+        <Sidebar.Menu>
+          {#each _state.roots as root}
+            <SidebarEntry id={root} level={0} {onSelect} />
+          {/each}
+        </Sidebar.Menu>
+      </Sidebar.GroupContent>
+    </Sidebar.Group>
+  {/if}
+</Sidebar.Content>

+ 67 - 46
src/lib/components/SidebarEntry.svelte

@@ -6,66 +6,87 @@
     createRequest,
     selectEntry,
   } from "$lib/state.svelte";
-  import * as Sidebar from "./ui/sidebar";
   import Self from "./SidebarEntry.svelte";
   import { setSetting } from "$lib/settings.svelte";
   import * as DropdownMenu from "./ui/dropdown-menu/index";
   import { Button } from "./ui/button";
   import DropdownMenuItem from "./ui/dropdown-menu/dropdown-menu-item.svelte";
+  import { ChevronDown, ChevronRight } from "@lucide/svelte";
 
   const isSelected = $derived(_state.entry?.id === id);
 </script>
 
-<div style={`margin-left: ${level}rem`}>
-  <Sidebar.MenuItem
-    onclick={(e) => {
-      e.stopPropagation();
-      selectEntry(id);
-      onSelect();
-      _state.indexes[id].open = !_state.indexes[id].open;
-      setSetting("lastEntry", _state.indexes[id]);
-    }}
-  >
-    <Sidebar.MenuButton>
-      {#snippet child()}
-        <div
-          class="flex items-center transition-colors"
-          class:border={isSelected}
+<div
+  class="p-1"
+  style={`margin-left: ${level}rem`}
+  class:bg-secondary={isSelected}
+>
+  <div class="border-l p-0 m-0 flex items-center transition-colors rounded-l">
+    {#if _state.indexes[id]!!.type === "Collection"}
+      {#if _state.indexes[id]!!.open}
+        <Button
+          class="p-1 w-fit"
+          variant="ghost"
+          size="icon-sm"
+          onclick={() => {
+            _state.indexes[id].open = !_state.indexes[id].open;
+          }}
         >
-          <p class="p-2 w-full">
-            {_state.indexes[id].name ||
-              _state.indexes[id].type + "(" + _state.indexes[id].id + ")"}
-          </p>
+          <ChevronDown />
+        </Button>
+      {:else}
+        <Button
+          class="p-0 w-fit"
+          variant="ghost"
+          size="icon-sm"
+          onclick={() => {
+            _state.indexes[id].open = !_state.indexes[id].open;
+          }}
+        >
+          <ChevronRight />
+        </Button>
+      {/if}
+    {/if}
 
-          <!-- ACTION MENU -->
+    <p
+      class="w-full cursor-pointer"
+      onclick={(e) => {
+        e.stopPropagation();
+        selectEntry(id);
+        onSelect();
+        setSetting("lastEntry", _state.indexes[id]);
+      }}
+    >
+      {_state.indexes[id].name ||
+        _state.indexes[id].type + "(" + _state.indexes[id].id + ")"}
+    </p>
 
-          {#if _state.indexes[id]?.type === "Collection"}
-            <DropdownMenu.Root>
-              <DropdownMenu.Trigger>
-                {#snippet child({ props })}
-                  <Button {...props} variant="outline">...</Button>
-                {/snippet}
-              </DropdownMenu.Trigger>
+    <!-- ACTION MENU -->
 
-              <DropdownMenu.Content align="start">
-                <DropdownMenuItem onclick={() => createCollection(id)}
-                  >New collection</DropdownMenuItem
-                >
+    {#if _state.indexes[id]?.type === "Collection"}
+      <DropdownMenu.Root>
+        <DropdownMenu.Trigger>
+          {#snippet child({ props })}
+            <Button {...props} variant="ghost" size="icon-sm">...</Button>
+          {/snippet}
+        </DropdownMenu.Trigger>
 
-                <DropdownMenuItem onclick={() => createRequest(id)}
-                  >New request</DropdownMenuItem
-                >
-              </DropdownMenu.Content>
-            </DropdownMenu.Root>
-          {/if}
-        </div>
-      {/snippet}
-    </Sidebar.MenuButton>
+        <DropdownMenu.Content align="start">
+          <DropdownMenuItem onclick={() => createCollection(id)}
+            >New collection</DropdownMenuItem
+          >
 
-    {#if _state.indexes[id].open && _state.children[id]?.length > 0}
-      {#each _state.children[id] as child}
-        <Self id={child} level={level + 0.25} {onSelect} />
-      {/each}
+          <DropdownMenuItem onclick={() => createRequest(id)}
+            >New request</DropdownMenuItem
+          >
+        </DropdownMenu.Content>
+      </DropdownMenu.Root>
     {/if}
-  </Sidebar.MenuItem>
+  </div>
+
+  {#if _state.indexes[id].open && _state.children[id]?.length > 0}
+    {#each _state.children[id] as child}
+      <Self id={child} level={level + 0.25} {onSelect} />
+    {/each}
+  {/if}
 </div>

+ 24 - 30
src/lib/components/WorkspaceEntry.svelte

@@ -154,34 +154,6 @@
   {@html atelierForest}
 </svelte:head>
 
-{#snippet entryPath()}
-  <!-- ENTRY PATH -->
-  <div class="h-8 flex items-center">
-    {#each referenceChain as ref}
-      <Button
-        class="p-0 h-fit cursor-pointer"
-        onclick={() => selectEntry(ref.id)}
-        variant="ghost"
-      >
-        {ref.name || ref.type + "(" + ref.id + ")"}
-      </Button>
-      <p class="pl-1 pr-1">/</p>
-    {/each}
-    <Editable
-      bind:value={_state.entry!!.name}
-      onSave={(value) => {
-        updateEntryName(value);
-      }}
-    >
-      {#snippet display({ value, startEdit })}
-        <h1 ondblclick={startEdit}>
-          {value || _state.entry!!.type + "(" + _state.entry!!.id + ")"}
-        </h1>
-      {/snippet}
-    </Editable>
-  </div>
-{/snippet}
-
 {#snippet authParams(
   entry: WorkspaceEntry & { auth: number | null; auth_inherit: boolean },
 )}
@@ -242,7 +214,18 @@
 {#if _state.entry?.type === "Collection"}
   <!-- COLLECTION VIEW -->
 
-  {@render entryPath()}
+  <Editable
+    bind:value={_state.entry!!.name}
+    onSave={(value) => {
+      updateEntryName(value);
+    }}
+  >
+    {#snippet display({ value, startEdit })}
+      <h1 ondblclick={startEdit}>
+        {value || _state.entry!!.type + "(" + _state.entry!!.id + ")"}
+      </h1>
+    {/snippet}
+  </Editable>
 
   <div class="flex flex-wrap p-2 border">
     <h2 class="pb-2 w-full border-b">Auth</h2>
@@ -251,7 +234,18 @@
 {:else if _state.entry?.type === "Request"}
   <!-- REQUEST WORK AREA -->
 
-  {@render entryPath()}
+  <Editable
+    bind:value={_state.entry!!.name}
+    onSave={(value) => {
+      updateEntryName(value);
+    }}
+  >
+    {#snippet display({ value, startEdit })}
+      <h1 ondblclick={startEdit}>
+        {value || _state.entry!!.type + "(" + _state.entry!!.id + ")"}
+      </h1>
+    {/snippet}
+  </Editable>
 
   <section class="h-[90%] space-y-4">
     <!-- URL BAR -->

+ 91 - 110
src/routes/+page.svelte

@@ -1,132 +1,113 @@
 <script lang="ts">
+  import * as Resizable from "$lib/components/ui/resizable/index";
   import * as Sidebar from "$lib/components/ui/sidebar/index.js";
-  import AppSidebar from "$lib/components/Sidebar.svelte";
   import { toggleMode } from "mode-watcher";
   import { Button } from "$lib/components/ui/button";
   import { SunIcon, MoonIcon, Lock } from "@lucide/svelte";
   import WorkspaceEntry from "$lib/components/WorkspaceEntry.svelte";
+  import { state as _state } from "$lib/state.svelte";
   import {
-    state as _state,
-    createEnvironment,
-    selectEnvironment,
+    listWorkspaces,
+    loadWorkspace,
+    selectEntry,
   } from "$lib/state.svelte";
   import { SlidersHorizontal } from "@lucide/svelte";
   import Environment from "$lib/components/Environment.svelte";
-  import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index";
   import Auth from "$lib/components/Auth.svelte";
-  import { Input } from "$lib/components/ui/input";
+  import type { Workspace } from "$lib/types";
+  import { onMount } from "svelte";
+  import { getSetting } from "$lib/settings.svelte";
+  import Header from "$lib/components/Header.svelte";
+  import AppSidebar from "$lib/components/Sidebar.svelte";
 
-  let displayModal: "env" | "auth" | null = $state(null);
-  let addEnvInput = $state("");
-</script>
+  let sidePane: Resizable.Pane;
+  let mainPane: Resizable.Pane;
 
-<Sidebar.Provider>
-  <AppSidebar onSelect={() => (displayModal = null)} />
+  let displayModal: "env" | "auth" | null = $state(null);
 
-  <main class="w-full p-4 space-y-4">
-    <header class="flex items-center w-full border-b pb-2">
-      <p class="w-full">{_state.workspace?.name ?? "-"}</p>
-      <p class="text-center mr-2">Environment:</p>
-      <DropdownMenu.Root>
-        <DropdownMenu.Trigger>
-          {#snippet child({ props })}
-            <div class="flex justify-center w-1/8">
-              <!-- Workspace name -->
-              <Sidebar.MenuButton
-                class="flex justify-center"
-                {...props}
-                variant="outline"
-              >
-                {_state.environment?.name || "-"}
-              </Sidebar.MenuButton>
-            </div>
-          {/snippet}
-        </DropdownMenu.Trigger>
+  let workspaces: Workspace[] = $state([]);
 
-        <DropdownMenu.Content align="center">
-          <DropdownMenu.Label>New environment</DropdownMenu.Label>
-          <form
-            class="flex w-full max-w-sm items-center gap-2"
-            onsubmit={(e) => {
-              e.preventDefault();
-              if (addEnvInput.length > 0 && _state.workspace) {
-                createEnvironment(_state.workspace.id, addEnvInput);
-                addEnvInput = "";
-              }
-            }}
-          >
-            <Input
-              type="text"
-              placeholder="My environment"
-              bind:value={addEnvInput}
-            />
-            <Button type="submit" variant="outline">+</Button>
-          </form>
+  onMount(async () => {
+    workspaces = await listWorkspaces();
+    const lastEntry = await getSetting("lastEntry");
+    if (lastEntry) {
+      const ws = workspaces.find((w) => w.id === lastEntry.workspace_id);
+      if (!ws) {
+        console.error("workspace for last entry not found", lastEntry);
+        return;
+      }
+      await loadWorkspace(ws);
+      selectEntry(lastEntry.id);
+    }
+  });
+</script>
 
-          <DropdownMenu.Separator />
+<Sidebar.Provider style="--sidebar-width: 3.5rem">
+  <div class="w-full">
+    <Resizable.PaneGroup direction="horizontal">
+      <Resizable.Pane bind:this={sidePane} defaultSize={15}>
+        <AppSidebar onSelect={() => (displayModal = null)} />
+      </Resizable.Pane>
 
-          <DropdownMenu.Item onSelect={() => selectEnvironment(null)}
-            >- {_state.environment === null ? " ✓" : ""}</DropdownMenu.Item
-          >
+      <Resizable.Handle
+        class="p-0.5"
+        ondblclick={() => {
+          sidePane.resize(15);
+          mainPane.resize(85);
+        }}
+      />
 
-          {#each _state.environments as env}
-            <DropdownMenu.Item onSelect={() => selectEnvironment(env.id)}
-              >{env.name}{_state.environment?.id === env.id
-                ? " ✓"
-                : ""}</DropdownMenu.Item
-            >
-          {/each}
-        </DropdownMenu.Content>
-      </DropdownMenu.Root>
-    </header>
-    {#if displayModal === "env"}
-      <Environment />
-    {:else if displayModal === "auth"}
-      <Auth />
-    {:else if _state.entry}
-      <WorkspaceEntry />
-    {:else}{/if}
-  </main>
+      <Resizable.Pane bind:this={mainPane} defaultSize={85}>
+        <main class="w-full h-full p-4 space-y-4">
+          <Header {workspaces} />
+          {#if displayModal === "env"}
+            <Environment />
+          {:else if displayModal === "auth"}
+            <Auth />
+          {:else if _state.entry}
+            <WorkspaceEntry />
+          {:else}{/if}
+        </main>
+      </Resizable.Pane>
+    </Resizable.PaneGroup>
+  </div>
 
-  <Sidebar.Provider style="--sidebar-width: 3.5rem">
-    <Sidebar.Root fixed={false} variant="floating" side="right">
-      <Sidebar.Menu class="items-center">
-        <Sidebar.MenuItem class="pt-2">
-          <Button onclick={toggleMode} variant="ghost" size="icon-sm">
-            <SunIcon
-              class="h-1 w-1 scale-100 rotate-0 transition-all! dark:scale-0 dark:-rotate-90"
-            />
-            <MoonIcon
-              class="absolute h-1 w-1 scale-0 rotate-90 transition-all! dark:scale-100 dark:rotate-0"
-            />
-            <span class="sr-only">Toggle theme</span>
-          </Button>
-        </Sidebar.MenuItem>
+  <Sidebar.Root fixed={false} variant="floating" side="right">
+    <Sidebar.Menu class="items-center">
+      <Sidebar.MenuItem class="pt-2">
+        <Button onclick={toggleMode} variant="ghost" size="icon-sm">
+          <SunIcon
+            class="h-1 w-1 scale-100 rotate-0 transition-all! dark:scale-0 dark:-rotate-90"
+          />
+          <MoonIcon
+            class="absolute h-1 w-1 scale-0 rotate-90 transition-all! dark:scale-100 dark:rotate-0"
+          />
+          <span class="sr-only">Toggle theme</span>
+        </Button>
+      </Sidebar.MenuItem>
 
-        <Sidebar.MenuItem class="pt-2">
-          <Button
-            onclick={() =>
-              (displayModal = displayModal === "env" ? null : "env")}
-            variant={displayModal === "env" ? "default" : "ghost"}
-            size="icon-sm"
-          >
-            <SlidersHorizontal />
-            <span class="sr-only">Display environment</span>
-          </Button>
-        </Sidebar.MenuItem>
+      <Sidebar.MenuItem class="pt-2">
+        <Button
+          onclick={() => (displayModal = displayModal === "env" ? null : "env")}
+          variant={displayModal === "env" ? "default" : "ghost"}
+          size="icon-sm"
+        >
+          <SlidersHorizontal />
+          <span class="sr-only">Display environment</span>
+        </Button>
+      </Sidebar.MenuItem>
 
-        <Sidebar.MenuItem class="pt-2">
-          <Button
-            onclick={() =>
-              (displayModal = displayModal === "auth" ? null : "auth")}
-            variant={displayModal === "auth" ? "default" : "ghost"}
-            size="icon-sm"
-          >
-            <Lock />
-            <span class="sr-only">Display auth</span>
-          </Button>
-        </Sidebar.MenuItem>
-      </Sidebar.Menu>
-    </Sidebar.Root>
-  </Sidebar.Provider>
+      <Sidebar.MenuItem class="pt-2">
+        <Button
+          onclick={() =>
+            (displayModal = displayModal === "auth" ? null : "auth")}
+          variant={displayModal === "auth" ? "default" : "ghost"}
+          size="icon-sm"
+        >
+          <Lock />
+          <span class="sr-only">Display auth</span>
+        </Button>
+      </Sidebar.MenuItem>
+    </Sidebar.Menu>
+  </Sidebar.Root>
 </Sidebar.Provider>