@scalar/api-client 3.5.1 → 3.6.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 (113) hide show
  1. package/CHANGELOG.md +13 -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 +3916 -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/components/data-table/DataTableInput.vue.d.ts +1 -1
  14. package/dist/v2/components/data-table/DataTableInput.vue.d.ts.map +1 -1
  15. package/dist/v2/constants.js +1 -1
  16. package/dist/v2/features/app/App.vue.d.ts +15 -31
  17. package/dist/v2/features/app/App.vue.d.ts.map +1 -1
  18. package/dist/v2/features/app/App.vue.js.map +1 -1
  19. package/dist/v2/features/app/App.vue.script.js +107 -28
  20. package/dist/v2/features/app/App.vue.script.js.map +1 -1
  21. package/dist/v2/features/app/app-state.d.ts +10 -14
  22. package/dist/v2/features/app/app-state.d.ts.map +1 -1
  23. package/dist/v2/features/app/app-state.js +53 -21
  24. package/dist/v2/features/app/app-state.js.map +1 -1
  25. package/dist/v2/features/app/components/AppHeader.vue.d.ts.map +1 -1
  26. package/dist/v2/features/app/components/AppHeader.vue.js.map +1 -1
  27. package/dist/v2/features/app/components/AppHeader.vue.script.js +1 -1
  28. package/dist/v2/features/app/components/AppHeader.vue.script.js.map +1 -1
  29. package/dist/v2/features/app/components/AppHeaderActions.vue.d.ts +32 -0
  30. package/dist/v2/features/app/components/AppHeaderActions.vue.d.ts.map +1 -0
  31. package/dist/v2/features/app/components/AppHeaderActions.vue.js +7 -0
  32. package/dist/v2/features/app/components/AppHeaderActions.vue.js.map +1 -0
  33. package/dist/v2/features/app/components/AppHeaderActions.vue.script.js +170 -0
  34. package/dist/v2/features/app/components/AppHeaderActions.vue.script.js.map +1 -0
  35. package/dist/v2/features/app/components/AppSidebar.vue.d.ts +2 -3
  36. package/dist/v2/features/app/components/AppSidebar.vue.d.ts.map +1 -1
  37. package/dist/v2/features/app/components/AppSidebar.vue.js +1 -1
  38. package/dist/v2/features/app/components/AppSidebar.vue.js.map +1 -1
  39. package/dist/v2/features/app/components/AppSidebar.vue.script.js +1 -2
  40. package/dist/v2/features/app/components/AppSidebar.vue.script.js.map +1 -1
  41. package/dist/v2/features/app/components/DocumentBreadcrumb.vue.d.ts +1 -2
  42. package/dist/v2/features/app/components/DocumentBreadcrumb.vue.d.ts.map +1 -1
  43. package/dist/v2/features/app/components/DocumentBreadcrumb.vue.js +1 -1
  44. package/dist/v2/features/app/components/DocumentBreadcrumb.vue.js.map +1 -1
  45. package/dist/v2/features/app/components/DocumentBreadcrumb.vue.script.js +3 -34
  46. package/dist/v2/features/app/components/DocumentBreadcrumb.vue.script.js.map +1 -1
  47. package/dist/v2/features/app/components/DocumentSyncIndicator.vue.d.ts +1 -1
  48. package/dist/v2/features/app/components/DocumentSyncIndicator.vue.d.ts.map +1 -1
  49. package/dist/v2/features/app/components/PublishDocumentModal.vue.d.ts +77 -0
  50. package/dist/v2/features/app/components/PublishDocumentModal.vue.d.ts.map +1 -0
  51. package/dist/v2/features/app/components/PublishDocumentModal.vue.js +7 -0
  52. package/dist/v2/features/app/components/PublishDocumentModal.vue.js.map +1 -0
  53. package/dist/v2/features/app/components/PublishDocumentModal.vue.script.js +209 -0
  54. package/dist/v2/features/app/components/PublishDocumentModal.vue.script.js.map +1 -0
  55. package/dist/v2/features/app/components/SyncConflictResolutionEditor.vue.d.ts.map +1 -0
  56. package/dist/v2/features/{collection → app}/components/SyncConflictResolutionEditor.vue.js +2 -2
  57. package/dist/v2/features/app/components/SyncConflictResolutionEditor.vue.js.map +1 -0
  58. package/dist/v2/features/{collection → app}/components/SyncConflictResolutionEditor.vue.script.js +1 -1
  59. package/dist/v2/features/{collection/components/SyncConflictResolutionEditor.vue.js.map → app/components/SyncConflictResolutionEditor.vue.script.js.map} +1 -1
  60. package/dist/v2/features/app/helpers/check-version-conflict.d.ts +8 -5
  61. package/dist/v2/features/app/helpers/check-version-conflict.d.ts.map +1 -1
  62. package/dist/v2/features/app/helpers/check-version-conflict.js +10 -7
  63. package/dist/v2/features/app/helpers/check-version-conflict.js.map +1 -1
  64. package/dist/v2/features/app/helpers/create-api-client-app.d.ts +8 -5
  65. package/dist/v2/features/app/helpers/create-api-client-app.d.ts.map +1 -1
  66. package/dist/v2/features/app/helpers/create-api-client-app.js +2 -2
  67. package/dist/v2/features/app/helpers/create-api-client-app.js.map +1 -1
  68. package/dist/v2/features/app/helpers/load-registry-document.d.ts +1 -10
  69. package/dist/v2/features/app/helpers/load-registry-document.d.ts.map +1 -1
  70. package/dist/v2/features/app/helpers/load-registry-document.js +6 -5
  71. package/dist/v2/features/app/helpers/load-registry-document.js.map +1 -1
  72. package/dist/v2/features/app/helpers/registry-error-messages.d.ts +23 -0
  73. package/dist/v2/features/app/helpers/registry-error-messages.d.ts.map +1 -0
  74. package/dist/v2/features/app/helpers/registry-error-messages.js +63 -0
  75. package/dist/v2/features/app/helpers/registry-error-messages.js.map +1 -0
  76. package/dist/v2/features/app/hooks/use-active-document-version.d.ts +2 -1
  77. package/dist/v2/features/app/hooks/use-active-document-version.d.ts.map +1 -1
  78. package/dist/v2/features/app/hooks/use-active-document-version.js.map +1 -1
  79. package/dist/v2/features/app/hooks/use-document-sync.d.ts +126 -0
  80. package/dist/v2/features/app/hooks/use-document-sync.d.ts.map +1 -0
  81. package/dist/v2/features/app/hooks/use-document-sync.js +448 -0
  82. package/dist/v2/features/app/hooks/use-document-sync.js.map +1 -0
  83. package/dist/v2/features/app/hooks/use-network-status.d.ts +29 -0
  84. package/dist/v2/features/app/hooks/use-network-status.d.ts.map +1 -0
  85. package/dist/v2/features/app/hooks/use-network-status.js +58 -0
  86. package/dist/v2/features/app/hooks/use-network-status.js.map +1 -0
  87. package/dist/v2/features/app/hooks/use-sidebar-documents.d.ts +1 -25
  88. package/dist/v2/features/app/hooks/use-sidebar-documents.d.ts.map +1 -1
  89. package/dist/v2/features/app/hooks/use-sidebar-documents.js.map +1 -1
  90. package/dist/v2/features/app/index.d.ts +1 -1
  91. package/dist/v2/features/app/index.d.ts.map +1 -1
  92. package/dist/v2/features/collection/DocumentCollection.vue.d.ts.map +1 -1
  93. package/dist/v2/features/collection/DocumentCollection.vue.js.map +1 -1
  94. package/dist/v2/features/collection/DocumentCollection.vue.script.js +43 -277
  95. package/dist/v2/features/collection/DocumentCollection.vue.script.js.map +1 -1
  96. package/dist/v2/features/command-palette/components/CommandPaletteOpenApiDocument.vue.d.ts.map +1 -1
  97. package/dist/v2/features/command-palette/components/CommandPaletteOpenApiDocument.vue.js.map +1 -1
  98. package/dist/v2/features/command-palette/components/CommandPaletteOpenApiDocument.vue.script.js +25 -9
  99. package/dist/v2/features/command-palette/components/CommandPaletteOpenApiDocument.vue.script.js.map +1 -1
  100. package/dist/v2/features/editor/hooks/use-three-way-merge-editor.d.ts.map +1 -1
  101. package/dist/v2/features/editor/hooks/use-three-way-merge-editor.js +5 -5
  102. package/dist/v2/features/editor/hooks/use-three-way-merge-editor.js.map +1 -1
  103. package/dist/v2/types/configuration.d.ts +273 -7
  104. package/dist/v2/types/configuration.d.ts.map +1 -1
  105. package/dist/vue-styles.css +1389 -0
  106. package/package.json +21 -15
  107. package/dist/v2/features/app/components/DocumentSyncIndicator.vue.js +0 -7
  108. package/dist/v2/features/app/components/DocumentSyncIndicator.vue.js.map +0 -1
  109. package/dist/v2/features/app/components/DocumentSyncIndicator.vue.script.js +0 -51
  110. package/dist/v2/features/app/components/DocumentSyncIndicator.vue.script.js.map +0 -1
  111. package/dist/v2/features/collection/components/SyncConflictResolutionEditor.vue.d.ts.map +0 -1
  112. package/dist/v2/features/collection/components/SyncConflictResolutionEditor.vue.script.js.map +0 -1
  113. /package/dist/v2/features/{collection → app}/components/SyncConflictResolutionEditor.vue.d.ts +0 -0
