@lotics/ui 5.6.1 → 5.7.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/AGENTS.md +6 -4
- package/package.json +10 -1
- package/src/agent_run.tsx +7 -32
- package/src/markdown.css +209 -0
- package/src/markdown.tsx +17 -0
- package/src/markdown.web.tsx +69 -0
- package/src/primitives_purity.test.ts +13 -7
package/AGENTS.md
CHANGED
|
@@ -270,9 +270,9 @@ Compose the surfaces as a loop, and reach for the right one by job:
|
|
|
270
270
|
attaching HOLDS the file for review; the human presses Send. The canvas (`tpl_dieline`) uses it this
|
|
271
271
|
way; while running the host swaps `Composer` for `AgentProgress` (same pill geometry, so it reads as
|
|
272
272
|
a morph).
|
|
273
|
-
- **Show the work** — `AgentRun`: a live feed of the agent's work — its streamed narration (
|
|
274
|
-
|
|
275
|
-
a white-check dot once done). No title, no avatar — just the work. A terminal "Done" step (the
|
|
273
|
+
- **Show the work** — `AgentRun`: a live feed of the agent's work — its streamed narration (rendered
|
|
274
|
+
as `Markdown`, with a trailing ▍ while live) + tool calls as a vertical `Stepper` of neutral dots (the
|
|
275
|
+
latest pulsing while live, a white-check dot once done). No title, no avatar — just the work. A terminal "Done" step (the
|
|
276
276
|
`complete` node) appears ONLY once the run finishes. A step can carry `peek` content to glance into
|
|
277
277
|
via a popover. Tool labels resolve from the raw tool name via a built-in default map + an optional
|
|
278
278
|
`labelForTool` override (localize THERE — the kit stays English). `doneLabel` localizes the terminal.
|
|
@@ -581,7 +581,9 @@ recipe for a screen JOB; copy and adapt). Pick by the job:
|
|
|
581
581
|
|
|
582
582
|
## Full component inventory (import as `@lotics/ui/<module>`)
|
|
583
583
|
|
|
584
|
-
text ·
|
|
584
|
+
text ·
|
|
585
|
+
markdown (Markdown — the single canonical markdown renderer for chat, apps, and `AgentRun`; rich on web via react-markdown/remark-gfm with copyable tables, plain-text on native; takes a markdown `children` string; import `@lotics/ui/markdown.css` once for styling) ·
|
|
586
|
+
card (Card · CardHeader · CardHeaderTitle · CardHeaderMeta · CardBody · CardFooter) ·
|
|
585
587
|
section_heading (SectionHeading · SectionHeadingTitle — compound, owns no margin) · badge ·
|
|
586
588
|
option_badge (OptionBadge — a select value as its configured colored badge) ·
|
|
587
589
|
member_chip (MemberChip — avatar + name; the universal person render) ·
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./tokens": "./src/tokens.ts",
|
|
@@ -63,6 +63,11 @@
|
|
|
63
63
|
"./activity_indicator": "./src/activity_indicator.tsx",
|
|
64
64
|
"./agent_run": "./src/agent_run.tsx",
|
|
65
65
|
"./agent_progress": "./src/agent_progress.tsx",
|
|
66
|
+
"./markdown": {
|
|
67
|
+
"react-native": "./src/markdown.tsx",
|
|
68
|
+
"default": "./src/markdown.web.tsx"
|
|
69
|
+
},
|
|
70
|
+
"./markdown.css": "./src/markdown.css",
|
|
66
71
|
"./confidence": "./src/confidence.tsx",
|
|
67
72
|
"./review_card": "./src/review_card.tsx",
|
|
68
73
|
"./change_review": "./src/change_review.tsx",
|
|
@@ -217,6 +222,10 @@
|
|
|
217
222
|
"directory": "packages/ui"
|
|
218
223
|
},
|
|
219
224
|
"license": "MIT",
|
|
225
|
+
"dependencies": {
|
|
226
|
+
"react-markdown": "^10.0.0",
|
|
227
|
+
"remark-gfm": "^4.0.0"
|
|
228
|
+
},
|
|
220
229
|
"peerDependencies": {
|
|
221
230
|
"@lotics/docx": "^0.1.0",
|
|
222
231
|
"@lotics/xlsx": "^0.1.0",
|
package/src/agent_run.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { colors, solid } from "./colors";
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
4
3
|
import { Text } from "./text";
|
|
5
4
|
import { type IconName } from "./icon";
|
|
5
|
+
import { Markdown } from "./markdown";
|
|
6
6
|
import { Peek } from "./peek";
|
|
7
7
|
import { Stepper, Step, type StepStatus } from "./stepper";
|
|
8
8
|
|
|
@@ -25,10 +25,10 @@ export interface AgentRunStep {
|
|
|
25
25
|
|
|
26
26
|
export interface AgentRunProps {
|
|
27
27
|
steps: AgentRunStep[];
|
|
28
|
-
/** The agent's streamed reasoning / answer text —
|
|
29
|
-
* above the feed, with a
|
|
28
|
+
/** The agent's streamed reasoning / answer text — rendered as markdown in a
|
|
29
|
+
* narration block above the feed, with a trailing ▍ while streaming. */
|
|
30
30
|
text?: string;
|
|
31
|
-
/** Whole-run state. `streaming` keeps the
|
|
31
|
+
/** Whole-run state. `streaming` keeps the trailing ▍ + the latest step
|
|
32
32
|
* pulsing; `done`/`error` settle it. Defaults to `streaming` while a step runs. */
|
|
33
33
|
state?: "streaming" | "done" | "error";
|
|
34
34
|
/** Localize / override a TOOL step's display label by its raw tool name
|
|
@@ -96,12 +96,7 @@ export function AgentRun(props: AgentRunProps) {
|
|
|
96
96
|
|
|
97
97
|
return (
|
|
98
98
|
<View accessibilityLabel={accessibilityLabel} style={{ gap: 12 }}>
|
|
99
|
-
{text ?
|
|
100
|
-
<Text size="sm" style={styles.narration}>
|
|
101
|
-
{text}
|
|
102
|
-
{streaming ? <Caret /> : null}
|
|
103
|
-
</Text>
|
|
104
|
-
) : null}
|
|
99
|
+
{text ? <Markdown>{streaming ? `${text} ▍` : text}</Markdown> : null}
|
|
105
100
|
|
|
106
101
|
<Stepper orientation="vertical" live={streaming}>
|
|
107
102
|
{steps.map((s, i) => {
|
|
@@ -147,23 +142,3 @@ export function AgentRun(props: AgentRunProps) {
|
|
|
147
142
|
</View>
|
|
148
143
|
);
|
|
149
144
|
}
|
|
150
|
-
|
|
151
|
-
// A blinking caret that trails the streamed text — the signature of live writing.
|
|
152
|
-
function Caret() {
|
|
153
|
-
const opacity = useRef(new Animated.Value(1)).current;
|
|
154
|
-
useEffect(() => {
|
|
155
|
-
const loop = Animated.loop(
|
|
156
|
-
Animated.sequence([
|
|
157
|
-
Animated.timing(opacity, { toValue: 0, duration: 480, useNativeDriver: false }),
|
|
158
|
-
Animated.timing(opacity, { toValue: 1, duration: 480, useNativeDriver: false }),
|
|
159
|
-
]),
|
|
160
|
-
);
|
|
161
|
-
loop.start();
|
|
162
|
-
return () => loop.stop();
|
|
163
|
-
}, [opacity]);
|
|
164
|
-
return <Animated.Text style={{ opacity, color: solid("blue") }}>▍</Animated.Text>;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const styles = StyleSheet.create({
|
|
168
|
-
narration: { lineHeight: 21, color: colors.zinc[700] },
|
|
169
|
-
});
|
package/src/markdown.css
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
.ui-markdown {
|
|
2
|
+
font-family: Inter_400Regular, "apple-system", "BlinkMacSystemFont", "Segoe UI",
|
|
3
|
+
"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
|
|
4
|
+
"Helvetica Neue", sans-serif;
|
|
5
|
+
font-size: 14px;
|
|
6
|
+
line-height: 24px;
|
|
7
|
+
letter-spacing: -0.4px;
|
|
8
|
+
color: rgba(24, 24, 27, 1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* Headings */
|
|
12
|
+
|
|
13
|
+
.ui-markdown h1 {
|
|
14
|
+
font-family: Inter_600SemiBold, "apple-system", "BlinkMacSystemFont", "Segoe UI",
|
|
15
|
+
"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
|
|
16
|
+
"Helvetica Neue", sans-serif;
|
|
17
|
+
font-size: 24px;
|
|
18
|
+
line-height: 30px;
|
|
19
|
+
font-weight: 600;
|
|
20
|
+
padding-top: 12px;
|
|
21
|
+
padding-bottom: 4px;
|
|
22
|
+
margin: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.ui-markdown h2 {
|
|
26
|
+
font-family: Inter_600SemiBold, "apple-system", "BlinkMacSystemFont", "Segoe UI",
|
|
27
|
+
"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
|
|
28
|
+
"Helvetica Neue", sans-serif;
|
|
29
|
+
font-size: 18px;
|
|
30
|
+
line-height: 24px;
|
|
31
|
+
font-weight: 600;
|
|
32
|
+
padding-top: 12px;
|
|
33
|
+
padding-bottom: 4px;
|
|
34
|
+
margin: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.ui-markdown h3 {
|
|
38
|
+
font-family: Inter_600SemiBold, "apple-system", "BlinkMacSystemFont", "Segoe UI",
|
|
39
|
+
"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
|
|
40
|
+
"Helvetica Neue", sans-serif;
|
|
41
|
+
font-size: 16px;
|
|
42
|
+
line-height: 22px;
|
|
43
|
+
font-weight: 600;
|
|
44
|
+
padding-top: 8px;
|
|
45
|
+
padding-bottom: 4px;
|
|
46
|
+
margin: 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.ui-markdown h4,
|
|
50
|
+
.ui-markdown h5,
|
|
51
|
+
.ui-markdown h6 {
|
|
52
|
+
font-family: Inter_500Medium, "apple-system", "BlinkMacSystemFont", "Segoe UI",
|
|
53
|
+
"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
|
|
54
|
+
"Helvetica Neue", sans-serif;
|
|
55
|
+
font-size: 14px;
|
|
56
|
+
line-height: 20px;
|
|
57
|
+
font-weight: 500;
|
|
58
|
+
padding-top: 8px;
|
|
59
|
+
padding-bottom: 4px;
|
|
60
|
+
margin: 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Block elements */
|
|
64
|
+
|
|
65
|
+
.ui-markdown p {
|
|
66
|
+
margin: 0;
|
|
67
|
+
padding: 4px 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.ui-markdown blockquote {
|
|
71
|
+
border-left: 3px solid rgba(228, 228, 231, 1);
|
|
72
|
+
margin: 4px 0;
|
|
73
|
+
padding-left: 12px;
|
|
74
|
+
color: rgba(113, 113, 122, 1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.ui-markdown hr {
|
|
78
|
+
border: none;
|
|
79
|
+
border-top: 1px solid rgba(228, 228, 231, 1);
|
|
80
|
+
margin: 8px 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Lists */
|
|
84
|
+
|
|
85
|
+
.ui-markdown ul,
|
|
86
|
+
.ui-markdown ol {
|
|
87
|
+
padding-left: 24px;
|
|
88
|
+
margin: 4px 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.ui-markdown li {
|
|
92
|
+
padding-top: 2px;
|
|
93
|
+
padding-bottom: 2px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Inline */
|
|
97
|
+
|
|
98
|
+
.ui-markdown strong {
|
|
99
|
+
font-family: Inter_500Medium, "apple-system", "BlinkMacSystemFont", "Segoe UI",
|
|
100
|
+
"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
|
|
101
|
+
"Helvetica Neue", sans-serif;
|
|
102
|
+
font-weight: 500;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.ui-markdown a {
|
|
106
|
+
color: rgba(59, 130, 246, 1);
|
|
107
|
+
text-decoration: underline;
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.ui-markdown del {
|
|
112
|
+
color: rgba(113, 113, 122, 1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Code */
|
|
116
|
+
|
|
117
|
+
.ui-markdown code {
|
|
118
|
+
padding: 1px 5px;
|
|
119
|
+
color: rgba(127, 29, 29, 1);
|
|
120
|
+
background-color: rgba(244, 244, 245, 1);
|
|
121
|
+
border-radius: 4px;
|
|
122
|
+
font-size: 13px;
|
|
123
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.ui-markdown pre {
|
|
127
|
+
background-color: rgba(244, 244, 245, 1);
|
|
128
|
+
padding: 12px;
|
|
129
|
+
border-radius: 8px;
|
|
130
|
+
margin: 4px 0;
|
|
131
|
+
overflow-x: auto;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.ui-markdown pre code {
|
|
135
|
+
padding: 0;
|
|
136
|
+
color: inherit;
|
|
137
|
+
background-color: transparent;
|
|
138
|
+
border-radius: 0;
|
|
139
|
+
font-size: 13px;
|
|
140
|
+
line-height: 20px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Tables */
|
|
144
|
+
|
|
145
|
+
.ui-markdown table {
|
|
146
|
+
border-collapse: collapse;
|
|
147
|
+
margin: 4px 0;
|
|
148
|
+
width: max-content;
|
|
149
|
+
min-width: 100%;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.ui-markdown th,
|
|
153
|
+
.ui-markdown td {
|
|
154
|
+
border: 1px solid rgba(228, 228, 231, 1);
|
|
155
|
+
padding: 6px 12px;
|
|
156
|
+
text-align: left;
|
|
157
|
+
white-space: nowrap;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.ui-markdown th {
|
|
161
|
+
background-color: rgba(244, 244, 245, 1);
|
|
162
|
+
font-family: Inter_500Medium, "apple-system", "BlinkMacSystemFont", "Segoe UI",
|
|
163
|
+
"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
|
|
164
|
+
"Helvetica Neue", sans-serif;
|
|
165
|
+
font-weight: 500;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Table copy button */
|
|
169
|
+
|
|
170
|
+
.ui-markdown-table-wrapper {
|
|
171
|
+
position: relative;
|
|
172
|
+
margin: 16px 0;
|
|
173
|
+
max-width: 100%;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.ui-markdown-table-scroll {
|
|
177
|
+
overflow-x: auto;
|
|
178
|
+
max-width: 100%;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.ui-markdown-table-wrapper table {
|
|
182
|
+
margin: 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.ui-markdown-table-copy {
|
|
186
|
+
position: absolute;
|
|
187
|
+
top: 1px;
|
|
188
|
+
right: 1px;
|
|
189
|
+
opacity: 0;
|
|
190
|
+
transition: opacity 0.15s ease-out;
|
|
191
|
+
background: rgba(244, 244, 245, 0.9);
|
|
192
|
+
border: 1px solid rgba(228, 228, 231, 1);
|
|
193
|
+
border-radius: 0 0 0 6px;
|
|
194
|
+
width: 28px;
|
|
195
|
+
height: 28px;
|
|
196
|
+
padding: 0;
|
|
197
|
+
cursor: pointer;
|
|
198
|
+
display: flex;
|
|
199
|
+
align-items: center;
|
|
200
|
+
justify-content: center;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.ui-markdown-table-wrapper:hover .ui-markdown-table-copy {
|
|
204
|
+
opacity: 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.ui-markdown-table-copy:hover {
|
|
208
|
+
background: rgba(228, 228, 231, 1);
|
|
209
|
+
}
|
package/src/markdown.tsx
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { View } from "react-native";
|
|
2
|
+
import { Text } from "./text";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Native fallback. The rich renderer (`markdown.web.tsx`) is react-markdown over
|
|
6
|
+
* the DOM (`<div>`/`<table>`/clipboard) — web-only. `@lotics/ui` also builds for
|
|
7
|
+
* native, so this renders the source as plain text. Rich markdown shows on web
|
|
8
|
+
* (custom-code apps + the frontend on web), which is where it's used; the web
|
|
9
|
+
* variant is picked via the conditional `exports` in package.json.
|
|
10
|
+
*/
|
|
11
|
+
export function Markdown({ children }: { children: string }) {
|
|
12
|
+
return (
|
|
13
|
+
<View>
|
|
14
|
+
<Text size="sm">{children}</Text>
|
|
15
|
+
</View>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import "./markdown.css";
|
|
2
|
+
import React, { useCallback, useRef, useState } from "react";
|
|
3
|
+
import { View } from "react-native";
|
|
4
|
+
import ReactMarkdown from "react-markdown";
|
|
5
|
+
import remarkGfm from "remark-gfm";
|
|
6
|
+
import { Icon } from "./icon";
|
|
7
|
+
import { colors } from "./colors";
|
|
8
|
+
|
|
9
|
+
export function Markdown({ children }: { children: string }) {
|
|
10
|
+
return (
|
|
11
|
+
<View>
|
|
12
|
+
<div className="ui-markdown">
|
|
13
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
|
14
|
+
{children}
|
|
15
|
+
</ReactMarkdown>
|
|
16
|
+
</div>
|
|
17
|
+
</View>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const markdownComponents = {
|
|
22
|
+
table: CopyableTable,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function CopyableTable(props: React.ComponentPropsWithoutRef<"table"> & { node?: unknown }) {
|
|
26
|
+
const { node: _node, children, ...rest } = props;
|
|
27
|
+
const tableRef = useRef<HTMLTableElement>(null);
|
|
28
|
+
const [copied, setCopied] = useState(false);
|
|
29
|
+
|
|
30
|
+
const handleCopy = useCallback(async () => {
|
|
31
|
+
const table = tableRef.current;
|
|
32
|
+
if (!table) return;
|
|
33
|
+
|
|
34
|
+
const rows = table.querySelectorAll("tr");
|
|
35
|
+
const tsv = Array.from(rows)
|
|
36
|
+
.map((row) =>
|
|
37
|
+
Array.from(row.querySelectorAll("th, td"))
|
|
38
|
+
.map((cell) => (cell.textContent ?? "").replace(/\t/g, " ").replace(/\n/g, " ").trim())
|
|
39
|
+
.join("\t"),
|
|
40
|
+
)
|
|
41
|
+
.join("\n");
|
|
42
|
+
|
|
43
|
+
await navigator.clipboard.writeText(tsv);
|
|
44
|
+
setCopied(true);
|
|
45
|
+
setTimeout(() => setCopied(false), 2000);
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="ui-markdown-table-wrapper">
|
|
50
|
+
<div className="ui-markdown-table-scroll">
|
|
51
|
+
<table ref={tableRef} {...rest}>
|
|
52
|
+
{children}
|
|
53
|
+
</table>
|
|
54
|
+
</div>
|
|
55
|
+
<button
|
|
56
|
+
className="ui-markdown-table-copy"
|
|
57
|
+
onClick={handleCopy}
|
|
58
|
+
type="button"
|
|
59
|
+
title={copied ? "Copied!" : "Copy table"}
|
|
60
|
+
>
|
|
61
|
+
<Icon
|
|
62
|
+
name={copied ? "check" : "copy"}
|
|
63
|
+
size={14}
|
|
64
|
+
color={copied ? colors.green[600] : colors.zinc[500]}
|
|
65
|
+
/>
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -8,10 +8,18 @@ import * as path from "node:path";
|
|
|
8
8
|
* `@lotics/ui` is the public, published, universal-rendering primitives
|
|
9
9
|
* package. Every file in `src/` is a primitive. It must not import any
|
|
10
10
|
* dependency that ties it to a specific Lotics runtime context: no chat
|
|
11
|
-
* SDK, no analytics, no i18n, no
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* SDK, no analytics, no i18n, and no Lotics domain types (which live in
|
|
12
|
+
* `@lotics/shared`). Lotics-coupled UI is in the workspace-private
|
|
13
|
+
* `@lotics/ui-internal` package, which may import from here and from
|
|
14
|
+
* `@lotics/shared` freely.
|
|
15
|
+
*
|
|
16
|
+
* Markdown (react-markdown/remark) is the one heavy third-party dependency
|
|
17
|
+
* the kit carries on purpose — it's the single canonical renderer for chat,
|
|
18
|
+
* apps, and `AgentRun`. It's generic, not a Lotics coupling; it stays
|
|
19
|
+
* universal via the `markdown.web.tsx` (DOM) / `markdown.tsx` (native
|
|
20
|
+
* plain-text) split and is tree-shaken out of any consumer that doesn't
|
|
21
|
+
* import `./markdown`. So it's allowed — the guards below remain for the
|
|
22
|
+
* genuine context couplings.
|
|
15
23
|
*
|
|
16
24
|
* The boundary is enforced by this test — without it primitives drift
|
|
17
25
|
* back into the parent app's contexts over months.
|
|
@@ -22,8 +30,6 @@ const FORBIDDEN_PATTERNS = [
|
|
|
22
30
|
/from\s+["']@lingui\//,
|
|
23
31
|
/from\s+["']@ai-sdk\//,
|
|
24
32
|
/from\s+["']ai["']/,
|
|
25
|
-
/from\s+["']react-markdown/,
|
|
26
|
-
/from\s+["']remark-/,
|
|
27
33
|
/from\s+["']@lotics\/shared/,
|
|
28
34
|
];
|
|
29
35
|
|
|
@@ -38,7 +44,7 @@ function listTopLevelSourceFiles(): string[] {
|
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
describe("packages/ui primitives — purity", () => {
|
|
41
|
-
it("no primitive imports posthog, lingui, ai-sdk,
|
|
47
|
+
it("no primitive imports posthog, lingui, ai-sdk, or @lotics/shared", () => {
|
|
42
48
|
const violations: string[] = [];
|
|
43
49
|
for (const file of listTopLevelSourceFiles()) {
|
|
44
50
|
const src = fs.readFileSync(file, "utf-8");
|