diff options
| author | Owen Jacobson <owen@grimoire.ca> | 2025-01-11 13:34:40 -0500 |
|---|---|---|
| committer | Owen Jacobson <owen@grimoire.ca> | 2025-01-11 13:34:40 -0500 |
| commit | ea0392a2bb12c158f9167105752f8fa315cff47d (patch) | |
| tree | dec4545ccbe44c8e2baf6e633308359f40ac610a /ui | |
| parent | 4e3ad13aca163e733724b205c250bdb67cc56c29 (diff) | |
| parent | 6d51f8568e337e768505ccfdef916b84dd6eb1b3 (diff) | |
Merge branch 'prop/stylize'
Diffstat (limited to 'ui')
33 files changed, 727 insertions, 221 deletions
@@ -1,13 +1,54 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -/* This should help minimize swipe-to-go-back behaviour, enabling our -* swipe-to-reveal-channel-menu behaviour. It won't work in all cases; in iOS -* Safari, when swiping from the screen edge, the OS gets th event and -* handles it before the browser does. -*/ -html, +@import url('styles/reset.css'); +@import url('styles/variables.css'); +@import url('styles/overscroll.css'); +@import url('styles/app-bar.css'); +@import url('styles/app-layout.css'); +@import url('styles/sidebar.css'); +@import url('styles/active-channel.css'); +@import url('styles/messages.css'); +@import url('styles/textarea.css'); +@import url('styles/forms.css'); +@import url('styles/invites.css'); + body { - overscroll-behavior-x: none; + background-color: var(--colour-active-channel-bg); + color: var(--dark-text); +} + +hr { + width: 90%; +} + +.no-active-channel { + display: table; + height: 100%; + width: 100%; + text-align: center; +} + +.vertical-aligner { + display: table-cell; + vertical-align: middle; + font-style: italic; +} + +/* TODO: + * put this somewhere appropriate, + * use a correct variable, + * be sensitive to background colour context. + */ +a, +a:hover, +a:visited, +a:active { + color: var(--light-text); +} + +/* Debugging */ +/* * / +div { + outline: 1px dashed grey; + background-color: var(--colour-background); + box-shadow: 5px 5px 5px var(--colour-border); } +/* */ diff --git a/ui/app.html b/ui/app.html index 5e7d92b..e6be518 100644 --- a/ui/app.html +++ b/ui/app.html @@ -7,7 +7,7 @@ <link rel="manifest" href="%sveltekit.assets%/manifest.json" /> %sveltekit.head% </head> - <body data-sveltekit-preload-data="hover" data-theme="skeleton"> - <div class="m-0 p-0 h-vh w-full">%sveltekit.body%</div> + <body data-sveltekit-preload-data="hover"> + <div>%sveltekit.body%</div> </body> </html> diff --git a/ui/lib/components/ActiveChannel.svelte b/ui/lib/components/ActiveChannel.svelte index ba62d6c..9c181e4 100644 --- a/ui/lib/components/ActiveChannel.svelte +++ b/ui/lib/components/ActiveChannel.svelte @@ -5,7 +5,5 @@ </script> {#each messageRuns as { sender, messages }} - <div> - <MessageRun {sender} {messages} /> - </div> + <MessageRun {sender} {messages} /> {/each} diff --git a/ui/lib/components/ChangePassword.svelte b/ui/lib/components/ChangePassword.svelte index 1e48bee..bf94ea7 100644 --- a/ui/lib/components/ChangePassword.svelte +++ b/ui/lib/components/ChangePassword.svelte @@ -22,11 +22,10 @@ } </script> -<form {onsubmit} bind:this={form}> +<form class="form" {onsubmit} bind:this={form}> <label >current password <input - class="input" name="currentPassword" type="password" placeholder="password" @@ -36,19 +35,12 @@ <label >new password - <input - class="input" - name="newPassword" - type="password" - placeholder="password" - bind:value={newPassword} - /> + <input name="newPassword" type="password" placeholder="password" bind:value={newPassword} /> </label> <label >confirm new password <input - class="input" name="confirmPassword" type="password" placeholder="password" @@ -56,5 +48,5 @@ /> </label> - <button class="btn bg-orange-500 mt-4" type="submit" {disabled}>change password</button> + <button type="submit" {disabled}>change password</button> </form> diff --git a/ui/lib/components/Channel.svelte b/ui/lib/components/Channel.svelte index 01b1c87..c73340f 100644 --- a/ui/lib/components/Channel.svelte +++ b/ui/lib/components/Channel.svelte @@ -2,13 +2,13 @@ let { id, name, active, hasUnreads } = $props(); </script> -<li class="rounded-full" class:bg-slate-400={active}> - <a href="/ch/{id}"> +<a href="/ch/{id}"> + <li class:active> {#if hasUnreads} - <span class="badge bg-warning-500">❦</span> + <span class="badge has-unreads">❦</span> {:else} - <span class="badge bg-primary-500">¶</span> + <span class="badge has-no-unreads">¶</span> {/if} - <span class="flex-auto">{name}</span> - </a> -</li> + <span>{name}</span> + </li> +</a> diff --git a/ui/lib/components/CreateChannelForm.svelte b/ui/lib/components/CreateChannelForm.svelte index d50e60d..85c85bb 100644 --- a/ui/lib/components/CreateChannelForm.svelte +++ b/ui/lib/components/CreateChannelForm.svelte @@ -15,16 +15,7 @@ } </script> -<form onsubmit={handleSubmit} class="form form-row flex-nowrap"> - <input - type="text" - placeholder="create channel" - bind:value={name} - {disabled} - class="input flex-auto h-6 w-9/12" - /> - <button type="submit" class="flex-none w-6 h-6">➕</button> +<form onsubmit={handleSubmit}> + <input type="text" placeholder="create channel" bind:value={name} {disabled} /> + <button type="submit">➕</button> </form> - -<style> -</style> diff --git a/ui/lib/components/Invite.svelte b/ui/lib/components/Invite.svelte index 937c911..29631f4 100644 --- a/ui/lib/components/Invite.svelte +++ b/ui/lib/components/Invite.svelte @@ -1,9 +1,13 @@ <script> - import { clipboard } from '@skeletonlabs/skeleton'; + import { copy } from 'svelte-copy'; let { id } = $props(); let inviteUrl = $derived(new URL(`/invite/${id}`, document.location)); + // Nota bene: we cannot use:copy={inviteUrl}, because inviteUrl is not a + // string, and therefore will get treated as an options object. So we must + // explicitly build an options object, containing the value that will + // eventually be interpreted as a string. </script> -<button class="btn bg-secondary-500" use:clipboard={inviteUrl}>copy</button> -<span data-clipboard="inviteUrl">{inviteUrl}</span> +<button use:copy={{ text: inviteUrl }}>copy</button> +<span class="invite-url-literal">{inviteUrl}</span> diff --git a/ui/lib/components/Invites.svelte b/ui/lib/components/Invites.svelte index 493bf1c..27d3754 100644 --- a/ui/lib/components/Invites.svelte +++ b/ui/lib/components/Invites.svelte @@ -13,12 +13,12 @@ } </script> -<form {onsubmit}> - <button class="btn bg-primary-500" type="submit">create invitation</button> +<form class="form" {onsubmit}> + <button type="submit">create invitation</button> </form> -<ul class="mt-4"> +<ul class="invite-list"> {#each invites as invite} - <li class="my-1"><Invite id={invite.id} /></li> + <li><Invite id={invite.id} /></li> {/each} </ul> diff --git a/ui/lib/components/LogIn.svelte b/ui/lib/components/LogIn.svelte index 7fb91e8..5bfdae2 100644 --- a/ui/lib/components/LogIn.svelte +++ b/ui/lib/components/LogIn.svelte @@ -8,23 +8,15 @@ } = $props(); </script> -<div class="card m-4 p-4"> - <form {onsubmit}> - <label class="label" for="username"> +<div> + <form class="form" {onsubmit}> + <label for="username"> username - <input - class="input" - name="username" - type="text" - placeholder="username" - bind:value={username} - {disabled} - /> + <input name="username" type="text" placeholder="username" bind:value={username} {disabled} /> </label> - <label class="label" for="password"> + <label for="password"> password <input - class="input" name="password" type="password" placeholder="password" @@ -32,7 +24,7 @@ {disabled} /> </label> - <button class="btn variant-filled" type="submit"> + <button type="submit"> {legend} </button> </form> diff --git a/ui/lib/components/LogOut.svelte b/ui/lib/components/LogOut.svelte index 52aa039..b699cfd 100644 --- a/ui/lib/components/LogOut.svelte +++ b/ui/lib/components/LogOut.svelte @@ -13,6 +13,6 @@ } </script> -<form {onsubmit}> - <button class="btn bg-orange-400" type="submit">log out</button> +<form class="form" {onsubmit}> + <button type="submit">log out</button> </form> diff --git a/ui/lib/components/Message.svelte b/ui/lib/components/Message.svelte index 5673248..1b1598b 100644 --- a/ui/lib/components/Message.svelte +++ b/ui/lib/components/Message.svelte @@ -25,42 +25,15 @@ } </script> -<div - class="message relative" - class:bg-warning-800={deleteArmed} - role="article" - data-at={at} - {onmouseleave} - > - <div class="handle chip bg-surface-700 absolute -top-6 right-0"> +<div class="message" class:delete-armed={deleteArmed} role="article" data-at={at} {onmouseleave}> + <div class="handle"> {atFormatted} {#if editable} <button onclick={onDelete}>🗑️</button> {/if} </div> - <section use:scroll class="py-1 message-body"> + <section use:scroll class="message-body"> <!-- eslint-disable-next-line svelte/no-at-html-tags --> {@html renderedBody} </section> </div> - -<style> - .message .handle { - display: none; - } - .message:hover .handle { - display: flex; - } - .message-body { - overflow: auto; - max-width: 80vw; - @media (width > 640px) { - /* 21rem is width of the nav bar in full-screen mode. */ - max-width: calc(90vw - 21rem); - } - } - .message-body:empty:after { - content: '.'; - visibility: hidden; - } -</style> diff --git a/ui/lib/components/MessageInput.svelte b/ui/lib/components/MessageInput.svelte index 22456f3..162db1b 100644 --- a/ui/lib/components/MessageInput.svelte +++ b/ui/lib/components/MessageInput.svelte @@ -23,17 +23,7 @@ } </script> -<form bind:this={form} onsubmit={onSubmit} class="flex flex-row flex-nowrap"> - <textarea - onkeydown={onKeyDown} - bind:value - {disabled} - type="search" - class="flex-auto h-6 py-0 input rounded-r-none text-nowrap" - ></textarea> - <button - color="primary variant-filled-secondary" - type="submit" - class="flex-none w-6 h-6 btn-icon variant-filled rounded-l-none">»</button - > +<form bind:this={form} onsubmit={onSubmit}> + <textarea onkeydown={onKeyDown} bind:value {disabled} type="search"></textarea> + <button type="submit">»</button> </form> diff --git a/ui/lib/components/MessageRun.svelte b/ui/lib/components/MessageRun.svelte index 39ee155..bee64e8 100644 --- a/ui/lib/components/MessageRun.svelte +++ b/ui/lib/components/MessageRun.svelte @@ -8,15 +8,11 @@ let ownMessage = $derived($currentUser !== null && $currentUser.id == sender); </script> -<div - class="card my-4 px-4 py-1 relative" - class:own-message={ownMessage} - class:other-message={!ownMessage} -> - <span class="chip variant-soft sticky top-o left-0"> +<div class="message-run" class:own-message={ownMessage} class:other-message={!ownMessage}> + <span class="username"> @{name}: </span> - {#each messages as { id, at, body, renderedBody }} - <Message {id} {at} {body} {renderedBody} editable={ownMessage} /> + {#each messages as message} + <Message {...message} editable={ownMessage} /> {/each} </div> diff --git a/ui/lib/store/channels.svelte.js b/ui/lib/store/channels.svelte.js index 8919be0..c82f9aa 100644 --- a/ui/lib/store/channels.svelte.js +++ b/ui/lib/store/channels.svelte.js @@ -1,5 +1,5 @@ import { DateTime } from 'luxon'; -const EPOCH_STRING = "1970-01-01T00:00:00Z"; +const EPOCH_STRING = '1970-01-01T00:00:00Z'; // For reasons unclear to me, a straight up class definition with a constructor // doesn't seem to work, reactively. So we resort to this. @@ -15,7 +15,7 @@ function makeChannelObject({ id, name, draft = '', lastReadAt = null, scrollPosi name, lastReadAt: lastReadAt || DateTime.fromISO(EPOCH_STRING), draft, - scrollPosition, + scrollPosition }; } @@ -33,10 +33,7 @@ export class Channels { } addChannel(id, name) { - this.channels = [ - ...this.channels, - makeChannelObject({ id, name }), - ]; + this.channels = [...this.channels, makeChannelObject({ id, name })]; this.sort(); return this; } diff --git a/ui/lib/store/messages.svelte.js b/ui/lib/store/messages.svelte.js index ba4c895..dadade6 100644 --- a/ui/lib/store/messages.svelte.js +++ b/ui/lib/store/messages.svelte.js @@ -5,7 +5,7 @@ import DOMPurify from 'dompurify'; const RUN_COALESCE_MAX_INTERVAL = 10 /* min */ * 60 /* sec */ * 1000; /* ms */ export class Messages { - channels = $state({}); // Mapping<ChannelId, Message> + channels = $state({}); // Mapping<ChannelId, Message> inChannel(channel) { return this.channels[channel] || []; diff --git a/ui/routes/(app)/+layout.svelte b/ui/routes/(app)/+layout.svelte index 81324fd..cbfef54 100644 --- a/ui/routes/(app)/+layout.svelte +++ b/ui/routes/(app)/+layout.svelte @@ -41,7 +41,7 @@ let hasUnreads = lastMessageAt > ch.lastReadAt; enrichedChannels.push({ ...ch, - hasUnreads, + hasUnreads }); } } @@ -104,8 +104,20 @@ gesture.destroy(); } }); + + function beforeUnload(evt) { + evt.preventDefault(); + if (events !== null) { + events.close(); + } + // For some compat reasons? + evt.returnValue = ''; + return ''; + } </script> +<svelte:window on:beforeunload={beforeUnload} /> + <svelte:head> <!-- TODO: unread count? --> <title>pilcrow</title> @@ -114,65 +126,15 @@ {#if loading} <h2>Loading…</h2> {:else} - <div id="interface" class="p-2"> + <div id="interface"> <nav id="sidebar" data-expanded={pageContext.showMenu}> - <div class="channel-list"> - <ChannelList active={channel} channels={enrichedChannels} /> - </div> + <ChannelList active={channel} channels={enrichedChannels} /> <div class="create-channel"> <CreateChannelForm /> </div> </nav> - <main class="pl-4"> + <main> {@render children?.()} </main> </div> {/if} - -<style> - /* Just some global CSS variables, don't mind them. - */ - :root { - --app-bar-height: 48px; - --input-row-height: 2rem; - --interface-padding: 16px; - } - - #interface { - margin: unset; - display: grid; - grid-template: - 'side main' 1fr - / auto 1fr; - height: calc(100vh - var(--app-bar-height)); - - @media (width > 640px) { - --overlay: static; - --translate: 0; - } - } - nav { - grid-area: side; - background-color: rgb(var(--color-surface-800)); - inset: auto auto 0 0; - padding: 0.25rem; - position: var(--overlay, absolute); - transition: translate 300ms ease-out; - width: 21rem; - height: calc(100vh - var(--app-bar-height) - var(--interface-padding)); - z-index: 10; - } - main { - grid-area: main; - height: calc(100vh - var(--app-bar-height) - var(--interface-padding)); - } - .channel-list { - height: calc( - 100vh - var(--app-bar-height) - var(--interface-padding) - var(--input-row-height) - ); - overflow: auto; - } - nav[data-expanded='false'] { - translate: var(--translate, -100% 0); - } -</style> diff --git a/ui/routes/(app)/+page.svelte b/ui/routes/(app)/+page.svelte index e69de29..007c5c6 100644 --- a/ui/routes/(app)/+page.svelte +++ b/ui/routes/(app)/+page.svelte @@ -0,0 +1,3 @@ +<div class="no-active-channel"> + <span class="vertical-aligner"> Please select or create a channel. </span> +</div> diff --git a/ui/routes/(app)/ch/[channel]/+page.svelte b/ui/routes/(app)/ch/[channel]/+page.svelte index 7d5b8cf..dbdb507 100644 --- a/ui/routes/(app)/ch/[channel]/+page.svelte +++ b/ui/routes/(app)/ch/[channel]/+page.svelte @@ -18,15 +18,17 @@ const elementTop = elRect.top; const elementBottom = elRect.bottom; - return ((parentTop < elementTop) && (parentBottom > elementBottom)); + return parentTop < elementTop && parentBottom > elementBottom; } function getLastVisibleMessage() { const parentElement = activeChannel; const childElements = parentElement.getElementsByClassName('message'); - const lastInView = Array.from(childElements).reverse().find((el) => { - return inView(parentElement, el); - }); + const lastInView = Array.from(childElements) + .reverse() + .find((el) => { + return inView(parentElement, el); + }); return lastInView; } @@ -50,14 +52,14 @@ }); function handleKeydown(event) { - if (event.key === 'Escape') { + if (event.key === 'Escape') { setLastRead(); // TODO: pass in "last message DT"? } } let lastReadCallback = null; function handleScroll() { - clearTimeout(lastReadCallback); // Fine if lastReadCallback is null still. + clearTimeout(lastReadCallback); // Fine if lastReadCallback is null still. lastReadCallback = setTimeout(setLastRead, 2 * 1000); } </script> @@ -67,15 +69,6 @@ <div class="active-channel" on:scroll={handleScroll} bind:this={activeChannel}> <ActiveChannel {messageRuns} /> </div> -<div class="create-message max-h-full"> +<div class="create-message"> <MessageInput {channel} /> </div> - -<style> - .active-channel { - height: calc( - 100vh - var(--app-bar-height) - var(--interface-padding) - var(--input-row-height) - ); - overflow: auto; - } -</style> diff --git a/ui/routes/(app)/me/+page.svelte b/ui/routes/(app)/me/+page.svelte index aded292..14a9db8 100644 --- a/ui/routes/(app)/me/+page.svelte +++ b/ui/routes/(app)/me/+page.svelte @@ -4,14 +4,8 @@ import ChangePassword from '$lib/components/ChangePassword.svelte'; </script> -<div class="mb-4"> - <ChangePassword /> -</div> - -<div class="mb-4"> - <Invites /> -</div> - -<div> - <LogOut /> -</div> +<ChangePassword /> +<hr /> +<Invites /> +<hr /> +<LogOut /> diff --git a/ui/routes/(login)/invite/[invite]/+page.svelte b/ui/routes/(login)/invite/[invite]/+page.svelte index 4433bd6..132cbc1 100644 --- a/ui/routes/(login)/invite/[invite]/+page.svelte +++ b/ui/routes/(login)/invite/[invite]/+page.svelte @@ -25,11 +25,11 @@ </script> {#await data.invite} - <div class="card m-4 p-4"> + <div class="invite-text"> <p>Loading invitation…</p> </div> {:then invite} - <div class="card m-4 p-4"> + <div class="invite-text"> <p>Hi there! {invite.issuer} invites you to the conversation.</p> </div> <LogIn {disabled} bind:username bind:password onsubmit={(event) => onSubmit(event, invite.id)} /> diff --git a/ui/routes/+layout.svelte b/ui/routes/+layout.svelte index 26033e0..750f1f8 100644 --- a/ui/routes/+layout.svelte +++ b/ui/routes/+layout.svelte @@ -4,7 +4,6 @@ import '../app.css'; import logo from '$lib/assets/logo.png'; - import { AppBar } from '@skeletonlabs/skeleton'; import { currentUser } from '$lib/store'; let pageContext = $state({ @@ -24,20 +23,20 @@ let { children } = $props(); </script> -<AppBar padding="px-4 pt-0 pb-4"> - <svelte:fragment slot="lead"> - <button onclick={toggleMenu} class="cursor-pointer"> - <img class="w-8 h-8" alt="logo" src={logo} /> +<div class="app-bar"> + <div class="lead"> + <button onclick={toggleMenu}> + <img alt="logo" src={logo} /> </button> - </svelte:fragment> + </div> <a href="/">pilcrow</a> - <svelte:fragment slot="trail"> + <div class="trail"> {#if $currentUser} - <div class="rounded-full bg-secondary-400 px-3 py-1"> + <div> <a href="/me">@{$currentUser.username}</a> </div> {/if} - </svelte:fragment> -</AppBar> + </div> +</div> {@render children?.()} diff --git a/ui/static/manifest.json b/ui/static/manifest.json index 5d735d0..6677d94 100644 --- a/ui/static/manifest.json +++ b/ui/static/manifest.json @@ -4,7 +4,7 @@ "start_url": "/", "display": "standalone", "background_color": "#fdfdfd", - "theme_color": "#2c3656", + "theme_color": "#211b2a", "orientation": "portrait-primary", "icons": [ { diff --git a/ui/styles/active-channel.css b/ui/styles/active-channel.css new file mode 100644 index 0000000..d6a9b42 --- /dev/null +++ b/ui/styles/active-channel.css @@ -0,0 +1,6 @@ +.active-channel { + padding-left: 1rem; + padding-right: 1rem; + overflow: auto; + flex-grow: 1; +} diff --git a/ui/styles/app-bar.css b/ui/styles/app-bar.css new file mode 100644 index 0000000..dcc447a --- /dev/null +++ b/ui/styles/app-bar.css @@ -0,0 +1,40 @@ +/* App Bar */ +.app-bar { + height: var(--app-bar-height); + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: stretch; + background-color: var(--colour-header-bg); +} + +.app-bar > * { + display: inline-block; +} + +.app-bar .lead { + flex-basis: 60px; +} + +.app-bar .lead button { + padding: 0; + border: 0; +} + +.app-bar .trail { + flex-basis: 60px; + line-height: var(--app-bar-height); +} + +.app-bar > a { + line-height: var(--app-bar-height); +} + +.app-bar a { + text-decoration: none; +} + +.app-bar button, +.app-bar button img { + height: var(--app-bar-height); +} diff --git a/ui/styles/app-layout.css b/ui/styles/app-layout.css new file mode 100644 index 0000000..82d9914 --- /dev/null +++ b/ui/styles/app-layout.css @@ -0,0 +1,56 @@ +/* TODO: generally remove literals from this file. */ + +#interface { + margin: unset; + display: grid; + grid-template: + 'side main' 1fr + / auto 1fr; + height: calc(100vh - var(--app-bar-height)); + + @media (width > 640px) { + --overlay: static; + --translate: 0; + } +} + +nav#sidebar { + grid-area: side; + inset: auto auto 0 0; + padding-right: 0.5rem; + position: var(--overlay, absolute); + transition: translate 300ms ease-out; + width: var(--nav-width); + height: 100vh; + z-index: 10; + + background-color: var(--colour-navbar-bg); + + @media (width > 640px) { + height: calc(100vh - var(--app-bar-height)); + } +} + +nav.list-nav { + height: calc(100vh - var(--input-row-height) - var(--interface-padding)); + overflow: auto; + + @media (width > 640px) { + height: calc( + 100vh - var(--app-bar-height) - var(--input-row-height) - var(--interface-padding) + ); + } +} + +main { + grid-area: main; + height: calc(100vh - var(--app-bar-height)); +} + +main textarea { + resize: none; +} + +nav[data-expanded='false'] { + translate: var(--translate, -100% 0); +} diff --git a/ui/styles/forms.css b/ui/styles/forms.css new file mode 100644 index 0000000..1d6421b --- /dev/null +++ b/ui/styles/forms.css @@ -0,0 +1,21 @@ +label { + display: block; + padding: 0.25rem; + font-style: italic; +} + +label input { + display: block; + width: 90%; + padding: 0.25rem; + border-radius: 0.25rem; + border: 1px solid var(--colour-input-border); +} + +form.form > button { + background-color: var(--colour-input-bg); + padding: 0.25rem; + border-radius: 0.25rem; + border: 1px solid var(--colour-input-border); + margin: 0.25rem; +} diff --git a/ui/styles/invites.css b/ui/styles/invites.css new file mode 100644 index 0000000..9cd3fd4 --- /dev/null +++ b/ui/styles/invites.css @@ -0,0 +1,11 @@ +.invite-list { + padding: 0.25rem; +} + +.invite-url-literal { + font-family: monospace; +} + +.invite-text { + margin: 1rem; +} diff --git a/ui/styles/messages.css b/ui/styles/messages.css new file mode 100644 index 0000000..a07c5d9 --- /dev/null +++ b/ui/styles/messages.css @@ -0,0 +1,121 @@ +.message-run { + position: relative; + border-radius: 0.25rem; + padding: 0 0 0.5rem 0; + margin-top: 1rem; + margin-bottom: 1rem; + box-shadow: 0 0.25rem 0.25rem rgba(0, 0, 0, 0.5); + overflow: hidden; +} + +.own-message { + background-color: var(--colour-message-run-self-bg); + border: 1px solid var(--colour-message-run-self-border); + margin-left: 2rem; +} + +.own-message * { + color: var(--colour-message-run-self-text); +} + +.other-message { + background-color: var(--colour-message-run-other-bg); + border: 1px solid var(--colour-message-run-other-border); + margin-right: 2rem; +} + +.other-message * { + color: var(--colour-message-run-other-text); +} + +.message-run > .username { + background-color: var(--colour-message-run-username-bg); + color: var(--colour-message-run-username-text); + display: inline-block; + border-bottom-right-radius: 0.25rem; + padding: 0.25rem; + border-bottom: 1px solid var(--colour-message-run-username-border); + border-right: 1px solid var(--colour-message-run-username-border); +} + +.message { + padding: 0.5rem 0 0 0.5rem; + position: relative; +} + +.message.delete-armed, +.message.delete-armed:hover { + background-color: var(--colour-warn); +} + +.message:hover { + background-color: var(--colour-message-hover-bg); +} +.message:hover * { + color: var(--colour-message-hover-text); +} + +.message .handle { + --text-size: 0.75rem; + float: right; + line-height: var(--text-size); + font-size: var(--text-size); + top: -0.75rem; + right: 0.5rem; + position: absolute; + padding: 0.25rem; + border-radius: 0.25rem; + display: none; + background-color: var(--colour-message-handle-bg); + color: var(--colour-message-handle-text); + border: 1px solid var(--colour-message-handle-border); +} + +.message:hover .handle { + display: flex; +} + +.message .handle button { + line-height: 0.35rem; + background: none; + border: none; + cursor: pointer; +} + +.message-body { + overflow: auto; + max-width: 80vw; + + @media (width > 640px) { + /* 21rem is width of the nav bar in full-screen mode. */ + max-width: calc(90vw - 21rem); + } +} + +.message-body:empty:after { + content: '.'; + visibility: hidden; +} + +/* For rendered message bodies: */ +.message-body blockquote { + margin-left: 0.25rem; + padding-left: 0.5rem; + border-left: 2px solid grey; + border-radius: 0.125rem; +} + +.message-body blockquote * { + color: grey; +} + +.message-body pre { + border: 1px solid #312e81; + border-radius: 0.25rem; + background-color: var(--colour-message-run-text); + padding: 0.25rem; +} + +.message-body code { + font-family: monospace; +} diff --git a/ui/styles/overscroll.css b/ui/styles/overscroll.css new file mode 100644 index 0000000..8898f9a --- /dev/null +++ b/ui/styles/overscroll.css @@ -0,0 +1,9 @@ +/* This should help minimize swipe-to-go-back behaviour, enabling our +* swipe-to-reveal-channel-menu behaviour. It won't work in all cases; in iOS +* Safari, when swiping from the screen edge, the OS gets th event and +* handles it before the browser does. +*/ +html, +body { + overscroll-behavior-x: none; +} diff --git a/ui/styles/reset.css b/ui/styles/reset.css new file mode 100644 index 0000000..a3f7681 --- /dev/null +++ b/ui/styles/reset.css @@ -0,0 +1,129 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +b, +u, +i, +center, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +canvas, +details, +embed, +figure, +figcaption, +footer, +header, +hgroup, +menu, +nav, +output, +ruby, +section, +summary, +time, +mark, +audio, +video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +menu, +nav, +section { + display: block; +} +body { + line-height: 1; +} +ol, +ul { + list-style: none; +} +blockquote, +q { + quotes: none; +} +blockquote:before, +blockquote:after, +q:before, +q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/ui/styles/sidebar.css b/ui/styles/sidebar.css new file mode 100644 index 0000000..5e5e16a --- /dev/null +++ b/ui/styles/sidebar.css @@ -0,0 +1,85 @@ +/* Sidebar and channel selector */ +#sidebar { + background-color: var(--colour-navbar-bg); +} + +.list-nav a { + text-decoration: none; +} + +.list-nav ul { + padding: 0.5rem; +} + +.list-nav li { + padding: 0.5rem; + border-radius: 0.5rem; + border: 1px solid var(--colour-navbar-border); + margin: 0.25rem; +} + +.list-nav li.active { + background-color: var(--colour-navbar-active-bg); + color: var(--colour-navbar-active-text); +} + +.list-nav li:hover { + background-color: var(--colour-navbar-hover-bg); + color: var(--colour-navbar-hover-text); +} + +/* create channel form */ +.create-channel { + padding-left: 0.5rem; +} + +.create-channel form { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: stretch; +} + +.create-channel input { + padding: 0.5rem; + border-radius: 0.5rem 0 0 0.5rem; + border: 1px solid var(--colour-input-border); + z-index: 1; /* Just to make the focus-active border go over the following button. */ + flex-grow: 1; + background-color: var(--colour-input-bg); + color: var(--colour-input-text); +} + +.create-channel button { + border-radius: 0 0.5rem 0.5rem 0; + border: 1px solid var(--colour-input-border); + background-color: var(--colour-input-bg); + color: var(--colour-input-text); +} + +.badge { + --dimensions: 1.25rem; + display: inline-block; + + width: var(--dimensions); + height: var(--dimensions); + border-radius: var(--dimensions); + line-height: var(--dimensions); + text-align: center; +} + +.badge.has-unreads { + /* TODO: Obvs this is a placeholder */ + border: 1px solid mediumaquamarine; + background-color: lightgreen; + color: black; +} + +.badge.has-no-unreads { + /* TODO: Obvs this is a placeholder */ + border: 1px solid bisque; + background-color: antiquewhite; + color: black; +} + +/* TODO: media-query stuff. Margin-left at a constant zero? */ diff --git a/ui/styles/textarea.css b/ui/styles/textarea.css new file mode 100644 index 0000000..c38de46 --- /dev/null +++ b/ui/styles/textarea.css @@ -0,0 +1,31 @@ +/* Message input */ +.create-message form { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: stretch; +} + +.create-message textarea { + padding: 0.5rem; + border-radius: 0.5rem 0 0 0.5rem; + border: 1px solid var(--colour-input-border); + z-index: 1; /* Just to make the focus-active border go over the following button. */ + flex-grow: 1; + background-color: var(--colour-input-bg); + color: var(--colour-input-text); +} + +.create-message button { + border-radius: 0 0.5rem 0.5rem 0; + border: 1px solid var(--colour-input-border); + background-color: var(--colour-input-bg); + color: var(--colour-input-text); +} + +main { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; +} diff --git a/ui/styles/variables.css b/ui/styles/variables.css new file mode 100644 index 0000000..80817f3 --- /dev/null +++ b/ui/styles/variables.css @@ -0,0 +1,72 @@ +:root { + /* + * Not great that these are in px, but not sure what else to do. + */ + --app-bar-height: 48px; + --input-row-height: 2rem; + --interface-padding: 16px; + --nav-width: 21rem; + + /* coloUrs */ + --colour-okay: #6a994e; + --colour-warn: #ebc3be; + --colour-error: #de5f55; + + /* I dunno, I liked this colour: */ + --colour-base: rgb(121, 96, 159); + + /* Light text is a bit hard to read; I may need to adjust it. */ + --light-text: color-mix(in srgb, var(--colour-base) 40%, white); + --dark-text: color-mix(in srgb, var(--colour-base) 40%, black); + + /* Header */ + --colour-header-bg: color-mix(in srgb, var(--colour-base) 30%, black); + --colour-header-border: color-mix(in srgb, var(--colour-header-bg) 50%, black); + --colour-header-text: var(--light-text); + + /* Navbar */ + --colour-navbar-bg: color-mix(in srgb, var(--colour-base) 50%, black); + --colour-navbar-border: color-mix(in srgb, var(--colour-navbar-bg) 50%, black); + --colour-navbar-text: var(--light-text); + --colour-navbar-active-bg: color-mix(in srgb, var(--colour-navbar-bg) 40%, white); + --colour-navbar-active-text: color-mix(in srgb, var(--colour-navbar-bg) 40%, black); + --colour-navbar-hover-bg: color-mix(in srgb, var(--colour-navbar-bg) 30%, white); + --colour-navbar-hover-text: color-mix(in srgb, var(--colour-navbar-bg) 30%, black); + + /* Input */ + --colour-input-bg: color-mix(in srgb, var(--colour-base) 10%, white); + --colour-input-border: color-mix(in srgb, var(--colour-input-bg) 50%, black); + --colour-input-text: var(--dark-text); + + /* Active channel */ + --colour-active-channel-bg: color-mix(in srgb, var(--colour-base) 25%, white); + + /* MessageRun */ + --colour-message-run-self-bg: color-mix(in srgb, var(--colour-base) 30%, white); + --colour-message-run-self-border: color-mix( + in srgb, + var(--colour-message-run-self-bg) 50%, + black + ); + --colour-message-run-other-bg: color-mix(in srgb, var(--colour-base) 50%, white); + --colour-message-run-other-border: color-mix( + in srgb, + var(--colour-message-run-other-bg) 50%, + black + ); + --colour-message-run-self-text: var(--dark-text); + --colour-message-run-other-text: var(--dark-text); + + --colour-message-run-username-bg: color-mix(in srgb, var(--colour-base) 70%, white); + --colour-message-run-username-border: color-mix(in srgb, var(--colour-base) 50%, black); + --colour-message-run-username-text: color-mix(in srgb, var(--colour-base) 50%, black); + + /* Message */ + --colour-message-hover-bg: color-mix(in srgb, var(--colour-base) 70%, white); + --colour-message-hover-text: var(--dark-text); + + /* Message handle */ + --colour-message-handle-bg: color-mix(in srgb, var(--colour-base) 90%, black); + --colour-message-handle-border: color-mix(in srgb, var(--colour-message-handle-bg) 50%, black); + --colour-message-handle-text: var(--dark-text); +} |
