Repository: enzonotario/vitepress-openapi (ts)

Issue:
Single column layout
### What would you like?

I'd like to be able to render the openapi specification with a signle column layout. It would be nice if it is configurable in the parameters of the components like OASpec or globally in the theme config. In addition to that, it would also be nice if the different child components could be enabled/disabled (i.e. turn off samples). 

### Why is this needed?

I want this because the standard theme does not have a lot of content width, Having a two column layout looks crowded and sometimes  the content is wrapped. Having all the child components below each other with a single column layout would bring more clarity to the rendered documentation.

### How could it be implemented?

_No response_

### Other information

_No response_

Pull request description:
feat(useTheme): allows to configure operation's slots and cols
<!-- Provide a general summary of your changes in the title above. -->

# Description

Allows to configure `OAOperation` slots and cols via `useTheme` for better customization.

## Related issues/external references

Closes #91 

## Types of changes

- New feature 


Patch:
diff --git a/dev/.vitepress/theme/components/ThemeConfiguration.vue b/dev/.vitepress/theme/components/ThemeConfiguration.vue
index f38702d..bbcbaa2 100644
--- a/dev/.vitepress/theme/components/ThemeConfiguration.vue
+++ b/dev/.vitepress/theme/components/ThemeConfiguration.vue
@@ -1,49 +1,122 @@
 <script setup>
 import { useTheme } from 'vitepress-openapi'
+import { onMounted, ref, watch } from 'vue'
+import { useUrlSearchParams } from '@vueuse/core'
+import { compressToURL, decompressFromURL } from '@amoutonbrady/lz-string'
+import { DEFAULT_OPERATION_SLOTS } from '../../../../src/composables/useTheme'
+
+const params = useUrlSearchParams('history')
+
+const sticky = ref(true)
 
 const themeConfig = useTheme()
+
+watch(() => themeConfig.getState(), (state) => {
+  params.themeConfig = compressToURL(JSON.stringify(state))
+}, { deep: true })
+
+onMounted(() => {
+  useTheme(params.themeConfig ? JSON.parse(decompressFromURL(params.themeConfig)) : {})
+})
 </script>
 
 <template>
-  <div class="grid grid-cols-3 items-start gap-4 p-2 bg-muted mb-4">
-    <label for="themeConfig" class="col-span-3 text-lg font-bold">
-      Theme Configuration
-    </label>
-
-    <label for="groupByTags" class="flex items-center gap-2">
-      <input
-        id="groupByTags"
-        type="checkbox"
-        :checked="themeConfig.getSpecConfig().groupByTags.value"
-        @change="themeConfig.setSpecConfig({ groupByTags: $event.target.checked })"
+  <div
+    :class="{
+      'sticky top-[65px] max-h-[250px] z-50 ': sticky,
+    }"
+    class="flex flex-col gap-2 p-2 bg-white border-b"
+  >
+    <div class="flex justify-between items-center">
+      <h3>Theme Configuration</h3>
+      <button
+        class="text-sm hover:bg-primary hover:text-primary-foreground rounded px-2 py-1"
+        @click="sticky = !sticky"
       >
-      Group by tags
-    </label>
-
-    <div class="flex flex-col">
-      <label for="collapsePaths" class="flex items-center gap-2">
-        <input
-          id="collapsePaths"
-          type="checkbox"
-          :checked="themeConfig.getSpecConfig().collapsePaths.value"
-          @change="themeConfig.setSpecConfig({ collapsePaths: $event.target.checked })"
-        >
-        Collapse paths
-
-      </label>
-      <small class="text-gray-500 dark:text-gray-400">
-        Toggle `Group by tags` to see the effect
-      </small>
+        {{ sticky ? 'Unstick' : 'Stick' }}
+      </button>
     </div>
 
