Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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.
28 changes: 28 additions & 0 deletions apps/docs/app/components/DocsPageHeaderLinks.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ const componentSourceUrl = computed(() => {
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.
// Gated on the same source map as the GitHub link so the two buttons pair up.
const componentStorybookUrl = 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 storybook = appConfig.storybook as { url?: string } | undefined;
if (!sources[slug] || !storybook?.url) return undefined;
return `${storybook.url}/?path=/docs/components-${slug}`;
});

const items = computed(() => [
[
{
Expand Down Expand Up @@ -90,6 +106,18 @@ async function copyPage() {
:ui="{ leadingIcon: 'text-neutral size-3.5' }"
/>

<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' }"
/>

<UFieldGroup size="md">
<UButton
:label="t('docs.copy.page')"
Expand Down
59 changes: 59 additions & 0 deletions apps/docs/app/middleware/section-redirect.global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { ContentNavigationItem } from "@nuxt/content";

// Section roots (e.g. /components, /utilities, /documentation/design) have no
// index page of their own. Rather than hard-coding a redirect target per
// section — which silently points at the wrong page whenever the sidebar is
// reordered or a higher-sorted page is added — resolve the first entry from the
// live navigation tree and redirect there. The redirect therefore always tracks
// whatever the sidebar shows first.
export default defineNuxtRouteMiddleware(async (to) => {
// Only the docs sections use this pattern; skip the landing page and the rest.
if (!/^\/(documentation|components|utilities)(\/|$)/.test(to.path)) return;

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

// Own cache key (not Docus' "navigation_docs") so we don't clash with its
// useAsyncData options; the query result is cached per request either way.
// Mirror Docus' transform: strip a `/docs` wrapper level if one exists so the
// tree (and its order) matches the sidebar exactly.
const { data: navigation } = await useAsyncData(
"section-redirect:navigation",
() => queryCollectionNavigation("docs"),
{
transform: (data: ContentNavigationItem[]) =>
data.find((item) => item.path === "/docs")?.children ?? data,
},
);

const nav = navigation.value;
if (!nav) return;

const node = findNode(nav, path);
// Leaf pages have no children; only section nodes are redirected.
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 when pages are added or reordered, so it must not be hard-cached.
return navigateTo(target, { redirectCode: 302, replace: true });
}
});

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.
59 changes: 4 additions & 55 deletions apps/docs/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,61 +80,10 @@ 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 },
},
},
// Section roots (/components, /utilities, /documentation/*) have no index page.
// Instead of hard-coding a redirect target per section (which goes stale when
// the sidebar is reordered), app/middleware/section-redirect.global.ts resolves
// the first entry from the live navigation tree and redirects there.
// 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