@elabs-ai/components-editor 4.0.0
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.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/chunk-LBC5VJBD.js +3748 -0
- package/dist/chunk-LBC5VJBD.js.map +1 -0
- package/dist/index.css +233 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +199 -0
- package/dist/index.js +362 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/monaco-environment.d.ts +2 -0
- package/dist/lib/monaco-environment.js +44 -0
- package/dist/lib/monaco-environment.js.map +1 -0
- package/dist/markdown/frontmatter.d.ts +11 -0
- package/dist/markdown/frontmatter.js +33 -0
- package/dist/markdown/frontmatter.js.map +1 -0
- package/dist/markdown/index.css +233 -0
- package/dist/markdown/index.css.map +1 -0
- package/dist/markdown/index.d.ts +1486 -0
- package/dist/markdown/index.js +4882 -0
- package/dist/markdown/index.js.map +1 -0
- package/dist/markdown/parse.d.ts +25 -0
- package/dist/markdown/parse.js +14 -0
- package/dist/markdown/parse.js.map +1 -0
- package/dist/markdown-editor-DfBZibAn.d.ts +702 -0
- package/package.json +101 -0
- package/src/ai-objects/ai-objects-integration.test.tsx +128 -0
- package/src/ai-objects/ai-objects.stories.tsx +233 -0
- package/src/ai-objects/ai-objects.test.tsx +205 -0
- package/src/ai-objects/decision-card.tsx +178 -0
- package/src/ai-objects/directives.ts +136 -0
- package/src/ai-objects/entity.tsx +181 -0
- package/src/ai-objects/index.ts +34 -0
- package/src/ai-objects/knowledge-card.tsx +143 -0
- package/src/calc-block/calc-block.stories.tsx +297 -0
- package/src/calc-block/calc-block.test.tsx +164 -0
- package/src/calc-block/calc-block.tsx +398 -0
- package/src/calc-block/calc-editor-monaco.ts +214 -0
- package/src/calc-block/calc-editor-prose.ts +128 -0
- package/src/calc-block/calc-editor.css +80 -0
- package/src/calc-block/calc-editor.stories.tsx +272 -0
- package/src/calc-block/calc-editor.test.ts +228 -0
- package/src/calc-block/calc-editor.ts +270 -0
- package/src/calc-block/calc-inline.stories.tsx +44 -0
- package/src/calc-block/calc-inline.test.tsx +44 -0
- package/src/calc-block/calc-inline.tsx +75 -0
- package/src/calc-block/index.ts +20 -0
- package/src/calc-block/types.ts +164 -0
- package/src/code-editor/code-editor.stories.tsx +218 -0
- package/src/code-editor/code-editor.test.tsx +134 -0
- package/src/code-editor/code-editor.tsx +250 -0
- package/src/code-editor/index.ts +6 -0
- package/src/code-workspace/code-workspace.stories.tsx +51 -0
- package/src/code-workspace/code-workspace.test.tsx +146 -0
- package/src/code-workspace/code-workspace.tsx +253 -0
- package/src/code-workspace/index.ts +6 -0
- package/src/copy-button/copy-button.tsx +51 -0
- package/src/copy-button/index.ts +1 -0
- package/src/css.d.ts +3 -0
- package/src/diff-editor/diff-editor.stories.tsx +44 -0
- package/src/diff-editor/diff-editor.test.tsx +68 -0
- package/src/diff-editor/diff-editor.tsx +151 -0
- package/src/diff-editor/index.ts +1 -0
- package/src/editor-context-menu/editor-context-menu.tsx +120 -0
- package/src/editor-context-menu/index.ts +1 -0
- package/src/editor-toolbar/editor-toolbar.tsx +83 -0
- package/src/editor-toolbar/index.ts +1 -0
- package/src/index.ts +48 -0
- package/src/lib/editor-completions-monaco.test.ts +206 -0
- package/src/lib/editor-completions-monaco.ts +127 -0
- package/src/lib/editor-completions.test.ts +156 -0
- package/src/lib/editor-completions.ts +153 -0
- package/src/lib/editor-content-access-prose.test.ts +411 -0
- package/src/lib/editor-content-access-prose.ts +172 -0
- package/src/lib/editor-content-access.test.ts +182 -0
- package/src/lib/editor-content-access.ts +109 -0
- package/src/lib/languages.ts +32 -0
- package/src/lib/markdown/diff.test.ts +80 -0
- package/src/lib/markdown/diff.ts +194 -0
- package/src/lib/markdown/directives.ts +342 -0
- package/src/lib/markdown/frontmatter.ts +50 -0
- package/src/lib/markdown/markdown-scale.test.ts +65 -0
- package/src/lib/markdown/markdown-scale.ts +62 -0
- package/src/lib/markdown/merge.test.ts +64 -0
- package/src/lib/markdown/merge.ts +158 -0
- package/src/lib/markdown/slugify.test.ts +156 -0
- package/src/lib/markdown/slugify.ts +47 -0
- package/src/lib/monaco-environment.ts +103 -0
- package/src/lib/monaco-theme-bridge.ts +293 -0
- package/src/lib/use-data-theme.ts +61 -0
- package/src/markdown/frontmatter.ts +14 -0
- package/src/markdown/index.ts +323 -0
- package/src/markdown/parse.test.ts +60 -0
- package/src/markdown/parse.ts +38 -0
- package/src/markdown-academic/citations.test.ts +77 -0
- package/src/markdown-academic/citations.tsx +442 -0
- package/src/markdown-academic/footnotes.tsx +235 -0
- package/src/markdown-academic/index.ts +32 -0
- package/src/markdown-academic/math.tsx +163 -0
- package/src/markdown-academic/toc.tsx +88 -0
- package/src/markdown-editor/completions/completions-menu.tsx +96 -0
- package/src/markdown-editor/completions/completions-prose.test.ts +356 -0
- package/src/markdown-editor/completions/completions-prose.ts +313 -0
- package/src/markdown-editor/completions/completions-widget.tsx +75 -0
- package/src/markdown-editor/completions/index.ts +37 -0
- package/src/markdown-editor/directive-nodes.ts +151 -0
- package/src/markdown-editor/directive-views.test.tsx +189 -0
- package/src/markdown-editor/directive-views.tsx +795 -0
- package/src/markdown-editor/exit-keymap.test.ts +88 -0
- package/src/markdown-editor/exit-keymap.ts +87 -0
- package/src/markdown-editor/index.ts +6 -0
- package/src/markdown-editor/markdown-editor.css +250 -0
- package/src/markdown-editor/markdown-editor.directives.test.tsx +105 -0
- package/src/markdown-editor/markdown-editor.fill.test.ts +34 -0
- package/src/markdown-editor/markdown-editor.paste-embed.test.tsx +297 -0
- package/src/markdown-editor/markdown-editor.stories.tsx +560 -0
- package/src/markdown-editor/markdown-editor.strictmode.test.tsx +38 -0
- package/src/markdown-editor/markdown-editor.table.test.tsx +287 -0
- package/src/markdown-editor/markdown-editor.tsx +560 -0
- package/src/markdown-editor/milkdown-react/editor.tsx +33 -0
- package/src/markdown-editor/milkdown-react/index.ts +8 -0
- package/src/markdown-editor/milkdown-react/types.ts +27 -0
- package/src/markdown-editor/milkdown-react/use-editor.ts +27 -0
- package/src/markdown-editor/milkdown-react/use-get-editor.ts +56 -0
- package/src/markdown-editor/milkdown-react/use-instance.ts +23 -0
- package/src/markdown-editor/paste-embed.ts +355 -0
- package/src/markdown-editor/slash/brand-slash-commands.test.ts +194 -0
- package/src/markdown-editor/slash/brand-slash-commands.ts +324 -0
- package/src/markdown-editor/slash/brand-slash-plugin.test.ts +254 -0
- package/src/markdown-editor/slash/brand-slash-plugin.ts +385 -0
- package/src/markdown-editor/slash/index.ts +107 -0
- package/src/markdown-editor/slash/insert-directive.test.ts +263 -0
- package/src/markdown-editor/slash/insert-directive.ts +345 -0
- package/src/markdown-editor/slash/monaco-slash-menu.test.tsx +198 -0
- package/src/markdown-editor/slash/monaco-slash-menu.tsx +356 -0
- package/src/markdown-editor/slash/shortcut-monaco.ts +69 -0
- package/src/markdown-editor/slash/shortcut.test.ts +157 -0
- package/src/markdown-editor/slash/shortcut.ts +57 -0
- package/src/markdown-editor/slash/slash-menu.stories.tsx +139 -0
- package/src/markdown-editor/slash/slash-menu.tsx +115 -0
- package/src/markdown-editor/slash/slash-scroll.test.tsx +52 -0
- package/src/markdown-editor/slash/slash-widget.tsx +97 -0
- package/src/markdown-editor/slash/source-slash-trigger.test.ts +42 -0
- package/src/markdown-editor/slash/source-slash-trigger.ts +46 -0
- package/src/markdown-editor/table-view.tsx +249 -0
- package/src/markdown-iteration/directive.tsx +92 -0
- package/src/markdown-iteration/edit-context.ts +56 -0
- package/src/markdown-iteration/index.ts +24 -0
- package/src/markdown-iteration/iteration-block.test.tsx +138 -0
- package/src/markdown-iteration/iteration-builder-dialog.stories.tsx +293 -0
- package/src/markdown-iteration/iteration-builder-dialog.tsx +312 -0
- package/src/markdown-iteration/iteration-builder.test.ts +252 -0
- package/src/markdown-iteration/iteration-builder.ts +303 -0
- package/src/markdown-iteration/iteration.test.ts +56 -0
- package/src/markdown-iteration/iteration.tsx +333 -0
- package/src/markdown-iteration/template-dialog.stories.tsx +63 -0
- package/src/markdown-iteration/template-dialog.test.tsx +104 -0
- package/src/markdown-iteration/template-dialog.tsx +116 -0
- package/src/markdown-outline/document-outline.stories.tsx +76 -0
- package/src/markdown-outline/document-outline.tsx +83 -0
- package/src/markdown-outline/index.ts +2 -0
- package/src/markdown-outline/markdown-outline.test.ts +49 -0
- package/src/markdown-outline/markdown-outline.ts +60 -0
- package/src/markdown-preview/code-fence.tsx +193 -0
- package/src/markdown-preview/index.ts +8 -0
- package/src/markdown-preview/markdown-preview-academic.stories.tsx +203 -0
- package/src/markdown-preview/markdown-preview-academic.test.tsx +257 -0
- package/src/markdown-preview/markdown-preview-iteration-calc.test.tsx +71 -0
- package/src/markdown-preview/markdown-preview-iteration-nested.test.tsx +58 -0
- package/src/markdown-preview/markdown-preview-iteration.stories.tsx +187 -0
- package/src/markdown-preview/markdown-preview-iteration.test.tsx +90 -0
- package/src/markdown-preview/markdown-preview-linking.test.tsx +61 -0
- package/src/markdown-preview/markdown-preview-transclusion.test.tsx +33 -0
- package/src/markdown-preview/markdown-preview.stories.tsx +108 -0
- package/src/markdown-preview/markdown-preview.test.tsx +411 -0
- package/src/markdown-preview/markdown-preview.tsx +1541 -0
- package/src/markdown-toolbar/index.ts +8 -0
- package/src/markdown-toolbar/markdown-commands.test.ts +84 -0
- package/src/markdown-toolbar/markdown-commands.ts +104 -0
- package/src/markdown-toolbar/markdown-toolbar.stories.tsx +56 -0
- package/src/markdown-toolbar/markdown-toolbar.tsx +263 -0
- package/src/markdown-workspace/focus-writing.test.ts +52 -0
- package/src/markdown-workspace/focus-writing.ts +38 -0
- package/src/markdown-workspace/index.ts +6 -0
- package/src/markdown-workspace/markdown-workspace.stories.tsx +688 -0
- package/src/markdown-workspace/markdown-workspace.test.tsx +326 -0
- package/src/markdown-workspace/markdown-workspace.tsx +787 -0
- package/src/mermaid-diagram/index.ts +1 -0
- package/src/mermaid-diagram/mermaid-diagram.stories.tsx +42 -0
- package/src/mermaid-diagram/mermaid-diagram.test.tsx +86 -0
- package/src/mermaid-diagram/mermaid-diagram.tsx +318 -0
- package/src/mermaid-diagram/mermaid-viewer.test.tsx +89 -0
- package/src/mermaid-diagram/mermaid-viewer.tsx +367 -0
- package/src/mermaid-diagram/real-parse.test.ts +42 -0
- package/src/mermaid-diagram/remediate.test.ts +60 -0
- package/src/mermaid-diagram/remediate.ts +124 -0
- package/src/mermaid-workspace/index.ts +1 -0
- package/src/mermaid-workspace/mermaid-workspace.stories.tsx +28 -0
- package/src/mermaid-workspace/mermaid-workspace.tsx +71 -0
- package/src/metric-block/index.ts +1 -0
- package/src/metric-block/metric-block.stories.tsx +40 -0
- package/src/metric-block/metric-block.tsx +14 -0
- package/src/monaco-workers.d.ts +10 -0
- package/src/prose/index.ts +14 -0
- package/src/prose/prose.stories.tsx +44 -0
- package/src/prose/prose.test.tsx +44 -0
- package/src/prose/prose.tsx +21 -0
- package/src/timeline/index.ts +18 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DecisionCard — renders a `:::decision{status=accepted date=2026-06-15}` directive
|
|
5
|
+
* as a decision-record card.
|
|
6
|
+
*
|
|
7
|
+
* Anatomy:
|
|
8
|
+
* - Status badge (accepted | rejected | proposed | superseded) in a header rail.
|
|
9
|
+
* - Date (ISO or human-readable) rendered as a `<time>` element.
|
|
10
|
+
* - Rationale body (the directive body text, rendered children).
|
|
11
|
+
* - Optional "Alternatives considered" section (comma-separated `alternatives=`
|
|
12
|
+
* attribute). The attribute shape is intentional: alternatives are typically
|
|
13
|
+
* short phrases, and keeping them out of the body lets the body stay prose.
|
|
14
|
+
*
|
|
15
|
+
* Status → Badge variant mapping:
|
|
16
|
+
* accepted → success · rejected → destructive · proposed → info ·
|
|
17
|
+
* superseded → secondary (neutral — de-emphasized, not a failure).
|
|
18
|
+
*
|
|
19
|
+
* The card is a `<section aria-label>` for landmark navigation.
|
|
20
|
+
*/
|
|
21
|
+
import {
|
|
22
|
+
Badge,
|
|
23
|
+
Card,
|
|
24
|
+
CardContent,
|
|
25
|
+
CardHeader,
|
|
26
|
+
Separator,
|
|
27
|
+
type BadgeProps,
|
|
28
|
+
} from "@elabs-ai/components-ui";
|
|
29
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
30
|
+
import { cva } from "class-variance-authority";
|
|
31
|
+
import { CheckCircle2, CircleDashed, Clock, RefreshCw } from "lucide-react";
|
|
32
|
+
import {
|
|
33
|
+
forwardRef,
|
|
34
|
+
type ComponentType,
|
|
35
|
+
type HTMLAttributes,
|
|
36
|
+
type ReactNode,
|
|
37
|
+
type SVGProps,
|
|
38
|
+
} from "react";
|
|
39
|
+
|
|
40
|
+
/* ------------------------------------------------------------------ */
|
|
41
|
+
/* Status vocabulary */
|
|
42
|
+
/* ------------------------------------------------------------------ */
|
|
43
|
+
|
|
44
|
+
export const DECISION_STATUSES = ["accepted", "rejected", "proposed", "superseded"] as const;
|
|
45
|
+
export type DecisionStatus = (typeof DECISION_STATUSES)[number];
|
|
46
|
+
|
|
47
|
+
const STATUS_BADGE_VARIANT: Record<DecisionStatus, BadgeProps["variant"]> = {
|
|
48
|
+
accepted: "success",
|
|
49
|
+
rejected: "destructive",
|
|
50
|
+
proposed: "info",
|
|
51
|
+
superseded: "secondary",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const STATUS_LABELS: Record<DecisionStatus, string> = {
|
|
55
|
+
accepted: "Accepted",
|
|
56
|
+
rejected: "Rejected",
|
|
57
|
+
proposed: "Proposed",
|
|
58
|
+
superseded: "Superseded",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const STATUS_ICONS: Record<DecisionStatus, ComponentType<SVGProps<SVGSVGElement>>> = {
|
|
62
|
+
accepted: CheckCircle2,
|
|
63
|
+
rejected: CircleDashed,
|
|
64
|
+
proposed: Clock,
|
|
65
|
+
superseded: RefreshCw,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function isDecisionStatus(s: string): s is DecisionStatus {
|
|
69
|
+
return DECISION_STATUSES.includes(s as DecisionStatus);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* ------------------------------------------------------------------ */
|
|
73
|
+
/* cva (status rail accent) */
|
|
74
|
+
/* ------------------------------------------------------------------ */
|
|
75
|
+
|
|
76
|
+
export const decisionCardVariants = cva("border-s-4", {
|
|
77
|
+
variants: {
|
|
78
|
+
status: {
|
|
79
|
+
accepted: "border-s-success",
|
|
80
|
+
rejected: "border-s-destructive",
|
|
81
|
+
proposed: "border-s-info",
|
|
82
|
+
superseded: "border-s-border",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
defaultVariants: { status: "proposed" },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/* ------------------------------------------------------------------ */
|
|
89
|
+
/* Component */
|
|
90
|
+
/* ------------------------------------------------------------------ */
|
|
91
|
+
|
|
92
|
+
export interface DecisionCardProps extends HTMLAttributes<HTMLElement> {
|
|
93
|
+
/**
|
|
94
|
+
* Decision outcome. One of the four canonical states; anything unrecognised
|
|
95
|
+
* renders as "proposed".
|
|
96
|
+
*/
|
|
97
|
+
status?: string;
|
|
98
|
+
/**
|
|
99
|
+
* ISO date or human-readable date of the decision
|
|
100
|
+
* (e.g. `"2026-06-15"` or `"June 2026"`).
|
|
101
|
+
*/
|
|
102
|
+
date?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Comma-separated alternative options considered before this decision.
|
|
105
|
+
* E.g. `"Redis cache, in-memory map, SQLite"`.
|
|
106
|
+
*/
|
|
107
|
+
alternatives?: string;
|
|
108
|
+
/** The rationale body (rendered markdown children of the directive). */
|
|
109
|
+
children?: ReactNode;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const DecisionCard = forwardRef<HTMLElement, DecisionCardProps>(function DecisionCard(
|
|
113
|
+
{ status: rawStatus, date, alternatives, children, className, ...props },
|
|
114
|
+
ref,
|
|
115
|
+
) {
|
|
116
|
+
const status: DecisionStatus = isDecisionStatus(rawStatus ?? "")
|
|
117
|
+
? (rawStatus as DecisionStatus)
|
|
118
|
+
: "proposed";
|
|
119
|
+
const badgeVariant = STATUS_BADGE_VARIANT[status];
|
|
120
|
+
const label = STATUS_LABELS[status];
|
|
121
|
+
const Icon = STATUS_ICONS[status];
|
|
122
|
+
|
|
123
|
+
const altItems = alternatives
|
|
124
|
+
? alternatives
|
|
125
|
+
.split(",")
|
|
126
|
+
.map((s) => s.trim())
|
|
127
|
+
.filter(Boolean)
|
|
128
|
+
: [];
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<section
|
|
132
|
+
ref={ref}
|
|
133
|
+
aria-label={`Decision: ${label}`}
|
|
134
|
+
className={cn("not-prose", className)}
|
|
135
|
+
{...props}
|
|
136
|
+
>
|
|
137
|
+
<Card className={cn(decisionCardVariants({ status }))}>
|
|
138
|
+
<CardHeader className="pb-3">
|
|
139
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
140
|
+
<Badge variant={badgeVariant} className="gap-1.5">
|
|
141
|
+
<Icon className="size-3" aria-hidden="true" />
|
|
142
|
+
{label}
|
|
143
|
+
</Badge>
|
|
144
|
+
{date ? (
|
|
145
|
+
<time dateTime={date} className="text-meta text-muted-foreground tabular-nums">
|
|
146
|
+
{date}
|
|
147
|
+
</time>
|
|
148
|
+
) : null}
|
|
149
|
+
</div>
|
|
150
|
+
</CardHeader>
|
|
151
|
+
|
|
152
|
+
{children ? (
|
|
153
|
+
<CardContent className="text-body text-foreground">{children}</CardContent>
|
|
154
|
+
) : null}
|
|
155
|
+
|
|
156
|
+
{altItems.length > 0 ? (
|
|
157
|
+
<>
|
|
158
|
+
<Separator />
|
|
159
|
+
<div className="px-6 py-4">
|
|
160
|
+
<p className="mb-2 text-meta font-medium text-muted-foreground">
|
|
161
|
+
Alternatives considered
|
|
162
|
+
</p>
|
|
163
|
+
<ul className="flex flex-wrap gap-1.5" aria-label="Alternatives considered">
|
|
164
|
+
{altItems.map((alt) => (
|
|
165
|
+
<li key={alt}>
|
|
166
|
+
<Badge variant="outline" className="text-meta">
|
|
167
|
+
{alt}
|
|
168
|
+
</Badge>
|
|
169
|
+
</li>
|
|
170
|
+
))}
|
|
171
|
+
</ul>
|
|
172
|
+
</div>
|
|
173
|
+
</>
|
|
174
|
+
) : null}
|
|
175
|
+
</Card>
|
|
176
|
+
</section>
|
|
177
|
+
);
|
|
178
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Directive renderer factories for the ai-objects trio.
|
|
3
|
+
*
|
|
4
|
+
* Each factory returns a `MarkdownDirectiveRenderer` for registration on
|
|
5
|
+
* `MarkdownPreview` via the `extensions.directives` prop. The factories are
|
|
6
|
+
* thin wrappers: they pass directive context into the standalone presentational
|
|
7
|
+
* components — the library renders, the consumer computes.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* import { aiObjectDirectives } from "@elabs-ai/components-editor/markdown";
|
|
11
|
+
*
|
|
12
|
+
* <MarkdownPreview
|
|
13
|
+
* extensions={{ directives: aiObjectDirectives({ resolveKnowledge }) }}
|
|
14
|
+
* />
|
|
15
|
+
*/
|
|
16
|
+
import { createElement } from "react";
|
|
17
|
+
|
|
18
|
+
import type { MarkdownDirectiveRenderer } from "../lib/markdown/directives";
|
|
19
|
+
import { DecisionCard } from "./decision-card";
|
|
20
|
+
import { EntityCard, EntityChip } from "./entity";
|
|
21
|
+
import { KnowledgeCard, type KnowledgeSourceResolver } from "./knowledge-card";
|
|
22
|
+
|
|
23
|
+
/* ------------------------------------------------------------------ */
|
|
24
|
+
/* Individual factories */
|
|
25
|
+
/* ------------------------------------------------------------------ */
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `decisionDirective()` — registers the `:::decision` container renderer.
|
|
29
|
+
*
|
|
30
|
+
* Authoring shape:
|
|
31
|
+
* :::decision{status=accepted date=2026-06-15 alternatives="Redis, SQLite"}
|
|
32
|
+
* We chose PostgreSQL because it already runs in prod.
|
|
33
|
+
* :::
|
|
34
|
+
*/
|
|
35
|
+
export function decisionDirective(): MarkdownDirectiveRenderer {
|
|
36
|
+
return {
|
|
37
|
+
name: "decision",
|
|
38
|
+
kinds: ["container"],
|
|
39
|
+
render({ attributes, children }) {
|
|
40
|
+
return createElement(DecisionCard, {
|
|
41
|
+
status: attributes.status,
|
|
42
|
+
date: attributes.date,
|
|
43
|
+
alternatives: attributes.alternatives,
|
|
44
|
+
children,
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `entityDirective()` — registers the `entity` directive for BOTH container
|
|
52
|
+
* (block card) and inline (chip) syntaxes.
|
|
53
|
+
*
|
|
54
|
+
* Authoring shapes:
|
|
55
|
+
* Container: :::entity{kind=org name="Acme Corp"}
|
|
56
|
+
* Acme Corp is the primary vendor.
|
|
57
|
+
* :::
|
|
58
|
+
* Inline: :entity[Acme Corp]{kind=org}
|
|
59
|
+
*/
|
|
60
|
+
export function entityDirective(): MarkdownDirectiveRenderer {
|
|
61
|
+
return {
|
|
62
|
+
name: "entity",
|
|
63
|
+
kinds: ["container", "inline"],
|
|
64
|
+
render({ kind, attributes, children, textValue }) {
|
|
65
|
+
if (kind === "inline") {
|
|
66
|
+
// `textValue` is the verbatim label text (markdown chars preserved);
|
|
67
|
+
// fall back to rendered children when positions were unavailable.
|
|
68
|
+
const label = textValue ?? (typeof children === "string" ? children : undefined);
|
|
69
|
+
return createElement(EntityChip, { kind: attributes.kind, children: label ?? children });
|
|
70
|
+
}
|
|
71
|
+
// Container → EntityCard (block)
|
|
72
|
+
return createElement(EntityCard, {
|
|
73
|
+
kind: attributes.kind,
|
|
74
|
+
name: attributes.name,
|
|
75
|
+
children,
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* `knowledgeDirective({ resolve })` — registers the `:::knowledge` container
|
|
83
|
+
* renderer. The `resolve` hook is consumer-supplied; omitting it causes all
|
|
84
|
+
* sources to render as plain unlinked labels (safe default).
|
|
85
|
+
*
|
|
86
|
+
* Authoring shape:
|
|
87
|
+
* :::knowledge{sources="notes/a.md, notes/b.md"}
|
|
88
|
+
* PostgreSQL was chosen because …
|
|
89
|
+
* :::
|
|
90
|
+
*/
|
|
91
|
+
export function knowledgeDirective(options?: {
|
|
92
|
+
resolve?: KnowledgeSourceResolver;
|
|
93
|
+
}): MarkdownDirectiveRenderer {
|
|
94
|
+
return {
|
|
95
|
+
name: "knowledge",
|
|
96
|
+
kinds: ["container"],
|
|
97
|
+
render({ attributes, children }) {
|
|
98
|
+
return createElement(KnowledgeCard, {
|
|
99
|
+
sources: attributes.sources,
|
|
100
|
+
resolve: options?.resolve,
|
|
101
|
+
children,
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* ------------------------------------------------------------------ */
|
|
108
|
+
/* Convenience bundle */
|
|
109
|
+
/* ------------------------------------------------------------------ */
|
|
110
|
+
|
|
111
|
+
export interface AiObjectDirectivesOptions {
|
|
112
|
+
/** Consumer-supplied resolver for `:::knowledge` source paths. */
|
|
113
|
+
resolveKnowledge?: KnowledgeSourceResolver;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* `aiObjectDirectives(options)` — returns all three directive renderers as an
|
|
118
|
+
* array ready to pass to `extensions.directives`:
|
|
119
|
+
* - `decisionDirective()`
|
|
120
|
+
* - `entityDirective()`
|
|
121
|
+
* - `knowledgeDirective({ resolve: options.resolveKnowledge })`
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* <MarkdownPreview
|
|
125
|
+
* extensions={{ directives: aiObjectDirectives({ resolveKnowledge }) }}
|
|
126
|
+
* />
|
|
127
|
+
*/
|
|
128
|
+
export function aiObjectDirectives(
|
|
129
|
+
options: AiObjectDirectivesOptions = {},
|
|
130
|
+
): MarkdownDirectiveRenderer[] {
|
|
131
|
+
return [
|
|
132
|
+
decisionDirective(),
|
|
133
|
+
entityDirective(),
|
|
134
|
+
knowledgeDirective({ resolve: options.resolveKnowledge }),
|
|
135
|
+
];
|
|
136
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EntityCard + EntityChip — renders `:::entity{kind=org name="Acme"}` (block card)
|
|
5
|
+
* and `:entity[Acme]{kind=org}` (inline chip) directives.
|
|
6
|
+
*
|
|
7
|
+
* One component handles both syntaxes; the renderer factory branches on `ctx.kind`.
|
|
8
|
+
*
|
|
9
|
+
* Entity kinds and their icon/tone:
|
|
10
|
+
* org → Building2 / secondary (companies, organisations)
|
|
11
|
+
* person → User / primary (people, authors)
|
|
12
|
+
* place → MapPin / info (locations, regions)
|
|
13
|
+
* product → Box / success (products, services)
|
|
14
|
+
* concept → Lightbulb / warning (ideas, topics)
|
|
15
|
+
* default → Tag / outline (anything else)
|
|
16
|
+
*
|
|
17
|
+
* The inline chip is a `<span>` — no block wrapper — so it stays inside `<p>` without
|
|
18
|
+
* creating invalid HTML. A11y: kind is conveyed via `aria-label` (not icon alone).
|
|
19
|
+
*/
|
|
20
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@elabs-ai/components-ui";
|
|
21
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
22
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
23
|
+
import { Box, Building2, Lightbulb, MapPin, Tag, User } from "lucide-react";
|
|
24
|
+
import {
|
|
25
|
+
forwardRef,
|
|
26
|
+
type ComponentType,
|
|
27
|
+
type HTMLAttributes,
|
|
28
|
+
type ReactNode,
|
|
29
|
+
type SVGProps,
|
|
30
|
+
} from "react";
|
|
31
|
+
|
|
32
|
+
/* ------------------------------------------------------------------ */
|
|
33
|
+
/* Kind vocabulary */
|
|
34
|
+
/* ------------------------------------------------------------------ */
|
|
35
|
+
|
|
36
|
+
export const ENTITY_KINDS = ["org", "person", "place", "product", "concept"] as const;
|
|
37
|
+
export type EntityKind = (typeof ENTITY_KINDS)[number];
|
|
38
|
+
|
|
39
|
+
type KindMeta = {
|
|
40
|
+
Icon: ComponentType<SVGProps<SVGSVGElement>>;
|
|
41
|
+
label: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const KIND_META: Record<string, KindMeta> = {
|
|
45
|
+
org: { Icon: Building2, label: "Organisation" },
|
|
46
|
+
person: { Icon: User, label: "Person" },
|
|
47
|
+
place: { Icon: MapPin, label: "Place" },
|
|
48
|
+
product: { Icon: Box, label: "Product" },
|
|
49
|
+
concept: { Icon: Lightbulb, label: "Concept" },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const DEFAULT_KIND_META: KindMeta = { Icon: Tag, label: "Entity" };
|
|
53
|
+
|
|
54
|
+
function kindMeta(kind: string | undefined): KindMeta {
|
|
55
|
+
return KIND_META[kind ?? ""] ?? DEFAULT_KIND_META;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* ------------------------------------------------------------------ */
|
|
59
|
+
/* cva */
|
|
60
|
+
/* ------------------------------------------------------------------ */
|
|
61
|
+
|
|
62
|
+
export const entityChipVariants = cva(
|
|
63
|
+
// inline-flex + align-middle keeps the chip in the text baseline;
|
|
64
|
+
// no `block` wrapper so it is valid inside <p>.
|
|
65
|
+
"inline-flex items-center gap-1 rounded-sm border px-1.5 py-0.5 text-meta font-medium align-middle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
66
|
+
{
|
|
67
|
+
variants: {
|
|
68
|
+
kind: {
|
|
69
|
+
org: "border-border bg-secondary/60 text-secondary-foreground",
|
|
70
|
+
// #399 — same reasoning as `concept` below: a 10% WASH is not a plate
|
|
71
|
+
// and not a mark, so the LABEL takes the on-surface `-text` rung. This
|
|
72
|
+
// is the row that had no `-text` rung to reach for until #399 minted it.
|
|
73
|
+
person: "border-primary/30 bg-primary/10 text-primary-text",
|
|
74
|
+
place: "border-info/30 bg-info/10 text-info-text",
|
|
75
|
+
product: "border-success/30 bg-success/10 text-success-text",
|
|
76
|
+
// `-text`, not `-foreground`: the chip is a 10% WASH on the page surface,
|
|
77
|
+
// not a solid `--warning` plate, so it needs the on-surface rung its
|
|
78
|
+
// place/product siblings use (#381 flipped `--warning-foreground` to
|
|
79
|
+
// light ink for the now-deep fill).
|
|
80
|
+
concept: "border-warning/30 bg-warning/10 text-warning-text",
|
|
81
|
+
default: "border-border text-foreground",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
defaultVariants: { kind: "default" },
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
function chipKind(kind: string | undefined): VariantProps<typeof entityChipVariants>["kind"] {
|
|
89
|
+
if (!kind) return "default";
|
|
90
|
+
const known: Array<VariantProps<typeof entityChipVariants>["kind"]> = [
|
|
91
|
+
"org",
|
|
92
|
+
"person",
|
|
93
|
+
"place",
|
|
94
|
+
"product",
|
|
95
|
+
"concept",
|
|
96
|
+
"default",
|
|
97
|
+
];
|
|
98
|
+
return known.includes(kind as never)
|
|
99
|
+
? (kind as VariantProps<typeof entityChipVariants>["kind"])
|
|
100
|
+
: "default";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* ------------------------------------------------------------------ */
|
|
104
|
+
/* EntityChip (inline) */
|
|
105
|
+
/* ------------------------------------------------------------------ */
|
|
106
|
+
|
|
107
|
+
export interface EntityChipProps extends HTMLAttributes<HTMLSpanElement> {
|
|
108
|
+
/** Entity kind — drives icon and tone. */
|
|
109
|
+
kind?: string;
|
|
110
|
+
/** Display label (the directive's label text or `name` attribute). */
|
|
111
|
+
children?: ReactNode;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const EntityChip = forwardRef<HTMLSpanElement, EntityChipProps>(function EntityChip(
|
|
115
|
+
{ kind: rawKind, children, className, ...props },
|
|
116
|
+
ref,
|
|
117
|
+
) {
|
|
118
|
+
const { Icon, label } = kindMeta(rawKind);
|
|
119
|
+
const resolvedKind = chipKind(rawKind);
|
|
120
|
+
return (
|
|
121
|
+
<span
|
|
122
|
+
ref={ref}
|
|
123
|
+
role="mark"
|
|
124
|
+
aria-label={children ? `${String(children)} (${label})` : label}
|
|
125
|
+
className={cn(entityChipVariants({ kind: resolvedKind }), className)}
|
|
126
|
+
{...props}
|
|
127
|
+
>
|
|
128
|
+
<Icon className="size-3 shrink-0" aria-hidden="true" />
|
|
129
|
+
<span>{children}</span>
|
|
130
|
+
</span>
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/* ------------------------------------------------------------------ */
|
|
135
|
+
/* EntityCard (block) */
|
|
136
|
+
/* ------------------------------------------------------------------ */
|
|
137
|
+
|
|
138
|
+
export interface EntityCardProps extends HTMLAttributes<HTMLElement> {
|
|
139
|
+
/** Entity kind — drives the icon and header tone. */
|
|
140
|
+
kind?: string;
|
|
141
|
+
/** Entity name. Falls back to children when absent. */
|
|
142
|
+
name?: string;
|
|
143
|
+
/** Description body (rendered markdown children of the directive). */
|
|
144
|
+
children?: ReactNode;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const EntityCard = forwardRef<HTMLElement, EntityCardProps>(function EntityCard(
|
|
148
|
+
{ kind: rawKind, name, children, className, ...props },
|
|
149
|
+
ref,
|
|
150
|
+
) {
|
|
151
|
+
const { Icon, label } = kindMeta(rawKind);
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<section
|
|
155
|
+
ref={ref}
|
|
156
|
+
aria-label={name ? `${label}: ${name}` : label}
|
|
157
|
+
className={cn("not-prose", className)}
|
|
158
|
+
{...props}
|
|
159
|
+
>
|
|
160
|
+
<Card>
|
|
161
|
+
<CardHeader className="pb-3">
|
|
162
|
+
<div className="flex items-center gap-2">
|
|
163
|
+
<span
|
|
164
|
+
className="flex size-7 shrink-0 items-center justify-center rounded-md bg-muted"
|
|
165
|
+
aria-hidden="true"
|
|
166
|
+
>
|
|
167
|
+
<Icon className="size-4 text-muted-foreground" />
|
|
168
|
+
</span>
|
|
169
|
+
<div className="min-w-0">
|
|
170
|
+
{name ? <CardTitle className="truncate">{name}</CardTitle> : null}
|
|
171
|
+
<p className="text-meta text-muted-foreground">{label}</p>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</CardHeader>
|
|
175
|
+
{children ? (
|
|
176
|
+
<CardContent className="text-body text-foreground">{children}</CardContent>
|
|
177
|
+
) : null}
|
|
178
|
+
</Card>
|
|
179
|
+
</section>
|
|
180
|
+
);
|
|
181
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Presentational components — usable standalone or composed by the directive factories.
|
|
2
|
+
export {
|
|
3
|
+
DecisionCard,
|
|
4
|
+
decisionCardVariants,
|
|
5
|
+
DECISION_STATUSES,
|
|
6
|
+
type DecisionCardProps,
|
|
7
|
+
type DecisionStatus,
|
|
8
|
+
} from "./decision-card";
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
EntityCard,
|
|
12
|
+
EntityChip,
|
|
13
|
+
entityChipVariants,
|
|
14
|
+
ENTITY_KINDS,
|
|
15
|
+
type EntityCardProps,
|
|
16
|
+
type EntityChipProps,
|
|
17
|
+
type EntityKind,
|
|
18
|
+
} from "./entity";
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
KnowledgeCard,
|
|
22
|
+
type KnowledgeCardProps,
|
|
23
|
+
type KnowledgeSourceResolved,
|
|
24
|
+
type KnowledgeSourceResolver,
|
|
25
|
+
} from "./knowledge-card";
|
|
26
|
+
|
|
27
|
+
// Directive renderer factories — wire these into MarkdownPreview `extensions.directives`.
|
|
28
|
+
export {
|
|
29
|
+
decisionDirective,
|
|
30
|
+
entityDirective,
|
|
31
|
+
knowledgeDirective,
|
|
32
|
+
aiObjectDirectives,
|
|
33
|
+
type AiObjectDirectivesOptions,
|
|
34
|
+
} from "./directives";
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* KnowledgeCard — renders a `:::knowledge{sources="notes/a.md, notes/b.md"}` directive
|
|
5
|
+
* as a sourced-fact card.
|
|
6
|
+
*
|
|
7
|
+
* Anatomy:
|
|
8
|
+
* - Fact body (rendered markdown children of the directive).
|
|
9
|
+
* - Sources section: each comma-split path is handed to the consumer's `resolve`
|
|
10
|
+
* hook (`resolve(path) => { href, title } | null`). Resolved paths render as
|
|
11
|
+
* `<a>` links; unresolved paths render as plain `<span>` labels — graceful
|
|
12
|
+
* degradation, no errors thrown.
|
|
13
|
+
*
|
|
14
|
+
* The library never reads files. Domain logic (vault index, file system, API) lives
|
|
15
|
+
* entirely in the consumer's `resolve` hook — the same principle as `resolveUrl` on
|
|
16
|
+
* `MarkdownPreview` and `evaluate` on calc blocks.
|
|
17
|
+
*
|
|
18
|
+
* `KnowledgeCard` is standalone (no `resolve` → sources rendered as plain labels).
|
|
19
|
+
* The `knowledgeDirective({ resolve })` factory wires the hook for directive use.
|
|
20
|
+
*/
|
|
21
|
+
import { Card, CardContent, CardFooter } from "@elabs-ai/components-ui";
|
|
22
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
23
|
+
import { BookOpen, FileText } from "lucide-react";
|
|
24
|
+
import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
|
|
25
|
+
|
|
26
|
+
/* ------------------------------------------------------------------ */
|
|
27
|
+
/* Resolver type (consumer-supplied; never bundled) */
|
|
28
|
+
/* ------------------------------------------------------------------ */
|
|
29
|
+
|
|
30
|
+
export interface KnowledgeSourceResolved {
|
|
31
|
+
href: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Consumer-supplied resolver: path → link data, or `null` when the path cannot
|
|
37
|
+
* be resolved (stale vault link, missing file, etc.). The card degrades
|
|
38
|
+
* gracefully — the source renders as a plain label rather than a broken link.
|
|
39
|
+
*/
|
|
40
|
+
export type KnowledgeSourceResolver = (path: string) => KnowledgeSourceResolved | null;
|
|
41
|
+
|
|
42
|
+
/* ------------------------------------------------------------------ */
|
|
43
|
+
/* Sub-component: a single resolved/unresolved source row */
|
|
44
|
+
/* ------------------------------------------------------------------ */
|
|
45
|
+
|
|
46
|
+
interface SourceRowProps {
|
|
47
|
+
path: string;
|
|
48
|
+
resolve?: KnowledgeSourceResolver;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function SourceRow({ path, resolve }: SourceRowProps) {
|
|
52
|
+
const resolved = resolve ? resolve(path) : null;
|
|
53
|
+
const display = resolved?.title ?? path;
|
|
54
|
+
|
|
55
|
+
if (resolved) {
|
|
56
|
+
return (
|
|
57
|
+
<a
|
|
58
|
+
href={resolved.href}
|
|
59
|
+
rel="noopener noreferrer"
|
|
60
|
+
target="_blank"
|
|
61
|
+
// #399 — a source link is TEXT: on-surface `-text` rung, not the fill.
|
|
62
|
+
className="flex min-w-0 items-center gap-1.5 text-meta text-primary-text underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
63
|
+
aria-label={`Source: ${display}`}
|
|
64
|
+
>
|
|
65
|
+
<FileText className="size-3 shrink-0" aria-hidden="true" />
|
|
66
|
+
<span className="truncate">{display}</span>
|
|
67
|
+
</a>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<span
|
|
73
|
+
className="flex min-w-0 items-center gap-1.5 text-meta text-muted-foreground"
|
|
74
|
+
aria-label={`Source (unresolved): ${display}`}
|
|
75
|
+
>
|
|
76
|
+
<FileText className="size-3 shrink-0" aria-hidden="true" />
|
|
77
|
+
<span className="truncate">{display}</span>
|
|
78
|
+
</span>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* ------------------------------------------------------------------ */
|
|
83
|
+
/* KnowledgeCard */
|
|
84
|
+
/* ------------------------------------------------------------------ */
|
|
85
|
+
|
|
86
|
+
export interface KnowledgeCardProps extends HTMLAttributes<HTMLElement> {
|
|
87
|
+
/**
|
|
88
|
+
* Comma-separated source paths (e.g. `"notes/a.md, notes/b.md"`).
|
|
89
|
+
* Each path is handed to `resolve`; unresolved paths render as plain labels.
|
|
90
|
+
*/
|
|
91
|
+
sources?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Consumer-supplied resolver. `undefined` → all sources render as plain labels
|
|
94
|
+
* (safe default — never throws, never reads files).
|
|
95
|
+
*/
|
|
96
|
+
resolve?: KnowledgeSourceResolver;
|
|
97
|
+
/** The fact body (rendered markdown children of the directive). */
|
|
98
|
+
children?: ReactNode;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const KnowledgeCard = forwardRef<HTMLElement, KnowledgeCardProps>(function KnowledgeCard(
|
|
102
|
+
{ sources, resolve, children, className, ...props },
|
|
103
|
+
ref,
|
|
104
|
+
) {
|
|
105
|
+
const sourcePaths = sources
|
|
106
|
+
? sources
|
|
107
|
+
.split(",")
|
|
108
|
+
.map((s) => s.trim())
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
: [];
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<section
|
|
114
|
+
ref={ref}
|
|
115
|
+
aria-label="Knowledge fact"
|
|
116
|
+
className={cn("not-prose", className)}
|
|
117
|
+
{...props}
|
|
118
|
+
>
|
|
119
|
+
<Card className="border-s-4 border-s-info">
|
|
120
|
+
<CardContent className="pt-4">
|
|
121
|
+
<div className="mb-2 flex items-center gap-1.5">
|
|
122
|
+
<BookOpen className="size-3.5 shrink-0 text-info-text" aria-hidden="true" />
|
|
123
|
+
<span className="text-meta font-medium text-info-text">Knowledge</span>
|
|
124
|
+
</div>
|
|
125
|
+
<div className="text-body text-foreground">{children}</div>
|
|
126
|
+
</CardContent>
|
|
127
|
+
|
|
128
|
+
{sourcePaths.length > 0 ? (
|
|
129
|
+
<CardFooter className="flex-col items-start gap-1 border-t border-border pt-3">
|
|
130
|
+
<p className="text-meta font-medium text-muted-foreground">Sources</p>
|
|
131
|
+
<ul className="flex w-full flex-col gap-1" aria-label="Sources">
|
|
132
|
+
{sourcePaths.map((path) => (
|
|
133
|
+
<li key={path} className="min-w-0">
|
|
134
|
+
<SourceRow path={path} resolve={resolve} />
|
|
135
|
+
</li>
|
|
136
|
+
))}
|
|
137
|
+
</ul>
|
|
138
|
+
</CardFooter>
|
|
139
|
+
) : null}
|
|
140
|
+
</Card>
|
|
141
|
+
</section>
|
|
142
|
+
);
|
|
143
|
+
});
|