a proof of concept realtime collaborative text editor using atproto as a sync server
jake.tngl.io/y-pds/
9.9 kB
629 lines
1@import url("./reset.css");
2
3@import url("https://esm.sh/prosemirror-view/style/prosemirror.css");
4@import url("https://esm.sh/prosemirror-menu/style/menu.css");
5@import url("https://esm.sh/prosemirror-example-setup/style/style.css");
6@import url("https://esm.sh/prosemirror-gapcursor/style/gapcursor.css");
7
8html {
9 --font:
10 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
11 "Open Sans", "Helvetica Neue", sans-serif;
12 font-family: var(--font);
13}
14
15input,
16select,
17textarea {
18 flex: 1;
19 width: 100%;
20 padding: 0.4rem 0.5rem;
21 border: 1px solid #ccc;
22 border-radius: 4px;
23 font-size: 0.875rem;
24 background: #fff;
25 transition: border-color 0.15s;
26
27 &:focus {
28 outline-offset: 1px;
29 outline: 2px solid rgba(from blue r g b / 0.5);
30 }
31}
32
33actor-typeahead {
34 flex: 1;
35}
36
37button,
38.ProseMirror-menuitem .share-menu-button {
39 padding: 0.4rem 0.75rem;
40 border: 1px solid #ccc;
41 border-radius: 4px;
42 background: #fff;
43 font-size: 0.875rem;
44 font-family: var(--font);
45 cursor: pointer;
46 transition: background 0.15s;
47
48 &:hover {
49 background: #f5f5f5;
50 }
51
52 &:active {
53 background: #eee;
54 }
55}
56
57label {
58 display: block;
59 flex: 1;
60 width: 100%;
61 font-size: 0.75rem;
62}
63
64html,
65body {
66 height: 100%;
67}
68
69body {
70 display: flex;
71 flex-direction: column;
72}
73
74#app {
75 flex: 1;
76 display: flex;
77 flex-direction: column;
78}
79
80.login {
81 margin: auto;
82 text-align: center;
83 display: flex;
84 flex-direction: column;
85 gap: 16px;
86}
87
88.login-form {
89 display: flex;
90 flex-direction: column;
91 gap: 8px;
92 text-align: left;
93}
94
95.login-blurb {
96 font-size: 0.75rem;
97}
98
99.loading {
100 display: flex;
101 width: 100%;
102 height: 100%;
103 justify-content: center;
104 align-items: center;
105 opacity: 0;
106 animation: 1s fade-in 1s forwards;
107}
108
109@keyframes fade-in {
110 from {
111 opacity: 0;
112 }
113
114 to {
115 opacity: 1;
116 }
117}
118
119.editor-container {
120 flex: 1;
121 display: flex;
122 flex-direction: column;
123 overflow: hidden;
124 min-height: 0;
125}
126
127#editor {
128 flex: 1;
129 min-height: 0;
130 overflow-y: auto;
131 padding: 0;
132 font-family: ui-serif, Georgia, serif;
133 font-size: 1rem;
134 line-height: 1.6;
135}
136
137.ProseMirror {
138 min-height: 100%;
139 outline: none;
140 padding: 1rem;
141 margin-trim: block;
142
143 > :first-child,
144 > :first-child p {
145 margin-top: 0;
146 }
147
148 > :last-child,
149 > :last-child p {
150 margin-bottom: 0;
151 }
152}
153
154.ProseMirror-menubar-wrapper {
155 height: 100%;
156}
157
158.ProseMirror-menubar {
159 display: flex;
160 padding-right: 0;
161}
162
163.ProseMirror-menuitem {
164 align-self: center;
165}
166
167.ProseMirror-menuitem:has(.toolbar-right) {
168 margin-left: auto;
169 padding: 4px;
170}
171
172.toolbar-right {
173 display: flex;
174 align-items: center;
175 gap: 4px;
176}
177
178dialog {
179 overflow: visible;
180 place-self: center;
181 width: 100%;
182 max-width: 720px;
183 border: none;
184 border-radius: 10px;
185 box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
186
187 &::backdrop {
188 background: rgba(0, 0, 0, 0.25);
189 }
190
191 header {
192 display: flex;
193 justify-content: space-between;
194 align-items: center;
195 border-bottom: 1px solid #dddddd;
196 }
197
198 > * {
199 padding: 12px;
200 }
201}
202
203.icon-button {
204 all: unset;
205 cursor: pointer;
206 display: flex;
207 align-items: center;
208 justify-content: center;
209 padding: 0.25rem;
210 border-radius: 4px;
211 color: #888;
212
213 &:hover {
214 color: #333;
215 background: #f0f0f0;
216 }
217}
218
219#editors {
220 ul {
221 list-style: none;
222 }
223
224 &:empty {
225 display: none;
226 }
227
228 > li {
229 display: flex;
230 align-items: center;
231 gap: 0.5rem;
232 padding: 0.25rem 0;
233
234 .avatar {
235 width: 32px;
236 height: 32px;
237 border-radius: 50%;
238 }
239
240 .member-info {
241 flex: 1;
242 display: flex;
243 flex-direction: column;
244 min-width: 0;
245 overflow: hidden;
246
247 strong {
248 font-size: 0.875rem;
249 text-overflow: ellipsis;
250 overflow: hidden;
251 white-space: nowrap;
252 }
253
254 small {
255 font-size: 0.75rem;
256 color: #888;
257 text-overflow: ellipsis;
258 overflow: hidden;
259 white-space: nowrap;
260 }
261 }
262 }
263}
264
265.copy-link {
266 display: flex;
267 align-items: center;
268 background: #f5f5f5;
269 border-radius: 4px;
270
271 input {
272 all: unset;
273 flex: 1;
274 color: #666;
275 padding: 0.4rem 0.5rem;
276 min-width: 0;
277 overflow: hidden;
278 text-overflow: ellipsis;
279 white-space: nowrap;
280 pointer-events: none;
281 }
282
283 .icon-button {
284 flex-shrink: 0;
285 }
286}
287
288.share-footer {
289 display: flex;
290 flex-direction: column;
291 gap: 12px;
292}
293
294#add-member {
295 display: flex;
296 gap: 0.5rem;
297 align-items: flex-end;
298}
299
300.readonly-banner {
301 position: fixed;
302 bottom: 1rem;
303 left: 50%;
304 transform: translateX(-50%);
305 background: #f8f8f8;
306 border: 1px solid #ddd;
307 border-radius: 8px;
308 padding: 0.5rem 1rem;
309 font-size: 0.8rem;
310 color: #666;
311 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
312 z-index: 10;
313 white-space: nowrap;
314 animation: 0.3s fade-in;
315}
316
317.collab-cursor {
318 position: relative;
319 border-left: 2px solid var(--color);
320 margin-left: -1px;
321 pointer-events: none;
322}
323
324.collab-cursor::after {
325 content: attr(data-name);
326 position: absolute;
327 top: -1.4em;
328 left: -2px;
329 background: var(--color);
330 color: white;
331 font-size: 0.7rem;
332 font-family: sans-serif;
333 padding: 0 4px;
334 border-radius: 3px 3px 3px 0;
335 white-space: nowrap;
336 pointer-events: none;
337}
338
339/* ── Devtools panel ── */
340
341.devtools-resize-handle {
342 height: 6px;
343 margin-top: -3px;
344 margin-bottom: -3px;
345 position: relative;
346 z-index: 10;
347 cursor: ns-resize;
348 flex-shrink: 0;
349}
350
351.devtools-panel {
352 display: flex;
353 flex-direction: column;
354 border-top: 1px solid #ddd;
355 background: #fff;
356 font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas,
357 "DejaVu Sans Mono", monospace;
358 font-size: 12px;
359}
360
361.devtools-header {
362 display: flex;
363 align-items: center;
364 gap: 4px;
365 background: #f5f5f5;
366 border-bottom: 1px solid #e0e0e0;
367 height: 32px;
368 padding: 0 6px;
369 flex-shrink: 0;
370}
371
372.devtools-tabs {
373 display: flex;
374 height: 100%;
375 margin-right: 4px;
376
377 button {
378 all: unset;
379 padding: 0 10px;
380 font-size: 11px;
381 font-family: var(--font);
382 cursor: pointer;
383 color: #666;
384 border-bottom: 2px solid transparent;
385 height: 100%;
386
387 &:hover {
388 color: #333;
389 background: rgba(0, 0, 0, 0.05);
390 }
391
392 &.active {
393 color: #1a73e8;
394 border-bottom-color: #1a73e8;
395 }
396 }
397}
398
399.devtools-pause {
400 display: flex;
401 align-items: center;
402 gap: 4px;
403 font-size: 11px;
404 font-family: var(--font);
405 padding: 3px 6px !important;
406}
407
408.devtools-pause,
409.devtools-close {
410 padding: 4px !important;
411 color: #888;
412}
413
414.devtools-body {
415 flex: 1;
416 display: flex;
417 overflow: hidden;
418 min-height: 0;
419}
420
421.devtools-tab-content {
422 flex: 1;
423 display: flex;
424 flex-direction: column;
425 overflow: hidden;
426 min-height: 0;
427}
428
429.devtools-subtabs {
430 display: flex;
431 flex-shrink: 0;
432 border-bottom: 1px solid #e8e8e8;
433 padding: 0 4px;
434 overflow-x: auto;
435
436 button {
437 all: unset;
438 padding: 3px 10px;
439 font-size: 11px;
440 font-family: var(--font);
441 cursor: pointer;
442 color: #888;
443 border-bottom: 2px solid transparent;
444 white-space: nowrap;
445
446 &:hover { color: #333; }
447 &.active { color: #1a73e8; border-bottom-color: #1a73e8; }
448 }
449}
450
451.devtools-body-inner {
452 flex: 1;
453 display: flex;
454 overflow: hidden;
455 min-width: 0;
456 min-height: 0;
457
458 &:not(.has-detail) {
459 overflow-y: auto;
460 }
461
462 &.has-detail > .devtools-timeline {
463 width: 340px;
464 flex-shrink: 0;
465 border-right: 1px solid #e8e8e8;
466 }
467}
468
469.devtools-empty {
470 color: #999;
471 text-align: center;
472 padding: 24px;
473 font-family: var(--font);
474}
475
476/* Timeline tab */
477
478.devtools-timeline {
479 flex: 1;
480 overflow-y: auto;
481 padding: 6px;
482 display: flex;
483 flex-direction: column;
484 gap: 4px;
485}
486
487.devtools-entry {
488 border: 1px solid #eee;
489 border-radius: 5px;
490 padding: 7px 9px;
491 background: #fafafa;
492 cursor: pointer;
493
494 &:hover {
495 background: #f0f4ff;
496 border-color: #c8d8ff;
497 }
498
499 &.active {
500 background: #e8f0fe;
501 border-color: #4285f4;
502 }
503}
504
505.devtools-entry-meta {
506 display: flex;
507 align-items: baseline;
508 gap: 8px;
509 margin-bottom: 3px;
510}
511
512.devtools-entry-index {
513 font-size: 10px;
514 font-weight: 700;
515 color: #bbb;
516 min-width: 14px;
517}
518
519.devtools-entry-author {
520 font-weight: 600;
521 font-size: 11px;
522 font-family: var(--font);
523}
524
525.devtools-entry-time {
526 font-size: 10px;
527 color: #aaa;
528 margin-left: auto;
529}
530
531.devtools-entry-uri {
532 font-size: 10px;
533 color: #1a73e8;
534 margin-bottom: 5px;
535 overflow: hidden;
536 text-overflow: ellipsis;
537 white-space: nowrap;
538}
539
540.devtools-entry-json {
541 font-size: 10px;
542 color: #555;
543 margin: 0;
544 white-space: pre;
545 line-height: 1.5;
546 overflow: hidden;
547 max-height: 60px;
548}
549
550/* Detail panel */
551
552.devtools-detail {
553 flex: 1;
554 overflow-y: auto;
555 padding: 10px;
556 min-width: 0;
557}
558
559.devtools-detail-meta {
560 font-size: 11px;
561 color: #888;
562 margin-bottom: 10px;
563 font-family: var(--font);
564}
565
566.devtools-detail-label {
567 font-size: 10px;
568 font-weight: 600;
569 color: #aaa;
570 text-transform: uppercase;
571 letter-spacing: 0.05em;
572 margin: 10px 0 4px;
573}
574
575.devtools-ops {
576 width: 100%;
577 border-collapse: collapse;
578 font-size: 11px;
579
580 th {
581 text-align: left;
582 color: #aaa;
583 font-weight: 600;
584 font-size: 10px;
585 text-transform: uppercase;
586 letter-spacing: 0.04em;
587 padding: 2px 8px 4px 0;
588 border-bottom: 1px solid #eee;
589 }
590
591 td {
592 padding: 3px 8px 3px 0;
593 vertical-align: top;
594 border-bottom: 1px solid #f5f5f5;
595 }
596}
597
598.devtools-op-num {
599 color: #888;
600 white-space: nowrap;
601}
602
603.devtools-op-desc {
604 color: #333;
605 word-break: break-word;
606}
607
608/* Doc Record tab */
609
610.devtools-doc {
611 padding: 10px;
612 overflow-y: auto;
613 flex: 1;
614}
615
616.devtools-doc-uri {
617 font-size: 11px;
618 color: #1a73e8;
619 margin-bottom: 8px;
620 word-break: break-all;
621}
622
623.devtools-doc-json {
624 font-size: 12px;
625 color: #333;
626 margin: 0;
627 white-space: pre;
628 line-height: 1.5;
629}