-    <label for="showPathsSummary" class="flex items-center gap-2">
-      <input
-        id="showPathsSummary"
-        type="checkbox"
-        :checked="themeConfig.getSpecConfig().showPathsSummary.value"
-        @change="themeConfig.setSpecConfig({ showPathsSummary: $event.target.checked })"
-      >
-      Show paths summary
-    </label>
+    <div
+      :class="{
+        'overflow-y-auto': sticky,
+      }"
+      class="flex flex-col gap-2"
+    >
+      <h3>Spec</h3>
+      <div class="grid grid-cols-3 items-start gap-4">
+        <label for="groupByTags" class="flex items-center gap-2">
+          <input
+            id="groupByTags"
+            type="checkbox"
+            :checked="themeConfig.getSpecConfig().groupByTags.value"
+            @change="themeConfig.setSpecConfig({ groupByTags: $event.target.checked })"
+          >
+          Group by tags
+        </label>
+
+        <div class="flex flex-col">
+          <label for="collapsePaths" class="flex items-center gap-2">
+            <input
+              id="collapsePaths"
+              type="checkbox"
+              :checked="themeConfig.getSpecConfig().collapsePaths.value"
+              @change="themeConfig.setSpecConfig({ collapsePaths: $event.target.checked })"
+            >
+            Collapse paths
+          </label>
+          <small class="text-gray-500 dark:text-gray-400">
+            Toggle `Group by tags` to see the effect
+          </small>
+        </div>
+
+        <label for="showPathsSummary" class="flex items-center gap-2">
+          <input
+            id="showPathsSummary"
+            type="checkbox"
+            :checked="themeConfig.getSpecConfig().showPathsSummary.value"
+            @change="themeConfig.setSpecConfig({ showPathsSummary: $event.target.checked })"
+          >
+          Show paths summary
+        </label>
+      </div>
+
+      <h3>Operation</h3>
+      <div class="grid grid-cols-2 gap-4">
+        <div>
+          <h5>Hidden Slots</h5>
+          <div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
+            <label v-for="slot in DEFAULT_OPERATION_SLOTS" :key="slot" class="flex items-center gap-2">
+              <input
+                type="checkbox"
+                :checked="themeConfig.getOperationHiddenSlots().includes(slot)"
+                @change="themeConfig.getOperationHiddenSlots().includes(slot) ? themeConfig.getOperationHiddenSlots().splice(themeConfig.getOperationHiddenSlots().indexOf(slot), 1) : themeConfig.getOperationHiddenSlots().push(slot)"
+              >
+              {{ slot }}
+            </label>
+          </div>
+        </div>
+
+        <div>
+          <h5>Operation Columns</h5>
+          <div class="grid grid-cols-2 gap-2">
+            <label v-for="col in [1, 2]" :key="col" class="flex items-center gap-2">
+              <input
+                type="radio"
+                :checked="themeConfig.getOperationCols() === col"
+                @change="themeConfig.setOperationCols(col)"
+              >
+              {{ col }}
+            </label>
+          </div>
+        </div>
+      </div>
+    </div>
   </div>
 </template>
