Usertest: See #1315, add a 'confirm'-button on top of the precise-input location if the map is tapped; fix: make sure the dragInvitation doesn't consume the first map interaction, get's hidden by a change in map location

This commit is contained in:
Pieter Vander Vennet 2023-06-14 23:21:19 +02:00
parent cc70e09e11
commit c5c6bba731
6 changed files with 429 additions and 387 deletions

View file

@ -11,9 +11,14 @@
function hide() { function hide() {
mainElem.style.visibility = "hidden" mainElem.style.visibility = "hidden"
} }
let initTime = Date.now()
if (hideSignal) { if (hideSignal) {
onDestroy( onDestroy(
hideSignal.addCallbackD(() => { hideSignal.addCallbackD(() => {
if(initTime + 1000 > Date.now()){
console.log("Ignoring hide signal")
return
}
console.log("Received hide signal") console.log("Received hide signal")
hide() hide()
return true return true
@ -27,8 +32,8 @@
} }
</script> </script>
<div bind:this={mainElem} class="absolute bottom-0 right-0 h-full w-full"> <div bind:this={mainElem} class="absolute bottom-0 right-0 h-full w-full pointer-events-none">
<div id="hand-container" class="pointer-events-none"> <div id="hand-container">
<img src="./assets/svg/hand.svg" /> <img src="./assets/svg/hand.svg" />
</div> </div>
</div> </div>

View file

@ -15,6 +15,7 @@
import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger" import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import {createEventDispatcher} from "svelte";
/** /**
* An advanced location input, which has support to: * An advanced location input, which has support to:
@ -44,6 +45,8 @@
lat: number lat: number
}>(undefined) }>(undefined)
const dispatch = createEventDispatcher<{click: {lon: number, lat: number}}>()
const xyz = Tiles.embedded_tile(coordinate.lat, coordinate.lon, 16) const xyz = Tiles.embedded_tile(coordinate.lat, coordinate.lon, 16)
const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined) const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let initialMapProperties: Partial<MapProperties> = { let initialMapProperties: Partial<MapProperties> = {
@ -106,6 +109,7 @@
<LocationInput <LocationInput
{map} {map}
on:click={data => dispatch("click", data)}
mapProperties={initialMapProperties} mapProperties={initialMapProperties}
value={preciseLocation} value={preciseLocation}
initialCoordinate={coordinate} initialCoordinate={coordinate}

View file

@ -1,17 +1,17 @@
<script lang="ts"> <script lang="ts">
import { Store, UIEventSource } from "../../../Logic/UIEventSource" import {Store, UIEventSource} from "../../../Logic/UIEventSource"
import type { MapProperties } from "../../../Models/MapProperties" import type {MapProperties} from "../../../Models/MapProperties"
import { Map as MlMap } from "maplibre-gl" import {Map as MlMap} from "maplibre-gl"
import { MapLibreAdaptor } from "../../Map/MapLibreAdaptor" import {MapLibreAdaptor} from "../../Map/MapLibreAdaptor"
import MaplibreMap from "../../Map/MaplibreMap.svelte" import MaplibreMap from "../../Map/MaplibreMap.svelte"
import DragInvitation from "../../Base/DragInvitation.svelte" import DragInvitation from "../../Base/DragInvitation.svelte"
import { GeoOperations } from "../../../Logic/GeoOperations" import {GeoOperations} from "../../../Logic/GeoOperations"
import ShowDataLayer from "../../Map/ShowDataLayer" import ShowDataLayer from "../../Map/ShowDataLayer"
import * as boundsdisplay from "../../../assets/layers/range/range.json" import * as boundsdisplay from "../../../assets/layers/range/range.json"
import StaticFeatureSource from "../../../Logic/FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../../../Logic/FeatureSource/Sources/StaticFeatureSource"
import * as turf from "@turf/turf" import * as turf from "@turf/turf"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { onDestroy } from "svelte" import {createEventDispatcher, onDestroy} from "svelte"
/** /**
* A visualisation to pick a location on a map background * A visualisation to pick a location on a map background
@ -35,8 +35,13 @@
mapProperties: MapProperties mapProperties: MapProperties
) => void = undefined ) => void = undefined
const dispatch = createEventDispatcher<{ click: { lon: number, lat: number } }>()
export let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined) export let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let mla = new MapLibreAdaptor(map, mapProperties) let mla = new MapLibreAdaptor(map, mapProperties)
mla.lastClickLocation.addCallbackAndRunD(lastClick => {
dispatch("click", lastClick)
})
mapProperties.location.syncWith(value) mapProperties.location.syncWith(value)
if (onCreated) { if (onCreated) {
onCreated(value, map, mla) onCreated(value, map, mla)
@ -56,7 +61,7 @@
// This is too far away - let's move back // This is too far away - let's move back
const correctLocation = GeoOperations.along(c, l, maxDistanceInMeters - 10) const correctLocation = GeoOperations.along(c, l, maxDistanceInMeters - 10)
window.setTimeout(() => { window.setTimeout(() => {
mla.location.setData({ lon: correctLocation[0], lat: correctLocation[1] }) mla.location.setData({lon: correctLocation[0], lat: correctLocation[1]})
}, 25) }, 25)
if (!rangeIsShown) { if (!rangeIsShown) {
@ -65,7 +70,7 @@
features: new StaticFeatureSource([ features: new StaticFeatureSource([
turf.circle(c, maxDistanceInMeters, { turf.circle(c, maxDistanceInMeters, {
units: "meters", units: "meters",
properties: { range: "yes", id: "0" }, properties: {range: "yes", id: "0"},
}), }),
]), ]),
}) })
@ -78,14 +83,14 @@
<div class="min-h-32 relative h-full cursor-pointer overflow-hidden"> <div class="min-h-32 relative h-full cursor-pointer overflow-hidden">
<div class="absolute top-0 left-0 h-full w-full cursor-pointer"> <div class="absolute top-0 left-0 h-full w-full cursor-pointer">
<MaplibreMap {map} /> <MaplibreMap {map}/>
</div> </div>
<div <div
class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center p-8 opacity-50" class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center p-8 opacity-50"
> >
<img class="h-full max-h-24" src="./assets/svg/move-arrows.svg" /> <img class="h-full max-h-24" src="./assets/svg/move-arrows.svg"/>
</div> </div>
<DragInvitation hideSignal={mla.location.stabilized(3000)} /> <DragInvitation hideSignal={mla.location}/>
</div> </div>

View file

@ -83,7 +83,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
// Workaround, 'ShowPointLayer' sets this flag // Workaround, 'ShowPointLayer' sets this flag
return return
} }
console.log(e)
const lon = e.lngLat.lng const lon = e.lngLat.lng
const lat = e.lngLat.lat const lat = e.lngLat.lat
lastClickLocation.setData({ lon, lat }) lastClickLocation.setData({ lon, lat })
@ -321,10 +320,9 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
if (this.location.data === undefined) { if (this.location.data === undefined) {
this.location.setData({ lon: lng, lat }) this.location.setData({ lon: lng, lat })
} else if (!isSetup) { } else if (!isSetup) {
const dt = this.location.data const lon = map.getCenter().lng
dt.lon = map.getCenter().lng const lat = map.getCenter().lat
dt.lat = map.getCenter().lat this.location.setData({ lon, lat })
this.location.ping()
} }
this.zoom.setData(Math.round(map.getZoom() * 10) / 10) this.zoom.setData(Math.round(map.getZoom() * 10) / 10)
const bounds = map.getBounds() const bounds = map.getBounds()

View file

@ -3,7 +3,7 @@
* This component ties together all the steps that are needed to create a new point. * This component ties together all the steps that are needed to create a new point.
* There are many subcomponents which help with that * There are many subcomponents which help with that
*/ */
import type { SpecialVisualizationState } from "../../SpecialVisualization" import type {SpecialVisualizationState} from "../../SpecialVisualization"
import PresetList from "./PresetList.svelte" import PresetList from "./PresetList.svelte"
import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig" import type PresetConfig from "../../../Models/ThemeConfig/PresetConfig"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
@ -12,27 +12,27 @@
import FromHtml from "../../Base/FromHtml.svelte" import FromHtml from "../../Base/FromHtml.svelte"
import Translations from "../../i18n/Translations.js" import Translations from "../../i18n/Translations.js"
import TagHint from "../TagHint.svelte" import TagHint from "../TagHint.svelte"
import { And } from "../../../Logic/Tags/And.js" import {And} from "../../../Logic/Tags/And.js"
import LoginToggle from "../../Base/LoginToggle.svelte" import LoginToggle from "../../Base/LoginToggle.svelte"
import Constants from "../../../Models/Constants.js" import Constants from "../../../Models/Constants.js"
import FilteredLayer from "../../../Models/FilteredLayer" import FilteredLayer from "../../../Models/FilteredLayer"
import { Store, UIEventSource } from "../../../Logic/UIEventSource" import {Store, UIEventSource} from "../../../Logic/UIEventSource"
import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid" import {EyeIcon, EyeOffIcon} from "@rgossiaux/svelte-heroicons/solid"
import LoginButton from "../../Base/LoginButton.svelte" import LoginButton from "../../Base/LoginButton.svelte"
import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte" import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte"
import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction" import CreateNewNodeAction from "../../../Logic/Osm/Actions/CreateNewNodeAction"
import { OsmWay } from "../../../Logic/Osm/OsmObject" import {OsmWay} from "../../../Logic/Osm/OsmObject"
import { Tag } from "../../../Logic/Tags/Tag" import {Tag} from "../../../Logic/Tags/Tag"
import type { WayId } from "../../../Models/OsmFeature" import type {WayId} from "../../../Models/OsmFeature"
import Loading from "../../Base/Loading.svelte" import Loading from "../../Base/Loading.svelte"
import type { GlobalFilter } from "../../../Models/GlobalFilter" import type {GlobalFilter} from "../../../Models/GlobalFilter"
import { onDestroy } from "svelte" import {onDestroy} from "svelte"
import NextButton from "../../Base/NextButton.svelte" import NextButton from "../../Base/NextButton.svelte"
import BackButton from "../../Base/BackButton.svelte" import BackButton from "../../Base/BackButton.svelte"
import ToSvelte from "../../Base/ToSvelte.svelte" import ToSvelte from "../../Base/ToSvelte.svelte"
import Svg from "../../../Svg" import Svg from "../../../Svg"
import MapControlButton from "../../Base/MapControlButton.svelte" import MapControlButton from "../../Base/MapControlButton.svelte"
import { Square3Stack3dIcon } from "@babeard/svelte-heroicons/solid" import {Square3Stack3dIcon} from "@babeard/svelte-heroicons/solid"
export let coordinate: { lon: number; lat: number } export let coordinate: { lon: number; lat: number }
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
@ -75,6 +75,9 @@
let preciseCoordinate: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined) let preciseCoordinate: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined)
let snappedToObject: UIEventSource<string> = new UIEventSource<string>(undefined) let snappedToObject: UIEventSource<string> = new UIEventSource<string>(undefined)
// Small helper variable: if the map is tapped, we should let the 'Next'-button grab some attention as users have to click _that_ to continue, not the map
let preciseInputIsTapped = false
let creating = false let creating = false
/** /**
@ -86,6 +89,7 @@
// When aborted, we force the contributors to place the pin _again_ // When aborted, we force the contributors to place the pin _again_
// This is because there might be a nearby object that was disabled; this forces them to re-evaluate the map // This is because there might be a nearby object that was disabled; this forces them to re-evaluate the map
state.lastClickObject.features.setData([]) state.lastClickObject.features.setData([])
preciseInputIsTapped = false
} }
async function confirm() { async function confirm() {
@ -145,17 +149,17 @@
2. What do we want to add? 2. What do we want to add?
3. Are all elements of this category visible? (i.e. there are no filters possibly hiding this, is the data still loading, ...) --> 3. Are all elements of this category visible? (i.e. there are no filters possibly hiding this, is the data still loading, ...) -->
<LoginButton osmConnection={state.osmConnection} slot="not-logged-in"> <LoginButton osmConnection={state.osmConnection} slot="not-logged-in">
<Tr slot="message" t={Translations.t.general.add.pleaseLogin} /> <Tr slot="message" t={Translations.t.general.add.pleaseLogin}/>
</LoginButton> </LoginButton>
{#if $isLoading} {#if $isLoading}
<div class="alert"> <div class="alert">
<Loading> <Loading>
<Tr t={Translations.t.general.add.stillLoading} /> <Tr t={Translations.t.general.add.stillLoading}/>
</Loading> </Loading>
</div> </div>
{:else if $zoom < Constants.minZoomLevelToAddNewPoint} {:else if $zoom < Constants.minZoomLevelToAddNewPoint}
<div class="alert"> <div class="alert">
<Tr t={Translations.t.general.add.zoomInFurther} /> <Tr t={Translations.t.general.add.zoomInFurther}/>
</div> </div>
{:else if selectedPreset === undefined} {:else if selectedPreset === undefined}
<!-- First, select the correct preset --> <!-- First, select the correct preset -->
@ -168,7 +172,7 @@
{:else if !$layerIsDisplayed} {:else if !$layerIsDisplayed}
<!-- Check that the layer is enabled, so that we don't add a duplicate --> <!-- Check that the layer is enabled, so that we don't add a duplicate -->
<div class="alert flex items-center justify-center"> <div class="alert flex items-center justify-center">
<EyeOffIcon class="w-8" /> <EyeOffIcon class="w-8"/>
<Tr <Tr
t={Translations.t.general.add.layerNotEnabled.Subs({ layer: selectedPreset.layer.name })} t={Translations.t.general.add.layerNotEnabled.Subs({ layer: selectedPreset.layer.name })}
/> />
@ -182,8 +186,8 @@
state.guistate.openFilterView(selectedPreset.layer) state.guistate.openFilterView(selectedPreset.layer)
}} }}
> >
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} /> <ToSvelte construct={Svg.layers_svg().SetClass("w-12")}/>
<Tr t={Translations.t.general.add.openLayerControl} /> <Tr t={Translations.t.general.add.openLayerControl}/>
</button> </button>
<button <button
@ -193,15 +197,15 @@
abort() abort()
}} }}
> >
<EyeIcon class="w-12" /> <EyeIcon class="w-12"/>
<Tr t={Translations.t.general.add.enableLayer.Subs({ name: selectedPreset.layer.name })} /> <Tr t={Translations.t.general.add.enableLayer.Subs({ name: selectedPreset.layer.name })}/>
</button> </button>
</div> </div>
{:else if $layerHasFilters} {:else if $layerHasFilters}
<!-- Some filters are enabled. The feature to add might already be mapped, but hidden --> <!-- Some filters are enabled. The feature to add might already be mapped, but hidden -->
<div class="alert flex items-center justify-center"> <div class="alert flex items-center justify-center">
<EyeOffIcon class="w-8" /> <EyeOffIcon class="w-8"/>
<Tr t={Translations.t.general.add.disableFiltersExplanation} /> <Tr t={Translations.t.general.add.disableFiltersExplanation}/>
</div> </div>
<div class="flex flex-wrap-reverse md:flex-nowrap"> <div class="flex flex-wrap-reverse md:flex-nowrap">
<button <button
@ -211,8 +215,8 @@
state.layerState.filteredLayers.get(selectedPreset.layer.id).disableAllFilters() state.layerState.filteredLayers.get(selectedPreset.layer.id).disableAllFilters()
}} }}
> >
<EyeOffIcon class="w-12" /> <EyeOffIcon class="w-12"/>
<Tr t={Translations.t.general.add.disableFilters} /> <Tr t={Translations.t.general.add.disableFilters}/>
</button> </button>
<button <button
class="flex w-full gap-x-1" class="flex w-full gap-x-1"
@ -221,8 +225,8 @@
state.guistate.openFilterView(selectedPreset.layer) state.guistate.openFilterView(selectedPreset.layer)
}} }}
> >
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")} /> <ToSvelte construct={Svg.layers_svg().SetClass("w-12")}/>
<Tr t={Translations.t.general.add.openLayerControl} /> <Tr t={Translations.t.general.add.openLayerControl}/>
</button> </button>
</div> </div>
{:else if !confirmedCategory} {:else if !confirmedCategory}
@ -233,23 +237,23 @@
/> />
</h2> </h2>
<Tr t={Translations.t.general.add.confirmIntro} /> <Tr t={Translations.t.general.add.confirmIntro}/>
{#if selectedPreset.preset.description} {#if selectedPreset.preset.description}
<Tr t={selectedPreset.preset.description} /> <Tr t={selectedPreset.preset.description}/>
{/if} {/if}
{#if selectedPreset.preset.exampleImages} {#if selectedPreset.preset.exampleImages}
<h3> <h3>
{#if selectedPreset.preset.exampleImages.length === 1} {#if selectedPreset.preset.exampleImages.length === 1}
<Tr t={Translations.t.general.example} /> <Tr t={Translations.t.general.example}/>
{:else} {:else}
<Tr t={Translations.t.general.examples} /> <Tr t={Translations.t.general.examples}/>
{/if} {/if}
</h3> </h3>
<span class="flex flex-wrap items-stretch"> <span class="flex flex-wrap items-stretch">
{#each selectedPreset.preset.exampleImages as src} {#each selectedPreset.preset.exampleImages as src}
<img {src} class="m-1 h-64 w-auto rounded-lg" /> <img {src} class="m-1 h-64 w-auto rounded-lg"/>
{/each} {/each}
</span> </span>
{/if} {/if}
@ -261,21 +265,21 @@
<div class="flex w-full flex-wrap-reverse md:flex-nowrap"> <div class="flex w-full flex-wrap-reverse md:flex-nowrap">
<BackButton on:click={() => (selectedPreset = undefined)} clss="w-full"> <BackButton on:click={() => (selectedPreset = undefined)} clss="w-full">
<Tr t={t.backToSelect} /> <Tr t={t.backToSelect}/>
</BackButton> </BackButton>
<NextButton on:click={() => (confirmedCategory = true)} clss="primary w-full"> <NextButton on:click={() => (confirmedCategory = true)} clss="primary w-full">
<div slot="image" class="relative"> <div slot="image" class="relative">
<FromHtml src={selectedPreset.icon} /> <FromHtml src={selectedPreset.icon}/>
<img class="absolute bottom-0 right-0 h-4 w-4" src="./assets/svg/confirm.svg" /> <img class="absolute bottom-0 right-0 h-4 w-4" src="./assets/svg/confirm.svg"/>
</div> </div>
<div class="w-full"> <div class="w-full">
<Tr t={selectedPreset.text} /> <Tr t={selectedPreset.text}/>
</div> </div>
</NextButton> </NextButton>
</div> </div>
{:else if _globalFilter?.length > 0 && _globalFilter?.length > checkedOfGlobalFilters} {:else if _globalFilter?.length > 0 && _globalFilter?.length > checkedOfGlobalFilters}
<Tr t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.safetyCheck} cls="mx-12" /> <Tr t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.safetyCheck} cls="mx-12"/>
<SubtleButton <SubtleButton
on:click={() => { on:click={() => {
checkedOfGlobalFilters = checkedOfGlobalFilters + 1 checkedOfGlobalFilters = checkedOfGlobalFilters + 1
@ -299,13 +303,14 @@
abort() abort()
}} }}
> >
<img slot="image" src="./assets/svg/close.svg" class="h-8 w-8" /> <img slot="image" src="./assets/svg/close.svg" class="h-8 w-8"/>
<Tr slot="message" t={Translations.t.general.cancel} /> <Tr slot="message" t={Translations.t.general.cancel}/>
</SubtleButton> </SubtleButton>
{:else if !creating} {:else if !creating}
<div class="relative w-full p-1"> <div class="relative w-full p-1">
<div class="h-96 max-h-screen w-full overflow-hidden rounded-xl"> <div class="h-96 max-h-screen w-full overflow-hidden rounded-xl">
<NewPointLocationInput <NewPointLocationInput
on:click={() => {preciseInputIsTapped = true}}
value={preciseCoordinate} value={preciseCoordinate}
snappedTo={snappedToObject} snappedTo={snappedToObject}
{state} {state}
@ -314,22 +319,31 @@
snapToLayers={selectedPreset.preset.preciseInput.snapToLayers} snapToLayers={selectedPreset.preset.preciseInput.snapToLayers}
/> />
</div> </div>
<div class={(preciseInputIsTapped ? "" : "hidden") + " absolute top-4 flex justify-center w-full"}>
<NextButton on:click={confirm} clss="primary w-fit ">
<div class="flex w-full justify-end gap-x-2">
<Tr t={Translations.t.general.add.confirmLocation}/>
</div>
</NextButton>
</div>
<div class="absolute bottom-0 left-0 p-4"> <div class="absolute bottom-0 left-0 p-4">
<MapControlButton <MapControlButton
on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)} on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)}
> >
<Square3Stack3dIcon class="h-6 w-6" /> <Square3Stack3dIcon class="h-6 w-6"/>
</MapControlButton> </MapControlButton>
</div> </div>
</div> </div>
<div class="flex flex-wrap-reverse md:flex-nowrap"> <div class="flex flex-wrap-reverse md:flex-nowrap">
<BackButton on:click={() => (selectedPreset = undefined)} clss="w-full"> <BackButton on:click={() => (selectedPreset = undefined)} clss="w-full">
<Tr t={t.backToSelect} /> <Tr t={t.backToSelect}/>
</BackButton> </BackButton>
<NextButton on:click={confirm} clss="primary w-full"> <NextButton on:click={confirm} clss={"primary w-full"}>
<div class="flex w-full justify-end gap-x-2"> <div class="flex w-full justify-end gap-x-2">
<Tr t={Translations.t.general.add.confirmLocation} /> <Tr t={Translations.t.general.add.confirmLocation}/>
</div> </div>
</NextButton> </NextButton>
</div> </div>

View file

@ -670,6 +670,10 @@ video {
position: fixed; position: fixed;
} }
.\!fixed {
position: fixed !important;
}
.absolute { .absolute {
position: absolute; position: absolute;
} }
@ -795,10 +799,6 @@ video {
margin: 1px; margin: 1px;
} }
.m-12 {
margin: 3rem;
}
.mx-1 { .mx-1 {
margin-left: 0.25rem; margin-left: 0.25rem;
margin-right: 0.25rem; margin-right: 0.25rem;
@ -1242,6 +1242,27 @@ video {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
} }
@-webkit-keyframes ping {
75%, 100% {
-webkit-transform: scale(2);
transform: scale(2);
opacity: 0;
}
}
@keyframes ping {
75%, 100% {
-webkit-transform: scale(2);
transform: scale(2);
opacity: 0;
}
}
.animate-ping {
-webkit-animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
}
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
} }
@ -1730,11 +1751,6 @@ video {
color: rgb(107 114 128 / var(--tw-text-opacity)); color: rgb(107 114 128 / var(--tw-text-opacity));
} }
.text-green-600 {
--tw-text-opacity: 1;
color: rgb(22 163 74 / var(--tw-text-opacity));
}
.underline { .underline {
text-decoration-line: underline; text-decoration-line: underline;
} }