Merge develop

This commit is contained in:
Pieter Vander Vennet 2024-08-29 23:39:56 +02:00
commit 3be286c2b1
30 changed files with 3315 additions and 2458 deletions

View file

@ -301,8 +301,11 @@
"logout": "Log out", "logout": "Log out",
"mappingsAreHidden": "Some options are hidden. Use search to show more options.", "mappingsAreHidden": "Some options are hidden. Use search to show more options.",
"menu": { "menu": {
"aboutCurrentThemeTitle": "About this map",
"aboutMapComplete": "About MapComplete", "aboutMapComplete": "About MapComplete",
"filter": "Filter data" "filter": "Filter data",
"moreUtilsTitle": "Discover more",
"showIntroduction": "Show introduction"
}, },
"morescreen": { "morescreen": {
"createYourOwnTheme": "Create your own MapComplete theme from scratch", "createYourOwnTheme": "Create your own MapComplete theme from scratch",

File diff suppressed because it is too large Load diff

View file

@ -4,24 +4,23 @@ import { MenuState } from "../../Models/MenuState"
export default class ThemeViewStateHashActor { export default class ThemeViewStateHashActor {
private readonly _state: ThemeViewState private readonly _state: ThemeViewState
private isUpdatingHash = false
public static readonly documentation = [ public static readonly documentation = [
"The URL-hash can contain multiple values:", "The URL-hash can contain multiple values:",
"", "",
"- The id of the currently selected object, e.g. `node/1234`", "- The id of the currently selected object, e.g. `node/1234`",
"- The currently opened menu view", "- The currently opened menu view",
"- The base64-encoded JSON-file specifying a custom theme (only when loading)",
"", "",
"### Possible hashes to open a menu", "### Possible hashes to open a menu",
"", "",
"The possible hashes are:", "The possible hashes are:",
"", "",
MenuState._menuviewTabs.map((tab) => "`menu:" + tab + "`").join(","), MenuState.pageNames.map((tab) => "`" + tab + "`").join(",")
MenuState._themeviewTabs.map((tab) => "`theme-menu:" + tab + "`").join(","),
] ]
/** /**
* Converts the hash to the appropriate themeview state and, vice versa, sets the hash. * Converts the hash to the appropriate theme-view state and, vice versa, sets the hash.
* *
* As the navigator-back-button changes the hash first, this class thus also handles the 'back'-button events. * As the navigator-back-button changes the hash first, this class thus also handles the 'back'-button events.
* *
@ -33,46 +32,40 @@ export default class ThemeViewStateHashActor {
constructor(state: ThemeViewState) { constructor(state: ThemeViewState) {
this._state = state this._state = state
const hashOnLoad = Hash.hash.data
const containsMenu = this.loadStateFromHash(hashOnLoad)
// First of all, try to recover the selected element // First of all, try to recover the selected element
if (Hash.hash.data) { if (!containsMenu && hashOnLoad?.length > 0) {
const hash = Hash.hash.data state.indexedFeatures.featuresById.addCallbackAndRunD(() => {
this.loadStateFromHash(hash)
Hash.hash.setData(hash) // reapply the previous hash
state.indexedFeatures.featuresById.addCallbackAndRunD((_) => {
let unregister = this.loadSelectedElementFromHash(hash)
// once that we have found a matching element, we can be sure the indexedFeaturesource was popuplated and that the job is done // once that we have found a matching element, we can be sure the indexedFeaturesource was popuplated and that the job is done
return unregister return this.loadSelectedElementFromHash(hashOnLoad)
}) })
} }
// Register a hash change listener to correctly handle the back button
Hash.hash.addCallback((hash) => {
if (!!hash) {
// There is still a hash
// We _only_ have to (at most) close the overlays in this case
if (state.previewedImage.data) {
state.previewedImage.setData(undefined)
return
}
const parts = hash.split(";")
if (parts.indexOf("background") < 0) {
state.guistate.backgroundLayerSelectionIsOpened.setData(false)
}
this.loadSelectedElementFromHash(hash)
} else {
this.back()
}
})
// At last, register callbacks on the state to update the hash when they change. // At last, register callbacks on the state to update the hash when they change.
// Note: these should use 'addCallback', not 'addCallbackAndRun' // Note: these should use 'addCallback', not 'addCallbackAndRun'
state.selectedElement.addCallback((_) => this.setHash()) state.selectedElement.addCallback(() => this.setHash())
state.guistate.allToggles.forEach(({ toggle, submenu }) => {
submenu?.addCallback((_) => this.setHash()) // Register a hash change listener to correctly handle the back button
toggle.addCallback((_) => this.setHash()) Hash.hash.addCallback((hash) => {
if(this.isUpdatingHash){
return
}
if (!hash) {
this.back()
} else {
if (!this.loadStateFromHash(hash)) {
this.loadSelectedElementFromHash(hash)
}
}
}) })
for (const key in state.guistate.pageStates) {
const toggle = state.guistate.pageStates[key]
toggle.addCallback(() => this.setHash())
}
// When all is done, set the hash. This must happen last to give the code above correct info // When all is done, set the hash. This must happen last to give the code above correct info
this.setHash() this.setHash()
} }
@ -80,15 +73,10 @@ export default class ThemeViewStateHashActor {
/** /**
* Selects the appropriate element * Selects the appropriate element
* Returns true if this method can be unregistered for the first run * Returns true if this method can be unregistered for the first run
* @param hash
* @private
*/ */
private loadSelectedElementFromHash(hash: string): boolean { private loadSelectedElementFromHash(hash: string): boolean {
const state = this._state const state = this._state
const selectedElement = state.selectedElement const selectedElement = state.selectedElement
// state.indexedFeatures.featuresById.stabilized(250)
hash = hash.split(";")[0] // The 'selectedElement' is always the _first_ item in the hash (if any)
// Set the hash based on the selected element... // Set the hash based on the selected element...
// ... search and select an element based on the hash // ... search and select an element based on the hash
@ -101,7 +89,7 @@ export default class ThemeViewStateHashActor {
if (!found) { if (!found) {
return false return false
} }
if (found.properties.id === "last_click") { if (found.properties.id.startsWith("last_click")) {
return true return true
} }
console.log( console.log(
@ -114,67 +102,47 @@ export default class ThemeViewStateHashActor {
return true return true
} }
private loadStateFromHash(hash: string) { private loadStateFromHash(hash: string): boolean {
const state = this._state for (const page in this._state.guistate.pageStates) {
for (const superpart of hash.split(";")) { if (page === hash) {
const parts = superpart.at(-1)?.split(":") ?? [] const toggle = this._state.guistate.pageStates[page]
toggle.set(true)
outer: for (const { toggle, name, submenu } of state.guistate.allToggles) { console.log("Loading menu view from hash:", page)
for (const part of parts) { return true
if (part.indexOf(":") < 0) {
if (part === name) {
toggle.setData(true)
continue outer
}
continue
}
const [main, submenuValue] = part.split(":")
if (part !== main) {
continue
}
toggle.setData(true)
submenu?.setData(submenuValue)
continue outer
}
// If we arrive here, the loop above has not found any match
toggle.setData(false)
} }
} }
return false
} }
private setHash() { /**
const s = this._state * Sets the hash based on:
let h = "" *
* 1. Selected element ID
* 2. A selected 'page' from the menu
*
* returns 'true' if a hash was set
*/
private setHash(): boolean {
this.isUpdatingHash = true
try {
for (const { toggle, showOverOthers, name, submenu } of s.guistate.allToggles) { const selectedElement = this._state.selectedElement.data
if (showOverOthers || !toggle.data) { if (selectedElement) {
continue Hash.hash.set(selectedElement.properties.id)
return true
} }
h = name for (const page in this._state.guistate.pageStates) {
if (submenu?.data) { const toggle = this._state.guistate.pageStates[page]
h += ":" + submenu.data if (toggle.data) {
Hash.hash.set(page)
return true
}
} }
Hash.hash.set(undefined)
return false
} finally {
this.isUpdatingHash = false
} }
if (s.selectedElement.data !== undefined) {
h = s.selectedElement.data.properties.id
}
for (const { toggle, showOverOthers, name, submenu } of s.guistate.allToggles) {
if (!showOverOthers || !toggle.data) {
continue
}
if (h) {
h += ";" + name
} else {
h = name
}
if (submenu?.data) {
h += ":" + submenu.data
}
}
Hash.hash.setData(h)
} }
private back() { private back() {
@ -183,13 +151,9 @@ export default class ThemeViewStateHashActor {
state.previewedImage.setData(undefined) state.previewedImage.setData(undefined)
return return
} }
// history.pushState(null, null, window.location.pathname);
if (state.selectedElement.data) {
state.selectedElement.setData(undefined)
return
}
if (state.guistate.closeAll()) { if (state.guistate.closeAll()) {
return return
} }
state.selectedElement.setData(undefined)
} }
} }

View file

@ -2,11 +2,10 @@ import LayerConfig from "./ThemeConfig/LayerConfig"
import { UIEventSource } from "../Logic/UIEventSource" import { UIEventSource } from "../Logic/UIEventSource"
import UserRelatedState from "../Logic/State/UserRelatedState" import UserRelatedState from "../Logic/State/UserRelatedState"
import { Utils } from "../Utils" import { Utils } from "../Utils"
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"
import Zoomcontrol from "../UI/Zoomcontrol" import Zoomcontrol from "../UI/Zoomcontrol"
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"
export type ThemeViewTabStates = (typeof MenuState._themeviewTabs)[number] export type PageType = (typeof MenuState.pageNames)[number]
export type MenuViewTabStates = (typeof MenuState._menuviewTabs)[number]
/** /**
* Indicates if a menu is open, and if so, which tab is selected; * Indicates if a menu is open, and if so, which tab is selected;
@ -15,141 +14,44 @@ export type MenuViewTabStates = (typeof MenuState._menuviewTabs)[number]
* Some convenience methods are provided for this as well * Some convenience methods are provided for this as well
*/ */
export class MenuState { export class MenuState {
public static readonly _themeviewTabs = ["intro", "download", "copyright", "share"] as const
public static readonly _menuviewTabs = [
"about", public static readonly pageNames = [
"settings", "copyright", "copyright_icons", "community_index", "hotkeys",
"favourites", "privacy", "filter", "background", "about_theme", "download", "favourites",
"community", "usersettings", "share"
"privacy",
"advanced",
] as const ] as const
public readonly themeIsOpened: UIEventSource<boolean>
public readonly themeViewTabIndex: UIEventSource<number>
public readonly themeViewTab: UIEventSource<ThemeViewTabStates>
public readonly menuIsOpened: UIEventSource<boolean>
public readonly menuViewTabIndex: UIEventSource<number>
public readonly menuViewTab: UIEventSource<MenuViewTabStates>
public readonly backgroundLayerSelectionIsOpened: UIEventSource<boolean> = public readonly menuIsOpened = new UIEventSource(false)
new UIEventSource<boolean>(false) public readonly pageStates: Record<PageType, UIEventSource<boolean>>
public readonly filtersPanelIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>(false)
public readonly privacyPanelIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>(false)
/**
* Standalone copyright panel
*/
public readonly copyrightPanelIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>(
false
)
public readonly communityIndexPanelIsOpened: UIEventSource<boolean> = new UIEventSource(false)
public readonly allToggles: {
toggle: UIEventSource<boolean>
name: string
submenu?: UIEventSource<string>
showOverOthers?: boolean
}[]
public readonly highlightedLayerInFilters: UIEventSource<string> = new UIEventSource<string>( public readonly highlightedLayerInFilters: UIEventSource<string> = new UIEventSource<string>(
undefined undefined
) )
public highlightedUserSetting: UIEventSource<string> = new UIEventSource<string>(undefined) public highlightedUserSetting: UIEventSource<string> = new UIEventSource<string>(undefined)
constructor(shouldOpenWelcomeMessage: boolean, themeid: string = "") { constructor(shouldShowWelcomeMessage: boolean, themeid: string) {
// Note: this class is _not_ responsible to update the Hash, @see ThemeViewStateHashActor for this // Note: this class is _not_ responsible to update the Hash, @see ThemeViewStateHashActor for this
if (themeid) { const states = {}
themeid += "-" for (const pageName of MenuState.pageNames) {
} const toggle = new UIEventSource(false)
this.themeIsOpened = LocalStorageSource.GetParsed( states[pageName] = toggle
themeid + "thememenuisopened",
shouldOpenWelcomeMessage
)
this.themeViewTabIndex = LocalStorageSource.GetParsed(themeid + "themeviewtabindex", 0)
this.themeViewTab = this.themeViewTabIndex.sync(
(i) => MenuState._themeviewTabs[i],
[],
(str) => MenuState._themeviewTabs.indexOf(<any>str)
)
this.menuIsOpened = LocalStorageSource.GetParsed(themeid + "menuisopened", false) toggle.addCallback(enabled => {
this.menuViewTabIndex = LocalStorageSource.GetParsed(themeid + "menuviewtabindex", 0) if (enabled) {
this.menuViewTab = this.menuViewTabIndex.sync( this.menuIsOpened.set(false)
(i) => MenuState._menuviewTabs[i],
[],
(str) => MenuState._menuviewTabs.indexOf(<any>str)
)
this.menuIsOpened.addCallbackAndRun((isOpen) => {
if (!isOpen) {
this.highlightedUserSetting.setData(undefined)
}
})
this.menuViewTab.addCallbackD((tab) => {
if (tab !== "settings") {
this.highlightedUserSetting.setData(undefined)
}
})
this.filtersPanelIsOpened.addCallbackAndRun((isOpen) => {
if (!isOpen) {
this.highlightedLayerInFilters.setData(undefined)
}
})
this.menuIsOpened.addCallbackAndRunD((opened) => {
if (opened) {
this.themeIsOpened.setData(false)
}
})
this.themeIsOpened.addCallbackAndRunD((opened) => {
if (opened) {
this.menuIsOpened.setData(false)
}
})
this.allToggles = [
{
toggle: this.privacyPanelIsOpened,
name: "privacy",
showOverOthers: true,
},
{
toggle: this.copyrightPanelIsOpened,
name: "copyright",
showOverOthers: true,
},
{
toggle: this.communityIndexPanelIsOpened,
name: "community",
showOverOthers: true,
},
{
toggle: this.filtersPanelIsOpened,
name: "filters",
showOverOthers: true,
},
{
toggle: this.menuIsOpened,
name: "menu",
submenu: this.menuViewTab,
},
{
toggle: this.themeIsOpened,
name: "theme-menu",
submenu: this.themeViewTab,
},
{
toggle: this.backgroundLayerSelectionIsOpened,
name: "background",
showOverOthers: true,
},
]
for (const toggle of this.allToggles) {
toggle.toggle.addCallback((isOpen) => {
if (!isOpen) {
this.resetZoomIfAllClosed()
} }
}) })
} }
this.pageStates = <Record<PageType, UIEventSource<boolean>>>states
const visitedBefore = LocalStorageSource.GetParsed<boolean>(
themeid + "thememenuisopened", false
)
if (!visitedBefore.data && shouldShowWelcomeMessage) {
this.pageStates.about_theme.set(true)
visitedBefore.set(true)
}
} }
private resetZoomIfAllClosed() { private resetZoomIfAllClosed() {
@ -160,7 +62,7 @@ export class MenuState {
} }
public openFilterView(highlightLayer?: LayerConfig | string) { public openFilterView(highlightLayer?: LayerConfig | string) {
this.filtersPanelIsOpened.setData(true) this.pageStates.filter.setData(true)
if (highlightLayer) { if (highlightLayer) {
if (typeof highlightLayer !== "string") { if (typeof highlightLayer !== "string") {
highlightLayer = highlightLayer.id highlightLayer = highlightLayer.id
@ -170,8 +72,6 @@ export class MenuState {
} }
public openUsersettings(highlightTagRendering?: string) { public openUsersettings(highlightTagRendering?: string) {
this.menuIsOpened.setData(true)
this.menuViewTab.setData("settings")
if ( if (
highlightTagRendering !== undefined && highlightTagRendering !== undefined &&
!UserRelatedState.availableUserSettingsIds.some((tr) => tr === highlightTagRendering) !UserRelatedState.availableUserSettingsIds.some((tr) => tr === highlightTagRendering)
@ -189,7 +89,7 @@ export class MenuState {
} }
public isSomethingOpen(): boolean { public isSomethingOpen(): boolean {
return this.allToggles.some((t) => t.toggle.data) return Object.values(this.pageStates).some((t) => t.data)
} }
/** /**
@ -197,14 +97,18 @@ export class MenuState {
* Returns 'true' if at least one menu was opened * Returns 'true' if at least one menu was opened
*/ */
public closeAll(): boolean { public closeAll(): boolean {
let somethingWasOpen = false for (const key in this.pageStates) {
for (const t of this.allToggles) { const toggle = this.pageStates[key]
somethingWasOpen = t.toggle.data const wasOpen = toggle.data
t.toggle.setData(false) toggle.setData(false)
if (somethingWasOpen) { if (wasOpen) {
break return true
} }
} }
return somethingWasOpen if (this.menuIsOpened.data) {
this.menuIsOpened.set(false)
return true
}
} }
} }

View file

@ -2,7 +2,11 @@ import LayoutConfig from "./ThemeConfig/LayoutConfig"
import { SpecialVisualizationState } from "../UI/SpecialVisualization" import { SpecialVisualizationState } from "../UI/SpecialVisualization"
import { Changes } from "../Logic/Osm/Changes" import { Changes } from "../Logic/Osm/Changes"
import { Store, UIEventSource } from "../Logic/UIEventSource" import { Store, UIEventSource } from "../Logic/UIEventSource"
import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource" import {
FeatureSource,
IndexedFeatureSource,
WritableFeatureSource
} from "../Logic/FeatureSource/FeatureSource"
import { OsmConnection } from "../Logic/Osm/OsmConnection" import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { ExportableMap, MapProperties } from "./MapProperties" import { ExportableMap, MapProperties } from "./MapProperties"
import LayerState from "../Logic/State/LayerState" import LayerState from "../Logic/State/LayerState"
@ -46,7 +50,9 @@ import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter"
import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage" import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"
import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor" import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor"
import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoElementsInViewDetector" import NoElementsInViewDetector, {
FeatureViewState
} from "../Logic/Actors/NoElementsInViewDetector"
import FilteredLayer from "./FilteredLayer" import FilteredLayer from "./FilteredLayer"
import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector" import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager" import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
@ -184,7 +190,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
"oauth_token", "oauth_token",
undefined, undefined,
"Used to complete the login" "Used to complete the login"
), )
}) })
this.userRelatedState = new UserRelatedState( this.userRelatedState = new UserRelatedState(
this.osmConnection, this.osmConnection,
@ -263,8 +269,8 @@ export default class ThemeViewState implements SpecialVisualizationState {
bbox.asGeoJson({ bbox.asGeoJson({
zoom: this.mapProperties.zoom.data, zoom: this.mapProperties.zoom.data,
...this.mapProperties.location.data, ...this.mapProperties.location.data,
id: "current_view_" + currentViewIndex, id: "current_view_" + currentViewIndex
}), })
] ]
}) })
) )
@ -281,10 +287,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
featurePropertiesStore: this.featureProperties, featurePropertiesStore: this.featureProperties,
osmConnection: this.osmConnection, osmConnection: this.osmConnection,
historicalUserLocations: this.geolocation.historicalUserLocations, historicalUserLocations: this.geolocation.historicalUserLocations,
featureSwitches: this.featureSwitches, featureSwitches: this.featureSwitches
}, },
layout?.isLeftRightSensitive() ?? false, layout?.isLeftRightSensitive() ?? false,
(e) => this.reportError(e) (e, extraMsg) => this.reportError(e, extraMsg)
) )
this.historicalUserLocations = this.geolocation.historicalUserLocations this.historicalUserLocations = this.geolocation.historicalUserLocations
this.newFeatures = new NewGeometryFromChangesFeatureSource( this.newFeatures = new NewGeometryFromChangesFeatureSource(
@ -309,7 +315,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
"leftover features, such as", "leftover features, such as",
features[0].properties features[0].properties
) )
}, }
} }
) )
this.perLayer = perLayer.perLayer this.perLayer = perLayer.perLayer
@ -365,7 +371,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
{ {
currentZoom: this.mapProperties.zoom, currentZoom: this.mapProperties.zoom,
layerState: this.layerState, layerState: this.layerState,
bounds: this.visualFeedbackViewportBounds, bounds: this.visualFeedbackViewportBounds
} }
) )
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
@ -409,7 +415,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
public focusOnMap() { public focusOnMap() {
if (this.map.data) { if (this.map.data) {
this.map.data.getCanvas().focus() this.map.data.getCanvas().focus()
console.log("Focused on map")
return return
} }
this.map.addCallbackAndRunD((map) => { this.map.addCallbackAndRunD((map) => {
@ -472,7 +477,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
doShowLayer, doShowLayer,
metaTags: this.userRelatedState.preferencesAsTags, metaTags: this.userRelatedState.preferencesAsTags,
selectedElement: this.selectedElement, selectedElement: this.selectedElement,
fetchStore: (id) => this.featureProperties.getStore(id), fetchStore: (id) => this.featureProperties.getStore(id)
}) })
}) })
return filteringFeatureSource return filteringFeatureSource
@ -499,7 +504,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
doShowLayer: flayerGps.isDisplayed, doShowLayer: flayerGps.isDisplayed,
layer: flayerGps.layerDef, layer: flayerGps.layerDef,
metaTags: this.userRelatedState.preferencesAsTags, metaTags: this.userRelatedState.preferencesAsTags,
selectedElement: this.selectedElement, selectedElement: this.selectedElement
}) })
} }
@ -545,8 +550,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
/** /**
* Selects the feature that is 'i' closest to the map center * Selects the feature that is 'i' closest to the map center
* @param i
* @private
*/ */
private selectClosestAtCenter(i: number = 0) { private selectClosestAtCenter(i: number = 0) {
if (this.userRelatedState.a11y.data !== "never") { if (this.userRelatedState.a11y.data !== "never") {
@ -575,23 +578,22 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.previewedImage.setData(undefined) this.previewedImage.setData(undefined)
return return
} }
this.selectedElement.setData(undefined) if(this.guistate.closeAll()){
this.guistate.closeAll() return
if (!this.guistate.isSomethingOpen()) {
Zoomcontrol.resetzoom()
this.focusOnMap()
} }
this.selectedElement.setData(undefined)
Zoomcontrol.resetzoom()
this.focusOnMap()
}) })
Hotkeys.RegisterHotkey({ nomod: "f" }, docs.selectFavourites, () => { Hotkeys.RegisterHotkey({ nomod: "f" }, docs.selectFavourites, () => {
this.guistate.menuViewTab.setData("favourites") this.guistate.pageStates.favourites.set(true)
this.guistate.menuIsOpened.setData(true)
}) })
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ {
nomod: " ", nomod: " ",
onUp: true, onUp: true
}, },
docs.selectItem, docs.selectItem,
() => { () => {
@ -599,8 +601,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
return false return false
} }
if ( if (
this.guistate.menuIsOpened.data || this.guistate.isSomethingOpen() ||
this.guistate.themeIsOpened.data ||
this.previewedImage.data !== undefined this.previewedImage.data !== undefined
) { ) {
return return
@ -624,7 +625,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ {
nomod: "" + i, nomod: "" + i,
onUp: true, onUp: true
}, },
doc, doc,
() => this.selectClosestAtCenter(i - 1) () => this.selectClosestAtCenter(i - 1)
@ -637,18 +638,18 @@ export default class ThemeViewState implements SpecialVisualizationState {
} }
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ {
nomod: "b", nomod: "b"
}, },
docs.openLayersPanel, docs.openLayersPanel,
() => { () => {
if (this.featureSwitches.featureSwitchBackgroundSelection.data) { if (this.featureSwitches.featureSwitchBackgroundSelection.data) {
this.guistate.backgroundLayerSelectionIsOpened.setData(true) this.guistate.pageStates.background.setData(true)
} }
} }
) )
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ {
nomod: "s", nomod: "s"
}, },
Translations.t.hotkeyDocumentation.openFilterPanel, Translations.t.hotkeyDocumentation.openFilterPanel,
() => { () => {
@ -670,7 +671,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
available, available,
category, category,
current.data, current.data,
skipLayers, skipLayers
) )
if (!best) { if (!best) {
return return
@ -726,7 +727,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
Hotkeys.RegisterHotkey( Hotkeys.RegisterHotkey(
{ {
shift: "T", shift: "T"
}, },
Translations.t.hotkeyDocumentation.translationMode, Translations.t.hotkeyDocumentation.translationMode,
() => { () => {
@ -758,7 +759,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.mapProperties.zoom.map((z) => Math.max(Math.floor(z), 0)), this.mapProperties.zoom.map((z) => Math.max(Math.floor(z), 0)),
this.mapProperties, this.mapProperties,
{ {
isActive: this.mapProperties.zoom.map((z) => z < maxzoom), isActive: this.mapProperties.zoom.map((z) => z < maxzoom)
} }
) )
@ -791,7 +792,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
favourite: this.favourites, favourite: this.favourites,
summary: this.featureSummary, summary: this.featureSummary,
last_click: this.lastClickObject, last_click: this.lastClickObject,
search: undefined
} }
this.closestFeatures.registerSource(specialLayers.favourite, "favourite") this.closestFeatures.registerSource(specialLayers.favourite, "favourite")
@ -846,7 +846,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
doShowLayer: flayer.isDisplayed, doShowLayer: flayer.isDisplayed,
layer: flayer.layerDef, layer: flayer.layerDef,
metaTags: this.userRelatedState.preferencesAsTags, metaTags: this.userRelatedState.preferencesAsTags,
selectedElement: this.selectedElement, selectedElement: this.selectedElement
}) })
}) })
const summaryLayerConfig = new LayerConfig(<LayerConfigJson>summaryLayer, "summaryLayer") const summaryLayerConfig = new LayerConfig(<LayerConfigJson>summaryLayer, "summaryLayer")
@ -854,7 +854,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
features: specialLayers.summary, features: specialLayers.summary,
layer: summaryLayerConfig, layer: summaryLayerConfig,
// doShowLayer: this.mapProperties.zoom.map((z) => z < maxzoom), // doShowLayer: this.mapProperties.zoom.map((z) => z < maxzoom),
selectedElement: this.selectedElement, selectedElement: this.selectedElement
}) })
const lastClickLayerConfig = new LayerConfig( const lastClickLayerConfig = new LayerConfig(
@ -865,28 +865,27 @@ export default class ThemeViewState implements SpecialVisualizationState {
lastClickLayerConfig.isShown === undefined lastClickLayerConfig.isShown === undefined
? specialLayers.last_click ? specialLayers.last_click
: specialLayers.last_click.features.mapD((fs) => : specialLayers.last_click.features.mapD((fs) =>
fs.filter((f) => { fs.filter((f) => {
const matches = lastClickLayerConfig.isShown.matchesProperties( const matches = lastClickLayerConfig.isShown.matchesProperties(
f.properties f.properties
) )
console.debug("LastClick ", f, "matches", matches) console.debug("LastClick ", f, "matches", matches)
return matches return matches
}) })
) )
new ShowDataLayer(this.map, { new ShowDataLayer(this.map, {
features: new StaticFeatureSource(lastClickFiltered), features: new StaticFeatureSource(lastClickFiltered),
layer: lastClickLayerConfig, layer: lastClickLayerConfig,
onClick: (feature) => { onClick: (feature) => {
console.log("Last click was clicked", feature)
if (this.mapProperties.zoom.data >= Constants.minZoomLevelToAddNewPoint) { if (this.mapProperties.zoom.data >= Constants.minZoomLevelToAddNewPoint) {
this.selectedElement.setData(feature) this.selectedElement.setData(feature)
return return
} }
this.map.data.flyTo({ this.map.data.flyTo({
zoom: Constants.minZoomLevelToAddNewPoint, zoom: Constants.minZoomLevelToAddNewPoint,
center: GeoOperations.centerpointCoordinates(feature), center: GeoOperations.centerpointCoordinates(feature)
}) })
}, }
}) })
} }
@ -901,10 +900,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.lastClickObject.clear() this.lastClickObject.clear()
} }
}) })
this.guistate.allToggles.forEach((toggle) => { Object.values(this.guistate.pageStates).forEach((toggle) => {
toggle.toggle.addCallbackD((isOpened) => { toggle.addCallbackD((isOpened) => {
if (!isOpened) { if (!isOpened) {
this.focusOnMap() if (!this.guistate.isSomethingOpen()) {
this.focusOnMap()
}
} }
}) })
}) })
@ -999,8 +1000,8 @@ export default class ThemeViewState implements SpecialVisualizationState {
userid: this.osmConnection.userDetails.data?.uid, userid: this.osmConnection.userDetails.data?.uid,
pendingChanges: this.changes.pendingChanges.data, pendingChanges: this.changes.pendingChanges.data,
previousChanges: this.changes.allChanges.data, previousChanges: this.changes.allChanges.data,
changeRewrites: Utils.MapToObj(this.changes._changesetHandler._remappings), changeRewrites: Utils.MapToObj(this.changes._changesetHandler._remappings)
}), })
}) })
} catch (e) { } catch (e) {
console.error("Could not upload an error report") console.error("Could not upload an error report")

View file

@ -1,48 +0,0 @@
<script lang="ts">
import { Store } from "../../Logic/UIEventSource"
import { onDestroy, onMount } from "svelte"
let elem: HTMLElement
let targetOuter: HTMLElement
export let isOpened: Store<boolean>
export let moveTo: Store<HTMLElement>
export let debug: string
function copySizeOf(htmlElem: HTMLElement) {
const target = htmlElem.getBoundingClientRect()
elem.style.left = target.x + "px"
elem.style.top = target.y + "px"
elem.style.width = target.width + "px"
elem.style.height = target.height + "px"
}
function animate(opened: boolean) {
const moveToElem = moveTo.data
if (opened) {
copySizeOf(targetOuter)
elem.style.background = "var(--background-color)"
} else if (moveToElem !== undefined) {
copySizeOf(moveToElem)
elem.style.background = "#ffffff00"
} else {
elem.style.left = "0px"
elem.style.top = "0px"
elem.style.background = "#ffffff00"
}
}
onDestroy(isOpened.addCallback((opened) => animate(opened)))
onMount(() => requestAnimationFrame(() => animate(isOpened.data)))
</script>
<div class={"pointer-events-none invisible absolute bottom-0 right-0 h-full w-screen p-4 md:p-6"}>
<div class="content h-full" bind:this={targetOuter} style="background: red" />
</div>
<div
bind:this={elem}
class="low-interaction pointer-events-none absolute bottom-0 right-0 rounded-2xl"
style="transition: all 0.5s ease-out, background-color 1.4s ease-out; background: var(--background-color);"
>
<!-- Classes should be the same as the 'floatoaver' -->
</div>

View file

@ -0,0 +1,30 @@
<script lang="ts">
import { Drawer } from "flowbite-svelte"
import { sineIn } from "svelte/easing"
import { UIEventSource } from "../../Logic/UIEventSource.js"
export let shown: UIEventSource<boolean>;
let transitionParams = {
x: -320,
duration: 200,
easing: sineIn
};
let hidden = !shown.data
$: {
shown.setData(!hidden)
}
shown.addCallback(sh => {
hidden = !sh
})
</script>
<Drawer placement="left"
transitionType="fly" {transitionParams}
divClass = "overflow-y-auto z-50 "
bind:hidden={hidden}>
<slot>
CONTENTS
</slot>
</Drawer>

View file

@ -13,7 +13,7 @@
</script> </script>
<a <a
href={Utils.prepareHref(href)} href={Utils.prepareHref(href) }
aria-label={ariaLabel} aria-label={ariaLabel}
title={ariaLabel} title={ariaLabel}
target={newTab ? "_blank" : undefined} target={newTab ? "_blank" : undefined}

View file

@ -1,18 +1,19 @@
<script lang="ts"> <script lang="ts">
import { OsmConnection } from "../../Logic/Osm/OsmConnection" import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Logout from "../../assets/svg/Logout.svelte"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import Tr from "./Tr.svelte" import Tr from "./Tr.svelte"
import ArrowRightOnRectangle from "@babeard/svelte-heroicons/solid/ArrowRightOnRectangle" import ArrowRightOnRectangle from "@babeard/svelte-heroicons/solid/ArrowRightOnRectangle"
export let osmConnection: OsmConnection export let osmConnection: OsmConnection
export let clss = ""
</script> </script>
<button <button
class={clss}
on:click={() => { on:click={() => {
osmConnection.LogOut() osmConnection.LogOut()
}} }}
> >
<ArrowRightOnRectangle class="h-6 w-6" /> <ArrowRightOnRectangle class="h-6 w-6 max-h-full" />
<Tr t={Translations.t.general.logout} /> <Tr t={Translations.t.general.logout} />
</button> </button>

View file

@ -14,15 +14,10 @@
export let arialabel: Translation = undefined export let arialabel: Translation = undefined
export let arialabelDynamic: Store<Translation> = new ImmutableStore(arialabel) export let arialabelDynamic: Store<Translation> = new ImmutableStore(arialabel)
let arialabelString = arialabelDynamic.bind((tr) => tr?.current) let arialabelString = arialabelDynamic.bind((tr) => tr?.current)
export let htmlElem: UIEventSource<HTMLElement> = undefined
let _htmlElem: HTMLElement
$: {
htmlElem?.setData(_htmlElem)
}
</script> </script>
<button <button
bind:this={_htmlElem}
on:click={(e) => dispatch("click", e)} on:click={(e) => dispatch("click", e)}
on:keydown on:keydown
use:ariaLabelStore={arialabelString} use:ariaLabelStore={arialabelString}

View file

@ -35,8 +35,8 @@
{#if $showButton} {#if $showButton}
<div class="flex flex-col"> <div class="flex flex-col">
<button class="as-link" on:click={openJosm}> <button class="as-link sidebar-button" on:click={openJosm}>
<Josm_logo class="h-6 w-6 pr-2" /> <Josm_logo class="h-6 w-6" />
<Tr t={t.editJosm} /> <Tr t={t.editJosm} />
</button> </button>

46
src/UI/Base/Page.svelte Normal file
View file

@ -0,0 +1,46 @@
<script lang="ts">
// A fake 'page' which can be shown; kind of a modal
import { UIEventSource } from "../../Logic/UIEventSource"
import { Modal } from "flowbite-svelte"
export let shown: UIEventSource<boolean>
let _shown = false
export let onlyLink: boolean = false
shown.addCallbackAndRun(sh => {
_shown = sh
})
export let fullscreen: boolean = false
const shared = "in-page normal-background dark:bg-gray-800 rounded-lg border-gray-200 dark:border-gray-700 border-gray-200 dark:border-gray-700 divide-gray-200 dark:divide-gray-700 shadow-md"
let defaultClass = "relative flex flex-col mx-auto w-full divide-y " + shared
if (fullscreen) {
defaultClass = shared
}
let dialogClass = "fixed top-0 start-0 end-0 h-modal inset-0 z-50 w-full p-4 flex"
if (fullscreen) {
dialogClass += " h-full-child"
}
let bodyClass = "h-full p-4 md:p-5 space-y-4 flex-1 overflow-y-auto overscroll-contain"
let headerClass = "flex justify-between items-center p-2 px-4 md:px-5 rounded-t-lg";
</script>
{#if !onlyLink}
<Modal open={_shown} on:close={() => shown.set(false)} outsideclose
size="xl"
{defaultClass} {bodyClass} {dialogClass} {headerClass}
color="none">
<h1 slot="header" class="w-full">
<slot name="header" />
</h1>
<slot />
{#if $$slots.footer}
<slot name="footer" />
{/if}
</Modal>
{:else}
<button class="as-link sidebar-button" on:click={() => shown.setData(true)}>
<slot name="link">
<slot name="header" />
</slot>
</button>
{/if}

View file

@ -6,4 +6,7 @@
<div class="flex h-full flex-col overflow-auto border-b-2 p-4"> <div class="flex h-full flex-col overflow-auto border-b-2 p-4">
<slot /> <slot />
</div> </div>
<slot class="border-t-gray-300 mt-1" name="footer" />
</div> </div>

View file

@ -1,103 +0,0 @@
<script lang="ts">
import Translations from "../i18n/Translations"
import { Utils } from "../../Utils"
import Constants from "../../Models/Constants"
import Tr from "../Base/Tr.svelte"
import Add from "../../assets/svg/Add.svelte"
import Github from "../../assets/svg/Github.svelte"
import Mastodon from "../../assets/svg/Mastodon.svelte"
import Liberapay from "../../assets/svg/Liberapay.svelte"
import { EyeIcon } from "@rgossiaux/svelte-heroicons/solid"
import MapillaryLink from "./MapillaryLink.svelte"
import OpenJosm from "../Base/OpenJosm.svelte"
import OpenIdEditor from "./OpenIdEditor.svelte"
import If from "../Base/If.svelte"
import Community from "../../assets/svg/Community.svelte"
import Bug from "../../assets/svg/Bug.svelte"
import ThemeViewState from "../../Models/ThemeViewState"
import DocumentChartBar from "@babeard/svelte-heroicons/outline/DocumentChartBar"
import DocumentMagnifyingGlass from "@babeard/svelte-heroicons/outline/DocumentMagnifyingGlass"
export let state: ThemeViewState
let layout = state.layout
let featureSwitches = state.featureSwitches
let showHome = featureSwitches.featureSwitchBackToThemeOverview
</script>
<div class="link-underline links-w-full m-2 flex flex-col gap-y-1">
<Tr t={Translations.t.general.aboutMapComplete.intro} />
{#if $showHome}
<a class="flex" href={Utils.HomepageLink()}>
<Add class="h-6 w-6" />
{#if Utils.isIframe}
<Tr t={Translations.t.general.seeIndex} />
{:else}
<Tr t={Translations.t.general.backToIndex} />
{/if}
</a>
{/if}
<a class="flex" href="https://github.com/pietervdvn/MapComplete/" target="_blank">
<Github class="h-6 w-6" />
<Tr t={Translations.t.general.attribution.gotoSourceCode} />
</a>
<a class="flex" href="https://github.com/pietervdvn/MapComplete/issues" target="_blank">
<Bug class="h-6 w-6" />
<Tr t={Translations.t.general.attribution.openIssueTracker} />
</a>
{#if layout.official}
<a
class="flex"
href={"https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Themes/" +
layout.id +
".md"}
target="_blank"
>
<DocumentMagnifyingGlass class="h-6 w-6" />
<Tr
t={Translations.t.general.attribution.openThemeDocumentation.Subs({
name: layout.title,
})}
/>
</a>
<a class="flex" href={Utils.OsmChaLinkFor(31, layout.id)}>
<DocumentChartBar class="h-6 w-6" />
<Tr t={Translations.t.general.attribution.openOsmcha.Subs({ theme: layout.title })} />
</a>
{/if}
<a class="flex" href="https://en.osm.town/@MapComplete" target="_blank">
<Mastodon class="h-6 w-6" />
<Tr t={Translations.t.general.attribution.followOnMastodon} />
</a>
<a class="flex" href="https://liberapay.com/pietervdvn/" target="_blank">
<Liberapay class="h-6 w-6" />
<Tr t={Translations.t.general.attribution.donate} />
</a>
<button class="as-link" on:click={() => state.guistate.communityIndexPanelIsOpened.setData(true)}>
<Community class="h-6 w-6" />
<Tr t={Translations.t.communityIndex.title} />
</button>
<If condition={featureSwitches.featureSwitchEnableLogin}>
<OpenIdEditor mapProperties={state.mapProperties} />
<OpenJosm {state} />
<MapillaryLink large={false} mapProperties={state.mapProperties} />
</If>
<button class="as-link" on:click={() => state.guistate.privacyPanelIsOpened.setData(true)}>
<EyeIcon class="h-6 w-6 pr-1" />
<Tr t={Translations.t.privacy.title} />
</button>
<div class="subtle">
{Constants.vNumber}
</div>
</div>

View file

@ -0,0 +1,22 @@
<script lang="ts">
import type { SpecialVisualizationState } from "../SpecialVisualization"
import IconCopyrightPanel from "./CopyrightSingleIcon.svelte"
import licenses from "../../assets/generated/license_info.json"
import type SmallLicense from "../../Models/smallLicense"
export let state: SpecialVisualizationState
let layoutToUse = state.layout
let iconAttributions: string[] = layoutToUse.getUsedImages()
const allLicenses = {}
for (const key in licenses) {
const license: SmallLicense = licenses[key]
allLicenses[license.path] = license
}
</script>
{#each iconAttributions as iconAttribution}
<IconCopyrightPanel iconPath={iconAttribution} license={allLicenses[iconAttribution]} />
{/each}

View file

@ -4,11 +4,7 @@
import contributors from "../../assets/contributors.json" import contributors from "../../assets/contributors.json"
import translators from "../../assets/translators.json" import translators from "../../assets/translators.json"
import { Translation, TypedTranslation } from "../i18n/Translation" import { Translation, TypedTranslation } from "../i18n/Translation"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import IconCopyrightPanel from "./IconCopyrightPanel.svelte"
import licenses from "../../assets/generated/license_info.json"
import type SmallLicense from "../../Models/smallLicense"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
import ContributorCount from "../../Logic/ContributorCount" import ContributorCount from "../../Logic/ContributorCount"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
@ -24,7 +20,6 @@
const t = Translations.t.general.attribution const t = Translations.t.general.attribution
const layoutToUse = state.layout const layoutToUse = state.layout
const iconAttributions: string[] = layoutToUse.getUsedImages()
let maintainer: Translation = undefined let maintainer: Translation = undefined
if (layoutToUse.credits !== undefined && layoutToUse.credits !== "") { if (layoutToUse.credits !== undefined && layoutToUse.credits !== "") {
@ -53,11 +48,7 @@
return Translations.t.general.attribution.attributionBackgroundLayer.Subs(props) return Translations.t.general.attribution.attributionBackgroundLayer.Subs(props)
}) })
const allLicenses = {}
for (const key in licenses) {
const license: SmallLicense = licenses[key]
allLicenses[license.path] = license
}
function calculateDataContributions(contributions: Map<string, number>): Translation { function calculateDataContributions(contributions: Map<string, number>): Translation {
if (contributions === undefined) { if (contributions === undefined) {
@ -121,9 +112,6 @@
</script> </script>
<div class="link-underline flex flex-col gap-y-4"> <div class="link-underline flex flex-col gap-y-4">
<h3>
<Tr t={t.attributionTitle} />
</h3>
<div class="flex items-center gap-x-2"> <div class="flex items-center gap-x-2">
<Osm_logo class="h-8 w-8 shrink-0" /> <Osm_logo class="h-8 w-8 shrink-0" />
<Tr t={t.attributionContent} /> <Tr t={t.attributionContent} />
@ -159,14 +147,6 @@
<Tr t={codeContributors(translators, t.translatedBy)} /> <Tr t={codeContributors(translators, t.translatedBy)} />
</div> </div>
<AccordionSingle>
<div slot="header">
<Tr t={t.iconAttribution.title} />
</div>
{#each iconAttributions as iconAttribution}
<IconCopyrightPanel iconPath={iconAttribution} license={allLicenses[iconAttribution]} />
{/each}
</AccordionSingle>
<div class="self-end"> <div class="self-end">
MapComplete {Constants.vNumber} MapComplete {Constants.vNumber}

View file

@ -9,9 +9,11 @@
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import Filter from "../../assets/svg/Filter.svelte" import Filter from "../../assets/svg/Filter.svelte"
import TitledPanel from "../Base/TitledPanel.svelte" import Page from "../Base/Page.svelte"
export let state: ThemeViewState export let state: ThemeViewState
export let onlyLink: boolean
let layout = state.layout let layout = state.layout
let allEnabled: boolean let allEnabled: boolean
@ -47,8 +49,12 @@
} }
</script> </script>
<TitledPanel> <Page {onlyLink} shown={state.guistate.pageStates.filter}>
<div class="mr-10 flex w-full flex-wrap items-center justify-between" slot="title"> <div class="flex" slot="link">
<Filter class="h-6 w-6" />
<Tr t={Translations.t.general.menu.filter} />
</div>
<div class="mr-16 flex w-full flex-wrap items-center justify-between" slot="header">
<div class="flex"> <div class="flex">
<Filter class="h-6 w-6 pr-2" /> <Filter class="h-6 w-6 pr-2" />
<Tr t={Translations.t.general.menu.filter} /> <Tr t={Translations.t.general.menu.filter} />
@ -80,4 +86,4 @@
zoomlevel={state.mapProperties.zoom} zoomlevel={state.mapProperties.zoom}
/> />
{/each} {/each}
</TitledPanel> </Page>

View file

@ -15,10 +15,6 @@
} }
</script> </script>
<AccordionSingle>
<div slot="header">
<Tr t={t.title} />
</div>
<Tr t={t.intro} /> <Tr t={t.intro} />
<table> <table>
<tr> <tr>
@ -47,4 +43,3 @@
</tr> </tr>
{/each} {/each}
</table> </table>
</AccordionSingle>

View file

@ -0,0 +1,361 @@
<script lang="ts">
// All the relevant links
import ThemeViewState from "../../Models/ThemeViewState"
import Translations from "../i18n/Translations"
import { CogIcon, EyeIcon, HeartIcon } from "@rgossiaux/svelte-heroicons/solid"
import Page from "../Base/Page.svelte"
import PrivacyPolicy from "./PrivacyPolicy.svelte"
import Tr from "../Base/Tr.svelte"
import If from "../Base/If.svelte"
import CommunityIndexView from "./CommunityIndexView.svelte"
import Community from "../../assets/svg/Community.svelte"
import LoginToggle from "../Base/LoginToggle.svelte"
import { Sidebar } from "flowbite-svelte"
import HotkeyTable from "./HotkeyTable.svelte"
import { Utils } from "../../Utils"
import Constants from "../../Models/Constants"
import Mastodon from "../../assets/svg/Mastodon.svelte"
import Liberapay from "../../assets/svg/Liberapay.svelte"
import DocumentMagnifyingGlass from "@babeard/svelte-heroicons/outline/DocumentMagnifyingGlass"
import DocumentChartBar from "@babeard/svelte-heroicons/outline/DocumentChartBar"
import OpenIdEditor from "./OpenIdEditor.svelte"
import OpenJosm from "../Base/OpenJosm.svelte"
import MapillaryLink from "./MapillaryLink.svelte"
import Github from "../../assets/svg/Github.svelte"
import Bug from "../../assets/svg/Bug.svelte"
import Add from "../../assets/svg/Add.svelte"
import CopyrightPanel from "./CopyrightPanel.svelte"
import CopyrightAllIcons from "./CopyrightAllIcons.svelte"
import LanguagePicker from "../InputElement/LanguagePicker.svelte"
import LoginButton from "../Base/LoginButton.svelte"
import SelectedElementView from "./SelectedElementView.svelte"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import usersettings from "../../assets/generated/layers/usersettings.json"
import UserRelatedState from "../../Logic/State/UserRelatedState"
import ArrowDownTray from "@babeard/svelte-heroicons/mini/ArrowDownTray"
import DownloadPanel from "../DownloadFlow/DownloadPanel.svelte"
import Favourites from "../Favourites/Favourites.svelte"
import ReviewsOverview from "../Reviews/ReviewsOverview.svelte"
import Share from "@babeard/svelte-heroicons/solid/Share"
import ShareScreen from "./ShareScreen.svelte"
import FilterPage from "./FilterPage.svelte"
import RasterLayerOverview from "../Map/RasterLayerOverview.svelte"
import ThemeIntroPanel from "./ThemeIntroPanel.svelte"
import Marker from "../Map/Marker.svelte"
import LogoutButton from "../Base/LogoutButton.svelte"
import { BoltIcon } from "@babeard/svelte-heroicons/mini"
import Copyright from "../../assets/svg/Copyright.svelte"
export let state: ThemeViewState
let userdetails = state.osmConnection.userDetails
let usersettingslayer = new LayerConfig(<LayerConfigJson>usersettings, "usersettings", true)
let layout = state.layout
let featureSwitches = state.featureSwitches
let showHome = featureSwitches.featureSwitchBackToThemeOverview
let pg = state.guistate.pageStates
export let onlyLink: boolean
const t = Translations.t.general.menu
</script>
<div class="flex flex-col p-2 sm:p-3 low-interaction gap-y-2 sm:gap-y-3 h-screen overflow-y-auto">
<!-- User related: avatar, settings, favourits, logout -->
<div class="sidebar-unit">
<LoginToggle {state}>
<LoginButton osmConnection={state.osmConnection} slot="not-logged-in"></LoginButton>
<div class="flex gap-x-4 items-center">
{#if $userdetails.img}
<img src={$userdetails.img} class="rounded-full w-14 h-14" />
{/if}
<b>{$userdetails.name}</b>
</div>
</LoginToggle>
<Page {onlyLink} shown={pg.usersettings}>
<div class="flex" slot="header">
<CogIcon class="h-6 w-6" />
<Tr t={UserRelatedState.usersettingsConfig.title.GetRenderValue({})} />
</div>
<!-- All shown components are set by 'usersettings.json', which happily uses some special visualisations created specifically for it -->
<LoginToggle {state}>
<div class="flex flex-col" slot="not-logged-in">
<LanguagePicker availableLanguages={layout.language} />
<Tr cls="alert" t={Translations.t.userinfo.notLoggedIn} />
<LoginButton clss="primary" osmConnection={state.osmConnection} />
</div>
<SelectedElementView
highlightedRendering={state.guistate.highlightedUserSetting}
layer={usersettingslayer}
selectedElement={{
type: "Feature",
properties: { id: "settings" },
geometry: { type: "Point", coordinates: [0, 0] },
}}
{state}
tags={state.userRelatedState.preferencesAsTags}
/>
</LoginToggle>
</Page>
<LoginToggle {state}>
<Page {onlyLink} shown={pg.favourites}>
<div class="flex" slot="header">
<HeartIcon class="h-6 w-6" />
<Tr t={Translations.t.favouritePoi.tab} />
</div>
<h3>
<Tr t={Translations.t.favouritePoi.title} />
</h3>
<div>
<Favourites {state} />
<h3>
<Tr t={Translations.t.reviews.your_reviews} />
</h3>
<ReviewsOverview {state} />
</div>
</Page>
<div class="self-end">
<LogoutButton osmConnection={state.osmConnection} />
</div>
</LoginToggle>
<LanguagePicker />
</div>
<!-- Theme related: documentation links, download, ... -->
<div class="sidebar-unit">
<h3>
<Tr t={t.aboutCurrentThemeTitle} />
</h3>
<Page {onlyLink} shown={pg.about_theme}>
<div slot="link" class="flex">
<Marker icons={layout.icon} size="h-6 w-6 mr-2" />
<Tr t={t.showIntroduction} />
</div>
<div class="flex" slot="header">
<Marker icons={layout.icon} size="h-8 w-8 mr-4" />
<Tr t={layout.title} />
</div>
<ThemeIntroPanel {state} />
</Page>
<FilterPage {onlyLink} {state} />
<RasterLayerOverview {onlyLink} {state} />
<Page {onlyLink} shown={pg.share}>
<div class="flex" slot="header">
<Share class="h-4 w-4" />
<Tr t={Translations.t.general.sharescreen.title} />
</div>
<ShareScreen {state} />
</Page>
{#if state.featureSwitches.featureSwitchEnableExport}
<Page {onlyLink} shown={pg.download}>
<div slot="header" class="flex">
<ArrowDownTray class="h-4 w-4" />
<Tr t={Translations.t.general.download.title} />
</div>
<DownloadPanel {state} />
</Page>
{/if}
{#if layout.official}
<a
class="flex"
href={"https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Themes/" +
layout.id +
".md"}
target="_blank"
>
<DocumentMagnifyingGlass class="h-6 w-6" />
<Tr
t={Translations.t.general.attribution.openThemeDocumentation.Subs({
name: layout.title,
})}
/>
</a>
<a class="flex" href={Utils.OsmChaLinkFor(31, layout.id)} target="_blank">
<DocumentChartBar class="h-6 w-6" />
<Tr t={Translations.t.general.attribution.openOsmcha.Subs({ theme: layout.title })} />
</a>
{/if}
</div>
<!-- Other links and tools for the given location: open iD/JOSM; community index, ... -->
<div class="sidebar-unit">
<h3>
<Tr t={t.moreUtilsTitle} />
</h3>
{#if $showHome}
<a class="flex" href={Utils.HomepageLink()}>
<Add class="h-6 w-6" />
{#if Utils.isIframe}
<Tr t={Translations.t.general.seeIndex} />
{:else}
<Tr t={Translations.t.general.backToIndex} />
{/if}
</a>
{/if}
<Page {onlyLink} shown={pg.community_index}>
<div class="flex" slot="header">
<Community class="h-6 w-6" />
<Tr t={Translations.t.communityIndex.title} />
</div>
<CommunityIndexView location={state.mapProperties.location} />
</Page>
<If condition={featureSwitches.featureSwitchEnableLogin}>
<OpenIdEditor mapProperties={state.mapProperties} />
<OpenJosm {state} />
<MapillaryLink large={false} mapProperties={state.mapProperties} />
</If>
</div>
<!-- About MC: various outward links, legal info, ... -->
<div class="sidebar-unit">
<h3>
<Tr t={Translations.t.general.menu.aboutMapComplete} />
</h3>
<div class="hidden-on-mobile">
<Page {onlyLink} shown={pg.hotkeys}>
<div class="flex" slot="header">
<BoltIcon class="w-6 h-6" />
<Tr t={ Translations.t.hotkeyDocumentation.title} />
</div>
<HotkeyTable />
</Page>
</div>
<a class="flex" href="https://github.com/pietervdvn/MapComplete/" target="_blank">
<Github class="h-6 w-6" />
<Tr t={Translations.t.general.attribution.gotoSourceCode} />
</a>
<a class="flex" href="https://github.com/pietervdvn/MapComplete/issues" target="_blank">
<Bug class="h-6 w-6" />
<Tr t={Translations.t.general.attribution.openIssueTracker} />
</a>
<a class="flex" href="https://en.osm.town/@MapComplete" target="_blank">
<Mastodon class="h-6 w-6" />
<Tr t={Translations.t.general.attribution.followOnMastodon} />
</a>
<a class="flex" href="https://liberapay.com/pietervdvn/" target="_blank">
<Liberapay class="h-6 w-6" />
<Tr t={Translations.t.general.attribution.donate} />
</a>
<Page {onlyLink} shown={pg.copyright}>
<div slot="header" class="flex">
<Copyright class="w-8 h-8" />
<Tr t={Translations.t.general.attribution.attributionTitle} />
</div>
<CopyrightPanel {state} />
</Page>
<Page {onlyLink} shown={pg.copyright_icons}>
<div slot="header" class="flex">
<Copyright class="w-8 h-8" />
<Tr t={ Translations.t.general.attribution.iconAttribution.title} />
</div>
<CopyrightAllIcons {state} />
</Page>
<Page {onlyLink} shown={pg.privacy}>
<div class="flex" slot="header">
<EyeIcon class="w-8 h-8" />
<Tr t={Translations.t.privacy.title} />
</div>
<PrivacyPolicy {state} />
</Page>
<div class="subtle self-end">
{Constants.vNumber}
</div>
</div>
</div>
<style>
:global(.sidebar-unit) {
display: flex;
flex-direction: column;
row-gap: 0.25rem;
background: var(--background-color);
padding: 0.5rem;
border-radius: 0.5rem;
}
:global(.sidebar-unit > h3) {
margin-top: 0;
margin-bottom: 0.5rem;
padding: 0.25rem;
}
:global(.sidebar-button svg, .sidebar-button img) {
width: 1.5rem;
height: 1.5rem;
margin-right: 0.5rem;
flex-shrink: 0;
}
:global(.sidebar-button .weblate-link > svg) {
width: 0.75rem;
height: 0.75rem;
flex-shrink: 0;
}
:global(.sidebar-button, .sidebar-button, .sidebar-unit > a) {
display: flex;
align-items: center;
border-radius: 0.25rem !important;
padding: 0.4rem 0.75rem !important;
text-decoration: none !important;
}
:global(.sidebar-button > svg , .sidebar-button > img, .sidebar-unit a > img, .sidebar-unit > a svg) {
margin-right: 0.5rem;
flex-shrink: 0;
}
:global(.sidebar-button:hover, .sidebar-unit > a:hover) {
background: var(--low-interaction-background) !important;
}
</style>

View file

@ -20,7 +20,7 @@
<MapControlButton <MapControlButton
arialabel={Translations.t.general.labels.background} arialabel={Translations.t.general.labels.background}
on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)} on:click={() => state.guistate.pageStates.background.setData(true)}
{htmlElem} {htmlElem}
> >
<StyleLoadingIndicator map={map ?? state.map} rasterLayer={state.mapProperties.rasterLayer}> <StyleLoadingIndicator map={map ?? state.map} rasterLayer={state.mapProperties.rasterLayer}>

View file

@ -12,8 +12,6 @@
import { GeoLocationState } from "../../Logic/State/GeoLocationState" import { GeoLocationState } from "../../Logic/State/GeoLocationState"
import If from "../Base/If.svelte" import If from "../Base/If.svelte"
import { ExclamationTriangleIcon } from "@babeard/svelte-heroicons/mini" import { ExclamationTriangleIcon } from "@babeard/svelte-heroicons/mini"
import Location_refused from "../../assets/svg/Location_refused.svelte"
import Location from "../../assets/svg/Location.svelte"
import ChevronDoubleLeft from "@babeard/svelte-heroicons/solid/ChevronDoubleLeft" import ChevronDoubleLeft from "@babeard/svelte-heroicons/solid/ChevronDoubleLeft"
import GeolocationIndicator from "./GeolocationIndicator.svelte" import GeolocationIndicator from "./GeolocationIndicator.svelte"
@ -38,7 +36,7 @@
const glstate = state.geolocation.geolocationState const glstate = state.geolocation.geolocationState
if (glstate.currentGPSLocation.data !== undefined) { if (glstate.currentGPSLocation.data !== undefined) {
const c: GeolocationCoordinates = glstate.currentGPSLocation.data const c: GeolocationCoordinates = glstate.currentGPSLocation.data
state.guistate.themeIsOpened.setData(false) state.guistate.pageStates.about_theme.setData(false)
const coor = { lon: c.longitude, lat: c.latitude } const coor = { lon: c.longitude, lat: c.latitude }
state.mapProperties.location.setData(coor) state.mapProperties.location.setData(coor)
} }
@ -65,7 +63,7 @@
<Tr t={layout.descriptionTail} /> <Tr t={layout.descriptionTail} />
<!-- Buttons: open map, go to location, search --> <!-- Buttons: open map, go to location, search -->
<NextButton clss="primary w-full" on:click={() => state.guistate.themeIsOpened.setData(false)}> <NextButton clss="primary w-full" on:click={() => state.guistate.pageStates.about_theme.setData(false)}>
<div class="flex w-full flex-col items-center"> <div class="flex w-full flex-col items-center">
<div class="flex w-full justify-center text-2xl"> <div class="flex w-full justify-center text-2xl">
<Tr t={Translations.t.general.openTheMap} /> <Tr t={Translations.t.general.openTheMap} />
@ -96,7 +94,7 @@
<div style="min-width: 16rem; " class="grow"> <div style="min-width: 16rem; " class="grow">
<Geosearch <Geosearch
bounds={state.mapProperties.bounds} bounds={state.mapProperties.bounds}
on:searchCompleted={() => state.guistate.themeIsOpened.setData(false)} on:searchCompleted={() => state.guistate.pageStates.about_theme.setData(false)}
on:searchIsValid={(event) => { on:searchIsValid={(event) => {
searchEnabled = event.detail searchEnabled = event.detail
}} }}
@ -140,20 +138,10 @@
{/if} {/if}
</div> </div>
{#if Utils.isIframe} <div class="link-underline flex justify-end text-sm mt-8">
<div class="link-underline flex justify-end"> <a href="https://mapcomplete.org" target="_blank">
<a href="https://mapcomplete.org" target="_blank"> <Tr t={Translations.t.general.poweredByMapComplete} />
<Tr t={Translations.t.general.poweredByMapComplete} /> </a>
</a> </div>
</div>
{:else}
<If condition={state.featureSwitches.featureSwitchBackToThemeOverview}>
<div class="link-underline m-2 mx-4 flex w-full">
<a class="flex w-fit items-center justify-end" href={Utils.HomepageLink()}>
<ChevronDoubleLeft class="h-4 w-4" />
<Tr t={Translations.t.general.backToIndex} />
</a>
</div>
</If>
{/if}
</div> </div>

View file

@ -65,10 +65,6 @@
<Tr cls="alert" t={Translations.t.general.download.toMuch} /> <Tr cls="alert" t={Translations.t.general.download.toMuch} />
{:else} {:else}
<div class="flex w-full flex-col" /> <div class="flex w-full flex-col" />
<h3>
<Tr t={t.title} />
</h3>
<DownloadButton <DownloadButton
{state} {state}
extension="geojson" extension="geojson"

View file

@ -62,7 +62,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
if (!MapLibreAdaptor.pmtilesInited) { if (!MapLibreAdaptor.pmtilesInited) {
maplibregl.addProtocol("pmtiles", new Protocol().tile) maplibregl.addProtocol("pmtiles", new Protocol().tile)
MapLibreAdaptor.pmtilesInited = true MapLibreAdaptor.pmtilesInited = true
console.log("PM-tiles protocol added" + "")
} }
this._maplibreMap = maplibreMap this._maplibreMap = maplibreMap

View file

@ -4,32 +4,30 @@
*/ */
import { Store, UIEventSource } from "../../Logic/UIEventSource" import { Store, UIEventSource } from "../../Logic/UIEventSource"
import type { RasterLayerPolygon } from "../../Models/RasterLayers" import type { RasterLayerPolygon } from "../../Models/RasterLayers"
import type { MapProperties } from "../../Models/MapProperties"
import { Map as MlMap } from "maplibre-gl"
import RasterLayerPicker from "./RasterLayerPicker.svelte" import RasterLayerPicker from "./RasterLayerPicker.svelte"
import type { EliCategory } from "../../Models/RasterLayerProperties" import type { EliCategory } from "../../Models/RasterLayerProperties"
import UserRelatedState from "../../Logic/State/UserRelatedState"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import TitledPanel from "../Base/TitledPanel.svelte"
import Loading from "../Base/Loading.svelte" import Loading from "../Base/Loading.svelte"
import Page from "../Base/Page.svelte"
import ThemeViewState from "../../Models/ThemeViewState"
import { Square3Stack3dIcon } from "@babeard/svelte-heroicons/solid"
export let availableLayers: { store: Store<RasterLayerPolygon[]> } export let state: ThemeViewState
export let mapproperties: MapProperties
export let userstate: UserRelatedState let map = state.map
export let map: Store<MlMap> let mapproperties = state.mapProperties
let userstate = state.userRelatedState
let shown = state.guistate.pageStates.background
let availableLayers: { store: Store<RasterLayerPolygon[]> } = state.availableLayers
let _availableLayers = availableLayers.store let _availableLayers = availableLayers.store
/**
* Used to toggle the background layers on/off
*/
export let visible: UIEventSource<boolean> = undefined
type CategoryType = "photo" | "map" | "other" | "osmbasedmap" type CategoryType = "photo" | "map" | "other" | "osmbasedmap"
const categories: Record<CategoryType, EliCategory[]> = { const categories: Record<CategoryType, EliCategory[]> = {
photo: ["photo", "historicphoto"], photo: ["photo", "historicphoto"],
map: ["map", "historicmap"], map: ["map", "historicmap"],
other: ["other", "elevation"], other: ["other", "elevation"],
osmbasedmap: ["osmbasedmap"], osmbasedmap: ["osmbasedmap"]
} }
function availableForCategory(type: CategoryType): Store<RasterLayerPolygon[]> { function availableForCategory(type: CategoryType): Store<RasterLayerPolygon[]> {
@ -45,27 +43,35 @@
const otherLayers = availableForCategory("other") const otherLayers = availableForCategory("other")
function onApply() { function onApply() {
visible.setData(false) shown.setData(false)
} }
function getPref(type: CategoryType): undefined | UIEventSource<string> { function getPref(type: CategoryType): undefined | UIEventSource<string> {
return userstate?.osmConnection?.GetPreference("preferred-layer-" + type) return userstate?.osmConnection?.GetPreference("preferred-layer-" + type)
} }
export let onlyLink: boolean
</script> </script>
<TitledPanel> <Page {onlyLink} shown={shown} fullscreen={true}>
<Tr slot="title" t={Translations.t.general.backgroundMap} /> <div slot="header" class="flex" >
<Square3Stack3dIcon class="h-6 w-6" />
<Tr t={Translations.t.general.backgroundMap} />
</div>
{#if $_availableLayers?.length < 1} {#if $_availableLayers?.length < 1}
<Loading /> <Loading />
{:else} {:else}
<div class="grid h-full w-full grid-cols-1 gap-2 md:grid-cols-2">
<div class="flex gap-x-2 flex-col sm:flex-row gap-y-2" style="height: calc( 100% - 5rem)">
<RasterLayerPicker <RasterLayerPicker
availableLayers={$photoLayers} availableLayers={$photoLayers}
favourite={getPref("photo")} favourite={getPref("photo")}
{map} {map}
{mapproperties} {mapproperties}
on:appliedLayer={onApply} on:appliedLayer={onApply}
{visible} {shown}
/> />
<RasterLayerPicker <RasterLayerPicker
availableLayers={$mapLayers} availableLayers={$mapLayers}
@ -73,7 +79,7 @@
{map} {map}
{mapproperties} {mapproperties}
on:appliedLayer={onApply} on:appliedLayer={onApply}
{visible} {shown}
/> />
<RasterLayerPicker <RasterLayerPicker
availableLayers={$osmbasedmapLayers} availableLayers={$osmbasedmapLayers}
@ -81,7 +87,7 @@
{map} {map}
{mapproperties} {mapproperties}
on:appliedLayer={onApply} on:appliedLayer={onApply}
{visible} {shown}
/> />
<RasterLayerPicker <RasterLayerPicker
availableLayers={$otherLayers} availableLayers={$otherLayers}
@ -89,8 +95,8 @@
{map} {map}
{mapproperties} {mapproperties}
on:appliedLayer={onApply} on:appliedLayer={onApply}
{visible} {shown}
/> />
</div> </div>
{/if} {/if}
</TitledPanel> </Page>

View file

@ -13,7 +13,7 @@
export let mapproperties: MapProperties export let mapproperties: MapProperties
export let map: Store<MlMap> export let map: Store<MlMap>
export let visible: Store<boolean> = undefined export let shown: Store<boolean> = undefined
let dispatch = createEventDispatcher<{ appliedLayer }>() let dispatch = createEventDispatcher<{ appliedLayer }>()
@ -48,10 +48,10 @@
let rasterLayerOnMap = UIEventSource.feedFrom(rasterLayer) let rasterLayerOnMap = UIEventSource.feedFrom(rasterLayer)
if (visible) { if (shown) {
onDestroy( onDestroy(
visible?.addCallbackAndRunD((visible) => { shown?.addCallbackAndRunD((shown) => {
if (visible) { if (shown) {
rasterLayerOnMap.setData(rasterLayer.data ?? availableLayers[0]) rasterLayerOnMap.setData(rasterLayer.data ?? availableLayers[0])
} else { } else {
rasterLayerOnMap.setData(undefined) rasterLayerOnMap.setData(undefined)
@ -85,7 +85,7 @@
rasterLayer={rasterLayerOnMap} rasterLayer={rasterLayerOnMap}
placedOverMap={map} placedOverMap={map}
placedOverMapProperties={mapproperties} placedOverMapProperties={mapproperties}
{visible} visible={shown}
/> />
</span> </span>
</button> </button>

View file

@ -401,6 +401,7 @@
> >
<input <input
type="radio" type="radio"
class="self-center mr-1"
bind:group={selectedMapping} bind:group={selectedMapping}
name={"mappings-radio-" + config.id} name={"mappings-radio-" + config.id}
value={i} value={i}
@ -412,6 +413,7 @@
<label class="flex gap-x-1"> <label class="flex gap-x-1">
<input <input
type="radio" type="radio"
class="self-center mr-1"
bind:group={selectedMapping} bind:group={selectedMapping}
name={"mappings-radio-" + config.id} name={"mappings-radio-" + config.id}
value={config.mappings?.length} value={config.mappings?.length}
@ -448,6 +450,7 @@
> >
<input <input
type="checkbox" type="checkbox"
class="self-center mr-1"
name={"mappings-checkbox-" + config.id + "-" + i} name={"mappings-checkbox-" + config.id + "-" + i}
bind:checked={checkedMappings[i]} bind:checked={checkedMappings[i]}
on:keypress={(e) => onInputKeypress(e)} on:keypress={(e) => onInputKeypress(e)}
@ -458,6 +461,7 @@
<label class="flex gap-x-1"> <label class="flex gap-x-1">
<input <input
type="checkbox" type="checkbox"
class="self-center mr-1"
name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length} name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length}
bind:checked={checkedMappings[config.mappings.length]} bind:checked={checkedMappings[config.mappings.length]}
on:keypress={(e) => onInputKeypress(e)} on:keypress={(e) => onInputKeypress(e)}

View file

@ -13,64 +13,39 @@
import type { MapProperties } from "../Models/MapProperties" import type { MapProperties } from "../Models/MapProperties"
import Geosearch from "./Search/Geosearch.svelte" import Geosearch from "./Search/Geosearch.svelte"
import Translations from "./i18n/Translations" import Translations from "./i18n/Translations"
import usersettings from "../assets/generated/layers/usersettings.json"
import { import {
CogIcon, MenuIcon
EyeIcon,
HeartIcon,
MenuIcon,
XCircleIcon
} from "@rgossiaux/svelte-heroicons/solid" } from "@rgossiaux/svelte-heroicons/solid"
import Tr from "./Base/Tr.svelte" import Tr from "./Base/Tr.svelte"
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
import FloatOver from "./Base/FloatOver.svelte" import FloatOver from "./Base/FloatOver.svelte"
import Constants from "../Models/Constants" import Constants from "../Models/Constants"
import TabbedGroup from "./Base/TabbedGroup.svelte"
import UserRelatedState from "../Logic/State/UserRelatedState"
import LoginToggle from "./Base/LoginToggle.svelte" import LoginToggle from "./Base/LoginToggle.svelte"
import LoginButton from "./Base/LoginButton.svelte"
import CopyrightPanel from "./BigComponents/CopyrightPanel.svelte"
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"
import ModalRight from "./Base/ModalRight.svelte" import ModalRight from "./Base/ModalRight.svelte"
import LevelSelector from "./BigComponents/LevelSelector.svelte" import LevelSelector from "./BigComponents/LevelSelector.svelte"
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"
import type { RasterLayerPolygon } from "../Models/RasterLayers" import type { RasterLayerPolygon } from "../Models/RasterLayers"
import { AvailableRasterLayers } from "../Models/RasterLayers" import { AvailableRasterLayers } from "../Models/RasterLayers"
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"
import IfHidden from "./Base/IfHidden.svelte"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte" import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte"
import StateIndicator from "./BigComponents/StateIndicator.svelte" import StateIndicator from "./BigComponents/StateIndicator.svelte"
import ShareScreen from "./BigComponents/ShareScreen.svelte"
import UploadingImageCounter from "./Image/UploadingImageCounter.svelte" import UploadingImageCounter from "./Image/UploadingImageCounter.svelte"
import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte" import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte"
import Cross from "../assets/svg/Cross.svelte" import Cross from "../assets/svg/Cross.svelte"
import LanguagePicker from "./InputElement/LanguagePicker.svelte"
import Min from "../assets/svg/Min.svelte" import Min from "../assets/svg/Min.svelte"
import Plus from "../assets/svg/Plus.svelte" import Plus from "../assets/svg/Plus.svelte"
import Filter from "../assets/svg/Filter.svelte" import Filter from "../assets/svg/Filter.svelte"
import Community from "../assets/svg/Community.svelte"
import Favourites from "./Favourites/Favourites.svelte"
import ImageOperations from "./Image/ImageOperations.svelte" import ImageOperations from "./Image/ImageOperations.svelte"
import VisualFeedbackPanel from "./BigComponents/VisualFeedbackPanel.svelte" import VisualFeedbackPanel from "./BigComponents/VisualFeedbackPanel.svelte"
import { Orientation } from "../Sensors/Orientation" import { Orientation } from "../Sensors/Orientation"
import GeolocationIndicator from "./BigComponents/GeolocationIndicator.svelte" import GeolocationIndicator from "./BigComponents/GeolocationIndicator.svelte"
import Compass_arrow from "../assets/svg/Compass_arrow.svelte" import Compass_arrow from "../assets/svg/Compass_arrow.svelte"
import ReverseGeocoding from "./BigComponents/ReverseGeocoding.svelte" import ReverseGeocoding from "./BigComponents/ReverseGeocoding.svelte"
import FilterPanel from "./BigComponents/FilterPanel.svelte"
import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte"
import { BBox } from "../Logic/BBox" import { BBox } from "../Logic/BBox"
import ReviewsOverview from "./Reviews/ReviewsOverview.svelte"
import ExtraLinkButton from "./BigComponents/ExtraLinkButton.svelte" import ExtraLinkButton from "./BigComponents/ExtraLinkButton.svelte"
import CloseAnimation from "./Base/CloseAnimation.svelte"
import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource" import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource"
import ArrowDownTray from "@babeard/svelte-heroicons/mini/ArrowDownTray"
import Share from "@babeard/svelte-heroicons/solid/Share"
import ChevronRight from "@babeard/svelte-heroicons/solid/ChevronRight"
import Marker from "./Map/Marker.svelte" import Marker from "./Map/Marker.svelte"
import AboutMapComplete from "./BigComponents/AboutMapComplete.svelte"
import HotkeyTable from "./BigComponents/HotkeyTable.svelte"
import SelectedElementPanel from "./Base/SelectedElementPanel.svelte" import SelectedElementPanel from "./Base/SelectedElementPanel.svelte"
import MenuDrawer from "./BigComponents/MenuDrawer.svelte"
import DrawerLeft from "./Base/DrawerLeft.svelte"
import type { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" import type { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
import { GeocodingUtils } from "../Logic/Geocoding/GeocodingProvider" import { GeocodingUtils } from "../Logic/Geocoding/GeocodingProvider"
@ -109,7 +84,6 @@
let visualFeedback = state.visualFeedback let visualFeedback = state.visualFeedback
let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined) let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined)
let mapproperties: MapProperties = state.mapProperties let mapproperties: MapProperties = state.mapProperties
let usersettingslayer = new LayerConfig(<LayerConfigJson>usersettings, "usersettings", true)
state.mapProperties.installCustomKeyboardHandler(viewport) state.mapProperties.installCustomKeyboardHandler(viewport)
let canZoomIn = mapproperties.maxzoom.map( let canZoomIn = mapproperties.maxzoom.map(
(mz) => mapproperties.zoom.data < mz, (mz) => mapproperties.zoom.data < mz,
@ -145,7 +119,6 @@
updateViewport() updateViewport()
}) })
let featureSwitches: FeatureSwitchState = state.featureSwitches let featureSwitches: FeatureSwitchState = state.featureSwitches
let availableLayers = state.availableLayers
let currentViewLayer: LayerConfig = layout.layers.find((l) => l.id === "current_view") let currentViewLayer: LayerConfig = layout.layers.find((l) => l.id === "current_view")
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer
let rasterLayerName = let rasterLayerName =
@ -157,8 +130,12 @@
}) })
) )
let previewedImage = state.previewedImage let previewedImage = state.previewedImage
let addNewFeatureMode = state.userRelatedState.addNewFeatureMode
let gpsAvailable = state.geolocation.geolocationState.gpsAvailable
let gpsButtonAriaLabel = state.geolocation.geolocationState.gpsStateExplanation
let debug = state.featureSwitches.featureSwitchIsDebugging let debug = state.featureSwitches.featureSwitchIsDebugging
debug.addCallbackAndRun((dbg) => { debug.addCallbackAndRun((dbg) => {
if (dbg) { if (dbg) {
document.body.classList.add("debug") document.body.classList.add("debug")
@ -179,26 +156,6 @@
animation?.cameraAnimation(mlmap) animation?.cameraAnimation(mlmap)
} }
/**
* Needed for the animations
*/
let openMapButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined)
let openMenuButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined)
let openCurrentViewLayerButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(
undefined
)
let _openNewElementButton: HTMLButtonElement
let openNewElementButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined)
$: {
openNewElementButton.setData(_openNewElementButton)
}
let openFilterButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined)
let openBackgroundButton: UIEventSource<HTMLElement> = new UIEventSource<HTMLElement>(undefined)
let addNewFeatureMode = state.userRelatedState.addNewFeatureMode
let gpsAvailable = state.geolocation.geolocationState.gpsAvailable
let gpsButtonAriaLabel = state.geolocation.geolocationState.gpsStateExplanation
</script> </script>
<main> <main>
@ -221,6 +178,55 @@
<div class="pointer-events-none absolute top-0 left-0 w-full"> <div class="pointer-events-none absolute top-0 left-0 w-full">
<!-- Top components --> <!-- Top components -->
<div
class="flex bg-black-light-transparent pointer-events-auto items-center justify-between px-4 py-1 flex-wrap-reverse">
<!-- Top bar with tools -->
<div class="flex items-center">
<MapControlButton
cls="m-0.5 p-0.5 sm:p-1"
arialabel={Translations.t.general.labels.menu}
on:click={() => {console.log("Opening...."); state.guistate.menuIsOpened.setData(true)}}
on:keydown={forwardEventToMap}
>
<MenuIcon class="h-6 w-6 cursor-pointer" />
</MapControlButton>
<MapControlButton
on:click={() => state.guistate.pageStates.about_theme.set(true)}
on:keydown={forwardEventToMap}
>
<div
class="m-0.5 mx-1 flex cursor-pointer items-center max-[480px]:w-full sm:mx-1 mr-2"
>
<Marker icons={layout.icon} size="h-6 w-6 shrink-0 mr-0.5 sm:mr-1 md:mr-2" />
<b class="mr-1">
<Tr t={layout.title} />
</b>
</div>
</MapControlButton>
</div>
<If condition={state.featureSwitches.featureSwitchSearch}>
<div class="w-full sm:w-64 my-2 sm:mt-0">
<Geosearch
bounds={state.mapProperties.bounds}
on:searchCompleted={() => {
state.map?.data?.getCanvas()?.focus()
}}
perLayer={state.perLayer}
selectedElement={state.selectedElement}
geolocationState={state.geolocation.geolocationState}
searcher={state.geosearch}
{state}
/>
</div>
</If>
</div>
<div class="pointer-events-auto float-right mt-1 flex flex-col px-1 max-[480px]:w-full sm:m-2"> <div class="pointer-events-auto float-right mt-1 flex flex-col px-1 max-[480px]:w-full sm:m-2">
<If condition={state.visualFeedback}> <If condition={state.visualFeedback}>
{#if $selectedElement === undefined} {#if $selectedElement === undefined}
@ -229,46 +235,12 @@
</div> </div>
{/if} {/if}
</If> </If>
<If condition={state.featureSwitches.featureSwitchSearch}>
<Geosearch
bounds={state.mapProperties.bounds}
on:searchCompleted={() => {
state.map?.data?.getCanvas()?.focus()
}}
perLayer={state.perLayer}
selectedElement={state.selectedElement}
geolocationState={state.geolocation.geolocationState}
searcher={state.geosearch}
{state}
/>
</If>
</div> </div>
<div class="float-left m-1 flex flex-col sm:mt-2"> <div class="float-left m-1 flex flex-col sm:mt-2">
<If condition={state.featureSwitches.featureSwitchWelcomeMessage}> <If condition={state.featureSwitches.featureSwitchWelcomeMessage}>
<MapControlButton
on:click={() => state.guistate.themeIsOpened.setData(true)}
on:keydown={forwardEventToMap}
htmlElem={openMapButton}
>
<div
class="m-0.5 mx-1 flex cursor-pointer items-center max-[480px]:w-full sm:mx-1 md:mx-2"
>
<Marker icons={layout.icon} size="h-4 w-4 md:h-8 md:w-8 mr-0.5 sm:mr-1 md:mr-2" />
<b class="mr-1">
<Tr t={layout.title} />
</b>
<ChevronRight class="h-4 w-4" />
</div>
</MapControlButton>
<MapControlButton
arialabel={Translations.t.general.labels.menu}
on:click={() => state.guistate.menuIsOpened.setData(true)}
on:keydown={forwardEventToMap}
htmlElem={openMenuButton}
>
<MenuIcon class="h-8 w-8 cursor-pointer" />
</MapControlButton>
</If> </If>
{#if currentViewLayer?.tagRenderings && currentViewLayer.defaultIcon()} {#if currentViewLayer?.tagRenderings && currentViewLayer.defaultIcon()}
<MapControlButton <MapControlButton
@ -276,7 +248,6 @@
state.selectCurrentView() state.selectCurrentView()
}} }}
on:keydown={forwardEventToMap} on:keydown={forwardEventToMap}
htmlElem={openCurrentViewLayerButton}
> >
<div class="h-8 w-8 cursor-pointer"> <div class="h-8 w-8 cursor-pointer">
<ToSvelte construct={() => currentViewLayer.defaultIcon()} /> <ToSvelte construct={() => currentViewLayer.defaultIcon()} />
@ -313,7 +284,6 @@
<button <button
class="low-interaction pointer-events-auto w-fit" class="low-interaction pointer-events-auto w-fit"
class:disabled={$currentZoom < Constants.minZoomLevelToAddNewPoint} class:disabled={$currentZoom < Constants.minZoomLevelToAddNewPoint}
bind:this={_openNewElementButton}
on:click={() => { on:click={() => {
state.openNewDialog() state.openNewDialog()
}} }}
@ -337,7 +307,6 @@
arialabel={Translations.t.general.labels.filter} arialabel={Translations.t.general.labels.filter}
on:click={() => state.guistate.openFilterView()} on:click={() => state.guistate.openFilterView()}
on:keydown={forwardEventToMap} on:keydown={forwardEventToMap}
htmlElem={openFilterButton}
> >
<Filter class="h-6 w-6" /> <Filter class="h-6 w-6" />
</MapControlButton> </MapControlButton>
@ -346,25 +315,18 @@
<OpenBackgroundSelectorButton <OpenBackgroundSelectorButton
hideTooltip={true} hideTooltip={true}
{state} {state}
htmlElem={openBackgroundButton}
/> />
</If> </If>
<a <button
class="bg-black-transparent pointer-events-auto ml-1 h-fit max-h-12 cursor-pointer self-end self-center overflow-hidden rounded-2xl px-1 text-white opacity-50 hover:opacity-100" class="unstyled bg-black-transparent pointer-events-auto ml-1 h-fit max-h-12 cursor-pointer overflow-hidden rounded-2xl px-1 text-white opacity-50 hover:opacity-100"
on:click={() => { style="background: #00000088; padding: 0.25rem; border-radius: 2rem;"
if (featureSwitches.featureSwitchWelcomeMessage.data) { on:click={() => {state.guistate.pageStates.copyright.set(true)}}
state.guistate.themeViewTab.setData("copyright")
state.guistate.themeIsOpened.setData(true)
} else {
state.guistate.copyrightPanelIsOpened.setData(true)
}
}}
> >
© <span class="hidden sm:inline sm:pr-2"> © <span class="hidden sm:inline sm:pr-2">
OpenStreetMap OpenStreetMap
<span class="hidden w-24 md:inline md:pr-2">, {rasterLayerName}</span> <span class="hidden w-24 md:inline md:pr-2">, {rasterLayerName}</span>
</span> </span>
</a> </button>
</div> </div>
</div> </div>
@ -434,6 +396,13 @@
<svelte:fragment slot="error" /> <svelte:fragment slot="error" />
</LoginToggle> </LoginToggle>
<DrawerLeft shown={state.guistate.menuIsOpened}>
<div class="h-screen overflow-y-auto">
<MenuDrawer onlyLink={true} {state} />
</div>
</DrawerLeft>
<MenuDrawer onlyLink={false} {state} />
{#if $selectedElement !== undefined && $selectedLayer !== undefined && !$selectedLayer.popupInFloatover} {#if $selectedElement !== undefined && $selectedLayer !== undefined && !$selectedLayer.popupInFloatover}
<!-- right modal with the selected element view --> <!-- right modal with the selected element view -->
<ModalRight <ModalRight
@ -476,225 +445,5 @@
</FloatOver> </FloatOver>
</If> </If>
<!-- big theme menu -->
<If condition={state.guistate.themeIsOpened}>
<FloatOver on:close={() => state.guistate.themeIsOpened.setData(false)}>
<span slot="close-button"><!-- Disable the close button --></span>
<TabbedGroup
condition1={state.featureSwitches.featureSwitchEnableExport}
tab={state.guistate.themeViewTabIndex}
>
<div slot="post-tablist">
<XCircleIcon
class="mr-2 h-8 w-8"
on:click={() => state.guistate.themeIsOpened.setData(false)}
/>
</div>
<div class="flex" slot="title0">
<Marker icons={layout.icon} size="h-4 w-4" />
<Tr t={layout.title} />
</div>
<div class="m-4 h-full" slot="content0">
<ThemeIntroPanel {state} />
</div>
<div class="flex" slot="title1">
<If condition={state.featureSwitches.featureSwitchEnableExport}>
<ArrowDownTray class="h-4 w-4" />
<Tr t={Translations.t.general.download.title} />
</If>
</div>
<div class="m-4" slot="content1">
<DownloadPanel {state} />
</div>
<div slot="title2">
<Tr t={Translations.t.general.attribution.title} />
</div>
<div slot="content2" class="m-2 flex flex-col">
<CopyrightPanel {state} />
</div>
<div class="flex" slot="title3">
<Share class="h-4 w-4" />
<Tr t={Translations.t.general.sharescreen.title} />
</div>
<div class="m-2" slot="content3">
<ShareScreen {state} />
</div>
</TabbedGroup>
</FloatOver>
</If>
<!-- Filterpane -->
<If condition={state.guistate.filtersPanelIsOpened}>
<FloatOver on:close={() => state.guistate.filtersPanelIsOpened.setData(false)}>
<FilterPanel {state} />
</FloatOver>
</If>
<!-- background layer selector -->
<IfHidden condition={state.guistate.backgroundLayerSelectionIsOpened}>
<FloatOver
on:close={() => {
state.guistate.backgroundLayerSelectionIsOpened.setData(false)
}}
>
<RasterLayerOverview
{availableLayers}
map={state.map}
mapproperties={state.mapProperties}
userstate={state.userRelatedState}
visible={state.guistate.backgroundLayerSelectionIsOpened}
/>
</FloatOver>
</IfHidden>
<!-- Menu page -->
<If condition={state.guistate.menuIsOpened}>
<FloatOver on:close={() => state.guistate.menuIsOpened.setData(false)}>
<span slot="close-button"><!-- Hide the default close button --></span>
<TabbedGroup
condition1={featureSwitches.featureSwitchEnableLogin}
condition2={state.featureSwitches.featureSwitchCommunityIndex}
tab={state.guistate.menuViewTabIndex}
>
<div slot="post-tablist">
<XCircleIcon
class="mr-2 h-8 w-8"
on:click={() => state.guistate.menuIsOpened.setData(false)}
/>
</div>
<div class="flex" slot="title0">
<Tr t={Translations.t.general.menu.aboutMapComplete} />
</div>
<div slot="content0" class="flex flex-col">
<AboutMapComplete {state} />
<div class="m-2 flex flex-col">
<HotkeyTable />
</div>
</div>
<div class="flex" slot="title1">
<CogIcon class="h-6 w-6" />
<Tr t={UserRelatedState.usersettingsConfig.title.GetRenderValue({})} />
</div>
<div class="links-as-button py-8" slot="content1">
<!-- All shown components are set by 'usersettings.json', which happily uses some special visualisations created specifically for it -->
<LoginToggle {state}>
<div class="flex flex-col" slot="not-logged-in">
<LanguagePicker availableLanguages={layout.language} />
<Tr cls="alert" t={Translations.t.userinfo.notLoggedIn} />
<LoginButton clss="primary" osmConnection={state.osmConnection} />
</div>
<SelectedElementView
highlightedRendering={state.guistate.highlightedUserSetting}
layer={usersettingslayer}
selectedElement={{
type: "Feature",
properties: { id: "settings" },
geometry: { type: "Point", coordinates: [0, 0] },
}}
{state}
tags={state.userRelatedState.preferencesAsTags}
/>
</LoginToggle>
</div>
<div class="flex" slot="title2">
<HeartIcon class="h-6 w-6" />
<Tr t={Translations.t.favouritePoi.tab} />
</div>
<div class="m-2 flex flex-col" slot="content2">
<h3>
<Tr t={Translations.t.favouritePoi.title} />
</h3>
<Favourites {state} />
<h3>
<Tr t={Translations.t.reviews.your_reviews} />
</h3>
<ReviewsOverview {state} />
</div>
</TabbedGroup>
</FloatOver>
</If>
<!-- Privacy policy -->
<If condition={state.guistate.privacyPanelIsOpened}>
<FloatOver on:close={() => state.guistate.privacyPanelIsOpened.setData(false)}>
<div class="flex h-full flex-col overflow-hidden">
<h2 class="low-interaction m-0 flex items-center p-4 drop-shadow-md">
<EyeIcon class="w-6 pr-2" />
<Tr t={Translations.t.privacy.title} />
</h2>
<div class="overflow-auto p-4">
<PrivacyPolicy {state} />
</div>
</div>
</FloatOver>
</If>
<!-- Attribution, copyright and about MapComplete (no menu case) -->
<If condition={state.guistate.copyrightPanelIsOpened}>
<FloatOver on:close={() => state.guistate.copyrightPanelIsOpened.setData(false)}>
<div class="flex h-full flex-col overflow-hidden">
<h1 class="low-interaction m-0 flex items-center p-4 drop-shadow-md">
<Tr t={Translations.t.general.attribution.title} />
</h1>
<div class="overflow-auto p-4">
<h2>
<Tr t={Translations.t.general.menu.aboutMapComplete} />
</h2>
<AboutMapComplete {state} />
<CopyrightPanel {state} />
</div>
</div>
</FloatOver>
</If>
<!-- Community index -->
<If condition={state.guistate.communityIndexPanelIsOpened}>
<FloatOver on:close={() => state.guistate.communityIndexPanelIsOpened.setData(false)}>
<div class="flex h-full flex-col overflow-hidden">
<h2 class="low-interaction m-0 flex items-center p-4">
<Community class="h-6 w-6" />
<Tr t={Translations.t.communityIndex.title} />
</h2>
<div class="overflow-auto p-4">
<CommunityIndexView location={state.mapProperties.location} />
</div>
</div>
</FloatOver>
</If>
<CloseAnimation isOpened={state.guistate.themeIsOpened} moveTo={openMapButton} debug="theme" />
<CloseAnimation isOpened={state.guistate.menuIsOpened} moveTo={openMenuButton} debug="menu" />
<CloseAnimation
isOpened={selectedLayer.map((sl) => sl !== undefined && sl === currentViewLayer)}
moveTo={openCurrentViewLayerButton}
debug="currentViewLayer"
/>
<CloseAnimation
isOpened={selectedElement.map(
(sl) => sl !== undefined && sl?.properties?.id === LastClickFeatureSource.newPointElementId
)}
moveTo={openNewElementButton}
debug="newElement"
/>
<CloseAnimation
isOpened={state.guistate.filtersPanelIsOpened}
moveTo={openFilterButton}
debug="filter"
/>
<CloseAnimation
isOpened={state.guistate.backgroundLayerSelectionIsOpened}
moveTo={openBackgroundButton}
debug="bg"
/>
</main> </main>

View file

@ -485,10 +485,18 @@ code {
color: var(--background-color); color: var(--background-color);
} }
.h-full-child > div {
height: 100%;
}
.bg-black-transparent { .bg-black-transparent {
background-color: #00000088; background-color: #00000088;
} }
.bg-black-light-transparent {
background-color: #00000044;
}
.block-ruby { .block-ruby {
display: block ruby; display: block ruby;
} }
@ -655,6 +663,10 @@ svg.apply-fill path {
max-width: 100%; max-width: 100%;
} }
.max-w-screen {
max-width: 100vw;
}
/************************* Experimental support for foldable devices ********************************/ /************************* Experimental support for foldable devices ********************************/
@media (horizontal-viewport-segments: 2) { @media (horizontal-viewport-segments: 2) {
.theme-list { .theme-list {

View file

@ -1,8 +1,10 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
const plugin = require("tailwindcss/plugin") const plugin = require("tailwindcss/plugin")
const flowbitePlugin = require("flowbite/plugin")
module.exports = { module.exports = {
content: ["./**/*.{html,ts,svelte}"], content: ["./**/*.{html,ts,svelte}", './node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}'],
theme: { theme: {
extend: { extend: {
maxHeight: { maxHeight: {
@ -12,10 +14,24 @@ module.exports = {
colors: { colors: {
subtle: "#dbeafe", subtle: "#dbeafe",
unsubtle: "#bfdbfe", unsubtle: "#bfdbfe",
// flowbite-svelte
primary: {
50: '#FFF5F2',
100: '#FFF1EE',
200: '#FFE4DE',
300: '#FFD5CC',
400: '#FFBCAD',
500: '#FE795D',
600: '#EF562F',
700: '#EB4F27',
800: '#CC4522',
900: '#A5371B'
}
}, },
}, },
}, },
plugins: [ plugins: [
flowbitePlugin,
plugin(function ({ addVariant, e }) { plugin(function ({ addVariant, e }) {
addVariant("landscape", ({ modifySelectors, separator }) => { addVariant("landscape", ({ modifySelectors, separator }) => {
modifySelectors(({ className }) => { modifySelectors(({ className }) => {