Styling: style menu- and theme-menu tabbed interface

This commit is contained in:
Pieter Vander Vennet 2023-05-07 23:19:30 +02:00
parent a4a3b8a5ad
commit 14927497bd
6 changed files with 181 additions and 125 deletions

View file

@ -1,20 +1,33 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import {createEventDispatcher} from "svelte";
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"; import {XCircleIcon} from "@rgossiaux/svelte-heroicons/solid";
/** /**
* The slotted element will be shown on top, with a lower-opacity border * The slotted element will be shown on top, with a lower-opacity border
*/ */
const dispatch = createEventDispatcher<{ close }>(); const dispatch = createEventDispatcher<{ close }>();
</script> </script>
<div class="absolute top-0 right-0 w-screen h-screen overflow-auto" style="background-color: #00000088"> <div class="absolute top-0 right-0 w-screen h-screen p-4 md:p-6" style="background-color: #00000088">
<div class="flex flex-col m-4 sm:m-6 p-4 sm:p-6 md:m-8 rounded normal-background"> <div class="content normal-background">
<slot name="close-button"> <div class="rounded-xl">
<div class="w-8 h-8 absolute right-10 top-10 cursor-pointer" on:click={() => dispatch("close")}> <slot></slot>
<XCircleIcon /> </div>
</div> <slot name="close-button">
</slot> <!-- The close button is placed _after_ the default slot in order to always paint it on top -->
<slot></slot> <div class="w-8 h-8 absolute right-10 top-10 cursor-pointer" on:click={() => dispatch("close")}>
</div> <XCircleIcon/>
</div>
</slot>
</div>
</div> </div>
<style>
.content {
height: calc( 100vh - 2rem );
border-radius: 0.5rem;
overflow-x: auto;
box-shadow: 0 0 1rem #00000088;
}
</style>

View file

