frontend: inital steps for responsive design and mobile support
This commit is contained in:
parent
1f800b6d4c
commit
2847cfcebc
14 changed files with 110 additions and 31 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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%;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
12
frontend/src/responsive.js
Normal file
12
frontend/src/responsive.js
Normal 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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue