@springbrand/message-panel 0.1.2 → 0.1.3-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/demo/camel-chat-showcase.tsx +1265 -0
  2. package/demo/fixtures.ts +292 -0
  3. package/demo/message-panel-chat-showcase.tsx +250 -0
  4. package/demo/message-panel-gallery.tsx +389 -0
  5. package/package.json +6 -16
  6. package/src/adapters/ai-sdk.ts +32 -0
  7. package/src/camel/camel-chat-messages.tsx +1435 -0
  8. package/src/camel/camel-model-select.tsx +88 -0
  9. package/src/camel/camel-plan-bar.tsx +115 -0
  10. package/src/camel/camel-prompt-input.tsx +547 -0
  11. package/src/camel/camel-tool-presentation.tsx +1270 -0
  12. package/src/camel/camel-turn.ts +334 -0
  13. package/src/camel/turn-summary-bar.tsx +81 -0
  14. package/src/chat-summary-panel.tsx +229 -0
  15. package/src/composer/chat-composer.tsx +216 -0
  16. package/src/composer/composer-attachments.tsx +138 -0
  17. package/src/composer/composer-trigger-popover.tsx +236 -0
  18. package/src/composer/composer.tsx +425 -0
  19. package/src/composer/drop-controller.ts +80 -0
  20. package/src/composer/types.ts +79 -0
  21. package/src/contracts.ts +138 -0
  22. package/src/conversation.tsx +76 -0
  23. package/src/internal/cn.ts +3 -0
  24. package/src/message-panel.tsx +368 -0
  25. package/src/message.tsx +254 -0
  26. package/src/parts/index.tsx +1149 -0
  27. package/src/parts/kind.ts +67 -0
  28. package/src/parts/plan.ts +64 -0
  29. package/src/parts/primitives.tsx +439 -0
  30. package/src/registry.ts +44 -0
  31. package/src/status.tsx +132 -0
  32. package/dist/demo/camel-chat-showcase.d.ts +0 -1
  33. package/dist/demo/camel-chat-showcase.js +0 -1007
  34. package/dist/demo/fixtures.d.ts +0 -32
  35. package/dist/demo/fixtures.js +0 -250
  36. package/dist/demo/index.js +0 -4
  37. package/dist/demo/message-panel-chat-showcase.d.ts +0 -1
  38. package/dist/demo/message-panel-chat-showcase.js +0 -114
  39. package/dist/demo/message-panel-gallery.d.ts +0 -1
  40. package/dist/demo/message-panel-gallery.js +0 -130
  41. package/dist/src/adapters/ai-sdk.d.ts +0 -4
  42. package/dist/src/adapters/ai-sdk.js +0 -25
  43. package/dist/src/camel/camel-chat-messages.d.ts +0 -44
  44. package/dist/src/camel/camel-chat-messages.js +0 -421
  45. package/dist/src/camel/camel-model-select.d.ts +0 -13
  46. package/dist/src/camel/camel-model-select.js +0 -7
  47. package/dist/src/camel/camel-plan-bar.d.ts +0 -5
  48. package/dist/src/camel/camel-plan-bar.js +0 -24
  49. package/dist/src/camel/camel-prompt-input.d.ts +0 -32
  50. package/dist/src/camel/camel-prompt-input.js +0 -182
  51. package/dist/src/camel/camel-tool-presentation.d.ts +0 -21
  52. package/dist/src/camel/camel-tool-presentation.js +0 -784
  53. package/dist/src/camel/camel-turn.d.ts +0 -57
  54. package/dist/src/camel/camel-turn.js +0 -203
  55. package/dist/src/camel/index.js +0 -7
  56. package/dist/src/camel/turn-summary-bar.d.ts +0 -8
  57. package/dist/src/camel/turn-summary-bar.js +0 -12
  58. package/dist/src/chat-summary-panel.d.ts +0 -22
  59. package/dist/src/chat-summary-panel.js +0 -26
  60. package/dist/src/composer/chat-composer.d.ts +0 -4
  61. package/dist/src/composer/chat-composer.js +0 -90
  62. package/dist/src/composer/composer-attachments.d.ts +0 -13
  63. package/dist/src/composer/composer-attachments.js +0 -47
  64. package/dist/src/composer/composer-trigger-popover.d.ts +0 -31
  65. package/dist/src/composer/composer-trigger-popover.js +0 -29
  66. package/dist/src/composer/composer.d.ts +0 -15
  67. package/dist/src/composer/composer.js +0 -180
  68. package/dist/src/composer/drop-controller.d.ts +0 -8
  69. package/dist/src/composer/drop-controller.js +0 -69
  70. package/dist/src/composer/index.js +0 -6
  71. package/dist/src/composer/types.d.ts +0 -62
  72. package/dist/src/composer/types.js +0 -1
  73. package/dist/src/contracts.d.ts +0 -94
  74. package/dist/src/contracts.js +0 -1
  75. package/dist/src/conversation.d.ts +0 -13
  76. package/dist/src/conversation.js +0 -18
  77. package/dist/src/index.js +0 -12
  78. package/dist/src/internal/cn.d.ts +0 -1
  79. package/dist/src/internal/cn.js +0 -3
  80. package/dist/src/message-panel.d.ts +0 -4
  81. package/dist/src/message-panel.js +0 -144
  82. package/dist/src/message.d.ts +0 -36
  83. package/dist/src/message.js +0 -83
  84. package/dist/src/parts/index.d.ts +0 -187
  85. package/dist/src/parts/index.js +0 -332
  86. package/dist/src/parts/kind.d.ts +0 -3
  87. package/dist/src/parts/kind.js +0 -69
  88. package/dist/src/parts/plan.d.ts +0 -10
  89. package/dist/src/parts/plan.js +0 -40
  90. package/dist/src/parts/primitives.d.ts +0 -70
  91. package/dist/src/parts/primitives.js +0 -153
  92. package/dist/src/registry.d.ts +0 -4
  93. package/dist/src/registry.js +0 -31
  94. package/dist/src/status.d.ts +0 -26
  95. package/dist/src/status.js +0 -74
  96. /package/{dist/demo/index.d.ts → demo/index.ts} +0 -0
  97. /package/{dist/src/camel/index.d.ts → src/camel/index.ts} +0 -0
  98. /package/{dist/src/composer/index.d.ts → src/composer/index.ts} +0 -0
  99. /package/{dist/src/index.d.ts → src/index.ts} +0 -0
