Refactoring: overhaul of the visual style with CSS
|
@ -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) {
|
||||
|
|
|
@ -637,6 +637,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
|||
or: [
|
||||
"__featureSwitchIsTesting=true",
|
||||
"__featureSwitchIsDebugging=true",
|
||||
"mapcomplete-show_tags=full",
|
||||
"mapcomplete-show_debug=yes",
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
{/if}
|
||||
{#if $featureSwitchIsTesting || $featureSwitchIsDebugging}
|
||||
<span class="subtle">{config.id}</span>
|
||||
{/if}
|
||||
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
</LoginToggle>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -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>>
|
||||
|
|
|
@ -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>
|
||||
|
|
24
assets/layers/usersettings/license_info.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
59
assets/layers/usersettings/translate_disabled.svg
Normal 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 |
50
assets/layers/usersettings/translate_mobile.svg
Normal 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 |
|
@ -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 |
|
@ -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 |
|
@ -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
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|