@scalar/api-client 3.2.1 → 3.3.0

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 (120) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/style.css +102 -130
  3. package/dist/v2/blocks/operation-block/OperationBlock.vue.d.ts.map +1 -1
  4. package/dist/v2/blocks/operation-block/OperationBlock.vue.js.map +1 -1
  5. package/dist/v2/blocks/operation-block/OperationBlock.vue.script.js +9 -0
  6. package/dist/v2/blocks/operation-block/OperationBlock.vue.script.js.map +1 -1
  7. package/dist/v2/blocks/operation-block/components/Header.vue.d.ts +2 -0
  8. package/dist/v2/blocks/operation-block/components/Header.vue.d.ts.map +1 -1
  9. package/dist/v2/blocks/operation-block/components/Header.vue.js +1 -1
  10. package/dist/v2/blocks/operation-block/components/Header.vue.js.map +1 -1
  11. package/dist/v2/blocks/operation-block/components/Header.vue.script.js +20 -10
  12. package/dist/v2/blocks/operation-block/components/Header.vue.script.js.map +1 -1
  13. package/dist/v2/blocks/operation-block/helpers/send-request.d.ts.map +1 -1
  14. package/dist/v2/blocks/operation-block/helpers/send-request.js +2 -2
  15. package/dist/v2/blocks/operation-block/helpers/send-request.js.map +1 -1
  16. package/dist/v2/blocks/request-block/RequestBlock.vue.script.js.map +1 -1
  17. package/dist/v2/blocks/request-block/components/RequestTable.vue.d.ts +2 -2
  18. package/dist/v2/blocks/request-block/components/RequestTableRow.vue.d.ts +2 -2
  19. package/dist/v2/blocks/response-block/components/ResponseEmpty.vue.d.ts +2 -2
  20. package/dist/v2/blocks/scalar-auth-selector-block/components/AuthSelector.vue.script.js.map +1 -1
  21. package/dist/v2/components/server/ServerDropdown.vue.d.ts +2 -2
  22. package/dist/v2/components/sidebar/Sidebar.vue.script.js +2 -2
  23. package/dist/v2/components/sidebar/Sidebar.vue.script.js.map +1 -1
  24. package/dist/v2/constants.js +1 -1
  25. package/dist/v2/features/app/App.vue.d.ts +22 -4
  26. package/dist/v2/features/app/App.vue.d.ts.map +1 -1
  27. package/dist/v2/features/app/App.vue.js.map +1 -1
  28. package/dist/v2/features/app/App.vue.script.js +57 -54
  29. package/dist/v2/features/app/App.vue.script.js.map +1 -1
  30. package/dist/v2/features/app/app-events.d.ts.map +1 -1
  31. package/dist/v2/features/app/app-events.js +4 -0
  32. package/dist/v2/features/app/app-events.js.map +1 -1
  33. package/dist/v2/features/app/app-state.d.ts +7 -1
  34. package/dist/v2/features/app/app-state.d.ts.map +1 -1
  35. package/dist/v2/features/app/app-state.js +24 -4
  36. package/dist/v2/features/app/app-state.js.map +1 -1
  37. package/dist/v2/features/app/components/AppHeader.vue.d.ts +20 -0
  38. package/dist/v2/features/app/components/AppHeader.vue.d.ts.map +1 -0
  39. package/dist/v2/features/app/components/AppHeader.vue.js +7 -0
  40. package/dist/v2/features/app/components/AppHeader.vue.js.map +1 -0
  41. package/dist/v2/features/app/components/AppHeader.vue.script.js +43 -0
  42. package/dist/v2/features/app/components/AppHeader.vue.script.js.map +1 -0
  43. package/dist/v2/features/app/components/AppSidebar.vue.d.ts +17 -56
  44. package/dist/v2/features/app/components/AppSidebar.vue.d.ts.map +1 -1
  45. package/dist/v2/features/app/components/AppSidebar.vue.js +1 -1
  46. package/dist/v2/features/app/components/AppSidebar.vue.js.map +1 -1
  47. package/dist/v2/features/app/components/AppSidebar.vue.script.js +437 -271
  48. package/dist/v2/features/app/components/AppSidebar.vue.script.js.map +1 -1
  49. package/dist/v2/features/app/helpers/load-registry-document.d.ts +18 -0
  50. package/dist/v2/features/app/helpers/load-registry-document.d.ts.map +1 -0
  51. package/dist/v2/features/app/helpers/load-registry-document.js +45 -0
  52. package/dist/v2/features/app/helpers/load-registry-document.js.map +1 -0
  53. package/dist/v2/features/app/helpers/routes.d.ts +6 -1
  54. package/dist/v2/features/app/helpers/routes.d.ts.map +1 -1
  55. package/dist/v2/features/app/helpers/routes.js +84 -75
  56. package/dist/v2/features/app/helpers/routes.js.map +1 -1
  57. package/dist/v2/features/app/hooks/use-document-filter.d.ts +38 -0
  58. package/dist/v2/features/app/hooks/use-document-filter.d.ts.map +1 -0
  59. package/dist/v2/features/app/hooks/use-document-filter.js +63 -0
  60. package/dist/v2/features/app/hooks/use-document-filter.js.map +1 -0
  61. package/dist/v2/features/app/hooks/use-sidebar-context-menu.d.ts +17258 -0
  62. package/dist/v2/features/app/hooks/use-sidebar-context-menu.d.ts.map +1 -0
  63. package/dist/v2/features/app/hooks/use-sidebar-context-menu.js +107 -0
  64. package/dist/v2/features/app/hooks/use-sidebar-context-menu.js.map +1 -0
  65. package/dist/v2/features/app/hooks/use-sidebar-documents.d.ts +95 -0
  66. package/dist/v2/features/app/hooks/use-sidebar-documents.d.ts.map +1 -0
  67. package/dist/v2/features/app/hooks/use-sidebar-documents.js +97 -0
  68. package/dist/v2/features/app/hooks/use-sidebar-documents.js.map +1 -0
  69. package/dist/v2/features/app/index.d.ts +1 -0
  70. package/dist/v2/features/app/index.d.ts.map +1 -1
  71. package/dist/v2/features/collection/components/GetStarted.vue.d.ts +13 -0
  72. package/dist/v2/features/collection/components/GetStarted.vue.d.ts.map +1 -0
  73. package/dist/v2/features/collection/components/GetStarted.vue.js +7 -0
  74. package/dist/v2/features/collection/components/GetStarted.vue.js.map +1 -0
  75. package/dist/v2/features/collection/components/GetStarted.vue.script.js +101 -0
  76. package/dist/v2/features/collection/components/GetStarted.vue.script.js.map +1 -0
  77. package/dist/v2/features/command-palette/components/CommandPaletteImport.vue.d.ts +7 -1
  78. package/dist/v2/features/command-palette/components/CommandPaletteImport.vue.d.ts.map +1 -1
  79. package/dist/v2/features/command-palette/components/CommandPaletteImport.vue.js.map +1 -1
  80. package/dist/v2/features/command-palette/components/CommandPaletteImport.vue.script.js +29 -10
  81. package/dist/v2/features/command-palette/components/CommandPaletteImport.vue.script.js.map +1 -1
  82. package/dist/v2/features/command-palette/helpers/generate-unique-slug.d.ts +4 -0
  83. package/dist/v2/features/command-palette/helpers/generate-unique-slug.d.ts.map +1 -1
  84. package/dist/v2/features/command-palette/helpers/generate-unique-slug.js +5 -1
  85. package/dist/v2/features/command-palette/helpers/generate-unique-slug.js.map +1 -1
  86. package/dist/v2/features/search/components/DocumentSearchModal.vue.d.ts +16 -0
  87. package/dist/v2/features/search/components/DocumentSearchModal.vue.d.ts.map +1 -0
  88. package/dist/v2/features/search/components/DocumentSearchModal.vue.js +9 -0
  89. package/dist/v2/features/search/components/DocumentSearchModal.vue.js.map +1 -0
  90. package/dist/v2/features/search/components/DocumentSearchModal.vue.script.js +123 -0
  91. package/dist/v2/features/search/components/DocumentSearchModal.vue.script.js.map +1 -0
  92. package/dist/v2/features/search/components/SearchResult.vue.d.ts +11 -0
  93. package/dist/v2/features/search/components/SearchResult.vue.d.ts.map +1 -0
  94. package/dist/v2/features/search/components/SearchResult.vue.js +7 -0
  95. package/dist/v2/features/search/components/SearchResult.vue.js.map +1 -0
  96. package/dist/v2/features/search/components/SearchResult.vue.script.js +71 -0
  97. package/dist/v2/features/search/components/SearchResult.vue.script.js.map +1 -0
  98. package/dist/v2/features/search/hooks/use-document-search.d.ts +19 -0
  99. package/dist/v2/features/search/hooks/use-document-search.d.ts.map +1 -0
  100. package/dist/v2/features/search/hooks/use-document-search.js +42 -0
  101. package/dist/v2/features/search/hooks/use-document-search.js.map +1 -0
  102. package/dist/v2/features/search/index.d.ts +2 -0
  103. package/dist/v2/features/search/index.d.ts.map +1 -1
  104. package/dist/v2/features/search/index.js +3 -1
  105. package/dist/v2/helpers/handle-hotkeys.d.ts.map +1 -1
  106. package/dist/v2/helpers/handle-hotkeys.js +8 -4
  107. package/dist/v2/helpers/handle-hotkeys.js.map +1 -1
  108. package/dist/v2/types/configuration.d.ts +1 -0
  109. package/dist/v2/types/configuration.d.ts.map +1 -1
  110. package/package.json +13 -12
  111. package/dist/assets/rabbit.ascii.virtual.js +0 -6
  112. package/dist/assets/rabbit.ascii.virtual.js.map +0 -1
  113. package/dist/assets/rabbitjump.ascii.virtual.js +0 -6
  114. package/dist/assets/rabbitjump.ascii.virtual.js.map +0 -1
  115. package/dist/v2/features/app/components/DownloadAppButton.vue.d.ts +0 -4
  116. package/dist/v2/features/app/components/DownloadAppButton.vue.d.ts.map +0 -1
  117. package/dist/v2/features/app/components/DownloadAppButton.vue.js +0 -9
  118. package/dist/v2/features/app/components/DownloadAppButton.vue.js.map +0 -1
  119. package/dist/v2/features/app/components/DownloadAppButton.vue.script.js +0 -21
  120. package/dist/v2/features/app/components/DownloadAppButton.vue.script.js.map +0 -1
