frontend: inital steps for responsive design and mobile support

This commit is contained in:
hippoz 2022-04-27 05:03:47 +03:00
parent 1f800b6d4c
commit 2847cfcebc
No known key found for this signature in database
GPG key ID: 7C52899193467641
14 changed files with 110 additions and 31 deletions

View file

@ -59,6 +59,11 @@
--radius-xxl: calc(5.25 * var(--sradius-unit)); --radius-xxl: calc(5.25 * var(--sradius-unit));
} }
html, body {
width: 100%;
height: 100%;
}
body { body {
color: var(--foreground-color-1); color: var(--foreground-color-1);
background-color: var(--background-color-1); background-color: var(--background-color-1);
@ -67,8 +72,6 @@ body {
font-weight: 400; font-weight: 400;
line-height: 1.75; line-height: 1.75;
width: 100vw;
height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
@ -89,7 +92,7 @@ body {
.top-bar { .top-bar {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: left;
height: 3.4em; height: 3.4em;
width: 100%; width: 100%;
padding: var(--space-sm); padding: var(--space-sm);
@ -105,14 +108,19 @@ body {
/* modal */ /* modal */
.modal-backdrop { .modal-backdrop {
position: absolute; overflow: hidden;
width: 100vw; display: flex;
height: 100vh; justify-content: center;
align-items: center;
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 0; z-index: 0;
top: 50%;
left: 50%;
background-color: rgba(0, 0, 0, 0.4); background-color: rgba(0, 0, 0, 0.4);
transform: translate(-50%, -50%);
} }
.modal-backdrop-opaque { .modal-backdrop-opaque {
@ -122,10 +130,6 @@ body {
.modal { .modal {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--background-color-2); background-color: var(--background-color-2);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
padding: var(--space-md); padding: var(--space-md);
@ -149,6 +153,17 @@ body {
float: left; float: left;
} }
@media screen and (max-width: 768px) {
.modal {
width: 100%;
height: 100%;
border-radius: 0px;
}
.modal-backdrop {
display: block;
}
}
/* input */ /* input */
.input-label { .input-label {
@ -205,6 +220,9 @@ body {
border: none; border: none;
border-radius: var(--radius-md); border-radius: var(--radius-md);
font: inherit; font: inherit;
}
.icon-button-auto {
margin-left: auto; margin-left: auto;
} }

View file

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset='utf-8'> <meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'> <meta name='viewport' content='width=device-width,height=device-height,initial-scale=1'>
<title>app</title> <title>app</title>

View file

@ -1,10 +1,22 @@
<script> <script>
import { HashIcon } from "svelte-feather-icons"; import { Edit2Icon, HashIcon, MenuIcon } from "svelte-feather-icons";
import { overlayStore, selectedChannel, showSidebar } from "../stores";
export let channel; export let channel;
</script> </script>
<style>
.menu-button {
margin-right: var(--space-md);
}
</style>
<div class="top-bar"> <div class="top-bar">
{#if !$showSidebar}
<button class="icon-button menu-button" on:click="{ () => selectedChannel.set({ id: -1, name: "none", creator_id: -1 }) }">
<MenuIcon />
</button>
{/if}
<HashIcon /> <HashIcon />
<span class="h5 top-bar-heading">{ channel.name }</span> <span class="h5 top-bar-heading" on:click="{ () => overlayStore.open('editChannel', {channel}) }">{ channel.name }</span>
</div> </div>

View file

@ -1,6 +1,6 @@
<script> <script>
import { CloudIcon } from "svelte-feather-icons"; import { CloudIcon } from "svelte-feather-icons";
import { gatewayStatus, selectedChannel } from "../stores"; import { gatewayStatus, showSidebar, selectedChannel } from "../stores";
import ChannelView from "./ChannelView.svelte"; import ChannelView from "./ChannelView.svelte";
import OverlayProvider from "./overlays/OverlayProvider.svelte"; import OverlayProvider from "./overlays/OverlayProvider.svelte";
import Sidebar from "./Sidebar.svelte"; import Sidebar from "./Sidebar.svelte";
@ -16,6 +16,8 @@
} }
</style> </style>
<OverlayProvider />
{#if !$gatewayStatus.ready} {#if !$gatewayStatus.ready}
<div class="top-bar"> <div class="top-bar">
<CloudIcon /> <CloudIcon />
@ -24,13 +26,17 @@
{/if} {/if}
<div class="flex-container"> <div class="flex-container">
<OverlayProvider />
<Sidebar />
{#if $selectedChannel.id === -1} {#if $selectedChannel.id === -1}
<Sidebar />
{#if $showSidebar}
<div class="fullscreen-message"> <div class="fullscreen-message">
no channel selected. no channel selected.
</div> </div>
{/if}
{:else} {:else}
{#if $showSidebar}
<Sidebar />
{/if}
<ChannelView channel={$selectedChannel} /> <ChannelView channel={$selectedChannel} />
{/if} {/if}
</div> </div>

View file

@ -49,7 +49,7 @@
<div class="message"> <div class="message">
<span class="author">{ message.author_username }</span> <span class="author">{ message.author_username }</span>
<span class="message-content" class:pending={ message._isPending }>{ message.content }</span> <span class="message-content" class:pending={ message._isPending }>{ message.content }</span>
<button class="icon-button edit-message" on:click="{ () => overlayStore.open('editMessage', { message }) }"> <button class="icon-button icon-button-auto edit-message" on:click="{ () => overlayStore.open('editMessage', { message }) }">
<MoreVerticalIcon /> <MoreVerticalIcon />
</button> </button>
</div> </div>

View file

@ -1,7 +1,7 @@
<script> <script>
import request from "../request"; import request from "../request";
import { apiRoute } from "../storage"; import { apiRoute } from "../storage";
import { messagesStoreProvider, overlayStore, userInfoStore } from "../stores"; import { messageInputFocusStatus, messagesStoreProvider, overlayStore, userInfoStore } from "../stores";
export let channel; export let channel;
let messageInput = ""; let messageInput = "";
@ -70,5 +70,13 @@
</style> </style>
<div class="message-input-container"> <div class="message-input-container">
<input placeholder={`Send something interesting to #${channel.name}`} type="text" class="message-input" on:keydown={ onKeydown } bind:value={ messageInput }> <input
placeholder={`Send something interesting to #${channel.name}`}
type="text"
class="message-input"
on:keydown={ onKeydown }
bind:value={ messageInput }
on:focus="{ () => messageInputFocusStatus.set(true) }"
on:blur="{ () => messageInputFocusStatus.set(false) }"
>
</div> </div>

View file

@ -1,6 +1,6 @@
<script> <script>
import { afterUpdate, beforeUpdate, onMount } from "svelte"; import { afterUpdate, beforeUpdate, onMount } from "svelte";
import { messagesStoreProvider } from "../stores.js"; import { messageInputFocusStatus, messagesStoreProvider } from "../stores.js";
import Message from "./Message.svelte"; import Message from "./Message.svelte";
export let channelId; export let channelId;
@ -8,8 +8,17 @@
let scrollAnchor; let scrollAnchor;
let shouldAutoscroll = true; let shouldAutoscroll = true;
let lastScrollHeight = null; let lastScrollHeight = null;
let isScrolledToBottom = false;
$: messages = messagesStoreProvider.getStore(channelId); $: messages = messagesStoreProvider.getStore(channelId);
$: {
console.log($messageInputFocusStatus, isScrolledToBottom);
if ($messageInputFocusStatus && isScrolledToBottom) {
setTimeout(() => {
scrollTarget.scrollTop = scrollTarget.scrollHeight;
}, 0);
}
}
afterUpdate(() => { afterUpdate(() => {
// hacky way to preserve scroll position when messages are pushed back // hacky way to preserve scroll position when messages are pushed back
@ -28,8 +37,10 @@
if ((scrollTop + offsetHeight) >= scrollHeight) { // user scrolled to bottom if ((scrollTop + offsetHeight) >= scrollHeight) { // user scrolled to bottom
messages.setIsCollectingOldMessages(true); messages.setIsCollectingOldMessages(true);
shouldAutoscroll = true; shouldAutoscroll = true;
isScrolledToBottom = true;
} else { } else {
shouldAutoscroll = false; shouldAutoscroll = false;
isScrolledToBottom = false;
if (scrollTop === 0) { if (scrollTop === 0) {
// load older messages if the user scrolls to the top. // load older messages if the user scrolls to the top.
// save the current scroll height if the server returned any messages, // save the current scroll height if the server returned any messages,

View file

@ -13,7 +13,7 @@
<HashIcon /> <HashIcon />
</div> </div>
<span>{ channel.name }</span> <span>{ channel.name }</span>
<button class="icon-button" on:click|stopPropagation="{ () => overlayStore.open('editChannel', { channel }) }"> <button class="icon-button icon-button-auto" on:click|stopPropagation="{ () => overlayStore.open('editChannel', { channel }) }">
<MoreVerticalIcon /> <MoreVerticalIcon />
</button> </button>
</button> </button>
@ -32,10 +32,18 @@
background-color: var(--background-color-1); background-color: var(--background-color-1);
border-right: 1px solid var(--background-color-2); border-right: 1px solid var(--background-color-2);
height: 100%; height: 100%;
width: 255px; min-width: 255px;
max-width: 255px; max-width: 255px;
} }
@media screen and (max-width: 768px) {
.sidebar-container {
flex-basis: 100%;
min-width: unset;
max-width: unset;
}
}
.sidebar { .sidebar {
width: 100%; width: 100%;
height: 100%; height: 100%;

View file

@ -52,7 +52,7 @@
</style> </style>
<div class="modal-backdrop modal-backdrop-opaque"> <div class="modal-backdrop modal-backdrop-opaque">
<div class="modal" transition:fly="{{ duration: 500, easing: quintInOut, y: 10 }}" on:click|stopPropagation on:outroend="{ outroEnd }"> <div class="modal" transition:fly="{{ duration: 300, easing: quintInOut, y: 10 }}" on:click|stopPropagation on:outroend="{ outroEnd }">
<div class="modal-header"> <div class="modal-header">
<span class="h4">Create an Account</span> <span class="h4">Create an Account</span>
</div> </div>

View file

@ -56,7 +56,7 @@
</style> </style>
<div class="modal-backdrop modal-backdrop-opaque"> <div class="modal-backdrop modal-backdrop-opaque">
<div class="modal" transition:fly="{{ duration: 500, easing: quintInOut, y: 10 }}" on:click|stopPropagation on:outroend="{ outroEnd }"> <div class="modal" transition:fly="{{ duration: 300, easing: quintInOut, y: 10 }}" on:click|stopPropagation on:outroend="{ outroEnd }">
<div class="modal-header"> <div class="modal-header">
<span class="h4">Welcome back!</span> <span class="h4">Welcome back!</span>
</div> </div>

View file

@ -26,7 +26,7 @@
{#key message} {#key message}
<div class="toast" transition:fly="{{ duration: 300, easing: quintInOut, y: 10 }}"> <div class="toast" transition:fly="{{ duration: 300, easing: quintInOut, y: 10 }}">
<span>{ message }</span> <span>{ message }</span>
<button class="icon-button" on:click="{ () => overlayStore.close('toast') }"> <button class="icon-button icon-button-auto" on:click="{ () => overlayStore.close('toast') }">
<XIcon /> <XIcon />
</button> </button>
</div> </div>

View file

@ -3,6 +3,7 @@ import gateway from './gateway';
import { getAuthToken, initStorageDefaults } from './storage'; import { getAuthToken, initStorageDefaults } from './storage';
import logging from "./logging"; import logging from "./logging";
import { authWithToken } from './auth'; import { authWithToken } from './auth';
import { initResponsiveHandlers } from './responsive';
window.__waffle = { window.__waffle = {
logging, logging,
@ -10,6 +11,7 @@ window.__waffle = {
}; };
initStorageDefaults(); initStorageDefaults();
initResponsiveHandlers();
authWithToken(getAuthToken()); authWithToken(getAuthToken());
// Remove loading screen // Remove loading screen

View file

@ -0,0 +1,12 @@
import { showSidebar } from "./stores";
export function initResponsiveHandlers() {
const mediaQuery = window.matchMedia('(min-width: 768px)');
const update = ({ matches }) => {
showSidebar.set(matches);
};
mediaQuery.addEventListener("change", update);
update(mediaQuery);
}

View file

@ -264,3 +264,5 @@ export const messagesStoreProvider = new MessagesStoreProvider();
export const userInfoStore = new UserInfoStore(); export const userInfoStore = new UserInfoStore();
export const overlayStore = new OverlayStore(); export const overlayStore = new OverlayStore();
export const selectedChannel = writable({ id: -1, name: "none", creator_id: -1 }); export const selectedChannel = writable({ id: -1, name: "none", creator_id: -1 });
export const showSidebar = writable(false);
export const messageInputFocusStatus = writable(false);