Create first version of useStyles hook
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -17,8 +17,8 @@
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, Ref } from 'react';
|
||||
import { Button as RAButton } from 'react-aria-components';
|
||||
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
|
||||
import type { ButtonProps } from './types';
|
||||
import { useStyles } from '../../definition';
|
||||
|
||||
/** @public */
|
||||
export const Button = forwardRef(
|
||||
@@ -33,15 +33,16 @@ export const Button = forwardRef(
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const responsiveSize = useResponsiveValue(size);
|
||||
const responsiveVariant = useResponsiveValue(variant);
|
||||
const { classNames, dataAttributes } = useStyles('Button', {
|
||||
size,
|
||||
variant,
|
||||
});
|
||||
|
||||
return (
|
||||
<RAButton
|
||||
className={clsx('canon-Button', className)}
|
||||
data-variant={responsiveVariant}
|
||||
data-size={responsiveSize}
|
||||
className={clsx(classNames.root, className)}
|
||||
ref={ref}
|
||||
{...dataAttributes}
|
||||
{...rest}
|
||||
>
|
||||
{iconStart}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { useBreakpoint, breakpoints } from './useBreakpoint';
|
||||
import { componentDefinitions } from '../utils/componentDefinitions';
|
||||
import type { ComponentDefinitionName } from '../types';
|
||||
|
||||
/**
|
||||
* Resolve a responsive value based on the current breakpoint
|
||||
* @param value - The responsive value (string or object with breakpoint keys)
|
||||
* @param breakpoint - The current breakpoint
|
||||
* @returns The resolved value for the current breakpoint
|
||||
*/
|
||||
function resolveResponsiveValue(
|
||||
value: string | Record<string, string>,
|
||||
breakpoint: string,
|
||||
): string | undefined {
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
const index = breakpoints.findIndex(b => b.id === breakpoint);
|
||||
|
||||
// Look for value at current breakpoint or smaller
|
||||
for (let i = index; i >= 0; i--) {
|
||||
if (value[breakpoints[i].id]) {
|
||||
return value[breakpoints[i].id];
|
||||
}
|
||||
}
|
||||
|
||||
// If no value found, check from smallest breakpoint up
|
||||
for (let i = 0; i < breakpoints.length; i++) {
|
||||
if (value[breakpoints[i].id]) {
|
||||
return value[breakpoints[i].id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook to get class names and data attributes for a component with responsive support
|
||||
* @param componentName - The name of the component
|
||||
* @param props - Object with prop values (can be responsive)
|
||||
* @returns Object with classNames and dataAttributes
|
||||
*/
|
||||
export function useStyles<T extends ComponentDefinitionName>(
|
||||
componentName: T,
|
||||
props: Record<string, any> = {},
|
||||
) {
|
||||
const { breakpoint } = useBreakpoint();
|
||||
const classNames = componentDefinitions[componentName].classNames;
|
||||
|
||||
// Resolve responsive values and generate data attributes
|
||||
const dataAttributes: Record<string, string> = {};
|
||||
const resolvedProps: Record<string, string> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
const resolvedValue = resolveResponsiveValue(value, breakpoint);
|
||||
if (resolvedValue !== undefined) {
|
||||
resolvedProps[key] = resolvedValue;
|
||||
dataAttributes[`data-${key}`] = resolvedValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
classNames,
|
||||
dataAttributes,
|
||||
resolvedProps, // Also return resolved props for convenience
|
||||
};
|
||||
}
|
||||
@@ -57,3 +57,6 @@ export * from './props';
|
||||
|
||||
// Hooks
|
||||
export { useBreakpoint } from './hooks/useBreakpoint';
|
||||
|
||||
// Component Definitions
|
||||
export * from './utils/componentDefinitions';
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { componentDefinitions } from './utils/componentDefinitions';
|
||||
|
||||
/** @public */
|
||||
export type AsProps =
|
||||
| 'div'
|
||||
@@ -131,3 +133,36 @@ export interface UtilityProps extends SpaceProps {
|
||||
justifyContent?: Responsive<JustifyContent>;
|
||||
rowSpan?: Responsive<Columns | 'full'>;
|
||||
}
|
||||
|
||||
// Base types for the component styles structure
|
||||
type ClassNamesMap = Record<string, string>;
|
||||
type DataAttributeValues = readonly (string | number | boolean)[];
|
||||
type DataAttributesMap = Record<string, DataAttributeValues>;
|
||||
|
||||
export interface ComponentDefinition {
|
||||
classNames: ClassNamesMap;
|
||||
dataAttributes?: DataAttributesMap;
|
||||
}
|
||||
|
||||
// Type utilities for extracting information from the component styles
|
||||
export type ComponentDefinitionName = keyof typeof componentDefinitions;
|
||||
|
||||
export type ComponentClassNames<T extends ComponentDefinitionName> =
|
||||
(typeof componentDefinitions)[T]['classNames'];
|
||||
|
||||
export type ComponentDataAttributes<T extends ComponentDefinitionName> =
|
||||
(typeof componentDefinitions)[T] extends { dataAttributes: infer DA }
|
||||
? DA
|
||||
: Record<string, never>;
|
||||
|
||||
export type ComponentDataAttributeProps<T extends ComponentDefinitionName> = {
|
||||
[K in keyof ComponentDataAttributes<T>]?: ComponentDataAttributes<T>[K] extends readonly (infer U)[]
|
||||
? U
|
||||
: never;
|
||||
};
|
||||
|
||||
// Helper type to check if a component has data attributes
|
||||
export type HasDataAttributes<T extends ComponentDefinitionName> =
|
||||
(typeof componentDefinitions)[T] extends { dataAttributes: any }
|
||||
? true
|
||||
: false;
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ComponentDefinition } from '../types';
|
||||
|
||||
export const componentDefinitions = {
|
||||
// Layout Components
|
||||
Box: {
|
||||
classNames: {
|
||||
root: 'canon-Box',
|
||||
},
|
||||
},
|
||||
|
||||
Container: {
|
||||
classNames: {
|
||||
root: 'canon-Container',
|
||||
},
|
||||
},
|
||||
|
||||
Flex: {
|
||||
classNames: {
|
||||
root: 'canon-Flex',
|
||||
},
|
||||
},
|
||||
|
||||
Grid: {
|
||||
classNames: {
|
||||
root: 'canon-Grid',
|
||||
},
|
||||
},
|
||||
|
||||
// Typography Components
|
||||
Text: {
|
||||
classNames: {
|
||||
root: 'canon-Text',
|
||||
},
|
||||
dataAttributes: {
|
||||
variant: ['body', 'caption', 'label'] as const,
|
||||
weight: ['regular', 'medium', 'bold'] as const,
|
||||
color: ['primary', 'secondary', 'muted'] as const,
|
||||
truncate: [true, false] as const,
|
||||
},
|
||||
},
|
||||
|
||||
Heading: {
|
||||
classNames: {
|
||||
root: 'canon-Heading',
|
||||
},
|
||||
dataAttributes: {
|
||||
variant: ['title1', 'title2', 'title3', 'subtitle'] as const,
|
||||
color: ['primary', 'secondary', 'muted'] as const,
|
||||
truncate: [true, false] as const,
|
||||
},
|
||||
},
|
||||
|
||||
// Form Components
|
||||
Button: {
|
||||
classNames: {
|
||||
root: 'canon-Button',
|
||||
},
|
||||
dataAttributes: {
|
||||
size: ['small', 'medium', 'large'] as const,
|
||||
variant: ['primary', 'secondary', 'ghost'] as const,
|
||||
},
|
||||
},
|
||||
|
||||
ButtonIcon: {
|
||||
classNames: {
|
||||
root: 'canon-ButtonIcon',
|
||||
},
|
||||
dataAttributes: {
|
||||
size: ['small', 'medium', 'large'] as const,
|
||||
variant: ['primary', 'secondary', 'ghost'] as const,
|
||||
},
|
||||
},
|
||||
|
||||
ButtonLink: {
|
||||
classNames: {
|
||||
root: 'canon-ButtonLink',
|
||||
},
|
||||
},
|
||||
|
||||
TextField: {
|
||||
classNames: {
|
||||
root: 'canon-TextField',
|
||||
input: 'canon-TextField-input',
|
||||
label: 'canon-TextField-label',
|
||||
error: 'canon-TextField-error',
|
||||
helper: 'canon-TextField-helper',
|
||||
container: 'canon-TextField-container',
|
||||
},
|
||||
dataAttributes: {
|
||||
invalid: [true, false] as const,
|
||||
disabled: [true, false] as const,
|
||||
},
|
||||
},
|
||||
|
||||
FieldLabel: {
|
||||
classNames: {
|
||||
root: 'canon-FieldLabel',
|
||||
required: 'canon-FieldLabel-required',
|
||||
},
|
||||
dataAttributes: {
|
||||
required: [true, false] as const,
|
||||
},
|
||||
},
|
||||
|
||||
Select: {
|
||||
classNames: {
|
||||
root: 'canon-Select',
|
||||
trigger: 'canon-Select-trigger',
|
||||
content: 'canon-Select-content',
|
||||
item: 'canon-Select-item',
|
||||
value: 'canon-Select-value',
|
||||
icon: 'canon-Select-icon',
|
||||
separator: 'canon-Select-separator',
|
||||
},
|
||||
dataAttributes: {
|
||||
invalid: [true, false] as const,
|
||||
disabled: [true, false] as const,
|
||||
open: [true, false] as const,
|
||||
},
|
||||
},
|
||||
|
||||
Checkbox: {
|
||||
classNames: {
|
||||
root: 'canon-Checkbox',
|
||||
input: 'canon-Checkbox-input',
|
||||
indicator: 'canon-Checkbox-indicator',
|
||||
label: 'canon-Checkbox-label',
|
||||
},
|
||||
dataAttributes: {
|
||||
checked: [true, false] as const,
|
||||
indeterminate: [true, false] as const,
|
||||
disabled: [true, false] as const,
|
||||
},
|
||||
},
|
||||
|
||||
Switch: {
|
||||
classNames: {
|
||||
root: 'canon-Switch',
|
||||
thumb: 'canon-Switch-thumb',
|
||||
track: 'canon-Switch-track',
|
||||
},
|
||||
dataAttributes: {
|
||||
checked: [true, false] as const,
|
||||
disabled: [true, false] as const,
|
||||
},
|
||||
},
|
||||
|
||||
// Navigation Components
|
||||
Link: {
|
||||
classNames: {
|
||||
root: 'canon-Link',
|
||||
},
|
||||
dataAttributes: {
|
||||
variant: ['primary', 'secondary'] as const,
|
||||
},
|
||||
},
|
||||
|
||||
Menu: {
|
||||
classNames: {
|
||||
root: 'canon-Menu',
|
||||
trigger: 'canon-Menu-trigger',
|
||||
content: 'canon-Menu-content',
|
||||
item: 'canon-Menu-item',
|
||||
separator: 'canon-Menu-separator',
|
||||
label: 'canon-Menu-label',
|
||||
group: 'canon-Menu-group',
|
||||
},
|
||||
},
|
||||
|
||||
Tabs: {
|
||||
classNames: {
|
||||
root: 'canon-Tabs',
|
||||
list: 'canon-Tabs-list',
|
||||
tab: 'canon-Tabs-tab',
|
||||
panel: 'canon-Tabs-panel',
|
||||
trigger: 'canon-Tabs-trigger',
|
||||
},
|
||||
},
|
||||
|
||||
// Data Display Components
|
||||
Table: {
|
||||
classNames: {
|
||||
root: 'canon-Table',
|
||||
header: 'canon-Table-header',
|
||||
body: 'canon-Table-body',
|
||||
footer: 'canon-Table-footer',
|
||||
row: 'canon-Table-row',
|
||||
cell: 'canon-Table-cell',
|
||||
headerCell: 'canon-Table-headerCell',
|
||||
caption: 'canon-Table-caption',
|
||||
},
|
||||
},
|
||||
|
||||
DataTable: {
|
||||
classNames: {
|
||||
root: 'canon-DataTable',
|
||||
container: 'canon-DataTable-container',
|
||||
header: 'canon-DataTable-header',
|
||||
body: 'canon-DataTable-body',
|
||||
row: 'canon-DataTable-row',
|
||||
cell: 'canon-DataTable-cell',
|
||||
headerCell: 'canon-DataTable-headerCell',
|
||||
toolbar: 'canon-DataTable-toolbar',
|
||||
pagination: 'canon-DataTable-pagination',
|
||||
search: 'canon-DataTable-search',
|
||||
filters: 'canon-DataTable-filters',
|
||||
},
|
||||
},
|
||||
|
||||
Avatar: {
|
||||
classNames: {
|
||||
root: 'canon-Avatar',
|
||||
image: 'canon-Avatar-image',
|
||||
fallback: 'canon-Avatar-fallback',
|
||||
indicator: 'canon-Avatar-indicator',
|
||||
},
|
||||
dataAttributes: {
|
||||
size: ['small', 'medium', 'large'] as const,
|
||||
},
|
||||
},
|
||||
|
||||
Icon: {
|
||||
classNames: {
|
||||
root: 'canon-Icon',
|
||||
},
|
||||
dataAttributes: {
|
||||
size: ['small', 'medium', 'large'] as const,
|
||||
},
|
||||
},
|
||||
|
||||
// Feedback Components
|
||||
Tooltip: {
|
||||
classNames: {
|
||||
root: 'canon-Tooltip',
|
||||
trigger: 'canon-Tooltip-trigger',
|
||||
content: 'canon-Tooltip-content',
|
||||
arrow: 'canon-Tooltip-arrow',
|
||||
},
|
||||
},
|
||||
|
||||
// Disclosure Components
|
||||
Collapsible: {
|
||||
classNames: {
|
||||
root: 'canon-Collapsible',
|
||||
trigger: 'canon-Collapsible-trigger',
|
||||
content: 'canon-Collapsible-content',
|
||||
icon: 'canon-Collapsible-icon',
|
||||
},
|
||||
dataAttributes: {
|
||||
open: [true, false] as const,
|
||||
},
|
||||
},
|
||||
|
||||
// Utility Components
|
||||
ScrollArea: {
|
||||
classNames: {
|
||||
root: 'canon-ScrollArea',
|
||||
viewport: 'canon-ScrollArea-viewport',
|
||||
scrollbar: 'canon-ScrollArea-scrollbar',
|
||||
thumb: 'canon-ScrollArea-thumb',
|
||||
corner: 'canon-ScrollArea-corner',
|
||||
track: 'canon-ScrollArea-track',
|
||||
},
|
||||
},
|
||||
} as const satisfies Record<string, ComponentDefinition>;
|
||||
Reference in New Issue
Block a user