@gogitcms/design-system 0.15.0-next.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 +77 -0
- package/css/components.css +312 -0
- package/css/tokens.css +212 -0
- package/package.json +65 -0
- package/src/ThemeProvider.tsx +86 -0
- package/src/__tests__/ApplyChangesModal.test.tsx +148 -0
- package/src/__tests__/BranchImport.test.tsx +46 -0
- package/src/__tests__/Button.test.tsx +45 -0
- package/src/__tests__/ChangeRequestSummary.test.tsx +57 -0
- package/src/__tests__/ContentBrowser.changes.test.tsx +611 -0
- package/src/__tests__/ContentBrowser.collab.test.tsx +322 -0
- package/src/__tests__/ContentBrowser.collabsync.test.tsx +264 -0
- package/src/__tests__/ContentBrowser.contentslot.test.tsx +53 -0
- package/src/__tests__/ContentBrowser.discriminator.test.tsx +142 -0
- package/src/__tests__/ContentBrowser.drafts.test.tsx +271 -0
- package/src/__tests__/ContentBrowser.fields.test.tsx +117 -0
- package/src/__tests__/ContentBrowser.media.test.tsx +140 -0
- package/src/__tests__/ContentBrowser.mixedcollab.test.tsx +63 -0
- package/src/__tests__/ContentBrowser.mixedvalues.test.tsx +38 -0
- package/src/__tests__/ContentBrowser.pagination.test.tsx +62 -0
- package/src/__tests__/ContentBrowser.previewtab.test.tsx +212 -0
- package/src/__tests__/ContentBrowser.reorder.test.tsx +45 -0
- package/src/__tests__/ContentBrowser.search.test.tsx +135 -0
- package/src/__tests__/ContentBrowser.selectvalue.test.tsx +185 -0
- package/src/__tests__/ContentBrowser.staged.test.tsx +132 -0
- package/src/__tests__/ContentBrowser.usermenu.test.tsx +56 -0
- package/src/__tests__/MediaBrowser.test.tsx +353 -0
- package/src/__tests__/MediaField.test.tsx +185 -0
- package/src/__tests__/Notifications.test.tsx +69 -0
- package/src/__tests__/Onboarding.test.tsx +287 -0
- package/src/__tests__/cssTokens.test.ts +201 -0
- package/src/__tests__/fieldComponents.test.ts +43 -0
- package/src/__tests__/reorder.test.ts +58 -0
- package/src/components/ApplyChangesModal.tsx +348 -0
- package/src/components/BranchImport.tsx +157 -0
- package/src/components/BranchMenu.tsx +192 -0
- package/src/components/Button.tsx +131 -0
- package/src/components/ChangeDetail.tsx +472 -0
- package/src/components/ChangeRequestSummary.tsx +173 -0
- package/src/components/CollabField.tsx +388 -0
- package/src/components/ContentBrowser.tsx +5073 -0
- package/src/components/Icon.tsx +28 -0
- package/src/components/Icon.web.tsx +31 -0
- package/src/components/Input.tsx +106 -0
- package/src/components/MediaBrowser.tsx +766 -0
- package/src/components/MediaField.tsx +670 -0
- package/src/components/MediaPreview.tsx +91 -0
- package/src/components/MediaPreview.web.tsx +169 -0
- package/src/components/NavRow.tsx +105 -0
- package/src/components/Notifications.tsx +301 -0
- package/src/components/Onboarding.tsx +751 -0
- package/src/components/ProjectMenu.tsx +124 -0
- package/src/components/Segment.tsx +87 -0
- package/src/components/Skeleton.tsx +216 -0
- package/src/components/Spinner.tsx +44 -0
- package/src/components/Text.tsx +85 -0
- package/src/components/documentDrafts.ts +213 -0
- package/src/components/layout.tsx +284 -0
- package/src/components/primitives.tsx +143 -0
- package/src/components/reorder.ts +40 -0
- package/src/fieldComponents.ts +63 -0
- package/src/icons.ts +102 -0
- package/src/index.ts +198 -0
- package/src/media.ts +229 -0
- package/src/theme.ts +116 -0
- package/src/web/Button.tsx +110 -0
- package/src/web/Icon.tsx +52 -0
- package/src/web/Input.tsx +39 -0
- package/src/web/index.ts +43 -0
- package/src/web/primitives.tsx +119 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import React, { useSyncExternalStore } from "react";
|
|
2
|
+
import { View, TextInput, Pressable } from "react-native";
|
|
3
|
+
import { Text } from "./Text";
|
|
4
|
+
import { Input } from "./Input";
|
|
5
|
+
import { useTheme } from "../ThemeProvider";
|
|
6
|
+
import type { CollabApi, CollabParticipant, CollabPeer, CollabRegister, CollabText } from "./ContentBrowser";
|
|
7
|
+
|
|
8
|
+
// useCollabText subscribes a component to one text field's shared CRDT value and
|
|
9
|
+
// the peers focused on it. Value/peers come straight from the binding so all
|
|
10
|
+
// clients converge; setValue writes back to the CRDT.
|
|
11
|
+
export function useCollabText(binding: CollabText) {
|
|
12
|
+
const value = useSyncExternalStore(binding.subscribe, binding.get, binding.get);
|
|
13
|
+
const peers = useSyncExternalStore(binding.subscribe, binding.peers, binding.peers);
|
|
14
|
+
return { value, peers, setValue: binding.set };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// useCollabRegister binds a scalar field (select/boolean/…) to its shared
|
|
18
|
+
// last-writer-wins register.
|
|
19
|
+
export function useCollabRegister(binding: CollabRegister) {
|
|
20
|
+
const value = useSyncExternalStore(binding.subscribe, binding.get, binding.get);
|
|
21
|
+
return { value, setValue: binding.set };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// useCollabSynced reports whether the shared document reflects the server room
|
|
25
|
+
// yet. Until it does, every binding reads empty — not because the field is
|
|
26
|
+
// empty, but because nothing has arrived — and a control that trusts that shows
|
|
27
|
+
// a document's saved value as blank.
|
|
28
|
+
//
|
|
29
|
+
// A host with no synced notion (tests, older impls) omits the pair; those
|
|
30
|
+
// consumers act immediately, as they did before it existed.
|
|
31
|
+
export function useCollabSynced(collab: CollabApi): boolean {
|
|
32
|
+
const subscribe = React.useCallback(
|
|
33
|
+
(cb: () => void) => collab.subscribeSynced?.(cb) ?? (() => {}),
|
|
34
|
+
[collab],
|
|
35
|
+
);
|
|
36
|
+
const get = React.useCallback(() => collab.synced?.() ?? true, [collab]);
|
|
37
|
+
return useSyncExternalStore(subscribe, get, get);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Holds a control together across the gap between it mounting and the shared
|
|
42
|
+
* document arriving.
|
|
43
|
+
*
|
|
44
|
+
* Two things went wrong in that window, both from the same mistake — reading an
|
|
45
|
+
* empty binding as though it were the field's value. The control showed a saved
|
|
46
|
+
* document as blank for as long as the handshake took. And an edit made in that
|
|
47
|
+
* moment went into the shared doc *before* the server's seeded value did, where
|
|
48
|
+
* it did not replace it but merged with it: typing "draft" into a field that
|
|
49
|
+
* turns out to hold "shipped" left "shippeddraft" behind, in the document, with
|
|
50
|
+
* nothing on screen to say where it came from.
|
|
51
|
+
*
|
|
52
|
+
* So until the doc has arrived the field is an ordinary local control. It shows
|
|
53
|
+
* the value the host loaded from the document, and an edit is held rather than
|
|
54
|
+
* written. Arrival writes the held edit through as a single replacement — which
|
|
55
|
+
* is what the user meant by it — and hands the field to the CRDT for good.
|
|
56
|
+
*
|
|
57
|
+
* Arrival is once-only, and a value in hand counts as proof of it whatever the
|
|
58
|
+
* provider reports. A later disconnect therefore changes nothing: edits keep
|
|
59
|
+
* going straight into the doc, so an offline stretch still merges the way a CRDT
|
|
60
|
+
* is meant to rather than being replayed over whatever happened meanwhile.
|
|
61
|
+
*/
|
|
62
|
+
export function useCollabValue<T>(
|
|
63
|
+
collab: CollabApi,
|
|
64
|
+
shared: T,
|
|
65
|
+
local: T,
|
|
66
|
+
write: (next: T) => void,
|
|
67
|
+
): { value: T; set: (next: T) => void } {
|
|
68
|
+
const synced = useCollabSynced(collab);
|
|
69
|
+
const everSynced = React.useRef(false);
|
|
70
|
+
if (synced) everSynced.current = true;
|
|
71
|
+
// `undefined` is a register nobody has written yet; "" is a Y.Text that has
|
|
72
|
+
// either not arrived or is genuinely empty — which of the two is what `synced`
|
|
73
|
+
// is here to settle.
|
|
74
|
+
const unset = shared === undefined || shared === null || shared === "";
|
|
75
|
+
const arrived = everSynced.current || !unset;
|
|
76
|
+
const [held, setHeld] = React.useState<[T] | null>(null);
|
|
77
|
+
// Held in a ref: the caller passes a fresh closure over its bindings each
|
|
78
|
+
// render, and in the effect's dependencies that would replay the flush.
|
|
79
|
+
const writeRef = React.useRef(write);
|
|
80
|
+
writeRef.current = write;
|
|
81
|
+
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
if (!arrived || !held) return;
|
|
84
|
+
writeRef.current(held[0]);
|
|
85
|
+
setHeld(null);
|
|
86
|
+
}, [arrived, held]);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
// A register still unwritten after arrival falls back to the local value
|
|
90
|
+
// too: nothing has claimed the field, so the document's own value is the
|
|
91
|
+
// best answer. An arrived *text* reading "" is an answer — the field is
|
|
92
|
+
// empty, possibly because a peer just cleared it — and stands.
|
|
93
|
+
value: held ? held[0] : arrived && shared !== undefined ? shared : local,
|
|
94
|
+
set: (next: T) => {
|
|
95
|
+
if (arrived) writeRef.current(next);
|
|
96
|
+
else setHeld([next]);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Reports a collaborator's change to the host as though the user had made it.
|
|
103
|
+
*
|
|
104
|
+
* A collab-bound control has two sources of truth and only ever told the host
|
|
105
|
+
* about one of them. Typing called `onChange`, so the host's value map, its save
|
|
106
|
+
* state and the draft it feeds the preview all moved. A change arriving over the
|
|
107
|
+
* CRDT re-rendered the control — through `useSyncExternalStore`, which is what
|
|
108
|
+
* makes the text on screen correct — and stopped there. The field showed the new
|
|
109
|
+
* value while every consumer of `onChange` still held the old one.
|
|
110
|
+
*
|
|
111
|
+
* Two things fell out of that. The preview never rebuilt for anyone but the
|
|
112
|
+
* author of the edit, because a draft is only sent when `onChange` fires. And
|
|
113
|
+
* the host's map stayed stale, so the next save wrote this field back as it was
|
|
114
|
+
* before the collaborator touched it — losing their work in the committed
|
|
115
|
+
* document, silently, with both screens showing the newer text.
|
|
116
|
+
*
|
|
117
|
+
* So: return an `emit` for the local path to call in place of `onChange`, and
|
|
118
|
+
* fire `onChange` for anything that changes the value without going through it.
|
|
119
|
+
* `emit` is what keeps a local edit from arriving twice — it settles the value
|
|
120
|
+
* on the way out, so the CRDT echo of the user's own keystroke is not read back
|
|
121
|
+
* as somebody else's change.
|
|
122
|
+
*
|
|
123
|
+
* The value is settled on the first render as well, so a field that mounts
|
|
124
|
+
* already in sync announces nothing: at that point the host seeded the map from
|
|
125
|
+
* the same document, and an edit event there would mark an untouched document
|
|
126
|
+
* unsaved.
|
|
127
|
+
*/
|
|
128
|
+
export function useCollabMirror<T>(value: T, onChange: (v: T) => void): (v: T) => void {
|
|
129
|
+
const settled = React.useRef(value);
|
|
130
|
+
// Held in a ref because the host passes a fresh arrow each render — in the
|
|
131
|
+
// effect's dependencies it would re-run on every render of the form, and the
|
|
132
|
+
// guard below is the only reason that would be harmless rather than a loop.
|
|
133
|
+
const cb = React.useRef(onChange);
|
|
134
|
+
cb.current = onChange;
|
|
135
|
+
|
|
136
|
+
React.useEffect(() => {
|
|
137
|
+
if (Object.is(settled.current, value)) return;
|
|
138
|
+
settled.current = value;
|
|
139
|
+
cb.current(value);
|
|
140
|
+
}, [value]);
|
|
141
|
+
|
|
142
|
+
return React.useCallback((next: T) => {
|
|
143
|
+
settled.current = next;
|
|
144
|
+
cb.current(next);
|
|
145
|
+
}, []);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// useCollabPeers re-renders on any awareness change (tick-based) and returns the
|
|
149
|
+
// peers focused exactly at a field path — for presence on non-text controls.
|
|
150
|
+
export function useCollabPeers(collab: CollabApi, path: string): CollabPeer[] {
|
|
151
|
+
useSyncExternalStore(collab.subscribeAwareness, collab.awarenessVersion, collab.awarenessVersion);
|
|
152
|
+
return collab.peersAt(path);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// useCollabPeersUnder is like useCollabPeers but for a group prefix (any peer
|
|
156
|
+
// focused on a descendant field), driving the group's presence highlight.
|
|
157
|
+
export function useCollabPeersUnder(collab: CollabApi, prefix: string): CollabPeer[] {
|
|
158
|
+
useSyncExternalStore(collab.subscribeAwareness, collab.awarenessVersion, collab.awarenessVersion);
|
|
159
|
+
return collab.peersUnder(prefix);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// useCollabParticipants re-renders on awareness changes and returns everyone
|
|
163
|
+
// connected to the document (excluding self).
|
|
164
|
+
export function useCollabParticipants(collab: CollabApi): CollabParticipant[] {
|
|
165
|
+
useSyncExternalStore(collab.subscribeAwareness, collab.awarenessVersion, collab.awarenessVersion);
|
|
166
|
+
return collab.participants();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function initials(name: string): string {
|
|
170
|
+
const parts = name.trim().split(/[\s._-]+/).filter(Boolean);
|
|
171
|
+
if (parts.length === 0) return "?";
|
|
172
|
+
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
173
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// PresenceAvatars renders overlapping, colored avatars for everyone connected to
|
|
177
|
+
// the document. Clicking one that is editing a field calls onFocusPeer with that
|
|
178
|
+
// field's path so the host can jump to it.
|
|
179
|
+
export function PresenceAvatars({
|
|
180
|
+
collab,
|
|
181
|
+
onFocusPeer,
|
|
182
|
+
}: {
|
|
183
|
+
collab: CollabApi;
|
|
184
|
+
onFocusPeer: (path: string) => void;
|
|
185
|
+
}) {
|
|
186
|
+
const t = useTheme();
|
|
187
|
+
const people = useCollabParticipants(collab);
|
|
188
|
+
if (people.length === 0) return null;
|
|
189
|
+
const size = 24;
|
|
190
|
+
return (
|
|
191
|
+
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
|
192
|
+
{people.map((p, i) => {
|
|
193
|
+
const editing = !!p.focus;
|
|
194
|
+
return (
|
|
195
|
+
<Pressable
|
|
196
|
+
key={p.id}
|
|
197
|
+
onPress={() => p.focus && onFocusPeer(p.focus)}
|
|
198
|
+
disabled={!editing}
|
|
199
|
+
accessibilityLabel={editing ? `${p.name} — editing ${p.focus}` : p.name}
|
|
200
|
+
style={[
|
|
201
|
+
{
|
|
202
|
+
width: size,
|
|
203
|
+
height: size,
|
|
204
|
+
borderRadius: size / 2,
|
|
205
|
+
backgroundColor: p.color,
|
|
206
|
+
borderWidth: 2,
|
|
207
|
+
borderColor: t.color.surfaceRaised,
|
|
208
|
+
alignItems: "center",
|
|
209
|
+
justifyContent: "center",
|
|
210
|
+
marginLeft: i === 0 ? 0 : -8,
|
|
211
|
+
},
|
|
212
|
+
// react-native-web accepts CSS cursor; not in the RN ViewStyle type.
|
|
213
|
+
{ cursor: editing ? "pointer" : "default" } as unknown as object,
|
|
214
|
+
]}
|
|
215
|
+
>
|
|
216
|
+
<Text variant="monoSm" color="inverted" style={{ fontSize: 10, lineHeight: 12 }}>
|
|
217
|
+
{initials(p.name)}
|
|
218
|
+
</Text>
|
|
219
|
+
</Pressable>
|
|
220
|
+
);
|
|
221
|
+
})}
|
|
222
|
+
</View>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// PresenceField wraps a control with a colored outline and floating name chips
|
|
227
|
+
// while one or more peers are focused on it. With no peers it renders its child
|
|
228
|
+
// unchanged (no layout shift beyond a transparent border).
|
|
229
|
+
export function PresenceField({
|
|
230
|
+
peers,
|
|
231
|
+
children,
|
|
232
|
+
}: {
|
|
233
|
+
peers: CollabPeer[];
|
|
234
|
+
children: React.ReactNode;
|
|
235
|
+
}) {
|
|
236
|
+
const t = useTheme();
|
|
237
|
+
const active = peers.length > 0;
|
|
238
|
+
const accent = active ? peers[0].color : "transparent";
|
|
239
|
+
return (
|
|
240
|
+
<View style={{ position: "relative" }}>
|
|
241
|
+
<View
|
|
242
|
+
style={{
|
|
243
|
+
borderWidth: 2,
|
|
244
|
+
borderColor: accent,
|
|
245
|
+
borderRadius: t.radius.md,
|
|
246
|
+
// Negative margin keeps the control's own layout box unchanged whether
|
|
247
|
+
// or not the presence border is showing.
|
|
248
|
+
margin: -2,
|
|
249
|
+
padding: 0,
|
|
250
|
+
}}
|
|
251
|
+
>
|
|
252
|
+
{children}
|
|
253
|
+
</View>
|
|
254
|
+
{active ? (
|
|
255
|
+
<View
|
|
256
|
+
pointerEvents="none"
|
|
257
|
+
style={{ position: "absolute", top: -16, right: 0, flexDirection: "row", gap: 4 }}
|
|
258
|
+
>
|
|
259
|
+
{peers.map((p) => (
|
|
260
|
+
<View
|
|
261
|
+
key={p.id}
|
|
262
|
+
style={{
|
|
263
|
+
backgroundColor: p.color,
|
|
264
|
+
borderRadius: 4,
|
|
265
|
+
paddingHorizontal: 6,
|
|
266
|
+
paddingVertical: 1,
|
|
267
|
+
}}
|
|
268
|
+
>
|
|
269
|
+
<Text variant="monoSm" color="inverted" style={{ fontSize: 10, lineHeight: 14 }}>
|
|
270
|
+
{p.name}
|
|
271
|
+
</Text>
|
|
272
|
+
</View>
|
|
273
|
+
))}
|
|
274
|
+
</View>
|
|
275
|
+
) : null}
|
|
276
|
+
</View>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// CollabInput is a single-line text field bound to a shared CRDT value. Local
|
|
281
|
+
// edits write to the CRDT and mirror to onChange (so the host's autosave/
|
|
282
|
+
// validation see the converged value); focus/blur announce presence. `local` is
|
|
283
|
+
// the host's own value for the field, which stands in until the shared document
|
|
284
|
+
// arrives (see useCollabValue).
|
|
285
|
+
export function CollabInput({
|
|
286
|
+
collab,
|
|
287
|
+
path,
|
|
288
|
+
local,
|
|
289
|
+
label,
|
|
290
|
+
mono,
|
|
291
|
+
keyboardType,
|
|
292
|
+
readOnly,
|
|
293
|
+
error,
|
|
294
|
+
onChange,
|
|
295
|
+
focusSignal,
|
|
296
|
+
}: {
|
|
297
|
+
collab: CollabApi;
|
|
298
|
+
path: string;
|
|
299
|
+
local: string;
|
|
300
|
+
label?: string;
|
|
301
|
+
mono?: boolean;
|
|
302
|
+
keyboardType?: "default" | "numeric";
|
|
303
|
+
readOnly?: boolean;
|
|
304
|
+
error?: boolean;
|
|
305
|
+
onChange: (v: string) => void;
|
|
306
|
+
focusSignal?: number;
|
|
307
|
+
}) {
|
|
308
|
+
const { value: shared, peers, setValue } = useCollabText(collab.text(path));
|
|
309
|
+
const { value, set } = useCollabValue(collab, shared, local, setValue);
|
|
310
|
+
const emit = useCollabMirror(value, onChange);
|
|
311
|
+
return (
|
|
312
|
+
<PresenceField peers={peers}>
|
|
313
|
+
<Input
|
|
314
|
+
label={label}
|
|
315
|
+
value={value}
|
|
316
|
+
onChangeText={(v) => {
|
|
317
|
+
set(v);
|
|
318
|
+
emit(v);
|
|
319
|
+
}}
|
|
320
|
+
mono={mono}
|
|
321
|
+
keyboardType={keyboardType}
|
|
322
|
+
size="md"
|
|
323
|
+
editable={!readOnly}
|
|
324
|
+
error={error}
|
|
325
|
+
onFocus={() => collab.setFocus(path)}
|
|
326
|
+
onBlur={() => collab.setFocus(null)}
|
|
327
|
+
focusSignal={focusSignal}
|
|
328
|
+
/>
|
|
329
|
+
</PresenceField>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// CollabTextarea is the multi-line counterpart of CollabInput.
|
|
334
|
+
export function CollabTextarea({
|
|
335
|
+
collab,
|
|
336
|
+
path,
|
|
337
|
+
local,
|
|
338
|
+
readOnly,
|
|
339
|
+
error,
|
|
340
|
+
onChange,
|
|
341
|
+
focusSignal,
|
|
342
|
+
}: {
|
|
343
|
+
collab: CollabApi;
|
|
344
|
+
path: string;
|
|
345
|
+
local: string;
|
|
346
|
+
readOnly?: boolean;
|
|
347
|
+
error?: boolean;
|
|
348
|
+
onChange: (v: string) => void;
|
|
349
|
+
focusSignal?: number;
|
|
350
|
+
}) {
|
|
351
|
+
const t = useTheme();
|
|
352
|
+
const { value: shared, peers, setValue } = useCollabText(collab.text(path));
|
|
353
|
+
const { value, set } = useCollabValue(collab, shared, local, setValue);
|
|
354
|
+
const emit = useCollabMirror(value, onChange);
|
|
355
|
+
const ref = React.useRef<TextInput>(null);
|
|
356
|
+
React.useEffect(() => {
|
|
357
|
+
if (focusSignal) ref.current?.focus();
|
|
358
|
+
}, [focusSignal]);
|
|
359
|
+
return (
|
|
360
|
+
<PresenceField peers={peers}>
|
|
361
|
+
<TextInput
|
|
362
|
+
ref={ref}
|
|
363
|
+
multiline
|
|
364
|
+
editable={!readOnly}
|
|
365
|
+
value={value}
|
|
366
|
+
onChangeText={(v) => {
|
|
367
|
+
set(v);
|
|
368
|
+
emit(v);
|
|
369
|
+
}}
|
|
370
|
+
onFocus={() => collab.setFocus(path)}
|
|
371
|
+
onBlur={() => collab.setFocus(null)}
|
|
372
|
+
// @ts-expect-error react-native-web
|
|
373
|
+
style={{
|
|
374
|
+
minHeight: 96,
|
|
375
|
+
padding: t.space(3),
|
|
376
|
+
borderWidth: 1,
|
|
377
|
+
borderColor: error ? t.color.diffDelFg : t.color.borderDefault,
|
|
378
|
+
borderRadius: t.radius.md,
|
|
379
|
+
backgroundColor: readOnly ? t.color.surfaceSunken : t.color.surfaceRaised,
|
|
380
|
+
color: readOnly ? t.color.textSecondary : t.color.textPrimary,
|
|
381
|
+
fontSize: 13,
|
|
382
|
+
textAlignVertical: "top",
|
|
383
|
+
outlineStyle: "none",
|
|
384
|
+
}}
|
|
385
|
+
/>
|
|
386
|
+
</PresenceField>
|
|
387
|
+
);
|
|
388
|
+
}
|