Commit e15025643a20
tarball3 files changed+168-51
@@ -59,7 +59,7 @@
5959
} from './lib/link-previews';6060
import { notifyTyping, stopTyping } from './lib/typing';6161
import { formatDay } from './lib/format';62
- import { lastSpaceRooms, lineHeight, messageSpacing, navFolded } from './lib/appearance';62
+ import { lastSpaceRooms, lineHeight, messageSpacing, navFolded, railFolded } from './lib/appearance';6363
import SettingsPanel from './lib/SettingsPanel.svelte';6464
import { packsForRoom, parseEncryptionState, parseOwnProfile, parseVerificationState, contentPreview, senderLabel } from './lib/types';6565
import type { MemberSummary, OwnProfile, Person, QuickReaction, ReplyReference, RoomSummary, SendState, TimelineEvent, TimelineItem } from './lib/types';@@ -970,16 +970,33 @@
970970
return 'dot-off';971971
}972972
973
+ let roomMenuOpen = false;974
+ let roomMenuEl: HTMLDivElement | null = null;975
+976
+ function roomMenuAction(action: () => void): void {977
+ roomMenuOpen = false;978
+ action();979
+ }980
+981
+ function closeRoomMenuOutside(ev: PointerEvent): void {982
+ if (roomMenuOpen && !roomMenuEl?.contains(ev.target as Node)) roomMenuOpen = false;983
+ }984
+973985
function onKeydown(ev: KeyboardEvent): void {974986
if (ev.key === 'Escape') {975
- if (dialog) dialog = null;987
+ if (roomMenuOpen) roomMenuOpen = false;988
+ else if (dialog) dialog = null;976989
else if (membersOpen && window.innerWidth < 1100) membersOpen = false;977990
else if (sidebarOpen) sidebarOpen = false;978991
}979992
}980993
</script>981994
982
-<svelte:window on:keydown={onKeydown} on:pointerup={() => (draggingScrollbar = false)} />995
+<svelte:window996
+ on:keydown={onKeydown}997
+ on:pointerdown={closeRoomMenuOutside}998
+ on:pointerup={() => (draggingScrollbar = false)}999
+/>9831000
9841001
{#if !booted}9851002
<div class="splash" role="status" aria-label="Loading Malice">@@ -994,7 +1011,14 @@
9941011
{:else if $sessionUser == null}9951012
<LoginPanel softLogout={$softLogout} on:done={(ev) => afterLogin(ev.detail)} />9961013
{:else}997
- <div class="app" class:nav-folded={$navFolded && selectedRoom} style:--msg-gap={`${$messageSpacing}px`} style:--msg-line-height={$lineHeight}>1014
+ <div1015
+ class="app"1016
+ class:nav-folded={$navFolded && selectedRoom}1017
+ class:rail-folded={$railFolded && selectedRoom}1018
+ class:members-open={selectedRoom && membersOpen}1019
+ style:--msg-gap={`${$messageSpacing}px`}1020
+ style:--msg-line-height={$lineHeight}1021
+ >9981022
<nav class="rail" aria-label="Servers and spaces">9991023
<button10001024
type="button"@@ -1307,13 +1331,31 @@
13071331
</button>13081332
<button13091333
type="button"1310
- class="icon-btn only-desktop"1334
+ class="icon-btn only-desktop pane-toggle"1335
+ aria-pressed={!$railFolded}1336
+ aria-label={$railFolded ? 'Show spaces' : 'Hide spaces'}1337
+ title={$railFolded ? 'Show spaces' : 'Hide spaces'}1338
+ on:click={() => railFolded.update((v) => !v)}1339
+ >1340
+ <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">1341
+ <rect x="3" y="4" width="18" height="16" rx="2" fill="none" stroke="currentColor" stroke-width="2" />1342
+ <circle cx="7" cy="9" r="1.4" fill="currentColor" />1343
+ <circle cx="7" cy="13" r="1.4" fill="currentColor" />1344
+ <circle cx="7" cy="17" r="1.4" fill="currentColor" />1345
+ </svg>1346
+ </button>1347
+ <button1348
+ type="button"1349
+ class="icon-btn only-desktop pane-toggle"13111350
aria-pressed={!$navFolded}1312
- aria-label={$navFolded ? 'Show spaces and rooms' : 'Hide spaces and rooms'}1313
- title={$navFolded ? 'Show spaces and rooms' : 'Hide spaces and rooms'}1351
+ aria-label={$navFolded ? 'Show channels' : 'Hide channels'}1352
+ title={$navFolded ? 'Show channels' : 'Hide channels'}13141353
on:click={() => navFolded.update((v) => !v)}13151354
>1316
- <span aria-hidden="true">{$navFolded ? '»' : '«'}</span>1355
+ <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">1356
+ <rect x="3" y="4" width="18" height="16" rx="2" fill="none" stroke="currentColor" stroke-width="2" />1357
+ <path d="M10 4v16" stroke="currentColor" stroke-width="2" />1358
+ </svg>13171359
</button>13181360
<div class="head-title">13191361
<div class="head-room">@@ -1339,34 +1381,7 @@
13391381
</div>13401382
</div>13411383
<div class="head-actions">1342
- <button1343
- type="button"1344
- class="ghost-btn"1345
- aria-label="Encryption and verification"1346
- title="Encryption and verification"1347
- on:click={() => (dialog = 'security')}1348
- >1349
- 🛡 Security1350
- </button>1351
- <button1352
- type="button"1353
- class="ghost-btn"1354
- aria-pressed={membersOpen}1355
- aria-label={membersOpen ? 'Hide members' : 'Show members'}1356
- on:click={toggleMembers}1357
- >1358
- Members1359
- </button>1360
- {#if selectedRoom.membership === 'Joined'}1361
- <button1362
- type="button"1363
- class="ghost-btn danger"1364
- disabled={leaveBusy}1365
- on:click={() => void leave()}1366
- >1367
- {leaveBusy ? 'Leaving…' : 'Leave'}1368
- </button>1369
- {:else}1384
+ {#if selectedRoom.membership !== 'Joined'}13701385
<button13711386
type="button"13721387
class="primary-btn"@@ -1378,12 +1393,48 @@
13781393
{/if}13791394
<button13801395
type="button"1381
- class="icon-btn only-mobile"1382
- aria-label="Toggle members"1396
+ class="icon-btn"1397
+ aria-pressed={membersOpen}1398
+ aria-label={membersOpen ? 'Hide members' : 'Show members'}1399
+ title="Members"13831400
on:click={toggleMembers}13841401
>1385
- <span aria-hidden="true">ⓘ</span>1402
+ <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">1403
+ <circle cx="12" cy="8" r="4" fill="none" stroke="currentColor" stroke-width="2" />1404
+ <path d="M4 20c0-4 3.6-6 8-6s8 2 8 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />1405
+ </svg>13861406
</button>1407
+ <div class="room-menu-wrap" bind:this={roomMenuEl}>1408
+ <button1409
+ type="button"1410
+ class="icon-btn"1411
+ aria-haspopup="menu"1412
+ aria-expanded={roomMenuOpen}1413
+ aria-label="Room options"1414
+ title="Room options"1415
+ on:click={() => (roomMenuOpen = !roomMenuOpen)}1416
+ >1417
+ <span aria-hidden="true">⋯</span>1418
+ </button>1419
+ {#if roomMenuOpen}1420
+ <div class="room-menu" role="menu">1421
+ <button type="button" role="menuitem" on:click={() => roomMenuAction(() => (dialog = 'security'))}>1422
+ Security1423
+ </button>1424
+ {#if selectedRoom.membership === 'Joined'}1425
+ <button1426
+ type="button"1427
+ role="menuitem"1428
+ class="danger"1429
+ disabled={leaveBusy}1430
+ on:click={() => roomMenuAction(() => void leave())}1431
+ >1432
+ {leaveBusy ? 'Leaving…' : 'Leave room'}1433
+ </button>1434
+ {/if}1435
+ </div>1436
+ {/if}1437
+ </div>13871438
</div>13881439
</header>13891440
@@ -1623,8 +1674,8 @@
16231674
{/if}16241675
</main>16251676
1626
- {#if selectedRoom && membersOpen}1627
- <aside class="members" aria-label={`Members of ${selectedRoom.name}`}>1677
+ {#if selectedRoom}1678
+ <aside class="members" aria-hidden={!membersOpen} aria-label={`Members of ${selectedRoom.name}`}>16281679
<div class="members-head">16291680
<h2>Members</h2>16301681
<button type="button" class="icon-btn" aria-label="Close members" on:click={() => (membersOpen = false)}>@@ -133,15 +133,38 @@ input, textarea { font: inherit; }
133133
}134134
135135
.app {136
+ --rail-w: 68px;137
+ --side-w: 300px;138
+ --members-w: 0px;139
+ --pane-ease: 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);136140
display: grid;137
- grid-template-columns: 68px 300px 1fr;141
+ grid-template-columns: var(--rail-w) var(--side-w) minmax(0, 1fr) var(--members-w);138142
height: 100dvh;139143
overflow: hidden;144
+ transition: grid-template-columns var(--pane-ease);145
+}146
+.app.rail-folded { --rail-w: 0px; }147
+.app.nav-folded { --side-w: 0px; }148
+.app.members-open { --members-w: 260px; }149
+.app > .rail { grid-column: 1; }150
+.app > .sidebar { grid-column: 2; }151
+.app > .chat { grid-column: 3; }152
+.app > .members { grid-column: 4; }153
+.app > .rail, .app > .sidebar, .app > .members {154
+ min-width: 0;155
+ transition: visibility 0s linear 0s;156
+}157
+.app > .rail { overflow-x: hidden; }158
+.app > .sidebar, .app > .members { overflow: hidden; }159
+.app > .sidebar > * { min-width: 300px; }160
+.app > .members > * { min-width: 260px; }161
+.app.rail-folded > .rail, .app.nav-folded > .sidebar, .app:not(.members-open) > .members {162
+ visibility: hidden;163
+ transition: visibility 0s linear 0.22s;164
+}165
+@media (prefers-reduced-motion: reduce) {166
+ .app { --pane-ease: 0s; }140167
}141
-.app:has(.members) { grid-template-columns: 68px 300px 1fr 260px; }142
-.app.nav-folded { grid-template-columns: 1fr; }143
-.app.nav-folded:has(.members) { grid-template-columns: 1fr 260px; }144
-.app.nav-folded > .rail, .app.nav-folded > .sidebar { display: none; }145168
146169
/* ——— Rail ——— */147170
.rail {@@ -397,7 +420,38 @@ input, textarea { font: inherit; }
397420
.dot-online { background: var(--ok); }398421
.dot-busy { background: var(--warn); }399422
.dot-off { background: var(--danger); }400
-.head-actions { display: flex; align-items: center; gap: 8px; }423
+.head-actions { display: flex; align-items: center; gap: 4px; }424
+.head-actions .icon-btn { display: grid; place-items: center; width: 34px; height: 34px; padding: 0; }425
+.head-actions .icon-btn[aria-pressed='true'], .head-actions .icon-btn[aria-expanded='true'] { background: var(--panel-2); color: var(--text); }426
+.room-menu-wrap { position: relative; }427
+.room-menu {428
+ position: absolute;429
+ top: calc(100% + 6px);430
+ right: 0;431
+ z-index: 40;432
+ min-width: 180px;433
+ display: flex;434
+ flex-direction: column;435
+ padding: 4px;436
+ background: var(--panel-2);437
+ border: 1px solid var(--border);438
+ border-radius: 8px;439
+ box-shadow: 0 8px 24px rgb(0 0 0 / 0.35);440
+}441
+.room-menu button {442
+ background: transparent;443
+ border: none;444
+ border-radius: 6px;445
+ color: var(--text);446
+ font: inherit;447
+ font-size: 13.5px;448
+ text-align: left;449
+ padding: 7px 10px;450
+ cursor: pointer;451
+}452
+.room-menu button:hover:not(:disabled) { background: var(--hover); }453
+.room-menu button:disabled { opacity: 0.5; cursor: default; }454
+.room-menu button.danger { color: var(--danger); }401455
402456
.primary-btn {403457
background: var(--accent);@@ -675,15 +729,26 @@ input, textarea { font: inherit; }
675729
.only-mobile { display: none; }676730
677731
@media (max-width: 1100px) {678
- .app, .app:has(.members), .app.nav-folded, .app.nav-folded:has(.members) { grid-template-columns: 60px 1fr; }679
- .app.nav-folded > .rail, .app.nav-folded > .sidebar { display: flex; }732
+ .app {733
+ --rail-w: 60px !important;734
+ --side-w: 0px !important;735
+ --members-w: 0px !important;736
+ transition: none;737
+ }738
+ .app > .rail, .app > .sidebar { visibility: visible !important; }739
+ .app > .sidebar { transition: transform var(--pane-ease); }740
+ .app > .sidebar > *, .app > .members > * { min-width: 0; }741
+ .app > .members { transition: transform var(--pane-ease), visibility 0s linear 0s; }742
+ .app:not(.members-open) > .members {743
+ transform: translateX(100%);744
+ transition: transform var(--pane-ease), visibility 0s linear 0.22s;745
+ }680746
.sidebar {681747
position: fixed;682748
top: 0; bottom: 0; left: 60px;683749
width: min(320px, 85vw);684750
z-index: 40;685751
transform: translateX(calc(-100% - 60px));686
- transition: transform 0.18s ease;687752
box-shadow: 8px 0 32px rgba(0, 0, 0, 0.5);688753
}689754
.sidebar.open { transform: none; }@@ -713,7 +778,7 @@ input, textarea { font: inherit; }
713778
}714779
715780
@media (max-width: 640px) {716
- .app, .app:has(.members), .app.nav-folded, .app.nav-folded:has(.members) { grid-template-columns: 52px 1fr; }781
+ .app { --rail-w: 52px !important; }717782
.sidebar { left: 52px; transform: translateX(calc(-100% - 52px)); }718783
.scrim { left: 52px; }719784
.rail-btn { width: 40px; height: 40px; font-size: 16px; }@@ -27,6 +27,7 @@ const isSpacing = (v: unknown): v is number =>
2727
typeof v === 'number' && Number.isFinite(v) && v >= MESSAGE_SPACING_MIN && v <= MESSAGE_SPACING_MAX;2828
2929
export const navFolded = persisted('malice.navFolded', false, isBool);30
+export const railFolded = persisted('malice.railFolded', false, isBool);3031
export const messageSpacing = persisted('malice.messageSpacing', MESSAGE_SPACING_DEFAULT, isSpacing);3132
3233
export const LINE_HEIGHT_MIN = 1.1;