create-foldkit-app 0.38.0 → 0.39.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.
- package/dist/templates/base/FOLDKIT.md +3 -1
- package/dist/templates/examples/canvas-art/src/main.ts +1 -1
- package/dist/templates/examples/charting/src/chartHost.ts +3 -4
- package/dist/templates/examples/charting/src/entry.ts +0 -2
- package/dist/templates/examples/charting/src/view/chart.ts +101 -36
- package/dist/templates/examples/embedding/src/main.ts +3 -3
- package/dist/templates/examples/generative-art/src/subscription.ts +1 -1
- package/dist/templates/examples/job-application/src/step/education/education.ts +14 -16
- package/dist/templates/examples/job-application/src/step/skills/skills.ts +14 -16
- package/dist/templates/examples/job-application/src/step/workHistory/workHistory.ts +14 -16
- package/dist/templates/examples/map/src/entry.ts +1 -2
- package/dist/templates/examples/map/src/main.ts +123 -145
- package/dist/templates/examples/map/src/mapHost.ts +10 -12
- package/dist/templates/examples/map/src/mount.test.ts +93 -68
- package/dist/templates/examples/pixel-art/src/subscription.ts +5 -5
- package/dist/templates/examples/routing/src/main.ts +3 -3
- package/dist/templates/examples/slow-warnings/src/main.ts +4 -4
- package/dist/templates/examples/snake/src/main.ts +4 -3
- package/dist/templates/examples/ssg/src/entry.server.ts +2 -0
- package/dist/templates/examples/ssg/src/entry.ts +1 -0
- package/dist/templates/examples/ssr/package.json +1 -1
- package/dist/templates/examples/ssr/src/entry.server.ts +2 -0
- package/dist/templates/examples/ssr/src/entry.ts +1 -0
- package/dist/templates/examples/stopwatch/src/main.ts +1 -0
- package/dist/templates/examples/ui-showcase/src/main.ts +49 -37
- package/dist/templates/examples/ui-showcase/src/ui/subscriptions.ts +1 -18
- package/dist/templates/examples/ui-showcase/src/ui/update.ts +2 -12
- package/dist/templates/examples/ui-showcase/src/ui/view/calendar.ts +9 -7
- package/dist/templates/examples/ui-showcase/src/ui/view/checkbox.ts +80 -65
- package/dist/templates/examples/ui-showcase/src/ui/view/dialog.ts +102 -123
- package/dist/templates/examples/ui-showcase/src/ui/view/disclosure.ts +128 -120
- package/dist/templates/examples/ui-showcase/src/ui/view/fieldset.ts +173 -144
- package/dist/templates/examples/ui-showcase/src/ui/view/fileDrop.ts +17 -15
- package/dist/templates/examples/ui-showcase/src/ui/view/hoverIntent.ts +64 -56
- package/dist/templates/examples/ui-showcase/src/ui/view/input.ts +32 -33
- package/dist/templates/examples/ui-showcase/src/ui/view/meter.ts +82 -73
- package/dist/templates/examples/ui-showcase/src/ui/view/popover.ts +100 -81
- package/dist/templates/examples/ui-showcase/src/ui/view/progress.ts +83 -68
- package/dist/templates/examples/ui-showcase/src/ui/view/radioGroup.ts +79 -85
- package/dist/templates/examples/ui-showcase/src/ui/view/select.ts +63 -67
- package/dist/templates/examples/ui-showcase/src/ui/view/slider.ts +72 -72
- package/dist/templates/examples/ui-showcase/src/ui/view/switch.ts +25 -26
- package/dist/templates/examples/ui-showcase/src/ui/view/tabs.ts +58 -46
- package/dist/templates/examples/ui-showcase/src/ui/view/textarea.ts +32 -39
- package/dist/templates/examples/ui-showcase/src/ui/view/tooltip.ts +48 -36
- package/dist/templates/examples/websocket-chat/src/main.ts +44 -18
- package/dist/templates/examples/websocket-chat/src/story.test.ts +33 -6
- package/dist/templates/release.json +12 -11
- package/dist/templates/rendering/ssg/src/entry.server.ts +2 -0
- package/dist/templates/rendering/ssg/src/entry.ts +1 -0
- package/dist/templates/rendering/ssg/vite.config.ts +1 -0
- package/dist/templates/rendering/ssr/scripts/serve.ts +4 -145
- package/dist/templates/rendering/ssr/src/entry.server.ts +2 -0
- package/dist/templates/rendering/ssr/src/entry.ts +1 -0
- package/dist/templates/rendering/ssr/vite.config.ts +1 -0
- package/dist/utils/files.js +2 -0
- package/package.json +1 -1
- package/dist/templates/examples/charting/src/subscription.ts +0 -70
|
@@ -3,14 +3,13 @@ import {
|
|
|
3
3
|
Array,
|
|
4
4
|
Effect,
|
|
5
5
|
Equal,
|
|
6
|
-
Function,
|
|
7
6
|
Option,
|
|
8
7
|
Queue,
|
|
9
8
|
Schema,
|
|
10
9
|
Stream,
|
|
11
10
|
String,
|
|
12
11
|
} from 'effect'
|
|
13
|
-
import { Command, Mount, Runtime,
|
|
12
|
+
import { Command, Mount, Runtime, Update } from 'foldkit'
|
|
14
13
|
import * as Dom from 'foldkit/dom'
|
|
15
14
|
import type { Document, Html } from 'foldkit/html'
|
|
16
15
|
import { HtmlBuilder } from 'foldkit/html'
|
|
@@ -89,6 +88,12 @@ export const Message = defineMessageUnion({
|
|
|
89
88
|
|
|
90
89
|
export type Message = typeof Message.Type
|
|
91
90
|
|
|
91
|
+
type MountMapMessage =
|
|
92
|
+
| typeof Message.SucceededMountMap.Type
|
|
93
|
+
| typeof Message.FailedMountMap.Type
|
|
94
|
+
| typeof Message.MovedMap.Type
|
|
95
|
+
| typeof Message.ClickedMarker.Type
|
|
96
|
+
|
|
92
97
|
// COMMAND
|
|
93
98
|
|
|
94
99
|
const flyToMap = (
|
|
@@ -310,101 +315,140 @@ export const init: Runtime.ApplicationInit<Model, Message> = () => ({
|
|
|
310
315
|
|
|
311
316
|
// MAP MOUNT
|
|
312
317
|
|
|
313
|
-
|
|
314
|
-
map:
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
markerElement.disabled = !isLive
|
|
333
|
-
})
|
|
318
|
+
const toMapMountError = (error: unknown): Error =>
|
|
319
|
+
error instanceof Error ? error : new Error(`Failed to mount map: ${error}`)
|
|
320
|
+
|
|
321
|
+
const addLocationMarkers = (
|
|
322
|
+
map: MapInstance,
|
|
323
|
+
maplibre: typeof import('maplibre-gl'),
|
|
324
|
+
) =>
|
|
325
|
+
Effect.try({
|
|
326
|
+
try: () =>
|
|
327
|
+
Array.forEach(featuredLocations, ({ id, lng, lat }) => {
|
|
328
|
+
const markerElement = document.createElement('button')
|
|
329
|
+
markerElement.setAttribute('data-location-id', id)
|
|
330
|
+
markerElement.setAttribute('aria-label', `Marker: ${id}`)
|
|
331
|
+
markerElement.className = markerStyle
|
|
332
|
+
new maplibre.Marker({ element: markerElement })
|
|
333
|
+
.setLngLat([lng, lat])
|
|
334
|
+
.addTo(map)
|
|
335
|
+
}),
|
|
336
|
+
catch: toMapMountError,
|
|
334
337
|
})
|
|
335
338
|
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
viewStateChanges: Stream.Stream<Mount.ViewState>,
|
|
339
|
+
const listenToMapMovesAndMarkerClicks = (
|
|
340
|
+
map: MapInstance,
|
|
341
|
+
queue: Queue.Enqueue<MountMapMessage>,
|
|
340
342
|
) =>
|
|
341
|
-
Effect.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
343
|
+
Effect.acquireRelease(
|
|
344
|
+
Effect.try({
|
|
345
|
+
try: () => {
|
|
346
|
+
const container = map.getContainer()
|
|
347
|
+
const onMoveEnd = () => {
|
|
348
|
+
Queue.offerUnsafe(
|
|
349
|
+
queue,
|
|
350
|
+
Message.MovedMap({ bounds: boundsFromMap(map) }),
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
const onContainerClick = (event: MouseEvent) => {
|
|
354
|
+
const target = event.target
|
|
355
|
+
if (!(target instanceof Element)) {
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
const marker = target.closest('[data-location-id]')
|
|
359
|
+
if (!(marker instanceof HTMLElement)) {
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
const locationId = marker.dataset['locationId']
|
|
363
|
+
if (locationId !== undefined) {
|
|
364
|
+
Queue.offerUnsafe(queue, Message.ClickedMarker({ locationId }))
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
map.on('moveend', onMoveEnd)
|
|
370
|
+
container.addEventListener('click', onContainerClick)
|
|
371
|
+
return { container, onMoveEnd, onContainerClick }
|
|
372
|
+
} catch (error) {
|
|
373
|
+
map.off('moveend', onMoveEnd)
|
|
374
|
+
container.removeEventListener('click', onContainerClick)
|
|
375
|
+
throw error
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
catch: toMapMountError,
|
|
379
|
+
}),
|
|
380
|
+
({ container, onMoveEnd, onContainerClick }) =>
|
|
381
|
+
Effect.sync(() => {
|
|
382
|
+
map.off('moveend', onMoveEnd)
|
|
383
|
+
container.removeEventListener('click', onContainerClick)
|
|
384
|
+
}),
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
const mountMap = (element: Element, hostId: string) =>
|
|
388
|
+
Stream.callback<MountMapMessage>(queue =>
|
|
389
|
+
Effect.gen(function* () {
|
|
390
|
+
if (!(element instanceof HTMLElement)) {
|
|
391
|
+
Queue.offerUnsafe(
|
|
392
|
+
queue,
|
|
393
|
+
Message.FailedMountMap({
|
|
394
|
+
reason: 'Map host is not an HTMLElement.',
|
|
395
|
+
}),
|
|
396
|
+
)
|
|
397
|
+
return yield* Effect.never
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const mapResource = yield* Effect.acquireRelease(
|
|
350
401
|
Effect.gen(function* () {
|
|
351
402
|
const maplibre = yield* Effect.tryPromise(() => import('maplibre-gl'))
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
featuredLocations,
|
|
362
|
-
({ id, lng, lat }) => {
|
|
363
|
-
const markerElement = document.createElement('button')
|
|
364
|
-
markerElement.setAttribute('data-location-id', id)
|
|
365
|
-
markerElement.setAttribute('aria-label', `Marker: ${id}`)
|
|
366
|
-
markerElement.className = markerStyle
|
|
367
|
-
new maplibre.Marker({ element: markerElement })
|
|
368
|
-
.setLngLat([lng, lat])
|
|
369
|
-
.addTo(map)
|
|
370
|
-
return markerElement
|
|
403
|
+
const map = yield* Effect.try({
|
|
404
|
+
try: () => {
|
|
405
|
+
maplibre.setWorkerUrl(maplibreWorkerUrl)
|
|
406
|
+
return new maplibre.Map({
|
|
407
|
+
container: element,
|
|
408
|
+
style: 'https://demotiles.maplibre.org/style.json',
|
|
409
|
+
center: [0, 20],
|
|
410
|
+
zoom: INITIAL_MAP_ZOOM,
|
|
411
|
+
})
|
|
371
412
|
},
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
return { map, markerElements }
|
|
413
|
+
catch: toMapMountError,
|
|
414
|
+
})
|
|
415
|
+
return { map, maplibre }
|
|
376
416
|
}),
|
|
377
|
-
() => Effect.sync(() => removeMap(hostId)),
|
|
417
|
+
({ map }) => Effect.sync(() => removeMap(hostId, map)),
|
|
378
418
|
)
|
|
419
|
+
const { map, maplibre } = mapResource
|
|
379
420
|
|
|
380
|
-
yield*
|
|
381
|
-
|
|
382
|
-
applyMapViewState(mountedMap, viewState),
|
|
383
|
-
),
|
|
384
|
-
Effect.forkScoped,
|
|
385
|
-
)
|
|
421
|
+
yield* addLocationMarkers(map, maplibre)
|
|
422
|
+
setMap(hostId, map)
|
|
386
423
|
|
|
387
|
-
|
|
424
|
+
yield* listenToMapMovesAndMarkerClicks(map, queue)
|
|
425
|
+
|
|
426
|
+
Queue.offerUnsafe(queue, Message.SucceededMountMap({ hostId }))
|
|
427
|
+
Queue.offerUnsafe(queue, Message.MovedMap({ bounds: boundsFromMap(map) }))
|
|
428
|
+
return yield* Effect.never
|
|
388
429
|
}).pipe(
|
|
389
430
|
Effect.catch(error =>
|
|
390
|
-
Effect.
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
431
|
+
Effect.sync(() =>
|
|
432
|
+
Queue.offerUnsafe(
|
|
433
|
+
queue,
|
|
434
|
+
Message.FailedMountMap({ reason: toMapMountError(error).message }),
|
|
435
|
+
),
|
|
394
436
|
),
|
|
395
437
|
),
|
|
396
|
-
)
|
|
397
|
-
|
|
438
|
+
),
|
|
439
|
+
)
|
|
398
440
|
|
|
399
|
-
export const MountMap = Mount.
|
|
441
|
+
export const MountMap = Mount.defineStream('MountMap', {
|
|
400
442
|
args: { hostId: Schema.String },
|
|
401
|
-
messages: [
|
|
402
|
-
|
|
403
|
-
|
|
443
|
+
messages: [
|
|
444
|
+
Message.SucceededMountMap,
|
|
445
|
+
Message.FailedMountMap,
|
|
446
|
+
Message.MovedMap,
|
|
447
|
+
Message.ClickedMarker,
|
|
448
|
+
],
|
|
449
|
+
execute: ({ element, hostId }) => mountMap(element, hostId),
|
|
404
450
|
})
|
|
405
451
|
|
|
406
|
-
// SUBSCRIPTIONS
|
|
407
|
-
|
|
408
452
|
const boundsFromMap = (map: MapInstance): Bounds => {
|
|
409
453
|
const bounds = map.getBounds()
|
|
410
454
|
return {
|
|
@@ -415,72 +459,6 @@ const boundsFromMap = (map: MapInstance): Bounds => {
|
|
|
415
459
|
}
|
|
416
460
|
}
|
|
417
461
|
|
|
418
|
-
const streamMapEvents = (hostId: string) =>
|
|
419
|
-
Stream.callback<Message>(queue =>
|
|
420
|
-
Effect.acquireRelease(
|
|
421
|
-
Effect.sync(() =>
|
|
422
|
-
Option.map(getMap(hostId), map => {
|
|
423
|
-
const onMoveEnd = () => {
|
|
424
|
-
Queue.offerUnsafe(
|
|
425
|
-
queue,
|
|
426
|
-
Message.MovedMap({ bounds: boundsFromMap(map) }),
|
|
427
|
-
)
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const onContainerClick = (event: MouseEvent) => {
|
|
431
|
-
const target = event.target
|
|
432
|
-
if (!(target instanceof Element)) {
|
|
433
|
-
return
|
|
434
|
-
}
|
|
435
|
-
const marker = target.closest('[data-location-id]')
|
|
436
|
-
if (!(marker instanceof HTMLElement)) {
|
|
437
|
-
return
|
|
438
|
-
}
|
|
439
|
-
const locationId = marker.dataset['locationId']
|
|
440
|
-
if (locationId !== undefined) {
|
|
441
|
-
Queue.offerUnsafe(queue, Message.ClickedMarker({ locationId }))
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
map.on('moveend', onMoveEnd)
|
|
446
|
-
map.getContainer().addEventListener('click', onContainerClick)
|
|
447
|
-
Queue.offerUnsafe(
|
|
448
|
-
queue,
|
|
449
|
-
Message.MovedMap({ bounds: boundsFromMap(map) }),
|
|
450
|
-
)
|
|
451
|
-
|
|
452
|
-
return { map, onMoveEnd, onContainerClick }
|
|
453
|
-
}),
|
|
454
|
-
),
|
|
455
|
-
maybeHandle =>
|
|
456
|
-
Effect.sync(() =>
|
|
457
|
-
Option.match(maybeHandle, {
|
|
458
|
-
onNone: Function.constVoid,
|
|
459
|
-
onSome: ({ map, onMoveEnd, onContainerClick }) => {
|
|
460
|
-
map.off('moveend', onMoveEnd)
|
|
461
|
-
map.getContainer().removeEventListener('click', onContainerClick)
|
|
462
|
-
},
|
|
463
|
-
}),
|
|
464
|
-
),
|
|
465
|
-
).pipe(Effect.flatMap(() => Effect.never)),
|
|
466
|
-
)
|
|
467
|
-
|
|
468
|
-
export const subscriptions = Subscription.make<Model, Message>()(entry => ({
|
|
469
|
-
mapEvents: entry(
|
|
470
|
-
{ maybeMapHostId: Schema.Option(Schema.String) },
|
|
471
|
-
{
|
|
472
|
-
modelToDependencies: model => ({
|
|
473
|
-
maybeMapHostId: model.maybeMapHostId,
|
|
474
|
-
}),
|
|
475
|
-
dependenciesToStream: ({ maybeMapHostId }) =>
|
|
476
|
-
Option.match(maybeMapHostId, {
|
|
477
|
-
onNone: () => Stream.empty,
|
|
478
|
-
onSome: streamMapEvents,
|
|
479
|
-
}),
|
|
480
|
-
},
|
|
481
|
-
),
|
|
482
|
-
}))
|
|
483
|
-
|
|
484
462
|
// VIEW
|
|
485
463
|
|
|
486
464
|
const HOST_ID = 'map-host-1'
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Option } from 'effect'
|
|
2
2
|
import type { Map as MapInstance } from 'maplibre-gl'
|
|
3
3
|
|
|
4
4
|
const mapsByHostId = new Map<string, MapInstance>()
|
|
5
5
|
|
|
6
|
-
/** Stash a live Map instance against an id so
|
|
7
|
-
*
|
|
8
|
-
* Model. Cleanup is paired in `OnMount`'s cleanup. */
|
|
6
|
+
/** Stash a live Map instance against an id so Commands can reach it without
|
|
7
|
+
* putting the mutable, unfreezable instance in the Model. */
|
|
9
8
|
export const setMap = (hostId: string, instance: MapInstance): void => {
|
|
10
9
|
mapsByHostId.set(hostId, instance)
|
|
11
10
|
}
|
|
@@ -13,11 +12,10 @@ export const setMap = (hostId: string, instance: MapInstance): void => {
|
|
|
13
12
|
export const getMap = (hostId: string): Option.Option<MapInstance> =>
|
|
14
13
|
Option.fromNullishOr(mapsByHostId.get(hostId))
|
|
15
14
|
|
|
16
|
-
export const removeMap = (hostId: string): void =>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
})
|
|
15
|
+
export const removeMap = (hostId: string, map: MapInstance): void => {
|
|
16
|
+
map.remove()
|
|
17
|
+
|
|
18
|
+
if (mapsByHostId.get(hostId) === map) {
|
|
19
|
+
mapsByHostId.delete(hostId)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { Deferred, Effect,
|
|
2
|
-
import { Mount } from 'foldkit'
|
|
1
|
+
import { Deferred, Effect, Stream } from 'effect'
|
|
3
2
|
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
|
4
3
|
|
|
5
|
-
import { MountMap } from './main'
|
|
4
|
+
import { Message, MountMap } from './main'
|
|
6
5
|
|
|
7
6
|
const maplibre = vi.hoisted(() => {
|
|
8
7
|
let container: HTMLElement | undefined
|
|
8
|
+
let onMoveEnd: (() => void) | undefined
|
|
9
9
|
|
|
10
10
|
return {
|
|
11
11
|
get container(): HTMLElement | undefined {
|
|
@@ -14,8 +14,15 @@ const maplibre = vi.hoisted(() => {
|
|
|
14
14
|
set container(nextContainer: HTMLElement | undefined) {
|
|
15
15
|
container = nextContainer
|
|
16
16
|
},
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
clearEventHandlers: (): void => {
|
|
18
|
+
onMoveEnd = undefined
|
|
19
|
+
},
|
|
20
|
+
emitMoveEnd: (): void => {
|
|
21
|
+
onMoveEnd?.()
|
|
22
|
+
},
|
|
23
|
+
setMoveEndListener: (listener: () => void): void => {
|
|
24
|
+
onMoveEnd = listener
|
|
25
|
+
},
|
|
19
26
|
makeMap: vi.fn(),
|
|
20
27
|
removeMap: vi.fn(),
|
|
21
28
|
}
|
|
@@ -23,11 +30,6 @@ const maplibre = vi.hoisted(() => {
|
|
|
23
30
|
|
|
24
31
|
vi.mock('maplibre-gl', () => {
|
|
25
32
|
class Map {
|
|
26
|
-
readonly keyboard = {
|
|
27
|
-
disable: maplibre.disableKeyboard,
|
|
28
|
-
enable: maplibre.enableKeyboard,
|
|
29
|
-
}
|
|
30
|
-
|
|
31
33
|
constructor({ container }: { container: HTMLElement }) {
|
|
32
34
|
maplibre.container = container
|
|
33
35
|
maplibre.makeMap()
|
|
@@ -36,6 +38,31 @@ vi.mock('maplibre-gl', () => {
|
|
|
36
38
|
remove(): void {
|
|
37
39
|
maplibre.removeMap()
|
|
38
40
|
}
|
|
41
|
+
|
|
42
|
+
on(eventName: string, listener: () => void): this {
|
|
43
|
+
if (eventName === 'moveend') {
|
|
44
|
+
maplibre.setMoveEndListener(listener)
|
|
45
|
+
}
|
|
46
|
+
return this
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
off(): this {
|
|
50
|
+
maplibre.clearEventHandlers()
|
|
51
|
+
return this
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getContainer(): HTMLElement {
|
|
55
|
+
return maplibre.container ?? document.createElement('div')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getBounds() {
|
|
59
|
+
return {
|
|
60
|
+
getWest: () => -180,
|
|
61
|
+
getSouth: () => -85,
|
|
62
|
+
getEast: () => 180,
|
|
63
|
+
getNorth: () => 85,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
39
66
|
}
|
|
40
67
|
|
|
41
68
|
class Marker {
|
|
@@ -58,70 +85,68 @@ vi.mock('maplibre-gl', () => {
|
|
|
58
85
|
return { Map, Marker, setWorkerUrl: vi.fn() }
|
|
59
86
|
})
|
|
60
87
|
|
|
88
|
+
const mountAndInteract = (host: HTMLElement): Effect.Effect<Array<Message>> =>
|
|
89
|
+
Effect.scoped(
|
|
90
|
+
Effect.gen(function* () {
|
|
91
|
+
const received: Array<Message> = []
|
|
92
|
+
const mounted = yield* Deferred.make<void>()
|
|
93
|
+
|
|
94
|
+
yield* MountMap({ hostId: 'test-map-host' })
|
|
95
|
+
.f(host, Stream.empty)
|
|
96
|
+
.pipe(
|
|
97
|
+
Stream.runForEach(message =>
|
|
98
|
+
Effect.sync(() => {
|
|
99
|
+
received.push(message)
|
|
100
|
+
if (message._tag === 'MovedMap') {
|
|
101
|
+
Effect.runSync(Deferred.succeed(mounted, undefined))
|
|
102
|
+
}
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
105
|
+
Effect.forkScoped,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
yield* Deferred.await(mounted)
|
|
109
|
+
const marker = host.querySelector('[data-location-id]')
|
|
110
|
+
if (marker === null) {
|
|
111
|
+
throw new Error('Expected a mounted marker')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
marker.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
115
|
+
maplibre.emitMoveEnd()
|
|
116
|
+
yield* Effect.yieldNow
|
|
117
|
+
|
|
118
|
+
expect(received.map(message => message._tag)).toEqual([
|
|
119
|
+
'SucceededMountMap',
|
|
120
|
+
'MovedMap',
|
|
121
|
+
'ClickedMarker',
|
|
122
|
+
'MovedMap',
|
|
123
|
+
])
|
|
124
|
+
return received
|
|
125
|
+
}),
|
|
126
|
+
)
|
|
127
|
+
|
|
61
128
|
describe('MountMap', () => {
|
|
62
129
|
beforeEach(() => {
|
|
63
130
|
maplibre.container = undefined
|
|
131
|
+
maplibre.clearEventHandlers()
|
|
64
132
|
vi.clearAllMocks()
|
|
65
133
|
})
|
|
66
134
|
|
|
67
|
-
test('
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
await Effect.runPromise(
|
|
71
|
-
Effect.scoped(
|
|
72
|
-
Effect.gen(function* () {
|
|
73
|
-
const observedInitialLive = yield* Deferred.make<void>()
|
|
74
|
-
const observedPaused = yield* Deferred.make<void>()
|
|
75
|
-
const observedResumed = yield* Deferred.make<void>()
|
|
76
|
-
maplibre.enableKeyboard.mockImplementation(() => {
|
|
77
|
-
if (maplibre.enableKeyboard.mock.calls.length === 1) {
|
|
78
|
-
Effect.runSync(Deferred.succeed(observedInitialLive, undefined))
|
|
79
|
-
} else if (maplibre.enableKeyboard.mock.calls.length === 2) {
|
|
80
|
-
Effect.runSync(Deferred.succeed(observedResumed, undefined))
|
|
81
|
-
}
|
|
82
|
-
})
|
|
83
|
-
maplibre.disableKeyboard.mockImplementation(() => {
|
|
84
|
-
Effect.runSync(Deferred.succeed(observedPaused, undefined))
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
const viewStates = yield* PubSub.unbounded<Mount.ViewState>({
|
|
88
|
-
replay: 1,
|
|
89
|
-
})
|
|
90
|
-
yield* PubSub.publish(viewStates, Mount.ViewState.make('Live'))
|
|
91
|
-
yield* MountMap({ hostId: 'test-map-host' })
|
|
92
|
-
.f(host, Stream.fromPubSub(viewStates))
|
|
93
|
-
.pipe(Stream.runDrain, Effect.forkScoped)
|
|
94
|
-
|
|
95
|
-
yield* Deferred.await(observedInitialLive)
|
|
96
|
-
yield* Effect.yieldNow
|
|
97
|
-
expect(maplibre.makeMap).toHaveBeenCalledOnce()
|
|
98
|
-
const markers = host.querySelectorAll('button[data-location-id]')
|
|
99
|
-
expect(markers.length).toBeGreaterThan(0)
|
|
100
|
-
for (const marker of markers) {
|
|
101
|
-
expect(marker).toHaveProperty('disabled', false)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
yield* PubSub.publish(viewStates, Mount.ViewState.make('Paused'))
|
|
105
|
-
yield* Deferred.await(observedPaused)
|
|
106
|
-
yield* Effect.yieldNow
|
|
107
|
-
expect(maplibre.makeMap).toHaveBeenCalledOnce()
|
|
108
|
-
for (const marker of markers) {
|
|
109
|
-
expect(marker).toHaveProperty('disabled', true)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
yield* PubSub.publish(viewStates, Mount.ViewState.make('Live'))
|
|
113
|
-
yield* Deferred.await(observedResumed)
|
|
114
|
-
yield* Effect.yieldNow
|
|
115
|
-
expect(maplibre.makeMap).toHaveBeenCalledOnce()
|
|
116
|
-
for (const marker of markers) {
|
|
117
|
-
expect(marker).toHaveProperty('disabled', false)
|
|
118
|
-
}
|
|
119
|
-
}),
|
|
120
|
-
),
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
expect(maplibre.disableKeyboard).toHaveBeenCalledOnce()
|
|
124
|
-
expect(maplibre.enableKeyboard).toHaveBeenCalledTimes(2)
|
|
135
|
+
test('rebinds map events when the same host id remounts', async () => {
|
|
136
|
+
const firstHost = document.createElement('div')
|
|
137
|
+
const firstMessages = await Effect.runPromise(mountAndInteract(firstHost))
|
|
125
138
|
expect(maplibre.removeMap).toHaveBeenCalledOnce()
|
|
139
|
+
|
|
140
|
+
maplibre.emitMoveEnd()
|
|
141
|
+
firstHost
|
|
142
|
+
.querySelector('[data-location-id]')
|
|
143
|
+
?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
144
|
+
expect(firstMessages).toHaveLength(4)
|
|
145
|
+
|
|
146
|
+
const secondHost = document.createElement('div')
|
|
147
|
+
await Effect.runPromise(mountAndInteract(secondHost))
|
|
148
|
+
|
|
149
|
+
expect(maplibre.makeMap).toHaveBeenCalledTimes(2)
|
|
150
|
+
expect(maplibre.removeMap).toHaveBeenCalledTimes(2)
|
|
126
151
|
})
|
|
127
152
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect, Match, Option, Schema, Stream } from 'effect'
|
|
2
|
-
import { Subscription } from 'foldkit'
|
|
2
|
+
import { Dom, Subscription } from 'foldkit'
|
|
3
3
|
|
|
4
4
|
import { Message } from './message'
|
|
5
5
|
import type { Model } from './model'
|
|
@@ -39,16 +39,16 @@ const toToolMessage = (event: KeyboardEvent): Option.Option<Message> => {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export const subscriptions = Subscription.make<Model, Message>()(entry => ({
|
|
42
|
-
undoRedoKeys: Subscription.
|
|
43
|
-
|
|
42
|
+
undoRedoKeys: Subscription.persistentEntry(
|
|
43
|
+
Dom.streamFromEventFilterMapPreventDefault({
|
|
44
44
|
target: document,
|
|
45
45
|
type: 'keydown',
|
|
46
46
|
filterMapEvent: toUndoRedoMessage,
|
|
47
47
|
}),
|
|
48
48
|
),
|
|
49
49
|
|
|
50
|
-
toolKeys: Subscription.
|
|
51
|
-
|
|
50
|
+
toolKeys: Subscription.persistentEntry(
|
|
51
|
+
Dom.streamFromEventFilterMap({
|
|
52
52
|
target: document,
|
|
53
53
|
type: 'keydown',
|
|
54
54
|
filterMapEvent: toToolMessage,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Array, Effect, Match, Option, Schema } from 'effect'
|
|
2
|
-
import { Command, Runtime, Subscription, Update } from 'foldkit'
|
|
2
|
+
import { Command, Dom, Runtime, Subscription, Update } from 'foldkit'
|
|
3
3
|
import { Document, Html, HtmlBuilder } from 'foldkit/html'
|
|
4
4
|
import { defineMessageUnion } from 'foldkit/message'
|
|
5
5
|
import { UrlRequest, load, pushUrl } from 'foldkit/navigation'
|
|
@@ -160,8 +160,8 @@ export const update = (model: Model, message: Message) =>
|
|
|
160
160
|
// SUBSCRIPTION
|
|
161
161
|
|
|
162
162
|
export const subscriptions = Subscription.make<Model, Message>()(() => ({
|
|
163
|
-
keyBindings: Subscription.
|
|
164
|
-
|
|
163
|
+
keyBindings: Subscription.persistentEntry(
|
|
164
|
+
Dom.streamFromKeyBindings<Message>({
|
|
165
165
|
bindings: [
|
|
166
166
|
{
|
|
167
167
|
keys: ['G', 'H'],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Array, Match, Number, Option, Schema, Stream, pipe } from 'effect'
|
|
2
|
-
import { Runtime, Subscription, type Update } from 'foldkit'
|
|
2
|
+
import { Dom, Runtime, Subscription, type Update } from 'foldkit'
|
|
3
3
|
import { type Document, type Html, HtmlBuilder, createLazy } from 'foldkit/html'
|
|
4
4
|
import { defineMessageUnion } from 'foldkit/message'
|
|
5
5
|
import { modifyFields } from 'foldkit/struct'
|
|
@@ -61,7 +61,7 @@ export const Message = defineMessageUnion({
|
|
|
61
61
|
|
|
62
62
|
export type Message = typeof Message.Type
|
|
63
63
|
|
|
64
|
-
const slowWarningTarget:
|
|
64
|
+
const slowWarningTarget: Dom.TypedEventTarget<{
|
|
65
65
|
[SLOW_WARNING_EVENT]: CustomEvent<SlowWarningReport>
|
|
66
66
|
}> = new EventTarget()
|
|
67
67
|
|
|
@@ -219,8 +219,8 @@ export const init: Runtime.ApplicationInit<Model, Message> = () => ({
|
|
|
219
219
|
// SUBSCRIPTION
|
|
220
220
|
|
|
221
221
|
export const subscriptions = Subscription.make<Model, Message>()(entry => ({
|
|
222
|
-
slowWarnings: Subscription.
|
|
223
|
-
|
|
222
|
+
slowWarnings: Subscription.persistentEntry(
|
|
223
|
+
Dom.streamFromEventFilterMap({
|
|
224
224
|
target: slowWarningTarget,
|
|
225
225
|
type: SLOW_WARNING_EVENT,
|
|
226
226
|
filterMapEvent: event =>
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
Stream,
|
|
9
9
|
pipe,
|
|
10
10
|
} from 'effect'
|
|
11
|
-
import { Command, Runtime, Subscription, type Update } from 'foldkit'
|
|
11
|
+
import { Command, Dom, Runtime, Subscription, type Update } from 'foldkit'
|
|
12
12
|
import { Document, Html, HtmlBuilder } from 'foldkit/html'
|
|
13
13
|
import { defineMessageUnion } from 'foldkit/message'
|
|
14
14
|
import { modifyFields } from 'foldkit/struct'
|
|
@@ -244,6 +244,7 @@ export const subscriptions = Subscription.make<Model, Message>()(entry => ({
|
|
|
244
244
|
dependenciesToStream: ({ isPlaying, interval }) =>
|
|
245
245
|
Stream.when(
|
|
246
246
|
Stream.tick(Duration.millis(interval)).pipe(
|
|
247
|
+
Stream.drop(1),
|
|
247
248
|
Stream.map(Message.TickedClock),
|
|
248
249
|
),
|
|
249
250
|
Effect.sync(() => isPlaying),
|
|
@@ -251,8 +252,8 @@ export const subscriptions = Subscription.make<Model, Message>()(entry => ({
|
|
|
251
252
|
},
|
|
252
253
|
),
|
|
253
254
|
|
|
254
|
-
keyboard: Subscription.
|
|
255
|
-
|
|
255
|
+
keyboard: Subscription.persistentEntry(
|
|
256
|
+
Dom.streamFromEventFilterMapPreventDefault({
|
|
256
257
|
target: document,
|
|
257
258
|
type: 'keydown',
|
|
258
259
|
filterMapEvent: keyboardEvent =>
|
|
@@ -3,6 +3,8 @@ import { Server } from 'foldkit/experimental'
|
|
|
3
3
|
|
|
4
4
|
import { init, view } from './main'
|
|
5
5
|
|
|
6
|
+
export const renderDocument = Server.renderDocument
|
|
7
|
+
|
|
6
8
|
export const prerenderPaths: ReadonlyArray<string> = ['/', '/about']
|
|
7
9
|
|
|
8
10
|
export const renderPage = (request: Request): Promise<Server.EntryResult> =>
|