feat(site): add TimeSlider, VolumeSlider, Popover API references (#685)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Darius Cepulis
2026-03-03 17:41:35 -06:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 916f4700ae
commit 8ab596ea30
71 changed files with 2093 additions and 98 deletions
@@ -0,0 +1,86 @@
import * as ts from 'typescript';
import { getJsDocComment } from './data-attrs-handler.js';
import type { CSSVarsExtraction } from './types.js';
function unwrapObjectLiteral(node: ts.Expression): ts.ObjectLiteralExpression | undefined {
if (ts.isObjectLiteralExpression(node)) {
return node;
}
if (ts.isParenthesizedExpression(node)) {
return unwrapObjectLiteral(node.expression);
}
if (ts.isAsExpression(node)) {
return unwrapObjectLiteral(node.expression);
}
if (ts.isSatisfiesExpression(node)) {
return unwrapObjectLiteral(node.expression);
}
return undefined;
}
/**
* Extract CSS custom properties from a css-vars file.
*
* Looks for patterns like:
* ```ts
* export const SliderCSSVars = {
* /** Fill level percentage (0100). *\/
* fill: '--media-slider-fill',
* } as const;
* ```
*/
export function extractCSSVars(filePath: string, program: ts.Program, componentName: string): CSSVarsExtraction | null {
const sourceFile = program.getSourceFile(filePath);
if (!sourceFile) {
return null;
}
const vars: Array<{ name: string; description: string }> = [];
const expectedName = `${componentName}CSSVars`;
function visit(node: ts.Node) {
if (ts.isVariableStatement(node)) {
for (const decl of node.declarationList.declarations) {
if (!ts.isIdentifier(decl.name) || decl.name.text !== expectedName || !decl.initializer) {
continue;
}
const objLiteral = unwrapObjectLiteral(decl.initializer);
if (!objLiteral) continue;
for (const prop of objLiteral.properties) {
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
let cssVarName = '';
if (ts.isStringLiteral(prop.initializer)) {
cssVarName = prop.initializer.text;
}
const description = getJsDocComment(prop, sourceFile);
if (cssVarName) {
vars.push({
name: cssVarName,
description: description || '',
});
}
}
}
}
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
if (vars.length === 0) {
return null;
}
return { vars };
}
+205 -36
View File
@@ -3,15 +3,18 @@ import * as path from 'node:path';
import * as ts from 'typescript';
import * as tae from 'typescript-api-extractor';
import { extractCore } from './core-handler.js';
import { extractCSSVars } from './css-vars-handler.js';
import { extractDataAttrs } from './data-attrs-handler.js';
import { abbreviateType } from './formatter.js';
import { extractHtml } from './html-handler.js';
import { extractPartDescription, extractParts } from './parts-handler.js';
import { extractPartDescription, extractParts, extractSubPartProps } from './parts-handler.js';
import {
type ComponentReference,
ComponentReferenceSchema,
type ComponentSource,
type CoreExtraction,
type CSSVarDef,
type CSSVarsExtraction,
type DataAttrDef,
type DataAttrsExtraction,
type PartReference,
@@ -78,6 +81,14 @@ function buildDataAttrs(dataAttrsData: DataAttrsExtraction): Record<string, Data
return dataAttributes;
}
function buildCSSVars(cssVarsData: CSSVarsExtraction): Record<string, CSSVarDef> {
const cssCustomProperties: Record<string, CSSVarDef> = {};
for (const v of cssVarsData.vars) {
cssCustomProperties[v.name] = { description: v.description };
}
return cssCustomProperties;
}
// Magenta prefix - visible on both light and dark terminals
const PREFIX = '\x1b[35m[api-docs-builder]\x1b[0m';
@@ -118,6 +129,7 @@ function discoverComponents(): ComponentSource[] {
// Look for core file
const coreFile = path.join(componentDir, `${dir.name}-core.ts`);
const dataAttrsFile = path.join(componentDir, `${dir.name}-data-attrs.ts`);
const cssVarsFile = path.join(componentDir, `${dir.name}-css-vars.ts`);
// Look for HTML element file
const htmlFile = path.join(HTML_UI_PATH, dir.name, `${dir.name}-element.ts`);
@@ -135,6 +147,10 @@ function discoverComponents(): ComponentSource[] {
source.dataAttrsPath = dataAttrsFile;
}
if (fs.existsSync(cssVarsFile)) {
source.cssVarsPath = cssVarsFile;
}
if (fs.existsSync(htmlFile)) {
source.htmlPath = htmlFile;
}
@@ -163,6 +179,7 @@ function createProgram(sources: ComponentSource[]): ts.Program {
for (const source of sources) {
if (source.corePath) files.push(source.corePath);
if (source.dataAttrsPath) files.push(source.dataAttrsPath);
if (source.cssVarsPath) files.push(source.cssVarsPath);
if (source.htmlPath) files.push(source.htmlPath);
if (source.partsIndexPath) files.push(source.partsIndexPath);
@@ -189,6 +206,40 @@ function createProgram(sources: ComponentSource[]): ts.Program {
files.push(fullPath);
}
}
// Include origin component files for re-exported parts
const partsSource = fs.readFileSync(source.partsIndexPath, 'utf-8');
const nonLocalImports = partsSource.match(/from\s+['"]([^.][^'"]*)['"]/g);
if (nonLocalImports) {
for (const match of nonLocalImports) {
const importPath = match.replace(/from\s+['"]/, '').replace(/['"]$/, '');
const originDir = path.resolve(path.dirname(source.partsIndexPath), importPath, '..');
const originKebab = path.basename(originDir);
// Include origin HTML element files
const originHtmlDir = path.join(HTML_UI_PATH, originKebab);
if (fs.existsSync(originHtmlDir)) {
const originElementFiles = fs.readdirSync(originHtmlDir).filter((f) => f.endsWith('-element.ts'));
for (const file of originElementFiles) {
const fullPath = path.join(originHtmlDir, file);
if (!files.includes(fullPath)) {
files.push(fullPath);
}
}
}
// Include origin React .tsx files for JSDoc description extraction
if (fs.existsSync(originDir)) {
const originReactFiles = fs.readdirSync(originDir).filter((f) => f.endsWith('.tsx'));
for (const file of originReactFiles) {
const fullPath = path.join(originDir, file);
if (!files.includes(fullPath)) {
files.push(fullPath);
}
}
}
}
}
}
}
@@ -216,6 +267,9 @@ function buildSingleComponentReference(source: ComponentSource, program: ts.Prog
// Extract data attributes
const dataAttrsData = source.dataAttrsPath ? extractDataAttrs(source.dataAttrsPath, program, source.name) : null;
// Extract CSS custom properties
const cssVarsData = source.cssVarsPath ? extractCSSVars(source.cssVarsPath, program, source.name) : null;
// Extract HTML element info
const htmlData = source.htmlPath ? extractHtml(source.htmlPath, program, source.name) : null;
@@ -226,6 +280,7 @@ function buildSingleComponentReference(source: ComponentSource, program: ts.Prog
props: buildProps(coreData),
state: buildState(coreData),
dataAttributes: dataAttrsData ? buildDataAttrs(dataAttrsData) : {},
cssCustomProperties: cssVarsData ? buildCSSVars(cssVarsData) : {},
platforms: {},
};
@@ -242,15 +297,36 @@ function buildSingleComponentReference(source: ComponentSource, program: ts.Prog
return result;
}
/**
* Check if a React source file instantiates a Core class (matches `new \w+Core\(`).
*/
function instantiatesCore(filePath: string): boolean {
try {
const content = fs.readFileSync(filePath, 'utf-8');
return /new \w+Core\(/.test(content);
} catch {
return false;
}
}
function usesDataAttrs(filePath: string): boolean {
try {
return fs.readFileSync(filePath, 'utf-8').includes('stateAttrMap');
} catch {
return false;
}
}
/**
* Discover parts and match them to HTML element files.
*
* Matching algorithm:
* 1. Parse `index.parts.ts` for named exports -> part names and source paths
* 2. Derive kebab segment from source: `./time-value` -> strip `./time-` prefix -> `value`
* 3. For each part, look for `{name}-{kebab}-element.ts` in HTML dir (e.g., `time-group-element.ts`)
* 4. The part with NO matching `{name}-{kebab}-element.ts` but where `{name}-element.ts` exists -> primary part
* 5. Primary part gets: shared core file, shared data-attrs, main element (`{name}-element.ts`)
* 2. Filter out non-local re-exports (sources not starting with './')
* 3. Derive kebab segment from source: `./time-value` -> strip `./time-` prefix -> `value`
* 4. For each part, look for `{name}-{kebab}-element.ts` in HTML dir
* 5. Primary part: the part whose React source file instantiates the Core class
* 6. Primary part gets: shared core file, shared data-attrs/css-vars, main element
*/
function discoverParts(source: ComponentSource, program: ts.Program): PartSource[] {
if (!source.partsIndexPath) return [];
@@ -258,34 +334,33 @@ function discoverParts(source: ComponentSource, program: ts.Program): PartSource
const partExports = extractParts(source.partsIndexPath, program);
if (partExports.length === 0) return [];
const localExports = partExports.filter((p) => p.source.startsWith('./'));
const nonLocalExports = partExports.filter((p) => !p.source.startsWith('./'));
if (localExports.length === 0 && nonLocalExports.length === 0) return [];
const componentKebab = source.kebab;
const htmlDir = path.join(HTML_UI_PATH, componentKebab);
const parts: PartSource[] = [];
let hasPrimary = false;
for (const partExport of partExports) {
// Process local exports
for (const partExport of localExports) {
const kebab = partKebabFromSource(partExport.source, componentKebab);
// Look for sub-part element file: {component}-{part}-element.ts
const subPartElementFile = path.join(htmlDir, `${componentKebab}-${kebab}-element.ts`);
const hasSubPartElement = fs.existsSync(subPartElementFile);
// Primary part: no matching sub-part element, main element exists, and first match wins.
const isPrimary = !hasSubPartElement && !!source.htmlPath && !hasPrimary;
if (!hasSubPartElement && !!source.htmlPath && hasPrimary) {
log.warn(
`${source.name}: Part "${partExport.name}" also matches primary criteria and was skipped (first-match-wins)`
);
}
if (isPrimary) hasPrimary = true;
// Resolve React source path for JSDoc description extraction
const reactFile = path.join(path.dirname(source.partsIndexPath), `${partExport.source.replace('./', '')}.tsx`);
const reactFile = path.join(path.dirname(source.partsIndexPath!), `${partExport.source.replace('./', '')}.tsx`);
const reactPath = fs.existsSync(reactFile) ? reactFile : undefined;
// Primary detection: the part whose React source instantiates the Core class
const isPrimary = !!reactPath && instantiatesCore(reactPath);
const subPartUsesDataAttrs = !isPrimary && !!reactPath && usesDataAttrs(reactPath);
const part: PartSource = {
name: partExport.name,
localName: partExport.localName,
@@ -293,17 +368,94 @@ function discoverParts(source: ComponentSource, program: ts.Program): PartSource
isPrimary,
htmlPath: hasSubPartElement ? subPartElementFile : isPrimary ? source.htmlPath : undefined,
reactPath,
dataAttrsPath: subPartUsesDataAttrs ? source.dataAttrsPath : undefined,
dataAttrsComponentName: subPartUsesDataAttrs ? source.name : undefined,
};
if (!part.htmlPath) {
log.warn(`${source.name}: Part "${partExport.name}" has no matching HTML element file`);
}
parts.push(part);
}
if (!hasPrimary) {
log.warn(`${source.name}: No primary part identified (expected one part to use ${componentKebab}-element.ts)`);
// Resolve re-exported parts from other components
if (nonLocalExports.length > 0) {
// Group by source module path
const bySource = new Map<string, typeof nonLocalExports>();
for (const exp of nonLocalExports) {
const list = bySource.get(exp.source) ?? [];
list.push(exp);
bySource.set(exp.source, list);
}
const partsDir = path.dirname(source.partsIndexPath!);
for (const [sourcePath, exports] of bySource) {
// Resolve the origin index.parts.ts file
const originPartsFile = path.resolve(partsDir, `${sourcePath}.ts`);
if (!fs.existsSync(originPartsFile)) {
log.warn(`${source.name}: Re-export source not found: ${originPartsFile}`);
continue;
}
// Derive origin component kebab from directory name
const originKebab = path.basename(path.dirname(originPartsFile));
const originHtmlDir = path.join(HTML_UI_PATH, originKebab);
const originReactDir = path.dirname(originPartsFile);
// Parse origin's exports to match re-exported names to original local exports
const originExports = extractParts(originPartsFile, program);
for (const reExport of exports) {
// Find the matching export in the origin file
const originExport = originExports.find((o) => o.name === reExport.name);
if (!originExport) {
log.warn(`${source.name}: Re-exported part "${reExport.name}" not found in ${originPartsFile}`);
continue;
}
// Derive kebab from the origin export's source path
const kebab = partKebabFromSource(originExport.source, originKebab);
// Look for element file in origin's HTML directory
const subPartElementFile = path.join(originHtmlDir, `${originKebab}-${kebab}-element.ts`);
const hasSubPartElement = fs.existsSync(subPartElementFile);
// Resolve React source from origin component for JSDoc description
const reactFile = path.join(originReactDir, `${originExport.source.replace('./', '')}.tsx`);
const reactPath = fs.existsSync(reactFile) ? reactFile : undefined;
const reExportUsesDataAttrs = !!reactPath && usesDataAttrs(reactPath);
const originDataAttrsFile = path.join(CORE_UI_PATH, originKebab, `${originKebab}-data-attrs.ts`);
const originDataAttrsPath =
reExportUsesDataAttrs && fs.existsSync(originDataAttrsFile) ? originDataAttrsFile : undefined;
const originComponentName = originDataAttrsPath ? kebabToPascal(originKebab) : undefined;
const part: PartSource = {
name: reExport.name,
localName: originExport.localName,
kebab,
isPrimary: false, // Re-exported parts are never primary
htmlPath: hasSubPartElement ? subPartElementFile : undefined,
reactPath,
dataAttrsPath: originDataAttrsPath,
dataAttrsComponentName: originComponentName,
};
parts.push(part);
}
}
}
const primaryCount = parts.filter((p) => p.isPrimary).length;
if (primaryCount === 0) {
log.warn(`${source.name}: No primary part identified (no part instantiates a Core class)`);
} else if (primaryCount > 1) {
log.warn(`${source.name}: Multiple parts instantiate Core — only the first will be treated as primary`);
let foundFirst = false;
for (const p of parts) {
if (p.isPrimary) {
if (foundFirst) p.isPrimary = false;
foundFirst = true;
}
}
}
// Primary part first so it appears first in the docs.
@@ -313,17 +465,21 @@ function discoverParts(source: ComponentSource, program: ts.Program): PartSource
/**
* Build the API reference for a multi-part component.
*
* Multi-part components have empty top-level props/state/dataAttributes.
* Multi-part components have empty top-level props/state/dataAttributes/cssCustomProperties.
* All data is in the `parts` record.
*
* For the primary part:
* - Props and state come from the shared core file (`{name}-core.ts`)
* - Data attributes come from the shared data-attrs file (`{name}-data-attrs.ts`)
* - CSS custom properties come from the shared css-vars file (`{name}-css-vars.ts`)
* - HTML tag comes from the main element file (`{name}-element.ts`)
*
* For non-primary parts:
* - Props, state, and data attributes are empty (no dedicated core file)
* - Props, state, data attributes, and CSS vars are empty (no dedicated core file)
* - HTML tag comes from their sub-part element file (`{name}-{part}-element.ts`)
*
* All parts get `platforms.react` (they come from `index.parts.ts`).
* Parts with matching HTML element files also get `platforms.html`.
*/
function buildMultiPartReference(
source: ComponentSource,
@@ -340,9 +496,10 @@ function buildMultiPartReference(
: undefined;
if (part.isPrimary) {
// Primary part: extract from shared core and data-attrs
// Primary part: extract from shared core, data-attrs, and css-vars
const coreData = source.corePath ? extractCore(source.corePath, program, source.name) : null;
const dataAttrsData = source.dataAttrsPath ? extractDataAttrs(source.dataAttrsPath, program, source.name) : null;
const cssVarsData = source.cssVarsPath ? extractCSSVars(source.cssVarsPath, program, source.name) : null;
const elementName = `${source.name}Element`;
const htmlData = part.htmlPath ? extractHtml(part.htmlPath, program, source.name, elementName) : null;
@@ -353,7 +510,8 @@ function buildMultiPartReference(
props: coreData ? sortProps(buildProps(coreData)) : {},
state: coreData ? buildState(coreData) : {},
dataAttributes: dataAttrsData ? buildDataAttrs(dataAttrsData) : {},
platforms: {},
cssCustomProperties: cssVarsData ? buildCSSVars(cssVarsData) : {},
platforms: { react: {} },
};
if (!partRef.description) delete partRef.description;
@@ -363,17 +521,26 @@ function buildMultiPartReference(
partsRecord[part.kebab] = partRef;
} else {
// Non-primary part: extract only HTML tag
const elementName = `${source.name}${part.name}Element`;
const htmlData = part.htmlPath ? extractHtml(part.htmlPath, program, source.name, elementName) : null;
// Non-primary part: extract HTML tag, shared data attributes, and custom React props
const elementName = part.htmlPath ? kebabToPascal(path.basename(part.htmlPath, '.ts')) : undefined;
const htmlData =
part.htmlPath && elementName ? extractHtml(part.htmlPath, program, source.name, elementName) : null;
const dataAttrsData =
part.dataAttrsPath && part.dataAttrsComponentName
? extractDataAttrs(part.dataAttrsPath, program, part.dataAttrsComponentName)
: null;
const subPartProps = part.reactPath ? extractSubPartProps(part.reactPath, program, part.localName) : {};
const partRef: PartReference = {
name: part.name,
description,
props: {},
props: sortProps(subPartProps),
state: {},
dataAttributes: {},
platforms: {},
dataAttributes: dataAttrsData ? buildDataAttrs(dataAttrsData) : {},
cssCustomProperties: {},
platforms: { react: {} },
};
if (!partRef.description) delete partRef.description;
@@ -390,6 +557,7 @@ function buildMultiPartReference(
props: {},
state: {},
dataAttributes: {},
cssCustomProperties: {},
platforms: {},
parts: partsRecord,
};
@@ -401,7 +569,8 @@ function buildMultiPartReference(
function buildComponentReference(source: ComponentSource, program: ts.Program): ComponentReference | null {
if (source.partsIndexPath) {
const parts = discoverParts(source, program);
if (parts.length > 0) {
// Single-part fallback: when filtering leaves only 1 part, use single-part mode
if (parts.length > 1) {
return buildMultiPartReference(source, program, parts);
}
}
@@ -1,5 +1,6 @@
import * as ts from 'typescript';
import * as tae from 'typescript-api-extractor';
import type { PropDef } from './types.js';
export interface PartExport {
/** PascalCase exported part name (e.g., "Value", "Group", "Separator"). */
@@ -72,3 +73,43 @@ export function extractPartDescription(filePath: string, program: ts.Program, pa
if (desc) desc = desc.replace(/\n*@example[\s\S]*$/, '').trim();
return desc || undefined;
}
/**
* Extract custom React-specific props from a sub-part's Props interface.
*
* Walks syntactic own members of `{localName}Props` (excluding inherited
* `UIComponentProps` members and `children`).
*/
export function extractSubPartProps(filePath: string, program: ts.Program, localName: string): Record<string, PropDef> {
const sourceFile = program.getSourceFile(filePath);
if (!sourceFile) return {};
const checker = program.getTypeChecker();
const props: Record<string, PropDef> = {};
ts.forEachChild(sourceFile, function visit(node) {
if (ts.isInterfaceDeclaration(node) && node.name.text === `${localName}Props`) {
for (const member of node.members) {
if (!ts.isPropertySignature(member) || !member.name || !ts.isIdentifier(member.name)) continue;
const name = member.name.text;
if (name === 'children' || !member.type) continue;
let typeStr = checker.typeToString(checker.getTypeFromTypeNode(member.type));
if (member.questionToken) typeStr = typeStr.replace(/ \| undefined$/, '');
const propDef: PropDef = { type: typeStr };
const symbol = checker.getSymbolAtLocation(member.name);
if (symbol) {
const docs = symbol.getDocumentationComment(checker);
const desc = docs.map((d) => d.text).join('');
if (desc) propDef.description = desc;
}
props[name] = propDef;
}
}
ts.forEachChild(node, visit);
});
return props;
}
@@ -0,0 +1,121 @@
import { describe, expect, it } from 'vitest';
import { extractCSSVars } from '../css-vars-handler.js';
import { createTestProgram } from './test-utils.js';
describe('extractCSSVars', () => {
it('extracts from {Name}CSSVars constant', () => {
const code = `
export const MockComponentCSSVars = {
fill: '--media-slider-fill',
pointer: '--media-slider-pointer',
} as const;
`;
const program = createTestProgram(code);
const result = extractCSSVars('test.ts', program, 'MockComponent');
expect(result).not.toBeNull();
expect(result!.vars).toHaveLength(2);
expect(result!.vars[0]!.name).toBe('--media-slider-fill');
expect(result!.vars[1]!.name).toBe('--media-slider-pointer');
});
it('extracts JSDoc comments as descriptions', () => {
const code = `
export const MockComponentCSSVars = {
/** Fill level percentage (0-100). */
fill: '--media-slider-fill',
/** Pointer position percentage (0-100). */
pointer: '--media-slider-pointer',
} as const;
`;
const program = createTestProgram(code);
const result = extractCSSVars('test.ts', program, 'MockComponent');
expect(result).not.toBeNull();
expect(result!.vars[0]!.description).toBe('Fill level percentage (0-100).');
expect(result!.vars[1]!.description).toBe('Pointer position percentage (0-100).');
});
it('handles object without as const', () => {
const code = `
export const MockComponentCSSVars = {
fill: '--media-slider-fill',
};
`;
const program = createTestProgram(code);
const result = extractCSSVars('test.ts', program, 'MockComponent');
expect(result).not.toBeNull();
expect(result!.vars).toHaveLength(1);
expect(result!.vars[0]!.name).toBe('--media-slider-fill');
});
it('handles as const satisfies expression', () => {
const code = `
export const MockComponentCSSVars = {
fill: '--media-slider-fill',
} as const satisfies Record<string, string>;
`;
const program = createTestProgram(code);
const result = extractCSSVars('test.ts', program, 'MockComponent');
expect(result).not.toBeNull();
expect(result!.vars).toHaveLength(1);
expect(result!.vars[0]!.name).toBe('--media-slider-fill');
});
it('returns null when constant not found', () => {
const code = `
export const OtherConstant = {
fill: '--media-slider-fill',
};
`;
const program = createTestProgram(code);
const result = extractCSSVars('test.ts', program, 'MockComponent');
expect(result).toBeNull();
});
it('returns empty description when no JSDoc present', () => {
const code = `
export const MockComponentCSSVars = {
fill: '--media-slider-fill',
} as const;
`;
const program = createTestProgram(code);
const result = extractCSSVars('test.ts', program, 'MockComponent');
expect(result).not.toBeNull();
expect(result!.vars[0]!.description).toBe('');
});
it('skips properties with non-string-literal values', () => {
const code = `
const PREFIX = '--media-';
export const MockComponentCSSVars = {
fill: PREFIX + 'fill',
pointer: '--media-slider-pointer',
} as const;
`;
const program = createTestProgram(code);
const result = extractCSSVars('test.ts', program, 'MockComponent');
expect(result).not.toBeNull();
expect(result!.vars).toHaveLength(1);
expect(result!.vars[0]!.name).toBe('--media-slider-pointer');
});
it('extracts single-line // comments as descriptions', () => {
const code = `
export const MockComponentCSSVars = {
// Fill level percentage (0-100).
fill: '--media-slider-fill',
} as const;
`;
const program = createTestProgram(code);
const result = extractCSSVars('test.ts', program, 'MockComponent');
expect(result).not.toBeNull();
expect(result!.vars[0]!.description).toBe('Fill level percentage (0-100).');
});
});
@@ -81,6 +81,20 @@ describe('extractParts', () => {
expect(result).toEqual([{ name: 'Root', localName: 'ControlsRoot', source: './controls-root' }]);
});
it('includes non-local re-exports (caller is responsible for filtering)', () => {
const code = `
export { Root, type RootProps } from './slider-root';
export { Thumb, type ThumbProps } from '../slider/index.parts';
`;
const program = createTestProgram(code);
const result = extractParts('test.ts', program);
expect(result).toEqual([
{ name: 'Root', localName: 'Root', source: './slider-root' },
{ name: 'Thumb', localName: 'Thumb', source: '../slider/index.parts' },
]);
});
});
describe('extractPartDescription', () => {
@@ -4,6 +4,7 @@
*/
export type {
ComponentReference,
CSSVarDef,
DataAttrDef,
PartReference,
PropDef,
@@ -28,6 +29,10 @@ export interface PartSource {
htmlPath?: string;
/** Path to React component file (for JSDoc description extraction). */
reactPath?: string;
/** Path to data-attrs file for shared data attributes (sub-parts only). */
dataAttrsPath?: string;
/** PascalCase component name for data-attrs variable lookup (e.g., "Slider"). */
dataAttrsComponentName?: string;
}
/**
@@ -42,6 +47,8 @@ export interface ComponentSource {
corePath?: string;
/** Path to data attrs file */
dataAttrsPath?: string;
/** Path to CSS vars file */
cssVarsPath?: string;
/** Path to HTML element file */
htmlPath?: string;
/** Path to index.parts.ts (if multi-part) */
@@ -77,6 +84,10 @@ export interface DataAttrsExtraction {
attrs: Array<{ name: string; description: string; type?: string }>;
}
export interface CSSVarsExtraction {
vars: Array<{ name: string; description: string }>;
}
export interface HtmlExtraction {
tagName: string;
}