@scalar/api-client 3.5.1 → 3.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +0 -1
  5. package/dist/style.css +3933 -4768
  6. package/dist/styles/tailwind.config.css +20 -0
  7. package/dist/styles/utilities.css +45 -0
  8. package/dist/v2/blocks/operation-code-sample/components/OperationCodeSample.vue.d.ts.map +1 -1
  9. package/dist/v2/blocks/operation-code-sample/components/OperationCodeSample.vue.js +1 -1
  10. package/dist/v2/blocks/operation-code-sample/components/OperationCodeSample.vue.js.map +1 -1
  11. package/dist/v2/blocks/operation-code-sample/components/OperationCodeSample.vue.script.js +1 -1
  12. package/dist/v2/blocks/operation-code-sample/components/OperationCodeSample.vue.script.js.map +1 -1
  13. package/dist/v2/blocks/request-block/RequestBlock.vue.script.js.map +1 -1
  14. package/dist/v2/blocks/scalar-auth-selector-block/components/AuthSelector.vue.script.js.map +1 -1
  15. package/dist/v2/components/data-table/DataTableInput.vue.d.ts +1 -1
  16. package/dist/v2/components/data-table/DataTableInput.vue.d.ts.map +1 -1
  17. package/dist/v2/constants.js +1 -1
  18. package/dist/v2/features/app/App.vue.d.ts +15 -31
  19. package/dist/v2/features/app/App.vue.d.ts.map +1 -1
  20. package/dist/v2/features/app/App.vue.js.map +1 -1
  21. package/dist/v2/features/app/App.vue.script.js +131 -50
  22. package/dist/v2/features/app/App.vue.script.js.map +1 -1
  23. package/dist/v2/features/app/app-state.d.ts +10 -14
  24. package/dist/v2/features/app/app-state.d.ts.map +1 -1
  25. package/dist/v2/features/app/app-state.js +53 -21
  26. package/dist/v2/features/app/app-state.js.map +1 -1
  27. package/dist/v2/features/app/components/AppHeader.vue.d.ts.map +1 -1
  28. package/dist/v2/features/app/components/AppHeader.vue.js.map +1 -1
  29. package/dist/v2/features/app/components/AppHeader.vue.script.js +4 -3
  30. package/dist/v2/features/app/components/AppHeader.vue.script.js.map +1 -1
  31. package/dist/v2/features/app/components/AppHeaderActions.vue.d.ts +32 -0
  32. package/dist/v2/features/app/components/AppHeaderActions.vue.d.ts.map +1 -0
  33. package/dist/v2/features/app/components/AppHeaderActions.vue.js +7 -0
  34. package/dist/v2/features/app/components/AppHeaderActions.vue.js.map +1 -0
  35. package/dist/v2/features/app/components/AppHeaderActions.vue.script.js +172 -0
  36. package/dist/v2/features/app/components/AppHeaderActions.vue.script.js.map +1 -0
  37. package/dist/v2/features/app/components/AppSidebar.vue.d.ts +2 -5
  38. package/dist/v2/features/app/components/AppSidebar.vue.d.ts.map +1 -1
  39. package/dist/v2/features/app/components/AppSidebar.vue.js +1 -1
  40. package/dist/v2/features/app/components/AppSidebar.vue.js.map +1 -1
  41. package/dist/v2/features/app/components/AppSidebar.vue.script.js +4 -9
  42. package/dist/v2/features/app/components/AppSidebar.vue.script.js.map +1 -1
  43. package/dist/v2/features/app/components/DesktopTabs.vue.d.ts.map +1 -1
  44. package/dist/v2/features/app/components/DesktopTabs.vue.js.map +1 -1
  45. package/dist/v2/features/app/components/DesktopTabs.vue.script.js +2 -2
  46. package/dist/v2/features/app/components/DesktopTabs.vue.script.js.map +1 -1
  47. package/dist/v2/features/app/components/DocumentBreadcrumb.vue.d.ts +1 -2
  48. package/dist/v2/features/app/components/DocumentBreadcrumb.vue.d.ts.map +1 -1
  49. package/dist/v2/features/app/components/DocumentBreadcrumb.vue.js +1 -1
  50. package/dist/v2/features/app/components/DocumentBreadcrumb.vue.js.map +1 -1
  51. package/dist/v2/features/app/components/DocumentBreadcrumb.vue.script.js +4 -35
  52. package/dist/v2/features/app/components/DocumentBreadcrumb.vue.script.js.map +1 -1
  53. package/dist/v2/features/app/components/DocumentSyncIndicator.vue.d.ts +1 -1
  54. package/dist/v2/features/app/components/DocumentSyncIndicator.vue.d.ts.map +1 -1
  55. package/dist/v2/features/app/components/PublishDocumentModal.vue.d.ts +77 -0
  56. package/dist/v2/features/app/components/PublishDocumentModal.vue.d.ts.map +1 -0
  57. package/dist/v2/features/app/components/PublishDocumentModal.vue.js +7 -0
  58. package/dist/v2/features/app/components/PublishDocumentModal.vue.js.map +1 -0
  59. package/dist/v2/features/app/components/PublishDocumentModal.vue.script.js +209 -0
  60. package/dist/v2/features/app/components/PublishDocumentModal.vue.script.js.map +1 -0
  61. package/dist/v2/features/app/components/SyncConflictResolutionEditor.vue.d.ts.map +1 -0
  62. package/dist/v2/features/{collection → app}/components/SyncConflictResolutionEditor.vue.js +2 -2
  63. package/dist/v2/features/app/components/SyncConflictResolutionEditor.vue.js.map +1 -0
  64. package/dist/v2/features/{collection → app}/components/SyncConflictResolutionEditor.vue.script.js +1 -1
  65. package/dist/v2/features/{collection/components/SyncConflictResolutionEditor.vue.js.map → app/components/SyncConflictResolutionEditor.vue.script.js.map} +1 -1
  66. package/dist/v2/features/app/helpers/check-version-conflict.d.ts +8 -5
  67. package/dist/v2/features/app/helpers/check-version-conflict.d.ts.map +1 -1
  68. package/dist/v2/features/app/helpers/check-version-conflict.js +10 -7
  69. package/dist/v2/features/app/helpers/check-version-conflict.js.map +1 -1
  70. package/dist/v2/features/app/helpers/create-api-client-app.d.ts +8 -5
  71. package/dist/v2/features/app/helpers/create-api-client-app.d.ts.map +1 -1
  72. package/dist/v2/features/app/helpers/create-api-client-app.js +2 -2
  73. package/dist/v2/features/app/helpers/create-api-client-app.js.map +1 -1
  74. package/dist/v2/features/app/helpers/load-registry-document.d.ts +1 -10
  75. package/dist/v2/features/app/helpers/load-registry-document.d.ts.map +1 -1
  76. package/dist/v2/features/app/helpers/load-registry-document.js +6 -5
  77. package/dist/v2/features/app/helpers/load-registry-document.js.map +1 -1
  78. package/dist/v2/features/app/helpers/registry-error-messages.d.ts +23 -0
  79. package/dist/v2/features/app/helpers/registry-error-messages.d.ts.map +1 -0
  80. package/dist/v2/features/app/helpers/registry-error-messages.js +63 -0
  81. package/dist/v2/features/app/helpers/registry-error-messages.js.map +1 -0
  82. package/dist/v2/features/app/hooks/use-active-document-version.d.ts +2 -1
  83. package/dist/v2/features/app/hooks/use-active-document-version.d.ts.map +1 -1
  84. package/dist/v2/features/app/hooks/use-active-document-version.js.map +1 -1
  85. package/dist/v2/features/app/hooks/use-document-sync.d.ts +126 -0
  86. package/dist/v2/features/app/hooks/use-document-sync.d.ts.map +1 -0
  87. package/dist/v2/features/app/hooks/use-document-sync.js +448 -0
  88. package/dist/v2/features/app/hooks/use-document-sync.js.map +1 -0
  89. package/dist/v2/features/app/hooks/use-network-status.d.ts +29 -0
  90. package/dist/v2/features/app/hooks/use-network-status.d.ts.map +1 -0
  91. package/dist/v2/features/app/hooks/use-network-status.js +58 -0
  92. package/dist/v2/features/app/hooks/use-network-status.js.map +1 -0
  93. package/dist/v2/features/app/hooks/use-sidebar-documents.d.ts +1 -25
  94. package/dist/v2/features/app/hooks/use-sidebar-documents.d.ts.map +1 -1
  95. package/dist/v2/features/app/hooks/use-sidebar-documents.js.map +1 -1
  96. package/dist/v2/features/app/index.d.ts +1 -1
  97. package/dist/v2/features/app/index.d.ts.map +1 -1
  98. package/dist/v2/features/collection/DocumentCollection.vue.d.ts.map +1 -1
  99. package/dist/v2/features/collection/DocumentCollection.vue.js.map +1 -1
  100. package/dist/v2/features/collection/DocumentCollection.vue.script.js +43 -277
  101. package/dist/v2/features/collection/DocumentCollection.vue.script.js.map +1 -1
  102. package/dist/v2/features/command-palette/components/CommandPaletteExample.vue.d.ts.map +1 -1
  103. package/dist/v2/features/command-palette/components/CommandPaletteExample.vue.js.map +1 -1
  104. package/dist/v2/features/command-palette/components/CommandPaletteExample.vue.script.js +35 -17
  105. package/dist/v2/features/command-palette/components/CommandPaletteExample.vue.script.js.map +1 -1
  106. package/dist/v2/features/command-palette/components/CommandPaletteImport.vue.d.ts.map +1 -1
  107. package/dist/v2/features/command-palette/components/CommandPaletteImport.vue.js.map +1 -1
  108. package/dist/v2/features/command-palette/components/CommandPaletteImport.vue.script.js +15 -13
  109. package/dist/v2/features/command-palette/components/CommandPaletteImport.vue.script.js.map +1 -1
  110. package/dist/v2/features/command-palette/components/CommandPaletteImportCurl.vue.d.ts.map +1 -1
  111. package/dist/v2/features/command-palette/components/CommandPaletteImportCurl.vue.js +1 -1
  112. package/dist/v2/features/command-palette/components/CommandPaletteImportCurl.vue.js.map +1 -1
  113. package/dist/v2/features/command-palette/components/CommandPaletteImportCurl.vue.script.js +51 -39
  114. package/dist/v2/features/command-palette/components/CommandPaletteImportCurl.vue.script.js.map +1 -1
  115. package/dist/v2/features/command-palette/components/CommandPaletteOpenApiDocument.vue.d.ts.map +1 -1
  116. package/dist/v2/features/command-palette/components/CommandPaletteOpenApiDocument.vue.js.map +1 -1
  117. package/dist/v2/features/command-palette/components/CommandPaletteOpenApiDocument.vue.script.js +30 -14
  118. package/dist/v2/features/command-palette/components/CommandPaletteOpenApiDocument.vue.script.js.map +1 -1
  119. package/dist/v2/features/command-palette/components/CommandPaletteRequest.vue.d.ts.map +1 -1
  120. package/dist/v2/features/command-palette/components/CommandPaletteRequest.vue.js.map +1 -1
  121. package/dist/v2/features/command-palette/components/CommandPaletteRequest.vue.script.js +44 -42
  122. package/dist/v2/features/command-palette/components/CommandPaletteRequest.vue.script.js.map +1 -1
  123. package/dist/v2/features/command-palette/components/CommandPaletteTag.vue.d.ts.map +1 -1
  124. package/dist/v2/features/command-palette/components/CommandPaletteTag.vue.js.map +1 -1
  125. package/dist/v2/features/command-palette/components/CommandPaletteTag.vue.script.js +33 -17
  126. package/dist/v2/features/command-palette/components/CommandPaletteTag.vue.script.js.map +1 -1
  127. package/dist/v2/features/command-palette/helpers/load-document-from-source.d.ts.map +1 -1
  128. package/dist/v2/features/command-palette/helpers/load-document-from-source.js +3 -2
  129. package/dist/v2/features/command-palette/helpers/load-document-from-source.js.map +1 -1
  130. package/dist/v2/features/editor/hooks/use-three-way-merge-editor.d.ts.map +1 -1
  131. package/dist/v2/features/editor/hooks/use-three-way-merge-editor.js +5 -5
  132. package/dist/v2/features/editor/hooks/use-three-way-merge-editor.js.map +1 -1
  133. package/dist/v2/helpers/is-url.d.ts.map +1 -1
  134. package/dist/v2/helpers/is-url.js +2 -1
  135. package/dist/v2/helpers/is-url.js.map +1 -1
  136. package/dist/v2/types/configuration.d.ts +273 -7
  137. package/dist/v2/types/configuration.d.ts.map +1 -1
  138. package/dist/vue-styles.css +1389 -0
  139. package/package.json +20 -14
  140. package/dist/v2/features/app/components/DocumentSyncIndicator.vue.js +0 -7
  141. package/dist/v2/features/app/components/DocumentSyncIndicator.vue.js.map +0 -1
  142. package/dist/v2/features/app/components/DocumentSyncIndicator.vue.script.js +0 -51
  143. package/dist/v2/features/app/components/DocumentSyncIndicator.vue.script.js.map +0 -1
  144. package/dist/v2/features/collection/components/SyncConflictResolutionEditor.vue.d.ts.map +0 -1
  145. package/dist/v2/features/collection/components/SyncConflictResolutionEditor.vue.script.js.map +0 -1
  146. /package/dist/v2/features/{collection → app}/components/SyncConflictResolutionEditor.vue.d.ts +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"DocumentCollection.vue.script.js","names":[],"sources":["../../../../src/v2/features/collection/DocumentCollection.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Document Collection Page\n *\n * Displays primary document editing and viewing interface, enabling users to:\n * - Choose a document icon\n * - Edit the document title\n * - Navigate among Overview, Servers, Authentication, Environment, Cookies, and Settings tabs\n */\nexport default {\n name: 'DocumentCollection',\n}\n</script>\n\n<script setup lang=\"ts\">\nimport {\n ScalarButton,\n ScalarModal,\n ScalarSavePrompt,\n useLoadingState,\n useModal,\n} from '@scalar/components'\nimport {\n ScalarIconCloudArrowDown,\n ScalarIconDownload,\n ScalarIconFloppyDisk,\n ScalarIconSpinner,\n ScalarIconWarning,\n} from '@scalar/icons'\nimport { LibraryIcon } from '@scalar/icons/library'\nimport { apply, type Difference, type merge } from '@scalar/json-magic/diff'\nimport { useToasts } from '@scalar/use-toasts'\nimport { deepClone } from '@scalar/workspace-store/helpers/deep-clone'\nimport { computed, ref } from 'vue'\nimport { RouterView } from 'vue-router'\n\nimport IconSelector from '@/components/IconSelector.vue'\nimport type { RouteProps } from '@/v2/features/app/helpers/routes'\nimport { downloadAsFile } from '@/v2/helpers/download-document'\n\nimport LabelInput from './components/LabelInput.vue'\nimport SyncConflictResolutionEditor from './components/SyncConflictResolutionEditor.vue'\nimport Tabs from './components/Tabs.vue'\n\nconst props = defineProps<RouteProps>()\n\n/** Snag the title from the info object */\nconst title = computed(() => props.document?.info?.title ?? '')\n\n/** Default to the folder icon */\nconst icon = computed(\n () => props.document?.['x-scalar-icon'] || 'interface-content-folder',\n)\n\nconst syncModal = useModal()\nconst dirtyBeforeSyncModal = useModal()\n\nconst isDocumentDirty = computed(\n () => props.document?.['x-scalar-is-dirty'] === true,\n)\n\nconst saveLoader = useLoadingState()\n\nconst documentSourceUrl = computed(\n () => props.document?.['x-scalar-original-source-url'] as string | undefined,\n)\n\nconst documentRegistryMeta = computed(\n () =>\n props.document?.['x-scalar-registry-meta'] as\n | {\n namespace: string\n slug: string\n version: string\n commitHash?: string\n }\n | undefined,\n)\n\n/** Show Sync when the document has a source URL or registry meta (registry can be used if fetchRegistryDocument is set). */\nconst canShowSyncButton = computed(\n () =>\n documentSourceUrl.value !== undefined ||\n documentRegistryMeta.value !== undefined,\n)\n\nconst { toast } = useToasts()\n\nconst undoChanges = () => {\n props.workspaceStore.revertDocumentChanges(props.documentSlug)\n}\n\nconst saveChanges = async () => {\n saveLoader.start()\n const res = await props.workspaceStore.saveDocument(props.documentSlug)\n await (res ? saveLoader.validate() : saveLoader.invalidate({ persist: true }))\n}\n\n/** Downloads the document as a JSON file using the last saved state. */\nconst downloadDocument = () => {\n const content = props.workspaceStore.exportDocument(\n props.documentSlug,\n 'json',\n false,\n )\n if (!content) return\n const baseName = title.value.replace(/[^\\w\\s-]/g, '').trim() || 'document'\n downloadAsFile(content, `${baseName}.json`)\n}\n\nconst handleSaveThenCloseDirtyModal = async () => {\n await props.workspaceStore.saveDocument(props.documentSlug)\n dirtyBeforeSyncModal.hide()\n await handleSyncFlow()\n}\n\nconst handleDiscardThenCloseDirtyModal = async () => {\n await props.workspaceStore.revertDocumentChanges(props.documentSlug)\n dirtyBeforeSyncModal.hide()\n await handleSyncFlow()\n}\n\nconst isSyncInProgress = ref(false)\n\nconst rebaseResult = ref<{\n originalDocument: Record<string, unknown>\n resolvedDocument: Record<string, unknown>\n conflicts: ReturnType<typeof merge>['conflicts']\n applyChanges: (\n applyChangesInput:\n | {\n resolvedConflicts: Difference<unknown>[]\n }\n | {\n resolvedDocument: Record<string, unknown>\n },\n ) => Promise<void>\n} | null>(null)\n\n/**\n * Resolves the source for syncing. Registry meta has priority over x-scalar-original-source-url\n * when fetchRegistryDocument is provided. Returns either a URL or the full document object.\n */\nconst resolveSyncInput = async (): Promise<\n { url: string } | { document: Record<string, unknown> } | null\n> => {\n const registryMeta = documentRegistryMeta.value\n if (registryMeta && props.fetchRegistryDocument) {\n try {\n const result = await props.fetchRegistryDocument({\n namespace: registryMeta.namespace,\n slug: registryMeta.slug,\n version: registryMeta.version,\n })\n if (!result.ok) {\n toast(result.error, 'error')\n return null\n }\n return { document: result.data }\n } catch (err) {\n toast('Failed to resolve document from registry', 'error')\n return null\n }\n }\n const url = documentSourceUrl.value\n if (url) {\n return { url }\n }\n return null\n}\n\n/**\n * Handles actions to perform when synchronization is complete.\n * Hides the sync modal, resets the sync progress flag, and emits the\n * 'hooks:on:rebase:document:complete' event with document metadata.\n */\nconst onSyncComplete = () => {\n syncModal.hide()\n isSyncInProgress.value = false\n // Display the toast to show that the sync is complete\n toast(\n 'Your document has been rebased with the latest version from the source.',\n 'info',\n )\n // Emit the event to notify other components that the sync is complete\n props.eventBus.emit('hooks:on:rebase:document:complete', {\n meta: {\n documentName: props.documentSlug,\n },\n })\n}\n\n/**\n * Handles errors that occur during synchronization.\n * If an error string is provided, it displays the error via toast.\n * Always resets the sync progress flag.\n */\nconst onSyncError = (error: string | null) => {\n if (error !== null) {\n toast(error, 'error')\n }\n isSyncInProgress.value = false\n}\n\n/**\n * Handles the synchronization flow for a document.\n * Checks for unsaved changes, resolves source (registry over URL),\n * initiates rebasing, handles conflicts, and emits completion events.\n * If conflicts are detected, a modal dialog is shown for user resolution.\n */\nconst handleSyncFlow = async () => {\n if (isDocumentDirty.value) {\n dirtyBeforeSyncModal.show()\n return\n }\n\n if (isSyncInProgress.value) {\n return\n }\n\n isSyncInProgress.value = true\n\n const input = await resolveSyncInput()\n if (!input) {\n onSyncError(null)\n return\n }\n\n const result = await props.workspaceStore.rebaseDocument({\n name: props.documentSlug,\n ...input,\n })\n\n if (result?.ok) {\n const originalDocument =\n props.workspaceStore.getOriginalDocument(props.documentSlug) ?? {}\n rebaseResult.value = {\n conflicts: result.conflicts,\n applyChanges: result.applyChanges,\n resolvedDocument: apply(deepClone(originalDocument), result.changes),\n originalDocument,\n }\n\n if (rebaseResult.value.conflicts.length > 0) {\n syncModal.show()\n } else {\n // If there is no conflict just rebase immediately\n await rebaseResult.value?.applyChanges({\n resolvedDocument: rebaseResult.value.resolvedDocument,\n })\n onSyncComplete()\n }\n } else if (result?.ok === false && result.type === 'NO_CHANGES_DETECTED') {\n // Emit the event either way even if there was no need to rebase the document\n onSyncComplete()\n } else {\n onSyncError('Failed to sync document')\n }\n}\n\n/*\n * Handles applying changes to the current document after conflict resolution.\n * Emits a completion event and hides the sync modal dialog.\n */\nconst handleApplyChanges = async ({\n resolvedDocument,\n}: {\n resolvedDocument: Record<string, unknown>\n}) => {\n await rebaseResult.value?.applyChanges({ resolvedDocument })\n props.eventBus.emit('hooks:on:rebase:document:complete', {\n meta: {\n documentName: props.documentSlug,\n },\n })\n syncModal.hide()\n}\n\n/**\n * Resets sync state when the sync conflict modal is closed (dismissed or after\n * applying changes). Ensures the Sync button is re-enabled and conflict state\n * is cleared.\n */\nconst onSyncModalClose = () => {\n isSyncInProgress.value = false\n rebaseResult.value = null\n}\n</script>\n\n<template>\n <div class=\"custom-scroll h-full\">\n <div\n v-if=\"document\"\n class=\"md:max-w-content w-full px-3 md:mx-auto\">\n <!-- Header -->\n <div\n :aria-label=\"`title: ${title}`\"\n class=\"md:max-w-content mx-auto flex h-fit w-full flex-col gap-2 pt-14 pb-3 md:pt-6\">\n <ScalarSavePrompt\n v-model=\"isDocumentDirty\"\n class=\"w-content-padded-4 max-w-full-padded-4 absolute\"\n :loader=\"saveLoader\"\n @discard=\"undoChanges\"\n @save=\"saveChanges\" />\n <div class=\"flex flex-row items-center justify-between gap-2\">\n <div class=\"flex min-w-0 items-center gap-2\">\n <IconSelector\n :modelValue=\"icon\"\n placement=\"bottom-start\"\n @update:modelValue=\"\n (icon) => eventBus.emit('document:update:icon', icon)\n \">\n <ScalarButton\n class=\"hover:bg-b-2 aspect-square h-7 w-7 cursor-pointer rounded border border-transparent p-0 hover:border-inherit\"\n variant=\"ghost\">\n <LibraryIcon\n class=\"text-c-2 size-5\"\n :src=\"icon\"\n stroke-width=\"2\" />\n </ScalarButton>\n </IconSelector>\n\n <div class=\"group relative ml-1.25 min-w-0\">\n <LabelInput\n class=\"text-xl font-bold\"\n inputId=\"documentName\"\n :modelValue=\"title\"\n @update:modelValue=\"\n (title) => eventBus.emit('document:update:info', { title })\n \" />\n </div>\n </div>\n\n <ScalarButton\n class=\"text-c-2 hover:text-c-1 flex shrink-0 items-center gap-2\"\n size=\"xs\"\n type=\"button\"\n variant=\"ghost\"\n @click=\"downloadDocument\">\n <ScalarIconDownload\n size=\"sm\"\n thickness=\"1.5\" />\n <span>Download document</span>\n </ScalarButton>\n\n <ScalarButton\n v-if=\"canShowSyncButton\"\n class=\"text-c-2 hover:text-c-1 shrink-0 gap-1.5\"\n data-testid=\"document-sync-button\"\n :disabled=\"isSyncInProgress\"\n size=\"xs\"\n :title=\"'Pull the latest version from the document source and merge with your local copy. Save your changes first if you have unsaved edits.'\"\n type=\"button\"\n variant=\"ghost\"\n @click=\"handleSyncFlow\">\n <ScalarIconSpinner\n v-if=\"isSyncInProgress\"\n class=\"size-3.5 animate-spin\"\n size=\"sm\" />\n <ScalarIconCloudArrowDown\n v-else\n class=\"size-3.5\"\n size=\"sm\"\n thickness=\"1.5\" />\n <span>Sync from source</span>\n </ScalarButton>\n </div>\n </div>\n\n <!-- Tabs -->\n <Tabs type=\"document\" />\n\n <!-- Router views -->\n <div class=\"px-1.5 pt-8 pb-20\">\n <RouterView v-slot=\"{ Component }\">\n <component\n :is=\"Component\"\n v-bind=\"props\"\n collectionType=\"document\" />\n </RouterView>\n </div>\n </div>\n\n <!-- Document not found -->\n <div\n v-else\n class=\"flex w-full flex-1 items-center justify-center\">\n <div class=\"flex h-full flex-col items-center justify-center\">\n <h1 class=\"text-2xl font-bold\">Document not found</h1>\n <p class=\"text-gray-500\">\n The document you are looking for does not exist.\n </p>\n </div>\n </div>\n </div>\n <ScalarModal\n bodyClass=\"border-t-0 rounded-t-lg flex flex-col gap-5\"\n size=\"xs\"\n :state=\"dirtyBeforeSyncModal\"\n title=\"Sync requires saved document\"\n @close=\"dirtyBeforeSyncModal.hide()\">\n <div class=\"flex flex-col gap-5\">\n <div class=\"flex gap-3\">\n <div\n aria-hidden=\"true\"\n class=\"bg-b-3 text-c-2 flex size-10 shrink-0 items-center justify-center rounded-lg\">\n <ScalarIconWarning class=\"text-yellow size-5\" />\n </div>\n <div class=\"min-w-0 flex-1 space-y-1\">\n <p class=\"text-c-1 text-sm leading-snug font-medium\">\n You have unsaved changes\n </p>\n <p class=\"text-c-2 text-sm leading-relaxed\">\n Save your work to keep changes, or discard to revert to the last\n saved version. Then you can sync with the source.\n </p>\n </div>\n </div>\n <div class=\"flex flex-wrap items-center justify-end gap-2 border-t pt-4\">\n <ScalarButton\n size=\"sm\"\n type=\"button\"\n variant=\"ghost\"\n @click=\"dirtyBeforeSyncModal.hide()\">\n Cancel\n </ScalarButton>\n <ScalarButton\n size=\"sm\"\n type=\"button\"\n variant=\"outlined\"\n @click=\"handleDiscardThenCloseDirtyModal\">\n Discard changes\n </ScalarButton>\n <ScalarButton\n class=\"flex items-center gap-2\"\n size=\"sm\"\n type=\"button\"\n variant=\"solid\"\n @click=\"handleSaveThenCloseDirtyModal\">\n <ScalarIconFloppyDisk\n size=\"sm\"\n thickness=\"1.5\" />\n Save and continue\n </ScalarButton>\n </div>\n </div>\n </ScalarModal>\n <ScalarModal\n v-if=\"rebaseResult\"\n bodyClass=\"sync-conflict-modal-root flex h-dvh flex-col p-4\"\n maxWidth=\"calc(100dvw - 32px)\"\n size=\"full\"\n :state=\"syncModal\"\n @close=\"onSyncModalClose\">\n <div class=\"flex h-full w-full flex-col gap-4 overflow-hidden\">\n <SyncConflictResolutionEditor\n :baseDocument=\"rebaseResult.originalDocument\"\n :conflicts=\"rebaseResult.conflicts\"\n :resolvedDocument=\"rebaseResult.resolvedDocument\"\n @applyChanges=\"(payload) => handleApplyChanges(payload)\" />\n </div>\n </ScalarModal>\n</template>\n\n<style>\n.full-size-styles:has(.sync-conflict-modal-root) {\n width: 100dvw !important;\n max-width: 100dvw !important;\n border-right: none !important;\n}\n\n.full-size-styles:has(.sync-conflict-modal-root)::after {\n display: none;\n}\n</style>\n"],"mappings":"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"}
