Refactoring: overhaul of the visual style with CSS

This commit is contained in:
Pieter Vander Vennet 2023-05-11 02:17:41 +02:00
parent a1f5032232
commit 7f1e8d3f9c
37 changed files with 1280 additions and 741 deletions

View file

@ -5,7 +5,7 @@
<div class="pl-2 p-1 flex">
<div class="animate-spin self-center w-6 h-6 min-w-6">
<ToSvelte construct={Svg.loading_ui}></ToSvelte>
<ToSvelte construct={Svg.loading_svg()}></ToSvelte>
</div>
<div class="ml-2">
<slot></slot>

View file

@ -8,6 +8,6 @@
</script>
<div on:click={e => dispatch("click", e)} class="subtle-background rounded-full h-fit w-fit m-0.5 md:m-1 p-0.5 sm:p-1 cursor-pointer">
<button on:click={e => dispatch("click", e)} class="secondary rounded-full h-fit w-fit m-0.5 md:m-1 p-0.5 sm:p-1">
<slot/>
</div>
</button>

View file

@ -1,73 +1,35 @@
<script lang="ts">
import { createEventDispatcher, onMount } from "svelte";
import { Store } from "../../Logic/UIEventSource";
import {createEventDispatcher} from "svelte";
import BaseUIElement from "../BaseUIElement";
import Img from "./Img";
import Translations from "../i18n/Translations";
import { ImmutableStore } from "../../Logic/UIEventSource.js";
export let imageUrl: string | BaseUIElement = undefined
export let message: string | BaseUIElement = undefined
export let options: {
url?: string | Store<string>
newTab?: boolean
imgSize?: string
extraClasses?: string
} = {}
// Website to open when clicked
let href: Store<string> = undefined
if (options?.url) {
href = typeof options?.url == "string" ? new ImmutableStore(options.url) : options.url
}
let imgElem: HTMLElement;
let msgElem: HTMLElement;
let imgClasses = "block justify-center shrink-0 mr-4 " + (options?.imgSize ?? "h-11 w-11");
const dispatch = createEventDispatcher<{click}>()
onMount(() => {
// Image
if (imgElem && imageUrl) {
let img: BaseUIElement
if ((imageUrl ?? "") === "") {
img = undefined
} else if (typeof imageUrl !== "string") {
img = imageUrl?.SetClass(imgClasses)
}
if (img) imgElem.replaceWith(img.ConstructElement())
}
// Message
if (msgElem && message) {
let msg = Translations.W(message)?.SetClass("block text-ellipsis no-images flex-shrink")
msgElem.replaceWith(msg.ConstructElement())
}
})
console.log("Slots:", $$slots)
</script>
<svelte:element
<button
class={(options.extraClasses??"") + 'flex hover:shadow-xl transition-[color,background-color,box-shadow] hover:bg-unsubtle cursor-pointer'}
href={$href}
target={options?.newTab ? "_blank" : ""}
this={href === undefined ? "span" : "a"}
on:click={(e) => dispatch("click", e)}
>
<slot name="image">
{#if imageUrl !== undefined}
{#if typeof imageUrl === "string"}
<Img src={imageUrl} class={imgClasses}></Img>
{:else }
<template bind:this={imgElem} />
{/if}
{/if}
</slot>
<slot name="message">
<template bind:this={msgElem} />
</slot>
</svelte:element>
<slot name="message"/>
</button>
<style lang="scss">
span,

View file

@ -6,6 +6,10 @@ import Lazy from "./Lazy"
import Loading from "./Loading"
import SubtleButtonSvelte from "./SubtleButton.svelte"
import SvelteUIElement from "./SvelteUIElement"
import SubtleLink from "./SubtleLink.svelte";
import Translations from "../i18n/Translations";
import Combine from "./Combine";
import Img from "./Img";
export class SubtleButton extends UIElement {
private readonly imageUrl: string | BaseUIElement
@ -34,11 +38,29 @@ export class SubtleButton extends UIElement {
}
protected InnerRender(): string | BaseUIElement {
return new SvelteUIElement(SubtleButtonSvelte, {
imageUrl: this?.imageUrl ?? undefined,
message: this?.message ?? "",
options: this?.options ?? {},
})
if(this.options.url !== undefined){
return new SvelteUIElement(SubtleLink, {href: this.options.url, newTab: this.options.newTab})
}
const classes = "block flex p-3 my-2 bg-subtle rounded-lg hover:shadow-xl hover:bg-unsubtle transition-colors transition-shadow link-no-underline";
const message = Translations.W(this.message)?.SetClass("block overflow-ellipsis no-images flex-shrink");
let img;
const imgClasses = "block justify-center flex-none mr-4 " + (this.options?.imgSize ?? "h-11 w-11")
if ((this.imageUrl ?? "") === "") {
img = undefined;
} else if (typeof (this.imageUrl) === "string") {
img = new Img(this.imageUrl)?.SetClass(imgClasses)
} else {
img = this.imageUrl?.SetClass(imgClasses);
}
const button = new Combine([
img,
message
]).SetClass("flex items-center group w-full")
this.SetClass(classes)
return button
}
public OnClickWithLoading(

63
UI/Base/SubtleLink.svelte Normal file
View file

@ -0,0 +1,63 @@
<script lang="ts">
import {createEventDispatcher, onMount} from "svelte";
import BaseUIElement from "../BaseUIElement";
import Img from "./Img";
export let imageUrl: string | BaseUIElement = undefined
export let href: string
export let newTab = false
export let options: {
imgSize?: string
// extraClasses?: string
} = {}
let imgElem: HTMLElement;
let imgClasses = "block justify-center shrink-0 mr-4 " + (options?.imgSize ?? "h-11 w-11");
onMount(() => {
// Image
if (imgElem && imageUrl) {
let img: BaseUIElement
if ((imageUrl ?? "") === "") {
img = undefined
} else if (typeof imageUrl !== "string") {
img = imageUrl?.SetClass(imgClasses)
}
if (img) imgElem.replaceWith(img.ConstructElement())
}
})
</script>
<a
class={(options.extraClasses??"") + 'flex hover:shadow-xl transition-[color,background-color,box-shadow] hover:bg-unsubtle cursor-pointer'}
{href}
target={newTab ? "_blank" : ""}}
>
<slot name="image">
{#if imageUrl !== undefined}
{#if typeof imageUrl === "string"}
<Img src={imageUrl} class={imgClasses}></Img>
{:else }
<template bind:this={imgElem} />
{/if}
{/if}
</slot>
<slot/>
</a>
<style lang="scss">
span,
a {
@apply flex p-3 my-2 py-4 rounded-lg shrink-0;
@apply items-center w-full no-underline;
@apply bg-subtle text-black;
:global(span) {
@apply block text-ellipsis;
}
}
</style>

View file

@ -19,10 +19,10 @@
<div class="tabbedgroup w-full h-full">
<TabGroup class="h-full w-full flex flex-col" defaultIndex={1}
on:change={(e) =>{if(e.detail >= 0){tab.setData( e.detail); }} }>
<div class="tablist flex bg-gray-300 items-center justify-between sticky top-0">
<div class="interactive flex items-center justify-between sticky top-0">
<TabList class="flex flex-wrap">
{#if $$slots.title1}
<Tab class={({selected}) => "tab "+(selected ? "tab-selected" : "tab-unselected")}>
<Tab class={({selected}) => "tab "+(selected ? "selected" : "secondary")}>
<div bind:this={tabElements[0]} class="flex">
<slot name="title0">
Tab 0
@ -31,28 +31,28 @@
</Tab>
{/if}
{#if $$slots.title1}
<Tab class={({selected}) => "tab "+(selected ? "tab-selected" : "tab-unselected")}>
<Tab class={({selected}) => "tab "+(selected ? "selected" : "secondary")}>
<div bind:this={tabElements[1]} class="flex">
<slot name="title1"/>
</div>
</Tab>
{/if}
{#if $$slots.title2}
<Tab class={({selected}) => "tab "+(selected ? "tab-selected" : "tab-unselected")}>
<Tab class={({selected}) => "tab "+(selected ? "selected" : "secondary")}>
<div bind:this={tabElements[2]} class="flex">
<slot name="title2"/>
</div>
</Tab>
{/if}
{#if $$slots.title3}
<Tab class={({selected}) => "tab "+(selected ? "tab-selected" : "tab-unselected")}>
<Tab class={({selected}) => "tab "+(selected ? "selected" : "secondary")}>
<div bind:this={tabElements[3]} class="flex">
<slot name="title3"/>
</div>
</Tab>
{/if}
{#if $$slots.title4}
<Tab class={({selected}) => "tab "+(selected ? "tab-selected" : "tab-unselected")}>
<Tab class={({selected}) => "tab "+(selected ? "selected" : "secondary")}>
<div bind:this={tabElements[4]} class="flex">
<slot name="title4"/>
</div>
@ -110,15 +110,6 @@
display: flex;
}
:global(.tab-selected) {
/**
For some reason, the exported tailwind style takes priority in production (but not in development)
As the tabs are buttons, tailwind restyles them
*/
background-color: var(--catch-detail-color) !important;
color: var(--catch-detail-color-contrast) !important;
}
:global(.tab-selected svg) {
fill: var(--catch-detail-color-contrast);
}

View file

@ -3,36 +3,30 @@
import {UIEventSource} from "../../Logic/UIEventSource"
import Constants from "../../Models/Constants"
import Svg from "../../Svg"
import SubtleButton from "../Base/SubtleButton.svelte"
import ToSvelte from "../Base/ToSvelte.svelte"
import Translations from "../i18n/Translations"
import SubtleLink from "../Base/SubtleLink.svelte";
import Tr from "../Base/Tr.svelte";
export let userDetails: UIEventSource<UserDetails>
const t = Translations.t.general.morescreen
console.log($userDetails.csCount < 50)
</script>
<div>
{#if $userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock}
<SubtleButton
options={{
url: "https://github.com/pietervdvn/MapComplete/issues",
newTab: true,
}}
<SubtleLink
url="https://github.com/pietervdvn/MapComplete/issues"
newTab={true}
>
<span slot="message">{t.requestATheme.toString()}</span>
</SubtleButton>
<Tr t={t.requestATheme}/>
</SubtleLink>
{:else}
<SubtleButton
options={{
url: "https://pietervdvn.github.io/mc/legacy/070/customGenerator.html",
}}
>
<span slot="image">
<ToSvelte construct={Svg.pencil_svg().SetClass("h-11 w-11 mx-4 bg-red")}/>
</span>
<span slot="message">{t.createYourOwnTheme.toString()}</span>
</SubtleButton>
<SubtleLink href="https://pietervdvn.github.io/mc/legacy/070/customGenerator.html">
<span slot="image">
<ToSvelte construct={Svg.pencil_svg().SetClass("h-11 w-11 mx-4 bg-red")}/>
</span>
<Tr t={t.createYourOwnTheme}/>
</SubtleLink>
{/if}
</div>

View file

@ -109,51 +109,6 @@ export default class MoreScreen extends Combine {
])
}
/**
* Creates a button linking to the given theme
* @private
*/
public static createLinkButton(
state: {
locationControl?: UIEventSource<Loc>
layoutToUse?: LayoutConfig
},
layout: {
id: string
icon: string
title: any
shortDescription: any
definition?: any
mustHaveLanguage?: boolean
},
isCustom: boolean = false
): BaseUIElement {
const url = MoreScreen.createUrlFor(layout, isCustom, state)
let content = new Combine([
new Translation(
layout.title,
!isCustom && !layout.mustHaveLanguage ? "themes:" + layout.id + ".title" : undefined
),
new Translation(layout.shortDescription)?.SetClass("subtle") ?? "",
]).SetClass("overflow-hidden flex flex-col")
if (state.layoutToUse === undefined) {
// Currently on the index screen: we style the buttons equally large
content = new Combine([content]).SetClass("flex flex-col justify-center h-24")
}
return new SubtleButton(layout.icon, content, { url, newTab: false })
}
public static CreateProffessionalSerivesButton() {
const t = Translations.t.professional.indexPage
return new Combine([
new Title(t.hook, 4),
t.hookMore,
new SubtleButton(undefined, t.button, { url: "./professional.html" }),
]).SetClass("flex flex-col border border-gray-300 p-2 rounded-lg")
}
public static MatchesLayout(
layout: {
id: string

View file

@ -17,6 +17,7 @@
import SubtleButton from "../Base/SubtleButton.svelte"
import ToSvelte from "../Base/ToSvelte.svelte"
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte";
export let search: UIEventSource<string>
@ -30,14 +31,12 @@
search.setData("")
}}
>
<span>
<SubtleButton>
<span slot="image">
<ToSvelte construct={Svg.search_disable_svg().SetClass("w-6 mr-2")} />
</span>
<span slot="message">{t.noSearch.toString()}</span>
<Tr t={t.noSearch} slot="message"/>
</SubtleButton>
</span>
</button>
</span>

View file

@ -1,8 +1,9 @@
<script lang="ts">
import SubtleButton from "../Base/SubtleButton.svelte"
import Title from "../Base/Title"
import ToSvelte from "../Base/ToSvelte.svelte"
import Translations from "../i18n/Translations"
import SubtleLink from "../Base/SubtleLink.svelte";
import Tr from "../Base/Tr.svelte";
const t = Translations.t.professional.indexPage
</script>
@ -12,9 +13,9 @@
<span>
{t.hookMore.toString()}
</span>
<SubtleButton options={{ url: "./professional.html" }}>
<span slot="message">{t.button.toString()}</span>
</SubtleButton>
<SubtleLink href="./professional.html">
<Tr slot="message" t={t.button} />
</SubtleLink>
</div>
<style lang="scss">

View file

@ -1,114 +1,94 @@
<script lang="ts">
import SubtleButton from "../Base/SubtleButton.svelte"
import { Translation } from "../i18n/Translation"
import * as personal from "../../assets/themes/personal/personal.json"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Constants from "../../Models/Constants"
import type Loc from "../../Models/Loc"
import type { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import Tr from "../Base/Tr.svelte"
import {Translation} from "../i18n/Translation"
import * as personal from "../../assets/themes/personal/personal.json"
import {ImmutableStore, Store, UIEventSource} from "../../Logic/UIEventSource"
import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection"
import Constants from "../../Models/Constants"
import type Loc from "../../Models/Loc"
import type {LayoutInformation} from "../../Models/ThemeConfig/LayoutConfig"
import Tr from "../Base/Tr.svelte"
import SubtleLink from "../Base/SubtleLink.svelte";
export let theme: LayoutInformation
export let isCustom: boolean = false
export let userDetails: UIEventSource<UserDetails>
export let state: { osmConnection: OsmConnection; locationControl?: UIEventSource<Loc> }
export let theme: LayoutInformation
export let isCustom: boolean = false
export let userDetails: UIEventSource<UserDetails>
export let state: { osmConnection: OsmConnection; locationControl?: UIEventSource<Loc> }
$: title = new Translation(
theme.title,
!isCustom && !theme.mustHaveLanguage ? "themes:" + theme.id + ".title" : undefined
)
$: description = new Translation(theme.shortDescription)
// TODO: Improve this function
function createUrl(
layout: { id: string; definition?: string },
isCustom: boolean,
state?: { locationControl?: UIEventSource<{ lat; lon; zoom }>; layoutToUse?: { id } }
): Store<string> {
if (layout === undefined) {
return undefined
}
if (layout.id === undefined) {
console.error("ID is undefined for layout", layout)
return undefined
}
if (layout.id === state?.layoutToUse?.id) {
return undefined
}
const currentLocation = state?.locationControl
let path = window.location.pathname
// Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html'
path = path.substr(0, path.lastIndexOf("/"))
// Path will now contain '/dir/dir', or empty string in case of nothing
if (path === "") {
path = "."
}
let linkPrefix = `${path}/${layout.id.toLowerCase()}.html?`
if (location.hostname === "localhost" || location.hostname === "127.0.0.1" || location.port === "1234") {
// Redirect to 'theme.html?layout=* instead of 'layout.html'. This is probably a debug run, where the routing does not work
linkPrefix = `${path}/theme.html?layout=${layout.id}&`
}
if (isCustom) {
linkPrefix = `${path}/theme.html?userlayout=${layout.id}&`
}
let hash = ""
if (layout.definition !== undefined) {
hash = "#" + btoa(JSON.stringify(layout.definition))
}
return (
currentLocation?.map((currentLocation) => {
const params = [
["z", currentLocation?.zoom],
["lat", currentLocation?.lat],
["lon", currentLocation?.lon],
]
.filter((part) => part[1] !== undefined)
.map((part) => part[0] + "=" + part[1])
.join("&")
return `${linkPrefix}${params}${hash}`
}) ?? new ImmutableStore<string>(`${linkPrefix}`)
$: title = new Translation(
theme.title,
!isCustom && !theme.mustHaveLanguage ? "themes:" + theme.id + ".title" : undefined
)
}
$: description = new Translation(theme.shortDescription)
// TODO: Improve this function
function createUrl(
layout: { id: string; definition?: string },
isCustom: boolean,
state?: { locationControl?: UIEventSource<{ lat; lon; zoom }>; layoutToUse?: { id } }
): Store<string> {
if (layout === undefined) {
return undefined
}
if (layout.id === undefined) {
console.error("ID is undefined for layout", layout)
return undefined
}
if (layout.id === state?.layoutToUse?.id) {
return undefined
}
const currentLocation = state?.locationControl
let path = window.location.pathname
// Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html'
path = path.substr(0, path.lastIndexOf("/"))
// Path will now contain '/dir/dir', or empty string in case of nothing
if (path === "") {
path = "."
}
let linkPrefix = `${path}/${layout.id.toLowerCase()}.html?`
if (location.hostname === "localhost" || location.hostname === "127.0.0.1" || location.port === "1234") {
// Redirect to 'theme.html?layout=* instead of 'layout.html'. This is probably a debug run, where the routing does not work
linkPrefix = `${path}/theme.html?layout=${layout.id}&`
}
if (isCustom) {
linkPrefix = `${path}/theme.html?userlayout=${layout.id}&`
}
let hash = ""
if (layout.definition !== undefined) {
hash = "#" + btoa(JSON.stringify(layout.definition))
}
return (
currentLocation?.map((currentLocation) => {
const params = [
["z", currentLocation?.zoom],
["lat", currentLocation?.lat],
["lon", currentLocation?.lon],
]
.filter((part) => part[1] !== undefined)
.map((part) => part[0] + "=" + part[1])
.join("&")
return `${linkPrefix}${params}${hash}`
}) ?? new ImmutableStore<string>(`${linkPrefix}`)
)
}
let href = createUrl(theme, isCustom, state)
</script>
{#if theme.id !== personal.id || $userDetails.csCount > Constants.userJourney.personalLayoutUnlock}
<div>
<SubtleButton options={{ url: createUrl(theme, isCustom, state) }}>
<img slot="image" src={theme.icon} class="block h-11 w-11 bg-red mx-4" alt="" />
<span slot="message" class="message">
<span>
<Tr t={title}></Tr>
<span class="subtle">
<Tr t={description}></Tr>
<SubtleLink href={ $href }>
<img slot="image" src={theme.icon} class="block h-11 w-11 bg-red mx-4" alt=""/>
<span class="flex flex-col text-ellipsis overflow-hidden">
<Tr t={title}/>
<span class="subtle max-h-12">
<Tr t={description}/>
</span>
</span>
</span>
</SubtleButton>
</div>
</SubtleLink>
{/if}
<style lang="scss">
div {
@apply h-32 min-h-[8rem] max-h-32 text-ellipsis overflow-hidden;
span.message {
@apply flex flex-col justify-center h-24;
& > span {
@apply flex flex-col overflow-hidden;
span:nth-child(2) {
@apply text-[#999];
}
}
}
}
</style>

View file

@ -17,7 +17,9 @@
const t = Translations.t.general
const currentIds: Store<string[]> = state.installedUserThemes
const stableIds = Stores.ListStabilized<string>(currentIds)
let customThemes
$: customThemes = Utils.NoNull($stableIds.map((id) => state.GetUnofficialTheme(id)))
$: console.log("Custom themes are", customThemes)
</script>
<ThemesList

View file

@ -28,7 +28,6 @@ export class CheckBox extends InputElementMap<number[], boolean> {
*/
export default class CheckBoxes extends InputElement<number[]> {
private static _nextId = 0
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false)
private readonly value: UIEventSource<number[]>
private readonly _elements: BaseUIElement[]
@ -65,12 +64,12 @@ export default class CheckBoxes extends InputElement<number[]> {
const label = document.createElement("label")
label.htmlFor = input.id
label.appendChild(input)
label.appendChild(inputI.ConstructElement())
label.classList.add("block", "w-full", "p-2", "cursor-pointer", "bg-red")
const wrapper = document.createElement("div")
wrapper.classList.add("wrapper", "flex", "w-full", "border", "border-gray-400", "mb-1")
wrapper.appendChild(input)
wrapper.appendChild(label)
formTag.appendChild(wrapper)
@ -78,11 +77,9 @@ export default class CheckBoxes extends InputElement<number[]> {
input.checked = selectedValues.indexOf(i) >= 0
if (input.checked) {
wrapper.classList.remove("border-gray-400")
wrapper.classList.add("border-black")
wrapper.classList.add("checked")
} else {
wrapper.classList.add("border-gray-400")
wrapper.classList.remove("border-black")
wrapper.classList.remove("checked")
}
})

View file

@ -69,6 +69,7 @@
if (index.data === forceIndex) {
forceIndex = undefined;
}
top = Math.max(top, 0)
}
Stores.Chronic(50).addCallback(_ => stabilize());
@ -103,7 +104,7 @@
<div class="h-full absolute w-min right-0">
{#each $floors as floor, i}
<button style={`height: ${HEIGHT}px; width: ${HEIGHT}px`}
class={"border-2 border-gray-300 flex content-box justify-center items-center "+(i === (forceIndex ?? $index) ? "selected": "normal-background" )
class={"m-0 border-2 border-gray-300 flex content-box justify-center items-center "+(i === (forceIndex ?? $index) ? "selected": "" )
}
on:click={() => {forceIndex = i}}
> {floor}</button>
@ -119,11 +120,6 @@
<svelte:window on:mousemove={onMove} on:mouseup={unclick} />
<style>
.selected {
background: var(--subtle-detail-color);
font-weight: bold;
border-color: black;
}
.draggable {
user-select: none;

View file

@ -22,6 +22,7 @@
// The type changed -> reset some values
validator = Validators.get(type)
_value.setData(value.data ?? "")
console.log("REseting validated input, _value is ", _value.data, validator?.getFeedback(_value.data, getCountry))
feedback = feedback?.setData(validator?.getFeedback(_value.data, getCountry));
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type
}

View file

@ -44,8 +44,13 @@ export abstract class Validator {
/**
* Gets a piece of feedback. By default, validation.<type> will be used, resulting in a generic 'not a valid <type>'.
* However, inheritors might overwrite this to give more specific feedback
*
* Returns 'undefined' if the element is valid
*/
public getFeedback(s: string, requestCountry?: () => string): Translation {
public getFeedback(s: string, requestCountry?: () => string): Translation | undefined {
if(this.isValid(s)){
return undefined
}
const tr = Translations.t.validation[this.name]
if (tr !== undefined) {
return tr["feedback"]

View file

@ -10,6 +10,9 @@ export default class PhoneValidator extends Validator {
getFeedback(s: string, requestCountry?: () => string): Translation {
if(this.isValid(s, requestCountry)){
return undefined
}
const tr = Translations.t.validation.phone
const generic = tr.feedback
if(requestCountry){

View file

@ -230,7 +230,7 @@
{/each}
</span>
{/if}
<TagHint embedIn={tags => t.presetInfo.Subs({tags})} osmConnection={state.osmConnection}
<TagHint embedIn={tags => t.presetInfo.Subs({tags})} {state}
tags={new And(selectedPreset.preset.tags)}></TagHint>

View file

@ -1,35 +1,41 @@
<script lang="ts">
import { OsmConnection } from "../../Logic/Osm/OsmConnection";
import { TagsFilter } from "../../Logic/Tags/TagsFilter";
import FromHtml from "../Base/FromHtml.svelte";
import Constants from "../../Models/Constants.js";
import { Translation } from "../i18n/Translation";
import Tr from "../Base/Tr.svelte";
import { onDestroy } from "svelte";
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import FromHtml from "../Base/FromHtml.svelte";
import Constants from "../../Models/Constants.js";
import {Translation} from "../i18n/Translation";
import Tr from "../Base/Tr.svelte";
import {onDestroy} from "svelte";
import type {SpecialVisualizationState} from "../SpecialVisualization";
/**
* A 'TagHint' will show the given tags in a human readable form.
* Depending on the options, it'll link through to the wiki or might be completely hidden
*/
export let tags: TagsFilter;
export let osmConnection: OsmConnection;
/**
* If given, this function will be called to embed the given tags hint into this translation
*/
export let embedIn: (() => Translation) | undefined = undefined;
const userDetails = osmConnection.userDetails;
let linkToWiki = false;
onDestroy(osmConnection.userDetails.addCallbackAndRunD(userdetails => {
linkToWiki = userdetails.csCount > Constants.userJourney.tagsVisibleAndWikiLinked;
}));
let tagsExplanation = "";
$: tagsExplanation = tags?.asHumanString(linkToWiki, false, {});
/**
* A 'TagHint' will show the given tags in a human readable form.
* Depending on the options, it'll link through to the wiki or might be completely hidden
*/
export let tags: TagsFilter;
export let state: SpecialVisualizationState;
/**
* If given, this function will be called to embed the given tags hint into this translation
*/
export let embedIn: (() => Translation) | undefined = undefined;
const userDetails = state.osmConnection.userDetails;
let linkToWiki = false;
onDestroy(state.osmConnection.userDetails.addCallbackAndRunD(userdetails => {
linkToWiki = userdetails.csCount > Constants.userJourney.tagsVisibleAndWikiLinked;
}));
let tagsExplanation = "";
$: tagsExplanation = tags?.asHumanString(linkToWiki, false, {});
</script>
{#if $userDetails.loggedIn}
{#if embedIn === undefined}
<FromHtml src={tagsExplanation} />
{:else}
<Tr t={embedIn(tagsExplanation)} />
{/if}
<div>
{#if tags === undefined}
<slot name="no-tags">
No tags
</slot>
{:else if embedIn === undefined}
<FromHtml src={tagsExplanation}/>
{:else}
<Tr t={embedIn(tagsExplanation)}/>
{/if}
</div>
{/if}

View file

@ -1,51 +1,48 @@
<script lang="ts">
import { UIEventSource } from "../../../Logic/UIEventSource";
import { Translation } from "../../i18n/Translation";
import ValidatedInput from "../../InputElement/ValidatedInput.svelte";
import Tr from "../../Base/Tr.svelte";
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
import Inline from "./Inline.svelte";
import { createEventDispatcher, onDestroy } from "svelte";
import InputHelper from "../../InputElement/InputHelper.svelte";
import type { Feature } from "geojson";
import {UIEventSource} from "../../../Logic/UIEventSource";
import {Translation} from "../../i18n/Translation";
import ValidatedInput from "../../InputElement/ValidatedInput.svelte";
import Tr from "../../Base/Tr.svelte";
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
import Inline from "./Inline.svelte";
import {createEventDispatcher, onDestroy} from "svelte";
import InputHelper from "../../InputElement/InputHelper.svelte";
import type {Feature} from "geojson";
export let value: UIEventSource<string>;
export let config: TagRenderingConfig;
export let tags: UIEventSource<Record<string, string>>;
export let value: UIEventSource<string>;
export let config: TagRenderingConfig;
export let tags: UIEventSource<Record<string, string>>;
export let feature: Feature = undefined;
let placeholder = config.freeform?.placeholder
$: {
placeholder = config.freeform?.placeholder
}
export let feature: Feature = undefined;
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined);
let placeholder = config.freeform?.placeholder
$: {
placeholder = config.freeform?.placeholder
}
let dispatch = createEventDispatcher<{ "selected" }>();
onDestroy(value.addCallbackD(() => {dispatch("selected")}))
function getCountry() {
return tags.data["_country"]
}
export let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined);
let dispatch = createEventDispatcher<{ "selected" }>();
onDestroy(value.addCallbackD(() => {
dispatch("selected")
}))
function getCountry() {
return tags.data["_country"]
}
</script>
<div class="inline-flex flex-col">
{#if config.freeform.inline}
<Inline key={config.freeform.key} {tags} template={config.render}>
<ValidatedInput {feedback} {getCountry} on:selected={() => dispatch("selected")}
type={config.freeform.type} {placeholder} {value}></ValidatedInput>
</Inline>
{:else}
<ValidatedInput {feedback} {getCountry} on:selected={() => dispatch("selected")}
type={config.freeform.type} {placeholder} {value}></ValidatedInput>
{#if config.freeform.inline}
<Inline key={config.freeform.key} {tags} template={config.render}>
<ValidatedInput {feedback} {getCountry} on:selected={() => dispatch("selected")}
type={config.freeform.type} {placeholder} {value}></ValidatedInput>
</Inline>
{:else}
<ValidatedInput {feedback} {getCountry} on:selected={() => dispatch("selected")}
type={config.freeform.type} {placeholder} {value}></ValidatedInput>
{/if}
<InputHelper args={config.freeform.helperArgs} {feature} type={config.freeform.type} {value}/>
{/if}
<InputHelper args={config.freeform.helperArgs} {feature} type={config.freeform.type} {value}/>
</div>
{#if $feedback !== undefined}
<div class="alert">
<Tr t={$feedback} />
</div>
{/if}

View file

@ -143,7 +143,7 @@
<TagRenderingQuestion
config={_firstQuestion} {layer} {selectedElement} {state} {tags}
on:saved={() => {skip(_firstQuestion, true)}}>
<button on:click={() => {skip(_firstQuestion)} }
<button class="secondary" on:click={() => {skip(_firstQuestion)} }
slot="cancel">
<Tr t={Translations.t.general.skip}></Tr>
</button>

View file

@ -4,7 +4,7 @@
import type {Feature} from "geojson";
import type {SpecialVisualizationState} from "../../SpecialVisualization";
import TagRenderingAnswer from "./TagRenderingAnswer.svelte";
import {PencilAltIcon} from "@rgossiaux/svelte-heroicons/solid";
import {PencilAltIcon, XCircleIcon} from "@rgossiaux/svelte-heroicons/solid";
import TagRenderingQuestion from "./TagRenderingQuestion.svelte";
import {onDestroy} from "svelte";
import Tr from "../../Base/Tr.svelte";
@ -33,10 +33,10 @@
if (editMode && htmlElem !== undefined) {
// EditMode switched to true, so the person wants to make a change
// Make sure that the question is in the scrollview!
// Some delay is applied to give Svelte the time to render the _question_
window.setTimeout(() => {
Utils.scrollIntoView(htmlElem)
}, 50)
}
@ -68,23 +68,28 @@
</script>
<div bind:this={htmlElem}>
<div bind:this={htmlElem} class="">
{#if config.question && $editingEnabled}
{#if editMode}
<TagRenderingQuestion {config} {tags} {selectedElement} {state} {layer}>
<button slot="cancel" on:click={() => {editMode = false}}>
<Tr t={Translations.t.general.cancel}/>
</button>
</TagRenderingQuestion>
<div class="m-1 mx-2">
<TagRenderingQuestion {config} {tags} {selectedElement} {state} {layer}>
<button slot="cancel" class="secondary" on:click={() => {editMode = false}}>
<Tr t={Translations.t.general.cancel}/>
</button>
<XCircleIcon slot="upper-right" class="w-8 h-8" on:click={() => {editMode = false}}/>
</TagRenderingQuestion>
</div>
{:else}
<div class="flex justify-between">
<div class="flex justify-between low-interaction items-center m-1 mx-2 p-1 px-2 rounded">
<TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer}/>
<button on:click={() => {editMode = true}} class="shrink-0 w-6 h-6 rounded-full subtle-background p-1">
<button on:click={() => {editMode = true}} class="shrink-0 w-8 h-8 rounded-full p-1 secondary self-start">
<PencilAltIcon/>
</button>
</div>
{/if}
{:else }
<TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer}/>
<div class="m-1 p-1 px-2 mx-2">
<TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer}/>
</div>
{/if}
</div>

View file

@ -75,7 +75,7 @@ let mappingIsHidden: Store<boolean> = tags.map(tags => {
{#if $matchesTerm && !$mappingIsHidden }
<label class="flex">
<label class={"flex "+ (mappingIsSelected ? "checked": "")}>
<slot/>
<TagRenderingMapping {mapping} {tags} {state} {selectedElement}
{layer}></TagRenderingMapping>

View file

@ -2,7 +2,6 @@
import {Store, UIEventSource} from "../../../Logic/UIEventSource";
import type {SpecialVisualizationState} from "../../SpecialVisualization";
import Tr from "../../Base/Tr.svelte";
import If from "../../Base/If.svelte";
import type {Feature} from "geojson";
import type {Mapping} from "../../../Models/ThemeConfig/TagRenderingConfig";
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
@ -10,15 +9,15 @@
import FreeformInput from "./FreeformInput.svelte";
import Translations from "../../i18n/Translations.js";
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction";
import {createEventDispatcher, onDestroy} from "svelte";
import {createEventDispatcher} from "svelte";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import {ExclamationIcon} from "@rgossiaux/svelte-heroicons/solid";
import SpecialTranslation from "./SpecialTranslation.svelte";
import TagHint from "../TagHint.svelte";
import LoginToggle from "../../Base/LoginToggle.svelte";
import SubtleButton from "../../Base/SubtleButton.svelte";
import Loading from "../../Base/Loading.svelte";
import TagRenderingMappingInput from "./TagRenderingMappingInput.svelte";
import {Translation} from "../../i18n/Translation";
export let config: TagRenderingConfig;
export let tags: UIEventSource<Record<string, string>>;
@ -26,13 +25,16 @@
export let state: SpecialVisualizationState;
export let layer: LayerConfig;
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined);
feedback.addCallbackAndRunD(f => console.trace("Feedback is now", f.txt))
// Will be bound if a freeform is available
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key]);
let selectedMapping: number = undefined;
let checkedMappings: boolean[];
$: {
mappings = config.mappings?.filter(m => {
if(typeof m.hideInAnswer === "boolean"){
if (typeof m.hideInAnswer === "boolean") {
return !m.hideInAnswer
}
return m.hideInAnswer.matchesProperties(tags.data)
@ -43,9 +45,10 @@
}
if (config.freeform?.key) {
freeformInput.setData(tags.data[config.freeform.key]);
}else{
} else {
freeformInput.setData(undefined)
}
feedback.setData(undefined)
}
let selectedTags: TagsFilter = undefined;
@ -108,24 +111,24 @@
).catch(console.error);
}
let featureSwitchIsTesting = state.featureSwitchIsTesting
let featureSwitchIsDebugging = state.featureSwitches.featureSwitchIsDebugging
let showTags = state.userRelatedState.showTags
</script>
{#if config.question !== undefined}
<div class="border border-black subtle-background flex flex-col">
<If condition={state.featureSwitchIsTesting}>
<div class="flex justify-between">
<span>
<SpecialTranslation t={config.question} {tags} {state} {layer} feature={selectedElement}></SpecialTranslation>
</span>
<span class="alert">{config.id}</span>
</div>
<SpecialTranslation slot="else" t={config.question} {tags} {state} {layer}
feature={selectedElement}></SpecialTranslation>
</If>
<div class="interactive border-interactive p-1 px-2 flex flex-col">
<div class="flex justify-between">
<span class="font-bold">
<SpecialTranslation t={config.question} {tags} {state} {layer}
feature={selectedElement}></SpecialTranslation>
</span>
<slot name="upper-right"/>
</div>
{#if config.questionhint}
<div class="subtle">
<div>
<SpecialTranslation t={config.questionhint} {tags} {state} {layer}
feature={selectedElement}></SpecialTranslation>
</div>
@ -140,7 +143,7 @@
{#if config.freeform?.key && !(mappings?.length > 0)}
<!-- There are no options to choose from, simply show the input element: fill out the text field -->
<FreeformInput {config} {tags} feature={selectedElement} value={freeformInput}/>
<FreeformInput {config} {tags} {feedback} feature={selectedElement} value={freeformInput}/>
{:else if mappings !== undefined && !config.multiAnswer}
<!-- Simple radiobuttons as mapping -->
<div class="flex flex-col">
@ -176,7 +179,7 @@
<label class="flex">
<input type="checkbox" name={"mappings-checkbox-"+config.id+"-"+config.mappings?.length}
bind:checked={checkedMappings[config.mappings.length]}>
<FreeformInput {config} {tags} feature={selectedElement} value={freeformInput}
<FreeformInput {config} {tags} {feedback} feature={selectedElement} value={freeformInput}
on:selected={() => checkedMappings[config.mappings.length] = true}/>
</label>
{/if}
@ -189,24 +192,34 @@
<img slot="image" src="./assets/svg/login.svg" class="w-8 h-8"/>
<Tr t={Translations.t.general.loginToStart} slot="message"></Tr>
</SubtleButton>
<TagHint osmConnection={state.osmConnection} tags={selectedTags}></TagHint>
<div>
<div class="flex justify-end">
<!-- TagRenderingQuestion-buttons -->
<slot name="cancel"></slot>
{#if selectedTags !== undefined}
<button on:click={onSave}>
<Tr t={Translations.t.general.save}></Tr>
</button>
{:else }
<div class="inline-flex w-6 h-6">
<!-- Invalid value; show an inactive button or something like that-->
<ExclamationIcon/>
{#if $feedback !== undefined}
<div class="alert">
<Tr t={$feedback}/>
</div>
{/if}
<slot name="cancel"></slot>
<button on:click={onSave} class={selectedTags === undefined ? "disabled" : "button-shadow"}>
<Tr t={Translations.t.general.save}></Tr>
</button>
</div>
{#if $showTags === "yes" || $showTags === "always" || $featureSwitchIsTesting || $featureSwitchIsDebugging}
<span class="flex justify-between flex-wrap">
<TagHint {state} tags={selectedTags}></TagHint>
<span class="flex flex-wrap">
{#if $featureSwitchIsTesting}
Testmode &nbsp;
{/if}
{#if $featureSwitchIsTesting || $featureSwitchIsDebugging}
<span class="subtle">{config.id}</span>
{/if}
</span>
</span>
{/if}
</LoginToggle>
</div>
{/if}

View file

@ -63,6 +63,7 @@ export interface SpecialVisualizationState {
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
readonly userRelatedState: {
readonly showTags: UIEventSource<"no" | undefined | "always" | "yes">;
readonly mangroveIdentity: MangroveIdentity
readonly showAllQuestionsAtOnce: UIEventSource<boolean>
readonly preferencesAsTags: Store<Record<string, string>>

View file

@ -1,6 +1,9 @@
<script lang="ts">
import Svg from "../Svg";
import Loading from "./Base/Loading.svelte";
import ToSvelte from "./Base/ToSvelte.svelte";
</script>
<div>
@ -11,11 +14,14 @@
<div class="normal-background">
<h2>Normal background</h2>
There are a few styles, such as the <span class="literal-code">normal-background</span>-style which is used if there is
There are a few styles, such as the <span class="literal-code">normal-background</span>-style which is used if
there is
nothing special going on. Some general information, with at most <a href="https://example.com" target="_blank">a
link to someplace</a>.
<span class="alert">Alert: something went wrong</span>
<span class="thanks">Thank you! Operation successful</span>
<ToSvelte construct={Svg.login_svg().SetClass("w-12 h-12")}/>
<Loading>Loading...</Loading>
</div>
<div class="low-interaction flex flex-col">
@ -24,35 +30,91 @@
There are <span class="literal-code">low-interaction</span> areas, where some buttons might appear.
</p>
<button class="btn">Main action</button>
<button class="btn-secondary">Secondary action</button>
<button class="btn-disabled">Disabled</button>
<div class="flex">
<button>
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")}/>
Main action
</button>
<button class="disabled">
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")}/>
Main action (disabled)
</button>
</div>
<div class="flex">
<button class="secondary">
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")}/>
Secondary action
</button>
<button class="secondary disabled">
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")}/>
Secondary action (disabled)
</button>
</div>
<input type="text">
<div>
<input id="html" name="fav_language" type="radio" value="HTML">
<label for="html">HTML</label><br>
<input id="css" name="fav_language" type="radio" value="CSS">
<label for="css">CSS</label><br>
<input id="javascript" name="fav_language" type="radio" value="JavaScript">
<label for="javascript">JavaScript</label>
<label for="html" class="checked">
<input id="html" name="fav_language" type="radio" value="HTML">
HTML (mimicks a <span class="literal-code">checked</span>-element)</label>
<label for="css">
<input id="css" name="fav_language" type="radio" value="CSS">
CSS</label>
<label for="javascript">
<input id="javascript" name="fav_language" type="radio" value="JavaScript">
<ToSvelte construct={Svg.community_svg().SetClass("w-8 h-8")}/>
JavaScript</label>
</div>
<span class="alert">Alert: something went wrong</span>
<span class="thanks">Thank you! Operation successful</span>
<ToSvelte construct={Svg.login_svg().SetClass("w-12 h-12")}/>
<Loading>Loading...</Loading>
</div>
<div class="interactive flex flex-col">
<h2>Interactive area</h2>
<p>
There are <span class="literal-code">interactive</span> areas, where some buttons might appear.
There are <span class="literal-code">interactive</span> areas, where many buttons and input elements
will appear.
</p>
<button class="btn">Main action</button>
<button class="btn-secondary">Secondary action</button>
<button class="btn-disabled">Disabled</button>
<div class="flex">
<button>
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")}/>
Main action
</button>
<button class="disabled">
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")}/>
Main action (disabled)
</button>
</div>
<div class="flex">
<button class="secondary">
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")}/>
Secondary action
</button>
<button class="secondary disabled">
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")}/>
Secondary action (disabled)
</button>
</div>
<span class="alert">Alert: something went wrong</span>
<span class="thanks">Thank you! Operation successful</span>
<ToSvelte construct={Svg.login_svg().SetClass("w-12 h-12")}/>
<Loading>Loading...</Loading>
<div>
<label for="html0">
<input id="html0" name="fav_language" type="radio" value="HTML">
HTML</label>
<label for="css0">
<input id="css0" name="fav_language" type="radio" value="CSS">
CSS</label>
<label for="javascript0">
<input id="javascript0" name="fav_language" type="radio" value="JavaScript">
JavaScript
</label>
</div>
</div>
</div>