AI pass on redesign
This commit is contained in:
@@ -127,7 +127,7 @@
|
||||
</div>
|
||||
<Button
|
||||
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
|
||||
class="center tooltip tooltip-left absolute right-4 h-10 w-10 min-w-10 rounded-full"
|
||||
class="center tooltip tooltip-left bg-primary text-primary-content absolute top-[7px] right-[7px] h-11 w-11 min-w-11 scale-90 rounded-full transition-transform motion-safe:hover:scale-100"
|
||||
disabled={$uploading || disabled}
|
||||
onclick={submit}>
|
||||
<Icon icon={Plane} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {type Instance} from "tippy.js"
|
||||
import {hash, formatTimestampAsTime} from "@welshman/lib"
|
||||
import {formatTimestampAsTime} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {thunks, mergeThunks, pubkey, deriveProfileDisplay, sendWrapped} from "@welshman/app"
|
||||
import {isMobile} from "@lib/html"
|
||||
@@ -16,7 +16,7 @@
|
||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||
import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte"
|
||||
import ChatMessageMenuMobile from "@app/components/ChatMessageMenuMobile.svelte"
|
||||
import {colors} from "@app/theme"
|
||||
import {getColor} from "@app/theme"
|
||||
import {makeDelete} from "@app/deletes"
|
||||
import {makeReaction} from "@app/reactions"
|
||||
import {pushModal} from "@app/modal"
|
||||
@@ -35,7 +35,7 @@
|
||||
const isOwn = event.pubkey === $pubkey
|
||||
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
||||
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
|
||||
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
||||
const colorValue = getColor(event.pubkey)
|
||||
|
||||
const reply = () => replyTo(event)
|
||||
const edit = canEdit?.(event) ? () => onEdit?.(event) : undefined
|
||||
@@ -101,7 +101,9 @@
|
||||
{/if}
|
||||
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
|
||||
<TapTarget
|
||||
class="bg-alt chat-bubble mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl min-w-[100px]"
|
||||
class="chat-bubble shadow-soft mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl min-w-[100px] {isOwn
|
||||
? 'bg-primary text-primary-content'
|
||||
: 'bg-base-100'}"
|
||||
onTap={showMobileMenu}>
|
||||
{#if showPubkey}
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -109,7 +111,7 @@
|
||||
<Button onclick={openProfile} class="flex items-center gap-1">
|
||||
<ProfileCircle
|
||||
pubkey={event.pubkey}
|
||||
class="border border-solid border-base-content"
|
||||
style="box-shadow: 0 0 0 1.5px {colorValue}"
|
||||
size={4} />
|
||||
<div class="flex items-center gap-2">
|
||||
<Button onclick={openProfile} class="text-sm font-bold" style="color: {colorValue}">
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
import {fly} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
|
||||
type Props = {
|
||||
icon?: string
|
||||
title: string
|
||||
children?: Snippet
|
||||
action?: Snippet
|
||||
}
|
||||
|
||||
const {icon, title, children, action}: Props = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="m-auto flex max-w-sm flex-col items-center gap-3 px-4 py-12 text-center"
|
||||
in:fly={{y: 16}}>
|
||||
{#if icon}
|
||||
<div class="bg-primary/10 text-primary center size-16 rounded-full motion-safe:animate-float">
|
||||
<Icon {icon} size={8} />
|
||||
</div>
|
||||
{/if}
|
||||
<h3 class="font-display text-xl font-bold tracking-tight">{title}</h3>
|
||||
{#if children}
|
||||
<p class="text-sm opacity-70">{@render children?.()}</p>
|
||||
{/if}
|
||||
{#if action}
|
||||
<div class="mt-1">{@render action?.()}</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -80,8 +80,9 @@
|
||||
for={id}
|
||||
aria-label="Drag and drop files here."
|
||||
style="background-image: url({url});"
|
||||
class="relative flex h-24 w-24 shrink-0 cursor-pointer items-center justify-center rounded-full border-2 border-solid border-base-content bg-base-300 bg-cover bg-center transition-all"
|
||||
class="avatar-blob relative flex h-24 w-24 shrink-0 cursor-pointer items-center justify-center border-2 border-dashed border-primary/40 bg-base-300 bg-cover bg-center transition-all motion-safe:hover:rotate-1 motion-safe:hover:scale-[1.02]"
|
||||
class:transparent={!url}
|
||||
class:border-solid={url || active}
|
||||
class:border-primary={active}
|
||||
ondragenter={stopPropagation(preventDefault(onDragEnter))}
|
||||
ondragover={stopPropagation(preventDefault(onDragOver))}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import CardButton from "@lib/components/CardButton.svelte"
|
||||
import LogIn from "@app/components/LogIn.svelte"
|
||||
import SignUp from "@app/components/SignUp.svelte"
|
||||
import {PLATFORM_TERMS, PLATFORM_PRIVACY, PLATFORM_NAME} from "@app/env"
|
||||
import {PLATFORM_TERMS, PLATFORM_PRIVACY, PLATFORM_NAME, PLATFORM_LOGO} from "@app/env"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
const logIn = () => pushModal(LogIn)
|
||||
@@ -19,9 +19,15 @@
|
||||
|
||||
<Modal>
|
||||
<ModalBody>
|
||||
<div class="py-2">
|
||||
<h1 class="heading">Welcome to {PLATFORM_NAME}!</h1>
|
||||
<p class="text-center">The chat app built for self-hosted communities.</p>
|
||||
<div class="flex flex-col items-center gap-3 py-2">
|
||||
<img
|
||||
src={PLATFORM_LOGO}
|
||||
alt={PLATFORM_NAME}
|
||||
class="shadow-soft ring-primary/20 size-16 rounded-2xl object-cover ring-4 motion-safe:animate-float" />
|
||||
<h1 class="heading">Welcome to <span class="brand">{PLATFORM_NAME}</span>!</h1>
|
||||
<p class="max-w-sm text-center opacity-80">
|
||||
A cozy home for your community — chat, connect, and own your little corner of the internet.
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={logIn}>
|
||||
<CardButton class="btn-primary">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {removeUndefined} from "@welshman/lib"
|
||||
import {deriveProfile} from "@welshman/app"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
import {deriveProfile, deriveProfileDisplay} from "@welshman/app"
|
||||
import {getColor, getBlobVariant} from "@app/theme"
|
||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
|
||||
type Props = {
|
||||
@@ -10,15 +10,39 @@
|
||||
class?: string
|
||||
size?: number
|
||||
url?: string
|
||||
shape?: "blob" | "circle"
|
||||
style?: string
|
||||
}
|
||||
|
||||
const {pubkey, url, size = 7, ...props}: Props = $props()
|
||||
const {pubkey, url, size = 7, shape = "blob", style = "", ...props}: Props = $props()
|
||||
|
||||
const profile = deriveProfile(pubkey, removeUndefined([url]))
|
||||
const display = deriveProfileDisplay(pubkey)
|
||||
|
||||
// Organic, hand-drawn-feeling mask. The variant is stable per pubkey so a
|
||||
// person's silhouette never changes; `shape="circle"` opts back into a disc.
|
||||
const shapeClass =
|
||||
shape === "circle"
|
||||
? "rounded-full"
|
||||
: ["avatar-blob", "avatar-blob-2", "avatar-blob-3"][getBlobVariant(pubkey) - 1]
|
||||
|
||||
const color = getColor(pubkey)
|
||||
const px = $derived(size * 4)
|
||||
const initial = $derived([...($display || "")].find(c => c.trim()) || "?")
|
||||
</script>
|
||||
|
||||
<ImageIcon
|
||||
{size}
|
||||
alt=""
|
||||
class={cx(props.class, "rounded-full")}
|
||||
src={$profile?.picture || UserRounded} />
|
||||
{#if $profile?.picture}
|
||||
<ImageIcon {size} alt="" {style} class={cx(props.class, shapeClass)} src={$profile.picture} />
|
||||
{:else}
|
||||
<!-- Fallback: a subtle gradient derived from the pubkey + the person's initial. -->
|
||||
<div
|
||||
class={cx(
|
||||
props.class,
|
||||
shapeClass,
|
||||
"font-display flex shrink-0 items-center justify-center font-bold text-white uppercase select-none",
|
||||
)}
|
||||
style="width:{px}px;height:{px}px;font-size:{px *
|
||||
0.45}px;background-image:linear-gradient(135deg,{color},color-mix(in oklab,{color},#000 28%));{style}">
|
||||
{initial}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -14,12 +14,14 @@
|
||||
const {pubkeys, size = 7, limit, class: className}: Props = $props()
|
||||
const effectiveLimit = $derived(limit ?? (isMobile ? 7 : 10))
|
||||
|
||||
// circle is one step smaller than box so the bg-base-100 wrapper reads as a
|
||||
// thin separating ring between overlapping avatars (Discord-style stack).
|
||||
const dimensions = $derived(
|
||||
size <= 5
|
||||
? {box: "h-5 w-5", overlap: "-mr-2", overflow: "text-[9px]"}
|
||||
? {box: "h-5 w-5", circle: 4, overlap: "-mr-2", overflow: "text-[9px]"}
|
||||
: size <= 6
|
||||
? {box: "h-6 w-6", overlap: "-mr-2.5", overflow: "text-[10px]"}
|
||||
: {box: "h-8 w-8", overlap: "-mr-3", overflow: "text-xs"},
|
||||
? {box: "h-6 w-6", circle: 5, overlap: "-mr-2.5", overflow: "text-[10px]"}
|
||||
: {box: "h-8 w-8", circle: 7, overlap: "-mr-3", overflow: "text-xs"},
|
||||
)
|
||||
|
||||
for (const pubkey of pubkeys) {
|
||||
@@ -37,20 +39,21 @@
|
||||
</script>
|
||||
|
||||
<div class={cx("flex", size <= 5 ? "pr-2" : "pr-3", className)}>
|
||||
{#each displayPubkeys as pubkey (pubkey)}
|
||||
{#each displayPubkeys as pubkey, i (pubkey)}
|
||||
<div
|
||||
class={cx(
|
||||
"z-feature inline-block flex items-center justify-center rounded-full bg-base-100",
|
||||
"z-feature inline-flex items-center justify-center rounded-full bg-base-100 transition-transform",
|
||||
dimensions.box,
|
||||
dimensions.overlap,
|
||||
i % 2 === 0 ? "rotate-2" : "-rotate-2",
|
||||
)}>
|
||||
<ProfileCircle class={cx(dimensions.box, "bg-base-300")} {pubkey} {size} />
|
||||
<ProfileCircle class="bg-base-300" shape="circle" {pubkey} size={dimensions.circle} />
|
||||
</div>
|
||||
{/each}
|
||||
{#if overflowCount > 0}
|
||||
<div
|
||||
class={cx(
|
||||
"z-feature inline-flex items-center justify-center rounded-full bg-neutral font-medium text-neutral-content",
|
||||
"z-feature bg-primary text-primary-content shadow-soft font-display inline-flex rotate-2 items-center justify-center rounded-full font-bold",
|
||||
dimensions.box,
|
||||
dimensions.overlap,
|
||||
dimensions.overflow,
|
||||
|
||||
@@ -2,8 +2,14 @@
|
||||
const {current, total}: {current: number; total: number} = $props()
|
||||
</script>
|
||||
|
||||
<div class="flex w-full">
|
||||
<div class="flex w-full gap-1.5">
|
||||
{#each Array(total) as _, i}
|
||||
<div class="h-1 flex-1 transition-colors {i < current ? 'bg-primary' : 'bg-base-300'}"></div>
|
||||
<div
|
||||
class="h-2 flex-1 rounded-full transition-colors duration-300 {i < current
|
||||
? 'bg-primary'
|
||||
: i === current
|
||||
? 'bg-primary/40 motion-safe:animate-pulse'
|
||||
: 'bg-base-300'}">
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -157,15 +157,15 @@
|
||||
data-tip={tooltip}
|
||||
class={cx(
|
||||
reactionClass,
|
||||
"flex-inline btn btn-outline btn-neutral btn-xs flex items-center gap-1 rounded-full text-xs font-normal bg-alt",
|
||||
"flex-inline btn btn-xs flex items-center gap-1 rounded-full border text-xs font-normal transition-transform motion-safe:hover:scale-110 motion-safe:active:scale-95",
|
||||
{
|
||||
tooltip: !noTooltip && !isMobile,
|
||||
"border-neutral-content/20": !isOwn,
|
||||
"btn-primary": isOwn,
|
||||
"bg-alt border-base-content/15": !isOwn,
|
||||
"border-primary/50 bg-primary/15 text-primary": isOwn,
|
||||
},
|
||||
)}>
|
||||
<Reaction event={zaps[0].request} />
|
||||
<span>{amount}</span>
|
||||
<span class="font-semibold">{amount}</span>
|
||||
</button>
|
||||
{/each}
|
||||
{#each groupedReactions.entries() as [key, events]}
|
||||
@@ -179,17 +179,17 @@
|
||||
data-tip={tooltip}
|
||||
class={cx(
|
||||
reactionClass,
|
||||
"flex-inline btn btn-outline btn-neutral btn-xs gap-1 rounded-full font-normal bg-alt",
|
||||
"flex-inline btn btn-xs gap-1 rounded-full border font-normal transition-transform motion-safe:hover:scale-110 motion-safe:active:scale-95",
|
||||
{
|
||||
tooltip: !noTooltip && !isMobile,
|
||||
"border-neutral-content/20": !isOwn,
|
||||
"btn-primary": isOwn,
|
||||
"bg-alt border-base-content/15": !isOwn,
|
||||
"border-primary/50 bg-primary/15 text-primary": isOwn,
|
||||
},
|
||||
)}
|
||||
onclick={stopPropagation(preventDefault(onClick))}>
|
||||
<Reaction event={events[0]} />
|
||||
{#if events.length > 1}
|
||||
<span>{events.length}</span>
|
||||
<span class="font-semibold">{events.length}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
||||
import {getColor} from "@app/theme"
|
||||
import {makeRoomPath, makeSpaceChatPath} from "@app/routes"
|
||||
|
||||
type Props = {
|
||||
@@ -25,7 +26,9 @@
|
||||
const onClick = () => goto(h ? makeRoomPath(url, h) : makeSpaceChatPath(url))
|
||||
</script>
|
||||
|
||||
<Button class="cv card2 bg-alt shadow-md" onclick={onClick}>
|
||||
<Button
|
||||
class="cv card2 bg-alt shadow-soft block w-full transition-all motion-safe:hover:-translate-y-0.5"
|
||||
onclick={onClick}>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
{#if h}
|
||||
@@ -39,7 +42,10 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<ProfileCircle pubkey={event.pubkey} size={10} />
|
||||
<ProfileCircle
|
||||
pubkey={event.pubkey}
|
||||
size={10}
|
||||
style="box-shadow: 0 0 0 2px {getColor(event.pubkey)}" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<NoteContentMinimal {event} />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {deriveRelay} from "@welshman/app"
|
||||
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
|
||||
import {getColor} from "@app/theme"
|
||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
|
||||
type Props = {
|
||||
@@ -12,10 +13,22 @@
|
||||
const {url, size = 7, ...props}: Props = $props()
|
||||
|
||||
const relay = deriveRelay(url)
|
||||
const px = size * 4
|
||||
const color = getColor(url)
|
||||
const letter = (url.replace(/^wss?:\/\//, "").replace(/^www\./, "")[0] || "?").toUpperCase()
|
||||
</script>
|
||||
|
||||
{#if $relay?.icon}
|
||||
<ImageIcon {size} alt="" src={$relay?.icon} class={props.class} />
|
||||
<ImageIcon {size} alt="" src={$relay.icon} class={cx(props.class, "squircle")} />
|
||||
{:else}
|
||||
<ImageIcon size={size - 2} alt="" src={RemoteControllerMinimalistic} class={props.class} />
|
||||
<!-- Lettered workspace tile (Slack/Discord-style) colored by the relay url. -->
|
||||
<div
|
||||
class={cx(
|
||||
props.class,
|
||||
"squircle font-display flex shrink-0 items-center justify-center font-bold text-white uppercase",
|
||||
)}
|
||||
style="width:{px}px;height:{px}px;font-size:{px *
|
||||
0.42}px;background-image:linear-gradient(135deg,{color},color-mix(in oklab,{color},#000 28%))">
|
||||
{letter}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
</div>
|
||||
<Button
|
||||
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
|
||||
class="center tooltip tooltip-left absolute right-4 h-10 w-10 min-w-10 rounded-full"
|
||||
class="center tooltip tooltip-left bg-primary text-primary-content absolute top-[7px] right-[7px] h-11 w-11 min-w-11 scale-90 rounded-full transition-transform motion-safe:hover:scale-100"
|
||||
disabled={$uploading}
|
||||
onclick={submit}>
|
||||
<Icon icon={Plane} />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import Volume from "@assets/icons/volume.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
import {getColor} from "@app/theme"
|
||||
import {deriveRoom} from "@app/groups"
|
||||
|
||||
interface Props {
|
||||
@@ -16,18 +17,26 @@
|
||||
|
||||
const room = deriveRoom(url, h)
|
||||
const isVoiceRoom = $derived($room.livekit)
|
||||
const px = size * 4
|
||||
// Voice rooms read warm/orange; text rooms get a per-room identity color.
|
||||
const color = $derived(isVoiceRoom ? "var(--color-secondary)" : getColor(h))
|
||||
</script>
|
||||
|
||||
{#if isVoiceRoom}
|
||||
<div class="flex shrink-0 items-center gap-1.5">
|
||||
<Icon size={size + 1} icon={Volume} />
|
||||
<Icon size={size + 1} icon={Volume} class="text-secondary" />
|
||||
{#if $room.picture}
|
||||
<span class="text-base">/</span>
|
||||
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
|
||||
<ImageIcon src={$room.picture} {size} alt="" class="squircle shadow-sm" />
|
||||
{/if}
|
||||
</div>
|
||||
{:else if $room.picture}
|
||||
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
|
||||
<ImageIcon src={$room.picture} {size} alt="" class="squircle shadow-sm" />
|
||||
{:else}
|
||||
<Icon icon={fallbackIcon} {size} />
|
||||
<!-- Colored room tile with the type glyph in white. -->
|
||||
<div
|
||||
class="squircle flex shrink-0 items-center justify-center text-white"
|
||||
style="width:{px}px;height:{px}px;background-image:linear-gradient(135deg,{color},color-mix(in oklab,{color},#000 28%))">
|
||||
<Icon icon={fallbackIcon} size={Math.max(3, size - 1)} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {readable} from "svelte/store"
|
||||
import {
|
||||
hash,
|
||||
gte,
|
||||
now,
|
||||
displayList,
|
||||
formatTimestampAsTime,
|
||||
formatTimestampAsDate,
|
||||
} from "@welshman/lib"
|
||||
import {gte, now, displayList, formatTimestampAsTime, formatTimestampAsDate} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {MESSAGE, COMMENT, getTag} from "@welshman/util"
|
||||
import {
|
||||
@@ -35,7 +28,7 @@
|
||||
import RoomItemMenuButton from "@app/components/RoomItemMenuButton.svelte"
|
||||
import RoomItemMenuMobile from "@app/components/RoomItemMenuMobile.svelte"
|
||||
import RoomItemContent from "@app/components/RoomItemContent.svelte"
|
||||
import {colors} from "@app/theme"
|
||||
import {getColor} from "@app/theme"
|
||||
import {ENABLE_ZAPS} from "@app/env"
|
||||
import {deriveEventsForUrl, deriveEvent} from "@app/repository"
|
||||
import {publishDelete} from "@app/deletes"
|
||||
@@ -60,7 +53,7 @@
|
||||
const today = formatTimestampAsDate(now())
|
||||
const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
|
||||
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
|
||||
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
||||
const colorValue = getColor(event.pubkey)
|
||||
|
||||
const qTag = getTag("q", event.tags)
|
||||
const isQuoteOnly = Boolean(
|
||||
@@ -95,10 +88,7 @@
|
||||
<div class="flex w-full gap-3 overflow-auto">
|
||||
{#if showPubkey}
|
||||
<Button onclick={openProfile} class="flex items-start pt-1.5 justify-center w-8 shrink-0">
|
||||
<ProfileCircle
|
||||
pubkey={event.pubkey}
|
||||
class="border border-solid border-base-content"
|
||||
size={8} />
|
||||
<ProfileCircle pubkey={event.pubkey} style="box-shadow: 0 0 0 2px {colorValue}" size={8} />
|
||||
</Button>
|
||||
{:else}
|
||||
<div class="w-8 shrink-0"></div>
|
||||
@@ -155,8 +145,9 @@
|
||||
</div>
|
||||
{#if !isMobile}
|
||||
<button
|
||||
class="join absolute right-2 top-0.5 border border-solid border-neutral text-xs opacity-0 transition-all pr-2"
|
||||
class:group-hover:opacity-100={!isMobile}>
|
||||
class="join bg-base-100 shadow-soft absolute right-2 top-0.5 translate-y-1 rounded-full p-0.5 text-xs opacity-0 transition-all"
|
||||
class:group-hover:opacity-100={!isMobile}
|
||||
class:group-hover:translate-y-0={!isMobile}>
|
||||
{#if ENABLE_ZAPS}
|
||||
<RoomItemZapButton {url} {event} />
|
||||
{/if}
|
||||
|
||||
@@ -95,17 +95,19 @@
|
||||
|
||||
<Modal>
|
||||
<ModalBody>
|
||||
<h1 class="heading">Join {PLATFORM_NAME}</h1>
|
||||
<h1 class="heading">Join <span class="brand">{PLATFORM_NAME}</span></h1>
|
||||
<p class="m-auto max-w-sm text-center">
|
||||
Censorship resistant digital spaces for communities. Meet new people, own your identity.
|
||||
</p>
|
||||
{#if hasPomade}
|
||||
<Button onclick={flows.email.start} class="btn btn-primary">
|
||||
<Button onclick={flows.email.start} class="btn btn-primary rounded-full">
|
||||
<Icon icon={Letter} />
|
||||
Sign up with email
|
||||
</Button>
|
||||
{/if}
|
||||
<Button onclick={flows.nostr.start} class="btn {hasPomade ? 'btn-neutral' : 'btn-primary'}">
|
||||
<Button
|
||||
onclick={flows.nostr.start}
|
||||
class="btn rounded-full {hasPomade ? 'btn-neutral' : 'btn-primary'}">
|
||||
<Icon icon={Key} />
|
||||
Generate a key
|
||||
</Button>
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
import {preventDefault} from "@lib/html"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
|
||||
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Modal from "@lib/components/Modal.svelte"
|
||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import ProgressBar from "@app/components/ProgressBar.svelte"
|
||||
|
||||
@@ -24,9 +23,12 @@
|
||||
|
||||
<Modal tag="form" onsubmit={preventDefault(next)}>
|
||||
<ModalBody>
|
||||
<ModalHeader>
|
||||
<ModalTitle>You're all set!</ModalTitle>
|
||||
</ModalHeader>
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<div class="center bg-primary/15 text-primary size-16 rounded-full motion-safe:animate-pop">
|
||||
<Icon icon={CheckCircle} size={9} />
|
||||
</div>
|
||||
<h1 class="heading">You're all set!</h1>
|
||||
</div>
|
||||
<p>
|
||||
You've created your profile, saved your keys, and now you're ready to start chatting — all
|
||||
without asking permission!
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
class="relative flex w-full flex-col rounded-xl p-3 transition-all hover:bg-base-100"
|
||||
onclick={openMenu}>
|
||||
<div class="flex items-center justify-between">
|
||||
<strong class="flex items-center gap-1 relative">
|
||||
<strong class="font-display relative flex items-center gap-1">
|
||||
<RelayName {url} class="ellipsize" />
|
||||
<div
|
||||
class="absolute -right-3 top-0 h-2 w-2 rounded-full bg-primary transition-all opacity-0"
|
||||
@@ -311,7 +311,7 @@
|
||||
<div
|
||||
class="flex shrink-0 flex-col gap-2 p-2 pt-0 -mt-4 pb-[calc(var(--saib)+0.25rem)] md:pb-2 z-nav">
|
||||
<VoiceWidget />
|
||||
<Button class="btn btn-neutral btn-sm h-10" onclick={showDetail}>
|
||||
<Button class="btn btn-ghost btn-sm bg-base-100 h-10 rounded-full" onclick={showDetail}>
|
||||
<SocketStatusIndicator {url} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import type {AbstractThunk} from "@welshman/app"
|
||||
import {thunkHasStatus, thunkIsComplete} from "@welshman/app"
|
||||
import {PublishStatus} from "@welshman/net"
|
||||
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ThunkPending from "@app/components/ThunkPending.svelte"
|
||||
import type {Toast} from "@app/toast"
|
||||
import {popToast} from "@app/toast"
|
||||
@@ -35,5 +37,8 @@
|
||||
{#if !isComplete}
|
||||
<ThunkPending {thunk} />
|
||||
{:else if !isFailure}
|
||||
<p class="text-xs opacity-75">Message sent!</p>
|
||||
<p class="flex items-center gap-1.5 text-xs opacity-80">
|
||||
<Icon icon={CheckCircle} size={4} class="text-success motion-safe:animate-pop" />
|
||||
Message sent!
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
+27
-16
@@ -1,29 +1,40 @@
|
||||
import twColors from "tailwindcss/colors"
|
||||
import {hash} from "@welshman/lib"
|
||||
import {kv} from "@app/storage"
|
||||
import {synced} from "@welshman/store"
|
||||
|
||||
// Per-identity color palette, biased toward warm, saturated, brand-adjacent
|
||||
// hues so each person/space is recognizable at a glance. Deduped (was two
|
||||
// `sky` entries) and trimmed of low-distinctness gray (`zinc`). The 600 weight
|
||||
// reads clearly on both the warm-paper light base and the warm-charcoal dark
|
||||
// base. Each entry is [name, hex].
|
||||
export const colors = [
|
||||
["amber", twColors.amber[600]],
|
||||
["blue", twColors.blue[600]],
|
||||
["cyan", twColors.cyan[600]],
|
||||
["emerald", twColors.emerald[600]],
|
||||
["fuchsia", twColors.fuchsia[600]],
|
||||
["green", twColors.green[600]],
|
||||
["indigo", twColors.indigo[600]],
|
||||
["sky", twColors.sky[600]],
|
||||
["lime", twColors.lime[600]],
|
||||
["orange", twColors.orange[600]],
|
||||
["pink", twColors.pink[600]],
|
||||
["purple", twColors.purple[600]],
|
||||
["red", twColors.red[600]],
|
||||
["rose", twColors.rose[600]],
|
||||
["sky", twColors.sky[600]],
|
||||
["teal", twColors.teal[600]],
|
||||
["violet", twColors.violet[600]],
|
||||
["indigo", twColors.indigo[600]],
|
||||
["fuchsia", twColors.fuchsia[600]],
|
||||
["pink", twColors.pink[600]],
|
||||
["rose", twColors.rose[600]],
|
||||
["red", twColors.red[600]],
|
||||
["orange", twColors.orange[600]],
|
||||
["amber", twColors.amber[600]],
|
||||
["yellow", twColors.yellow[600]],
|
||||
["zinc", twColors.zinc[600]],
|
||||
["lime", twColors.lime[600]],
|
||||
["green", twColors.green[600]],
|
||||
["emerald", twColors.emerald[600]],
|
||||
["teal", twColors.teal[600]],
|
||||
["cyan", twColors.cyan[600]],
|
||||
["sky", twColors.sky[600]],
|
||||
["blue", twColors.blue[600]],
|
||||
]
|
||||
|
||||
// Single source of truth for per-pubkey identity color — reused by username
|
||||
// text, avatar gradient fallbacks, and avatar rings. Deterministic per pubkey.
|
||||
export const getColor = (pubkey = "") => colors[hash(pubkey) % colors.length][1]
|
||||
|
||||
// Which blob variant (1-3) a pubkey gets, so an avatar's organic shape is stable.
|
||||
export const getBlobVariant = (pubkey = "") => (hash(pubkey) % 3) + 1
|
||||
|
||||
export const theme = synced({
|
||||
key: "theme",
|
||||
defaultValue: window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
|
||||
|
||||
Reference in New Issue
Block a user