@@ -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 /** Slot for custom file upload component that can trigger import */\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 * 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 if (isPostmanCollection(text)) {\n eventBus.emit('ui:open:command-palette', {\n action: 'import-postman-collection',\n payload: {\n inputValue: text,\n },\n })\n await loader.clear()\n return\n }\n await handleImport(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=\"handleImport\"\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'\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":""}
@@ -98,6 +98,33 @@ var CommandPaletteImport_vue_vue_type_script_setup_true_lang_default = /* @__PUR
98
98
  });
99
99
  };
100
100
  /**
101
+ * Import a file, routing Postman collections to the Postman import modal and
102
+ * everything else through the OpenAPI import flow.
103
+ *
104
+ * Shared between the default file picker and the `fileUpload` slot so custom
105
+ * path-based importers get the same Postman detection behavior.
106
+ *
107
+ * When `type` is `'file'` the `source` is a file path resolved through the
108
+ * configured `fileLoader`; when `type` is `'raw'` the `source` is treated as
109
+ * the file's text content directly.
110
+ */
111
+ const handleFileImport = async (source, type = "raw") => {
112
+ const rawContent = await (async () => {
113
+ if (type === "raw") return source;
114
+ const result = await __props.fileLoader?.exec(source);
115
+ return result?.ok ? result.raw : "";
116
+ })();
117
+ if (isPostmanCollection(rawContent)) {
118
+ __props.eventBus.emit("ui:open:command-palette", {
119
+ action: "import-postman-collection",
120
+ payload: { inputValue: rawContent }
121
+ });
122
+ await loader.clear();
123
+ return;
124
+ }
125
+ await handleImport(source, type);
126
+ };
127
+ /**
101
128
  * Handle file selection and import from file dialog.
102
129
  * Reads the file as text and imports it as OpenAPI or Postman collection.
103
130
  * Shows loading state during the import process.
@@ -109,15 +136,7 @@ var CommandPaletteImport_vue_vue_type_script_setup_true_lang_default = /* @__PUR
109
136
  loader.start();
110
137
  const onLoad = async (event) => {
111
138
  const text = event.target?.result;
112
- if (isPostmanCollection(text)) {
113
- __props.eventBus.emit("ui:open:command-palette", {
114
- action: "import-postman-collection",
115
- payload: { inputValue: text }
116
- });
117
- await loader.clear();
118
- return;
119
- }
120
- await handleImport(text, "raw");
139
+ await handleFileImport(text, "raw");
121
140
  };
122
141
  const reader = new FileReader();
123
142
  reader.onload = onLoad;
@@ -152,7 +171,7 @@ var CommandPaletteImport_vue_vue_type_script_setup_true_lang_default = /* @__PUR
152
171
  loader: unref(loader),
153
172
  onSubmit: _cache[2] || (_cache[2] = ($event) => handleImport(inputContent.value))
154
173
  }, {
155
- options: withCtx(() => [createElementVNode("div", _hoisted_2, [renderSlot(_ctx.$slots, "fileUpload", { import: handleImport }, () => [createVNode(unref(ScalarButton), {
174
+ options: withCtx(() => [createElementVNode("div", _hoisted_2, [renderSlot(_ctx.$slots, "fileUpload", { import: handleFileImport }, () => [createVNode(unref(ScalarButton), {
156
175
  class: "hover:bg-b-2 relative max-h-8 gap-1.5 p-2 text-xs",
157
176
  variant: "outlined",
158
177
  onClick: unref(openSpecFileDialog)
@@ -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 /** Slot for custom file upload component that can trigger import */\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 * 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 if (isPostmanCollection(text)) {\n eventBus.emit('ui:open:command-palette', {\n action: 'import-postman-collection',\n payload: {\n inputValue: text,\n },\n })\n await loader.clear()\n return\n }\n await handleImport(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=\"handleImport\"\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'\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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"}
@@ -7,6 +7,10 @@
7
7
  * The function will retry up to 100 times to find a unique slug. If all attempts fail,
