@eclosion-tech/react-native-yjs-text 0.1.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/LICENSE +21 -0
  3. package/README.md +323 -0
  4. package/SPEC.md +346 -0
  5. package/android/build.gradle +26 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/tech/eclosion/yjstext/YjsTextModule.kt +77 -0
  8. package/android/src/main/java/tech/eclosion/yjstext/YjsTextSupport.kt +135 -0
  9. package/android/src/main/java/tech/eclosion/yjstext/YjsTextView.kt +424 -0
  10. package/build/YTextInput.d.ts +23 -0
  11. package/build/YTextInput.d.ts.map +1 -0
  12. package/build/YTextInput.js +178 -0
  13. package/build/YTextInput.js.map +1 -0
  14. package/build/YTextRenderer.d.ts +15 -0
  15. package/build/YTextRenderer.d.ts.map +1 -0
  16. package/build/YTextRenderer.js +85 -0
  17. package/build/YTextRenderer.js.map +1 -0
  18. package/build/bridge.d.ts +88 -0
  19. package/build/bridge.d.ts.map +1 -0
  20. package/build/bridge.js +231 -0
  21. package/build/bridge.js.map +1 -0
  22. package/build/index.d.ts +13 -0
  23. package/build/index.d.ts.map +1 -0
  24. package/build/index.js +12 -0
  25. package/build/index.js.map +1 -0
  26. package/build/internal/NativeYTextInputView.d.ts +114 -0
  27. package/build/internal/NativeYTextInputView.d.ts.map +1 -0
  28. package/build/internal/NativeYTextInputView.js +27 -0
  29. package/build/internal/NativeYTextInputView.js.map +1 -0
  30. package/build/internal/editorRegistry.d.ts +23 -0
  31. package/build/internal/editorRegistry.d.ts.map +1 -0
  32. package/build/internal/editorRegistry.js +26 -0
  33. package/build/internal/editorRegistry.js.map +1 -0
  34. package/build/schema.d.ts +51 -0
  35. package/build/schema.d.ts.map +1 -0
  36. package/build/schema.js +134 -0
  37. package/build/schema.js.map +1 -0
  38. package/build/types.d.ts +182 -0
  39. package/build/types.d.ts.map +1 -0
  40. package/build/types.js +11 -0
  41. package/build/types.js.map +1 -0
  42. package/build/useYTextEditor.d.ts +21 -0
  43. package/build/useYTextEditor.d.ts.map +1 -0
  44. package/build/useYTextEditor.js +166 -0
  45. package/build/useYTextEditor.js.map +1 -0
  46. package/expo-module.config.json +9 -0
  47. package/ios/YjsText.podspec +30 -0
  48. package/ios/YjsTextModule.swift +75 -0
  49. package/ios/YjsTextSupport.swift +135 -0
  50. package/ios/YjsTextView.swift +464 -0
  51. package/package.json +124 -0
  52. package/src/YTextInput.tsx +263 -0
  53. package/src/YTextRenderer.tsx +96 -0
  54. package/src/bridge.ts +283 -0
  55. package/src/index.ts +21 -0
  56. package/src/internal/NativeYTextInputView.tsx +126 -0
  57. package/src/internal/editorRegistry.ts +50 -0
  58. package/src/schema.ts +157 -0
  59. package/src/types.ts +194 -0
  60. package/src/useYTextEditor.ts +171 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,99 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ Initial release. See [`SPEC.md`](./SPEC.md) for the full design and the
