forked from MapComplete/MapComplete
		
	Add themes to search functionality, including quickswitch between recent themes
This commit is contained in:
		
							parent
							
								
									b4866cdbac
								
							
						
					
					
						commit
						329865a15e
					
				
					 22 changed files with 679 additions and 431 deletions
				
			
		|  | @ -393,6 +393,9 @@ | |||
|         "search": { | ||||
|             "error": "Something went wrong…", | ||||
|             "nothing": "Nothing found…", | ||||
|             "nothingFor": "No results found for {term}", | ||||
|             "recentThemes": "Recently visited maps", | ||||
|             "recents": "Recent searches", | ||||
|             "search": "Search a location", | ||||
|             "searchShort": "Search…", | ||||
|             "searching": "Searching…" | ||||
|  |  | |||
|  | @ -2558,11 +2558,6 @@ video { | |||
|   border-color: rgb(209 213 219 / var(--tw-border-opacity)); | ||||
| } | ||||
| 
 | ||||
| .border-red-500 { | ||||
|   --tw-border-opacity: 1; | ||||
|   border-color: rgb(239 68 68 / var(--tw-border-opacity)); | ||||
| } | ||||
| 
 | ||||
| .border-gray-800 { | ||||
|   --tw-border-opacity: 1; | ||||
|   border-color: rgb(31 41 55 / var(--tw-border-opacity)); | ||||
|  | @ -2658,6 +2653,11 @@ video { | |||
|   border-color: rgb(34 197 94 / var(--tw-border-opacity)); | ||||
| } | ||||
| 
 | ||||
| .border-red-500 { | ||||
|   --tw-border-opacity: 1; | ||||
|   border-color: rgb(239 68 68 / var(--tw-border-opacity)); | ||||
| } | ||||
| 
 | ||||
| .border-gray-700 { | ||||
|   --tw-border-opacity: 1; | ||||
|   border-color: rgb(55 65 81 / var(--tw-border-opacity)); | ||||
|  | @ -4379,10 +4379,10 @@ video { | |||
| } | ||||
| 
 | ||||
| :root { | ||||
|   /*  | ||||
|     * The main colour scheme of mapcomplete is configured here. | ||||
|     * For a custom styling, set 'customCss' in your layoutConfig and overwrite some of these. | ||||
|     */ | ||||
|   /* | ||||
|       * The main colour scheme of mapcomplete is configured here. | ||||
|       * For a custom styling, set 'customCss' in your layoutConfig and overwrite some of these. | ||||
|       */ | ||||
|   /* No support for dark mode yet, we disable it to prevent some elements to suddenly toggle */ | ||||
|   color-scheme: only light; | ||||
|   /* Main color of the application: the background and text colours */ | ||||
|  | @ -4407,9 +4407,9 @@ video { | |||
|   --disabled: #B8B8B8; | ||||
|   --disabled-font: #B8B8B8; | ||||
|   /** | ||||
|      * Base colour of interactive elements, mainly the 'subtle button' | ||||
|      * @deprecated | ||||
|      */ | ||||
|        * 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; | ||||
|  | @ -4419,14 +4419,14 @@ video { | |||
|   --catch-detail-color-contrast: #fb3afb; | ||||
|   --image-carousel-height: 350px; | ||||
|   /** Technical value, used by icon.svelte | ||||
|      */ | ||||
|        */ | ||||
|   --svg-color: #000000; | ||||
| } | ||||
| 
 | ||||
| @font-face{ | ||||
|   font-family:"Source Sans Pro"; | ||||
| @font-face { | ||||
|   font-family: "Source Sans Pro"; | ||||
| 
 | ||||
|   src:url("/assets/source-sans-pro.regular.ttf") format("woff"); | ||||
|   src: url("/assets/source-sans-pro.regular.ttf") format("woff"); | ||||
| } | ||||
| 
 | ||||
| /***********************************************************************\ | ||||
|  | @ -4663,18 +4663,18 @@ select:hover { | |||
| 
 | ||||
| .neutral-label { | ||||
|   /** This label styles as normal text. It's power comes from the many :not(.neutral-label) entries. | ||||
|      * Placed here for autocompletion | ||||
|      */ | ||||
|        * Placed here for autocompletion | ||||
|        */ | ||||
| } | ||||
| 
 | ||||
| label:not(.neutral-label):not(.button) { | ||||
|   /** | ||||
|      * Label should _contain_ the input element | ||||
|      */ | ||||
|        * Label should _contain_ the input element | ||||
|        */ | ||||
|   padding: 0.25rem; | ||||
|   padding-right: 0.5rem; | ||||
|   padding-left: 0.5rem; | ||||
|   margin:0.25rem; | ||||
|   margin: 0.25rem; | ||||
|   border-radius: 0.5rem; | ||||
|   width: 100%; | ||||
|   box-sizing: border-box; | ||||
|  | @ -4887,6 +4887,10 @@ a.link-underline { | |||
|   color: unset !important; | ||||
| } | ||||
| 
 | ||||
| a:hover { | ||||
|   background-color: var(--low-interaction-background); | ||||
| } | ||||
| 
 | ||||
| .disable-links a.must-link, | ||||
| .disable-links .must-link a { | ||||
|   /* Hide links if they are disabled */ | ||||
|  | @ -4901,7 +4905,7 @@ a.link-underline { | |||
| 
 | ||||
| .selected svg:not(.noselect *) path.selectable { | ||||
|   /* A marker on the map gets the 'selected' class when it's properties are displayed | ||||
|     */ | ||||
|       */ | ||||
|   stroke: white !important; | ||||
|   stroke-width: 20px !important; | ||||
|   overflow: visible !important; | ||||
|  | @ -4915,7 +4919,7 @@ a.link-underline { | |||
| 
 | ||||
| .selected svg { | ||||
|   /* A marker on the map gets the 'selected' class when it's properties are displayed | ||||
|     */ | ||||
|       */ | ||||
|   overflow: visible !important; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,12 +1,13 @@ | |||
| import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider" | ||||
| import { Utils } from "../../Utils" | ||||
| 
 | ||||
| export default class CombinedSearcher implements GeocodingProvider { | ||||
|     private _providers: ReadonlyArray<GeocodingProvider> | ||||
|     private _providersWithSuggest: ReadonlyArray<GeocodingProvider> | ||||
| 
 | ||||
|     constructor(...providers: ReadonlyArray<GeocodingProvider>) { | ||||
|         this._providers = providers | ||||
|         this._providersWithSuggest = providers.filter(pr => pr.suggest !== undefined) | ||||
|         this._providers = Utils.NoNull(providers) | ||||
|         this._providersWithSuggest = this._providers.filter(pr => pr.suggest !== undefined) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ export type GeoCodeResult = { | |||
|     osm_type?: "node" | "way" | "relation" | ||||
|     osm_id?: string, | ||||
|     category?: GeocodingCategory, | ||||
|     importance?: number | ||||
|     payload?: object | ||||
| } | ||||
| 
 | ||||
| export interface GeocodingOptions { | ||||
|  |  | |||
|  | @ -7,21 +7,20 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | |||
| 
 | ||||
| export class RecentSearch { | ||||
| 
 | ||||
|     private readonly _recentSearches: UIEventSource<string[]> | ||||
|     public readonly recentSearches: Store<string[]> | ||||
| 
 | ||||
|     private readonly _seenThisSession: UIEventSource<GeoCodeResult[]> = new UIEventSource<GeoCodeResult[]>([]) | ||||
|     public readonly seenThisSession: Store<GeoCodeResult[]> = this._seenThisSession | ||||
|     private readonly _seenThisSession: UIEventSource<GeoCodeResult[]> | ||||
|     public readonly seenThisSession: Store<GeoCodeResult[]> | ||||
| 
 | ||||
|     constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store<Feature> }) { | ||||
|         const longPref = state.osmConnection.preferencesHandler.GetLongPreference("recent-searches") | ||||
|         this._recentSearches = longPref.sync(str => !str ? [] : <string[]>JSON.parse(str), [], strs => JSON.stringify(strs)) | ||||
|         this.recentSearches = this._recentSearches | ||||
|      //   const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches")
 | ||||
|         this._seenThisSession =  new UIEventSource<GeoCodeResult[]>([])//UIEventSource.asObject<GeoCodeResult[]>(prefs, [])
 | ||||
|         this.seenThisSession = this._seenThisSession | ||||
| 
 | ||||
| 
 | ||||
|         state.selectedElement.addCallbackAndRunD(selected => { | ||||
|             const [osm_type, osm_id] = selected.properties.id.split("/") | ||||
|             const [lon, lat] = GeoOperations.centerpointCoordinates(selected) | ||||
|             const entry = <GeoCodeResult> { | ||||
|             const entry = <GeoCodeResult>{ | ||||
|                 feature: selected, | ||||
|                 osm_id, osm_type, | ||||
|                 description: "Viewed recently", | ||||
|  | @ -33,7 +32,7 @@ export class RecentSearch { | |||
|     } | ||||
| 
 | ||||
|     addSelected(entry: GeoCodeResult) { | ||||
|         const arr = [...this.seenThisSession.data.slice(0, 20), entry] | ||||
|         const arr = [...(this.seenThisSession.data ?? []).slice(0, 20), entry] | ||||
| 
 | ||||
|         const seenIds = new Set<string>() | ||||
|         for (let i = arr.length - 1; i >= 0; i--) { | ||||
|  |  | |||
							
								
								
									
										43
									
								
								src/Logic/Geocoding/ThemeSearch.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/Logic/Geocoding/ThemeSearch.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | |||
| import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider" | ||||
| import * as themeOverview from "../../assets/generated/theme_overview.json" | ||||
| import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import { SpecialVisualizationState } from "../../UI/SpecialVisualization" | ||||
| import { Utils } from "../../Utils" | ||||
| import MoreScreen from "../../UI/BigComponents/MoreScreen" | ||||
| import { Store } from "../UIEventSource" | ||||
| 
 | ||||
| export default class ThemeSearch implements GeocodingProvider { | ||||
| 
 | ||||
|     private static allThemes: MinimalLayoutInformation[] = (themeOverview["default"] ?? themeOverview) | ||||
|     private readonly _state: SpecialVisualizationState | ||||
|     private readonly _knownHiddenThemes: Store<Set<string>> | ||||
| 
 | ||||
|     constructor(state: SpecialVisualizationState) { | ||||
|         this._state = state | ||||
|         this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection) | ||||
|     } | ||||
| 
 | ||||
|     search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> { | ||||
|         return this.suggest(query, options) | ||||
|     } | ||||
| 
 | ||||
|     async suggest?(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> { | ||||
|         if(query.length < 1){ | ||||
|             return [] | ||||
|         } | ||||
|         const limit = options?.limit ?? 4 | ||||
|         query = Utils.simplifyStringForSearch(query) | ||||
|         const withMatch = ThemeSearch.allThemes | ||||
|             .filter(th => !th.hideFromOverview ) | ||||
|             .filter(th => th.id !== this._state.layout.id) | ||||
|             .filter(th => MoreScreen.MatchesLayout(th, query)) | ||||
|             .slice(0, limit + 1) | ||||
| 
 | ||||
|         return withMatch.map(match => (<GeoCodeResult> { | ||||
|             payload: match, | ||||
|             osm_id: match.id | ||||
|         })) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -71,7 +71,7 @@ export class OsmPreferences { | |||
|             } | ||||
|             if (str === null) { | ||||
|                 console.error("Deleting " + allStartWith) | ||||
|                 let count = parseInt(length.data) | ||||
|                 const count = parseInt(length.data) | ||||
|                 for (let i = 0; i < count; i++) { | ||||
|                     // Delete all the preferences
 | ||||
|                     self.GetPreference(allStartWith + "-" + i, "", subOptions).setData("") | ||||
|  |  | |||
|  | @ -78,6 +78,10 @@ export default class UserRelatedState { | |||
|     public readonly preferencesAsTags: UIEventSource<Record<string, string>> | ||||
|     private readonly _mapProperties: MapProperties | ||||
| 
 | ||||
|     private readonly _recentlyVisitedThemes: UIEventSource<string[]> | ||||
|     public readonly recentlyVisitedThemes: Store<string[]> | ||||
| 
 | ||||
| 
 | ||||
|     constructor( | ||||
|         osmConnection: OsmConnection, | ||||
|         layout?: LayoutConfig, | ||||
|  | @ -109,7 +113,7 @@ export default class UserRelatedState { | |||
|         this.showAllQuestionsAtOnce = UIEventSource.asBoolean( | ||||
|             this.osmConnection.GetPreference("show-all-questions", "false", { | ||||
|                 documentation: | ||||
|                     "Either 'true' or 'false'. If set, all questions will be shown all at once", | ||||
|                     "Either 'true' or 'false'. If set, all questions will be shown all at once" | ||||
|             }) | ||||
|         ) | ||||
|         this.language = this.osmConnection.GetPreference("language") | ||||
|  | @ -129,7 +133,7 @@ export default class UserRelatedState { | |||
|             undefined, | ||||
|             { | ||||
|                 documentation: | ||||
|                     "The ID of a layer or layer category that MapComplete uses by default", | ||||
|                     "The ID of a layer or layer category that MapComplete uses by default" | ||||
|             } | ||||
|         ) | ||||
| 
 | ||||
|  | @ -137,12 +141,12 @@ export default class UserRelatedState { | |||
|             "preferences-add-new-mode", | ||||
|             "button_click_right", | ||||
|             { | ||||
|                 documentation: "How adding a new feature is done", | ||||
|                 documentation: "How adding a new feature is done" | ||||
|             } | ||||
|         ) | ||||
| 
 | ||||
|         this.imageLicense = this.osmConnection.GetPreference("pictures-license", "CC0", { | ||||
|             documentation: "The license under which new images are uploaded", | ||||
|             documentation: "The license under which new images are uploaded" | ||||
|         }) | ||||
|         this.installedUserThemes = this.InitInstalledUserThemes() | ||||
| 
 | ||||
|  | @ -150,6 +154,30 @@ export default class UserRelatedState { | |||
| 
 | ||||
|         this.preferencesAsTags = this.initAmendedPrefs(layout, featureSwitches) | ||||
| 
 | ||||
|         const prefs = this.osmConnection | ||||
|         this._recentlyVisitedThemes = UIEventSource.asObject(prefs.GetLongPreference("recently-visited-themes"), []) | ||||
|         this.recentlyVisitedThemes = this._recentlyVisitedThemes | ||||
|         if (layout) { | ||||
|             const osmConn =this.osmConnection | ||||
|             const recentlyVisited = this._recentlyVisitedThemes | ||||
|             function update() { | ||||
|                 if (!osmConn.isLoggedIn.data) { | ||||
|                     return | ||||
|                 } | ||||
|                 const previously = recentlyVisited.data | ||||
|                 if (previously[0] === layout.id) { | ||||
|                     return true | ||||
|                 } | ||||
|                 const newThemes = Utils.Dedup([layout.id, ...previously]).slice(0, 30) | ||||
|                 recentlyVisited.set(newThemes) | ||||
|                 return true | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             this._recentlyVisitedThemes.addCallbackAndRun(() => update()) | ||||
|             this.osmConnection.isLoggedIn.addCallbackAndRun(() => update()) | ||||
|         } | ||||
| 
 | ||||
|         this.syncLanguage() | ||||
|     } | ||||
| 
 | ||||
|  | @ -171,13 +199,13 @@ export default class UserRelatedState { | |||
| 
 | ||||
|     public GetUnofficialTheme(id: string): | ||||
|         | { | ||||
|               id: string | ||||
|               icon: string | ||||
|               title: any | ||||
|               shortDescription: any | ||||
|               definition?: any | ||||
|               isOfficial: boolean | ||||
|           } | ||||
|         id: string | ||||
|         icon: string | ||||
|         title: any | ||||
|         shortDescription: any | ||||
|         definition?: any | ||||
|         isOfficial: boolean | ||||
|     } | ||||
|         | undefined { | ||||
|         console.log("GETTING UNOFFICIAL THEME") | ||||
|         const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id) | ||||
|  | @ -202,8 +230,8 @@ export default class UserRelatedState { | |||
|         } catch (e) { | ||||
|             console.warn( | ||||
|                 "Removing theme " + | ||||
|                     id + | ||||
|                     " as it could not be parsed from the preferences; the content is:", | ||||
|                 id + | ||||
|                 " as it could not be parsed from the preferences; the content is:", | ||||
|                 str | ||||
|             ) | ||||
|             pref.setData(null) | ||||
|  | @ -233,7 +261,7 @@ export default class UserRelatedState { | |||
|                     icon: layout.icon, | ||||
|                     title: layout.title.translations, | ||||
|                     shortDescription: layout.shortDescription.translations, | ||||
|                     definition: layout["definition"], | ||||
|                     definition: layout["definition"] | ||||
|                 }) | ||||
|             ) | ||||
|         } | ||||
|  | @ -273,13 +301,13 @@ export default class UserRelatedState { | |||
|                         id: "home", | ||||
|                         "user:home": "yes", | ||||
|                         _lon: homeLonLat[0], | ||||
|                         _lat: homeLonLat[1], | ||||
|                         _lat: homeLonLat[1] | ||||
|                     }, | ||||
|                     geometry: { | ||||
|                         type: "Point", | ||||
|                         coordinates: homeLonLat, | ||||
|                     }, | ||||
|                 }, | ||||
|                         coordinates: homeLonLat | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }) | ||||
|         return new StaticFeatureSource(feature) | ||||
|  | @ -300,7 +328,7 @@ export default class UserRelatedState { | |||
|             _applicationOpened: new Date().toISOString(), | ||||
|             _supports_sharing: | ||||
|                 typeof window === "undefined" ? "no" : window.navigator.share ? "yes" : "no", | ||||
|             _iframe: Utils.isIframe ? "yes" : "no", | ||||
|             _iframe: Utils.isIframe ? "yes" : "no" | ||||
|         }) | ||||
| 
 | ||||
|         for (const key in Constants.userJourney) { | ||||
|  | @ -355,18 +383,18 @@ export default class UserRelatedState { | |||
|                     const zenLinks: { link: string; id: string }[] = Utils.NoNull([ | ||||
|                         hasMissingTheme | ||||
|                             ? { | ||||
|                                   id: "theme:" + layout.id, | ||||
|                                   link: LinkToWeblate.hrefToWeblateZen( | ||||
|                                       language, | ||||
|                                       "themes", | ||||
|                                       layout.id | ||||
|                                   ), | ||||
|                               } | ||||
|                                 id: "theme:" + layout.id, | ||||
|                                 link: LinkToWeblate.hrefToWeblateZen( | ||||
|                                     language, | ||||
|                                     "themes", | ||||
|                                     layout.id | ||||
|                                 ) | ||||
|                             } | ||||
|                             : undefined, | ||||
|                         ...missingLayers.map((id) => ({ | ||||
|                             id: "layer:" + id, | ||||
|                             link: LinkToWeblate.hrefToWeblateZen(language, "layers", id), | ||||
|                         })), | ||||
|                             link: LinkToWeblate.hrefToWeblateZen(language, "layers", id) | ||||
|                         })) | ||||
|                     ]) | ||||
|                     const untranslated_count = untranslated.length | ||||
|                     amendedPrefs.data["_translation_total"] = "" + total | ||||
|  | @ -391,8 +419,8 @@ export default class UserRelatedState { | |||
|             for (const k in userDetails) { | ||||
|                 amendedPrefs.data["_" + k] = "" + userDetails[k] | ||||
|             } | ||||
|             if(userDetails.description){ | ||||
|                 amendedPrefs.data["_description_html"] =  Utils.purify(new Showdown.Converter() | ||||
|             if (userDetails.description) { | ||||
|                 amendedPrefs.data["_description_html"] = Utils.purify(new Showdown.Converter() | ||||
|                     .makeHtml(userDetails.description) | ||||
|                     ?.replace(/>/g, ">") | ||||
|                     ?.replace(/</g, "<") | ||||
|  |  | |||
|  | @ -104,7 +104,9 @@ export abstract class Store<T> implements Readable<T> { | |||
|         extraStoresToWatch: Store<any>[], | ||||
|         callbackDestroyFunction: (f: () => void) => void | ||||
|     ): Store<J> | ||||
| 
 | ||||
|     M | ||||
| 
 | ||||
|     public mapD<J>( | ||||
|         f: (t: Exclude<T, undefined | null>) => J, | ||||
|         extraStoresToWatch?: Store<any>[], | ||||
|  | @ -246,6 +248,7 @@ export abstract class Store<T> implements Readable<T> { | |||
|             return f(<Exclude<T, undefined | null>>t) | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public stabilized(millisToStabilize): Store<T> { | ||||
|         if (Utils.runningFromConsole) { | ||||
|             return this | ||||
|  | @ -311,12 +314,14 @@ export class ImmutableStore<T> extends Store<T> { | |||
|     public readonly data: T | ||||
|     static FALSE = new ImmutableStore<boolean>(false) | ||||
|     static TRUE = new ImmutableStore<boolean>(true) | ||||
| 
 | ||||
|     constructor(data: T) { | ||||
|         super() | ||||
|         this.data = data | ||||
|     } | ||||
| 
 | ||||
|     private static readonly pass: () => void = () => {} | ||||
|     private static readonly pass: () => void = () => { | ||||
|     } | ||||
| 
 | ||||
|     addCallback(_: (data: T) => void): () => void { | ||||
|         // pass: data will never change
 | ||||
|  | @ -718,6 +723,27 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> { | |||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     static asObject<T extends object>(stringUIEventSource: UIEventSource<string>, defaultV: T): UIEventSource<T> { | ||||
|         return stringUIEventSource.sync( | ||||
|             (str) => { | ||||
|                 if (str === undefined || str === null || str === "") { | ||||
|                     return defaultV | ||||
|                 } | ||||
|                 try { | ||||
|                     return <T> JSON.parse(str) | ||||
|                 } catch (e) { | ||||
|                     console.error("Could not parse value", str,"due to",e) | ||||
|                     return defaultV | ||||
|                 } | ||||
|             }, | ||||
|             [], | ||||
|             (b) => { | ||||
|                 console.log("Stringifying", b) | ||||
|                 return JSON.stringify(b) ?? "" | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create a new UIEVentSource. Whenever 'source' changes, the returned UIEventSource will get this value as well. | ||||
|      * However, this value can be overriden without affecting source | ||||
|  | @ -863,7 +889,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> { | |||
| 
 | ||||
|         const newSource = new UIEventSource<J>(f(this.data), "map(" + this.tag + ")@" + callee) | ||||
| 
 | ||||
|         const update = function () { | ||||
|         const update = function() { | ||||
|             newSource.setData(f(self.data)) | ||||
|             return allowUnregister && newSource._callbacks.length() === 0 | ||||
|         } | ||||
|  |  | |||
|  | @ -12,7 +12,21 @@ import { RasterLayerProperties } from "../RasterLayerProperties" | |||
| 
 | ||||
| import { ConversionContext } from "./Conversion/ConversionContext" | ||||
| import { Translatable } from "./Json/Translatable" | ||||
| import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" | ||||
| 
 | ||||
| /** | ||||
|  * Minimal information about a theme | ||||
|  **/ | ||||
| export class MinimalLayoutInformation { | ||||
|     id: string | ||||
|     icon: string | ||||
|     title: Translatable | ||||
|     shortDescription: Translatable | ||||
|     definition?: Translatable | ||||
|     mustHaveLanguage?: boolean | ||||
|     hideFromOverview?: boolean | ||||
|     keywords?: (Translatable | TagRenderingConfigJson)[] | ||||
| } | ||||
| /** | ||||
|  * Minimal information about a theme | ||||
|  **/ | ||||
|  | @ -27,6 +41,8 @@ export class LayoutInformation { | |||
|     keywords?: (Translatable | Translation)[] | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| export default class LayoutConfig implements LayoutInformation { | ||||
|     public static readonly defaultSocialImage = "assets/SocialImage.png" | ||||
|     public readonly id: string | ||||
|  |  | |||
|  | @ -81,6 +81,7 @@ import CoordinateSearch from "../Logic/Geocoding/CoordinateSearch" | |||
| import LocalElementSearch from "../Logic/Geocoding/LocalElementSearch" | ||||
| import { RecentSearch } from "../Logic/Geocoding/RecentSearch" | ||||
| import PhotonSearch from "../Logic/Geocoding/PhotonSearch" | ||||
| import ThemeSearch from "../Logic/Geocoding/ThemeSearch" | ||||
| 
 | ||||
| /** | ||||
|  * | ||||
|  | @ -393,6 +394,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|             new LocalElementSearch(this, 5), | ||||
|             new PhotonSearch(), // new NominatimGeocoding(),
 | ||||
|             new CoordinateSearch(), | ||||
|             this.featureSwitches.featureSwitchBackToThemeOverview.data ? new ThemeSearch(this) : undefined | ||||
|         ) | ||||
| 
 | ||||
|         this.recentlySearched = new RecentSearch(this) | ||||
|  |  | |||
|  | @ -35,7 +35,7 @@ | |||
|       "oauth_token", | ||||
|       undefined, | ||||
|       "Used to complete the login" | ||||
|     ), | ||||
|     ) | ||||
|   }) | ||||
|   const state = new UserRelatedState(osmConnection) | ||||
|   const t = Translations.t.index | ||||
|  | @ -44,7 +44,7 @@ | |||
|   let userLanguages = osmConnection.userDetails.map((ud) => ud.languages) | ||||
|   let themeSearchText: UIEventSource<string | undefined> = new UIEventSource<string>(undefined) | ||||
| 
 | ||||
|   document.addEventListener("keydown", function (event) { | ||||
|   document.addEventListener("keydown", function(event) { | ||||
|     if (event.ctrlKey && event.code === "KeyF") { | ||||
|       document.getElementById("theme-search")?.focus() | ||||
|       event.preventDefault() | ||||
|  | @ -55,20 +55,10 @@ | |||
|   const hiddenThemes: LayoutInformation[] = | ||||
|     (themeOverview["default"] ?? themeOverview)?.filter((layout) => layout.hideFromOverview) ?? [] | ||||
|   { | ||||
|     const prefix = "mapcomplete-hidden-theme-" | ||||
|     const userPreferences = state.osmConnection.preferencesHandler.preferences | ||||
|     visitedHiddenThemes = userPreferences.map((preferences) => { | ||||
|       const knownIds = new Set<string>( | ||||
|         Object.keys(preferences) | ||||
|           .filter((key) => key.startsWith(prefix)) | ||||
|           .map((key) => key.substring(prefix.length, key.length - "-enabled".length)) | ||||
|       ) | ||||
|       return hiddenThemes.filter( | ||||
|         (theme) => | ||||
|           knownIds.has(theme.id) || | ||||
|           state.osmConnection.userDetails.data.name === "Pieter Vander Vennet" | ||||
|       ) | ||||
|     }) | ||||
|     visitedHiddenThemes = MoreScreen.knownHiddenThemes(state.osmConnection) | ||||
|       .map((knownIds) => hiddenThemes.filter((theme) => | ||||
|         knownIds.has(theme.id) || state.osmConnection.userDetails.data.name === "Pieter Vander Vennet" | ||||
|       )) | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
|  | @ -103,7 +93,7 @@ | |||
| 
 | ||||
|     <form | ||||
|       class="flex justify-center" | ||||
|       on:submit|preventDefault={(_) => MoreScreen.applySearch(themeSearchText.data)} | ||||
|       on:submit|preventDefault={() => MoreScreen.applySearch(themeSearchText.data)} | ||||
|     > | ||||
|       <label | ||||
|         class="neutral-label my-2 flex w-full items-center rounded-full border-2 border-black sm:w-1/2" | ||||
|  |  | |||
|  | @ -116,8 +116,8 @@ | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   let suggestions: Store<GeoCodeResult[]> = searchContents.stabilized(250).bindD(search => | ||||
|     UIEventSource.FromPromise(searcher.suggest(search), err => console.error(err)) | ||||
|   let suggestions: Store<{success: GeoCodeResult[]} | {error}> = searchContents.stabilized(250).bindD(search => | ||||
|     UIEventSource.FromPromiseWithErr(searcher.suggest(search)) | ||||
|   ) | ||||
| 
 | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,19 +1,27 @@ | |||
| import { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import { ImmutableStore, Store } from "../../Logic/UIEventSource" | ||||
| import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import { Store } from "../../Logic/UIEventSource" | ||||
| import { Utils } from "../../Utils" | ||||
| import themeOverview from "../../assets/generated/theme_overview.json" | ||||
| import Locale from "../i18n/Locale" | ||||
| import { Translatable } from "../../Models/ThemeConfig/Json/Translatable" | ||||
| import { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson" | ||||
| import { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||
| 
 | ||||
| export default class MoreScreen { | ||||
|     public static readonly officialThemes: LayoutInformation[] = themeOverview | ||||
| 
 | ||||
|     public static readonly officialThemes: MinimalLayoutInformation[] = themeOverview | ||||
|     public static readonly officialThemesById: Map<string, MinimalLayoutInformation> = new Map<string, MinimalLayoutInformation>() | ||||
|     static { | ||||
|         for (const th of MoreScreen.officialThemes) { | ||||
|             MoreScreen.officialThemesById.set(th.id, th) | ||||
|         } | ||||
|     } | ||||
|     public static applySearch(searchTerm: string) { | ||||
|         searchTerm = searchTerm.toLowerCase() | ||||
|         if (!searchTerm) { | ||||
|             return | ||||
|         } | ||||
|         if (searchTerm === "personal") { | ||||
|             window.location.href = MoreScreen.createUrlFor({ id: "personal" }, false).data | ||||
|             window.location.href = MoreScreen.createUrlFor({ id: "personal" }, false) | ||||
|         } | ||||
|         if (searchTerm === "bugs" || searchTerm === "issues") { | ||||
|             window.location.href = "https://github.com/pietervdvn/MapComplete/issues" | ||||
|  | @ -38,22 +46,22 @@ export default class MoreScreen { | |||
|                 MoreScreen.MatchesLayout(th, searchTerm) | ||||
|         ) | ||||
|         if (publicTheme !== undefined) { | ||||
|             window.location.href = MoreScreen.createUrlFor(publicTheme, false).data | ||||
|             window.location.href = MoreScreen.createUrlFor(publicTheme, false) | ||||
|         } | ||||
|         const hiddenTheme = MoreScreen.officialThemes.find( | ||||
|             (th) => th.id !== "personal" && MoreScreen.MatchesLayout(th, searchTerm) | ||||
|         ) | ||||
|         if (hiddenTheme !== undefined) { | ||||
|             window.location.href = MoreScreen.createUrlFor(hiddenTheme, false).data | ||||
|             window.location.href = MoreScreen.createUrlFor(hiddenTheme, false) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static MatchesLayout( | ||||
|         layout: { | ||||
|             id: string | ||||
|             title: any | ||||
|             shortDescription: any | ||||
|             keywords?: any[] | ||||
|             title: Translatable | ||||
|             shortDescription: Translatable | ||||
|             keywords?: (Translatable | TagRenderingConfigJson)[] | ||||
|         }, | ||||
|         search: string | ||||
|     ): boolean { | ||||
|  | @ -72,7 +80,7 @@ export default class MoreScreen { | |||
|             if (entity === undefined) { | ||||
|                 continue | ||||
|             } | ||||
|             const term = entity["*"] ?? entity[Locale.language.data] | ||||
|             const term: string = entity["*"] ?? entity[Locale.language.data] | ||||
|             if (Utils.RemoveDiacritics(term?.toLowerCase())?.indexOf(search) >= 0) { | ||||
|                 return true | ||||
|             } | ||||
|  | @ -82,10 +90,10 @@ export default class MoreScreen { | |||
|     } | ||||
| 
 | ||||
|     public static createUrlFor( | ||||
|         layout: { id: string; definition?: string }, | ||||
|         layout: { id: string }, | ||||
|         isCustom: boolean, | ||||
|         state?: { layoutToUse?: { id } } | ||||
|     ): Store<string> { | ||||
|     ): string { | ||||
|         if (layout === undefined) { | ||||
|             return undefined | ||||
|         } | ||||
|  | @ -115,11 +123,22 @@ export default class MoreScreen { | |||
|             linkPrefix = `${path}/theme.html?userlayout=${layout.id}&` | ||||
|         } | ||||
| 
 | ||||
|         let hash = "" | ||||
|         if (layout.definition !== undefined) { | ||||
|             hash = "#" + btoa(JSON.stringify(layout.definition)) | ||||
|         } | ||||
| 
 | ||||
|         return new ImmutableStore<string>(`${linkPrefix}${hash}`) | ||||
|         return `${linkPrefix}` | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gives all the IDs of the hidden themes which were previously visited | ||||
|      * @param osmConnection | ||||
|      */ | ||||
|     public static knownHiddenThemes(osmConnection: OsmConnection): Store<Set<string>> { | ||||
|         const prefix = "mapcomplete-hidden-theme-" | ||||
|         const userPreferences = osmConnection.preferencesHandler.preferences | ||||
|         return userPreferences.map((preferences) => | ||||
|             new Set<string>( | ||||
|                 Object.keys(preferences) | ||||
|                     .filter((key) => key.startsWith(prefix)) | ||||
|                     .map((key) => key.substring(prefix.length, key.length - "-enabled".length)) | ||||
|             )) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -12,11 +12,15 @@ | |||
|   import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte" | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import ArrowUp from "@babeard/svelte-heroicons/mini/ArrowUp" | ||||
|   import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" | ||||
|   import Tr from "../Base/Tr.svelte" | ||||
|   import { Translation } from "../i18n/Translation" | ||||
|   import MoreScreen from "./MoreScreen" | ||||
| 
 | ||||
|   export let entry: GeoCodeResult | ||||
|   export let state: SpecialVisualizationState | ||||
|   let layer: LayerConfig | ||||
|   let tags : UIEventSource<Record<string, string>> | ||||
|   let tags: UIEventSource<Record<string, string>> | ||||
|   if (entry.feature?.properties?.id) { | ||||
|     layer = state.layout.getMatchingLayer(entry.feature.properties) | ||||
|     tags = state.featureProperties.getStore(entry.feature.properties.id) | ||||
|  | @ -28,6 +32,8 @@ | |||
|   let mapRotation = state.mapProperties.rotation | ||||
|   let inView = state.mapProperties.bounds.mapD(bounds => bounds.contains([entry.lon, entry.lat])) | ||||
| 
 | ||||
|   let otherTheme: MinimalLayoutInformation | undefined = <MinimalLayoutInformation>entry.payload | ||||
| 
 | ||||
|   function select() { | ||||
|     console.log("Selected search entry", entry) | ||||
|     if (entry.boundingbox) { | ||||
|  | @ -41,44 +47,60 @@ | |||
|     } else { | ||||
|       state.mapProperties.flyTo(entry.lon, entry.lat, GeocodingUtils.categoryToZoomLevel[entry.category] ?? 17) | ||||
|     } | ||||
|     if (entry.feature) { | ||||
|     if (entry.feature?.properties?.id) { | ||||
|       state.selectedElement.set(entry.feature) | ||||
|     } | ||||
|     state.recentlySearched.addSelected(entry) | ||||
|     dispatch("select") | ||||
|   } | ||||
| </script> | ||||
| <button class="unstyled w-full link-no-underline" on:click={() => select() }> | ||||
|   <div class="p-2 flex items-center w-full gap-y-2 w-full"> | ||||
| 
 | ||||
|     {#if layer} | ||||
|       <ToSvelte construct={() => layer.defaultIcon(entry.feature.properties).SetClass("w-6 h-6")} /> | ||||
|     {:else if entry.category} | ||||
|       <Icon icon={GeocodingUtils.categoryToIcon[entry.category]} clss="w-6 h-6 shrink-0" color="#aaa" /> | ||||
|     {/if} | ||||
|     <div class="flex flex-col items-start pl-2 w-full"> | ||||
|       <div class="flex flex-wrap gap-x-2 justify-between w-full"> | ||||
|         <b class="nowrap"> | ||||
|           {#if layer && $tags?.id} | ||||
|             <TagRenderingAnswer config={layer.title} selectedElement={entry.feature} {state} {tags} {layer} /> | ||||
|           {:else} | ||||
|             {entry.display_name ?? entry.osm_id} | ||||
|           {/if} | ||||
|         </b> | ||||
|         <div class="flex gap-x-1 items-center"> | ||||
|           {#if $bearing && !$inView} | ||||
|             <ArrowUp class="w-4 h-4 shrink-0" style={`transform: rotate(${$bearing - $mapRotation}deg)`} /> | ||||
|           {/if} | ||||
|           {#if $distance} | ||||
|             {GeoOperations.distanceToHuman($distance)} | ||||
|           {/if} | ||||
|         </div> | ||||
|       </div> | ||||
|       {#if entry.description} | ||||
|         <div class="subtle flex justify-between w-full"> | ||||
|           {entry.description} | ||||
|         </div> | ||||
|       {/if} | ||||
| {#if otherTheme} | ||||
|   <a href={ MoreScreen.createUrlFor(otherTheme, false)} class="flex items-center p-2 w-full gap-y-2 rounded-xl" > | ||||
| 
 | ||||
|     <Icon icon={otherTheme.icon} clss="w-6 h-6 m-1" /> | ||||
|     <div class="flex flex-col"> | ||||
|       <b> | ||||
|         <Tr t={new Translation(otherTheme.title)} /> | ||||
|       </b> | ||||
|       <!--<Tr t={new Translation(otherTheme.shortDescription)} /> --> | ||||
|     </div> | ||||
|   </div> | ||||
| </button> | ||||
|   </a> | ||||
| 
 | ||||
| {:else} | ||||
|   <button class="unstyled w-full link-no-underline" on:click={() => select() }> | ||||
|     <div class="p-2 flex items-center w-full gap-y-2"> | ||||
|       {#if layer} | ||||
|         <ToSvelte construct={() => layer.defaultIcon(entry.feature.properties).SetClass("w-6 h-6")} /> | ||||
|       {:else if entry.category} | ||||
|         <Icon icon={GeocodingUtils.categoryToIcon[entry.category]} clss="w-6 h-6 shrink-0" color="#aaa" /> | ||||
|       {/if} | ||||
|       <div class="flex flex-col items-start pl-2 w-full"> | ||||
|         <div class="flex flex-wrap gap-x-2 justify-between w-full"> | ||||
|           <b class="nowrap"> | ||||
|             {#if layer && $tags?.id} | ||||
|               <TagRenderingAnswer config={layer.title} selectedElement={entry.feature} {state} {tags} {layer} /> | ||||
|             {:else} | ||||
|               {entry.display_name ?? entry.osm_id} | ||||
|             {/if} | ||||
|           </b> | ||||
|           <div class="flex gap-x-1 items-center"> | ||||
|             {#if $bearing && !$inView} | ||||
|               <ArrowUp class="w-4 h-4 shrink-0" style={`transform: rotate(${$bearing - $mapRotation}deg)`} /> | ||||
|             {/if} | ||||
|             {#if $distance} | ||||
|               {GeoOperations.distanceToHuman($distance)} | ||||
|             {/if} | ||||
|           </div> | ||||
|         </div> | ||||
|         {#if entry.description} | ||||
|           <div class="subtle flex justify-between w-full"> | ||||
|             {entry.description} | ||||
|           </div> | ||||
|         {/if} | ||||
| 
 | ||||
|       </div> | ||||
|     </div> | ||||
|   </button> | ||||
| 
 | ||||
| {/if} | ||||
|  |  | |||
|  | @ -3,59 +3,81 @@ | |||
|   import SearchResult from "./SearchResult.svelte" | ||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||
|   import { XMarkIcon } from "@babeard/svelte-heroicons/solid" | ||||
|   import { Store } from "../../Logic/UIEventSource" | ||||
|   import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import Loading from "../Base/Loading.svelte" | ||||
|   import Tr from "../Base/Tr.svelte" | ||||
|   import Translations from "../i18n/Translations" | ||||
|   import MoreScreen from "./MoreScreen" | ||||
| 
 | ||||
|   export let state: SpecialVisualizationState | ||||
|   export let results: GeoCodeResult[] | ||||
|   export let results: { success: GeoCodeResult[] } | { error } | ||||
|   export let searchTerm: Store<string> | ||||
|   export let isFocused: Store<boolean> | ||||
|   export let isFocused: UIEventSource<boolean> | ||||
| 
 | ||||
|   let recentlySeen: Store<GeoCodeResult[]> = state.recentlySearched.seenThisSession | ||||
|   let recentThemes = state.userRelatedState.recentlyVisitedThemes.mapD(thms => thms.filter(th => th !== state.layout.id).slice(0, 3)) | ||||
|   let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview | ||||
| </script> | ||||
| 
 | ||||
| <div class="w-full collapsable" style="height: 50rem;" class:collapsed={!$isFocused}> | ||||
|   {#if $searchTerm.length > 0 && results === undefined} | ||||
|   {#if results?.["error"] !== undefined} | ||||
|     <div class="searchbox normal-background items-center"> | ||||
|       An error occured | ||||
|     </div> | ||||
| 
 | ||||
|   {:else if $searchTerm.length > 0 && results === undefined} | ||||
|     <div class="searchbox normal-background items-center"> | ||||
|       <Loading /> | ||||
|     </div> | ||||
|   {:else if results?.length > 0} | ||||
|   {:else if results?.["success"]?.length > 0} | ||||
|     <div class="relative w-full h-full"> | ||||
|       <div class="absolute top-0 right-0 searchbox normal-background" | ||||
|            style="width: 25rem"> | ||||
|         <div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto"> | ||||
| 
 | ||||
|           {#each results as entry (entry)} | ||||
|           {#each results["success"] as entry (entry)} | ||||
|             <SearchResult on:select {entry} {state} /> | ||||
|           {/each} | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
| 
 | ||||
|       <div class="absolute top-2 right-2 cursor-pointer" on:click={() => close()}> | ||||
|       <div class="absolute top-2 right-2 cursor-pointer" on:click={() => isFocused.setData(false)}> | ||||
|         <XMarkIcon class="w-4 h-4 hover:bg-stone-200 rounded-full" /> | ||||
|       </div> | ||||
|     </div> | ||||
|   {:else } | ||||
|   {:else if $searchTerm.length > 0 || $recentlySeen?.length > 0 || $recentThemes?.length > 0} | ||||
| 
 | ||||
|       <div class="searchbox normal-background "> | ||||
|     {#if $searchTerm.length > 0} | ||||
|       <!-- TODO add translation --> | ||||
|         <b class="flex justify-center p-4">No results found for {$searchTerm}</b> | ||||
|     {/if} | ||||
|     <div class="searchbox normal-background overflow-y-auto h-full"> | ||||
|       {#if $searchTerm.length > 0} | ||||
|         <b class="flex justify-center p-4"> | ||||
|           <Tr t={Translations.t.general.search.nothingFor.Subs({term: $searchTerm})} /> | ||||
|         </b> | ||||
|       {/if} | ||||
| 
 | ||||
|     {#if $recentlySeen?.length > 0} | ||||
|       <!-- TODO add translation --> | ||||
|         <h4>Recent searches</h4> | ||||
|       {#if $recentlySeen?.length > 0} | ||||
|         <h3 class="mx-2"> | ||||
|           <Tr t={Translations.t.general.search.recents} /> | ||||
|         </h3> | ||||
|         {#each $recentlySeen as entry} | ||||
|           <SearchResult {entry} {state} on:select /> | ||||
|         {/each} | ||||
|     {/if} | ||||
|       </div> | ||||
|       {/if} | ||||
| 
 | ||||
|       {#if $recentThemes?.length > 0 && $allowOtherThemes} | ||||
|         <h3 class="mx-2"> | ||||
|           <Tr t={Translations.t.general.search.recentThemes} /> | ||||
|         </h3> | ||||
|         {#each $recentThemes as themeId (themeId)} | ||||
|           <SearchResult | ||||
|             entry={{payload: MoreScreen.officialThemesById.get(themeId), display_name: themeId, lat: 0, lon: 0}} {state} | ||||
|             on:select /> | ||||
|         {/each} | ||||
|       {/if} | ||||
|     </div> | ||||
|   {/if} | ||||
| </div> | ||||
| 
 | ||||
| 
 | ||||
| <style> | ||||
|     .searchbox { | ||||
|         display: flex; | ||||
|  | @ -68,7 +90,8 @@ | |||
| 
 | ||||
|     .collapsable { | ||||
|         max-height: 50vh; | ||||
|         transition: max-height 350ms ease-in-out; | ||||
|         transition: max-height 400ms linear; | ||||
|         transition-delay: 500ms; | ||||
|         overflow: hidden; | ||||
|         padding: 0 !important; | ||||
|     } | ||||
|  |  | |||
|  | @ -1,5 +1,4 @@ | |||
| <script lang="ts"> | ||||
|   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" | ||||
|  |  | |||
|  | @ -23,15 +23,16 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | |||
|         "dragRotate", | ||||
|         "dragPan", | ||||
|         "keyboard", | ||||
|         "touchZoomRotate", | ||||
|         "touchZoomRotate" | ||||
|     ] | ||||
|     private static maplibre_zoom_handlers = [ | ||||
|         "scrollZoom", | ||||
|         "boxZoom", | ||||
|         "doubleClickZoom", | ||||
|         "touchZoomRotate", | ||||
|         "touchZoomRotate" | ||||
|     ] | ||||
|     readonly location: UIEventSource<{ lon: number; lat: number }> | ||||
|     private readonly isFlying = new UIEventSource(false) | ||||
|     readonly zoom: UIEventSource<number> | ||||
|     readonly bounds: UIEventSource<BBox> | ||||
|     readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined> | ||||
|  | @ -105,6 +106,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | |||
|         new RasterLayerHandler(this._maplibreMap, this.rasterLayer) | ||||
| 
 | ||||
|         const clickmodes = ["left", "middle", "right"] as const | ||||
| 
 | ||||
|         function handleClick(e: maplibregl.MapMouseEvent, mode?: "left" | "right" | "middle") { | ||||
|             if (e.originalEvent["consumed"]) { | ||||
|                 // Workaround, 'ShowPointLayer' sets this flag
 | ||||
|  | @ -144,7 +146,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | |||
|             self.SetRotation(self.rotation.data) | ||||
|             self.setTerrain(self.useTerrain.data) | ||||
|             this.updateStores(true) | ||||
|             map.on("moveend", () => this.updateStores()) | ||||
|             map.on("movestart", () => { | ||||
|                 this.isFlying.setData(true) | ||||
|             }) | ||||
|             map.on("moveend", () => { | ||||
|                 this.isFlying.setData(false) | ||||
|                 this.updateStores() | ||||
|             }) | ||||
|             map.on("click", (e) => { | ||||
|                 handleClick(e) | ||||
|             }) | ||||
|  | @ -228,9 +236,9 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | |||
|         return { | ||||
|             map: mlmap, | ||||
|             ui: new SvelteUIElement(MaplibreMap, { | ||||
|                 map: mlmap, | ||||
|                 map: mlmap | ||||
|             }), | ||||
|             mapproperties: new MapLibreAdaptor(mlmap), | ||||
|             mapproperties: new MapLibreAdaptor(mlmap) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -298,7 +306,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | |||
|     ) { | ||||
|         const event = { | ||||
|             date: new Date(), | ||||
|             key: key, | ||||
|             key: key | ||||
|         } | ||||
| 
 | ||||
|         for (let i = 0; i < this._onKeyNavigation.length; i++) { | ||||
|  | @ -487,7 +495,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | |||
|         const bounds = map.getBounds() | ||||
|         const bbox = new BBox([ | ||||
|             [bounds.getEast(), bounds.getNorth()], | ||||
|             [bounds.getWest(), bounds.getSouth()], | ||||
|             [bounds.getWest(), bounds.getSouth()] | ||||
|         ]) | ||||
|         if (this.bounds.data === undefined || !isSetup) { | ||||
|             this.bounds.setData(bbox) | ||||
|  | @ -501,6 +509,9 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | |||
|         if (!map || z === undefined) { | ||||
|             return | ||||
|         } | ||||
|         if (this.isFlying.data) { | ||||
|             return | ||||
|         } | ||||
|         if (Math.abs(map.getZoom() - z) > 0.01) { | ||||
|             map.setZoom(z) | ||||
|         } | ||||
|  | @ -648,9 +659,22 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | |||
|         if (!hasDiff) { | ||||
|             return | ||||
|         } | ||||
|         this.lockZoom() | ||||
|         map.fitBounds(bounds.toLngLat()) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Should be called before making an animation. | ||||
|      * First, 'isFlying' is set to true. This will disable the zoom control | ||||
|      * Then, zoom is set to '1', which is very low. This will generally disable all layers, after which this function will return | ||||
|      * | ||||
|      * Then, a zoom/pan/... animation can be made; after which a 'moveEnd'-event will trigger the 'isFlying' to be set to false and the zoom to be set correctly | ||||
|      */ | ||||
|     private lockZoom() { | ||||
|         this.isFlying.setData(true) | ||||
|         this.zoom.setData(1) | ||||
|     } | ||||
| 
 | ||||
|     private async setTerrain(useTerrain: boolean) { | ||||
|         const map = this._maplibreMap.data | ||||
|         if (!map) { | ||||
|  | @ -665,14 +689,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | |||
|                 type: "raster-dem", | ||||
|                 url: | ||||
|                     "https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=" + | ||||
|                     Constants.maptilerApiKey, | ||||
|                     Constants.maptilerApiKey | ||||
|             }) | ||||
|             try { | ||||
|                 while (!map?.isStyleLoaded()) { | ||||
|                     await Utils.waitFor(250) | ||||
|                 } | ||||
|                 map.setTerrain({ | ||||
|                     source: id, | ||||
|                     source: id | ||||
|                 }) | ||||
|             } catch (e) { | ||||
|                 console.error(e) | ||||
|  | @ -680,10 +704,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public flyTo(lon: number, lat: number, zoom: number){ | ||||
|         this._maplibreMap.data?.flyTo({ | ||||
|             zoom, | ||||
|             center: [lon, lat], | ||||
|     public flyTo(lon: number, lat: number, zoom: number) { | ||||
|         this.lockZoom() | ||||
|         window.requestAnimationFrame(() => { | ||||
|             this._maplibreMap.data?.flyTo({ | ||||
|                 zoom, | ||||
|                 center: [lon, lat] | ||||
|             }) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { Store, UIEventSource } from "../Logic/UIEventSource" | ||||
| import BaseUIElement from "./BaseUIElement" | ||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" | ||||
| import LayoutConfig, { MinimalLayoutInformation } from "../Models/ThemeConfig/LayoutConfig" | ||||
| import { | ||||
|     FeatureSource, | ||||
|     IndexedFeatureSource, | ||||
|  | @ -87,6 +87,7 @@ export interface SpecialVisualizationState { | |||
|         readonly showAllQuestionsAtOnce: UIEventSource<boolean> | ||||
|         readonly preferencesAsTags: UIEventSource<Record<string, string>> | ||||
|         readonly language: UIEventSource<string> | ||||
|         readonly recentlyVisitedThemes: Store<string[]> | ||||
|     } | ||||
| 
 | ||||
|     readonly availableLayers: Store<RasterLayerPolygon[]> | ||||
|  |  | |||
|  | @ -62,7 +62,7 @@ | |||
|             return "offline" | ||||
|         } | ||||
|       }), | ||||
|       message: osmApi, | ||||
|       message: osmApi | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|  | @ -90,7 +90,7 @@ | |||
|         } | ||||
|         const files: string[] = s["success"]["allFiles"] | ||||
|         return "Contains " + (files.length ?? "no") + " files" | ||||
|       }), | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
|   { | ||||
|  | @ -106,7 +106,7 @@ | |||
|           return "degraded" | ||||
|         } | ||||
|       }), | ||||
|       message: simpleMessage(testDownload(Constants.GeoIpServer + "/ip")), | ||||
|       message: simpleMessage(testDownload(Constants.GeoIpServer + "/ip")) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|  | @ -125,7 +125,7 @@ | |||
|         } | ||||
|         return "degraded" | ||||
|       }), | ||||
|       message: simpleMessage(status), | ||||
|       message: simpleMessage(status) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|  | @ -144,7 +144,7 @@ | |||
|         } | ||||
|         return "online" | ||||
|       }), | ||||
|       message: simpleMessage(status), | ||||
|       message: simpleMessage(status) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|  | @ -183,7 +183,7 @@ | |||
| 
 | ||||
|         const json = JSON.stringify(s["success"], null, "  ") | ||||
|         return "Database is " + Math.floor(timediffDays) + " days out of sync\n\n" + json | ||||
|       }), | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|  | @ -202,7 +202,45 @@ | |||
|         } | ||||
|         return "degraded" | ||||
|       }), | ||||
|       message: status.map((s) => JSON.stringify(s)), | ||||
|       message: status.map((s) => JSON.stringify(s)) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   { | ||||
|     const s = Constants.nominatimEndpoint | ||||
|     const status = testDownload(s + "/search.php?q=Brugge") | ||||
|     services.push({ | ||||
|       name: s, | ||||
|       message: simpleMessage(status), | ||||
|       status: status.mapD(s => { | ||||
|         if (s["error"]) { | ||||
|           return "offline" | ||||
|         } | ||||
|         const data = s["success"] | ||||
|         if (Array.isArray(data)) { | ||||
|           return "online" | ||||
|         } | ||||
|         return "degraded" | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   { | ||||
|     const s = Constants.photonEndpoint | ||||
|     const status = testDownload(s + "/api/?q=Brugge") | ||||
|     services.push({ | ||||
|       name: s, | ||||
|       status: status.mapD(s => { | ||||
|         if (s["error"]) { | ||||
|           return "offline" | ||||
|         } | ||||
|         const data = s["success"] | ||||
|         if (Array.isArray(data.features) && data.features.length > 0) { | ||||
|           return "online" | ||||
|         } | ||||
|         return "degraded" | ||||
|       }), | ||||
|       message: simpleMessage(status) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|  | @ -228,7 +266,7 @@ | |||
| 
 | ||||
|           return "online" | ||||
|         }), | ||||
|         message: simpleMessage(status), | ||||
|         message: simpleMessage(status) | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
|  | @ -241,7 +279,7 @@ | |||
|           return "online" | ||||
|         } | ||||
|         return "offline" | ||||
|       }), | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -263,14 +263,14 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|         return res | ||||
|     } | ||||
| 
 | ||||
|     public static NoNull<T>(array: T[] | undefined): T[] | undefined | ||||
|     public static NoNull<T>(array: ReadonlyArray<T> | undefined): T[] | undefined | ||||
|     public static NoNull<T>(array: undefined): undefined | ||||
|     public static NoNull<T>(array: T[]): T[] | ||||
|     public static NoNull<T>(array: T[]): NonNullable<T>[] { | ||||
|     public static NoNull<T>(array: ReadonlyArray<T>): T[] | ||||
|     public static NoNull<T>(array: ReadonlyArray<T>): NonNullable<T>[] { | ||||
|         return <any>array?.filter((o) => o !== undefined && o !== null) | ||||
|     } | ||||
| 
 | ||||
|     public static Hist(array: string[]): Map<string, number> { | ||||
|     public static Hist(array: ReadonlyArray<string>): Map<string, number> { | ||||
|         const hist = new Map<string, number>() | ||||
|         for (const s of array) { | ||||
|             hist.set(s, 1 + (hist.get(s) ?? 0)) | ||||
|  |  | |||
							
								
								
									
										495
									
								
								src/index.css
									
										
									
									
									
								
							
							
						
						
									
										495
									
								
								src/index.css
									
										
									
									
									
								
							|  | @ -12,63 +12,63 @@ | |||
| @tailwind utilities; | ||||
| 
 | ||||
| :root { | ||||
|   /*  | ||||
|     * The main colour scheme of mapcomplete is configured here. | ||||
|     * For a custom styling, set 'customCss' in your layoutConfig and overwrite some of these. | ||||
|     */ | ||||
|     /* | ||||
|       * The main colour scheme of mapcomplete is configured here. | ||||
|       * For a custom styling, set 'customCss' in your layoutConfig and overwrite some of these. | ||||
|       */ | ||||
| 
 | ||||
|     /* No support for dark mode yet, we disable it to prevent some elements to suddenly toggle */ | ||||
|     color-scheme: only light; | ||||
| 
 | ||||
|   /* Main color of the application: the background and text colours */ | ||||
|   --background-color: white; | ||||
|   /* Main text colour. Also styles some elements, such as the 'close popup'-button or 'back-arrow' (in mobile) */ | ||||
|   --foreground-color: black; | ||||
|     /* Main color of the application: the background and text colours */ | ||||
|     --background-color: white; | ||||
|     /* Main text colour. Also styles some elements, such as the 'close popup'-button or 'back-arrow' (in mobile) */ | ||||
|     --foreground-color: black; | ||||
| 
 | ||||
|   /* A colour scheme to indicate an error or warning */ | ||||
|   --alert-color: #fee4d1; | ||||
|   --alert-foreground-color: var(--foreground-color); | ||||
|     /* A colour scheme to indicate an error or warning */ | ||||
|     --alert-color: #fee4d1; | ||||
|     --alert-foreground-color: var(--foreground-color); | ||||
| 
 | ||||
|   --low-interaction-background: #eeeeee; | ||||
|   --low-interaction-background-50: #eeeeee90; | ||||
|   --low-interaction-foreground: black; | ||||
|   --low-interaction-contrast: #ff00ff; | ||||
|     --low-interaction-background: #eeeeee; | ||||
|     --low-interaction-background-50: #eeeeee90; | ||||
|     --low-interaction-foreground: black; | ||||
|     --low-interaction-contrast: #ff00ff; | ||||
| 
 | ||||
|   --interactive-background: #dddddd; | ||||
|   --interactive-foreground: black; | ||||
|   --interactive-contrast: #ff00ff; | ||||
|     --interactive-background: #dddddd; | ||||
|     --interactive-foreground: black; | ||||
|     --interactive-contrast: #ff00ff; | ||||
| 
 | ||||
|   --button-background: #282828; | ||||
|   --button-background-hover: #484848; | ||||
|     --button-background: #282828; | ||||
|     --button-background-hover: #484848; | ||||
|     --button-primary-background-hover: #353535; | ||||
| 
 | ||||
|   --button-foreground: white; | ||||
|   --button-border-color: #F7F7F7; | ||||
|   --disabled: #B8B8B8; | ||||
|     --button-foreground: white; | ||||
|     --button-border-color: #F7F7F7; | ||||
|     --disabled: #B8B8B8; | ||||
|     --disabled-font: #B8B8B8; | ||||
| 
 | ||||
|   /** | ||||
|      * 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; | ||||
|     /** | ||||
|        * 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; | ||||
| 
 | ||||
|   --catch-detail-color: black; /*#3a3aeb;*/ | ||||
|   --catch-detail-foregroundcolor: white; | ||||
|   --catch-detail-color-contrast: #fb3afb; | ||||
|     --catch-detail-color: black; /*#3a3aeb;*/ | ||||
|     --catch-detail-foregroundcolor: white; | ||||
|     --catch-detail-color-contrast: #fb3afb; | ||||
| 
 | ||||
|   --image-carousel-height: 350px; | ||||
|     --image-carousel-height: 350px; | ||||
| 
 | ||||
|   /** Technical value, used by icon.svelte | ||||
|      */ | ||||
|   --svg-color: #000000; | ||||
|     /** Technical value, used by icon.svelte | ||||
|        */ | ||||
|     --svg-color: #000000; | ||||
| } | ||||
| 
 | ||||
| @font-face{ | ||||
|     font-family:"Source Sans Pro"; | ||||
|     src:url("/assets/source-sans-pro.regular.ttf") format("woff"); | ||||
| @font-face { | ||||
|     font-family: "Source Sans Pro"; | ||||
|     src: url("/assets/source-sans-pro.regular.ttf") format("woff"); | ||||
| } | ||||
| 
 | ||||
| /***********************************************************************\ | ||||
|  | @ -76,13 +76,13 @@ | |||
| \***********************************************************************/ | ||||
| html, | ||||
| body { | ||||
|   height: 100%; | ||||
|   min-height: 100vh; | ||||
|   min-height: -webkit-fill-available; | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
|   background-color: var(--background-color); | ||||
|   color: var(--foreground-color); | ||||
|     height: 100%; | ||||
|     min-height: 100vh; | ||||
|     min-height: -webkit-fill-available; | ||||
|     margin: 0; | ||||
|     padding: 0; | ||||
|     background-color: var(--background-color); | ||||
|     color: var(--foreground-color); | ||||
| 
 | ||||
|     font-family: 'Source Sans Pro'; | ||||
|     font-style: normal; | ||||
|  | @ -93,52 +93,52 @@ body { | |||
| 
 | ||||
| svg, | ||||
| img { | ||||
|   box-sizing: content-box; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|     box-sizing: content-box; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
| } | ||||
| 
 | ||||
| li { | ||||
|   margin-left: 0.5em; | ||||
|   padding-left: 0.2em; | ||||
|   margin-top: 0.1em; | ||||
|     margin-left: 0.5em; | ||||
|     padding-left: 0.2em; | ||||
|     margin-top: 0.1em; | ||||
| } | ||||
| 
 | ||||
| li::marker { | ||||
|   content: "•"; | ||||
|     content: "•"; | ||||
| } | ||||
| 
 | ||||
| h1 { | ||||
|   font-size: xx-large; | ||||
|   margin-top: 0.6em; | ||||
|   margin-bottom: 0.4em; | ||||
|   font-weight: bold; | ||||
|     font-size: xx-large; | ||||
|     margin-top: 0.6em; | ||||
|     margin-bottom: 0.4em; | ||||
|     font-weight: bold; | ||||
| } | ||||
| 
 | ||||
| h2 { | ||||
|   font-size: x-large; | ||||
|   margin-top: 0.5em; | ||||
|   margin-bottom: 0; /*Disable margin bottom to play nicely with accordeons from flowbite*/ | ||||
|   font-weight: bold; | ||||
|     font-size: x-large; | ||||
|     margin-top: 0.5em; | ||||
|     margin-bottom: 0; /*Disable margin bottom to play nicely with accordeons from flowbite*/ | ||||
|     font-weight: bold; | ||||
| } | ||||
| 
 | ||||
| h3 { | ||||
|   font-size: larger; | ||||
|   margin-top: 0.6em; | ||||
|   margin-bottom: 0; | ||||
|   font-weight: bold; | ||||
|     font-size: larger; | ||||
|     margin-top: 0.6em; | ||||
|     margin-bottom: 0; | ||||
|     font-weight: bold; | ||||
| } | ||||
| 
 | ||||
| p { | ||||
|   padding-top: 0.1em; | ||||
|     padding-top: 0.1em; | ||||
| } | ||||
| 
 | ||||
| input { | ||||
|   color: var(--foreground-color); | ||||
|     color: var(--foreground-color); | ||||
| } | ||||
| 
 | ||||
| input[type="text"] { | ||||
|   width: 100%; | ||||
|     width: 100%; | ||||
| } | ||||
| 
 | ||||
| /************************* BIG CATEGORIES ********************************/ | ||||
|  | @ -149,33 +149,33 @@ input[type="text"] { | |||
|  */ | ||||
| 
 | ||||
| .subtle-background { | ||||
|   background: var(--subtle-detail-color); | ||||
|   color: var(--subtle-detail-color-contrast); | ||||
|     background: var(--subtle-detail-color); | ||||
|     color: var(--subtle-detail-color-contrast); | ||||
| } | ||||
| 
 | ||||
| .normal-background { | ||||
|   background: var(--background-color); | ||||
|   color: var(--foreground-color); | ||||
|     background: var(--background-color); | ||||
|     color: var(--foreground-color); | ||||
| } | ||||
| 
 | ||||
| .low-interaction { | ||||
|   background: var(--low-interaction-background); | ||||
|   color: var(--low-interaction-foreground); | ||||
|     background: var(--low-interaction-background); | ||||
|     color: var(--low-interaction-foreground); | ||||
| } | ||||
| 
 | ||||
| .interactive { | ||||
|   background: var(--interactive-background); | ||||
|   color: var(--interactive-foreground); | ||||
|     background: var(--interactive-background); | ||||
|     color: var(--interactive-foreground); | ||||
| } | ||||
| 
 | ||||
| .border-interactive { | ||||
|   border: 2px dashed var(--catch-detail-color-contrast); | ||||
|   border-radius: 0.5rem; | ||||
|     border: 2px dashed var(--catch-detail-color-contrast); | ||||
|     border-radius: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .border-region { | ||||
|   border: 2px dashed var(--interactive-background); | ||||
|   border-radius: 0.5rem; | ||||
|     border: 2px dashed var(--interactive-background); | ||||
|     border-radius: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| /******************* Styling of input elements **********************/ | ||||
|  | @ -244,9 +244,11 @@ button.disabled { | |||
|     color: var(--disabled-font); | ||||
|     cursor: unset; | ||||
| } | ||||
| 
 | ||||
| button.disabled svg path { | ||||
|     transition: all 200ms; | ||||
| } | ||||
| 
 | ||||
| button.disabled svg path { | ||||
|     fill: var(--disabled-font); | ||||
|     stroke: var(--disabled-font); | ||||
|  | @ -294,49 +296,49 @@ button.unstyled { | |||
| } | ||||
| 
 | ||||
| .links-w-full a:not(.weblate-link), .links-w-full button.as-link { | ||||
|   display: flex; | ||||
|   column-gap: 0.25rem; | ||||
|   padding-left: 0.5rem; | ||||
|   padding-right: 0.5rem; | ||||
|   width: 100%; | ||||
|     display: flex; | ||||
|     column-gap: 0.25rem; | ||||
|     padding-left: 0.5rem; | ||||
|     padding-right: 0.5rem; | ||||
|     width: 100%; | ||||
| } | ||||
| 
 | ||||
| select { | ||||
|   border: 2px solid #00000000; | ||||
|   color: var(--foreground-color) !important; | ||||
|   background-color: var(--low-interaction-background) !important; | ||||
|     border: 2px solid #00000000; | ||||
|     color: var(--foreground-color) !important; | ||||
|     background-color: var(--low-interaction-background) !important; | ||||
| } | ||||
| 
 | ||||
| select:hover { | ||||
|   border-color: var(--catch-detail-color-contrast); | ||||
|     border-color: var(--catch-detail-color-contrast); | ||||
| } | ||||
| 
 | ||||
| .neutral-label { | ||||
|   /** This label styles as normal text. It's power comes from the many :not(.neutral-label) entries. | ||||
|      * Placed here for autocompletion | ||||
|      */ | ||||
|     /** This label styles as normal text. It's power comes from the many :not(.neutral-label) entries. | ||||
|        * Placed here for autocompletion | ||||
|        */ | ||||
| } | ||||
| 
 | ||||
| label:not(.neutral-label):not(.button) { | ||||
|   /** | ||||
|      * Label should _contain_ the input element | ||||
|      */ | ||||
|   padding: 0.25rem; | ||||
|   padding-right: 0.5rem; | ||||
|   padding-left: 0.5rem; | ||||
|   margin:0.25rem; | ||||
|   border-radius: 0.5rem; | ||||
|   width: 100%; | ||||
|   box-sizing: border-box; | ||||
|   transition: all 250ms; | ||||
|     /** | ||||
|        * Label should _contain_ the input element | ||||
|        */ | ||||
|     padding: 0.25rem; | ||||
|     padding-right: 0.5rem; | ||||
|     padding-left: 0.5rem; | ||||
|     margin: 0.25rem; | ||||
|     border-radius: 0.5rem; | ||||
|     width: 100%; | ||||
|     box-sizing: border-box; | ||||
|     transition: all 250ms; | ||||
| } | ||||
| 
 | ||||
| label.button { | ||||
|   width: 100%; | ||||
|     width: 100%; | ||||
| } | ||||
| 
 | ||||
| label:hover:not(.neutral-label) { | ||||
|   background-color: var(--low-interaction-background); | ||||
|     background-color: var(--low-interaction-background); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -346,7 +348,7 @@ label.checked:not(.neutral-label) { | |||
| } | ||||
| 
 | ||||
| textarea { | ||||
|   color: black; | ||||
|     color: black; | ||||
| } | ||||
| 
 | ||||
| h2.group { | ||||
|  | @ -368,97 +370,97 @@ h2.group { | |||
|  */ | ||||
| 
 | ||||
| .thanks { | ||||
|   /* The class to indicate 'operation successful' or 'thank you for contributing' */ | ||||
|   font-weight: bold; | ||||
|   border-radius: 1em; | ||||
|   margin: 0.25em; | ||||
|   text-align: center; | ||||
|   padding: 0.25rem; | ||||
|   padding-left: 0.5rem; | ||||
|   padding-right: 0.5rem; | ||||
|   border: 3px dotted #58cd27; | ||||
|   background-color: #58cd2722; | ||||
|     /* The class to indicate 'operation successful' or 'thank you for contributing' */ | ||||
|     font-weight: bold; | ||||
|     border-radius: 1em; | ||||
|     margin: 0.25em; | ||||
|     text-align: center; | ||||
|     padding: 0.25rem; | ||||
|     padding-left: 0.5rem; | ||||
|     padding-right: 0.5rem; | ||||
|     border: 3px dotted #58cd27; | ||||
|     background-color: #58cd2722; | ||||
| } | ||||
| 
 | ||||
| .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; | ||||
|   border: 2px dotted #ff9143; | ||||
|     /* 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; | ||||
|     border: 2px dotted #ff9143; | ||||
| } | ||||
| 
 | ||||
| .warning { | ||||
|   /* The class to convey important information, but not as grave as 'alert' */ | ||||
|   background-color: var(--low-interaction-background); | ||||
|   color: var(--alert-foreground-color); | ||||
|   font-weight: bold; | ||||
|   border-radius: 1em; | ||||
|   margin: 0.25em; | ||||
|   text-align: center; | ||||
|   padding: 0.15em 0.3em; | ||||
|   border: 3px dotted #ff9143; | ||||
|     /* The class to convey important information, but not as grave as 'alert' */ | ||||
|     background-color: var(--low-interaction-background); | ||||
|     color: var(--alert-foreground-color); | ||||
|     font-weight: bold; | ||||
|     border-radius: 1em; | ||||
|     margin: 0.25em; | ||||
|     text-align: center; | ||||
|     padding: 0.15em 0.3em; | ||||
|     border: 3px dotted #ff9143; | ||||
| } | ||||
| 
 | ||||
| .low-interaction .warning { | ||||
|   background-color: var(--interactive-background); | ||||
|     background-color: var(--interactive-background); | ||||
| } | ||||
| 
 | ||||
| .information { | ||||
|   /* The class to convey important information which does _not_ denote an error... */ | ||||
|   background-color: var(--low-interaction-background); | ||||
|   color: var(--alert-foreground-color); | ||||
|   border-radius: 1em; | ||||
|   margin: 0.25em; | ||||
|   text-align: center; | ||||
|   padding: 0.15em 0.3em; | ||||
|   border: 3px dotted var(--catch-detail-color-contrast); | ||||
|     /* The class to convey important information which does _not_ denote an error... */ | ||||
|     background-color: var(--low-interaction-background); | ||||
|     color: var(--alert-foreground-color); | ||||
|     border-radius: 1em; | ||||
|     margin: 0.25em; | ||||
|     text-align: center; | ||||
|     padding: 0.15em 0.3em; | ||||
|     border: 3px dotted var(--catch-detail-color-contrast); | ||||
| } | ||||
| 
 | ||||
| .low-interaction .interactive { | ||||
|   background-color: var(--interactive-background); | ||||
|     background-color: var(--interactive-background); | ||||
| } | ||||
| 
 | ||||
| .subtle { | ||||
|   /* For all information that is not important for 99% of the users */ | ||||
|   color: #666; | ||||
|   font-weight: normal; | ||||
|     /* For all information that is not important for 99% of the users */ | ||||
|     color: #666; | ||||
|     font-weight: normal; | ||||
| } | ||||
| 
 | ||||
| .low-interaction .subtle { | ||||
|   color: #444; | ||||
|     color: #444; | ||||
| } | ||||
| 
 | ||||
| .interactive .subtle { | ||||
|   color: #333; | ||||
|     color: #333; | ||||
| } | ||||
| 
 | ||||
| .link-underline .subtle a { | ||||
|   text-decoration: underline 1px #7193bb88; | ||||
|   -webkit-text-decoration: underline; | ||||
|   color: #7193bb; | ||||
|     text-decoration: underline 1px #7193bb88; | ||||
|     -webkit-text-decoration: underline; | ||||
|     color: #7193bb; | ||||
| } | ||||
| 
 | ||||
| .literal-code, | ||||
| 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; | ||||
|     /* 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; | ||||
|     background-color: #b3b3b3; | ||||
| } | ||||
| 
 | ||||
| /************************** UTILITY ************************/ | ||||
|  | @ -468,98 +470,104 @@ code { | |||
|  */ | ||||
| 
 | ||||
| .text-white a { | ||||
|   /* Used solely in 'imageAttribution'  and in many themes*/ | ||||
|   color: var(--background-color); | ||||
|     /* Used solely in 'imageAttribution'  and in many themes*/ | ||||
|     color: var(--background-color); | ||||
| } | ||||
| 
 | ||||
| .bg-black-transparent { | ||||
|   background-color: #00000088; | ||||
|     background-color: #00000088; | ||||
| } | ||||
| 
 | ||||
| .block-ruby { | ||||
|   display: block ruby; | ||||
|     display: block ruby; | ||||
| } | ||||
| 
 | ||||
| .rounded-left-full { | ||||
|   border-bottom-left-radius: 999rem; | ||||
|   border-top-left-radius: 999rem; | ||||
|     border-bottom-left-radius: 999rem; | ||||
|     border-top-left-radius: 999rem; | ||||
| } | ||||
| 
 | ||||
| .rounded-right-full { | ||||
|   border-bottom-right-radius: 999rem; | ||||
|   border-top-right-radius: 999rem; | ||||
|     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; | ||||
|     /* Used solely in 'imageAttribution' and in many themes for the label*/ | ||||
|     display: none; | ||||
| } | ||||
| 
 | ||||
| .weblate-link { | ||||
|   display: inline-block; | ||||
|   flex-shrink: 0; | ||||
|   margin: 0; | ||||
|   padding: 0.25rem; | ||||
|   width: 1.2rem; | ||||
|   height: 1.2rem; | ||||
|   border: unset; | ||||
|   border-radius: 5rem; | ||||
|   backdrop-filter: var(--low-interaction-background); | ||||
|     display: inline-block; | ||||
|     flex-shrink: 0; | ||||
|     margin: 0; | ||||
|     padding: 0.25rem; | ||||
|     width: 1.2rem; | ||||
|     height: 1.2rem; | ||||
|     border: unset; | ||||
|     border-radius: 5rem; | ||||
|     backdrop-filter: var(--low-interaction-background); | ||||
| } | ||||
| 
 | ||||
| .no-weblate .weblate-link { | ||||
|   display: none !important; | ||||
|     display: none !important; | ||||
| } | ||||
| 
 | ||||
| .link-underline a { | ||||
|   text-decoration: underline 1px var(--foreground-color); | ||||
|   -webkit-text-decoration: underline; | ||||
|     text-decoration: underline 1px var(--foreground-color); | ||||
|     -webkit-text-decoration: underline; | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| a.link-underline { | ||||
|   text-decoration: underline 1px var(--foreground-color); | ||||
|   -webkit-text-decoration: underline; | ||||
|     text-decoration: underline 1px var(--foreground-color); | ||||
|     -webkit-text-decoration: underline; | ||||
| } | ||||
| 
 | ||||
| .link-no-underline a, a.link-no-underline { | ||||
|   text-decoration: none; | ||||
|     text-decoration: none; | ||||
| } | ||||
| 
 | ||||
| .disable-links a { | ||||
|   pointer-events: none; | ||||
|   text-decoration: none !important; | ||||
|   color: var(--subtle-detail-color-contrast) !important; | ||||
|     pointer-events: none; | ||||
|     text-decoration: none !important; | ||||
|     color: var(--subtle-detail-color-contrast) !important; | ||||
| } | ||||
| 
 | ||||
| .enable-links a { | ||||
|   pointer-events: unset; | ||||
|   text-decoration: underline !important; | ||||
|   -webkit-text-decoration: underline !important; | ||||
|   color: unset !important; | ||||
|     pointer-events: unset; | ||||
|     text-decoration: underline !important; | ||||
|     -webkit-text-decoration: underline !important; | ||||
|     color: unset !important; | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| a:hover { | ||||
|     background-color: var(--low-interaction-background); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| .disable-links a.must-link, | ||||
| .disable-links .must-link a { | ||||
|   /* Hide links if they are disabled */ | ||||
|   display: none; | ||||
|     /* Hide links if they are disabled */ | ||||
|     display: none; | ||||
| } | ||||
| 
 | ||||
| .zebra-table tr:nth-child(even) { | ||||
|   background-color: #f2f2f2; | ||||
|     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 | ||||
|     */ | ||||
|   stroke: white !important; | ||||
|   stroke-width: 20px !important; | ||||
|   overflow: visible !important; | ||||
|   -webkit-animation: glowing-drop-shadow 1s ease-in-out infinite alternate; | ||||
|   -moz-animation: glowing-drop-shadow 1s ease-in-out infinite alternate; | ||||
|   animation: glowing-drop-shadow 1s ease-in-out infinite alternate; | ||||
|     /* A marker on the map gets the 'selected' class when it's properties are displayed | ||||
|       */ | ||||
|     stroke: white !important; | ||||
|     stroke-width: 20px !important; | ||||
|     overflow: visible !important; | ||||
|     -webkit-animation: glowing-drop-shadow 1s ease-in-out infinite alternate; | ||||
|     -moz-animation: glowing-drop-shadow 1s ease-in-out infinite alternate; | ||||
|     animation: glowing-drop-shadow 1s ease-in-out infinite alternate; | ||||
| } | ||||
| 
 | ||||
| .selected .light-icon svg:not(.noselect *) path.selectable { | ||||
|  | @ -568,76 +576,75 @@ a.link-underline { | |||
| } | ||||
| 
 | ||||
| .selected svg { | ||||
|   /* A marker on the map gets the 'selected' class when it's properties are displayed | ||||
|     */ | ||||
|   overflow: visible !important; | ||||
|     /* A marker on the map gets the 'selected' class when it's properties are displayed | ||||
|       */ | ||||
|     overflow: visible !important; | ||||
| } | ||||
| 
 | ||||
| svg.apply-fill path { | ||||
|   fill: var(--svg-color); | ||||
|     fill: var(--svg-color); | ||||
| } | ||||
| 
 | ||||
| .compass_arrow { | ||||
|   width: calc(2.5rem - 1px); | ||||
|   height: calc(2.5rem - 1px); | ||||
|     width: calc(2.5rem - 1px); | ||||
|     height: calc(2.5rem - 1px); | ||||
| } | ||||
| 
 | ||||
| @media (min-width: 640px) { | ||||
|   .compass_arrow { | ||||
|     width: calc(2.75rem - 1px); | ||||
|     height: calc(2.75rem - 1px); | ||||
|   } | ||||
|     .compass_arrow { | ||||
|         width: calc(2.75rem - 1px); | ||||
|         height: calc(2.75rem - 1px); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @-webkit-keyframes glowing-drop-shadow { | ||||
|   from { | ||||
|     filter: drop-shadow(5px 5px 60px rgb(128 128 128 / 0.6)); | ||||
|   } | ||||
|   to { | ||||
|     filter: drop-shadow(5px 5px 80px rgb(0.5 0.5 0.5 / 0.8)); | ||||
|   } | ||||
|     from { | ||||
|         filter: drop-shadow(5px 5px 60px rgb(128 128 128 / 0.6)); | ||||
|     } | ||||
|     to { | ||||
|         filter: drop-shadow(5px 5px 80px rgb(0.5 0.5 0.5 / 0.8)); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @keyframes slide { | ||||
|   /* This is the animation on the marker to add a new point - it slides through all the possible presets */ | ||||
|   from { | ||||
|     transform: translateX(0%); | ||||
|   } | ||||
|     /* This is the animation on the marker to add a new point - it slides through all the possible presets */ | ||||
|     from { | ||||
|         transform: translateX(0%); | ||||
|     } | ||||
| 
 | ||||
|   to { | ||||
|     transform: translateX(calc(-100% + 42px)); | ||||
|   } | ||||
|     to { | ||||
|         transform: translateX(calc(-100% + 42px)); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| /************************* 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; | ||||
|     /* 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; | ||||
|     /* Legacy: should be replaced by headlessui disclosure in time */ | ||||
|     transition: max-height 0.5s ease-in-out; | ||||
|     overflow-y: hidden; | ||||
| } | ||||
| 
 | ||||
| .min-h-32 { | ||||
|   min-height: 8rem; | ||||
|     min-height: 8rem; | ||||
| } | ||||
| 
 | ||||
| .max-w-full { | ||||
|   max-width: 100%; | ||||
|     max-width: 100%; | ||||
| } | ||||
| 
 | ||||
| /************************* Experimental support for foldable devices ********************************/ | ||||
| @media (horizontal-viewport-segments: 2) { | ||||
|   .theme-list { | ||||
|     display: grid; | ||||
|     grid-auto-flow: row; | ||||
|     grid-template-columns: repeat(2, minmax(0, 1fr)); | ||||
|   } | ||||
|     .theme-list { | ||||
|         display: grid; | ||||
|         grid-auto-flow: row; | ||||
|         grid-template-columns: repeat(2, minmax(0, 1fr)); | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue