Repository: scalar/scalar
Pull request #5458: fix(api-client): environment collection
Author: login: antlio
Labels: name: priority
Created: 2025-04-21T19:20:35Z | Merged: 2025-04-28T13:49:39Z

**Problem**

currently collection environment usage is not properly usable due to issues:
- environment selector position is overflowing once selected
- environment collection routes needs to be updated
- pill plugin lacks reactivity on environment change
- draft collection if shown in environment page
- environment selector is displaying uid instead of name
- environment collection name cannot be updated
- zod schema prevent short name usage for environment

**Solution**

this pr fixes #5384 #4349 + listed above issues +:
- updates default environment variable pill color for better accessibility
- adds icon on global variable pill

`environment selector`
| before | after |
| -------|------|
| <img width="334" alt="image" src="https://github.com/user-attachments/assets/d10061de-eaba-4168-b01b-ac5bcd9b36e2" /> | <img width="334" alt="image" src="https://github.com/user-attachments/assets/34a3ed0c-7694-4ad9-a24f-f7b61d38797f" /> |
| selector overflowing once selected | selector is teleported | 

`environment active state`
| before | after |
| -------|------|
| <img width="334" alt="image" src="https://github.com/user-attachments/assets/63cc4b8b-f601-4e8d-997d-7a82890a4398" /> | <img width="334" alt="image" src="https://github.com/user-attachments/assets/3b176950-6513-4ff4-81e1-358267cb67fb" /> |
| lacks active state and shows draft | active state shown and draft filtered out | 

**Checklist**

I’ve gone through the following:

- [x] I’ve added an explanation _why_ this change is needed.
- [x] I’ve added a changeset (`pnpm changeset`).
- [ ] I’ve added tests for the regression or new feature.
- [ ] I’ve updated the documentation.

