{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "dynamic-dialog", "title": "Dynamic Dialog", "description": "A dynamic dialog component.", "files": [ { "path": "components/dynamic-dialog/dynamic-dialog.tsx", "content": "\"use client\"\r\n\r\nimport React, { useState, useEffect, useContext, createContext, useRef } from 'react';\r\nimport { dialogObservable } from '@/lib/dynamic-dialog-state';\r\nimport type { DialogActions, DismissReason } from '@/lib/dynamic-dialog-state';\r\n\r\ninterface DynamicDialogContextValue {\r\n actions: DialogActions;\r\n componentProps: Record;\r\n}\r\n\r\nconst DynamicDialogContext = createContext(null);\r\n\r\nexport function useDynamicDialog(): DialogActions;\r\nexport function useDynamicDialog(): DialogActions & { props: TProps };\r\nexport function useDynamicDialog() {\r\n const ctx = useContext(DynamicDialogContext);\r\n if (!ctx) throw new Error('useDynamicDialog must be used within a dialog rendered by DynamicDialogProvider');\r\n return { ...ctx.actions, props: ctx.componentProps };\r\n}\r\n\r\ninterface DialogStateItem {\r\n id: string;\r\n open: boolean;\r\n Component: React.ComponentType>;\r\n componentProps: Record;\r\n beforeClose?: () => boolean | Promise;\r\n}\r\n\r\nexport interface DynamicDialogProviderProps {\r\n /** Milliseconds to wait after close animation before unmounting the dialog. Must match the CSS exit animation duration. */\r\n removeDelay?: number;\r\n}\r\n\r\nfunction DynamicDialogItem({ id, open, Component, componentProps, beforeClose }: DialogStateItem) {\r\n const confirm = (value?: unknown) => dialogObservable.confirmDialog(id, value);\r\n const dismiss = (reason?: DismissReason, value?: unknown) => dialogObservable.dismissDialog(id, reason, value);\r\n const onOpenChange = async (isOpen: boolean) => {\r\n if (!isOpen) {\r\n if (beforeClose) {\r\n const canClose = await beforeClose();\r\n if (!canClose) return;\r\n }\r\n dismiss('close');\r\n }\r\n };\r\n\r\n const actions: DialogActions = { confirm, dismiss, open, onOpenChange };\r\n\r\n return (\r\n \r\n \r\n \r\n );\r\n}\r\n\r\nexport function DynamicDialogProvider({ removeDelay = 300 }: DynamicDialogProviderProps) {\r\n const [dialogs, setDialogs] = useState([]);\r\n const removeDelayRef = useRef(removeDelay);\r\n removeDelayRef.current = removeDelay;\r\n\r\n useEffect(() => {\r\n const unsubscribe = dialogObservable.subscribe((event) => {\r\n switch (event.action) {\r\n case 'SHOW_DIALOG':\r\n setDialogs(prev => [...prev, {\r\n id: event.id,\r\n open: true,\r\n Component: event.Component,\r\n componentProps: event.componentProps,\r\n beforeClose: event.beforeClose,\r\n }]);\r\n break;\r\n case 'UPDATE_DIALOG':\r\n setDialogs(prev =>\r\n prev.map(d => d.id === event.id ? { ...d, componentProps: event.componentProps } : d)\r\n );\r\n break;\r\n case 'HIDE_DIALOG':\r\n setDialogs(prev =>\r\n prev.map(d => d.id === event.id ? { ...d, open: false } : d)\r\n );\r\n setTimeout(() => {\r\n setDialogs(prev => prev.filter(d => d.id !== event.id));\r\n }, removeDelayRef.current);\r\n break;\r\n }\r\n });\r\n\r\n return () => {\r\n unsubscribe();\r\n dialogObservable.dismissAllDialogs('close');\r\n };\r\n }, []);\r\n\r\n return (\r\n <>\r\n {dialogs.map(d => (\r\n \r\n ))}\r\n \r\n );\r\n}\r\n", "type": "registry:file", "target": "@components/dynamic-dialog/dynamic-dialog.tsx" }, { "path": "lib/dynamic-dialog-state.ts", "content": "import React from \"react\";\r\n\r\ntype BuiltInDismissReason = \"cancel\" | \"close\" | \"time-out\" | \"success\" | \"error\";\r\n\r\n// Extension point — users augment this interface to add custom dismiss reasons:\r\n// declare module '@/lib/dynamic-dialog-state' {\r\n// interface DismissReasonRegistry { 'my-reason': true }\r\n// }\r\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\r\nexport interface DismissReasonRegistry {}\r\n\r\nexport type DismissReason = BuiltInDismissReason | keyof DismissReasonRegistry;\r\n\r\n// When T is a concrete type, confirm requires a value. When T is void or unknown (unspecified), value is optional.\r\ntype ConfirmFn =\r\n [T] extends [void] ? () => void :\r\n unknown extends T ? (value?: T) => void :\r\n (value: T) => void;\r\n\r\nexport interface DialogActions {\r\n confirm: ConfirmFn;\r\n dismiss: (reason?: DismissReason, value?: T) => void;\r\n open: boolean;\r\n onOpenChange: (open: boolean) => void;\r\n}\r\n\r\nexport type DialogComponentProps = TProps &\r\n DialogActions;\r\n\r\nexport type DialogOptions = {\r\n id?: string;\r\n singleton?: boolean;\r\n /** Return false (or Promise) to prevent the dialog from closing when the user triggers a close gesture. Does not block programmatic dismiss(). */\r\n beforeClose?: () => boolean | Promise;\r\n};\r\n\r\nexport interface DialogResultData {\r\n id: string;\r\n confirmed: boolean;\r\n value?: T;\r\n reason?: DismissReason;\r\n}\r\n\r\nexport interface DialogResult> extends PromiseLike<\r\n DialogResultData\r\n> {\r\n id: string;\r\n dismiss: (reason?: DismissReason, value?: TValue) => void;\r\n update: (newProps: Partial) => void;\r\n}\r\n\r\ntype PendingDialog = {\r\n resolve: (value: DialogResultData) => void;\r\n componentProps: Record;\r\n};\r\n\r\ntype AnyComponentType = React.ComponentType>;\r\n\r\ntype DialogEvent =\r\n | { action: \"SHOW_DIALOG\"; id: string; Component: AnyComponentType; componentProps: Record; beforeClose?: () => boolean | Promise }\r\n | { action: \"UPDATE_DIALOG\"; id: string; componentProps: Record }\r\n | { action: \"HIDE_DIALOG\"; id: string };\r\n\r\ntype DialogSubscriber = (event: DialogEvent) => void;\r\n\r\nclass DialogObservable {\r\n private subscribers: DialogSubscriber[] = [];\r\n private dialogId = 0;\r\n private pendingDialogs = new Map();\r\n\r\n subscribe(callback: DialogSubscriber) {\r\n this.subscribers.push(callback);\r\n return () => {\r\n this.subscribers = this.subscribers.filter((s) => s !== callback);\r\n };\r\n }\r\n\r\n private notify(event: DialogEvent) {\r\n this.subscribers.forEach((cb) => cb(event));\r\n }\r\n\r\n getPendingIds(): string[] {\r\n return Array.from(this.pendingDialogs.keys());\r\n }\r\n\r\n hasPending(id: string): boolean {\r\n return this.pendingDialogs.has(id);\r\n }\r\n\r\n showDialog(\r\n Component: AnyComponentType,\r\n componentProps: Record,\r\n options: DialogOptions,\r\n ): DialogResult> {\r\n const id = options.id ?? `dialog-${++this.dialogId}`;\r\n\r\n if (options.singleton && this.pendingDialogs.has(id)) {\r\n this.dismissDialog(id, \"close\");\r\n }\r\n\r\n const resultPromise = new Promise>((resolve) => {\r\n this.pendingDialogs.set(id, {\r\n resolve: (data) => resolve(data as DialogResultData),\r\n componentProps,\r\n });\r\n });\r\n\r\n this.notify({ action: \"SHOW_DIALOG\", id, Component, componentProps, beforeClose: options.beforeClose });\r\n\r\n return {\r\n id,\r\n dismiss: (reason: DismissReason = \"close\", value?: T) =>\r\n this.dismissDialog(id, reason, value),\r\n then: (onFulfilled, onRejected) =>\r\n resultPromise.then(onFulfilled, onRejected),\r\n update: (newProps: Partial>) =>\r\n this.updateDialog(id, newProps),\r\n };\r\n }\r\n\r\n updateDialog(id: string, newProps: Record) {\r\n const dialog = this.pendingDialogs.get(id);\r\n if (!dialog) return;\r\n dialog.componentProps = { ...dialog.componentProps, ...newProps };\r\n this.notify({ action: \"UPDATE_DIALOG\", id, componentProps: dialog.componentProps });\r\n }\r\n\r\n confirmDialog(id: string, value?: unknown) {\r\n const dialog = this.pendingDialogs.get(id);\r\n if (!dialog) return;\r\n dialog.resolve({ id, confirmed: true, value });\r\n this.pendingDialogs.delete(id);\r\n this.notify({ action: \"HIDE_DIALOG\", id });\r\n }\r\n\r\n dismissDialog(id: string, reason: DismissReason = \"close\", value?: unknown) {\r\n const dialog = this.pendingDialogs.get(id);\r\n if (!dialog) return;\r\n dialog.resolve({ id, confirmed: false, value, reason });\r\n this.pendingDialogs.delete(id);\r\n this.notify({ action: \"HIDE_DIALOG\", id });\r\n }\r\n\r\n dismissAllDialogs(reason: DismissReason = \"close\", value?: unknown) {\r\n Array.from(this.pendingDialogs.keys()).forEach((id) =>\r\n this.dismissDialog(id, reason, value),\r\n );\r\n }\r\n}\r\n\r\nexport const dialogObservable = new DialogObservable();\r\n\r\ntype OwnProps = Omit>;\r\n\r\nexport function dialog<\r\n TProps extends DialogActions,\r\n TValue = TProps extends DialogActions ? V : unknown,\r\n>(\r\n Component: React.ComponentType,\r\n defaultOptions?: DialogOptions,\r\n): (arg?: {\r\n props?: OwnProps;\r\n options?: DialogOptions;\r\n}) => DialogResult> {\r\n return (arg) => {\r\n const componentProps = (arg?.props ?? {}) as Record;\r\n const options: DialogOptions = { ...defaultOptions, ...arg?.options };\r\n const baseResult = dialogObservable.showDialog(\r\n Component as AnyComponentType,\r\n componentProps,\r\n options,\r\n );\r\n return {\r\n id: baseResult.id,\r\n dismiss: baseResult.dismiss,\r\n then: baseResult.then,\r\n update: (newProps: Partial>) =>\r\n dialogObservable.updateDialog(baseResult.id, newProps as Record),\r\n };\r\n };\r\n}\r\n\r\ndialog.dismiss = (\r\n id?: string,\r\n reason: DismissReason = \"close\",\r\n value?: unknown,\r\n) => {\r\n if (id) dialogObservable.dismissDialog(id, reason, value);\r\n else dialogObservable.dismissAllDialogs(reason, value);\r\n};\r\n", "type": "registry:file", "target": "@lib/dynamic-dialog-state.ts" } ], "type": "registry:component" }