mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-18 06:24:25 +01:00
feat: color themes
This commit is contained in:
BIN
public/images/rabbit_muted_256.png
Normal file
BIN
public/images/rabbit_muted_256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -6,6 +6,7 @@ import { persistQueryClient } from '@tanstack/query-persist-client-core';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/solid-query';
|
||||
import { get as getItem, set as setItem, del as removeItem } from 'idb-keyval';
|
||||
|
||||
import useColorTheme from '@/hooks/useColorTheme';
|
||||
import i18nextInstance from '@/i18n/i18n';
|
||||
import { I18NextProvider } from '@/i18n/useTranslation';
|
||||
|
||||
@@ -47,6 +48,8 @@ const App: Component = () => {
|
||||
onCleanup(() => unsubscribe());
|
||||
});
|
||||
|
||||
useColorTheme(document.body);
|
||||
|
||||
return (
|
||||
<I18NextProvider i18next={i18next}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
||||
@@ -114,9 +114,9 @@ const ReactionAction = (props: { event: NostrEvent }) => {
|
||||
<div
|
||||
class="flex shrink-0 items-center gap-1"
|
||||
classList={{
|
||||
'text-zinc-400': !isReactedByMe() || isReactedByMeWithEmoji(),
|
||||
'hover:text-rose-400': !isReactedByMe() || isReactedByMeWithEmoji(),
|
||||
'text-rose-400':
|
||||
'text-fg-tertiary': !isReactedByMe() || isReactedByMeWithEmoji(),
|
||||
'hover:text-r-reaction': !isReactedByMe() || isReactedByMeWithEmoji(),
|
||||
'text-r-reaction':
|
||||
(isReactedByMe() && !isReactedByMeWithEmoji()) || publishReactionMutation.isPending,
|
||||
}}
|
||||
>
|
||||
@@ -130,7 +130,7 @@ const ReactionAction = (props: { event: NostrEvent }) => {
|
||||
</Show>
|
||||
</button>
|
||||
<Show when={!config().hideCount && !config().showEmojiReaction && reactions().length > 0}>
|
||||
<div class="text-sm text-zinc-400">
|
||||
<div class="text-sm text-fg-tertiary">
|
||||
{formatSiPrefix(reactions().length, { minDigits: 4 })}
|
||||
</div>
|
||||
</Show>
|
||||
@@ -139,9 +139,9 @@ const ReactionAction = (props: { event: NostrEvent }) => {
|
||||
<div
|
||||
class="flex shrink-0 items-center gap-1"
|
||||
classList={{
|
||||
'text-zinc-400': !isReactedByMe() || !isReactedByMeWithEmoji(),
|
||||
'hover:text-rose-400': !isReactedByMe() || !isReactedByMeWithEmoji(),
|
||||
'text-rose-400':
|
||||
'text-fg-tertiary': !isReactedByMe() || !isReactedByMeWithEmoji(),
|
||||
'hover:text-r-reaction': !isReactedByMe() || !isReactedByMeWithEmoji(),
|
||||
'text-r-reaction':
|
||||
(isReactedByMe() && isReactedByMeWithEmoji()) || publishReactionMutation.isPending,
|
||||
}}
|
||||
>
|
||||
@@ -199,16 +199,18 @@ const RepostAction = (props: { event: NostrEvent }) => {
|
||||
<div
|
||||
class="flex shrink-0 items-center gap-1"
|
||||
classList={{
|
||||
'text-zinc-400': !isRepostedByMe(),
|
||||
'hover:text-green-400': !isRepostedByMe(),
|
||||
'text-green-400': isRepostedByMe() || publishRepostMutation.isPending,
|
||||
'text-fg-tertiary': !isRepostedByMe(),
|
||||
'hover:text-r-repost': !isRepostedByMe(),
|
||||
'text-r-repost': isRepostedByMe() || publishRepostMutation.isPending,
|
||||
}}
|
||||
>
|
||||
<button class="h-4 w-4" onClick={handleRepost} disabled={publishRepostMutation.isPending}>
|
||||
<ArrowPathRoundedSquare />
|
||||
<button onClick={handleRepost} disabled={publishRepostMutation.isPending}>
|
||||
<span class="flex h-4 w-4">
|
||||
<ArrowPathRoundedSquare />
|
||||
</span>
|
||||
</button>
|
||||
<Show when={!config().hideCount && reposts().length > 0}>
|
||||
<div class="text-sm text-zinc-400">
|
||||
<div class="text-sm text-fg-tertiary">
|
||||
{formatSiPrefix(reposts().length, { minDigits: 4 })}
|
||||
</div>
|
||||
</Show>
|
||||
@@ -293,13 +295,14 @@ const EmojiReactions: Component<{ event: NostrEvent }> = (props) => {
|
||||
|
||||
return (
|
||||
<button
|
||||
class="flex h-6 max-w-[128px] items-center rounded border px-1"
|
||||
class="flex h-8 max-w-[128px] items-center rounded border border-border px-1 sm:h-6"
|
||||
classList={{
|
||||
'text-zinc-400': !isReactedByMeWithThisContent,
|
||||
'hover:bg-zinc-50': !isReactedByMeWithThisContent,
|
||||
'bg-rose-50': isReactedByMeWithThisContent,
|
||||
'border-rose-200': isReactedByMeWithThisContent,
|
||||
'text-rose-400': isReactedByMeWithThisContent,
|
||||
'text-fg-tertiary': !isReactedByMeWithThisContent,
|
||||
'hover:bg-r-reaction/10': !isReactedByMeWithThisContent,
|
||||
'hover:border-r-reaction/40': !isReactedByMeWithThisContent,
|
||||
'bg-r-reaction/10': isReactedByMeWithThisContent,
|
||||
'border-r-reaction/40': isReactedByMeWithThisContent,
|
||||
'text-r-reaction': isReactedByMeWithThisContent,
|
||||
}}
|
||||
type="button"
|
||||
onClick={(ev) => {
|
||||
@@ -401,18 +404,20 @@ const Actions: Component<ActionProps> = (props) => {
|
||||
<EmojiReactions event={props.event} />
|
||||
<div class="actions flex w-52 items-center justify-between gap-8 pt-1">
|
||||
<button
|
||||
class="h-4 w-4 shrink-0 text-zinc-400 hover:text-zinc-500"
|
||||
class="shrink-0 text-fg-tertiary hover:text-fg-tertiary/70"
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
props.onClickReply();
|
||||
}}
|
||||
>
|
||||
<ChatBubbleLeft />
|
||||
<span class="flex h-4 w-4">
|
||||
<ChatBubbleLeft />
|
||||
</span>
|
||||
</button>
|
||||
<RepostAction event={props.event} />
|
||||
<ReactionAction event={props.event} />
|
||||
<ContextMenu menu={menu}>
|
||||
<span class="inline-block h-4 w-4 text-zinc-400 hover:text-zinc-500">
|
||||
<span class="inline-block h-4 w-4 text-fg-tertiary hover:text-fg-tertiary/70">
|
||||
<EllipsisHorizontal />
|
||||
</span>
|
||||
</ContextMenu>
|
||||
|
||||
@@ -5,7 +5,7 @@ type ColumnItemProps = {
|
||||
};
|
||||
|
||||
const ColumnItem: Component<ColumnItemProps> = (props) => (
|
||||
<div class="block shrink-0 overflow-hidden border-b p-1">{props.children}</div>
|
||||
<div class="block shrink-0 overflow-hidden border-b border-border p-1">{props.children}</div>
|
||||
);
|
||||
|
||||
export default ColumnItem;
|
||||
|
||||
@@ -25,7 +25,7 @@ const MenuItemDisplay: Component<MenuItemDisplayProps> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<li class="border-b hover:bg-stone-200">
|
||||
<li class="border-b border-border hover:bg-bg-tertiary">
|
||||
<button class="w-full px-4 py-1 text-start" onClick={handleClick}>
|
||||
{props.item.content()}
|
||||
</button>
|
||||
@@ -46,7 +46,7 @@ const ContextMenu: Component<ContextMenuProps> = (props) => {
|
||||
button={props.children}
|
||||
position="bottom"
|
||||
>
|
||||
<ul class="min-w-[96px] rounded border bg-white shadow-md">
|
||||
<ul class="min-w-[96px] rounded border border-border bg-bg shadow-md">
|
||||
<For each={props.menu.filter((e) => e.when == null || e.when())}>
|
||||
{(item: MenuItem) => <MenuItemDisplay item={item} onClose={close} />}
|
||||
</For>
|
||||
|
||||
@@ -13,7 +13,7 @@ const isPlus = (r: ReactionTypes) => r.type === 'LikeDislike' && r.content === '
|
||||
const EmojiDisplay: Component<EmojiDisplayProps> = (props) => (
|
||||
<Switch fallback={<span class="truncate">{props.reactionTypes.content}</span>}>
|
||||
<Match when={isPlus(props.reactionTypes)}>
|
||||
<span class="inline-block h-4 w-4 pt-[1px] text-rose-400">
|
||||
<span class="inline-block h-4 w-4 pt-[1px] text-r-reaction">
|
||||
<HeartSolid />
|
||||
</span>
|
||||
</Match>
|
||||
|
||||
@@ -26,7 +26,7 @@ const tryEncodeNevent = (eventId: string) => {
|
||||
};
|
||||
|
||||
const EventLink: Component<EventLinkProps> = (props) => (
|
||||
<button class="text-blue-500 underline">
|
||||
<button class="text-link underline">
|
||||
<Show when={props.kind == null || props.kind === 1} fallback={tryEncodeNevent(props.eventId)}>
|
||||
{tryEncodeNote(props.eventId)}
|
||||
</Show>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createSignal, createMemo, onMount, Show, For, type Component, type JSX } from 'solid-js';
|
||||
|
||||
import { createMutation } from '@tanstack/solid-query';
|
||||
import ExclamationTriangle from 'heroicons/24/outline/exclamation-triangle.svg';
|
||||
import FaceSmile from 'heroicons/24/outline/face-smile.svg';
|
||||
import Photo from 'heroicons/24/outline/photo.svg';
|
||||
import XMark from 'heroicons/24/outline/x-mark.svg';
|
||||
@@ -365,7 +366,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
<Show when={contentWarning()}>
|
||||
<input
|
||||
type="text"
|
||||
class="rounded"
|
||||
class="rounded-md border border-border bg-bg ring-border placeholder:text-fg-secondary focus:border-border focus:ring-primary"
|
||||
placeholder={i18n()('posting.contentWarningReason')}
|
||||
maxLength={32}
|
||||
onInput={(ev) => setContentWarningReason(ev.currentTarget.value)}
|
||||
@@ -379,7 +380,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
emojiTextAreaRef(el);
|
||||
}}
|
||||
name="text"
|
||||
class="min-h-[40px] rounded-md border-none focus:ring-rose-300"
|
||||
class="min-h-[40px] rounded-md border border-border bg-bg ring-border placeholder:text-fg-secondary focus:border-border focus:ring-primary"
|
||||
rows={4}
|
||||
placeholder={placeholder(mode())}
|
||||
onInput={handleInput}
|
||||
@@ -392,42 +393,56 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
<div class="flex items-end justify-end gap-1">
|
||||
<Show when={mode() === 'reply'}>
|
||||
<div class="flex-1">
|
||||
<button class="h-5 w-5 text-stone-500" onClick={() => close()}>
|
||||
<button class="h-5 w-5 text-fg-secondary" onClick={() => close()}>
|
||||
<XMark />
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<EmojiPicker customEmojis={true} onEmojiSelect={handleEmojiSelect}>
|
||||
<span class="inline-block h-8 w-8 rounded bg-primary p-2 font-bold text-white">
|
||||
<span
|
||||
class="inline-block rounded bg-primary font-bold text-primary-fg"
|
||||
classList={{
|
||||
'h-8': mode() === 'normal',
|
||||
'w-8': mode() === 'normal',
|
||||
'p-2': mode() === 'normal',
|
||||
'h-7': mode() === 'reply',
|
||||
'w-7': mode() === 'reply',
|
||||
'p-[6px]': mode() === 'reply',
|
||||
}}
|
||||
>
|
||||
<FaceSmile />
|
||||
</span>
|
||||
</EmojiPicker>
|
||||
<button
|
||||
class="flex items-center justify-center rounded p-2 text-xs font-bold text-white"
|
||||
class="flex items-center justify-center rounded p-2 text-xs font-bold text-primary-fg"
|
||||
classList={{
|
||||
'bg-rose-300': !contentWarning(),
|
||||
'bg-rose-400': contentWarning(),
|
||||
'bg-primary': !contentWarning(),
|
||||
'bg-primary-hover': contentWarning(),
|
||||
'h-8': mode() === 'normal',
|
||||
'w-8': mode() === 'normal',
|
||||
'p-2': mode() === 'normal',
|
||||
'h-7': mode() === 'reply',
|
||||
'w-7': mode() === 'reply',
|
||||
'p-[6px]': mode() === 'reply',
|
||||
}}
|
||||
type="button"
|
||||
aria-label={i18n()('posting.contentWarning')}
|
||||
title={i18n()('posting.contentWarning')}
|
||||
onClick={() => setContentWarning((e) => !e)}
|
||||
>
|
||||
<span>CW</span>
|
||||
<ExclamationTriangle />
|
||||
</button>
|
||||
<button
|
||||
class="rounded bg-primary p-2 font-bold text-white"
|
||||
class="rounded font-bold text-primary-fg"
|
||||
classList={{
|
||||
'bg-primary-disabled': fileUploadDisabled(),
|
||||
'bg-primary': !fileUploadDisabled(),
|
||||
'h-8': mode() === 'normal',
|
||||
'w-8': mode() === 'normal',
|
||||
'p-2': mode() === 'normal',
|
||||
'h-7': mode() === 'reply',
|
||||
'w-7': mode() === 'reply',
|
||||
'p-[6px]': mode() === 'reply',
|
||||
}}
|
||||
type="button"
|
||||
title={i18n()('posting.uploadImage')}
|
||||
@@ -438,7 +453,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
<Photo />
|
||||
</button>
|
||||
<button
|
||||
class="rounded bg-primary p-2 font-bold text-white"
|
||||
class="rounded p-2 font-bold text-primary-fg"
|
||||
classList={{
|
||||
'bg-primary-disabled': submitDisabled(),
|
||||
'bg-primary': !submitDisabled(),
|
||||
@@ -457,7 +472,6 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
class="rounded bg-primary"
|
||||
type="file"
|
||||
hidden
|
||||
name="image"
|
||||
|
||||
@@ -53,19 +53,19 @@ const Post: Component<PostProps> = (props) => {
|
||||
<div class="flex justify-between gap-1 text-xs">
|
||||
<button
|
||||
type="button"
|
||||
class="author flex min-w-0 select-text truncate hover:text-blue-500"
|
||||
class="author flex min-w-0 select-text truncate hover:text-link"
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault();
|
||||
props?.onShowProfile?.();
|
||||
}}
|
||||
>
|
||||
<span class="author flex min-w-0 truncate hover:text-blue-500">
|
||||
<span class="author flex min-w-0 truncate hover:text-link">
|
||||
<Show when={(author()?.display_name?.length ?? 0) > 0}>
|
||||
<div class="author-name truncate pr-1 font-bold hover:underline">
|
||||
{author()?.display_name}
|
||||
</div>
|
||||
</Show>
|
||||
<div class="author-username truncate text-zinc-600">
|
||||
<div class="author-username truncate text-fg-secondary">
|
||||
<Show
|
||||
when={author()?.name != null}
|
||||
fallback={`@${npubEncodeFallback(props.authorPubkey)}`}
|
||||
@@ -99,7 +99,7 @@ const Post: Component<PostProps> = (props) => {
|
||||
</div>
|
||||
<Show when={overflow()}>
|
||||
<button
|
||||
class="mt-2 w-full rounded border p-2 text-center text-xs text-stone-600 shadow-sm hover:shadow"
|
||||
class="mt-2 w-full rounded border border-border p-2 text-center text-xs text-fg-secondary shadow-sm hover:bg-bg-tertiary hover:shadow"
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
setShowOverflow((current) => !current);
|
||||
|
||||
@@ -39,24 +39,24 @@ const SearchButton = () => {
|
||||
}}
|
||||
position="right"
|
||||
button={
|
||||
<span class="inline-block h-9 w-9 rounded-full border border-primary p-2 text-2xl font-bold text-primary">
|
||||
<span class="inline-block h-9 w-9 rounded-full border border-primary p-2 text-2xl font-bold text-primary hover:border-primary-hover hover:text-primary-hover">
|
||||
<MagnifyingGlass />
|
||||
</span>
|
||||
}
|
||||
onOpen={() => inputRef?.focus()}
|
||||
>
|
||||
<form
|
||||
class="flex w-72 items-center gap-1 rounded-md bg-white p-4 shadow-md"
|
||||
class="flex w-72 items-center gap-1 rounded-md border border-border bg-bg p-4 shadow-md"
|
||||
onSubmit={handleSearchSubmit}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
class="h-8 w-full rounded border border-stone-300 focus:border-rose-100 focus:ring-rose-300"
|
||||
class="h-8 w-full rounded border border-border bg-bg focus:border-primary focus:ring-border"
|
||||
type="text"
|
||||
value={query()}
|
||||
onChange={(ev) => setQuery(ev.currentTarget.value)}
|
||||
/>
|
||||
<button class="h-8 w-8 rounded bg-primary p-1 text-white" type="submit">
|
||||
<button class="h-8 w-8 rounded bg-primary p-1 text-primary-fg" type="submit">
|
||||
<MagnifyingGlass />
|
||||
</button>
|
||||
</form>
|
||||
@@ -68,7 +68,7 @@ const SideBar: Component = () => {
|
||||
let textAreaRef: HTMLTextAreaElement | undefined;
|
||||
|
||||
const { showAddColumn, showAbout } = useModalState();
|
||||
const { config } = useConfig();
|
||||
const { config, getColorTheme } = useConfig();
|
||||
|
||||
const [formOpened, setFormOpened] = createSignal(false);
|
||||
const [configOpened, setConfigOpened] = createSignal(false);
|
||||
@@ -93,11 +93,11 @@ const SideBar: Component = () => {
|
||||
}));
|
||||
|
||||
return (
|
||||
<div class="flex shrink-0 flex-row border-r bg-sidebar-bg">
|
||||
<div class="flex w-14 flex-auto flex-col items-center gap-3 border-r border-rose-200 pt-5">
|
||||
<div class="flex shrink-0 flex-row bg-r-sidebar">
|
||||
<div class="flex w-14 flex-auto flex-col items-center gap-3 border-r border-border pt-5">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<button
|
||||
class="h-9 w-9 rounded-full border border-primary bg-primary p-2 text-2xl text-white"
|
||||
class="h-9 w-9 rounded-full border border-primary bg-primary p-2 text-2xl text-primary-fg hover:border-primary-hover hover:bg-primary-hover"
|
||||
onClick={() => toggleForm()}
|
||||
>
|
||||
<PencilSquare />
|
||||
@@ -107,13 +107,13 @@ const SideBar: Component = () => {
|
||||
<div class="grow" />
|
||||
<div class="flex flex-col items-center pb-2">
|
||||
<button
|
||||
class="h-10 w-12 rounded-full p-3 text-2xl text-primary"
|
||||
class="h-10 w-12 rounded-full p-3 text-2xl text-primary hover:border-primary-hover hover:text-primary-hover"
|
||||
onClick={() => showAddColumn()}
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
<button
|
||||
class="h-10 w-12 p-3 text-primary"
|
||||
class="h-10 w-12 p-3 text-primary hover:border-primary-hover hover:text-primary-hover"
|
||||
onClick={() => setConfigOpened((current) => !current)}
|
||||
>
|
||||
<Cog6Tooth />
|
||||
@@ -121,13 +121,14 @@ const SideBar: Component = () => {
|
||||
<button class="pt-2" onClick={() => showAbout()}>
|
||||
<img
|
||||
class="h-8 w-8"
|
||||
src={resolveAsset('images/rabbit_app_256.png')}
|
||||
src={resolveAsset(getColorTheme().rabbitIconPath)}
|
||||
alt="About rabbit"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="border-r border-border"
|
||||
classList={{
|
||||
static: formOpened() || config().keepOpenPostForm,
|
||||
hidden: !(formOpened() || config().keepOpenPostForm),
|
||||
|
||||
@@ -19,12 +19,14 @@ const BasicColumnHeader: Component<BasicColumnHeaderProps> = (props) => {
|
||||
<div class="flex h-8 items-center gap-1 px-2">
|
||||
<h2 class="flex min-w-0 flex-1 items-center gap-1">
|
||||
<Show when={props.icon} keyed>
|
||||
{(icon) => <span class="inline-block h-4 w-4 shrink-0 text-gray-700">{icon}</span>}
|
||||
{(icon) => <span class="inline-block h-4 w-4 shrink-0 text-fg-secondary">{icon}</span>}
|
||||
</Show>
|
||||
<span class="column-name truncate">{props.name}</span>
|
||||
<span class="truncate">{props.name}</span>
|
||||
</h2>
|
||||
<button class="h-4 w-4" onClick={() => toggleSettingsOpened()}>
|
||||
<EllipsisVertical />
|
||||
<button class="flex h-full place-items-center" onClick={() => toggleSettingsOpened()}>
|
||||
<span class="inline-block h-4 w-4">
|
||||
<EllipsisVertical />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<Show when={isSettingsOpened()}>{props.settings()}</Show>
|
||||
|
||||
@@ -45,7 +45,7 @@ const Column: Component<ColumnProps> = (props) => {
|
||||
<TimelineContext.Provider value={timelineState}>
|
||||
<div
|
||||
ref={columnDivRef}
|
||||
class="flex w-[80vw] shrink-0 snap-center snap-always flex-col border-r sm:snap-align-none"
|
||||
class="flex w-[80vw] shrink-0 snap-center snap-always flex-col border-r border-border sm:snap-align-none"
|
||||
classList={{
|
||||
'sm:w-[500px]': width() === 'widest',
|
||||
'sm:w-[360px]': width() === 'wide',
|
||||
@@ -58,7 +58,7 @@ const Column: Component<ColumnProps> = (props) => {
|
||||
keyed
|
||||
fallback={
|
||||
<>
|
||||
<div class="shrink-0 border-b">{props.header}</div>
|
||||
<div class="shrink-0 border-b border-border">{props.header}</div>
|
||||
<div class="scrollbar flex flex-col overflow-y-scroll scroll-smooth pb-16">
|
||||
{props.children}
|
||||
</div>
|
||||
@@ -67,7 +67,7 @@ const Column: Component<ColumnProps> = (props) => {
|
||||
>
|
||||
{(timeline) => (
|
||||
<>
|
||||
<div class="flex shrink-0 items-center border-b bg-white px-2">
|
||||
<div class="flex shrink-0 items-center border-b border-border px-2">
|
||||
<button
|
||||
class="flex w-full items-center gap-1"
|
||||
onClick={() => timelineState?.clearTimeline()}
|
||||
|
||||
@@ -20,7 +20,7 @@ type ColumnSettingsSectionProps = {
|
||||
};
|
||||
|
||||
const ColumnSettingsSection: Component<ColumnSettingsSectionProps> = (props) => (
|
||||
<div class="flex flex-col gap-2 border-b p-2">
|
||||
<div class="flex flex-col gap-2 border-b border-border p-2">
|
||||
<div>{props.title}</div>
|
||||
<div>{props.children}</div>
|
||||
</div>
|
||||
@@ -41,29 +41,29 @@ const ColumnSettings: Component<ColumnSettingsProps> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex flex-col border-t">
|
||||
<div class="flex flex-col border-t border-border">
|
||||
<ColumnSettingsSection title={i18n()('column.config.columnWidth')}>
|
||||
<div class="scrollbar flex h-9 gap-2 overflow-x-scroll">
|
||||
<button
|
||||
class="rounded-md border px-4 hover:bg-stone-100"
|
||||
class="rounded-md border border-border px-4"
|
||||
onClick={() => setColumnWidth('widest')}
|
||||
>
|
||||
{i18n()('column.config.widest')}
|
||||
</button>
|
||||
<button
|
||||
class="rounded-md border px-4 hover:bg-stone-100"
|
||||
class="rounded-md border border-border px-4"
|
||||
onClick={() => setColumnWidth('wide')}
|
||||
>
|
||||
{i18n()('column.config.wide')}
|
||||
</button>
|
||||
<button
|
||||
class="rounded-md border px-4 hover:bg-stone-100"
|
||||
class="rounded-md border border-border px-4"
|
||||
onClick={() => setColumnWidth('medium')}
|
||||
>
|
||||
{i18n()('column.config.medium')}
|
||||
</button>
|
||||
<button
|
||||
class="rounded-md border px-4 hover:bg-stone-100"
|
||||
class="rounded-md border border-border px-4"
|
||||
onClick={() => setColumnWidth('narrow')}
|
||||
>
|
||||
{i18n()('column.config.narrow')}
|
||||
@@ -91,7 +91,7 @@ const ColumnSettings: Component<ColumnSettingsProps> = (props) => {
|
||||
</button>
|
||||
<div class="flex-1" />
|
||||
<button
|
||||
class="px-2 py-4 text-rose-500 hover:text-rose-600"
|
||||
class="px-2 py-4 text-danger hover:text-rose-600"
|
||||
title={i18n()('column.config.removeColumn')}
|
||||
onClick={() => removeColumn(props.column.id)}
|
||||
>
|
||||
|
||||
@@ -52,13 +52,13 @@ const SearchColumnHeader: Component<SearchColumnHeaderProps> = (props) => {
|
||||
<div class="flex flex-col">
|
||||
<div class="flex h-8 items-center gap-1 px-2">
|
||||
<h2 class="flex items-center gap-1">
|
||||
<span class="inline-block h-4 w-4 text-gray-700">
|
||||
<span class="inline-block h-4 w-4 text-fg-secondary">
|
||||
<MagnifyingGlass />
|
||||
</span>
|
||||
</h2>
|
||||
<form class="flex-1" onSubmit={handleSubmit}>
|
||||
<input
|
||||
class="w-full rounded border border-stone-300 px-1 py-0 focus:border-rose-100 focus:ring-rose-300"
|
||||
class="w-full rounded border border-border bg-bg px-1 py-0 ring-border focus:border-border focus:ring-primary"
|
||||
type="text"
|
||||
name="query"
|
||||
value={query()}
|
||||
|
||||
@@ -31,19 +31,19 @@ const ProfileListItem: Component<ProfileListItemProps> = (props) => {
|
||||
<div class="flex justify-between gap-1 text-xs">
|
||||
<button
|
||||
type="button"
|
||||
class="profile flex min-w-0 select-text truncate hover:text-blue-500"
|
||||
class="profile flex min-w-0 select-text truncate hover:text-link"
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault();
|
||||
props?.onShowProfile?.();
|
||||
}}
|
||||
>
|
||||
<span class="profile flex min-w-0 truncate hover:text-blue-500">
|
||||
<span class="profile flex min-w-0 truncate hover:text-link">
|
||||
<Show when={(profile()?.display_name?.length ?? 0) > 0}>
|
||||
<div class="profile-name truncate pr-1 font-bold hover:underline">
|
||||
{profile()?.display_name}
|
||||
</div>
|
||||
</Show>
|
||||
<div class="profile-username truncate text-zinc-600">
|
||||
<div class="profile-username truncate text-fg-secondary">
|
||||
<Show
|
||||
when={profile()?.name}
|
||||
fallback={`@${npubEncodeFallback(props.pubkey)}`}
|
||||
|
||||
@@ -58,7 +58,7 @@ const ReactionDisplay: Component<ReactionDisplayProps> = (props) => {
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-1 overflow-hidden">
|
||||
<button
|
||||
class="select-text truncate font-bold hover:text-blue-500 hover:underline"
|
||||
class="select-text truncate font-bold hover:text-link hover:underline"
|
||||
onClick={() => showProfile(props.event.pubkey)}
|
||||
>
|
||||
<UserDisplayName pubkey={props.event.pubkey} />
|
||||
|
||||
@@ -27,13 +27,13 @@ const Repost: Component<RepostProps> = (props) => {
|
||||
<div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="flex shrink-0 place-items-center pl-[2px]" aria-hidden="true">
|
||||
<span class="h-4 w-4 text-green-500">
|
||||
<span class="h-4 w-4 text-r-repost">
|
||||
<ArrowPathRoundedSquare />
|
||||
</span>
|
||||
</div>
|
||||
<div class="notification-user flex min-w-0 flex-1 overflow-hidden text-xs">
|
||||
<button
|
||||
class="select-text truncate hover:text-blue-500 hover:underline"
|
||||
class="select-text truncate hover:text-link hover:underline"
|
||||
onClick={() => showProfile(props.event.pubkey)}
|
||||
>
|
||||
<UserDisplayName pubkey={props.event.pubkey} />
|
||||
|
||||
@@ -66,7 +66,7 @@ const TextNote: Component<TextNoteProps> = (props) => {
|
||||
<div class="textnote-content">
|
||||
<Show when={showReplyEvent()} keyed>
|
||||
{(id) => (
|
||||
<div class="mt-1 rounded border p-1">
|
||||
<div class="mt-1 rounded border border-border p-1">
|
||||
<LazyLoad fallback={<div class="h-12" />}>
|
||||
{() => <EventDisplayById eventId={id} actions={false} embedding={false} />}
|
||||
</LazyLoad>
|
||||
@@ -79,7 +79,7 @@ const TextNote: Component<TextNoteProps> = (props) => {
|
||||
<For each={event().taggedPubkeys()}>
|
||||
{(replyToPubkey: string) => (
|
||||
<button
|
||||
class="select-text pr-1 text-blue-500 hover:underline"
|
||||
class="select-text pr-1 text-link hover:underline"
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
showProfile(replyToPubkey);
|
||||
|
||||
@@ -91,7 +91,7 @@ const ZapReceipt: Component<ZapReceiptProps> = (props) => {
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-1 overflow-hidden">
|
||||
<button
|
||||
class="select-text truncate font-bold hover:text-blue-500 hover:underline"
|
||||
class="select-text truncate font-bold hover:text-link hover:underline"
|
||||
onClick={() => showProfile(event().senderPubkey())}
|
||||
>
|
||||
<UserDisplayName pubkey={event().senderPubkey()} />
|
||||
@@ -101,7 +101,7 @@ const ZapReceipt: Component<ZapReceiptProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
<Show when={event().description().content.length > 0}>
|
||||
<div class="ml-7 whitespace-pre-wrap break-all rounded border border-zinc-300 px-1 text-sm">
|
||||
<div class="ml-7 whitespace-pre-wrap break-all rounded border border-border px-1 text-sm">
|
||||
{event().description().content}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createSignal, type Component, type JSX, Show } from 'solid-js';
|
||||
|
||||
import ExclamationTriangle from 'heroicons/24/outline/exclamation-triangle.svg';
|
||||
|
||||
import { useTranslation } from '@/i18n/useTranslation';
|
||||
import { ContentWarning } from '@/nostr/event/TextNoteLike';
|
||||
|
||||
@@ -17,12 +19,14 @@ const ContentWarningDisplay: Component<ContentWarningDisplayProps> = (props) =>
|
||||
when={!props.contentWarning.contentWarning || showContentWarning()}
|
||||
fallback={
|
||||
<button
|
||||
class="mt-2 w-full rounded border p-2 text-center text-xs text-stone-600 shadow-sm hover:shadow"
|
||||
class="mt-2 flex w-full flex-col items-center rounded border border-border p-2 text-center text-xs text-fg-secondary"
|
||||
onClick={() => setShowContentWarning(true)}
|
||||
>
|
||||
{i18n()('post.contentWarning.show')}
|
||||
<span class="inline-block h-4 w-4">
|
||||
<ExclamationTriangle />
|
||||
</span>
|
||||
<span>{i18n()('post.contentWarning.show')}</span>
|
||||
<Show when={props.contentWarning.reason != null}>
|
||||
<br />
|
||||
<span>
|
||||
{i18n()('post.contentWarning.reason')}: {props.contentWarning.reason}
|
||||
</span>
|
||||
@@ -33,7 +37,7 @@ const ContentWarningDisplay: Component<ContentWarningDisplayProps> = (props) =>
|
||||
<div>{props.children}</div>
|
||||
<Show when={props.contentWarning.contentWarning}>
|
||||
<button
|
||||
class="text-xs text-stone-600 hover:text-stone-800"
|
||||
class="text-xs text-fg-secondary hover:text-fg-secondary/70"
|
||||
onClick={() => setShowContentWarning(false)}
|
||||
>
|
||||
隠す
|
||||
|
||||
@@ -20,7 +20,7 @@ const ImageDisplay: Component<ImageDisplayProps> = (props) => {
|
||||
when={!hidden()}
|
||||
fallback={
|
||||
<button
|
||||
class="rounded bg-stone-300 p-3 text-xs text-stone-600 hover:shadow"
|
||||
class="rounded bg-bg-tertiary p-3 text-xs text-fg-secondary hover:shadow"
|
||||
onClick={() => setHidden(false)}
|
||||
>
|
||||
{i18n()('post.showImage')}
|
||||
|
||||
@@ -12,7 +12,7 @@ const MentionedUserDisplay = (props: MentionedUserDisplayProps) => {
|
||||
showProfile(props.pubkey);
|
||||
};
|
||||
return (
|
||||
<button class="inline select-text text-blue-500 underline" onClick={handleClick}>
|
||||
<button class="inline select-text text-link underline" onClick={handleClick}>
|
||||
<GeneralUserMentionDisplay pubkey={props.pubkey} />
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -32,14 +32,24 @@ const youtubeUrl = (videoId: string): string => {
|
||||
const TwitterEmbed: Component<{ class?: string; href: string }> = (props) => {
|
||||
let twitterRef: HTMLQuoteElement | undefined;
|
||||
|
||||
const { getColorTheme } = useConfig();
|
||||
|
||||
createEffect(() => {
|
||||
if (isTwitterUrl(props.href)) {
|
||||
window.twttr?.widgets?.load(twitterRef);
|
||||
}
|
||||
});
|
||||
|
||||
const dataTheme = () => {
|
||||
const colorTheme = getColorTheme();
|
||||
if (colorTheme.brightness === 'dark') {
|
||||
return 'dark';
|
||||
}
|
||||
return 'light';
|
||||
};
|
||||
|
||||
return (
|
||||
<blockquote class="twitter-tweet" ref={twitterRef}>
|
||||
<blockquote ref={twitterRef} class="twitter-tweet" data-theme={dataTheme()}>
|
||||
<a
|
||||
class={props.class}
|
||||
href={twitterUrl(props.href)}
|
||||
@@ -61,16 +71,16 @@ const OgpEmbed: Component<{ class?: string; url: string }> = (props) => {
|
||||
<Show when={ogp()} fallback={<SafeLink class={props.class} href={props.url} />} keyed>
|
||||
{(ogpProps) => (
|
||||
<SafeLink href={props.url}>
|
||||
<div class="my-2 rounded-lg border transition-colors hover:bg-slate-100">
|
||||
<div class="my-2 rounded-lg border border-border transition-colors hover:bg-bg-tertiary">
|
||||
<img
|
||||
alt={ogpProps.title}
|
||||
class="max-w-full rounded-t-lg object-contain shadow"
|
||||
src={ogpProps.image}
|
||||
/>
|
||||
<div class="mb-1 p-1">
|
||||
<div class="text-xs text-slate-500">{new URL(ogpProps.url).host}</div>
|
||||
<div class="text-xs text-fg-secondary">{new URL(ogpProps.url).host}</div>
|
||||
<div class="text-sm">{ogpProps.title}</div>
|
||||
<div class="text-xs text-slate-500">{ogpProps.description}</div>
|
||||
<div class="text-xs text-fg-secondary">{ogpProps.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</SafeLink>
|
||||
|
||||
@@ -57,14 +57,14 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
||||
if (isWebSocketUrl(item.content)) {
|
||||
return (
|
||||
<button
|
||||
class="select-text text-blue-500 underline"
|
||||
class="select-text text-link underline"
|
||||
onClick={() => addRelayColumn(item.content)}
|
||||
>
|
||||
{item.content}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return <PreviewedLink class="text-blue-500 underline" href={item.content} />;
|
||||
return <PreviewedLink class="text-link underline" href={item.content} />;
|
||||
}
|
||||
if (item.type === 'TagReferenceResolved') {
|
||||
if (item.reference == null) {
|
||||
@@ -83,7 +83,7 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
||||
if (item.type === 'Bech32Entity') {
|
||||
if (item.data.type === 'note' && props.embedding) {
|
||||
return (
|
||||
<div class="my-1 rounded border p-1">
|
||||
<div class="my-1 rounded border border-border p-1">
|
||||
<EventDisplayById
|
||||
eventId={item.data.data}
|
||||
actions={false}
|
||||
@@ -95,7 +95,7 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
||||
}
|
||||
if (item.data.type === 'nevent' && props.embedding) {
|
||||
return (
|
||||
<div class="my-1 rounded border p-1">
|
||||
<div class="my-1 rounded border border-border p-1">
|
||||
<EventDisplayById eventId={item.data.data.id} actions={false} embedding={false} />
|
||||
</div>
|
||||
);
|
||||
@@ -109,20 +109,17 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
||||
if (item.data.type === 'nrelay') {
|
||||
const url: string = item.data.data;
|
||||
return (
|
||||
<button
|
||||
class="select-text text-blue-500 underline"
|
||||
onClick={() => addRelayColumn(url)}
|
||||
>
|
||||
<button class="select-text text-link underline" onClick={() => addRelayColumn(url)}>
|
||||
{url} ({item.content})
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return <span class="text-blue-500 underline">{item.content}</span>;
|
||||
return <span class="text-link underline">{item.content}</span>;
|
||||
}
|
||||
if (item.type === 'HashTag') {
|
||||
return (
|
||||
<button
|
||||
class="select-text text-blue-500 underline"
|
||||
class="select-text text-link underline"
|
||||
onClick={() => addHashTagColumn(item.content)}
|
||||
>
|
||||
{item.content}
|
||||
|
||||
@@ -19,7 +19,7 @@ const VideoDisplay: Component<VideoDisplayProps> = (props) => {
|
||||
when={!hidden()}
|
||||
fallback={
|
||||
<button
|
||||
class="rounded bg-stone-300 p-3 text-xs text-stone-600 hover:shadow"
|
||||
class="rounded bg-bg-tertiary p-3 text-xs text-fg-secondary hover:shadow"
|
||||
onClick={() => setHidden(false)}
|
||||
>
|
||||
{i18n()('post.showVideo')}
|
||||
|
||||
@@ -56,7 +56,7 @@ const About: Component<AboutProps> = (props) => {
|
||||
<p class="my-4">
|
||||
おかしな動作を見つけたら
|
||||
<a
|
||||
class="text-blue-500 underline"
|
||||
class="text-link underline"
|
||||
href="https://github.com/syusui-s/rabbit/issues/new/choose"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -70,7 +70,7 @@ const About: Component<AboutProps> = (props) => {
|
||||
|
||||
<p class="my-4">
|
||||
ソースコードは
|
||||
<SafeLink class="text-blue-400 underline" href="https://github.com/syusui-s/rabbit">
|
||||
<SafeLink class="text-link underline" href="https://github.com/syusui-s/rabbit">
|
||||
GitHub
|
||||
</SafeLink>
|
||||
で入手できます。
|
||||
@@ -81,7 +81,7 @@ const About: Component<AboutProps> = (props) => {
|
||||
<p class="my-4">
|
||||
Copyright (C) 2023 Shusui Moyatani and{' '}
|
||||
<SafeLink
|
||||
class="text-blue-400 underline"
|
||||
class="text-link underline"
|
||||
href="https://github.com/syusui-s/rabbit/graphs/contributors"
|
||||
>
|
||||
Rabbit contributors
|
||||
@@ -104,17 +104,15 @@ const About: Component<AboutProps> = (props) => {
|
||||
<p class="my-4">
|
||||
あなたは、このプログラムに付随してGNUアフェロー一般公衆ライセンスのコピーを受け取っていることでしょう。
|
||||
そうでなければ、
|
||||
<a class="link" href="https://www.gnu.org/licenses/">
|
||||
https://www.gnu.org/licenses/
|
||||
</a>
|
||||
<SafeLink href="https://www.gnu.org/licenses/" />
|
||||
をご参照ください。
|
||||
</p>
|
||||
|
||||
<a class="text-blue-500 underline" href="https://gpl.mhatta.org/agpl.ja.html">
|
||||
<a class="text-link underline" href="https://gpl.mhatta.org/agpl.ja.html">
|
||||
参考訳
|
||||
</a>
|
||||
|
||||
<pre class="max-h-96 overflow-scroll rounded bg-zinc-100 p-4 text-xs">
|
||||
<pre class="scorllbar max-h-96 overflow-scroll rounded bg-bg-secondary p-4 text-xs">
|
||||
{packageInfo()?.self.licenseText}
|
||||
</pre>
|
||||
|
||||
@@ -126,7 +124,7 @@ const About: Component<AboutProps> = (props) => {
|
||||
<h3 class="mb-2 mt-4 font-mono">
|
||||
{p.name}@{p.version} ({p.licenseSpdx})
|
||||
</h3>
|
||||
<pre class="max-h-96 overflow-scroll rounded bg-zinc-100 p-4 text-xs">
|
||||
<pre class="scrollbar max-h-96 overflow-scroll rounded bg-bg-secondary p-4 text-xs">
|
||||
{p.licenseText}
|
||||
</pre>
|
||||
</>
|
||||
|
||||
@@ -81,7 +81,7 @@ const AddColumn: Component<AddColumnProps> = (props) => {
|
||||
<BasicModal onClose={props.onClose}>
|
||||
<div class="flex flex-wrap p-4">
|
||||
<button
|
||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
|
||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 hover:text-primary sm:basis-1/4"
|
||||
onClick={() => addFollowingColumn()}
|
||||
>
|
||||
<span class="inline-block h-8 w-8">
|
||||
@@ -90,7 +90,7 @@ const AddColumn: Component<AddColumnProps> = (props) => {
|
||||
{i18n()('column.home')}
|
||||
</button>
|
||||
<button
|
||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
|
||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 hover:text-primary sm:basis-1/4"
|
||||
onClick={() => addNotificationColumn()}
|
||||
>
|
||||
<span class="inline-block h-8 w-8">
|
||||
@@ -99,7 +99,7 @@ const AddColumn: Component<AddColumnProps> = (props) => {
|
||||
{i18n()('column.notification')}
|
||||
</button>
|
||||
<button
|
||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
|
||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 hover:text-primary sm:basis-1/4"
|
||||
onClick={() => addJapanRelaysColumn()}
|
||||
>
|
||||
<span class="inline-block h-8 w-8">
|
||||
@@ -130,7 +130,7 @@ const AddColumn: Component<AddColumnProps> = (props) => {
|
||||
</button>
|
||||
*/}
|
||||
<button
|
||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
|
||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 hover:text-primary sm:basis-1/4"
|
||||
onClick={() => addSearchColumn()}
|
||||
>
|
||||
<span class="inline-block h-8 w-8">
|
||||
@@ -139,7 +139,7 @@ const AddColumn: Component<AddColumnProps> = (props) => {
|
||||
{i18n()('column.search')}
|
||||
</button>
|
||||
<button
|
||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
|
||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 hover:text-primary sm:basis-1/4"
|
||||
onClick={() => addMyPostsColumn()}
|
||||
>
|
||||
<span class="inline-block h-8 w-8">
|
||||
@@ -148,7 +148,7 @@ const AddColumn: Component<AddColumnProps> = (props) => {
|
||||
{i18n()('column.myPosts')}
|
||||
</button>
|
||||
<button
|
||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 sm:basis-1/4"
|
||||
class="flex basis-1/2 flex-col items-center gap-2 py-8 hover:text-primary sm:basis-1/4"
|
||||
onClick={() => addMyReactionsColumn()}
|
||||
>
|
||||
<span class="inline-block h-8 w-8">
|
||||
|
||||
@@ -14,7 +14,7 @@ const BasicModal: Component<BasicModalProps> = (props) => (
|
||||
<Modal onClose={() => props.onClose?.()}>
|
||||
<div class="w-[640px] max-w-full">
|
||||
<button
|
||||
class="w-full pt-1 text-start text-stone-800"
|
||||
class="w-full pt-1 text-start text-fg-secondary/50"
|
||||
aria-label="Close"
|
||||
onClick={() => props.onClose?.()}
|
||||
>
|
||||
@@ -24,7 +24,7 @@ const BasicModal: Component<BasicModalProps> = (props) => (
|
||||
</Show>
|
||||
</span>
|
||||
</button>
|
||||
<div class="flex max-h-[calc(100vh-6em)] flex-col overflow-y-scroll rounded-xl border bg-white text-stone-700 shadow-lg">
|
||||
<div class="scrollbar flex max-h-[calc(100vh-6em)] flex-col overflow-y-scroll rounded-xl border border-border bg-bg text-fg shadow-lg">
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import XMark from 'heroicons/24/outline/x-mark.svg';
|
||||
|
||||
import BasicModal from '@/components/modal/BasicModal';
|
||||
import UserNameDisplay from '@/components/UserDisplayName';
|
||||
import { colorThemes } from '@/core/colorThemes';
|
||||
import useConfig, { type Config } from '@/core/useConfig';
|
||||
import useModalState from '@/hooks/useModalState';
|
||||
import { useTranslation } from '@/i18n/useTranslation';
|
||||
@@ -36,7 +37,7 @@ const ProfileSection = () => {
|
||||
<h3 class="font-bold">{i18n()('config.profile.profile')}</h3>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="rounded border border-rose-300 px-4 py-2 font-bold text-rose-300"
|
||||
class="rounded border border-primary px-4 py-2 font-bold text-primary"
|
||||
onClick={() =>
|
||||
ensureNonNull([pubkey()])(([pubkeyNonNull]) => {
|
||||
showProfile(pubkeyNonNull);
|
||||
@@ -46,7 +47,7 @@ const ProfileSection = () => {
|
||||
{i18n()('config.profile.openProfile')}
|
||||
</button>
|
||||
<button
|
||||
class="rounded border border-rose-300 px-4 py-2 font-bold text-rose-300"
|
||||
class="rounded border border-primary px-4 py-2 font-bold text-primary"
|
||||
onClick={() => showProfileEdit()}
|
||||
>
|
||||
{i18n()('config.profile.editProfile')}
|
||||
@@ -116,14 +117,15 @@ const RelayConfig = () => {
|
||||
</ul>
|
||||
<form class="flex gap-2" onSubmit={handleClickAddRelay}>
|
||||
<input
|
||||
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||
class="flex-1 rounded-md border border-border bg-bg ring-border placeholder:text-fg-secondary focus:border-border focus:ring-primary"
|
||||
type="text"
|
||||
name="relayUrl"
|
||||
placeholder="wss://..."
|
||||
value={relayUrlInput()}
|
||||
pattern={RelayUrlRegex}
|
||||
onChange={(ev) => setRelayUrlInput(ev.currentTarget.value)}
|
||||
/>
|
||||
<button type="submit" class="rounded bg-rose-300 p-2 font-bold text-white">
|
||||
<button type="submit" class="rounded bg-primary p-2 font-bold text-primary-fg">
|
||||
{i18n()('config.relays.addRelay')}
|
||||
</button>
|
||||
</form>
|
||||
@@ -132,7 +134,7 @@ const RelayConfig = () => {
|
||||
<h3 class="pb-1 font-bold">{i18n()('config.relays.importRelays')}</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded bg-rose-300 p-2 font-bold text-white"
|
||||
class="rounded bg-primary p-2 font-bold text-primary-fg"
|
||||
onClick={() => {
|
||||
importFromNIP07().catch((err) => {
|
||||
console.error('failed to import relays', err);
|
||||
@@ -147,6 +149,47 @@ const RelayConfig = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ColorThemeConfig = () => {
|
||||
const i18n = useTranslation();
|
||||
const { config, setConfig } = useConfig();
|
||||
|
||||
const isCurrentlyUsing = (id: string) => {
|
||||
const colorThemeConfig = config().colorTheme;
|
||||
if (colorThemeConfig.type === 'specific') {
|
||||
return colorThemeConfig.id === id;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const updateColorTheme = (id: string) => {
|
||||
setConfig((current) => ({ ...current, colorTheme: { type: 'specific', id } }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="py-2">
|
||||
<h3 class="font-bold">{i18n()('config.display.colorTheme')}</h3>
|
||||
<div class="flex max-h-[25vh] flex-col overflow-scroll rounded-md border border-border">
|
||||
<For each={Object.values(colorThemes)}>
|
||||
{(colorTheme) => (
|
||||
<button
|
||||
type="button"
|
||||
class="border-t border-border px-2 py-1 text-left"
|
||||
classList={{
|
||||
'bg-primary': isCurrentlyUsing(colorTheme.id),
|
||||
'text-primary-fg': isCurrentlyUsing(colorTheme.id),
|
||||
'text-fg': !isCurrentlyUsing(colorTheme.id),
|
||||
}}
|
||||
onClick={() => updateColorTheme(colorTheme.id)}
|
||||
>
|
||||
{colorTheme.name}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DateFormatConfig = () => {
|
||||
const i18n = useTranslation();
|
||||
const { config, setConfig } = useConfig();
|
||||
@@ -186,12 +229,12 @@ const DateFormatConfig = () => {
|
||||
<div class="flex flex-1 flex-row items-center gap-1 sm:flex-col">
|
||||
<button
|
||||
type="button"
|
||||
class="w-48 rounded border border-rose-300 p-2 font-bold sm:w-full"
|
||||
class="w-48 rounded border border-primary p-2 font-bold sm:w-full"
|
||||
classList={{
|
||||
'bg-rose-300': config().dateFormat === id,
|
||||
'text-white': config().dateFormat === id,
|
||||
'bg-white': config().dateFormat !== id,
|
||||
'text-rose-300': config().dateFormat !== id,
|
||||
'bg-primary': config().dateFormat === id,
|
||||
'text-primary-fg': config().dateFormat === id,
|
||||
'bg-bg': config().dateFormat !== id,
|
||||
'text-primary': config().dateFormat !== id,
|
||||
}}
|
||||
onClick={() => updateDateFormat(id)}
|
||||
>
|
||||
@@ -211,17 +254,17 @@ const ToggleButton = (props: {
|
||||
onClick: JSX.EventHandler<HTMLButtonElement, MouseEvent>;
|
||||
}) => (
|
||||
<button
|
||||
class="flex h-[24px] w-[48px] items-center rounded-full border border-primary px-1"
|
||||
class="flex h-[24px] w-[48px] items-center rounded-full border border-primary/80 px-1"
|
||||
classList={{
|
||||
'bg-white': !props.value,
|
||||
'bg-bg-tertiary': !props.value,
|
||||
'justify-start': !props.value,
|
||||
'bg-rose-300': props.value,
|
||||
'bg-primary': props.value,
|
||||
'justify-end': props.value,
|
||||
}}
|
||||
area-label={props.value}
|
||||
onClick={(event) => props.onClick(event)}
|
||||
>
|
||||
<span class="m-[-2px] inline-block h-5 w-5 rounded-full border border-primary bg-white shadow" />
|
||||
<span class="m-[-3px] inline-block h-5 w-5 rounded-full border bg-primary-fg shadow" />
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -301,7 +344,7 @@ const EmojiConfig = () => {
|
||||
<label class="flex flex-1 items-center gap-1">
|
||||
<div class="w-9">{i18n()('config.customEmoji.shortcode')}</div>
|
||||
<input
|
||||
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||
class="flex-1 rounded-md border-border bg-bg placeholder:text-fg-secondary focus:border-border focus:ring-primary"
|
||||
type="text"
|
||||
name="shortcode"
|
||||
placeholder="smiley"
|
||||
@@ -314,7 +357,7 @@ const EmojiConfig = () => {
|
||||
<label class="flex flex-1 items-center gap-1">
|
||||
<div class="w-9">{i18n()('config.customEmoji.url')}</div>
|
||||
<input
|
||||
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||
class="flex-1 rounded-md border-border bg-bg placeholder:text-fg-secondary focus:border-border focus:ring-primary"
|
||||
type="text"
|
||||
name="url"
|
||||
value={urlInput()}
|
||||
@@ -324,7 +367,10 @@ const EmojiConfig = () => {
|
||||
onChange={(ev) => setUrlInput(ev.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" class="w-24 self-end rounded bg-rose-300 p-2 font-bold text-white">
|
||||
<button
|
||||
type="submit"
|
||||
class="w-24 self-end rounded bg-primary p-2 font-bold text-primary-fg"
|
||||
>
|
||||
{i18n()('config.customEmoji.addEmoji')}
|
||||
</button>
|
||||
</form>
|
||||
@@ -359,13 +405,16 @@ const EmojiImport = () => {
|
||||
<p>{i18n()('config.customEmoji.emojiImportDescription')}</p>
|
||||
<form class="flex flex-col gap-2" onSubmit={handleClickSaveEmoji}>
|
||||
<textarea
|
||||
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||
class="flex-1 rounded-md border-border bg-bg placeholder:text-fg-secondary focus:border-border focus:ring-primary"
|
||||
name="json"
|
||||
value={jsonInput()}
|
||||
placeholder='{ "smiley": "https://example.com/smiley.png" }'
|
||||
onChange={(ev) => setJSONInput(ev.currentTarget.value)}
|
||||
/>
|
||||
<button type="submit" class="w-24 self-end rounded bg-rose-300 p-2 font-bold text-white">
|
||||
<button
|
||||
type="submit"
|
||||
class="w-24 self-end rounded bg-primary p-2 font-bold text-primary-fg"
|
||||
>
|
||||
{i18n()('config.customEmoji.importEmoji')}
|
||||
</button>
|
||||
</form>
|
||||
@@ -421,13 +470,13 @@ const MuteConfig = () => {
|
||||
</ul>
|
||||
<form class="flex gap-2" onSubmit={handleClickAddKeyword}>
|
||||
<input
|
||||
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||
class="flex-1 rounded-md border border-border bg-bg ring-border focus:border-border focus:ring-primary"
|
||||
type="text"
|
||||
name="keyword"
|
||||
value={keywordInput()}
|
||||
onChange={(ev) => setKeywordInput(ev.currentTarget.value)}
|
||||
/>
|
||||
<button type="submit" class="rounded bg-rose-300 p-2 font-bold text-white">
|
||||
<button type="submit" class="rounded bg-primary p-2 font-bold text-primary-fg">
|
||||
{i18n()('config.mute.add')}
|
||||
</button>
|
||||
</form>
|
||||
@@ -551,6 +600,7 @@ const ConfigUI = (props: ConfigProps) => {
|
||||
icon: () => <PaintBrush />,
|
||||
render: () => (
|
||||
<>
|
||||
<ColorThemeConfig />
|
||||
<DateFormatConfig />
|
||||
<ReactionConfig />
|
||||
<EmbeddingConfig />
|
||||
@@ -594,7 +644,7 @@ const ConfigUI = (props: ConfigProps) => {
|
||||
{(menuItem, i) => (
|
||||
<li class="w-full">
|
||||
<button
|
||||
class="flex w-full gap-2 py-3 hover:text-rose-400"
|
||||
class="flex w-full gap-2 py-3 hover:text-primary"
|
||||
onClick={() => setMenuIndex(i)}
|
||||
>
|
||||
<span class="inline-block h-6 w-6">{menuItem.icon()}</span>
|
||||
|
||||
@@ -24,14 +24,18 @@ const EventDebugModal: Component<EventDebugModalProps> = (props) => {
|
||||
<BasicModal onClose={props.onClose}>
|
||||
<div class="p-2">
|
||||
<h2 class="text-lg font-bold">JSON</h2>
|
||||
<pre class="whitespace-pre-wrap break-all rounded-lg border p-4 text-xs">{json()}</pre>
|
||||
<pre class="whitespace-pre-wrap break-all rounded-lg border border-border p-4 text-xs">
|
||||
{json()}
|
||||
</pre>
|
||||
<div class="flex justify-end">
|
||||
<Copy class="h-4 w-4" text={json()} />
|
||||
<Copy class="h-4 w-4 hover:text-primary" text={json()} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<h2 class="text-lg font-bold">Found in these relays</h2>
|
||||
<pre class="whitespace-pre-wrap break-all rounded-lg border p-2 text-xs">{seenOn()}</pre>
|
||||
<pre class="whitespace-pre-wrap break-all rounded-lg border border-border p-2 text-xs">
|
||||
{seenOn()}
|
||||
</pre>
|
||||
</div>
|
||||
</BasicModal>
|
||||
);
|
||||
|
||||
@@ -302,7 +302,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
<Match when={props.pubkey === myPubkey()}>
|
||||
<button
|
||||
class="rounded-full border border-primary px-4 py-2
|
||||
text-center font-bold text-primary hover:bg-primary hover:text-white sm:w-20"
|
||||
text-center font-bold text-primary hover:bg-primary hover:text-primary-fg sm:w-20"
|
||||
onClick={() => showProfileEdit()}
|
||||
>
|
||||
{i18n()('profile.editProfile')}
|
||||
@@ -320,8 +320,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
</Match>
|
||||
<Match when={following()}>
|
||||
<button
|
||||
class="rounded-full border border-primary bg-primary px-4 py-2
|
||||
text-center font-bold text-white hover:bg-rose-500 sm:w-36"
|
||||
class="rounded-full border border-primary bg-primary px-4 py-2 text-center font-bold text-primary-fg hover:bg-primary-hover sm:w-36"
|
||||
onMouseEnter={() => setHoverFollowButton(true)}
|
||||
onMouseLeave={() => setHoverFollowButton(false)}
|
||||
onClick={() => unfollow()}
|
||||
@@ -334,8 +333,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
</Match>
|
||||
<Match when={!following()}>
|
||||
<button
|
||||
class="w-28 rounded-full border border-primary px-4 py-2 text-primary
|
||||
hover:border-rose-400 hover:text-rose-400"
|
||||
class="w-28 rounded-full border border-primary px-4 py-2 text-primary hover:border-primary-hover hover:text-primary-hover"
|
||||
onClick={() => follow()}
|
||||
disabled={updateContactsMutation.isPending}
|
||||
>
|
||||
@@ -344,10 +342,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
</Match>
|
||||
</Switch>
|
||||
<ContextMenu menu={menu}>
|
||||
<button
|
||||
class="w-10 rounded-full border border-primary p-2 text-primary
|
||||
hover:border-rose-400 hover:text-rose-400"
|
||||
>
|
||||
<button class="w-10 rounded-full border border-primary p-2 text-primary hover:border-primary-hover hover:text-primary-hover">
|
||||
<EllipsisHorizontal />
|
||||
</button>
|
||||
</ContextMenu>
|
||||
@@ -378,7 +373,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
{nip05Identifier()?.ident}
|
||||
<Switch
|
||||
fallback={
|
||||
<span class="inline-block h-4 w-4 text-rose-500">
|
||||
<span class="inline-block h-4 w-4 text-danger">
|
||||
<ExclamationCircle />
|
||||
</span>
|
||||
}
|
||||
@@ -389,7 +384,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={isVerified()}>
|
||||
<span class="inline-block h-4 w-4 text-blue-400">
|
||||
<span class="inline-block h-4 w-4 text-link">
|
||||
<CheckCircle />
|
||||
</span>
|
||||
</Match>
|
||||
@@ -409,7 +404,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<div class="flex border-t px-4 py-2">
|
||||
<div class="flex border-t border-border px-4 py-2">
|
||||
<button class="flex flex-1 flex-col items-start" onClick={() => setModal('Following')}>
|
||||
<div class="text-sm">{i18n()('profile.following')}</div>
|
||||
<div class="text-xl">
|
||||
@@ -429,7 +424,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
when={showFollowers()}
|
||||
fallback={
|
||||
<button
|
||||
class="text-sm hover:text-stone-800 hover:underline"
|
||||
class="text-sm hover:text-fg-secondary"
|
||||
onClick={() => setShowFollowers(true)}
|
||||
>
|
||||
{i18n()('profile.loadFollowers')}
|
||||
@@ -444,14 +439,14 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={(profile()?.website ?? '').length > 0}>
|
||||
<ul class="border-t px-5 py-2 text-xs">
|
||||
<ul class="border-t border-border px-5 py-2 text-xs">
|
||||
<Show when={profile()?.website} keyed>
|
||||
{(website) => (
|
||||
<li class="flex items-center gap-1">
|
||||
<span class="inline-block h-4 w-4" area-label="website" title="website">
|
||||
<GlobeAlt />
|
||||
</span>
|
||||
<SafeLink class="text-blue-500 underline" href={website} />
|
||||
<SafeLink class="text-link underline" href={website} />
|
||||
</li>
|
||||
)}
|
||||
</Show>
|
||||
@@ -462,7 +457,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||
<UserList data={userFollowingPubkeys()} pubkeyExtractor={(e) => e} onClose={closeModal} />
|
||||
</Match>
|
||||
</Switch>
|
||||
<ul class="border-t p-1">
|
||||
<ul class="border-t border-border p-1">
|
||||
<Timeline events={events()} />
|
||||
</ul>
|
||||
</BasicModal>
|
||||
|
||||
@@ -78,8 +78,6 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
|
||||
const loading = () => query.isPending || mutation.isPending;
|
||||
const disabled = () => loading();
|
||||
|
||||
setInterval(() => console.log(query.isPending, mutation.isPending), 1000);
|
||||
|
||||
const otherProperties = () =>
|
||||
omit(profile(), [
|
||||
'picture',
|
||||
@@ -170,7 +168,7 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
|
||||
{i18n()('profile.edit.icon')}
|
||||
</label>
|
||||
<input
|
||||
class="w-full rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||
class="w-full rounded-md border-border bg-bg ring-border focus:border-border focus:ring-primary"
|
||||
type="text"
|
||||
id="picture"
|
||||
name="picture"
|
||||
@@ -186,7 +184,7 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
|
||||
{i18n()('profile.edit.banner')}
|
||||
</label>
|
||||
<input
|
||||
class="w-full rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||
class="w-full rounded-md border-border bg-bg focus:border-border focus:ring-primary"
|
||||
type="text"
|
||||
id="banner"
|
||||
name="banner"
|
||||
@@ -204,12 +202,11 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
|
||||
<div class="flex w-full items-center gap-2">
|
||||
<span>@</span>
|
||||
<input
|
||||
class="flex-1 rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||
class="flex-1 rounded-md border-border bg-bg ring-border focus:border-border focus:ring-primary"
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={name()}
|
||||
// pattern="^[a-zA-Z_][a-zA-Z0-9_]+$"
|
||||
maxlength="32"
|
||||
required
|
||||
disabled={disabled()}
|
||||
@@ -223,7 +220,7 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
|
||||
{i18n()('profile.edit.displayName')}
|
||||
</label>
|
||||
<input
|
||||
class="w-full rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||
class="w-full rounded-md border-border bg-bg ring-border focus:border-border focus:ring-primary"
|
||||
type="text"
|
||||
name="displayName"
|
||||
value={displayName()}
|
||||
@@ -238,7 +235,7 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
|
||||
{i18n()('profile.edit.about')}
|
||||
</label>
|
||||
<textarea
|
||||
class="w-full rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||
class="w-full rounded-md border-border bg-bg ring-border focus:border-border focus:ring-primary"
|
||||
name="about"
|
||||
value={about()}
|
||||
rows="5"
|
||||
@@ -251,7 +248,7 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
|
||||
{i18n()('profile.edit.website')}
|
||||
</label>
|
||||
<input
|
||||
class="w-full rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||
class="w-full rounded-md border-border bg-bg ring-border focus:border-border focus:ring-primary"
|
||||
type="text"
|
||||
name="website"
|
||||
value={website()}
|
||||
@@ -266,7 +263,7 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
|
||||
{i18n()('profile.edit.nip05')}
|
||||
</label>
|
||||
<input
|
||||
class="w-full rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||
class="w-full rounded-md border-border bg-bg ring-border focus:border-border focus:ring-primary"
|
||||
type="text"
|
||||
name="nip05"
|
||||
value={nip05()}
|
||||
@@ -283,7 +280,7 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
|
||||
</label>
|
||||
<span class="text-xs">{i18n()('profile.edit.lightningAddressDescription')}</span>
|
||||
<input
|
||||
class="w-full rounded-md focus:border-rose-100 focus:ring-rose-300"
|
||||
class="w-full rounded-md border-border bg-bg ring-border focus:border-border focus:ring-primary"
|
||||
type="text"
|
||||
name="website"
|
||||
value={lightningAddress()}
|
||||
@@ -312,14 +309,18 @@ const ProfileEdit: Component<ProfileEditProps> = (props) => {
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded bg-rose-300 p-2 font-bold text-white hover:bg-rose-400"
|
||||
class="rounded p-2 font-bold text-primary-fg hover:bg-primary-hover"
|
||||
classList={{
|
||||
'bg-primary': !mutation.isPending,
|
||||
'bg-primary-disabled': mutation.isPending,
|
||||
}}
|
||||
disabled={mutation.isPending}
|
||||
>
|
||||
{i18n()('profile.edit.save')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded border border-rose-300 p-2 font-bold text-rose-300 hover:border-rose-400 hover:text-rose-400"
|
||||
class="rounded border border-primary p-2 font-bold text-primary hover:border-primary-hover hover:text-primary-hover"
|
||||
onClick={() => props.onClose()}
|
||||
>
|
||||
{i18n()('profile.edit.cancel')}
|
||||
|
||||
@@ -24,7 +24,7 @@ const UserList = <T,>(props: UserListProps<T>): JSX.Element => {
|
||||
{(e) => {
|
||||
const pubkey = () => props.pubkeyExtractor(e);
|
||||
return (
|
||||
<div class="flex border-t py-1">
|
||||
<div class="flex border-t border-border py-1">
|
||||
<Show when={props.renderInfo} keyed>
|
||||
{(render) => render(e)}
|
||||
</Show>
|
||||
|
||||
@@ -28,10 +28,7 @@ const Copy: Component<CopyProps> = (props) => {
|
||||
<ClipboardDocument />
|
||||
</button>
|
||||
<Show when={showPopup()}>
|
||||
<div
|
||||
class="absolute left-[-2.5rem] top-[-1.5rem] rounded
|
||||
bg-rose-300 p-1 text-xs font-bold text-white shadow"
|
||||
>
|
||||
<div class="absolute left-[-2.5rem] top-[-1.5rem] rounded bg-primary p-1 text-xs font-bold text-primary-fg shadow">
|
||||
Copied!
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
31
src/core/colorThemes.ts
Normal file
31
src/core/colorThemes.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export type ColorTheme = {
|
||||
id: string;
|
||||
name: string;
|
||||
brightness: 'light' | 'dark';
|
||||
className?: string;
|
||||
rabbitIconPath: string;
|
||||
};
|
||||
|
||||
export const colorThemes: Record<string, ColorTheme> = {
|
||||
sakura: {
|
||||
id: 'sakura',
|
||||
name: 'Sakura',
|
||||
brightness: 'light',
|
||||
className: 'theme-sakura',
|
||||
rabbitIconPath: 'images/rabbit_app_256.png',
|
||||
},
|
||||
cinnamon: {
|
||||
id: 'cinnamon',
|
||||
name: 'Cinnamon',
|
||||
brightness: 'light',
|
||||
className: 'theme-cinnamon',
|
||||
rabbitIconPath: 'images/rabbit_muted_256.png',
|
||||
},
|
||||
yozakura: {
|
||||
id: 'yozakura',
|
||||
name: 'Yozakura',
|
||||
brightness: 'dark',
|
||||
className: 'theme-yozakura',
|
||||
rabbitIconPath: 'images/rabbit_256.png',
|
||||
},
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import uniq from 'lodash/uniq';
|
||||
import * as Kind from 'nostr-tools/kinds';
|
||||
import { type Event as NostrEvent } from 'nostr-tools/pure';
|
||||
|
||||
import { colorThemes, type ColorTheme } from '@/core/colorThemes';
|
||||
import {
|
||||
ColumnType,
|
||||
createFollowingColumn,
|
||||
@@ -26,10 +27,16 @@ export type CustomEmojiConfig = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type ColorThemeConfig = {
|
||||
type: 'specific';
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type Config = {
|
||||
relayUrls: string[];
|
||||
columns: ColumnType[];
|
||||
customEmojis: Record<string, CustomEmojiConfig>;
|
||||
colorTheme: ColorThemeConfig;
|
||||
dateFormat: 'relative' | 'absolute-long' | 'absolute-short';
|
||||
keepOpenPostForm: boolean;
|
||||
useEmojiReaction: boolean;
|
||||
@@ -48,6 +55,8 @@ export type Config = {
|
||||
type UseConfig = {
|
||||
config: Accessor<Config>;
|
||||
setConfig: Setter<Config>;
|
||||
// display
|
||||
getColorTheme: () => ColorTheme;
|
||||
// relay
|
||||
addRelay: (url: string) => void;
|
||||
removeRelay: (url: string) => void;
|
||||
@@ -83,6 +92,7 @@ const InitialConfig = (): Config => ({
|
||||
relayUrls: initialRelays(),
|
||||
columns: [],
|
||||
customEmojis: {},
|
||||
colorTheme: { type: 'specific', id: 'sakura' },
|
||||
dateFormat: 'relative',
|
||||
keepOpenPostForm: false,
|
||||
useEmojiReaction: true,
|
||||
@@ -114,6 +124,14 @@ const [config, setConfig] = createRoot(() =>
|
||||
const useConfig = (): UseConfig => {
|
||||
const i18n = useTranslation();
|
||||
|
||||
const getColorTheme = (): ColorTheme | null => {
|
||||
const colorThemeConfig = config.colorTheme;
|
||||
if (colorThemeConfig.type === 'specific' && colorThemes[colorThemeConfig.id] != null) {
|
||||
return colorThemes[colorThemeConfig.id];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const addRelay = (relayUrl: string) => {
|
||||
setConfig('relayUrls', (current) => uniq([...current, relayUrl]));
|
||||
};
|
||||
@@ -233,6 +251,8 @@ const useConfig = (): UseConfig => {
|
||||
return {
|
||||
config: () => config,
|
||||
setConfig,
|
||||
// display
|
||||
getColorTheme,
|
||||
// relay
|
||||
addRelay,
|
||||
removeRelay,
|
||||
|
||||
23
src/hooks/useColorTheme.ts
Normal file
23
src/hooks/useColorTheme.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createEffect, onCleanup } from 'solid-js';
|
||||
|
||||
import useConfig from '@/core/useConfig';
|
||||
|
||||
export const useColorTheme = (el: HTMLElement) => {
|
||||
const { getColorTheme } = useConfig();
|
||||
|
||||
createEffect(() => {
|
||||
const colorTheme = getColorTheme();
|
||||
if (colorTheme == null) return;
|
||||
|
||||
const { className } = colorTheme;
|
||||
if (className != null) {
|
||||
el.classList.add(className);
|
||||
|
||||
onCleanup(() => {
|
||||
el.classList.remove(className);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default useColorTheme;
|
||||
@@ -26,7 +26,7 @@ const useEmojiComplete = () => {
|
||||
},
|
||||
template: (config: CustomEmojiConfig) => {
|
||||
const e = (
|
||||
<div class="flex gap-1 border-b px-2 py-1">
|
||||
<div class="flex gap-1 border-b border-border px-2 py-1">
|
||||
<img class="h-6 max-w-[3rem]" src={config.url} alt={config.shortcode} />
|
||||
<div>{config.shortcode}</div>
|
||||
</div>
|
||||
@@ -38,10 +38,10 @@ const useEmojiComplete = () => {
|
||||
],
|
||||
{
|
||||
dropdown: {
|
||||
className: 'bg-white shadow rounded',
|
||||
className: 'bg-bg shadow rounded',
|
||||
item: {
|
||||
className: 'cursor-pointer',
|
||||
activeClassName: 'bg-rose-100 cursor-pointer',
|
||||
activeClassName: 'bg-bg-tertiary cursor-pointer',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
105
src/index.css
105
src/index.css
@@ -2,36 +2,99 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* LIGHT Sakura */
|
||||
.theme-sakura {
|
||||
--color-fg: 38 25 23;
|
||||
--color-fg-secondary: 87 83 78;
|
||||
--color-fg-tertiary: 168 162 158;
|
||||
--color-bg: 255 255 255;
|
||||
--color-bg-secondary: 168 162 158;
|
||||
--color-bg-tertiary: 235 235 233;
|
||||
--color-primary: 253 164 175;
|
||||
--color-primary-fg: 255 255 255;
|
||||
--color-primary-disabled: 254 205 211;
|
||||
--color-primary-hover: 253 155 167;
|
||||
--color-danger: 244 63 94;
|
||||
--color-link: 59 130 246;
|
||||
--color-border: 231 229 228;
|
||||
--color-r-sidebar: 255 228 230;
|
||||
--color-r-reaction: 251 113 133;
|
||||
--color-r-repost: 74 222 128;
|
||||
--color-scroll-thumb: 254 205 211;
|
||||
--color-scroll-bg: 255 255 255 / 0.7;
|
||||
}
|
||||
|
||||
/* LIGHT Cinnamon */
|
||||
.theme-cinnamon {
|
||||
--color-fg: 49 38 38;
|
||||
--color-fg-secondary: 87 80 58;
|
||||
--color-fg-tertiary: 168 162 158;
|
||||
--color-bg: 234 220 211;
|
||||
--color-bg-secondary: 168 162 158;
|
||||
--color-bg-tertiary: 220 205 190;
|
||||
--color-primary: 67 57 56;
|
||||
--color-primary-fg: 255 255 255;
|
||||
--color-primary-disabled: 100 90 90;
|
||||
--color-primary-hover: 80 64 63;
|
||||
--color-danger: 244 63 94;
|
||||
--color-link: 74 149 188;
|
||||
--color-border: 215 198 189;
|
||||
--color-r-sidebar: 133 118 106;
|
||||
--color-r-reaction: 249 110 130;
|
||||
--color-r-repost: 0 190 0;
|
||||
--color-scroll-thumb: 49 38 38;
|
||||
--color-scroll-bg: 245 235 222 / 0.7;
|
||||
}
|
||||
|
||||
/* DARK Yozakura */
|
||||
.theme-yozakura {
|
||||
--color-fg: 255 235 235;
|
||||
--color-fg-secondary: 220 180 180;
|
||||
--color-fg-tertiary: 160 130 130;
|
||||
--color-bg: 35 27 33;
|
||||
--color-bg-secondary: 50 40 50;
|
||||
--color-bg-tertiary: 63 40 50;
|
||||
--color-primary: 217 121 133;
|
||||
--color-primary-fg: var(--color-fg);
|
||||
--color-primary-disabled: 158 93 102;
|
||||
--color-primary-hover: 220 110 120;
|
||||
--color-danger: 244 63 94;
|
||||
--color-link: 59 130 246;
|
||||
--color-border: 70 50 60;
|
||||
--color-r-sidebar: 35 27 33;
|
||||
--color-r-reaction: 251 113 133;
|
||||
--color-r-repost: 139 191 67;
|
||||
--color-scroll-thumb: var(--color-primary-fg);
|
||||
--color-scroll-bg: var(--color-bg-tertiary);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
|
||||
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@apply bg-bg text-fg;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.link {
|
||||
@apply underline text-blue-500;
|
||||
}
|
||||
|
||||
em-emoji-picker {
|
||||
--background-rgb: 85, 170, 255;
|
||||
--border-radius: 8px;
|
||||
--color-border-over: rgba(0, 0, 0, 0.1);
|
||||
--color-border: rgba(0, 0, 0, 0.05);
|
||||
--color-border-over: rgba(0, 0, 0, 0.3);
|
||||
--color-border: rgba(0, 0, 0, 0.2);
|
||||
--category-icon-size: 20px;
|
||||
--font-size: 16px;
|
||||
--rgb-accent: 253, 164, 175;
|
||||
--rgb-background: 255, 255, 255;
|
||||
--rgb-color: 28, 25, 23;
|
||||
--rgb-input: 255, 255, 255;
|
||||
--shadow: 0 5px 8px -8px #222;
|
||||
--font-size: 14px;
|
||||
--rgb-accent: var(--color-primary);
|
||||
--rgb-background: var(--color-bg);
|
||||
--rgb-color: var(--color-fg);
|
||||
--rgb-input: var(--color-bg);
|
||||
--shadow: 0 8px 8px -8px #000;
|
||||
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(var(--color-border));
|
||||
height: 50vh;
|
||||
min-height: 400px;
|
||||
max-height: 800px;
|
||||
@@ -39,19 +102,23 @@ em-emoji-picker {
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
scrollbar-color: rgb(var(--color-scroll-thumb)) rgb(var(--color-scroll-bg));
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-scrollbar:vertical {
|
||||
width: 8px;
|
||||
width: 5px;
|
||||
}
|
||||
.scrollbar::-webkit-scrollbar:horizontal {
|
||||
height: 8px;
|
||||
height: 5px;
|
||||
}
|
||||
.scrollbar::-webkit-scrollbar {
|
||||
background-color: #fbf9f9;
|
||||
background-color: rgb(var(--color-scroll-bg));
|
||||
}
|
||||
.scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #fce9ec;
|
||||
border-radius: 8px;
|
||||
background-color: rgb(var(--color-scroll-thumb));
|
||||
border-radius: 2px;
|
||||
}
|
||||
.scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #fcd9dc;
|
||||
background-color: rgb(var(--color-scroll-thumb));
|
||||
}
|
||||
|
||||
@@ -142,6 +142,7 @@ export default {
|
||||
},
|
||||
display: {
|
||||
display: 'Display',
|
||||
colorTheme: 'Color theme',
|
||||
timeNotation: 'Time notation',
|
||||
relativeTimeNotation: 'Relative',
|
||||
relativeTimeNotationExample: '7s',
|
||||
|
||||
@@ -138,6 +138,7 @@ export default {
|
||||
},
|
||||
display: {
|
||||
display: '表示',
|
||||
colorTheme: 'カラーテーマ',
|
||||
timeNotation: '時刻の表記',
|
||||
relativeTimeNotation: '相対表記',
|
||||
relativeTimeNotationExample: '7秒前',
|
||||
|
||||
@@ -49,16 +49,11 @@ const Hello: Component = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="mx-auto flex max-w-[640px] flex-col items-center p-4 text-stone-600">
|
||||
<div class="flex flex-col items-center gap-4 rounded bg-white p-4">
|
||||
<div class="mx-auto flex max-w-[640px] flex-col items-center p-4 text-fg">
|
||||
<div class="flex flex-col items-center gap-4 rounded p-4">
|
||||
<img src={resolveAsset('images/rabbit_256.png')} width="96" alt="logo" height="96" />
|
||||
<h1 class="text-5xl font-black text-rose-300">Rabbit</h1>
|
||||
<h1 class="text-5xl font-black text-primary">Rabbit</h1>
|
||||
<div>Rabbit is a Web client for Nostr.</div>
|
||||
<p class="text-center">
|
||||
<span class="font-bold text-rose-400">注意: 現在ベータ版です。</span>
|
||||
<br />
|
||||
未実装の機能やバグがあることを承知の上でご利用ください。
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-md p-8 shadow-md">
|
||||
<Switch>
|
||||
@@ -72,7 +67,7 @@ const Hello: Component = () => {
|
||||
初めて利用する方も、他のクライアントをつかっている方も
|
||||
<br />
|
||||
<a
|
||||
class="text-blue-500 underline"
|
||||
class="text-link underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://scrapbox.io/nostr/NIP-07#63e1c10c8b8fcb00000584fc"
|
||||
@@ -86,7 +81,7 @@ const Hello: Component = () => {
|
||||
</Match>
|
||||
<Match when={signerStatus() === 'available'}>
|
||||
<button
|
||||
class="rounded bg-rose-400 p-4 text-lg font-bold text-white hover:shadow-md"
|
||||
class="rounded bg-primary p-4 text-lg font-bold text-primary-fg hover:shadow-md"
|
||||
onClick={handleLogin}
|
||||
>
|
||||
{i18n()('hello.loginWithSigner')}
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { Component } from 'solid-js';
|
||||
|
||||
const NotFound: Component = () => (
|
||||
<div class="container mx-auto max-w-[640px] py-10">
|
||||
<h1 class="text-4xl font-bold text-stone-700">お探しのページは見つかりませんでした</h1>
|
||||
<h1 class="text-4xl font-bold text-fg">お探しのページは見つかりませんでした</h1>
|
||||
<p class="pt-4">
|
||||
<a class="text-blue-500 underline" href="/">
|
||||
<a class="text-link underline" href="/">
|
||||
← トップに戻る
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -1,16 +1,41 @@
|
||||
import tailwindForms from '@tailwindcss/forms';
|
||||
import { type Config } from 'tailwindcss';
|
||||
import colors from 'tailwindcss/colors';
|
||||
|
||||
// 参考
|
||||
// https://github.com/shadcn-ui/taxonomy/blob/main/tailwind.config.js
|
||||
// https://github.com/shadcn-ui/taxonomy/blob/651f984e52edd65d40ccd55e299c1baeea3ff017/styles/globals.css#L78
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// a color for primary actions like a submit button.
|
||||
primary: colors.rose['300'],
|
||||
'primary-disabled': colors.rose['200'],
|
||||
'sidebar-bg': colors.rose['100'],
|
||||
fg: {
|
||||
DEFAULT: 'rgb(var(--color-fg))',
|
||||
secondary: 'rgb(var(--color-fg-secondary))',
|
||||
tertiary: 'rgb(var(--color-fg-tertiary))',
|
||||
},
|
||||
bg: {
|
||||
DEFAULT: 'rgb(var(--color-bg))',
|
||||
secondary: 'rgb(var(--color-bg-secondary))',
|
||||
tertiary: 'rgb(var(--color-bg-tertiary))',
|
||||
},
|
||||
border: 'rgb(var(--color-border))',
|
||||
primary: {
|
||||
DEFAULT: 'rgb(var(--color-primary))',
|
||||
disabled: 'rgb(var(--color-primary-disabled))',
|
||||
hover: 'rgb(var(--color-primary-hover))',
|
||||
fg: 'rgb(var(--color-primary-fg))',
|
||||
},
|
||||
danger: {
|
||||
DEFAULT: 'rgb(var(--color-danger))',
|
||||
fg: 'rgb(var(--color-danger-fg))',
|
||||
},
|
||||
link: 'rgb(var(--color-link))',
|
||||
r: {
|
||||
sidebar: 'rgb(var(--color-r-sidebar))',
|
||||
reaction: 'rgb(var(--color-r-reaction))',
|
||||
repost: 'rgb(var(--color-r-repost))',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user