feat: color themes

This commit is contained in:
Shusui MOYATANI
2024-01-06 18:02:50 +09:00
parent e36176c8cb
commit 357e337f66
44 changed files with 459 additions and 215 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -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}>

View File

@@ -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}>
<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();
}}
>
<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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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);

View File

@@ -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),

View File

@@ -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()}>
<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>

View File

@@ -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()}

View File

@@ -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)}
>

View File

@@ -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()}

View File

@@ -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)}`}

View File

@@ -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} />

View File

@@ -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} />

View File

@@ -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);

View File

@@ -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>

View File

@@ -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)}
>

View File

@@ -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')}

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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}

View File

@@ -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')}

View File

@@ -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>
</>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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')}

View File

@@ -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>

View File

@@ -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
View 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',
},
};

View File

@@ -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,

View 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;

View File

@@ -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',
},
},
},

View File

@@ -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));
}

View File

@@ -142,6 +142,7 @@ export default {
},
display: {
display: 'Display',
colorTheme: 'Color theme',
timeNotation: 'Time notation',
relativeTimeNotation: 'Relative',
relativeTimeNotationExample: '7s',

View File

@@ -138,6 +138,7 @@ export default {
},
display: {
display: '表示',
colorTheme: 'カラーテーマ',
timeNotation: '時刻の表記',
relativeTimeNotation: '相対表記',
relativeTimeNotationExample: '7秒前',

View File

@@ -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')}

View File

@@ -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>

View File

@@ -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))',
},
},
},
},