refactor(ui): add surface prop types and validation to defineComponent

Adds shared prop types for surface-aware components and type-level
validation to ensure proper surface prop declarations.

- Add `LeafSurfaceProps` and `ContainerSurfaceProps` shared types
- Add `SurfacePropsConstraint` type that errors if `surface: 'leaf'`
  is set but `onSurface` is missing from props (and vice versa for
  container components)
- Update Button to extend `LeafSurfaceProps` instead of declaring
  `onSurface` manually

Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
Johan Persson
2026-01-19 10:49:05 +01:00
parent a3d776707d
commit ec0dc5b835
4 changed files with 34 additions and 11 deletions
+3 -9
View File
@@ -16,21 +16,15 @@
import type { ReactElement, ReactNode, CSSProperties } from 'react';
import type { ButtonProps as RAButtonProps } from 'react-aria-components';
import type { Responsive, Surface } from '../../types';
import type { LeafSurfaceProps, Responsive } from '../../types';
/**
* Button's own properties (excluding inherited React Aria props)
*
* @public
*/
export type ButtonOwnProps = {
/** @public */
export type ButtonOwnProps = LeafSurfaceProps & {
size?: Responsive<'small' | 'medium' | 'large'>;
variant?: Responsive<'primary' | 'secondary' | 'tertiary'>;
iconStart?: ReactElement;
iconEnd?: ReactElement;
loading?: boolean;
/** Surface the button is placed on. Defaults to context surface if available */
onSurface?: Responsive<Surface>;
children?: ReactNode;
className?: string;
style?: CSSProperties;
@@ -14,13 +14,13 @@
* limitations under the License.
*/
import type { ComponentConfig } from './types';
import type { ComponentConfig, SurfacePropsConstraint } from './types';
export function defineComponent<P extends Record<string, any>>() {
return <
const S extends Record<string, string>,
const C extends ComponentConfig<P, S>,
>(
config: C,
config: C & SurfacePropsConstraint<P, C['surface']>,
): C => config;
}
@@ -39,6 +39,25 @@ export interface ComponentConfig<
surface?: 'container' | 'leaf';
}
/**
* Type constraint that validates surface props are present in the props type.
* - If surface is 'leaf', P must include 'onSurface'
* - If surface is 'container', P must include 'surface'
*/
export type SurfacePropsConstraint<P, Surface> = Surface extends 'leaf'
? 'onSurface' extends keyof P
? {}
: {
__error: 'Leaf components must include onSurface in props type. Extend LeafProps.';
}
: Surface extends 'container'
? 'surface' extends keyof P
? {}
: {
__error: 'Container components must include surface in props type. Extend ContainerProps.';
}
: {};
export interface UseDefinitionOptions<D extends ComponentConfig<any, any>> {
utilityTarget?: keyof D['classNames'] | null;
classNameTarget?: keyof D['classNames'] | null;
+10
View File
@@ -191,3 +191,13 @@ export type Surface =
| 'warning'
| 'success'
| 'auto';
/** @public */
export interface LeafSurfaceProps {
onSurface?: Responsive<Surface>;
}
/** @public */
export interface ContainerSurfaceProps {
surface?: Responsive<Surface>;
}