@atom.io/template-react-realtime-text-editor 0.0.10 → 0.0.12

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.
@@ -0,0 +1,81 @@
1
+ import { createServer } from "node:http"
2
+
3
+ import { Server } from "socket.io"
4
+
5
+ import { identityById } from "../src/identities.ts"
6
+ import {
7
+ createMarkdownDocumentService,
8
+ type MarkdownDocumentService,
9
+ } from "./service.ts"
10
+
11
+ export type MarkdownCollaborationHttpServer = {
12
+ close(): Promise<void>
13
+ readonly collaboration: MarkdownDocumentService
14
+ }
15
+
16
+ export async function createMarkdownCollaborationHttpServer(options: {
17
+ readonly collaboration?: MarkdownDocumentService
18
+ readonly port: number
19
+ }): Promise<MarkdownCollaborationHttpServer> {
20
+ const collaboration =
21
+ options.collaboration ?? (await createMarkdownDocumentService())
22
+ const httpServer = createServer((request, response) => {
23
+ if (request.url === `/health`) {
24
+ response.writeHead(200, { "content-type": `application/json` })
25
+ response.end(
26
+ JSON.stringify({ ok: true, revision: collaboration.revision }),
27
+ )
28
+ return
29
+ }
30
+ response.writeHead(404)
31
+ response.end()
32
+ })
33
+ const socketServer = new Server(httpServer)
34
+
35
+ socketServer.on(`connection`, (socket) => {
36
+ const requestedActor =
37
+ typeof socket.handshake.auth.actor === `string`
38
+ ? socket.handshake.auth.actor
39
+ : undefined
40
+ const session =
41
+ typeof socket.handshake.auth.session === `string`
42
+ ? socket.handshake.auth.session
43
+ : ``
44
+ const identity = identityById(requestedActor)
45
+ if (
46
+ identity.id !== requestedActor ||
47
+ session.length === 0 ||
48
+ session.includes(`::`)
49
+ ) {
50
+ socket.disconnect(true)
51
+ return
52
+ }
53
+ void collaboration.bindSocket({ actor: identity.id, session, socket })
54
+ })
55
+
56
+ await new Promise<void>((resolve, reject) => {
57
+ const onError = (error: Error): void => {
58
+ reject(error)
59
+ }
60
+ httpServer.once(`error`, onError)
61
+ httpServer.listen(options.port, `127.0.0.1`, () => {
62
+ httpServer.off(`error`, onError)
63
+ resolve()
64
+ })
65
+ })
66
+
67
+ let closed = false
68
+ return {
69
+ async close() {
70
+ if (closed) return
71
+ closed = true
72
+ await new Promise<void>((resolve) =>
73
+ socketServer.close(() => {
74
+ resolve()
75
+ }),
76
+ )
77
+ collaboration[Symbol.dispose]()
78
+ },
79
+ collaboration,
80
+ }
81
+ }
package/node/server.ts CHANGED
@@ -1,67 +1,19 @@
1
- import { createServer } from "node:http"
1
+ import { createMarkdownCollaborationHttpServer } from "./http-server.ts"
2
2
 
