@djangocfg/ui-tools 2.1.312 → 2.1.313
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/README.md +38 -22
- package/dist/{DocsLayout-W5JLRNSZ.mjs → DocsLayout-ESVQZO3V.mjs} +3 -3
- package/dist/{DocsLayout-W5JLRNSZ.mjs.map → DocsLayout-ESVQZO3V.mjs.map} +1 -1
- package/dist/{DocsLayout-ZXD2CUOH.cjs → DocsLayout-KUPDWJ3G.cjs} +48 -48
- package/dist/{DocsLayout-ZXD2CUOH.cjs.map → DocsLayout-KUPDWJ3G.cjs.map} +1 -1
- package/dist/Player-M3GC3VPE.mjs +4 -0
- package/dist/Player-M3GC3VPE.mjs.map +1 -0
- package/dist/Player-ZGQKKOWI.css +65 -0
- package/dist/Player-ZGQKKOWI.css.map +1 -0
- package/dist/Player-ZL2X5LGG.cjs +13 -0
- package/dist/Player-ZL2X5LGG.cjs.map +1 -0
- package/dist/{chunk-CXVGN6ZW.cjs → chunk-DFTVB66S.cjs} +7 -6
- package/dist/chunk-DFTVB66S.cjs.map +1 -0
- package/dist/{chunk-2QY3LJR6.mjs → chunk-EUADAUBQ.mjs} +5 -4
- package/dist/chunk-EUADAUBQ.mjs.map +1 -0
- package/dist/chunk-FX2QFYWF.mjs +2059 -0
- package/dist/chunk-FX2QFYWF.mjs.map +1 -0
- package/dist/{chunk-6HNAPVZ2.mjs → chunk-GBLQTHWT.mjs} +11 -13
- package/dist/chunk-GBLQTHWT.mjs.map +1 -0
- package/dist/{chunk-FYLR232K.cjs → chunk-S44PW6NK.cjs} +11 -13
- package/dist/chunk-S44PW6NK.cjs.map +1 -0
- package/dist/chunk-ZLQHUZDU.cjs +2061 -0
- package/dist/chunk-ZLQHUZDU.cjs.map +1 -0
- package/dist/components-WYEZL5TE.cjs +26 -0
- package/dist/{components-3RTH76CV.cjs.map → components-WYEZL5TE.cjs.map} +1 -1
- package/dist/components-ZAGG2PBO.mjs +5 -0
- package/dist/{components-5GVVL2Q6.mjs.map → components-ZAGG2PBO.mjs.map} +1 -1
- package/dist/index.cjs +36 -220
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +65 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +44 -500
- package/dist/index.d.ts +44 -500
- package/dist/index.mjs +16 -62
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/components/markdown/MarkdownMessage/ActionRow.tsx +48 -0
- package/src/components/markdown/MarkdownMessage/ChatMessageRow.tsx +97 -0
- package/src/components/markdown/MarkdownMessage/CodeBlock.tsx +9 -13
- package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +77 -2
- package/src/components/markdown/MarkdownMessage/MarkdownMessage.tsx +2 -3
- package/src/components/markdown/MarkdownMessage/README.md +72 -0
- package/src/components/markdown/MarkdownMessage/components.tsx +3 -3
- package/src/components/markdown/MarkdownMessage/index.ts +6 -0
- package/src/index.ts +2 -11
- package/src/tools/AudioPlayer/AudioPlayer.story.tsx +454 -107
- package/src/tools/AudioPlayer/Player.tsx +80 -0
- package/src/tools/AudioPlayer/PlayerShell.tsx +122 -0
- package/src/tools/AudioPlayer/README.md +139 -204
- package/src/tools/AudioPlayer/audio/audioContext.ts +39 -0
- package/src/tools/AudioPlayer/audio/decodePeaks.ts +36 -0
- package/src/tools/AudioPlayer/audio/index.ts +4 -0
- package/src/tools/AudioPlayer/audio/mediaElementSourceCache.ts +20 -0
- package/src/tools/AudioPlayer/audio/peaksCache.ts +37 -0
- package/src/tools/AudioPlayer/context/AudioRefContext.tsx +9 -0
- package/src/tools/AudioPlayer/context/ControlsContext.tsx +7 -0
- package/src/tools/AudioPlayer/context/LevelsContext.tsx +7 -0
- package/src/tools/AudioPlayer/context/MetaContext.tsx +16 -0
- package/src/tools/AudioPlayer/context/PlayerProvider.tsx +314 -0
- package/src/tools/AudioPlayer/context/StateContext.tsx +7 -0
- package/src/tools/AudioPlayer/context/index.ts +16 -15
- package/src/tools/AudioPlayer/context/selectors.ts +36 -0
- package/src/tools/AudioPlayer/hooks/index.ts +12 -39
- package/src/tools/AudioPlayer/hooks/useActivePlayer.ts +31 -0
- package/src/tools/AudioPlayer/hooks/useAnalyser.ts +62 -0
- package/src/tools/AudioPlayer/hooks/useAudioElementEvents.ts +102 -0
- package/src/tools/AudioPlayer/hooks/useKeyboardShortcuts.ts +91 -0
- package/src/tools/AudioPlayer/hooks/useMediaSession.ts +74 -0
- package/src/tools/AudioPlayer/hooks/usePeaks.ts +83 -0
- package/src/tools/AudioPlayer/hooks/usePlayerPreferences.ts +21 -0
- package/src/tools/AudioPlayer/hooks/usePlayheadLoop.ts +77 -0
- package/src/tools/AudioPlayer/hooks/useResizeObserver.ts +20 -0
- package/src/tools/AudioPlayer/hooks/useThemeWatcher.ts +22 -0
- package/src/tools/AudioPlayer/index.ts +63 -134
- package/src/tools/AudioPlayer/lazy.tsx +8 -97
- package/src/tools/AudioPlayer/parts/Controls/ControlsRow.tsx +30 -0
- package/src/tools/AudioPlayer/parts/Controls/IconButton.tsx +62 -0
- package/src/tools/AudioPlayer/parts/Controls/LoopButton.tsx +33 -0
- package/src/tools/AudioPlayer/parts/Controls/PlayButton.tsx +86 -0
- package/src/tools/AudioPlayer/parts/Controls/SkipButton.tsx +17 -0
- package/src/tools/AudioPlayer/parts/Controls/VolumeControl.tsx +171 -0
- package/src/tools/AudioPlayer/parts/Controls/index.ts +6 -0
- package/src/tools/AudioPlayer/parts/Cover/Cover.tsx +24 -0
- package/src/tools/AudioPlayer/parts/Cover/CoverPlaceholder.tsx +27 -0
- package/src/tools/AudioPlayer/parts/Cover/ReactivePulse.tsx +66 -0
- package/src/tools/AudioPlayer/parts/Cover/index.ts +3 -0
- package/src/tools/AudioPlayer/parts/ErrorState/ErrorState.tsx +35 -0
- package/src/tools/AudioPlayer/parts/ErrorState/index.ts +1 -0
- package/src/tools/AudioPlayer/parts/Layout/CompactLayout.tsx +25 -0
- package/src/tools/AudioPlayer/parts/Layout/DefaultLayout.tsx +48 -0
- package/src/tools/AudioPlayer/parts/Layout/index.ts +2 -0
- package/src/tools/AudioPlayer/parts/Meta/Artist.tsx +14 -0
- package/src/tools/AudioPlayer/parts/Meta/TimeDisplay.tsx +49 -0
- package/src/tools/AudioPlayer/parts/Meta/Title.tsx +13 -0
- package/src/tools/AudioPlayer/parts/Meta/index.ts +3 -0
- package/src/tools/AudioPlayer/parts/Skeleton/CoverSkeleton.tsx +13 -0
- package/src/tools/AudioPlayer/parts/Skeleton/MetaSkeleton.tsx +10 -0
- package/src/tools/AudioPlayer/parts/Skeleton/index.ts +2 -0
- package/src/tools/AudioPlayer/parts/Waveform/BarsWaveform.tsx +48 -0
- package/src/tools/AudioPlayer/parts/Waveform/LiveWaveform.tsx +95 -0
- package/src/tools/AudioPlayer/parts/Waveform/PeaksWaveform.tsx +100 -0
- package/src/tools/AudioPlayer/parts/Waveform/ProgressBar.tsx +76 -0
- package/src/tools/AudioPlayer/parts/Waveform/Waveform.tsx +74 -0
- package/src/tools/AudioPlayer/parts/Waveform/WaveformSkeleton.tsx +16 -0
- package/src/tools/AudioPlayer/parts/Waveform/index.ts +8 -0
- package/src/tools/AudioPlayer/parts/Waveform/waveformInteraction.ts +106 -0
- package/src/tools/AudioPlayer/parts/Waveform/waveformRenderer.ts +91 -0
- package/src/tools/AudioPlayer/parts/index.ts +1 -0
- package/src/tools/AudioPlayer/store/activePlayerBus.ts +63 -0
- package/src/tools/AudioPlayer/store/createLevelsStore.ts +37 -0
- package/src/tools/AudioPlayer/store/index.ts +16 -0
- package/src/tools/AudioPlayer/store/preferencesStore.ts +104 -0
- package/src/tools/AudioPlayer/styles/webview-safe.css +77 -0
- package/src/tools/AudioPlayer/types.ts +95 -0
- package/src/tools/AudioPlayer/utils/bucketize.ts +27 -0
- package/src/tools/AudioPlayer/utils/clamp.ts +5 -0
- package/src/tools/AudioPlayer/utils/dpr.ts +19 -0
- package/src/tools/AudioPlayer/utils/formatTime.ts +12 -8
- package/src/tools/AudioPlayer/utils/index.ts +4 -5
- package/src/tools/AudioPlayer/utils/readCssVar.ts +7 -0
- package/src/tools/AudioPlayer/utils/resolveCanvasColor.ts +28 -0
- package/src/tools/index.ts +5 -75
- package/dist/chunk-2QY3LJR6.mjs.map +0 -1
- package/dist/chunk-6HNAPVZ2.mjs.map +0 -1
- package/dist/chunk-CXVGN6ZW.cjs.map +0 -1
- package/dist/chunk-F2N7P5XU.cjs +0 -30
- package/dist/chunk-F2N7P5XU.cjs.map +0 -1
- package/dist/chunk-FYLR232K.cjs.map +0 -1
- package/dist/chunk-HMHIVEMS.mjs +0 -1619
- package/dist/chunk-HMHIVEMS.mjs.map +0 -1
- package/dist/chunk-JWB2EWQO.mjs +0 -5
- package/dist/chunk-JWB2EWQO.mjs.map +0 -1
- package/dist/chunk-YZX6FH3H.cjs +0 -1656
- package/dist/chunk-YZX6FH3H.cjs.map +0 -1
- package/dist/components-3RTH76CV.cjs +0 -27
- package/dist/components-5GVVL2Q6.mjs +0 -5
- package/dist/components-CPHOUQ5F.cjs +0 -46
- package/dist/components-CPHOUQ5F.cjs.map +0 -1
- package/dist/components-OTK43IMD.mjs +0 -6
- package/dist/components-OTK43IMD.mjs.map +0 -1
- package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +0 -225
- package/src/tools/AudioPlayer/components/HybridCompactPlayer.tsx +0 -163
- package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +0 -284
- package/src/tools/AudioPlayer/components/HybridWaveform.tsx +0 -286
- package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +0 -151
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +0 -110
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +0 -58
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +0 -45
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +0 -82
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +0 -8
- package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +0 -6
- package/src/tools/AudioPlayer/components/index.ts +0 -23
- package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +0 -158
- package/src/tools/AudioPlayer/effects/index.ts +0 -412
- package/src/tools/AudioPlayer/hooks/useAudioBus.ts +0 -76
- package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +0 -403
- package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +0 -96
- package/src/tools/AudioPlayer/hooks/useVisualization.tsx +0 -207
- package/src/tools/AudioPlayer/types/effects.ts +0 -73
- package/src/tools/AudioPlayer/types/index.ts +0 -27
- package/src/tools/AudioPlayer/utils/debug.ts +0 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-tools",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.313",
|
|
4
4
|
"description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-tools",
|
|
@@ -91,8 +91,8 @@
|
|
|
91
91
|
"check": "tsc --noEmit"
|
|
92
92
|
},
|
|
93
93
|
"peerDependencies": {
|
|
94
|
-
"@djangocfg/i18n": "^2.1.
|
|
95
|
-
"@djangocfg/ui-core": "^2.1.
|
|
94
|
+
"@djangocfg/i18n": "^2.1.313",
|
|
95
|
+
"@djangocfg/ui-core": "^2.1.313",
|
|
96
96
|
"consola": "^3.4.2",
|
|
97
97
|
"lodash-es": "^4.18.1",
|
|
98
98
|
"lucide-react": "^0.545.0",
|
|
@@ -144,10 +144,10 @@
|
|
|
144
144
|
"@maplibre/maplibre-gl-geocoder": "^1.7.0"
|
|
145
145
|
},
|
|
146
146
|
"devDependencies": {
|
|
147
|
-
"@djangocfg/i18n": "^2.1.
|
|
147
|
+
"@djangocfg/i18n": "^2.1.313",
|
|
148
148
|
"@djangocfg/playground": "workspace:*",
|
|
149
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
150
|
-
"@djangocfg/ui-core": "^2.1.
|
|
149
|
+
"@djangocfg/typescript-config": "^2.1.313",
|
|
150
|
+
"@djangocfg/ui-core": "^2.1.313",
|
|
151
151
|
"@types/lodash-es": "^4.17.12",
|
|
152
152
|
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
|
|
153
153
|
"@types/node": "^24.7.2",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React 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
|
+
export const ActionRow: React.FC<ActionRowProps> = ({ value, isUser, visible }) => {
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
className={`flex ${isUser ? 'justify-end' : 'justify-start'}
|
|
32
|
+
transition-opacity duration-150
|
|
33
|
+
${visible ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}`}
|
|
34
|
+
>
|
|
35
|
+
<CopyButton
|
|
36
|
+
value={value}
|
|
37
|
+
size="icon"
|
|
38
|
+
variant="ghost"
|
|
39
|
+
iconClassName="h-3.5 w-3.5"
|
|
40
|
+
className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
|
41
|
+
// Hidden rows must not be reachable via Tab — otherwise
|
|
42
|
+
// keyboard users land on an invisible button.
|
|
43
|
+
tabIndex={visible ? 0 : -1}
|
|
44
|
+
aria-hidden={!visible}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { 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
|
+
export const ChatMessageRow: React.FC<ChatMessageRowProps> = ({
|
|
40
|
+
isUser,
|
|
41
|
+
children,
|
|
42
|
+
actions,
|
|
43
|
+
closeDelayMs = 250,
|
|
44
|
+
className = '',
|
|
45
|
+
}) => {
|
|
46
|
+
const isTouch = useMediaQuery('(hover: none), (pointer: coarse)');
|
|
47
|
+
const [hovered, setHovered] = useState(false);
|
|
48
|
+
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
49
|
+
|
|
50
|
+
const cancelClose = useCallback(() => {
|
|
51
|
+
if (closeTimer.current) {
|
|
52
|
+
clearTimeout(closeTimer.current);
|
|
53
|
+
closeTimer.current = null;
|
|
54
|
+
}
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const open = useCallback(() => {
|
|
58
|
+
cancelClose();
|
|
59
|
+
setHovered(true);
|
|
60
|
+
}, [cancelClose]);
|
|
61
|
+
|
|
62
|
+
const scheduleClose = useCallback(() => {
|
|
63
|
+
cancelClose();
|
|
64
|
+
closeTimer.current = setTimeout(() => setHovered(false), closeDelayMs);
|
|
65
|
+
}, [cancelClose, closeDelayMs]);
|
|
66
|
+
|
|
67
|
+
useEffect(() => () => cancelClose(), [cancelClose]);
|
|
68
|
+
|
|
69
|
+
// Touch: always-visible. Hover: state-driven.
|
|
70
|
+
const visible = isTouch || hovered;
|
|
71
|
+
|
|
72
|
+
// On touch, `static` positioning so the row claims real layout
|
|
73
|
+
// space (otherwise an absolute always-visible row would still
|
|
74
|
+
// overlap the next message). On hover devices, `absolute` so the
|
|
75
|
+
// hidden row doesn't leave dead air between bubbles.
|
|
76
|
+
const rowPositionClass = isTouch ? 'static mt-1' : 'absolute top-full';
|
|
77
|
+
const rowSideClass = isUser ? 'right-0' : 'left-0';
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
className={`relative flex flex-col ${isUser ? 'items-end' : 'items-start'} ${className}`}
|
|
82
|
+
onPointerEnter={isTouch ? undefined : open}
|
|
83
|
+
onPointerLeave={isTouch ? undefined : scheduleClose}
|
|
84
|
+
onFocusCapture={open}
|
|
85
|
+
onBlurCapture={(e) => {
|
|
86
|
+
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) scheduleClose();
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
{children}
|
|
90
|
+
{actions && (
|
|
91
|
+
<div className={`${rowPositionClass} ${rowSideClass}`}>
|
|
92
|
+
{actions(visible, isUser)}
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
@@ -19,27 +19,23 @@ interface CodeBlockProps {
|
|
|
19
19
|
* branch below still ships its own button because the plain <pre>
|
|
20
20
|
* has no toolbar of its own.
|
|
21
21
|
*/
|
|
22
|
-
export const CodeBlock: React.FC<CodeBlockProps> = ({ code, language
|
|
22
|
+
export const CodeBlock: React.FC<CodeBlockProps> = ({ code, language }) => {
|
|
23
23
|
const theme = useResolvedTheme();
|
|
24
24
|
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
// palette in both themes / both bubble roles; we don't branch on
|
|
31
|
-
// `isUser` here on purpose.
|
|
32
|
-
const textSizeClass = isCompact ? 'text-xs' : 'text-sm';
|
|
33
|
-
|
|
25
|
+
// Chat fences are always rendered in PrettyCode's compact mode:
|
|
26
|
+
// 12px font, tighter padding, line-height 1.4. A fenced block in
|
|
27
|
+
// a chat bubble shouldn't outweigh two paragraphs of body text —
|
|
28
|
+
// the standalone PrettyCode story keeps the larger default for
|
|
29
|
+
// docs/diff viewers.
|
|
34
30
|
return (
|
|
35
|
-
<div className="my-
|
|
31
|
+
<div className="my-2">
|
|
36
32
|
<PrettyCode
|
|
37
33
|
data={code}
|
|
38
34
|
language={language}
|
|
39
|
-
className=
|
|
35
|
+
className="text-xs"
|
|
40
36
|
customBg="bg-code"
|
|
41
37
|
mode={theme}
|
|
42
|
-
isCompact
|
|
38
|
+
isCompact
|
|
43
39
|
// Disable click-to-scroll-isolation in chat markdown: code
|
|
44
40
|
// fences here are part of an assistant reply, not a docs
|
|
45
41
|
// viewer. Forcing the user to click into a small block to
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
2
|
import { defineStory } from '@djangocfg/playground';
|
|
3
3
|
import { MarkdownMessage } from './MarkdownMessage';
|
|
4
|
+
import { ActionRow } from './ActionRow';
|
|
5
|
+
import { ChatMessageRow } from './ChatMessageRow';
|
|
4
6
|
import type { LinkRule } from './types';
|
|
5
7
|
|
|
6
8
|
export default defineStory({
|
|
@@ -329,15 +331,25 @@ function ChatBubble({
|
|
|
329
331
|
content,
|
|
330
332
|
rules,
|
|
331
333
|
plainText,
|
|
334
|
+
actions,
|
|
332
335
|
}: {
|
|
333
336
|
role: 'user' | 'assistant';
|
|
334
337
|
content: string;
|
|
335
338
|
rules?: LinkRule[];
|
|
336
339
|
plainText?: boolean;
|
|
340
|
+
/** Show the copy-on-hover ActionRow under the bubble. */
|
|
341
|
+
actions?: 'copy';
|
|
337
342
|
}) {
|
|
338
343
|
const isUser = role === 'user';
|
|
339
344
|
return (
|
|
340
|
-
<
|
|
345
|
+
<ChatMessageRow
|
|
346
|
+
isUser={isUser}
|
|
347
|
+
actions={
|
|
348
|
+
actions === 'copy'
|
|
349
|
+
? (visible) => <ActionRow value={content} isUser={isUser} visible={visible} />
|
|
350
|
+
: undefined
|
|
351
|
+
}
|
|
352
|
+
>
|
|
341
353
|
<div
|
|
342
354
|
className={`max-w-[85%] rounded-xl px-3 py-2 ${
|
|
343
355
|
isUser
|
|
@@ -352,7 +364,7 @@ function ChatBubble({
|
|
|
352
364
|
plainText={plainText}
|
|
353
365
|
/>
|
|
354
366
|
</div>
|
|
355
|
-
</
|
|
367
|
+
</ChatMessageRow>
|
|
356
368
|
);
|
|
357
369
|
}
|
|
358
370
|
|
|
@@ -694,3 +706,66 @@ export const MermaidDiagrams = () => (
|
|
|
694
706
|
</div>
|
|
695
707
|
</div>
|
|
696
708
|
);
|
|
709
|
+
|
|
710
|
+
// CopyActions — opt-in action row under the bubble. The `<ActionRow>`
|
|
711
|
+
// component lives outside the bubble (so it doesn't fight the
|
|
712
|
+
// saturated `bg-primary` palette on user messages); the `group` class
|
|
713
|
+
// on the parent flex container makes it appear on hover. On touch
|
|
714
|
+
// devices it stays at 50% opacity so the affordance is discoverable
|
|
715
|
+
// without hover.
|
|
716
|
+
export const CopyActions = () => (
|
|
717
|
+
<div className="mx-auto max-w-2xl space-y-6 p-6">
|
|
718
|
+
<div>
|
|
719
|
+
<h3 className="mb-2 text-sm font-semibold">
|
|
720
|
+
Hover any message to reveal the copy button
|
|
721
|
+
</h3>
|
|
722
|
+
<p className="mb-3 text-xs text-muted-foreground">
|
|
723
|
+
<code>ActionRow</code> is exported separately from{' '}
|
|
724
|
+
<code>MarkdownMessage</code> on purpose — placement is a
|
|
725
|
+
layout concern of the bubble, not of the renderer. Wrap your
|
|
726
|
+
bubble + <code><ActionRow></code> in a{' '}
|
|
727
|
+
<code>group</code> div, and the row toggles via{' '}
|
|
728
|
+
<code>group-hover</code>. On <code>@media (hover: none)</code>{' '}
|
|
729
|
+
it stays at 50% opacity instead of hiding.
|
|
730
|
+
</p>
|
|
731
|
+
<div className="rounded-lg border border-border bg-card p-4 space-y-8">
|
|
732
|
+
<ChatBubble
|
|
733
|
+
role="user"
|
|
734
|
+
content="How do I install cmdop?"
|
|
735
|
+
plainText
|
|
736
|
+
actions="copy"
|
|
737
|
+
/>
|
|
738
|
+
<ChatBubble
|
|
739
|
+
role="assistant"
|
|
740
|
+
content={`Run this from your terminal:
|
|
741
|
+
|
|
742
|
+
\`\`\`bash
|
|
743
|
+
curl -sSL cmdop.com/install.sh | bash
|
|
744
|
+
\`\`\`
|
|
745
|
+
|
|
746
|
+
It auto-detects your shell and adds \`cmdop\` to PATH.`}
|
|
747
|
+
actions="copy"
|
|
748
|
+
/>
|
|
749
|
+
<ChatBubble
|
|
750
|
+
role="user"
|
|
751
|
+
content={'Multi\nline\nuser message\nto verify copy preserves all newlines'}
|
|
752
|
+
plainText
|
|
753
|
+
actions="copy"
|
|
754
|
+
/>
|
|
755
|
+
</div>
|
|
756
|
+
</div>
|
|
757
|
+
|
|
758
|
+
<div>
|
|
759
|
+
<h3 className="mb-2 text-sm font-semibold">
|
|
760
|
+
Without <code>actions</code> — no extra row, no overhead
|
|
761
|
+
</h3>
|
|
762
|
+
<p className="mb-3 text-xs text-muted-foreground">
|
|
763
|
+
Default behaviour. Existing consumers see no visual change.
|
|
764
|
+
</p>
|
|
765
|
+
<div className="rounded-lg border border-border bg-card p-4 space-y-3">
|
|
766
|
+
<ChatBubble role="user" content="Plain user bubble." plainText />
|
|
767
|
+
<ChatBubble role="assistant" content="Plain assistant bubble." />
|
|
768
|
+
</div>
|
|
769
|
+
</div>
|
|
770
|
+
</div>
|
|
771
|
+
);
|
|
@@ -153,7 +153,7 @@ export const MarkdownMessage: React.FC<MarkdownMessageProps> = ({
|
|
|
153
153
|
// WhatsApp/Telegram bubble density.
|
|
154
154
|
return (
|
|
155
155
|
<div
|
|
156
|
-
className={`${textSizeClass} leading-snug break-words whitespace-pre-wrap ${className}`}
|
|
156
|
+
className={`${textSizeClass} font-normal antialiased leading-snug break-words whitespace-pre-wrap ${className}`}
|
|
157
157
|
>
|
|
158
158
|
{displayContent}
|
|
159
159
|
{collapsible && shouldCollapse && (
|
|
@@ -177,14 +177,13 @@ export const MarkdownMessage: React.FC<MarkdownMessageProps> = ({
|
|
|
177
177
|
<div className={className}>
|
|
178
178
|
<div
|
|
179
179
|
className={`
|
|
180
|
-
prose ${proseClass} max-w-none break-words overflow-hidden ${textSizeClass}
|
|
180
|
+
prose ${proseClass} max-w-none break-words overflow-hidden ${textSizeClass} font-normal antialiased
|
|
181
181
|
${isUser ? 'prose-invert' : 'dark:prose-invert'}
|
|
182
182
|
[&>*]:leading-relaxed
|
|
183
183
|
[&>*:first-child]:mt-0 [&>*:last-child]:mb-0
|
|
184
184
|
[&_p]:my-2
|
|
185
185
|
[&_ul]:my-2 [&_ol]:my-2 [&_ul]:pl-5 [&_ol]:pl-5
|
|
186
186
|
[&_li]:my-1 [&_li>p]:my-0
|
|
187
|
-
[&_pre]:my-3
|
|
188
187
|
[&_h1]:mt-4 [&_h1]:mb-2 [&_h1]:text-base [&_h1]:font-semibold
|
|
189
188
|
[&_h2]:mt-3.5 [&_h2]:mb-1.5 [&_h2]:text-[15px] [&_h2]:font-semibold
|
|
190
189
|
[&_h3]:mt-3 [&_h3]:mb-1 [&_h3]:text-sm [&_h3]:font-medium
|
|
@@ -23,6 +23,7 @@ import { MarkdownMessage } from '@djangocfg/ui-tools';
|
|
|
23
23
|
| Custom URL schemes via declarative `linkRules` | `./linkRules.ts` |
|
|
24
24
|
| Plain-text fast path — short messages skip the markdown pipeline | `./plainText.ts` |
|
|
25
25
|
| Optional collapsible "Read more" for long replies | `useCollapsibleContent` |
|
|
26
|
+
| Hover-revealed copy action under the bubble (separate `<ActionRow>` export) | `<CopyButton>` (ui-core) |
|
|
26
27
|
| User vs assistant palette via `isUser` flag | semantic theme tokens |
|
|
27
28
|
|
|
28
29
|
## Why these plugins
|
|
@@ -83,6 +84,74 @@ The plain `a` renderer keeps its palette and typography; `linkRules` only fires
|
|
|
83
84
|
when both the `protocols` and the `match` predicate accept the href. See
|
|
84
85
|
`linkRules.ts` for the sanitize-extension logic.
|
|
85
86
|
|
|
87
|
+
## Copy action (`<ChatMessageRow>` + `<ActionRow>`)
|
|
88
|
+
|
|
89
|
+
Two pieces, two responsibilities:
|
|
90
|
+
|
|
91
|
+
- **`<ActionRow>`** — dumb leaf. Knows the side (`isUser`) and the
|
|
92
|
+
text to copy (`value`). Owner controls visibility via `visible`.
|
|
93
|
+
- **`<ChatMessageRow>`** — opinionated wrapper. Owns the bubble +
|
|
94
|
+
action-row layout, hover state with a 250 ms close delay, touch
|
|
95
|
+
fallback, focus management, and absolute positioning for the row
|
|
96
|
+
so a hidden row never claims vertical space.
|
|
97
|
+
|
|
98
|
+
The split exists because:
|
|
99
|
+
|
|
100
|
+
1. `MarkdownMessage` doesn't know whether it lives in a saturated
|
|
101
|
+
`bg-primary` bubble or a neutral card — putting the copy icon
|
|
102
|
+
inside would force it to fight the bubble's palette.
|
|
103
|
+
2. CSS-only `group-hover` bridges flicker when the cursor crosses
|
|
104
|
+
the gap between the bubble and the (initially-invisible) row.
|
|
105
|
+
State + a small close timeout is the same pattern Radix Tooltip
|
|
106
|
+
uses for the same reason.
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import { MarkdownMessage, ChatMessageRow, ActionRow } from '@djangocfg/ui-tools';
|
|
110
|
+
|
|
111
|
+
function ChatBubble({ role, content }) {
|
|
112
|
+
const isUser = role === 'user';
|
|
113
|
+
return (
|
|
114
|
+
<ChatMessageRow
|
|
115
|
+
isUser={isUser}
|
|
116
|
+
actions={(visible) => (
|
|
117
|
+
<ActionRow value={content} isUser={isUser} visible={visible} />
|
|
118
|
+
)}
|
|
119
|
+
>
|
|
120
|
+
<div
|
|
121
|
+
className={
|
|
122
|
+
isUser
|
|
123
|
+
? 'bg-primary text-primary-foreground rounded-xl px-3 py-2'
|
|
124
|
+
: 'bg-card border border-border rounded-xl px-3 py-2'
|
|
125
|
+
}
|
|
126
|
+
>
|
|
127
|
+
<MarkdownMessage content={content} isUser={isUser} plainText={isUser} />
|
|
128
|
+
</div>
|
|
129
|
+
</ChatMessageRow>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Behaviour:
|
|
135
|
+
|
|
136
|
+
- **Desktop:** hidden until you hover anywhere over the row (bubble
|
|
137
|
+
or its action area). 250 ms close delay so cursor travel from
|
|
138
|
+
bubble to button never flickers. Fades over 150 ms.
|
|
139
|
+
- **Touch (`@media (hover: none), (pointer: coarse)`):** always
|
|
140
|
+
visible at full opacity, in-flow under the bubble (so it claims
|
|
141
|
+
honest layout space — touch users have no hover, the affordance
|
|
142
|
+
must always be reachable).
|
|
143
|
+
- **Keyboard:** `focus-within` opens the row, `blur` outside the row
|
|
144
|
+
schedules the same close timeout.
|
|
145
|
+
- **Side:** the row sits on the same side as the bubble (`isUser`
|
|
146
|
+
flips alignment); the icon itself is never mirrored.
|
|
147
|
+
|
|
148
|
+
Pass the original `content` string to `ActionRow.value` so collapsed
|
|
149
|
+
bubbles still copy the full text, not the truncated `displayContent`.
|
|
150
|
+
|
|
151
|
+
The `actions` prop is a render-prop (`(visible, isUser) => ReactNode`)
|
|
152
|
+
so it's already future-proof for richer rows like
|
|
153
|
+
`[Copy] [Like] [Regenerate]`.
|
|
154
|
+
|
|
86
155
|
## Files
|
|
87
156
|
|
|
88
157
|
| File | Purpose |
|
|
@@ -94,6 +163,8 @@ when both the `protocols` and the `match` predicate accept the href. See
|
|
|
94
163
|
| `plainText.ts` | `looksLikePlainProse` heuristic + `extractTextFromChildren` utility |
|
|
95
164
|
| `sanitize.ts` | Extended rehype-sanitize schema + `urlTransform` builder |
|
|
96
165
|
| `CollapseToggle.tsx` | "Read more / Show less" affordance for long replies |
|
|
166
|
+
| `ActionRow.tsx` | Dumb action row (copy button); owner toggles `visible` |
|
|
167
|
+
| `ChatMessageRow.tsx` | Bubble + action-row wrapper with hover state and touch fallback |
|
|
97
168
|
| `MarkdownMessage.story.tsx` | Storybook — kitchen sink, link rules, mermaid, soft breaks, plugin sampler |
|
|
98
169
|
|
|
99
170
|
## Stories
|
|
@@ -109,3 +180,4 @@ Open the storybook and look at:
|
|
|
109
180
|
- `Markdown Message / Kitchen Sink` — every block element together, used to spot
|
|
110
181
|
spacing regressions at a glance.
|
|
111
182
|
- `Markdown Message / Mermaid Diagrams` — flowchart + sequence diagram in a bubble.
|
|
183
|
+
- `Markdown Message / Copy Actions` — hover-revealed copy button on user and assistant bubbles.
|
|
@@ -21,10 +21,10 @@ export function createMarkdownComponents(
|
|
|
21
21
|
|
|
22
22
|
return {
|
|
23
23
|
h1: ({ children }) => (
|
|
24
|
-
<h1 className={`${headingBase} font-
|
|
24
|
+
<h1 className={`${headingBase} font-semibold mb-2 mt-3 first:mt-0`}>{children}</h1>
|
|
25
25
|
),
|
|
26
26
|
h2: ({ children }) => (
|
|
27
|
-
<h2 className={`${headingSm} font-
|
|
27
|
+
<h2 className={`${headingSm} font-semibold mb-2 mt-3 first:mt-0`}>{children}</h2>
|
|
28
28
|
),
|
|
29
29
|
h3: ({ children }) => (
|
|
30
30
|
<h3 className={`${headingSm} font-semibold mb-1 mt-2 first:mt-0`}>{children}</h3>
|
|
@@ -40,7 +40,7 @@ export function createMarkdownComponents(
|
|
|
40
40
|
),
|
|
41
41
|
|
|
42
42
|
p: ({ children }) => (
|
|
43
|
-
<p className={`${textSize} mb-4 last:mb-0 leading-
|
|
43
|
+
<p className={`${textSize} mb-4 last:mb-0 leading-relaxed break-words`}>{children}</p>
|
|
44
44
|
),
|
|
45
45
|
|
|
46
46
|
ul: ({ children }) => (
|
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
* - `LinkRule` — the declarative link-handling primitive.
|
|
8
8
|
* - `extractTextFromChildren` — utility for callers that build their
|
|
9
9
|
* own custom `a` renderers and need a string label.
|
|
10
|
+
* - `ActionRow` — dumb action row (copy button), takes `visible`
|
|
11
|
+
* from its owner.
|
|
12
|
+
* - `ChatMessageRow` — opinionated bubble + action-row wrapper
|
|
13
|
+
* with state-driven hover, close-delay and touch handling.
|
|
10
14
|
*/
|
|
11
15
|
export { MarkdownMessage, default } from './MarkdownMessage';
|
|
12
16
|
export type { MarkdownMessageProps, LinkRule } from './types';
|
|
13
17
|
export { extractTextFromChildren } from './plainText';
|
|
18
|
+
export { ActionRow } from './ActionRow';
|
|
19
|
+
export { ChatMessageRow } from './ChatMessageRow';
|
package/src/index.ts
CHANGED
|
@@ -79,17 +79,8 @@ export { LazyJsonSchemaForm } from './tools/JsonForm/lazy';
|
|
|
79
79
|
// LottiePlayer (~200KB)
|
|
80
80
|
export { LazyLottiePlayer } from './tools/LottiePlayer/lazy';
|
|
81
81
|
|
|
82
|
-
// AudioPlayer (
|
|
83
|
-
export {
|
|
84
|
-
LazyHybridAudioPlayer,
|
|
85
|
-
LazyHybridSimplePlayer,
|
|
86
|
-
LazyHybridCompactPlayer,
|
|
87
|
-
} from './tools/AudioPlayer/lazy';
|
|
88
|
-
export type { HybridCompactPlayerProps } from './tools/AudioPlayer/lazy';
|
|
89
|
-
|
|
90
|
-
// AudioBus — global audio exclusivity (one player at a time)
|
|
91
|
-
export { useAudioBus, useAudioBusStore } from './tools/AudioPlayer/hooks/useAudioBus';
|
|
92
|
-
export type { UseAudioBusReturn } from './tools/AudioPlayer/hooks/useAudioBus';
|
|
82
|
+
// AudioPlayer v6 — under construction (see @dev/@refactoring6-audioplayer/).
|
|
83
|
+
export { LazyPlayer as LazyAudioPlayer } from './tools/AudioPlayer/lazy';
|
|
93
84
|
|
|
94
85
|
// VideoPlayer (~150KB)
|
|
95
86
|
export { LazyVideoPlayer } from './tools/VideoPlayer/lazy';
|