@@ -2,6 +2,7 @@ import type { AppState } from '@scalar/api-client/v2/features/app';
2
2
  import type { TraversedDocument } from '@scalar/workspace-store/schemas/navigation';
3
3
  import { type MaybeRefOrGetter } from 'vue';
4
4
  import { type VersionStatus } from '../../../../v2/features/app/helpers/compute-version-status.js';
5
+ import type { RegistryDocument } from '../../../../v2/types/configuration';
5
6
  export type { VersionStatus };
6
7
  /**
7
8
  * A single version of a registry-backed document.
@@ -93,31 +94,6 @@ export type SidebarDocumentItem = {
93
94
  /** Key of the version currently surfaced at the parent level (matches a `versions[].key`). */
94
95
  activeVersionKey?: string;
95
96
  };
96
- type RegistryDocumentVersion = {
97
- version: string;
98
- commitHash?: string;
99
- };
100
- export type RegistryDocument = {
101
- namespace: string;
102
- slug: string;
103
- title: string;
104
- versions: RegistryDocumentVersion[];
105
- };
106
- /**
107
- * Loading-aware wrapper for the registry documents prop.
108
- *
109
- * The sidebar uses the `status` to decide whether to render skeleton
110
- * placeholders while the registry is being fetched. `documents` is optional
111
- * during loading so callers can either render nothing or stream in cached
112
- * results while a refresh is still in flight.
113
- */
114
- export type RegistryDocumentsState = {
115
- status: 'loading';
116
- documents?: RegistryDocument[];
117
- } | {
118
- status: 'success';
119
- documents: RegistryDocument[];
120
- };
121
97
  /**
122
98
  * Builds a unified list of sidebar documents.
123
99
  *
@@ -1 +1 @@
1
- {"version":3,"file":"use-sidebar-documents.d.ts","sourceRoot":"","sources":["../../../../../src/v2/features/app/hooks/use-sidebar-documents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAA;AAClE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AACnF,OAAO,EAAE,KAAK,gBAAgB,EAAqB,MAAM,KAAK,CAAA;AAE9D,OAAO,EAAE,KAAK,aAAa,EAAwB,MAAM,kDAAkD,CAAA;AAE3G,YAAY,EAAE,aAAa,EAAE,CAAA;AAE7B;;;;;;;GAOG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,qGAAqG;IACrG,GAAG,EAAE,MAAM,CAAA;IACX,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAA;IACf,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAA;IACb,yEAAyE;IACzE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B;;;;;OAKG;IACH,MAAM,EAAE,aAAa,CAAA;IACrB;;;;;OAKG;IACH,QAAQ,EAAE,OAAO,CAAA;IACjB,2GAA2G;IAC3G,UAAU,CAAC,EAAE,iBAAiB,CAAA;CAC/B,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,iHAAiH;IACjH,GAAG,EAAE,MAAM,CAAA;IACX;;;;;OAKG;IACH,KAAK,EAAE,MAAM,CAAA;IACb,+GAA+G;IAC/G,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,sGAAsG;IACtG,QAAQ,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IAC9C,0FAA0F;IAC1F,UAAU,CAAC,EAAE,iBAAiB,CAAA;IAC9B,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,sBAAsB,EAAE,CAAA;IACnC,8FAA8F;IAC9F,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,CAAA;AAsBD,KAAK,uBAAuB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,uBAAuB,EAAE,CAAA;CACpC,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,sBAAsB,GAC9B;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,SAAS,CAAC,EAAE,gBAAgB,EAAE,CAAA;CAAE,GACrD;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,gBAAgB,EAAE,CAAA;CAAE,CAAA;AAMxD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,GAAG,EACH,WAAW,GACZ,EAAE;IACD,GAAG,EAAE,QAAQ,CAAA;IACb,WAAW,EAAE,gBAAgB,CAAC,gBAAgB,EAAE,CAAC,CAAA;CAClD;;;;EA+HA"}
1
+ {"version":3,"file":"use-sidebar-documents.d.ts","sourceRoot":"","sources":["../../../../../src/v2/features/app/hooks/use-sidebar-documents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAA;AAClE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AACnF,OAAO,EAAE,KAAK,gBAAgB,EAAqB,MAAM,KAAK,CAAA;AAE9D,OAAO,EAAE,KAAK,aAAa,EAAwB,MAAM,kDAAkD,CAAA;AAC3G,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAEhE,YAAY,EAAE,aAAa,EAAE,CAAA;AAE7B;;;;;;;GAOG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,qGAAqG;IACrG,GAAG,EAAE,MAAM,CAAA;IACX,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAA;IACf,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAA;IACb,yEAAyE;IACzE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B;;;;;OAKG;IACH,MAAM,EAAE,aAAa,CAAA;IACrB;;;;;OAKG;IACH,QAAQ,EAAE,OAAO,CAAA;IACjB,2GAA2G;IAC3G,UAAU,CAAC,EAAE,iBAAiB,CAAA;CAC/B,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,iHAAiH;IACjH,GAAG,EAAE,MAAM,CAAA;IACX;;;;;OAKG;IACH,KAAK,EAAE,MAAM,CAAA;IACb,+GAA+G;IAC/G,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,sGAAsG;IACtG,QAAQ,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IAC9C,0FAA0F;IAC1F,UAAU,CAAC,EAAE,iBAAiB,CAAA;IAC9B,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,sBAAsB,EAAE,CAAA;IACnC,8FAA8F;IAC9F,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,CAAA;AA0BD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,GAAG,EACH,WAAW,GACZ,EAAE;IACD,GAAG,EAAE,QAAQ,CAAA;IACb,WAAW,EAAE,gBAAgB,CAAC,gBAAgB,EAAE,CAAC,CAAA;CAClD;;;;EA+HA"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-sidebar-documents.js","names":[],"sources":["../../../../../src/v2/features/app/hooks/use-sidebar-documents.ts"],"sourcesContent":["import type { AppState } from '@scalar/api-client/v2/features/app'\nimport type { TraversedDocument } from '@scalar/workspace-store/schemas/navigation'\nimport { type MaybeRefOrGetter, computed, toValue } from 'vue'\n\nimport { type VersionStatus, computeVersionStatus } from '@/v2/features/app/helpers/compute-version-status'\n\nexport type { VersionStatus }\n\n/**\n * A single version of a registry-backed document.\n *\n * On team workspaces, a registry document can advertise multiple versions and\n * each one may or may not have been imported into the local workspace store.\n * Loaded versions surface their `documentName` and traversal `navigation`,\n * unloaded versions are placeholders the sidebar can fetch on demand.\n */\nexport type SidebarDocumentVersion = {\n /** Stable key: workspace `documentName` when loaded, otherwise `${namespace}/${slug}@${version}`. */\n key: string\n /** Version identifier as advertised by the registry. */\n version: string\n /** User-facing label for the version row. */\n title: string\n /** Workspace store document name when this version is loaded locally. */\n documentName?: string\n /**\n * Commit hash recorded on the locally loaded workspace document, if any.\n * Undefined when the version has not been imported into the workspace\n * store or when the loaded document does not carry a hash.\n */\n commitHash?: string\n /**\n * Commit hash advertised by the registry for this version, if any.\n * Compared against `commitHash` to derive `status`.\n */\n registryCommitHash?: string\n /**\n * Sync status surfaced for the version row. Derived from the local /\n * registry commit hashes, the document's dirty flag and the cached\n * conflict-check result on `x-scalar-registry-meta`. `unknown` is used\n * for versions that are not loaded into the workspace store yet.\n */\n status: VersionStatus\n /**\n * True when this row is the canonical \"latest\" version of the group: the\n * first version the registry advertises. Drafts (locally-created versions\n * the registry has not seen yet) never get this flag, even when they are\n * surfaced ahead of the registry-advertised rows in the picker.\n */\n isLatest: boolean\n /** Traversal tree for this version. Populated only when the version is loaded into the workspace store. */\n navigation?: TraversedDocument\n}\n\n/**\n * A unified item for the top-level of our sidebar.\n *\n * It can represent one of three things:\n * - A workspace document that only exists locally (no registry link)\n * - A workspace document that was imported from the registry\n * - A registry document that has not yet been imported into the store\n *\n * For registry-backed entries, the parent fields (`navigation`, `documentName`)\n * mirror the active version so the sidebar template can render a single tree\n * without having to look up the active version itself. The full version list\n * lives in `versions`.\n */\nexport type SidebarDocumentItem = {\n /** Stable key used for sidebar state: `@namespace/slug` for registry-backed entries, document name otherwise. */\n key: string\n /**\n * User-facing title of the document. Registry-backed entries always surface\n * the registry's title so the sidebar matches what the registry advertises,\n * even when a locally loaded copy has been renamed. Standalone entries use\n * the workspace document title.\n */\n title: string\n /** Name of the document inside the workspace store (mirrors the active version on registry-backed entries). */\n documentName?: string\n /** Registry coordinates for the document group (without the version, which lives on `versions[]`). */\n registry?: { namespace: string; slug: string }\n /** Traversal tree of the active version (or the lone document for standalone entries). */\n navigation?: TraversedDocument\n /** Whether the document is pinned (todo: derived from `x-scalar-pinned`) */\n isPinned?: boolean\n /**\n * All known versions of the document, ordered with the registry's ordering\n * preserved (latest first by convention). Loaded versions surface their\n * workspace `documentName` and `navigation`, unloaded ones act as\n * placeholders the sidebar can fetch on demand. Undefined for standalone\n * entries that have no registry coordinates.\n */\n versions?: SidebarDocumentVersion[]\n /** Key of the version currently surfaced at the parent level (matches a `versions[].key`). */\n activeVersionKey?: string\n}\n\n/** Internal projection of a workspace document used during grouping. */\ntype WorkspaceDocumentEntry = {\n documentName: string\n title: string\n navigation?: TraversedDocument\n isPinned?: boolean\n /** Whether the workspace document has uncommitted local edits. */\n isDirty?: boolean\n registry?: {\n namespace: string\n slug: string\n version?: string\n commitHash?: string\n /** Last registry hash the conflict cache was computed against. */\n conflictCheckedAgainstHash?: string\n /** Cached conflict-check outcome for `conflictCheckedAgainstHash`. */\n hasConflict?: boolean\n }\n}\n\ntype RegistryDocumentVersion = {\n version: string\n commitHash?: string\n}\n\nexport type RegistryDocument = {\n namespace: string\n slug: string\n title: string\n versions: RegistryDocumentVersion[]\n}\n\n/**\n * Loading-aware wrapper for the registry documents prop.\n *\n * The sidebar uses the `status` to decide whether to render skeleton\n * placeholders while the registry is being fetched. `documents` is optional\n * during loading so callers can either render nothing or stream in cached\n * results while a refresh is still in flight.\n */\nexport type RegistryDocumentsState =\n | { status: 'loading'; documents?: RegistryDocument[] }\n | { status: 'success'; documents: RegistryDocument[] }\n\nconst registryKey = (namespace: string, slug: string) => `@${namespace}/${slug}`\n\nconst versionKey = (namespace: string, slug: string, version: string) => `${registryKey(namespace, slug)}@${version}`\n\n/**\n * Builds a unified list of sidebar documents.\n *\n * Behavior:\n * - Local workspaces (`teamUid === 'local'`) only show workspace documents.\n * Registry documents are not considered at all.\n * - Team workspaces group workspace documents by `namespace + slug` from\n * `x-scalar-registry-meta`. Each unique registry coordinate produces a\n * single sidebar entry whose versions are exposed as `versions`. The\n * `versions` array merges the registry-advertised versions with any\n * loaded workspace counterparts (matched by `version`). Registry\n * documents that have no loaded match are still surfaced as entries\n * waiting to be fetched.\n */\nexport function useSidebarDocuments({\n app,\n managedDocs,\n}: {\n app: AppState\n managedDocs: MaybeRefOrGetter<RegistryDocument[]>\n}) {\n const isTeamWorkspace = app.workspace.isTeamWorkspace\n\n /** Raw workspace documents projected into the shape the grouping logic needs. */\n const workspaceEntries = computed<WorkspaceDocumentEntry[]>(() => {\n const store = app.store.value\n if (!store) {\n return []\n }\n\n return Object.entries(store.workspace.documents).map(([name, doc]) => {\n const registry = doc?.['x-scalar-registry-meta']\n const navigation = doc?.['x-scalar-navigation'] as TraversedDocument | undefined\n\n const title = navigation?.title || doc?.info?.title || 'Untitled'\n\n return {\n documentName: name,\n title,\n navigation,\n // TODO: we can implement this later\n isPinned: false,\n isDirty: doc?.['x-scalar-is-dirty'] === true,\n registry: registry\n ? {\n namespace: registry.namespace,\n slug: registry.slug,\n version: registry.version,\n commitHash: registry.commitHash,\n conflictCheckedAgainstHash: registry.conflictCheckedAgainstHash,\n hasConflict: registry.hasConflict,\n }\n : undefined,\n }\n })\n })\n\n const documents = computed<SidebarDocumentItem[]>(() => {\n // Local workspaces: show the workspace document list as-is, no registry\n // grouping and no registry document lookups. The sidebar key is derived\n // from the workspace document name (which is guaranteed unique because\n // it is the map key in `store.workspace.documents`). If we keyed by\n // registry coordinates instead, two local documents sharing the same\n // `x-scalar-registry-meta` would produce duplicate Vue `:key`s in the\n // sidebar `v-for` and silently collide on re-render.\n if (!isTeamWorkspace.value) {\n return workspaceEntries.value.map<SidebarDocumentItem>((entry) => ({\n key: entry.documentName,\n title: entry.title,\n documentName: entry.documentName,\n registry: undefined,\n navigation: entry.navigation,\n isPinned: entry.isPinned ?? false,\n }))\n }\n\n const activeDocumentSlug = app.activeEntities.documentSlug.value\n\n // 1. Bucket workspace documents by their registry `namespace + slug`.\n // Documents without registry meta remain standalone entries.\n const workspaceByRegistry = new Map<string, WorkspaceDocumentEntry[]>()\n const standalone: WorkspaceDocumentEntry[] = []\n\n for (const entry of workspaceEntries.value) {\n if (!entry.registry) {\n standalone.push(entry)\n continue\n }\n const key = registryKey(entry.registry.namespace, entry.registry.slug)\n const bucket = workspaceByRegistry.get(key)\n if (bucket) {\n bucket.push(entry)\n } else {\n workspaceByRegistry.set(key, [entry])\n }\n }\n\n // 2. Build entries from the registry, merging in any loaded workspace\n // counterparts. The registry's version order is preserved so the\n // \"first\" version on each entry is the latest one advertised.\n const grouped: SidebarDocumentItem[] = []\n const consumedRegistryKeys = new Set<string>()\n\n for (const doc of toValue(managedDocs)) {\n const key = registryKey(doc.namespace, doc.slug)\n consumedRegistryKeys.add(key)\n const loaded = workspaceByRegistry.get(key) ?? []\n const item = buildRegistryItem({ key, registry: doc, loaded, activeDocumentSlug })\n if (item) {\n grouped.push(item)\n }\n }\n\n // 3. Workspace docs that point at a registry coordinate the registry has\n // not advertised (yet) still need to appear in the sidebar so the user\n // is not stranded without an entry to click.\n for (const [key, loaded] of workspaceByRegistry) {\n if (consumedRegistryKeys.has(key)) {\n continue\n }\n const first = loaded[0]?.registry\n if (!first) {\n continue\n }\n const item = buildRegistryItem({\n key,\n registry: {\n namespace: first.namespace,\n slug: first.slug,\n title: loaded[0]?.title ?? first.slug,\n versions: [],\n },\n loaded,\n activeDocumentSlug,\n })\n if (item) {\n grouped.push(item)\n }\n }\n\n return [...grouped, ...standalone.map(toStandaloneItem)]\n })\n\n const pinned = computed(() => documents.value.filter((d) => d.isPinned))\n const rest = computed(() => documents.value.filter((d) => !d.isPinned))\n\n return { documents, pinned, rest }\n}\n\n/** Project a standalone (non-registry) workspace entry into a sidebar item. */\nconst toStandaloneItem = (entry: WorkspaceDocumentEntry): SidebarDocumentItem => ({\n key: entry.documentName,\n title: entry.title,\n documentName: entry.documentName,\n registry: undefined,\n navigation: entry.navigation,\n isPinned: entry.isPinned ?? false,\n})\n\n/**\n * Build a single registry-backed sidebar item. The version list is assembled\n * by walking the registry's advertised versions in order and pairing them\n * with any loaded workspace document that claims the same `version` string.\n * Loaded versions that the registry has not advertised are appended at the\n * end so they stay visible until the registry catches up.\n */\nconst buildRegistryItem = ({\n key,\n registry,\n loaded,\n activeDocumentSlug,\n}: {\n key: string\n registry: RegistryDocument\n loaded: WorkspaceDocumentEntry[]\n activeDocumentSlug: string | undefined\n}): SidebarDocumentItem | undefined => {\n // Index loaded workspace docs by the version they claim. A workspace doc\n // that does not declare a `version` is treated as an orphan and pushed to\n // the bottom of the version list — it still belongs to the group but we\n // cannot reconcile it with the registry.\n const loadedByVersion = new Map<string, WorkspaceDocumentEntry>()\n const orphans: WorkspaceDocumentEntry[] = []\n\n for (const entry of loaded) {\n const version = entry.registry?.version\n if (!version) {\n orphans.push(entry)\n continue\n }\n if (loadedByVersion.has(version)) {\n // Multiple workspace docs claim the same version; keep the first one\n // (registry order wins) and surface the rest as orphans so they remain\n // visible instead of silently disappearing.\n orphans.push(entry)\n continue\n }\n loadedByVersion.set(version, entry)\n }\n\n const versions: SidebarDocumentVersion[] = []\n\n // Registry-backed rows always surface the registry title so the sidebar\n // matches what the registry advertises. Local renames are intentionally\n // ignored here; the slug is the last-resort fallback so the row always has\n // something to render.\n const groupTitle = registry.title || registry.slug\n\n // Build the drafts list first: entries left in `loadedByVersion` after we\n // remove every version the registry advertises. The workspace store\n // preserves insertion order, so the newest draft is naturally last —\n // reverse it here to surface the most recently created draft first.\n const draftEntries: [string, WorkspaceDocumentEntry][] = []\n for (const [version, match] of loadedByVersion) {\n if (registry.versions.some((v) => v.version === version)) {\n continue\n }\n draftEntries.push([version, match])\n }\n draftEntries.reverse()\n\n // Drafts go on top: the user just created them and the registry has not\n // seen them yet, so they are the most relevant rows in the picker.\n // Drafts never carry the \"Latest\" badge — that label always belongs to\n // the latest registry-advertised version, regardless of row position.\n for (const [version, match] of draftEntries) {\n versions.push({\n key: match.documentName,\n version,\n title: groupTitle,\n documentName: match.documentName,\n commitHash: match.registry?.commitHash,\n registryCommitHash: undefined,\n status: computeVersionStatus({\n isLoaded: true,\n localHash: match.registry?.commitHash,\n registryHash: undefined,\n isDirty: match.isDirty,\n }),\n isLatest: false,\n navigation: match.navigation,\n })\n }\n\n // Then the registry-advertised versions, in the order the registry\n // returned them (latest first by convention). The first one is the\n // canonical \"latest\" — flagged here so the picker can render the badge\n // independent of row order in the array.\n registry.versions.forEach((v, registryIndex) => {\n const match = loadedByVersion.get(v.version)\n const localHash = match?.registry?.commitHash\n const registryHash = v.commitHash\n versions.push({\n key: match ? match.documentName : versionKey(registry.namespace, registry.slug, v.version),\n version: v.version,\n title: groupTitle,\n documentName: match?.documentName,\n commitHash: localHash,\n registryCommitHash: registryHash,\n status: computeVersionStatus({\n isLoaded: Boolean(match),\n localHash,\n registryHash,\n isDirty: match?.isDirty,\n conflictCheckedAgainstHash: match?.registry?.conflictCheckedAgainstHash,\n hasConflict: match?.registry?.hasConflict,\n }),\n isLatest: registryIndex === 0,\n navigation: match?.navigation,\n })\n })\n\n // Loaded docs that did not declare a version at all.\n for (const orphan of orphans) {\n versions.push({\n key: orphan.documentName,\n version: orphan.registry?.version ?? '',\n title: groupTitle,\n documentName: orphan.documentName,\n commitHash: orphan.registry?.commitHash,\n registryCommitHash: undefined,\n status: computeVersionStatus({\n isLoaded: true,\n localHash: orphan.registry?.commitHash,\n registryHash: undefined,\n isDirty: orphan.isDirty,\n }),\n isLatest: false,\n navigation: orphan.navigation,\n })\n }\n\n if (versions.length === 0) {\n return undefined\n }\n\n // Pick the active version: prefer a loaded match against the active\n // document slug, then any loaded version, then fall back to the first\n // version (the latest advertised by the registry).\n const activeVersion =\n versions.find((v) => v.documentName !== undefined && v.documentName === activeDocumentSlug) ??\n versions.find((v) => v.documentName !== undefined) ??\n versions[0]!\n\n return {\n key,\n title: groupTitle,\n documentName: activeVersion.documentName,\n registry: { namespace: registry.namespace, slug: registry.slug },\n navigation: activeVersion.navigation,\n isPinned: loaded.some((e) => e.isPinned),\n versions,\n activeVersionKey: activeVersion.key,\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-sidebar-documents.js","names":[],"sources":["../../../../../src/v2/features/app/hooks/use-sidebar-documents.ts"],"sourcesContent":["import type { AppState } from '@scalar/api-client/v2/features/app'\nimport type { TraversedDocument } from '@scalar/workspace-store/schemas/navigation'\nimport { type MaybeRefOrGetter, computed, toValue } from 'vue'\n\nimport { type VersionStatus, computeVersionStatus } from '@/v2/features/app/helpers/compute-version-status'\nimport type { RegistryDocument } from '@/v2/types/configuration'\n\nexport type { VersionStatus }\n\n/**\n * A single version of a registry-backed document.\n *\n * On team workspaces, a registry document can advertise multiple versions and\n * each one may or may not have been imported into the local workspace store.\n * Loaded versions surface their `documentName` and traversal `navigation`,\n * unloaded versions are placeholders the sidebar can fetch on demand.\n */\nexport type SidebarDocumentVersion = {\n /** Stable key: workspace `documentName` when loaded, otherwise `${namespace}/${slug}@${version}`. */\n key: string\n /** Version identifier as advertised by the registry. */\n version: string\n /** User-facing label for the version row. */\n title: string\n /** Workspace store document name when this version is loaded locally. */\n documentName?: string\n /**\n * Commit hash recorded on the locally loaded workspace document, if any.\n * Undefined when the version has not been imported into the workspace\n * store or when the loaded document does not carry a hash.\n */\n commitHash?: string\n /**\n * Commit hash advertised by the registry for this version, if any.\n * Compared against `commitHash` to derive `status`.\n */\n registryCommitHash?: string\n /**\n * Sync status surfaced for the version row. Derived from the local /\n * registry commit hashes, the document's dirty flag and the cached\n * conflict-check result on `x-scalar-registry-meta`. `unknown` is used\n * for versions that are not loaded into the workspace store yet.\n */\n status: VersionStatus\n /**\n * True when this row is the canonical \"latest\" version of the group: the\n * first version the registry advertises. Drafts (locally-created versions\n * the registry has not seen yet) never get this flag, even when they are\n * surfaced ahead of the registry-advertised rows in the picker.\n */\n isLatest: boolean\n /** Traversal tree for this version. Populated only when the version is loaded into the workspace store. */\n navigation?: TraversedDocument\n}\n\n/**\n * A unified item for the top-level of our sidebar.\n *\n * It can represent one of three things:\n * - A workspace document that only exists locally (no registry link)\n * - A workspace document that was imported from the registry\n * - A registry document that has not yet been imported into the store\n *\n * For registry-backed entries, the parent fields (`navigation`, `documentName`)\n * mirror the active version so the sidebar template can render a single tree\n * without having to look up the active version itself. The full version list\n * lives in `versions`.\n */\nexport type SidebarDocumentItem = {\n /** Stable key used for sidebar state: `@namespace/slug` for registry-backed entries, document name otherwise. */\n key: string\n /**\n * User-facing title of the document. Registry-backed entries always surface\n * the registry's title so the sidebar matches what the registry advertises,\n * even when a locally loaded copy has been renamed. Standalone entries use\n * the workspace document title.\n */\n title: string\n /** Name of the document inside the workspace store (mirrors the active version on registry-backed entries). */\n documentName?: string\n /** Registry coordinates for the document group (without the version, which lives on `versions[]`). */\n registry?: { namespace: string; slug: string }\n /** Traversal tree of the active version (or the lone document for standalone entries). */\n navigation?: TraversedDocument\n /** Whether the document is pinned (todo: derived from `x-scalar-pinned`) */\n isPinned?: boolean\n /**\n * All known versions of the document, ordered with the registry's ordering\n * preserved (latest first by convention). Loaded versions surface their\n * workspace `documentName` and `navigation`, unloaded ones act as\n * placeholders the sidebar can fetch on demand. Undefined for standalone\n * entries that have no registry coordinates.\n */\n versions?: SidebarDocumentVersion[]\n /** Key of the version currently surfaced at the parent level (matches a `versions[].key`). */\n activeVersionKey?: string\n}\n\n/** Internal projection of a workspace document used during grouping. */\ntype WorkspaceDocumentEntry = {\n documentName: string\n title: string\n navigation?: TraversedDocument\n isPinned?: boolean\n /** Whether the workspace document has uncommitted local edits. */\n isDirty?: boolean\n registry?: {\n namespace: string\n slug: string\n version?: string\n commitHash?: string\n /** Last registry hash the conflict cache was computed against. */\n conflictCheckedAgainstHash?: string\n /** Cached conflict-check outcome for `conflictCheckedAgainstHash`. */\n hasConflict?: boolean\n }\n}\n\nconst registryKey = (namespace: string, slug: string) => `@${namespace}/${slug}`\n\nconst versionKey = (namespace: string, slug: string, version: string) => `${registryKey(namespace, slug)}@${version}`\n\n/**\n * Builds a unified list of sidebar documents.\n *\n * Behavior:\n * - Local workspaces (`teamUid === 'local'`) only show workspace documents.\n * Registry documents are not considered at all.\n * - Team workspaces group workspace documents by `namespace + slug` from\n * `x-scalar-registry-meta`. Each unique registry coordinate produces a\n * single sidebar entry whose versions are exposed as `versions`. The\n * `versions` array merges the registry-advertised versions with any\n * loaded workspace counterparts (matched by `version`). Registry\n * documents that have no loaded match are still surfaced as entries\n * waiting to be fetched.\n */\nexport function useSidebarDocuments({\n app,\n managedDocs,\n}: {\n app: AppState\n managedDocs: MaybeRefOrGetter<RegistryDocument[]>\n}) {\n const isTeamWorkspace = app.workspace.isTeamWorkspace\n\n /** Raw workspace documents projected into the shape the grouping logic needs. */\n const workspaceEntries = computed<WorkspaceDocumentEntry[]>(() => {\n const store = app.store.value\n if (!store) {\n return []\n }\n\n return Object.entries(store.workspace.documents).map(([name, doc]) => {\n const registry = doc?.['x-scalar-registry-meta']\n const navigation = doc?.['x-scalar-navigation'] as TraversedDocument | undefined\n\n const title = navigation?.title || doc?.info?.title || 'Untitled'\n\n return {\n documentName: name,\n title,\n navigation,\n // TODO: we can implement this later\n isPinned: false,\n isDirty: doc?.['x-scalar-is-dirty'] === true,\n registry: registry\n ? {\n namespace: registry.namespace,\n slug: registry.slug,\n version: registry.version,\n commitHash: registry.commitHash,\n conflictCheckedAgainstHash: registry.conflictCheckedAgainstHash,\n hasConflict: registry.hasConflict,\n }\n : undefined,\n }\n })\n })\n\n const documents = computed<SidebarDocumentItem[]>(() => {\n // Local workspaces: show the workspace document list as-is, no registry\n // grouping and no registry document lookups. The sidebar key is derived\n // from the workspace document name (which is guaranteed unique because\n // it is the map key in `store.workspace.documents`). If we keyed by\n // registry coordinates instead, two local documents sharing the same\n // `x-scalar-registry-meta` would produce duplicate Vue `:key`s in the\n // sidebar `v-for` and silently collide on re-render.\n if (!isTeamWorkspace.value) {\n return workspaceEntries.value.map<SidebarDocumentItem>((entry) => ({\n key: entry.documentName,\n title: entry.title,\n documentName: entry.documentName,\n registry: undefined,\n navigation: entry.navigation,\n isPinned: entry.isPinned ?? false,\n }))\n }\n\n const activeDocumentSlug = app.activeEntities.documentSlug.value\n\n // 1. Bucket workspace documents by their registry `namespace + slug`.\n // Documents without registry meta remain standalone entries.\n const workspaceByRegistry = new Map<string, WorkspaceDocumentEntry[]>()\n const standalone: WorkspaceDocumentEntry[] = []\n\n for (const entry of workspaceEntries.value) {\n if (!entry.registry) {\n standalone.push(entry)\n continue\n }\n const key = registryKey(entry.registry.namespace, entry.registry.slug)\n const bucket = workspaceByRegistry.get(key)\n if (bucket) {\n bucket.push(entry)\n } else {\n workspaceByRegistry.set(key, [entry])\n }\n }\n\n // 2. Build entries from the registry, merging in any loaded workspace\n // counterparts. The registry's version order is preserved so the\n // \"first\" version on each entry is the latest one advertised.\n const grouped: SidebarDocumentItem[] = []\n const consumedRegistryKeys = new Set<string>()\n\n for (const doc of toValue(managedDocs)) {\n const key = registryKey(doc.namespace, doc.slug)\n consumedRegistryKeys.add(key)\n const loaded = workspaceByRegistry.get(key) ?? []\n const item = buildRegistryItem({ key, registry: doc, loaded, activeDocumentSlug })\n if (item) {\n grouped.push(item)\n }\n }\n\n // 3. Workspace docs that point at a registry coordinate the registry has\n // not advertised (yet) still need to appear in the sidebar so the user\n // is not stranded without an entry to click.\n for (const [key, loaded] of workspaceByRegistry) {\n if (consumedRegistryKeys.has(key)) {\n continue\n }\n const first = loaded[0]?.registry\n if (!first) {\n continue\n }\n const item = buildRegistryItem({\n key,\n registry: {\n namespace: first.namespace,\n slug: first.slug,\n title: loaded[0]?.title ?? first.slug,\n versions: [],\n },\n loaded,\n activeDocumentSlug,\n })\n if (item) {\n grouped.push(item)\n }\n }\n\n return [...grouped, ...standalone.map(toStandaloneItem)]\n })\n\n const pinned = computed(() => documents.value.filter((d) => d.isPinned))\n const rest = computed(() => documents.value.filter((d) => !d.isPinned))\n\n return { documents, pinned, rest }\n}\n\n/** Project a standalone (non-registry) workspace entry into a sidebar item. */\nconst toStandaloneItem = (entry: WorkspaceDocumentEntry): SidebarDocumentItem => ({\n key: entry.documentName,\n title: entry.title,\n documentName: entry.documentName,\n registry: undefined,\n navigation: entry.navigation,\n isPinned: entry.isPinned ?? false,\n})\n\n/**\n * Build a single registry-backed sidebar item. The version list is assembled\n * by walking the registry's advertised versions in order and pairing them\n * with any loaded workspace document that claims the same `version` string.\n * Loaded versions that the registry has not advertised are appended at the\n * end so they stay visible until the registry catches up.\n */\nconst buildRegistryItem = ({\n key,\n registry,\n loaded,\n activeDocumentSlug,\n}: {\n key: string\n registry: RegistryDocument\n loaded: WorkspaceDocumentEntry[]\n activeDocumentSlug: string | undefined\n}): SidebarDocumentItem | undefined => {\n // Index loaded workspace docs by the version they claim. A workspace doc\n // that does not declare a `version` is treated as an orphan and pushed to\n // the bottom of the version list — it still belongs to the group but we\n // cannot reconcile it with the registry.\n const loadedByVersion = new Map<string, WorkspaceDocumentEntry>()\n const orphans: WorkspaceDocumentEntry[] = []\n\n for (const entry of loaded) {\n const version = entry.registry?.version\n if (!version) {\n orphans.push(entry)\n continue\n }\n if (loadedByVersion.has(version)) {\n // Multiple workspace docs claim the same version; keep the first one\n // (registry order wins) and surface the rest as orphans so they remain\n // visible instead of silently disappearing.\n orphans.push(entry)\n continue\n }\n loadedByVersion.set(version, entry)\n }\n\n const versions: SidebarDocumentVersion[] = []\n\n // Registry-backed rows always surface the registry title so the sidebar\n // matches what the registry advertises. Local renames are intentionally\n // ignored here; the slug is the last-resort fallback so the row always has\n // something to render.\n const groupTitle = registry.title || registry.slug\n\n // Build the drafts list first: entries left in `loadedByVersion` after we\n // remove every version the registry advertises. The workspace store\n // preserves insertion order, so the newest draft is naturally last —\n // reverse it here to surface the most recently created draft first.\n const draftEntries: [string, WorkspaceDocumentEntry][] = []\n for (const [version, match] of loadedByVersion) {\n if (registry.versions.some((v) => v.version === version)) {\n continue\n }\n draftEntries.push([version, match])\n }\n draftEntries.reverse()\n\n // Drafts go on top: the user just created them and the registry has not\n // seen them yet, so they are the most relevant rows in the picker.\n // Drafts never carry the \"Latest\" badge — that label always belongs to\n // the latest registry-advertised version, regardless of row position.\n for (const [version, match] of draftEntries) {\n versions.push({\n key: match.documentName,\n version,\n title: groupTitle,\n documentName: match.documentName,\n commitHash: match.registry?.commitHash,\n registryCommitHash: undefined,\n status: computeVersionStatus({\n isLoaded: true,\n localHash: match.registry?.commitHash,\n registryHash: undefined,\n isDirty: match.isDirty,\n }),\n isLatest: false,\n navigation: match.navigation,\n })\n }\n\n // Then the registry-advertised versions, in the order the registry\n // returned them (latest first by convention). The first one is the\n // canonical \"latest\" — flagged here so the picker can render the badge\n // independent of row order in the array.\n registry.versions.forEach((v, registryIndex) => {\n const match = loadedByVersion.get(v.version)\n const localHash = match?.registry?.commitHash\n const registryHash = v.commitHash\n versions.push({\n key: match ? match.documentName : versionKey(registry.namespace, registry.slug, v.version),\n version: v.version,\n title: groupTitle,\n documentName: match?.documentName,\n commitHash: localHash,\n registryCommitHash: registryHash,\n status: computeVersionStatus({\n isLoaded: Boolean(match),\n localHash,\n registryHash,\n isDirty: match?.isDirty,\n conflictCheckedAgainstHash: match?.registry?.conflictCheckedAgainstHash,\n hasConflict: match?.registry?.hasConflict,\n }),\n isLatest: registryIndex === 0,\n navigation: match?.navigation,\n })\n })\n\n // Loaded docs that did not declare a version at all.\n for (const orphan of orphans) {\n versions.push({\n key: orphan.documentName,\n version: orphan.registry?.version ?? '',\n title: groupTitle,\n documentName: orphan.documentName,\n commitHash: orphan.registry?.commitHash,\n registryCommitHash: undefined,\n status: computeVersionStatus({\n isLoaded: true,\n localHash: orphan.registry?.commitHash,\n registryHash: undefined,\n isDirty: orphan.isDirty,\n }),\n isLatest: false,\n navigation: orphan.navigation,\n })\n }\n\n if (versions.length === 0) {\n return undefined\n }\n\n // Pick the active version: prefer a loaded match against the active\n // document slug, then any loaded version, then fall back to the first\n // version (the latest advertised by the registry).\n const activeVersion =\n versions.find((v) => v.documentName !== undefined && v.documentName === activeDocumentSlug) ??\n versions.find((v) => v.documentName !== undefined) ??\n versions[0]!\n\n return {\n key,\n title: groupTitle,\n documentName: activeVersion.documentName,\n registry: { namespace: registry.namespace, slug: registry.slug },\n navigation: activeVersion.navigation,\n isPinned: loaded.some((e) => e.isPinned),\n versions,\n activeVersionKey: activeVersion.key,\n }\n}\n"],"mappings":"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"}
@@ -2,9 +2,9 @@ export { type AppState, createAppState } from '../../../v2/features/app/app-stat
2
2
  export { default as CreateWorkspaceModal } from '../../../v2/features/app/components/CreateWorkspaceModal.vue.js';
3
3
  export { default as CommandActionForm } from '../../../v2/features/command-palette/components/CommandActionForm.vue.js';
4
4
  export { default as CommandActionInput } from '../../../v2/features/command-palette/components/CommandActionInput.vue.js';
5
+ export type { RegistryAdapter, RegistryDocumentsState } from '../../../v2/types/configuration';
5
6
  export type { ClientLayout } from '../../../v2/types/layout';
6
7
  export { type CommandPaletteAction, type CommandPaletteRoute, baseClientActions, baseRoutes, useCommandPaletteState, } from '../command-palette/hooks/use-command-palette-state.js';
7
8
  export { default as ClientApp } from './App.vue.js';
8
9
  export { createApiClientApp, createAppRouter } from './helpers/create-api-client-app.js';
9
- export type { RegistryDocumentsState } from './hooks/use-sidebar-documents.js';
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/v2/features/app/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC3E,OAAO,EAAE,OAAO,IAAI,oBAAoB,EAAE,MAAM,uDAAuD,CAAA;AACvG,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,gEAAgE,CAAA;AAC7G,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,iEAAiE,CAAA;AAC/G,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,iBAAiB,EACjB,UAAU,EACV,sBAAsB,GACvB,MAAM,oDAAoD,CAAA;AAC3D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA;AACrF,YAAY,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/v2/features/app/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC3E,OAAO,EAAE,OAAO,IAAI,oBAAoB,EAAE,MAAM,uDAAuD,CAAA;AACvG,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,gEAAgE,CAAA;AAC7G,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,iEAAiE,CAAA;AAC/G,YAAY,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAA;AACvF,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,iBAAiB,EACjB,UAAU,EACV,sBAAsB,GACvB,MAAM,oDAAoD,CAAA;AAC3D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"DocumentCollection.vue.d.ts","sourceRoot":"","sources":["../../../../src/v2/features/collection/DocumentCollection.vue"],"names":[],"mappings":"AAofA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAA;AAQlE;;;;;;;GAOG;wBACkB,OAAO,YAAY;AAAxC,wBAAyC;AAKzC,QAAA,MAAM,YAAY,gSAo4Bd,CAAC"}
1
+ {"version":3,"file":"DocumentCollection.vue.d.ts","sourceRoot":"","sources":["../../../../src/v2/features/collection/DocumentCollection.vue"],"names":[],"mappings":"AAwJA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAA;AAOlE;;;;;;;GAOG;wBACkB,OAAO,YAAY;AAAxC,wBAAyC;AAKzC,QAAA,MAAM,YAAY,gSAmTd,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"DocumentCollection.vue.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":""}
1
+ {"version":3,"file":"DocumentCollection.vue.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":""}
@@ -1,16 +1,12 @@
1
1
  import IconSelector_default from "../../../components/IconSelector.vue.js";
2
2
  import { downloadAsFile } from "../../helpers/download-document.js";
3
3
  import LabelInput_default from "./components/LabelInput.vue.js";
4
- import SyncConflictResolutionEditor_default from "./components/SyncConflictResolutionEditor.vue.js";
5
4
  import Tabs_default from "./components/Tabs.vue.js";
6
- import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, mergeProps, openBlock, ref, resolveDynamicComponent, unref, withCtx } from "vue";
7
- import { ScalarButton, ScalarModal, ScalarSavePrompt, useLoadingState, useModal } from "@scalar/components";
8
- import { ScalarIconCloudArrowDown, ScalarIconDownload, ScalarIconFloppyDisk, ScalarIconSpinner, ScalarIconWarning } from "@scalar/icons";
9
- import { useToasts } from "@scalar/use-toasts";
5
+ import { computed, createBlock, createElementBlock, createElementVNode, createVNode, defineComponent, mergeProps, openBlock, resolveDynamicComponent, unref, withCtx } from "vue";
6
+ import { ScalarButton } from "@scalar/components";
7
+ import { ScalarIconDownload } from "@scalar/icons";
10
8
  import { RouterView } from "vue-router";
11
9
  import { LibraryIcon } from "@scalar/icons/library";
12
- import { apply } from "@scalar/json-magic/diff";
13
- import { deepClone } from "@scalar/workspace-store/helpers/deep-clone";
14
10
  //#region src/v2/features/collection/DocumentCollection.vue?vue&type=script&setup=true&lang.ts
15
11
  var _hoisted_1 = { class: "custom-scroll h-full" };
16
12
  var _hoisted_2 = {
@@ -26,14 +22,6 @@ var _hoisted_8 = {
26
22
  key: 1,
27
23
  class: "flex w-full flex-1 items-center justify-center"
28
24
  };
29
- var _hoisted_9 = { class: "flex flex-col gap-5" };
30
- var _hoisted_10 = { class: "flex gap-3" };
31
- var _hoisted_11 = {
32
- "aria-hidden": "true",
33
- class: "bg-b-3 text-c-2 flex size-10 shrink-0 items-center justify-center rounded-lg"
34
- };
35
- var _hoisted_12 = { class: "flex flex-wrap items-center justify-end gap-2 border-t pt-4" };
36
- var _hoisted_13 = { class: "flex h-full w-full flex-col gap-4 overflow-hidden" };
37
25
  var DocumentCollection_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({
38
26
  name: "DocumentCollection",
39
27
  props: {
@@ -63,280 +51,58 @@ var DocumentCollection_vue_vue_type_script_setup_true_lang_default = /* @__PURE_
63
51
  const title = computed(() => props.document?.info?.title ?? "");
64
52
  /** Default to the folder icon */
65
53
  const icon = computed(() => props.document?.["x-scalar-icon"] || "interface-content-folder");
66
- const syncModal = useModal();
67
- const dirtyBeforeSyncModal = useModal();
68
- const isDocumentDirty = computed(() => props.document?.["x-scalar-is-dirty"] === true);
69
- const saveLoader = useLoadingState();
70
- const documentSourceUrl = computed(() => props.document?.["x-scalar-original-source-url"]);
71
- const documentRegistryMeta = computed(() => props.document?.["x-scalar-registry-meta"]);
72
- /** Show Sync when the document has a source URL or registry meta (registry can be used if fetchRegistryDocument is set). */
73
- const canShowSyncButton = computed(() => documentSourceUrl.value !== void 0 || documentRegistryMeta.value !== void 0);
74
- const { toast } = useToasts();
75
- const undoChanges = () => {
76
- props.workspaceStore.revertDocumentChanges(props.documentSlug);
77
- };
78
- const saveChanges = async () => {
79
- saveLoader.start();
80
- await (await props.workspaceStore.saveDocument(props.documentSlug) ? saveLoader.validate() : saveLoader.invalidate({ persist: true }));
81
- };
82
54
  /** Downloads the document as a JSON file using the last saved state. */
83
55
  const downloadDocument = () => {
84
56
  const content = props.workspaceStore.exportDocument(props.documentSlug, "json", false);
85
57
  if (!content) return;
86
58
  downloadAsFile(content, `${title.value.replace(/[^\w\s-]/g, "").trim() || "document"}.json`);
87
59
  };
88
- const handleSaveThenCloseDirtyModal = async () => {
89
- await props.workspaceStore.saveDocument(props.documentSlug);
90
- dirtyBeforeSyncModal.hide();
91
- await handleSyncFlow();
92
- };
93
- const handleDiscardThenCloseDirtyModal = async () => {
94
- await props.workspaceStore.revertDocumentChanges(props.documentSlug);
95
- dirtyBeforeSyncModal.hide();
96
- await handleSyncFlow();
97
- };
98
- const isSyncInProgress = ref(false);
99
- const rebaseResult = ref(null);
100
- /**
101
- * Resolves the source for syncing. Registry meta has priority over x-scalar-original-source-url
102
- * when fetchRegistryDocument is provided. Returns either a URL or the full document object.
103
- */
104
- const resolveSyncInput = async () => {
105
- const registryMeta = documentRegistryMeta.value;
106
- if (registryMeta && props.fetchRegistryDocument) try {
107
- const result = await props.fetchRegistryDocument({
108
- namespace: registryMeta.namespace,
109
- slug: registryMeta.slug,
110
- version: registryMeta.version
111
- });
112
- if (!result.ok) {
113
- toast(result.error, "error");
114
- return null;
115
- }
116
- return { document: result.data };
117
- } catch (err) {
118
- toast("Failed to resolve document from registry", "error");
119
- return null;
120
- }
121
- const url = documentSourceUrl.value;
122
- if (url) return { url };
123
- return null;
124
- };
125
- /**
126
- * Handles actions to perform when synchronization is complete.
127
- * Hides the sync modal, resets the sync progress flag, and emits the
128
- * 'hooks:on:rebase:document:complete' event with document metadata.
129
- */
130
- const onSyncComplete = () => {
131
- syncModal.hide();
132
- isSyncInProgress.value = false;
133
- toast("Your document has been rebased with the latest version from the source.", "info");
134
- props.eventBus.emit("hooks:on:rebase:document:complete", { meta: { documentName: props.documentSlug } });
135
- };
136
- /**
137
- * Handles errors that occur during synchronization.
138
- * If an error string is provided, it displays the error via toast.
139
- * Always resets the sync progress flag.
140
- */
141
- const onSyncError = (error) => {
142
- if (error !== null) toast(error, "error");
143
- isSyncInProgress.value = false;
144
- };
145
- /**
146
- * Handles the synchronization flow for a document.
147
- * Checks for unsaved changes, resolves source (registry over URL),
148
- * initiates rebasing, handles conflicts, and emits completion events.
149
- * If conflicts are detected, a modal dialog is shown for user resolution.
150
- */
151
- const handleSyncFlow = async () => {
152
- if (isDocumentDirty.value) {
153
- dirtyBeforeSyncModal.show();
154
- return;
155
- }
156
- if (isSyncInProgress.value) return;
157
- isSyncInProgress.value = true;
158
- const input = await resolveSyncInput();
159
- if (!input) {
160
- onSyncError(null);
161
- return;
162
- }
163
- const result = await props.workspaceStore.rebaseDocument({
164
- name: props.documentSlug,
165
- ...input
166
- });
167
- if (result?.ok) {
168
- const originalDocument = props.workspaceStore.getOriginalDocument(props.documentSlug) ?? {};
169
- rebaseResult.value = {
170
- conflicts: result.conflicts,
171
- applyChanges: result.applyChanges,
172
- resolvedDocument: apply(deepClone(originalDocument), result.changes),
173
- originalDocument
174
- };
175
- if (rebaseResult.value.conflicts.length > 0) syncModal.show();
176
- else {
177
- await rebaseResult.value?.applyChanges({ resolvedDocument: rebaseResult.value.resolvedDocument });
178
- onSyncComplete();
179
- }
180
- } else if (result?.ok === false && result.type === "NO_CHANGES_DETECTED") onSyncComplete();
181
- else onSyncError("Failed to sync document");
182
- };
183
- const handleApplyChanges = async ({ resolvedDocument }) => {
184
- await rebaseResult.value?.applyChanges({ resolvedDocument });
185
- props.eventBus.emit("hooks:on:rebase:document:complete", { meta: { documentName: props.documentSlug } });
186
- syncModal.hide();
187
- };
188
- /**
189
- * Resets sync state when the sync conflict modal is closed (dismissed or after
190
- * applying changes). Ensures the Sync button is re-enabled and conflict state
191
- * is cleared.
192
- */
193
- const onSyncModalClose = () => {
194
- isSyncInProgress.value = false;
195
- rebaseResult.value = null;
196
- };
197
60
  return (_ctx, _cache) => {
198
- return openBlock(), createElementBlock(Fragment, null, [
199
- createElementVNode("div", _hoisted_1, [__props.document ? (openBlock(), createElementBlock("div", _hoisted_2, [
200
- createElementVNode("div", {
201
- "aria-label": `title: ${title.value}`,
202
- class: "md:max-w-content mx-auto flex h-fit w-full flex-col gap-2 pt-14 pb-3 md:pt-6"
203
- }, [createVNode(unref(ScalarSavePrompt), {
204
- modelValue: isDocumentDirty.value,
205
- "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => isDocumentDirty.value = $event),
206
- class: "w-content-padded-4 max-w-full-padded-4 absolute",
207
- loader: unref(saveLoader),
208
- onDiscard: undoChanges,
209
- onSave: saveChanges
210
- }, null, 8, ["modelValue", "loader"]), createElementVNode("div", _hoisted_4, [
211
- createElementVNode("div", _hoisted_5, [createVNode(IconSelector_default, {
212
- modelValue: icon.value,
213
- placement: "bottom-start",
214
- "onUpdate:modelValue": _cache[1] || (_cache[1] = (icon) => __props.eventBus.emit("document:update:icon", icon))
215
- }, {
216
- default: withCtx(() => [createVNode(unref(ScalarButton), {
217
- class: "hover:bg-b-2 aspect-square h-7 w-7 cursor-pointer rounded border border-transparent p-0 hover:border-inherit",
218
- variant: "ghost"
219
- }, {
220
- default: withCtx(() => [createVNode(unref(LibraryIcon), {
221
- class: "text-c-2 size-5",
222
- src: icon.value,
223
- "stroke-width": "2"
224
- }, null, 8, ["src"])]),
225
- _: 1
226
- })]),
227
- _: 1
228
- }, 8, ["modelValue"]), createElementVNode("div", _hoisted_6, [createVNode(LabelInput_default, {
229
- class: "text-xl font-bold",
230
- inputId: "documentName",
231
- modelValue: title.value,
232
- "onUpdate:modelValue": _cache[2] || (_cache[2] = (title) => __props.eventBus.emit("document:update:info", { title }))
233
- }, null, 8, ["modelValue"])])]),
234
- createVNode(unref(ScalarButton), {
235
- class: "text-c-2 hover:text-c-1 flex shrink-0 items-center gap-2",
236
- size: "xs",
237
- type: "button",
238
- variant: "ghost",
239
- onClick: downloadDocument
240
- }, {
241
- default: withCtx(() => [createVNode(unref(ScalarIconDownload), {
242
- size: "sm",
243
- thickness: "1.5"
244
- }), _cache[6] || (_cache[6] = createElementVNode("span", null, "Download document", -1))]),
245
- _: 1
246
- }),
247
- canShowSyncButton.value ? (openBlock(), createBlock(unref(ScalarButton), {
248
- key: 0,
249
- class: "text-c-2 hover:text-c-1 shrink-0 gap-1.5",
250
- "data-testid": "document-sync-button",
251
- disabled: isSyncInProgress.value,
252
- size: "xs",
253
- title: "Pull the latest version from the document source and merge with your local copy. Save your changes first if you have unsaved edits.",
254
- type: "button",
255
- variant: "ghost",
256
- onClick: handleSyncFlow
257
- }, {
258
- default: withCtx(() => [isSyncInProgress.value ? (openBlock(), createBlock(unref(ScalarIconSpinner), {
259
- key: 0,
260
- class: "size-3.5 animate-spin",
261
- size: "sm"
262
- })) : (openBlock(), createBlock(unref(ScalarIconCloudArrowDown), {
263
- key: 1,
264
- class: "size-3.5",
265
- size: "sm",
266
- thickness: "1.5"
267
- })), _cache[7] || (_cache[7] = createElementVNode("span", null, "Sync from source", -1))]),
268
- _: 1
269
- }, 8, ["disabled"])) : createCommentVNode("", true)
270
- ])], 8, _hoisted_3),
271
- createVNode(Tabs_default, { type: "document" }),
272
- createElementVNode("div", _hoisted_7, [createVNode(unref(RouterView), null, {
273
- default: withCtx(({ Component }) => [(openBlock(), createBlock(resolveDynamicComponent(Component), mergeProps(props, { collectionType: "document" }), null, 16))]),
61
+ return openBlock(), createElementBlock("div", _hoisted_1, [__props.document ? (openBlock(), createElementBlock("div", _hoisted_2, [
62
+ createElementVNode("div", {
63
+ "aria-label": `title: ${title.value}`,
64
+ class: "md:max-w-content mx-auto flex h-fit w-full flex-col gap-2 pt-14 pb-3 md:pt-6"
65
+ }, [createElementVNode("div", _hoisted_4, [createElementVNode("div", _hoisted_5, [createVNode(IconSelector_default, {
66
+ modelValue: icon.value,
67
+ placement: "bottom-start",
68
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = (icon) => __props.eventBus.emit("document:update:icon", icon))
69
+ }, {
70
+ default: withCtx(() => [createVNode(unref(ScalarButton), {
71
+ class: "hover:bg-b-2 aspect-square h-7 w-7 cursor-pointer rounded border border-transparent p-0 hover:border-inherit",
72
+ variant: "ghost"
73
+ }, {
74
+ default: withCtx(() => [createVNode(unref(LibraryIcon), {
75
+ class: "text-c-2 size-5",
76
+ src: icon.value,
77
+ "stroke-width": "2"
78
+ }, null, 8, ["src"])]),
274
79
  _: 1
275
- })])
276
- ])) : (openBlock(), createElementBlock("div", _hoisted_8, [..._cache[8] || (_cache[8] = [createElementVNode("div", { class: "flex h-full flex-col items-center justify-center" }, [createElementVNode("h1", { class: "text-2xl font-bold" }, "Document not found"), createElementVNode("p", { class: "text-gray-500" }, " The document you are looking for does not exist. ")], -1)])]))]),
277
- createVNode(unref(ScalarModal), {
278
- bodyClass: "border-t-0 rounded-t-lg flex flex-col gap-5",
80
+ })]),
81
+ _: 1
82
+ }, 8, ["modelValue"]), createElementVNode("div", _hoisted_6, [createVNode(LabelInput_default, {
83
+ class: "text-xl font-bold",
84
+ inputId: "documentName",
85
+ modelValue: title.value,
86
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = (title) => __props.eventBus.emit("document:update:info", { title }))
87
+ }, null, 8, ["modelValue"])])]), createVNode(unref(ScalarButton), {
88
+ class: "text-c-2 hover:text-c-1 flex shrink-0 items-center gap-2",
279
89
  size: "xs",
280
- state: unref(dirtyBeforeSyncModal),
281
- title: "Sync requires saved document",
282
- onClose: _cache[4] || (_cache[4] = ($event) => unref(dirtyBeforeSyncModal).hide())
90
+ type: "button",
91
+ variant: "ghost",
92
+ onClick: downloadDocument
283
93
  }, {
284
- default: withCtx(() => [createElementVNode("div", _hoisted_9, [createElementVNode("div", _hoisted_10, [createElementVNode("div", _hoisted_11, [createVNode(unref(ScalarIconWarning), { class: "text-yellow size-5" })]), _cache[9] || (_cache[9] = createElementVNode("div", { class: "min-w-0 flex-1 space-y-1" }, [createElementVNode("p", { class: "text-c-1 text-sm leading-snug font-medium" }, " You have unsaved changes "), createElementVNode("p", { class: "text-c-2 text-sm leading-relaxed" }, " Save your work to keep changes, or discard to revert to the last saved version. Then you can sync with the source. ")], -1))]), createElementVNode("div", _hoisted_12, [
285
- createVNode(unref(ScalarButton), {
286
- size: "sm",
287
- type: "button",
288
- variant: "ghost",
289
- onClick: _cache[3] || (_cache[3] = ($event) => unref(dirtyBeforeSyncModal).hide())
290
- }, {
291
- default: withCtx(() => [..._cache[10] || (_cache[10] = [createTextVNode(" Cancel ", -1)])]),
292
- _: 1
293
- }),
294
- createVNode(unref(ScalarButton), {
295
- size: "sm",
296
- type: "button",
297
- variant: "outlined",
298
- onClick: handleDiscardThenCloseDirtyModal
299
- }, {
300
- default: withCtx(() => [..._cache[11] || (_cache[11] = [createTextVNode(" Discard changes ", -1)])]),
301
- _: 1
302
- }),
303
- createVNode(unref(ScalarButton), {
304
- class: "flex items-center gap-2",
305
- size: "sm",
306
- type: "button",
307
- variant: "solid",
308
- onClick: handleSaveThenCloseDirtyModal
309
- }, {
310
- default: withCtx(() => [createVNode(unref(ScalarIconFloppyDisk), {
311
- size: "sm",
312
- thickness: "1.5"
313
- }), _cache[12] || (_cache[12] = createTextVNode(" Save and continue ", -1))]),
314
- _: 1
315
- })
316
- ])])]),
94
+ default: withCtx(() => [createVNode(unref(ScalarIconDownload), {
95
+ size: "sm",
96
+ thickness: "1.5"
97
+ }), _cache[2] || (_cache[2] = createElementVNode("span", null, "Download document", -1))]),
317
98
  _: 1
318
- }, 8, ["state"]),
319
- rebaseResult.value ? (openBlock(), createBlock(unref(ScalarModal), {
320
- key: 0,
321
- bodyClass: "sync-conflict-modal-root flex h-dvh flex-col p-4",
322
- maxWidth: "calc(100dvw - 32px)",
323
- size: "full",
324
- state: unref(syncModal),
325
- onClose: onSyncModalClose
326
- }, {
327
- default: withCtx(() => [createElementVNode("div", _hoisted_13, [createVNode(SyncConflictResolutionEditor_default, {
328
- baseDocument: rebaseResult.value.originalDocument,
329
- conflicts: rebaseResult.value.conflicts,
330
- resolvedDocument: rebaseResult.value.resolvedDocument,
331
- onApplyChanges: _cache[5] || (_cache[5] = (payload) => handleApplyChanges(payload))
332
- }, null, 8, [
333
- "baseDocument",
334
- "conflicts",
335
- "resolvedDocument"
336
- ])])]),
99
+ })])], 8, _hoisted_3),
100
+ createVNode(Tabs_default, { type: "document" }),
101
+ createElementVNode("div", _hoisted_7, [createVNode(unref(RouterView), null, {
102
+ default: withCtx(({ Component }) => [(openBlock(), createBlock(resolveDynamicComponent(Component), mergeProps(props, { collectionType: "document" }), null, 16))]),
337
103
  _: 1
338
- }, 8, ["state"])) : createCommentVNode("", true)
339
- ], 64);
104
+ })])
105
+ ])) : (openBlock(), createElementBlock("div", _hoisted_8, [..._cache[3] || (_cache[3] = [createElementVNode("div", { class: "flex h-full flex-col items-center justify-center" }, [createElementVNode("h1", { class: "text-2xl font-bold" }, "Document not found"), createElementVNode("p", { class: "text-gray-500" }, " The document you are looking for does not exist. ")], -1)])]))]);
340
106
  };
341
107
  }
342
108
  });