@@ -0,0 +1,67 @@
1
+ import type { RichPartKind } from "../contracts";
2
+
3
+ function objectOf(value: unknown): Record<string, unknown> {
4
+ return value != null && typeof value === "object"
5
+ ? (value as Record<string, unknown>)
6
+ : {};
7
+ }
8
+
9
+ export function toolNameOfPart(part: unknown): string | null {
10
+ const record = objectOf(part);
11
+ const type = typeof record.type === "string" ? record.type : "";
12
+ if (type === "toolCall") {
13
+ return typeof record.name === "string" ? record.name : "tool";
14
+ }
15
+ if (type === "dynamic-tool") {
16
+ return typeof record.toolName === "string" ? record.toolName : "dynamic";
17
+ }
18
+ return type.startsWith("tool-") ? type.slice(5) : null;
19
+ }
20
+
21
+ export function resolvePartKind(part: unknown): RichPartKind | null {
22
+ const record = objectOf(part);
23
+ const type = typeof record.type === "string" ? record.type : "";
24
+ const toolName = toolNameOfPart(part);
25
+ if (type === "thinking") return "reasoning";
26
+ if (type === "tool") return "tool";
27
+ if (toolName === "update_plan") return "plan";
28
+ if (toolName === "ask_user") return "ask_user";
29
+ if (toolName === "suggest_followups") return "suggestions";
30
+ if (toolName === "schedule") return "schedule";
31
+ if (toolName === "parallel") return "parallel";
32
+ if (
33
+ toolName === "subagents" ||
34
+ toolName === "run_agent" ||
35
+ toolName === "run_temporary_agent" ||
36
+ toolName === "fanout" ||
37
+ toolName === "extract" ||
38
+ toolName === "dispatch_background"
39
+ ) {
40
+ return "subagents";
41
+ }
42
+ if (toolName) return "tool";
43
+ const normalized = type.startsWith("data-")
44
+ ? type.slice(5).replaceAll("-", "_")
45
+ : type;
46
+ return [
47
+ "text",
48
+ "reasoning",
49
+ "error",
50
+ "image",
51
+ "chart",
52
+ "agent_card",
53
+ "plan",
54
+ "schedule",
55
+ "parallel",
56
+ "subagents",
57
+ "confirmation",
58
+ "file",
59
+ "dashboard",
60
+ "ask_user",
61
+ "suggestions",
62
+ ].includes(normalized)
63
+ ? (normalized as RichPartKind)
64
+ : type === "reasoning-file"
65
+ ? "file"
66
+ : null;
67
+ }
@@ -0,0 +1,64 @@
1
+ import { resolvePartKind } from "./kind";
2
+
3
+ type UnknownRecord = Record<string, unknown>;
4
+
5
+ export type PlanStepStatus = "pending" | "in_progress" | "done";
6
+
7
+ export interface PlanStep {
8
+ text: string;
9
+ status?: PlanStepStatus;
10
+ }
11
+
12
+ function recordOf(value: unknown): UnknownRecord {
13
+ return value != null && typeof value === "object"
14
+ ? (value as UnknownRecord)
15
+ : {};
16
+ }
17
+
18
+ export function planStepsOfPart(part: unknown): PlanStep[] {
19
+ const record = recordOf(part);
20
+ const data = recordOf(
21
+ record.data ?? record.input ?? record.arguments ?? record,
22
+ );
23
+ if (!Array.isArray(data.steps)) return [];
24
+
25
+ return data.steps.flatMap((step) => {
26
+ const value = recordOf(step);
27
+ const text = typeof value.text === "string" ? value.text.trim() : "";
28
+ if (!text) return [];
29
+ const status = value.status === "completed"
30
+ ? "done"
31
+ : value.status === "in_progress" || value.status === "done"
32
+ ? value.status
33
+ : "pending";
34
+ return [{ text, status }];
35
+ });
36
+ }
37
+
38
+ export function projectLatestPlan(
39
+ messages: readonly {
40
+ role: string;
41
+ parts: readonly unknown[];
42
+ }[],
43
+ ): PlanStep[] | null {
44
+ for (
45
+ let messageIndex = messages.length - 1;
46
+ messageIndex >= 0;
47
+ messageIndex -= 1
48
+ ) {
49
+ const message = messages[messageIndex];
50
+ if (message.role !== "assistant") continue;
51
+ for (
52
+ let partIndex = message.parts.length - 1;
53
+ partIndex >= 0;
54
+ partIndex -= 1
55
+ ) {
56
+ const part = message.parts[partIndex];
57
+ if (resolvePartKind(part) !== "plan") continue;
58
+ const steps = planStepsOfPart(part);
59
+ if (steps.length > 0) return steps;
60
+ if (recordOf(part).state !== "input-streaming") return null;
61
+ }
62
+ }
63
+ return null;
64
+ }
@@ -0,0 +1,439 @@
1
+ import {
2
+ BookOpenIcon,
3
+ BrainIcon,
4
+ CheckCircle2Icon,
5
+ ChevronDownIcon,
6
+ ChevronRightIcon,
7
+ CircleIcon,
8
+ ClockIcon,
9
+ WrenchIcon,
10
+ XCircleIcon,
11
+ } from "lucide-react";
12
+ import {
13
+ createContext,
14
+ forwardRef,
15
+ useContext,
16
+ useMemo,
17
+ useState,
18
+ type ButtonHTMLAttributes,
19
+ type DetailsHTMLAttributes,
20
+ type HTMLAttributes,
21
+ type ReactNode,
22
+ } from "react";
23
+ import type { AIToolPart } from "../contracts";
24
+ import { cn } from "../internal/cn";
25
+ import { MessageResponse } from "../message";
26
+
27
+ interface ReasoningContextValue {
28
+ open: boolean;
29
+ toggle: () => void;
30
+ isStreaming: boolean;
31
+ duration?: number;
32
+ }
33
+
34
+ const ReasoningContext = createContext<ReasoningContextValue | null>(null);
35
+
36
+ export interface ReasoningProps extends HTMLAttributes<HTMLDivElement> {
37
+ isStreaming?: boolean;
38
+ open?: boolean;
39
+ defaultOpen?: boolean;
40
+ onOpenChange?: (open: boolean) => void;
41
+ duration?: number;
42
+ }
43
+
44
+ export function Reasoning({
45
+ isStreaming = false,
46
+ open,
47
+ defaultOpen,
48
+ onOpenChange,
49
+ duration,
50
+ className,
51
+ children,
52
+ ...props
53
+ }: ReasoningProps) {
54
+ const [internalOpen, setInternalOpen] = useState(defaultOpen ?? isStreaming);
55
+ const resolvedOpen = open ?? internalOpen;
56
+ const toggle = () => {
57
+ const next = !resolvedOpen;
58
+ if (open === undefined) setInternalOpen(next);
59
+ onOpenChange?.(next);
60
+ };
61
+ const value = useMemo(
62
+ () => ({ open: resolvedOpen, toggle, isStreaming, duration }),
63
+ [resolvedOpen, isStreaming, duration],
64
+ );
65
+ return (
66
+ <ReasoningContext.Provider value={value}>
67
+ <div className={cn("sb-reasoning", className)} {...props}>
68
+ {children}
69
+ </div>
70
+ </ReasoningContext.Provider>
71
+ );
72
+ }
73
+
74
+ export function ReasoningTrigger({
75
+ children,
76
+ className,
77
+ ...props
78
+ }: ButtonHTMLAttributes<HTMLButtonElement>) {
79
+ const context = useContext(ReasoningContext);
80
+ if (!context) throw new Error("ReasoningTrigger must be used within Reasoning");
81
+ const label = context.isStreaming
82
+ ? "Thinking…"
83
+ : context.duration
84
+ ? `Thought for ${context.duration}s`
85
+ : "Reasoning";
86
+ return (
87
+ <button
88
+ type="button"
89
+ className={cn("sb-reasoning__trigger", className)}
90
+ onClick={context.toggle}
91
+ aria-expanded={context.open}
92
+ {...props}
93
+ >
94
+ {children ?? (
95
+ <>
96
+ <BrainIcon size={16} />
97
+ <span>{label}</span>
98
+ <ChevronDownIcon size={16} aria-hidden="true" />
99
+ </>
100
+ )}
101
+ </button>
102
+ );
103
+ }
104
+
105
+ export function ReasoningContent({
106
+ children,
107
+ className,
108
+ ...props
109
+ }: Omit<HTMLAttributes<HTMLDivElement>, "children"> & { children: string }) {
110
+ const context = useContext(ReasoningContext);
111
+ if (!context) throw new Error("ReasoningContent must be used within Reasoning");
112
+ if (!context.open) return null;
113
+ return (
114
+ <div className={cn("sb-reasoning__content", className)} {...props}>
115
+ <MessageResponse streaming={context.isStreaming}>{children}</MessageResponse>
116
+ </div>
117
+ );
118
+ }
119
+
120
+ type ToolPart = AIToolPart;
121
+ export type ToolState = ToolPart["state"];
122
+
123
+ const toolStatus: Record<
124
+ ToolState,
125
+ { label: string; icon: ReactNode; tone: string }
126
+ > = {
127
+ "approval-requested": {
128
+ label: "Awaiting approval",
129
+ icon: <ClockIcon size={14} />,
130
+ tone: "warning",
131
+ },
132
+ "approval-responded": {
133
+ label: "Responded",
134
+ icon: <CheckCircle2Icon size={14} />,
135
+ tone: "info",
136
+ },
137
+ "input-available": {
138
+ label: "Running",
139
+ icon: <ClockIcon size={14} />,
140
+ tone: "running",
141
+ },
142
+ "input-streaming": {
143
+ label: "Pending",
144
+ icon: <CircleIcon size={14} />,
145
+ tone: "running",
146
+ },
147
+ "output-available": {
148
+ label: "Completed",
149
+ icon: <CheckCircle2Icon size={14} />,
150
+ tone: "success",
151
+ },
152
+ "output-denied": {
153
+ label: "Denied",
154
+ icon: <XCircleIcon size={14} />,
155
+ tone: "warning",
156
+ },
157
+ "output-error": {
158
+ label: "Error",
159
+ icon: <XCircleIcon size={14} />,
160
+ tone: "error",
161
+ },
162
+ };
163
+
164
+ export function getStatusBadge(status: ToolState) {
165
+ const value = toolStatus[status];
166
+ return (
167
+ <span className="sb-tool-line__status" data-tone={value.tone}>
168
+ {value.icon}
169
+ {value.label}
170
+ </span>
171
+ );
172
+ }
173
+
174
+ export const Tool = forwardRef<
175
+ HTMLDetailsElement,
176
+ DetailsHTMLAttributes<HTMLDetailsElement>
177
+ >(({ className, ...props }, ref) => (
178
+ <details ref={ref} className={cn("sb-tool-line", className)} {...props} />
179
+ ));
180
+ Tool.displayName = "Tool";
181
+
182
+ type ToolHeaderProps = HTMLAttributes<HTMLElement> & {
183
+ title?: string;
184
+ type: ToolPart["type"];
185
+ state: ToolState;
186
+ toolName?: string;
187
+ };
188
+
189
+ export function ToolHeader({
190
+ className,
191
+ title,
192
+ type,
193
+ state,
194
+ toolName,
195
+ ...props
196
+ }: ToolHeaderProps) {
197
+ const derived =
198
+ type === "dynamic-tool"
199
+ ? toolName ?? "tool"
200
+ : type.split("-").slice(1).join("-");
201
+ return (
202
+ <summary className={cn("sb-tool-line__trigger", className)} {...props}>
203
+ <WrenchIcon size={16} />
204
+ <span>{title ?? derived}</span>
205
+ {state !== "output-available" && getStatusBadge(state)}
206
+ <ChevronRightIcon
207
+ size={16}
208
+ className="sb-tool-line__chevron"
209
+ aria-hidden="true"
210
+ />
211
+ </summary>
212
+ );
213
+ }
214
+
215
+ export const ToolContent = forwardRef<
216
+ HTMLDivElement,
217
+ HTMLAttributes<HTMLDivElement>
218
+ >(({ className, ...props }, ref) => (
219
+ <div ref={ref} className={cn("sb-tool-line__details", className)} {...props} />
220
+ ));
221
+ ToolContent.displayName = "ToolContent";
222
+
223
+ export function ToolInput({
224
+ input,
225
+ className,
226
+ ...props
227
+ }: HTMLAttributes<HTMLPreElement> & { input: unknown }) {
228
+ return (
229
+ <pre className={cn("sb-terminal", className)} {...props}>
230
+ {JSON.stringify(input, null, 2)}
231
+ </pre>
232
+ );
233
+ }
234
+
235
+ export function ToolOutput({
236
+ output,
237
+ errorText,
238
+ className,
239
+ ...props
240
+ }: HTMLAttributes<HTMLPreElement> & {
241
+ output: unknown;
242
+ errorText?: string;
243
+ }) {
244
+ if (output == null && !errorText) return null;
245
+ const content =
246
+ typeof output === "string"
247
+ ? output
248
+ : output == null
249
+ ? ""
250
+ : JSON.stringify(output, null, 2);
251
+ return (
252
+ <pre
253
+ className={cn("sb-terminal", className)}
254
+ data-error={errorText ? "true" : undefined}
255
+ {...props}
256
+ >
257
+ {errorText || content}
258
+ </pre>
259
+ );
260
+ }
261
+
262
+ interface ConfirmationContextValue {
263
+ state: ToolState | "pending" | "confirmed" | "cancelled";
264
+ approval?: { id: string; approved?: boolean };
265
+ }
266
+
267
+ const ConfirmationContext = createContext<ConfirmationContextValue | null>(null);
268
+
269
+ export interface ConfirmationProps extends HTMLAttributes<HTMLDivElement> {
270
+ state: ToolState | "pending" | "confirmed" | "cancelled";
271
+ approval?: { id: string; approved?: boolean };
272
+ }
273
+
274
+ export function Confirmation({
275
+ state,
276
+ approval,
277
+ className,
278
+ ...props
279
+ }: ConfirmationProps) {
280
+ const value = useMemo(() => ({ state, approval }), [state, approval]);
281
+ return (
282
+ <ConfirmationContext.Provider value={value}>
283
+ <div className={cn("sb-confirmation", className)} {...props} />
284
+ </ConfirmationContext.Provider>
285
+ );
286
+ }
287
+
288
+ function useConfirmation() {
289
+ const context = useContext(ConfirmationContext);
290
+ if (!context) throw new Error("Confirmation child must be used within Confirmation");
291
+ return context;
292
+ }
293
+
294
+ export const ConfirmationTitle = ({
295
+ className,
296
+ ...props
297
+ }: HTMLAttributes<HTMLDivElement>) => (
298
+ <div className={cn("sb-confirmation__title", className)} {...props} />
299
+ );
300
+
301
+ export function ConfirmationRequest({ children }: { children?: ReactNode }) {
302
+ const { state } = useConfirmation();
303
+ return state === "approval-requested" || state === "pending" ? children : null;
304
+ }
305
+
306
+ export function ConfirmationAccepted({ children }: { children?: ReactNode }) {
307
+ const { state, approval } = useConfirmation();
308
+ return state === "confirmed" || approval?.approved === true ? children : null;
309
+ }
310
+
311
+ export function ConfirmationRejected({ children }: { children?: ReactNode }) {
312
+ const { state, approval } = useConfirmation();
313
+ return state === "cancelled" ||
314
+ state === "output-denied" ||
315
+ approval?.approved === false
316
+ ? children
317
+ : null;
318
+ }
319
+
320
+ export function ConfirmationActions({
321
+ className,
322
+ ...props
323
+ }: HTMLAttributes<HTMLDivElement>) {
324
+ const { state } = useConfirmation();
325
+ if (state !== "approval-requested" && state !== "pending") return null;
326
+ return (
327
+ <div
328
+ className={cn("sb-confirmation__actions", className)}
329
+ {...props}
330
+ />
331
+ );
332
+ }
333
+
334
+ export const ConfirmationAction = forwardRef<
335
+ HTMLButtonElement,
336
+ ButtonHTMLAttributes<HTMLButtonElement>
337
+ >(({ className, ...props }, ref) => (
338
+ <button
339
+ ref={ref}
340
+ type="button"
341
+ className={cn("sb-button", className)}
342
+ {...props}
343
+ />
344
+ ));
345
+ ConfirmationAction.displayName = "ConfirmationAction";
346
+
347
+ export const Plan = forwardRef<
348
+ HTMLDivElement,
349
+ HTMLAttributes<HTMLDivElement> & {
350
+ isStreaming?: boolean;
351
+ }
352
+ >(({ className, isStreaming, ...props }, ref) => (
353
+ <div
354
+ ref={ref}
355
+ className={cn("sb-plan", className)}
356
+ data-streaming={isStreaming ? "true" : undefined}
357
+ {...props}
358
+ />
359
+ ));
360
+ Plan.displayName = "Plan";
361
+
362
+ export const PlanHeader = ({
363
+ className,
364
+ ...props
365
+ }: HTMLAttributes<HTMLDivElement>) => (
366
+ <div className={cn("sb-plan__header", className)} {...props} />
367
+ );
368
+ export const PlanTitle = ({
369
+ className,
370
+ ...props
371
+ }: HTMLAttributes<HTMLHeadingElement>) => (
372
+ <h3 className={cn("sb-plan__title", className)} {...props} />
373
+ );
374
+ export const PlanDescription = ({
375
+ className,
376
+ ...props
377
+ }: HTMLAttributes<HTMLParagraphElement>) => (
378
+ <p className={cn("sb-plan__description", className)} {...props} />
379
+ );
380
+ export const PlanContent = ({
381
+ className,
382
+ ...props
383
+ }: HTMLAttributes<HTMLDivElement>) => (
384
+ <div className={cn("sb-plan__content", className)} {...props} />
385
+ );
386
+ export const PlanFooter = ({
387
+ className,
388
+ ...props
389
+ }: HTMLAttributes<HTMLDivElement>) => (
390
+ <div className={cn("sb-plan__footer", className)} {...props} />
391
+ );
392
+
393
+ export const Sources = forwardRef<
394
+ HTMLDetailsElement,
395
+ DetailsHTMLAttributes<HTMLDetailsElement>
396
+ >(({ className, ...props }, ref) => (
397
+ <details ref={ref} className={cn("sb-sources", className)} {...props} />
398
+ ));
399
+ Sources.displayName = "Sources";
400
+
401
+ export function SourcesTrigger({
402
+ count,
403
+ children,
404
+ className,
405
+ ...props
406
+ }: HTMLAttributes<HTMLElement> & { count: number }) {
407
+ return (
408
+ <summary className={cn("sb-sources__trigger", className)} {...props}>
409
+ {children ?? (
410
+ <>
411
+ <span>来源 {count} 个</span>
412
+ <ChevronDownIcon size={15} />
413
+ </>
414
+ )}
415
+ </summary>
416
+ );
417
+ }
418
+ export const SourcesContent = ({
419
+ className,
420
+ ...props
421
+ }: HTMLAttributes<HTMLDivElement>) => (
422
+ <div className={cn("sb-sources__content", className)} {...props} />
423
+ );
424
+ export function Source({
425
+ title,
426
+ children,
427
+ ...props
428
+ }: React.AnchorHTMLAttributes<HTMLAnchorElement> & { title?: string }) {
429
+ return (
430
+ <a target="_blank" rel="noreferrer" {...props}>
431
+ {children ?? (
432
+ <>
433
+ <BookOpenIcon size={14} />
434
+ <span>{title}</span>
435
+ </>
436
+ )}
437
+ </a>
438
+ );
439
+ }
@@ -0,0 +1,44 @@
1
+ import type {
2
+ PartRendererComponent,
3
+ PartRendererDefinition,
4
+ PartRendererRegistry,
5
+ } from "./contracts";
6
+ import { DefaultPartRenderer } from "./parts";
7
+
8
+ export function createPartRendererRegistry(
9
+ definitions: readonly PartRendererDefinition[],
10
+ ): PartRendererRegistry {
11
+ const stable = [...definitions];
12
+ return {
13
+ names: stable.map((definition) => definition.name),
14
+ resolve(part: unknown): PartRendererComponent | undefined {
15
+ return stable.find((definition) => definition.match(part))?.component;
16
+ },
17
+ };
18
+ }
19
+
20
+ export function mergePartRendererRegistries(
21
+ ...registries: Array<PartRendererRegistry | undefined>
22
+ ): PartRendererRegistry {
23
+ const resolved = registries.filter(
24
+ (registry): registry is PartRendererRegistry => registry !== undefined,
25
+ );
26
+ return {
27
+ names: resolved.flatMap((registry) => registry.names),
28
+ resolve(part: unknown) {
29
+ for (const registry of resolved) {
30
+ const component = registry.resolve(part);
31
+ if (component) return component;
32
+ }
33
+ return undefined;
34
+ },
35
+ };
36
+ }
37
+
38
+ export const defaultPartRendererRegistry = createPartRendererRegistry([
39
+ {
40
+ name: "default",
41
+ match: () => true,
42
+ component: DefaultPartRenderer,
43
+ },
44
+ ]);