<!-- See the [contributing guide](https://github.com/scalar/scalar/blob/main/CONTRIBUTING.md) for more information. -->


Linked issue #5384: Selecting environment with short name throws an error
### What happens?

Environment does not change.
```
ZodError: [
  {
    "code": "too_small",
    "minimum": 7,
    "type": "string",
    "inclusive": true,
    "exact": false,
    "message": "String must contain at least 7 character(s)",
    "path": [
      "uid"
    ]
  }
]
```

I used environment name `owner` which is less than 7 characters. There is no visual indication about this error, also why is this validation even here?

### What did you expect to happen?

Environment should change.

### OpenAPI Document

_No response_
Linked issue comments: user: login: hanspagel
created_at: 2025-04-11T09:32:42Z
body: Thanks for the report!
author_association: MEMBER

Diff:
diff --git a/.changeset/chilly-donuts-mate.md b/.changeset/chilly-donuts-mate.md
new file mode 100644
index 00000000000..8bc903520af
--- /dev/null
+++ b/.changeset/chilly-donuts-mate.md
@@ -0,0 +1,5 @@
+---
+'@scalar/api-client': patch
+---
+
+feat: makes collection environment draggable
diff --git a/.changeset/hip-geckos-call.md b/.changeset/hip-geckos-call.md
new file mode 100644
index 00000000000..043269bdc11
--- /dev/null
+++ b/.changeset/hip-geckos-call.md
@@ -0,0 +1,5 @@
+---
+'@scalar/api-client': patch
+---
+
+fix: updates string template replacement value for operation
diff --git a/.changeset/sour-shirts-reflect.md b/.changeset/sour-shirts-reflect.md
new file mode 100644
index 00000000000..7f2fb48a778
--- /dev/null
+++ b/.changeset/sour-shirts-reflect.md
@@ -0,0 +1,5 @@
+---
+'@scalar/api-client': patch
+---
+
+fix: environment collection management
diff --git a/packages/api-client/src/components/CodeInput/CodeInput.test.ts b/packages/api-client/src/components/CodeInput/CodeInput.test.ts
index 6e3cb10ebe6..4d74076288e 100644
--- a/packages/api-client/src/components/CodeInput/CodeInput.test.ts
+++ b/packages/api-client/src/components/CodeInput/CodeInput.test.ts
@@ -1,12 +1,13 @@
 import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
 import { mount, type VueWrapper } from '@vue/test-utils'
 import CodeInput from './CodeInput.vue'
-import { useCodeMirror, type Extension } from '@scalar/use-codemirror'
+import { useCodeMirror } from '@scalar/use-codemirror'
 import { useClipboard } from '@scalar/use-hooks/useClipboard'
 import { enableConsoleError, enableConsoleWarn } from '@/vitest.setup'
-import { ref } from 'vue'
+import { ref, toValue } from 'vue'
 import { environmentSchema } from '@scalar/oas-utils/entities/environment'
 import { workspaceSchema } from '@scalar/oas-utils/entities/workspace'
+import { nanoid } from 'nanoid'
 
 // Mock dependencies
 vi.mock('@scalar/use-codemirror', async (importOriginal) => {
@@ -53,13 +54,24 @@ describe('CodeInput', () => {
     }
   })
 
+  const defaultProps = {
+    modelValue: 'test value',
+    environment: environmentSchema.parse({
+      uid: nanoid(),
+      name: 'Test Environment',
+      value: 'test',
+      color: '#000000',
+    }),
+    envVariables: [],
+    workspace: workspaceSchema.parse({
+      name: 'Test Workspace',
+    }),
+  }
+
   const createWrapper = (props = {}, attrs = {}) => {
     return mount(CodeInput, {
       props: {
-        modelValue: 'test value',
-        environment: environmentSchema.parse({}),
-        envVariables: [],
-        workspace: workspaceSchema.parse({}),
+        ...defaultProps,
         ...props,
       },
       attrs,
@@ -170,7 +182,7 @@ describe('CodeInput', () => {
     })
 
     // Check that extensions are set up correctly
-    const extensions = vi.mocked(useCodeMirror).mock.calls[0]?.[0].extensions as Extension[]
-    expect(extensions.length).toBeGreaterThan(0)
+    const extensions = toValue(vi.mocked(useCodeMirror).mock.calls[0]?.[0].extensions)
+    expect(extensions?.length).toBeGreaterThan(0)
   })
 })
diff --git a/packages/api-client/src/components/CodeInput/CodeInput.vue b/packages/api-client/src/components/CodeInput/CodeInput.vue
index 7fd8825e587..c6f4c465722 100644
--- a/packages/api-client/src/components/CodeInput/CodeInput.vue
+++ b/packages/api-client/src/components/CodeInput/CodeInput.vue
@@ -141,15 +141,24 @@ const extensions: Extension[] = []
 if (props.colorPicker) {
   extensions.push(colorPickerExtension)
 }
-extensions.push(
+
+// Create a reactive pill plugin that updates when environment changes
+const pillPluginExtension = computed(() =>
   pillPlugin({
     environment: props.environment,
     envVariables: props.envVariables,
     workspace: props.workspace,
     isReadOnly: layout === 'modal',
   }),
-  backspaceCommand,
 )
+
+// Base extensions that will be used for the editor
+const baseExtensions = computed(() => [
+  ...extensions,
+  pillPluginExtension.value,
+  backspaceCommand,
+])
+
 const codeMirrorRef: Ref<HTMLDivElement | null> = ref(null)
 
 const { codeMirror } = useCodeMirror({
@@ -169,7 +178,7 @@ const { codeMirror } = useCodeMirror({
   lineNumbers: toRef(() => props.lineNumbers),
   language: toRef(() => props.language),
   lint: toRef(() => props.lint),
-  extensions,
+  extensions: baseExtensions,
   placeholder: toRef(() => props.placeholder),
 })
 
@@ -502,7 +511,7 @@ export default {
   background: var(--scalar-background-3) !important;
 }
 .dark-mode .cm-pill {
-  background: color-mix(in srgb, var(--tw-bg-base), transparent 80%) !important;
+  background: color-mix(in srgb, var(--tw-bg-base), transparent 90%) !important;
 }
 .cm-pill:first-of-type {
   margin-left: 0;
diff --git a/packages/api-client/src/components/CodeInput/codeVariableWidget.ts b/packages/api-client/src/components/CodeInput/codeVariableWidget.ts
index 4c3e83307b7..1db5e39c5f8 100644
--- a/packages/api-client/src/components/CodeInput/codeVariableWidget.ts
+++ b/packages/api-client/src/components/CodeInput/codeVariableWidget.ts
@@ -14,12 +14,14 @@ import {
   WidgetType,
 } from '@scalar/use-codemirror'
 import { createApp, defineComponent, h } from 'vue'
+import { nanoid } from 'nanoid'
 
 /**
  * Displays the value of a variable of the active environment in a pill
  */
 class PillWidget extends WidgetType {
   private app: any
+  private uid: string
   environment: Environment | undefined
   envVariables: EnvVariables | undefined
   workspace: Workspace | undefined
@@ -38,6 +40,7 @@ class PillWidget extends WidgetType {
     this.envVariables = envVariables
     this.workspace = workspace
     this.isReadOnly = isReadOnly ?? false
+    this.uid = nanoid()
   }
 
   toDOM() {
@@ -52,14 +55,22 @@ class PillWidget extends WidgetType {
           ? parseEnvVariables(this.envVariables).find((thing) => thing.key === this.variableName)
           : undefined
 
-        // Set the pill color based on the environment or fallback to grey
-        const pillColor = val && this.environment ? getEnvColor(this.environment) : '#8E8E8E'
+        // Set the pill color based on the environment source or fallback to default color
+        const isGlobal = val?.source === 'global'
+        const pillColor = isGlobal ? '#FFFFFF' : val && this.environment ? getEnvColor(this.environment) : '#FFFFFF'
 
-        span.style.setProperty('--tw-bg-base', pillColor || '#8E8E8E')
+        span.style.setProperty('--tw-bg-base', pillColor)
 
         // Set opacity based on the existence of a value
         span.style.opacity = val?.value ? '1' : '0.5'
 
+        // Tooltip trigger element
+        const tooltipTrigger = h('div', { class: 'flex items-center gap-1 whitespace-nowrap' }, [
+          (isGlobal || (this.environment?.name === 'No Environment' && val?.value)) &&
+            h(ScalarIcon, { class: 'size-2.5 -ml-1', icon: 'Globe' }),
+          h('span', this.variableName),
+        ])
+
         const tooltipContent = val?.value
           ? h('div', { class: 'p-2' }, val.value)
           : h('div', { class: 'divide-y divide-1/2 grid' }, [
@@ -101,15 +112,12 @@ class PillWidget extends WidgetType {
             sideOffset: 6,
           },
           {
-            trigger: () => h('span', `${this.variableName}`),
+            trigger: () => tooltipTrigger,
             content: () =>
               h(
                 'div',
                 {
-                  class: [
-                    'border w-content rounded  bg-b-1 brightness-lifted text-xxs leading-5 text-c-1',
-                    val?.value ? 'border-solid' : 'border-dashed',
-                  ],
+                  class: ['border w-content rounded bg-b-1 brightness-lifted text-xxs leading-5 text-c-1'],
                 },
                 tooltipContent,
               ),
@@ -131,7 +139,7 @@ class PillWidget extends WidgetType {
   }
 
   override eq(other: WidgetType) {
-    return other instanceof PillWidget && other.variableName === this.variableName
+    return other instanceof PillWidget && other.variableName === this.variableName && other.uid === this.uid
   }
 
   override ignoreEvent() {
diff --git a/packages/api-client/src/components/EnvironmentSelector/EnvironmentSelector.vue b/packages/api-client/src/components/EnvironmentSelector/EnvironmentSelector.vue
index d3f134ba062..14dd7a55376 100644
--- a/packages/api-client/src/components/EnvironmentSelector/EnvironmentSelector.vue
+++ b/packages/api-client/src/components/EnvironmentSelector/EnvironmentSelector.vue
@@ -12,6 +12,7 @@ import { computed, onMounted, watch } from 'vue'
 import { useRouter } from 'vue-router'
 
 import { useLayout } from '@/hooks'
+import { PathId } from '@/routes'
 import { useWorkspace } from '@/store'
 import { useActiveEntities } from '@/store/active-entities'
 
@@ -33,11 +34,12 @@ const updateSelected = (uid: string) => {
     activeWorkspace.value.activeEnvironmentId = uid
   }
 }
-const createNewEnvironment = () =>
+
+const redirectToEnvironments = () =>
   router.push({
-    name: 'environment',
+    name: 'environment.default',
     params: {
-      environment: activeWorkspace.value?.uid,
+      [PathId.Workspace]: activeWorkspace.value?.uid,
     },
   })
 
@@ -45,7 +47,7 @@ const selectedEnvironment = computed(() => {
   const { value: environment } = activeEnvironment
   const { value: collection } = activeCollection
   return (
-    environment?.uid ||
+    environment?.name ||
     collection?.['x-scalar-active-environment'] ||
     'No Environment'
   )
@@ -83,62 +85,53 @@ onMounted(() => {
 })
 </script>
 <template>
-  <div>
-    <ScalarDropdown placement="bottom-end">
-      <ScalarButton
-        class="text-c-1 hover:bg-b-2 h-auto w-fit justify-start px-1.5 py-1.5 pl-2 font-normal"
-        fullWidth
-        variant="ghost">
-        <h2 class="m-0 flex items-center gap-1.5 whitespace-nowrap font-medium">
-          {{ selectedEnvironment }}
-        </h2>
-      </ScalarButton>
-      <!-- Workspace list -->
-      <template #items>
-        <ScalarDropdownItem
-          v-for="environment in availableEnvironments"
-          :key="environment.uid"
-          class="group/item flex items-center gap-1.5 overflow-hidden text-ellipsis whitespace-nowrap"
-          @click.stop="updateSelected(environment.uid)">
-          <ScalarListboxCheckbox
-            :selected="
-              activeCollection?.['x-scalar-active-environment'] ===
-              environment.uid
-            " />
-          {{ environment.name }}
-        </ScalarDropdownItem>
-        <ScalarDropdownItem
-          class="group/item flex items-center gap-1.5 overflow-hidden text-ellipsis whitespace-nowrap"
-          @click.stop="updateSelected('')">
-          <div
-            class="flex h-4 w-4 items-center justify-center rounded-full p-[3px]"
-            :class="
-              activeEnvironment?.uid === '' &&
-              activeCollection?.['x-scalar-active-environment'] === ''
-                ? 'bg-c-accent text-b-1'
-                : 'shadow-border text-transparent'
-            ">
-            <ScalarIcon
-              class="size-2.5"
-              icon="Checkmark"
-              thickness="3" />
-          </div>
-          No Environment
-        </ScalarDropdownItem>
-        <ScalarDropdownDivider />
-        <!-- Manage environments -->
-        <ScalarDropdownItem
-          v-if="layout !== 'modal'"
-          class="flex items-center gap-1.5"
-          @click="createNewEnvironment">
-          <div class="flex h-4 w-4 items-center justify-center">
-            <ScalarIcon
-              icon="Brackets"
-              size="sm" />
-          </div>
-          <span class="leading-none">Manage Environments</span>
-        </ScalarDropdownItem>
-      </template>
-    </ScalarDropdown>
-  </div>
+  <ScalarDropdown teleport>
+    <ScalarButton
+      class="text-c-1 hover:bg-b-2 line-clamp-1 h-auto w-fit justify-start px-1.5 py-1.5 font-normal"
+      fullWidth
+      variant="ghost">
+      <h2 class="m-0 flex items-center gap-1.5 whitespace-nowrap font-medium">
+        {{ selectedEnvironment }}
+      </h2>
+    </ScalarButton>
+    <!-- Workspace list -->
+    <template #items>
+      <ScalarDropdownItem
+        v-for="environment in availableEnvironments"
+        :key="environment.uid"
+        class="group/item flex items-center gap-1.5 overflow-hidden text-ellipsis whitespace-nowrap"
+        @click.stop="updateSelected(environment.uid)">
+        <ScalarListboxCheckbox
+          :selected="
+            activeCollection?.['x-scalar-active-environment'] ===
+            environment.uid
+          " />
+        {{ environment.name }}
+      </ScalarDropdownItem>
+      <ScalarDropdownItem
+        class="group/item flex items-center gap-1.5 overflow-hidden text-ellipsis whitespace-nowrap"
+        @click.stop="updateSelected('')">
+        <ScalarListboxCheckbox
+          :selected="
+            (activeEnvironment?.uid === '' &&
+              activeCollection?.['x-scalar-active-environment'] === '') ||
+            activeEnvironment?.name === 'No Environment'
+          " />
+        No Environment
+      </ScalarDropdownItem>
+      <ScalarDropdownDivider />
+      <!-- Manage environments -->
+      <ScalarDropdownItem
+        v-if="layout !== 'modal'"
+        class="flex items-center gap-1.5"
+        @click="redirectToEnvironments">
+        <div class="flex h-4 w-4 items-center justify-center">
+          <ScalarIcon
+            icon="Brackets"
+            size="sm" />
+        </div>
+        <span class="leading-none">Manage Environments</span>
+      </ScalarDropdownItem>
+    </template>
+  </ScalarDropdown>
 </template>
diff --git a/packages/api-client/src/components/SideNav/SideNav.vue b/packages/api-client/src/components/SideNav/SideNav.vue
index 37e91300487..534ae85beae 100644
--- a/packages/api-client/src/components/SideNav/SideNav.vue
+++ b/packages/api-client/src/components/SideNav/SideNav.vue
@@ -44,7 +44,13 @@ const { activeWorkspace } = useActiveEntities()
         )"
         :key="i">
         <SideNavRouterLink
-          :active="Boolean(currentRoute.name === displayName.toLowerCase())"
+          :active="
+            Boolean(
+              currentRoute.name === displayName.toLowerCase() ||
+                (displayName.toLowerCase() === 'environment' &&
+                  currentRoute.name === 'environment.collection'),
+            )
+          "
           :icon="icon"
           :to="{
             ...to,
diff --git a/packages/api-client/src/components/Sidebar/SidebarListElement.vue b/packages/api-client/src/components/Sidebar/SidebarListElement.vue
index 27e73783c60..45fec9a131b 100644
--- a/packages/api-client/src/components/Sidebar/SidebarListElement.vue
+++ b/packages/api-client/src/components/Sidebar/SidebarListElement.vue
@@ -1,10 +1,21 @@
 <script setup lang="ts">
 import { ScalarIcon, type Icon } from '@scalar/components'
+import {
+  Draggable,
+  type DraggableProps,
+  type DraggingItem,
+  type HoveredItem,
+} from '@scalar/draggable'
+import { computed, ref } from 'vue'
 import { useRouter, type RouteLocationRaw } from 'vue-router'
 
 import SidebarListElementActions from '@/components/Sidebar/SidebarListElementActions.vue'
 
-const props = defineProps<{
+const {
+  isDraggable = false,
+  isDroppable = false,
+  to,
+} = defineProps<{
   variable: {
     uid: string
     name: string
@@ -17,21 +28,35 @@ const props = defineProps<{
   isDeletable?: boolean
   isCopyable?: boolean
   isRenameable?: boolean
+  /**
+   * Toggle dragging on and off
+   *
+   * @default false
+   */
+  isDraggable?: boolean
+  /**
+   * Prevents items from being hovered and dropped into
+   *
+   * @default false
+   */
+  isDroppable?: DraggableProps['isDroppable']
+  collectionId?: string
 }>()
 
 const emit = defineEmits<{
   (e: 'delete', id: string): void
   (e: 'colorModal', id: string): void
   (e: 'rename', id: string): void
+  (e: 'onDragEnd', draggingItem: DraggingItem, hoveredItem: HoveredItem): void
 }>()
 
 const router = useRouter()
 
 const handleNavigation = (event: MouseEvent) => {
   if (event.metaKey) {
-    window.open(router.resolve(props.to).href, '_blank')
+    window.open(router.resolve(to).href, '_blank')
   } else {
-    router.push(props.to)
+    router.push(to)
   }
 }
 
@@ -46,46 +71,72 @@ const handleColorClick = (uid: string) => {
 const handleRename = (id: string) => {
   emit('rename', id)
 }
+
+/** The draggable component */
+const draggableRef = ref<{
+  draggingItem: DraggingItem
+  hoveredItem: HoveredItem
+} | null>(null)
+
+/** Calculate offsets for drag and drop */
+const getDraggableOffsets = computed(() => ({
+  ceiling: 0.5,
+  floor: 0.5,
+}))
 </script>
 
 <template>
   <li>
-    <router-link
-      class="text-c-2 hover:bg-b-2 group relative flex h-8 items-center gap-1.5 rounded py-1 pr-1.5 font-medium no-underline"
-      :class="[variable.color ? 'pl-1' : 'pl-1.5']"
-      exactActiveClass="bg-b-2 text-c-1"
-      role="button"
-      :to="to"
-      @click.prevent="handleNavigation($event)">
-      <button
-        v-if="variable.color"
-        class="hover:bg-b-3 rounded p-1.5"
-        type="button"
-        @click="handleColorClick(variable.uid)">
-        <div
-          class="h-2.5 w-2.5 rounded-xl"
-          :style="{ backgroundColor: variable.color }"></div>
-      </button>
-      <ScalarIcon
-        v-if="variable.icon"
-        class="text-sidebar-c-2 size-3.5 stroke-[2.25]"
-        :icon="variable.icon" />
-      <span
-        class="empty-variable-name line-clamp-1 break-all text-sm group-hover:pr-5">
-        {{ variable.name }}
-      </span>
-      <SidebarListElementActions
-        :isCopyable="Boolean(isCopyable)"
-        :isDeletable="Boolean(isDeletable)"
-        :isRenameable="Boolean(isRenameable)"
-        :variable="{ ...variable, isDefault: variable.isDefault ?? false }"
-        :warningMessage="warningMessage"
-        @delete="handleDelete"
-        @rename="handleRename" />
-    </router-link>
+    <Draggable
+      :id="variable.uid"
+      ref="draggableRef"
+      :ceiling="getDraggableOffsets.ceiling"
+      :floor="getDraggableOffsets.floor"
+      :isDraggable="isDraggable"
+      :isDroppable="isDroppable"
+      :parentIds="collectionId ? [collectionId] : []"
+      @onDragEnd="(...args) => $emit('onDragEnd', ...args)">
+      <router-link
+        class="text-c-2 hover:bg-b-2 group relative flex h-8 items-center gap-1.5 rounded py-1 pr-1.5 font-medium no-underline"
+        :class="[variable.color ? 'pl-5' : 'pl-1.5']"
+        exactActiveClass="bg-b-2 !text-c-1"
+        role="button"
+        :to="to"
+        @click.prevent="handleNavigation($event)">
+        <button
+          v-if="variable.color"
+          class="hover:bg-b-3 rounded p-1.5"
+          type="button"
+          @click="handleColorClick(variable.uid)">
+          <div
+            class="h-2.5 w-2.5 rounded-xl"
+            :style="{ backgroundColor: variable.color }"></div>
+        </button>
+        <ScalarIcon
+          v-if="variable.icon"
+          class="text-sidebar-c-2 size-3.5 stroke-[2.25]"
+          :icon="variable.icon" />
+        <span
+          class="empty-variable-name line-clamp-1 break-all text-sm group-hover:pr-5">
+          {{ variable.name }}
+        </span>
+        <SidebarListElementActions
+          :isCopyable="Boolean(isCopyable)"
+          :isDeletable="Boolean(isDeletable)"
+          :isRenameable="Boolean(isRenameable)"
+          :variable="{ ...variable, isDefault: variable.isDefault ?? false }"
+          :warningMessage="warningMessage"
+          @delete="handleDelete"
+          @rename="handleRename" />
+      </router-link>
+    </Draggable>
   </li>
 </template>
 
+<style>
+@import '@scalar/draggable/style.css';
+</style>
+
 <style scoped>
 .empty-variable-name:empty:before {
   content: 'Untitled';
diff --git a/packages/api-client/src/libs/env-helpers.test.ts b/packages/api-client/src/libs/env-helpers.test.ts
index a3a96ebd3f6..781e11072bb 100644
--- a/packages/api-client/src/libs/env-helpers.test.ts
+++ b/packages/api-client/src/libs/env-helpers.test.ts
@@ -1,11 +1,13 @@
 import { environmentSchema } from '@scalar/oas-utils/entities/environment'
 import { describe, expect, it } from 'vitest'
+import { nanoid } from 'nanoid'
 
 import { getEnvColor } from './env-helpers'
 
 describe('getEnvColor', () => {
   it('returns the environment color when provided', () => {
     const env = environmentSchema.parse({
+      uid: nanoid(),
       color: '#FF0000',
       name: 'Production',
       value: 'production',
@@ -16,11 +18,11 @@ describe('getEnvColor', () => {
 
   it('returns default color when environment is undefined', () => {
     // @ts-expect-error testing undefined
-    expect(getEnvColor(undefined)).toBe('#8E8E8E')
+    expect(getEnvColor(undefined)).toBe('#FFFFFF')
   })
 
   it('returns default color when environment is null', () => {
     // @ts-expect-error testing null
-    expect(getEnvColor(null)).toBe('#8E8E8E')
+    expect(getEnvColor(null)).toBe('#FFFFFF')
   })
 })
diff --git a/packages/api-client/src/libs/env-helpers.ts b/packages/api-client/src/libs/env-helpers.ts
index 814a8fe4e72..a20da9ee7ce 100644
--- a/packages/api-client/src/libs/env-helpers.ts
+++ b/packages/api-client/src/libs/env-helpers.ts
@@ -1,6 +1,14 @@
 import type { Environment } from '@scalar/oas-utils/entities/environment'
 
-export type EnvVariables = { key: string; value: string; source: string }[]
+export type EnvVariables = { key: string; value: string; source: 'global' | 'collection' }[]
+
+export type EnvConfig = {
+  variables: Record<string, string | { default: string; description?: string | undefined }>
+  color?: string | undefined
+  description?: string | undefined
+}
 
 /** Gets the color of an environment with default fallback */
-export const getEnvColor = (environment: Environment) => (environment ? environment.color : '#8E8E8E')
+export function getEnvColor(environment: Environment): string {
+  return environment?.color || '#FFFFFF'
+}
diff --git a/packages/api-client/src/libs/send-request/create-request-operation.ts b/packages/api-client/src/libs/send-request/create-request-operation.ts
index 31a325ef81c..c111bc87e8a 100644
--- a/packages/api-client/src/libs/send-request/create-request-operation.ts
+++ b/packages/api-client/src/libs/send-request/create-request-operation.ts
@@ -72,7 +72,8 @@ export const createRequestOperation = ({
     }, {})
 
     const serverString = replaceTemplateVariables(server?.url ?? '', env)
-    const pathString = replaceTemplateVariables(request.path, pathVariables)
+    // Replace environment variables, then path variables
+    const pathString = replaceTemplateVariables(replaceTemplateVariables(request.path, env), pathVariables)
 
     /**
      * Start building the main URL, we cannot use the URL class yet as it does not work with relative servers
diff --git a/packages/api-client/src/libs/string-template.test.ts b/packages/api-client/src/libs/string-template.test.ts
index ac9c50e0dd9..6e61bc6257b 100644
--- a/packages/api-client/src/libs/string-template.test.ts
+++ b/packages/api-client/src/libs/string-template.test.ts
@@ -33,24 +33,36 @@ describe('Replaces template vars with context values', () => {
       city: 'Peel',
       country: 'Bananaland',
     },
+    single: {
+      name: 'Nolan',
+      age: '1 month',
+    },
   }
+
   it('Handles double curly variable substitution', () => {
     const res = replaceTemplateVariables('My name is {{name}} from {{address.city}}', ctx)
     expect(res).toEqual('My name is Dave from Peel')
   })
+
   it('Handles single curly variable substitution', () => {
     const res = replaceTemplateVariables('My name is {name} from { address.city }', ctx)
     expect(res).toEqual('My name is Dave from Peel')
   })
+
   it('Handles colon variable substitution', () => {
     const res = replaceTemplateVariables('My name is :name from :address.city', ctx)
     expect(res).toEqual('My name is Dave from Peel')
   })
+
   it('Handles object conversion to string', () => {
     const res = replaceTemplateVariables('My name is {{name}} from {{address}}', ctx)
-
     expect(res).toEqual(`My name is Dave from ${JSON.stringify(ctx.address)}`)
   })
+
+  it('Handles mixed double and single curly braces independently', () => {
+    const res = replaceTemplateVariables('{{name}} and {single.name} which is {single.age}', ctx)
+    expect(res).toEqual('Dave and Nolan which is 1 month')
+  })
 })
 
 describe('Gets dot nested paths for an environment', () => {
diff --git a/packages/api-client/src/libs/string-template.ts b/packages/api-client/src/libs/string-template.ts
index 8bb169e32e6..cdd6fc46579 100644
--- a/packages/api-client/src/libs/string-template.ts
+++ b/packages/api-client/src/libs/string-template.ts
@@ -19,17 +19,36 @@ export function getDotPathValue(path: string, context: object) {
  * - :colon
  */
 export function replaceTemplateVariables(templateString: string, context: object) {
-  const matches = templateString.match(REGEX.TEMPLATE_VARIABLE) ?? []
-
   let substitutedString = templateString
-  matches.forEach((m) => {
-    const key = m.startsWith(':') ? m.slice(1) : m.replace(/[{}]/g, '').trim()
+  const usedKeys = new Set<string>()
 
+  // Handle double curly braces first
+  substitutedString = substitutedString.replace(REGEX.VARIABLES, (_, variable) => {
+    const key = variable.trim()
+    usedKeys.add(key)
     const value = getDotPathValue(key, context)
-    // value can be an empty string but not null or undefined
-    if (value !== null && value !== undefined) {
-      substitutedString = substitutedString.replaceAll(m, value)
+    return value !== null && value !== undefined ? value : `{{${key}}}`
+  })
+
+  // Handle single curly braces, skipping already used keys
+  substitutedString = substitutedString.replace(REGEX.PATH, (_, variable) => {
+    const key = variable.trim()
+    if (usedKeys.has(key)) {
+      return `{${key}}`
+    }
+    const value = getDotPathValue(key, context)
+    return value !== null && value !== undefined ? value : `{${key}}`
+  })
+
+  // Handle colon format, skipping already used keys
+  substitutedString = substitutedString.replace(/:\b[\w.]+\b/g, (match) => {
+    const key = match.slice(1)
+    if (usedKeys.has(key)) {
+      return match
     }
+    const value = getDotPathValue(key, context)
+    // value can be an empty string but not null or undefined
+    return value !== null && value !== undefined ? value : match
   })
 
   return substitutedString
diff --git a/packages/api-client/src/store/active-entities.ts b/packages/api-client/src/store/active-entities.ts
index 665def85090..1133c1f5cbc 100644
--- a/packages/api-client/src/store/active-entities.ts
+++ b/packages/api-client/src/store/active-entities.ts
@@ -79,7 +79,7 @@ export const createActiveEntitiesStore = ({
     if (!activeWorkspace.value?.activeEnvironmentId) {
       return environmentSchema.parse({
         uid: 'default',
-        color: '#0082D0',
+        color: '#FFFFFF',
         name: 'No Environment',
         value: JSON.stringify(activeWorkspace.value?.environments, null, 2),
       })
@@ -100,14 +100,14 @@ export const createActiveEntitiesStore = ({
         ),
         color:
           activeEnvironmentCollection['x-scalar-environments']?.[activeWorkspace.value?.activeEnvironmentId]?.color ||
-          '#0082D0',
+          '#FFFFFF',
         isDefault: false,
       })
     }
 
     return environmentSchema.parse({
       uid: 'default',
-      color: '#0082D0',
+      color: '#FFFFFF',
       name: 'No Environment',
       value: JSON.stringify(activeWorkspace.value.environments, null, 2),
     })
diff --git a/packages/api-client/src/store/environment.ts b/packages/api-client/src/store/environment.ts
index a9b35f9a25d..34707412be0 100644
--- a/packages/api-client/src/store/environment.ts
+++ b/packages/api-client/src/store/environment.ts
@@ -15,7 +15,7 @@ export function createStoreEnvironments(useLocalStorage: boolean) {
     environmentSchema.parse({
       uid: 'default',
       name: 'Default Environment',
-      color: '#0082D0',
+      color: '#FFFFFF',
       value: JSON.stringify({ exampleKey: 'exampleValue' }, null, 2),
       isDefault: true,
     }),
diff --git a/packages/api-client/src/views/Environment/Environment.vue b/packages/api-client/src/views/Environment/Environment.vue
index 3b228a5b286..12ac6741bff 100644
--- a/packages/api-client/src/views/Environment/Environment.vue
+++ b/packages/api-client/src/views/Environment/Environment.vue
@@ -8,7 +8,7 @@ import {
 import { LibraryIcon } from '@scalar/icons/library'
 import type { Collection } from '@scalar/oas-utils/entities/spec'
 import { useToasts } from '@scalar/use-toasts'
-import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
+import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 
 import CodeInput from '@/components/CodeInput/CodeInput.vue'
@@ -22,12 +22,14 @@ import ViewLayoutContent from '@/components/ViewLayout/ViewLayoutContent.vue'
 import ViewLayoutSection from '@/components/ViewLayout/ViewLayoutSection.vue'
 import { useSidebar } from '@/hooks/useSidebar'
 import type { HotKeyEvent } from '@/libs'
+import type { EnvConfig } from '@/libs/env-helpers'
 import { PathId } from '@/routes'
 import { useWorkspace } from '@/store'
 import { useActiveEntities } from '@/store/active-entities'
 
 import EnvironmentColorModal from './EnvironmentColorModal.vue'
 import EnvironmentModal from './EnvironmentModal.vue'
+import { environmentDragHandlerFactory } from './handle-drag'
 
 const router = useRouter()
 const route = useRoute()
@@ -73,12 +75,19 @@ function environmentNameToast(
   }
 }
 
+// Returns non-draft collections only
+const nonDraftWorkspaceCollections = computed(() => {
+  return activeWorkspaceCollections.value.filter(
+    (collection) => collection.info?.title !== 'Drafts',
+  )
+})
+
 function addEnvironment(environment: {
   name: string
   color: string
   collectionId: Collection['uid'] | undefined
 }) {
-  const environmentNameUsed = activeWorkspaceCollections.value.some(
+  const environmentNameUsed = nonDraftWorkspaceCollections.value.some(
     (collection) => {
       const enviromentName = Object.keys(
         collection['x-scalar-environments'] || {},
@@ -115,34 +124,34 @@ function addEnvironment(environment: {
 }
 
 function handleEnvironmentUpdate(raw: string) {
-  if (activeEnvironment) {
-    const updatedValue = parseEnvironmentValue(raw)
-
-    if (currentEnvironmentId.value === 'default') {
-      workspaceMutators.edit(
-        activeWorkspace.value?.uid,
-        'environments',
-        updatedValue,
-      )
-    } else {
-      const collection = activeWorkspaceCollections.value.find(
-        (c) => c['x-scalar-environments']?.[currentEnvironmentId.value ?? ''],
-      )
-      if (
-        collection?.['x-scalar-environments']?.[
-          currentEnvironmentId.value ?? ''
-        ]
-      ) {
-        const environment =
-          collection['x-scalar-environments'][currentEnvironmentId.value ?? '']
-        if (environment) {
-          environment.variables = updatedValue
-          collectionMutators.edit(
-            collection.uid,
-            'x-scalar-environments',
-            collection['x-scalar-environments'],
-          )
-        }
+  if (!activeEnvironment) {
+    return
+  }
+
+  const updatedValue = parseEnvironmentValue(raw)
+
+  if (currentEnvironmentId.value === 'default') {
+    workspaceMutators.edit(
+      activeWorkspace.value?.uid,
+      'environments',
+      updatedValue,
+    )
+  } else {
+    const collection = activeWorkspaceCollections.value.find(
+      (c) => c['x-scalar-environments']?.[currentEnvironmentId.value ?? ''],
+    )
+    if (
+      collection?.['x-scalar-environments']?.[currentEnvironmentId.value ?? '']
+    ) {
+      const environment =
+        collection['x-scalar-environments'][currentEnvironmentId.value ?? '']
+      if (environment) {
+        environment.variables = updatedValue
+        collectionMutators.edit(
+          collection.uid,
+          'x-scalar-environments',
+          collection['x-scalar-environments'],
+        )
       }
     }
   }
@@ -192,13 +201,13 @@ const submitColorChange = (color: string) => {
 }
 
 function removeCollectionEnvironment(environmentName: string) {
-  activeWorkspaceCollections.value.forEach((collection) => {
+  nonDraftWorkspaceCollections.value.forEach((collection) => {
     collectionMutators.removeEnvironment(environmentName, collection.uid)
   })
 
   // Redirect to last available environment
   const remainingCollectionEnvironments =
-    activeWorkspaceCollections.value.flatMap((collection) =>
+    nonDraftWorkspaceCollections.value.flatMap((collection) =>
       Object.keys(collection['x-scalar-environments'] || {}),
     )
 
@@ -222,8 +231,8 @@ function removeCollectionEnvironment(environmentName: string) {
     router.push({
       name: 'environment.collection',
       params: {
-        collectionId: currentCollection?.uid,
-        environmentId: lastCollectionEnvironment,
+        [PathId.Collection]: currentCollection?.uid,
+        [PathId.Environment]: lastCollectionEnvironment,
       },
     })
     if (currentCollection && !collapsedSidebarFolders[currentCollection.uid]) {
@@ -232,8 +241,10 @@ function removeCollectionEnvironment(environmentName: string) {
   } else {
     currentEnvironmentId.value = 'default'
     router.push({
-      name: 'environment',
-      params: { environment: 'default' },
+      name: 'environment.default',
+      params: {
+        [PathId.Workspace]: activeWorkspace.value?.uid,
+      },
     })
   }
 }
@@ -330,7 +341,7 @@ function handleCancelRename() {
 }
 
 function handleRename(newName: string) {
-  const environmentNameUsed = activeWorkspaceCollections.value.some(
+  const environmentNameUsed = nonDraftWorkspaceCollections.value.some(
     (collection) => {
       const enviromentName = Object.keys(
         collection['x-scalar-environments'] || {},
@@ -351,19 +362,31 @@ function handleRename(newName: string) {
       if (
         collection['x-scalar-environments']?.[selectedEnvironmentId.value ?? '']
       ) {
-        const env =
-          collection['x-scalar-environments'][selectedEnvironmentId.value ?? '']
-        if (env) {
-          delete collection['x-scalar-environments'][
-            selectedEnvironmentId.value ?? ''
-          ]
-          collection['x-scalar-environments'][newName] = env
-          collectionMutators.edit(
-            collection.uid,
-            'x-scalar-environments',
-            collection['x-scalar-environments'],
-          )
-        }
+        const environments = collection['x-scalar-environments']
+        // Maintains order of environments in the sidebar as we use uid as the name
+        const orderedEnvs: Record<string, EnvConfig> = {}
+
+        // Preserve order by rebuilding the environments object
+        Object.keys(environments).forEach((key) => {
+          const environment = environments[key]
+
+          if (!environment) {
+            return
+          }
+
+          if (key === selectedEnvironmentId.value) {
+            orderedEnvs[newName] = environment
+          } else {
+            orderedEnvs[key] = environment
+          }
+        })
+
+        collection['x-scalar-environments'] = orderedEnvs
+        collectionMutators.edit(
+          collection.uid,
+          'x-scalar-environments',
+          collection['x-scalar-environments'],
+        )
       }
     })
   }
@@ -377,6 +400,12 @@ function handleRename(newName: string) {
   tempEnvironmentName.value = undefined
   editModal.hide()
 }
+
+// Replace handleEnvironmentDragEnd with the factory
+const { handleDragEnd, isDroppable } = environmentDragHandlerFactory(
+  activeWorkspaceCollections,
+  collectionMutators,
+)
 </script>
 <template>
   <ViewLayout>
@@ -389,7 +418,7 @@ function handleRename(newName: string) {
               class="text-xs"
               :isCopyable="false"
               :to="{
-                name: 'environment.default',
+                name: 'environment',
                 params: {
                   [PathId.Environment]: 'default',
                 },
@@ -402,7 +431,7 @@ function handleRename(newName: string) {
                 isDefault: true,
               }" />
             <li
-              v-for="collection in activeWorkspaceCollections"
+              v-for="collection in nonDraftWorkspaceCollections"
               :key="collection.uid"
               class="gap-1/2 flex flex-col">
               <button
@@ -439,13 +468,15 @@ function handleRename(newName: string) {
                     'x-scalar-environments'
                   ]"
                   :key="environmentName"
-                  class="text-xs [&>a]:pl-5"
+                  class="text-xs"
                   :collectionId="collection.uid"
                   :isCopyable="false"
                   :isDeletable="true"
                   :isRenameable="true"
+                  :isDraggable="true"
+                  :isDroppable="isDroppable"
                   :to="{
-                    name: 'collection.environment',
+                    name: 'environment.collection',
                     params: {
                       [PathId.Collection]: collection.uid,
                       [PathId.Environment]: environmentName,
@@ -455,7 +486,7 @@ function handleRename(newName: string) {
                   :variable="{
                     name: environmentName,
                     uid: environmentName,
-                    color: environment.color ?? '#8E8E8E',
+                    color: environment.color ?? '#FFFFFF',
                     isDefault: false,
                   }"
                   :warningMessage="`Are you sure you want to delete this environment?`"
@@ -464,7 +495,8 @@ function handleRename(newName: string) {
                   "
                   @colorModal="handleOpenColorModal(environmentName)"
                   @delete="removeCollectionEnvironment(environmentName)"
-                  @rename="openRenameModal(environmentName, collection.uid)" />
+                  @rename="openRenameModal(environmentName, collection.uid)"
+                  @onDragEnd="handleDragEnd" />
                 <ScalarButton
                   v-if="
                     Object.keys(collection['x-scalar-environments'] || {})
@@ -520,7 +552,7 @@ function handleRename(newName: string) {
       @cancel="colorModal.hide()"
       @submit="submitColorChange" />
     <EnvironmentModal
-      :activeWorkspaceCollections="activeWorkspaceCollections"
+      :activeWorkspaceCollections="nonDraftWorkspaceCollections"
       :collectionId="selectedCollectionId"
       :state="environmentModal"
       @cancel="environmentModal.hide()"
diff --git a/packages/api-client/src/views/Environment/EnvironmentColors.vue b/packages/api-client/src/views/Environment/EnvironmentColors.vue
index 7300dda6282..a39e44ad676 100644
--- a/packages/api-client/src/views/Environment/EnvironmentColors.vue
+++ b/packages/api-client/src/views/Environment/EnvironmentColors.vue
@@ -22,7 +22,7 @@ const showCustomInput = ref(false)
 const showSelector = ref(false)
 
 const colorOptions = [
-  { color: '#8E8E8E' },
+  { color: '#FFFFFF' },
   { color: '#EF0006' },
   { color: '#EDBE20' },
   { color: '#069061' },
diff --git a/packages/api-client/src/views/Environment/EnvironmentModal.vue b/packages/api-client/src/views/Environment/EnvironmentModal.vue
index 2433992bbd1..1bcdf0cbb2b 100644
--- a/packages/api-client/src/views/Environment/EnvironmentModal.vue
+++ b/packages/api-client/src/views/Environment/EnvironmentModal.vue
@@ -38,7 +38,7 @@ const emit = defineEmits<{
 const { events } = useWorkspace()
 
 const environmentName = ref('')
-const selectedColor = ref('#8E8E8E')
+const selectedColor = ref('#FFFFFF')
 
 const collections = computed(() => [
   ...props.activeWorkspaceCollections
@@ -65,7 +65,7 @@ watch(
   (isOpen) => {
     if (isOpen) {
       environmentName.value = ''
-      selectedColor.value = '#8E8E8E'
+      selectedColor.value = '#FFFFFF'
       if (props.collectionId) {
         selectedEnvironment.value = collections.value.find(
           (collection) => collection.id === props.collectionId,
diff --git a/packages/api-client/src/views/Environment/EnvironmentVariableDropdown.vue b/packages/api-client/src/views/Environment/EnvironmentVariableDropdown.vue
index aff205b59a9..f4292de756a 100644
--- a/packages/api-client/src/views/Environment/EnvironmentVariableDropdown.vue
+++ b/packages/api-client/src/views/Environment/EnvironmentVariableDropdown.vue
@@ -137,16 +137,16 @@ onClickOutside(
             class="font-code text-xxs hover:bg-b-2 flex h-8 cursor-pointer items-center justify-between gap-1.5 rounded p-1.5 transition-colors duration-150"
             :class="{ 'bg-b-2': index === selectedVariableIndex }"
             @click="selectVariable(item.key)">
-            <div class="flex items-center gap-1.5 whitespace-nowrap">
+            <div class="flex items-center gap-2 whitespace-nowrap">
               <span
                 v-if="item.source === 'collection'"
-                class="h-2.5 w-2.5 min-w-2.5 rounded-full"
+                class="h-2.25 w-2.25 min-w-2.25 rounded-full"
                 :style="{
                   backgroundColor: getEnvColor(environment),
                 }"></span>
               <ScalarIcon
                 v-else
-                class="w-2.5"
+                class="-ml-1/2 h-2.5 w-2.5"
                 icon="Globe" />
               {{ item.key }}
             </div>
diff --git a/packages/api-client/src/views/Environment/handle-drag.ts b/packages/api-client/src/views/Environment/handle-drag.ts
new file mode 100644
index 00000000000..bef0dd49fa7
--- /dev/null
+++ b/packages/api-client/src/views/Environment/handle-drag.ts
@@ -0,0 +1,72 @@
+import type { ActiveEntitiesStore } from '@/store/active-entities'
+import type { DraggingItem, HoveredItem } from '@scalar/draggable'
+import type { Collection } from '@scalar/oas-utils/entities/spec'
+
+type CollectionMutator = {
+  edit: (uid: Collection['uid'], path: 'x-scalar-environments', value: Collection['x-scalar-environments']) => void
+}
+
+/** Create environment DnD handlers */
+export function environmentDragHandlerFactory(
+  activeWorkspaceCollections: ActiveEntitiesStore['activeWorkspaceCollections'],
+  collectionMutator: CollectionMutator,
+) {
+  /** Drag handler that mutates the collection's environments order */
+  const handleDragEnd = (draggingItem: DraggingItem, hoveredItem: HoveredItem) => {
+    if (!draggingItem || !hoveredItem) {
+      return
+    }
+
+    const { id: draggingUid, parentId: draggingParentUid } = draggingItem
+    const { id: hoveredUid, parentId: hoveredParentUid, offset } = hoveredItem
+
+    // Only allow reordering within the same collection
+    if (draggingParentUid !== hoveredParentUid) {
+      return
+    }
+
+    const collection = activeWorkspaceCollections.value.find((c) => c.uid === draggingParentUid)
+
+    if (!collection || !collection['x-scalar-environments']) {
+      return
+    }
+
+    const environments = collection['x-scalar-environments']
+    const envKeys = Object.keys(environments)
+    const orderedEnvs = { ...environments }
+
+    // Remove dragged item from its position
+    const draggedIndex = envKeys.findIndex((key) => key === draggingUid)
+    envKeys.splice(draggedIndex, 1)
+
+    // Insert at new position
+    const hoveredIndex = envKeys.findIndex((key) => key === hoveredUid)
+    const targetIndex = hoveredIndex + (offset === 1 ? 1 : 0)
+    envKeys.splice(targetIndex, 0, draggingUid)
+
+    // Rebuild ordered environments object
+    const reorderedEnvs = envKeys.reduce(
+      (acc, key) => {
+        const env = environments[key]
+        if (env) {
+          acc[key] = env
+        }
+        return acc
+      },
+      {} as typeof orderedEnvs,
+    )
+
+    collection['x-scalar-environments'] = reorderedEnvs
+    collectionMutator.edit(collection.uid, 'x-scalar-environments', collection['x-scalar-environments'])
+  }
+
+  /** Ensure only environments within the same collection can be dropped */
+  const isDroppable = (draggingItem: DraggingItem, hoveredItem: HoveredItem) => {
+    return draggingItem.parentId === hoveredItem.parentId
+  }
+
+  return {
+    handleDragEnd,
+    isDroppable,
+  }
+}
diff --git a/packages/api-client/src/views/Request/RequestSection/RequestParams.vue b/packages/api-client/src/views/Request/RequestSection/RequestParams.vue
index 6fd924d02a2..caee32c3468 100644
--- a/packages/api-client/src/views/Request/RequestSection/RequestParams.vue
+++ b/packages/api-client/src/views/Request/RequestSection/RequestParams.vue
@@ -40,10 +40,17 @@ const {
 }>()
 
 const { requestExampleMutators } = useWorkspace()
+const isTooltipReady = ref(false)
 
 const params = computed(() => example.parameters[paramKey] ?? [])
 
-onMounted(() => defaultRow())
+onMounted(() => {
+  nextTick(() => {
+    // TODO: currently waiting for the tooltip to be ready otherwise error is thrown on mount
+    isTooltipReady.value = true
+    defaultRow()
+  })
+})
 
 /** Add a new row to a given parameter list */
 const addRow = () => {
@@ -164,7 +171,9 @@ const itemCount = computed(
   () => params.value.filter((param) => param.key || param.value).length,
 )
 
-const showTooltip = computed(() => params.value.length > 1)
+const showTooltip = computed(
+  () => isTooltipReady.value && params.value.length > 1,
+)
 
 watch(
   () => example,
diff --git a/packages/api-client/src/views/Request/RequestSection/RequestSection.test.ts b/packages/api-client/src/views/Request/RequestSection/RequestSection.test.ts
index 49f691f014f..64c57a07c2d 100644
--- a/packages/api-client/src/views/Request/RequestSection/RequestSection.test.ts
+++ b/packages/api-client/src/views/Request/RequestSection/RequestSection.test.ts
@@ -46,7 +46,7 @@ describe('RequestSection', () => {
         operation: {
           // @ts-expect-error
           uid: '1',
-          method: 'get',
+          method: 'post',
           path: '/test',
           summary: 'Test Operation',
         },
diff --git a/packages/api-client/src/views/Request/RequestSection/RequestSection.vue b/packages/api-client/src/views/Request/RequestSection/RequestSection.vue
index 03d2b917fdd..6718c404c80 100644
--- a/packages/api-client/src/views/Request/RequestSection/RequestSection.vue
+++ b/packages/api-client/src/views/Request/RequestSection/RequestSection.vue
@@ -227,8 +227,7 @@ const labelRequestNameId = useId()
         :readOnlyEntries="activeWorkspaceCookies"
         :role="selectedFilter === 'All' ? 'none' : 'tabpanel'"
         title="Cookies"
-        :workspace="workspace"
-        workspaceParamKey="cookies" />
+        :workspace="workspace" />
       <RequestParams
         v-show="selectedFilter === 'All' || selectedFilter === 'Headers'"
         :id="filterIds.Headers"
@@ -258,7 +257,7 @@ const labelRequestNameId = useId()
         title="Query Parameters"
         :workspace="workspace" />
       <RequestBody
-        v-show="
+        v-if="
           operation.method &&
           (selectedFilter === 'All' || selectedFilter === 'Body') &&
           canMethodHaveBody(operation.method)
diff --git a/packages/oas-utils/src/entities/environment/environment.ts b/packages/oas-utils/src/entities/environment/environment.ts
index a2e783f1342..a8d3a98a274 100644
--- a/packages/oas-utils/src/entities/environment/environment.ts
+++ b/packages/oas-utils/src/entities/environment/environment.ts
@@ -1,11 +1,11 @@
 import { z } from 'zod'
 
-import { type ENTITY_BRANDS, nanoidSchema } from '@scalar/types/utils'
+import type { ENTITY_BRANDS } from '@scalar/types/utils'
 
 export const environmentSchema = z.object({
-  uid: nanoidSchema.brand<ENTITY_BRANDS['ENVIRONMENT']>(),
+  uid: z.string().brand<ENTITY_BRANDS['ENVIRONMENT']>(),
   name: z.string().optional().default('Default Environment'),
-  color: z.string().optional().default('#0082D0'),
+  color: z.string().optional().default('#FFFFFF'),
   value: z.string().default(''),
   isDefault: z.boolean().optional(),
 })


Reviews:
user: login: amritk
body: Code looks good! Did not test but as its broken now it should only get better eh 🙂 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-24T22:46:28Z
user: login: antlio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T05:36:56Z
user: login: hanspagel
body: works (again), great!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-28T12:36:40Z

Review comments:
diff_hunk: @@ -53,13 +54,25 @@ describe('CodeInput', () => {
     }
   })
 
+  const defaultProps = {
+    modelValue: 'test value',
+    environment: environmentSchema.parse({
+      uid: nanoid(),
path: packages/api-client/src/components/CodeInput/CodeInput.test.ts
user: login: amritk
body: ```suggestion
```
uids are automatically added!
created_at: 2025-04-24T22:44:53Z
author_association: MEMBER
diff_hunk: @@ -53,13 +54,25 @@ describe('CodeInput', () => {
     }
   })
 
+  const defaultProps = {
+    modelValue: 'test value',
+    environment: environmentSchema.parse({
+      uid: nanoid(),
+      name: 'Test Environment',
+      value: 'test',
+      color: '#000000',
+    }),
+    envVariables: [],
+    workspace: workspaceSchema.parse({
+      uid: nanoid(),
path: packages/api-client/src/components/CodeInput/CodeInput.test.ts
user: login: amritk
body: ```suggestion
```
created_at: 2025-04-24T22:45:03Z
author_association: MEMBER
diff_hunk: @@ -53,13 +54,25 @@ describe('CodeInput', () => {
     }
   })
 
+  const defaultProps = {
+    modelValue: 'test value',
+    environment: environmentSchema.parse({
+      uid: nanoid(),
path: packages/api-client/src/components/CodeInput/CodeInput.test.ts
user: login: antlio
body: does it still after the last commit update? (as we remove the nanoid schema from env entity)
created_at: 2025-04-25T05:36:56Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-21T19:20:40Z
body: ###  🦋  Changeset detected

Latest commit: 96a33949d6a1f7adbbcc8f57b009ce8cc32deb6d

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 11 packages</summary>

  | Name                          | Type  |
| ----------------------------- | ----- |
| @scalar/api-client            | Patch |
| @scalar/nuxt                  | Patch |
| @scalar/api-client-react      | Patch |
| @scalar/api-reference-editor  | Patch |
| @scalar/api-reference         | Patch |
| @scalar/play-button           | Patch |
| scalar-app                    | Patch |
| @scalar/aspnetcore            | Patch |
| @scalar/fastify-api-reference | Patch |
| @scalar/api-reference-react   | Patch |
| @scalar/cli                   | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/scalar/scalar/new/anthony/fix-api-client-environment-global?filename=.changeset/tender-bottles-argue.md&value=---%0A%22%40scalar%2Fapi-client%22%3A%20patch%0A%22%40scalar%2Foas-utils%22%3A%20patch%0A---%0A%0Afix(api-client)%3A%20environment%20collection%0A)


author_association: NONE
user: login: relativeci[bot]
created_at: 2025-04-21T19:30:36Z
body: ## [#9505](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9505-DENNlfmSFb6fNYXmUFBA?utm_source=github&utm_campaign=pr-report&utm_content=title "Bundle Size — 2.44MiB (+0.07%).") Bundle Size — 2.44MiB (+0.07%).
96a3394(current) vs f88d5cc **[main#9503](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9503-iXa2oQqufyrdCqsKheoc?utm_source=github&utm_content=report-header&utm_campaign=pr-report "View baseline job report")**(baseline)

> [!WARNING]
> Bundle contains 6 duplicate packages – [View duplicate packages](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9505-DENNlfmSFb6fNYXmUFBA/packages?bp=%7B%22filters%22%3A%22duplicate-1%22%7D&utm_source=github&utm_campaign=pr-report&utm_content=insights)
<details>
<summary><strong>Bundle metrics</strong>&nbsp;&nbsp;<img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-blue.svg?sanitize=true" width="8" alt="Change" align="center"> <code>3 changes</code> <img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-red.svg?sanitize=true" width="8" alt="Regression" align="center"> <code>1 regression</code></summary>
<br>


|            |       Current<br />[#9505](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9505-DENNlfmSFb6fNYXmUFBA?utm_source=github&utm_content=summary&utm_campaign=pr-report "View bundle analysis report") |      Baseline<br />[#9503](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9503-iXa2oQqufyrdCqsKheoc?utm_source=github&utm_content=summary&utm_campaign=pr-report "View baseline bundle analysis report") |
|:--|--:|--:|
| <strong><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-red.svg?sanitize=true" width="8" alt="Regression" align="center">&nbsp;&nbsp;[Initial JS](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9505-DENNlfmSFb6fNYXmUFBA/assets?ba=%7B%22filters%22%3A%22et.entrypoint-0_et.initial-1_et.chunk-0_et.asset-0_ft.CSS-0_ft.JS-1_ft.IMG-0_ft.MEDIA-0_ft.FONT-0_ft.HTML-0_ft.OTHER-0%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View initial JS assets")</strong> | <strong>`2.44MiB`(`+0.07%`)</strong> | <strong>`2.43MiB`</strong> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Initial CSS](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9505-DENNlfmSFb6fNYXmUFBA/assets?ba=%7B%22filters%22%3A%22et.entrypoint-0_et.initial-1_et.chunk-0_et.asset-0_ft.CSS-1_ft.JS-0_ft.IMG-0_ft.MEDIA-0_ft.FONT-0_ft.HTML-0_ft.OTHER-0%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View initial CSS assets")</span> | <span>`0B`</span> | <span>`0B`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-blue.svg?sanitize=true" width="8" alt="Change" align="center">&nbsp;&nbsp;[Cache Invalidation](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9505-DENNlfmSFb6fNYXmUFBA/assets?ba=%7B%22filters%22%3A%22changed-1%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View changed assets")</span> | <span>`100%`</span> | <span>`0%`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Chunks](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9505-DENNlfmSFb6fNYXmUFBA/assets?ba=%7B%22filters%22%3A%22et.entrypoint-0_et.initial-0_et.chunk-1_et.asset-0%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View chunks")</span> | <span>`1`</span> | <span>`1`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Assets](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9505-DENNlfmSFb6fNYXmUFBA/assets?ba=%7B%22filters%22%3A%22changed-0%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View assets")</span> | <span>`1`</span> | <span>`1`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-blue.svg?sanitize=true" width="8" alt="Change" align="center">&nbsp;&nbsp;[Modules](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9505-DENNlfmSFb6fNYXmUFBA/modules?bm=%7B%22metric%22%3A%22totalSize%22%2C%22filters%22%3A%22changed-0%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View modules")</span> | <span>`1493`(`+0.07%`)</span> | <span>`1492`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Duplicate Modules](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9505-DENNlfmSFb6fNYXmUFBA/modules?bm=%7B%22metric%22%3A%22duplicateSize%22%2C%22filters%22%3A%22changed-0_md-1%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View duplicate modules")</span> | <span>`0`</span> | <span>`0`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Duplicate Code](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9505-DENNlfmSFb6fNYXmUFBA/modules?bm=%7B%22metric%22%3A%22duplicateSize%22%2C%22filters%22%3A%22changed-0_md-1%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View duplicate modules")</span> | <span>`0%`</span> | <span>`0%`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Packages](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9505-DENNlfmSFb6fNYXmUFBA/packages?bp=%7B%22filters%22%3A%22changed-0%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View packages")</span> | <span>`172`</span> | <span>`172`</span> |
| <span><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-gray.svg?sanitize=true" width="8" alt="No change" align="center">&nbsp;&nbsp;[Duplicate Packages](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9505-DENNlfmSFb6fNYXmUFBA/packages?bp=%7B%22filters%22%3A%22duplicate-1%22%7D&utm_source=github&utm_content=summary&utm_campaign=pr-report "View duplicate packages")</span> | <span>`6`</span> | <span>`6`</span> |
</details>


<details>
<summary><strong>Bundle size by type</strong>&nbsp;&nbsp;<img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-blue.svg?sanitize=true" width="8" alt="Change" align="center"> <code>1 change</code> <img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-red.svg?sanitize=true" width="8" alt="Regression" align="center"> <code>1 regression</code></summary>
<br>


|            |       Current<br />[#9505](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9505-DENNlfmSFb6fNYXmUFBA?utm_source=github&utm_content=totals&utm_campaign=pr-report "View bundle analysis report") |      Baseline<br />[#9503](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9503-iXa2oQqufyrdCqsKheoc?utm_source=github&utm_content=totals&utm_campaign=pr-report "View baseline bundle analysis report") |
|:--|--:|--:|
| <strong><img src="https://raw.githubusercontent.com/relative-ci/relative-ci-github-app-assets/v0.0.4/reports/circle-red.svg?sanitize=true" width="8" alt="Regression" align="center">&nbsp;&nbsp;[JS](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9505-DENNlfmSFb6fNYXmUFBA/assets?ba=%7B%22filters%22%3A%22ft.CSS-0_ft.JS-1_ft.IMG-0_ft.MEDIA-0_ft.FONT-0_ft.HTML-0_ft.OTHER-0%22%7D&utm_source=github&utm_content=totals&utm_campaign=pr-report "View JS assets")</strong> | <strong>`2.44MiB` (`+0.07%`)</strong> | <strong>`2.43MiB`</strong> |
</details>


[Bundle analysis report](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/jobs/9505-DENNlfmSFb6fNYXmUFBA?utm_source=github&utm_content=report-footer&utm_campaign=pr-report) [Branch **anthony/fix-api-client-environme...**](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX/tree/anthony%2Ffix-api-client-environment-global?utm_source=github&utm_content=report-footer&utm_campaign=pr-report) [Project dashboard](https://app.relative-ci.com/projects/WjV7m24scsHglgIEsSBX?utm_source=github&utm_content=report-footer&utm_campaign=pr-report)---<sup>Generated by [RelativeCI](https://relative-ci.com/?utm_source=github&utm_content=report-footer&utm_campaign=pr-report) [Documentation](https://relative-ci.com/documentation?utm_source=github&utm_content=report-footer&utm_campaign=pr-report) [Report issue](https://github.com/relative-ci/roadmap/issues)</sup>
<!--RELATIVE_CI_PROJECT_NAME="scalar"-->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-21T19:30:49Z
body: **Cloudflare Preview for the API Client**

https://0d650dbf.client-staging-7m0.pages.dev
<!-- thollander/actions-comment-pull-request "cloudflare-preview" -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-21T19:31:27Z
body: **Preview Examples**

https://035e901b-1c05-4bae-9f69-16b1debb3f9f--scalar-deploy-preview.netlify.app
<!-- thollander/actions-comment-pull-request "netlify-preview" -->
author_association: CONTRIBUTOR
user: login: hanspagel
created_at: 2025-04-25T13:18:18Z
body: I just tested it and we don’t seem to replace the environment variables with their values.

See how it’s detected in the address bar, but still sent as {{foo}} in the request.

<img width="1494" alt="Screenshot 2025-04-25 at 15 17 27" src="https://github.com/user-attachments/assets/90fa09e2-84c1-499c-abf0-f319dad468be" />

<img width="722" alt="Screenshot 2025-04-25 at 15 18 13" src="https://github.com/user-attachments/assets/937b6295-c68d-4a62-b3ed-6e9178b603e5" />

author_association: MEMBER
user: login: antlio
created_at: 2025-04-25T13:24:54Z
body: > I just tested it and we don’t seem to replace the environment variables with their values.
> 
> See how it’s detected in the address bar, but still sent as {{foo}} in the request.

yes it's currently broken - i fixed this in another PR - or should i cherry pick and add it here?

author_association: CONTRIBUTOR
user: login: hanspagel
created_at: 2025-04-25T14:02:43Z
body: let’s just get the other PR in then ✌️ 
author_association: MEMBER