@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,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `@djangocfg/widget-code/json-editor` subpath entrypoint.
|
|
5
|
+
*
|
|
6
|
+
* Heavy editor (~58 KB minified for `json-edit-react` alone). Lazy-load
|
|
7
|
+
* so consumers that only display JSON pay nothing — they should use
|
|
8
|
+
* `@djangocfg/widget-data/json-tree` instead, which is ~10× lighter.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createLazyComponent, LoadingFallback } from '@djangocfg/widget-kit/lazy';
|
|
12
|
+
import type { JsonEditorProps } from './JsonEditor';
|
|
13
|
+
|
|
14
|
+
export type { JsonEditorProps } from './JsonEditor';
|
|
15
|
+
|
|
16
|
+
export const LazyJsonEditor = createLazyComponent<JsonEditorProps>(
|
|
17
|
+
() => import('./JsonEditor').then((m) => ({ default: m.JsonEditor })),
|
|
18
|
+
{
|
|
19
|
+
displayName: 'LazyJsonEditor',
|
|
20
|
+
fallback: <LoadingFallback minHeight={120} text="Loading JSON editor…" />,
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export { LazyJsonEditor as JsonEditor };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { memo } from 'react';
|
|
4
|
+
import { CopyButton } from '@djangocfg/ui-core/components';
|
|
5
|
+
|
|
6
|
+
interface ActionRowProps {
|
|
7
|
+
/** Raw text to copy — pass the same source string used for render. */
|
|
8
|
+
value: string;
|
|
9
|
+
/** Bubble side. Just controls flex alignment; nothing is mirrored. */
|
|
10
|
+
isUser: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Whether the row is currently shown. Owner-controlled (typically
|
|
13
|
+
* `<ChatMessageRow>` toggles this on hover with a small close delay).
|
|
14
|
+
* The component is intentionally dumb about visibility — owners
|
|
15
|
+
* have full control over reveal logic (hover, focus, always-on,
|
|
16
|
+
* touch behaviour, etc.).
|
|
17
|
+
*/
|
|
18
|
+
visible: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Action row that lives directly under a chat bubble.
|
|
22
|
+
// Visibility is fully owner-controlled via `visible`.
|
|
23
|
+
//
|
|
24
|
+
// We render at `opacity-0` (not `display:none`) so the fade in/out
|
|
25
|
+
// stays smooth across re-renders. While invisible we drop pointer
|
|
26
|
+
// events so a hidden row can't intercept clicks meant for the page
|
|
27
|
+
// underneath.
|
|
28
|
+
//
|
|
29
|
+
// Memoised: re-renders only when `value`, `isUser` or `visible` change.
|
|
30
|
+
// All props are primitives, so default shallow comparison is sufficient.
|
|
31
|
+
function ActionRowRaw({ value, isUser, visible }: ActionRowProps) {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
className={`flex ${isUser ? 'justify-end' : 'justify-start'}
|
|
35
|
+
transition-opacity duration-150
|
|
36
|
+
${visible ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}`}
|
|
37
|
+
>
|
|
38
|
+
<CopyButton
|
|
39
|
+
value={value}
|
|
40
|
+
size="icon"
|
|
41
|
+
variant="ghost"
|
|
42
|
+
iconClassName="h-3.5 w-3.5"
|
|
43
|
+
className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
|
44
|
+
// Hidden rows must not be reachable via Tab — otherwise
|
|
45
|
+
// keyboard users land on an invisible button.
|
|
46
|
+
tabIndex={visible ? 0 : -1}
|
|
47
|
+
aria-hidden={!visible}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const ActionRow = memo(ActionRowRaw);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { useMediaQuery } from '@djangocfg/ui-core/hooks';
|
|
5
|
+
|
|
6
|
+
interface ChatMessageRowProps {
|
|
7
|
+
/** Side of the conversation — drives flex alignment and `isUser`
|
|
8
|
+
* passed to `actions`. */
|
|
9
|
+
isUser: boolean;
|
|
10
|
+
/** The bubble (your own JSX). Anything goes — `<MarkdownMessage>`,
|
|
11
|
+
* custom card, multi-element composition. */
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
/** Render-prop for the action row. Receives `visible` so the row
|
|
14
|
+
* knows when to fade in/out. Render `null` to opt out. */
|
|
15
|
+
actions?: (visible: boolean, isUser: boolean) => React.ReactNode;
|
|
16
|
+
/** Close delay in ms after the cursor leaves the row. Long enough
|
|
17
|
+
* to bridge cursor travel from bubble → action button without
|
|
18
|
+
* flicker. Default 250ms (Radix Tooltip-ish). */
|
|
19
|
+
closeDelayMs?: number;
|
|
20
|
+
/** Optional class on the column container. */
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Owner of the bubble + action row layout.
|
|
25
|
+
//
|
|
26
|
+
// Why a state-based hover instead of `group-hover` CSS:
|
|
27
|
+
// - CSS-only hover bridges break when the cursor crosses gaps
|
|
28
|
+
// between the bubble and the (initially-invisible) row — the
|
|
29
|
+
// row's `pointer-events-none` lets the cursor "fall through",
|
|
30
|
+
// `group-hover` flips off, the row vanishes mid-travel.
|
|
31
|
+
// - State + a small close timeout gives Radix-Tooltip-style
|
|
32
|
+
// stability: enter shows immediately, leave waits N ms.
|
|
33
|
+
// - On touch (`@media (hover:none)`) hover doesn't exist; we
|
|
34
|
+
// always pass `visible={true}` so the affordance is reachable.
|
|
35
|
+
//
|
|
36
|
+
// The action row is rendered **absolutely** under the bubble so
|
|
37
|
+
// hidden rows don't allocate vertical space — no dead-air gap
|
|
38
|
+
// between consecutive messages.
|
|
39
|
+
//
|
|
40
|
+
// Memoised: re-renders only when `isUser`, `children`, `actions`,
|
|
41
|
+
// `closeDelayMs` or `className` change. `actions` render-prop is
|
|
42
|
+
// compared by reference — callers should stabilise it with useCallback.
|
|
43
|
+
function ChatMessageRowRaw({
|
|
44
|
+
isUser,
|
|
45
|
+
children,
|
|
46
|
+
actions,
|
|
47
|
+
closeDelayMs = 250,
|
|
48
|
+
className = '',
|
|
49
|
+
}: ChatMessageRowProps) {
|
|
50
|
+
const isTouch = useMediaQuery('(hover: none), (pointer: coarse)');
|
|
51
|
+
const [hovered, setHovered] = useState(false);
|
|
52
|
+
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
53
|
+
|
|
54
|
+
const cancelClose = useCallback(() => {
|
|
55
|
+
if (closeTimer.current) {
|
|
56
|
+
clearTimeout(closeTimer.current);
|
|
57
|
+
closeTimer.current = null;
|
|
58
|
+
}
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const open = useCallback(() => {
|
|
62
|
+
cancelClose();
|
|
63
|
+
setHovered(true);
|
|
64
|
+
}, [cancelClose]);
|
|
65
|
+
|
|
66
|
+
const scheduleClose = useCallback(() => {
|
|
67
|
+
cancelClose();
|
|
68
|
+
closeTimer.current = setTimeout(() => setHovered(false), closeDelayMs);
|
|
69
|
+
}, [cancelClose, closeDelayMs]);
|
|
70
|
+
|
|
71
|
+
useEffect(() => () => cancelClose(), [cancelClose]);
|
|
72
|
+
|
|
73
|
+
// Touch: always-visible. Hover: state-driven.
|
|
74
|
+
const visible = isTouch || hovered;
|
|
75
|
+
|
|
76
|
+
// On touch, `static` positioning so the row claims real layout
|
|
77
|
+
// space (otherwise an absolute always-visible row would still
|
|
78
|
+
// overlap the next message). On hover devices, `absolute` so the
|
|
79
|
+
// hidden row doesn't leave dead air between bubbles.
|
|
80
|
+
const rowPositionClass = isTouch ? 'static mt-1' : 'absolute top-full';
|
|
81
|
+
const rowSideClass = isUser ? 'right-0' : 'left-0';
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
className={`relative flex flex-col ${isUser ? 'items-end' : 'items-start'} ${className}`}
|
|
86
|
+
onPointerEnter={isTouch ? undefined : open}
|
|
87
|
+
onPointerLeave={isTouch ? undefined : scheduleClose}
|
|
88
|
+
onFocusCapture={open}
|
|
89
|
+
onBlurCapture={(e) => {
|
|
90
|
+
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) scheduleClose();
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
{children}
|
|
94
|
+
{actions && (
|
|
95
|
+
<div className={`${rowPositionClass} ${rowSideClass}`}>
|
|
96
|
+
{actions(visible, isUser)}
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const ChatMessageRow = memo(ChatMessageRowRaw);
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import React, { memo } from 'react';
|
|
2
|
+
import { CopyButton } from '@djangocfg/ui-core/components';
|
|
3
|
+
import PrettyCode from '../pretty-code';
|
|
4
|
+
|
|
5
|
+
interface CodeBlockProps {
|
|
6
|
+
code: string;
|
|
7
|
+
language: string;
|
|
8
|
+
isUser: boolean;
|
|
9
|
+
isCompact?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Code surface palette. Default `'auto'` — follow the host theme.
|
|
12
|
+
* `'dark'` / `'light'` pin the palette regardless of theme.
|
|
13
|
+
*/
|
|
14
|
+
codeTheme?: 'dark' | 'light' | 'auto';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Markdown code block. Renders <PrettyCode> directly — its own
|
|
19
|
+
* FloatingToolbar already carries the language tag and Copy action.
|
|
20
|
+
*
|
|
21
|
+
* Earlier versions stacked an extra <CopyButton> here too, which
|
|
22
|
+
* surfaced as two copy buttons on every code fence. The fallback
|
|
23
|
+
* branch below still ships its own button because the plain <pre>
|
|
24
|
+
* has no toolbar of its own.
|
|
25
|
+
*
|
|
26
|
+
* Memoised: re-renders only when `code`, `language`, `isUser` or
|
|
27
|
+
* `isCompact` change. `code` is the main trigger — long code blocks
|
|
28
|
+
* should not re-render when parent chat scrolls.
|
|
29
|
+
*/
|
|
30
|
+
function CodeBlockRaw({ code, language, codeTheme = 'auto' }: CodeBlockProps) {
|
|
31
|
+
// Chat fences are always rendered in PrettyCode's compact mode:
|
|
32
|
+
// 12px font, tighter padding, line-height 1.4. A fenced block in
|
|
33
|
+
// a chat bubble shouldn't outweigh two paragraphs of body text —
|
|
34
|
+
// the standalone PrettyCode story keeps the larger default for
|
|
35
|
+
// docs/diff viewers.
|
|
36
|
+
//
|
|
37
|
+
// `mode` must be UNDEFINED to follow the theme: PrettyCode resolves it as
|
|
38
|
+
// `mode ?? resolvedTheme`, so passing a concrete value — as this did with
|
|
39
|
+
// its old `'dark'` default — pins the palette and defeats that fallback.
|
|
40
|
+
// The pin also propagated to `CodePanelHeader` (`dark={isDarkMode}`), which
|
|
41
|
+
// is why the language label and copy icon stayed `text-white/40` on a light
|
|
42
|
+
// panel. `'auto'` is the default now; a host can still pin deliberately.
|
|
43
|
+
const pinnedMode = codeTheme === 'auto' ? undefined : codeTheme;
|
|
44
|
+
return (
|
|
45
|
+
<div className="my-2">
|
|
46
|
+
<PrettyCode
|
|
47
|
+
data={code}
|
|
48
|
+
language={language}
|
|
49
|
+
className="text-xs"
|
|
50
|
+
mode={pinnedMode}
|
|
51
|
+
// No `customBg` — let PrettyCode paint the surface via its inline
|
|
52
|
+
// `var(--code)` style (see surfaceStyle there). We used to pass
|
|
53
|
+
// `customBg="bg-code"`, but that Tailwind utility only exists in the
|
|
54
|
+
// generated CSS if the CONSUMER scans ui-core/widget source in its
|
|
55
|
+
// Tailwind `content` globs — and most (e.g. cmdop) do not. The class
|
|
56
|
+
// then landed in the DOM with no rule behind it, so the panel rendered
|
|
57
|
+
// with a transparent surface in light mode (in dark mode the ambient
|
|
58
|
+
// dark page hid it). The inline var()-based surface needs no content
|
|
59
|
+
// scan, so it renders correctly in every consumer and both themes.
|
|
60
|
+
isCompact
|
|
61
|
+
// Disable click-to-scroll-isolation in chat markdown: code
|
|
62
|
+
// fences here are part of an assistant reply, not a docs
|
|
63
|
+
// viewer. Forcing the user to click into a small block to
|
|
64
|
+
// scroll past it interrupts the reading flow. The standalone
|
|
65
|
+
// PrettyCode use cases (docs, diff viewers, long code panes)
|
|
66
|
+
// keep the default `true`.
|
|
67
|
+
scrollIsolation={false}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const CodeBlock = memo(CodeBlockRaw);
|
|
74
|
+
|
|
75
|
+
/** Simple `<pre>` fallback used when CodeBlock throws (lazy module
|
|
76
|
+
* failure, missing PrettyCode peer, etc).
|
|
77
|
+
*
|
|
78
|
+
* Memoised: re-renders only when `code`, `language`, `isUser` or
|
|
79
|
+
* `isCompact` change.
|
|
80
|
+
*/
|
|
81
|
+
function CodeBlockFallbackRaw({ code, codeTheme = 'auto' }: CodeBlockProps) {
|
|
82
|
+
// Only an explicit `'dark'` pin takes the hard-coded dark surface. `'auto'`
|
|
83
|
+
// uses the token branch, which is already theme-aware (`--code*` is defined
|
|
84
|
+
// in both light.css and dark.css) — so it renders correctly in EITHER theme.
|
|
85
|
+
// Testing `=== 'dark'` and letting everything else fall through to a fixed
|
|
86
|
+
// LIGHT surface would have been the same class of bug this fixes, inverted.
|
|
87
|
+
const isDark = codeTheme === 'dark';
|
|
88
|
+
// On the dark surface the copy button is always light; on a light
|
|
89
|
+
// surface it follows the semantic muted token.
|
|
90
|
+
const copyButtonClass =
|
|
91
|
+
`absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity h-8 w-8 ` +
|
|
92
|
+
(isDark
|
|
93
|
+
? 'text-white/70 hover:text-white hover:bg-white/15'
|
|
94
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-muted');
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="relative group my-3">
|
|
98
|
+
<CopyButton
|
|
99
|
+
value={code}
|
|
100
|
+
variant="ghost"
|
|
101
|
+
className={copyButtonClass}
|
|
102
|
+
title="Copy code"
|
|
103
|
+
/>
|
|
104
|
+
{isDark ? (
|
|
105
|
+
// Fixed dark surface to match PrettyCode's default.
|
|
106
|
+
<pre
|
|
107
|
+
className="p-3 rounded text-xs font-mono overflow-x-auto border"
|
|
108
|
+
style={{
|
|
109
|
+
backgroundColor: '#0d1117',
|
|
110
|
+
color: '#e5e7eb',
|
|
111
|
+
borderColor: '#1f2937',
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<code>{code}</code>
|
|
115
|
+
</pre>
|
|
116
|
+
) : (
|
|
117
|
+
<pre
|
|
118
|
+
className="p-3 rounded text-xs font-mono overflow-x-auto border"
|
|
119
|
+
// Inline `var(--code*)` rather than `bg-code`/`text-code-foreground`/
|
|
120
|
+
// `border-code-border` classes: those utilities only exist when the
|
|
121
|
+
// consumer scans this widget's source in its Tailwind content globs, which
|
|
122
|
+
// many don't — see the note in CodeBlockRaw. The vars are defined in
|
|
123
|
+
// both light.css and dark.css, so this stays theme-aware everywhere.
|
|
124
|
+
style={{
|
|
125
|
+
backgroundColor: 'var(--code)',
|
|
126
|
+
color: 'var(--code-foreground)',
|
|
127
|
+
borderColor: 'var(--code-border)',
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
<code>{code}</code>
|
|
131
|
+
</pre>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const CodeBlockFallback = memo(CodeBlockFallbackRaw);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React, { memo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { TOGGLE } from '../role-tokens';
|
|
4
|
+
|
|
5
|
+
interface CollapseToggleProps {
|
|
6
|
+
isCollapsed: boolean;
|
|
7
|
+
onClick: () => void;
|
|
8
|
+
readMoreLabel: string;
|
|
9
|
+
showLessLabel: string;
|
|
10
|
+
isUser: boolean;
|
|
11
|
+
isCompact: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** "Read more..." / "Show less" button.
|
|
15
|
+
*
|
|
16
|
+
* Memoised: re-renders only when `isCollapsed`, `onClick`, `readMoreLabel`,
|
|
17
|
+
* `showLessLabel`, `isUser` or `isCompact` change. `onClick` is compared
|
|
18
|
+
* by reference — callers should stabilise it with useCallback.
|
|
19
|
+
*/
|
|
20
|
+
function CollapseToggleRaw({
|
|
21
|
+
isCollapsed,
|
|
22
|
+
onClick,
|
|
23
|
+
readMoreLabel,
|
|
24
|
+
showLessLabel,
|
|
25
|
+
isUser,
|
|
26
|
+
isCompact,
|
|
27
|
+
}: CollapseToggleProps) {
|
|
28
|
+
const textSize = isCompact ? 'text-xs' : 'text-sm';
|
|
29
|
+
return (
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
onClick={onClick}
|
|
33
|
+
className={`
|
|
34
|
+
${textSize} font-medium cursor-pointer
|
|
35
|
+
transition-colors duration-200
|
|
36
|
+
${isUser ? TOGGLE.user : TOGGLE.assistant}
|
|
37
|
+
inline-flex items-center gap-1
|
|
38
|
+
mt-1
|
|
39
|
+
`}
|
|
40
|
+
>
|
|
41
|
+
{isCollapsed ? (
|
|
42
|
+
<>
|
|
43
|
+
{readMoreLabel}
|
|
44
|
+
<Chevron direction="down" />
|
|
45
|
+
</>
|
|
46
|
+
) : (
|
|
47
|
+
<>
|
|
48
|
+
{showLessLabel}
|
|
49
|
+
<Chevron direction="up" />
|
|
50
|
+
</>
|
|
51
|
+
)}
|
|
52
|
+
</button>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const Chevron = memo(function Chevron({ direction }: { direction: 'up' | 'down' }) {
|
|
57
|
+
return (
|
|
58
|
+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
59
|
+
<path
|
|
60
|
+
strokeLinecap="round"
|
|
61
|
+
strokeLinejoin="round"
|
|
62
|
+
strokeWidth={2}
|
|
63
|
+
d={direction === 'down' ? 'M19 9l-7 7-7-7' : 'M5 15l7-7 7 7'}
|
|
64
|
+
/>
|
|
65
|
+
</svg>
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export const CollapseToggle = memo(CollapseToggleRaw);
|