@uxfront/layer-docs 0.4.1 → 0.5.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.
- package/README.md +31 -220
- package/app/app.config.ts +8 -92
- package/app/components/content/FrameworkSwitcher.vue +66 -40
- package/app/components/docs/DocsAsideLeftTop.vue +9 -15
- package/app/components/docs/DocsFrameworkSelect.vue +4 -7
- package/app/composables/useFramework.ts +30 -38
- package/nuxt.config.ts +14 -170
- package/package.json +10 -77
- package/CHANGELOG.md +0 -192
- package/LICENSE +0 -21
- package/app/app.vue +0 -138
- package/app/assets/css/main.css +0 -15
- package/app/components/IconMenuToggle.vue +0 -92
- package/app/components/LanguageSelect.vue +0 -73
- package/app/components/MorphingGradientBackground.vue +0 -261
- package/app/components/OgImage/OgImageDocs.satori.vue +0 -40
- package/app/components/OgImage/OgImageLanding.satori.vue +0 -41
- package/app/components/app/AppFooter.vue +0 -13
- package/app/components/app/AppFooterCenter.vue +0 -17
- package/app/components/app/AppFooterLeft.vue +0 -21
- package/app/components/app/AppFooterRight.vue +0 -33
- package/app/components/app/AppHeader.vue +0 -123
- package/app/components/app/AppHeaderAttribution.vue +0 -45
- package/app/components/app/AppHeaderBody.vue +0 -14
- package/app/components/app/AppHeaderCTA.vue +0 -31
- package/app/components/app/AppHeaderCenter.vue +0 -10
- package/app/components/app/AppHeaderLogo.vue +0 -16
- package/app/components/app/AppOgDecoration.vue +0 -27
- package/app/components/app/AppOgLogo.vue +0 -19
- package/app/components/app/AppSearch.vue +0 -59
- package/app/components/app/AppSubHeader.vue +0 -21
- package/app/components/content/BrowserFrame.vue +0 -28
- package/app/components/content/GradientPageHero.vue +0 -35
- package/app/components/content/StorybookEmbed.vue +0 -160
- package/app/components/content/Video.vue +0 -103
- package/app/components/docs/DocsAsideLeftBody.vue +0 -20
- package/app/components/docs/DocsAsideRightBottom.vue +0 -15
- package/app/components/docs/DocsPageHeaderLinks.vue +0 -75
- package/app/composables/useDocsSections.ts +0 -57
- package/app/composables/useDocusI18n.ts +0 -49
- package/app/constants/sections.ts +0 -25
- package/app/error.vue +0 -140
- package/app/layouts/default.vue +0 -24
- package/app/pages/[[lang]]/[...slug].vue +0 -58
- package/app/pages/[[lang]]/docs/[section]/[...slug].vue +0 -180
- package/app/plugins/i18n.ts +0 -21
- package/app/plugins/posthog.client.ts +0 -56
- package/app/types/non-route-categories.ts +0 -12
- package/app/utils/flattenNavigation.ts +0 -22
- package/app/utils/foldNonRouteCategories.ts +0 -47
- package/app/utils/prerender.ts +0 -9
- package/app/utils/storybookEmbed.test.ts +0 -98
- package/app/utils/storybookEmbed.ts +0 -93
- package/i18n/locales/ar.json +0 -24
- package/i18n/locales/be.json +0 -24
- package/i18n/locales/bn.json +0 -24
- package/i18n/locales/ca.json +0 -24
- package/i18n/locales/ckb.json +0 -24
- package/i18n/locales/cs.json +0 -24
- package/i18n/locales/da.json +0 -24
- package/i18n/locales/de.json +0 -24
- package/i18n/locales/el.json +0 -24
- package/i18n/locales/en.json +0 -24
- package/i18n/locales/et.json +0 -24
- package/i18n/locales/fr.json +0 -24
- package/i18n/locales/he.json +0 -24
- package/i18n/locales/hi.json +0 -24
- package/i18n/locales/hy.json +0 -24
- package/i18n/locales/it.json +0 -24
- package/i18n/locales/ja.json +0 -24
- package/i18n/locales/kk.json +0 -24
- package/i18n/locales/km.json +0 -24
- package/i18n/locales/ko.json +0 -24
- package/i18n/locales/ky.json +0 -24
- package/i18n/locales/lb.json +0 -24
- package/i18n/locales/ms.json +0 -24
- package/i18n/locales/nb.json +0 -24
- package/i18n/locales/pl.json +0 -24
- package/i18n/locales/ru.json +0 -24
- package/i18n/locales/sl.json +0 -24
- package/i18n/locales/sv.json +0 -24
- package/i18n/locales/uk.json +0 -24
- package/i18n/locales/ur.json +0 -24
- package/i18n/locales/vi.json +0 -24
- package/modules/config.ts +0 -144
- package/modules/optimizeDeps.ts +0 -45
- package/modules/routing.ts +0 -20
- package/nuxt.schema.ts +0 -374
- package/server/plugins/llms-redirect.ts +0 -60
- package/server/routes/raw/[...slug].md.get.ts +0 -74
- package/storybook/index.test.ts +0 -110
- package/storybook/index.ts +0 -362
- package/test/brand-palette.ts +0 -235
- package/test/no-brand-leakage.test.ts +0 -124
- package/tsconfig.json +0 -17
- package/utils/accent.ts +0 -80
- package/utils/content.ts +0 -193
- package/utils/git.ts +0 -114
- package/utils/meta.ts +0 -28
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { useIntersectionObserver } from "@vueuse/core";
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
poster?: string;
|
|
6
|
-
src?: string;
|
|
7
|
-
sources?: string[];
|
|
8
|
-
controls?: boolean;
|
|
9
|
-
autoplay?: boolean;
|
|
10
|
-
loop?: boolean;
|
|
11
|
-
muted?: boolean;
|
|
12
|
-
playsinline?: boolean;
|
|
13
|
-
pauseOnExit?: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
17
|
-
poster: "",
|
|
18
|
-
src: "",
|
|
19
|
-
sources: undefined,
|
|
20
|
-
controls: false,
|
|
21
|
-
autoplay: false,
|
|
22
|
-
loop: false,
|
|
23
|
-
muted: true,
|
|
24
|
-
playsinline: true,
|
|
25
|
-
pauseOnExit: true,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const playing = ref(false);
|
|
29
|
-
const loaded = ref(false);
|
|
30
|
-
const isIntersecting = ref(false);
|
|
31
|
-
const posterLoaded = ref(false);
|
|
32
|
-
|
|
33
|
-
const videoRef = useTemplateRef<HTMLVideoElement | null>("video");
|
|
34
|
-
|
|
35
|
-
const { stop } = useIntersectionObserver(videoRef, ([entry], observerElement) => {
|
|
36
|
-
isIntersecting.value = entry?.isIntersecting || false;
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const allSources = computed(() => props.sources ?? [props.src]);
|
|
40
|
-
|
|
41
|
-
const lazyPoster = computed(() => {
|
|
42
|
-
if (isIntersecting.value && !posterLoaded.value) {
|
|
43
|
-
posterLoaded.value = true;
|
|
44
|
-
return props.poster;
|
|
45
|
-
} else if (posterLoaded.value) {
|
|
46
|
-
return props.poster;
|
|
47
|
-
} else {
|
|
48
|
-
return "";
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
onUnmounted(() => {
|
|
53
|
-
stop();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
watch(isIntersecting, (currentValue, previousValue) => {
|
|
57
|
-
// Pause video if it was playing and is no longer intersecting
|
|
58
|
-
if (props.pauseOnExit) {
|
|
59
|
-
if (previousValue && playing.value) {
|
|
60
|
-
videoRef.value?.pause();
|
|
61
|
-
} else if (currentValue && !playing.value) {
|
|
62
|
-
videoRef.value?.play();
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Load if video is intersecting
|
|
67
|
-
if (currentValue && !loaded.value) {
|
|
68
|
-
videoRef.value?.load();
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
const onPlay = () => {
|
|
73
|
-
playing.value = true;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const onPause = () => {
|
|
77
|
-
playing.value = false;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const onLoaded = () => {
|
|
81
|
-
loaded.value = true;
|
|
82
|
-
};
|
|
83
|
-
</script>
|
|
84
|
-
|
|
85
|
-
<template>
|
|
86
|
-
<video
|
|
87
|
-
ref="video"
|
|
88
|
-
class="w-full h-full object-cover"
|
|
89
|
-
preload="none"
|
|
90
|
-
:poster="lazyPoster"
|
|
91
|
-
:src="allSources[0]"
|
|
92
|
-
:controls="controls"
|
|
93
|
-
:autoplay="autoplay"
|
|
94
|
-
:loop="loop"
|
|
95
|
-
:muted="muted"
|
|
96
|
-
playsinline
|
|
97
|
-
@play="onPlay"
|
|
98
|
-
@pause="onPause"
|
|
99
|
-
@loadeddata="onLoaded"
|
|
100
|
-
>
|
|
101
|
-
<source v-for="source in allSources" :src="source" :key="source" />
|
|
102
|
-
</video>
|
|
103
|
-
</template>
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { ContentNavigationItem } from "@nuxt/content";
|
|
3
|
-
|
|
4
|
-
const route = useRoute();
|
|
5
|
-
const navigationMap = inject<Ref<Record<string, ContentNavigationItem[]> | null>>("navigation");
|
|
6
|
-
|
|
7
|
-
const navigation = computed<ContentNavigationItem[]>(() => {
|
|
8
|
-
const section = findDocsSectionBySlug(route.params.section as string);
|
|
9
|
-
if (!section) return [];
|
|
10
|
-
return navigationMap?.value?.[section.key] ?? [];
|
|
11
|
-
});
|
|
12
|
-
</script>
|
|
13
|
-
|
|
14
|
-
<template>
|
|
15
|
-
<UContentNavigation :key="route.params.section as string" highlight :navigation="navigation">
|
|
16
|
-
<template #link-title="{ link }">
|
|
17
|
-
{{ link.title }}
|
|
18
|
-
</template>
|
|
19
|
-
</UContentNavigation>
|
|
20
|
-
</template>
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
const appConfig = useAppConfig();
|
|
3
|
-
const { t } = useDocusI18n();
|
|
4
|
-
</script>
|
|
5
|
-
|
|
6
|
-
<template>
|
|
7
|
-
<div v-if="appConfig.toc?.bottom?.links?.length" class="hidden lg:block space-y-6">
|
|
8
|
-
<USeparator type="dashed" />
|
|
9
|
-
|
|
10
|
-
<UPageLinks
|
|
11
|
-
:title="appConfig.toc?.bottom?.title || t('docs.links')"
|
|
12
|
-
:links="appConfig.toc?.bottom?.links"
|
|
13
|
-
/>
|
|
14
|
-
</div>
|
|
15
|
-
</template>
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { useClipboard } from "@vueuse/core";
|
|
3
|
-
|
|
4
|
-
const route = useRoute();
|
|
5
|
-
|
|
6
|
-
const { copy, copied } = useClipboard();
|
|
7
|
-
const { t } = useDocusI18n();
|
|
8
|
-
|
|
9
|
-
const markdownLink = computed(() => `${window?.location?.origin}/raw${route.path}.md`);
|
|
10
|
-
const items = [
|
|
11
|
-
{
|
|
12
|
-
label: t("docs.copy.link"),
|
|
13
|
-
icon: "i-lucide-link",
|
|
14
|
-
onSelect() {
|
|
15
|
-
copy(markdownLink.value);
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
label: t("docs.copy.view"),
|
|
20
|
-
icon: "i-simple-icons:markdown",
|
|
21
|
-
target: "_blank",
|
|
22
|
-
to: markdownLink.value,
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
label: t("docs.copy.gpt"),
|
|
26
|
-
icon: "i-simple-icons:openai",
|
|
27
|
-
target: "_blank",
|
|
28
|
-
to: `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${markdownLink.value} so I can ask questions about it.`)}`,
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
label: t("docs.copy.claude"),
|
|
32
|
-
icon: "i-simple-icons:anthropic",
|
|
33
|
-
target: "_blank",
|
|
34
|
-
to: `https://claude.ai/new?q=${encodeURIComponent(`Read ${markdownLink.value} so I can ask questions about it.`)}`,
|
|
35
|
-
},
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
async function copyPage() {
|
|
39
|
-
const page = await $fetch<string>(`/raw${route.path}.md`);
|
|
40
|
-
copy(page);
|
|
41
|
-
}
|
|
42
|
-
</script>
|
|
43
|
-
|
|
44
|
-
<template>
|
|
45
|
-
<UFieldGroup size="sm">
|
|
46
|
-
<UButton
|
|
47
|
-
:label="t('docs.copy.page')"
|
|
48
|
-
:icon="copied ? 'i-lucide-check' : 'i-lucide-copy'"
|
|
49
|
-
color="neutral"
|
|
50
|
-
variant="soft"
|
|
51
|
-
:ui="{
|
|
52
|
-
leadingIcon: 'text-neutral size-3.5',
|
|
53
|
-
}"
|
|
54
|
-
@click="copyPage"
|
|
55
|
-
/>
|
|
56
|
-
|
|
57
|
-
<UDropdownMenu
|
|
58
|
-
size="sm"
|
|
59
|
-
:items="items"
|
|
60
|
-
:content="{
|
|
61
|
-
align: 'end',
|
|
62
|
-
side: 'bottom',
|
|
63
|
-
sideOffset: 8,
|
|
64
|
-
}"
|
|
65
|
-
>
|
|
66
|
-
<UButton
|
|
67
|
-
icon="i-lucide-chevron-down"
|
|
68
|
-
color="neutral"
|
|
69
|
-
variant="soft"
|
|
70
|
-
class="border-l border-muted"
|
|
71
|
-
:aria-label="t('docs.copy.options')"
|
|
72
|
-
/>
|
|
73
|
-
</UDropdownMenu>
|
|
74
|
-
</UFieldGroup>
|
|
75
|
-
</template>
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import type { ContentNavigationItem } from "@nuxt/content";
|
|
2
|
-
|
|
3
|
-
export type DocsSectionLink = {
|
|
4
|
-
label: string;
|
|
5
|
-
icon: string;
|
|
6
|
-
slug: string;
|
|
7
|
-
to: string;
|
|
8
|
-
active: boolean;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
function findFirstLeafPath(items: ContentNavigationItem[] | undefined): string | undefined {
|
|
12
|
-
if (!items?.length) return undefined;
|
|
13
|
-
for (const item of items) {
|
|
14
|
-
if (item.page !== false && item.path) return item.path;
|
|
15
|
-
if (item.children?.length) {
|
|
16
|
-
const nested = findFirstLeafPath(item.children);
|
|
17
|
-
if (nested) return nested;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return items[0]?.path;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Flattens the consumer's `DOCS_SECTIONS` descriptor into the sub-header link
|
|
25
|
-
* list, resolving each section's first content leaf as its landing target.
|
|
26
|
-
* `DOCS_SECTIONS` / `findDocsSectionBySlug` are auto-imported from the
|
|
27
|
-
* consumer's `app/constants/` directory (declared via `imports.dirs`).
|
|
28
|
-
*/
|
|
29
|
-
export function useDocsSections() {
|
|
30
|
-
const route = useRoute();
|
|
31
|
-
const navigation = inject<Ref<Record<string, ContentNavigationItem[]> | null>>("navigation");
|
|
32
|
-
|
|
33
|
-
const sections = computed<DocsSectionLink[]>(() =>
|
|
34
|
-
DOCS_SECTIONS.map((section): DocsSectionLink => {
|
|
35
|
-
const tree = navigation?.value?.[section.key];
|
|
36
|
-
const firstPath = findFirstLeafPath(tree);
|
|
37
|
-
return {
|
|
38
|
-
label: section.label,
|
|
39
|
-
icon: section.icon,
|
|
40
|
-
slug: section.slug,
|
|
41
|
-
to: firstPath || `/docs/${section.slug}`,
|
|
42
|
-
active: route.params.section === section.slug,
|
|
43
|
-
};
|
|
44
|
-
}),
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
const activeSection = computed(() => sections.value.find((section) => section.active));
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Whether the section switcher is worth rendering. A single section (or none)
|
|
51
|
-
* gives the reader nothing to switch between, so the sub-header is suppressed
|
|
52
|
-
* entirely rather than rendering an empty bar.
|
|
53
|
-
*/
|
|
54
|
-
const hasSectionSwitcher = computed(() => sections.value.length > 1);
|
|
55
|
-
|
|
56
|
-
return { sections, activeSection, hasSectionSwitcher };
|
|
57
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import type { LocaleObject } from "@nuxtjs/i18n";
|
|
2
|
-
import type { Ref } from "vue";
|
|
3
|
-
import en from "../../i18n/locales/en.json";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* `@nuxtjs/i18n` auto-imports. This layer ships with i18n disabled by default —
|
|
7
|
-
* the module is not registered unless the consumer opts in — so the enabled
|
|
8
|
-
* branch below is inert until then. These declarations let the file typecheck
|
|
9
|
-
* without the module generating the globals; when i18n is enabled (the consumer
|
|
10
|
-
* registers `@nuxtjs/i18n`) the real composables are used.
|
|
11
|
-
*/
|
|
12
|
-
declare function useI18n(): { locale: Ref<string>; t: (key: string) => string };
|
|
13
|
-
declare function useLocalePath(): (route: string, locale?: string) => string;
|
|
14
|
-
declare function useSwitchLocalePath(): (locale: string) => string;
|
|
15
|
-
|
|
16
|
-
export const useDocusI18n = () => {
|
|
17
|
-
const config = useRuntimeConfig().public;
|
|
18
|
-
const isEnabled = ref(!!config.i18n);
|
|
19
|
-
|
|
20
|
-
if (!isEnabled.value) {
|
|
21
|
-
return {
|
|
22
|
-
isEnabled,
|
|
23
|
-
locale: ref("en"),
|
|
24
|
-
locales: [],
|
|
25
|
-
localePath: (path: string) => path,
|
|
26
|
-
switchLocalePath: (_locale?: string): string => "",
|
|
27
|
-
t: (key: string): string => {
|
|
28
|
-
const path = key.split(".");
|
|
29
|
-
return path.reduce(
|
|
30
|
-
(acc: unknown, curr) => (acc as Record<string, unknown>)?.[curr],
|
|
31
|
-
en,
|
|
32
|
-
) as string;
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const { locale, t } = useI18n();
|
|
38
|
-
const filteredLocales =
|
|
39
|
-
(config.docus as { filteredLocales: LocaleObject<string>[] })?.filteredLocales || [];
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
isEnabled,
|
|
43
|
-
locale,
|
|
44
|
-
locales: filteredLocales,
|
|
45
|
-
t,
|
|
46
|
-
localePath: useLocalePath(),
|
|
47
|
-
switchLocalePath: useSwitchLocalePath(),
|
|
48
|
-
};
|
|
49
|
-
};
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { DocsSectionDescriptor } from "../../utils/content";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Section descriptor as consumed by the theme's runtime nav/search components.
|
|
5
|
-
* Extends the build-time {@link DocsSectionDescriptor} with the UI-only fields
|
|
6
|
-
* (`icon`) those components read.
|
|
7
|
-
*/
|
|
8
|
-
export interface DocsSectionItem extends DocsSectionDescriptor {
|
|
9
|
-
/** Icon name (e.g. `i-lucide-book-open`) shown next to the section link. */
|
|
10
|
-
icon: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Neutral default topology: no sections. The consuming app supplies the real
|
|
15
|
-
* `DOCS_SECTIONS` by placing its own `app/constants/sections.ts`; Nuxt
|
|
16
|
-
* auto-import precedence (consumer app dir > layer dir) makes the consumer's
|
|
17
|
-
* array win at build time. Shipping an empty default here keeps the layer's own
|
|
18
|
-
* type-check and build green in isolation without leaking any brand topology.
|
|
19
|
-
*/
|
|
20
|
-
export const DOCS_SECTIONS: readonly DocsSectionItem[] = [];
|
|
21
|
-
|
|
22
|
-
/** Resolve a section by its URL slug, or `undefined` when none matches. */
|
|
23
|
-
export function findDocsSectionBySlug(slug: string): DocsSectionItem | undefined {
|
|
24
|
-
return DOCS_SECTIONS.find((section) => section.slug === slug);
|
|
25
|
-
}
|
package/app/error.vue
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { NuxtError } from "#app";
|
|
3
|
-
import type { ContentNavigationItem, PageCollections } from "@nuxt/content";
|
|
4
|
-
import * as nuxtUiLocales from "@nuxt/ui/locale";
|
|
5
|
-
|
|
6
|
-
const props = defineProps<{
|
|
7
|
-
error: NuxtError;
|
|
8
|
-
}>();
|
|
9
|
-
|
|
10
|
-
const { locale, locales, isEnabled, t, switchLocalePath } = useDocusI18n();
|
|
11
|
-
const appConfig = useAppConfig();
|
|
12
|
-
|
|
13
|
-
const lang = computed(
|
|
14
|
-
() => nuxtUiLocales[locale.value as keyof typeof nuxtUiLocales]?.code || "en",
|
|
15
|
-
);
|
|
16
|
-
const dir = computed(() => nuxtUiLocales[locale.value as keyof typeof nuxtUiLocales]?.dir || "ltr");
|
|
17
|
-
useHead({
|
|
18
|
-
htmlAttrs: {
|
|
19
|
-
lang,
|
|
20
|
-
dir,
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const localizedError = computed(() => {
|
|
25
|
-
return {
|
|
26
|
-
...props.error,
|
|
27
|
-
statusMessage: t("common.error.title"),
|
|
28
|
-
};
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
useSeoMeta({
|
|
32
|
-
title: () => t("common.error.title"),
|
|
33
|
-
description: () => t("common.error.description"),
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
if (isEnabled.value) {
|
|
37
|
-
const route = useRoute();
|
|
38
|
-
const defaultLocale = (useRuntimeConfig().public.i18n as { defaultLocale: string }).defaultLocale;
|
|
39
|
-
onMounted(() => {
|
|
40
|
-
const currentLocale = route.path.split("/")[1];
|
|
41
|
-
if (!locales.some((locale) => locale.code === currentLocale)) {
|
|
42
|
-
return navigateTo(switchLocalePath(defaultLocale) as string);
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const getCollectionName = (key: string) =>
|
|
48
|
-
(isEnabled.value ? `docs_${key}_${locale.value}` : `docs_${key}`) as keyof PageCollections;
|
|
49
|
-
|
|
50
|
-
const { data: navigation } = await useAsyncData(
|
|
51
|
-
() => `navigation_${locale.value}`,
|
|
52
|
-
async () => {
|
|
53
|
-
const results = await Promise.all(
|
|
54
|
-
DOCS_SECTIONS.map(async (section) => {
|
|
55
|
-
const collectionName = getCollectionName(section.key);
|
|
56
|
-
const data = await queryCollectionNavigation(collectionName);
|
|
57
|
-
const rootResult =
|
|
58
|
-
data.find((item) => item.path === `/docs/${section.slug}`)?.children ||
|
|
59
|
-
data.find((item) => item.path === "/docs")?.children ||
|
|
60
|
-
data ||
|
|
61
|
-
[];
|
|
62
|
-
const localeResult =
|
|
63
|
-
rootResult.find((item) => item.path === `/${locale.value}`)?.children || rootResult;
|
|
64
|
-
const sectionPath = `/docs/${section.slug}`;
|
|
65
|
-
let result: ContentNavigationItem[];
|
|
66
|
-
if (Array.isArray(section.folder)) {
|
|
67
|
-
const rootIdx = "rootFolder" in section ? (section.rootFolder as number) : -1;
|
|
68
|
-
const rootWrapper =
|
|
69
|
-
localeResult.length === 1 && localeResult[0]?.path === sectionPath
|
|
70
|
-
? localeResult[0]
|
|
71
|
-
: null;
|
|
72
|
-
const allItems = rootWrapper ? (rootWrapper.children ?? []) : localeResult;
|
|
73
|
-
|
|
74
|
-
if (rootIdx >= 0) {
|
|
75
|
-
const nonRootPaths = new Set(
|
|
76
|
-
section.folder
|
|
77
|
-
.filter((_: string, i: number) => i !== rootIdx)
|
|
78
|
-
.map((f: string) => `${sectionPath}/${f.replace(/^\d+\./, "")}`),
|
|
79
|
-
);
|
|
80
|
-
const items: ContentNavigationItem[] = [];
|
|
81
|
-
for (let i = 0; i < section.folder.length; i++) {
|
|
82
|
-
if (i === rootIdx) {
|
|
83
|
-
items.push({
|
|
84
|
-
...rootWrapper,
|
|
85
|
-
title: rootWrapper?.title ?? section.label,
|
|
86
|
-
path: sectionPath,
|
|
87
|
-
children: allItems.filter((item) => !nonRootPaths.has(item.path)),
|
|
88
|
-
} as ContentNavigationItem);
|
|
89
|
-
} else {
|
|
90
|
-
const found = allItems.find(
|
|
91
|
-
(item) =>
|
|
92
|
-
item.path === `${sectionPath}/${section.folder[i]?.replace(/^\d+\./, "")}`,
|
|
93
|
-
);
|
|
94
|
-
if (found) items.push(found);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
result = items;
|
|
98
|
-
} else {
|
|
99
|
-
result = section.folder
|
|
100
|
-
.map((folder: string) =>
|
|
101
|
-
allItems.find(
|
|
102
|
-
(item) => item.path === `${sectionPath}/${folder.replace(/^\d+\./, "")}`,
|
|
103
|
-
),
|
|
104
|
-
)
|
|
105
|
-
.filter((item): item is ContentNavigationItem => item !== undefined);
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
result = localeResult;
|
|
109
|
-
}
|
|
110
|
-
return [
|
|
111
|
-
section.key,
|
|
112
|
-
foldNonRouteCategories(flattenNavigation(result), appConfig.nonRouteCategories ?? {}),
|
|
113
|
-
] as const;
|
|
114
|
-
}),
|
|
115
|
-
);
|
|
116
|
-
return Object.fromEntries(results) as Record<string, ContentNavigationItem[]>;
|
|
117
|
-
},
|
|
118
|
-
{ watch: [locale] },
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
const flatNavigation = computed(() =>
|
|
122
|
-
navigation.value ? Object.values(navigation.value).flat() : [],
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
provide("navigation", navigation);
|
|
126
|
-
</script>
|
|
127
|
-
|
|
128
|
-
<template>
|
|
129
|
-
<UApp>
|
|
130
|
-
<AppHeader />
|
|
131
|
-
|
|
132
|
-
<UError :error="localizedError" />
|
|
133
|
-
|
|
134
|
-
<AppFooter />
|
|
135
|
-
|
|
136
|
-
<ClientOnly>
|
|
137
|
-
<AppSearch :navigation="flatNavigation" />
|
|
138
|
-
</ClientOnly>
|
|
139
|
-
</UApp>
|
|
140
|
-
</template>
|
package/app/layouts/default.vue
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
const route = useRoute();
|
|
3
|
-
const isDocs = computed(() => route.path.startsWith("/docs"));
|
|
4
|
-
</script>
|
|
5
|
-
|
|
6
|
-
<template>
|
|
7
|
-
<AppHeader />
|
|
8
|
-
<UMain>
|
|
9
|
-
<slot v-if="!isDocs" />
|
|
10
|
-
<UContainer v-else>
|
|
11
|
-
<UPage>
|
|
12
|
-
<template #left>
|
|
13
|
-
<UPageAside>
|
|
14
|
-
<DocsAsideLeftTop />
|
|
15
|
-
<DocsAsideLeftBody />
|
|
16
|
-
</UPageAside>
|
|
17
|
-
</template>
|
|
18
|
-
|
|
19
|
-
<slot />
|
|
20
|
-
</UPage>
|
|
21
|
-
</UContainer>
|
|
22
|
-
</UMain>
|
|
23
|
-
<AppFooter />
|
|
24
|
-
</template>
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { Collections } from "@nuxt/content";
|
|
3
|
-
import { kebabCase } from "scule";
|
|
4
|
-
|
|
5
|
-
definePageMeta({
|
|
6
|
-
layout: "default",
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
const route = useRoute();
|
|
10
|
-
const { locale, isEnabled } = useDocusI18n();
|
|
11
|
-
|
|
12
|
-
// Dynamic collection name based on i18n status
|
|
13
|
-
const collectionName = computed(() => (isEnabled.value ? `landing_${locale.value}` : "landing"));
|
|
14
|
-
|
|
15
|
-
const { data: page } = await useAsyncData(kebabCase(route.path), () =>
|
|
16
|
-
queryCollection(collectionName.value as keyof Collections)
|
|
17
|
-
.path(route.path)
|
|
18
|
-
.first(),
|
|
19
|
-
);
|
|
20
|
-
if (!page.value) {
|
|
21
|
-
throw createError({
|
|
22
|
-
statusCode: 404,
|
|
23
|
-
statusMessage: "Page not found",
|
|
24
|
-
fatal: true,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const title = page.value.seo?.title || page.value.title;
|
|
29
|
-
const description = page.value.seo?.description || page.value.description;
|
|
30
|
-
|
|
31
|
-
useSeoMeta({
|
|
32
|
-
title,
|
|
33
|
-
description,
|
|
34
|
-
ogTitle: title,
|
|
35
|
-
ogDescription: description,
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const appConfig = useAppConfig();
|
|
39
|
-
|
|
40
|
-
// An explicit `seo.ogImage` in the page's front matter wins; otherwise the card
|
|
41
|
-
// is generated from the shared template.
|
|
42
|
-
if (page.value?.seo?.ogImage) {
|
|
43
|
-
useSeoMeta({
|
|
44
|
-
ogImage: page.value.seo.ogImage,
|
|
45
|
-
twitterImage: page.value.seo.ogImage,
|
|
46
|
-
});
|
|
47
|
-
} else {
|
|
48
|
-
defineOgImage("DocsSatori", {
|
|
49
|
-
headline: appConfig.header?.title,
|
|
50
|
-
title,
|
|
51
|
-
description,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
</script>
|
|
55
|
-
|
|
56
|
-
<template>
|
|
57
|
-
<ContentRenderer v-if="page" :value="page" />
|
|
58
|
-
</template>
|