feat: add token system and ui contracts
This commit is contained in:
@@ -1,4 +1,35 @@
|
||||
export { cn } from "./lib/cn";
|
||||
export { cva, cx, type VariantProps } from "./lib/cva";
|
||||
export { motionDurations, motionEasings } from "./lib/motion";
|
||||
|
||||
export {
|
||||
authoringChecklist,
|
||||
commonSlotNames,
|
||||
commonStateNames,
|
||||
createDataAttributes,
|
||||
createSlot,
|
||||
cvaConventions,
|
||||
dataAttr,
|
||||
withRootProps,
|
||||
type AsChildProp,
|
||||
type ButtonLikeElementProps,
|
||||
type CommonComponentProps,
|
||||
type ControllableStateProps,
|
||||
type DataAttributes,
|
||||
type DisableableProps,
|
||||
type FieldStateProps,
|
||||
type InputLikeElementProps,
|
||||
type InvalidatableProps,
|
||||
type LoadingProps,
|
||||
type PressableStateProps,
|
||||
type ReadonlyProps,
|
||||
type RequiredProps,
|
||||
type SlotAttributes
|
||||
} from "./lib/contracts";
|
||||
export {
|
||||
getMotionRecipeClassNames,
|
||||
motionDistances,
|
||||
motionDurations,
|
||||
motionEasings,
|
||||
motionRecipes,
|
||||
motionScales,
|
||||
type MotionRecipeName
|
||||
} from "./lib/motion";
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
|
||||
export type CommonComponentProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export type AsChildProp = {
|
||||
asChild?: boolean;
|
||||
};
|
||||
|
||||
export type DisableableProps = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export type InvalidatableProps = {
|
||||
invalid?: boolean;
|
||||
};
|
||||
|
||||
export type LoadingProps = {
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
export type ReadonlyProps = {
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
export type RequiredProps = {
|
||||
required?: boolean;
|
||||
};
|
||||
|
||||
export type PressableStateProps = CommonComponentProps &
|
||||
DisableableProps &
|
||||
LoadingProps &
|
||||
AsChildProp;
|
||||
|
||||
export type FieldStateProps = CommonComponentProps &
|
||||
DisableableProps &
|
||||
InvalidatableProps &
|
||||
ReadonlyProps &
|
||||
RequiredProps;
|
||||
|
||||
export type ControllableStateProps<T> = {
|
||||
defaultValue?: T;
|
||||
onValueChange?: (value: T) => void;
|
||||
value?: T;
|
||||
};
|
||||
|
||||
export type DataAttributes = Partial<Record<`data-${string}`, string | undefined>>;
|
||||
|
||||
export type SlotAttributes<Name extends string> = {
|
||||
"data-slot": Name;
|
||||
};
|
||||
|
||||
export const commonSlotNames = [
|
||||
{
|
||||
slot: "root",
|
||||
guidance: "The outermost element rendered by the component."
|
||||
},
|
||||
{
|
||||
slot: "label",
|
||||
guidance: "Primary visible label or title within the component."
|
||||
},
|
||||
{
|
||||
slot: "description",
|
||||
guidance: "Secondary supporting copy connected to the component."
|
||||
},
|
||||
{
|
||||
slot: "control",
|
||||
guidance: "Focusable or interactive control surface."
|
||||
},
|
||||
{
|
||||
slot: "input",
|
||||
guidance: "Typed value entry element such as input or textarea."
|
||||
},
|
||||
{
|
||||
slot: "trigger",
|
||||
guidance: "Element that opens, closes, or toggles related content."
|
||||
},
|
||||
{
|
||||
slot: "content",
|
||||
guidance: "Popover, drawer, menu, dialog, or expandable content region."
|
||||
},
|
||||
{
|
||||
slot: "icon",
|
||||
guidance: "Decorative or stateful icon container."
|
||||
}
|
||||
] as const;
|
||||
|
||||
export const commonStateNames = [
|
||||
{
|
||||
state: "state",
|
||||
guidance: "Use for finite machine-like values such as open, closed, active, or inactive."
|
||||
},
|
||||
{
|
||||
state: "disabled",
|
||||
guidance: "Set when interaction is blocked."
|
||||
},
|
||||
{
|
||||
state: "invalid",
|
||||
guidance: "Set when validation fails or the field is in an error state."
|
||||
},
|
||||
{
|
||||
state: "loading",
|
||||
guidance: "Set when the component is waiting on async work."
|
||||
},
|
||||
{
|
||||
state: "readonly",
|
||||
guidance: "Set when the value can be viewed but not edited."
|
||||
},
|
||||
{
|
||||
state: "required",
|
||||
guidance: "Set when the field requires a value."
|
||||
},
|
||||
{
|
||||
state: "orientation",
|
||||
guidance: "Use for horizontal or vertical layout state when styling depends on it."
|
||||
}
|
||||
] as const;
|
||||
|
||||
export const authoringChecklist = [
|
||||
"Expose `className` on every styled public component.",
|
||||
"Forward `ref` on every focusable or measurable public component.",
|
||||
"Use `asChild` only for components whose root element is meant to be polymorphic.",
|
||||
"Represent boolean UI states with empty-string `data-*` attributes.",
|
||||
"Represent finite machine states with `data-state=\"...\"` and keep the values stable.",
|
||||
"Name internal stylable parts with `data-slot` so variants and docs can target them consistently.",
|
||||
"Prefer controlled and uncontrolled APIs together when the component manages user state.",
|
||||
"Consume tokens and motion recipes instead of raw visual values."
|
||||
] as const;
|
||||
|
||||
export const cvaConventions = [
|
||||
"Put shared layout and focus primitives in the CVA base string, not in individual variants.",
|
||||
"Reserve `variant` for semantic appearance changes and `size` only when spacing or density genuinely changes.",
|
||||
"Keep default variants explicit so stories and tests do not depend on implicit visual fallbacks.",
|
||||
"Prefer a small stable variant surface over one-off booleans that fragment the API."
|
||||
] as const;
|
||||
|
||||
export function dataAttr(active?: boolean) {
|
||||
return active ? "" : undefined;
|
||||
}
|
||||
|
||||
export function createDataAttributes(
|
||||
states: Record<string, boolean | number | string | null | undefined>
|
||||
): DataAttributes {
|
||||
const attributes: DataAttributes = {};
|
||||
|
||||
for (const [name, value] of Object.entries(states)) {
|
||||
const key = `data-${name}` as const;
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
attributes[key] = dataAttr(value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value !== null && value !== undefined) {
|
||||
attributes[key] = String(value);
|
||||
}
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
export function createSlot<Name extends string>(name: Name): SlotAttributes<Name> {
|
||||
return { "data-slot": name };
|
||||
}
|
||||
|
||||
export function withRootProps<T extends object>(
|
||||
props: T,
|
||||
options: {
|
||||
slot?: string;
|
||||
states?: Record<string, boolean | number | string | null | undefined>;
|
||||
} = {}
|
||||
) {
|
||||
return {
|
||||
...props,
|
||||
...(options.slot ? createSlot(options.slot) : {}),
|
||||
...(options.states ? createDataAttributes(options.states) : {})
|
||||
};
|
||||
}
|
||||
|
||||
export type ButtonLikeElementProps = ComponentPropsWithoutRef<"button"> &
|
||||
PressableStateProps;
|
||||
|
||||
export type InputLikeElementProps = ComponentPropsWithoutRef<"input"> &
|
||||
FieldStateProps;
|
||||
@@ -1,11 +1,44 @@
|
||||
export const motionDurations = {
|
||||
instant: "var(--dur-instant)",
|
||||
fast: "var(--dur-fast)",
|
||||
base: "var(--dur-base)",
|
||||
slow: "var(--dur-slow)"
|
||||
slow: "var(--dur-slow)",
|
||||
deliberate: "var(--dur-deliberate)"
|
||||
} as const;
|
||||
|
||||
export const motionEasings = {
|
||||
standard: "var(--ease-standard)",
|
||||
emphasized: "var(--ease-emphasized)"
|
||||
emphasized: "var(--ease-emphasized)",
|
||||
exit: "var(--ease-exit)"
|
||||
} as const;
|
||||
|
||||
export const motionDistances = {
|
||||
xs: "var(--distance-xs)",
|
||||
sm: "var(--distance-sm)",
|
||||
md: "var(--distance-md)",
|
||||
lg: "var(--distance-lg)"
|
||||
} as const;
|
||||
|
||||
export const motionScales = {
|
||||
press: "var(--scale-press)",
|
||||
hover: "var(--scale-hover)",
|
||||
pop: "var(--scale-pop)"
|
||||
} as const;
|
||||
|
||||
export const motionRecipes = {
|
||||
transition: "motion-transition",
|
||||
pressable: "motion-pressable",
|
||||
enterFade: "motion-enter-fade",
|
||||
enterRise: "motion-enter-rise",
|
||||
overlayEnter: "motion-overlay-enter",
|
||||
overlayExit: "motion-overlay-exit",
|
||||
exitFade: "motion-exit-fade",
|
||||
exitDrop: "motion-exit-drop",
|
||||
ring: "motion-ring"
|
||||
} as const;
|
||||
|
||||
export type MotionRecipeName = keyof typeof motionRecipes;
|
||||
|
||||
export function getMotionRecipeClassNames(...recipes: MotionRecipeName[]) {
|
||||
return recipes.map((recipe) => motionRecipes[recipe]).join(" ");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user