summaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2025-01-11 13:34:40 -0500
committerOwen Jacobson <owen@grimoire.ca>2025-01-11 13:34:40 -0500
commitea0392a2bb12c158f9167105752f8fa315cff47d (patch)
treedec4545ccbe44c8e2baf6e633308359f40ac610a /ui
parent4e3ad13aca163e733724b205c250bdb67cc56c29 (diff)
parent6d51f8568e337e768505ccfdef916b84dd6eb1b3 (diff)
Merge branch 'prop/stylize'
Diffstat (limited to 'ui')
-rw-r--r--ui/app.css63
-rw-r--r--ui/app.html4
-rw-r--r--ui/lib/components/ActiveChannel.svelte4
-rw-r--r--ui/lib/components/ChangePassword.svelte14
-rw-r--r--ui/lib/components/Channel.svelte14
-rw-r--r--ui/lib/components/CreateChannelForm.svelte15
-rw-r--r--ui/lib/components/Invite.svelte10
-rw-r--r--ui/lib/components/Invites.svelte8
-rw-r--r--ui/lib/components/LogIn.svelte20
-rw-r--r--ui/lib/components/LogOut.svelte4
-rw-r--r--ui/lib/components/Message.svelte33
-rw-r--r--ui/lib/components/MessageInput.svelte16
-rw-r--r--ui/lib/components/MessageRun.svelte12
-rw-r--r--ui/lib/store/channels.svelte.js9
-rw-r--r--ui/lib/store/messages.svelte.js2
-rw-r--r--ui/routes/(app)/+layout.svelte70
-rw-r--r--ui/routes/(app)/+page.svelte3
-rw-r--r--ui/routes/(app)/ch/[channel]/+page.svelte25
-rw-r--r--ui/routes/(app)/me/+page.svelte16
-rw-r--r--ui/routes/(login)/invite/[invite]/+page.svelte4
-rw-r--r--ui/routes/+layout.svelte19
-rw-r--r--ui/static/manifest.json2
-rw-r--r--ui/styles/active-channel.css6
-rw-r--r--ui/styles/app-bar.css40
-rw-r--r--ui/styles/app-layout.css56
-rw-r--r--ui/styles/forms.css21
-rw-r--r--ui/styles/invites.css11
-rw-r--r--ui/styles/messages.css121
-rw-r--r--ui/styles/overscroll.css9
-rw-r--r--ui/styles/reset.css129
-rw-r--r--ui/styles/sidebar.css85
-rw-r--r--ui/styles/textarea.css31
-rw-r--r--ui/styles/variables.css72
33 files changed, 727 insertions, 221 deletions
diff --git a/ui/app.css b/ui/app.css
index b316dea..a02106d 100644
--- a/ui/app.css
+++ b/ui/app.css
@@ -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">&#x2795;</button>
+<form onsubmit={handleSubmit}>
+ <input type="text" placeholder="create channel" bind:value={name} {disabled} />
+ <button type="submit">&#x2795;</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}>&#x1F5D1;&#xFE0F;</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">&raquo;</button
- >
+<form bind:this={form} onsubmit={onSubmit}>
+ <textarea onkeydown={onKeyDown} bind:value {disabled} type="search"></textarea>
+ <button type="submit">&raquo;</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&hellip;</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);
+}