Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 8 additions & 12 deletions packages/app/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,18 @@ const ConfiguredMarkedInput = createMarkedInput({
options: [
{
markup: PrimaryMarkup,
slotProps: {
mark: ({value, meta}) => ({label: value || '', primary: true, onClick: () => alert(meta)}),
overlay: {
trigger: '@',
data: ['First', 'Second', 'Third', 'Fourth', 'Fifth', 'Sixth'],
},
mark: ({value, meta}) => ({label: value || '', primary: true, onClick: () => alert(meta)}),
overlay: {
trigger: '@',
data: ['First', 'Second', 'Third', 'Fourth', 'Fifth', 'Sixth'],
},
},
{
markup: DefaultMarkup,
slotProps: {
mark: ({value}) => ({label: value || ''}),
overlay: {
trigger: '/',
data: ['Seventh', 'Eight', 'Ninth'],
},
mark: ({value}) => ({label: value || ''}),
overlay: {
trigger: '/',
data: ['Seventh', 'Eight', 'Ninth'],
},
},
],
Expand Down
32 changes: 10 additions & 22 deletions packages/markput/src/components/MarkedInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,44 +9,33 @@ import {Whisper} from './Whisper'
import type {CoreMarkputProps, OverlayTrigger} from '@markput/core'

/**
* Props for MarkedInput component with hierarchical type support.
* Props for MarkedInput component.
*
* Type parameters:
* - `TMarkProps` - Type of props for the global Mark component (default: MarkProps)
* - `TOverlayProps` - Type of props for the global Overlay component (default: OverlayProps)
*
* The global Mark and Overlay components serve as defaults when options don't specify
* their own slot components. Each option can override these with option.slots.
*
* Default types:
* - TMarkProps = MarkProps: Type-safe base props (value, meta, nested, children)
* - TOverlayProps = OverlayProps: Type-safe overlay props (trigger, data)
* @template TMarkProps - Type of props for the global Mark component
* @template TOverlayProps - Type of props for the global Overlay component
*
* @example
* ```typescript
* // Using global Mark component with custom props type
* interface ButtonProps { label: string; onClick: () => void }
* <MarkedInput<ButtonProps>
* Mark={Button}
* <MarkedInput<ChipProps>
* Mark={Chip}
* options={[{
* markup: '@[__value__]',
* slotProps: { mark: { label: 'Click me', onClick: () => {} } }
* mark: { label: 'Click me' }
* }]}
* />
* ```
*/
export interface MarkedInputProps<TMarkProps = MarkProps, TOverlayProps = OverlayProps> extends CoreMarkputProps {
/** Ref to handler */
ref?: ForwardedRef<MarkedInputHandler>
/** Global component used for rendering markups (fallback for option.slots.mark) */
/** Global component used for rendering markups (fallback for option.mark.slot) */
Mark?: ComponentType<TMarkProps>
/** Global component used for rendering overlays like suggestions, mentions, etc (fallback for option.slots.overlay) */
/** Global component used for rendering overlays (fallback for option.overlay.slot) */
Overlay?: ComponentType<TOverlayProps>
/**
* Configuration options for markups and overlays.
* Each option can specify its own slot components and props via option.slots and option.slotProps.
* Each option can specify its own slot component via mark.slot or overlay.slot.
* Falls back to global Mark/Overlay components when not specified.
* @default [{overlayTrigger: '@', markup: '@[__label__](__value__)', data: []}]
*/
options?: Option<TMarkProps, TOverlayProps>[]
/** Additional classes */
Expand All @@ -73,10 +62,9 @@ export interface MarkedInputProps<TMarkProps = MarkProps, TOverlayProps = Overla
}

export interface MarkedInputComponent {
<TMarkProps = any, TOverlayProps = OverlayProps>(
<TMarkProps = MarkProps, TOverlayProps = OverlayProps>(
props: MarkedInputProps<TMarkProps, TOverlayProps>
): JSX.Element | null

displayName?: string
}

Expand Down
2 changes: 1 addition & 1 deletion packages/markput/src/components/StoreProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const StoreProvider = ({props, children}: StoreProviderProps) => {
return <StoreContext.Provider value={store} children={children} />
}

function normalizeProps(props: MarkedInputProps<any>): MarkedInputProps {
function normalizeProps(props: MarkedInputProps): MarkedInputProps {
const className = mergeClassNames(DEFAULT_CLASS_NAME, props.className, props.slotProps?.container?.className)
const style = mergeStyles(props.style, props.slotProps?.container?.style)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {KEYBOARD} from '@markput/core'
export const Suggestions = () => {
const {match, select, style, ref} = useOverlay()
const [active, setActive] = useState(NaN)
const data = match.option.slotProps?.overlay?.data || []
const data = match.option.overlay?.data || []
const filtered = useMemo(
() => data.filter(s => s.toLowerCase().indexOf(match.value.toLowerCase()) > -1),
[match.value, data]
Expand Down
10 changes: 4 additions & 6 deletions packages/markput/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {Option} from './types'
/**
* React-specific default options for MarkedInput.
* Extends core DEFAULT_OPTIONS with framework-specific configuration:
* - Includes trigger configuration via slotProps.overlay.trigger
* - Includes trigger configuration via overlay.trigger
* - Provides empty data array for overlay suggestions
*
* Architecture:
Expand All @@ -15,11 +15,9 @@ import type {Option} from './types'
export const DEFAULT_OPTIONS: Option[] = [
{
markup: DEFAULT_MARKUP,
slotProps: {
overlay: {
trigger: DEFAULT_OVERLAY_TRIGGER,
data: [],
},
overlay: {
trigger: DEFAULT_OVERLAY_TRIGGER,
data: [],
},
},
]
5 changes: 1 addition & 4 deletions packages/markput/src/features/overlay/useTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ export const useTrigger = () => {
useListener(
SystemEvent.CheckTrigger,
_ =>
(store.overlayMatch = TriggerFinder.find(
store.props.options,
(option: Option) => option.slotProps?.overlay?.trigger
)),
(store.overlayMatch = TriggerFinder.find(store.props.options, (option: Option) => option.overlay?.trigger)),
[]
)
}
42 changes: 17 additions & 25 deletions packages/markput/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import type {CoreOption} from '@markput/core'
export type PropsOf<T> = T extends ComponentType<infer P> ? (P extends object ? P : never) : never

/**
* Simplified props passed to Mark components via slotProps
* Props passed to Mark components.
*/
export interface MarkProps {
/** Custom component to render this mark */
slot?: ComponentType<MarkProps>
/** Main content value of the mark */
value?: string
/** Additional metadata for the mark */
Expand All @@ -22,53 +24,43 @@ export interface MarkProps {
}

/**
* Default props for Overlay components via slotProps.
* Props for Overlay components.
*/
export interface OverlayProps {
/** Custom component to render this overlay */
slot?: ComponentType<OverlayProps>
/** Trigger character(s) that activate the overlay */
trigger?: string
/** Data array for suggestions/autocomplete */
data?: string[]
}

// ============================================================================
// Option Interface with Automatic Type Inference
// Option Interface
// ============================================================================

/**
* React-specific markup option for defining mark behavior and styling.
*
* @template TMarkProps - Type of props for the mark component
* @template TOverlayProps - Type of props for the overlay component
*
* @example
* const option: Option = {
* const option: Option<ChipProps> = {
* markup: '@[__value__]',
* slots: { mark: Button },
* slotProps: { mark: { label: 'Click' } }
* mark: { slot: Chip, label: 'Click' }
* }
*/
export interface Option<TMarkProps = MarkProps, TOverlayProps = OverlayProps> extends CoreOption {
/**
* Per-option slot components.
* Props for the mark component.
* Can be a static object or a function that transforms MarkProps.
*/
slots?: {
/** Mark component for this option. */
mark?: ComponentType<TMarkProps>
/** Overlay component for this option. */
overlay?: ComponentType<TOverlayProps>
}
mark?: TMarkProps | ((props: MarkProps) => TMarkProps)
/**
* Props for slot components.
* Props for the overlay component.
*/
slotProps?: {
/**
* Props for the mark component.
* Can be a static object or a function that transforms MarkProps.
*/
mark?: TMarkProps | ((props: MarkProps) => TMarkProps)
/**
* Props for the overlay component.
*/
overlay?: TOverlayProps
}
overlay?: TOverlayProps
}

export interface ConfiguredMarkedInput<TMarkProps = MarkProps, TOverlayProps = OverlayProps> extends FunctionComponent<
Expand Down
8 changes: 4 additions & 4 deletions packages/markput/src/utils/functions/createMarkedInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import type {ConfiguredMarkedInput, MarkProps, OverlayProps} from '../../types'
/**
* Create the configured MarkedInput component.
*
* @template TMarkProps - Type of props for the Mark component (default: MarkProps)
* @template TOverlayProps - Type of props for the Overlay component (default: OverlayProps)
* @template TMarkProps - Type of props for the global Mark component
* @template TOverlayProps - Type of props for the global Overlay component
*/
export function createMarkedInput<TMarkProps = MarkProps, TOverlayProps = OverlayProps>(
configs: Omit<MarkedInputProps<TMarkProps, TOverlayProps>, 'value' | 'onChange'>
): ConfiguredMarkedInput<TMarkProps, TOverlayProps> {
const ConfiguredMarkedInput = (props: MarkedInputProps<any, any>, ref: ForwardedRef<any>) => {
const assignedProps: MarkedInputProps = Object.assign({}, configs, props)
const ConfiguredMarkedInput = (props: MarkedInputProps<TMarkProps, TOverlayProps>, ref: ForwardedRef<any>) => {
const assignedProps = Object.assign({}, configs, props) as MarkedInputProps
return _MarkedInput(assignedProps, ref)
}

Expand Down
65 changes: 28 additions & 37 deletions packages/markput/src/utils/hooks/useSlot.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {ComponentType} from 'react'
import {useStore} from './useStore'
import type {Option, MarkProps} from '../../types'
import type {Option, MarkProps, OverlayProps} from '../../types'

/**
* Slot type that can be resolved
Expand All @@ -9,21 +9,19 @@ export type SlotType = 'mark' | 'overlay'

/**
* Helper type for mark slot return value.
* Simplifies function overload signatures.
*/
type MarkSlotReturn = readonly [ComponentType<any>, any]
type MarkSlotReturn = readonly [ComponentType<any>, MarkProps]

/**
* Helper type for overlay slot return value.
* Simplifies function overload signatures.
*/
type OverlaySlotReturn = readonly [ComponentType<any>, any]
type OverlaySlotReturn = readonly [ComponentType<any>, OverlayProps]

/**
* Resolves a mark slot component and its props with proper fallback chain.
*
* @param type - Must be 'mark'
* @param option - Option containing per-option slot configuration
* @param option - Option containing mark configuration
* @param baseProps - MarkProps to use as fallback or to transform
* @returns Tuple of [MarkComponent, props] - Component is guaranteed to exist
* @throws Error if no mark component found
Expand All @@ -32,18 +30,13 @@ type OverlaySlotReturn = readonly [ComponentType<any>, any]
* const [Mark, props] = useSlot('mark', option, markPropsData)
* return <Mark {...props} />
*/
export function useSlot(
type: 'mark',
option?: Option<any, any>,
baseProps?: MarkProps,
defaultComponent?: never
): MarkSlotReturn
export function useSlot(type: 'mark', option?: Option, baseProps?: MarkProps, defaultComponent?: never): MarkSlotReturn

/**
* Resolves an overlay slot component and its props with proper fallback chain.
*
* @param type - Must be 'overlay'
* @param option - Option containing per-option slot configuration
* @param option - Option containing overlay configuration
* @param baseProps - Base props for overlay (usually undefined)
* @param defaultComponent - Default overlay component to use as fallback
* @returns Tuple of [OverlayComponent, props] - Component is guaranteed to exist
Expand All @@ -55,7 +48,7 @@ export function useSlot(
*/
export function useSlot(
type: 'overlay',
option?: Option<any, any>,
option?: Option,
baseProps?: any,
defaultComponent?: ComponentType<any>
): OverlaySlotReturn
Expand All @@ -64,19 +57,19 @@ export function useSlot(
* Implementation: Resolves a slot component and its props with proper fallback chain.
*
* Component resolution:
* 1. option.slots[type]
* 1. resolved props.slot
* 2. global component (store.props.Mark or store.props.Overlay)
* 3. defaultComponent (if provided)
* 4. throws error if none found
*
* Props resolution:
* 1. If option.slotProps[type] is a function: call with baseProps
* 2. If option.slotProps[type] is an object: use directly
* 1. If option.mark/overlay is a function: call with baseProps
* 2. If option.mark/overlay is an object: use directly
* 3. Otherwise: use baseProps as fallback
*/
export function useSlot(
type: SlotType,
option?: Option<any, any>,
option?: Option,
baseProps?: any,
defaultComponent?: ComponentType<any>
): MarkSlotReturn | OverlaySlotReturn {
Expand All @@ -85,35 +78,33 @@ export function useSlot(
| ComponentType<any>
| undefined

// Resolve component: option.slots[type] → global component → defaultComponent
const Component = (option?.slots?.[type] || globalComponent || defaultComponent) as ComponentType<any>

// Throw error if component not found
if (!Component) {
throw new Error(
`No ${type} component found. ` +
`Provide either option.slots.${type}, global ${type === 'mark' ? 'Mark' : 'Overlay'}, or a defaultComponent.`
)
}

// Resolve props based on slotProps configuration
const slotPropsConfig = option?.slotProps?.[type]

// Resolve props based on option configuration
const optionConfig = type === 'mark' ? option?.mark : option?.overlay
let props: any

if (slotPropsConfig !== undefined) {
// If slotProps is defined, use it
if (typeof slotPropsConfig === 'function') {
if (optionConfig !== undefined) {
if (typeof optionConfig === 'function') {
// If it's a function, transform baseProps
props = slotPropsConfig(baseProps)
props = optionConfig(baseProps)
} else {
// If it's an object, use it directly
props = slotPropsConfig
props = optionConfig
}
} else {
// Otherwise, use baseProps as fallback
props = baseProps ?? {}
}

// Resolve component: props.slot → global component → defaultComponent
const Component = (props.slot || globalComponent || defaultComponent) as ComponentType<any>

// Throw error if component not found
if (!Component) {
throw new Error(
`No ${type} component found. ` +
`Provide either option.${type}.slot, global ${type === 'mark' ? 'Mark' : 'Overlay'}, or a defaultComponent.`
)
}

return [Component, props]
}
Loading