UNPKG

tweak-tools

Version:

Tweak your React projects until awesomeness

366 lines (316 loc) 10.4 kB
/** * Types exposed through the public API */ import type { VectorSettings } from '../components/Vector/vector-types' import { StoreType, Data, DataInput } from './internal' import type { BeautifyUnionType, UnionToIntersection } from './utils' export type RenderFn = (get: (key: string) => any) => boolean /** * Utility types that joins a value with its settings */ export type InputWithSettings<V extends unknown, Settings = {}, K extends string = 'value'> = { [key in K]: V } & { type?: TweakInputs } & Settings /** * Either the raw value, either the value with its settings * In other words => value || { value, ...settings } */ export type MergedInputWithSettings<V, Settings = {}, K extends string = 'value'> = | V | InputWithSettings<V, Settings, K> /** * Special Inputs */ export enum SpecialInputs { BUTTON = 'BUTTON', BUTTON_GROUP = 'BUTTON_GROUP', MONITOR = 'MONITOR', FOLDER = 'FOLDER', } export enum TweakInputs { SELECT = 'SELECT', IMAGE = 'IMAGE', NUMBER = 'NUMBER', COLOR = 'COLOR', STRING = 'STRING', BOOLEAN = 'BOOLEAN', INTERVAL = 'INTERVAL', VECTOR3D = 'VECTOR3D', VECTOR2D = 'VECTOR2D', } export type ButtonSettings = { disabled?: boolean } export type ButtonInput = { type: SpecialInputs.BUTTON onClick: (get: (path: string) => any) => void settings: ButtonSettings } export type ButtonGroupOpts = { [title: string]: (get: (path: string) => any) => void } export type ButtonGroupInputOpts = | ButtonGroupOpts | { label?: string | JSX.Element | null opts: ButtonGroupOpts } export type ButtonGroupInput = { type: SpecialInputs.BUTTON_GROUP opts: ButtonGroupInputOpts } export type MonitorSettings = { graph?: boolean; interval?: number } export type MonitorInput = { type: SpecialInputs.MONITOR objectOrFn: React.MutableRefObject<any> | Function settings: MonitorSettings } export type SpecialInput = MonitorInput | ButtonInput | ButtonGroupInput export type FolderSettings = { collapsed?: boolean render?: RenderFn color?: string /** works similar to css order property */ order?: number } export type NumberSettings = { min?: number; max?: number; step?: number } export type VectorObj = Record<string, number> export type Vector2dArray = [number, number] export type Vector2d = Vector2dArray | VectorObj export type Vector2dSettings = VectorSettings<Vector2d, 'x' | 'y'> & { joystick?: boolean | 'invertY'; lock?: boolean } export type Vector2dInput = MergedInputWithSettings<Vector2d, Vector2dSettings> export type Vector3dArray = [number, number, number] export type Vector3d = Vector3dArray | VectorObj export type Vector3dSettings = VectorSettings<Vector3d, 'x' | 'y' | 'z'> & { lock?: boolean } export type Vector3dInput = MergedInputWithSettings<Vector3d, Vector3dSettings> export type IntervalInput = { value: [number, number]; min: number; max: number } export type ImageInput = { image: undefined | string } type SelectInput = { options: any[] | Record<string, any>; value?: any } type SelectWithValueInput<T, K> = { options: T[] | Record<string, T>; value: K } type SelectWithoutValueInput<T> = { options: T[] | Record<string, T> } type ColorRgbaInput = { r: number; g: number; b: number; a?: number } type ColorHslaInput = { h: number; s: number; l: number; a?: number } type ColorHsvaInput = { h: number; s: number; v: number; a?: number } export type ColorVectorInput = ColorRgbaInput | ColorHslaInput | ColorHsvaInput type BooleanInput = boolean type StringSettings = { rows?: boolean | number; editable?: boolean } type StringInput = InputWithSettings<string, StringSettings> export type FolderInput<Schema> = { type: SpecialInputs.FOLDER schema: Schema settings: FolderSettings } export type CustomInput<Value> = { type: string; __customInput: Value } type SchemaItem = | InputWithSettings<number, NumberSettings> | InputWithSettings<boolean> | InputWithSettings<string> | IntervalInput | ColorVectorInput | Vector2dInput | Vector3dInput | ImageInput | SelectInput | BooleanInput | StringInput | CustomInput<unknown> type GenericSchemaItemOptions = { render?: RenderFn label?: string | JSX.Element hint?: string order?: number } type OnHandlerContext = DataInput & { get(path: string): any } type OnChangeHandlerContext = OnHandlerContext & { /** * Whether the onChange handler is invoked initially. */ initial: boolean } export type OnChangeHandler = (value: any, path: string, context: OnChangeHandlerContext) => void type TransientOnChangeSchemaItemOptions = { onChange: OnChangeHandler transient?: true } type NonTransientOnChangeSchemaItemOptions = { onChange: OnChangeHandler transient: false } type NoOnChangeSchemaItemOptions = { onChange?: undefined transient?: undefined } type OnChangeSchemaItemOptions = | TransientOnChangeSchemaItemOptions | NonTransientOnChangeSchemaItemOptions | NoOnChangeSchemaItemOptions export type InputOptions = GenericSchemaItemOptions & OnChangeSchemaItemOptions & { optional?: boolean disabled?: boolean onEditStart?: (value: any, path: string, context: OnHandlerContext) => void onEditEnd?: (value: any, path: string, context: OnHandlerContext) => void } type SchemaItemWithOptions = | number | boolean | string | (SchemaItem & InputOptions) | (SpecialInput & GenericSchemaItemOptions) | FolderInput<unknown> export type Schema = Record<string, SchemaItemWithOptions> /** * Dummy type used internally to flag non compatible input types. * @internal */ type NotAPrimitiveType = { ____: 'NotAPrimitiveType' } type PrimitiveToValue<P> = P extends CustomInput<infer CustomValue> ? BeautifyUnionType<CustomValue> : P extends ImageInput ? string | undefined : P extends SelectWithValueInput<infer SelectValue, infer Options> ? SelectValue | Options : P extends SelectWithoutValueInput<infer Options> ? Options : P extends IntervalInput ? [number, number] : P extends { value: infer Value } ? PrimitiveToValue<Value> : P extends VectorObj ? P : P extends Vector3dArray ? [number, number, number] : P extends Vector2dArray ? [number, number] : P extends number ? number : P extends string ? string : P extends boolean ? boolean : NotAPrimitiveType export type SchemaToValues<Schema, IncludeTransient extends boolean = false> = BeautifyUnionType< UnionToIntersection<Tree<IncludeTransient, Schema>> > type EndLeaf = { ___leaf: 'leaf' } type Join<Leaf1, Leaf1Key extends keyof Leaf1, Leaf2> = EndLeaf extends Leaf2 ? { [i in Leaf1Key]: Leaf1[Leaf1Key] } : Leaf2 type Tree<IncludeTransient extends boolean, Leaf, LeafKey extends string | number | symbol = ''> = { // if it's a folder we run the type check on it's schema key 0: Leaf extends { schema: infer Schema } ? { [Key in keyof Schema]: Join<Schema, Key, Schema[Key]> } : never 1: never // if the leaf is an object, we run the type check on each of its keys 2: { [Key in LeafKey]: Leaf extends { optional: true } | { disabled: true } ? PrimitiveToValue<Leaf> | undefined : PrimitiveToValue<Leaf> } // recursivity 3: { [Key in keyof Leaf]: Join<Leaf, Key, Tree<IncludeTransient, Leaf[Key], Key>> }[keyof Leaf] // dead end 4: EndLeaf }[LeafKey extends '' ? 3 : Leaf extends FolderInput<unknown> ? 0 : Leaf extends SpecialInput ? 1 : PrimitiveToValue<Leaf> extends NotAPrimitiveType ? Leaf extends object ? 3 : 4 : // if an input has the onChange property then it's transient and isn't returned Leaf extends TransientOnChangeSchemaItemOptions ? IncludeTransient extends true ? 2 : 1 : 2] /** * If P is '' then T is the whole schema and we shouldn't run any type check * on the schema, to the risk that { a: 1, b: 2 } is recognized as Vector * instead of a two number inputs. */ /** * Interface to build a plugin. * * @public */ export interface Plugin<Input, Value = Input, InternalSettings = {}> { /** * The component that shows the input value; */ component: React.ComponentType /** * Normalizes the input into a { value, settings } object. * * @example * Let's consider a color with an inverted settings option that computes the negative * of that color. The plugin could look something like: * ```ts * myColorPlugin({ color: '#fff', inverted: true }) * ``` * * In that case, your normalize funciton would be something like: * ```ts * function normalize({ color, inverted }) { * return { value: color, settings: { inverted }} * } * ``` */ normalize?: (input: Input, path: string, data: Data) => { value: Value; settings?: InternalSettings } /** * Sanitizes the user value before registering it to the store. For * example, the Number plugin would santize "3.00" into 3. If the provided * value isn't formatted properly, the sanitize function should throw. */ sanitize?: (value: any, settings: InternalSettings, prevValue: any, path: string, store: StoreType) => Value /** * Formats the value into the value that will be displayed by the component. * If the input value of the Number plugin, then format will add proper * padding and show "3.00". * (Prop name in useInputContext context hook is `displayedValue`) */ format?: (value: any, settings: InternalSettings) => any } export type InputContextProps = { id: string label: string | JSX.Element hint?: string path: string key: string optional: boolean disabled: boolean disable: (flag: boolean) => void storeId: string value: unknown displayValue: unknown onChange: React.Dispatch<any> emitOnEditStart: () => void emitOnEditEnd: () => void onUpdate: (v: any | ((v: any) => any)) => void settings: unknown setSettings: (v: any) => void } /** * Interface consumed by the useInputContext hook so that its returned values * are properly typed. * * @example * ```ts * useInputContext<TweakInputProps<boolean>>() * ``` * @public */ export interface TweakInputProps<V, InternalSettings = {}, DisplayValue = V> { path?: string id?: string hint?: string disabled?: boolean displayValue: DisplayValue value: V onChange: React.Dispatch<any> emitOnEditStart: () => void emitOnEditEnd: () => void onUpdate: (v: any | ((v: any) => any)) => void settings: InternalSettings setSettings: (v: Partial<InternalSettings>) => void }