@byline/richtext-lexical 6.4.0 → 6.6.0
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/dist/field/extensions/code-highlight/code-highlight-extension.js +2 -1
- package/dist/field/extensions/inline-image/inline-image-extension.js +4 -2
- package/dist/field/extensions/inline-image/inline-image-modal.css +67 -0
- package/dist/field/extensions/inline-image/inline-image-modal.js +173 -98
- package/dist/field/extensions/inline-image/inline-image-node-component.css +37 -0
- package/dist/field/extensions/inline-image/inline-image-node-component.d.ts +3 -1
- package/dist/field/extensions/inline-image/inline-image-node-component.js +39 -1
- package/dist/field/extensions/inline-image/inline-image-node.d.ts +13 -2
- package/dist/field/extensions/inline-image/inline-image-node.js +63 -13
- package/dist/field/extensions/inline-image/node-types.d.ts +19 -0
- package/dist/field/extensions/inline-image/populate.d.ts +22 -0
- package/dist/field/extensions/inline-image/populate.js +19 -1
- package/dist/field/extensions/inline-image/types.d.ts +7 -0
- package/dist/field/extensions/link/link-href.d.ts +30 -0
- package/dist/field/extensions/link/link-href.js +14 -0
- package/dist/field/extensions/link/link-modal.js +96 -288
- package/dist/field/extensions/link/link-target-fields.css +59 -0
- package/dist/field/extensions/link/link-target-fields.d.ts +38 -0
- package/dist/field/extensions/link/link-target-fields.js +227 -0
- package/dist/field/extensions/link/link-target-state.d.ts +54 -0
- package/dist/field/extensions/link/link-target-state.js +70 -0
- package/dist/field/extensions/link/populate.d.ts +15 -1
- package/dist/field/extensions/link/populate.js +55 -50
- package/dist/field/markdown/lexical-to-markdown.js +6 -1
- package/dist/field/plugins/toolbar-plugin/index.js +2 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +4 -2
- package/dist/validate/createEmptyEditorState.d.ts +2 -2
- package/dist/validate/hasText.d.ts +2 -2
- package/package.json +25 -24
- package/src/field/extensions/code-highlight/code-highlight-extension.ts +8 -1
- package/src/field/extensions/inline-image/inline-image-extension.tsx +5 -0
- package/src/field/extensions/inline-image/inline-image-modal.css +100 -0
- package/src/field/extensions/inline-image/inline-image-modal.tsx +178 -85
- package/src/field/extensions/inline-image/inline-image-node-component.css +44 -0
- package/src/field/extensions/inline-image/inline-image-node-component.tsx +52 -0
- package/src/field/extensions/inline-image/inline-image-node.test.tsx +331 -0
- package/src/field/extensions/inline-image/inline-image-node.tsx +108 -11
- package/src/field/extensions/inline-image/node-types.ts +19 -0
- package/src/field/extensions/inline-image/populate-link.test.node.ts +320 -0
- package/src/field/extensions/inline-image/populate.ts +44 -0
- package/src/field/extensions/inline-image/types.ts +7 -0
- package/src/field/extensions/link/link-href.test.node.ts +78 -0
- package/src/field/extensions/link/link-href.ts +52 -0
- package/src/field/extensions/link/link-modal.tsx +86 -325
- package/src/field/extensions/link/link-target-fields.css +81 -0
- package/src/field/extensions/link/link-target-fields.tsx +275 -0
- package/src/field/extensions/link/link-target-state.test.node.ts +197 -0
- package/src/field/extensions/link/link-target-state.ts +158 -0
- package/src/field/extensions/link/populate.ts +94 -67
- package/src/field/markdown/lexical-to-markdown.test.node.ts +68 -0
- package/src/field/markdown/lexical-to-markdown.ts +10 -1
- package/src/field/plugins/toolbar-plugin/index.tsx +7 -4
- package/src/server.ts +11 -5
- package/src/validate/createEmptyEditorState.ts +2 -7
- package/src/validate/hasText.test.ts +1 -2
- package/src/validate/hasText.ts +2 -9
|
@@ -11,92 +11,32 @@
|
|
|
11
11
|
import type * as React from 'react'
|
|
12
12
|
import { useMemo, useState } from 'react'
|
|
13
13
|
|
|
14
|
-
import { RelationPicker } from '@byline/admin/react'
|
|
15
14
|
import type { MultiCollectionDefinition } from '@byline/core'
|
|
16
|
-
import { getAdminConfig,
|
|
17
|
-
import {
|
|
18
|
-
Button,
|
|
19
|
-
Checkbox,
|
|
20
|
-
CloseIcon,
|
|
21
|
-
IconButton,
|
|
22
|
-
Input,
|
|
23
|
-
Label,
|
|
24
|
-
Modal,
|
|
25
|
-
RadioGroup,
|
|
26
|
-
RadioGroupItem,
|
|
27
|
-
Select,
|
|
28
|
-
type SelectValue,
|
|
29
|
-
} from '@byline/ui/react'
|
|
15
|
+
import { getAdminConfig, isSingleton } from '@byline/core'
|
|
16
|
+
import { Button, CloseIcon, IconButton, Input, Modal } from '@byline/ui/react'
|
|
30
17
|
|
|
31
18
|
import { useModalFormState } from '../../shared/useModalFormState'
|
|
32
|
-
import {
|
|
33
|
-
import
|
|
19
|
+
import { LinkTargetFields } from './link-target-fields'
|
|
20
|
+
import {
|
|
21
|
+
fromLinkAttributes,
|
|
22
|
+
type LinkTargetState,
|
|
23
|
+
toLinkAttributes,
|
|
24
|
+
validateLinkTarget,
|
|
25
|
+
} from './link-target-state'
|
|
34
26
|
import type { LinkAttributes } from '.'
|
|
35
|
-
import type {
|
|
27
|
+
import type { LinkModalProps } from './link-modal-types'
|
|
36
28
|
|
|
37
29
|
interface FormState {
|
|
38
30
|
text: string
|
|
39
|
-
|
|
40
|
-
url: string
|
|
41
|
-
newTab: boolean
|
|
42
|
-
/** Which collection the Select is currently previewing — UI state only. */
|
|
43
|
-
targetCollection: string | null
|
|
44
|
-
/** Currently chosen document relation. `targetCollectionPath` is the
|
|
45
|
-
* source-of-truth collection for this doc and is independent of
|
|
46
|
-
* `targetCollection` so the user can explore other collections in the
|
|
47
|
-
* Select without losing their pick. */
|
|
48
|
-
picked: DocumentRelation | null
|
|
31
|
+
target: LinkTargetState
|
|
49
32
|
}
|
|
50
33
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
targetCollection: linkable[0]?.path ?? null,
|
|
58
|
-
picked: null,
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function fromLinkData(
|
|
63
|
-
data: LinkData | undefined,
|
|
64
|
-
linkable: MultiCollectionDefinition[]
|
|
65
|
-
): FormState {
|
|
66
|
-
const base = emptyState(linkable)
|
|
67
|
-
if (!data) return base
|
|
68
|
-
const fields = data.fields
|
|
69
|
-
if (!fields) return base
|
|
70
|
-
// Default to internal when:
|
|
71
|
-
// • the stored data already says internal, or
|
|
72
|
-
// • this is a fresh placeholder link from the toolbar (linkType: 'custom'
|
|
73
|
-
// with an empty / `https://` url) — in that case the user hasn't
|
|
74
|
-
// decided yet, so prefer the picker when any collection has
|
|
75
|
-
// `linksInEditor: true`.
|
|
76
|
-
const url = fields.linkType === 'internal' ? '' : (fields.url ?? '')
|
|
77
|
-
const isPlaceholderUrl = url === '' || url === 'https://'
|
|
78
|
-
const wantsInternal = linkable.length > 0 && (fields.linkType === 'internal' || isPlaceholderUrl)
|
|
79
|
-
const linkType: 'custom' | 'internal' = wantsInternal ? 'internal' : 'custom'
|
|
80
|
-
const picked: DocumentRelation | null =
|
|
81
|
-
fields.linkType === 'internal'
|
|
82
|
-
? {
|
|
83
|
-
targetDocumentId: fields.targetDocumentId,
|
|
84
|
-
targetCollectionId: fields.targetCollectionId,
|
|
85
|
-
targetCollectionPath: fields.targetCollectionPath,
|
|
86
|
-
document: fields.document,
|
|
87
|
-
}
|
|
88
|
-
: null
|
|
89
|
-
return {
|
|
90
|
-
text: data.text ?? '',
|
|
91
|
-
linkType,
|
|
92
|
-
// Don't surface the placeholder `https://` in the URL input — it makes
|
|
93
|
-
// the field look pre-filled with garbage.
|
|
94
|
-
url: isPlaceholderUrl ? '' : url,
|
|
95
|
-
newTab: fields.newTab ?? false,
|
|
96
|
-
targetCollection: picked?.targetCollectionPath ?? linkable[0]?.path ?? null,
|
|
97
|
-
picked,
|
|
98
|
-
}
|
|
99
|
-
}
|
|
34
|
+
/**
|
|
35
|
+
* A link node exists because it points somewhere, so "no target" is not a
|
|
36
|
+
* valid outcome here — unlike the inline-image modal, where the link is
|
|
37
|
+
* optional. Everything else about the target sub-form is shared.
|
|
38
|
+
*/
|
|
39
|
+
const LINK_TARGET_OPTIONS = { allowNone: false } as const
|
|
100
40
|
|
|
101
41
|
export const LinkModal: React.FC<LinkModalProps> = ({
|
|
102
42
|
isOpen = false,
|
|
@@ -113,109 +53,32 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
|
|
113
53
|
[]
|
|
114
54
|
)
|
|
115
55
|
|
|
116
|
-
const [pickerOpen, setPickerOpen] = useState(false)
|
|
117
56
|
const [urlError, setUrlError] = useState<string | null>(null)
|
|
118
57
|
|
|
119
58
|
const [state, setState] = useModalFormState<FormState>(
|
|
120
59
|
isOpen,
|
|
121
|
-
() =>
|
|
60
|
+
() => ({
|
|
61
|
+
text: dataFromProps?.text ?? '',
|
|
62
|
+
target: fromLinkAttributes(dataFromProps?.fields, linkable, LINK_TARGET_OPTIONS),
|
|
63
|
+
}),
|
|
122
64
|
() => setUrlError(null)
|
|
123
65
|
)
|
|
124
66
|
|
|
125
|
-
const targetDefinition = state.targetCollection
|
|
126
|
-
? getCollectionDefinition(state.targetCollection)
|
|
127
|
-
: null
|
|
128
|
-
const targetDef: MultiCollectionDefinition | null =
|
|
129
|
-
targetDefinition != null && !isSingleton(targetDefinition) ? targetDefinition : null
|
|
130
|
-
|
|
131
|
-
const collectionItems: SelectValue<string>[] = useMemo(
|
|
132
|
-
() => linkable.map((c) => ({ label: c.labels.singular, value: c.path })),
|
|
133
|
-
[linkable]
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
const pickedLabel: string | null = useMemo(() => {
|
|
137
|
-
if (state.linkType !== 'internal' || !state.picked) return null
|
|
138
|
-
const title = state.picked.document?.title
|
|
139
|
-
if (typeof title === 'string' && title.length > 0) return title
|
|
140
|
-
// No title cached — show a stable stub keyed off the collection.
|
|
141
|
-
const pickedDefinition = getCollectionDefinition(state.picked.targetCollectionPath)
|
|
142
|
-
const pickedDef =
|
|
143
|
-
pickedDefinition != null && !isSingleton(pickedDefinition) ? pickedDefinition : null
|
|
144
|
-
const short = state.picked.targetDocumentId.slice(0, 8)
|
|
145
|
-
return `${pickedDef?.labels.singular ?? state.picked.targetCollectionPath} · ${short}…`
|
|
146
|
-
}, [state.linkType, state.picked])
|
|
147
|
-
|
|
148
|
-
const handlePickerSelect = (selection: {
|
|
149
|
-
targetDocumentId: string
|
|
150
|
-
targetCollectionId: string
|
|
151
|
-
record?: Record<string, any>
|
|
152
|
-
}) => {
|
|
153
|
-
setPickerOpen(false)
|
|
154
|
-
setState((s) => {
|
|
155
|
-
const targetCollection = s.targetCollection as string
|
|
156
|
-
// Normalise the picked record into a small `{ title, path }` envelope.
|
|
157
|
-
// `useAsTitle` is always in the picker projection; `path` is top-level
|
|
158
|
-
// metadata on every list response. This is everything the public
|
|
159
|
-
// client needs to build a link to the document — no afterRead hook
|
|
160
|
-
// needed for the common case.
|
|
161
|
-
const titleField = getCollectionDefinition(targetCollection)?.useAsTitle
|
|
162
|
-
const title = titleField ? selection.record?.fields?.[titleField] : undefined
|
|
163
|
-
const path = selection.record?.path
|
|
164
|
-
const document: Record<string, any> = {}
|
|
165
|
-
if (typeof title === 'string' && title.length > 0) document.title = title
|
|
166
|
-
if (typeof path === 'string' && path.length > 0) document.path = path
|
|
167
|
-
return {
|
|
168
|
-
...s,
|
|
169
|
-
picked: {
|
|
170
|
-
targetDocumentId: selection.targetDocumentId,
|
|
171
|
-
targetCollectionId: selection.targetCollectionId,
|
|
172
|
-
targetCollectionPath: targetCollection,
|
|
173
|
-
document: Object.keys(document).length > 0 ? document : undefined,
|
|
174
|
-
},
|
|
175
|
-
}
|
|
176
|
-
})
|
|
177
|
-
}
|
|
178
|
-
|
|
179
67
|
const handleSave = () => {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return
|
|
185
|
-
}
|
|
68
|
+
const problem = validateLinkTarget(state.target)
|
|
69
|
+
if (problem != null) {
|
|
70
|
+
setUrlError(problem)
|
|
71
|
+
return
|
|
186
72
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
73
|
+
const fields = toLinkAttributes(state.target)
|
|
74
|
+
if (fields == null) {
|
|
75
|
+
setUrlError('Pick a target document')
|
|
76
|
+
return
|
|
192
77
|
}
|
|
193
78
|
|
|
194
|
-
const picked = state.picked as DocumentRelation
|
|
195
|
-
// Always embed the picker's `{ title, path }` envelope on save —
|
|
196
|
-
// the in-editor display reads it back to render a label without a
|
|
197
|
-
// round-trip, and the server-side write-time walker
|
|
198
|
-
// (`embedRichTextFields` in `@byline/core` + `lexicalEditorEmbedServer`)
|
|
199
|
-
// refreshes / canonicalises it on persistence.
|
|
200
|
-
const fields: LinkAttributes =
|
|
201
|
-
state.linkType === 'custom'
|
|
202
|
-
? {
|
|
203
|
-
linkType: 'custom',
|
|
204
|
-
url: state.url,
|
|
205
|
-
newTab: state.newTab,
|
|
206
|
-
}
|
|
207
|
-
: {
|
|
208
|
-
linkType: 'internal',
|
|
209
|
-
newTab: state.newTab,
|
|
210
|
-
targetDocumentId: picked.targetDocumentId,
|
|
211
|
-
targetCollectionId: picked.targetCollectionId,
|
|
212
|
-
targetCollectionPath: picked.targetCollectionPath,
|
|
213
|
-
document: picked.document,
|
|
214
|
-
}
|
|
215
|
-
|
|
216
79
|
onSubmit({
|
|
217
80
|
text: state.text.length > 0 ? state.text : null,
|
|
218
|
-
fields,
|
|
81
|
+
fields: fields as LinkAttributes,
|
|
219
82
|
})
|
|
220
83
|
onClose()
|
|
221
84
|
}
|
|
@@ -223,163 +86,61 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
|
|
223
86
|
if (!isOpen) return null
|
|
224
87
|
|
|
225
88
|
return (
|
|
226
|
-
|
|
227
|
-
<Modal
|
|
228
|
-
<Modal.
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
<
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
<
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
{state.linkType === 'internal' && (
|
|
283
|
-
<div className="flex flex-col gap-3 mt-2">
|
|
284
|
-
{linkable.length > 1 && (
|
|
285
|
-
<div>
|
|
286
|
-
<Label
|
|
287
|
-
id="link-target-collection"
|
|
288
|
-
htmlFor="link-target-collection"
|
|
289
|
-
className="mb-1"
|
|
290
|
-
label="Target collection"
|
|
291
|
-
/>
|
|
292
|
-
<Select<string>
|
|
293
|
-
size="sm"
|
|
294
|
-
items={collectionItems}
|
|
295
|
-
placeholder="Target collection"
|
|
296
|
-
value={state.targetCollection ?? undefined}
|
|
297
|
-
onValueChange={(value) => {
|
|
298
|
-
if (value == null) return
|
|
299
|
-
// Switching the Select is exploratory — we keep the
|
|
300
|
-
// currently picked document; only the picker target
|
|
301
|
-
// changes. Picking a new document via the picker is
|
|
302
|
-
// the only path that replaces `picked`.
|
|
303
|
-
setState((s) => ({ ...s, targetCollection: value }))
|
|
304
|
-
}}
|
|
305
|
-
/>
|
|
306
|
-
</div>
|
|
307
|
-
)}
|
|
308
|
-
|
|
309
|
-
<div className="border rounded p-3">
|
|
310
|
-
<Label
|
|
311
|
-
id="link-target-document"
|
|
312
|
-
htmlFor="link-target-document"
|
|
313
|
-
className="mb-1"
|
|
314
|
-
label="Target document"
|
|
315
|
-
/>
|
|
316
|
-
|
|
317
|
-
<div className="flex items-center justify-between gap-2">
|
|
318
|
-
{pickedLabel && (
|
|
319
|
-
<span className="text-sm text-accent-400 truncate">{pickedLabel}</span>
|
|
320
|
-
)}
|
|
321
|
-
<Button
|
|
322
|
-
size="sm"
|
|
323
|
-
variant="outlined"
|
|
324
|
-
intent="noeffect"
|
|
325
|
-
type="button"
|
|
326
|
-
onClick={() => setPickerOpen(true)}
|
|
327
|
-
disabled={!state.targetCollection}
|
|
328
|
-
>
|
|
329
|
-
{pickedLabel
|
|
330
|
-
? 'Change'
|
|
331
|
-
: `Pick ${targetDef?.labels.singular ?? 'document'}…`}
|
|
332
|
-
</Button>
|
|
333
|
-
</div>
|
|
334
|
-
</div>
|
|
335
|
-
|
|
336
|
-
{urlError && state.linkType === 'internal' && (
|
|
337
|
-
<span className="text-xs text-red-400">{urlError}</span>
|
|
338
|
-
)}
|
|
339
|
-
</div>
|
|
340
|
-
)}
|
|
341
|
-
|
|
342
|
-
<Checkbox
|
|
343
|
-
id="link-new-tab"
|
|
344
|
-
name="newTab"
|
|
345
|
-
label="Open in new tab"
|
|
346
|
-
checked={state.newTab}
|
|
347
|
-
onCheckedChange={(checked) => setState((s) => ({ ...s, newTab: checked === true }))}
|
|
348
|
-
/>
|
|
349
|
-
</div>
|
|
350
|
-
</Modal.Content>
|
|
351
|
-
<Modal.Actions className="flex gap-3">
|
|
352
|
-
<Button
|
|
353
|
-
size="sm"
|
|
354
|
-
intent="noeffect"
|
|
355
|
-
type="button"
|
|
356
|
-
onClick={onClose}
|
|
357
|
-
className="min-w-[70px]"
|
|
358
|
-
>
|
|
359
|
-
Cancel
|
|
360
|
-
</Button>
|
|
361
|
-
<Button
|
|
362
|
-
size="sm"
|
|
363
|
-
intent="primary"
|
|
364
|
-
type="button"
|
|
365
|
-
onClick={handleSave}
|
|
366
|
-
className="min-w-[70px]"
|
|
367
|
-
>
|
|
368
|
-
Save
|
|
369
|
-
</Button>
|
|
370
|
-
</Modal.Actions>
|
|
371
|
-
</Modal.Container>
|
|
372
|
-
</Modal>
|
|
373
|
-
|
|
374
|
-
{state.linkType === 'internal' && state.targetCollection && (
|
|
375
|
-
<RelationPicker
|
|
376
|
-
targetCollectionPath={state.targetCollection}
|
|
377
|
-
targetDefinition={targetDef}
|
|
378
|
-
isOpen={pickerOpen}
|
|
379
|
-
onSelect={handlePickerSelect}
|
|
380
|
-
onDismiss={() => setPickerOpen(false)}
|
|
381
|
-
/>
|
|
382
|
-
)}
|
|
383
|
-
</>
|
|
89
|
+
<Modal isOpen={isOpen} onDismiss={onClose} closeOnOverlayClick={false}>
|
|
90
|
+
<Modal.Container style={{ maxWidth: '480px', width: '100%' }}>
|
|
91
|
+
<Modal.Header className="flex items-center justify-between pt-4 mb-4">
|
|
92
|
+
<h3 className="m-0 text-xl">Edit link</h3>
|
|
93
|
+
<IconButton aria-label="Close" size="xs" onClick={onClose}>
|
|
94
|
+
<CloseIcon width="15px" height="15px" svgClassName="white-icon" />
|
|
95
|
+
</IconButton>
|
|
96
|
+
</Modal.Header>
|
|
97
|
+
<Modal.Content>
|
|
98
|
+
<div className="flex flex-col gap-4">
|
|
99
|
+
<Input
|
|
100
|
+
id="link-text"
|
|
101
|
+
className="mb-2"
|
|
102
|
+
name="text"
|
|
103
|
+
label="Link text"
|
|
104
|
+
placeholder="Visible link text"
|
|
105
|
+
value={state.text}
|
|
106
|
+
onChange={(e) => setState((s) => ({ ...s, text: e.target.value }))}
|
|
107
|
+
/>
|
|
108
|
+
|
|
109
|
+
<LinkTargetFields
|
|
110
|
+
idPrefix="link"
|
|
111
|
+
state={state.target}
|
|
112
|
+
onChange={(target) => {
|
|
113
|
+
setUrlError(null)
|
|
114
|
+
setState((s) => ({ ...s, target }))
|
|
115
|
+
}}
|
|
116
|
+
linkable={linkable}
|
|
117
|
+
allowNone={false}
|
|
118
|
+
error={urlError}
|
|
119
|
+
legend="Link type"
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
</Modal.Content>
|
|
123
|
+
<Modal.Actions className="flex gap-3">
|
|
124
|
+
<Button
|
|
125
|
+
size="sm"
|
|
126
|
+
intent="noeffect"
|
|
127
|
+
type="button"
|
|
128
|
+
onClick={onClose}
|
|
129
|
+
className="min-w-[70px]"
|
|
130
|
+
>
|
|
131
|
+
Cancel
|
|
132
|
+
</Button>
|
|
133
|
+
<Button
|
|
134
|
+
size="sm"
|
|
135
|
+
intent="primary"
|
|
136
|
+
type="button"
|
|
137
|
+
onClick={handleSave}
|
|
138
|
+
className="min-w-[70px]"
|
|
139
|
+
>
|
|
140
|
+
Save
|
|
141
|
+
</Button>
|
|
142
|
+
</Modal.Actions>
|
|
143
|
+
</Modal.Container>
|
|
144
|
+
</Modal>
|
|
384
145
|
)
|
|
385
146
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link-target picker — the "Target document" frame.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately mirrors the admin relation field
|
|
5
|
+
* (`packages/admin/src/fields/relation/relation-field.module.css`), because
|
|
6
|
+
* this is the same job: show the chosen document and offer a way to change
|
|
7
|
+
* it. Same frame token, same primary-bordered tile, same icon-button pair,
|
|
8
|
+
* so the editor's picker reads as the widget editors already know rather
|
|
9
|
+
* than a second dialect.
|
|
10
|
+
*
|
|
11
|
+
* Own class names rather than reusing `.byline-field-relation-*`: this
|
|
12
|
+
* package renders inside hosts that may not load the admin field
|
|
13
|
+
* stylesheet, and silently inheriting another package's internals would
|
|
14
|
+
* break without warning. The values are what keep the two in step.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
.byline-link-target-frame {
|
|
18
|
+
margin-top: 0.25rem;
|
|
19
|
+
padding: 0.5rem;
|
|
20
|
+
border: var(--border-width-thin, 1px) var(--border-style-solid, solid) var(--field-border);
|
|
21
|
+
border-radius: var(--border-radius-md, 6px);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* The chosen document: summary on the left, actions pinned right. */
|
|
25
|
+
.byline-link-target-tile {
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: flex-start;
|
|
28
|
+
justify-content: space-between;
|
|
29
|
+
gap: var(--spacing-8, 0.5rem);
|
|
30
|
+
padding: var(--spacing-8, 0.5rem);
|
|
31
|
+
border: var(--border-width-thin, 1px) var(--border-style-solid, solid) var(--primary-500);
|
|
32
|
+
border-radius: var(--border-radius-md, 6px);
|
|
33
|
+
font-size: var(--font-size-xs);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.byline-link-target-summary {
|
|
37
|
+
display: flex;
|
|
38
|
+
min-width: 0;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
gap: 0.125rem;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Small "type" line above the value — the target collection's label. */
|
|
44
|
+
.byline-link-target-kind {
|
|
45
|
+
color: var(--gray-500);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.byline-link-target-value {
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
color: var(--gray-900);
|
|
51
|
+
text-overflow: ellipsis;
|
|
52
|
+
white-space: nowrap;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.byline-link-target-actions {
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-shrink: 0;
|
|
58
|
+
align-items: center;
|
|
59
|
+
gap: var(--spacing-4, 0.25rem);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.byline-link-target-actions .byline-button {
|
|
63
|
+
color: var(--gray-900);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
:is([data-theme="dark"], .dark) {
|
|
67
|
+
/* A darker hairline than `--field-border` gives on this surface: the
|
|
68
|
+
modal sits on an already-raised panel, so the default reads as a
|
|
69
|
+
bright outline rather than a division. */
|
|
70
|
+
.byline-link-target-frame {
|
|
71
|
+
border-color: var(--canvas-700, var(--field-border));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.byline-link-target-value {
|
|
75
|
+
color: var(--gray-100);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.byline-link-target-actions .byline-button {
|
|
79
|
+
color: var(--gray-200);
|
|
80
|
+
}
|
|
81
|
+
}
|