@ -1,79 +1,121 @@
<script lang="ts"> <script lang="ts">
/** /**
* Thin wrapper around 'TabGroup' which binds the state * Thin wrapper around 'TabGroup' which binds the state
*/ */
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui"; import {Tab, TabGroup, TabList, TabPanel, TabPanels} from "@rgossiaux/svelte-headlessui";
import { UIEventSource } from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
export let tab: UIEventSource<number>; export let tab: UIEventSource<number>;
let tabElements: HTMLElement[] = []; let tabElements: HTMLElement[] = [];
$: tabElements[$tab]?.click(); $: tabElements[$tab]?.click();
$: { $: {
if (tabElements[tab.data]) { if (tabElements[tab.data]) {
window.setTimeout(() => tabElements[tab.data].click(), 50) window.setTimeout(() => tabElements[tab.data].click(), 50)
}
} }
}
</script> </script>
<TabGroup defaultIndex={1} on:change={(e) =>{if(e.detail >= 0){tab.setData( e.detail); }} }> <div class="tabbedgroup w-full h-full">
<TabList> <TabGroup class="h-full w-full flex flex-col" defaultIndex={1}
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}> on:change={(e) =>{if(e.detail >= 0){tab.setData( e.detail); }} }>
<div bind:this={tabElements[0]} class="flex"> <div class="flex bg-gray-300 items-center justify-between sticky top-0">
<slot name="title0"> <TabList class="flex flex-wrap">
Tab 0 {#if $$slots.title1}
</slot> <Tab class={({selected}) => "tab "+(selected ? "tab-selected" : "tab-unselected")}>
</div> <div bind:this={tabElements[0]} class="flex">
</Tab> <slot name="title0">
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}> Tab 0
<div bind:this={tabElements[1]} class="flex"> </slot>
<slot name="title1" /> </div>
</div> </Tab>
</Tab> {/if}
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}> {#if $$slots.title1}
<div bind:this={tabElements[2]} class="flex"> <Tab class={({selected}) => "tab "+(selected ? "tab-selected" : "tab-unselected")}>
<slot name="title2" /> <div bind:this={tabElements[1]} class="flex">
</div> <slot name="title1"/>
</Tab> </div>
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}> </Tab>
<div bind:this={tabElements[3]} class="flex"> {/if}
<slot name="title3" /> {#if $$slots.title2}
</div> <Tab class={({selected}) => "tab "+(selected ? "tab-selected" : "tab-unselected")}>
</Tab> <div bind:this={tabElements[2]} class="flex">
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}> <slot name="title2"/>
<div bind:this={tabElements[4]} class="flex"> </div>
<slot name="title4" /> </Tab>
</div> {/if}
</Tab> {#if $$slots.title3}
</TabList> <Tab class={({selected}) => "tab "+(selected ? "tab-selected" : "tab-unselected")}>
<TabPanels defaultIndex={$tab}> <div bind:this={tabElements[3]} class="flex">
<TabPanel> <slot name="title3"/>
<slot name="content0"> </div>
<div>Empty</div> </Tab>
</slot> {/if}
</TabPanel> {#if $$slots.title4}
<TabPanel> <Tab class={({selected}) => "tab "+(selected ? "tab-selected" : "tab-unselected")}>
<slot name="content1" /> <div bind:this={tabElements[4]} class="flex">
</TabPanel> <slot name="title4"/>
<TabPanel> </div>
<slot name="content2" /> </Tab>
</TabPanel> {/if}
<TabPanel> </TabList>
<slot name="content3" /> <slot name="post-tablist"/>
</TabPanel> </div>
<TabPanel> <div class="overflow-y-auto normal-background">
<slot name="content4" />
</TabPanel>
</TabPanels>
</TabGroup>
<TabPanels defaultIndex={$tab}>
<TabPanel>
<slot name="content0">
<div>Empty</div>
</slot>
</TabPanel>
<TabPanel>
<slot name="content1"/>
</TabPanel>
<TabPanel>
<slot name="content2"/>
</TabPanel>
<TabPanel>
<slot name="content3"/>
</TabPanel>
<TabPanel>
<slot name="content4"/>
</TabPanel>
</TabPanels>
</div>
</TabGroup>
</div>
<style> <style>
.tab-selected { .tabbedgroup {
max-height: 100vh;
height: 100%;
}
:global(.tab) {
margin: 0.25rem;
padding: 0.25rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
border-radius: 1rem;
}
:global(.tab .flex) {
align-items: center;
gap: 0.25rem;
}
:global(.tab span|div) {
align-items: center;
gap: 0.25rem;
display: flex;
}
:global(.tab-selected) {
background-color: rgb(59 130 246); background-color: rgb(59 130 246);
color: rgb(255 255 255); color: rgb(255 255 255);
} }
.tab-unselected { :global(.tab-unselected) {
background-color: rgb(255 255 255); background-color: rgb(255 255 255);
color: rgb(0 0 0); color: rgb(0 0 0);
} }

View file

@ -29,13 +29,15 @@
{:else} {:else}
<UserCircleIcon class="w-12 h-12" /> <UserCircleIcon class="w-12 h-12" />
{/if} {/if}
<div class="flex flex-col relative"> <div class="flex flex-col">
<h3>{$userdetails.name}</h3> <h3>{$userdetails.name}</h3>
{#if description} {#if description}
<FromHtml src={description} /> <FromHtml src={description} />
<a href={osmConnection.Backend() + "/profile/edit"} target="_blank"> <a href={osmConnection.Backend() + "/profile/edit"} target="_blank" class="link-no-underline flex subtle-background items-center w-fit self-end">
<PencilAltIcon class="p-2 w-6 h-6 subtle-background rounded-full absolute right-1 bottom-1" /> <PencilAltIcon slot="image" class="p-2 w-8 h-8" />
<Tr slot="message" t={Translations.t.userinfo.editDescription} />
</a> </a>
{:else} {:else}
<Tr t={Translations.t. userinfo.noDescription} /> <Tr t={Translations.t. userinfo.noDescription} />
<a href={osmConnection.Backend() + "/profile/edit"} target="_blank" class="flex subtle-background items-center"> <a href={osmConnection.Backend() + "/profile/edit"} target="_blank" class="flex subtle-background items-center">

View file

@ -16,7 +16,7 @@
import type {MapProperties} from "../Models/MapProperties"; import type {MapProperties} from "../Models/MapProperties";
import Geosearch from "./BigComponents/Geosearch.svelte"; import Geosearch from "./BigComponents/Geosearch.svelte";
import Translations from "./i18n/Translations"; import Translations from "./i18n/Translations";
import {CogIcon, EyeIcon, MenuIcon} from "@rgossiaux/svelte-heroicons/solid"; import {CogIcon, EyeIcon, MenuIcon, XCircleIcon} from "@rgossiaux/svelte-heroicons/solid";
import Tr from "./Base/Tr.svelte"; import Tr from "./Base/Tr.svelte";
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"; import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte";
import FloatOver from "./Base/FloatOver.svelte"; import FloatOver from "./Base/FloatOver.svelte";
@ -149,9 +149,12 @@
<If condition={selectedElementView.map(v => v !== undefined && selectedLayer.data !== undefined && !selectedLayer.data.popupInFloatover,[ selectedLayer] )}> <If condition={selectedElementView.map(v => v !== undefined && selectedLayer.data !== undefined && !selectedLayer.data.popupInFloatover,[ selectedLayer] )}>
<ModalRight on:close={() => {selectedElement.setData(undefined)}}> <ModalRight on:close={() => {selectedElement.setData(undefined)}}>
<div class="absolute flex flex-col h-full w-full normal-background"> <div class="absolute flex flex-col h-full w-full normal-background">
<ToSvelte construct={new VariableUiElement(selectedElementTitle)}></ToSvelte> <ToSvelte construct={new VariableUiElement(selectedElementTitle)}>
<!-- Title -->
<ToSvelte construct={new VariableUiElement(selectedElementView)}></ToSvelte> </ToSvelte>
<ToSvelte construct={new VariableUiElement(selectedElementView).SetClass("overflow-auto")}>
<!-- Main view -->
</ToSvelte>
</div> </div>
</ModalRight> </ModalRight>
</If> </If>
@ -163,9 +166,14 @@
</If> </If>
<If condition={state.guistate.themeIsOpened}> <If condition={state.guistate.themeIsOpened}>
<!-- Theme page --> <!-- Theme menu -->
<FloatOver on:close={() => state.guistate.themeIsOpened.setData(false)}> <FloatOver>
<span slot="close-button"><!-- Disable the close button --></span>
<TabbedGroup tab={state.guistate.themeViewTabIndex}> <TabbedGroup tab={state.guistate.themeViewTabIndex}>
<div slot="post-tablist">
<XCircleIcon class="w-8 h-8 mr-2" on:click={() => state.guistate.themeIsOpened.setData(false)}/>
</div>
<Tr slot="title0" t={layout.title}/> <Tr slot="title0" t={layout.title}/>
<div slot="content0"> <div slot="content0">
@ -240,8 +248,12 @@
<If condition={state.guistate.menuIsOpened}> <If condition={state.guistate.menuIsOpened}>
<!-- Menu page --> <!-- Menu page -->
<FloatOver on:close={() => state.guistate.menuIsOpened.setData(false)}> <FloatOver>
<span slot="close-button"><!-- Hide the default close button --></span>
<TabbedGroup tab={state.guistate.menuViewTabIndex}> <TabbedGroup tab={state.guistate.menuViewTabIndex}>
<div slot="post-tablist">
<XCircleIcon class="w-8 h-8 mr-2" on:click={() => state.guistate.menuIsOpened.setData(false)}/>
</div>
<div class="flex" slot="title0"> <div class="flex" slot="title0">
<Tr t={Translations.t.general.menu.aboutMapComplete}/> <Tr t={Translations.t.general.menu.aboutMapComplete}/>
</div> </div>
@ -276,16 +288,13 @@
<Tr t={Translations.t.general.attribution.openOsmcha.Subs({theme: "MapComplete"})}/> <Tr t={Translations.t.general.attribution.openOsmcha.Subs({theme: "MapComplete"})}/>
</a> </a>
{Constants.vNumber} {Constants.vNumber}
<ToSvelte construct={Hotkeys.generateDocumentationDynamic}></ToSvelte>
</div> </div>
<If condition={state.osmConnection.isLoggedIn} slot="title1"> <div class="flex" slot="title1">
<div class="flex"> <CogIcon class="w-6 h-6"/>
<CogIcon class="w-6 h-6"/> <Tr t={UserRelatedState.usersettingsConfig.title.GetRenderValue({})}/>
<Tr t={UserRelatedState.usersettingsConfig.title.GetRenderValue({})}/> </div>
</div>
</If>
<div slot="content1"> <div slot="content1">
<!-- All shown components are set by 'usersettings.json', which happily uses some special visualisations created specifically for it --> <!-- All shown components are set by 'usersettings.json', which happily uses some special visualisations created specifically for it -->
@ -316,6 +325,12 @@
<Tr t={Translations.t.privacy.title}></Tr> <Tr t={Translations.t.privacy.title}></Tr>
</div> </div>
<ToSvelte construct={() => new PrivacyPolicy()} slot="content3"></ToSvelte> <ToSvelte construct={() => new PrivacyPolicy()} slot="content3"></ToSvelte>
<Tr slot="title4" t={Translations.t.advanced.title}/>
<div class="flex flex-col" slot="content4">
<ToSvelte construct={Hotkeys.generateDocumentationDynamic}></ToSvelte>
</div>
</TabbedGroup> </TabbedGroup>
</FloatOver> </FloatOver>
</If> </If>

View file

@ -735,14 +735,6 @@ video {
top: 1rem; top: 1rem;
} }
.right-1 {
right: 0.25rem;
}
.bottom-1 {
bottom: 0.25rem;
}
.left-1\/2 { .left-1\/2 {
left: 50%; left: 50%;
} }
@ -905,10 +897,6 @@ video {
margin-bottom: 2.5rem; margin-bottom: 2.5rem;
} }
.mt-0 {
margin-top: 0px;
}
.mt-8 { .mt-8 {
margin-top: 2rem; margin-top: 2rem;
} }
@ -1464,6 +1452,11 @@ video {
background-color: rgb(219 234 254 / var(--tw-bg-opacity)); background-color: rgb(219 234 254 / var(--tw-bg-opacity));
} }
.bg-gray-300 {
--tw-bg-opacity: 1;
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
}
.bg-black { .bg-black {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity)); background-color: rgb(0 0 0 / var(--tw-bg-opacity));
@ -1479,11 +1472,6 @@ video {
background-color: rgb(224 231 255 / var(--tw-bg-opacity)); background-color: rgb(224 231 255 / var(--tw-bg-opacity));
} }
.bg-gray-300 {
--tw-bg-opacity: 1;
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
}
.bg-red-500 { .bg-red-500 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity)); background-color: rgb(239 68 68 / var(--tw-bg-opacity));
@ -2324,10 +2312,6 @@ input {
margin: 0.5rem; margin: 0.5rem;
} }
.sm\:m-6 {
margin: 1.5rem;
}
.sm\:mx-1 { .sm\:mx-1 {
margin-left: 0.25rem; margin-left: 0.25rem;
margin-right: 0.25rem; margin-right: 0.25rem;
@ -2382,10 +2366,6 @@ input {
border-width: 4px; border-width: 4px;
} }
.sm\:p-6 {
padding: 1.5rem;
}
.sm\:p-1 { .sm\:p-1 {
padding: 0.25rem; padding: 0.25rem;
} }
@ -2418,10 +2398,6 @@ input {
margin: 0.25rem; margin: 0.25rem;
} }
.md\:m-8 {
margin: 2rem;
}
.md\:m-2 { .md\:m-2 {
margin: 0.5rem; margin: 0.5rem;
} }
@ -2483,6 +2459,10 @@ input {
border-top-width: 2px; border-top-width: 2px;
} }
.md\:p-6 {
padding: 1.5rem;
}
.md\:p-4 { .md\:p-4 {
padding: 1rem; padding: 1rem;
} }

View file

@ -1,4 +1,7 @@
{ {
"advanced": {
"title": "Advanced features"
},
"centerMessage": { "centerMessage": {
"loadingData": "Loading data…", "loadingData": "Loading data…",
"ready": "Done!", "ready": "Done!",
@ -961,6 +964,7 @@
"notImmediate": "Translations are not updated directly. This typically takes a few days" "notImmediate": "Translations are not updated directly. This typically takes a few days"
}, },
"userinfo": { "userinfo": {
"editDescription": "Edit your profile description",
"gotoInbox": "Open your inbox", "gotoInbox": "Open your inbox",
"gotoSettings": "Go to your settings on OpenStreetMap.org", "gotoSettings": "Go to your settings on OpenStreetMap.org",
"noDescription": "You don't have a description on your profile yet", "noDescription": "You don't have a description on your profile yet",