3
- import { Silo } from "atom.io"
4
- import type { UserKey } from "atom.io/realtime"
5
- import { createMosaicServer, realtime } from "atom.io/realtime-server"
6
- import { Server } from "socket.io"
7
-
8
- import { identityById } from "../src/identities.ts"
9
- import { markdownAtomRegistration } from "./mosaic-atom.ts"
10
-
11
- const PORT = 3000
12
- const mosaic = createMosaicServer({ registrations: [markdownAtomRegistration] })
13
- const silo = new Silo({
14
- name: `markdown-collaboration-server`,
15
- lifespan: `immortal`,
16
- isProduction: process.env.NODE_ENV === `production`,
17
- })
18
- const httpServer = createServer((request, response) => {
19
- if (request.url === `/health`) {
20
- response.writeHead(200, { "content-type": `application/json` })
21
- response.end(JSON.stringify({ ok: true }))
22
- return
23
- }
24
- response.writeHead(404)
25
- response.end()
26
- })
27
- const socketServer = new Server(httpServer)
28
-
29
- const disposeRealtime = realtime(
30
- socketServer,
31
- (handshake) => {
32
- const requestedId =
33
- typeof handshake.auth.userId === `string`
34
- ? handshake.auth.userId
35
- : undefined
36
- const sessionId =
37
- typeof handshake.auth.sessionId === `string`
38
- ? handshake.auth.sessionId
39
- : undefined
40
- if (!sessionId || sessionId.includes(`::`)) {
41
- return new Error(`A valid Mosaic session is required.`)
42
- }
43
- const identity = identityById(requestedId)
44
- return `user::${identity.id}::${sessionId}` satisfies UserKey
45
- },
46
- ({ socket, consumer }) => {
47
- const [, actor, session] = consumer.split(`::`)
48
- if (!actor || !session) {
49
- throw new Error(`Malformed Mosaic user session.`)
50
- }
51
- return mosaic.connect({ actor, session, socket })
52
- },
53
- silo.store,
3
+ const portArgument = process.argv.indexOf(`--port`)
4
+ const configuredPort =
5
+ portArgument === -1 ? process.env.PORT : process.argv[portArgument + 1]
6
+ const PORT = Number.parseInt(configuredPort ?? `3000`, 10)
7
+ if (!Number.isSafeInteger(PORT) || PORT < 1 || PORT > 65_535) {
8
+ throw new Error(`PORT must be an integer between 1 and 65535.`)
9
+ }
10
+ const server = await createMarkdownCollaborationHttpServer({ port: PORT })
11
+ console.log(
12
+ `Markdown collaboration server listening on http://localhost:${PORT}`,
54
13
  )
55
14
 
56
- httpServer.listen(PORT, () => {
57
- console.log(
58
- `Markdown collaboration server listening on http://localhost:${PORT}`,
59
- )
60
- })
61
-
62
15
  const shutdown = async (): Promise<void> => {
63
- await disposeRealtime()
64
- await mosaic.dispose()
16
+ await server.close()
65
17
  }
66
18
  process.once(`SIGINT`, () => void shutdown())
67
19
  process.once(`SIGTERM`, () => void shutdown())
@@ -0,0 +1,310 @@
1
+ import { Silo } from "atom.io"
2
+ import type {
3
+ MosaicAcceptedDomainBatchEnvelope,
4
+ MosaicTextIndexLookup,
5
+ MosaicTextIndexRange,
6
+ MosaicTextIndexSummary,
7
+ MosaicTextRelativePosition,
8
+ MosaicTextSnapshot,
9
+ } from "@atom.io/realtime"
10
+ import {
11
+ bindMosaicDomainHistoryServerSocket,
12
+ bindMosaicDomainPresenceServerSocket,
13
+ bindMosaicDomainResidencyServerSocket,
14
+ createMosaicDomainBatchServer,
15
+ createMosaicDomainHistoryCoordinator,
16
+ createMosaicDomainPresenceServer,
17
+ createMosaicDomainResidencyServer,
18
+ createMosaicTextDocumentCoordinator,
19
+ InMemoryMosaicDomainCheckpointStorage,
20
+ type MosaicDomainCheckpointStorageAdapter,
21
+ type MosaicDomainHistoryConnection,
22
+ type MosaicTextDocumentInstrumentation,
23
+ type MosaicTextDocumentRangeInspection,
24
+ } from "@atom.io/realtime/server"
25
+ import type { Socket } from "socket.io"
26
+ import { z } from "zod"
27
+
28
+ import {
29
+ activateMarkdownDocumentDomain,
30
+ MARKDOWN_INDEX_OPTIONS,
31
+ markdownIndexMemberModel,
32
+ markdownIndexRootModel,
33
+ markdownSourceAtom,
34
+ markdownSourceModel,
35
+ type MarkdownDocumentDomain,
36
+ type MarkdownText,
37
+ } from "../src/document-domain.ts"
38
+ import { INITIAL_MARKDOWN } from "../src/initial-markdown.ts"
39
+ import {
40
+ MARKDOWN_EVENTS,
41
+ type MarkdownAcknowledgement,
42
+ type MarkdownCommand,
43
+ } from "../src/protocol.ts"
44
+
45
+ export type MarkdownDocumentInstrumentation =
46
+ MosaicTextDocumentInstrumentation & {
47
+ readonly activeFullDocumentModels: 1
48
+ readonly maximumFullDocumentModels: 1
49
+ }
50
+
51
+ export type MarkdownDocumentRangeInspection = MosaicTextDocumentRangeInspection
52
+
53
+ export type MarkdownDocumentService = Disposable & {
54
+ bindSocket(options: {
55
+ readonly actor: string
56
+ readonly session: string
57
+ readonly socket: Socket
58
+ }): Promise<() => Promise<void>>
59
+ command(identity: {
60
+ readonly actor: string
61
+ readonly command: MarkdownCommand
62
+ readonly session: string
63
+ }): Promise<MosaicAcceptedDomainBatchEnvelope>
64
+ readonly domain: MarkdownDocumentDomain
65
+ history(identity: {
66
+ readonly actor: string
67
+ readonly session: string
68
+ }): MosaicDomainHistoryConnection
69
+ readonly instrumentation: MarkdownDocumentInstrumentation
70
+ readonly indexSummary: MosaicTextIndexSummary | null
71
+ inspectRange(
72
+ range: MosaicTextIndexRange,
73
+ ): Promise<MarkdownDocumentRangeInspection>
74
+ readonly length: number
75
+ materialize(): string
76
+ positionAtOffset(offset: number): Promise<MosaicTextIndexLookup>
77
+ resolvePosition(position: MosaicTextRelativePosition): Promise<number>
78
+ readonly revision: number
79
+ sourceSnapshot(): MosaicTextSnapshot
80
+ }
81
+
82
+ export async function createMarkdownDocumentService(
83
+ options: {
84
+ readonly authorizeImport?: (identity: {
85
+ readonly actor: string
86
+ readonly session: string
87
+ }) => boolean | Promise<boolean>
88
+ readonly initialText?: string
89
+ readonly presenceTtlMs?: number
90
+ readonly silo?: Silo
91
+ readonly storage?: MosaicDomainCheckpointStorageAdapter
92
+ } = {},
93
+ ): Promise<MarkdownDocumentService> {
94
+ let bootstrapText = options.initialText ?? INITIAL_MARKDOWN
95
+ const silo =
96
+ options.silo ??
97
+ new Silo({
98
+ isProduction: process.env.NODE_ENV === `production`,
99
+ lifespan: `ephemeral`,
100
+ name: `markdown-document-server`,
101
+ })
102
+ const domain = await activateMarkdownDocumentDomain({ silo })
103
+ const storage = options.storage ?? new InMemoryMosaicDomainCheckpointStorage()
104
+ const batches = createMosaicDomainBatchServer({
105
+ domain,
106
+ limits: {
107
+ maxBytes: 192 * 1024 * 1024,
108
+ maxMembers: 65_536,
109
+ maxOperations: 65_536,
110
+ },
111
+ storage,
112
+ })
113
+ const source = (): InstanceType<typeof MarkdownText> =>
114
+ silo.getState(markdownSourceAtom) as InstanceType<typeof MarkdownText>
115
+ const document = await createMosaicTextDocumentCoordinator({
116
+ authorizeImport:
117
+ options.authorizeImport ??
118
+ (({ actor, session }) => actor === `ada` || session === `bootstrap`),
119
+ batches,
120
+ domain,
121
+ index: {
122
+ memberAddress: (id) => domain.address(`indexMembers`, id),
123
+ memberModel: markdownIndexMemberModel,
124
+ options: MARKDOWN_INDEX_OPTIONS,
125
+ rootAddress: domain.address(`indexRoot`),
126
+ rootModel: markdownIndexRootModel,
127
+ },
128
+ serviceIdentity: { actor: `markdown-service`, session: `markdown-service` },
129
+ source: {
130
+ address: domain.address(`source`),
131
+ model: markdownSourceModel,
132
+ read: source,
133
+ },
134
+ })
135
+ const history = createMosaicDomainHistoryCoordinator({
136
+ batches,
137
+ completeCompensation: document.completeCompensation,
138
+ domain,
139
+ limits: { undoStepsPerActor: 100 },
140
+ storage,
141
+ })
142
+ const residency = createMosaicDomainResidencyServer({
143
+ batches,
144
+ domain,
145
+ maxResidentMembers: 128,
146
+ range: {
147
+ resolve: ({ limit, member, range }) =>
148
+ member === `indexMembers`
149
+ ? document.resolveRange(range, limit)
150
+ : Promise.resolve([]),
151
+ schema: z
152
+ .object({
153
+ end: z.number().int().nonnegative(),
154
+ kind: z.literal(`utf16-range`),
155
+ start: z.number().int().nonnegative(),
156
+ })
157
+ .strict()
158
+ .refine(({ end, start }) => end >= start),
159
+ },
160
+ })
161
+ const presence = createMosaicDomainPresenceServer({
162
+ domain,
163
+ ttlMs: options.presenceTtlMs,
164
+ })
165
+ const submitCommand = ({
166
+ actor,
167
+ command: input,
168
+ session,
169
+ }: {
170
+ readonly actor: string
171
+ readonly command: MarkdownCommand
172
+ readonly session: string
173
+ }): Promise<MosaicAcceptedDomainBatchEnvelope> =>
174
+ document.submit({
175
+ actor,
176
+ command:
177
+ input.type === `import`
178
+ ? input
179
+ : {
180
+ gestureId: input.gestureId,
181
+ selection: { anchor: input.anchor, head: input.head },
182
+ sequence: input.sequence,
183
+ text: input.text,
184
+ type: `replace`,
185
+ },
186
+ session,
187
+ })
188
+
189
+ if (bootstrapText.length > 0) {
190
+ await submitCommand({
191
+ actor: `system`,
192
+ command: {
193
+ gestureId: `bootstrap`,
194
+ sequence: 1,
195
+ text: bootstrapText,
196
+ type: `import`,
197
+ },
198
+ session: `bootstrap`,
199
+ })
200
+ await history.flush()
201
+ }
202
+ bootstrapText = ``
203
+
204
+ let disposed = false
205
+ const cleanups = new Set<() => Promise<void>>()
206
+ const acknowledge = <Value>(
207
+ work: Promise<Value>,
208
+ respond: (acknowledgement: MarkdownAcknowledgement<Value>) => void,
209
+ ): void => {
210
+ void work.then(
211
+ (value) => {
212
+ respond({ ok: true, value })
213
+ },
214
+ (error: unknown) => {
215
+ respond({
216
+ ok: false,
217
+ reason: error instanceof Error ? error.message : String(error),
218
+ })
219
+ },
220
+ )
221
+ }
222
+
223
+ return {
224
+ bindSocket({ actor, session, socket }) {
225
+ const unbindResidency = bindMosaicDomainResidencyServerSocket(
226
+ residency.connect({ actor, session }),
227
+ socket,
228
+ )
229
+ const unbindHistory = bindMosaicDomainHistoryServerSocket(
230
+ history.connect({ actor, session }),
231
+ socket,
232
+ { session },
233
+ )
234
+ const unbindPresence = bindMosaicDomainPresenceServerSocket(
235
+ presence.connect({ actor, session }),
236
+ socket,
237
+ )
238
+ const onCommand = (input: MarkdownCommand, respond: any): void => {
239
+ acknowledge(submitCommand({ actor, command: input, session }), respond)
240
+ }
241
+ const onMaterialize = (respond: any): void => {
242
+ acknowledge(Promise.resolve(document.materialize()), respond)
243
+ }
244
+ const onPosition = (offset: number, respond: any): void => {
245
+ acknowledge(document.positionAtOffset(offset), respond)
246
+ }
247
+ const onResolve = (
248
+ position: MosaicTextRelativePosition,
249
+ respond: any,
250
+ ): void => {
251
+ acknowledge(Promise.resolve(document.resolvePosition(position)), respond)
252
+ }
253
+ socket.on(MARKDOWN_EVENTS.command, onCommand)
254
+ socket.on(MARKDOWN_EVENTS.materialize, onMaterialize)
255
+ socket.on(MARKDOWN_EVENTS.positionAtOffset, onPosition)
256
+ socket.on(MARKDOWN_EVENTS.resolvePosition, onResolve)
257
+ let cleaned = false
258
+ const cleanup = async (): Promise<void> => {
259
+ if (cleaned) return
260
+ cleaned = true
261
+ socket.off(MARKDOWN_EVENTS.command, onCommand)
262
+ socket.off(MARKDOWN_EVENTS.materialize, onMaterialize)
263
+ socket.off(MARKDOWN_EVENTS.positionAtOffset, onPosition)
264
+ socket.off(MARKDOWN_EVENTS.resolvePosition, onResolve)
265
+ unbindResidency()
266
+ unbindHistory()
267
+ await unbindPresence()
268
+ cleanups.delete(cleanup)
269
+ }
270
+ cleanups.add(cleanup)
271
+ return Promise.resolve(cleanup)
272
+ },
273
+ command: submitCommand,
274
+ domain,
275
+ history: (identity) => history.connect(identity),
276
+ get instrumentation() {
277
+ return {
278
+ ...document.instrumentation,
279
+ activeFullDocumentModels: 1 as const,
280
+ maximumFullDocumentModels: 1 as const,
281
+ }
282
+ },
283
+ get indexSummary() {
284
+ return document.indexSummary
285
+ },
286
+ inspectRange: document.inspectRange,
287
+ get length() {
288
+ return document.length
289
+ },
290
+ materialize: document.materialize,
291
+ positionAtOffset: document.positionAtOffset,
292
+ resolvePosition: (position) =>
293
+ Promise.resolve(document.resolvePosition(position)),
294
+ get revision() {
295
+ return batches.revision
296
+ },
297
+ sourceSnapshot: () => source().toJSON(),
298
+ [Symbol.dispose]() {
299
+ if (disposed) return
300
+ disposed = true
301
+ for (const cleanup of cleanups) void cleanup().catch(() => undefined)
302
+ presence[Symbol.dispose]()
303
+ residency[Symbol.dispose]()
304
+ history[Symbol.dispose]()
305
+ document[Symbol.dispose]()
306
+ batches.dispose()
307
+ domain[Symbol.dispose]()
308
+ },
309
+ }
310
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atom.io/template-react-realtime-text-editor",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "A realtime text editor built with atom.io, React, and Lasertag.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,35 +10,43 @@
10
10
  "license": "Apache-2.0",
11
11
  "type": "module",
12
12
  "dependencies": {
13
- "atom.io": "0.53.2",
14
- "react": "19.3.0",
15
- "react-dom": "19.3.0",
16
- "socket.io": "4.8.3",
17
- "socket.io-client": "4.8.3",
18
- "zod": "4.6.5"
13
+ "@atom.io/react": "0.1.0",
14
+ "@atom.io/realtime": "0.1.0",
15
+ "@lexical/react": "0.52.0",
16
+ "@lexical/selection": "0.52.0",
17
+ "atom.io": "0.54.0",
18
+ "lexical": "0.52.0",
19
+ "react": "19.3.1",
20
+ "react-dom": "19.3.1",
21
+ "socket.io": "4.8.4",
22
+ "socket.io-client": "4.8.4",
23
+ "zod": "4.6.7"
19
24
  },
20
25
  "devDependencies": {
21
- "@dprint/json": "0.24.0",
22
- "@dprint/markdown": "0.24.0",
23
- "@dprint/oxc": "0.40.0",
26
+ "@atom.io/eslint-plugin": "0.1.0",
27
+ "@dprint/json": "0.25.2",
28
+ "@dprint/markdown": "0.28.0",
29
+ "@dprint/oxc": "0.47.0",
30
+ "@playwright/test": "1.64.1",
24
31
  "@testing-library/dom": "10.4.2",
25
32
  "@testing-library/react": "16.3.3",
26
- "@types/node": "26.6.1",
33
+ "@types/node": "26.6.5",
27
34
  "@types/react": "19.3.0",
28
35
  "@types/react-dom": "19.3.0",
29
- "@typescript-eslint/parser": "8.70.0",
30
- "@vitejs/plugin-react": "6.1.1",
31
- "concurrently": "10.0.5",
32
- "dprint": "0.57.4",
36
+ "@typescript-eslint/parser": "8.72.0",
37
+ "@typescript/native": "npm:typescript@7.0.2",
38
+ "@vitejs/plugin-react": "6.1.2",
39
+ "concurrently": "10.0.6",
40
+ "dprint": "0.64.1",
33
41
  "dprint-plugin-malva": "0.16.0",
34
- "eslint": "10.10.0",
35
- "happy-dom": "20.14.5",
36
- "lasertag": "0.8.2",
37
- "oxlint": "1.83.0",
38
- "oxlint-tsgolint": "7.0.2001",
39
- "typescript": "7.0.2",
40
- "vite": "8.3.0",
41
- "vitest": "5.0.1"
42
+ "eslint": "10.13.0",
43
+ "happy-dom": "21.0.4",
44
+ "lasertag": "0.8.9",
45
+ "oxlint": "1.89.0",
46
+ "oxlint-tsgolint": "7.0.2004",
47
+ "typescript": "npm:@typescript/typescript6@6.0.2",
48
+ "vite": "8.3.5",
49
+ "vitest": "5.0.5"
42
50
  },
43
51
  "engines": {
44
52
  "node": ">=24.12.0"
@@ -58,6 +66,7 @@
58
66
  "lint:types": "tsc -b",
59
67
  "preview": "concurrently \"vite preview\" \"node node/server.ts\"",
60
68
  "test": "vitest run",
69
+ "test:browser": "playwright test",
61
70
  "test:once": "vitest run"
62
71
  }
63
72
  }
@@ -0,0 +1,34 @@
1
+ import { defineConfig } from "@playwright/test"
2
+
3
+ const FRONTEND_PORT = 5_527
4
+ const executablePath = process.env.MOSAIC_PLAYWRIGHT_EXECUTABLE_PATH
5
+
6
+ export default defineConfig({
7
+ fullyParallel: false,
8
+ outputDir: `test-results/browser`,
9
+ reporter: process.env.CI ? [[`github`], [`list`]] : `list`,
10
+ retries: 0,
11
+ testDir: `tests/browser`,
12
+ timeout: 60_000,
13
+ use: {
14
+ baseURL: `http://127.0.0.1:${FRONTEND_PORT}`,
15
+ launchOptions:
16
+ executablePath === undefined
17
+ ? {}
18
+ : {
19
+ executablePath,
20
+ },
21
+ screenshot: `only-on-failure`,
22
+ trace: `retain-on-failure`,
23
+ video: `retain-on-failure`,
24
+ },
25
+ webServer: [
26
+ {
27
+ command: `pnpm exec vite --mode browser-conformance --host 127.0.0.1 --port ${FRONTEND_PORT} --strictPort`,
28
+ reuseExistingServer: false,
29
+ timeout: 30_000,
30
+ url: `http://127.0.0.1:${FRONTEND_PORT}`,
31
+ },
32
+ ],
33
+ workers: 1,
34
+ })
@@ -0,0 +1,88 @@
1
+ .LexicalMarkdownEditor_editor {
2
+ position: relative;
3
+ display: block;
4
+ min-height: 32rem;
5
+ }
6
+
7
+ .LexicalMarkdownEditor_contentEditable {
8
+ position: relative;
9
+ z-index: 1;
10
+ min-height: 32rem;
11
+ padding: 1.45rem 1.45rem 3rem 3.7rem;
12
+ outline: 0;
13
+ color: var(--editor-ink);
14
+ caret-color: var(--accent);
15
+ font-family: "DM Mono", monospace;
16
+ font-size: 0.83rem;
17
+ line-height: 1.72rem;
18
+ overflow-wrap: anywhere;
19
+ tab-size: 2;
20
+ white-space: pre-wrap;
21
+ }
22
+
23
+ .LexicalMarkdownEditor_paragraph {
24
+ margin: 0;
25
+ }
26
+
27
+ .LexicalMarkdownEditor_placeholder {
28
+ position: absolute;
29
+ top: 1.45rem;
30
+ left: 3.7rem;
31
+ display: block;
32
+ color: var(--editor-placeholder);
33
+ font-family: "DM Mono", monospace;
34
+ font-size: 0.83rem;
35
+ pointer-events: none;
36
+ }
37
+
38
+ .LexicalMarkdownEditor_overlays {
39
+ position: absolute;
40
+ z-index: 2;
41
+ inset: 0;
42
+ display: block;
43
+ pointer-events: none;
44
+ }
45
+
46
+ .LexicalMarkdownEditor_presence {
47
+ display: contents;
48
+ }
49
+
50
+ .LexicalMarkdownEditor_selection,
51
+ .LexicalMarkdownEditor_caret {
52
+ position: absolute;
53
+ display: block;
54
+ }
55
+
56
+ .LexicalMarkdownEditor_selection {
57
+ border-radius: 0.18rem;
58
+ background: color-mix(
59
+ in srgb,
60
+ var(--collaborator-color) 25%,
61
+ transparent
62
+ );
63
+ box-shadow: inset 0 -1px var(--collaborator-color);
64
+ }
65
+
66
+ .LexicalMarkdownEditor_caret {
67
+ width: 2px;
68
+ background: var(--collaborator-color);
69
+ box-shadow: 0 0 0 1px var(--overlay-outline);
70
+ }
71
+
72
+ .LexicalMarkdownEditor_label {
73
+ position: absolute;
74
+ top: -1.25rem;
75
+ left: -1px;
76
+ display: block;
77
+ max-width: 10rem;
78
+ padding: 0.16rem 0.38rem;
79
+ border-radius: 0.28rem 0.28rem 0.28rem 0;
80
+ color: white;
81
+ background: var(--collaborator-color);
82
+ font-family: system-ui, sans-serif;
83
+ font-size: 0.58rem;
84
+ font-weight: 750;
85
+ line-height: 1.05rem;
86
+ text-overflow: ellipsis;
87
+ white-space: nowrap;
88
+ }
@@ -0,0 +1,8 @@
1
+ lexical-markdown-editor.class {
2
+ display: block;
3
+ min-height: 32rem;
4
+
5
+ > mosaic-lexical-text-editor {
6
+ display: contents;
7
+ }
8
+ }