6
+ [`README`](./README.md) for usage.
7
+
8
+ ### Added
9
+
10
+ - TypeScript layer
11
+ - `YTextInput`, `YTextRenderer`, `useYTextEditor`
12
+ - `defaultSchema` covering the v0.1 canonical mark set
13
+ (`bold`, `italic`, `underline`, `strike`, `code`, `link`)
14
+ - `Y.Text` ↔ runs delta translation with loop-breaking and
15
+ `Y.RelativePosition`-based caret stability
16
+ - iOS native view (`YjsTextView`) backed by `UITextView`
17
+ - `NSAttributedString` rendering with schema-driven mark styles
18
+ - Edit capture via `textView(_:shouldChangeTextIn:replacementText:)`
19
+ - Focus / selection events, imperative `focus` / `blur` / `setSelection`
20
+ - Android native view (`YjsTextView`) backed by `AppCompatEditText`
21
+ - `SpannableStringBuilder` rendering with the same schema-driven marks
22
+ - Edit capture via `TextWatcher`
23
+ - Focus / selection events, imperative methods matching iOS
24
+ - Expo Modules registration and `expo-module.config.json` wiring
25
+ - Example Expo app (`example/`) exercising every default mark and
26
+ programmatic mutations via `useYTextEditor`
27
+ - Example bare RN app (`example-bare/`) using `@react-native-community/cli`
28
+ scaffolding with Expo Modules autolinking wired in via Podfile,
29
+ settings.gradle, `AppDelegate.swift`, and `MainApplication.kt`. Proves the
30
+ library installs without the Expo app shell.
31
+ - pnpm workspace setup so the examples consume the library directly from
32
+ source, with Metro configs (one per example) that follow the workspace
33
+ symlink and block-list the library's hoisted `node_modules` to avoid
34
+ duplicate-React hook errors
35
+ - Jest suite (59 tests, ~1.5s, `babel-jest` + jsdom env)
36
+ - `schema.test.ts` — `compileRenderSpec` / `validateMarks` (required attrs,
37
+ type checks, unknown-mark rejection, declared-default fallback)
38
+ - `bridge.test.ts` — `ytextToRuns`, `inheritedMarksAt`, `marksInRange`,
39
+ `applyReplaceEdit` (insert / delete / replace / inherit / sanitise /
40
+ null-out-on-violation), `formatRange` (origin tag + observer round-trip),
41
+ and round-trip stability of `Y.RelativePosition` selection across remote
42
+ edits
43
+ - `useYTextEditor.test.ts` — every imperative command driven through a
44
+ fake `EditorHandle`, including the collapsed-caret pending-mark
45
+ behaviour and the marks-at-selection overlay logic
46
+ - `YTextRenderer.test.tsx` — verifies merged underline+strike decorations
47
+ and that `null` / `false` mark values are not applied
48
+
49
+ ### Fixed (during initial build verification)
50
+
51
+ - `applyReplaceEdit` actively `null`s out any inherited mark that fails the
52
+ schema sanitiser (instead of silently letting it leak into newly inserted
53
+ characters via Y.Text's default attribute inheritance). Also honours
54
+ `pendingMarks[mark] = false` as an explicit "do not inherit".
55
+ - Android: switched `EditText.BufferType` → `TextView.BufferType`
56
+ (`EditText` doesn't re-expose the nested enum in Kotlin) and replaced the
57
+ deprecated `DisplayMetrics.scaledDensity` with
58
+ `TypedValue.applyDimension(COMPLEX_UNIT_SP, ...)`.
59
+ - `formatRange` no longer tags its transaction with `ORIGIN_LOCAL_VIEW`, so
60
+ toolbar-driven format changes round-trip through the Y.Text observer and
61
+ trigger a re-render of the editor view (previously only the read-only
62
+ `YTextRenderer` would update on the first toggle).
63
+ - `null` / `false` mark values are now treated as "explicitly off" everywhere
64
+ (JS renderer, iOS `attributes(for:)`, Android `applyRunSpans`) so a Y.Text
65
+ delta of `{ bold: null }` does not visually re-bold the affected run.
66
+ - Selection prop is now sent to native as a single atomic
67
+ `pendingSelection: { from, to, version }` record. Splitting it into
68
+ separate `selection` + `selectionVersion` props was triggering Fabric prop
69
+ re-ordering races where the version setter fired before the range setter
70
+ and the caret jumped back to a stale selection.
71
+ - `runs` setter on both iOS and Android now unconditionally rebuilds the
72
+ attributed/spannable text. The previous `contentVersion` companion prop
73
+ was racing with `runs` under Fabric (version setter fired first, runs
74
+ arrived second) and causing every format toggle to appear one edit
75
+ behind.
76
+ - iOS native re-assignment of `attributedText` / Android re-assignment of
77
+ `editText.text` is now wrapped in a `suppressSelectionEvents` guard.
78
+ UIKit / Android both fire a synthetic selection-change to `{0, 0}` as
79
+ part of swapping the text in place; without the guard that
80
+ `onNativeSelectionChange` would overwrite JS's `selectionRef` and the
81
+ next toolbar tap would read a collapsed caret and silently do nothing.
82
+ - iOS combined bold + italic on system fonts: `UIFontDescriptor`'s symbolic-
83
+ trait path drops `.traitBold` when `.traitItalic` is also requested.
84
+ `makeFont` now tries multiple descriptor paths and, when no true bold-
85
+ italic font can be resolved, returns an `NSObliqueness` slant on top of
86
+ the bold font so the user still sees both effects.
87
+
88
+ ### Known limitations (see `SPEC.md` § Versioning roadmap)
89
+
90
+ - CJK / complex IME composition uses each platform's default editor
91
+ behaviour
92
+ - Paste from clipboard does not preserve formatting attributes
93
+ - Hardware keyboard shortcuts not bound by default
94
+ - Android: applying `code` (or any other `backgroundColor` mark) over a
95
+ selected range visually masks the system selection highlight. The
96
+ selection is still active and toolbar toggles still apply to it; this is
97
+ a draw-order quirk of `BackgroundColorSpan` in the Android text pipeline.
98
+ - All bridge traffic goes through the standard Expo Modules ABI (no JSI
99
+ fast path yet)
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,323 @@
1
+ # @eclosion-tech/react-native-yjs-text
2
+
3
+ Native React Native rich text editor backed by `Y.Text`. **No WebView, no
4
+ `contenteditable`, no DOM.**
5
+
6
+ Built as an Expo Module — works in any Expo SDK 52+ project (and in bare RN
7
+ projects via `expo-modules-core`).
8
+
9
+ > **Status:** v0.1.0 — alpha. The TypeScript layer, Swift iOS view, and Kotlin
10
+ > Android view all compile cleanly. Both example apps (Expo-managed in
11
+ > `example/` and bare RN in `example-bare/`) link the library and produce a
12
+ > working `.app` / `.apk`. A 50-case Jest suite covers the bridge, schema,
13
+ > and editor hook. The remaining v0.1 work is on-device behavioural
14
+ > verification (typing, mark toggling, caret stability under remote edits).
15
+
16
+ ---
17
+
18
+ ## Why this exists
19
+
20
+ Every existing rich-text editor in React Native today wraps a browser editor
21
+ in a `WebView`: `react-native-pell-rich-editor`, `@10play/tentap-editor`,
22
+ `react-native-cn-quill`. They all pay the WebView tax — worse performance,
23
+ broken keyboard behaviour, gesture / scroll conflicts, accessibility
24
+ regressions, platform-specific bugs that don't reproduce in browsers.
25
+
26
+ `Y.Text` is already a platform-independent rich-text primitive — a sequence of
27
+ characters with arbitrary formatting attributes. That maps cleanly onto
28
+ `contenteditable` (via `y-prosemirror` on the web) **and** onto
29
+ `NSAttributedString` / `Spannable` on native mobile. This library is the
30
+ native-view side of that binding.
31
+
32
+ See [`SPEC.md`](./SPEC.md) for the full design rationale, non-goals, and
33
+ versioning roadmap.
34
+
35
+ ## What ships in v0.1
36
+
37
+ - `YTextInput` — editable view backed by `UITextView` (iOS) /
38
+ `AppCompatEditText` (Android)
39
+ - `YTextRenderer` — read-only renderer that uses RN's native `<Text>` with
40
+ attributed spans (no `UITextView` instance, much cheaper)
41
+ - `useYTextEditor` — imperative editor hook (`toggleMark`, `setMark`,
42
+ `removeMark`, `insertText`, `deleteRange`, `focus`, `blur`,
43
+ `getSelection`, `setSelection`, `marksAtSelection`)
44
+ - `defaultSchema` — `bold`, `italic`, `underline`, `strike`, `code`, `link`
45
+ (matching the y-prosemirror mark convention so the same `Y.Text` content
46
+ edits identically on web)
47
+ - Bidirectional sync between user edits and `Y.Text` mutations, with
48
+ `Y.RelativePosition`-based caret preservation across remote insertions
49
+
50
+ ## Install
51
+
52
+ ### Expo (managed or prebuild)
53
+
54
+ ```sh
55
+ npx expo install @eclosion-tech/react-native-yjs-text yjs isomorphic-webcrypto
56
+ npx expo prebuild # if you don't already have native projects
57
+ npx expo run:ios # or run:android
58
+ ```
59
+
60
+ Expo Go can't load the library (native code); you need a dev build.
61
+
62
+ > **Why `isomorphic-webcrypto`?** yjs's underlying utility lib `lib0`
63
+ > generates random doc / client IDs via `crypto.getRandomValues`. React
64
+ > Native doesn't expose webcrypto by default, so `lib0`'s RN entry-point
65
+ > hard-requires `isomorphic-webcrypto` and expects the consumer to install
66
+ > it. (You can alternatively install `react-native-get-random-values` and
67
+ > `import 'react-native-get-random-values'` *before* the first yjs import —
68
+ > then `lib0` skips its webcrypto shim and uses the polyfilled native one.
69
+ > Either works; pick whichever your app already has.)
70
+
71
+ ### Bare React Native (RNCLI scaffold, brownfield, etc.)
72
+
73
+ The library uses the Expo Modules API for its native side, so a bare RN host
74
+ needs Expo Modules autolinking. **This does not turn your app into "an Expo
75
+ app"** — no app shell, no Expo Go runtime, no expo-router. You're only
76
+ opting into native autolinking.
77
+
78
+ ```sh
79
+ npm install @eclosion-tech/react-native-yjs-text yjs expo isomorphic-webcrypto
80
+ # expo brings in expo-modules-core + the autolinking scripts; nothing else
81
+ # isomorphic-webcrypto is required by yjs's lib0 dep — see the Expo section
82
+ # above for the alternative `react-native-get-random-values` route
83
+ ```
84
+
85
+ Then three small wiring changes:
86
+
87
+ 1. **`ios/Podfile`** — load Expo's autolinking + call `use_expo_modules!`:
88
+
89
+ ```ruby
90
+ require File.join(
91
+ File.dirname(`node --print "require.resolve('expo/package.json')"`),
92
+ "scripts/autolinking"
93
+ )
94
+
95
+ platform :ios, '16.4' # expo-modules-core requires 16.4+
96
+
97
+ target 'YourApp' do
98
+ use_expo_modules!
99
+ config = use_native_modules!
100
+ # ...rest of the standard RN Podfile target...
101
+ end
102
+ ```
103
+
104
+ 2. **`ios/YourApp/AppDelegate.swift`** — subclass Expo's app-delegate /
105
+ factory wrappers so Expo Modules register at boot:
106
+
107
+ ```swift
108
+ internal import Expo // matches the access level of the generated
109
+ // ExpoModulesProvider.swift
110
+ import React
111
+ import ReactAppDependencyProvider
112
+
113
+ @main
114
+ class AppDelegate: ExpoAppDelegate {
115
+ var window: UIWindow?
116
+ var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
117
+ var reactNativeFactory: RCTReactNativeFactory?
118
+
119
+ public override func application(/* ... */) -> Bool {
120
+ let delegate = ReactNativeDelegate()
121
+ let factory = ExpoReactNativeFactory(delegate: delegate)
122
+ delegate.dependencyProvider = RCTAppDependencyProvider()
123
+ reactNativeDelegate = delegate
124
+ reactNativeFactory = factory
125
+ window = UIWindow(frame: UIScreen.main.bounds)
126
+ factory.startReactNative(withModuleName: "YourApp",
127
+ in: window, launchOptions: launchOptions)
128
+ return super.application(application,
129
+ didFinishLaunchingWithOptions: launchOptions)
130
+ }
131
+ }
132
+
133
+ class ReactNativeDelegate: ExpoReactNativeFactoryDelegate { /* ... */ }
134
+ ```
135
+
136
+ 3. **`android/settings.gradle`** — pull in the Expo autolinking gradle
137
+ plugin and call `expoAutolinking.useExpoModules()`. **`android/app/
138
+ src/main/java/.../MainApplication.kt`** — replace
139
+ `DefaultReactHost.getDefaultReactHost(...)` with
140
+ `ExpoReactHostFactory.getDefaultReactHost(...)` and add
141
+ `ApplicationLifecycleDispatcher.onApplicationCreate(this)` /
142
+ `onConfigurationChanged(this, newConfig)`.
143
+
144
+ The complete working set of changes for a fresh `@react-native-community/
145
+ cli init` project lives in [`example-bare/`](./example-bare/) — copy from
146
+ there if you'd rather diff against a known-good reference than apply the
147
+ snippets above by hand.
148
+
149
+ ## Usage
150
+
151
+ ```tsx
152
+ import {
153
+ YTextInput,
154
+ YTextRenderer,
155
+ defaultSchema,
156
+ useYTextEditor,
157
+ } from '@eclosion-tech/react-native-yjs-text';
158
+ import * as Y from 'yjs';
159
+
160
+ const doc = new Y.Doc();
161
+ const yText = doc.getText('content');
162
+
163
+ function Editor() {
164
+ const editor = useYTextEditor(yText, defaultSchema);
165
+ return (
166
+ <>
167
+ <Toolbar editor={editor} />
168
+ <YTextInput
169
+ yText={yText}
170
+ schema={defaultSchema}
171
+ style={{ fontSize: 16, color: '#111' }}
172
+ placeholder="Start typing…"
173
+ />
174
+ <YTextRenderer yText={yText} schema={defaultSchema} />
175
+ </>
176
+ );
177
+ }
178
+
179
+ function Toolbar({ editor }) {
180
+ return (
181
+ <View>
182
+ <Button onPress={() => editor.toggleMark('bold')} title="B" />
183
+ <Button onPress={() => editor.toggleMark('italic')} title="I" />
184
+ <Button
185
+ onPress={() => editor.setMark('link', { href: 'https://yjs.dev' })}
186
+ title="Link"
187
+ />
188
+ </View>
189
+ );
190
+ }
191
+ ```
192
+
193
+ The library **does not own the `Y.Doc` / `Y.Text`** — the consumer creates,
194
+ syncs (via `y-websocket`, `y-indexeddb`, a custom provider, whatever), and
195
+ disposes of them. The component subscribes to a passed-in `Y.Text` and edits
196
+ it in place.
197
+
198
+ See [`example/App.tsx`](./example/App.tsx) for a full demo that exercises
199
+ every default mark, programmatic mutations, and `YTextRenderer`.
200
+
201
+ ## Schemas
202
+
203
+ A schema declares which marks the editor accepts. Marks not in the schema
204
+ are dropped on insert — this is what makes the editor safe for AI-generated
205
+ content. Custom marks are declared by extending `defaultSchema`:
206
+
207
+ ```ts
208
+ import { defaultSchema, type Schema } from '@eclosion-tech/react-native-yjs-text';
209
+
210
+ const schema: Schema = {
211
+ marks: {
212
+ ...defaultSchema.marks,
213
+ 'pear.mention': {
214
+ attrs: { userId: { type: 'string', required: true } },
215
+ renderStyle: { color: '#0066cc', backgroundColor: '#e6f0ff' },
216
+ onTap: (attrs) => console.log('mention tapped', attrs.userId),
217
+ },
218
+ },
219
+ };
220
+ ```
221
+
222
+ Only a subset of RN's `TextStyle` survives the bridge into native rendering:
223
+ `fontWeight`, `fontStyle`, `fontFamily`, `fontSize`, `color`,
224
+ `backgroundColor`, `textDecorationLine`. Keys outside that list are silently
225
+ dropped (a deliberate cross-platform-safety choice — see
226
+ `RENDERABLE_TEXT_STYLE_KEYS` in `src/schema.ts`).
227
+
228
+ ## Building & running
229
+
230
+ This repo is a pnpm workspace with three members:
231
+
232
+ - `.` — the library
233
+ - `example/` — Expo SDK 56 demo (uses `expo run:ios` / `expo run:android`)
234
+ - `example-bare/` — vanilla `@react-native-community/cli` demo (no Expo
235
+ runtime around the host app, only Expo Modules autolinking on the native
236
+ side)
237
+
238
+ ```sh
239
+ # From the repo root
240
+ pnpm install # installs all three workspace members
241
+ pnpm prepare # builds the library's TS into `build/`
242
+ pnpm test # runs the Jest suite (50 tests, ~1s)
243
+
244
+ # Expo example
245
+ cd example
246
+ npx pod-install # or: cd ios && pod install
247
+ npx expo run:ios # or: npx expo run:android
248
+
249
+ # Bare RN example
250
+ cd example-bare
251
+ (cd ios && pod install)
252
+ npx react-native run-ios # or: npx react-native run-android
253
+ ```
254
+
255
+ The bare example doubles as the canonical integration test for non-Expo
256
+ hosts; if it boots, your `@react-native-community/cli init`-shaped project
257
+ will too.
258
+
259
+ ## Architecture
260
+
261
+ ```
262
+ ┌─────────────────────────────────────────┐
263
+ │ Consumer application │
264
+ │ - manages the Y.Doc / Y.Text │
265
+ │ - builds toolbar / slash menu UI │
266
+ │ - composes multiple YTextInputs into │
267
+ │ its own block model │
268
+ └──────────────┬──────────────────────────┘
269
+ │ <YTextInput yText={...} schema={...} />
270
+
271
+ ┌──────────────▼──────────────────────────┐
272
+ │ react-native-yjs-text (TS layer) │
273
+ │ - YTextInput / YTextRenderer │
274
+ │ - useYTextEditor command API │
275
+ │ - schema definitions / mark validation│
276
+ │ - Y.Text ↔ runs ↔ native event bridge │
277
+ └──────────────┬──────────────────────────┘
278
+ │ Expo Modules (Fabric / TurboModules)
279
+
280
+ ┌──────────────▼──────────┬───────────────┐
281
+ │ iOS native │ Android native│
282
+ │ - YjsTextView (ExpoView │ - YjsTextView │
283
+ │ + UITextView subview) │ (ExpoView + │
284
+ │ - NSAttributedString │ AppCompatEditText)
285
+ │ - UITextViewDelegate │ - SpannableStringBuilder
286
+ │ shouldChangeTextIn │ - TextWatcher │
287
+ └─────────────────────────┴───────────────┘
288
+ ```
289
+
290
+ ## Known gaps in v0.1
291
+
292
+ Per the spec ([`SPEC.md` § Versioning roadmap](./SPEC.md#versioning-roadmap)):
293
+
294
+ - **IME composition for CJK / Korean / IMEs with multi-stage input.** We
295
+ hand the composition string straight through to `Y.Text` on every keystroke
296
+ in the composition session, which collaborators see as a flurry of
297
+ character-by-character inserts and deletes instead of one atomic
298
+ composition commit. A "compose locally, transact on commit" path lands in
299
+ v0.2 once we add `UITextInput.markedTextRange` / Android
300
+ `InputConnection.setComposingText` handling.
301
+ - **Paste from system clipboard** loses formatting — content is inserted as
302
+ plain runs.
303
+ - **Hardware-keyboard shortcuts** (⌘B / Ctrl+B etc.) are not bound by
304
+ default; consumers can wire their own via the imperative editor API.
305
+ - **Bridge traffic** goes through the standard Expo Modules ABI on every
306
+ edit. v0.2 will move the typing-hot-path event (`onContentChange`) to a
307
+ JSI direct call to skip JSON ser/de per keystroke.
308
+ - **No first-party block model.** Each `YTextInput` edits one inline
309
+ region; the consumer composes them into paragraphs / headings / lists.
310
+ See `SPEC.md` for why this is by design.
311
+ - **Selection highlight is visually masked by `backgroundColor` marks.**
312
+ Both `UITextView` (iOS) and `EditText` (Android) draw `NSAttributedString.
313
+ backgroundColor` / `BackgroundColorSpan` on top of the system selection
314
+ highlight, so selecting text that already has, e.g., the default `code`
315
+ mark applied makes the selection rectangle invisible inside the marked
316
+ range. The selection is still active functionally — toolbar toggles and
317
+ keyboard actions work as normal — only the visual rectangle is occluded.
318
+ iOS users still see the two grab-handle circles. Mirrors how Mobile
319
+ Safari, iOS Notes, and most platform editors render the same situation.
320
+
321
+ ## License
322
+
323
+ MIT. See [`LICENSE`](./LICENSE).