dsh-side-chat-plus 0.3.1 → 0.3.3
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 +326 -317
- package/README.zh.md +268 -261
- package/dsh.plugin.json +1 -1
- package/lib/client-registry.js +125 -125
- package/lib/client-registry.js.map +1 -1
- package/lib/client.js +125 -125
- package/lib/client.js.map +1 -1
- package/lib/index.js +7 -2
- package/package.json +16 -1
- package/src/client/attachments/AttachmentRail.module.css +89 -89
- package/src/client/attachments/AttachmentRail.tsx +173 -173
- package/src/client/attachments/DropOverlay.module.css +38 -38
- package/src/client/attachments/DropOverlay.tsx +62 -62
- package/src/client/attachments/ImageLightbox.module.css +44 -44
- package/src/client/attachments/ImageLightbox.tsx +58 -58
- package/src/client/attachments/MessageImage.module.css +61 -61
- package/src/client/attachments/MessageImage.tsx +120 -120
- package/src/client/attachments/index.ts +19 -19
- package/src/client/client.module.css +1032 -1032
- package/src/client/index.tsx +1966 -1966
- package/src/client/locales.ts +173 -173
- package/src/context-types.ts +384 -384
- package/src/index.ts +14 -6
|
@@ -1,120 +1,120 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
2
|
-
import type { SidechatImageRef } from '../api.ts'
|
|
3
|
-
import { ImageLightbox } from './ImageLightbox.tsx'
|
|
4
|
-
import type { ImageLightboxLabels } from './ImageLightbox.tsx'
|
|
5
|
-
import css from './MessageImage.module.css'
|
|
6
|
-
|
|
7
|
-
/** Loads one durable image's bytes into a displayable URL. */
|
|
8
|
-
export type ImageLoader = (ref: SidechatImageRef) => Promise<string>
|
|
9
|
-
|
|
10
|
-
/** Message-image strings the owner resolves from its own locale namespace. */
|
|
11
|
-
export interface MessageImageLabels {
|
|
12
|
-
/** Fallback display name for an unnamed image. */
|
|
13
|
-
image: string
|
|
14
|
-
/** Thumbnail tooltip inviting the original-image preview. */
|
|
15
|
-
open: string
|
|
16
|
-
/** Accessible thumbnail label; receives the image's display name. */
|
|
17
|
-
openNamed: (label: string) => string
|
|
18
|
-
/** Loading placeholder shown until bytes resolve. */
|
|
19
|
-
loading: string
|
|
20
|
-
/** Retry-control label shown when the load fails. */
|
|
21
|
-
loadFailed: string
|
|
22
|
-
/** Lightbox strings forwarded to the opened preview. */
|
|
23
|
-
lightbox: ImageLightboxLabels
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Display box for a lone image: long edge 240px with the rendered aspect
|
|
27
|
-
* ratio clamped to [0.25, 4] — the overflow is cropped by
|
|
28
|
-
* `object-fit: cover` — and never upscaled past the image's natural size. The
|
|
29
|
-
* crop anchor keeps the top of very tall images and the left of very wide
|
|
30
|
-
* ones, where the informative content usually starts. */
|
|
31
|
-
function singleFit(
|
|
32
|
-
dimensions: { readonly width: number; readonly height: number },
|
|
33
|
-
): { width: number; height: number; objectPosition: string } {
|
|
34
|
-
const natural = dimensions.width / dimensions.height
|
|
35
|
-
const ratio = Math.min(4, Math.max(0.25, natural))
|
|
36
|
-
const box = ratio >= 1 ? { width: 240, height: 240 / ratio } : { width: 240 * ratio, height: 240 }
|
|
37
|
-
const scale = Math.min(1, dimensions.width / box.width, dimensions.height / box.height)
|
|
38
|
-
return {
|
|
39
|
-
width: Math.max(1, Math.round(box.width * scale)),
|
|
40
|
-
height: Math.max(1, Math.round(box.height * scale)),
|
|
41
|
-
objectPosition: natural < 0.25 ? 'center top' : natural > 4 ? 'left center' : 'center',
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Compact history renderer with retryable loading and click-to-open original
|
|
47
|
-
* preview. A lone image renders at its `singleFit` size; an image among
|
|
48
|
-
* several renders as a fixed 64px square tile.
|
|
49
|
-
*/
|
|
50
|
-
export function MessageImage({ image, load, variant, labels }: {
|
|
51
|
-
image: SidechatImageRef
|
|
52
|
-
load: ImageLoader
|
|
53
|
-
variant: 'single' | 'tile'
|
|
54
|
-
labels: MessageImageLabels
|
|
55
|
-
}) {
|
|
56
|
-
const [loaded, setLoaded] = useState<string | null>(null)
|
|
57
|
-
const [error, setError] = useState(false)
|
|
58
|
-
const [open, setOpen] = useState(false)
|
|
59
|
-
// Retry re-arms the one load effect below, so every attempt — first load or
|
|
60
|
-
// retry — runs under the same liveness guard and the same reset.
|
|
61
|
-
const [attempt, setAttempt] = useState(0)
|
|
62
|
-
const request = useCallback(() => { setAttempt(a => a + 1) }, [])
|
|
63
|
-
const close = useCallback(() => { setOpen(false) }, [])
|
|
64
|
-
const label = image.name === undefined || image.name === '' ? labels.image : image.name
|
|
65
|
-
const fit = useMemo(() => (variant === 'single' ? singleFit(image) : undefined), [image, variant])
|
|
66
|
-
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
let live = true
|
|
69
|
-
setError(false)
|
|
70
|
-
setLoaded(null)
|
|
71
|
-
void load(image).then((url) => { if (live) setLoaded(url) }).catch(() => { if (live) setError(true) })
|
|
72
|
-
return () => { live = false }
|
|
73
|
-
}, [image, load, attempt])
|
|
74
|
-
|
|
75
|
-
if (error) return <button type="button" className={css.error} data-variant={variant} onClick={request}>{labels.loadFailed}</button>
|
|
76
|
-
const src = loaded
|
|
77
|
-
return (
|
|
78
|
-
<>
|
|
79
|
-
<button
|
|
80
|
-
type="button"
|
|
81
|
-
className={css.frame}
|
|
82
|
-
data-variant={variant}
|
|
83
|
-
style={fit === undefined ? undefined : { width: fit.width, height: fit.height }}
|
|
84
|
-
title={labels.open}
|
|
85
|
-
aria-label={labels.openNamed(label)}
|
|
86
|
-
onClick={() => { if (src !== null) setOpen(true) }}
|
|
87
|
-
>
|
|
88
|
-
{src === null
|
|
89
|
-
? <span className={css.loading}>{labels.loading}</span>
|
|
90
|
-
: <img src={src} alt={label} style={fit === undefined ? undefined : { objectPosition: fit.objectPosition }} />}
|
|
91
|
-
</button>
|
|
92
|
-
{open && src !== null && <ImageLightbox src={src} alt={label} labels={labels.lightbox} onClose={close} />}
|
|
93
|
-
</>
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Wrapping image group: a lone image renders large, several render as 64px
|
|
98
|
-
* square tiles. */
|
|
99
|
-
export function ImageGallery({ images, load, align, labels }: {
|
|
100
|
-
images: readonly SidechatImageRef[]
|
|
101
|
-
load: ImageLoader
|
|
102
|
-
align: 'start' | 'end'
|
|
103
|
-
labels: MessageImageLabels
|
|
104
|
-
}) {
|
|
105
|
-
if (images.length === 0) return null
|
|
106
|
-
const variant = images.length === 1 ? 'single' : 'tile'
|
|
107
|
-
return (
|
|
108
|
-
<div className={css.gallery} data-align={align}>
|
|
109
|
-
{images.map((image, index) => (
|
|
110
|
-
<MessageImage
|
|
111
|
-
key={`${image.attachmentId}:${index}`}
|
|
112
|
-
image={image}
|
|
113
|
-
load={load}
|
|
114
|
-
variant={variant}
|
|
115
|
-
labels={labels}
|
|
116
|
-
/>
|
|
117
|
-
))}
|
|
118
|
-
</div>
|
|
119
|
-
)
|
|
120
|
-
}
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
2
|
+
import type { SidechatImageRef } from '../api.ts'
|
|
3
|
+
import { ImageLightbox } from './ImageLightbox.tsx'
|
|
4
|
+
import type { ImageLightboxLabels } from './ImageLightbox.tsx'
|
|
5
|
+
import css from './MessageImage.module.css'
|
|
6
|
+
|
|
7
|
+
/** Loads one durable image's bytes into a displayable URL. */
|
|
8
|
+
export type ImageLoader = (ref: SidechatImageRef) => Promise<string>
|
|
9
|
+
|
|
10
|
+
/** Message-image strings the owner resolves from its own locale namespace. */
|
|
11
|
+
export interface MessageImageLabels {
|
|
12
|
+
/** Fallback display name for an unnamed image. */
|
|
13
|
+
image: string
|
|
14
|
+
/** Thumbnail tooltip inviting the original-image preview. */
|
|
15
|
+
open: string
|
|
16
|
+
/** Accessible thumbnail label; receives the image's display name. */
|
|
17
|
+
openNamed: (label: string) => string
|
|
18
|
+
/** Loading placeholder shown until bytes resolve. */
|
|
19
|
+
loading: string
|
|
20
|
+
/** Retry-control label shown when the load fails. */
|
|
21
|
+
loadFailed: string
|
|
22
|
+
/** Lightbox strings forwarded to the opened preview. */
|
|
23
|
+
lightbox: ImageLightboxLabels
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Display box for a lone image: long edge 240px with the rendered aspect
|
|
27
|
+
* ratio clamped to [0.25, 4] — the overflow is cropped by
|
|
28
|
+
* `object-fit: cover` — and never upscaled past the image's natural size. The
|
|
29
|
+
* crop anchor keeps the top of very tall images and the left of very wide
|
|
30
|
+
* ones, where the informative content usually starts. */
|
|
31
|
+
function singleFit(
|
|
32
|
+
dimensions: { readonly width: number; readonly height: number },
|
|
33
|
+
): { width: number; height: number; objectPosition: string } {
|
|
34
|
+
const natural = dimensions.width / dimensions.height
|
|
35
|
+
const ratio = Math.min(4, Math.max(0.25, natural))
|
|
36
|
+
const box = ratio >= 1 ? { width: 240, height: 240 / ratio } : { width: 240 * ratio, height: 240 }
|
|
37
|
+
const scale = Math.min(1, dimensions.width / box.width, dimensions.height / box.height)
|
|
38
|
+
return {
|
|
39
|
+
width: Math.max(1, Math.round(box.width * scale)),
|
|
40
|
+
height: Math.max(1, Math.round(box.height * scale)),
|
|
41
|
+
objectPosition: natural < 0.25 ? 'center top' : natural > 4 ? 'left center' : 'center',
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compact history renderer with retryable loading and click-to-open original
|
|
47
|
+
* preview. A lone image renders at its `singleFit` size; an image among
|
|
48
|
+
* several renders as a fixed 64px square tile.
|
|
49
|
+
*/
|
|
50
|
+
export function MessageImage({ image, load, variant, labels }: {
|
|
51
|
+
image: SidechatImageRef
|
|
52
|
+
load: ImageLoader
|
|
53
|
+
variant: 'single' | 'tile'
|
|
54
|
+
labels: MessageImageLabels
|
|
55
|
+
}) {
|
|
56
|
+
const [loaded, setLoaded] = useState<string | null>(null)
|
|
57
|
+
const [error, setError] = useState(false)
|
|
58
|
+
const [open, setOpen] = useState(false)
|
|
59
|
+
// Retry re-arms the one load effect below, so every attempt — first load or
|
|
60
|
+
// retry — runs under the same liveness guard and the same reset.
|
|
61
|
+
const [attempt, setAttempt] = useState(0)
|
|
62
|
+
const request = useCallback(() => { setAttempt(a => a + 1) }, [])
|
|
63
|
+
const close = useCallback(() => { setOpen(false) }, [])
|
|
64
|
+
const label = image.name === undefined || image.name === '' ? labels.image : image.name
|
|
65
|
+
const fit = useMemo(() => (variant === 'single' ? singleFit(image) : undefined), [image, variant])
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
let live = true
|
|
69
|
+
setError(false)
|
|
70
|
+
setLoaded(null)
|
|
71
|
+
void load(image).then((url) => { if (live) setLoaded(url) }).catch(() => { if (live) setError(true) })
|
|
72
|
+
return () => { live = false }
|
|
73
|
+
}, [image, load, attempt])
|
|
74
|
+
|
|
75
|
+
if (error) return <button type="button" className={css.error} data-variant={variant} onClick={request}>{labels.loadFailed}</button>
|
|
76
|
+
const src = loaded
|
|
77
|
+
return (
|
|
78
|
+
<>
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
className={css.frame}
|
|
82
|
+
data-variant={variant}
|
|
83
|
+
style={fit === undefined ? undefined : { width: fit.width, height: fit.height }}
|
|
84
|
+
title={labels.open}
|
|
85
|
+
aria-label={labels.openNamed(label)}
|
|
86
|
+
onClick={() => { if (src !== null) setOpen(true) }}
|
|
87
|
+
>
|
|
88
|
+
{src === null
|
|
89
|
+
? <span className={css.loading}>{labels.loading}</span>
|
|
90
|
+
: <img src={src} alt={label} style={fit === undefined ? undefined : { objectPosition: fit.objectPosition }} />}
|
|
91
|
+
</button>
|
|
92
|
+
{open && src !== null && <ImageLightbox src={src} alt={label} labels={labels.lightbox} onClose={close} />}
|
|
93
|
+
</>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Wrapping image group: a lone image renders large, several render as 64px
|
|
98
|
+
* square tiles. */
|
|
99
|
+
export function ImageGallery({ images, load, align, labels }: {
|
|
100
|
+
images: readonly SidechatImageRef[]
|
|
101
|
+
load: ImageLoader
|
|
102
|
+
align: 'start' | 'end'
|
|
103
|
+
labels: MessageImageLabels
|
|
104
|
+
}) {
|
|
105
|
+
if (images.length === 0) return null
|
|
106
|
+
const variant = images.length === 1 ? 'single' : 'tile'
|
|
107
|
+
return (
|
|
108
|
+
<div className={css.gallery} data-align={align}>
|
|
109
|
+
{images.map((image, index) => (
|
|
110
|
+
<MessageImage
|
|
111
|
+
key={`${image.attachmentId}:${index}`}
|
|
112
|
+
image={image}
|
|
113
|
+
load={load}
|
|
114
|
+
variant={variant}
|
|
115
|
+
labels={labels}
|
|
116
|
+
/>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Image presentation owned by this plugin.
|
|
3
|
-
*
|
|
4
|
-
* The DSH attachment UI package registers these through conversation slots and
|
|
5
|
-
* exports no React components (`packages/client/AGENTS.md`: a feature plugin
|
|
6
|
-
* MUST NOT runtime-import another feature plugin's values — UI crosses
|
|
7
|
-
* packages through slots). A standalone side panel sits outside the
|
|
8
|
-
* conversation slot tree and cannot reach those slots, so it carries its own
|
|
9
|
-
* presentation, built only on `ui-primitives` (a sanctioned static owner).
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export { AttachmentRail } from './AttachmentRail.tsx'
|
|
13
|
-
export type { AttachmentRailItem, AttachmentRailLabels } from './AttachmentRail.tsx'
|
|
14
|
-
export { DropOverlay } from './DropOverlay.tsx'
|
|
15
|
-
export type { DropOverlayLabels } from './DropOverlay.tsx'
|
|
16
|
-
export { ImageGallery, MessageImage } from './MessageImage.tsx'
|
|
17
|
-
export type { ImageLoader, MessageImageLabels } from './MessageImage.tsx'
|
|
18
|
-
export { ImageLightbox } from './ImageLightbox.tsx'
|
|
19
|
-
export type { ImageLightboxLabels } from './ImageLightbox.tsx'
|
|
1
|
+
/**
|
|
2
|
+
* Image presentation owned by this plugin.
|
|
3
|
+
*
|
|
4
|
+
* The DSH attachment UI package registers these through conversation slots and
|
|
5
|
+
* exports no React components (`packages/client/AGENTS.md`: a feature plugin
|
|
6
|
+
* MUST NOT runtime-import another feature plugin's values — UI crosses
|
|
7
|
+
* packages through slots). A standalone side panel sits outside the
|
|
8
|
+
* conversation slot tree and cannot reach those slots, so it carries its own
|
|
9
|
+
* presentation, built only on `ui-primitives` (a sanctioned static owner).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export { AttachmentRail } from './AttachmentRail.tsx'
|
|
13
|
+
export type { AttachmentRailItem, AttachmentRailLabels } from './AttachmentRail.tsx'
|
|
14
|
+
export { DropOverlay } from './DropOverlay.tsx'
|
|
15
|
+
export type { DropOverlayLabels } from './DropOverlay.tsx'
|
|
16
|
+
export { ImageGallery, MessageImage } from './MessageImage.tsx'
|
|
17
|
+
export type { ImageLoader, MessageImageLabels } from './MessageImage.tsx'
|
|
18
|
+
export { ImageLightbox } from './ImageLightbox.tsx'
|
|
19
|
+
export type { ImageLightboxLabels } from './ImageLightbox.tsx'
|