8
8
  * it returns null, which should be handled as an import error.
9
9
  *
10
+ * When the input is missing or contains only whitespace, it falls back to
11
+ * `'default'` so the workspace store never ends up with a document keyed by an
12
+ * empty string (for example, when a registry document has no `info.title`).
13
+ *
10
14
  * @param defaultValue - The original document title to base the slug on
11
15
  * @param currentDocuments - Set of existing document slugs to check against
12
16
  *
@@ -1 +1 @@
1
- {"version":3,"file":"generate-unique-slug.d.ts","sourceRoot":"","sources":["../../../../../src/v2/features/command-palette/helpers/generate-unique-slug.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,kBAAkB,GAAU,cAAc,MAAM,GAAG,SAAS,EAAE,kBAAkB,GAAG,CAAC,MAAM,CAAC,gCAOvG,CAAA"}
1
+ {"version":3,"file":"generate-unique-slug.d.ts","sourceRoot":"","sources":["../../../../../src/v2/features/command-palette/helpers/generate-unique-slug.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,kBAAkB,GAAU,cAAc,MAAM,GAAG,SAAS,EAAE,kBAAkB,GAAG,CAAC,MAAM,CAAC,gCASvG,CAAA"}
@@ -10,6 +10,10 @@ import { generateUniqueValue } from "@scalar/workspace-store/helpers/generate-un
10
10
  * The function will retry up to 100 times to find a unique slug. If all attempts fail,
11
11
  * it returns null, which should be handled as an import error.
12
12
  *
13
+ * When the input is missing or contains only whitespace, it falls back to
14
+ * `'default'` so the workspace store never ends up with a document keyed by an
15
+ * empty string (for example, when a registry document has no `info.title`).
16
+ *
13
17
  * @param defaultValue - The original document title to base the slug on
14
18
  * @param currentDocuments - Set of existing document slugs to check against
15
19
  *
@@ -17,7 +21,7 @@ import { generateUniqueValue } from "@scalar/workspace-store/helpers/generate-un
17
21
  */
18
22
  var generateUniqueSlug = async (defaultValue, currentDocuments) => {
19
23
  return await generateUniqueValue({
20
- defaultValue: defaultValue ?? "default",
24
+ defaultValue: defaultValue?.trim() || "default",
21
25
  validation: (value) => !currentDocuments.has(value),
22
26
  maxRetries: 100,
23
27
  transformation: slugify
@@ -1 +1 @@
1
- {"version":3,"file":"generate-unique-slug.js","names":[],"sources":["../../../../../src/v2/features/command-palette/helpers/generate-unique-slug.ts"],"sourcesContent":["import { generateUniqueValue } from '@scalar/workspace-store/helpers/generate-unique-value'\n\nimport { slugify } from '@/v2/helpers/slugify'\n\n/**\n * Generates a unique slug for an imported document based on its title.\n *\n * This ensures the imported document does not conflict with existing documents\n * by appending a number suffix if necessary (e.g., \"my-api\", \"my-api-1\", \"my-api-2\").\n *\n * The function will retry up to 100 times to find a unique slug. If all attempts fail,\n * it returns null, which should be handled as an import error.\n *\n * @param defaultValue - The original document title to base the slug on\n * @param currentDocuments - Set of existing document slugs to check against\n *\n * @returns Promise resolving to a unique slug, or null if unable to generate one\n */\nexport const generateUniqueSlug = async (defaultValue: string | undefined, currentDocuments: Set<string>) => {\n return await generateUniqueValue({\n defaultValue: defaultValue ?? 'default',\n validation: (value) => !currentDocuments.has(value),\n maxRetries: 100,\n transformation: slugify,\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAkBA,IAAa,qBAAqB,OAAO,cAAkC,qBAAkC;AAC3G,QAAO,MAAM,oBAAoB;EAC/B,cAAc,gBAAgB;EAC9B,aAAa,UAAU,CAAC,iBAAiB,IAAI,MAAM;EACnD,YAAY;EACZ,gBAAgB;EACjB,CAAC"}
1
+ {"version":3,"file":"generate-unique-slug.js","names":[],"sources":["../../../../../src/v2/features/command-palette/helpers/generate-unique-slug.ts"],"sourcesContent":["import { generateUniqueValue } from '@scalar/workspace-store/helpers/generate-unique-value'\n\nimport { slugify } from '@/v2/helpers/slugify'\n\n/**\n * Generates a unique slug for an imported document based on its title.\n *\n * This ensures the imported document does not conflict with existing documents\n * by appending a number suffix if necessary (e.g., \"my-api\", \"my-api-1\", \"my-api-2\").\n *\n * The function will retry up to 100 times to find a unique slug. If all attempts fail,\n * it returns null, which should be handled as an import error.\n *\n * When the input is missing or contains only whitespace, it falls back to\n * `'default'` so the workspace store never ends up with a document keyed by an\n * empty string (for example, when a registry document has no `info.title`).\n *\n * @param defaultValue - The original document title to base the slug on\n * @param currentDocuments - Set of existing document slugs to check against\n *\n * @returns Promise resolving to a unique slug, or null if unable to generate one\n */\nexport const generateUniqueSlug = async (defaultValue: string | undefined, currentDocuments: Set<string>) => {\n const base = defaultValue?.trim() || 'default'\n\n return await generateUniqueValue({\n defaultValue: base,\n validation: (value) => !currentDocuments.has(value),\n maxRetries: 100,\n transformation: slugify,\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAsBA,IAAa,qBAAqB,OAAO,cAAkC,qBAAkC;AAG3G,QAAO,MAAM,oBAAoB;EAC/B,cAHW,cAAc,MAAM,IAAI;EAInC,aAAa,UAAU,CAAC,iBAAiB,IAAI,MAAM;EACnD,YAAY;EACZ,gBAAgB;EACjB,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { type ModalState } from '@scalar/components';
2
+ import type { OpenApiDocument } from '@scalar/workspace-store/schemas/v3.1/strict/openapi-document';
3
+ type __VLS_Props = {
4
+ /** Controls the visibility of the search modal. */
5
+ modalState: ModalState;
6
+ /** The document whose entries should be searched. */
7
+ document: OpenApiDocument | undefined;
8
+ };
9
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
10
+ select: (id: string) => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ onSelect?: ((id: string) => any) | undefined;
13
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ declare const _default: typeof __VLS_export;
15
+ export default _default;
16
+ //# sourceMappingURL=DocumentSearchModal.vue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DocumentSearchModal.vue.d.ts","sourceRoot":"","sources":["../../../../../src/v2/features/search/components/DocumentSearchModal.vue"],"names":[],"mappings":"AA4JA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8DAA8D,CAAA;AAQnG,KAAK,WAAW,GAAG;IACjB,mDAAmD;IACnD,UAAU,EAAE,UAAU,CAAA;IACtB,qDAAqD;IACrD,QAAQ,EAAE,eAAe,GAAG,SAAS,CAAA;CACtC,CAAC;AAgQF,QAAA,MAAM,YAAY;;;;kFAGhB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
@@ -0,0 +1,9 @@
1
+ import _plugin_vue_export_helper_default from "../../../../_virtual/_plugin-vue_export-helper.js";
2
+ import DocumentSearchModal_vue_vue_type_script_setup_true_lang_default from "./DocumentSearchModal.vue.script.js";
3
+ /* empty css */
4
+ //#region src/v2/features/search/components/DocumentSearchModal.vue
5
+ var DocumentSearchModal_default = /* @__PURE__ */ _plugin_vue_export_helper_default(DocumentSearchModal_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-a00657cc"]]);
6
+ //#endregion
7
+ export { DocumentSearchModal_default as default };
8
+
9
+ //# sourceMappingURL=DocumentSearchModal.vue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DocumentSearchModal.vue.js","names":[],"sources":["../../../../../src/v2/features/search/components/DocumentSearchModal.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport {\n ScalarModal,\n ScalarSearchInput,\n ScalarSearchResultList,\n type ModalState,\n} from '@scalar/components'\nimport type { OpenApiDocument } from '@scalar/workspace-store/schemas/v3.1/strict/openapi-document'\nimport { nanoid } from 'nanoid'\nimport { computed, ref, watch } from 'vue'\n\nimport { useDocumentSearch } from '@/v2/features/search/hooks/use-document-search'\n\nimport SearchResult from './SearchResult.vue'\n\nconst props = defineProps<{\n /** Controls the visibility of the search modal. */\n modalState: ModalState\n /** The document whose entries should be searched. */\n document: OpenApiDocument | undefined\n}>()\n\nconst emit = defineEmits<{\n /** Emitted when the user picks a result, passing the navigation id. */\n (e: 'select', id: string): void\n}>()\n\n/** Base id for the search form. */\nconst id = nanoid()\nconst listboxId = `${id}-search-result`\nconst instructionsId = `${id}-search-instructions`\n\nconst { query, results } = useDocumentSearch(() => props.document)\n\nconst selectedIndex = ref<number | undefined>(undefined)\n\n/**\n * Reset the query every time the modal is opened so users always start from a\n * clean state. Matches the reference search modal behaviour.\n */\nwatch(\n () => props.modalState.open,\n (open) => {\n if (open) {\n query.value = ''\n selectedIndex.value = undefined\n }\n },\n)\n\n/** Keyboard navigation, wrapping around the ends of the result list. */\nconst navigateSearchResults = (direction: 'up' | 'down') => {\n const offset = direction === 'up' ? -1 : 1\n const length = results.value.length\n if (length === 0) {\n return\n }\n\n if (typeof selectedIndex.value === 'number') {\n selectedIndex.value = (selectedIndex.value + offset + length) % length\n return\n }\n\n selectedIndex.value = offset === -1 ? length - 1 : 0\n}\n\nfunction handleSelect(idx: number | undefined) {\n if (typeof idx !== 'number' || !results.value[idx]) {\n return\n }\n\n const result = results.value[idx]\n props.modalState.hide()\n emit('select', result.item.id)\n}\n\n/**\n * aria-activedescendant for the combobox. Each result item must render the\n * matching id for assistive tech to announce the selection correctly.\n */\nconst activeDescendantId = computed(() => {\n const selectedResult = results.value[selectedIndex.value ?? -1]\n return selectedResult ? `search-result-${selectedResult.item.id}` : undefined\n})\n</script>\n\n<template>\n <ScalarModal\n aria-label=\"Document Search\"\n :state=\"modalState\"\n variant=\"search\">\n <div\n class=\"mb-0 flex flex-col\"\n role=\"search\">\n <ScalarSearchInput\n v-model=\"query\"\n :aria-activedescendant=\"activeDescendantId\"\n aria-autocomplete=\"list\"\n :aria-controls=\"listboxId\"\n :aria-describedby=\"instructionsId\"\n role=\"combobox\"\n @blur=\"selectedIndex = undefined\"\n @keydown.down.stop.prevent=\"navigateSearchResults('down')\"\n @keydown.enter.stop.prevent=\"() => handleSelect(selectedIndex)\"\n @keydown.up.stop.prevent=\"navigateSearchResults('up')\" />\n </div>\n <ScalarSearchResultList\n :id=\"listboxId\"\n aria-label=\"Document Search Results\"\n class=\"custom-scroll px-1 pb-1\"\n :noResults=\"!results.length\">\n <template #query>\n {{ query }}\n </template>\n <SearchResult\n v-for=\"(result, idx) in results\"\n :id=\"`search-result-${result.item.id}`\"\n :key=\"result.refIndex\"\n :isSelected=\"selectedIndex === idx\"\n :result=\"result\"\n @click.prevent=\"() => handleSelect(idx)\" />\n </ScalarSearchResultList>\n <div\n :id=\"instructionsId\"\n class=\"ref-search-meta\">\n <span\n aria-hidden=\"true\"\n class=\"contents\">\n <span>↑↓ Navigate</span>\n <span>⏎ Select</span>\n </span>\n <span class=\"sr-only\">\n Press up arrow / down arrow to navigate, enter to select, type to filter\n results\n </span>\n </div>\n </ScalarModal>\n</template>\n\n<style scoped>\n.ref-search-meta {\n background: var(--scalar-background-1);\n border-bottom-left-radius: var(--scalar-radius-lg);\n border-bottom-right-radius: var(--scalar-radius-lg);\n padding: 6px 12px;\n font-size: var(--scalar-font-size-4);\n color: var(--scalar-color-3);\n font-weight: var(--scalar-semibold);\n display: flex;\n gap: 12px;\n border-top: var(--scalar-border-width) solid var(--scalar-border-color);\n}\n</style>\n"],"mappings":""}
@@ -0,0 +1,123 @@
1
+ import { useDocumentSearch } from "../hooks/use-document-search.js";
2
+ import SearchResult_default from "./SearchResult.vue.js";
3
+ import { Fragment, computed, createBlock, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, isRef, openBlock, ref, renderList, toDisplayString, unref, watch, withCtx, withKeys, withModifiers } from "vue";
4
+ import { ScalarModal, ScalarSearchInput, ScalarSearchResultList } from "@scalar/components";
5
+ import { nanoid } from "nanoid";
6
+ //#region src/v2/features/search/components/DocumentSearchModal.vue?vue&type=script&setup=true&lang.ts
7
+ var _hoisted_1 = {
8
+ class: "mb-0 flex flex-col",
9
+ role: "search"
10
+ };
11
+ var DocumentSearchModal_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({
12
+ __name: "DocumentSearchModal",
13
+ props: {
14
+ modalState: {},
15
+ document: {}
16
+ },
17
+ emits: ["select"],
18
+ setup(__props, { emit: __emit }) {
19
+ const props = __props;
20
+ const emit = __emit;
21
+ /** Base id for the search form. */
22
+ const id = nanoid();
23
+ const listboxId = `${id}-search-result`;
24
+ const instructionsId = `${id}-search-instructions`;
25
+ const { query, results } = useDocumentSearch(() => props.document);
26
+ const selectedIndex = ref(void 0);
27
+ /**
28
+ * Reset the query every time the modal is opened so users always start from a
29
+ * clean state. Matches the reference search modal behaviour.
30
+ */
31
+ watch(() => props.modalState.open, (open) => {
32
+ if (open) {
33
+ query.value = "";
34
+ selectedIndex.value = void 0;
35
+ }
36
+ });
37
+ /** Keyboard navigation, wrapping around the ends of the result list. */
38
+ const navigateSearchResults = (direction) => {
39
+ const offset = direction === "up" ? -1 : 1;
40
+ const length = results.value.length;
41
+ if (length === 0) return;
42
+ if (typeof selectedIndex.value === "number") {
43
+ selectedIndex.value = (selectedIndex.value + offset + length) % length;
44
+ return;
45
+ }
46
+ selectedIndex.value = offset === -1 ? length - 1 : 0;
47
+ };
48
+ function handleSelect(idx) {
49
+ if (typeof idx !== "number" || !results.value[idx]) return;
50
+ const result = results.value[idx];
51
+ props.modalState.hide();
52
+ emit("select", result.item.id);
53
+ }
54
+ /**
55
+ * aria-activedescendant for the combobox. Each result item must render the
56
+ * matching id for assistive tech to announce the selection correctly.
57
+ */
58
+ const activeDescendantId = computed(() => {
59
+ const selectedResult = results.value[selectedIndex.value ?? -1];
60
+ return selectedResult ? `search-result-${selectedResult.item.id}` : void 0;
61
+ });
62
+ return (_ctx, _cache) => {
63
+ return openBlock(), createBlock(unref(ScalarModal), {
64
+ "aria-label": "Document Search",
65
+ state: __props.modalState,
66
+ variant: "search"
67
+ }, {
68
+ default: withCtx(() => [
69
+ createElementVNode("div", _hoisted_1, [createVNode(unref(ScalarSearchInput), {
70
+ modelValue: unref(query),
71
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => isRef(query) ? query.value = $event : null),
72
+ "aria-activedescendant": activeDescendantId.value,
73
+ "aria-autocomplete": "list",
74
+ "aria-controls": listboxId,
75
+ "aria-describedby": instructionsId,
76
+ role: "combobox",
77
+ onBlur: _cache[1] || (_cache[1] = ($event) => selectedIndex.value = void 0),
78
+ onKeydown: [
79
+ _cache[2] || (_cache[2] = withKeys(withModifiers(($event) => navigateSearchResults("down"), ["stop", "prevent"]), ["down"])),
80
+ _cache[3] || (_cache[3] = withKeys(withModifiers(() => handleSelect(selectedIndex.value), ["stop", "prevent"]), ["enter"])),
81
+ _cache[4] || (_cache[4] = withKeys(withModifiers(($event) => navigateSearchResults("up"), ["stop", "prevent"]), ["up"]))
82
+ ]
83
+ }, null, 8, ["modelValue", "aria-activedescendant"])]),
84
+ createVNode(unref(ScalarSearchResultList), {
85
+ id: listboxId,
86
+ "aria-label": "Document Search Results",
87
+ class: "custom-scroll px-1 pb-1",
88
+ noResults: !unref(results).length
89
+ }, {
90
+ query: withCtx(() => [createTextVNode(toDisplayString(unref(query)), 1)]),
91
+ default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(results), (result, idx) => {
92
+ return openBlock(), createBlock(SearchResult_default, {
93
+ id: `search-result-${result.item.id}`,
94
+ key: result.refIndex,
95
+ isSelected: selectedIndex.value === idx,
96
+ result,
97
+ onClick: withModifiers(() => handleSelect(idx), ["prevent"])
98
+ }, null, 8, [
99
+ "id",
100
+ "isSelected",
101
+ "result",
102
+ "onClick"
103
+ ]);
104
+ }), 128))]),
105
+ _: 1
106
+ }, 8, ["noResults"]),
107
+ createElementVNode("div", {
108
+ id: instructionsId,
109
+ class: "ref-search-meta"
110
+ }, [..._cache[5] || (_cache[5] = [createElementVNode("span", {
111
+ "aria-hidden": "true",
112
+ class: "contents"
113
+ }, [createElementVNode("span", null, "↑↓ Navigate"), createElementVNode("span", null, "⏎ Select")], -1), createElementVNode("span", { class: "sr-only" }, " Press up arrow / down arrow to navigate, enter to select, type to filter results ", -1)])])
114
+ ]),
115
+ _: 1
116
+ }, 8, ["state"]);
117
+ };
118
+ }
119
+ });
120
+ //#endregion
121
+ export { DocumentSearchModal_vue_vue_type_script_setup_true_lang_default as default };
122
+
123
+ //# sourceMappingURL=DocumentSearchModal.vue.script.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DocumentSearchModal.vue.script.js","names":[],"sources":["../../../../../src/v2/features/search/components/DocumentSearchModal.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport {\n ScalarModal,\n ScalarSearchInput,\n ScalarSearchResultList,\n type ModalState,\n} from '@scalar/components'\nimport type { OpenApiDocument } from '@scalar/workspace-store/schemas/v3.1/strict/openapi-document'\nimport { nanoid } from 'nanoid'\nimport { computed, ref, watch } from 'vue'\n\nimport { useDocumentSearch } from '@/v2/features/search/hooks/use-document-search'\n\nimport SearchResult from './SearchResult.vue'\n\nconst props = defineProps<{\n /** Controls the visibility of the search modal. */\n modalState: ModalState\n /** The document whose entries should be searched. */\n document: OpenApiDocument | undefined\n}>()\n\nconst emit = defineEmits<{\n /** Emitted when the user picks a result, passing the navigation id. */\n (e: 'select', id: string): void\n}>()\n\n/** Base id for the search form. */\nconst id = nanoid()\nconst listboxId = `${id}-search-result`\nconst instructionsId = `${id}-search-instructions`\n\nconst { query, results } = useDocumentSearch(() => props.document)\n\nconst selectedIndex = ref<number | undefined>(undefined)\n\n/**\n * Reset the query every time the modal is opened so users always start from a\n * clean state. Matches the reference search modal behaviour.\n */\nwatch(\n () => props.modalState.open,\n (open) => {\n if (open) {\n query.value = ''\n selectedIndex.value = undefined\n }\n },\n)\n\n/** Keyboard navigation, wrapping around the ends of the result list. */\nconst navigateSearchResults = (direction: 'up' | 'down') => {\n const offset = direction === 'up' ? -1 : 1\n const length = results.value.length\n if (length === 0) {\n return\n }\n\n if (typeof selectedIndex.value === 'number') {\n selectedIndex.value = (selectedIndex.value + offset + length) % length\n return\n }\n\n selectedIndex.value = offset === -1 ? length - 1 : 0\n}\n\nfunction handleSelect(idx: number | undefined) {\n if (typeof idx !== 'number' || !results.value[idx]) {\n return\n }\n\n const result = results.value[idx]\n props.modalState.hide()\n emit('select', result.item.id)\n}\n\n/**\n * aria-activedescendant for the combobox. Each result item must render the\n * matching id for assistive tech to announce the selection correctly.\n */\nconst activeDescendantId = computed(() => {\n const selectedResult = results.value[selectedIndex.value ?? -1]\n return selectedResult ? `search-result-${selectedResult.item.id}` : undefined\n})\n</script>\n\n<template>\n <ScalarModal\n aria-label=\"Document Search\"\n :state=\"modalState\"\n variant=\"search\">\n <div\n class=\"mb-0 flex flex-col\"\n role=\"search\">\n <ScalarSearchInput\n v-model=\"query\"\n :aria-activedescendant=\"activeDescendantId\"\n aria-autocomplete=\"list\"\n :aria-controls=\"listboxId\"\n :aria-describedby=\"instructionsId\"\n role=\"combobox\"\n @blur=\"selectedIndex = undefined\"\n @keydown.down.stop.prevent=\"navigateSearchResults('down')\"\n @keydown.enter.stop.prevent=\"() => handleSelect(selectedIndex)\"\n @keydown.up.stop.prevent=\"navigateSearchResults('up')\" />\n </div>\n <ScalarSearchResultList\n :id=\"listboxId\"\n aria-label=\"Document Search Results\"\n class=\"custom-scroll px-1 pb-1\"\n :noResults=\"!results.length\">\n <template #query>\n {{ query }}\n </template>\n <SearchResult\n v-for=\"(result, idx) in results\"\n :id=\"`search-result-${result.item.id}`\"\n :key=\"result.refIndex\"\n :isSelected=\"selectedIndex === idx\"\n :result=\"result\"\n @click.prevent=\"() => handleSelect(idx)\" />\n </ScalarSearchResultList>\n <div\n :id=\"instructionsId\"\n class=\"ref-search-meta\">\n <span\n aria-hidden=\"true\"\n class=\"contents\">\n <span>↑↓ Navigate</span>\n <span>⏎ Select</span>\n </span>\n <span class=\"sr-only\">\n Press up arrow / down arrow to navigate, enter to select, type to filter\n results\n </span>\n </div>\n </ScalarModal>\n</template>\n\n<style scoped>\n.ref-search-meta {\n background: var(--scalar-background-1);\n border-bottom-left-radius: var(--scalar-radius-lg);\n border-bottom-right-radius: var(--scalar-radius-lg);\n padding: 6px 12px;\n font-size: var(--scalar-font-size-4);\n color: var(--scalar-color-3);\n font-weight: var(--scalar-semibold);\n display: flex;\n gap: 12px;\n border-top: var(--scalar-border-width) solid var(--scalar-border-color);\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;EAeA,MAAM,QAAQ;EAOd,MAAM,OAAO;;EAMb,MAAM,KAAK,QAAO;EAClB,MAAM,YAAY,GAAG,GAAG;EACxB,MAAM,iBAAiB,GAAG,GAAG;EAE7B,MAAM,EAAE,OAAO,YAAY,wBAAwB,MAAM,SAAQ;EAEjE,MAAM,gBAAgB,IAAwB,KAAA,EAAS;;;;;AAMvD,cACQ,MAAM,WAAW,OACtB,SAAS;AACR,OAAI,MAAM;AACR,UAAM,QAAQ;AACd,kBAAc,QAAQ,KAAA;;IAG5B;;EAGA,MAAM,yBAAyB,cAA6B;GAC1D,MAAM,SAAS,cAAc,OAAO,KAAK;GACzC,MAAM,SAAS,QAAQ,MAAM;AAC7B,OAAI,WAAW,EACb;AAGF,OAAI,OAAO,cAAc,UAAU,UAAU;AAC3C,kBAAc,SAAS,cAAc,QAAQ,SAAS,UAAU;AAChE;;AAGF,iBAAc,QAAQ,WAAW,KAAK,SAAS,IAAI;;EAGrD,SAAS,aAAa,KAAyB;AAC7C,OAAI,OAAO,QAAQ,YAAY,CAAC,QAAQ,MAAM,KAC5C;GAGF,MAAM,SAAS,QAAQ,MAAM;AAC7B,SAAM,WAAW,MAAK;AACtB,QAAK,UAAU,OAAO,KAAK,GAAE;;;;;;EAO/B,MAAM,qBAAqB,eAAe;GACxC,MAAM,iBAAiB,QAAQ,MAAM,cAAc,SAAS;AAC5D,UAAO,iBAAiB,iBAAiB,eAAe,KAAK,OAAO,KAAA;IACrE;;uBAIC,YAiDc,MAAA,YAAA,EAAA;IAhDZ,cAAW;IACV,OAAO,QAAA;IACR,SAAQ;;2BAeF;KAdN,mBAcM,OAdN,YAcM,CAXJ,YAU2D,MAAA,kBAAA,EAAA;kBAThD,MAAA,MAAK;wFAAA,QAAA,SAAA;MACb,yBAAuB,mBAAA;MACxB,qBAAkB;MACjB,iBAAe;MACf,oBAAkB;MACnB,MAAK;MACJ,QAAI,OAAA,OAAA,OAAA,MAAA,WAAE,cAAA,QAAgB,KAAA;MACtB,WAAO;oEAAoB,sBAAqB,OAAA,EAAA,CAAA,QAAA,UAAA,CAAA,EAAA,CAAA,OAAA,CAAA;8DACd,aAAa,cAAA,MAAa,EAAA,CAAA,QAAA,UAAA,CAAA,EAAA,CAAA,QAAA,CAAA;oEACnC,sBAAqB,KAAA,EAAA,CAAA,QAAA,UAAA,CAAA,EAAA,CAAA,KAAA,CAAA;;;KAEnD,YAeyB,MAAA,uBAAA,EAAA;MAdtB,IAAI;MACL,cAAW;MACX,OAAM;MACL,WAAS,CAAG,MAAA,QAAO,CAAC;;MACV,OAAK,cACH,CAAA,gBAAA,gBAAR,MAAA,MAAK,CAAA,EAAA,EAAA,CAAA,CAAA;6BAGwB,EAAA,UAAA,KAAA,EADlC,mBAM6C,UAAA,MAAA,WALnB,MAAA,QAAO,GAAvB,QAAQ,QAAG;2BADrB,YAM6C,sBAAA;QAJ1C,IAAE,iBAAmB,OAAO,KAAK;QACjC,KAAK,OAAO;QACZ,YAAY,cAAA,UAAkB;QACtB;QACR,SAAK,oBAAgB,aAAa,IAAG,EAAA,CAAA,UAAA,CAAA;;;;;;;;;;KAE1C,mBAaM,OAAA;MAZH,IAAI;MACL,OAAM;uCACN,mBAKO,QAAA;MAJL,eAAY;MACZ,OAAM;SACN,mBAAwB,QAAA,MAAlB,cAAW,EACjB,mBAAqB,QAAA,MAAf,WAAQ,CAAA,EAAA,GAAA,EAEhB,mBAGO,QAAA,EAHD,OAAM,WAAS,EAAC,sFAGtB,GAAA,CAAA,EAAA,CAAA"}
@@ -0,0 +1,11 @@
1
+ import type { FuseResult } from 'fuse.js';
2
+ import type { FuseData } from '../../../../v2/features/search/types';
3
+ type __VLS_Props = {
4
+ id: string;
5
+ isSelected: boolean;
6
+ result: FuseResult<FuseData>;
7
+ };
8
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
9
+ declare const _default: typeof __VLS_export;
10
+ export default _default;
11
+ //# sourceMappingURL=SearchResult.vue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SearchResult.vue.d.ts","sourceRoot":"","sources":["../../../../../src/v2/features/search/components/SearchResult.vue"],"names":[],"mappings":"AAoFA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAEzC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAE1D,KAAK,WAAW,GAAG;IACjB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,OAAO,CAAA;IACnB,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAA;CAC7B,CAAC;AAyHF,QAAA,MAAM,YAAY,kSAEhB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
@@ -0,0 +1,7 @@
1
+ import SearchResult_vue_vue_type_script_setup_true_lang_default from "./SearchResult.vue.script.js";
2
+ //#region src/v2/features/search/components/SearchResult.vue
3
+ var SearchResult_default = SearchResult_vue_vue_type_script_setup_true_lang_default;
4
+ //#endregion
5
+ export { SearchResult_default as default };
6
+
7
+ //# sourceMappingURL=SearchResult.vue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SearchResult.vue.js","names":[],"sources":["../../../../../src/v2/features/search/components/SearchResult.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ScalarSearchResultItem } from '@scalar/components'\nimport {\n ScalarIconTag,\n ScalarIconTerminalWindow,\n ScalarIconTextAlignLeft,\n} from '@scalar/icons'\nimport type { ScalarIconComponent } from '@scalar/icons/types'\nimport { HttpMethod } from '@scalar/sidebar'\nimport type { FuseResult } from 'fuse.js'\n\nimport type { FuseData } from '@/v2/features/search/types'\n\ndefineProps<{\n id: string\n isSelected: boolean\n result: FuseResult<FuseData>\n}>()\n\n/**\n * Icon used for each search result type. Operations use the terminal glyph to\n * match the sidebar's operation indicator, tags and headings use their closest\n * semantic equivalents.\n */\nconst ENTRY_ICONS: { [x in FuseData['type']]: ScalarIconComponent } = {\n heading: ScalarIconTextAlignLeft,\n operation: ScalarIconTerminalWindow,\n tag: ScalarIconTag,\n}\n\nconst ENTRY_LABELS: { [x in FuseData['type']]: string } = {\n heading: 'Heading',\n operation: 'Operation',\n tag: 'Tag',\n}\n</script>\n\n<template>\n <ScalarSearchResultItem\n :id=\"id\"\n :icon=\"ENTRY_ICONS[result.item.type]\"\n :selected=\"isSelected\">\n <span>\n <span class=\"sr-only\">{{ ENTRY_LABELS[result.item.type] }}:&nbsp;</span>\n {{ result.item.title }}\n <span class=\"sr-only\">,</span>\n </span>\n <template\n v-if=\"\n result.item.type === 'operation' &&\n (result.item.method || result.item.path) &&\n result.item.path !== result.item.title\n \"\n #description>\n <span class=\"inline-flex items-center gap-1\">\n <HttpMethod\n aria-hidden=\"true\"\n :method=\"result.item.method ?? 'get'\" />\n <span class=\"sr-only\">\n HTTP Method: {{ result.item.method ?? 'get' }}\n </span>\n <span class=\"sr-only\">Path:&nbsp;</span>\n {{ result.item.path }}\n </span>\n </template>\n <template\n v-else-if=\"result.item.description\"\n #description>\n <span class=\"sr-only\">Description:&nbsp;</span>\n {{ result.item.description }}\n </template>\n </ScalarSearchResultItem>\n</template>\n"],"mappings":""}
@@ -0,0 +1,71 @@
1
+ import { createBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, openBlock, toDisplayString, unref, withCtx } from "vue";
2
+ import { ScalarSearchResultItem } from "@scalar/components";
3
+ import { ScalarIconTag, ScalarIconTerminalWindow, ScalarIconTextAlignLeft } from "@scalar/icons";
4
+ import { HttpMethod } from "@scalar/sidebar";
5
+ //#region src/v2/features/search/components/SearchResult.vue?vue&type=script&setup=true&lang.ts
6
+ var _hoisted_1 = { class: "sr-only" };
7
+ var _hoisted_2 = { class: "inline-flex items-center gap-1" };
8
+ var _hoisted_3 = { class: "sr-only" };
9
+ var SearchResult_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({
10
+ __name: "SearchResult",
11
+ props: {
12
+ id: {},
13
+ isSelected: { type: Boolean },
14
+ result: {}
15
+ },
16
+ setup(__props) {
17
+ /**
18
+ * Icon used for each search result type. Operations use the terminal glyph to
19
+ * match the sidebar's operation indicator, tags and headings use their closest
20
+ * semantic equivalents.
21
+ */
22
+ const ENTRY_ICONS = {
23
+ heading: ScalarIconTextAlignLeft,
24
+ operation: ScalarIconTerminalWindow,
25
+ tag: ScalarIconTag
26
+ };
27
+ const ENTRY_LABELS = {
28
+ heading: "Heading",
29
+ operation: "Operation",
30
+ tag: "Tag"
31
+ };
32
+ return (_ctx, _cache) => {
33
+ return openBlock(), createBlock(unref(ScalarSearchResultItem), {
34
+ id: __props.id,
35
+ icon: ENTRY_ICONS[__props.result.item.type],
36
+ selected: __props.isSelected
37
+ }, createSlots({
38
+ default: withCtx(() => [createElementVNode("span", null, [
39
+ createElementVNode("span", _hoisted_1, toDisplayString(ENTRY_LABELS[__props.result.item.type]) + ":\xA0", 1),
40
+ createTextVNode(" " + toDisplayString(__props.result.item.title) + " ", 1),
41
+ _cache[0] || (_cache[0] = createElementVNode("span", { class: "sr-only" }, ",", -1))
42
+ ])]),
43
+ _: 2
44
+ }, [__props.result.item.type === "operation" && (__props.result.item.method || __props.result.item.path) && __props.result.item.path !== __props.result.item.title ? {
45
+ name: "description",
46
+ fn: withCtx(() => [createElementVNode("span", _hoisted_2, [
47
+ createVNode(unref(HttpMethod), {
48
+ "aria-hidden": "true",
49
+ method: __props.result.item.method ?? "get"
50
+ }, null, 8, ["method"]),
51
+ createElementVNode("span", _hoisted_3, " HTTP Method: " + toDisplayString(__props.result.item.method ?? "get"), 1),
52
+ _cache[1] || (_cache[1] = createElementVNode("span", { class: "sr-only" }, "Path:\xA0", -1)),
53
+ createTextVNode(" " + toDisplayString(__props.result.item.path), 1)
54
+ ])]),
55
+ key: "0"
56
+ } : __props.result.item.description ? {
57
+ name: "description",
58
+ fn: withCtx(() => [_cache[2] || (_cache[2] = createElementVNode("span", { class: "sr-only" }, "Description:\xA0", -1)), createTextVNode(" " + toDisplayString(__props.result.item.description), 1)]),
59
+ key: "1"
60
+ } : void 0]), 1032, [
61
+ "id",
62
+ "icon",
63
+ "selected"
64
+ ]);
65
+ };
66
+ }
67
+ });
68
+ //#endregion
69
+ export { SearchResult_vue_vue_type_script_setup_true_lang_default as default };
70
+
71
+ //# sourceMappingURL=SearchResult.vue.script.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SearchResult.vue.script.js","names":[],"sources":["../../../../../src/v2/features/search/components/SearchResult.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ScalarSearchResultItem } from '@scalar/components'\nimport {\n ScalarIconTag,\n ScalarIconTerminalWindow,\n ScalarIconTextAlignLeft,\n} from '@scalar/icons'\nimport type { ScalarIconComponent } from '@scalar/icons/types'\nimport { HttpMethod } from '@scalar/sidebar'\nimport type { FuseResult } from 'fuse.js'\n\nimport type { FuseData } from '@/v2/features/search/types'\n\ndefineProps<{\n id: string\n isSelected: boolean\n result: FuseResult<FuseData>\n}>()\n\n/**\n * Icon used for each search result type. Operations use the terminal glyph to\n * match the sidebar's operation indicator, tags and headings use their closest\n * semantic equivalents.\n */\nconst ENTRY_ICONS: { [x in FuseData['type']]: ScalarIconComponent } = {\n heading: ScalarIconTextAlignLeft,\n operation: ScalarIconTerminalWindow,\n tag: ScalarIconTag,\n}\n\nconst ENTRY_LABELS: { [x in FuseData['type']]: string } = {\n heading: 'Heading',\n operation: 'Operation',\n tag: 'Tag',\n}\n</script>\n\n<template>\n <ScalarSearchResultItem\n :id=\"id\"\n :icon=\"ENTRY_ICONS[result.item.type]\"\n :selected=\"isSelected\">\n <span>\n <span class=\"sr-only\">{{ ENTRY_LABELS[result.item.type] }}:&nbsp;</span>\n {{ result.item.title }}\n <span class=\"sr-only\">,</span>\n </span>\n <template\n v-if=\"\n result.item.type === 'operation' &&\n (result.item.method || result.item.path) &&\n result.item.path !== result.item.title\n \"\n #description>\n <span class=\"inline-flex items-center gap-1\">\n <HttpMethod\n aria-hidden=\"true\"\n :method=\"result.item.method ?? 'get'\" />\n <span class=\"sr-only\">\n HTTP Method: {{ result.item.method ?? 'get' }}\n </span>\n <span class=\"sr-only\">Path:&nbsp;</span>\n {{ result.item.path }}\n </span>\n </template>\n <template\n v-else-if=\"result.item.description\"\n #description>\n <span class=\"sr-only\">Description:&nbsp;</span>\n {{ result.item.description }}\n </template>\n </ScalarSearchResultItem>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;EAwBA,MAAM,cAAgE;GACpE,SAAS;GACT,WAAW;GACX,KAAK;GACP;EAEA,MAAM,eAAoD;GACxD,SAAS;GACT,WAAW;GACX,KAAK;GACP;;uBAIE,YAiCyB,MAAA,uBAAA,EAAA;IAhCtB,IAAI,QAAA;IACJ,MAAM,YAAY,QAAA,OAAO,KAAK;IAC9B,UAAU,QAAA;;2BAKJ,CAJP,mBAIO,QAAA,MAAA;KAHL,mBAAwE,QAAxE,YAAwE,gBAA/C,aAAa,QAAA,OAAO,KAAK,MAAI,GAAI,SAAO,EAAA;qBAAO,MACxE,gBAAG,QAAA,OAAO,KAAK,MAAK,GAAG,KACvB,EAAA;+BAAA,mBAA8B,QAAA,EAAxB,OAAM,WAAS,EAAC,KAAC,GAAA;;;OAGR,QAAA,OAAO,KAAK,SAAI,gBAA6B,QAAA,OAAO,KAAK,UAAU,QAAA,OAAO,KAAK,SAAiB,QAAA,OAAO,KAAK,SAAS,QAAA,OAAO,KAAK,QAAA;UAK/I;sBAUM,CATP,mBASO,QATP,YASO;KARL,YAE0C,MAAA,WAAA,EAAA;MADxC,eAAY;MACX,QAAQ,QAAA,OAAO,KAAK,UAAM;;KAC7B,mBAEO,QAFP,YAAsB,mBACP,gBAAG,QAAA,OAAO,KAAK,UAAM,MAAA,EAAA,EAAA;+BAEpC,mBAAwC,QAAA,EAAlC,OAAM,WAAS,EAAC,aAAW,GAAA;qBAAO,MACxC,gBAAG,QAAA,OAAO,KAAK,KAAI,EAAA,EAAA;;;OAIV,QAAA,OAAO,KAAK,cAAA;UACtB;sBAC8C,CAAA,OAAA,OAAA,OAAA,KAA/C,mBAA+C,QAAA,EAAzC,OAAM,WAAS,EAAC,oBAAkB,GAAA,GAAA,gBAAO,MAC/C,gBAAG,QAAA,OAAO,KAAK,YAAW,EAAA,EAAA,CAAA,CAAA"}
@@ -0,0 +1,19 @@
1
+ import type { OpenApiDocument } from '@scalar/workspace-store/schemas/v3.1/strict/openapi-document';
2
+ import type { FuseResult } from 'fuse.js';
3
+ import { type MaybeRefOrGetter } from 'vue';
4
+ import type { FuseData } from '../types';
5
+ /**
6
+ * Fuzzy search scoped to a single OpenAPI document.
7
+ *
8
+ * Mirrors the behaviour of the reference search modal (`@scalar/api-reference`)
9
+ * but stays local to api-client so the two packages do not have a circular
10
+ * dependency. The index is rebuilt whenever the source document changes.
11
+ *
12
+ * When the query is empty we surface the first `MAX_SEARCH_RESULTS` entries of
13
+ * the index as a zero-state list, matching the reference UX.
14
+ */
15
+ export declare function useDocumentSearch(document: MaybeRefOrGetter<OpenApiDocument | undefined>): {
16
+ results: import("vue").ComputedRef<FuseResult<FuseData>[]>;
17
+ query: import("vue").Ref<string, string>;
18
+ };
19
+ //# sourceMappingURL=use-document-search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-document-search.d.ts","sourceRoot":"","sources":["../../../../../src/v2/features/search/hooks/use-document-search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8DAA8D,CAAA;AACnG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACzC,OAAO,EAAE,KAAK,gBAAgB,EAA0B,MAAM,KAAK,CAAA;AAInE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAIxC;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,gBAAgB,CAAC,eAAe,GAAG,SAAS,CAAC;;;EAiCxF"}