dsh-side-chat-plus 0.3.2 → 0.3.4
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 +327 -326
- package/README.zh.md +269 -268
- package/dsh.plugin.json +3 -3
- package/lib/client-registry.js +397 -181
- package/lib/client-registry.js.map +1 -1
- package/lib/client.js +397 -181
- package/lib/client.js.map +1 -1
- package/lib/index.js +22 -7
- package/lib/types/client/locales.d.ts +18 -0
- package/lib/types/context-types.d.ts +36 -4
- package/lib/types/settings-shared.d.ts +4 -0
- package/package.json +37 -37
- 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 +1051 -1032
- package/src/client/index.tsx +2209 -1966
- package/src/client/locales.ts +191 -173
- package/src/context-types.ts +415 -384
- package/src/index.ts +29 -9
- package/src/settings-shared.ts +6 -0
|
@@ -1,173 +1,173 @@
|
|
|
1
|
-
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
IconChevronLeftOutline14,
|
|
4
|
-
IconChevronRightOutline14,
|
|
5
|
-
IconCloseFill14,
|
|
6
|
-
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
7
|
-
import css from './AttachmentRail.module.css'
|
|
8
|
-
|
|
9
|
-
/** One rail thumbnail; strings arrive resolved (zero-cordis atom). */
|
|
10
|
-
export interface AttachmentRailItem {
|
|
11
|
-
/** Stable identity for the React key. */
|
|
12
|
-
id: string
|
|
13
|
-
/** Object or data URL rendered as the thumbnail. */
|
|
14
|
-
previewUrl: string
|
|
15
|
-
/** Image alt text (display name with the owner's fallback applied). */
|
|
16
|
-
alt: string
|
|
17
|
-
/** Accessible label of the item's remove control. */
|
|
18
|
-
removeLabel: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Rail-level strings the owner resolves from its own locale namespace. */
|
|
22
|
-
export interface AttachmentRailLabels {
|
|
23
|
-
/** Accessible name of the rail group. */
|
|
24
|
-
group: string
|
|
25
|
-
/** Thumbnail tooltip inviting the original-image preview. */
|
|
26
|
-
open: string
|
|
27
|
-
/** Accessible label of the left paging arrow. */
|
|
28
|
-
scrollLeft: string
|
|
29
|
-
/** Accessible label of the right paging arrow. */
|
|
30
|
-
scrollRight: string
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Approximate pixels per wheel step for `deltaMode` LINE deltas (Firefox notch wheels report lines). */
|
|
34
|
-
const WHEEL_LINE_PX = 16
|
|
35
|
-
|
|
36
|
-
/** Smooth paging unless the user asked for reduced motion. */
|
|
37
|
-
function pageBehavior(): ScrollBehavior {
|
|
38
|
-
return window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth'
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Horizontal thumbnail rail over the caller's draft attachments.
|
|
43
|
-
*
|
|
44
|
-
* The rail scrolls with its scrollbar hidden; overflow is announced by edge
|
|
45
|
-
* arrows recomputed from scroll geometry on scroll, item-count changes, and
|
|
46
|
-
* rail size changes (a ResizeObserver on the rail element, so sidebar or panel
|
|
47
|
-
* resizes count, not only window resizes). A vertical wheel pans the rail
|
|
48
|
-
* horizontally and is consumed exclusively, a newly added item is revealed at
|
|
49
|
-
* the rail's end, and each thumbnail opens on a single click while its remove
|
|
50
|
-
* control sits inside the card. The owner decides mounting.
|
|
51
|
-
*/
|
|
52
|
-
export function AttachmentRail<T extends AttachmentRailItem>({ items, labels, onOpen, onRemove }: {
|
|
53
|
-
items: readonly T[]
|
|
54
|
-
labels: AttachmentRailLabels
|
|
55
|
-
onOpen: (item: T) => void
|
|
56
|
-
onRemove: (item: T) => void
|
|
57
|
-
}) {
|
|
58
|
-
const railRef = useRef<HTMLDivElement | null>(null)
|
|
59
|
-
// null marks the first layout pass: a rail that MOUNTS over an existing
|
|
60
|
-
// draft is initial display, not growth, and must not jump to the end.
|
|
61
|
-
const countRef = useRef<number | null>(null)
|
|
62
|
-
const [edges, setEdges] = useState({ left: false, right: false })
|
|
63
|
-
const updateEdges = useCallback(() => {
|
|
64
|
-
const el = railRef.current
|
|
65
|
-
if (el === null) return
|
|
66
|
-
// 1px slack: engines report fractional scroll positions at the edges.
|
|
67
|
-
const left = el.scrollLeft > 1
|
|
68
|
-
const right = el.scrollLeft < el.scrollWidth - el.clientWidth - 1
|
|
69
|
-
setEdges(prev => prev.left === left && prev.right === right ? prev : { left, right })
|
|
70
|
-
}, [])
|
|
71
|
-
useLayoutEffect(() => {
|
|
72
|
-
const grew = countRef.current !== null && items.length > countRef.current
|
|
73
|
-
countRef.current = items.length
|
|
74
|
-
const el = railRef.current
|
|
75
|
-
if (el === null) return
|
|
76
|
-
// A newly added attachment lands at the rail's end: reveal it.
|
|
77
|
-
if (grew) el.scrollLeft = el.scrollWidth - el.clientWidth
|
|
78
|
-
updateEdges()
|
|
79
|
-
}, [items.length, updateEdges])
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
const el = railRef.current
|
|
82
|
-
if (el === null) return
|
|
83
|
-
// The rail's width follows the composer, which resizes with sidebars and
|
|
84
|
-
// panels, not only the window — observe the element itself.
|
|
85
|
-
let disconnect = (): void => {}
|
|
86
|
-
if (typeof ResizeObserver !== 'undefined') {
|
|
87
|
-
const observer = new ResizeObserver(updateEdges)
|
|
88
|
-
observer.observe(el)
|
|
89
|
-
disconnect = () => { observer.disconnect() }
|
|
90
|
-
}
|
|
91
|
-
// The rail scrolls horizontally ONLY: any wheel tick with a vertical
|
|
92
|
-
// component is consumed — without preventDefault it would also scroll the
|
|
93
|
-
// conversation behind the composer, and React's root wheel listener is
|
|
94
|
-
// passive, so the exclusion needs this manually attached non-passive
|
|
95
|
-
// listener.
|
|
96
|
-
const onWheel = (event: globalThis.WheelEvent): void => {
|
|
97
|
-
if (event.deltaY === 0) return
|
|
98
|
-
const scale = event.deltaMode === WheelEvent.DOM_DELTA_LINE
|
|
99
|
-
? WHEEL_LINE_PX
|
|
100
|
-
: event.deltaMode === WheelEvent.DOM_DELTA_PAGE ? el.clientWidth : 1
|
|
101
|
-
event.preventDefault()
|
|
102
|
-
el.scrollBy({
|
|
103
|
-
left: event.deltaX !== 0
|
|
104
|
-
? event.deltaX * scale
|
|
105
|
-
: Math.sign(event.deltaY) * Math.min(Math.abs(event.deltaY) * scale, 60),
|
|
106
|
-
behavior: 'auto',
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
el.addEventListener('wheel', onWheel, { passive: false })
|
|
110
|
-
return () => {
|
|
111
|
-
disconnect()
|
|
112
|
-
el.removeEventListener('wheel', onWheel)
|
|
113
|
-
}
|
|
114
|
-
}, [updateEdges])
|
|
115
|
-
const page = (direction: -1 | 1): void => {
|
|
116
|
-
const el = railRef.current
|
|
117
|
-
if (el === null) return
|
|
118
|
-
// One viewport minus a card keeps the last visible thumbnail as context.
|
|
119
|
-
el.scrollBy({ left: direction * Math.max(el.clientWidth - 64, 200), behavior: pageBehavior() })
|
|
120
|
-
}
|
|
121
|
-
return (
|
|
122
|
-
<div className={css.root}>
|
|
123
|
-
{edges.left && (
|
|
124
|
-
<button
|
|
125
|
-
type="button"
|
|
126
|
-
className={`${css.arrow} ${css.arrowLeft}`}
|
|
127
|
-
aria-label={labels.scrollLeft}
|
|
128
|
-
onClick={() => { page(-1) }}
|
|
129
|
-
>
|
|
130
|
-
<IconChevronLeftOutline14 />
|
|
131
|
-
</button>
|
|
132
|
-
)}
|
|
133
|
-
<div
|
|
134
|
-
ref={railRef}
|
|
135
|
-
className={css.rail}
|
|
136
|
-
role="group"
|
|
137
|
-
aria-label={labels.group}
|
|
138
|
-
onScroll={updateEdges}
|
|
139
|
-
>
|
|
140
|
-
{items.map(item => (
|
|
141
|
-
<div key={item.id} className={css.item}>
|
|
142
|
-
<button
|
|
143
|
-
type="button"
|
|
144
|
-
className={css.thumbnail}
|
|
145
|
-
title={labels.open}
|
|
146
|
-
onClick={() => { onOpen(item) }}
|
|
147
|
-
>
|
|
148
|
-
<img src={item.previewUrl} alt={item.alt} />
|
|
149
|
-
</button>
|
|
150
|
-
<button
|
|
151
|
-
type="button"
|
|
152
|
-
className={css.remove}
|
|
153
|
-
aria-label={item.removeLabel}
|
|
154
|
-
onClick={() => { onRemove(item) }}
|
|
155
|
-
>
|
|
156
|
-
<IconCloseFill14 size={12} />
|
|
157
|
-
</button>
|
|
158
|
-
</div>
|
|
159
|
-
))}
|
|
160
|
-
</div>
|
|
161
|
-
{edges.right && (
|
|
162
|
-
<button
|
|
163
|
-
type="button"
|
|
164
|
-
className={`${css.arrow} ${css.arrowRight}`}
|
|
165
|
-
aria-label={labels.scrollRight}
|
|
166
|
-
onClick={() => { page(1) }}
|
|
167
|
-
>
|
|
168
|
-
<IconChevronRightOutline14 />
|
|
169
|
-
</button>
|
|
170
|
-
)}
|
|
171
|
-
</div>
|
|
172
|
-
)
|
|
173
|
-
}
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
IconChevronLeftOutline14,
|
|
4
|
+
IconChevronRightOutline14,
|
|
5
|
+
IconCloseFill14,
|
|
6
|
+
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
7
|
+
import css from './AttachmentRail.module.css'
|
|
8
|
+
|
|
9
|
+
/** One rail thumbnail; strings arrive resolved (zero-cordis atom). */
|
|
10
|
+
export interface AttachmentRailItem {
|
|
11
|
+
/** Stable identity for the React key. */
|
|
12
|
+
id: string
|
|
13
|
+
/** Object or data URL rendered as the thumbnail. */
|
|
14
|
+
previewUrl: string
|
|
15
|
+
/** Image alt text (display name with the owner's fallback applied). */
|
|
16
|
+
alt: string
|
|
17
|
+
/** Accessible label of the item's remove control. */
|
|
18
|
+
removeLabel: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Rail-level strings the owner resolves from its own locale namespace. */
|
|
22
|
+
export interface AttachmentRailLabels {
|
|
23
|
+
/** Accessible name of the rail group. */
|
|
24
|
+
group: string
|
|
25
|
+
/** Thumbnail tooltip inviting the original-image preview. */
|
|
26
|
+
open: string
|
|
27
|
+
/** Accessible label of the left paging arrow. */
|
|
28
|
+
scrollLeft: string
|
|
29
|
+
/** Accessible label of the right paging arrow. */
|
|
30
|
+
scrollRight: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Approximate pixels per wheel step for `deltaMode` LINE deltas (Firefox notch wheels report lines). */
|
|
34
|
+
const WHEEL_LINE_PX = 16
|
|
35
|
+
|
|
36
|
+
/** Smooth paging unless the user asked for reduced motion. */
|
|
37
|
+
function pageBehavior(): ScrollBehavior {
|
|
38
|
+
return window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Horizontal thumbnail rail over the caller's draft attachments.
|
|
43
|
+
*
|
|
44
|
+
* The rail scrolls with its scrollbar hidden; overflow is announced by edge
|
|
45
|
+
* arrows recomputed from scroll geometry on scroll, item-count changes, and
|
|
46
|
+
* rail size changes (a ResizeObserver on the rail element, so sidebar or panel
|
|
47
|
+
* resizes count, not only window resizes). A vertical wheel pans the rail
|
|
48
|
+
* horizontally and is consumed exclusively, a newly added item is revealed at
|
|
49
|
+
* the rail's end, and each thumbnail opens on a single click while its remove
|
|
50
|
+
* control sits inside the card. The owner decides mounting.
|
|
51
|
+
*/
|
|
52
|
+
export function AttachmentRail<T extends AttachmentRailItem>({ items, labels, onOpen, onRemove }: {
|
|
53
|
+
items: readonly T[]
|
|
54
|
+
labels: AttachmentRailLabels
|
|
55
|
+
onOpen: (item: T) => void
|
|
56
|
+
onRemove: (item: T) => void
|
|
57
|
+
}) {
|
|
58
|
+
const railRef = useRef<HTMLDivElement | null>(null)
|
|
59
|
+
// null marks the first layout pass: a rail that MOUNTS over an existing
|
|
60
|
+
// draft is initial display, not growth, and must not jump to the end.
|
|
61
|
+
const countRef = useRef<number | null>(null)
|
|
62
|
+
const [edges, setEdges] = useState({ left: false, right: false })
|
|
63
|
+
const updateEdges = useCallback(() => {
|
|
64
|
+
const el = railRef.current
|
|
65
|
+
if (el === null) return
|
|
66
|
+
// 1px slack: engines report fractional scroll positions at the edges.
|
|
67
|
+
const left = el.scrollLeft > 1
|
|
68
|
+
const right = el.scrollLeft < el.scrollWidth - el.clientWidth - 1
|
|
69
|
+
setEdges(prev => prev.left === left && prev.right === right ? prev : { left, right })
|
|
70
|
+
}, [])
|
|
71
|
+
useLayoutEffect(() => {
|
|
72
|
+
const grew = countRef.current !== null && items.length > countRef.current
|
|
73
|
+
countRef.current = items.length
|
|
74
|
+
const el = railRef.current
|
|
75
|
+
if (el === null) return
|
|
76
|
+
// A newly added attachment lands at the rail's end: reveal it.
|
|
77
|
+
if (grew) el.scrollLeft = el.scrollWidth - el.clientWidth
|
|
78
|
+
updateEdges()
|
|
79
|
+
}, [items.length, updateEdges])
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const el = railRef.current
|
|
82
|
+
if (el === null) return
|
|
83
|
+
// The rail's width follows the composer, which resizes with sidebars and
|
|
84
|
+
// panels, not only the window — observe the element itself.
|
|
85
|
+
let disconnect = (): void => {}
|
|
86
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
87
|
+
const observer = new ResizeObserver(updateEdges)
|
|
88
|
+
observer.observe(el)
|
|
89
|
+
disconnect = () => { observer.disconnect() }
|
|
90
|
+
}
|
|
91
|
+
// The rail scrolls horizontally ONLY: any wheel tick with a vertical
|
|
92
|
+
// component is consumed — without preventDefault it would also scroll the
|
|
93
|
+
// conversation behind the composer, and React's root wheel listener is
|
|
94
|
+
// passive, so the exclusion needs this manually attached non-passive
|
|
95
|
+
// listener.
|
|
96
|
+
const onWheel = (event: globalThis.WheelEvent): void => {
|
|
97
|
+
if (event.deltaY === 0) return
|
|
98
|
+
const scale = event.deltaMode === WheelEvent.DOM_DELTA_LINE
|
|
99
|
+
? WHEEL_LINE_PX
|
|
100
|
+
: event.deltaMode === WheelEvent.DOM_DELTA_PAGE ? el.clientWidth : 1
|
|
101
|
+
event.preventDefault()
|
|
102
|
+
el.scrollBy({
|
|
103
|
+
left: event.deltaX !== 0
|
|
104
|
+
? event.deltaX * scale
|
|
105
|
+
: Math.sign(event.deltaY) * Math.min(Math.abs(event.deltaY) * scale, 60),
|
|
106
|
+
behavior: 'auto',
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
el.addEventListener('wheel', onWheel, { passive: false })
|
|
110
|
+
return () => {
|
|
111
|
+
disconnect()
|
|
112
|
+
el.removeEventListener('wheel', onWheel)
|
|
113
|
+
}
|
|
114
|
+
}, [updateEdges])
|
|
115
|
+
const page = (direction: -1 | 1): void => {
|
|
116
|
+
const el = railRef.current
|
|
117
|
+
if (el === null) return
|
|
118
|
+
// One viewport minus a card keeps the last visible thumbnail as context.
|
|
119
|
+
el.scrollBy({ left: direction * Math.max(el.clientWidth - 64, 200), behavior: pageBehavior() })
|
|
120
|
+
}
|
|
121
|
+
return (
|
|
122
|
+
<div className={css.root}>
|
|
123
|
+
{edges.left && (
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
className={`${css.arrow} ${css.arrowLeft}`}
|
|
127
|
+
aria-label={labels.scrollLeft}
|
|
128
|
+
onClick={() => { page(-1) }}
|
|
129
|
+
>
|
|
130
|
+
<IconChevronLeftOutline14 />
|
|
131
|
+
</button>
|
|
132
|
+
)}
|
|
133
|
+
<div
|
|
134
|
+
ref={railRef}
|
|
135
|
+
className={css.rail}
|
|
136
|
+
role="group"
|
|
137
|
+
aria-label={labels.group}
|
|
138
|
+
onScroll={updateEdges}
|
|
139
|
+
>
|
|
140
|
+
{items.map(item => (
|
|
141
|
+
<div key={item.id} className={css.item}>
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
className={css.thumbnail}
|
|
145
|
+
title={labels.open}
|
|
146
|
+
onClick={() => { onOpen(item) }}
|
|
147
|
+
>
|
|
148
|
+
<img src={item.previewUrl} alt={item.alt} />
|
|
149
|
+
</button>
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
className={css.remove}
|
|
153
|
+
aria-label={item.removeLabel}
|
|
154
|
+
onClick={() => { onRemove(item) }}
|
|
155
|
+
>
|
|
156
|
+
<IconCloseFill14 size={12} />
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
))}
|
|
160
|
+
</div>
|
|
161
|
+
{edges.right && (
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
className={`${css.arrow} ${css.arrowRight}`}
|
|
165
|
+
aria-label={labels.scrollRight}
|
|
166
|
+
onClick={() => { page(1) }}
|
|
167
|
+
>
|
|
168
|
+
<IconChevronRightOutline14 />
|
|
169
|
+
</button>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
.mask {
|
|
2
|
-
position: fixed;
|
|
3
|
-
inset: 0;
|
|
4
|
-
z-index: 999;
|
|
5
|
-
display: flex;
|
|
6
|
-
align-items: center;
|
|
7
|
-
justify-content: center;
|
|
8
|
-
/* Decoration only: drag targeting must stay on the page below. */
|
|
9
|
-
pointer-events: none;
|
|
10
|
-
background: rgba(255, 255, 255, 0.72);
|
|
11
|
-
border: 2px dashed var(--dsw-color-border-strong, #4d6bfe);
|
|
12
|
-
border-radius: 12px;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
.wrap {
|
|
16
|
-
display: flex;
|
|
17
|
-
flex-direction: column;
|
|
18
|
-
align-items: center;
|
|
19
|
-
gap: 12px;
|
|
20
|
-
text-align: center;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
.illustration {
|
|
24
|
-
display: flex;
|
|
25
|
-
align-items: center;
|
|
26
|
-
justify-content: center;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.title {
|
|
30
|
-
font-size: 16px;
|
|
31
|
-
font-weight: 600;
|
|
32
|
-
color: var(--dsw-color-text, #1f2329);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.desc {
|
|
36
|
-
font-size: 13px;
|
|
37
|
-
color: var(--dsw-color-text-secondary, #646a73);
|
|
38
|
-
}
|
|
1
|
+
.mask {
|
|
2
|
+
position: fixed;
|
|
3
|
+
inset: 0;
|
|
4
|
+
z-index: 999;
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
/* Decoration only: drag targeting must stay on the page below. */
|
|
9
|
+
pointer-events: none;
|
|
10
|
+
background: rgba(255, 255, 255, 0.72);
|
|
11
|
+
border: 2px dashed var(--dsw-color-border-strong, #4d6bfe);
|
|
12
|
+
border-radius: 12px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.wrap {
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 12px;
|
|
20
|
+
text-align: center;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.illustration {
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.title {
|
|
30
|
+
font-size: 16px;
|
|
31
|
+
font-weight: 600;
|
|
32
|
+
color: var(--dsw-color-text, #1f2329);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.desc {
|
|
36
|
+
font-size: 13px;
|
|
37
|
+
color: var(--dsw-color-text-secondary, #646a73);
|
|
38
|
+
}
|
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
import { createPortal } from 'react-dom'
|
|
2
|
-
import css from './DropOverlay.module.css'
|
|
3
|
-
|
|
4
|
-
/** Drop-overlay strings the owner resolves from its own locale namespace. */
|
|
5
|
-
export interface DropOverlayLabels {
|
|
6
|
-
/** Headline inviting the drop, or naming why it is unavailable. */
|
|
7
|
-
title: string
|
|
8
|
-
/** Limits line under the title; shown only while drops are accepted. */
|
|
9
|
-
desc?: string | undefined
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Full-viewport invitation shown while a file drag is over the page.
|
|
14
|
-
* Decoration only: `pointer-events: none` keeps drag targeting on the page
|
|
15
|
-
* below, so the owner's document-level listeners keep an accurate enter/leave
|
|
16
|
-
* count and own accept/reject. Rendered through a body portal for the same
|
|
17
|
-
* transformed-ancestor reason as the lightbox.
|
|
18
|
-
*/
|
|
19
|
-
export function DropOverlay({ disabled, labels }: {
|
|
20
|
-
disabled: boolean
|
|
21
|
-
labels: DropOverlayLabels
|
|
22
|
-
}) {
|
|
23
|
-
return createPortal(
|
|
24
|
-
<div className={css.mask} role="status">
|
|
25
|
-
<div className={css.wrap}>
|
|
26
|
-
<div className={css.illustration} aria-hidden="true">
|
|
27
|
-
{disabled ? <BlockedIllustration /> : <UploadIllustration />}
|
|
28
|
-
</div>
|
|
29
|
-
<div className={css.title}>{labels.title}</div>
|
|
30
|
-
{!disabled && labels.desc !== undefined && <div className={css.desc}>{labels.desc}</div>}
|
|
31
|
-
</div>
|
|
32
|
-
</div>,
|
|
33
|
-
document.body,
|
|
34
|
-
)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Tilted photo-and-note cards. */
|
|
38
|
-
const UploadIllustration = () => (
|
|
39
|
-
<svg width="96" height="72" viewBox="0 0 96 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
40
|
-
<rect y="14" width="38" height="38" rx="10" transform="rotate(-22 0 14)" fill="#9ce5ed" />
|
|
41
|
-
<rect x="62" y="7" width="37" height="43" rx="8" transform="rotate(17 62 7)" fill="#679efe" />
|
|
42
|
-
<rect x="26" y="30" width="38" height="38" rx="10" fill="#3964fe" />
|
|
43
|
-
<path
|
|
44
|
-
d="M33 62c1-2 4-8 7-14 1-2 5-2 6 1 2 5 5 9 7 10 3 1 7-9 17 2"
|
|
45
|
-
stroke="#fff"
|
|
46
|
-
strokeWidth="3"
|
|
47
|
-
fill="none"
|
|
48
|
-
/>
|
|
49
|
-
<circle cx="53" cy="43" r="4" fill="#fff" />
|
|
50
|
-
</svg>
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
/** Greyed cards with a blocked badge. */
|
|
54
|
-
const BlockedIllustration = () => (
|
|
55
|
-
<svg width="96" height="72" viewBox="0 0 96 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
56
|
-
<rect y="14" width="38" height="38" rx="10" transform="rotate(-22 0 14)" fill="#c4c9d0" />
|
|
57
|
-
<rect x="62" y="7" width="37" height="43" rx="8" transform="rotate(17 62 7)" fill="#b0b6be" />
|
|
58
|
-
<rect x="26" y="30" width="38" height="38" rx="10" fill="#d8dce1" />
|
|
59
|
-
<circle cx="45" cy="49" r="12" stroke="#fff" strokeWidth="3" fill="none" />
|
|
60
|
-
<path d="M36 40l18 18" stroke="#fff" strokeWidth="3" strokeLinecap="round" />
|
|
61
|
-
</svg>
|
|
62
|
-
)
|
|
1
|
+
import { createPortal } from 'react-dom'
|
|
2
|
+
import css from './DropOverlay.module.css'
|
|
3
|
+
|
|
4
|
+
/** Drop-overlay strings the owner resolves from its own locale namespace. */
|
|
5
|
+
export interface DropOverlayLabels {
|
|
6
|
+
/** Headline inviting the drop, or naming why it is unavailable. */
|
|
7
|
+
title: string
|
|
8
|
+
/** Limits line under the title; shown only while drops are accepted. */
|
|
9
|
+
desc?: string | undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Full-viewport invitation shown while a file drag is over the page.
|
|
14
|
+
* Decoration only: `pointer-events: none` keeps drag targeting on the page
|
|
15
|
+
* below, so the owner's document-level listeners keep an accurate enter/leave
|
|
16
|
+
* count and own accept/reject. Rendered through a body portal for the same
|
|
17
|
+
* transformed-ancestor reason as the lightbox.
|
|
18
|
+
*/
|
|
19
|
+
export function DropOverlay({ disabled, labels }: {
|
|
20
|
+
disabled: boolean
|
|
21
|
+
labels: DropOverlayLabels
|
|
22
|
+
}) {
|
|
23
|
+
return createPortal(
|
|
24
|
+
<div className={css.mask} role="status">
|
|
25
|
+
<div className={css.wrap}>
|
|
26
|
+
<div className={css.illustration} aria-hidden="true">
|
|
27
|
+
{disabled ? <BlockedIllustration /> : <UploadIllustration />}
|
|
28
|
+
</div>
|
|
29
|
+
<div className={css.title}>{labels.title}</div>
|
|
30
|
+
{!disabled && labels.desc !== undefined && <div className={css.desc}>{labels.desc}</div>}
|
|
31
|
+
</div>
|
|
32
|
+
</div>,
|
|
33
|
+
document.body,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Tilted photo-and-note cards. */
|
|
38
|
+
const UploadIllustration = () => (
|
|
39
|
+
<svg width="96" height="72" viewBox="0 0 96 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
40
|
+
<rect y="14" width="38" height="38" rx="10" transform="rotate(-22 0 14)" fill="#9ce5ed" />
|
|
41
|
+
<rect x="62" y="7" width="37" height="43" rx="8" transform="rotate(17 62 7)" fill="#679efe" />
|
|
42
|
+
<rect x="26" y="30" width="38" height="38" rx="10" fill="#3964fe" />
|
|
43
|
+
<path
|
|
44
|
+
d="M33 62c1-2 4-8 7-14 1-2 5-2 6 1 2 5 5 9 7 10 3 1 7-9 17 2"
|
|
45
|
+
stroke="#fff"
|
|
46
|
+
strokeWidth="3"
|
|
47
|
+
fill="none"
|
|
48
|
+
/>
|
|
49
|
+
<circle cx="53" cy="43" r="4" fill="#fff" />
|
|
50
|
+
</svg>
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
/** Greyed cards with a blocked badge. */
|
|
54
|
+
const BlockedIllustration = () => (
|
|
55
|
+
<svg width="96" height="72" viewBox="0 0 96 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
56
|
+
<rect y="14" width="38" height="38" rx="10" transform="rotate(-22 0 14)" fill="#c4c9d0" />
|
|
57
|
+
<rect x="62" y="7" width="37" height="43" rx="8" transform="rotate(17 62 7)" fill="#b0b6be" />
|
|
58
|
+
<rect x="26" y="30" width="38" height="38" rx="10" fill="#d8dce1" />
|
|
59
|
+
<circle cx="45" cy="49" r="12" stroke="#fff" strokeWidth="3" fill="none" />
|
|
60
|
+
<path d="M36 40l18 18" stroke="#fff" strokeWidth="3" strokeLinecap="round" />
|
|
61
|
+
</svg>
|
|
62
|
+
)
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
.backdrop {
|
|
2
|
-
position: fixed;
|
|
3
|
-
inset: 0;
|
|
4
|
-
z-index: 1000;
|
|
5
|
-
display: flex;
|
|
6
|
-
align-items: center;
|
|
7
|
-
justify-content: center;
|
|
8
|
-
padding: 32px;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
.mask {
|
|
12
|
-
position: absolute;
|
|
13
|
-
inset: 0;
|
|
14
|
-
background: rgba(0, 0, 0, 0.72);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
.image {
|
|
18
|
-
position: relative;
|
|
19
|
-
max-width: 100%;
|
|
20
|
-
max-height: 100%;
|
|
21
|
-
object-fit: contain;
|
|
22
|
-
border-radius: 8px;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.close {
|
|
26
|
-
position: absolute;
|
|
27
|
-
top: 20px;
|
|
28
|
-
right: 20px;
|
|
29
|
-
display: flex;
|
|
30
|
-
align-items: center;
|
|
31
|
-
justify-content: center;
|
|
32
|
-
width: 32px;
|
|
33
|
-
height: 32px;
|
|
34
|
-
padding: 0;
|
|
35
|
-
border: none;
|
|
36
|
-
border-radius: 50%;
|
|
37
|
-
background: rgba(255, 255, 255, 0.16);
|
|
38
|
-
color: #fff;
|
|
39
|
-
cursor: pointer;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
.close:hover {
|
|
43
|
-
background: rgba(255, 255, 255, 0.28);
|
|
44
|
-
}
|
|
1
|
+
.backdrop {
|
|
2
|
+
position: fixed;
|
|
3
|
+
inset: 0;
|
|
4
|
+
z-index: 1000;
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
padding: 32px;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.mask {
|
|
12
|
+
position: absolute;
|
|
13
|
+
inset: 0;
|
|
14
|
+
background: rgba(0, 0, 0, 0.72);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.image {
|
|
18
|
+
position: relative;
|
|
19
|
+
max-width: 100%;
|
|
20
|
+
max-height: 100%;
|
|
21
|
+
object-fit: contain;
|
|
22
|
+
border-radius: 8px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.close {
|
|
26
|
+
position: absolute;
|
|
27
|
+
top: 20px;
|
|
28
|
+
right: 20px;
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
width: 32px;
|
|
33
|
+
height: 32px;
|
|
34
|
+
padding: 0;
|
|
35
|
+
border: none;
|
|
36
|
+
border-radius: 50%;
|
|
37
|
+
background: rgba(255, 255, 255, 0.16);
|
|
38
|
+
color: #fff;
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.close:hover {
|
|
43
|
+
background: rgba(255, 255, 255, 0.28);
|
|
44
|
+
}
|