@djangocfg/widget-code 0.1.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.
- package/LICENSE +21 -0
- package/README.md +38 -0
- package/package.json +119 -0
- package/src/diff-viewer/DiffViewer.tsx +168 -0
- package/src/diff-viewer/README.md +33 -0
- package/src/diff-viewer/components/CopyButton.tsx +49 -0
- package/src/diff-viewer/components/DiffLineContent.tsx +48 -0
- package/src/diff-viewer/components/SplitView.tsx +270 -0
- package/src/diff-viewer/components/UnifiedView.tsx +189 -0
- package/src/diff-viewer/diff-viewer.stories.tsx +168 -0
- package/src/diff-viewer/hooks/useDiff.ts +47 -0
- package/src/diff-viewer/hooks/useHighlighter.ts +54 -0
- package/src/diff-viewer/index.ts +22 -0
- package/src/diff-viewer/lazy.tsx +25 -0
- package/src/diff-viewer/types.ts +125 -0
- package/src/diff-viewer/utils/computeDiff.ts +171 -0
- package/src/editor/README.md +187 -0
- package/src/editor/components/DiffEditor.tsx +158 -0
- package/src/editor/components/Editor.tsx +293 -0
- package/src/editor/components/index.ts +2 -0
- package/src/editor/context/EditorProvider.tsx +212 -0
- package/src/editor/context/index.ts +1 -0
- package/src/editor/editor.stories.tsx +397 -0
- package/src/editor/hooks/index.ts +4 -0
- package/src/editor/hooks/useEditor.ts +36 -0
- package/src/editor/hooks/useEditorTheme.ts +91 -0
- package/src/editor/hooks/useLanguage.ts +29 -0
- package/src/editor/hooks/useMonaco.ts +79 -0
- package/src/editor/index.ts +16 -0
- package/src/editor/lazy.tsx +76 -0
- package/src/editor/lib/index.ts +2 -0
- package/src/editor/lib/languages.ts +227 -0
- package/src/editor/lib/themes.ts +42 -0
- package/src/editor/types/index.ts +137 -0
- package/src/editor/workers/index.ts +1 -0
- package/src/editor/workers/setup.ts +58 -0
- package/src/index.ts +10 -0
- package/src/json-editor/JsonEditor.tsx +115 -0
- package/src/json-editor/index.ts +1 -0
- package/src/json-editor/lazy.tsx +24 -0
- package/src/markdown-message/ActionRow.tsx +53 -0
- package/src/markdown-message/ChatMessageRow.tsx +103 -0
- package/src/markdown-message/CodeBlock.tsx +137 -0
- package/src/markdown-message/CollapseToggle.tsx +69 -0
- package/src/markdown-message/MarkdownMessage.tsx +376 -0
- package/src/markdown-message/README.md +221 -0
- package/src/markdown-message/builtinRules.tsx +92 -0
- package/src/markdown-message/components.tsx +224 -0
- package/src/markdown-message/index.ts +22 -0
- package/src/markdown-message/linkRules.ts +83 -0
- package/src/markdown-message/plainText.ts +83 -0
- package/src/markdown-message/sanitize.ts +88 -0
- package/src/markdown-message/streamingBlocks.ts +168 -0
- package/src/markdown-message/types.ts +168 -0
- package/src/panel-header/CodePanelHeader.tsx +121 -0
- package/src/panel-header/index.ts +1 -0
- package/src/pretty-code/PrettyCode.client.tsx +402 -0
- package/src/pretty-code/README.md +81 -0
- package/src/pretty-code/index.tsx +72 -0
- package/src/pretty-code/lazy.tsx +69 -0
- package/src/pretty-code/pretty-code.stories.tsx +353 -0
- package/src/pretty-code/registerPrismLanguages.ts +116 -0
- package/src/role-tokens.ts +39 -0
- package/src/use-collapsible-content.ts +236 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role-aware inline styling for markdown rendered on a two-role surface
|
|
3
|
+
* (a chat bubble being the only one today).
|
|
4
|
+
*
|
|
5
|
+
* These live in `common/` rather than in the chat package because their
|
|
6
|
+
* consumer is `MarkdownMessage`, which is NOT chat-only — OpenapiViewer,
|
|
7
|
+
* PrettyCode and UrlChip render it too. The chat widget re-exports them from
|
|
8
|
+
* its own `styles/bubbleTokens`, so chat code keeps one import site and the
|
|
9
|
+
* dependency runs one way: chat → code, never back.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** The two surfaces markdown can be painted on. */
|
|
13
|
+
export type MarkdownSurfaceRole = 'user' | 'assistant';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Anchor (link) classes for markdown content rendered inside a bubble.
|
|
17
|
+
*
|
|
18
|
+
* The two roles CANNOT share a link color, because the user bubble is an
|
|
19
|
+
* INVERTED surface. `--primary` is tuned for contrast against the page, so on a
|
|
20
|
+
* bubble painted with `--foreground` it is ink-on-ink: in the light theme a
|
|
21
|
+
* deep cyan (`192 90% 35%`) on near-black, which all but disappears.
|
|
22
|
+
*
|
|
23
|
+
* So the user bubble draws its link from `--background` — the token that IS the
|
|
24
|
+
* readable counterpart of the surface it sits on, in every theme by definition —
|
|
25
|
+
* and marks the link with an underline rather than a hue shift. The assistant
|
|
26
|
+
* sits on the plain transcript, so brand-primary works there as usual.
|
|
27
|
+
*/
|
|
28
|
+
export const ANCHOR = {
|
|
29
|
+
user:
|
|
30
|
+
'text-background underline decoration-background/50 underline-offset-2 ' +
|
|
31
|
+
'hover:decoration-background transition-colors break-all',
|
|
32
|
+
assistant: 'text-primary underline hover:text-primary/80 transition-colors break-all',
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
/** Inline secondary action (e.g. "Show more / less"). Same logic as anchors. */
|
|
36
|
+
export const TOGGLE = {
|
|
37
|
+
user: 'text-background/80 hover:text-background',
|
|
38
|
+
assistant: 'text-primary hover:text-primary/80',
|
|
39
|
+
} as const;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface UseCollapsibleContentOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Maximum character length before collapsing
|
|
8
|
+
* If both maxLength and maxLines are set, the stricter limit applies
|
|
9
|
+
*/
|
|
10
|
+
maxLength?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Maximum number of lines before collapsing
|
|
13
|
+
* If both maxLength and maxLines are set, the stricter limit applies
|
|
14
|
+
*/
|
|
15
|
+
maxLines?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Start in expanded state (default: false - starts collapsed)
|
|
18
|
+
*/
|
|
19
|
+
defaultExpanded?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UseCollapsibleContentResult {
|
|
23
|
+
/** Whether content is currently collapsed */
|
|
24
|
+
isCollapsed: boolean;
|
|
25
|
+
/** Toggle between collapsed/expanded state */
|
|
26
|
+
toggleCollapsed: () => void;
|
|
27
|
+
/** Set collapsed state directly */
|
|
28
|
+
setCollapsed: (collapsed: boolean) => void;
|
|
29
|
+
/** Content to display (truncated if collapsed, full if expanded) */
|
|
30
|
+
displayContent: string;
|
|
31
|
+
/** Whether the content exceeds limits and should be collapsible */
|
|
32
|
+
shouldCollapse: boolean;
|
|
33
|
+
/** Original content length */
|
|
34
|
+
originalLength: number;
|
|
35
|
+
/** Original line count */
|
|
36
|
+
originalLineCount: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Smart truncation that doesn't break words or markdown syntax
|
|
41
|
+
*/
|
|
42
|
+
function smartTruncate(content: string, maxLength: number): string {
|
|
43
|
+
if (content.length <= maxLength) {
|
|
44
|
+
return content;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Find a good break point (space, newline) near maxLength
|
|
48
|
+
let breakPoint = maxLength;
|
|
49
|
+
|
|
50
|
+
// Look backwards for a space or newline
|
|
51
|
+
while (breakPoint > maxLength - 50 && breakPoint > 0) {
|
|
52
|
+
const char = content[breakPoint];
|
|
53
|
+
if (char === ' ' || char === '\n' || char === '\t') {
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
breakPoint--;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// If we couldn't find a good break point, just use maxLength
|
|
60
|
+
if (breakPoint <= maxLength - 50) {
|
|
61
|
+
breakPoint = maxLength;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let truncated = content.slice(0, breakPoint).trimEnd();
|
|
65
|
+
|
|
66
|
+
// Fix unclosed markdown syntax
|
|
67
|
+
truncated = fixUnclosedMarkdown(truncated);
|
|
68
|
+
|
|
69
|
+
return truncated;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Truncate by line count
|
|
74
|
+
*/
|
|
75
|
+
function truncateByLines(content: string, maxLines: number): string {
|
|
76
|
+
const lines = content.split('\n');
|
|
77
|
+
|
|
78
|
+
if (lines.length <= maxLines) {
|
|
79
|
+
return content;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let truncated = lines.slice(0, maxLines).join('\n').trimEnd();
|
|
83
|
+
|
|
84
|
+
// Fix unclosed markdown syntax
|
|
85
|
+
truncated = fixUnclosedMarkdown(truncated);
|
|
86
|
+
|
|
87
|
+
return truncated;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Fix unclosed markdown syntax to prevent rendering issues
|
|
92
|
+
*/
|
|
93
|
+
function fixUnclosedMarkdown(content: string): string {
|
|
94
|
+
let result = content;
|
|
95
|
+
|
|
96
|
+
// Count occurrences of markdown markers
|
|
97
|
+
const countOccurrences = (str: string, marker: string): number => {
|
|
98
|
+
const escaped = marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
99
|
+
const matches = str.match(new RegExp(escaped, 'g'));
|
|
100
|
+
return matches ? matches.length : 0;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Fix unclosed bold (**) - must have even count
|
|
104
|
+
const boldCount = countOccurrences(result, '**');
|
|
105
|
+
if (boldCount % 2 !== 0) {
|
|
106
|
+
result += '**';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Fix unclosed italic (*) - but not **
|
|
110
|
+
// Remove ** first for counting, then count single *
|
|
111
|
+
const withoutBold = result.replace(/\*\*/g, '');
|
|
112
|
+
const italicCount = countOccurrences(withoutBold, '*');
|
|
113
|
+
if (italicCount % 2 !== 0) {
|
|
114
|
+
result += '*';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fix unclosed inline code (`)
|
|
118
|
+
const codeCount = countOccurrences(result, '`');
|
|
119
|
+
// Ignore triple backticks for code blocks
|
|
120
|
+
const tripleCount = countOccurrences(result, '```');
|
|
121
|
+
const singleCodeCount = codeCount - (tripleCount * 3);
|
|
122
|
+
if (singleCodeCount % 2 !== 0) {
|
|
123
|
+
result += '`';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Fix unclosed code blocks (```)
|
|
127
|
+
if (tripleCount % 2 !== 0) {
|
|
128
|
+
result += '\n```';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Fix unclosed strikethrough (~~)
|
|
132
|
+
const strikeCount = countOccurrences(result, '~~');
|
|
133
|
+
if (strikeCount % 2 !== 0) {
|
|
134
|
+
result += '~~';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Fix unclosed underline bold (__)
|
|
138
|
+
const underlineBoldCount = countOccurrences(result, '__');
|
|
139
|
+
if (underlineBoldCount % 2 !== 0) {
|
|
140
|
+
result += '__';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Fix unclosed underline italic (_) - but not __
|
|
144
|
+
const withoutUnderlineBold = result.replace(/__/g, '');
|
|
145
|
+
const underlineItalicCount = countOccurrences(withoutUnderlineBold, '_');
|
|
146
|
+
if (underlineItalicCount % 2 !== 0) {
|
|
147
|
+
result += '_';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Hook for managing collapsible content with "Read more..." functionality
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```tsx
|
|
158
|
+
* const { isCollapsed, toggleCollapsed, displayContent, shouldCollapse } = useCollapsibleContent(
|
|
159
|
+
* longText,
|
|
160
|
+
* { maxLength: 300, maxLines: 5 }
|
|
161
|
+
* );
|
|
162
|
+
*
|
|
163
|
+
* return (
|
|
164
|
+
* <div>
|
|
165
|
+
* <Markdown content={displayContent} />
|
|
166
|
+
* {shouldCollapse && (
|
|
167
|
+
* <button onClick={toggleCollapsed}>
|
|
168
|
+
* {isCollapsed ? 'Read more...' : 'Show less'}
|
|
169
|
+
* </button>
|
|
170
|
+
* )}
|
|
171
|
+
* </div>
|
|
172
|
+
* );
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
export function useCollapsibleContent(
|
|
176
|
+
content: string,
|
|
177
|
+
options: UseCollapsibleContentOptions = {}
|
|
178
|
+
): UseCollapsibleContentResult {
|
|
179
|
+
const { maxLength, maxLines, defaultExpanded = false } = options;
|
|
180
|
+
|
|
181
|
+
const [isCollapsed, setIsCollapsed] = useState(!defaultExpanded);
|
|
182
|
+
|
|
183
|
+
const originalLength = content.length;
|
|
184
|
+
const originalLineCount = content.split('\n').length;
|
|
185
|
+
|
|
186
|
+
const { shouldCollapse, truncatedContent } = useMemo(() => {
|
|
187
|
+
// If no limits set, don't collapse
|
|
188
|
+
if (maxLength === undefined && maxLines === undefined) {
|
|
189
|
+
return { shouldCollapse: false, truncatedContent: content };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let needsCollapse = false;
|
|
193
|
+
let result = content;
|
|
194
|
+
|
|
195
|
+
// Check line limit first (usually more restrictive for chat)
|
|
196
|
+
if (maxLines !== undefined && originalLineCount > maxLines) {
|
|
197
|
+
needsCollapse = true;
|
|
198
|
+
result = truncateByLines(result, maxLines);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Then check character limit
|
|
202
|
+
if (maxLength !== undefined && result.length > maxLength) {
|
|
203
|
+
needsCollapse = true;
|
|
204
|
+
result = smartTruncate(result, maxLength);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { shouldCollapse: needsCollapse, truncatedContent: result };
|
|
208
|
+
}, [content, maxLength, maxLines, originalLineCount]);
|
|
209
|
+
|
|
210
|
+
const displayContent = useMemo(() => {
|
|
211
|
+
if (!shouldCollapse || !isCollapsed) {
|
|
212
|
+
return content;
|
|
213
|
+
}
|
|
214
|
+
return truncatedContent;
|
|
215
|
+
}, [content, truncatedContent, shouldCollapse, isCollapsed]);
|
|
216
|
+
|
|
217
|
+
const toggleCollapsed = useCallback(() => {
|
|
218
|
+
setIsCollapsed((prev) => !prev);
|
|
219
|
+
}, []);
|
|
220
|
+
|
|
221
|
+
const setCollapsed = useCallback((collapsed: boolean) => {
|
|
222
|
+
setIsCollapsed(collapsed);
|
|
223
|
+
}, []);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
isCollapsed,
|
|
227
|
+
toggleCollapsed,
|
|
228
|
+
setCollapsed,
|
|
229
|
+
displayContent,
|
|
230
|
+
shouldCollapse,
|
|
231
|
+
originalLength,
|
|
232
|
+
originalLineCount,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export default useCollapsibleContent;
|