@uxfront/layer-docs 0.4.1 → 0.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.
- package/README.md +42 -213
- package/app/app.config.ts +8 -92
- package/app/components/app/AppHeaderLeft.vue +84 -0
- 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 +43 -38
- package/nuxt.config.ts +16 -170
- package/package.json +11 -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,180 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { kebabCase } from "scule";
|
|
3
|
-
import type { ContentNavigationItem, Collections, PageCollectionItemBase } from "@nuxt/content";
|
|
4
|
-
|
|
5
|
-
type DocsPageItem = PageCollectionItemBase & {
|
|
6
|
-
links?: Array<{
|
|
7
|
-
label: string;
|
|
8
|
-
icon: string;
|
|
9
|
-
to: string;
|
|
10
|
-
target?: string;
|
|
11
|
-
}>;
|
|
12
|
-
seo?: { title?: string; description?: string };
|
|
13
|
-
};
|
|
14
|
-
import { findPageHeadline } from "@nuxt/content/utils";
|
|
15
|
-
|
|
16
|
-
definePageMeta({
|
|
17
|
-
layout: "default",
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const route = useRoute();
|
|
21
|
-
const { locale, isEnabled, t } = useDocusI18n();
|
|
22
|
-
const appConfig = useAppConfig();
|
|
23
|
-
const navigationMap = inject<Ref<Record<string, ContentNavigationItem[]>>>("navigation");
|
|
24
|
-
|
|
25
|
-
const sectionSlug = computed(() => route.params.section as string);
|
|
26
|
-
const section = computed(() => findDocsSectionBySlug(sectionSlug.value));
|
|
27
|
-
|
|
28
|
-
if (!section.value) {
|
|
29
|
-
throw createError({
|
|
30
|
-
statusCode: 404,
|
|
31
|
-
statusMessage: "Page not found",
|
|
32
|
-
fatal: true,
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const collectionName = computed(() =>
|
|
37
|
-
isEnabled.value ? `docs_${section.value!.key}_${locale.value}` : `docs_${section.value!.key}`,
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
const navigation = computed(() => navigationMap?.value?.[section.value!.key] ?? []);
|
|
41
|
-
|
|
42
|
-
const [{ data: page }, { data: surround }] = await Promise.all([
|
|
43
|
-
useAsyncData(
|
|
44
|
-
kebabCase(route.path),
|
|
45
|
-
() =>
|
|
46
|
-
queryCollection(collectionName.value as keyof Collections)
|
|
47
|
-
.path(route.path)
|
|
48
|
-
.first() as Promise<DocsPageItem>,
|
|
49
|
-
),
|
|
50
|
-
useAsyncData(`${kebabCase(route.path)}-surround`, () => {
|
|
51
|
-
return queryCollectionItemSurroundings(collectionName.value as keyof Collections, route.path, {
|
|
52
|
-
fields: ["description"],
|
|
53
|
-
});
|
|
54
|
-
}),
|
|
55
|
-
]);
|
|
56
|
-
|
|
57
|
-
if (!page.value) {
|
|
58
|
-
throw createError({
|
|
59
|
-
statusCode: 404,
|
|
60
|
-
statusMessage: "Page not found",
|
|
61
|
-
fatal: true,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Add the page path to the prerender list
|
|
66
|
-
addPrerenderPath(`/raw${route.path}.md`);
|
|
67
|
-
|
|
68
|
-
const title = page.value.seo?.title || page.value.title;
|
|
69
|
-
const description = page.value.seo?.description || page.value.description;
|
|
70
|
-
|
|
71
|
-
useSeoMeta({
|
|
72
|
-
title,
|
|
73
|
-
ogTitle: title,
|
|
74
|
-
description,
|
|
75
|
-
ogDescription: description,
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const headline = ref(findPageHeadline(navigation.value, page.value?.path));
|
|
79
|
-
watch(
|
|
80
|
-
() => navigation.value,
|
|
81
|
-
() => {
|
|
82
|
-
headline.value = findPageHeadline(navigation.value, page.value?.path) || headline.value;
|
|
83
|
-
},
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
defineOgImage("DocsSatori", {
|
|
87
|
-
headline: headline.value,
|
|
88
|
-
title,
|
|
89
|
-
description,
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const github = computed(() => (appConfig.github ? appConfig.github : null));
|
|
93
|
-
|
|
94
|
-
const editLink = computed(() => {
|
|
95
|
-
if (!github.value) {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return [
|
|
100
|
-
github.value.url,
|
|
101
|
-
"edit",
|
|
102
|
-
github.value.branch,
|
|
103
|
-
github.value.rootDir,
|
|
104
|
-
"content",
|
|
105
|
-
`${page.value?.stem}.${page.value?.extension}`,
|
|
106
|
-
]
|
|
107
|
-
.filter(Boolean)
|
|
108
|
-
.join("/");
|
|
109
|
-
});
|
|
110
|
-
</script>
|
|
111
|
-
|
|
112
|
-
<template>
|
|
113
|
-
<UPage v-if="page">
|
|
114
|
-
<UPageHeader
|
|
115
|
-
:title="page.title"
|
|
116
|
-
:description="page.description"
|
|
117
|
-
:headline="headline"
|
|
118
|
-
:ui="{
|
|
119
|
-
wrapper: 'flex-row items-center flex-wrap justify-between',
|
|
120
|
-
}"
|
|
121
|
-
>
|
|
122
|
-
<template #links>
|
|
123
|
-
<UButton
|
|
124
|
-
v-for="(link, index) in (page as DocsPageItem).links"
|
|
125
|
-
:key="index"
|
|
126
|
-
size="sm"
|
|
127
|
-
v-bind="link"
|
|
128
|
-
/>
|
|
129
|
-
|
|
130
|
-
<DocsPageHeaderLinks />
|
|
131
|
-
</template>
|
|
132
|
-
</UPageHeader>
|
|
133
|
-
|
|
134
|
-
<UPageBody>
|
|
135
|
-
<ContentRenderer v-if="page" :value="page" />
|
|
136
|
-
|
|
137
|
-
<!-- `decorative` drops the `separator` role: the rule is decoration around
|
|
138
|
-
the edit/report actions, and a separator with focusable descendants is
|
|
139
|
-
a `nested-interactive` violation. -->
|
|
140
|
-
<USeparator decorative>
|
|
141
|
-
<div v-if="github" class="flex items-center gap-2 text-sm text-muted">
|
|
142
|
-
<UButton
|
|
143
|
-
variant="link"
|
|
144
|
-
color="neutral"
|
|
145
|
-
:to="editLink"
|
|
146
|
-
target="_blank"
|
|
147
|
-
icon="i-lucide-pen"
|
|
148
|
-
:ui="{ leadingIcon: 'size-4' }"
|
|
149
|
-
>
|
|
150
|
-
{{ t("docs.edit") }}
|
|
151
|
-
</UButton>
|
|
152
|
-
<span>{{ t("common.or") }}</span>
|
|
153
|
-
<UButton
|
|
154
|
-
variant="link"
|
|
155
|
-
color="neutral"
|
|
156
|
-
:to="`${github.url}/issues/new/choose`"
|
|
157
|
-
target="_blank"
|
|
158
|
-
icon="i-lucide-alert-circle"
|
|
159
|
-
:ui="{ leadingIcon: 'size-4' }"
|
|
160
|
-
>
|
|
161
|
-
{{ t("docs.report") }}
|
|
162
|
-
</UButton>
|
|
163
|
-
</div>
|
|
164
|
-
</USeparator>
|
|
165
|
-
<UContentSurround :surround="surround" />
|
|
166
|
-
</UPageBody>
|
|
167
|
-
|
|
168
|
-
<template v-if="page?.body?.toc?.links?.length" #right>
|
|
169
|
-
<UContentToc
|
|
170
|
-
highlight
|
|
171
|
-
:title="appConfig.toc?.title || t('docs.toc')"
|
|
172
|
-
:links="page.body?.toc?.links"
|
|
173
|
-
>
|
|
174
|
-
<template #bottom>
|
|
175
|
-
<DocsAsideRightBottom />
|
|
176
|
-
</template>
|
|
177
|
-
</UContentToc>
|
|
178
|
-
</template>
|
|
179
|
-
</UPage>
|
|
180
|
-
</template>
|
package/app/plugins/i18n.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export default defineNuxtPlugin(() => {
|
|
2
|
-
// Consumers opt out via `app.config` (`i18nRedirect.enabled: false`).
|
|
3
|
-
if (!useAppConfig().i18nRedirect?.enabled) {
|
|
4
|
-
return;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
const nuxtApp = useNuxtApp();
|
|
8
|
-
|
|
9
|
-
const i18nConfig = nuxtApp.$config.public.i18n as { defaultLocale?: string } | undefined;
|
|
10
|
-
if (!i18nConfig) {
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
addRouteMiddleware((to) => {
|
|
15
|
-
if (to.path === "/") {
|
|
16
|
-
const cookieLocale = useCookie("i18n_redirected").value || i18nConfig.defaultLocale || "en";
|
|
17
|
-
|
|
18
|
-
return navigateTo(`/${cookieLocale}`);
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
});
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import type { ConfigDefaults } from "posthog-js";
|
|
2
|
-
import { defineNuxtPlugin } from "#app";
|
|
3
|
-
|
|
4
|
-
export default defineNuxtPlugin(async () => {
|
|
5
|
-
if (import.meta.dev) {
|
|
6
|
-
return {};
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
// Consumers opt out via `app.config` (`analytics.enabled: false`).
|
|
10
|
-
if (!useAppConfig().analytics?.enabled) {
|
|
11
|
-
return {};
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const runtimeConfig = useRuntimeConfig();
|
|
15
|
-
|
|
16
|
-
// No key provisioned (`NUXT_PUBLIC_POSTHOG_KEY` unset) — no-op instead of
|
|
17
|
-
// initialising a client that would fire requests at nothing.
|
|
18
|
-
if (!runtimeConfig.public.posthog.key) {
|
|
19
|
-
return {};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// `posthog-js` is an OPTIONAL peer dependency. A static top-level import
|
|
23
|
-
// compiles to a module-scope `throw` for every consumer that omits it, and
|
|
24
|
-
// that throw runs before any of the guards above — killing the entire client
|
|
25
|
-
// bundle, not just analytics, so the site serves as a dead static document.
|
|
26
|
-
// Importing here keeps the unresolvable specifier behind the opt-in, and the
|
|
27
|
-
// catch keeps a missing or broken module a no-analytics degradation rather
|
|
28
|
-
// than a dead app.
|
|
29
|
-
let posthog;
|
|
30
|
-
try {
|
|
31
|
-
({ default: posthog } = await import("posthog-js"));
|
|
32
|
-
} catch (error) {
|
|
33
|
-
console.warn(
|
|
34
|
-
"[layer-docs] analytics is enabled and a PostHog key is set, but " +
|
|
35
|
-
"`posthog-js` could not be loaded — continuing without analytics. " +
|
|
36
|
-
"Install `posthog-js` to enable it.",
|
|
37
|
-
error,
|
|
38
|
-
);
|
|
39
|
-
return {};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const posthogClient = posthog.init(runtimeConfig.public.posthog.key, {
|
|
43
|
-
api_host: runtimeConfig.public.posthog.host,
|
|
44
|
-
defaults: runtimeConfig.public.posthog.defaults as ConfigDefaults,
|
|
45
|
-
person_profiles: "identified_only", // or 'always' to create profiles for anonymous users as well
|
|
46
|
-
loaded: (posthog) => {
|
|
47
|
-
if (import.meta.env.MODE === "development") posthog.debug();
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
provide: {
|
|
53
|
-
posthog: () => posthogClient,
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
});
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export interface NonRouteCategoryMeta {
|
|
2
|
-
title: string;
|
|
3
|
-
icon?: string | false;
|
|
4
|
-
defaultOpen?: boolean;
|
|
5
|
-
order: number;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
declare module "nuxt/schema" {
|
|
9
|
-
interface AppConfig {
|
|
10
|
-
nonRouteCategories?: Record<string, NonRouteCategoryMeta>;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { ContentNavigationItem } from "@nuxt/content";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Flatten navigation items that are false-positive groups.
|
|
5
|
-
* Nuxt Content's index detection regex matches filenames containing "index"
|
|
6
|
-
* as a substring (e.g. "z-index"), incorrectly nesting them as groups.
|
|
7
|
-
* This flattens groups with a single child sharing the same path.
|
|
8
|
-
*/
|
|
9
|
-
export function flattenNavigation(items: ContentNavigationItem[]): ContentNavigationItem[] {
|
|
10
|
-
return items.map((item) => {
|
|
11
|
-
if (item.children?.length === 1 && item.children[0]?.path === item.path) {
|
|
12
|
-
const { children: _, ...parent } = item;
|
|
13
|
-
return { ...parent, ...item.children[0], children: undefined };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (item.children?.length) {
|
|
17
|
-
return { ...item, children: flattenNavigation(item.children) };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return item;
|
|
21
|
-
});
|
|
22
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { ContentNavigationItem } from "@nuxt/content";
|
|
2
|
-
import type { NonRouteCategoryMeta } from "~/types/non-route-categories";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Rebuilds the sidebar grouping for non-route categories.
|
|
6
|
-
*
|
|
7
|
-
* Pages under a `route: false` folder are flattened in the URL, so the native
|
|
8
|
-
* navigation tree lists them as direct children with the category level gone.
|
|
9
|
-
* This re-nests each such page under a synthetic group node (rendered as a
|
|
10
|
-
* non-navigating accordion header), matching pages to categories by `stem`.
|
|
11
|
-
*/
|
|
12
|
-
export function foldNonRouteCategories(
|
|
13
|
-
items: ContentNavigationItem[],
|
|
14
|
-
categories: Record<string, NonRouteCategoryMeta>,
|
|
15
|
-
): ContentNavigationItem[] {
|
|
16
|
-
return items.map((node) => {
|
|
17
|
-
if (!node.children?.length) {
|
|
18
|
-
return node;
|
|
19
|
-
}
|
|
20
|
-
const passthrough: ContentNavigationItem[] = [];
|
|
21
|
-
const groups = new Map<
|
|
22
|
-
string,
|
|
23
|
-
{ meta: NonRouteCategoryMeta; children: ContentNavigationItem[] }
|
|
24
|
-
>();
|
|
25
|
-
for (const child of node.children) {
|
|
26
|
-
const dir = child.stem ? child.stem.split("/").slice(0, -1).join("/") : "";
|
|
27
|
-
const meta = categories[dir];
|
|
28
|
-
if (meta && child.page !== false) {
|
|
29
|
-
const group = groups.get(dir) ?? { meta, children: [] };
|
|
30
|
-
group.children.push(child);
|
|
31
|
-
groups.set(dir, group);
|
|
32
|
-
} else {
|
|
33
|
-
passthrough.push(foldNonRouteCategories([child], categories)[0]!);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
const groupNodes = [...groups.values()]
|
|
37
|
-
.sort((a, b) => a.meta.order - b.meta.order || a.meta.title.localeCompare(b.meta.title))
|
|
38
|
-
.map(({ meta, children }) => ({
|
|
39
|
-
title: meta.title,
|
|
40
|
-
...(meta.icon ? { icon: meta.icon } : {}),
|
|
41
|
-
...(meta.defaultOpen !== undefined ? { defaultOpen: meta.defaultOpen } : {}),
|
|
42
|
-
page: false as const,
|
|
43
|
-
children,
|
|
44
|
-
})) as ContentNavigationItem[];
|
|
45
|
-
return { ...node, children: [...passthrough, ...groupNodes] };
|
|
46
|
-
});
|
|
47
|
-
}
|
package/app/utils/prerender.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
buildStorybookEmbedUrl,
|
|
4
|
-
resolveStorybookBaseUrl,
|
|
5
|
-
STORYBOOK_EMBED_DEFAULT_HEIGHTS,
|
|
6
|
-
storybookEmbedCssHeight,
|
|
7
|
-
} from "./storybookEmbed";
|
|
8
|
-
|
|
9
|
-
const TEMPLATE = "https://{framework}.storybook.example.com";
|
|
10
|
-
const STORY = "components-actions-button--default";
|
|
11
|
-
|
|
12
|
-
describe("resolveStorybookBaseUrl", () => {
|
|
13
|
-
it("substitutes every occurrence of the framework placeholder", () => {
|
|
14
|
-
expect(resolveStorybookBaseUrl("https://{framework}.example.com/{framework}", "vue")).toBe(
|
|
15
|
-
"https://vue.example.com/vue",
|
|
16
|
-
);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("passes a template without a placeholder through, for a single Storybook", () => {
|
|
20
|
-
expect(resolveStorybookBaseUrl("https://storybook.example.com", "vue")).toBe(
|
|
21
|
-
"https://storybook.example.com",
|
|
22
|
-
);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("trims trailing slashes so the built URL never doubles them", () => {
|
|
26
|
-
expect(resolveStorybookBaseUrl("https://storybook.example.com//")).toBe(
|
|
27
|
-
"https://storybook.example.com",
|
|
28
|
-
);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe("buildStorybookEmbedUrl", () => {
|
|
33
|
-
it("addresses the preview surface by default and marks itself as a direct embed", () => {
|
|
34
|
-
expect(buildStorybookEmbedUrl({ template: TEMPLATE, story: STORY, framework: "vue" })).toBe(
|
|
35
|
-
`https://vue.storybook.example.com/iframe.html?id=${STORY}&viewMode=story&docsEmbed=1`,
|
|
36
|
-
);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// `full` and `panel` reproduce the manager URLs the migrating docs site
|
|
40
|
-
// already ships; a changed parameter here is a changed rendering there.
|
|
41
|
-
it("addresses the manager in full mode", () => {
|
|
42
|
-
expect(
|
|
43
|
-
buildStorybookEmbedUrl({ template: TEMPLATE, story: STORY, framework: "vue", mode: "full" }),
|
|
44
|
-
).toBe(
|
|
45
|
-
`https://vue.storybook.example.com/?path=/story/${STORY}&full=1&shortcuts=false&singleStory=true`,
|
|
46
|
-
);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("addresses the manager with its addon panel in panel mode", () => {
|
|
50
|
-
expect(
|
|
51
|
-
buildStorybookEmbedUrl({ template: TEMPLATE, story: STORY, framework: "vue", mode: "panel" }),
|
|
52
|
-
).toBe(
|
|
53
|
-
`https://vue.storybook.example.com/?path=/story/${STORY}&shortcuts=false&singleStory=true`,
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("only marks the preview surface — a manager embed is relayed, not direct", () => {
|
|
58
|
-
for (const mode of ["full", "panel"] as const) {
|
|
59
|
-
expect(buildStorybookEmbedUrl({ template: TEMPLATE, story: STORY, mode })).not.toContain(
|
|
60
|
-
"docsEmbed",
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("encodes the story id rather than splicing it into the query raw", () => {
|
|
66
|
-
expect(
|
|
67
|
-
buildStorybookEmbedUrl({ template: "https://sb.example.com", story: "a&b=c" }),
|
|
68
|
-
).toContain("id=a%26b%3Dc&");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("degrades to a same-origin URL when no consumer configured a host", () => {
|
|
72
|
-
expect(buildStorybookEmbedUrl({ template: "", story: STORY })).toBe(
|
|
73
|
-
`/iframe.html?id=${STORY}&viewMode=story&docsEmbed=1`,
|
|
74
|
-
);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe("STORYBOOK_EMBED_DEFAULT_HEIGHTS", () => {
|
|
79
|
-
// Reserved before the story reports anything — the space that keeps the embed
|
|
80
|
-
// from shifting the page. Panel carries the addon panel on top of the story.
|
|
81
|
-
it("reserves more room for the panel surface than the bare story", () => {
|
|
82
|
-
expect(STORYBOOK_EMBED_DEFAULT_HEIGHTS).toEqual({ preview: 320, full: 320, panel: 600 });
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe("storybookEmbedCssHeight", () => {
|
|
87
|
-
it("reads a bare number as pixels, in either type markdown may deliver", () => {
|
|
88
|
-
expect(storybookEmbedCssHeight(420)).toBe("420px");
|
|
89
|
-
expect(storybookEmbedCssHeight("420")).toBe("420px");
|
|
90
|
-
expect(storybookEmbedCssHeight(" 420 ")).toBe("420px");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("passes a CSS length through untouched", () => {
|
|
94
|
-
expect(storybookEmbedCssHeight("30rem")).toBe("30rem");
|
|
95
|
-
expect(storybookEmbedCssHeight("420px")).toBe("420px");
|
|
96
|
-
expect(storybookEmbedCssHeight("min(80vh, 600px)")).toBe("min(80vh, 600px)");
|
|
97
|
-
});
|
|
98
|
-
});
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { DOCS_EMBED_PARAM } from "../../storybook";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Which Storybook surface a `StorybookEmbed` addresses.
|
|
5
|
-
*
|
|
6
|
-
* The three values are not styling choices — each is a different URL against a
|
|
7
|
-
* different Storybook surface, and the height signal reaches the page by a
|
|
8
|
-
* different route in each. See `@uxfront/layer-docs/storybook`.
|
|
9
|
-
*
|
|
10
|
-
* - `preview` — `iframe.html`, the story on its own with no Storybook UI.
|
|
11
|
-
* - `full` — the manager in full-canvas mode: story plus toolbar, no panel.
|
|
12
|
-
* - `panel` — the manager with its addon panel, for stories documented through
|
|
13
|
-
* controls or actions.
|
|
14
|
-
*/
|
|
15
|
-
export type StorybookEmbedMode = "preview" | "full" | "panel";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Height an embed occupies before Storybook reports its own, and the height it
|
|
19
|
-
* keeps if no bridge is installed on the Storybook side.
|
|
20
|
-
*
|
|
21
|
-
* The panel default is taller because the addon panel is a fixed cost on top of
|
|
22
|
-
* the story. Reserving the space up front is what keeps the embed from shifting
|
|
23
|
-
* the page when the real height arrives.
|
|
24
|
-
*/
|
|
25
|
-
export const STORYBOOK_EMBED_DEFAULT_HEIGHTS: Record<StorybookEmbedMode, number> = {
|
|
26
|
-
preview: 320,
|
|
27
|
-
full: 320,
|
|
28
|
-
panel: 600,
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Resolve a Storybook base URL from a template.
|
|
33
|
-
*
|
|
34
|
-
* A `{framework}` placeholder addresses a per-framework Storybook deployment
|
|
35
|
-
* (`https://{framework}.storybook.example.com`); a template without one is used
|
|
36
|
-
* as-is, so a single-Storybook site needs no placeholder.
|
|
37
|
-
*/
|
|
38
|
-
export function resolveStorybookBaseUrl(template: string, framework?: string): string {
|
|
39
|
-
return template.replaceAll("{framework}", framework ?? "").replace(/\/+$/, "");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface StorybookEmbedUrlOptions {
|
|
43
|
-
/** Base URL template, optionally containing `{framework}`. */
|
|
44
|
-
template: string;
|
|
45
|
-
/** Storybook story id, e.g. `components-actions-button--default`. */
|
|
46
|
-
story: string;
|
|
47
|
-
/** Framework substituted into the template's `{framework}` placeholder. */
|
|
48
|
-
framework?: string;
|
|
49
|
-
/** Storybook surface to address. Defaults to `preview`. */
|
|
50
|
-
mode?: StorybookEmbedMode;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Build the `src` for an embedded story.
|
|
55
|
-
*
|
|
56
|
-
* `preview` is marked with `docsEmbed=1`: embedded that way the docs page is
|
|
57
|
-
* the preview's direct parent, so the preview bridge must post the final height
|
|
58
|
-
* itself rather than the intermediate one it sends to a manager.
|
|
59
|
-
*
|
|
60
|
-
* An empty template yields a root-relative URL rather than a broken absolute
|
|
61
|
-
* one, which is what an unconfigured consumer gets — a same-origin 404 it can
|
|
62
|
-
* see, not a silent request to nowhere.
|
|
63
|
-
*/
|
|
64
|
-
export function buildStorybookEmbedUrl(options: StorybookEmbedUrlOptions): string {
|
|
65
|
-
const { template, story, framework, mode = "preview" } = options;
|
|
66
|
-
|
|
67
|
-
const base = resolveStorybookBaseUrl(template, framework);
|
|
68
|
-
const id = encodeURIComponent(story);
|
|
69
|
-
|
|
70
|
-
if (mode === "preview") {
|
|
71
|
-
return `${base}/iframe.html?id=${id}&viewMode=story&${DOCS_EMBED_PARAM}=1`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// `singleStory` hides the sidebar, `shortcuts` stops the embed from
|
|
75
|
-
// swallowing keystrokes meant for the page around it, and `full` drops the
|
|
76
|
-
// addon panel.
|
|
77
|
-
const full = mode === "full" ? "&full=1" : "";
|
|
78
|
-
|
|
79
|
-
return `${base}/?path=/story/${id}${full}&shortcuts=false&singleStory=true`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Coerce an author-supplied height to a CSS length. A bare number — in either
|
|
84
|
-
* type, since markdown attributes arrive as strings — means pixels; anything
|
|
85
|
-
* else is already a CSS length and is passed through.
|
|
86
|
-
*/
|
|
87
|
-
export function storybookEmbedCssHeight(height: number | string): string {
|
|
88
|
-
if (typeof height === "number") return `${height}px`;
|
|
89
|
-
|
|
90
|
-
const trimmed = height.trim();
|
|
91
|
-
|
|
92
|
-
return /^\d+(\.\d+)?$/.test(trimmed) ? `${trimmed}px` : trimmed;
|
|
93
|
-
}
|
package/i18n/locales/ar.json
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"common": {
|
|
3
|
-
"or": "أو",
|
|
4
|
-
"error": {
|
|
5
|
-
"title": "الصفحة غير موجودة",
|
|
6
|
-
"description": "نأسف، لكن الصفحة التي تبحث عنها غير موجودة."
|
|
7
|
-
}
|
|
8
|
-
},
|
|
9
|
-
"docs": {
|
|
10
|
-
"copy": {
|
|
11
|
-
"page": "نسخ الصفحة",
|
|
12
|
-
"options": "خيارات نسخ إضافية",
|
|
13
|
-
"link": "نسخ صفحة Markdown",
|
|
14
|
-
"view": "عرض كـ Markdown",
|
|
15
|
-
"gpt": "فتح في ChatGPT",
|
|
16
|
-
"claude": "فتح في Claude"
|
|
17
|
-
},
|
|
18
|
-
"framework": "إطار العمل",
|
|
19
|
-
"links": "المجتمع",
|
|
20
|
-
"toc": "في هذه الصفحة",
|
|
21
|
-
"report": "الإبلاغ عن مشكلة",
|
|
22
|
-
"edit": "تحرير هذه الصفحة"
|
|
23
|
-
}
|
|
24
|
-
}
|
package/i18n/locales/be.json
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"common": {
|
|
3
|
-
"or": "або",
|
|
4
|
-
"error": {
|
|
5
|
-
"title": "Старонка не знойдзена",
|
|
6
|
-
"description": "Прабачце, але старонка, якую вы шукаеце, не знойдзена."
|
|
7
|
-
}
|
|
8
|
-
},
|
|
9
|
-
"docs": {
|
|
10
|
-
"copy": {
|
|
11
|
-
"page": "Скапіраваць старонку",
|
|
12
|
-
"options": "Дадатковыя параметры капіравання",
|
|
13
|
-
"link": "Скапіраваць Markdown-старонку",
|
|
14
|
-
"view": "Праглядзець як Markdown",
|
|
15
|
-
"gpt": "Адкрыць у ChatGPT",
|
|
16
|
-
"claude": "Адкрыць у Claude"
|
|
17
|
-
},
|
|
18
|
-
"framework": "Фреймворк",
|
|
19
|
-
"links": "Супольнасць",
|
|
20
|
-
"toc": "На гэтай старонцы",
|
|
21
|
-
"report": "Паведаміць пра праблему",
|
|
22
|
-
"edit": "Рэдагаваць гэтую старонку"
|
|
23
|
-
}
|
|
24
|
-
}
|
package/i18n/locales/bn.json
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"common": {
|
|
3
|
-
"or": "অথবা",
|
|
4
|
-
"error": {
|
|
5
|
-
"title": "পেজ খুঁজে পাওয়া যায়নি",
|
|
6
|
-
"description": "দুঃখিত, আপনি যে পেজটি খুঁজছেন সেটি পাওয়া যায়নি।"
|
|
7
|
-
}
|
|
8
|
-
},
|
|
9
|
-
"docs": {
|
|
10
|
-
"copy": {
|
|
11
|
-
"page": "পেজ কপি করুন",
|
|
12
|
-
"options": "আরও কপি অপশন",
|
|
13
|
-
"link": "Markdown পেজ কপি করুন",
|
|
14
|
-
"view": "Markdown হিসেবে দেখুন",
|
|
15
|
-
"gpt": "ChatGPT এ খুলুন",
|
|
16
|
-
"claude": "Claude এ খুলুন"
|
|
17
|
-
},
|
|
18
|
-
"framework": "ফ্রেমওয়ার্ক",
|
|
19
|
-
"links": "কমিউনিটি",
|
|
20
|
-
"toc": "এই পেজে",
|
|
21
|
-
"report": "সমস্যা রিপোর্ট করুন",
|
|
22
|
-
"edit": "এই পেজ সম্পাদনা করুন"
|
|
23
|
-
}
|
|
24
|
-
}
|
package/i18n/locales/ca.json
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"common": {
|
|
3
|
-
"or": "o",
|
|
4
|
-
"error": {
|
|
5
|
-
"title": "Pàgina no trobada",
|
|
6
|
-
"description": "Ho sentim, però la pàgina que cerques no existeix."
|
|
7
|
-
}
|
|
8
|
-
},
|
|
9
|
-
"docs": {
|
|
10
|
-
"copy": {
|
|
11
|
-
"page": "Copiar pàgina",
|
|
12
|
-
"options": "Més opcions de còpia",
|
|
13
|
-
"link": "Copiar pàgina Markdown",
|
|
14
|
-
"view": "Veure com a Markdown",
|
|
15
|
-
"gpt": "Obrir a ChatGPT",
|
|
16
|
-
"claude": "Obrir a Claude"
|
|
17
|
-
},
|
|
18
|
-
"framework": "Framework",
|
|
19
|
-
"links": "Comunitat",
|
|
20
|
-
"toc": "En aquesta pàgina",
|
|
21
|
-
"report": "Informar d'un problema",
|
|
22
|
-
"edit": "Editar aquesta pàgina"
|
|
23
|
-
}
|
|
24
|
-
}
|
package/i18n/locales/ckb.json
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"common": {
|
|
3
|
-
"or": "یان",
|
|
4
|
-
"error": {
|
|
5
|
-
"title": "پەڕە نەدۆزرایەوە",
|
|
6
|
-
"description": "بەداخەوە، ئەم پەڕەیە نەدۆزرایەوە."
|
|
7
|
-
}
|
|
8
|
-
},
|
|
9
|
-
"docs": {
|
|
10
|
-
"copy": {
|
|
11
|
-
"page": "کۆپیکردنی پەڕە",
|
|
12
|
-
"options": "بژاردەی زیاتری لەبەرگرتنەوە",
|
|
13
|
-
"link": "کۆپیکردنی پەڕەی Markdown",
|
|
14
|
-
"view": "بینین وەک Markdown",
|
|
15
|
-
"gpt": "کردنەوە لە ChatGPT",
|
|
16
|
-
"claude": "کردنەوە لە Claude"
|
|
17
|
-
},
|
|
18
|
-
"framework": "چوارچێوە",
|
|
19
|
-
"links": "کۆمەڵگا",
|
|
20
|
-
"toc": "لەم پەڕەدا",
|
|
21
|
-
"report": "ڕاپۆرتکردنی کێشە",
|
|
22
|
-
"edit": "دەستکاریکردنی ئەم پەڕەیە"
|
|
23
|
-
}
|
|
24
|
-
}
|