+
+<style scoped>
+h3 {
+  margin-top: 0;
+}
+</style>
diff --git a/dev/criptoya-argentina/index.md b/dev/criptoya-argentina/index.md
index 2fff151..ada22c7 100644
--- a/dev/criptoya-argentina/index.md
+++ b/dev/criptoya-argentina/index.md
@@ -34,7 +34,7 @@ onUnmounted(() => {
         schemaViewer: {
             deep: Infinity,
         },
-        request: {
+        requestBody: {
             defaultView: 'json',
         },
     })
diff --git a/dev/one-page.md b/dev/one-page.md
index 164d73b..5e5e615 100644
--- a/dev/one-page.md
+++ b/dev/one-page.md
@@ -11,6 +11,10 @@ import ThemeConfiguration from './.vitepress/theme/components/ThemeConfiguration
 const { isDark } = useData()
 </script>
 
+<div class="relative flex flex-col gap-4">
+
 <ThemeConfiguration />
 
 <OASpec :isDark="isDark" />
+
+</div>
diff --git a/docs/composables/useTheme.md b/docs/composables/useTheme.md
index 23f1b18..5b1a0cf 100644
--- a/docs/composables/useTheme.md
+++ b/docs/composables/useTheme.md
@@ -50,6 +50,23 @@ export default {
             operation: {
                 // Set the operation badges. The order is respected.
                 badges: ['deprecated'],
+                // Slots to render in the OAOperation component.
+                slots: [
+                    'header',
+                    'path',
+                    'description',
+                    'security',
+                    'request-body',
+                    'responses',
+                    'try-it',
+                    'code-samples',
+                    'branding',
+                    'footer',
+                ],
+                // Slots to hide in the OAOperation component.
+                hiddenSlots: [],
+                // Set the number of columns to use in the OAOperation component.
+                cols: 2,
             },
             // Set the i18n configuration.
             i18n: {
diff --git a/docs/example/one-page.md b/docs/example/one-page.md
index de5c691..b971a41 100644
--- a/docs/example/one-page.md
+++ b/docs/example/one-page.md
@@ -11,6 +11,10 @@ import ThemeConfiguration from '@dev/.vitepress/theme/components/ThemeConfigurat
 const { isDark } = useData()
 </script>
 
+<div class="relative flex flex-col gap-4">
+
 <ThemeConfiguration />
 
 <OASpec :isDark="isDark" />
+
+</div>
diff --git a/package.json b/package.json
index 22b421a..987fffe 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "vitepress-openapi",
   "type": "module",
-  "version": "0.0.3-alpha.43",
+  "version": "0.0.3-alpha.44",
   "packageManager": "pnpm@9.1.1",
   "homepage": "https://vitepress-openapi.vercel.app/",
   "repository": {
@@ -55,6 +55,7 @@
     "tailwindcss-animate": "^1.0.7"
   },
   "devDependencies": {
+    "@amoutonbrady/lz-string": "^0.1.0",
     "@antfu/eslint-config": "^2.27.1",
     "@sindresorhus/slugify": "^2.2.1",
     "@trojs/openapi-dereference": "^0.2.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index df00437..26867cc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -33,6 +33,9 @@ importers:
         specifier: ^1.0.7
         version: 1.0.7(tailwindcss@3.4.10)
     devDependencies:
+      '@amoutonbrady/lz-string':
+        specifier: ^0.1.0
+        version: 0.1.0
       '@antfu/eslint-config':
         specifier: ^2.27.1
         version: 2.27.3(@typescript-eslint/utils@8.3.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.5.4))(@vue/compiler-sfc@3.5.12)(eslint-plugin-format@0.1.2(eslint@9.10.0(jiti@1.21.6)))(eslint@9.10.0(jiti@1.21.6))(svelte@4.2.19)(typescript@5.5.4)(vitest@2.0.5(@types/node@22.5.1)(sass@1.79.2))
@@ -192,6 +195,9 @@ packages:
     resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
     engines: {node: '>=10'}
 
+  '@amoutonbrady/lz-string@0.1.0':
+    resolution: {integrity: sha512-WsUuFexe01PUHKD8SUdiV3OQmauZWKyoo/7bJx3pj1l5SxLSe5//l1nQcU3DJlYL4JnhA8SqImT0/1xgUt6k5g==}
+
   '@ampproject/remapping@2.3.0':
     resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
     engines: {node: '>=6.0.0'}
@@ -3105,6 +3111,8 @@ snapshots:
 
   '@alloc/quick-lru@5.2.0': {}
 
+  '@amoutonbrady/lz-string@0.1.0': {}
+
   '@ampproject/remapping@2.3.0':
     dependencies:
       '@jridgewell/gen-mapping': 0.3.5
diff --git a/src/components/Operation/OAOperation.vue b/src/components/Operation/OAOperation.vue
index aaf2056..6c5b4fe 100644
--- a/src/components/Operation/OAOperation.vue
+++ b/src/components/Operation/OAOperation.vue
@@ -106,29 +106,6 @@ function hasSlot(name) {
       </div>
     </template>
 
-    <template
-      v-if="hasSlot('path-mobile')"
-      #path-mobile="pathMobile"
-    >
-      <slot
-        name="path-mobile"
-        v-bind="pathMobile"
-      />
-    </template>
-    <template
-      v-else
-      #path-mobile="pathMobile"
-    >
-      <OAPathEndpoint
-        :path="pathMobile.path"
-        :method="pathMobile.method"
-        :base-url="pathMobile.baseUrl"
-        :hide-base-url="pathMobile.hideBaseUrl"
-        :deprecated="pathMobile.deprecated"
-        class="sm:hidden"
-      />
-    </template>
-
     <template
       v-if="hasSlot('description')"
       #description="description"
@@ -263,7 +240,6 @@ function hasSlot(name) {
         :base-url="path.baseUrl"
         :hide-base-url="path.hideBaseUrl"
         :deprecated="path.deprecated"
-        class="hidden sm:flex"
       />
     </template>
 
diff --git a/src/components/Path/OAPath.vue b/src/components/Path/OAPath.vue
index 6e59035..2ceebb1 100644
--- a/src/components/Path/OAPath.vue
+++ b/src/components/Path/OAPath.vue
@@ -1,5 +1,5 @@
 <script setup>
-import { defineProps } from 'vue'
+import { computed, defineProps } from 'vue'
 import { useTheme } from 'vitepress-openapi/composables/useTheme'
 import { getOpenApiInstance } from 'vitepress-openapi'
 
@@ -35,6 +35,10 @@ const operationParameters = operationParsed?.parameters
 const operationRequestBody = operationParsed?.requestBody?.content?.['application/json']?.schema
 
 const operationResponses = operationParsed?.responses
+
+const operationSlots = computed(() => themeConfig.getOperationSlots().filter(slot => !themeConfig.getOperationHiddenSlots().includes(slot)))
+
+const operationCols = computed(() => themeConfig.getOperationCols())
 </script>
 
 <template>
@@ -43,123 +47,168 @@ const operationResponses = operationParsed?.responses
       v-if="operation"
       class="OAPath flex flex-col space-y-8"
     >
-      <slot
-        name="header"
-        :operation="operation"
-        :method="operationMethod"
-        :base-url="baseUrl"
-        :path="operationPath"
-        :deprecated="operation.deprecated"
-      />
-
-      <div class="relative grid grid-cols-1 sm:grid-cols-2 gap-4">
+      <template v-if="operationSlots.includes('header')">
+        <slot
+          name="header"
+          :operation="operation"
+          :method="operationMethod"
+          :base-url="baseUrl"
+          :path="operationPath"
+          :deprecated="operation.deprecated"
+        />
+      </template>
+
+      <div
+        :class="{
+          'sm:grid-cols-1': operationCols === 1,
+          'sm:grid-cols-2': operationCols === 2,
+        }"
+        class="relative grid grid-cols-1 gap-4"
+      >
         <div class="OAPathContentStart flex flex-col">
-          <div class="flex flex-col">
-            <slot
-              name="path-mobile"
-              :operation-id="props.id"
-              :operation="operation"
-              :method="operationMethod"
-              :base-url="baseUrl"
-              :path="operationPath"
-              :hide-base-url="!themeConfig.getShowBaseURL()"
-              :deprecated="operation.deprecated"
-            />
+          <div class="flex flex-col gap-4">
+            <div
+              v-if="operationSlots.includes('path')"
+              :class="{
+                'sm:hidden': operationCols === 2,
+              }"
+            >
+              <slot
+                name="path"
+                :operation-id="props.id"
+                :operation="operation"
+                :method="operationMethod"
+                :base-url="baseUrl"
+                :path="operationPath"
+                :hide-base-url="!themeConfig.getShowBaseURL()"
+                :deprecated="operation.deprecated"
+              />
+            </div>
+
+            <template v-if="operationSlots.includes('description')">
+              <slot
+                name="description"
+                :operation="operation"
+                :method="operationMethod"
+                :base-url="baseUrl"
+                :path="operationPath"
+              />
+            </template>
+          </div>
 
+          <template v-if="operationSlots.includes('security')">
             <slot
-              name="description"
+              v-if="Object.keys(securitySchemes).length"
+              name="security"
               :operation="operation"
               :method="operationMethod"
               :base-url="baseUrl"
               :path="operationPath"
+              :security-schemes="securitySchemes"
             />
-          </div>
+          </template>
 
-          <slot
-            v-if="Object.keys(securitySchemes).length"
-            name="security"
-            :operation="operation"
-            :method="operationMethod"
-            :base-url="baseUrl"
-            :path="operationPath"
-            :security-schemes="securitySchemes"
-          />
-
-          <slot
-            v-if="operationParameters?.length"
-            name="parameters"
-            :operation-id="props.id"
-            :parameters="operationParameters"
-          />
-
-          <slot
-            v-if="operationRequestBody"
-            name="request-body"
-            :operation-id="props.id"
-            :schema="operationRequestBody"
-          />
-
-          <slot
-            v-if="operationResponses"
-            name="responses"
-            :operation-id="props.id"
-            :responses="operationResponses"
-          />
-        </div>
-
-        <div class="flex flex-col">
-          <div class="OAPathContentEnd sticky top-[100px] inset-x-0 flex flex-col space-y-4">
+          <template v-if="operationSlots.includes('parameters')">
             <slot
-              name="path"
+              v-if="operationParameters?.length"
+              name="parameters"
               :operation-id="props.id"
-              :operation="operation"
-              :method="operationMethod"
-              :base-url="baseUrl"
-              :path="operationPath"
-              :hide-base-url="!themeConfig.getShowBaseURL()"
-              :deprecated="operation.deprecated"
+              :parameters="operationParameters"
             />
+          </template>
 
+          <template v-if="operationSlots.includes('request-body')">
             <slot
-              name="try-it"
+              v-if="operationRequestBody"
+              name="request-body"
               :operation-id="props.id"
-              :path="operationPath"
-              :method="operationMethod"
-              :base-url="baseUrl"
-              :parameters="operationParameters"
               :schema="operationRequestBody"
-              :security-schemes="securitySchemes"
             />
+          </template>
 
+          <template v-if="operationSlots.includes('responses')">
             <slot
-              name="code-samples"
+              v-if="operationResponses"
+              name="responses"
               :operation-id="props.id"
-              :operation="operation"
-              :method="operationMethod"
-              :base-url="baseUrl"
-              :path="operationPath"
+              :responses="operationResponses"
             />
+          </template>
+        </div>
+
+        <div class="flex flex-col">
+          <div
+            class="OAPathContentEnd sticky top-[100px] inset-x-0 flex flex-col space-y-4"
+            :class="{
+              '!px-0': operationCols === 1,
+            }"
+          >
+            <div
+              v-if="operationSlots.includes('path') && operationCols === 2"
+              :class="{
+                'hidden sm:block': operationCols === 2,
+              }"
+            >
+              <slot
+                name="path"
+                :operation-id="props.id"
+                :operation="operation"
+                :method="operationMethod"
+                :base-url="baseUrl"
+                :path="operationPath"
+                :hide-base-url="!themeConfig.getShowBaseURL()"
+                :deprecated="operation.deprecated"
+              />
+            </div>
+
+            <template v-if="operationSlots.includes('try-it')">
+              <slot
+                name="try-it"
+                :operation-id="props.id"
+                :path="operationPath"
+                :method="operationMethod"
+                :base-url="baseUrl"
+                :parameters="operationParameters"
+                :schema="operationRequestBody"
+                :security-schemes="securitySchemes"
+              />
+            </template>
+
+            <template v-if="operationSlots.includes('code-samples')">
+              <slot
+                name="code-samples"
+                :operation-id="props.id"
+                :operation="operation"
+                :method="operationMethod"
+                :base-url="baseUrl"
+                :path="operationPath"
+              />
+            </template>
           </div>
         </div>
       </div>
 
-      <slot
-        name="branding"
-        :operation-id="props.id"
-        :operation="operation"
-        :method="operationMethod"
-        :base-url="baseUrl"
-        :path="operationPath"
-      />
-
-      <slot
-        name="footer"
-        :operation-id="props.id"
-        :operation="operation"
-        :method="operationMethod"
-        :base-url="baseUrl"
-        :path="operationPath"
-      />
+      <template v-if="operationSlots.includes('branding')">
+        <slot
+          name="branding"
+          :operation-id="props.id"
+          :operation="operation"
+          :method="operationMethod"
+          :base-url="baseUrl"
+          :path="operationPath"
+        />
+      </template>
+
+      <template v-if="operationSlots.includes('footer')">
+        <slot
+          name="footer"
+          :operation-id="props.id"
+          :operation="operation"
+          :method="operationMethod"
+          :base-url="baseUrl"
+          :path="operationPath"
+        />
+      </template>
     </div>
   </div>
 </template>
diff --git a/src/components/Path/OAPaths.vue b/src/components/Path/OAPaths.vue
index 2b5a133..fbf4455 100644
--- a/src/components/Path/OAPaths.vue
+++ b/src/components/Path/OAPaths.vue
@@ -2,6 +2,7 @@
 import { defineProps } from 'vue'
 import OALazy from 'vitepress-openapi/components/Common/Lazy/OALazy.vue'
 import type { OperationSlot } from 'vitepress-openapi/types'
+import { useTheme } from 'vitepress-openapi'
 
 const { openapi, paths, isDark } = defineProps({
   openapi: {
@@ -20,6 +21,8 @@ const { openapi, paths, isDark } = defineProps({
 
 const slots = defineSlots<OperationSlot>()
 
+const themeConfig = useTheme()
+
 const operations = Object.entries(paths).reduce((acc, [_, path]) => {
   return [
     ...acc,
@@ -30,13 +33,15 @@ const operations = Object.entries(paths).reduce((acc, [_, path]) => {
     }),
   ]
 }, [])
+
+const lazyRendering = themeConfig.getSpecConfig().lazyRendering.value
 </script>
 
 <template>
   <OALazy
     v-for="(operation, operationIdx) in operations"
     :key="operation.operationId"
-    :is-lazy="operationIdx > 0"
+    :is-lazy="lazyRendering && operationIdx > 0"
   >
     <OAOperation
       :operation-id="operation.operationId"
diff --git a/src/components/Path/OAPathsByTags.vue b/src/components/Path/OAPathsByTags.vue
index f85aad3..e49d296 100644
--- a/src/components/Path/OAPathsByTags.vue
+++ b/src/components/Path/OAPathsByTags.vue
@@ -103,7 +103,7 @@ const lazyRendering = themeConfig.getSpecConfig().lazyRendering.value
   <OALazy
     v-for="(tagPaths, tagIdx) in internalTags"
     :key="tagPaths.tag"
-    :is-lazy="tagIdx > 0"
+    :is-lazy="lazyRendering && tagIdx > 0"
   >
     <Collapsible
       v-if="Object.keys(tagPaths.paths).length"
diff --git a/src/composables/useTheme.ts b/src/composables/useTheme.ts
index 9e5971e..b15baa6 100644
--- a/src/composables/useTheme.ts
+++ b/src/composables/useTheme.ts
@@ -3,6 +3,7 @@ import vitesseLight from 'shiki/themes/vitesse-light.mjs'
 import vitesseDark from 'shiki/themes/vitesse-dark.mjs'
 import { deepUnref } from '../lib/deepUnref'
 import { locales } from '../locales'
+import type { OperationSlot } from '../types'
 
 export interface ThemeConfig {
   highlighterTheme: {
@@ -11,9 +12,12 @@ export interface ThemeConfig {
   }
 }
 
+export interface PathConfig {
+  showBaseURL: Ref<boolean>
+}
+
 export interface RequestConfig {
   defaultView: Ref<'schema' | 'contentType'>
-  showBaseURL: Ref<boolean>
 }
 
 export interface JsonViewerConfig {
@@ -56,7 +60,10 @@ export interface SecurityConfig {
 type OperationBadges = 'deprecated' | 'operationId'
 
 export interface OperationConfig {
-  badges: Ref<OperationBadges[]>
+  badges?: Ref<OperationBadges[]>
+  slots?: Ref<OperationSlot[]>
+  hiddenSlots?: Ref<OperationSlot[]>
+  cols?: Ref<1 | 2>
 }
 
 export interface I18nConfig {
@@ -75,7 +82,8 @@ export interface SpecConfig {
 
 export interface UseThemeConfig {
   theme?: Partial<ThemeConfig>
-  request?: Partial<RequestConfig>
+  path?: Partial<PathConfig>
+  requestBody?: Partial<RequestConfig>
   jsonViewer?: Partial<JsonViewerConfig>
   schemaViewer?: Partial<SchemaViewerConfig>
   headingLevels?: Partial<HeadingLevels>
@@ -87,6 +95,19 @@ export interface UseThemeConfig {
   spec?: Partial<SpecConfig>
 }
 
+export const DEFAULT_OPERATION_SLOTS: OperationSlot[] = [
+  'header',
+  'path',
+  'description',
+  'security',
+  'request-body',
+  'responses',
+  'try-it',
+  'code-samples',
+  'branding',
+  'footer',
+]
+
 const themeConfig: UseThemeConfig = {
   theme: {
     highlighterTheme: {
@@ -94,10 +115,12 @@ const themeConfig: UseThemeConfig = {
       dark: vitesseDark,
     },
   },
-  request: {
-    defaultView: ref<'schema' | 'contentType'>('contentType'),
+  path: {
     showBaseURL: ref<boolean>(false),
   },
+  requestBody: {
+    defaultView: ref<'schema' | 'contentType'>('contentType'),
+  },
   jsonViewer: {
     deep: ref<number>(Number.POSITIVE_INFINITY),
   },
@@ -129,6 +152,9 @@ const themeConfig: UseThemeConfig = {
   },
   operation: {
     badges: ref<OperationBadges[]>(['deprecated']),
+    slots: ref(DEFAULT_OPERATION_SLOTS),
+    hiddenSlots: ref([]),
+    cols: ref(2),
   },
   i18n: {
     locale: ref<'es' | 'en'>('en'),
@@ -155,12 +181,12 @@ export function useTheme(config: Partial<UseThemeConfig> = {}) {
       }
     }
 
-    if (config?.request?.defaultView !== undefined) {
-      setSchemaDefaultView(config.request.defaultView)
+    if (config?.requestBody?.defaultView !== undefined) {
+      setSchemaDefaultView(config.requestBody.defaultView)
     }
 
-    if (config?.request?.showBaseURL !== undefined) {
-      setShowBaseURL(config.request.showBaseURL)
+    if (config?.path?.showBaseURL !== undefined) {
+      setShowBaseURL(config.path.showBaseURL)
     }
 
     if (config?.jsonViewer?.deep !== undefined) {
@@ -207,6 +233,18 @@ export function useTheme(config: Partial<UseThemeConfig> = {}) {
       setOperationBadges(config.operation.badges)
     }
 
+    if (config?.operation?.slots !== undefined) {
+      setOperationSlots(config.operation.slots)
+    }
+
+    if (config?.operation?.hiddenSlots !== undefined) {
+      setOperationHiddenSlots(config.operation.hiddenSlots)
+    }
+
+    if (config?.operation?.cols !== undefined) {
+      setOperationCols(config.operation.cols)
+    }
+
     if (config?.i18n !== undefined) {
       setI18nConfig(config.i18n)
     }
@@ -220,6 +258,10 @@ export function useTheme(config: Partial<UseThemeConfig> = {}) {
     useTheme({ ...defaultThemeConfig })
   }
 
+  function getState() {
+    return deepUnref(themeConfig)
+  }
+
   function getLocale(): 'es' | 'en' | string {
     return themeConfig.i18n.locale.value
   }
@@ -238,19 +280,19 @@ export function useTheme(config: Partial<UseThemeConfig> = {}) {
   }
 
   function getSchemaDefaultView(): 'schema' | 'contentType' {
-    return themeConfig.request.defaultView.value
+    return themeConfig.requestBody.defaultView.value
   }
 
   function setSchemaDefaultView(value: 'schema' | 'contentType') {
-    themeConfig.request.defaultView.value = value
+    themeConfig.requestBody.defaultView.value = value
   }
 
   function getShowBaseURL(): boolean {
-    return themeConfig.request.showBaseURL.value
+    return themeConfig.path.showBaseURL.value
   }
 
   function setShowBaseURL(value: boolean) {
-    themeConfig.request.showBaseURL.value = value
+    themeConfig.path.showBaseURL.value = value
   }
 
   function getJsonViewerDeep(): number {
@@ -355,6 +397,30 @@ export function useTheme(config: Partial<UseThemeConfig> = {}) {
     themeConfig.operation.badges.value = value
   }
 
+  function getOperationSlots(): OperationSlot[] {
+    return themeConfig.operation.slots.value
+  }
+
+  function setOperationSlots(value: OperationSlot[]) {
+    themeConfig.operation.slots.value = value
+  }
+
+  function getOperationHiddenSlots(): OperationSlot[] {
+    return themeConfig.operation.hiddenSlots.value
+  }
+
+  function setOperationHiddenSlots(value: OperationSlot[]) {
+    themeConfig.operation.hiddenSlots.value = value
+  }
+
+  function getOperationCols(): 1 | 2 {
+    return themeConfig.operation.cols.value
+  }
+
+  function setOperationCols(value: number) {
+    themeConfig.operation.cols.value = value
+  }
+
   function getI18nConfig(): I18nConfig {
     return themeConfig.i18n
   }
@@ -400,8 +466,9 @@ export function useTheme(config: Partial<UseThemeConfig> = {}) {
   }
 
   return {
-    schemaConfig: themeConfig.request,
+    schemaConfig: themeConfig.requestBody,
     reset,
+    getState,
     getLocale,
     setLocale,
     getHighlighterTheme,
@@ -432,6 +499,12 @@ export function useTheme(config: Partial<UseThemeConfig> = {}) {
     setSecuritySelectedScheme,
     getOperationBadges,
     setOperationBadges,
+    getOperationSlots,
+    setOperationSlots,
+    getOperationHiddenSlots,
+    setOperationHiddenSlots,
+    getOperationCols,
+    setOperationCols,
     getI18nConfig,
     setI18nConfig,
     getSpecConfig,
diff --git a/src/types.ts b/src/types.ts
index dbd66fe..c017b5f 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -4,7 +4,6 @@
 export type OperationSlot =
   | 'header'
   | 'path'
-  | 'path-mobile'
   | 'description'
   | 'security'
   | 'request-body'
@@ -13,3 +12,8 @@ export type OperationSlot =
   | 'code-samples'
   | 'branding'
   | 'footer'
+  // TODO: Implement these slots.
+  // | 'summary'
+  // | 'tags'
+  // | 'servers'
+  // | 'parameters'


Test patch:
diff --git a/test/composables/useTheme.test.ts b/test/composables/useTheme.test.ts
index 650e1cf..e7b1943 100644
--- a/test/composables/useTheme.test.ts
+++ b/test/composables/useTheme.test.ts
@@ -9,10 +9,12 @@ describe('composition API', () => {
         light: {},
         dark: {},
       },
-      request: {
-        defaultView: 'schema',
+      path: {
         showBaseURL: true,
       },
+      requestBody: {
+        defaultView: 'schema',
+      },
       jsonViewer: {
         deep: 5,
       },