a proof of concept realtime collaborative text editor using atproto as a sync server
jake.tngl.io/y-pds/
5.1 kB
322 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 {
120 flex: 1;
121 overflow-y: auto;
122 padding: 0;
123 font-family: ui-serif, Georgia, serif;
124 font-size: 1rem;
125 line-height: 1.6;
126}
127
128.ProseMirror {
129 min-height: 100%;
130 outline: none;
131 padding: 1rem;
132 margin-trim: block;
133
134 > :first-child,
135 > :first-child p {
136 margin-top: 0;
137 }
138
139 > :last-child,
140 > :last-child p {
141 margin-bottom: 0;
142 }
143}
144
145.ProseMirror-menubar-wrapper {
146 height: 100%;
147}
148
149.ProseMirror-menubar {
150 display: flex;
151 padding-right: 0;
152}
153
154.ProseMirror-menuitem {
155 align-self: center;
156}
157
158.ProseMirror-menuitem:has(.share-menu-button) {
159 margin-left: auto;
160 padding: 4px;
161}
162
163dialog {
164 overflow: visible;
165 place-self: center;
166 width: 100%;
167 max-width: 720px;
168 border: none;
169 border-radius: 10px;
170 box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
171
172 &::backdrop {
173 background: rgba(0, 0, 0, 0.25);
174 }
175
176 header {
177 display: flex;
178 justify-content: space-between;
179 align-items: center;
180 border-bottom: 1px solid #dddddd;
181 }
182
183 > * {
184 padding: 12px;
185 }
186}
187
188.icon-button {
189 all: unset;
190 cursor: pointer;
191 display: flex;
192 align-items: center;
193 justify-content: center;
194 padding: 0.25rem;
195 border-radius: 4px;
196 color: #888;
197
198 &:hover {
199 color: #333;
200 background: #f0f0f0;
201 }
202}
203
204#editors {
205 ul {
206 list-style: none;
207 }
208
209 &:empty {
210 display: none;
211 }
212
213 > li {
214 display: flex;
215 align-items: center;
216 gap: 0.5rem;
217 padding: 0.25rem 0;
218
219 .avatar {
220 width: 32px;
221 height: 32px;
222 border-radius: 50%;
223 }
224
225 .member-info {
226 flex: 1;
227 display: flex;
228 flex-direction: column;
229 min-width: 0;
230 overflow: hidden;
231
232 strong {
233 font-size: 0.875rem;
234 text-overflow: ellipsis;
235 overflow: hidden;
236 white-space: nowrap;
237 }
238
239 small {
240 font-size: 0.75rem;
241 color: #888;
242 text-overflow: ellipsis;
243 overflow: hidden;
244 white-space: nowrap;
245 }
246 }
247 }
248}
249
250.copy-link {
251 display: flex;
252 align-items: center;
253 background: #f5f5f5;
254 border-radius: 4px;
255
256 input {
257 all: unset;
258 flex: 1;
259 color: #666;
260 padding: 0.4rem 0.5rem;
261 min-width: 0;
262 overflow: hidden;
263 text-overflow: ellipsis;
264 white-space: nowrap;
265 pointer-events: none;
266 }
267
268 .icon-button {
269 flex-shrink: 0;
270 }
271}
272
273.share-footer {
274 display: flex;
275 flex-direction: column;
276 gap: 12px;
277}
278
279#add-member {
280 display: flex;
281 gap: 0.5rem;
282 align-items: flex-end;
283}
284
285.readonly-banner {
286 position: fixed;
287 bottom: 1rem;
288 left: 50%;
289 transform: translateX(-50%);
290 background: #f8f8f8;
291 border: 1px solid #ddd;
292 border-radius: 8px;
293 padding: 0.5rem 1rem;
294 font-size: 0.8rem;
295 color: #666;
296 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
297 z-index: 10;
298 white-space: nowrap;
299 animation: 0.3s fade-in;
300}
301
302.collab-cursor {
303 position: relative;
304 border-left: 2px solid var(--color);
305 margin-left: -1px;
306 pointer-events: none;
307}
308
309.collab-cursor::after {
310 content: attr(data-name);
311 position: absolute;
312 top: -1.4em;
313 left: -2px;
314 background: var(--color);
315 color: white;
316 font-size: 0.7rem;
317 font-family: sans-serif;
318 padding: 0 4px;
319 border-radius: 3px 3px 3px 0;
320 white-space: nowrap;
321 pointer-events: none;
322}