diff --git a/.gitignore b/.gitignore index 576b4157aa..6d55785571 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ /.astro /dist /public/assets/code-viewer +/public/ig-themes # browser auto-generated files: /browser/node_modules diff --git a/astro.config.mjs b/astro.config.mjs index b10a500b68..2e6e511ddb 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -40,26 +40,44 @@ function stripSampleInstantiation() { } /** - * Vite plugin: convert local CSS/SCSS imports in sample index.ts files to - * `?inline` imports with dynamic ` + )} diff --git a/src/pages/[...slug].astro b/src/pages/[...slug].astro index d1f7efa702..2a3b86c810 100644 --- a/src/pages/[...slug].astro +++ b/src/pages/[...slug].astro @@ -13,7 +13,7 @@ */ import SampleLayout from '../layouts/SampleLayout.astro'; -import { keyToSlug, slugToInfo, extractSampleHtml } from '../utils/samples'; +import { keyToSlug, slugToInfo, extractSampleHtml, resolveSampleStyles } from '../utils/samples'; // All sample index.html files (raw HTML for server-side rendering) // Path is relative to THIS file: src/pages/[...slug].astro @@ -24,6 +24,23 @@ const htmlModules = import.meta.glob('../../samples/**/index.html', { eager: true, }) as Record; +// Raw entry-module source for every sample. Used only to learn which +// stylesheets each sample imports, and in what order. +const tsSources = import.meta.glob('../../samples/**/src/index.ts', { + query: '?raw', + import: 'default', + eager: true, +}) as Record; + +// Every sample-local stylesheet, compiled to plain CSS text (SCSS included). +// These are inlined into so the sample is styled at first paint instead +// of after its (multi-megabyte) JS chunk finally evaluates. +const sampleCss = import.meta.glob('../../samples/**/src/*.{css,scss}', { + query: '?inline', + import: 'default', + eager: true, +}) as Record; + const PKG_PREFIX = '../../samples/'; const HTML_SUFFIX = '/index.html'; @@ -55,10 +72,26 @@ const info = slugToInfo(slug)!; const rawHtml = htmlModules[`${PKG_PREFIX}${slug}${HTML_SUFFIX}`] ?? ''; const sampleHtml = extractSampleHtml(rawHtml); +// Resolve this sample's stylesheets so SampleLayout can put them in . +// Theme sheets become s to public/ig-themes/; sample-local files are +// inlined. Anything that fails to resolve is dropped here and left to the +// sample module's own import, so a miss degrades to the old behaviour rather +// than losing the styles. +const base = import.meta.env.BASE_URL.replace(/\/$/, ''); +const styles = resolveSampleStyles(tsSources[`${PKG_PREFIX}${slug}/src/index.ts`] ?? '', slug, base) + .map(style => + style.kind === 'link' + ? style + : sampleCss[style.key] !== undefined + ? { kind: 'inline' as const, css: sampleCss[style.key] } + : null, + ) + .filter(v => v !== null); + const pageTitle = `${info.displayName} | ${info.componentName} | Ignite UI Web Components`; --- - + @@ -73,9 +106,19 @@ const pageTitle = `${info.displayName} | ${info.componentName} | Ignite UI Web C // Vite resolves this glob at build time → one lazy chunk per sample const sampleModules = import.meta.glob('../../samples/**/src/index.ts'); + // Lifts the layout.css guard that hides not-yet-upgraded custom elements. + // Must run on EVERY exit path, including failures — a sample that throws + // should still show whatever it managed to render, not stay blank. + function markReady() { + document.documentElement.dataset.sampleReady = ''; + } + function loadSample() { const slug = document.documentElement.dataset.sampleSlug; - if (!slug) return; + if (!slug) { + markReady(); + return; + } const moduleKey = `../../samples/${slug}/src/index.ts`; @@ -91,10 +134,11 @@ const pageTitle = `${info.displayName} | ${info.componentName} | Ignite UI Web C } }).catch((err: unknown) => { console.error(`[astro] failed to load sample module: ${moduleKey}`, err); - }); + }).finally(markReady); } else { console.warn(`[astro] no sample module found for slug "${slug}". Key: ${moduleKey}`); console.info('[astro] available keys:', Object.keys(sampleModules).slice(0, 5)); + markReady(); } } diff --git a/src/utils/samples.ts b/src/utils/samples.ts index 6600543424..4e79e5da6a 100644 --- a/src/utils/samples.ts +++ b/src/utils/samples.ts @@ -129,3 +129,70 @@ export function buildNavTree(samples: SampleInfo[]): NavGroup[] { return [...groupMap.values()]; } + +// --------------------------------------------------------------------------- +// Sample stylesheet resolution +// --------------------------------------------------------------------------- + +/** + * A stylesheet a sample entry module pulls in, resolved to how the page should + * put it in . + * + * - `link` → an Ignite UI theme, served from public/ig-themes/ (cached across + * every sample page, and re-pointable by the docs theming widget). + * - `glob` → a sample-local file, whose compiled text the page inlines. + */ +export type SampleStyle = + | { kind: 'link'; href: string; pkg: 'webcomponents' | 'grids'; variant: string; theme: string } + | { kind: 'glob'; key: string }; + +/** `import 'x.css';` / `import "./y.scss";` — the same shape the build plugin strips. */ +export const CSS_IMPORT_RE = /^import\s+['"]([^'"]+\.(?:css|scss))['"];?\s*$/gm; + +/** Bare theme specifiers, e.g. `igniteui-webcomponents-grids/grids/themes/light/material.css` */ +const THEME_SPEC_RE = + /^igniteui-webcomponents(-grids\/grids)?\/themes\/(light|dark)\/(material|bootstrap|fluent|indigo)\.css$/; + +/** + * Read a sample entry module's source and return, in source order, every + * stylesheet it imports. + * + * Order matters: it is the cascade order, so the page must emit these into + * exactly as they appear here. + * + * @param source raw text of the sample's src/index.ts + * @param slug e.g. "inputs/button-group/overview" + * @param base site base path, '' or e.g. '/webcomponents-demos' + */ +export function resolveSampleStyles(source: string, slug: string, base: string): SampleStyle[] { + const styles: SampleStyle[] = []; + CSS_IMPORT_RE.lastIndex = 0; + + for (const match of source.matchAll(CSS_IMPORT_RE)) { + const spec = match[1]; + + const theme = THEME_SPEC_RE.exec(spec); + if (theme) { + const pkg = theme[1] ? 'grids' : 'webcomponents'; + styles.push({ + kind: 'link', + href: `${base}/ig-themes/${pkg}/${theme[2]}/${theme[3]}.css`, + pkg, + variant: theme[2], + theme: theme[3], + }); + continue; + } + + // Relative to the sample's src/ directory. Every sample-local stylesheet + // sits next to index.ts, so `./name.css` is the only relative shape used. + if (spec.startsWith('./')) { + styles.push({ kind: 'glob', key: `../../samples/${slug}/src/${spec.slice(2)}` }); + continue; + } + + console.warn(`[samples] ${slug}: unrecognised stylesheet import "${spec}" — not inlined`); + } + + return styles; +}