From c8e824314523da43c6c15fa29cf9e57625252807 Mon Sep 17 00:00:00 2001 From: Rahul Suvarna Date: Mon, 1 Jun 2026 14:41:16 +0200 Subject: [PATCH] docs: added footer to the templates page --- .changeset/goofy-results-drive.md | 5 + packages/docs/.storybook/preview.css | 13 +++ packages/docs/.storybook/preview.js | 152 ++++++++++++++++++++++++++- 3 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 .changeset/goofy-results-drive.md diff --git a/.changeset/goofy-results-drive.md b/.changeset/goofy-results-drive.md new file mode 100644 index 0000000000..4aa7ec8a15 --- /dev/null +++ b/.changeset/goofy-results-drive.md @@ -0,0 +1,5 @@ +--- +'@solid-design-system/docs': patch +--- + +Added footer to template pages diff --git a/packages/docs/.storybook/preview.css b/packages/docs/.storybook/preview.css index 0e6feed3b5..15842756b9 100644 --- a/packages/docs/.storybook/preview.css +++ b/packages/docs/.storybook/preview.css @@ -113,3 +113,16 @@ sd-notification#versioning-info::part(base) { .sbdocs .sd-deprecated-badge { @apply inline-flex items-center rounded-sm bg-warning px-1 py-0.5 text-[10px] font-bold uppercase leading-none text-black; } + +.template-global-footer { + @apply m-0 box-border w-full bg-neutral-100 px-32 py-20 flex flex-col items-start gap-12; +} + +.template-global-footer__inner { + @apply w-full flex flex-col items-start gap-12; + max-width: 100%; +} + +.template-global-footer__links { + @apply flex flex-col gap-2; +} diff --git a/packages/docs/.storybook/preview.js b/packages/docs/.storybook/preview.js index 6b3ce42a75..d65d5304df 100644 --- a/packages/docs/.storybook/preview.js +++ b/packages/docs/.storybook/preview.js @@ -4,6 +4,8 @@ import '../../tokens/themes/bb/bb.css'; import '../../tokens/themes/vb/vb.css'; import '../../tokens/themes/ui-dark/ui-dark.css'; import '../../tokens/themes/ui-light/ui-light.css'; +import { addons } from 'storybook/preview-api'; +import { DOCS_RENDERED, STORY_RENDERED } from 'storybook/internal/core-events'; import { withThemeByClassName } from './addons/with-theme.js'; import { storybookUtilities } from '../scripts/storybook/helper.js'; import docsCodepenEnhancer from '../scripts/storybook/docs-codepen-enhancer.js'; @@ -23,8 +25,156 @@ const deprecatedBadgeDecorator = Story => { return Story(); }; +const TEMPLATE_FOOTER_ID = 'template-global-footer'; +const SD_TAG_REGEX = /<(sd-[a-z][a-z0-9-]*)/g; +const TITLE_REGEX = /title\s*:\s*['"`]([^'"`]+)['"`]/; +const LINE_COMMENT_REGEX = /\/\/[^\n]*/g; +const BLOCK_COMMENT_REGEX = /\/\*[\s\S]*?\*\//g; + +const COMPONENT_LABEL_OVERRIDES = { + 'sd-textarea': 'Text Area' +}; + +const getComponentLabel = tagName => { + if (COMPONENT_LABEL_OVERRIDES[tagName]) { + return COMPONENT_LABEL_OVERRIDES[tagName]; + } + + return tagName + .replace('sd-', '') + .split('-') + .map(segment => segment.charAt(0).toUpperCase() + segment.slice(1)) + .join(' '); +}; + +const buildTemplateComponentMap = () => { + const sources = import.meta.glob('../src/stories/templates/*.stories.ts', { + query: '?raw', + import: 'default', + eager: true + }); + + const map = {}; + + Object.values(sources).forEach(rawSource => { + const stripped = rawSource.replace(BLOCK_COMMENT_REGEX, '').replace(LINE_COMMENT_REGEX, ''); + + const titleMatch = stripped.match(TITLE_REGEX); + if (!titleMatch) return; + + const tags = new Set(); + let tagMatch; + SD_TAG_REGEX.lastIndex = 0; + while ((tagMatch = SD_TAG_REGEX.exec(stripped)) !== null) { + tags.add(tagMatch[1]); + } + + if (tags.size === 0) return; + map[titleMatch[1]] = [...tags].sort(); + }); + + return map; +}; + +const TEMPLATE_COMPONENT_MAP = buildTemplateComponentMap(); + +const buildLinksFromTags = tags => + tags.map(tag => ({ + label: getComponentLabel(tag), + href: `./?path=/docs/components-${tag}-overview--docs` + })); + +const removeTemplateFooter = () => { + document.getElementById(TEMPLATE_FOOTER_ID)?.remove(); +}; + +const createTemplateFooter = ({ title, links }) => { + const footer = document.createElement('footer'); + footer.id = TEMPLATE_FOOTER_ID; + footer.className = 'template-global-footer'; + + const linksMarkup = links + .map( + ({ label, href }) => + `${label}` + ) + .join(''); + + footer.innerHTML = ` + + `; + return footer; +}; + +const renderTemplateFooter = (storyTitle, templateFooterConfig) => { + const tags = TEMPLATE_COMPONENT_MAP[storyTitle]; + if (!tags || tags.length === 0) return; + + const targetRoot = document.getElementById('storybook-docs'); + if (!targetRoot) return; + + const signature = `${storyTitle}::${tags.join('|')}`; + const existingFooter = document.getElementById(TEMPLATE_FOOTER_ID); + + if ( + existingFooter && + existingFooter.parentElement === targetRoot && + existingFooter.dataset.componentSignature === signature + ) { + return; + } + + if (existingFooter) { + existingFooter.remove(); + } + + const title = templateFooterConfig?.title || 'Utilized Components and Styles'; + const links = buildLinksFromTags(tags); + const footer = createTemplateFooter({ title, links }); + footer.dataset.componentSignature = signature; + + targetRoot.appendChild(footer); +}; + +let activeTemplateStoryTitle = null; +let activeTemplateFooterConfig = null; + +const channel = addons.getChannel?.(); +if (channel) { + const handleRender = () => { + if (!activeTemplateStoryTitle) { + removeTemplateFooter(); + return; + } + renderTemplateFooter(activeTemplateStoryTitle, activeTemplateFooterConfig); + }; + + channel.on(DOCS_RENDERED, handleRender); + channel.on(STORY_RENDERED, handleRender); +} + +const templateFooterDecorator = (Story, context) => { + const isTemplatePage = context.title?.startsWith('Templates/'); + + if (isTemplatePage) { + activeTemplateStoryTitle = context.title; + activeTemplateFooterConfig = context.parameters?.templateFooter; + } else { + activeTemplateStoryTitle = null; + activeTemplateFooterConfig = null; + removeTemplateFooter(); + } + + return Story(); +}; + export const preview = { - decorators: [theme, deprecatedBadgeDecorator], + decorators: [theme, deprecatedBadgeDecorator, templateFooterDecorator], parameters: { chromatic: { disableSnapshot: true,