| 1 | import { COMPOSER_CONTEXT_KINDS, type KnownComposerContextKind } from "@t3tools/contracts"; |
| 2 | |
| 3 | export interface ContextPresentationCapability { |
| 4 | details: "none" | "tooltip" | "popover"; |
| 5 | expanded: "none" | "inline-block" | "modal"; |
| 6 | defaultDraftView: "compact" | "expanded"; |
| 7 | } |
| 8 | |
| 9 | export interface ContextPresentationDefinition { |
| 10 | kind: KnownComposerContextKind; |
| 11 | capabilities: ContextPresentationCapability; |
| 12 | } |
| 13 | |
| 14 | const DEFINITIONS = [ |
| 15 | { |
| 16 | kind: "image", |
| 17 | capabilities: { details: "tooltip", expanded: "modal", defaultDraftView: "compact" }, |
| 18 | }, |
| 19 | { |
| 20 | kind: "file", |
| 21 | capabilities: { details: "tooltip", expanded: "modal", defaultDraftView: "compact" }, |
| 22 | }, |
| 23 | { |
| 24 | kind: "terminal", |
| 25 | capabilities: { details: "popover", expanded: "none", defaultDraftView: "compact" }, |
| 26 | }, |
| 27 | { |
| 28 | kind: "element", |
| 29 | capabilities: { details: "popover", expanded: "none", defaultDraftView: "compact" }, |
| 30 | }, |
| 31 | { |
| 32 | kind: "preview-annotation", |
| 33 | capabilities: { details: "popover", expanded: "modal", defaultDraftView: "compact" }, |
| 34 | }, |
| 35 | { |
| 36 | kind: "review-comment", |
| 37 | capabilities: { details: "popover", expanded: "none", defaultDraftView: "compact" }, |
| 38 | }, |
| 39 | { |
| 40 | kind: "mention", |
| 41 | capabilities: { details: "none", expanded: "none", defaultDraftView: "compact" }, |
| 42 | }, |
| 43 | { |
| 44 | kind: "skill", |
| 45 | capabilities: { details: "tooltip", expanded: "none", defaultDraftView: "compact" }, |
| 46 | }, |
| 47 | { |
| 48 | kind: "thread", |
| 49 | capabilities: { details: "tooltip", expanded: "none", defaultDraftView: "compact" }, |
| 50 | }, |
| 51 | ] as const satisfies ReadonlyArray<ContextPresentationDefinition>; |
| 52 | |
| 53 | function buildDefinitionRegistry( |
| 54 | definitions: ReadonlyArray<ContextPresentationDefinition>, |
| 55 | ): ReadonlyMap<KnownComposerContextKind, ContextPresentationDefinition> { |
| 56 | const registry = new Map<KnownComposerContextKind, ContextPresentationDefinition>(); |
| 57 | for (const definition of definitions) { |
| 58 | if (registry.has(definition.kind)) { |
| 59 | throw new Error(`Duplicate context presentation definition: ${definition.kind}`); |
| 60 | } |
| 61 | registry.set(definition.kind, definition); |
| 62 | } |
| 63 | for (const kind of COMPOSER_CONTEXT_KINDS) { |
| 64 | if (!registry.has(kind)) { |
| 65 | throw new Error(`Missing context presentation definition: ${kind}`); |
| 66 | } |
| 67 | } |
| 68 | return registry; |
| 69 | } |
| 70 | |
| 71 | export const CONTEXT_PRESENTATION_DEFINITIONS = buildDefinitionRegistry(DEFINITIONS); |
| 72 | |
| 73 | export function contextPresentationDefinition( |
| 74 | kind: KnownComposerContextKind, |
| 75 | ): ContextPresentationDefinition { |
| 76 | return CONTEXT_PRESENTATION_DEFINITIONS.get(kind)!; |
| 77 | } |
| 78 | |
| 79 | export interface ContextPresentationHandler<TRecord, TRenderContext, TResult> { |
| 80 | kind: KnownComposerContextKind; |
| 81 | canRender?: (record: TRecord, context: TRenderContext) => boolean; |
| 82 | render: ( |
| 83 | record: TRecord, |
| 84 | context: TRenderContext, |
| 85 | definition: ContextPresentationDefinition, |
| 86 | ) => TResult; |
| 87 | } |
| 88 | |
| 89 | export interface ContextPresentationRegistry<TRecord, TRenderContext, TResult> { |
| 90 | definition(kind: KnownComposerContextKind): ContextPresentationDefinition; |
| 91 | render(kind: string, record: TRecord | undefined, context: TRenderContext): TResult; |
| 92 | } |
| 93 | |
| 94 | /** |
| 95 | * Creates one checked dispatcher for a rendering surface. Definitions are shared; renderers stay |
| 96 | * surface-specific so web composer and transcript presentation can differ without drifting on |
| 97 | * capability semantics. |
| 98 | */ |
| 99 | export function createContextPresentationRegistry<TRecord, TRenderContext, TResult>(options: { |
| 100 | handlers: ReadonlyArray<ContextPresentationHandler<TRecord, TRenderContext, TResult>>; |
| 101 | requiredKinds?: ReadonlyArray<KnownComposerContextKind>; |
| 102 | fallback: (kind: string, record: TRecord | undefined, context: TRenderContext) => TResult; |
| 103 | }): ContextPresentationRegistry<TRecord, TRenderContext, TResult> { |
| 104 | const handlers = new Map< |
| 105 | KnownComposerContextKind, |
| 106 | ContextPresentationHandler<TRecord, TRenderContext, TResult> |
| 107 | >(); |
| 108 | for (const handler of options.handlers) { |
| 109 | if (handlers.has(handler.kind)) { |
| 110 | throw new Error(`Duplicate context presentation handler: ${handler.kind}`); |
| 111 | } |
| 112 | handlers.set(handler.kind, handler); |
| 113 | } |
| 114 | for (const kind of options.requiredKinds ?? []) { |
| 115 | if (!handlers.has(kind)) { |
| 116 | throw new Error(`Missing context presentation handler: ${kind}`); |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | return { |
| 121 | definition: contextPresentationDefinition, |
| 122 | render(kind, record, context) { |
| 123 | const handler = handlers.get(kind as KnownComposerContextKind); |
| 124 | if (!handler || record === undefined || handler.canRender?.(record, context) === false) { |
| 125 | return options.fallback(kind, record, context); |
| 126 | } |
| 127 | return handler.render(record, context, contextPresentationDefinition(handler.kind)); |
| 128 | }, |
| 129 | }; |
| 130 | } |