Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/docs/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ export default defineAppConfig({
branch: "main",
rootDir: "apps/docs",
},
storybook: {
// Base URL of the deployed component-library Storybook. Component pages link
// to their autodocs page here (see DocsPageHeaderLinks.vue).
url: "https://storybook.meteor.shopware.com",
},
ui: {
colors: {
primary: "brand",
Expand Down
3 changes: 3 additions & 0 deletions apps/docs/app/assets/icons/storybook.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 42 additions & 10 deletions apps/docs/app/components/DocsPageHeaderLinks.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,43 @@ const mcpServerUrl = computed(
() => `${window?.location?.origin}${joinURL(appBaseURL, mcpRoute)}`,
);

// Link a component page to its source folder on GitHub. The slug -> folder map
// is provided at build time by modules/meteor-components.ts.
const componentSourceUrl = computed(() => {
// The slug -> repo-relative source folder map, provided at build time by
// modules/meteor-components.ts. Its keys double as the set of known components.
const componentSourcePaths = computed(
() =>
(runtimeConfig.public.componentSourcePaths ?? {}) as Record<string, string>,
);

// The current component slug, or undefined outside a component page. Both the
// GitHub and Storybook links are gated on this so they only show for real
// components and always appear together.
const componentSlug = computed(() => {
if (!route.path.startsWith("/components/")) return undefined;
const slug = route.path.split("/").filter(Boolean).pop() ?? "";
const sources = (runtimeConfig.public.componentSourcePaths ?? {}) as Record<
string,
string
>;
const path = sources[slug];
const slug = route.path.split("/").filter(Boolean).pop();
return slug && componentSourcePaths.value[slug] ? slug : undefined;
});

// Link a component page to its source folder on GitHub.
const componentSourceUrl = computed(() => {
if (!componentSlug.value) return undefined;
const path = componentSourcePaths.value[componentSlug.value];
const github = appConfig.github as
| { url?: string; branch?: string }
| undefined;
if (!path || !github?.url) return undefined;
return `${github.url}/tree/${github.branch || "main"}/${path}`;
});

// Link a component page to its Storybook entry. The docs slug matches the
// Storybook id prefix (e.g. "data-table" -> components-data-table); Storybook
// resolves that to the component's autodocs (or first story) automatically.
const componentStorybookUrl = computed(() => {
if (!componentSlug.value) return undefined;
const storybook = appConfig.storybook as { url?: string } | undefined;
if (!storybook?.url) return undefined;
return `${storybook.url}/?path=/docs/components-${componentSlug.value}`;
});

const items = computed(() => [
[
{
Expand Down Expand Up @@ -78,12 +98,24 @@ async function copyPage() {

<template>
<div class="flex items-center gap-2">
<UButton
v-if="componentStorybookUrl"
:to="componentStorybookUrl"
target="_blank"
icon="i-custom:storybook"
label="Storybook"
color="neutral"
variant="outline"
size="md"
:ui="{ leadingIcon: 'text-neutral size-3.5' }"
/>

<UButton
v-if="componentSourceUrl"
:to="componentSourceUrl"
target="_blank"
icon="i-simple-icons:github"
label="Source"
label="GitHub"
color="neutral"
variant="outline"
size="md"
Expand Down
72 changes: 72 additions & 0 deletions apps/docs/app/middleware/section-redirect.global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { ContentNavigationItem } from "@nuxt/content";

// Section roots (e.g. /components, /utilities, /documentation/design) have no
// index page. Redirect each to its first sidebar entry, resolved from the live
// navigation tree — so the target never goes stale when the sidebar is reordered,
// and brand-new sections (top-level or nested) work without touching this file.
export default defineNuxtRouteMiddleware(async (to) => {
// The landing page is the only non-content top-level route; skip it so it does
// not wait on the navigation query. Every other route is cheap to check.
if (to.path === "/") return;

const path = to.path.replace(/\/+$/, "") || "/";

// Reuse the navigation tree Docus already fetched (keyed "navigation_docs")
// instead of fetching our own copy, which would be serialised into the payload
// a second time on every docs page. On a direct SSR hit the middleware can run
// before Docus' fetch, so query the collection directly as a fallback.
const cached = useNuxtData<ContentNavigationItem[]>("navigation_docs");
let nav = cached.data.value ?? undefined;
if (!nav) {
const data = await queryCollectionNavigation("docs").catch(() => undefined);
// Mirror Docus' transform: strip a `/docs` wrapper level if one exists so the
// tree (and its order) matches the sidebar exactly.
nav = data?.find((item) => item.path === "/docs")?.children ?? data;
}

if (nav) {
const node = findNode(nav, path);
// Only section nodes (those with children) redirect; leaf pages and unknown
// routes fall through to normal rendering / 404.
if (!node?.children?.length) return;
const target = firstLeafPath(node);
if (target && target !== path) {
// 302 (temporary): the target is derived from navigation order and can
// change as pages are added or reordered, so it must not be hard-cached.
return navigateTo(target, { redirectCode: 302, replace: true });
}
return;
}

// Fail-safe: if the navigation tree can't be loaded, the section entry points
// linked from the header (see useMainNav) must still resolve instead of 404ing.
const fallback = SECTION_ROOT_FALLBACKS[path];
if (fallback)
return navigateTo(fallback, { redirectCode: 302, replace: true });
});

// Only the header-linked roots need a hard-coded safety net; deeper section roots
// are non-critical if navigation is momentarily unavailable.
const SECTION_ROOT_FALLBACKS: Record<string, string> = {
"/documentation": "/documentation/getting-started/installation",
"/components": "/components/action-menu",
"/utilities": "/utilities/components/inset",
};

function findNode(
items: ContentNavigationItem[],
path: string,
): ContentNavigationItem | undefined {
for (const item of items) {
if (item.path === path) return item;
const found = item.children && findNode(item.children, path);
if (found) return found;
}
return undefined;
}

function firstLeafPath(item: ContentNavigationItem): string {
let current = item;
while (current.children?.length) current = current.children[0]!;
return current.path;
}
4 changes: 2 additions & 2 deletions apps/docs/content/2.components/card.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Show the link or unlink toggle when a card represents values that can be inherit
- The tabs area sits below the header and can be used for closely related views of the same section.
- The content area holds the main information, form fields, or other section content.

**Card** also exposes [**Inset**](/components/inset) as a companion layout utility for cases where content inside the card should visually break out to the card edges without hard-coding spacing values.
**Card** also exposes [**Inset**](/utilities/components/inset) as a companion layout utility for cases where content inside the card should visually break out to the card edges without hard-coding spacing values.

- Use **Inset** inside the default card content when an inner block should align to the card's outer padding instead of the current content flow.
- Use **Inset** in the `footer` slot when the footer needs its own full-width background or custom padding treatment while still staying aligned to the card spacing tokens.
Expand Down Expand Up @@ -109,4 +109,4 @@ Show the link or unlink toggle when a card represents values that can be inherit

## Related components

- [**Inset**](/components/inset): when you only need spacing or padded grouping inside an existing surface.
- [**Inset**](/utilities/components/inset): when you only need spacing or padded grouping inside an existing surface.
55 changes: 0 additions & 55 deletions apps/docs/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,61 +80,6 @@ export default defineNuxtConfig({
// per environment with NUXT_SITE_URL.
url: "https://meteor.shopware.com",
},
// Section roots have no index page, so permanently redirect each to its first
// child instead of 404ing.
routeRules: {
"/documentation": {
redirect: {
to: "/documentation/getting-started/installation",
statusCode: 301,
},
},
"/documentation/getting-started": {
redirect: {
to: "/documentation/getting-started/installation",
statusCode: 301,
},
},
"/documentation/guidelines": {
redirect: {
to: "/documentation/guidelines/design-principles",
statusCode: 301,
},
},
"/documentation/design": {
redirect: { to: "/documentation/design/tokens", statusCode: 301 },
},
"/documentation/content": {
redirect: { to: "/documentation/content/wording", statusCode: 301 },
},
"/components": {
redirect: { to: "/components/action-menu", statusCode: 301 },
},
"/utilities": {
redirect: {
to: "/utilities/components/theme-provider",
statusCode: 301,
},
},
"/utilities/composables": {
redirect: {
to: "/utilities/composables/use-snackbar",
statusCode: 301,
},
},
"/utilities/components": {
redirect: {
to: "/utilities/components/theme-provider",
statusCode: 301,
},
},
"/utilities/directives": {
redirect: { to: "/utilities/directives/tooltip", statusCode: 301 },
},
"/utilities/plugins": {
redirect: { to: "/utilities/plugins/device-helper", statusCode: 301 },
},
},
// modules/ is auto-scanned, so meteor-components and component-examples load
// automatically. meteor-components registers its work via the
// `component-meta:extend` hook (fired during the build), so module order
Expand Down
18 changes: 1 addition & 17 deletions packages/component-library/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,16 @@
import remarkGfmModule from "remark-gfm";
import type { StorybookConfig } from "@storybook/vue3-vite";
import { mergeConfig } from "vite";
import path from "path";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const remarkGfm = (remarkGfmModule as any).default ?? remarkGfmModule;

const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-links",
{
// eslint-disable-next-line storybook/no-uninstalled-addons
name: "@storybook/addon-docs",
options: {
mdxPluginOptions: {
mdxCompileOptions: {
remarkPlugins: [remarkGfm],
},
},
},
},
{
name: "@storybook/addon-essentials",
options: {
backgrounds: false,
outline: false,
docs: false,
},
},
"@storybook/addon-interactions",
Expand Down
51 changes: 50 additions & 1 deletion packages/component-library/.storybook/manager.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,58 @@
import { addons } from "@storybook/manager-api";
import React from "react";
import { addons, types, useStorybookApi, useStorybookState } from "@storybook/manager-api";
import { IconButton } from "@storybook/components";
import { ShareAltIcon } from "@storybook/icons";
import { shopwareTheme } from "./shopwareTheme";

const DOCS_BASE_URL = "https://meteor.shopware.com";

// Map the active Storybook entry to its page on the docs site. Component titles follow
// "Components/<Name>" and the docs URL uses the kebab-cased name, e.g.
// "Components/Data Table" -> https://meteor.shopware.com/components/data-table.
// Anything outside the Components section (directives, composables) links to the docs home.
function getDocsUrl(title) {
const segments = (title ?? "").split("/");
if (segments[0] !== "Components" || !segments[1]) {
return DOCS_BASE_URL;
}
const slug = segments[1].trim().toLowerCase().replace(/\s+/g, "-");
return `${DOCS_BASE_URL}/components/${slug}`;
}

function DocsLink() {
const api = useStorybookApi();
// Subscribe to state so the link updates as the user navigates between stories.
const { storyId } = useStorybookState();
const data = api.getCurrentStoryData();
const href = getDocsUrl(data && data.title);

return React.createElement(
IconButton,
{
key: storyId,
as: "a",
href,
target: "_blank",
rel: "noopener noreferrer",
title: "Open in the Meteor documentation",
},
React.createElement("span", { style: { marginRight: 6 } }, "Documentation"),
React.createElement(ShareAltIcon, null),
);
}

addons.setConfig({
theme: shopwareTheme,
sidebar: {
collapsedRoots: ["composables", "directives"],
},
});

addons.register("meteor/docs-link", () => {
addons.add("meteor/docs-link", {
type: types.TOOL,
title: "Meteor documentation",
match: () => true,
render: () => React.createElement(DocsLink),
});
});
Loading
Loading