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

@ -14,6 +14,7 @@ import usersettings from "../../assets/generated/layers/usersettings.json"
import Locale from "../../UI/i18n/Locale"
import LinkToWeblate from "../../UI/Base/LinkToWeblate"
import FeatureSwitchState from "./FeatureSwitchState"
import Constants from "../../Models/Constants";
/**
* The part of the state which keeps track of user-related stuff, e.g. the OSM-connection,
@ -32,6 +33,9 @@ export default class UserRelatedState {
public readonly installedUserThemes: Store<string[]>
public readonly showAllQuestionsAtOnce: UIEventSource<boolean>
public readonly showTags: UIEventSource<"no" | undefined | "always" | "yes">;
public readonly homeLocation: FeatureSource
/**
@ -88,6 +92,7 @@ export default class UserRelatedState {
"Either 'true' or 'false'. If set, all questions will be shown all at once",
})
)
this.showTags = <UIEventSource<any>> this.osmConnection.GetPreference("show_tags")
this.mangroveIdentity = new MangroveIdentity(
this.osmConnection.GetLongPreference("identity", "mangrove")
@ -254,6 +259,10 @@ export default class UserRelatedState {
_supports_sharing: window.navigator.share ? "yes" : "no"
})
for (const key in Constants.userJourney) {
amendedPrefs.data["__userjourney_"+key] = Constants.userJourney[key]
}
const osmConnection = this.osmConnection
osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => {
for (const k in newPrefs) {

View file

@ -637,6 +637,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
or: [
"__featureSwitchIsTesting=true",
"__featureSwitchIsDebugging=true",
"mapcomplete-show_tags=full",
"mapcomplete-show_debug=yes",
],
},

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>

View file

@ -0,0 +1,24 @@
[
{
"path": "translate_disabled.svg",
"license": "CC-BY-SA 3.0",
"authors": [
"MGalloway (WMF)"
],
"sources": [
"https://commons.wikimedia.org/wiki/File:OOjs_UI_icon_language-ltr.svg"
]
},
{
"path": "translate_mobile.svg",
"license": "CC-BY-SA 3.0",
"authors": [
"MGalloway (WMF)",
"@ tyskrat"
],
"sources": [
"https://commons.wikimedia.org/wiki/File:OOjs_UI_icon_language-ltr.svg",
"https://www.onlinewebfonts.com/icon/1059"
]
}
]

View file

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="375px"
height="375px"
viewBox="0 0 375 375"
version="1.1"
id="svg7"
sodipodi:docname="translate_disabled.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs11" />
<sodipodi:namedview
id="namedview9"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="2.04"
inkscape:cx="187.7451"
inkscape:cy="187.5"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg7" />
<g
id="surface1"
transform="translate(0.37913603,-36.477482)">
<path
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 178.26953,214.03906 c -24.23047,-23.65625 -45.57812,-44.42187 -57.6914,-92.30859 h 84.80468 V 85.960938 H 121.15234 V 38.652344 H 84.808594 V 86.539062 H 0 v 35.769528 h 86.539062 c 0,0 -0.578124,6.92188 -1.730468,12.11328 C 72.691406,181.73047 58.269531,211.73047 0,241.15234 l 12.117188,35.76953 C 67.5,247.5 96.347656,210.57812 109.03906,169.61719 c 12.11328,31.15234 32.88281,56.53515 56.53906,79.61328 z m 0,0"
id="path2" />
<path
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 290.19141,98.078125 h -48.46094 l -84.8086,238.269535 h 36.34766 L 217.5,264.80859 h 96.92187 l 24.23047,71.53907 H 375 Z m -60.57422,130.960935 36.34375,-95.1914 36.34765,95.76953 z m 0,0"
id="path4" />
</g>
<g
id="surface1-3"
transform="matrix(1.3884336,0,0,1.3884336,-154.57939,-335.40822)"
style="fill:#ff0000;fill-opacity:1">
<path
style="fill:#ff0000;fill-opacity:1;stroke:#b40000;stroke-width:34.2679;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1"
d="M 364.47831,491.73933 252.63509,379.89612"
id="path826" />
<path
style="fill:#ff0000;fill-opacity:1;stroke:#b40000;stroke-width:34.2684;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1"
d="M 364.64638,379.35739 252.80473,491.199"
id="path828" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="492.01099"
height="848.2948"
viewBox="0 0 492.01099 848.2948"
version="1.1"
id="svg7"
sodipodi:docname="translate_mobile.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs11" />
<sodipodi:namedview
id="namedview9"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="0.87893548"
inkscape:cx="192.27805"
inkscape:cy="365.78339"
inkscape:window-width="1920"
inkscape:window-height="995"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg7" />
<g
id="surface1"
transform="translate(62.542641,230.35576)">
<path
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 178.26953,214.03906 c -24.23047,-23.65625 -45.57812,-44.42187 -57.6914,-92.30859 h 84.80468 V 85.960938 H 121.15234 V 38.652344 H 84.808594 V 86.539062 H 0 v 35.769528 h 86.539062 c 0,0 -0.578124,6.92188 -1.730468,12.11328 C 72.691406,181.73047 58.269531,211.73047 0,241.15234 l 12.117188,35.76953 C 67.5,247.5 96.347656,210.57812 109.03906,169.61719 c 12.11328,31.15234 32.88281,56.53515 56.53906,79.61328 z m 0,0"
id="path2" />
<path
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 290.19141,98.078125 h -48.46094 l -84.8086,238.269535 h 36.34766 L 217.5,264.80859 h 96.92187 l 24.23047,71.53907 H 375 Z m -60.57422,130.960935 36.34375,-95.1914 36.34765,95.76953 z m 0,0"
id="path4" />
</g>
<path
d="M 94.914761,848.29478 H 397.00196 c 52.4529,0 95.00902,-40.3317 95.00902,-90.19258 V 90.25856 C 492.01098,40.44481 449.47371,0 397.09622,0 H 95.009016 C 42.565548,0 0,40.44481 0,90.25856 v 667.83421 c 0,49.84203 42.537271,90.20201 95.009016,90.20201 M 246.09974,817.59594 c -23.49776,0 -42.47129,-19.03951 -42.47129,-42.39589 0,-23.46007 18.94525,-42.43359 42.47129,-42.43359 23.44122,0 42.35819,18.94525 42.35819,42.43359 0,23.38466 -18.94525,42.39589 -42.35819,42.39589 M 181.36543,54.9695 h 129.3838 c 5.24058,0 9.51976,3.60997 9.51976,8.03052 0,4.43941 -4.26033,8.04938 -9.51976,8.04938 h -129.3838 c -5.18403,0 -9.51976,-3.60997 -9.51976,-8.04938 -9.4e-4,-4.42055 4.27918,-8.03052 9.51976,-8.03052 M 37.258993,156.74602 c 0,-7.48384 6.36221,-13.55386 14.21365,-13.55386 H 440.65144 c 7.87029,0 14.20423,6.04174 14.20423,13.55386 v 532.44636 c 0,7.45557 -6.33394,13.50674 -14.20423,13.50674 H 51.472643 c -7.85144,0 -14.21365,-6.05117 -14.21365,-13.50674 V 156.74602"
id="path2-2"
style="stroke-width:9.4255" />
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -6,6 +6,6 @@
<path style="fill:none;stroke-width:2.116667;stroke-linecap:round;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:0.988235;stroke-miterlimit:4;" d="M 25.405787 283.770823 L 23.286365 283.770823 " transform="matrix(14.173228,0,0,14.173228,0.0000135166,-3834.448583)"/>
<path style="fill:none;stroke-width:2.116667;stroke-linecap:round;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:0.988235;stroke-miterlimit:4;" d="M 13.229166 295.948823 L 13.229166 293.831329 " transform="matrix(14.173228,0,0,14.173228,0.0000135166,-3834.448583)"/>
<path style="fill:none;stroke-width:2.116667;stroke-linecap:round;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:0.988235;stroke-miterlimit:4;" d="M 13.229166 275.057488 L 13.229166 271.612392 " transform="matrix(14.173228,0,0,14.173228,0.0000135166,-3834.448583)"/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 235.789062 187.5 C 235.789062 214.167969 214.167969 235.789062 187.5 235.789062 C 160.832031 235.789062 139.210938 214.167969 139.210938 187.5 C 139.210938 160.832031 160.832031 139.210938 187.5 139.210938 C 214.167969 139.210938 235.789062 160.832031 235.789062 187.5 Z M 235.789062 187.5 "/>
<path style="stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 235.789062 187.5 C 235.789062 214.167969 214.167969 235.789062 187.5 235.789062 C 160.832031 235.789062 139.210938 214.167969 139.210938 187.5 C 139.210938 160.832031 160.832031 139.210938 187.5 139.210938 C 214.167969 139.210938 235.789062 160.832031 235.789062 187.5 Z M 235.789062 187.5 "/>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg xmlns="http://www.w3.org/2000/svg" width="375px" height="375px" viewBox="0 0 375 375" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 47.824219 142.808594 C 31.394531 142.578125 16.113281 151.210938 7.828125 165.40625 C -0.453125 179.597656 -0.453125 197.152344 7.828125 211.34375 C 16.113281 225.535156 31.394531 234.171875 47.824219 233.941406 L 327.667969 233.941406 C 344.101562 234.171875 359.382812 225.535156 367.664062 211.34375 C 375.945312 197.152344 375.945312 179.597656 367.664062 165.40625 C 359.382812 151.210938 344.101562 142.578125 327.667969 142.808594 Z M 47.824219 142.808594 "/>
<path d="M 47.824219 142.808594 C 31.394531 142.578125 16.113281 151.210938 7.828125 165.40625 C -0.453125 179.597656 -0.453125 197.152344 7.828125 211.34375 C 16.113281 225.535156 31.394531 234.171875 47.824219 233.941406 L 327.667969 233.941406 C 344.101562 234.171875 359.382812 225.535156 367.664062 211.34375 C 375.945312 197.152344 375.945312 179.597656 367.664062 165.40625 C 359.382812 151.210938 344.101562 142.578125 327.667969 142.808594 Z M 47.824219 142.808594 "/>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 751 B

After

Width:  |  Height:  |  Size: 678 B

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg xmlns="http://www.w3.org/2000/svg" width="375px" height="375px" viewBox="0 0 375 375" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 46.554688 142.492188 C 30.003906 142.261719 14.609375 150.957031 6.269531 165.253906 C -2.074219 179.550781 -2.074219 197.234375 6.269531 211.53125 C 14.609375 225.828125 30.003906 234.523438 46.554688 234.292969 L 328.445312 234.292969 C 344.996094 234.523438 360.390625 225.828125 368.734375 211.53125 C 377.074219 197.234375 377.074219 179.550781 368.734375 165.253906 C 360.390625 150.957031 344.996094 142.261719 328.445312 142.492188 Z M 46.554688 142.492188 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 186.34375 0.0078125 C 161.007812 0.386719 140.769531 21.21875 141.128906 46.554688 L 141.128906 328.445312 C 140.898438 344.996094 149.59375 360.390625 163.890625 368.734375 C 178.1875 377.074219 195.871094 377.074219 210.167969 368.734375 C 224.464844 360.390625 233.160156 344.996094 232.929688 328.445312 L 232.929688 46.554688 C 233.105469 34.152344 228.253906 22.203125 219.476562 13.433594 C 210.699219 4.664062 198.75 -0.175781 186.34375 0.0078125 Z M 186.34375 0.0078125 "/>
<path d="M 46.554688 142.492188 C 30.003906 142.261719 14.609375 150.957031 6.269531 165.253906 C -2.074219 179.550781 -2.074219 197.234375 6.269531 211.53125 C 14.609375 225.828125 30.003906 234.523438 46.554688 234.292969 L 328.445312 234.292969 C 344.996094 234.523438 360.390625 225.828125 368.734375 211.53125 C 377.074219 197.234375 377.074219 179.550781 368.734375 165.253906 C 360.390625 150.957031 344.996094 142.261719 328.445312 142.492188 Z M 46.554688 142.492188 "/>
<path d="M 186.34375 0.0078125 C 161.007812 0.386719 140.769531 21.21875 141.128906 46.554688 L 141.128906 328.445312 C 140.898438 344.996094 149.59375 360.390625 163.890625 368.734375 C 178.1875 377.074219 195.871094 377.074219 210.167969 368.734375 C 224.464844 360.390625 233.160156 344.996094 232.929688 328.445312 L 232.929688 46.554688 C 233.105469 34.152344 228.253906 22.203125 219.476562 13.433594 C 210.699219 4.664062 198.75 -0.175781 186.34375 0.0078125 Z M 186.34375 0.0078125 "/>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

514
index.css
View file

@ -26,28 +26,38 @@
--alert-color: #fee4d1;
--alert-foreground-color: var(--foreground-color);
--low-interaction-background: #eeeeee;
--low-interaction-foreground: black;
--low-interaction-contrast: #ff00ff;
--interactive-background: #dddddd;
--interactive-foreground: black;
--interactive-contrast: #ff00ff;
--button-background: #737373;
--button-foreground: white;
/**
* Base colour of interactive elements, mainly the 'subtle button'
* @deprecated
*/
--subtle-detail-color: #dbeafe;
--subtle-detail-color-contrast: black;
--subtle-detail-color-light-contrast: lightgrey;
/**
* A stronger variant of the 'subtle-detail-colour'
* Used as subtle button hover
*/
--unsubtle-detail-color: #bfdbfe;
--unsubtle-detail-color-contrast: black;
--catch-detail-color: #3a3aeb;
--catch-detail-color-contrast: white;
--catch-detail-color: black; /*#3a3aeb;*/
--catch-detail-foregroundcolor: white;
--catch-detail-color-contrast: #fb3afb;
--image-carousel-height: 350px;
}
/***********************************************************************\
* Various tweaks and settings to make some behaviours more predictable *
\***********************************************************************/
html,
body {
height: 100%;
@ -60,7 +70,6 @@ body {
font-family: "Helvetica Neue", Arial, sans-serif;
}
svg,
img {
box-sizing: content-box;
@ -68,97 +77,16 @@ img {
height: 100%;
}
.no-images img {
/* Used solely in 'imageAttribution' */
display: none;
}
.text-white a {
/* Used solely in 'imageAttribution' */
color: var(--background-color);
}
.weblate-link {
/* Weblate-links are the little translation icon next to translatable sentences. Due to their special nature, they are exempt from some rules */
}
a {
color: var(--foreground-color);
}
.btn {
line-height: 1.25rem;
--tw-text-opacity: 1;
color: var(--catch-detail-color-contrast);
--tw-bg-opacity: 1;
background-color: var(--catch-detail-color);
display: inline-flex;
border-radius: 1.5rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
padding-left: 1.25rem;
padding-right: 1.25rem;
font-size: large;
font-weight: bold;
transition: 100ms;
/*-- invisible border: rendered on hover*/
border: 3px solid var(--unsubtle-detail-color);
}
.btn:hover {
border: 3px solid var(--catch-detail-color);
}
.btn-secondary {
background-color: var(--catch-detail-color);
filter: saturate(0.5);
}
.btn-secondary:hover {
background-color: var(--catch-detail-color);
filter: unset;
}
.btn-disabled {
filter: saturate(0.3);
cursor: default;
}
.btn-disabled:hover {
border: 3px solid var(--unsubtle-detail-color);
}
.rounded-left-full {
border-bottom-left-radius: 999rem;
border-top-left-radius: 999rem;
}
.rounded-right-full {
border-bottom-right-radius: 999rem;
border-top-right-radius: 999rem;
}
.link-underline a {
text-decoration: underline 1px var(--foreground-color);
}
a.link-underline {
text-decoration: underline 1px var(--foreground-color);
}
.link-no-underline a {
text-decoration: none;
}
li {
margin-left: 0.5em;
padding-left: 0.2em;
margin-top: 0.1em;
}
li::marker {
content: "•";
}
h1 {
font-size: x-large;
margin-top: 0.6em;
@ -181,21 +109,22 @@ h3 {
font-weight: bold;
}
h3 {
font-size: larger;
margin-top: 0.6em;
margin-bottom: 0;
font-weight: bolder;
}
p {
padding-top: 0.1em;
}
li::marker {
content: "•";
input {
color: var(--foreground-color);
}
/************************* BIG CATEGORIES ********************************/
/**
* The main classes that dictate the structure of the entire app,
* and some interactive elements
*/
.subtle-background {
background: var(--subtle-detail-color);
color: var(--subtle-detail-color-contrast);
@ -206,10 +135,291 @@ li::marker {
color: var(--foreground-color);
}
.low-interaction {
background: var(--low-interaction-background);
color: var(--low-interaction-foreground)
}
.interactive {
background: var(--interactive-background);
color: var(--interactive-foreground)
}
.border-interactive {
border: 2px dashed var(--catch-detail-color-contrast);
border-radius: 0.5rem;
}
/******************* Styling of input elements **********************/
/**
* This very important section defines what the various input elements look like within the 'low-interaction' and 'interactive'-blocks
*/
button.disabled {
cursor: default;
border: 2px dashed var(--button-background);
background: unset;
color: unset;
box-shadow: none;
}
button.disabled:hover {
cursor: default;
border: 2px dashed var(--button-background);
background: unset;
color: unset;
}
button:hover {
border: 2px solid var(--catch-detail-color-contrast);
background-color: var(--catch-detail-color);
color: var(--catch-detail-foregroundcolor);
}
button:hover img {
background: var(--low-interaction-background);
border-radius: 100rem;
}
button {
display: inline-flex;
line-height: 1.25rem;
margin: 0.2rem;
padding: 0.4rem;
padding-left: 0.6rem;
padding-right: 0.6rem;
font-size: large;
font-weight: bold;
color: var(--button-foreground);
background: var(--button-background);
/*-- invisible border: rendered on hover*/
border: 2px solid var(--button-background);
border-radius: 0.5rem;
transition: all 250ms;
--tw-text-opacity: 1;
--tw-bg-opacity: 1;
}
button .button-shadow {
box-shadow: 0 5px 10px #88888888;
}
button.selected {
background-color: var(--catch-detail-color);
border-color: var(--catch-detail-color);
color: var(--catch-detail-foregroundcolor);
}
button.selected svg path {
fill: var(--catch-detail-foregroundcolor) !important;
}
button svg path {
fill: var(--button-foreground) !important;;
}
.interactive button.disabled svg path {
fill: var(--interactive-foreground) !important;;
}
.low-interaction button.disabled svg path {
fill: var(--low-interaction-foreground) !important;;
}
.normal-background button.disabled svg path {
fill: var(--foreground-color) !important;
}
button.disabled.secondary:hover {
background: unset;
color: unset;
}
button.secondary {
background: var(--low-interaction-background);
color: var(--low-interaction-foreground);
border-color: var(--button-background);
}
.interactive button.secondary {
background: var(--interactive-background);
color: var(--interactive-foreground);
}
button.secondary svg path {
fill: var(--low-interaction-foreground) !important;
transition: all 250ms;
}
button.secondary.disabled {
background: unset;
color: var(--low-interaction-foreground);
}
button.secondary.disabled svg path {
fill: var(--low-interaction-foreground) !important;
}
button.secondary:hover {
background-color: var(--catch-detail-color);
color: var(--catch-detail-foregroundcolor);
border-color: var(--catch-detail-color-contrast);
}
button.secondary:hover svg path {
fill: var(--catch-detail-foregroundcolor) !important;;
}
button.secondary.disabled:hover svg path {
fill: var(--low-interaction-foreground) !important;;
}
label {
/**
* Label should _contain_ the input element
*/
border: 2px solid var(--interactive-background);
padding: 0.25rem;
padding-right: 0.5rem;
padding-left: 0.5rem;
border-radius: 0.5rem;
background-color: var(--low-interaction-background);
width: 100%;
display: block;
box-sizing: border-box;
transition: all 250ms;
}
label:hover {
background-color: var(--catch-detail-color);
color: var(--catch-detail-foregroundcolor);
border: 2px solid var(--interactive-contrast)
}
label img {
padding: 0.25rem;
border-radius: 0.25rem;
background: var(--low-interaction-background);
}
label svg path {
transition: all 250ms;
}
label:hover svg path {
fill: var(--catch-detail-foregroundcolor) !important;
}
label.checked {
border: 2px solid var(--foreground-color);
}
/************************* OTHER CATEGORIES ********************************/
/**
* Smaller categories which convey some semantic information but don't define bigger blocks.
* As they are _semantic_ categories, they can be styled
*/
.thanks {
/* The class to indicate 'operation successful' or 'thank you for contributing' */
background-color: #43d904;
font-weight: bold;
border-radius: 1em;
margin: 0.25em;
text-align: center;
padding: 0.15em 0.3em;
}
.alert {
/* The class to convey important information, e.g. 'invalid', 'something went wrong', 'warning: testmode', ... */
background-color: var(--alert-color);
color: var(--alert-foreground-color);
font-weight: bold;
border-radius: 1em;
margin: 0.25em;
text-align: center;
padding: 0.15em 0.3em;
}
.subtle {
/* For all information that is not important for 99% of the users */
color: #999;
}
.link-underline .subtle a {
text-decoration: underline 1px #7193bb88;
color: #7193bb;
}
.literal-code {
/* A codeblock */
display: inline-block;
background-color: lightgray;
padding: 0.1rem;
padding-left: 0.35rem;
padding-right: 0.35rem;
word-break: break-word;
color: black;
box-sizing: border-box;
font-family: monospace;
}
.interactive .literal-code {
background-color: #b3b3b3;
}
/************************** UTILITY ************************/
/**
* Utility classes are there for a specific function to pin down browser behaviour (and cannot be changed)
*/
.text-white a {
/* Used solely in 'imageAttribution' and in many themes*/
color: var(--background-color);
}
.block-ruby {
display: block ruby;
}
.rounded-left-full {
border-bottom-left-radius: 999rem;
border-top-left-radius: 999rem;
}
.rounded-right-full {
border-bottom-right-radius: 999rem;
border-top-right-radius: 999rem;
}
.no-images img {
/* Used solely in 'imageAttribution' and in many themes for the label*/
display: none;
}
.link-underline a {
text-decoration: underline 1px var(--foreground-color);
}
a.link-underline {
text-decoration: underline 1px var(--foreground-color);
}
.link-no-underline a {
text-decoration: none;
}
.disable-links a {
pointer-events: none;
text-decoration: none !important;
@ -229,6 +439,13 @@ li::marker {
}
.zebra-table tr:nth-child(even) {
background-color: #f2f2f2;
}
/************************* MISC ELEMENTS *************************/
.selected svg:not(.noselect *) path.selectable {
/* A marker on the map gets the 'selected' class when it's properties are displayed
*/
@ -246,7 +463,6 @@ li::marker {
overflow: visible !important;
}
@-webkit-keyframes glowing-drop-shadow {
from {
filter: drop-shadow(5px 5px 60px rgb(128 128 128 / 0.6));
@ -256,37 +472,6 @@ li::marker {
}
}
/**************** GENERIC ****************/
.alert {
background-color: var(--alert-color);
color: var(--alert-foreground-color);
font-weight: bold;
border-radius: 1em;
margin: 0.25em;
text-align: center;
padding: 0.15em 0.3em;
}
.subtle {
color: #999;
}
.link-underline .subtle a {
text-decoration: underline 1px #7193bb88;
color: #7193bb;
}
.thanks {
background-color: #43d904;
font-weight: bold;
border-radius: 1em;
margin: 0.25em;
text-align: center;
padding: 0.15em 0.3em;
}
@keyframes slide {
/* This is the animation on the marker to add a new point - it slides through all the possible presets */
from {
@ -298,48 +483,6 @@ li::marker {
}
}
/***************** Info box (box containing features and questions ******************/
input {
color: var(--foreground-color);
}
.literal-code {
display: inline-block;
background-color: lightgray;
padding: 0.5em;
word-break: break-word;
color: black;
box-sizing: border-box;
}
/** Switch layout **/
.small-image img {
height: 1em;
max-width: 1em;
}
.small-image {
height: 1em;
max-width: 1em;
}
.slideshow-item img {
height: var(--image-carousel-height);
width: unset;
}
.animate-height {
transition: max-height 0.5s ease-in-out;
overflow-y: hidden;
}
.zebra-table tr:nth-child(even) {
background-color: #f2f2f2;
}
.glowing-shadow {
-webkit-animation: glowing 1s ease-in-out infinite alternate;
-moz-animation: glowing 1s ease-in-out infinite alternate;
@ -355,3 +498,20 @@ input {
}
}
/************************* LEGACY MARKER - CLEANUP BELOW ********************************/
.slideshow-item img {
/* Legacy: should be replace when the image element is ported to Svelte*/
height: var(--image-carousel-height);
width: unset;
}
.animate-height {
/* Legacy: should be replaced by headlessui disclosure in time */
transition: max-height 0.5s ease-in-out;
overflow-y: hidden;
}

View file

@ -11,7 +11,7 @@
"start": "npm run generate:layeroverview && npm run strt",
"strt": "vite --host",
"strttest": "export NODE_OPTIONS=--max_old_space_size=8364 && parcel serve test.html assets/templates/*.svg assets/templates/fonts/*.ttf",
"watch:css": "tailwindcss -i index.css -o css/index-tailwind-output.css --watch",
"watch:css": "tailwindcss -i index.css -o public/css/index-tailwind-output.css --watch",
"generate:css": "tailwindcss -i index.css -o public/css/index-tailwind-output.css",
"generate:doctests": "doctest-ts-improved . --ignore .*.spec.ts --ignore .*ConfigJson.ts",
"test:run-only": "vitest --run test",
@ -45,7 +45,7 @@
"weblate-add-upstream": "git remote add weblate-github git@github.com:weblate/MapComplete.git && git remote add weblate-hosted-core https://hosted.weblate.org/git/mapcomplete/core/ && git remote add weblate-hosted-layers https://hosted.weblate.org/git/mapcomplete/layers/",
"weblate-merge": "git remote update weblate-github; git merge weblate-github/weblate-mapcomplete-core weblate-github/weblate-mapcomplete-layers weblate-github/weblate-mapcomplete-layer-translations",
"weblate-fix-heavy": "git fetch weblate-hosted-layers; git fetch weblate-hosted-core; git merge weblate-hosted-layers/master weblate-hosted-core/master ",
"housekeeping": "git pull && npm run weblate-fix-heavy && npm run generate && npm run generate:docs && npm run generate:contributor-list && vite-node scripts/fetchLanguages.ts && npm run format && git add assets/ langs/ Docs/ **/*.ts Docs/* && git commit -m 'chore: automated housekeeping...'",
"housekeeping": "git pull && npx update-browserslist-db@latest && npm run weblate-fix-heavy && npm run generate && npm run generate:docs && npm run generate:contributor-list && vite-node scripts/fetchLanguages.ts && npm run format && git add assets/ langs/ Docs/ **/*.ts Docs/* && git commit -m 'chore: automated housekeeping...'",
"parseSchools": "vite-node scripts/schools/amendSchoolData.ts"
},
"keywords": [

View file

@ -825,6 +825,11 @@ video {
margin-bottom: 0.75rem;
}
.mx-2 {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
.mr-2 {
margin-right: 0.5rem;
}
@ -1055,6 +1060,10 @@ video {
max-height: 6rem;
}
.max-h-12 {
max-height: 3rem;
}
.min-h-\[8rem\] {
min-height: 8rem;
}
@ -1281,6 +1290,10 @@ video {
row-gap: 0.25rem;
}
.self-start {
align-self: flex-start;
}
.self-end {
align-self: flex-end;
}
@ -1307,6 +1320,10 @@ video {
white-space: nowrap;
}
.overflow-ellipsis {
text-overflow: ellipsis;
}
.text-ellipsis {
text-overflow: ellipsis;
}
@ -1443,11 +1460,6 @@ video {
background-color: rgb(219 234 254 / var(--tw-bg-opacity));
}
.bg-gray-300 {
--tw-bg-opacity: 1;
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
}
.bg-black {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
@ -1463,6 +1475,11 @@ video {
background-color: rgb(224 231 255 / var(--tw-bg-opacity));
}
.bg-gray-300 {
--tw-bg-opacity: 1;
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
}
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
@ -1792,6 +1809,12 @@ video {
transition-duration: 150ms;
}
.transition-shadow {
transition-property: box-shadow;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.ease-in-out {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
@ -1816,23 +1839,32 @@ video {
/* A colour scheme to indicate an error or warning */
--alert-color: #fee4d1;
--alert-foreground-color: var(--foreground-color);
--low-interaction-background: #eeeeee;
--low-interaction-foreground: black;
--low-interaction-contrast: #ff00ff;
--interactive-background: #dddddd;
--interactive-foreground: black;
--interactive-contrast: #ff00ff;
--button-background: #737373;
--button-foreground: white;
/**
* Base colour of interactive elements, mainly the 'subtle button'
* @deprecated
*/
--subtle-detail-color: #dbeafe;
--subtle-detail-color-contrast: black;
--subtle-detail-color-light-contrast: lightgrey;
/**
* A stronger variant of the 'subtle-detail-colour'
* Used as subtle button hover
*/
--unsubtle-detail-color: #bfdbfe;
--unsubtle-detail-color-contrast: black;
--catch-detail-color: #3a3aeb;
--catch-detail-color-contrast: white;
--catch-detail-color: black;
/*#3a3aeb;*/
--catch-detail-foregroundcolor: white;
--catch-detail-color-contrast: #fb3afb;
--image-carousel-height: 350px;
}
/***********************************************************************\
* Various tweaks and settings to make some behaviours more predictable *
\***********************************************************************/
html,
body {
height: 100%;
@ -1852,67 +1884,311 @@ img {
height: 100%;
}
.no-images img {
/* Used solely in 'imageAttribution' */
display: none;
li {
margin-left: 0.5em;
padding-left: 0.2em;
margin-top: 0.1em;
}
.text-white a {
/* Used solely in 'imageAttribution' */
color: var(--background-color);
li::marker {
content: "•";
}
.weblate-link {
/* Weblate-links are the little translation icon next to translatable sentences. Due to their special nature, they are exempt from some rules */
h1 {
font-size: x-large;
margin-top: 0.6em;
margin-bottom: 0.4em;
font-weight: bold;
}
a {
h2 {
font-size: large;
margin-top: 0.5em;
margin-bottom: 0.3em;
font-weight: bold;
}
h3 {
font-size: larger;
margin-top: 0.6em;
margin-bottom: 0;
font-weight: bold;
}
p {
padding-top: 0.1em;
}
input {
color: var(--foreground-color);
}
.btn {
line-height: 1.25rem;
--tw-text-opacity: 1;
color: var(--catch-detail-color-contrast);
--tw-bg-opacity: 1;
/************************* BIG CATEGORIES ********************************/
/**
* The main classes that dictate the structure of the entire app,
* and some interactive elements
*/
.subtle-background {
background: var(--subtle-detail-color);
color: var(--subtle-detail-color-contrast);
}
.normal-background {
background: var(--background-color);
color: var(--foreground-color);
}
.low-interaction {
background: var(--low-interaction-background);
color: var(--low-interaction-foreground)
}
.interactive {
background: var(--interactive-background);
color: var(--interactive-foreground)
}
.border-interactive {
border: 2px dashed var(--catch-detail-color-contrast);
border-radius: 0.5rem;
}
/******************* Styling of input elements **********************/
/**
* This very important section defines what the various input elements look like within the 'low-interaction' and 'interactive'-blocks
*/
button.disabled {
cursor: default;
border: 2px dashed var(--button-background);
background: unset;
color: unset;
box-shadow: none;
}
button.disabled:hover {
cursor: default;
border: 2px dashed var(--button-background);
background: unset;
color: unset;
}
button:hover {
border: 2px solid var(--catch-detail-color-contrast);
background-color: var(--catch-detail-color);
color: var(--catch-detail-foregroundcolor);
}
button:hover img {
background: var(--low-interaction-background);
border-radius: 100rem;
}
button {
display: inline-flex;
border-radius: 1.5rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
padding-left: 1.25rem;
padding-right: 1.25rem;
line-height: 1.25rem;
margin: 0.2rem;
padding: 0.4rem;
padding-left: 0.6rem;
padding-right: 0.6rem;
font-size: large;
font-weight: bold;
transition: 100ms;
color: var(--button-foreground);
background: var(--button-background);
/*-- invisible border: rendered on hover*/
border: 3px solid var(--unsubtle-detail-color);
border: 2px solid var(--button-background);
border-radius: 0.5rem;
transition: all 250ms;
--tw-text-opacity: 1;
--tw-bg-opacity: 1;
}
.btn:hover {
border: 3px solid var(--catch-detail-color);
button .button-shadow {
box-shadow: 0 5px 10px #88888888;
}
.btn-secondary {
button.selected {
background-color: var(--catch-detail-color);
-webkit-filter: saturate(0.5);
filter: saturate(0.5);
border-color: var(--catch-detail-color);
color: var(--catch-detail-foregroundcolor);
}
.btn-secondary:hover {
button.selected svg path {
fill: var(--catch-detail-foregroundcolor) !important;
}
button svg path {
fill: var(--button-foreground) !important;
}
.interactive button.disabled svg path {
fill: var(--interactive-foreground) !important;
}
.low-interaction button.disabled svg path {
fill: var(--low-interaction-foreground) !important;
}
.normal-background button.disabled svg path {
fill: var(--foreground-color) !important;
}
button.disabled.secondary:hover {
background: unset;
color: unset;
}
button.secondary {
background: var(--low-interaction-background);
color: var(--low-interaction-foreground);
border-color: var(--button-background);
}
.interactive button.secondary {
background: var(--interactive-background);
color: var(--interactive-foreground);
}
button.secondary svg path {
fill: var(--low-interaction-foreground) !important;
transition: all 250ms;
}
button.secondary.disabled {
background: unset;
color: var(--low-interaction-foreground);
}
button.secondary.disabled svg path {
fill: var(--low-interaction-foreground) !important;
}
button.secondary:hover {
background-color: var(--catch-detail-color);
-webkit-filter: unset;
filter: unset;
color: var(--catch-detail-foregroundcolor);
border-color: var(--catch-detail-color-contrast);
}
.btn-disabled {
-webkit-filter: saturate(0.3);
filter: saturate(0.3);
cursor: default;
button.secondary:hover svg path {
fill: var(--catch-detail-foregroundcolor) !important;
}
.btn-disabled:hover {
border: 3px solid var(--unsubtle-detail-color);
button.secondary.disabled:hover svg path {
fill: var(--low-interaction-foreground) !important;
}
label {
/**
* Label should _contain_ the input element
*/
border: 2px solid var(--interactive-background);
padding: 0.25rem;
padding-right: 0.5rem;
padding-left: 0.5rem;
border-radius: 0.5rem;
background-color: var(--low-interaction-background);
width: 100%;
display: block;
box-sizing: border-box;
transition: all 250ms;
}
label:hover {
background-color: var(--catch-detail-color);
color: var(--catch-detail-foregroundcolor);
border: 2px solid var(--interactive-contrast)
}
label img {
padding: 0.25rem;
border-radius: 0.25rem;
background: var(--low-interaction-background);
}
label svg path {
transition: all 250ms;
}
label:hover svg path {
fill: var(--catch-detail-foregroundcolor) !important;
}
label.checked {
border: 2px solid var(--foreground-color);
}
/************************* OTHER CATEGORIES ********************************/
/**
* Smaller categories which convey some semantic information but don't define bigger blocks.
* As they are _semantic_ categories, they can be styled
*/
.thanks {
/* The class to indicate 'operation successful' or 'thank you for contributing' */
background-color: #43d904;
font-weight: bold;
border-radius: 1em;
margin: 0.25em;
text-align: center;
padding: 0.15em 0.3em;
}
.alert {
/* The class to convey important information, e.g. 'invalid', 'something went wrong', 'warning: testmode', ... */
background-color: var(--alert-color);
color: var(--alert-foreground-color);
font-weight: bold;
border-radius: 1em;
margin: 0.25em;
text-align: center;
padding: 0.15em 0.3em;
}
.subtle {
/* For all information that is not important for 99% of the users */
color: #999;
}
.link-underline .subtle a {
-webkit-text-decoration: underline 1px #7193bb88;
text-decoration: underline 1px #7193bb88;
color: #7193bb;
}
.literal-code {
/* A codeblock */
display: inline-block;
background-color: lightgray;
padding: 0.1rem;
padding-left: 0.35rem;
padding-right: 0.35rem;
word-break: break-word;
color: black;
box-sizing: border-box;
font-family: monospace;
}
.interactive .literal-code {
background-color: #b3b3b3;
}
/************************** UTILITY ************************/
/**
* Utility classes are there for a specific function to pin down browser behaviour (and cannot be changed)
*/
.text-white a {
/* Used solely in 'imageAttribution' and in many themes*/
color: var(--background-color);
}
.block-ruby {
display: block ruby;
}
.rounded-left-full {
@ -1925,6 +2201,11 @@ a {
border-top-right-radius: 999rem;
}
.no-images img {
/* Used solely in 'imageAttribution' and in many themes for the label*/
display: none;
}
.link-underline a {
-webkit-text-decoration: underline 1px var(--foreground-color);
text-decoration: underline 1px var(--foreground-color);
@ -1939,51 +2220,6 @@ a.link-underline {
text-decoration: none;
}
li {
margin-left: 0.5em;
padding-left: 0.2em;
margin-top: 0.1em;
}
h2 {
font-size: x-large;
margin-bottom: 0. 4em;
font-size: large;
margin-top: 0.5em;
margin-bottom: 0.3em;
font-weight: bold;
}
h3 {
font-weight: bold;
font-size: larger;
margin-top: 0.6em;
margin-bottom: 0;
font-weight: bolder;
}
p {
padding-top: 0.1em;
}
li::marker {
content: "•";
}
.subtle-background {
background: var(--subtle-detail-color);
color: var(--subtle-detail-color-contrast);
}
.normal-background {
background: var(--background-color);
color: var(--foreground-color);
}
.block-ruby {
display: block ruby;
}
.disable-links a {
pointer-events: none;
text-decoration: none !important;
@ -2002,6 +2238,12 @@ li::marker {
display: none;
}
.zebra-table tr:nth-child(even) {
background-color: #f2f2f2;
}
/************************* MISC ELEMENTS *************************/
.selected svg:not(.noselect *) path.selectable {
/* A marker on the map gets the 'selected' class when it's properties are displayed
*/
@ -2030,41 +2272,6 @@ li::marker {
}
}
/**************** GENERIC ****************/
.alert {
background-color: var(--alert-color);
color: var(--alert-foreground-color);
font-weight: bold;
border-radius: 1em;
margin: 0.25em;
text-align: center;
padding: 0.15em 0.3em;
}
.shadow {
box-shadow: 0 0 20px var(--shadow-color);
}
.subtle {
color: #999;
}
.link-underline .subtle a {
-webkit-text-decoration: underline 1px #7193bb88;
text-decoration: underline 1px #7193bb88;
color: #7193bb;
}
.thanks {
background-color: #43d904;
font-weight: bold;
border-radius: 1em;
margin: 0.25em;
text-align: center;
padding: 0.15em 0.3em;
}
@-webkit-keyframes slide {
/* This is the animation on the marker to add a new point - it slides through all the possible presets */
@ -2093,47 +2300,6 @@ li::marker {
}
}
/***************** Info box (box containing features and questions ******************/
input {
color: var(--foreground-color);
}
.literal-code {
display: inline-block;
background-color: lightgray;
padding: 0.5em;
word-break: break-word;
color: black;
box-sizing: border-box;
}
/** Switch layout **/
.small-image img {
height: 1em;
max-width: 1em;
}
.small-image {
height: 1em;
max-width: 1em;
}
.slideshow-item img {
height: var(--image-carousel-height);
width: unset;
}
.animate-height {
transition: max-height 0.5s ease-in-out;
overflow-y: hidden;
}
.zebra-table tr:nth-child(even) {
background-color: #f2f2f2;
}
.glowing-shadow {
-webkit-animation: glowing 1s ease-in-out infinite alternate;
animation: glowing 1s ease-in-out infinite alternate;
@ -2149,6 +2315,20 @@ input {
}
}
/************************* LEGACY MARKER - CLEANUP BELOW ********************************/
.slideshow-item img {
/* Legacy: should be replace when the image element is ported to Svelte*/
height: var(--image-carousel-height);
width: unset;
}
.animate-height {
/* Legacy: should be replaced by headlessui disclosure in time */
transition: max-height 0.5s ease-in-out;
overflow-y: hidden;
}
.hover\:bg-unsubtle:hover {
--tw-bg-opacity: 1;
background-color: rgb(191 219 254 / var(--tw-bg-opacity));
@ -2400,3 +2580,4 @@ input {
}
}