diff --git a/src/lang/locales/en-US.json b/src/lang/locales/en-US.json index 04f995b89..d43ffb55d 100644 --- a/src/lang/locales/en-US.json +++ b/src/lang/locales/en-US.json @@ -69,7 +69,11 @@ "view-symbol": "View symbol", "view-sample-code": "View sample code" }, - "view-more": "View more" + "view-more": "View more", + "hero": { + "title": "Developer Documentation", + "copy": "Browse the latest API reference." + } }, "declarations": { "hide-other-declarations": "Hide other declarations", diff --git a/src/lang/locales/ja-JP.json b/src/lang/locales/ja-JP.json index 280bffe1d..b24244253 100644 --- a/src/lang/locales/ja-JP.json +++ b/src/lang/locales/ja-JP.json @@ -69,7 +69,11 @@ "view-symbol": "記号を表示", "view-sample-code": "サンプルコードを表示" }, - "view-more": "さらに表示" + "view-more": "さらに表示", + "hero": { + "title": "デベロッパドキュメント", + "copy": "最新のAPIリファレンスを閲覧できます。" + } }, "declarations": { "hide-other-declarations": "ほかの宣言を非表示", diff --git a/src/lang/locales/ko-KR.json b/src/lang/locales/ko-KR.json index 68b1afe7c..7dad7ec32 100644 --- a/src/lang/locales/ko-KR.json +++ b/src/lang/locales/ko-KR.json @@ -69,7 +69,11 @@ "view-symbol": "기호 보기", "view-sample-code": "샘플 코드 보기" }, - "view-more": "더 보기" + "view-more": "더 보기", + "hero": { + "title": "개발자 문서", + "copy": "최신 API 레퍼런스를 확인하세요." + } }, "declarations": { "hide-other-declarations": "다른 선언 가리기", diff --git a/src/lang/locales/zh-CN.json b/src/lang/locales/zh-CN.json index 5b779adf1..70236c2de 100644 --- a/src/lang/locales/zh-CN.json +++ b/src/lang/locales/zh-CN.json @@ -69,7 +69,11 @@ "view-symbol": "查看符号", "view-sample-code": "查看示例代码" }, - "view-more": "查看更多" + "view-more": "查看更多", + "hero": { + "title": "开发者文档", + "copy": "浏览最新的 API 参考。" + } }, "declarations": { "hide-other-declarations": "隐藏其他声明", diff --git a/src/mixins/indexDataFetcher.js b/src/mixins/indexDataFetcher.js index 10d41881d..2940a80df 100644 --- a/src/mixins/indexDataFetcher.js +++ b/src/mixins/indexDataFetcher.js @@ -28,8 +28,10 @@ export default { interfaceLanguages, references = {}, } = await fetchData(this.indexDataPath); + const topLevelNodes = Object.values(interfaceLanguages || {}).flat(); const flatChildren = Object.freeze(flattenNavigationIndex(interfaceLanguages)); IndexStore.setFlatChildren(flatChildren); + IndexStore.setTopLevelNodes(topLevelNodes); IndexStore.setTechnologyProps(extractTechnologyProps(interfaceLanguages)); IndexStore.setReferences(references); IndexStore.setIncludedArchiveIdentifiers(includedArchiveIdentifiers); diff --git a/src/routes.js b/src/routes.js index 11343bed5..d916fa49e 100644 --- a/src/routes.js +++ b/src/routes.js @@ -16,6 +16,14 @@ import { import ServerError from 'theme/views/ServerError.vue'; import NotFound from 'theme/views/NotFound.vue'; +export const homeRoute = { + path: '/', + name: 'home-index', + component: () => import( + /* webpackChunkName: "home-index" */ 'theme/views/Index.vue' + ), +}; + export const fallbackRoutes = [ { path: '*', @@ -54,6 +62,7 @@ export const pagesRoutes = [ ]; export default [ + homeRoute, ...pagesRoutes, ...fallbackRoutes, ]; diff --git a/src/setup-utils/SwiftDocCRenderRouter.js b/src/setup-utils/SwiftDocCRenderRouter.js index 26a70308d..8558a9b89 100644 --- a/src/setup-utils/SwiftDocCRenderRouter.js +++ b/src/setup-utils/SwiftDocCRenderRouter.js @@ -14,12 +14,13 @@ import { restoreScrollOnReload, scrollBehavior, } from 'docc-render/utils/router-utils'; -import routes, { fallbackRoutes } from 'docc-render/routes'; +import { homeRoute, pagesRoutes, fallbackRoutes } from 'docc-render/routes'; import { baseUrl } from 'docc-render/utils/theme-settings'; import { addPrefixedRoutes } from 'docc-render/utils/route-utils'; const defaultRoutes = [ - ...addPrefixedRoutes(routes), + homeRoute, + ...addPrefixedRoutes(pagesRoutes), ...fallbackRoutes, ]; diff --git a/src/stores/IndexStore.js b/src/stores/IndexStore.js index 39333424f..600f5b290 100644 --- a/src/stores/IndexStore.js +++ b/src/stores/IndexStore.js @@ -11,6 +11,7 @@ export default { state: { flatChildren: null, + topLevelNodes: [], references: {}, apiChanges: null, apiChangesVersion: null, @@ -21,6 +22,7 @@ export default { }, reset() { this.state.flatChildren = null; + this.state.topLevelNodes = []; this.state.references = {}; this.state.apiChanges = null; this.state.apiChangesVersion = null; @@ -35,6 +37,9 @@ export default { setReferences(references) { this.state.references = references; }, + setTopLevelNodes(nodes) { + this.state.topLevelNodes = nodes || []; + }, setApiChanges(diff) { this.state.apiChanges = diff; }, diff --git a/src/views/Index.vue b/src/views/Index.vue new file mode 100644 index 000000000..85a12288b --- /dev/null +++ b/src/views/Index.vue @@ -0,0 +1,315 @@ + + + + + + + diff --git a/tests/unit/mixins/indexDataFetcher.spec.js b/tests/unit/mixins/indexDataFetcher.spec.js index aefb01d7a..29329f3ac 100644 --- a/tests/unit/mixins/indexDataFetcher.spec.js +++ b/tests/unit/mixins/indexDataFetcher.spec.js @@ -258,6 +258,7 @@ describe('indexDataFetcher', () => { errorFetching: false, errorFetchingDiffs: false, technologyProps: {}, + topLevelNodes: [], }); }); diff --git a/tests/unit/stores/IndexStore.spec.js b/tests/unit/stores/IndexStore.spec.js index 76f6990cd..a460012e0 100644 --- a/tests/unit/stores/IndexStore.spec.js +++ b/tests/unit/stores/IndexStore.spec.js @@ -57,6 +57,7 @@ describe('IndexStore', () => { errorFetching: false, errorFetchingDiffs: false, technologyProps: {}, + topLevelNodes: [], }; beforeEach(() => { diff --git a/tests/unit/views/Index.spec.js b/tests/unit/views/Index.spec.js new file mode 100644 index 000000000..a13e3a498 --- /dev/null +++ b/tests/unit/views/Index.spec.js @@ -0,0 +1,98 @@ +/** + * This source file is part of the Swift.org open source project + * + * Copyright (c) 2025 Apple Inc. and the Swift project authors + * Licensed under Apache License v2.0 with Runtime Library Exception + * + * See https://swift.org/LICENSE.txt for license information + * See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import { shallowMount } from '@vue/test-utils'; +import Index from '@/views/Index.vue'; +import Navigator from 'docc-render/components/Navigator.vue'; + +const defaultMocks = { + $bridge: { + on: jest.fn(), + off: jest.fn(), + send: jest.fn(), + }, + $route: {}, +}; + +const messages = { + 'documentation.hero.title': 'Developer Documentation', + 'documentation.hero.copy': 'Browse the latest API reference.', +}; + +const DocumentationLayoutStub = { + name: 'DocumentationLayout', + props: ['enableNavigator', 'interfaceLanguage', 'references', 'navigatorFixedWidth', 'quickNavNodes'], + template: '
', +}; + +const baseDataFn = ( + Index.options && Index.options.data + ? Index.options.data.bind(Index) + : () => ({}) +); +const baseData = baseDataFn(); + +const mountWith = (indexStateOverrides = {}, extraOptions = {}) => shallowMount(Index, { + mocks: { + ...defaultMocks, + $t: key => messages[key] || key, + $te: key => Boolean(messages[key]), + }, + stubs: { + DocumentationLayout: DocumentationLayoutStub, + Navigator: false, + QuickNavigationButton: true, + TopicTypeIcon: true, + RouterLink: { + render(h) { return h('a', {}, this.$slots.default); }, + }, + ...(extraOptions.stubs || {}), + }, + data: () => ({ + ...baseData, + indexState: { + ...(baseData.indexState || {}), + flatChildren: [], + topLevelNodes: [], + references: {}, + ...indexStateOverrides, + }, + }), + computed: { + indexNodes: () => [], + ...(extraOptions.computed || {}), + }, +}); + +describe('Index view', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders hero text from translation keys', () => { + const wrapper = mountWith(); + expect(wrapper.find('.hero__title').text()).toBe('Developer Documentation'); + expect(wrapper.find('.hero__lede').text()).toBe('Browse the latest API reference.'); + }); + + it('passes top-level nodes to navigator only', () => { + const topLevelNodes = [{ path: '/documentation/foo', title: 'Foo', type: 'module' }]; + const wrapper = mountWith({ topLevelNodes }); + const nav = wrapper.findComponent(Navigator); + expect(nav.exists()).toBe(true); + expect(nav.props('flatChildren').length).toBe(1); + expect(nav.props('flatChildren')[0].path).toBe('/documentation/foo'); + }); + + it('hides sections when lists are empty', () => { + const wrapper = mountWith(); + expect(wrapper.find('section.index-section--grid').exists()).toBe(false); + }); +});