lexxy-realtime 0.4.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1ca9f7e8da5b292e73222ff67b6f49f6abe35825faf45af947157d4442fa37ce
4
+ data.tar.gz: a5aa12d0c2835df82d5eefa99e884b1920af507e824eb7ee3ee032d8b1c06a41
5
+ SHA512:
6
+ metadata.gz: 2eb4bbdc5584615480b1b8cde54df071b1a38cce8c84cd6eaca8e14dbce7a57617f3f5c758d9b3c9dbc226a2c69c15d2f1a84ef69b5aa986824c7182498fb420
7
+ data.tar.gz: a9ec33bca8104051993e0d4a2fc6233afb459bf9e8a6cd63d72b1801f316244d5ef161d38e33d1ca7c325446f87d16622eb8bd7ce3cb20952eaf13d146d0f0ed
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 JP Camara
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.
data/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # lexxy-realtime (Rails gem)
2
+
3
+ Collaborative [Lexxy](https://github.com/basecamp/lexxy) editing for
4
+ Rails, backed by [yrby](https://github.com/jpcamara/yrby)'s Ruby
5
+ implementation of Yjs.
6
+
7
+ ## Install
8
+
9
+ Requires Ruby 3.4+, Rails 8.0.2+, and a working
10
+ [Lexxy](https://github.com/basecamp/lexxy) installation (its gem and
11
+ editor JavaScript). Import maps and bundlers (esbuild, Vite, webpack)
12
+ both work.
13
+
14
+ ```ruby
15
+ # Gemfile
16
+ gem "lexxy-realtime"
17
+ ```
18
+
19
+ ```bash
20
+ bin/rails generate lexxy_realtime:install
21
+ bin/rails db:migrate
22
+ ```
23
+
24
+ The generator creates `app/channels/document_channel.rb`, installs
25
+ yrby's table migration, and adds the standard Action Cable files when
26
+ they are missing. The `Y::Document` and `Y::DocumentUpdate` models come
27
+ from `yrby-rails`.
28
+
29
+ With a bundler, install the JavaScript package and import it next to
30
+ your Lexxy import:
31
+
32
+ ```bash
33
+ npm install lexxy-realtime # yarn, bun, and pnpm also work
34
+ ```
35
+
36
+ With import maps, there is no npm install; the generator pins assets
37
+ this gem ships:
38
+
39
+ ```ruby
40
+ # config/importmap.rb, added by the generator
41
+ pin "lexical", to: "lexxy_realtime/lexical.js"
42
+ pin "@37signals/lexxy", to: "lexxy_realtime/lexxy.js"
43
+ pin "lexxy-realtime", to: "lexxy_realtime/lexxy-realtime.js"
44
+ pin "@rails/activestorage", to: "activestorage.esm.js"
45
+ ```
46
+
47
+ `lexical` is the one module the Lexxy and lexxy-realtime bundles share,
48
+ so both ship with it external and it resolves through its own pin. The
49
+ `@37signals/lexxy` pin must point at this gem's build: Lexxy's own
50
+ asset bundles a second copy of `lexical`, and two copies break the
51
+ collaboration binding, so remove any pin of Lexxy's asset. Keep
52
+ `stylesheet_link_tag "lexxy"` for the editor's CSS. These assets are a
53
+ stopgap until Lexxy ships import-map-ready builds itself.
54
+
55
+ Either way, your entrypoint imports both:
56
+
57
+ ```js
58
+ import "@37signals/lexxy"
59
+ import "lexxy-realtime"
60
+ ```
61
+
62
+ ## Use
63
+
64
+ ```ruby
65
+ class Post < ApplicationRecord
66
+ has_collaborative_rich_text :body # a regular Action Text attribute
67
+ end
68
+ ```
69
+
70
+ ```erb
71
+ <%= form_with model: @post do |form| %>
72
+ <%= form.collaborative_rich_textarea :body %>
73
+ <% end %>
74
+ ```
75
+
76
+ Add your app's access check to `authorized?` in the generated channel,
77
+ then open the page in two browsers and edit together. The record must be persisted
78
+ (the document key derives from it). A record with an existing body works: the
79
+ first collaborative open seeds the document from it.
80
+
81
+ Encryption works the way Action Text's does:
82
+
83
+ ```ruby
84
+ has_collaborative_rich_text :body, encrypted: true
85
+ ```
86
+
87
+ The rendered body goes through `ActionText::EncryptedRichText`, and the
88
+ collaborative document (CRDT state and update payloads) is stored through
89
+ yrby's `Y::EncryptedDocument`. Both use Active Record encryption, so the
90
+ app must configure encryption keys. Without Action Text, declare
91
+ `encrypts` on the plain attribute yourself.
92
+
93
+ Use it for new attributes. Existing plaintext rows need migration
94
+ before you add `encrypted: true`: enable `support_unencrypted_data`,
95
+ rewrite each document, update, and rich-text row through its encrypted
96
+ class, then turn it back off. There is no built-in task for that yet.
97
+ And if your channel came from an earlier pre-release checkout, update it
98
+ to the current record-based storage first; a channel calling
99
+ `Y::Document` directly stores encrypted attributes as plaintext.
100
+
101
+ ## How the body stays current
102
+
103
+ The channel records each CRDT update, renders the full document with
104
+ `Y::Lexxy`, and saves the HTML through the Action Text writer. This
105
+ happens synchronously in `refresh_collaborative_rich_text`, so
106
+ reads use the stored `post.body` value.
107
+
108
+ If rendering fails, the update remains stored and the error is logged.
109
+ The next successful update renders the full document again. Until then,
110
+ `post.body` keeps its previous value.
111
+
112
+ ## Access control
113
+
114
+ The form helper gives clients a signed GlobalID scoped to one record and
115
+ field. `DocumentChannel` uses it to locate the record. Put the user
116
+ access check in `authorized?` (for example,
117
+ `record.editable_by?(current_user)`).
118
+
119
+ ## Configuration
120
+
121
+ ```ruby
122
+ LexxyRealtime.identity = ->(view) { { name: view.current_user.handle, color: nil } }
123
+ ```
124
+
125
+ By default, identity uses the first available `current_user` value from
126
+ `name`, `username`, or `handle`, then falls back to `"Anonymous"`.
127
+
128
+ Full documentation, the demo app, and the JavaScript package:
129
+ [repository README](https://github.com/jpcamara/lexxy-realtime#readme).