@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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -0
  3. package/package.json +119 -0
  4. package/src/diff-viewer/DiffViewer.tsx +168 -0
  5. package/src/diff-viewer/README.md +33 -0
  6. package/src/diff-viewer/components/CopyButton.tsx +49 -0
  7. package/src/diff-viewer/components/DiffLineContent.tsx +48 -0
  8. package/src/diff-viewer/components/SplitView.tsx +270 -0
  9. package/src/diff-viewer/components/UnifiedView.tsx +189 -0
  10. package/src/diff-viewer/diff-viewer.stories.tsx +168 -0
  11. package/src/diff-viewer/hooks/useDiff.ts +47 -0
  12. package/src/diff-viewer/hooks/useHighlighter.ts +54 -0
  13. package/src/diff-viewer/index.ts +22 -0
  14. package/src/diff-viewer/lazy.tsx +25 -0
  15. package/src/diff-viewer/types.ts +125 -0
  16. package/src/diff-viewer/utils/computeDiff.ts +171 -0
  17. package/src/editor/README.md +187 -0
  18. package/src/editor/components/DiffEditor.tsx +158 -0
  19. package/src/editor/components/Editor.tsx +293 -0
  20. package/src/editor/components/index.ts +2 -0
  21. package/src/editor/context/EditorProvider.tsx +212 -0
  22. package/src/editor/context/index.ts +1 -0
  23. package/src/editor/editor.stories.tsx +397 -0
  24. package/src/editor/hooks/index.ts +4 -0
  25. package/src/editor/hooks/useEditor.ts +36 -0
  26. package/src/editor/hooks/useEditorTheme.ts +91 -0
  27. package/src/editor/hooks/useLanguage.ts +29 -0
  28. package/src/editor/hooks/useMonaco.ts +79 -0
  29. package/src/editor/index.ts +16 -0
  30. package/src/editor/lazy.tsx +76 -0
  31. package/src/editor/lib/index.ts +2 -0
  32. package/src/editor/lib/languages.ts +227 -0
  33. package/src/editor/lib/themes.ts +42 -0
  34. package/src/editor/types/index.ts +137 -0
  35. package/src/editor/workers/index.ts +1 -0
  36. package/src/editor/workers/setup.ts +58 -0
  37. package/src/index.ts +10 -0
  38. package/src/json-editor/JsonEditor.tsx +115 -0
  39. package/src/json-editor/index.ts +1 -0
  40. package/src/json-editor/lazy.tsx +24 -0
  41. package/src/markdown-message/ActionRow.tsx +53 -0
  42. package/src/markdown-message/ChatMessageRow.tsx +103 -0
  43. package/src/markdown-message/CodeBlock.tsx +137 -0
  44. package/src/markdown-message/CollapseToggle.tsx +69 -0
  45. package/src/markdown-message/MarkdownMessage.tsx +376 -0
  46. package/src/markdown-message/README.md +221 -0
  47. package/src/markdown-message/builtinRules.tsx +92 -0
  48. package/src/markdown-message/components.tsx +224 -0
  49. package/src/markdown-message/index.ts +22 -0
  50. package/src/markdown-message/linkRules.ts +83 -0
  51. package/src/markdown-message/plainText.ts +83 -0
  52. package/src/markdown-message/sanitize.ts +88 -0
  53. package/src/markdown-message/streamingBlocks.ts +168 -0
  54. package/src/markdown-message/types.ts +168 -0
  55. package/src/panel-header/CodePanelHeader.tsx +121 -0
  56. package/src/panel-header/index.ts +1 -0
  57. package/src/pretty-code/PrettyCode.client.tsx +402 -0
  58. package/src/pretty-code/README.md +81 -0
  59. package/src/pretty-code/index.tsx +72 -0
  60. package/src/pretty-code/lazy.tsx +69 -0
  61. package/src/pretty-code/pretty-code.stories.tsx +353 -0
  62. package/src/pretty-code/registerPrismLanguages.ts +116 -0
  63. package/src/role-tokens.ts +39 -0
  64. 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;