1
+ {"version":3,"file":"DocumentCollection.vue.script.js","names":[],"sources":["../../../../src/v2/features/collection/DocumentCollection.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Document Collection Page\n *\n * Displays primary document editing and viewing interface, enabling users to:\n * - Choose a document icon\n * - Edit the document title\n * - Navigate among Overview, Servers, Authentication, Environment, Cookies, and Settings tabs\n */\nexport default {\n name: 'DocumentCollection',\n}\n</script>\n\n<script setup lang=\"ts\">\nimport { ScalarButton } from '@scalar/components'\nimport { ScalarIconDownload } from '@scalar/icons'\nimport { LibraryIcon } from '@scalar/icons/library'\nimport { computed } from 'vue'\nimport { RouterView } from 'vue-router'\n\nimport IconSelector from '@/components/IconSelector.vue'\nimport type { RouteProps } from '@/v2/features/app/helpers/routes'\nimport { downloadAsFile } from '@/v2/helpers/download-document'\n\nimport LabelInput from './components/LabelInput.vue'\nimport Tabs from './components/Tabs.vue'\n\nconst props = defineProps<RouteProps>()\n\n/** Snag the title from the info object */\nconst title = computed(() => props.document?.info?.title ?? '')\n\n/** Default to the folder icon */\nconst icon = computed(\n () => props.document?.['x-scalar-icon'] || 'interface-content-folder',\n)\n\n/** Downloads the document as a JSON file using the last saved state. */\nconst downloadDocument = () => {\n const content = props.workspaceStore.exportDocument(\n props.documentSlug,\n 'json',\n false,\n )\n if (!content) return\n const baseName = title.value.replace(/[^\\w\\s-]/g, '').trim() || 'document'\n downloadAsFile(content, `${baseName}.json`)\n}\n</script>\n\n<template>\n <div class=\"custom-scroll h-full\">\n <div\n v-if=\"document\"\n class=\"md:max-w-content w-full px-3 md:mx-auto\">\n <!-- Header -->\n <div\n :aria-label=\"`title: ${title}`\"\n class=\"md:max-w-content mx-auto flex h-fit w-full flex-col gap-2 pt-14 pb-3 md:pt-6\">\n <div class=\"flex flex-row items-center justify-between gap-2\">\n <div class=\"flex min-w-0 items-center gap-2\">\n <IconSelector\n :modelValue=\"icon\"\n placement=\"bottom-start\"\n @update:modelValue=\"\n (icon) => eventBus.emit('document:update:icon', icon)\n \">\n <ScalarButton\n class=\"hover:bg-b-2 aspect-square h-7 w-7 cursor-pointer rounded border border-transparent p-0 hover:border-inherit\"\n variant=\"ghost\">\n <LibraryIcon\n class=\"text-c-2 size-5\"\n :src=\"icon\"\n stroke-width=\"2\" />\n </ScalarButton>\n </IconSelector>\n\n <div class=\"group relative ml-1.25 min-w-0\">\n <LabelInput\n class=\"text-xl font-bold\"\n inputId=\"documentName\"\n :modelValue=\"title\"\n @update:modelValue=\"\n (title) => eventBus.emit('document:update:info', { title })\n \" />\n </div>\n </div>\n\n <ScalarButton\n class=\"text-c-2 hover:text-c-1 flex shrink-0 items-center gap-2\"\n size=\"xs\"\n type=\"button\"\n variant=\"ghost\"\n @click=\"downloadDocument\">\n <ScalarIconDownload\n size=\"sm\"\n thickness=\"1.5\" />\n <span>Download document</span>\n </ScalarButton>\n </div>\n </div>\n\n <!-- Tabs -->\n <Tabs type=\"document\" />\n\n <!-- Router views -->\n <div class=\"px-1.5 pt-8 pb-20\">\n <RouterView v-slot=\"{ Component }\">\n <component\n :is=\"Component\"\n v-bind=\"props\"\n collectionType=\"document\" />\n </RouterView>\n </div>\n </div>\n\n <!-- Document not found -->\n <div\n v-else\n class=\"flex w-full flex-1 items-center justify-center\">\n <div class=\"flex h-full flex-col items-center justify-center\">\n <h1 class=\"text-2xl font-bold\">Document not found</h1>\n <p class=\"text-gray-500\">\n The document you are looking for does not exist.\n </p>\n </div>\n </div>\n </div>\n</template>\n\n<style>\n.full-size-styles:has(.sync-conflict-modal-root) {\n width: 100dvw !important;\n max-width: 100dvw !important;\n border-right: none !important;\n}\n\n.full-size-styles:has(.sync-conflict-modal-root)::after {\n display: none;\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;CAUE,MAAM;;;;;;;;;;;;;;;;;;;;;;;EAkBR,MAAM,QAAQ;;EAGd,MAAM,QAAQ,eAAe,MAAM,UAAU,MAAM,SAAS,GAAE;;EAG9D,MAAM,OAAO,eACL,MAAM,WAAW,oBAAoB,2BAC7C;;EAGA,MAAM,yBAAyB;GAC7B,MAAM,UAAU,MAAM,eAAe,eACnC,MAAM,cACN,QACA,MACF;AACA,OAAI,CAAC,QAAS;AAEd,kBAAe,SAAS,GADP,MAAM,MAAM,QAAQ,aAAa,GAAG,CAAC,MAAM,IAAI,WAC5B,OAAM;;;uBAK1C,mBA4EM,OA5EN,YA4EM,CA1EI,QAAA,YAAA,WAAA,EADR,mBA8DM,OA9DN,YA8DM;IA1DJ,mBA4CM,OAAA;KA3CH,cAAU,UAAY,MAAA;KACvB,OAAM;QACN,mBAwCM,OAxCN,YAwCM,CAvCJ,mBA0BM,OA1BN,YA0BM,CAzBJ,YAce,sBAAA;KAbZ,YAAY,KAAA;KACb,WAAU;KACT,uBAAiB,OAAA,OAAA,OAAA,MAAoB,SAAS,QAAA,SAAS,KAAI,wBAAyB,KAAI;;4BAU1E,CAPf,YAOe,MAAA,aAAA,EAAA;MANb,OAAM;MACN,SAAQ;;6BAIa,CAHrB,YAGqB,MAAA,YAAA,EAAA;OAFnB,OAAM;OACL,KAAK,KAAA;OACN,gBAAa;;;;;2BAInB,mBAQM,OARN,YAQM,CAPJ,YAMM,oBAAA;KALJ,OAAM;KACN,SAAQ;KACP,YAAY,MAAA;KACZ,uBAAiB,OAAA,OAAA,OAAA,MAAsB,UAAU,QAAA,SAAS,KAAI,wBAAA,EAA2B,OAAK,CAAA;qCAMrG,YAUe,MAAA,aAAA,EAAA;KATb,OAAM;KACN,MAAK;KACL,MAAK;KACL,SAAQ;KACP,SAAO;;4BAGY,CAFpB,YAEoB,MAAA,mBAAA,EAAA;MADlB,MAAK;MACL,WAAU;mCACZ,mBAA8B,QAAA,MAAxB,qBAAiB,GAAA,EAAA,CAAA;;;IAM7B,YAAwB,cAAA,EAAlB,MAAK,YAAU,CAAA;IAGrB,mBAOM,OAPN,YAOM,CANJ,YAKa,MAAA,WAAA,EAAA,MAAA;uBADmB,EAJV,gBAAS,EAAA,WAAA,EAC7B,YAG8B,wBAFvB,UAAS,EADhB,WAEU,OAAK,EACb,gBAAe,YAAU,CAAA,EAAA,MAAA,GAAA,EAAA,CAAA;;;uBAMjC,mBASM,OATN,YASM,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,CANJ,mBAKM,OAAA,EALD,OAAM,oDAAkD,EAAA,CAC3D,mBAAsD,MAAA,EAAlD,OAAM,sBAAoB,EAAC,qBAAkB,EACjD,mBAEI,KAAA,EAFD,OAAM,iBAAe,EAAC,qDAEzB,CAAA,EAAA,GAAA,CAAA,EAAA,CAAA,EAAA,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"CommandPaletteExample.vue.d.ts","sourceRoot":"","sources":["../../../../../src/v2/features/command-palette/components/CommandPaletteExample.vue"],"names":[],"mappings":"AAgWA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAA;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AACvE,OAAO,KAAK,EAEV,gBAAgB,EAEjB,MAAM,4CAA4C,CAAA;AASnD;;;;;;;;;;;;;GAaG;wBACkB,OAAO,YAAY;AAAxC,wBAAyC;AAKzC,QAAA,MAAM,YAAY;IAEd,iEAAiE;oBACjD,cAAc;IAC9B,uDAAuD;cAC7C,iBAAiB;IAC3B,4CAA4C;mBAC7B,MAAM;IACrB,4DAA4D;kBAC9C,MAAM;IACpB,qCAAqC;cAC3B,gBAAgB;;;;;IAT1B,iEAAiE;oBACjD,cAAc;IAC9B,uDAAuD;cAC7C,iBAAiB;IAC3B,4CAA4C;mBAC7B,MAAM;IACrB,4DAA4D;kBAC9C,MAAM;IACpB,qCAAqC;cAC3B,gBAAgB;;;;kFA0hB1B,CAAC"}
1
+ {"version":3,"file":"CommandPaletteExample.vue.d.ts","sourceRoot":"","sources":["../../../../../src/v2/features/command-palette/components/CommandPaletteExample.vue"],"names":[],"mappings":"AA+XA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAA;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AACvE,OAAO,KAAK,EAEV,gBAAgB,EAEjB,MAAM,4CAA4C,CAAA;AASnD;;;;;;;;;;;;;GAaG;wBACkB,OAAO,YAAY;AAAxC,wBAAyC;AAKzC,QAAA,MAAM,YAAY;IAEd,iEAAiE;oBACjD,cAAc;IAC9B,uDAAuD;cAC7C,iBAAiB;IAC3B,4CAA4C;mBAC7B,MAAM;IACrB,4DAA4D;kBAC9C,MAAM;IACpB,qCAAqC;cAC3B,gBAAgB;;;;;IAT1B,iEAAiE;oBACjD,cAAc;IAC9B,uDAAuD;cAC7C,iBAAiB;IAC3B,4CAA4C;mBAC7B,MAAM;IACrB,4DAA4D;kBAC9C,MAAM;IACpB,qCAAqC;cAC3B,gBAAgB;;;;kFA8jB1B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"CommandPaletteExample.vue.js","names":[],"sources":["../../../../../src/v2/features/command-palette/components/CommandPaletteExample.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Command Palette Example Component\n *\n * Provides a form for creating a new example for an API operation.\n * Users can name the example, select a document (collection), and choose an operation.\n * Automatically navigates to the example route which creates the example.\n *\n * @example\n * <CommandPaletteExample\n * :workspaceStore=\"workspaceStore\"\n * @close=\"handleClose\"\n * @back=\"handleBack\"\n * />\n */\nexport default {\n name: 'CommandPaletteExample',\n}\n</script>\n\n<script setup lang=\"ts\">\nimport {\n ScalarButton,\n ScalarDropdown,\n ScalarDropdownItem,\n ScalarIcon,\n ScalarListbox,\n} from '@scalar/components'\nimport type { HttpMethod } from '@scalar/helpers/http/http-methods'\nimport type { WorkspaceStore } from '@scalar/workspace-store/client'\nimport type { WorkspaceEventBus } from '@scalar/workspace-store/events'\nimport type {\n TraversedEntry,\n TraversedExample,\n TraversedOperation,\n} from '@scalar/workspace-store/schemas/navigation'\nimport { computed, ref, watch } from 'vue'\n\nimport HttpMethodBadge from '@/v2/blocks/operation-code-sample/components/HttpMethod.vue'\n\nimport CommandActionForm from './CommandActionForm.vue'\nimport CommandActionInput from './CommandActionInput.vue'\n\nconst { workspaceStore, eventBus, documentName, operationId, example } =\n defineProps<{\n /** The workspace store for accessing documents and operations */\n workspaceStore: WorkspaceStore\n /** Event bus for emitting operation creation events */\n eventBus: WorkspaceEventBus\n /** Document id to create the example for */\n documentName?: string\n /** Preselected path and method to create the example for */\n operationId?: string\n /** Existing example for edit mode */\n example?: TraversedExample\n }>()\n\nconst emit = defineEmits<{\n /** Emitted when the example is created successfully */\n (event: 'close'): void\n /** Emitted when user navigates back (e.g., backspace on empty input) */\n (event: 'back', keyboardEvent: KeyboardEvent): void\n}>()\n\n/** Operation option type for selectors */\ntype OperationOption = {\n id: string\n label: string\n path: string\n method: HttpMethod\n exampleNames: string[]\n}\n\nconst isEditMode = computed(() => example !== undefined)\n\nconst exampleName = ref(example?.name ?? '')\nconst exampleNameTrimmed = computed(() => exampleName.value.trim())\n\n/** All available documents (collections) in the workspace */\nconst availableDocuments = computed(() =>\n Object.entries(workspaceStore.workspace.documents).map(\n ([name, document]) => ({\n id: name,\n label: document.info.title || name,\n }),\n ),\n)\n\nconst selectedDocument = ref<{ id: string; label: string } | undefined>(\n documentName\n ? availableDocuments.value.find((document) => document.id === documentName)\n : (availableDocuments.value[0] ?? undefined),\n)\n\n/**\n * Recursively traverse navigation entries to find all operations.\n * Operations can be nested under tags or at the document level.\n */\nconst getAllOperations = (entries: TraversedEntry[]): TraversedOperation[] => {\n const operations: TraversedOperation[] = []\n\n for (const entry of entries) {\n if (entry.type === 'operation') {\n operations.push(entry)\n }\n\n /** Recursively traverse child entries if they exist */\n if ('children' in entry && entry.children) {\n operations.push(...getAllOperations(entry.children))\n }\n }\n\n return operations\n}\n\n/** All available operations for the selected document */\nconst availableOperations = computed(() => {\n if (!selectedDocument.value) {\n return []\n }\n\n const document = workspaceStore.workspace.documents[selectedDocument.value.id]\n if (!document || !document['x-scalar-navigation']) {\n return []\n }\n\n const navigation = document['x-scalar-navigation']\n const operations = getAllOperations(navigation.children ?? [])\n\n return operations.map((operation) => ({\n id: operation.id,\n label: `${operation.method.toUpperCase()} ${operation.path}`,\n path: operation.path,\n method: operation.method,\n exampleNames:\n operation.children\n ?.filter((child): child is TraversedExample => child.type === 'example')\n .map((child) => child.name) ?? [],\n }))\n})\n\nconst selectedOperation = ref<OperationOption | undefined>(\n operationId\n ? availableOperations.value.find(\n (operation) => operation.id === operationId,\n )\n : undefined,\n)\n\n/** Reset operation selection when document changes */\nwatch(\n selectedDocument,\n () => {\n selectedOperation.value = operationId\n ? availableOperations.value.find(\n (operation) => operation.id === operationId,\n )\n : (availableOperations.value[0] ?? undefined)\n },\n { immediate: true },\n)\n\n/** Handle operation selection from dropdown */\nconst handleSelect = (operation: OperationOption | undefined): void => {\n if (operation) {\n selectedOperation.value = operation\n }\n}\n\n/**\n * Check if the form should be disabled.\n * Disabled when any required field is missing or empty.\n */\nconst isDisabled = computed<boolean>(() => {\n if (\n !exampleNameTrimmed.value ||\n !selectedDocument.value ||\n !selectedOperation.value\n ) {\n return true\n }\n\n if (isEditMode.value && example) {\n if (exampleNameTrimmed.value === example.name) {\n return true\n }\n }\n\n if (\n selectedOperation.value.exampleNames.some(\n (name) => name === exampleNameTrimmed.value && name !== example?.name,\n )\n ) {\n return true\n }\n\n return false\n})\n\n/**\n * Navigate to the example route which will create it automatically.\n * The route handler will create the example with the provided details.\n */\nconst handleSubmit = (): void => {\n if (isDisabled.value || !selectedDocument.value || !selectedOperation.value) {\n return\n }\n\n if (isEditMode.value && example) {\n eventBus.emit('operation:rename:example', {\n documentName: selectedDocument.value.id,\n meta: {\n path: selectedOperation.value.path,\n method: selectedOperation.value.method,\n exampleKey: example.name,\n },\n payload: {\n name: exampleNameTrimmed.value,\n },\n })\n emit('close')\n return\n }\n\n eventBus.emit('operation:create:draft-example', {\n documentName: selectedDocument.value.id,\n meta: {\n path: selectedOperation.value.path,\n method: selectedOperation.value.method,\n },\n exampleName: exampleNameTrimmed.value,\n })\n\n emit('close')\n}\n\n/** Handle back navigation when user presses backspace on empty input */\nconst handleBack = (event: KeyboardEvent): void => {\n if (isEditMode.value) {\n return\n }\n\n emit('back', event)\n}\n\n/** Handle cancel action in edit mode */\nconst handleCancel = (): void => {\n emit('close')\n}\n</script>\n<template>\n <CommandActionForm\n :disabled=\"isDisabled\"\n @submit=\"handleSubmit\">\n <CommandActionInput\n v-model=\"exampleName\"\n label=\"Example Name\"\n placeholder=\"Example Name\"\n @delete=\"handleBack\" />\n\n <!-- Selectors for document and operation -->\n <template #options>\n <div\n v-if=\"!isEditMode\"\n class=\"flex flex-1 gap-1\">\n <!-- Document (collection) selector -->\n <ScalarListbox\n v-model=\"selectedDocument\"\n :options=\"availableDocuments\">\n <ScalarButton\n class=\"hover:bg-b-2 max-h-8 w-[150px] min-w-[150px] justify-between gap-1 p-2 text-xs\"\n variant=\"outlined\">\n <span :class=\"selectedDocument ? 'text-c-1 truncate' : 'text-c-3'\">\n {{\n selectedDocument ? selectedDocument.label : 'Select Document'\n }}\n </span>\n <ScalarIcon\n class=\"text-c-3\"\n icon=\"ChevronDown\"\n size=\"md\" />\n </ScalarButton>\n </ScalarListbox>\n\n <!-- Operation selector (path + method) -->\n <ScalarDropdown\n placement=\"bottom\"\n resize>\n <ScalarButton\n class=\"hover:bg-b-2 max-h-8 w-full justify-between gap-1 p-2 text-xs\"\n :disabled=\"!availableOperations.length\"\n variant=\"outlined\">\n <span\n v-if=\"selectedOperation\"\n class=\"text-c-1 truncate\">\n {{ selectedOperation.path }}\n </span>\n <span\n v-else\n class=\"text-c-3\">\n Select Operation\n </span>\n <div class=\"flex items-center gap-2\">\n <HttpMethodBadge\n v-if=\"selectedOperation\"\n :method=\"selectedOperation.method\" />\n <ScalarIcon\n class=\"text-c-3\"\n icon=\"ChevronDown\"\n size=\"md\" />\n </div>\n </ScalarButton>\n\n <!-- Dropdown list of all operations -->\n <template #items>\n <div class=\"custom-scroll max-h-40\">\n <ScalarDropdownItem\n v-for=\"operation in availableOperations\"\n :key=\"operation.id\"\n class=\"flex h-7 w-full items-center justify-between px-1 pr-[26px]\"\n @click=\"handleSelect(operation)\">\n <span class=\"truncate\">{{ operation.path }}</span>\n <HttpMethodBadge :method=\"operation.method\" />\n </ScalarDropdownItem>\n </div>\n </template>\n </ScalarDropdown>\n </div>\n\n <ScalarButton\n v-else\n class=\"max-h-8 px-3 text-xs\"\n variant=\"outlined\"\n @click=\"handleCancel\">\n Cancel\n </ScalarButton>\n </template>\n\n <template #submit>{{ isEditMode ? 'Save' : 'Create Example' }}</template>\n </CommandActionForm>\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"CommandPaletteExample.vue.js","names":[],"sources":["../../../../../src/v2/features/command-palette/components/CommandPaletteExample.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Command Palette Example Component\n *\n * Provides a form for creating a new example for an API operation.\n * Users can name the example, select a document (collection), and choose an operation.\n * Automatically navigates to the example route which creates the example.\n *\n * @example\n * <CommandPaletteExample\n * :workspaceStore=\"workspaceStore\"\n * @close=\"handleClose\"\n * @back=\"handleBack\"\n * />\n */\nexport default {\n name: 'CommandPaletteExample',\n}\n</script>\n\n<script setup lang=\"ts\">\nimport {\n ScalarButton,\n ScalarDropdown,\n ScalarDropdownItem,\n ScalarIcon,\n ScalarListbox,\n} from '@scalar/components'\nimport type { HttpMethod } from '@scalar/helpers/http/http-methods'\nimport type { WorkspaceStore } from '@scalar/workspace-store/client'\nimport type { WorkspaceEventBus } from '@scalar/workspace-store/events'\nimport type {\n TraversedEntry,\n TraversedExample,\n TraversedOperation,\n} from '@scalar/workspace-store/schemas/navigation'\nimport { computed, ref, watch, type ComputedRef } from 'vue'\n\nimport HttpMethodBadge from '@/v2/blocks/operation-code-sample/components/HttpMethod.vue'\n\nimport CommandActionForm from './CommandActionForm.vue'\nimport CommandActionInput from './CommandActionInput.vue'\n\nconst { workspaceStore, eventBus, documentName, operationId, example } =\n defineProps<{\n /** The workspace store for accessing documents and operations */\n workspaceStore: WorkspaceStore\n /** Event bus for emitting operation creation events */\n eventBus: WorkspaceEventBus\n /** Document id to create the example for */\n documentName?: string\n /** Preselected path and method to create the example for */\n operationId?: string\n /** Existing example for edit mode */\n example?: TraversedExample\n }>()\n\nconst emit = defineEmits<{\n /** Emitted when the example is created successfully */\n (event: 'close'): void\n /** Emitted when user navigates back (e.g., backspace on empty input) */\n (event: 'back', keyboardEvent: KeyboardEvent): void\n}>()\n\n/** Operation option type for selectors */\ntype OperationOption = {\n id: string\n label: string\n path: string\n method: HttpMethod\n exampleNames: string[]\n}\n\nconst isEditMode = computed(() => example !== undefined)\n\nconst exampleName = ref(example?.name ?? '')\nconst exampleNameTrimmed = computed(() => exampleName.value.trim())\n\n/** All available documents (collections) in the workspace */\nconst availableDocuments = computed(() =>\n Object.entries(workspaceStore.workspace.documents).map(\n ([name, document]) => ({\n id: name,\n label: document.info.title || name,\n }),\n ),\n)\n\nconst selectedDocument = ref<{ id: string; label: string } | undefined>(\n documentName\n ? availableDocuments.value.find((document) => document.id === documentName)\n : (availableDocuments.value[0] ?? undefined),\n)\n\n/**\n * Recursively traverse navigation entries to find all operations.\n * Operations can be nested under tags or at the document level.\n */\nconst getAllOperations = (entries: TraversedEntry[]): TraversedOperation[] => {\n const operations: TraversedOperation[] = []\n\n for (const entry of entries) {\n if (entry.type === 'operation') {\n operations.push(entry)\n }\n\n /** Recursively traverse child entries if they exist */\n if ('children' in entry && entry.children) {\n operations.push(...getAllOperations(entry.children))\n }\n }\n\n return operations\n}\n\n/** All available operations for the selected document */\nconst availableOperations = computed(() => {\n if (!selectedDocument.value) {\n return []\n }\n\n const document = workspaceStore.workspace.documents[selectedDocument.value.id]\n if (!document || !document['x-scalar-navigation']) {\n return []\n }\n\n const navigation = document['x-scalar-navigation']\n const operations = getAllOperations(navigation.children ?? [])\n\n return operations.map((operation) => ({\n id: operation.id,\n label: `${operation.method.toUpperCase()} ${operation.path}`,\n path: operation.path,\n method: operation.method,\n exampleNames:\n operation.children\n ?.filter((child): child is TraversedExample => child.type === 'example')\n .map((child) => child.name) ?? [],\n }))\n})\n\nconst selectedOperation = ref<OperationOption | undefined>(\n operationId\n ? availableOperations.value.find(\n (operation) => operation.id === operationId,\n )\n : undefined,\n)\n\n/** Reset operation selection when document changes */\nwatch(\n selectedDocument,\n () => {\n selectedOperation.value = operationId\n ? availableOperations.value.find(\n (operation) => operation.id === operationId,\n )\n : (availableOperations.value[0] ?? undefined)\n },\n { immediate: true },\n)\n\n/** Handle operation selection from dropdown */\nconst handleSelect = (operation: OperationOption | undefined): void => {\n if (operation) {\n selectedOperation.value = operation\n }\n}\n\n/**\n * Validation message surfaced under the input.\n *\n * Resolves to `null` when the form is valid; otherwise to a human-readable\n * reason the user can act on. Empty input or an unchanged name in edit mode\n * are treated as the default state so the modal does not greet the user\n * with a misleading error.\n */\nconst errorMessage: ComputedRef<string | null> = computed(() => {\n if (\n !exampleNameTrimmed.value ||\n !selectedDocument.value ||\n !selectedOperation.value\n ) {\n return null\n }\n\n if (isEditMode.value && exampleNameTrimmed.value === example?.name) {\n return null\n }\n\n const nameConflict = selectedOperation.value.exampleNames.some(\n (name) => name === exampleNameTrimmed.value && name !== example?.name,\n )\n\n if (nameConflict) {\n return `An example named \"${exampleNameTrimmed.value}\" already exists for ${selectedOperation.value.method.toUpperCase()} ${selectedOperation.value.path}. Try a different name.`\n }\n\n return null\n})\n\n/**\n * Submit is blocked while required fields are missing, the name is unchanged\n * in edit mode, or another example already uses the same name. The inline\n * `errorMessage` explains the duplicate case so the user knows how to recover.\n */\nconst isDisabled = computed<boolean>(() => {\n if (\n !exampleNameTrimmed.value ||\n !selectedDocument.value ||\n !selectedOperation.value\n ) {\n return true\n }\n\n if (isEditMode.value && exampleNameTrimmed.value === example?.name) {\n return true\n }\n\n return errorMessage.value !== null\n})\n\n/**\n * Navigate to the example route which will create it automatically.\n * The route handler will create the example with the provided details.\n */\nconst handleSubmit = (): void => {\n if (isDisabled.value || !selectedDocument.value || !selectedOperation.value) {\n return\n }\n\n if (isEditMode.value && example) {\n eventBus.emit('operation:rename:example', {\n documentName: selectedDocument.value.id,\n meta: {\n path: selectedOperation.value.path,\n method: selectedOperation.value.method,\n exampleKey: example.name,\n },\n payload: {\n name: exampleNameTrimmed.value,\n },\n })\n emit('close')\n return\n }\n\n eventBus.emit('operation:create:draft-example', {\n documentName: selectedDocument.value.id,\n meta: {\n path: selectedOperation.value.path,\n method: selectedOperation.value.method,\n },\n exampleName: exampleNameTrimmed.value,\n })\n\n emit('close')\n}\n\n/** Handle back navigation when user presses backspace on empty input */\nconst handleBack = (event: KeyboardEvent): void => {\n if (isEditMode.value) {\n return\n }\n\n emit('back', event)\n}\n\n/** Handle cancel action in edit mode */\nconst handleCancel = (): void => {\n emit('close')\n}\n</script>\n<template>\n <CommandActionForm\n :disabled=\"isDisabled\"\n @submit=\"handleSubmit\">\n <CommandActionInput\n v-model=\"exampleName\"\n label=\"Example Name\"\n placeholder=\"Example Name\"\n @delete=\"handleBack\" />\n\n <p\n v-if=\"errorMessage\"\n class=\"text-red px-2 pb-1 text-xs\"\n data-testid=\"command-palette-example-error\"\n role=\"alert\">\n {{ errorMessage }}\n </p>\n\n <!-- Selectors for document and operation -->\n <template #options>\n <div\n v-if=\"!isEditMode\"\n class=\"flex flex-1 gap-1\">\n <!-- Document (collection) selector -->\n <ScalarListbox\n v-model=\"selectedDocument\"\n :options=\"availableDocuments\">\n <ScalarButton\n class=\"hover:bg-b-2 max-h-8 w-[150px] min-w-[150px] justify-between gap-1 p-2 text-xs\"\n variant=\"outlined\">\n <span :class=\"selectedDocument ? 'text-c-1 truncate' : 'text-c-3'\">\n {{\n selectedDocument ? selectedDocument.label : 'Select Document'\n }}\n </span>\n <ScalarIcon\n class=\"text-c-3\"\n icon=\"ChevronDown\"\n size=\"md\" />\n </ScalarButton>\n </ScalarListbox>\n\n <!-- Operation selector (path + method) -->\n <ScalarDropdown\n placement=\"bottom\"\n resize>\n <ScalarButton\n class=\"hover:bg-b-2 max-h-8 w-full justify-between gap-1 p-2 text-xs\"\n :disabled=\"!availableOperations.length\"\n variant=\"outlined\">\n <span\n v-if=\"selectedOperation\"\n class=\"text-c-1 truncate\">\n {{ selectedOperation.path }}\n </span>\n <span\n v-else\n class=\"text-c-3\">\n Select Operation\n </span>\n <div class=\"flex items-center gap-2\">\n <HttpMethodBadge\n v-if=\"selectedOperation\"\n :method=\"selectedOperation.method\" />\n <ScalarIcon\n class=\"text-c-3\"\n icon=\"ChevronDown\"\n size=\"md\" />\n </div>\n </ScalarButton>\n\n <!-- Dropdown list of all operations -->\n <template #items>\n <div class=\"custom-scroll max-h-40\">\n <ScalarDropdownItem\n v-for=\"operation in availableOperations\"\n :key=\"operation.id\"\n class=\"flex h-7 w-full items-center justify-between px-1 pr-[26px]\"\n @click=\"handleSelect(operation)\">\n <span class=\"truncate\">{{ operation.path }}</span>\n <HttpMethodBadge :method=\"operation.method\" />\n </ScalarDropdownItem>\n </div>\n </template>\n </ScalarDropdown>\n </div>\n\n <ScalarButton\n v-else\n class=\"max-h-8 px-3 text-xs\"\n variant=\"outlined\"\n @click=\"handleCancel\">\n Cancel\n </ScalarButton>\n </template>\n\n <template #submit>{{ isEditMode ? 'Save' : 'Create Example' }}</template>\n </CommandActionForm>\n</template>\n"],"mappings":""}
@@ -6,19 +6,25 @@ import { ScalarButton, ScalarDropdown, ScalarDropdownItem, ScalarIcon, ScalarLis
6
6
  //#region src/v2/features/command-palette/components/CommandPaletteExample.vue?vue&type=script&setup=true&lang.ts
7
7
  var _hoisted_1 = {
8
8
  key: 0,
9
- class: "flex flex-1 gap-1"
9
+ class: "text-red px-2 pb-1 text-xs",
10
+ "data-testid": "command-palette-example-error",
11
+ role: "alert"
10
12
  };
11
13
  var _hoisted_2 = {
12
14
  key: 0,
13
- class: "text-c-1 truncate"
15
+ class: "flex flex-1 gap-1"
14
16
  };
15
17
  var _hoisted_3 = {
18
+ key: 0,
19
+ class: "text-c-1 truncate"
20
+ };
21
+ var _hoisted_4 = {
16
22
  key: 1,
17
23
  class: "text-c-3"
18
24
  };
19
- var _hoisted_4 = { class: "flex items-center gap-2" };
20
- var _hoisted_5 = { class: "custom-scroll max-h-40" };
21
- var _hoisted_6 = { class: "truncate" };
25
+ var _hoisted_5 = { class: "flex items-center gap-2" };
26
+ var _hoisted_6 = { class: "custom-scroll max-h-40" };
27
+ var _hoisted_7 = { class: "truncate" };
22
28
  var CommandPaletteExample_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({
23
29
  name: "CommandPaletteExample",
24
30
  props: {
@@ -78,16 +84,28 @@ var CommandPaletteExample_vue_vue_type_script_setup_true_lang_default = /* @__PU
78
84
  if (operation) selectedOperation.value = operation;
79
85
  };
80
86
  /**
81
- * Check if the form should be disabled.
82
- * Disabled when any required field is missing or empty.
87
+ * Validation message surfaced under the input.
88
+ *
89
+ * Resolves to `null` when the form is valid; otherwise to a human-readable
90
+ * reason the user can act on. Empty input or an unchanged name in edit mode
91
+ * are treated as the default state so the modal does not greet the user
92
+ * with a misleading error.
93
+ */
94
+ const errorMessage = computed(() => {
95
+ if (!exampleNameTrimmed.value || !selectedDocument.value || !selectedOperation.value) return null;
96
+ if (isEditMode.value && exampleNameTrimmed.value === __props.example?.name) return null;
97
+ if (selectedOperation.value.exampleNames.some((name) => name === exampleNameTrimmed.value && name !== __props.example?.name)) return `An example named "${exampleNameTrimmed.value}" already exists for ${selectedOperation.value.method.toUpperCase()} ${selectedOperation.value.path}. Try a different name.`;
98
+ return null;
99
+ });
100
+ /**
101
+ * Submit is blocked while required fields are missing, the name is unchanged
102
+ * in edit mode, or another example already uses the same name. The inline
103
+ * `errorMessage` explains the duplicate case so the user knows how to recover.
83
104
  */
84
105
  const isDisabled = computed(() => {
85
106
  if (!exampleNameTrimmed.value || !selectedDocument.value || !selectedOperation.value) return true;
86
- if (isEditMode.value && __props.example) {
87
- if (exampleNameTrimmed.value === __props.example.name) return true;
88
- }
89
- if (selectedOperation.value.exampleNames.some((name) => name === exampleNameTrimmed.value && name !== __props.example?.name)) return true;
90
- return false;
107
+ if (isEditMode.value && exampleNameTrimmed.value === __props.example?.name) return true;
108
+ return errorMessage.value !== null;
91
109
  });
92
110
  /**
93
111
  * Navigate to the example route which will create it automatically.
@@ -132,7 +150,7 @@ var CommandPaletteExample_vue_vue_type_script_setup_true_lang_default = /* @__PU
132
150
  disabled: isDisabled.value,
133
151
  onSubmit: handleSubmit
134
152
  }, {
135
- options: withCtx(() => [!isEditMode.value ? (openBlock(), createElementBlock("div", _hoisted_1, [createVNode(unref(ScalarListbox), {
153
+ options: withCtx(() => [!isEditMode.value ? (openBlock(), createElementBlock("div", _hoisted_2, [createVNode(unref(ScalarListbox), {
136
154
  modelValue: selectedDocument.value,
137
155
  "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => selectedDocument.value = $event),
138
156
  options: availableDocuments.value
@@ -153,13 +171,13 @@ var CommandPaletteExample_vue_vue_type_script_setup_true_lang_default = /* @__PU
153
171
  placement: "bottom",
154
172
  resize: ""
155
173
  }, {
156
- items: withCtx(() => [createElementVNode("div", _hoisted_5, [(openBlock(true), createElementBlock(Fragment, null, renderList(availableOperations.value, (operation) => {
174
+ items: withCtx(() => [createElementVNode("div", _hoisted_6, [(openBlock(true), createElementBlock(Fragment, null, renderList(availableOperations.value, (operation) => {
157
175
  return openBlock(), createBlock(unref(ScalarDropdownItem), {
158
176
  key: operation.id,
159
177
  class: "flex h-7 w-full items-center justify-between px-1 pr-[26px]",
160
178
  onClick: ($event) => handleSelect(operation)
161
179
  }, {
162
- default: withCtx(() => [createElementVNode("span", _hoisted_6, toDisplayString(operation.path), 1), createVNode(HttpMethod_default, { method: operation.method }, null, 8, ["method"])]),
180
+ default: withCtx(() => [createElementVNode("span", _hoisted_7, toDisplayString(operation.path), 1), createVNode(HttpMethod_default, { method: operation.method }, null, 8, ["method"])]),
163
181
  _: 2
164
182
  }, 1032, ["onClick"]);
165
183
  }), 128))])]),
@@ -168,7 +186,7 @@ var CommandPaletteExample_vue_vue_type_script_setup_true_lang_default = /* @__PU
168
186
  disabled: !availableOperations.value.length,
169
187
  variant: "outlined"
170
188
  }, {
171
- default: withCtx(() => [selectedOperation.value ? (openBlock(), createElementBlock("span", _hoisted_2, toDisplayString(selectedOperation.value.path), 1)) : (openBlock(), createElementBlock("span", _hoisted_3, " Select Operation ")), createElementVNode("div", _hoisted_4, [selectedOperation.value ? (openBlock(), createBlock(HttpMethod_default, {
189
+ default: withCtx(() => [selectedOperation.value ? (openBlock(), createElementBlock("span", _hoisted_3, toDisplayString(selectedOperation.value.path), 1)) : (openBlock(), createElementBlock("span", _hoisted_4, " Select Operation ")), createElementVNode("div", _hoisted_5, [selectedOperation.value ? (openBlock(), createBlock(HttpMethod_default, {
172
190
  key: 0,
173
191
  method: selectedOperation.value.method
174
192
  }, null, 8, ["method"])) : createCommentVNode("", true), createVNode(unref(ScalarIcon), {
@@ -195,7 +213,7 @@ var CommandPaletteExample_vue_vue_type_script_setup_true_lang_default = /* @__PU
195
213
  label: "Example Name",
196
214
  placeholder: "Example Name",
197
215
  onDelete: handleBack
198
- }, null, 8, ["modelValue"])]),
216
+ }, null, 8, ["modelValue"]), errorMessage.value ? (openBlock(), createElementBlock("p", _hoisted_1, toDisplayString(errorMessage.value), 1)) : createCommentVNode("", true)]),
199
217
  _: 1
200
218
  }, 8, ["disabled"]);
201
219
  };
@@ -1 +1 @@
1
- {"version":3,"file":"CommandPaletteExample.vue.script.js","names":[],"sources":["../../../../../src/v2/features/command-palette/components/CommandPaletteExample.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Command Palette Example Component\n *\n * Provides a form for creating a new example for an API operation.\n * Users can name the example, select a document (collection), and choose an operation.\n * Automatically navigates to the example route which creates the example.\n *\n * @example\n * <CommandPaletteExample\n * :workspaceStore=\"workspaceStore\"\n * @close=\"handleClose\"\n * @back=\"handleBack\"\n * />\n */\nexport default {\n name: 'CommandPaletteExample',\n}\n</script>\n\n<script setup lang=\"ts\">\nimport {\n ScalarButton,\n ScalarDropdown,\n ScalarDropdownItem,\n ScalarIcon,\n ScalarListbox,\n} from '@scalar/components'\nimport type { HttpMethod } from '@scalar/helpers/http/http-methods'\nimport type { WorkspaceStore } from '@scalar/workspace-store/client'\nimport type { WorkspaceEventBus } from '@scalar/workspace-store/events'\nimport type {\n TraversedEntry,\n TraversedExample,\n TraversedOperation,\n} from '@scalar/workspace-store/schemas/navigation'\nimport { computed, ref, watch } from 'vue'\n\nimport HttpMethodBadge from '@/v2/blocks/operation-code-sample/components/HttpMethod.vue'\n\nimport CommandActionForm from './CommandActionForm.vue'\nimport CommandActionInput from './CommandActionInput.vue'\n\nconst { workspaceStore, eventBus, documentName, operationId, example } =\n defineProps<{\n /** The workspace store for accessing documents and operations */\n workspaceStore: WorkspaceStore\n /** Event bus for emitting operation creation events */\n eventBus: WorkspaceEventBus\n /** Document id to create the example for */\n documentName?: string\n /** Preselected path and method to create the example for */\n operationId?: string\n /** Existing example for edit mode */\n example?: TraversedExample\n }>()\n\nconst emit = defineEmits<{\n /** Emitted when the example is created successfully */\n (event: 'close'): void\n /** Emitted when user navigates back (e.g., backspace on empty input) */\n (event: 'back', keyboardEvent: KeyboardEvent): void\n}>()\n\n/** Operation option type for selectors */\ntype OperationOption = {\n id: string\n label: string\n path: string\n method: HttpMethod\n exampleNames: string[]\n}\n\nconst isEditMode = computed(() => example !== undefined)\n\nconst exampleName = ref(example?.name ?? '')\nconst exampleNameTrimmed = computed(() => exampleName.value.trim())\n\n/** All available documents (collections) in the workspace */\nconst availableDocuments = computed(() =>\n Object.entries(workspaceStore.workspace.documents).map(\n ([name, document]) => ({\n id: name,\n label: document.info.title || name,\n }),\n ),\n)\n\nconst selectedDocument = ref<{ id: string; label: string } | undefined>(\n documentName\n ? availableDocuments.value.find((document) => document.id === documentName)\n : (availableDocuments.value[0] ?? undefined),\n)\n\n/**\n * Recursively traverse navigation entries to find all operations.\n * Operations can be nested under tags or at the document level.\n */\nconst getAllOperations = (entries: TraversedEntry[]): TraversedOperation[] => {\n const operations: TraversedOperation[] = []\n\n for (const entry of entries) {\n if (entry.type === 'operation') {\n operations.push(entry)\n }\n\n /** Recursively traverse child entries if they exist */\n if ('children' in entry && entry.children) {\n operations.push(...getAllOperations(entry.children))\n }\n }\n\n return operations\n}\n\n/** All available operations for the selected document */\nconst availableOperations = computed(() => {\n if (!selectedDocument.value) {\n return []\n }\n\n const document = workspaceStore.workspace.documents[selectedDocument.value.id]\n if (!document || !document['x-scalar-navigation']) {\n return []\n }\n\n const navigation = document['x-scalar-navigation']\n const operations = getAllOperations(navigation.children ?? [])\n\n return operations.map((operation) => ({\n id: operation.id,\n label: `${operation.method.toUpperCase()} ${operation.path}`,\n path: operation.path,\n method: operation.method,\n exampleNames:\n operation.children\n ?.filter((child): child is TraversedExample => child.type === 'example')\n .map((child) => child.name) ?? [],\n }))\n})\n\nconst selectedOperation = ref<OperationOption | undefined>(\n operationId\n ? availableOperations.value.find(\n (operation) => operation.id === operationId,\n )\n : undefined,\n)\n\n/** Reset operation selection when document changes */\nwatch(\n selectedDocument,\n () => {\n selectedOperation.value = operationId\n ? availableOperations.value.find(\n (operation) => operation.id === operationId,\n )\n : (availableOperations.value[0] ?? undefined)\n },\n { immediate: true },\n)\n\n/** Handle operation selection from dropdown */\nconst handleSelect = (operation: OperationOption | undefined): void => {\n if (operation) {\n selectedOperation.value = operation\n }\n}\n\n/**\n * Check if the form should be disabled.\n * Disabled when any required field is missing or empty.\n */\nconst isDisabled = computed<boolean>(() => {\n if (\n !exampleNameTrimmed.value ||\n !selectedDocument.value ||\n !selectedOperation.value\n ) {\n return true\n }\n\n if (isEditMode.value && example) {\n if (exampleNameTrimmed.value === example.name) {\n return true\n }\n }\n\n if (\n selectedOperation.value.exampleNames.some(\n (name) => name === exampleNameTrimmed.value && name !== example?.name,\n )\n ) {\n return true\n }\n\n return false\n})\n\n/**\n * Navigate to the example route which will create it automatically.\n * The route handler will create the example with the provided details.\n */\nconst handleSubmit = (): void => {\n if (isDisabled.value || !selectedDocument.value || !selectedOperation.value) {\n return\n }\n\n if (isEditMode.value && example) {\n eventBus.emit('operation:rename:example', {\n documentName: selectedDocument.value.id,\n meta: {\n path: selectedOperation.value.path,\n method: selectedOperation.value.method,\n exampleKey: example.name,\n },\n payload: {\n name: exampleNameTrimmed.value,\n },\n })\n emit('close')\n return\n }\n\n eventBus.emit('operation:create:draft-example', {\n documentName: selectedDocument.value.id,\n meta: {\n path: selectedOperation.value.path,\n method: selectedOperation.value.method,\n },\n exampleName: exampleNameTrimmed.value,\n })\n\n emit('close')\n}\n\n/** Handle back navigation when user presses backspace on empty input */\nconst handleBack = (event: KeyboardEvent): void => {\n if (isEditMode.value) {\n return\n }\n\n emit('back', event)\n}\n\n/** Handle cancel action in edit mode */\nconst handleCancel = (): void => {\n emit('close')\n}\n</script>\n<template>\n <CommandActionForm\n :disabled=\"isDisabled\"\n @submit=\"handleSubmit\">\n <CommandActionInput\n v-model=\"exampleName\"\n label=\"Example Name\"\n placeholder=\"Example Name\"\n @delete=\"handleBack\" />\n\n <!-- Selectors for document and operation -->\n <template #options>\n <div\n v-if=\"!isEditMode\"\n class=\"flex flex-1 gap-1\">\n <!-- Document (collection) selector -->\n <ScalarListbox\n v-model=\"selectedDocument\"\n :options=\"availableDocuments\">\n <ScalarButton\n class=\"hover:bg-b-2 max-h-8 w-[150px] min-w-[150px] justify-between gap-1 p-2 text-xs\"\n variant=\"outlined\">\n <span :class=\"selectedDocument ? 'text-c-1 truncate' : 'text-c-3'\">\n {{\n selectedDocument ? selectedDocument.label : 'Select Document'\n }}\n </span>\n <ScalarIcon\n class=\"text-c-3\"\n icon=\"ChevronDown\"\n size=\"md\" />\n </ScalarButton>\n </ScalarListbox>\n\n <!-- Operation selector (path + method) -->\n <ScalarDropdown\n placement=\"bottom\"\n resize>\n <ScalarButton\n class=\"hover:bg-b-2 max-h-8 w-full justify-between gap-1 p-2 text-xs\"\n :disabled=\"!availableOperations.length\"\n variant=\"outlined\">\n <span\n v-if=\"selectedOperation\"\n class=\"text-c-1 truncate\">\n {{ selectedOperation.path }}\n </span>\n <span\n v-else\n class=\"text-c-3\">\n Select Operation\n </span>\n <div class=\"flex items-center gap-2\">\n <HttpMethodBadge\n v-if=\"selectedOperation\"\n :method=\"selectedOperation.method\" />\n <ScalarIcon\n class=\"text-c-3\"\n icon=\"ChevronDown\"\n size=\"md\" />\n </div>\n </ScalarButton>\n\n <!-- Dropdown list of all operations -->\n <template #items>\n <div class=\"custom-scroll max-h-40\">\n <ScalarDropdownItem\n v-for=\"operation in availableOperations\"\n :key=\"operation.id\"\n class=\"flex h-7 w-full items-center justify-between px-1 pr-[26px]\"\n @click=\"handleSelect(operation)\">\n <span class=\"truncate\">{{ operation.path }}</span>\n <HttpMethodBadge :method=\"operation.method\" />\n </ScalarDropdownItem>\n </div>\n </template>\n </ScalarDropdown>\n </div>\n\n <ScalarButton\n v-else\n class=\"max-h-8 px-3 text-xs\"\n variant=\"outlined\"\n @click=\"handleCancel\">\n Cancel\n </ScalarButton>\n </template>\n\n <template #submit>{{ isEditMode ? 'Save' : 'Create Example' }}</template>\n </CommandActionForm>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"CommandPaletteExample.vue.script.js","names":[],"sources":["../../../../../src/v2/features/command-palette/components/CommandPaletteExample.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Command Palette Example Component\n *\n * Provides a form for creating a new example for an API operation.\n * Users can name the example, select a document (collection), and choose an operation.\n * Automatically navigates to the example route which creates the example.\n *\n * @example\n * <CommandPaletteExample\n * :workspaceStore=\"workspaceStore\"\n * @close=\"handleClose\"\n * @back=\"handleBack\"\n * />\n */\nexport default {\n name: 'CommandPaletteExample',\n}\n</script>\n\n<script setup lang=\"ts\">\nimport {\n ScalarButton,\n ScalarDropdown,\n ScalarDropdownItem,\n ScalarIcon,\n ScalarListbox,\n} from '@scalar/components'\nimport type { HttpMethod } from '@scalar/helpers/http/http-methods'\nimport type { WorkspaceStore } from '@scalar/workspace-store/client'\nimport type { WorkspaceEventBus } from '@scalar/workspace-store/events'\nimport type {\n TraversedEntry,\n TraversedExample,\n TraversedOperation,\n} from '@scalar/workspace-store/schemas/navigation'\nimport { computed, ref, watch, type ComputedRef } from 'vue'\n\nimport HttpMethodBadge from '@/v2/blocks/operation-code-sample/components/HttpMethod.vue'\n\nimport CommandActionForm from './CommandActionForm.vue'\nimport CommandActionInput from './CommandActionInput.vue'\n\nconst { workspaceStore, eventBus, documentName, operationId, example } =\n defineProps<{\n /** The workspace store for accessing documents and operations */\n workspaceStore: WorkspaceStore\n /** Event bus for emitting operation creation events */\n eventBus: WorkspaceEventBus\n /** Document id to create the example for */\n documentName?: string\n /** Preselected path and method to create the example for */\n operationId?: string\n /** Existing example for edit mode */\n example?: TraversedExample\n }>()\n\nconst emit = defineEmits<{\n /** Emitted when the example is created successfully */\n (event: 'close'): void\n /** Emitted when user navigates back (e.g., backspace on empty input) */\n (event: 'back', keyboardEvent: KeyboardEvent): void\n}>()\n\n/** Operation option type for selectors */\ntype OperationOption = {\n id: string\n label: string\n path: string\n method: HttpMethod\n exampleNames: string[]\n}\n\nconst isEditMode = computed(() => example !== undefined)\n\nconst exampleName = ref(example?.name ?? '')\nconst exampleNameTrimmed = computed(() => exampleName.value.trim())\n\n/** All available documents (collections) in the workspace */\nconst availableDocuments = computed(() =>\n Object.entries(workspaceStore.workspace.documents).map(\n ([name, document]) => ({\n id: name,\n label: document.info.title || name,\n }),\n ),\n)\n\nconst selectedDocument = ref<{ id: string; label: string } | undefined>(\n documentName\n ? availableDocuments.value.find((document) => document.id === documentName)\n : (availableDocuments.value[0] ?? undefined),\n)\n\n/**\n * Recursively traverse navigation entries to find all operations.\n * Operations can be nested under tags or at the document level.\n */\nconst getAllOperations = (entries: TraversedEntry[]): TraversedOperation[] => {\n const operations: TraversedOperation[] = []\n\n for (const entry of entries) {\n if (entry.type === 'operation') {\n operations.push(entry)\n }\n\n /** Recursively traverse child entries if they exist */\n if ('children' in entry && entry.children) {\n operations.push(...getAllOperations(entry.children))\n }\n }\n\n return operations\n}\n\n/** All available operations for the selected document */\nconst availableOperations = computed(() => {\n if (!selectedDocument.value) {\n return []\n }\n\n const document = workspaceStore.workspace.documents[selectedDocument.value.id]\n if (!document || !document['x-scalar-navigation']) {\n return []\n }\n\n const navigation = document['x-scalar-navigation']\n const operations = getAllOperations(navigation.children ?? [])\n\n return operations.map((operation) => ({\n id: operation.id,\n label: `${operation.method.toUpperCase()} ${operation.path}`,\n path: operation.path,\n method: operation.method,\n exampleNames:\n operation.children\n ?.filter((child): child is TraversedExample => child.type === 'example')\n .map((child) => child.name) ?? [],\n }))\n})\n\nconst selectedOperation = ref<OperationOption | undefined>(\n operationId\n ? availableOperations.value.find(\n (operation) => operation.id === operationId,\n )\n : undefined,\n)\n\n/** Reset operation selection when document changes */\nwatch(\n selectedDocument,\n () => {\n selectedOperation.value = operationId\n ? availableOperations.value.find(\n (operation) => operation.id === operationId,\n )\n : (availableOperations.value[0] ?? undefined)\n },\n { immediate: true },\n)\n\n/** Handle operation selection from dropdown */\nconst handleSelect = (operation: OperationOption | undefined): void => {\n if (operation) {\n selectedOperation.value = operation\n }\n}\n\n/**\n * Validation message surfaced under the input.\n *\n * Resolves to `null` when the form is valid; otherwise to a human-readable\n * reason the user can act on. Empty input or an unchanged name in edit mode\n * are treated as the default state so the modal does not greet the user\n * with a misleading error.\n */\nconst errorMessage: ComputedRef<string | null> = computed(() => {\n if (\n !exampleNameTrimmed.value ||\n !selectedDocument.value ||\n !selectedOperation.value\n ) {\n return null\n }\n\n if (isEditMode.value && exampleNameTrimmed.value === example?.name) {\n return null\n }\n\n const nameConflict = selectedOperation.value.exampleNames.some(\n (name) => name === exampleNameTrimmed.value && name !== example?.name,\n )\n\n if (nameConflict) {\n return `An example named \"${exampleNameTrimmed.value}\" already exists for ${selectedOperation.value.method.toUpperCase()} ${selectedOperation.value.path}. Try a different name.`\n }\n\n return null\n})\n\n/**\n * Submit is blocked while required fields are missing, the name is unchanged\n * in edit mode, or another example already uses the same name. The inline\n * `errorMessage` explains the duplicate case so the user knows how to recover.\n */\nconst isDisabled = computed<boolean>(() => {\n if (\n !exampleNameTrimmed.value ||\n !selectedDocument.value ||\n !selectedOperation.value\n ) {\n return true\n }\n\n if (isEditMode.value && exampleNameTrimmed.value === example?.name) {\n return true\n }\n\n return errorMessage.value !== null\n})\n\n/**\n * Navigate to the example route which will create it automatically.\n * The route handler will create the example with the provided details.\n */\nconst handleSubmit = (): void => {\n if (isDisabled.value || !selectedDocument.value || !selectedOperation.value) {\n return\n }\n\n if (isEditMode.value && example) {\n eventBus.emit('operation:rename:example', {\n documentName: selectedDocument.value.id,\n meta: {\n path: selectedOperation.value.path,\n method: selectedOperation.value.method,\n exampleKey: example.name,\n },\n payload: {\n name: exampleNameTrimmed.value,\n },\n })\n emit('close')\n return\n }\n\n eventBus.emit('operation:create:draft-example', {\n documentName: selectedDocument.value.id,\n meta: {\n path: selectedOperation.value.path,\n method: selectedOperation.value.method,\n },\n exampleName: exampleNameTrimmed.value,\n })\n\n emit('close')\n}\n\n/** Handle back navigation when user presses backspace on empty input */\nconst handleBack = (event: KeyboardEvent): void => {\n if (isEditMode.value) {\n return\n }\n\n emit('back', event)\n}\n\n/** Handle cancel action in edit mode */\nconst handleCancel = (): void => {\n emit('close')\n}\n</script>\n<template>\n <CommandActionForm\n :disabled=\"isDisabled\"\n @submit=\"handleSubmit\">\n <CommandActionInput\n v-model=\"exampleName\"\n label=\"Example Name\"\n placeholder=\"Example Name\"\n @delete=\"handleBack\" />\n\n <p\n v-if=\"errorMessage\"\n class=\"text-red px-2 pb-1 text-xs\"\n data-testid=\"command-palette-example-error\"\n role=\"alert\">\n {{ errorMessage }}\n </p>\n\n <!-- Selectors for document and operation -->\n <template #options>\n <div\n v-if=\"!isEditMode\"\n class=\"flex flex-1 gap-1\">\n <!-- Document (collection) selector -->\n <ScalarListbox\n v-model=\"selectedDocument\"\n :options=\"availableDocuments\">\n <ScalarButton\n class=\"hover:bg-b-2 max-h-8 w-[150px] min-w-[150px] justify-between gap-1 p-2 text-xs\"\n variant=\"outlined\">\n <span :class=\"selectedDocument ? 'text-c-1 truncate' : 'text-c-3'\">\n {{\n selectedDocument ? selectedDocument.label : 'Select Document'\n }}\n </span>\n <ScalarIcon\n class=\"text-c-3\"\n icon=\"ChevronDown\"\n size=\"md\" />\n </ScalarButton>\n </ScalarListbox>\n\n <!-- Operation selector (path + method) -->\n <ScalarDropdown\n placement=\"bottom\"\n resize>\n <ScalarButton\n class=\"hover:bg-b-2 max-h-8 w-full justify-between gap-1 p-2 text-xs\"\n :disabled=\"!availableOperations.length\"\n variant=\"outlined\">\n <span\n v-if=\"selectedOperation\"\n class=\"text-c-1 truncate\">\n {{ selectedOperation.path }}\n </span>\n <span\n v-else\n class=\"text-c-3\">\n Select Operation\n </span>\n <div class=\"flex items-center gap-2\">\n <HttpMethodBadge\n v-if=\"selectedOperation\"\n :method=\"selectedOperation.method\" />\n <ScalarIcon\n class=\"text-c-3\"\n icon=\"ChevronDown\"\n size=\"md\" />\n </div>\n </ScalarButton>\n\n <!-- Dropdown list of all operations -->\n <template #items>\n <div class=\"custom-scroll max-h-40\">\n <ScalarDropdownItem\n v-for=\"operation in availableOperations\"\n :key=\"operation.id\"\n class=\"flex h-7 w-full items-center justify-between px-1 pr-[26px]\"\n @click=\"handleSelect(operation)\">\n <span class=\"truncate\">{{ operation.path }}</span>\n <HttpMethodBadge :method=\"operation.method\" />\n </ScalarDropdownItem>\n </div>\n </template>\n </ScalarDropdown>\n </div>\n\n <ScalarButton\n v-else\n class=\"max-h-8 px-3 text-xs\"\n variant=\"outlined\"\n @click=\"handleCancel\">\n Cancel\n </ScalarButton>\n </template>\n\n <template #submit>{{ isEditMode ? 'Save' : 'Create Example' }}</template>\n </CommandActionForm>\n</template>\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CommandPaletteImport.vue.d.ts","sourceRoot":"","sources":["../../../../../src/v2/features/command-palette/components/CommandPaletteImport.vue"],"names":[],"mappings":"AAyZA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAG7D,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAkBvE;;;;;;;;GAQG;wBACkB,OAAO,YAAY;AAAxC,wBAAyC;AAKzC,QAAA,MAAM,YAAY;IAEhB,+CAA+C;oBAC/B,cAAc;IAC9B,uDAAuD;cAC7C,iBAAiB;IAC3B,oCAAoC;iBACvB,YAAY;;;;;IALzB,+CAA+C;oBAC/B,cAAc;IAC9B,uDAAuD;cAC7C,iBAAiB;IAC3B,oCAAoC;iBACvB,YAAY;;;;;IAczB;;;;;;OAMG;sBACe;QAChB,8DAA8D;QAC9D,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;KAChE,GAAG,IAAI;EAugBN,CAAC;AACL,KAAK,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAChC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KACV,CAAA;CACD,CAAC"}
1
+ {"version":3,"file":"CommandPaletteImport.vue.d.ts","sourceRoot":"","sources":["../../../../../src/v2/features/command-palette/components/CommandPaletteImport.vue"],"names":[],"mappings":"AA6ZA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAG7D,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAiBvE;;;;;;;;GAQG;wBACkB,OAAO,YAAY;AAAxC,wBAAyC;AAKzC,QAAA,MAAM,YAAY;IAEhB,+CAA+C;oBAC/B,cAAc;IAC9B,uDAAuD;cAC7C,iBAAiB;IAC3B,oCAAoC;iBACvB,YAAY;;;;;IALzB,+CAA+C;oBAC/B,cAAc;IAC9B,uDAAuD;cAC7C,iBAAiB;IAC3B,oCAAoC;iBACvB,YAAY;;;;;IAczB;;;;;;OAMG;sBACe;QAChB,8DAA8D;QAC9D,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;KAChE,GAAG,IAAI;EA4gBN,CAAC;AACL,KAAK,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAChC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KACV,CAAA;CACD,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"CommandPaletteImport.vue.js","names":[],"sources":["../../../../../src/v2/features/command-palette/components/CommandPaletteImport.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Command Palette Import Component\n *\n * Provides a form for importing OpenAPI descriptions from URL, file, or pasted JSON/YAML.\n * Postman collection JSON and Postman files open {@link CommandPaletteImportPostman}.\n * cURL commands redirect to {@link CommandPaletteImportCurl}.\n *\n * Supports watch mode for URL imports to automatically update when content changes.\n */\nexport default {\n name: 'CommandPaletteImport',\n}\n</script>\n\n<script setup lang=\"ts\">\nimport {\n ScalarButton,\n ScalarCodeBlock,\n ScalarIcon,\n ScalarTooltip,\n useLoadingState,\n} from '@scalar/components'\nimport { isLocalUrl } from '@scalar/helpers/url/is-local-url'\nimport type { LoaderPlugin } from '@scalar/json-magic/bundle'\nimport { isPostmanCollection } from '@scalar/postman-to-openapi'\nimport { useToasts } from '@scalar/use-toasts'\nimport {\n createWorkspaceStore,\n type WorkspaceStore,\n} from '@scalar/workspace-store/client'\nimport type { WorkspaceEventBus } from '@scalar/workspace-store/events'\nimport { computed, ref, watch } from 'vue'\nimport { useRouter } from 'vue-router'\n\nimport { useFileDialog } from '@/hooks/use-file-dialog'\nimport { getOpenApiDocumentDetails } from '@/v2/features/command-palette/helpers/get-openapi-document-details'\nimport { importDocumentToWorkspace } from '@/v2/features/command-palette/helpers/import-document-to-workspace'\nimport {\n loadDocumentFromSource,\n type ImportEventData,\n} from '@/v2/features/command-palette/helpers/load-document-from-source'\nimport { isUrl } from '@/v2/helpers/is-url'\n\nimport CommandActionForm from './CommandActionForm.vue'\nimport CommandActionInput from './CommandActionInput.vue'\nimport WatchModeToggle from './WatchModeToggle.vue'\n\nconst { workspaceStore, eventBus, fileLoader } = defineProps<{\n /** The workspace store for adding documents */\n workspaceStore: WorkspaceStore\n /** Event bus for emitting operation creation events */\n eventBus: WorkspaceEventBus\n /** Loader plugin for file import */\n fileLoader?: LoaderPlugin\n}>()\n\nconst emit = defineEmits<{\n /** Emitted when the import is complete or cancelled */\n (event: 'close'): void\n /** Emitted when user navigates back (e.g., backspace on empty input) */\n (event: 'back', keyboardEvent: KeyboardEvent): void\n}>()\n\ndefineSlots<{\n /**\n * Slot for custom file upload component that can trigger import.\n *\n * The provided `import` function automatically detects Postman collections\n * and routes them to the Postman import modal, matching the behavior of the\n * default file picker.\n */\n fileUpload(props: {\n /** Function to trigger import with source content and type */\n import: (source: string, type: 'file' | 'raw') => Promise<void>\n }): void\n}>()\n\nconst { toast } = useToasts()\n\nconst router = useRouter()\nconst loader = useLoadingState()\n\nconst inputContent = ref('')\nconst watchMode = ref(false)\n\n/** Check if the input content is a URL */\nconst isUrlInput = computed<boolean>(() => isUrl(inputContent.value))\nconst isLocalUrlInput = computed<boolean>(\n () => isUrlInput.value && isLocalUrl(inputContent.value),\n)\n\nconst documentDetails = computed(() =>\n getOpenApiDocumentDetails(inputContent.value),\n)\n\n/** Get the document type for syntax highlighting */\nconst documentType = computed<string>(() =>\n documentDetails.value ? documentDetails.value.type : 'json',\n)\n\n/** Check if the form should be disabled (when input is empty) */\nconst isDisabled = computed<boolean>(() => {\n return !inputContent.value.trim()\n})\n\n/**\n * Toggle watchMode based on whether the input is a local URL.\n * Only enables watch mode for local URLs, not for files or pasted content.\n */\nwatch(isLocalUrlInput, (value: boolean) => {\n watchMode.value = value\n})\n\n/**\n * Handles errors during the import process.\n * Shows an error toast, invalidates the loader to show an error state,\n * and closes the command palette modal.\n *\n * @param errorMessage - The error message to display and log\n */\nconst handleImportError = async (errorMessage: string) => {\n // Log the error\n console.error(errorMessage)\n toast(errorMessage, 'error')\n\n // Invalidate the loader to show the error state\n await loader.invalidate()\n\n // Close the command palette\n emit('close')\n}\n\n/**\n * Directly imports a document into the workspace without showing the modal.\n * This is used when there is only one workspace and it is empty.\n */\nconst handleImport = async (\n newSource: string,\n type?: ImportEventData['type'],\n): Promise<void> => {\n loader.start()\n\n const TEMP_DOCUMENT_NAME = 'drafts'\n\n // First load the document into a draft store\n // This is to get the title of the document so we can generate a unique slug for store\n const draftStore = createWorkspaceStore({\n fileLoader,\n meta: {\n /** Ensure we use the active proxy to fetch documents */\n 'x-scalar-active-proxy':\n workspaceStore.workspace['x-scalar-active-proxy'],\n },\n })\n\n const eventType = (() => {\n if (type) {\n return type\n }\n\n if (isUrlInput.value) {\n return 'url'\n }\n\n return 'raw'\n })()\n\n const isSuccessfullyLoaded = await loadDocumentFromSource(\n draftStore,\n { source: newSource, type: eventType },\n TEMP_DOCUMENT_NAME,\n watchMode.value,\n )\n\n if (!isSuccessfullyLoaded) {\n return handleImportError('Failed to import document')\n }\n\n const importResult = await importDocumentToWorkspace({\n workspaceStore,\n workspaceState: draftStore.exportWorkspace(),\n name: TEMP_DOCUMENT_NAME,\n })\n\n if (!importResult.ok) {\n return handleImportError(importResult.error)\n }\n\n // Validate the loader to show the success state\n await loader.validate()\n\n // Navigate to the document overview page\n navigateToDocument(importResult.slug)\n\n // Close the command palette\n emit('close')\n}\n\n/** Navigate to the document overview page after successful import */\nconst navigateToDocument = (documentName: string): void => {\n router.push({\n name: 'document.overview',\n params: { documentSlug: documentName },\n })\n}\n\n/**\n * Import a file, routing Postman collections to the Postman import modal and\n * everything else through the OpenAPI import flow.\n *\n * Shared between the default file picker and the `fileUpload` slot so custom\n * path-based importers get the same Postman detection behavior.\n *\n * When `type` is `'file'` the `source` is a file path resolved through the\n * configured `fileLoader`; when `type` is `'raw'` the `source` is treated as\n * the file's text content directly.\n */\nconst handleFileImport: (\n source: string,\n type?: 'file' | 'raw',\n) => Promise<void> = async (source, type = 'raw') => {\n // Resolve the raw text content so we can sniff for a Postman collection.\n // For raw pastes / uploads the source already is the text. For path-based\n // imports we delegate to the file loader plugin, if one is configured.\n const rawContent = await (async (): Promise<string> => {\n if (type === 'raw') {\n return source\n }\n\n const result = await fileLoader?.exec(source)\n return result?.ok ? result.raw : ''\n })()\n\n if (isPostmanCollection(rawContent)) {\n eventBus.emit('ui:open:command-palette', {\n action: 'import-postman-collection',\n payload: {\n inputValue: rawContent,\n },\n })\n await loader.clear()\n return\n }\n\n await handleImport(source, type)\n}\n\n/**\n * Handle file selection and import from file dialog.\n * Reads the file as text and imports it as OpenAPI or Postman collection.\n * Shows loading state during the import process.\n */\nconst { open: openSpecFileDialog } = useFileDialog({\n onChange: (files) => {\n const [file] = files ?? []\n\n if (!file) {\n return\n }\n\n loader.start()\n\n const onLoad = async (event: ProgressEvent<FileReader>): Promise<void> => {\n const text = event.target?.result as string\n await handleFileImport(text, 'raw')\n }\n\n const reader = new FileReader()\n reader.onload = onLoad\n reader.readAsText(file)\n },\n multiple: false,\n accept: '.json,.yaml,.yml',\n})\n\n/**\n * Handle input changes.\n * Detects cURL commands and redirects to the cURL import command.\n */\nconst handleInput = (value: string): void => {\n const trimmed = value.trim()\n\n if (trimmed.toLowerCase().startsWith('curl')) {\n return eventBus.emit('ui:open:command-palette', {\n action: 'import-curl-command',\n payload: {\n inputValue: value,\n },\n })\n }\n\n if (isPostmanCollection(trimmed)) {\n return eventBus.emit('ui:open:command-palette', {\n action: 'import-postman-collection',\n payload: {\n inputValue: value,\n },\n })\n }\n\n inputContent.value = value\n}\n\n/** Handle back navigation when user presses backspace on empty input */\nconst handleBack = (event: KeyboardEvent): void => {\n emit('back', event)\n}\n</script>\n<template>\n <CommandActionForm\n :disabled=\"isDisabled\"\n :loader\n @submit=\"handleImport(inputContent)\">\n <!-- URL or cURL input mode -->\n <template v-if=\"!documentDetails || isUrlInput\">\n <CommandActionInput\n :modelValue=\"inputContent\"\n placeholder=\"OpenAPI/Swagger/Postman URL or cURL\"\n @delete=\"handleBack\"\n @update:modelValue=\"handleInput\" />\n </template>\n\n <!-- Preview mode for pasted content -->\n <template v-else>\n <!-- Preview header with clear button -->\n <div class=\"flex justify-between\">\n <div class=\"text-c-2 min-h-8 w-full py-2 pl-12 text-center text-xs\">\n Preview\n </div>\n <ScalarButton\n class=\"hover:bg-b-2 relative ml-auto max-h-8 gap-1.5 p-2 text-xs\"\n variant=\"ghost\"\n @click=\"inputContent = ''\">\n Clear\n </ScalarButton>\n </div>\n\n <!-- Code preview with syntax highlighting -->\n <ScalarCodeBlock\n v-if=\"documentDetails && !isUrlInput\"\n class=\"bg-b-2 mt-1 max-h-[40dvh] rounded border px-2 py-1 text-sm\"\n :content=\"inputContent\"\n :copy=\"false\"\n :lang=\"documentType\" />\n </template>\n\n <!-- Actions: File upload and watch mode toggle -->\n <template #options>\n <div class=\"flex w-full flex-row items-center justify-between gap-3\">\n <!-- Custom file upload slot or default button -->\n <slot\n :import=\"handleFileImport\"\n name=\"fileUpload\">\n <!-- Default file upload button -->\n <ScalarButton\n class=\"hover:bg-b-2 relative max-h-8 gap-1.5 p-2 text-xs\"\n variant=\"outlined\"\n @click=\"openSpecFileDialog\">\n JSON, or YAML File\n <ScalarIcon\n class=\"text-c-3\"\n icon=\"Upload\"\n size=\"md\" />\n </ScalarButton>\n </slot>\n\n <!-- Watch mode toggle (only enabled for URL imports) -->\n <ScalarTooltip\n :content=\"\n isUrlInput\n ? 'Watch mode automatically updates the API client when the OpenAPI URL content changes, ensuring your client remains up-to-date.'\n : 'Watch mode is only available for URL imports. When enabled it automatically updates the API client when the OpenAPI URL content changes.'\n \"\n placement=\"bottom\">\n <WatchModeToggle\n v-model=\"watchMode\"\n :disabled=\"!isUrlInput\" />\n </ScalarTooltip>\n </div>\n </template>\n\n <!-- Dynamic submit button text based on import type -->\n <template #submit>\n Import\n <template v-if=\"isUrlInput\">from URL</template>\n <template v-else-if=\"documentDetails && documentType\">\n <template v-if=\"documentDetails.title\">\n \"{{ documentDetails.title }}\"\n </template>\n <template v-else>\n {{ documentDetails.version }}\n </template>\n </template>\n <template v-else>Collection</template>\n </template>\n </CommandActionForm>\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"CommandPaletteImport.vue.js","names":[],"sources":["../../../../../src/v2/features/command-palette/components/CommandPaletteImport.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Command Palette Import Component\n *\n * Provides a form for importing OpenAPI descriptions from URL, file, or pasted JSON/YAML.\n * Postman collection JSON and Postman files open {@link CommandPaletteImportPostman}.\n * cURL commands redirect to {@link CommandPaletteImportCurl}.\n *\n * Supports watch mode for URL imports to automatically update when content changes.\n */\nexport default {\n name: 'CommandPaletteImport',\n}\n</script>\n\n<script setup lang=\"ts\">\nimport {\n ScalarButton,\n ScalarCodeBlock,\n ScalarIcon,\n ScalarTooltip,\n useLoadingState,\n} from '@scalar/components'\nimport { isLocalUrl } from '@scalar/helpers/url/is-local-url'\nimport type { LoaderPlugin } from '@scalar/json-magic/bundle'\nimport { isPostmanCollection } from '@scalar/postman-to-openapi'\nimport { useToasts } from '@scalar/use-toasts'\nimport {\n createWorkspaceStore,\n type WorkspaceStore,\n} from '@scalar/workspace-store/client'\nimport type { WorkspaceEventBus } from '@scalar/workspace-store/events'\nimport { computed, ref, watch } from 'vue'\n\nimport { useFileDialog } from '@/hooks/use-file-dialog'\nimport { getOpenApiDocumentDetails } from '@/v2/features/command-palette/helpers/get-openapi-document-details'\nimport { importDocumentToWorkspace } from '@/v2/features/command-palette/helpers/import-document-to-workspace'\nimport {\n loadDocumentFromSource,\n type ImportEventData,\n} from '@/v2/features/command-palette/helpers/load-document-from-source'\nimport { isUrl } from '@/v2/helpers/is-url'\n\nimport CommandActionForm from './CommandActionForm.vue'\nimport CommandActionInput from './CommandActionInput.vue'\nimport WatchModeToggle from './WatchModeToggle.vue'\n\nconst { workspaceStore, eventBus, fileLoader } = defineProps<{\n /** The workspace store for adding documents */\n workspaceStore: WorkspaceStore\n /** Event bus for emitting operation creation events */\n eventBus: WorkspaceEventBus\n /** Loader plugin for file import */\n fileLoader?: LoaderPlugin\n}>()\n\nconst emit = defineEmits<{\n /** Emitted when the import is complete or cancelled */\n (event: 'close'): void\n /** Emitted when user navigates back (e.g., backspace on empty input) */\n (event: 'back', keyboardEvent: KeyboardEvent): void\n}>()\n\ndefineSlots<{\n /**\n * Slot for custom file upload component that can trigger import.\n *\n * The provided `import` function automatically detects Postman collections\n * and routes them to the Postman import modal, matching the behavior of the\n * default file picker.\n */\n fileUpload(props: {\n /** Function to trigger import with source content and type */\n import: (source: string, type: 'file' | 'raw') => Promise<void>\n }): void\n}>()\n\nconst { toast } = useToasts()\n\nconst loader = useLoadingState()\n\nconst inputContent = ref('')\nconst watchMode = ref(false)\n\n/** Trim paste noise for URL checks without changing raw JSON/YAML content. */\nconst normalizedInputContent = computed<string>(() => inputContent.value.trim())\n\n/** Check if the input content is a URL */\nconst isUrlInput = computed<boolean>(() => isUrl(normalizedInputContent.value))\nconst isLocalUrlInput = computed<boolean>(\n () => isUrlInput.value && isLocalUrl(normalizedInputContent.value),\n)\n\nconst documentDetails = computed(() =>\n getOpenApiDocumentDetails(inputContent.value),\n)\n\n/** Get the document type for syntax highlighting */\nconst documentType = computed<string>(() =>\n documentDetails.value ? documentDetails.value.type : 'json',\n)\n\n/** Check if the form should be disabled (when input is empty) */\nconst isDisabled = computed<boolean>(() => {\n return !inputContent.value.trim()\n})\n\n/**\n * Toggle watchMode based on whether the input is a local URL.\n * Only enables watch mode for local URLs, not for files or pasted content.\n */\nwatch(isLocalUrlInput, (value: boolean) => {\n watchMode.value = value\n})\n\n/**\n * Handles errors during the import process.\n * Shows an error toast, invalidates the loader to show an error state,\n * and closes the command palette modal.\n *\n * @param errorMessage - The error message to display and log\n */\nconst handleImportError = async (errorMessage: string) => {\n // Log the error\n console.error(errorMessage)\n toast(errorMessage, 'error')\n\n // Invalidate the loader to show the error state\n await loader.invalidate()\n\n // Close the command palette\n emit('close')\n}\n\n/**\n * Directly imports a document into the workspace without showing the modal.\n * This is used when there is only one workspace and it is empty.\n */\nconst handleImport = async (\n newSource: string,\n type?: ImportEventData['type'],\n): Promise<void> => {\n loader.start()\n\n const TEMP_DOCUMENT_NAME = 'drafts'\n\n // First load the document into a draft store\n // This is to get the title of the document so we can generate a unique slug for store\n const draftStore = createWorkspaceStore({\n fileLoader,\n meta: {\n /** Ensure we use the active proxy to fetch documents */\n 'x-scalar-active-proxy':\n workspaceStore.workspace['x-scalar-active-proxy'],\n },\n })\n\n const eventType = (() => {\n if (type) {\n return type\n }\n\n if (isUrl(newSource.trim())) {\n return 'url'\n }\n\n return 'raw'\n })()\n\n const source = eventType === 'url' ? newSource.trim() : newSource\n\n const isSuccessfullyLoaded = await loadDocumentFromSource(\n draftStore,\n { source, type: eventType },\n TEMP_DOCUMENT_NAME,\n watchMode.value,\n )\n\n if (!isSuccessfullyLoaded) {\n return handleImportError('Failed to import document')\n }\n\n const importResult = await importDocumentToWorkspace({\n workspaceStore,\n workspaceState: draftStore.exportWorkspace(),\n name: TEMP_DOCUMENT_NAME,\n })\n\n if (!importResult.ok) {\n return handleImportError(importResult.error)\n }\n\n // Validate the loader to show the success state\n await loader.validate()\n\n // Navigate to the document overview page\n navigateToDocument(importResult.slug)\n\n // Close the command palette\n emit('close')\n}\n\n/** Navigate to the document overview page after successful import */\nconst navigateToDocument = (documentName: string): void => {\n eventBus.emit('ui:navigate', {\n page: 'document',\n path: 'overview',\n documentSlug: documentName,\n })\n}\n\n/**\n * Import a file, routing Postman collections to the Postman import modal and\n * everything else through the OpenAPI import flow.\n *\n * Shared between the default file picker and the `fileUpload` slot so custom\n * path-based importers get the same Postman detection behavior.\n *\n * When `type` is `'file'` the `source` is a file path resolved through the\n * configured `fileLoader`; when `type` is `'raw'` the `source` is treated as\n * the file's text content directly.\n */\nconst handleFileImport: (\n source: string,\n type?: 'file' | 'raw',\n) => Promise<void> = async (source, type = 'raw') => {\n // Resolve the raw text content so we can sniff for a Postman collection.\n // For raw pastes / uploads the source already is the text. For path-based\n // imports we delegate to the file loader plugin, if one is configured.\n const rawContent = await (async (): Promise<string> => {\n if (type === 'raw') {\n return source\n }\n\n const result = await fileLoader?.exec(source)\n return result?.ok ? result.raw : ''\n })()\n\n if (isPostmanCollection(rawContent)) {\n eventBus.emit('ui:open:command-palette', {\n action: 'import-postman-collection',\n payload: {\n inputValue: rawContent,\n },\n })\n await loader.clear()\n return\n }\n\n await handleImport(source, type)\n}\n\n/**\n * Handle file selection and import from file dialog.\n * Reads the file as text and imports it as OpenAPI or Postman collection.\n * Shows loading state during the import process.\n */\nconst { open: openSpecFileDialog } = useFileDialog({\n onChange: (files) => {\n const [file] = files ?? []\n\n if (!file) {\n return\n }\n\n loader.start()\n\n const onLoad = async (event: ProgressEvent<FileReader>): Promise<void> => {\n const text = event.target?.result as string\n await handleFileImport(text, 'raw')\n }\n\n const reader = new FileReader()\n reader.onload = onLoad\n reader.readAsText(file)\n },\n multiple: false,\n accept: '.json,.yaml,.yml',\n})\n\n/**\n * Handle input changes.\n * Detects cURL commands and redirects to the cURL import command.\n */\nconst handleInput = (value: string): void => {\n const trimmed = value.trim()\n\n if (trimmed.toLowerCase().startsWith('curl')) {\n return eventBus.emit('ui:open:command-palette', {\n action: 'import-curl-command',\n payload: {\n inputValue: value,\n },\n })\n }\n\n if (isPostmanCollection(trimmed)) {\n return eventBus.emit('ui:open:command-palette', {\n action: 'import-postman-collection',\n payload: {\n inputValue: value,\n },\n })\n }\n\n inputContent.value = value\n}\n\n/** Handle back navigation when user presses backspace on empty input */\nconst handleBack = (event: KeyboardEvent): void => {\n emit('back', event)\n}\n</script>\n<template>\n <CommandActionForm\n :disabled=\"isDisabled\"\n :loader\n @submit=\"handleImport(inputContent)\">\n <!-- URL or cURL input mode -->\n <template v-if=\"!documentDetails || isUrlInput\">\n <CommandActionInput\n :modelValue=\"inputContent\"\n placeholder=\"OpenAPI/Swagger/Postman URL or cURL\"\n @delete=\"handleBack\"\n @update:modelValue=\"handleInput\" />\n </template>\n\n <!-- Preview mode for pasted content -->\n <template v-else>\n <!-- Preview header with clear button -->\n <div class=\"flex justify-between\">\n <div class=\"text-c-2 min-h-8 w-full py-2 pl-12 text-center text-xs\">\n Preview\n </div>\n <ScalarButton\n class=\"hover:bg-b-2 relative ml-auto max-h-8 gap-1.5 p-2 text-xs\"\n variant=\"ghost\"\n @click=\"inputContent = ''\">\n Clear\n </ScalarButton>\n </div>\n\n <!-- Code preview with syntax highlighting -->\n <ScalarCodeBlock\n v-if=\"documentDetails && !isUrlInput\"\n class=\"bg-b-2 mt-1 max-h-[40dvh] rounded border px-2 py-1 text-sm\"\n :content=\"inputContent\"\n :copy=\"false\"\n :lang=\"documentType\" />\n </template>\n\n <!-- Actions: File upload and watch mode toggle -->\n <template #options>\n <div class=\"flex w-full flex-row items-center justify-between gap-3\">\n <!-- Custom file upload slot or default button -->\n <slot\n :import=\"handleFileImport\"\n name=\"fileUpload\">\n <!-- Default file upload button -->\n <ScalarButton\n class=\"hover:bg-b-2 relative max-h-8 gap-1.5 p-2 text-xs\"\n variant=\"outlined\"\n @click=\"openSpecFileDialog\">\n JSON, or YAML File\n <ScalarIcon\n class=\"text-c-3\"\n icon=\"Upload\"\n size=\"md\" />\n </ScalarButton>\n </slot>\n\n <!-- Watch mode toggle (only enabled for URL imports) -->\n <ScalarTooltip\n :content=\"\n isUrlInput\n ? 'Watch mode automatically updates the API client when the OpenAPI URL content changes, ensuring your client remains up-to-date.'\n : 'Watch mode is only available for URL imports. When enabled it automatically updates the API client when the OpenAPI URL content changes.'\n \"\n placement=\"bottom\">\n <WatchModeToggle\n v-model=\"watchMode\"\n :disabled=\"!isUrlInput\" />\n </ScalarTooltip>\n </div>\n </template>\n\n <!-- Dynamic submit button text based on import type -->\n <template #submit>\n Import\n <template v-if=\"isUrlInput\">from URL</template>\n <template v-else-if=\"documentDetails && documentType\">\n <template v-if=\"documentDetails.title\">\n \"{{ documentDetails.title }}\"\n </template>\n <template v-else>\n {{ documentDetails.version }}\n </template>\n </template>\n <template v-else>Collection</template>\n </template>\n </CommandActionForm>\n</template>\n"],"mappings":""}
@@ -12,7 +12,6 @@ import { useToasts } from "@scalar/use-toasts";
12
12
  import { isLocalUrl } from "@scalar/helpers/url/is-local-url";
13
13
  import { isPostmanCollection } from "@scalar/postman-to-openapi";
14
14
  import { createWorkspaceStore } from "@scalar/workspace-store/client";
15
- import { useRouter } from "vue-router";
16
15
  //#region src/v2/features/command-palette/components/CommandPaletteImport.vue?vue&type=script&setup=true&lang.ts
17
16
  var _hoisted_1 = { class: "flex justify-between" };
18
17
  var _hoisted_2 = { class: "flex w-full flex-row items-center justify-between gap-3" };
@@ -27,13 +26,14 @@ var CommandPaletteImport_vue_vue_type_script_setup_true_lang_default = /* @__PUR
27
26
  setup(__props, { emit: __emit }) {
28
27
  const emit = __emit;
29
28
  const { toast } = useToasts();
30
- const router = useRouter();
31
29
  const loader = useLoadingState();
32
30
  const inputContent = ref("");
33
31
  const watchMode = ref(false);
32
+ /** Trim paste noise for URL checks without changing raw JSON/YAML content. */
33
+ const normalizedInputContent = computed(() => inputContent.value.trim());
34
34
  /** Check if the input content is a URL */
35
- const isUrlInput = computed(() => isUrl(inputContent.value));
36
- const isLocalUrlInput = computed(() => isUrlInput.value && isLocalUrl(inputContent.value));
35
+ const isUrlInput = computed(() => isUrl(normalizedInputContent.value));
36
+ const isLocalUrlInput = computed(() => isUrlInput.value && isLocalUrl(normalizedInputContent.value));
37
37
  const documentDetails = computed(() => getOpenApiDocumentDetails(inputContent.value));
38
38
  /** Get the document type for syntax highlighting */
39
39
  const documentType = computed(() => documentDetails.value ? documentDetails.value.type : "json");
@@ -72,13 +72,14 @@ var CommandPaletteImport_vue_vue_type_script_setup_true_lang_default = /* @__PUR
72
72
  fileLoader: __props.fileLoader,
73
73
  meta: { "x-scalar-active-proxy": __props.workspaceStore.workspace["x-scalar-active-proxy"] }
74
74
  });
75
+ const eventType = (() => {
76
+ if (type) return type;
77
+ if (isUrl(newSource.trim())) return "url";
78
+ return "raw";
79
+ })();
75
80
  if (!await loadDocumentFromSource(draftStore, {
76
- source: newSource,
77
- type: (() => {
78
- if (type) return type;
79
- if (isUrlInput.value) return "url";
80
- return "raw";
81
- })()
81
+ source: eventType === "url" ? newSource.trim() : newSource,
82
+ type: eventType
82
83
  }, TEMP_DOCUMENT_NAME, watchMode.value)) return handleImportError("Failed to import document");
83
84
  const importResult = await importDocumentToWorkspace({
84
85
  workspaceStore: __props.workspaceStore,
@@ -92,9 +93,10 @@ var CommandPaletteImport_vue_vue_type_script_setup_true_lang_default = /* @__PUR
92
93
  };
93
94
  /** Navigate to the document overview page after successful import */
94
95
  const navigateToDocument = (documentName) => {
95
- router.push({
96
- name: "document.overview",
97
- params: { documentSlug: documentName }
96
+ __props.eventBus.emit("ui:navigate", {
97
+ page: "document",
98
+ path: "overview",
99
+ documentSlug: documentName
98
100
  });
99
101
  };
100
102
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"CommandPaletteImport.vue.script.js","names":[],"sources":["../../../../../src/v2/features/command-palette/components/CommandPaletteImport.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Command Palette Import Component\n *\n * Provides a form for importing OpenAPI descriptions from URL, file, or pasted JSON/YAML.\n * Postman collection JSON and Postman files open {@link CommandPaletteImportPostman}.\n * cURL commands redirect to {@link CommandPaletteImportCurl}.\n *\n * Supports watch mode for URL imports to automatically update when content changes.\n */\nexport default {\n name: 'CommandPaletteImport',\n}\n</script>\n\n<script setup lang=\"ts\">\nimport {\n ScalarButton,\n ScalarCodeBlock,\n ScalarIcon,\n ScalarTooltip,\n useLoadingState,\n} from '@scalar/components'\nimport { isLocalUrl } from '@scalar/helpers/url/is-local-url'\nimport type { LoaderPlugin } from '@scalar/json-magic/bundle'\nimport { isPostmanCollection } from '@scalar/postman-to-openapi'\nimport { useToasts } from '@scalar/use-toasts'\nimport {\n createWorkspaceStore,\n type WorkspaceStore,\n} from '@scalar/workspace-store/client'\nimport type { WorkspaceEventBus } from '@scalar/workspace-store/events'\nimport { computed, ref, watch } from 'vue'\nimport { useRouter } from 'vue-router'\n\nimport { useFileDialog } from '@/hooks/use-file-dialog'\nimport { getOpenApiDocumentDetails } from '@/v2/features/command-palette/helpers/get-openapi-document-details'\nimport { importDocumentToWorkspace } from '@/v2/features/command-palette/helpers/import-document-to-workspace'\nimport {\n loadDocumentFromSource,\n type ImportEventData,\n} from '@/v2/features/command-palette/helpers/load-document-from-source'\nimport { isUrl } from '@/v2/helpers/is-url'\n\nimport CommandActionForm from './CommandActionForm.vue'\nimport CommandActionInput from './CommandActionInput.vue'\nimport WatchModeToggle from './WatchModeToggle.vue'\n\nconst { workspaceStore, eventBus, fileLoader } = defineProps<{\n /** The workspace store for adding documents */\n workspaceStore: WorkspaceStore\n /** Event bus for emitting operation creation events */\n eventBus: WorkspaceEventBus\n /** Loader plugin for file import */\n fileLoader?: LoaderPlugin\n}>()\n\nconst emit = defineEmits<{\n /** Emitted when the import is complete or cancelled */\n (event: 'close'): void\n /** Emitted when user navigates back (e.g., backspace on empty input) */\n (event: 'back', keyboardEvent: KeyboardEvent): void\n}>()\n\ndefineSlots<{\n /**\n * Slot for custom file upload component that can trigger import.\n *\n * The provided `import` function automatically detects Postman collections\n * and routes them to the Postman import modal, matching the behavior of the\n * default file picker.\n */\n fileUpload(props: {\n /** Function to trigger import with source content and type */\n import: (source: string, type: 'file' | 'raw') => Promise<void>\n }): void\n}>()\n\nconst { toast } = useToasts()\n\nconst router = useRouter()\nconst loader = useLoadingState()\n\nconst inputContent = ref('')\nconst watchMode = ref(false)\n\n/** Check if the input content is a URL */\nconst isUrlInput = computed<boolean>(() => isUrl(inputContent.value))\nconst isLocalUrlInput = computed<boolean>(\n () => isUrlInput.value && isLocalUrl(inputContent.value),\n)\n\nconst documentDetails = computed(() =>\n getOpenApiDocumentDetails(inputContent.value),\n)\n\n/** Get the document type for syntax highlighting */\nconst documentType = computed<string>(() =>\n documentDetails.value ? documentDetails.value.type : 'json',\n)\n\n/** Check if the form should be disabled (when input is empty) */\nconst isDisabled = computed<boolean>(() => {\n return !inputContent.value.trim()\n})\n\n/**\n * Toggle watchMode based on whether the input is a local URL.\n * Only enables watch mode for local URLs, not for files or pasted content.\n */\nwatch(isLocalUrlInput, (value: boolean) => {\n watchMode.value = value\n})\n\n/**\n * Handles errors during the import process.\n * Shows an error toast, invalidates the loader to show an error state,\n * and closes the command palette modal.\n *\n * @param errorMessage - The error message to display and log\n */\nconst handleImportError = async (errorMessage: string) => {\n // Log the error\n console.error(errorMessage)\n toast(errorMessage, 'error')\n\n // Invalidate the loader to show the error state\n await loader.invalidate()\n\n // Close the command palette\n emit('close')\n}\n\n/**\n * Directly imports a document into the workspace without showing the modal.\n * This is used when there is only one workspace and it is empty.\n */\nconst handleImport = async (\n newSource: string,\n type?: ImportEventData['type'],\n): Promise<void> => {\n loader.start()\n\n const TEMP_DOCUMENT_NAME = 'drafts'\n\n // First load the document into a draft store\n // This is to get the title of the document so we can generate a unique slug for store\n const draftStore = createWorkspaceStore({\n fileLoader,\n meta: {\n /** Ensure we use the active proxy to fetch documents */\n 'x-scalar-active-proxy':\n workspaceStore.workspace['x-scalar-active-proxy'],\n },\n })\n\n const eventType = (() => {\n if (type) {\n return type\n }\n\n if (isUrlInput.value) {\n return 'url'\n }\n\n return 'raw'\n })()\n\n const isSuccessfullyLoaded = await loadDocumentFromSource(\n draftStore,\n { source: newSource, type: eventType },\n TEMP_DOCUMENT_NAME,\n watchMode.value,\n )\n\n if (!isSuccessfullyLoaded) {\n return handleImportError('Failed to import document')\n }\n\n const importResult = await importDocumentToWorkspace({\n workspaceStore,\n workspaceState: draftStore.exportWorkspace(),\n name: TEMP_DOCUMENT_NAME,\n })\n\n if (!importResult.ok) {\n return handleImportError(importResult.error)\n }\n\n // Validate the loader to show the success state\n await loader.validate()\n\n // Navigate to the document overview page\n navigateToDocument(importResult.slug)\n\n // Close the command palette\n emit('close')\n}\n\n/** Navigate to the document overview page after successful import */\nconst navigateToDocument = (documentName: string): void => {\n router.push({\n name: 'document.overview',\n params: { documentSlug: documentName },\n })\n}\n\n/**\n * Import a file, routing Postman collections to the Postman import modal and\n * everything else through the OpenAPI import flow.\n *\n * Shared between the default file picker and the `fileUpload` slot so custom\n * path-based importers get the same Postman detection behavior.\n *\n * When `type` is `'file'` the `source` is a file path resolved through the\n * configured `fileLoader`; when `type` is `'raw'` the `source` is treated as\n * the file's text content directly.\n */\nconst handleFileImport: (\n source: string,\n type?: 'file' | 'raw',\n) => Promise<void> = async (source, type = 'raw') => {\n // Resolve the raw text content so we can sniff for a Postman collection.\n // For raw pastes / uploads the source already is the text. For path-based\n // imports we delegate to the file loader plugin, if one is configured.\n const rawContent = await (async (): Promise<string> => {\n if (type === 'raw') {\n return source\n }\n\n const result = await fileLoader?.exec(source)\n return result?.ok ? result.raw : ''\n })()\n\n if (isPostmanCollection(rawContent)) {\n eventBus.emit('ui:open:command-palette', {\n action: 'import-postman-collection',\n payload: {\n inputValue: rawContent,\n },\n })\n await loader.clear()\n return\n }\n\n await handleImport(source, type)\n}\n\n/**\n * Handle file selection and import from file dialog.\n * Reads the file as text and imports it as OpenAPI or Postman collection.\n * Shows loading state during the import process.\n */\nconst { open: openSpecFileDialog } = useFileDialog({\n onChange: (files) => {\n const [file] = files ?? []\n\n if (!file) {\n return\n }\n\n loader.start()\n\n const onLoad = async (event: ProgressEvent<FileReader>): Promise<void> => {\n const text = event.target?.result as string\n await handleFileImport(text, 'raw')\n }\n\n const reader = new FileReader()\n reader.onload = onLoad\n reader.readAsText(file)\n },\n multiple: false,\n accept: '.json,.yaml,.yml',\n})\n\n/**\n * Handle input changes.\n * Detects cURL commands and redirects to the cURL import command.\n */\nconst handleInput = (value: string): void => {\n const trimmed = value.trim()\n\n if (trimmed.toLowerCase().startsWith('curl')) {\n return eventBus.emit('ui:open:command-palette', {\n action: 'import-curl-command',\n payload: {\n inputValue: value,\n },\n })\n }\n\n if (isPostmanCollection(trimmed)) {\n return eventBus.emit('ui:open:command-palette', {\n action: 'import-postman-collection',\n payload: {\n inputValue: value,\n },\n })\n }\n\n inputContent.value = value\n}\n\n/** Handle back navigation when user presses backspace on empty input */\nconst handleBack = (event: KeyboardEvent): void => {\n emit('back', event)\n}\n</script>\n<template>\n <CommandActionForm\n :disabled=\"isDisabled\"\n :loader\n @submit=\"handleImport(inputContent)\">\n <!-- URL or cURL input mode -->\n <template v-if=\"!documentDetails || isUrlInput\">\n <CommandActionInput\n :modelValue=\"inputContent\"\n placeholder=\"OpenAPI/Swagger/Postman URL or cURL\"\n @delete=\"handleBack\"\n @update:modelValue=\"handleInput\" />\n </template>\n\n <!-- Preview mode for pasted content -->\n <template v-else>\n <!-- Preview header with clear button -->\n <div class=\"flex justify-between\">\n <div class=\"text-c-2 min-h-8 w-full py-2 pl-12 text-center text-xs\">\n Preview\n </div>\n <ScalarButton\n class=\"hover:bg-b-2 relative ml-auto max-h-8 gap-1.5 p-2 text-xs\"\n variant=\"ghost\"\n @click=\"inputContent = ''\">\n Clear\n </ScalarButton>\n </div>\n\n <!-- Code preview with syntax highlighting -->\n <ScalarCodeBlock\n v-if=\"documentDetails && !isUrlInput\"\n class=\"bg-b-2 mt-1 max-h-[40dvh] rounded border px-2 py-1 text-sm\"\n :content=\"inputContent\"\n :copy=\"false\"\n :lang=\"documentType\" />\n </template>\n\n <!-- Actions: File upload and watch mode toggle -->\n <template #options>\n <div class=\"flex w-full flex-row items-center justify-between gap-3\">\n <!-- Custom file upload slot or default button -->\n <slot\n :import=\"handleFileImport\"\n name=\"fileUpload\">\n <!-- Default file upload button -->\n <ScalarButton\n class=\"hover:bg-b-2 relative max-h-8 gap-1.5 p-2 text-xs\"\n variant=\"outlined\"\n @click=\"openSpecFileDialog\">\n JSON, or YAML File\n <ScalarIcon\n class=\"text-c-3\"\n icon=\"Upload\"\n size=\"md\" />\n </ScalarButton>\n </slot>\n\n <!-- Watch mode toggle (only enabled for URL imports) -->\n <ScalarTooltip\n :content=\"\n isUrlInput\n ? 'Watch mode automatically updates the API client when the OpenAPI URL content changes, ensuring your client remains up-to-date.'\n : 'Watch mode is only available for URL imports. When enabled it automatically updates the API client when the OpenAPI URL content changes.'\n \"\n placement=\"bottom\">\n <WatchModeToggle\n v-model=\"watchMode\"\n :disabled=\"!isUrlInput\" />\n </ScalarTooltip>\n </div>\n </template>\n\n <!-- Dynamic submit button text based on import type -->\n <template #submit>\n Import\n <template v-if=\"isUrlInput\">from URL</template>\n <template v-else-if=\"documentDetails && documentType\">\n <template v-if=\"documentDetails.title\">\n \"{{ documentDetails.title }}\"\n </template>\n <template v-else>\n {{ documentDetails.version }}\n </template>\n </template>\n <template v-else>Collection</template>\n </template>\n </CommandActionForm>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"CommandPaletteImport.vue.script.js","names":[],"sources":["../../../../../src/v2/features/command-palette/components/CommandPaletteImport.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Command Palette Import Component\n *\n * Provides a form for importing OpenAPI descriptions from URL, file, or pasted JSON/YAML.\n * Postman collection JSON and Postman files open {@link CommandPaletteImportPostman}.\n * cURL commands redirect to {@link CommandPaletteImportCurl}.\n *\n * Supports watch mode for URL imports to automatically update when content changes.\n */\nexport default {\n name: 'CommandPaletteImport',\n}\n</script>\n\n<script setup lang=\"ts\">\nimport {\n ScalarButton,\n ScalarCodeBlock,\n ScalarIcon,\n ScalarTooltip,\n useLoadingState,\n} from '@scalar/components'\nimport { isLocalUrl } from '@scalar/helpers/url/is-local-url'\nimport type { LoaderPlugin } from '@scalar/json-magic/bundle'\nimport { isPostmanCollection } from '@scalar/postman-to-openapi'\nimport { useToasts } from '@scalar/use-toasts'\nimport {\n createWorkspaceStore,\n type WorkspaceStore,\n} from '@scalar/workspace-store/client'\nimport type { WorkspaceEventBus } from '@scalar/workspace-store/events'\nimport { computed, ref, watch } from 'vue'\n\nimport { useFileDialog } from '@/hooks/use-file-dialog'\nimport { getOpenApiDocumentDetails } from '@/v2/features/command-palette/helpers/get-openapi-document-details'\nimport { importDocumentToWorkspace } from '@/v2/features/command-palette/helpers/import-document-to-workspace'\nimport {\n loadDocumentFromSource,\n type ImportEventData,\n} from '@/v2/features/command-palette/helpers/load-document-from-source'\nimport { isUrl } from '@/v2/helpers/is-url'\n\nimport CommandActionForm from './CommandActionForm.vue'\nimport CommandActionInput from './CommandActionInput.vue'\nimport WatchModeToggle from './WatchModeToggle.vue'\n\nconst { workspaceStore, eventBus, fileLoader } = defineProps<{\n /** The workspace store for adding documents */\n workspaceStore: WorkspaceStore\n /** Event bus for emitting operation creation events */\n eventBus: WorkspaceEventBus\n /** Loader plugin for file import */\n fileLoader?: LoaderPlugin\n}>()\n\nconst emit = defineEmits<{\n /** Emitted when the import is complete or cancelled */\n (event: 'close'): void\n /** Emitted when user navigates back (e.g., backspace on empty input) */\n (event: 'back', keyboardEvent: KeyboardEvent): void\n}>()\n\ndefineSlots<{\n /**\n * Slot for custom file upload component that can trigger import.\n *\n * The provided `import` function automatically detects Postman collections\n * and routes them to the Postman import modal, matching the behavior of the\n * default file picker.\n */\n fileUpload(props: {\n /** Function to trigger import with source content and type */\n import: (source: string, type: 'file' | 'raw') => Promise<void>\n }): void\n}>()\n\nconst { toast } = useToasts()\n\nconst loader = useLoadingState()\n\nconst inputContent = ref('')\nconst watchMode = ref(false)\n\n/** Trim paste noise for URL checks without changing raw JSON/YAML content. */\nconst normalizedInputContent = computed<string>(() => inputContent.value.trim())\n\n/** Check if the input content is a URL */\nconst isUrlInput = computed<boolean>(() => isUrl(normalizedInputContent.value))\nconst isLocalUrlInput = computed<boolean>(\n () => isUrlInput.value && isLocalUrl(normalizedInputContent.value),\n)\n\nconst documentDetails = computed(() =>\n getOpenApiDocumentDetails(inputContent.value),\n)\n\n/** Get the document type for syntax highlighting */\nconst documentType = computed<string>(() =>\n documentDetails.value ? documentDetails.value.type : 'json',\n)\n\n/** Check if the form should be disabled (when input is empty) */\nconst isDisabled = computed<boolean>(() => {\n return !inputContent.value.trim()\n})\n\n/**\n * Toggle watchMode based on whether the input is a local URL.\n * Only enables watch mode for local URLs, not for files or pasted content.\n */\nwatch(isLocalUrlInput, (value: boolean) => {\n watchMode.value = value\n})\n\n/**\n * Handles errors during the import process.\n * Shows an error toast, invalidates the loader to show an error state,\n * and closes the command palette modal.\n *\n * @param errorMessage - The error message to display and log\n */\nconst handleImportError = async (errorMessage: string) => {\n // Log the error\n console.error(errorMessage)\n toast(errorMessage, 'error')\n\n // Invalidate the loader to show the error state\n await loader.invalidate()\n\n // Close the command palette\n emit('close')\n}\n\n/**\n * Directly imports a document into the workspace without showing the modal.\n * This is used when there is only one workspace and it is empty.\n */\nconst handleImport = async (\n newSource: string,\n type?: ImportEventData['type'],\n): Promise<void> => {\n loader.start()\n\n const TEMP_DOCUMENT_NAME = 'drafts'\n\n // First load the document into a draft store\n // This is to get the title of the document so we can generate a unique slug for store\n const draftStore = createWorkspaceStore({\n fileLoader,\n meta: {\n /** Ensure we use the active proxy to fetch documents */\n 'x-scalar-active-proxy':\n workspaceStore.workspace['x-scalar-active-proxy'],\n },\n })\n\n const eventType = (() => {\n if (type) {\n return type\n }\n\n if (isUrl(newSource.trim())) {\n return 'url'\n }\n\n return 'raw'\n })()\n\n const source = eventType === 'url' ? newSource.trim() : newSource\n\n const isSuccessfullyLoaded = await loadDocumentFromSource(\n draftStore,\n { source, type: eventType },\n TEMP_DOCUMENT_NAME,\n watchMode.value,\n )\n\n if (!isSuccessfullyLoaded) {\n return handleImportError('Failed to import document')\n }\n\n const importResult = await importDocumentToWorkspace({\n workspaceStore,\n workspaceState: draftStore.exportWorkspace(),\n name: TEMP_DOCUMENT_NAME,\n })\n\n if (!importResult.ok) {\n return handleImportError(importResult.error)\n }\n\n // Validate the loader to show the success state\n await loader.validate()\n\n // Navigate to the document overview page\n navigateToDocument(importResult.slug)\n\n // Close the command palette\n emit('close')\n}\n\n/** Navigate to the document overview page after successful import */\nconst navigateToDocument = (documentName: string): void => {\n eventBus.emit('ui:navigate', {\n page: 'document',\n path: 'overview',\n documentSlug: documentName,\n })\n}\n\n/**\n * Import a file, routing Postman collections to the Postman import modal and\n * everything else through the OpenAPI import flow.\n *\n * Shared between the default file picker and the `fileUpload` slot so custom\n * path-based importers get the same Postman detection behavior.\n *\n * When `type` is `'file'` the `source` is a file path resolved through the\n * configured `fileLoader`; when `type` is `'raw'` the `source` is treated as\n * the file's text content directly.\n */\nconst handleFileImport: (\n source: string,\n type?: 'file' | 'raw',\n) => Promise<void> = async (source, type = 'raw') => {\n // Resolve the raw text content so we can sniff for a Postman collection.\n // For raw pastes / uploads the source already is the text. For path-based\n // imports we delegate to the file loader plugin, if one is configured.\n const rawContent = await (async (): Promise<string> => {\n if (type === 'raw') {\n return source\n }\n\n const result = await fileLoader?.exec(source)\n return result?.ok ? result.raw : ''\n })()\n\n if (isPostmanCollection(rawContent)) {\n eventBus.emit('ui:open:command-palette', {\n action: 'import-postman-collection',\n payload: {\n inputValue: rawContent,\n },\n })\n await loader.clear()\n return\n }\n\n await handleImport(source, type)\n}\n\n/**\n * Handle file selection and import from file dialog.\n * Reads the file as text and imports it as OpenAPI or Postman collection.\n * Shows loading state during the import process.\n */\nconst { open: openSpecFileDialog } = useFileDialog({\n onChange: (files) => {\n const [file] = files ?? []\n\n if (!file) {\n return\n }\n\n loader.start()\n\n const onLoad = async (event: ProgressEvent<FileReader>): Promise<void> => {\n const text = event.target?.result as string\n await handleFileImport(text, 'raw')\n }\n\n const reader = new FileReader()\n reader.onload = onLoad\n reader.readAsText(file)\n },\n multiple: false,\n accept: '.json,.yaml,.yml',\n})\n\n/**\n * Handle input changes.\n * Detects cURL commands and redirects to the cURL import command.\n */\nconst handleInput = (value: string): void => {\n const trimmed = value.trim()\n\n if (trimmed.toLowerCase().startsWith('curl')) {\n return eventBus.emit('ui:open:command-palette', {\n action: 'import-curl-command',\n payload: {\n inputValue: value,\n },\n })\n }\n\n if (isPostmanCollection(trimmed)) {\n return eventBus.emit('ui:open:command-palette', {\n action: 'import-postman-collection',\n payload: {\n inputValue: value,\n },\n })\n }\n\n inputContent.value = value\n}\n\n/** Handle back navigation when user presses backspace on empty input */\nconst handleBack = (event: KeyboardEvent): void => {\n emit('back', event)\n}\n</script>\n<template>\n <CommandActionForm\n :disabled=\"isDisabled\"\n :loader\n @submit=\"handleImport(inputContent)\">\n <!-- URL or cURL input mode -->\n <template v-if=\"!documentDetails || isUrlInput\">\n <CommandActionInput\n :modelValue=\"inputContent\"\n placeholder=\"OpenAPI/Swagger/Postman URL or cURL\"\n @delete=\"handleBack\"\n @update:modelValue=\"handleInput\" />\n </template>\n\n <!-- Preview mode for pasted content -->\n <template v-else>\n <!-- Preview header with clear button -->\n <div class=\"flex justify-between\">\n <div class=\"text-c-2 min-h-8 w-full py-2 pl-12 text-center text-xs\">\n Preview\n </div>\n <ScalarButton\n class=\"hover:bg-b-2 relative ml-auto max-h-8 gap-1.5 p-2 text-xs\"\n variant=\"ghost\"\n @click=\"inputContent = ''\">\n Clear\n </ScalarButton>\n </div>\n\n <!-- Code preview with syntax highlighting -->\n <ScalarCodeBlock\n v-if=\"documentDetails && !isUrlInput\"\n class=\"bg-b-2 mt-1 max-h-[40dvh] rounded border px-2 py-1 text-sm\"\n :content=\"inputContent\"\n :copy=\"false\"\n :lang=\"documentType\" />\n </template>\n\n <!-- Actions: File upload and watch mode toggle -->\n <template #options>\n <div class=\"flex w-full flex-row items-center justify-between gap-3\">\n <!-- Custom file upload slot or default button -->\n <slot\n :import=\"handleFileImport\"\n name=\"fileUpload\">\n <!-- Default file upload button -->\n <ScalarButton\n class=\"hover:bg-b-2 relative max-h-8 gap-1.5 p-2 text-xs\"\n variant=\"outlined\"\n @click=\"openSpecFileDialog\">\n JSON, or YAML File\n <ScalarIcon\n class=\"text-c-3\"\n icon=\"Upload\"\n size=\"md\" />\n </ScalarButton>\n </slot>\n\n <!-- Watch mode toggle (only enabled for URL imports) -->\n <ScalarTooltip\n :content=\"\n isUrlInput\n ? 'Watch mode automatically updates the API client when the OpenAPI URL content changes, ensuring your client remains up-to-date.'\n : 'Watch mode is only available for URL imports. When enabled it automatically updates the API client when the OpenAPI URL content changes.'\n \"\n placement=\"bottom\">\n <WatchModeToggle\n v-model=\"watchMode\"\n :disabled=\"!isUrlInput\" />\n </ScalarTooltip>\n </div>\n </template>\n\n <!-- Dynamic submit button text based on import type -->\n <template #submit>\n Import\n <template v-if=\"isUrlInput\">from URL</template>\n <template v-else-if=\"documentDetails && documentType\">\n <template v-if=\"documentDetails.title\">\n \"{{ documentDetails.title }}\"\n </template>\n <template v-else>\n {{ documentDetails.version }}\n </template>\n </template>\n <template v-else>Collection</template>\n </template>\n </CommandActionForm>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;CAWE,MAAM;;;;;;;;EA6CR,MAAM,OAAO;EAqBb,MAAM,EAAE,UAAU,WAAU;EAE5B,MAAM,SAAS,iBAAgB;EAE/B,MAAM,eAAe,IAAI,GAAE;EAC3B,MAAM,YAAY,IAAI,MAAK;;EAG3B,MAAM,yBAAyB,eAAuB,aAAa,MAAM,MAAM,CAAA;;EAG/E,MAAM,aAAa,eAAwB,MAAM,uBAAuB,MAAM,CAAA;EAC9E,MAAM,kBAAkB,eAChB,WAAW,SAAS,WAAW,uBAAuB,MAAM,CACpE;EAEA,MAAM,kBAAkB,eACtB,0BAA0B,aAAa,MAAM,CAC/C;;EAGA,MAAM,eAAe,eACnB,gBAAgB,QAAQ,gBAAgB,MAAM,OAAO,OACvD;;EAGA,MAAM,aAAa,eAAwB;AACzC,UAAO,CAAC,aAAa,MAAM,MAAK;IACjC;;;;;AAMD,QAAM,kBAAkB,UAAmB;AACzC,aAAU,QAAQ;IACnB;;;;;;;;EASD,MAAM,oBAAoB,OAAO,iBAAyB;AAExD,WAAQ,MAAM,aAAY;AAC1B,SAAM,cAAc,QAAO;AAG3B,SAAM,OAAO,YAAW;AAGxB,QAAK,QAAO;;;;;;EAOd,MAAM,eAAe,OACnB,WACA,SACkB;AAClB,UAAO,OAAM;GAEb,MAAM,qBAAqB;GAI3B,MAAM,aAAa,qBAAqB;IACtC,YAAS,QAAA;IACT,MAAM,EAEJ,yBACE,QAAA,eAAe,UAAU,0BAC5B;IACF,CAAA;GAED,MAAM,mBAAmB;AACvB,QAAI,KACF,QAAO;AAGT,QAAI,MAAM,UAAU,MAAM,CAAC,CACzB,QAAO;AAGT,WAAO;OACN;AAWH,OAAI,CAPyB,MAAM,uBACjC,YACA;IAAE,QAJW,cAAc,QAAQ,UAAU,MAAM,GAAG;IAI5C,MAAM;IAAW,EAC3B,oBACA,UAAU,MACZ,CAGE,QAAO,kBAAkB,4BAA2B;GAGtD,MAAM,eAAe,MAAM,0BAA0B;IACnD,gBAAa,QAAA;IACb,gBAAgB,WAAW,iBAAiB;IAC5C,MAAM;IACP,CAAA;AAED,OAAI,CAAC,aAAa,GAChB,QAAO,kBAAkB,aAAa,MAAK;AAI7C,SAAM,OAAO,UAAS;AAGtB,sBAAmB,aAAa,KAAI;AAGpC,QAAK,QAAO;;;EAId,MAAM,sBAAsB,iBAA+B;AACzD,WAAA,SAAS,KAAK,eAAe;IAC3B,MAAM;IACN,MAAM;IACN,cAAc;IACf,CAAA;;;;;;;;;;;;;EAcH,MAAM,mBAGe,OAAO,QAAQ,OAAO,UAAU;GAInD,MAAM,aAAa,OAAO,YAA6B;AACrD,QAAI,SAAS,MACX,QAAO;IAGT,MAAM,SAAS,MAAM,QAAA,YAAY,KAAK,OAAM;AAC5C,WAAO,QAAQ,KAAK,OAAO,MAAM;OAChC;AAEH,OAAI,oBAAoB,WAAW,EAAE;AACnC,YAAA,SAAS,KAAK,2BAA2B;KACvC,QAAQ;KACR,SAAS,EACP,YAAY,YACb;KACF,CAAA;AACD,UAAM,OAAO,OAAM;AACnB;;AAGF,SAAM,aAAa,QAAQ,KAAI;;;;;;;EAQjC,MAAM,EAAE,MAAM,uBAAuB,cAAc;GACjD,WAAW,UAAU;IACnB,MAAM,CAAC,QAAQ,SAAS,EAAC;AAEzB,QAAI,CAAC,KACH;AAGF,WAAO,OAAM;IAEb,MAAM,SAAS,OAAO,UAAoD;KACxE,MAAM,OAAO,MAAM,QAAQ;AAC3B,WAAM,iBAAiB,MAAM,MAAK;;IAGpC,MAAM,SAAS,IAAI,YAAW;AAC9B,WAAO,SAAS;AAChB,WAAO,WAAW,KAAI;;GAExB,UAAU;GACV,QAAQ;GACT,CAAA;;;;;EAMD,MAAM,eAAe,UAAwB;GAC3C,MAAM,UAAU,MAAM,MAAK;AAE3B,OAAI,QAAQ,aAAa,CAAC,WAAW,OAAO,CAC1C,QAAO,QAAA,SAAS,KAAK,2BAA2B;IAC9C,QAAQ;IACR,SAAS,EACP,YAAY,OACb;IACF,CAAA;AAGH,OAAI,oBAAoB,QAAQ,CAC9B,QAAO,QAAA,SAAS,KAAK,2BAA2B;IAC9C,QAAQ;IACR,SAAS,EACP,YAAY,OACb;IACF,CAAA;AAGH,gBAAa,QAAQ;;;EAIvB,MAAM,cAAc,UAA+B;AACjD,QAAK,QAAQ,MAAK;;;uBAIlB,YAsFoB,2BAAA;IArFjB,UAAU,WAAA;IACV,QAAA,MAAA,OAAM;IACN,UAAM,OAAA,OAAA,OAAA,MAAA,WAAE,aAAa,aAAA,MAAY;;IAmCvB,SAAO,cA+BV,CA9BN,mBA8BM,OA9BN,YA8BM,CA5BJ,WAcO,KAAA,QAAA,cAAA,EAbJ,QAAQ,kBAAgB,QAapB,CAVL,YASe,MAAA,aAAA,EAAA;KARb,OAAM;KACN,SAAQ;KACP,SAAO,MAAA,mBAAkB;;4BAE1B,CAAA,OAAA,OAAA,OAAA,KAAA,gBAF4B,wBAE5B,GAAA,GAAA,YAGc,MAAA,WAAA,EAAA;MAFZ,OAAM;MACN,MAAK;MACL,MAAK;;;0BAKX,YAUgB,MAAA,cAAA,EAAA;KATb,SAAsB,WAAA,QAAA,mIAAA;KAKvB,WAAU;;4BAGkB,CAF5B,YAE4B,yBAAA;kBADjB,UAAA;6EAAS,QAAA;MACjB,UAAQ,CAAG,WAAA;;;;IAMT,QAAM,cAEf,CAAA,OAAA,OAAA,OAAA,KAAA,gBAFgB,YAEhB,GAAA,GAAgB,WAAA,SAAA,WAAA,EAAhB,mBAA+C,UAAA,EAAA,KAAA,GAAA,EAAA,CAAA,gBAAnB,WAAQ,CAAA,EAAA,GAAA,IACf,gBAAA,SAAmB,aAAA,SAAA,WAAA,EAAxC,mBAOW,UAAA,EAAA,KAAA,GAAA,EAAA,CANO,gBAAA,MAAgB,SAAA,WAAA,EAAhC,mBAEW,UAAA,EAAA,KAAA,GAAA,EAAA,CAAA,gBAF4B,QACpC,gBAAG,gBAAA,MAAgB,MAAK,GAAG,OAC9B,EAAA,CAAA,EAAA,GAAA,KAAA,WAAA,EACA,mBAEW,UAAA,EAAA,KAAA,GAAA,EAAA,CAAA,gBAAA,gBADN,gBAAA,MAAgB,QAAO,EAAA,EAAA,CAAA,EAAA,GAAA,EAAA,EAAA,GAAA,KAAA,WAAA,EAG9B,mBAAsC,UAAA,EAAA,KAAA,GAAA,EAAA,CAAA,gBAArB,aAAU,CAAA,EAAA,GAAA,EAAA,CAAA;2BAzElB,CAAA,CANM,gBAAA,SAAmB,WAAA,SAAA,WAAA,EAClC,YAIqC,4BAAA;;KAHlC,YAAY,aAAA;KACb,aAAY;KACX,UAAQ;KACR,uBAAmB;iDAIxB,mBAqBW,UAAA,EAAA,KAAA,GAAA,EAAA,CAnBT,mBAUM,OAVN,YAUM,CAAA,OAAA,OAAA,OAAA,KATJ,mBAEM,OAAA,EAFD,OAAM,0DAAwD,EAAC,aAEpE,GAAA,GACA,YAKe,MAAA,aAAA,EAAA;KAJb,OAAM;KACN,SAAQ;KACP,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,aAAA,QAAY;;4BAEtB,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,CAAA,gBAF6B,WAE7B,GAAA,CAAA,EAAA,CAAA;;UAKM,gBAAA,SAAe,CAAK,WAAA,SAAA,WAAA,EAD5B,YAKyB,MAAA,gBAAA,EAAA;;KAHvB,OAAM;KACL,SAAS,aAAA;KACT,MAAM;KACN,MAAM,aAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"CommandPaletteImportCurl.vue.d.ts","sourceRoot":"","sources":["../../../../../src/v2/features/command-palette/components/CommandPaletteImportCurl.vue"],"names":[],"mappings":"AAwOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAA;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAUvE;;;;;;;;;;;;;;;;;GAiBG;wBACkB,OAAO,YAAY;AAAxC,wBAAyC;AAKzC,QAAA,MAAM,YAAY;IAEhB,iEAAiE;oBACjD,cAAc;IAC9B,uDAAuD;cAC7C,iBAAiB;IAC3B,kDAAkD;gBACtC,MAAM;;;;;IALlB,iEAAiE;oBACjD,cAAc;IAC9B,uDAAuD;cAC7C,iBAAiB;IAC3B,kDAAkD;gBACtC,MAAM;;;;kFA8ShB,CAAC"}
1
+ {"version":3,"file":"CommandPaletteImportCurl.vue.d.ts","sourceRoot":"","sources":["../../../../../src/v2/features/command-palette/components/CommandPaletteImportCurl.vue"],"names":[],"mappings":"AA0PA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAA;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AASvE;;;;;;;;;;;;;;;;;GAiBG;wBACkB,OAAO,YAAY;AAAxC,wBAAyC;AAKzC,QAAA,MAAM,YAAY;IAEhB,iEAAiE;oBACjD,cAAc;IAC9B,uDAAuD;cAC7C,iBAAiB;IAC3B,kDAAkD;gBACtC,MAAM;;;;;IALlB,iEAAiE;oBACjD,cAAc;IAC9B,uDAAuD;cAC7C,iBAAiB;IAC3B,kDAAkD;gBACtC,MAAM;;;;kFAsUhB,CAAC"}
@@ -2,7 +2,7 @@ import _plugin_vue_export_helper_default from "../../../../_virtual/_plugin-vue_
2
2
  import CommandPaletteImportCurl_vue_vue_type_script_setup_true_lang_default from "./CommandPaletteImportCurl.vue.script.js";
3
3
  /* empty css */
4
4
  //#region src/v2/features/command-palette/components/CommandPaletteImportCurl.vue
5
- var CommandPaletteImportCurl_default = /* @__PURE__ */ _plugin_vue_export_helper_default(CommandPaletteImportCurl_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-f4568236"]]);
5
+ var CommandPaletteImportCurl_default = /* @__PURE__ */ _plugin_vue_export_helper_default(CommandPaletteImportCurl_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-311ac78f"]]);
6
6
  //#endregion
7
7
  export { CommandPaletteImportCurl_default as default };
8
8