diff --git a/packages/@luke-ui/docs-tools/src/discover-exports.ts b/packages/@luke-ui/docs-tools/src/discover-exports.ts index c47bed3f..5b9d937c 100644 --- a/packages/@luke-ui/docs-tools/src/discover-exports.ts +++ b/packages/@luke-ui/docs-tools/src/discover-exports.ts @@ -56,18 +56,14 @@ export function discoverExports( const barrelPaths = new Set(options.barrelPaths ?? DEFAULT_BARREL_PATHS); const result: Array = []; for (const [path, target] of Object.entries(exportsField)) { - let shape: ExportShape; - let tier: ExportTier; - if (!JS_TARGET.test(target)) { - shape = 'asset'; - tier = 'n/a'; - } else if (barrelPaths.has(path)) { - shape = 'barrel'; - tier = 'n/a'; - } else { - shape = 'component'; - tier = path.endsWith('/primitive') ? 'primitive' : 'composed'; - } + const { shape, tier } = ((): { shape: ExportShape; tier: ExportTier } => { + if (!JS_TARGET.test(target)) return { shape: 'asset', tier: 'n/a' }; + if (barrelPaths.has(path)) return { shape: 'barrel', tier: 'n/a' }; + return { + shape: 'component', + tier: path.endsWith('/primitive') ? 'primitive' : 'composed', + }; + })(); const sourcePath = shape === 'asset' ? undefined : sourceFromExport(options.packageRoot, target); const pageKind = pageKindFor({ shape, sourcePath, tier }); diff --git a/packages/@luke-ui/docs-tools/src/parse-types.ts b/packages/@luke-ui/docs-tools/src/parse-types.ts index c5ecf1ca..127b550e 100644 --- a/packages/@luke-ui/docs-tools/src/parse-types.ts +++ b/packages/@luke-ui/docs-tools/src/parse-types.ts @@ -1,5 +1,6 @@ import type { ExpressionWithTypeArguments, + FunctionDeclaration, InterfaceDeclaration, PropertySignature, SourceFile, @@ -64,17 +65,15 @@ export function parseComponent(sourcePath: string): ParsedComponent { const propsLookup = createPropsLookup(sourceFile); const exportedFns = sourceFile.getFunctions(); - let exportedFn; - let documentedExportedFn; - for (let i = 0; i < exportedFns.length; i++) { - const fn = exportedFns[i]; - if (!fn?.isExported()) continue; - - documentedExportedFn ??= fn.getJsDocs().length > 0 ? fn : undefined; - + let exportedFn: FunctionDeclaration | undefined; + let documentedExportedFn: FunctionDeclaration | undefined; + for (const fn of exportedFns) { + if (!fn.isExported()) continue; + if (documentedExportedFn === undefined && fn.getJsDocs().length > 0) { + documentedExportedFn = fn; + } const name = fn.getName(); if (!name) continue; - const props = propsLookup.byName.get(`${name}Props`); if (props && readTierTag(props) !== undefined) { exportedFn = fn; @@ -151,12 +150,7 @@ function readTierTag(decl: PropsDeclaration): ExportTier | undefined { for (const tag of jsDoc.getTags()) { if (tag.getTagName() === 'tier') { const value = tag.getCommentText()?.trim().toLowerCase(); - switch (value) { - case 'atom': - case 'composed': - case 'primitive': - return value; - } + if (value === 'atom' || value === 'composed' || value === 'primitive') return value; } } } @@ -217,38 +211,34 @@ function readExtends(decl: InterfaceDeclaration): Array { const symbol = expr.getSymbol(); const declarations = symbol?.getDeclarations() ?? []; - if (declarations.length > 0) { - const firstDecl = declarations[0]; - const sourceFilePath = firstDecl?.getSourceFile().getFilePath() ?? ''; - - if (sourceFilePath.includes('/node_modules/') && !isTsBuiltinPath(sourceFilePath)) { - // Direct external reference (e.g. SomeExternalInterface) - result.push({ from: 'external', module: moduleFromPath(sourceFilePath), typeName }); - continue; + if (declarations.length === 0) { + // No declarations — check type arguments for external references + const resolved = resolveExternalFromTypeArgs(heritage); + if (resolved) { + result.push({ from: 'external', module: resolved.module, typeName: resolved.typeName }); + } else { + result.push({ from: 'external', typeName }); } + continue; + } - if (isTsBuiltinPath(sourceFilePath)) { - // TypeScript utility type (Omit, Pick, etc.) — look at type args for the real external dep - const resolved = resolveExternalFromTypeArgs(heritage); - if (resolved) { - result.push({ from: 'external', module: resolved.module, typeName: resolved.typeName }); - } - // Skip if the utility type only wraps local types - continue; - } + const firstDecl = declarations[0]; + const sourceFilePath = firstDecl?.getSourceFile().getFilePath() ?? ''; - // Local package type - result.push({ from: 'package', typeName }); + if (sourceFilePath.includes('/node_modules/') && !isTsBuiltinPath(sourceFilePath)) { + result.push({ from: 'external', module: moduleFromPath(sourceFilePath), typeName }); continue; } - // No declarations — check type arguments for external references - const resolved = resolveExternalFromTypeArgs(heritage); - if (resolved) { - result.push({ from: 'external', module: resolved.module, typeName: resolved.typeName }); - } else { - result.push({ from: 'external', typeName }); + if (isTsBuiltinPath(sourceFilePath)) { + const resolved = resolveExternalFromTypeArgs(heritage); + if (resolved) { + result.push({ from: 'external', module: resolved.module, typeName: resolved.typeName }); + } + continue; } + + result.push({ from: 'package', typeName }); } return result; } @@ -502,12 +492,10 @@ function appendPropNamesFromDeclaration( function toParsedProp(prop: PropertySignature): ParsedProp { const jsDoc = prop.getJsDocs()[0]; const description = jsDoc?.getDescription().trim() ?? ''; - let defaultValue: string | undefined; - for (const tag of jsDoc?.getTags() ?? []) { - if (tag.getTagName() === 'default') { - defaultValue = tag.getCommentText()?.trim(); - } - } + const defaultValue = (jsDoc?.getTags() ?? []) + .find((tag) => tag.getTagName() === 'default') + ?.getCommentText() + ?.trim(); return { default: defaultValue, description, diff --git a/packages/@luke-ui/rainbow-sprinkles/src/create-runtime-fn.ts b/packages/@luke-ui/rainbow-sprinkles/src/create-runtime-fn.ts index 52c3f8b3..df81ec9f 100644 --- a/packages/@luke-ui/rainbow-sprinkles/src/create-runtime-fn.ts +++ b/packages/@luke-ui/rainbow-sprinkles/src/create-runtime-fn.ts @@ -287,19 +287,13 @@ export function createRuntimeFn | undefined; - let styleCache: Map | undefined; - const c = cache.get(property); - if (c) { - classCache = c.class; - styleCache = c.style; - } else { - classCache = new Map(); - styleCache = new Map(); - cache.set(property, { class: classCache, style: styleCache }); + let propertyCache = cache.get(property); + if (!propertyCache) { + propertyCache = { class: new Map(), style: new Map() }; + cache.set(property, propertyCache); } - className.push(assignClasses(propertyConfig, propValue, classCache)); - assignVars(style, propertyConfig, propValue, styleCache); + className.push(assignClasses(propertyConfig, propValue, propertyCache.class)); + assignVars(style, propertyConfig, propValue, propertyCache.style); } return { ...otherProps, diff --git a/packages/@luke-ui/rainbow-sprinkles/src/define-properties.ts b/packages/@luke-ui/rainbow-sprinkles/src/define-properties.ts index 32308b02..ad37d179 100644 --- a/packages/@luke-ui/rainbow-sprinkles/src/define-properties.ts +++ b/packages/@luke-ui/rainbow-sprinkles/src/define-properties.ts @@ -20,6 +20,38 @@ function mapValues, R>( return result; } +type ConditionValue = { + '@media'?: string; + '@supports'?: string; + '@container'?: string; + selector?: string; +}; + +/** + * Wrap a base style object in each active condition (media query, supports, + * container, or selector) declared on the condition. Conditions nest in + * declaration order, matching vanilla-extract's layered style rule shape. + */ +function wrapConditionStyle( + base: Record, + condition: ConditionValue, +): Record { + let styleValue: Record = base; + if (condition['@media']) { + styleValue = { '@media': { [condition['@media']]: styleValue } }; + } + if (condition['@supports']) { + styleValue = { '@supports': { [condition['@supports']]: styleValue } }; + } + if (condition['@container']) { + styleValue = { '@container': { [condition['@container']]: styleValue } }; + } + if (condition.selector) { + styleValue = { selectors: { [condition.selector]: styleValue } }; + } + return styleValue; +} + function createStyles( property: string, scale: Record | true, @@ -33,7 +65,7 @@ function createStyles( name: string; } { if (!conditions) { - const cssVar = createVar(property)!; + const cssVar = createVar(property); const styleValue = { [property]: cssVar }; const className = style( options['@layer'] ? { '@layer': { [options['@layer']]: styleValue } } : styleValue, @@ -46,34 +78,21 @@ function createStyles( vars: { default: cssVar }, }; } - const vars = mapValues( - conditions, - (_, conditionName) => createVar(`${property}-${conditionName}`)!, + const vars = mapValues(conditions, (_, conditionName) => + createVar(`${property}-${conditionName}`), ); const classes = mapValues(conditions, (conditionValue, conditionName) => { - let styleValue: Record = { [property]: vars[conditionName] }; - if (conditionValue['@media']) { - styleValue = { '@media': { [conditionValue['@media']]: styleValue } }; - } - if (conditionValue['@supports']) { - styleValue = { '@supports': { [conditionValue['@supports']]: styleValue } }; - } - if (conditionValue['@container']) { - styleValue = { '@container': { [conditionValue['@container']]: styleValue } }; - } - if (conditionValue.selector) { - styleValue = { selectors: { [conditionValue.selector]: styleValue } }; - } + const styleValue = wrapConditionStyle({ [property]: vars[conditionName] }, conditionValue); return style( options['@layer'] ? { '@layer': { [options['@layer']]: styleValue } } : styleValue, `${property}-${conditionName}`, ); }); return { - dynamic: { conditions: classes, default: classes[defaultCondition]! }, + dynamic: { conditions: classes, default: classes[defaultCondition] ?? '' }, dynamicScale: scale, name: property, - vars: { conditions: vars, default: vars[defaultCondition]! }, + vars: { conditions: vars, default: vars[defaultCondition] ?? '' }, }; } @@ -105,23 +124,7 @@ function createStaticStyles( }; } const classes = mapValues(conditions, (conditionValue, conditionName) => { - let conditionalStyleValue: Record = { [property]: scaleValue }; - if (conditionValue['@media']) { - conditionalStyleValue = { '@media': { [conditionValue['@media']]: conditionalStyleValue } }; - } - if (conditionValue['@supports']) { - conditionalStyleValue = { - '@supports': { [conditionValue['@supports']]: conditionalStyleValue }, - }; - } - if (conditionValue['@container']) { - conditionalStyleValue = { - '@container': { [conditionValue['@container']]: conditionalStyleValue }, - }; - } - if (conditionValue.selector) { - conditionalStyleValue = { selectors: { [conditionValue.selector]: conditionalStyleValue } }; - } + const conditionalStyleValue = wrapConditionStyle({ [property]: scaleValue }, conditionValue); return style( options['@layer'] ? { '@layer': { [options['@layer']]: conditionalStyleValue } } @@ -129,7 +132,7 @@ function createStaticStyles( `${property}-${scaleKey}-${conditionName}`, ); }); - return { conditions: classes, default: classes[defaultCondition]! }; + return { conditions: classes, default: classes[defaultCondition] ?? '' }; }); return { name: property, staticScale: scale, values }; } @@ -157,13 +160,11 @@ export function defineProperties(options: DefinePropertiesOptions): DefineProper const config: SprinkleProperties = {}; if (dynamicProperties) { - for (const dynamicProp of Object.keys(dynamicProperties)) { + for (const [dynamicProp, scale] of Object.entries(dynamicProperties)) { config[dynamicProp] = createStyles( dynamicProp, - // biome-ignore lint/suspicious/noExplicitAny: dynamic property access - (dynamicProperties as Record)[dynamicProp] as - | Record - | true, + // biome-ignore lint/suspicious/noExplicitAny: CSS property scales are dynamic + scale as Record | true, conditions, defaultCondition ?? '', { '@layer': options['@layer'] }, @@ -172,13 +173,11 @@ export function defineProperties(options: DefinePropertiesOptions): DefineProper } if (staticProperties) { - for (const staticProp of Object.keys(staticProperties)) { + for (const [staticProp, scale] of Object.entries(staticProperties)) { const staticStyle = createStaticStyles( staticProp, - // biome-ignore lint/suspicious/noExplicitAny: dynamic property access - (staticProperties as Record)[staticProp] as - | ReadonlyArray - | Record, + // biome-ignore lint/suspicious/noExplicitAny: CSS property scales are dynamic + scale as ReadonlyArray | Record, conditions, defaultCondition ?? '', { '@layer': options['@layer'] }, @@ -186,5 +185,6 @@ export function defineProperties(options: DefinePropertiesOptions): DefineProper config[staticProp] = Object.assign({}, config[staticProp], staticStyle); } } + return { config }; } diff --git a/packages/@luke-ui/react/src/icon/index.tsx b/packages/@luke-ui/react/src/icon/index.tsx index 34816c76..d2b09ab5 100644 --- a/packages/@luke-ui/react/src/icon/index.tsx +++ b/packages/@luke-ui/react/src/icon/index.tsx @@ -37,16 +37,6 @@ export function IconSpritesheetProvider({ return {children}; } -function useIconSpritesheetHref(): string { - const href = useContext(IconSpritesheetContext); - if (!href) { - throw new Error( - 'IconSpritesheetProvider is required. Wrap your app with .', - ); - } - return href; -} - /** * Props for the built-in `Icon` component. * @@ -109,6 +99,22 @@ export function createIcon({ }; } +/** Renders an icon from the shared sprite sheet. */ +export function Icon(props: IconProps): JSX.Element { + const spritesheetHref = useIconSpritesheetHref(); + return ; +} + +function useIconSpritesheetHref(): string { + const href = useContext(IconSpritesheetContext); + if (!href) { + throw new Error( + 'IconSpritesheetProvider is required. Wrap your app with .', + ); + } + return href; +} + type SpritesheetIconProps = IconProps & { spritesheetHref: string; name: (typeof iconNames)[number]; @@ -118,9 +124,3 @@ const SpritesheetIcon = createIcon({ path: ({ name, spritesheetHref }) => , viewBox: ({ name }) => iconViewBoxes[name], }); - -/** Renders an icon from the shared sprite sheet. */ -export function Icon(props: IconProps): JSX.Element { - const spritesheetHref = useIconSpritesheetHref(); - return ; -} diff --git a/packages/@luke-ui/react/src/styles/vars.css.ts b/packages/@luke-ui/react/src/styles/vars.css.ts index 2bc6eb8e..b8bd0179 100644 --- a/packages/@luke-ui/react/src/styles/vars.css.ts +++ b/packages/@luke-ui/react/src/styles/vars.css.ts @@ -1,12 +1,6 @@ import { createTheme, createThemeContract } from '@vanilla-extract/css'; -import { - colorToCssString, - cubicBezierToString, - dimensionToRemString, - durationToString, -} from '../tokens/converters.js'; +import { toCssValue } from '../tokens/converters.js'; import { tokenKeys } from '../tokens/groups.js'; -import type { ColorTokenValue } from '../tokens/index.js'; import { tokens } from '../tokens/index.js'; import { classSelector, lukeUiClassNames } from './class-names.js'; import { globalStyleInLayer } from './layered-style.css.js'; @@ -27,30 +21,13 @@ function toContract(group: T): Record, nu return contract; } -function toCssValue(type: string, value: unknown): string { - if (type === 'color') { - return colorToCssString(value as ColorTokenValue); - } - - if (type === 'cubicBezier') { - return cubicBezierToString(value as [number, number, number, number]); - } - - if (type === 'dimension') { - return dimensionToRemString(value as { unit: 'px' | 'rem'; value: number }); - } - - if (type === 'duration') { - return durationToString(value as { unit: 'ms' | 's'; value: number }); - } - - return String(value); -} - function toStringValues(group: T): Record, string> { const values = {} as Record, string>; for (const key of tokenKeys(group)) { - values[key] = toCssValue(group.$type, (group[key] as { $value: unknown }).$value); + const entry = group[key]; + if (entry && typeof entry === 'object' && '$value' in entry) { + values[key] = toCssValue(group.$type, entry.$value); + } } return values; } diff --git a/packages/@luke-ui/react/src/tokens/converters.ts b/packages/@luke-ui/react/src/tokens/converters.ts index 311ffb25..0f7168c8 100644 --- a/packages/@luke-ui/react/src/tokens/converters.ts +++ b/packages/@luke-ui/react/src/tokens/converters.ts @@ -52,3 +52,63 @@ function formatNumber(value: number): string { const normalized = Object.is(value, -0) ? 0 : value; return `${normalized}`; } + +export function isColorTokenValue(v: unknown): v is ColorTokenValue { + return ( + typeof v === 'object' && + v !== null && + 'colorSpace' in v && + typeof v.colorSpace === 'string' && + 'components' in v && + Array.isArray(v.components) + ); +} + +function isCubicBezierTokenValue(v: unknown): v is CubicBezierTokenValue { + return Array.isArray(v) && v.length === 4 && v.every((n) => typeof n === 'number'); +} + +function isDimensionTokenValue(v: unknown): v is DimensionTokenValue { + return ( + typeof v === 'object' && + v !== null && + 'unit' in v && + (v.unit === 'px' || v.unit === 'rem') && + 'value' in v && + typeof v.value === 'number' + ); +} + +function isDurationTokenValue(v: unknown): v is DurationTokenValue { + return ( + typeof v === 'object' && + v !== null && + 'unit' in v && + (v.unit === 'ms' || v.unit === 's') && + 'value' in v && + typeof v.value === 'number' + ); +} + +/** + * Convert a token value to its CSS string form, dispatching on the token's + * `$type`. Values whose shape does not match the declared type fall back to + * `String(value)` so a malformed token never crashes the build. + */ +export function toCssValue(type: string, value: unknown): string { + switch (type) { + case 'color': + if (isColorTokenValue(value)) return colorToCssString(value); + break; + case 'cubicBezier': + if (isCubicBezierTokenValue(value)) return cubicBezierToString(value); + break; + case 'dimension': + if (isDimensionTokenValue(value)) return dimensionToRemString(value); + break; + case 'duration': + if (isDurationTokenValue(value)) return durationToString(value); + break; + } + return String(value); +} diff --git a/packages/@luke-ui/react/src/tokens/index.ts b/packages/@luke-ui/react/src/tokens/index.ts index ec1d3989..b22e9e2b 100644 --- a/packages/@luke-ui/react/src/tokens/index.ts +++ b/packages/@luke-ui/react/src/tokens/index.ts @@ -67,6 +67,8 @@ export { cubicBezierToString, dimensionToRemString, durationToString, + isColorTokenValue, + toCssValue, } from './converters.js'; export { tokenKeys, toTokenGroup } from './groups.js'; export type { Breakpoint } from './media-queries.js'; diff --git a/packages/@luke-ui/react/src/tokens/tokens-vars.stories.tsx b/packages/@luke-ui/react/src/tokens/tokens-vars.stories.tsx index c1994995..e7ff52b9 100644 --- a/packages/@luke-ui/react/src/tokens/tokens-vars.stories.tsx +++ b/packages/@luke-ui/react/src/tokens/tokens-vars.stories.tsx @@ -1,19 +1,13 @@ import { Heading } from '@luke-ui/react/heading'; import { Text } from '@luke-ui/react/text'; import { vars } from '@luke-ui/react/theme'; -import type { - ColorTokenValue, - CubicBezierTokenValue, - DimensionTokenValue, - DurationTokenValue, -} from '@luke-ui/react/tokens'; +import type { ColorTokenValue } from '@luke-ui/react/tokens'; import { colorToCssString, - cubicBezierToString, - dimensionToRemString, - durationToString, + isColorTokenValue, tokenKeys, tokens, + toCssValue, } from '@luke-ui/react/tokens'; import ColorJs from 'colorjs.io'; import type { CSSProperties } from 'react'; @@ -387,26 +381,6 @@ function formatRawValue(value: unknown): string { return JSON.stringify(value); } -function toCssValue(type: string, value: unknown): string { - if (type === 'color') { - return colorToCssString(value as ColorTokenValue); - } - - if (type === 'cubicBezier') { - return cubicBezierToString(value as CubicBezierTokenValue); - } - - if (type === 'dimension') { - return dimensionToRemString(value as DimensionTokenValue); - } - - if (type === 'duration') { - return durationToString(value as DurationTokenValue); - } - - return String(value); -} - function colorToDisplayValue(value: ColorTokenValue): string { if (value.colorSpace === 'srgb' && value.alpha === undefined) { try { @@ -465,10 +439,12 @@ function getColorRows(groupName: string, group: StoryTokenGroup): Array { - const colorValue = value as ColorTokenValue; + if (!isColorTokenValue(value)) { + return { cssValue: String(value), displayValue: String(value) }; + } return { - cssValue: colorToCssString(colorValue), - displayValue: colorToDisplayValue(colorValue), + cssValue: colorToCssString(value), + displayValue: colorToDisplayValue(value), }; }); } @@ -536,7 +512,7 @@ function getVarRows(node: unknown, path: Array = []): Array { return Object.entries(node).flatMap(([key, value]) => getVarRows(value, [...path, key])); } -function renderTokenPreview(row: TokenRow) { +function TokenPreview({ row }: { row: TokenRow }) { if (row.type === 'color') { return ; } @@ -570,7 +546,7 @@ function renderTokenPreview(row: TokenRow) { return -; } -function renderVarPreview(row: VarRow) { +function VarPreview({ row }: { row: VarRow }) { if (row.path.includes('foregroundColor')) { return Aa; } @@ -813,7 +789,9 @@ function TokensVarsReference() { {row.type} {row.value} {row.cssValue} - {renderTokenPreview(row)} + + + ))} @@ -844,7 +822,9 @@ function TokensVarsReference() { {row.path} {row.cssVar} - {renderVarPreview(row)} + + + ))}