common components
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from "svelte";
|
||||
import { auth } from "./auth/auth.svelte";
|
||||
import { connectionState } from "./connection.svelte";
|
||||
import { connectionBuilder, getStdbHost, getStdbDbName } from "./config";
|
||||
import InnerSpacetimeDBProvider from "./InnerSpacetimeDBProvider.svelte";
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { TokenStore, getStdbHost, getStdbDbName } from "../config";
|
||||
import SpacetimeProvider from "../SpacetimeProvider.svelte";
|
||||
import ComboBoxInput from "../chat/components/ComboBoxInput.svelte";
|
||||
import Button from "../chat/components/ui/Button.svelte";
|
||||
|
||||
let { children, showServerSettings, onToggleServerSettings: _onToggleServerSettings } = $props<{
|
||||
children: any,
|
||||
@@ -157,19 +158,19 @@
|
||||
{/if}
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 12px; width: 100%">
|
||||
<button
|
||||
<Button
|
||||
onclick={() => {
|
||||
userWantsToConnect = true;
|
||||
auth.signinRedirect();
|
||||
}}
|
||||
disabled={auth.isLoading}
|
||||
class="btn-primary"
|
||||
style="width: 100%"
|
||||
>
|
||||
{auth.isLoading ? "Loading..." : "Login with OIDC"}
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={() => {
|
||||
console.log("AuthGate: Reconnect/Guest clicked. hasStoredToken:", hasStoredToken);
|
||||
userWantsToConnect = true;
|
||||
@@ -178,16 +179,15 @@
|
||||
}
|
||||
_onToggleServerSettings(false);
|
||||
}}
|
||||
class="btn-secondary"
|
||||
style="width: 100%"
|
||||
>
|
||||
{hasStoredToken ? "Reconnect as last user" : "Connect as guest"}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="server-settings-section" style="margin-top: 24px; width: 100%; border-top: 1px solid var(--background-modifier-accent);">
|
||||
<div style="padding-top: 16px; display: flex; flex-direction: column; gap: 16px;">
|
||||
<div class="form-group" style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
|
||||
<label for="stdb-connection" style="font-size: 0.7rem; font-weight: bold; color: var(--text-muted); text-transform: uppercase; text-align: left; width: 100%;">
|
||||
Instance hostname:database
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Modal from "./Modal.svelte";
|
||||
import ModalLayout from "./ui/ModalLayout.svelte";
|
||||
import Button from "./ui/Button.svelte";
|
||||
|
||||
let {
|
||||
title = "Confirm",
|
||||
@@ -18,110 +20,33 @@
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
} = $props();
|
||||
|
||||
function handleCancel() {
|
||||
onCancel();
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
onConfirm();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal onClose={handleCancel}>
|
||||
<div class="confirm-modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>{title}</h2>
|
||||
</div>
|
||||
<Modal onClose={onCancel}>
|
||||
<ModalLayout {title} onClose={onCancel}>
|
||||
<p class="confirm-message">{message}</p>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn-ghost" onclick={handleCancel}>
|
||||
{#snippet footer()}
|
||||
<Button variant="ghost" onclick={onCancel}>
|
||||
{cancelText}
|
||||
</button>
|
||||
<button
|
||||
class="btn-primary {isDanger ? 'danger' : ''}"
|
||||
onclick={handleConfirm}
|
||||
</Button>
|
||||
<Button
|
||||
variant={isDanger ? "danger" : "primary"}
|
||||
onclick={onConfirm}
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
{/snippet}
|
||||
</ModalLayout>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.confirm-modal-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 24px 16px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
color: var(--header-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0 16px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-body p {
|
||||
.confirm-message {
|
||||
margin: 0;
|
||||
color: var(--text-normal);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
background-color: var(--background-secondary);
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 24px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--brand-hover);
|
||||
}
|
||||
|
||||
.btn-primary.danger {
|
||||
background-color: var(--status-danger);
|
||||
}
|
||||
|
||||
.btn-primary.danger:hover {
|
||||
background-color: #d83a3d;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
padding-top: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { portal } from "../../portal";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let {
|
||||
onClose,
|
||||
children,
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
|
||||
<div class="rich-text">
|
||||
<div class="text-content">
|
||||
{#each parts as part, index}
|
||||
{#each parts as part, _index}
|
||||
{#if part.match(urlRegex)}
|
||||
<a href={part} target="_blank" rel="noopener noreferrer" class="url-link">
|
||||
{part}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
import { getContext } from "svelte";
|
||||
import type { ChatService } from "../services/chat.svelte";
|
||||
import Modal from "./Modal.svelte";
|
||||
import ModalLayout from "./ui/ModalLayout.svelte";
|
||||
import Input from "./ui/Input.svelte";
|
||||
import Button from "./ui/Button.svelte";
|
||||
|
||||
const chat = getContext<ChatService>("chat");
|
||||
|
||||
@@ -15,28 +18,14 @@
|
||||
</script>
|
||||
|
||||
<Modal onClose={() => (chat.showDiscoveryModal = false)} maxWidth="500px">
|
||||
<div class="discovery-modal">
|
||||
<div class="modal-header">
|
||||
<h2>Discover Servers</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="close-btn"
|
||||
onclick={() => (chat.showDiscoveryModal = false)}
|
||||
aria-label="Close"
|
||||
>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<input
|
||||
<ModalLayout title="Discover Servers" onClose={() => (chat.showDiscoveryModal = false)}>
|
||||
<div class="discovery-content">
|
||||
<Input
|
||||
id="server-search"
|
||||
name="server-search"
|
||||
type="text"
|
||||
autofocus
|
||||
placeholder="Search for servers..."
|
||||
bind:value={searchTerm}
|
||||
class="search-input"
|
||||
style="margin-bottom: 16px;"
|
||||
/>
|
||||
|
||||
<div class="server-list-container">
|
||||
@@ -53,75 +42,26 @@
|
||||
</div>
|
||||
<span class="server-name">{server.name}</span>
|
||||
</div>
|
||||
<button
|
||||
class="btn-primary"
|
||||
<Button
|
||||
onclick={() => chat.handleJoinServer(server.id)}
|
||||
disabled={!chat.isFullyAuthenticated}
|
||||
size="small"
|
||||
>
|
||||
Join
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalLayout>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.discovery-modal {
|
||||
.discovery-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 24px 16px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
color: var(--header-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--interactive-normal);
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: var(--interactive-hover);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0 16px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
background-color: var(--background-tertiary);
|
||||
color: var(--text-normal);
|
||||
border: 1px solid var(--background-modifier-accent);
|
||||
border-radius: 4px;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
.server-list-container {
|
||||
@@ -170,25 +110,4 @@
|
||||
font-weight: bold;
|
||||
color: var(--header-primary);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--brand);
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background-color: var(--brand-hover);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
import { getContext } from "svelte";
|
||||
import type { ChatService } from "../services/chat.svelte";
|
||||
import { optimizeEmoji } from "../utils";
|
||||
import { portal } from "../../portal";
|
||||
import Button from "./ui/Button.svelte";
|
||||
import Input from "./ui/Input.svelte";
|
||||
import Switch from "./ui/Switch.svelte";
|
||||
|
||||
let { onClose } = $props<{ onClose: () => void }>();
|
||||
|
||||
@@ -111,45 +113,53 @@
|
||||
</button>
|
||||
{/each}
|
||||
<div class="sidebar-separator"></div>
|
||||
<button class="sidebar-item danger" onclick={() => {
|
||||
chat.confirmModal = {
|
||||
title: "Leave Server",
|
||||
message: `Are you sure you want to leave '${server?.name}'? You will need an invite to join again.`,
|
||||
confirmText: "Leave Server",
|
||||
cancelText: "Cancel",
|
||||
isDanger: true,
|
||||
onConfirm: () => {
|
||||
if (chat.activeServerId) {
|
||||
chat.handleLeaveServer(chat.activeServerId);
|
||||
onClose();
|
||||
<Button
|
||||
variant="danger"
|
||||
size="medium"
|
||||
class="sidebar-item-btn"
|
||||
onclick={() => {
|
||||
chat.confirmModal = {
|
||||
title: "Leave Server",
|
||||
message: `Are you sure you want to leave '${server?.name}'? You will need an invite to join again.`,
|
||||
confirmText: "Leave Server",
|
||||
cancelText: "Cancel",
|
||||
isDanger: true,
|
||||
onConfirm: () => {
|
||||
if (chat.activeServerId) {
|
||||
chat.handleLeaveServer(chat.activeServerId);
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}}>
|
||||
};
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
Leave Server
|
||||
</button>
|
||||
<div class="sidebar-separator"></div>
|
||||
<button class="sidebar-item danger" onclick={() => {
|
||||
chat.confirmModal = {
|
||||
title: `Delete '${server?.name}'`,
|
||||
message: `Are you sure you want to delete ${server?.name}? This action is permanent and will delete all channels, messages, and associated data. This cannot be undone.`,
|
||||
confirmText: "Delete Server",
|
||||
cancelText: "Cancel",
|
||||
isDanger: true,
|
||||
onConfirm: async () => {
|
||||
if (server) {
|
||||
chat.handleDeleteServer(server.id);
|
||||
onClose();
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="medium"
|
||||
class="sidebar-item-btn"
|
||||
onclick={() => {
|
||||
chat.confirmModal = {
|
||||
title: `Delete '${server?.name}'`,
|
||||
message: `Are you sure you want to delete ${server?.name}? This action is permanent and will delete all channels, messages, and associated data. This cannot be undone.`,
|
||||
confirmText: "Delete Server",
|
||||
cancelText: "Cancel",
|
||||
isDanger: true,
|
||||
onConfirm: async () => {
|
||||
if (server) {
|
||||
chat.handleDeleteServer(server.id);
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}}>
|
||||
};
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
Delete Server
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</Button>
|
||||
</div>
|
||||
<div class="settings-main">
|
||||
<div class="settings-content">
|
||||
<div class="content-header">
|
||||
@@ -179,7 +189,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
{#if isOwner}
|
||||
<label class="btn-primary" style="margin-top: 12px; font-size: 0.8rem; padding: 6px 12px; cursor: pointer;">
|
||||
<label class="btn primary small" style="margin-top: 12px; cursor: pointer;">
|
||||
Change Avatar
|
||||
<input type="file" accept="image/*" onchange={handleAvatarChange} disabled={isUploading} style="display: none;" />
|
||||
</label>
|
||||
@@ -187,33 +197,20 @@
|
||||
</div>
|
||||
|
||||
<div class="name-edit-section">
|
||||
<div class="form-group">
|
||||
<label for="server-name">Server Name</label>
|
||||
<input
|
||||
id="server-name"
|
||||
type="text"
|
||||
bind:value={serverName}
|
||||
disabled={!isOwner}
|
||||
class="styled-input"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
id="server-name"
|
||||
label="Server Name"
|
||||
bind:value={serverName}
|
||||
disabled={!isOwner || isUploading}
|
||||
/>
|
||||
|
||||
<div class="form-group" style="margin-top: 24px;">
|
||||
<label style="display: flex; align-items: center; justify-content: space-between; cursor: pointer;">
|
||||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<span>Public Server</span>
|
||||
<span style="font-size: 0.75rem; color: var(--text-muted); text-transform: none;">Whether this server is visible in the Discovery tab.</span>
|
||||
</div>
|
||||
<div class="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={isPublic}
|
||||
disabled={!isOwner}
|
||||
/>
|
||||
<span class="slider"></span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<Switch
|
||||
label="Public Server"
|
||||
description="Whether this server is visible in the Discovery tab."
|
||||
bind:checked={isPublic}
|
||||
disabled={!isOwner || isUploading}
|
||||
style="margin-top: 24px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -237,7 +234,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="footer-actions">
|
||||
<button class="btn-ghost" onclick={() => {
|
||||
<Button variant="ghost" onclick={() => {
|
||||
if (server) {
|
||||
serverName = server.name;
|
||||
isPublic = server.public;
|
||||
@@ -245,10 +242,15 @@
|
||||
newAvatarFile = null;
|
||||
errorMessage = null;
|
||||
}
|
||||
}}>Reset</button>
|
||||
<button class="btn-success" onclick={handleSave} disabled={isUploading}>
|
||||
{isUploading ? "Saving..." : "Save Changes"}
|
||||
</button>
|
||||
}}>Reset</Button>
|
||||
<Button
|
||||
variant="success"
|
||||
onclick={handleSave}
|
||||
loading={isUploading}
|
||||
disabled={isUploading}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -352,6 +354,33 @@
|
||||
margin: 8px 10px;
|
||||
}
|
||||
|
||||
:global(.sidebar-item-btn) {
|
||||
margin: 2px 0;
|
||||
justify-content: flex-start !important;
|
||||
text-align: left;
|
||||
width: 100% !important;
|
||||
padding: 6px 10px !important;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
font-size: 1rem !important;
|
||||
color: var(--interactive-normal) !important;
|
||||
transition: all 0.1s !important;
|
||||
}
|
||||
|
||||
:global(.sidebar-item-btn.danger) {
|
||||
color: var(--status-danger) !important;
|
||||
}
|
||||
|
||||
:global(.sidebar-item-btn:hover) {
|
||||
background-color: var(--background-modifier-hover) !important;
|
||||
color: var(--interactive-hover) !important;
|
||||
}
|
||||
|
||||
:global(.sidebar-item-btn.danger:hover) {
|
||||
background-color: var(--status-danger) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.settings-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -427,89 +456,6 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.styled-input {
|
||||
background-color: var(--background-tertiary);
|
||||
border: 1px solid var(--background-modifier-accent);
|
||||
color: var(--text-normal);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.styled-input:focus {
|
||||
border-color: var(--brand);
|
||||
}
|
||||
|
||||
.styled-input:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Toggle Switch */
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #72767d;
|
||||
transition: .2s;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: .2s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: var(--status-positive);
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px var(--status-positive);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(18px);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.settings-footer {
|
||||
position: absolute;
|
||||
@@ -541,35 +487,6 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: var(--status-positive);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 20px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
|
||||
.btn-success:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-success:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Custom Confirmation Modal */
|
||||
.danger-overlay {
|
||||
background-color: rgba(0, 0, 0, 0.85);
|
||||
@@ -606,21 +523,6 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: var(--status-danger);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 24px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from { transform: translateY(100%); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import AudioSettings from "./settings/AudioSettings.svelte";
|
||||
import ScreenSharingSettings from "./settings/ScreenSharingSettings.svelte";
|
||||
import SecuritySettings from "./settings/SecuritySettings.svelte";
|
||||
import Button from "./ui/Button.svelte";
|
||||
|
||||
let { onClose, currentUser }: { onClose: () => void, currentUser: Types.User | undefined } = $props();
|
||||
|
||||
@@ -166,19 +167,24 @@
|
||||
</button>
|
||||
{/each}
|
||||
<div class="sidebar-separator"></div>
|
||||
<button class="sidebar-item danger" onclick={() => {
|
||||
chat.confirmModal = {
|
||||
title: "Log Out",
|
||||
message: "Are you sure you want to log out? You will need to sign in again to access your account.",
|
||||
confirmText: "Log Out",
|
||||
cancelText: "Cancel",
|
||||
isDanger: true,
|
||||
onConfirm: () => auth.logout()
|
||||
};
|
||||
}}>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="medium"
|
||||
class="sidebar-item-btn"
|
||||
onclick={() => {
|
||||
chat.confirmModal = {
|
||||
title: "Log Out",
|
||||
message: "Are you sure you want to log out? You will need to sign in again to access your account.",
|
||||
confirmText: "Log Out",
|
||||
cancelText: "Cancel",
|
||||
isDanger: true,
|
||||
onConfirm: () => auth.logout()
|
||||
};
|
||||
}}
|
||||
>
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
Logout
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="settings-main">
|
||||
@@ -225,7 +231,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="footer-actions">
|
||||
<button class="btn-ghost" onclick={() => {
|
||||
<Button variant="ghost" onclick={() => {
|
||||
localName = currentUser?.name || "";
|
||||
localStatus = currentUser?.status || "";
|
||||
biography = currentUser?.biography || "";
|
||||
@@ -234,8 +240,8 @@
|
||||
newAvatarFile = null;
|
||||
newBannerFile = null;
|
||||
errorMessage = null;
|
||||
}}>Reset</button>
|
||||
<button class="btn-success" onclick={handleSave}>Save Changes</button>
|
||||
}}>Reset</Button>
|
||||
<Button variant="success" onclick={handleSave}>Save Changes</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -321,21 +327,39 @@
|
||||
color: var(--interactive-active);
|
||||
}
|
||||
|
||||
.sidebar-item.danger {
|
||||
color: var(--status-danger);
|
||||
}
|
||||
|
||||
.sidebar-item.danger:hover {
|
||||
background-color: var(--status-danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar-separator {
|
||||
height: 1px;
|
||||
background-color: var(--background-modifier-accent);
|
||||
margin: 8px 10px;
|
||||
}
|
||||
|
||||
:global(.sidebar-item-btn) {
|
||||
margin: 2px 0;
|
||||
justify-content: flex-start !important;
|
||||
text-align: left;
|
||||
width: 100% !important;
|
||||
padding: 6px 10px !important;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
font-size: 1rem !important;
|
||||
color: var(--interactive-normal) !important;
|
||||
transition: all 0.1s !important;
|
||||
}
|
||||
|
||||
:global(.sidebar-item-btn.danger) {
|
||||
color: var(--status-danger) !important;
|
||||
}
|
||||
|
||||
:global(.sidebar-item-btn:hover) {
|
||||
background-color: var(--background-modifier-hover) !important;
|
||||
color: var(--interactive-hover) !important;
|
||||
}
|
||||
|
||||
:global(.sidebar-item-btn.danger:hover) {
|
||||
background-color: var(--status-danger) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.settings-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -404,30 +428,6 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: var(--status-positive);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 20px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from { transform: translateY(100%); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { getContext } from "svelte";
|
||||
import type { ChatService } from "../../services/chat.svelte";
|
||||
import type * as Types from "../../../module_bindings/types";
|
||||
import Input from "../ui/Input.svelte";
|
||||
import Button from "../ui/Button.svelte";
|
||||
|
||||
let {
|
||||
localName = $bindable(),
|
||||
@@ -86,58 +88,54 @@
|
||||
</div>
|
||||
|
||||
<div class="account-edit-box">
|
||||
<div class="form-group">
|
||||
<label for="display-name">Display Name</label>
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
id="display-name"
|
||||
type="text"
|
||||
bind:value={localName}
|
||||
placeholder="Enter your display name"
|
||||
/>
|
||||
<button class="btn-clear" onclick={() => localName = ""} aria-label="Clear name">
|
||||
<div class="edit-group">
|
||||
<Input
|
||||
id="display-name"
|
||||
label="Display Name"
|
||||
bind:value={localName}
|
||||
placeholder="Enter your display name"
|
||||
/>
|
||||
{#if localName}
|
||||
<Button variant="clear" class="clear-btn" onclick={() => localName = ""} title="Clear name">
|
||||
<i class="fas fa-times-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="user-status">Status</label>
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
id="user-status"
|
||||
type="text"
|
||||
bind:value={localStatus}
|
||||
placeholder="Set a status"
|
||||
maxlength="128"
|
||||
/>
|
||||
<button class="btn-clear" onclick={() => localStatus = ""} aria-label="Clear status">
|
||||
<div class="edit-group">
|
||||
<Input
|
||||
id="user-status"
|
||||
label="Status"
|
||||
bind:value={localStatus}
|
||||
placeholder="Set a status"
|
||||
maxlength="128"
|
||||
/>
|
||||
{#if localStatus}
|
||||
<Button variant="clear" class="clear-btn" onclick={() => localStatus = ""} title="Clear status">
|
||||
<i class="fas fa-times-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="biography">Biography</label>
|
||||
<textarea
|
||||
id="biography"
|
||||
bind:value={biography}
|
||||
placeholder="Tell us about yourself..."
|
||||
rows="4"
|
||||
maxlength="200"
|
||||
></textarea>
|
||||
<div class="textarea-footer">{biography.length}/200</div>
|
||||
</div>
|
||||
<Input
|
||||
id="biography"
|
||||
type="textarea"
|
||||
label="Biography"
|
||||
bind:value={biography}
|
||||
placeholder="Tell us about yourself..."
|
||||
description={`${biography.length}/200`}
|
||||
class="bio-input"
|
||||
/>
|
||||
|
||||
<div class="avatar-actions">
|
||||
<button class="btn-secondary small" onclick={() => {
|
||||
<Button variant="secondary" size="small" onclick={() => {
|
||||
avatarPreview = null;
|
||||
newAvatarFile = null;
|
||||
}}>Remove Avatar</button>
|
||||
<button class="btn-secondary small" onclick={() => {
|
||||
}}>Remove Avatar</Button>
|
||||
<Button variant="secondary" size="small" onclick={() => {
|
||||
bannerPreview = null;
|
||||
newBannerFile = null;
|
||||
}}>Remove Banner</button>
|
||||
}}>Remove Banner</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,58 +267,34 @@
|
||||
margin: 0 16px 16px 16px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
.edit-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--background-primary);
|
||||
border-radius: 4px;
|
||||
padding-right: 8px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-group input[type="text"], .form-group textarea {
|
||||
padding: 10px;
|
||||
background-color: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
border: none;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
border-radius: 4px;
|
||||
font-family: inherit;
|
||||
:global(.clear-btn) {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 32px; /* Position it correctly relative to the label and input */
|
||||
padding: 4px !important;
|
||||
font-size: 1.1rem !important;
|
||||
}
|
||||
|
||||
:global(.edit-group .form-group) {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.input-wrapper input[type="text"] {
|
||||
flex: 1;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.textarea-footer {
|
||||
text-align: right;
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-muted);
|
||||
:global(.bio-input textarea) {
|
||||
height: 100px;
|
||||
resize: none !important;
|
||||
}
|
||||
|
||||
.avatar-actions {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { getContext } from "svelte";
|
||||
import type { WebRTCService } from "../../services/webrtc/webrtc.svelte";
|
||||
import Dropdown from "../Dropdown.svelte";
|
||||
import Button from "../ui/Button.svelte";
|
||||
|
||||
const webrtc = getContext<WebRTCService>("webrtc");
|
||||
|
||||
@@ -35,13 +36,14 @@
|
||||
<div class="voice-sensitivity-box">
|
||||
<div class="label-with-action">
|
||||
<label for="voice-threshold-slider">Input Sensitivity</label>
|
||||
<button
|
||||
class="test-mic-btn {webrtc.isTestingMic ? 'active' : ''}"
|
||||
<Button
|
||||
variant={webrtc.isTestingMic ? "danger" : "primary"}
|
||||
size="small"
|
||||
onclick={() => webrtc.toggleMicTest()}
|
||||
>
|
||||
<i class="fas {webrtc.isTestingMic ? 'fa-stop' : 'fa-microphone'}"></i>
|
||||
{webrtc.isTestingMic ? "Stop Testing" : "Test Mic"}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="voice-meter-container">
|
||||
@@ -144,22 +146,6 @@
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.test-mic-btn {
|
||||
background-color: var(--brand);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s;
|
||||
}
|
||||
|
||||
.test-mic-btn.active {
|
||||
background-color: var(--status-danger);
|
||||
}
|
||||
|
||||
.threshold-slider {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { getContext } from "svelte";
|
||||
import type { ChatService } from "../../services/chat.svelte";
|
||||
import { optimizeEmoji, getCustomEmojiUrl } from "../../utils";
|
||||
import Button from "../ui/Button.svelte";
|
||||
import Input from "../ui/Input.svelte";
|
||||
|
||||
const chat = getContext<ChatService>("chat");
|
||||
|
||||
@@ -138,20 +140,22 @@
|
||||
<div class="emoji-upload-form-box">
|
||||
<form class="emoji-upload-form" onsubmit={handleEmojiSubmit}>
|
||||
<div class="form-row">
|
||||
<div class="form-group" style="flex: 1; margin-bottom: 0;">
|
||||
<label for="emoji-name">Emoji Name</label>
|
||||
<div class="input-wrapper">
|
||||
<span class="emoji-prefix">:</span>
|
||||
<input
|
||||
id="emoji-name"
|
||||
type="text"
|
||||
bind:value={newEmojiName}
|
||||
placeholder="emoji-name"
|
||||
/>
|
||||
<span class="emoji-suffix">:</span>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<Input
|
||||
id="emoji-name"
|
||||
label="Emoji Name"
|
||||
bind:value={newEmojiName}
|
||||
placeholder="emoji-name"
|
||||
>
|
||||
{#snippet prefix()}
|
||||
<span class="emoji-prefix">:</span>
|
||||
{/snippet}
|
||||
{#snippet suffix()}
|
||||
<span class="emoji-suffix">:</span>
|
||||
{/snippet}
|
||||
</Input>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<div class="form-group">
|
||||
<label>Emoji Image</label>
|
||||
<label class="custom-file-upload">
|
||||
<i class="fas fa-image"></i>
|
||||
@@ -159,14 +163,15 @@
|
||||
<input type="file" accept="image/*" onchange={onEmojiFileChange} style="display: none;" />
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
class="btn-success"
|
||||
variant="success"
|
||||
style="align-self: flex-end; height: 38px; padding: 0 20px;"
|
||||
disabled={!newEmojiFile || !newEmojiName.trim() || isEmojiUploading}
|
||||
loading={isEmojiUploading}
|
||||
>
|
||||
{isEmojiUploading ? "Uploading..." : "Upload"}
|
||||
</button>
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
{#if emojiError}
|
||||
<div class="form-error" style="color: var(--status-danger); font-size: 0.75rem; margin-top: 8px;">
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
import type { ChatService } from "../../services/chat.svelte";
|
||||
import Button from "../ui/Button.svelte";
|
||||
import Input from "../ui/Input.svelte";
|
||||
|
||||
const chat = getContext<ChatService>("chat");
|
||||
|
||||
@@ -51,9 +53,9 @@
|
||||
<label>Your Public Key</label>
|
||||
<div class="key-display">
|
||||
<pre>{chat.myPublicKey}</pre>
|
||||
<button class="btn-secondary btn-small" onclick={copyPublicKey}>
|
||||
<Button variant="secondary" size="small" class="copy-btn" onclick={copyPublicKey}>
|
||||
<i class="far fa-copy"></i> Copy
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<p class="help-text">Share this key with others so they can send you encrypted messages (automatically handled by Zep).</p>
|
||||
</div>
|
||||
@@ -61,9 +63,9 @@
|
||||
<div class="danger-zone">
|
||||
<h4>Danger Zone</h4>
|
||||
<p>Generating a new key will prevent you from reading old encrypted messages.</p>
|
||||
<button class="btn-danger" onclick={() => chat.generateEncryptionKey(name, email)} disabled={isGenerating}>
|
||||
<Button variant="danger" onclick={() => chat.generateEncryptionKey(name, email)} loading={isGenerating}>
|
||||
Regenerate Keys
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="key-status-card missing">
|
||||
@@ -78,21 +80,29 @@
|
||||
|
||||
<div class="setup-form shadow-box">
|
||||
<h4>Generate New Keypair</h4>
|
||||
<div class="form-group">
|
||||
<label for="key-name">Full Name</label>
|
||||
<input id="key-name" type="text" bind:value={name} placeholder="e.g. Alice Smith" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="key-email">Email Address</label>
|
||||
<input id="key-email" type="email" bind:value={email} placeholder="alice@example.com" />
|
||||
</div>
|
||||
<button class="btn-primary" onclick={handleGenerate} disabled={isGenerating || !name || !email}>
|
||||
{#if isGenerating}
|
||||
<i class="fas fa-spinner fa-spin"></i> Generating...
|
||||
{:else}
|
||||
Generate Keys
|
||||
{/if}
|
||||
</button>
|
||||
<Input
|
||||
id="key-name"
|
||||
label="Full Name"
|
||||
bind:value={name}
|
||||
placeholder="e.g. Alice Smith"
|
||||
style="margin-bottom: 16px;"
|
||||
/>
|
||||
<Input
|
||||
id="key-email"
|
||||
label="Email Address"
|
||||
type="email"
|
||||
bind:value={email}
|
||||
placeholder="alice@example.com"
|
||||
style="margin-bottom: 24px;"
|
||||
/>
|
||||
<Button
|
||||
onclick={handleGenerate}
|
||||
disabled={isGenerating || !name || !email}
|
||||
loading={isGenerating}
|
||||
style="width: 100%;"
|
||||
>
|
||||
Generate Keys
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -178,16 +188,6 @@
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: var(--background-tertiary);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: var(--text-normal);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.key-display {
|
||||
position: relative;
|
||||
background-color: var(--background-secondary-alt);
|
||||
@@ -207,12 +207,10 @@
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.key-display .btn-small {
|
||||
:global(.copy-btn) {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
children,
|
||||
onclick,
|
||||
type = "button",
|
||||
variant = "primary",
|
||||
size = "medium",
|
||||
disabled = false,
|
||||
loading = false,
|
||||
class: className = "",
|
||||
style = "",
|
||||
title = ""
|
||||
}: {
|
||||
children?: any,
|
||||
onclick?: (e: MouseEvent) => void,
|
||||
type?: "button" | "submit" | "reset",
|
||||
variant?: "primary" | "secondary" | "ghost" | "danger" | "success" | "clear",
|
||||
size?: "small" | "medium" | "large",
|
||||
disabled?: boolean,
|
||||
loading?: boolean,
|
||||
class?: string,
|
||||
style?: string,
|
||||
title?: string
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
{type}
|
||||
{onclick}
|
||||
{disabled}
|
||||
{title}
|
||||
class="btn {variant} {size} {className}"
|
||||
{style}
|
||||
>
|
||||
{#if loading}
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
{:else if children}
|
||||
{@render children()}
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-family: inherit;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Variants */
|
||||
.primary {
|
||||
background-color: var(--brand);
|
||||
color: white;
|
||||
}
|
||||
.primary:hover:not(:disabled) {
|
||||
background-color: var(--brand-hover);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background-color: var(--background-secondary);
|
||||
color: var(--text-normal);
|
||||
border: 1px solid var(--background-modifier-accent);
|
||||
}
|
||||
.secondary:hover:not(:disabled) {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.ghost {
|
||||
background: transparent;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
.ghost:hover:not(:disabled) {
|
||||
background-color: var(--background-modifier-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.danger {
|
||||
background-color: var(--status-danger);
|
||||
color: white;
|
||||
}
|
||||
.danger:hover:not(:disabled) {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: var(--status-positive);
|
||||
color: white;
|
||||
}
|
||||
.success:hover:not(:disabled) {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
.clear {
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
padding: 0;
|
||||
}
|
||||
.clear:hover:not(:disabled) {
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
/* Sizes */
|
||||
.small {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.medium {
|
||||
padding: 10px 24px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.large {
|
||||
padding: 12px 32px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,173 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
value = $bindable(),
|
||||
label,
|
||||
id,
|
||||
type = "text",
|
||||
placeholder = "",
|
||||
disabled = false,
|
||||
description,
|
||||
error,
|
||||
autofocus = false,
|
||||
prefix,
|
||||
suffix,
|
||||
class: className = "",
|
||||
style = ""
|
||||
}: {
|
||||
value: string,
|
||||
label?: string,
|
||||
id?: string,
|
||||
type?: string,
|
||||
placeholder?: string,
|
||||
disabled?: boolean,
|
||||
description?: string,
|
||||
error?: string | null,
|
||||
autofocus?: boolean,
|
||||
prefix?: any,
|
||||
suffix?: any,
|
||||
class?: string,
|
||||
style?: string
|
||||
} = $props();
|
||||
|
||||
function handleAutofocus(node: HTMLInputElement | HTMLTextAreaElement) {
|
||||
if (autofocus) {
|
||||
node.focus();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="form-group {className}" {style}>
|
||||
{#if label}
|
||||
<label for={id}>
|
||||
{label}
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
<div class="input-container {error ? 'error' : ''} {disabled ? 'disabled' : ''}">
|
||||
{#if prefix}
|
||||
<div class="prefix">
|
||||
{@render prefix()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if type === "textarea"}
|
||||
<textarea
|
||||
{id}
|
||||
bind:value
|
||||
{placeholder}
|
||||
{disabled}
|
||||
use:handleAutofocus
|
||||
class="styled-input"
|
||||
></textarea>
|
||||
{:else}
|
||||
<input
|
||||
{id}
|
||||
{type}
|
||||
bind:value
|
||||
{placeholder}
|
||||
{disabled}
|
||||
use:handleAutofocus
|
||||
class="styled-input"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if suffix}
|
||||
<div class="suffix">
|
||||
{@render suffix()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if description && !error}
|
||||
<p class="description">{description}</p>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<p class="error-message">{error}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--background-tertiary);
|
||||
border: 1px solid var(--background-modifier-accent);
|
||||
border-radius: 4px;
|
||||
transition: border-color 0.2s;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.input-container:focus-within {
|
||||
border-color: var(--brand);
|
||||
}
|
||||
|
||||
.input-container.error {
|
||||
border-color: var(--status-danger);
|
||||
}
|
||||
|
||||
.input-container.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.styled-input {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-normal);
|
||||
padding: 10px;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.prefix, .suffix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
color: var(--text-muted);
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.prefix {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.suffix {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
textarea.styled-input {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 0.75rem;
|
||||
color: var(--status-danger);
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
title,
|
||||
onClose,
|
||||
children,
|
||||
footer,
|
||||
maxWidth = "440px",
|
||||
class: className = ""
|
||||
}: {
|
||||
title?: string,
|
||||
onClose?: () => void,
|
||||
children: any,
|
||||
footer?: any,
|
||||
maxWidth?: string,
|
||||
class?: string
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="modal-layout {className}" style="max-width: {maxWidth};">
|
||||
{#if title || onClose}
|
||||
<div class="modal-header">
|
||||
{#if title}
|
||||
<h2>{title}</h2>
|
||||
{/if}
|
||||
{#if onClose}
|
||||
<button class="close-btn" onclick={onClose} aria-label="Close modal">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="modal-body">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
{#if footer}
|
||||
<div class="modal-footer">
|
||||
{@render footer()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.modal-layout {
|
||||
width: 100%;
|
||||
background-color: var(--background-primary);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--elevation-high);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 24px 16px 16px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
color: var(--header-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--interactive-normal);
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: var(--interactive-hover);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0 16px 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
background-color: var(--background-secondary);
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,123 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
checked = $bindable(),
|
||||
label,
|
||||
description,
|
||||
disabled = false,
|
||||
class: className = "",
|
||||
style = ""
|
||||
}: {
|
||||
checked: boolean,
|
||||
label?: string,
|
||||
description?: string,
|
||||
disabled?: boolean,
|
||||
class?: string,
|
||||
style?: string
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="switch-container {className}" {style}>
|
||||
<label class="switch-label-wrapper">
|
||||
{#if label || description}
|
||||
<div class="label-text">
|
||||
{#if label}
|
||||
<span class="label">{label}</span>
|
||||
{/if}
|
||||
{#if description}
|
||||
<span class="description">{description}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked
|
||||
{disabled}
|
||||
/>
|
||||
<span class="slider"></span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.switch-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.switch-label-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 1rem;
|
||||
color: var(--header-primary);
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #72767d;
|
||||
transition: .2s;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: .2s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: var(--status-positive);
|
||||
}
|
||||
|
||||
input:disabled + .slider {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(18px);
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Identity } from "spacetimedb";
|
||||
import { SvelteMap, SvelteSet } from "svelte/reactivity";
|
||||
import { SvelteSet } from "svelte/reactivity";
|
||||
import * as Types from "../../module_bindings/types";
|
||||
import { getUsername, formatTime } from "../utils";
|
||||
import { getConnection } from "../../config";
|
||||
import { DatabaseService } from "./database.svelte";
|
||||
import { NavigationService } from "./navigation.svelte";
|
||||
import { ThemeService, themeService } from "./theme.svelte";
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { tables } from "../../module_bindings";
|
||||
import { useTable } from "spacetimedb/svelte";
|
||||
import * as Types from "../../module_bindings/types";
|
||||
import { getConnection } from "../../config";
|
||||
import { untrack } from "svelte";
|
||||
import type { Identity } from "spacetimedb";
|
||||
|
||||
export class DatabaseService {
|
||||
@@ -75,7 +73,7 @@ export class DatabaseService {
|
||||
return map;
|
||||
});
|
||||
|
||||
constructor(identity: () => Identity | null) {
|
||||
constructor(_identity: () => Identity | null) {
|
||||
const [serversStore, serversReadyStore] = useTable(tables.visible_servers);
|
||||
const [channelsStore, channelsReadyStore] = useTable(tables.visible_channels);
|
||||
const [directMessagesStore] = useTable(tables.visible_direct_messages);
|
||||
|
||||
Reference in New Issue
Block a user