tangled
alpha
login
or
join now
evbogue.com
/
wiredove
1
fork
atom
a demonstration replicated social networking web app built with anproto
wiredove.net/
social
ed25519
protocols
1
fork
atom
overview
issues
pulls
pipelines
composer: add event mode UI behind flag
Everett Bogue
1 month ago
f0566119
611411ee
+580
-15
2 changed files
expand all
collapse all
unified
split
composer.js
style.css
+405
-11
composer.js
···
7
7
import { markdown } from './markdown.js'
8
8
import { imgUpload } from './upload.js'
9
9
10
10
+
const ENABLE_EVENT_COMPOSER = false
11
11
+
10
12
async function pushLocalNotification({ hash, author, text }) {
11
13
try {
12
14
await fetch('/push-now', {
···
80
82
}
81
83
82
84
const pubkey = await apds.pubkey()
85
85
+
let composerMode = 'message'
86
86
+
87
87
+
const eventDate = h('input', {type: 'date'})
88
88
+
const makeSelect = (placeholder, values) => {
89
89
+
const select = document.createElement('select')
90
90
+
const empty = document.createElement('option')
91
91
+
empty.value = ''
92
92
+
empty.textContent = placeholder
93
93
+
select.appendChild(empty)
94
94
+
for (const value of values) {
95
95
+
const option = document.createElement('option')
96
96
+
option.value = value
97
97
+
option.textContent = value
98
98
+
select.appendChild(option)
99
99
+
}
100
100
+
return select
101
101
+
}
102
102
+
const timeOptions = []
103
103
+
for (let minutes = 0; minutes < 24 * 60; minutes += 30) {
104
104
+
const hour24 = Math.floor(minutes / 60)
105
105
+
const minute = minutes % 60
106
106
+
const ampm = hour24 < 12 ? 'AM' : 'PM'
107
107
+
const hour12 = hour24 % 12 === 0 ? 12 : hour24 % 12
108
108
+
const label = `${hour12}:${String(minute).padStart(2, '0')}${ampm.toLowerCase()}`
109
109
+
timeOptions.push(label)
110
110
+
}
111
111
+
const eventStartTime = makeSelect('Start time', timeOptions)
112
112
+
const eventEndTime = makeSelect('End time', timeOptions)
113
113
+
const fallbackTimezones = [
114
114
+
'UTC',
115
115
+
'America/New_York',
116
116
+
'America/Chicago',
117
117
+
'America/Denver',
118
118
+
'America/Los_Angeles',
119
119
+
'Europe/London',
120
120
+
'Europe/Paris',
121
121
+
'Asia/Tokyo'
122
122
+
]
123
123
+
const timezoneOptions = (typeof Intl !== 'undefined' && Intl.supportedValuesOf)
124
124
+
? Intl.supportedValuesOf('timeZone')
125
125
+
: fallbackTimezones
126
126
+
const eventTimezone = makeSelect('Timezone (optional)', timezoneOptions)
127
127
+
const localTz = (typeof Intl !== 'undefined')
128
128
+
? Intl.DateTimeFormat().resolvedOptions().timeZone
129
129
+
: ''
130
130
+
if (localTz) { eventTimezone.value = localTz }
131
131
+
const eventLocation = h('input', {placeholder: 'Location'})
132
132
+
const eventLocationStatus = h('div', {classList: 'event-location-status'})
133
133
+
const locationResults = h('div', {classList: 'event-location-results'})
134
134
+
const locationWrapper = h('div', {classList: 'event-location-wrapper'}, [
135
135
+
eventLocation,
136
136
+
locationResults
137
137
+
])
138
138
+
let locationTimer
139
139
+
let locationController
140
140
+
let locationItems = []
141
141
+
let locationHighlight = -1
142
142
+
const setLocationStatus = (msg, isError = false) => {
143
143
+
eventLocationStatus.textContent = msg
144
144
+
eventLocationStatus.classList.toggle('error', isError)
145
145
+
}
146
146
+
const clearLocationResults = () => {
147
147
+
locationResults.innerHTML = ''
148
148
+
locationItems = []
149
149
+
locationHighlight = -1
150
150
+
locationResults.style.display = 'none'
151
151
+
}
152
152
+
const chooseLocation = (displayName, shortLabel) => {
153
153
+
eventLocation.value = shortLabel || displayName
154
154
+
eventLocation.dataset.full = displayName
155
155
+
clearLocationResults()
156
156
+
setLocationStatus('Location selected.')
157
157
+
}
158
158
+
const updateHighlight = () => {
159
159
+
const options = locationResults.querySelectorAll('.event-location-option')
160
160
+
options.forEach((option, index) => {
161
161
+
option.classList.toggle('active', index === locationHighlight)
162
162
+
})
163
163
+
}
164
164
+
const formatLocationLabel = (match) => {
165
165
+
const address = match.address || {}
166
166
+
const name = match.name || (match.namedetails && match.namedetails.name)
167
167
+
const road = address.road
168
168
+
const house = address.house_number
169
169
+
const city = address.city || address.town || address.village || address.hamlet
170
170
+
const region = address.state || address.region
171
171
+
const country = address.country
172
172
+
const street = house && road ? `${house} ${road}` : road
173
173
+
const labelParts = [name, street, city, region].filter(Boolean)
174
174
+
if (labelParts.length) { return labelParts.join(', ') }
175
175
+
const fallbackParts = [match.display_name, country].filter(Boolean)
176
176
+
return fallbackParts.join(', ') || 'Unknown'
177
177
+
}
178
178
+
const renderLocationResults = (data) => {
179
179
+
clearLocationResults()
180
180
+
locationItems = data
181
181
+
locationResults.style.display = 'flex'
182
182
+
for (let i = 0; i < data.length; i += 1) {
183
183
+
const match = data[i]
184
184
+
const label = formatLocationLabel(match)
185
185
+
const option = h('div', {
186
186
+
classList: 'event-location-option',
187
187
+
onmousedown: (e) => {
188
188
+
e.preventDefault()
189
189
+
chooseLocation(match.display_name, label)
190
190
+
}
191
191
+
}, [label])
192
192
+
locationResults.appendChild(option)
193
193
+
}
194
194
+
}
195
195
+
const updateLocationList = async () => {
196
196
+
const query = eventLocation.value.trim()
197
197
+
if (query.length < 3) {
198
198
+
clearLocationResults()
199
199
+
setLocationStatus('')
200
200
+
return
201
201
+
}
202
202
+
if (locationController) { locationController.abort() }
203
203
+
locationController = new AbortController()
204
204
+
setLocationStatus('Searching...')
205
205
+
try {
206
206
+
const url = `https://nominatim.openstreetmap.org/search?format=json&addressdetails=1&namedetails=1&limit=5&q=${encodeURIComponent(query)}`
207
207
+
const res = await fetch(url, { headers: { 'accept': 'application/json' }, signal: locationController.signal })
208
208
+
if (!res.ok) { throw new Error('Lookup failed') }
209
209
+
const data = await res.json()
210
210
+
if (!Array.isArray(data) || data.length === 0) {
211
211
+
clearLocationResults()
212
212
+
setLocationStatus('No results found.', true)
213
213
+
return
214
214
+
}
215
215
+
renderLocationResults(data)
216
216
+
setLocationStatus('')
217
217
+
} catch (err) {
218
218
+
if (err && err.name === 'AbortError') { return }
219
219
+
setLocationStatus('Could not fetch suggestions.', true)
220
220
+
}
221
221
+
}
222
222
+
eventLocation.addEventListener('input', () => {
223
223
+
clearTimeout(locationTimer)
224
224
+
eventLocation.dataset.full = ''
225
225
+
locationTimer = setTimeout(updateLocationList, 300)
226
226
+
})
227
227
+
eventLocation.addEventListener('keydown', (e) => {
228
228
+
if (!locationItems.length) { return }
229
229
+
if (e.key === 'ArrowDown') {
230
230
+
e.preventDefault()
231
231
+
locationHighlight = Math.min(locationHighlight + 1, locationItems.length - 1)
232
232
+
updateHighlight()
233
233
+
} else if (e.key === 'ArrowUp') {
234
234
+
e.preventDefault()
235
235
+
locationHighlight = Math.max(locationHighlight - 1, 0)
236
236
+
updateHighlight()
237
237
+
} else if (e.key === 'Enter') {
238
238
+
if (locationHighlight >= 0) {
239
239
+
e.preventDefault()
240
240
+
chooseLocation(locationItems[locationHighlight].display_name)
241
241
+
}
242
242
+
} else if (e.key === 'Escape') {
243
243
+
clearLocationResults()
244
244
+
}
245
245
+
})
246
246
+
eventLocation.addEventListener('blur', () => {
247
247
+
setTimeout(() => { clearLocationResults() }, 150)
248
248
+
})
249
249
+
const eventHelp = h('div', {style: 'display: none; color: #b00020; font-size: 12px; margin: 4px 0;'})
250
250
+
const eventFields = h('div', {style: 'display: none;'}, [
251
251
+
h('div', [eventDate]),
252
252
+
h('div', {classList: 'event-time-row'}, [eventStartTime, eventEndTime, eventTimezone]),
253
253
+
h('div', [locationWrapper]),
254
254
+
h('div', [eventLocationStatus]),
255
255
+
h('div', [eventHelp])
256
256
+
])
257
257
+
258
258
+
const parseTime = (label) => {
259
259
+
const match = label.match(/^(\d{1,2}):(\d{2})(am|pm)$/i)
260
260
+
if (!match) { return '' }
261
261
+
let hour24 = Number.parseInt(match[1], 10)
262
262
+
const minute = match[2]
263
263
+
const ampm = match[3].toLowerCase()
264
264
+
if (ampm === 'am') {
265
265
+
if (hour24 === 12) { hour24 = 0 }
266
266
+
} else if (ampm === 'pm') {
267
267
+
if (hour24 < 12) { hour24 += 12 }
268
268
+
}
269
269
+
return `${String(hour24).padStart(2, '0')}:${minute}`
270
270
+
}
271
271
+
272
272
+
const buildEventYaml = () => {
273
273
+
const meta = buildComposeMeta()
274
274
+
const lines = ['---']
275
275
+
if (meta.start) { lines.push(`start: ${meta.start}`) }
276
276
+
if (meta.end) { lines.push(`end: ${meta.end}`) }
277
277
+
if (meta.loc) { lines.push(`loc: ${meta.loc}`) }
278
278
+
if (meta.tz) { lines.push(`tz: ${meta.tz}`) }
279
279
+
lines.push('---')
280
280
+
lines.push(textarea.value)
281
281
+
return lines.join('\n')
282
282
+
}
283
283
+
284
284
+
const renderEventPreview = async (showRaw) => {
285
285
+
const dateValue = eventDate.value || 'Date not set'
286
286
+
const startLabel = eventStartTime.value || 'Start not set'
287
287
+
const endLabel = eventEndTime.value || 'End not set'
288
288
+
const loc = eventLocation.value.trim() || 'Location not set'
289
289
+
const tz = eventTimezone.value
290
290
+
content.innerHTML = ''
291
291
+
if (showRaw) {
292
292
+
content.textContent = buildEventYaml()
293
293
+
return
294
294
+
}
295
295
+
const bodyHtml = await markdown(textarea.value)
296
296
+
const timeLine = tz
297
297
+
? `${dateValue} • ${startLabel}–${endLabel} • ${tz}`
298
298
+
: `${dateValue} • ${startLabel}–${endLabel}`
299
299
+
const summary = h('div', {classList: 'event-preview'}, [
300
300
+
h('div', {classList: 'event-preview-meta'}, [timeLine]),
301
301
+
h('div', {classList: 'event-preview-meta'}, [loc])
302
302
+
])
303
303
+
const body = h('div')
304
304
+
body.innerHTML = bodyHtml
305
305
+
content.appendChild(summary)
306
306
+
content.appendChild(body)
307
307
+
}
308
308
+
309
309
+
const renderMessagePreview = async (showRaw) => {
310
310
+
if (showRaw) {
311
311
+
content.textContent = textarea.value
312
312
+
return
313
313
+
}
314
314
+
content.innerHTML = await markdown(textarea.value)
315
315
+
}
316
316
+
317
317
+
let messageToggle
318
318
+
let eventToggle
319
319
+
let modeToggle
320
320
+
const updateToggleState = () => {
321
321
+
if (!ENABLE_EVENT_COMPOSER) { return }
322
322
+
if (!messageToggle || !eventToggle) { return }
323
323
+
const isEvent = composerMode === 'event'
324
324
+
messageToggle.classList.toggle('active', !isEvent)
325
325
+
eventToggle.classList.toggle('active', isEvent)
326
326
+
messageToggle.setAttribute('aria-pressed', String(!isEvent))
327
327
+
eventToggle.setAttribute('aria-pressed', String(isEvent))
328
328
+
if (modeToggle) {
329
329
+
modeToggle.classList.toggle('event-active', isEvent)
330
330
+
}
331
331
+
}
332
332
+
333
333
+
const setComposerMode = (mode) => {
334
334
+
if (!ENABLE_EVENT_COMPOSER) {
335
335
+
composerMode = 'message'
336
336
+
return
337
337
+
}
338
338
+
composerMode = mode
339
339
+
if (mode === 'event') {
340
340
+
eventFields.style = 'display: block;'
341
341
+
textarea.placeholder = 'Write event details'
342
342
+
} else {
343
343
+
eventFields.style = 'display: none;'
344
344
+
textarea.placeholder = 'Write a message'
345
345
+
}
346
346
+
updateToggleState()
347
347
+
}
348
348
+
349
349
+
const buildComposeMeta = () => {
350
350
+
if (!ENABLE_EVENT_COMPOSER || composerMode !== 'event') { return { ...replyObj } }
351
351
+
const meta = { ...replyObj }
352
352
+
const loc = (eventLocation.dataset.full || eventLocation.value).trim()
353
353
+
const dateValue = eventDate.value
354
354
+
const startLabel = eventStartTime.value
355
355
+
const endLabel = eventEndTime.value
356
356
+
const tz = eventTimezone.value
357
357
+
const startValue = startLabel ? parseTime(startLabel) : ''
358
358
+
const endValue = endLabel ? parseTime(endLabel) : ''
359
359
+
const start = (dateValue && startValue)
360
360
+
? Math.floor(new Date(`${dateValue}T${startValue}`).getTime() / 1000)
361
361
+
: null
362
362
+
const end = (dateValue && endValue)
363
363
+
? Math.floor(new Date(`${dateValue}T${endValue}`).getTime() / 1000)
364
364
+
: null
365
365
+
if (Number.isFinite(start)) { meta.start = start }
366
366
+
if (Number.isFinite(end)) { meta.end = end }
367
367
+
if (loc) { meta.loc = loc }
368
368
+
if (tz) { meta.tz = tz }
369
369
+
return meta
370
370
+
}
83
371
84
372
const publishButton = h('button', {style: 'float: right;', onclick: async (e) => {
85
85
-
e.target.disabled = true
86
86
-
e.target.textContent = 'Publishing...'
87
87
-
const published = await apds.compose(textarea.value, replyObj)
373
373
+
const button = e.target
374
374
+
button.disabled = true
375
375
+
button.textContent = 'Publishing...'
376
376
+
if (ENABLE_EVENT_COMPOSER && composerMode === 'event') {
377
377
+
const dateValue = eventDate.value
378
378
+
const startLabel = eventStartTime.value
379
379
+
const endLabel = eventEndTime.value
380
380
+
const startValue = startLabel ? parseTime(startLabel) : ''
381
381
+
const endValue = endLabel ? parseTime(endLabel) : ''
382
382
+
const loc = (eventLocation.dataset.full || eventLocation.value).trim()
383
383
+
const startMs = (dateValue && startValue)
384
384
+
? new Date(`${dateValue}T${startValue}`).getTime()
385
385
+
: NaN
386
386
+
const endMs = (dateValue && endValue)
387
387
+
? new Date(`${dateValue}T${endValue}`).getTime()
388
388
+
: NaN
389
389
+
const setHelp = (msg, focusEl) => {
390
390
+
eventHelp.textContent = msg
391
391
+
eventHelp.style = 'display: block; color: #b00020; font-size: 12px; margin: 4px 0;'
392
392
+
if (focusEl) { focusEl.focus() }
393
393
+
}
394
394
+
eventHelp.style = 'display: none;'
395
395
+
if (!loc) {
396
396
+
setHelp('Location is required.', eventLocation)
397
397
+
button.disabled = false
398
398
+
button.textContent = 'Publish'
399
399
+
return
400
400
+
}
401
401
+
if (!dateValue) {
402
402
+
setHelp('Event date is required.', eventDate)
403
403
+
button.disabled = false
404
404
+
button.textContent = 'Publish'
405
405
+
return
406
406
+
}
407
407
+
if (!startValue) {
408
408
+
setHelp('Start time is required.', eventStartTime)
409
409
+
button.disabled = false
410
410
+
button.textContent = 'Publish'
411
411
+
return
412
412
+
}
413
413
+
if (!endValue) {
414
414
+
setHelp('End time is required.', eventEndTime)
415
415
+
button.disabled = false
416
416
+
button.textContent = 'Publish'
417
417
+
return
418
418
+
}
419
419
+
if (endMs < startMs) {
420
420
+
setHelp('End time must be after the start time.', eventEndTime)
421
421
+
button.disabled = false
422
422
+
button.textContent = 'Publish'
423
423
+
return
424
424
+
}
425
425
+
}
426
426
+
const published = await apds.compose(textarea.value, buildComposeMeta())
88
427
textarea.value = ''
89
428
const signed = await apds.get(published)
90
429
const opened = await apds.open(signed)
···
142
481
}
143
482
}}, ['Publish'])
144
483
484
484
+
if (ENABLE_EVENT_COMPOSER) {
485
485
+
messageToggle = h('button', {type: 'button', onclick: () => setComposerMode('message')}, ['Message'])
486
486
+
eventToggle = h('button', {type: 'button', onclick: () => setComposerMode('event')}, ['Event'])
487
487
+
const toggleIndicator = h('span', {classList: 'composer-toggle-indicator'})
488
488
+
modeToggle = h('div', {classList: 'composer-toggle'}, [
489
489
+
toggleIndicator,
490
490
+
messageToggle,
491
491
+
eventToggle
492
492
+
])
493
493
+
updateToggleState()
494
494
+
}
495
495
+
496
496
+
const rawDiv = h('div', {classList: 'message-raw'})
497
497
+
let rawshow = true
498
498
+
let rawContent
499
499
+
let rawText = ''
500
500
+
const updateRawText = () => {
501
501
+
rawText = (ENABLE_EVENT_COMPOSER && composerMode === 'event') ? buildEventYaml() : textarea.value
502
502
+
if (rawContent) { rawContent.textContent = rawText }
503
503
+
}
504
504
+
const rawToggle = h('a', {classList: 'material-symbols-outlined', onclick: () => {
505
505
+
updateRawText()
506
506
+
if (rawshow) {
507
507
+
if (!rawContent) {
508
508
+
rawContent = h('pre', {classList: 'hljs'}, [rawText])
509
509
+
}
510
510
+
rawDiv.appendChild(rawContent)
511
511
+
rawshow = false
512
512
+
} else {
513
513
+
rawContent.parentNode.removeChild(rawContent)
514
514
+
rawshow = true
515
515
+
}
516
516
+
}}, ['Code'])
517
517
+
518
518
+
const renderPreview = async () => {
519
519
+
updateRawText()
520
520
+
if (ENABLE_EVENT_COMPOSER && composerMode === 'event') {
521
521
+
await renderEventPreview(false)
522
522
+
} else {
523
523
+
await renderMessagePreview(false)
524
524
+
}
525
525
+
}
526
526
+
145
527
const previewButton = h('button', {style: 'float: right;', onclick: async () => {
146
528
textareaDiv.style = 'display: none;'
147
529
previewDiv.style = 'display: block;'
148
148
-
content.innerHTML = await markdown(textarea.value)
530
530
+
await renderPreview()
149
531
}}, ['Preview'])
150
532
151
151
-
const textareaDiv = h('div', {classList: 'composer'}, [
152
152
-
textarea,
153
153
-
previewButton
154
154
-
])
533
533
+
const textareaDiv = h('div', {classList: 'composer'}, ENABLE_EVENT_COMPOSER
534
534
+
? [modeToggle, eventFields, textarea, previewButton]
535
535
+
: [textarea, previewButton]
536
536
+
)
155
537
156
538
const content = h('div')
157
539
158
158
-
const previewDiv = h('div', {style: 'display: none;'}, [
159
159
-
content,
540
540
+
const previewControls = h('div', {classList: 'preview-controls'}, [
160
541
publishButton,
161
542
h('button', {style: 'float: right;', onclick: () => {
162
543
textareaDiv.style = 'display: block;'
···
164
545
}}, ['Cancel'])
165
546
])
166
547
548
548
+
const previewDiv = h('div', {style: 'display: none;'}, [
549
549
+
content,
550
550
+
rawDiv,
551
551
+
previewControls
552
552
+
])
553
553
+
167
554
const meta = h('span', {classList: 'message-meta'}, [
168
555
h('span', {classList: 'pubkey'}, [pubkey.substring(0, 6)]),
556
556
+
' ',
557
557
+
rawToggle,
169
558
' ',
170
559
cancel,
171
560
])
···
177
566
await imgUpload(textarea)
178
567
])
179
568
569
569
+
const composerHeader = h('div', {classList: 'composer-header'}, ENABLE_EVENT_COMPOSER
570
570
+
? [await nameSpan(), modeToggle]
571
571
+
: [await nameSpan()]
572
572
+
)
573
573
+
180
574
const composerDiv = h('div', [
181
575
meta,
182
576
h('div', {classList: 'message-main'}, [
183
577
h('span', [await avatarSpan()]),
184
578
h('div', {classList: 'message-stack'}, [
185
185
-
await nameSpan(),
579
579
+
composerHeader,
186
580
bodyWrap
187
581
])
188
582
])
+175
-4
style.css
···
116
116
cursor: pointer;
117
117
}
118
118
119
119
-
textarea, input {
119
119
+
.composer-toggle {
120
120
+
display: inline-flex;
121
121
+
gap: 2px;
122
122
+
margin-bottom: 6px;
123
123
+
background: #f0f0f0;
124
124
+
border: 1px solid #e4e4e4;
125
125
+
border-radius: 999px;
126
126
+
padding: 2px;
127
127
+
position: relative;
128
128
+
overflow: hidden;
129
129
+
}
130
130
+
131
131
+
.composer-toggle button {
132
132
+
border-radius: 999px;
133
133
+
padding: 4px 10px;
134
134
+
border: none;
135
135
+
background: transparent;
136
136
+
position: relative;
137
137
+
z-index: 1;
138
138
+
}
139
139
+
140
140
+
.composer-toggle button.active {
141
141
+
font-weight: 600;
142
142
+
}
143
143
+
144
144
+
.composer-toggle-indicator {
145
145
+
position: absolute;
146
146
+
top: 2px;
147
147
+
bottom: 2px;
148
148
+
left: 2px;
149
149
+
width: calc(50% - 2px);
150
150
+
border-radius: 999px;
151
151
+
background: #fff;
152
152
+
border: 1px solid #e4e4e4;
153
153
+
transition: transform 180ms ease;
154
154
+
pointer-events: none;
155
155
+
}
156
156
+
157
157
+
.composer-toggle.event-active .composer-toggle-indicator {
158
158
+
transform: translateX(100%);
159
159
+
}
160
160
+
161
161
+
textarea, input, select {
120
162
font-size: 1em;
121
163
font-family: "Source Sans 3", sans-serif;
122
164
border: 1px solid #f8f8f8;
···
129
171
130
172
.composer { margin-left: 0; margin-top: 0;}
131
173
132
132
-
textarea:hover, textarea:focus, input:hover, input:focus, {
174
174
+
textarea:hover, textarea:focus, input:hover, input:focus, select:hover, select:focus, {
133
175
background: transparent;
134
176
}
135
177
136
136
-
textarea:focus, input:focus {
178
178
+
textarea:focus, input:focus, select:focus {
137
179
outline: none !important;
138
180
}
139
181
···
144
186
height: 150px;
145
187
}
146
188
189
189
+
.composer-header {
190
190
+
display: flex;
191
191
+
align-items: center;
192
192
+
gap: 8px;
193
193
+
flex-wrap: wrap;
194
194
+
}
195
195
+
196
196
+
.event-preview {
197
197
+
border: 1px solid #e4e4e4;
198
198
+
border-radius: 6px;
199
199
+
padding: 8px 10px;
200
200
+
margin-bottom: 8px;
201
201
+
background: #fafafa;
202
202
+
}
203
203
+
204
204
+
.event-preview-title {
205
205
+
font-weight: 600;
206
206
+
margin-bottom: 2px;
207
207
+
}
208
208
+
209
209
+
.event-preview-meta {
210
210
+
color: #777;
211
211
+
font-size: 0.95em;
212
212
+
}
213
213
+
214
214
+
.event-location-wrapper {
215
215
+
position: relative;
216
216
+
}
217
217
+
218
218
+
.event-location-wrapper input {
219
219
+
width: 100%;
220
220
+
}
221
221
+
222
222
+
.event-time-row {
223
223
+
display: flex;
224
224
+
gap: 6px;
225
225
+
flex-wrap: wrap;
226
226
+
}
227
227
+
228
228
+
.event-time-row select {
229
229
+
min-width: 120px;
230
230
+
}
231
231
+
232
232
+
.event-location-results {
233
233
+
position: absolute;
234
234
+
z-index: 5;
235
235
+
top: calc(100% + 4px);
236
236
+
left: 0;
237
237
+
right: 0;
238
238
+
display: none;
239
239
+
flex-direction: column;
240
240
+
gap: 2px;
241
241
+
padding: 4px;
242
242
+
border: 1px solid #e4e4e4;
243
243
+
background: #fff;
244
244
+
border-radius: 6px;
245
245
+
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
246
246
+
}
247
247
+
248
248
+
.event-location-option {
249
249
+
text-align: left;
250
250
+
padding: 6px 8px;
251
251
+
border-radius: 4px;
252
252
+
cursor: pointer;
253
253
+
}
254
254
+
255
255
+
.event-location-option:hover,
256
256
+
.event-location-option.active {
257
257
+
background: #f2f2f2;
258
258
+
}
259
259
+
260
260
+
.event-location-status {
261
261
+
font-size: 0.9em;
262
262
+
color: #2a6a2a;
263
263
+
}
264
264
+
265
265
+
.event-location-status.error {
266
266
+
color: #b00020;
267
267
+
}
268
268
+
269
269
+
.preview-controls {
270
270
+
display: flex;
271
271
+
align-items: center;
272
272
+
gap: 8px;
273
273
+
margin-top: 8px;
274
274
+
}
275
275
+
147
276
a {
148
277
color: #045fd0;
149
278
text-decoration: none;
···
305
434
.message:hover { border: 1px solid #333;}
306
435
.new-posts-button { background: #3a2f12; border: 1px solid #6b5320; color: #f5d27a; }
307
436
308
308
-
textarea, input, iframe { background: #222; color: #f5f5f5; border: 1px solid #222;}
437
437
+
textarea, input, select, iframe { background: #222; color: #f5f5f5; border: 1px solid #222;}
309
438
310
439
button { color: #ccc; background: #333; border: 1px solid #444;}
311
440
button:hover { background: #222;}
312
441
hr { border: 1px solid #333;}
313
442
pre, code { background: #333; color: #f5f5f5;}
314
443
a {color: #50afe4;}
444
444
+
445
445
+
.composer-toggle {
446
446
+
background: #2a2a2a;
447
447
+
border-color: #333;
448
448
+
}
449
449
+
450
450
+
.composer-toggle-indicator {
451
451
+
background: #3a3a3a;
452
452
+
border-color: #4a4a4a;
453
453
+
}
454
454
+
455
455
+
.event-preview {
456
456
+
background: #1f1f1f;
457
457
+
border-color: #2a2a2a;
458
458
+
}
459
459
+
460
460
+
.event-preview-meta {
461
461
+
color: #a5a5a5;
462
462
+
}
463
463
+
464
464
+
.event-location-status {
465
465
+
color: #7bc27b;
466
466
+
}
467
467
+
468
468
+
.event-location-status.error {
469
469
+
color: #f28b82;
470
470
+
}
471
471
+
472
472
+
.event-location-results {
473
473
+
background: #1f1f1f;
474
474
+
border-color: #333;
475
475
+
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
476
476
+
}
477
477
+
478
478
+
.event-location-option {
479
479
+
color: #f5f5f5;
480
480
+
}
481
481
+
482
482
+
.event-location-option:hover,
483
483
+
.event-location-option.active {
484
484
+
background: #2b2b2b;
485
485
+
}
315
486
316
487
}
317
488