mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-18 22:44:26 +01:00
feat: beatify config
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import { createSignal, Show, For, type JSX, batch } from 'solid-js';
|
import { createSignal, Show, For, type JSX, batch } from 'solid-js';
|
||||||
|
|
||||||
import ArrowLeft from 'heroicons/24/outline/arrow-left.svg';
|
import ArrowLeft from 'heroicons/24/outline/arrow-left.svg';
|
||||||
|
import ChevronDown from 'heroicons/24/outline/chevron-down.svg';
|
||||||
|
import ChevronUp from 'heroicons/24/outline/chevron-up.svg';
|
||||||
import EyeSlash from 'heroicons/24/outline/eye-slash.svg';
|
import EyeSlash from 'heroicons/24/outline/eye-slash.svg';
|
||||||
import FaceSmile from 'heroicons/24/outline/face-smile.svg';
|
import FaceSmile from 'heroicons/24/outline/face-smile.svg';
|
||||||
import PaintBrush from 'heroicons/24/outline/paint-brush.svg';
|
import PaintBrush from 'heroicons/24/outline/paint-brush.svg';
|
||||||
@@ -10,6 +12,8 @@ import XMark from 'heroicons/24/outline/x-mark.svg';
|
|||||||
|
|
||||||
import BasicModal from '@/components/modal/BasicModal';
|
import BasicModal from '@/components/modal/BasicModal';
|
||||||
import UserNameDisplay from '@/components/UserDisplayName';
|
import UserNameDisplay from '@/components/UserDisplayName';
|
||||||
|
import LazyLoad from '@/components/utils/LazyLoad';
|
||||||
|
import usePopup from '@/components/utils/usePopup';
|
||||||
import { colorThemes } from '@/core/colorThemes';
|
import { colorThemes } from '@/core/colorThemes';
|
||||||
import useConfig, { type Config } from '@/core/useConfig';
|
import useConfig, { type Config } from '@/core/useConfig';
|
||||||
import useModalState from '@/hooks/useModalState';
|
import useModalState from '@/hooks/useModalState';
|
||||||
@@ -27,17 +31,62 @@ const BaseUrlRegex = (schemaRegex: string) =>
|
|||||||
const HttpUrlRegex = BaseUrlRegex('https?');
|
const HttpUrlRegex = BaseUrlRegex('https?');
|
||||||
const RelayUrlRegex = BaseUrlRegex('wss?');
|
const RelayUrlRegex = BaseUrlRegex('wss?');
|
||||||
|
|
||||||
|
const Section = (props: { title: string; initialOpened?: boolean; children: JSX.Element }) => {
|
||||||
|
const [opened, setOpened] = createSignal(props.initialOpened ?? true);
|
||||||
|
const toggleOpened = () => setOpened((current) => !current);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="mb-2 rounded border border-border shadow hover:shadow-md">
|
||||||
|
<h3 class="text-lg font-bold">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex w-full items-center p-2 text-start"
|
||||||
|
onClick={() => toggleOpened()}
|
||||||
|
>
|
||||||
|
<span class="flex-1 hover:text-fg-secondary">{props.title}</span>
|
||||||
|
<span class="inline-block h-4 w-4 shrink-0 text-fg">
|
||||||
|
<Show when={opened()} fallback={<ChevronDown />}>
|
||||||
|
<ChevronUp />
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</h3>
|
||||||
|
<Show when={opened()}>
|
||||||
|
<div class="border-t border-border p-2">{props.children}</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToggleButton = (props: {
|
||||||
|
value: boolean;
|
||||||
|
onClick: JSX.EventHandler<HTMLButtonElement, MouseEvent>;
|
||||||
|
}) => (
|
||||||
|
<button
|
||||||
|
class="flex h-[24px] w-[48px] items-center rounded-full border border-primary/80 px-1"
|
||||||
|
classList={{
|
||||||
|
'bg-bg-tertiary': !props.value,
|
||||||
|
'justify-start': !props.value,
|
||||||
|
'bg-primary': props.value,
|
||||||
|
'justify-end': props.value,
|
||||||
|
}}
|
||||||
|
area-label={props.value}
|
||||||
|
onClick={(event) => props.onClick(event)}
|
||||||
|
>
|
||||||
|
<span class="m-[-3px] inline-block h-5 w-5 rounded-full border bg-primary-fg shadow" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
const ProfileSection = () => {
|
const ProfileSection = () => {
|
||||||
const i18n = useTranslation();
|
const i18n = useTranslation();
|
||||||
const pubkey = usePubkey();
|
const pubkey = usePubkey();
|
||||||
const { showProfile, showProfileEdit } = useModalState();
|
const { showProfile, showProfileEdit } = useModalState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="py-2">
|
<Section title={i18n()('config.profile.profile')}>
|
||||||
<h3 class="font-bold">{i18n()('config.profile.profile')}</h3>
|
<div class="flex gap-2 py-1">
|
||||||
<div class="flex gap-2">
|
|
||||||
<button
|
<button
|
||||||
class="rounded border border-primary px-4 py-2 font-bold text-primary"
|
class="rounded border border-primary px-4 py-1 font-bold text-primary"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
ensureNonNull([pubkey()])(([pubkeyNonNull]) => {
|
ensureNonNull([pubkey()])(([pubkeyNonNull]) => {
|
||||||
showProfile(pubkeyNonNull);
|
showProfile(pubkeyNonNull);
|
||||||
@@ -47,13 +96,13 @@ const ProfileSection = () => {
|
|||||||
{i18n()('config.profile.openProfile')}
|
{i18n()('config.profile.openProfile')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="rounded border border-primary px-4 py-2 font-bold text-primary"
|
class="rounded border border-primary px-4 py-1 font-bold text-primary"
|
||||||
onClick={() => showProfileEdit()}
|
onClick={() => showProfileEdit()}
|
||||||
>
|
>
|
||||||
{i18n()('config.profile.editProfile')}
|
{i18n()('config.profile.editProfile')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,23 +147,10 @@ const RelayConfig = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="py-2">
|
<Section title={i18n()('config.relays.relays')}>
|
||||||
<h3 class="font-bold">{i18n()('config.relays.relays')}</h3>
|
|
||||||
<p class="py-1">
|
<p class="py-1">
|
||||||
{i18n()('config.relays.numOfRelays', { count: config().relayUrls.length })}
|
{i18n()('config.relays.numOfRelays', { count: config().relayUrls.length })}
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
|
||||||
<For each={config().relayUrls}>
|
|
||||||
{(relayUrl: string) => (
|
|
||||||
<li class="flex items-center">
|
|
||||||
<div class="flex-1 truncate">{relayUrl}</div>
|
|
||||||
<button class="h-3 w-3 shrink-0" onClick={() => removeRelay(relayUrl)}>
|
|
||||||
<XMark />
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</ul>
|
|
||||||
<form class="flex gap-2" onSubmit={handleClickAddRelay}>
|
<form class="flex gap-2" onSubmit={handleClickAddRelay}>
|
||||||
<input
|
<input
|
||||||
class="flex-1 rounded-md border border-border bg-bg ring-border placeholder:text-fg-secondary focus:border-border focus:ring-primary"
|
class="flex-1 rounded-md border border-border bg-bg ring-border placeholder:text-fg-secondary focus:border-border focus:ring-primary"
|
||||||
@@ -129,9 +165,20 @@ const RelayConfig = () => {
|
|||||||
{i18n()('config.relays.addRelay')}
|
{i18n()('config.relays.addRelay')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
<ul class="pt-2">
|
||||||
<div class="py-2">
|
<For each={config().relayUrls}>
|
||||||
<h3 class="pb-1 font-bold">{i18n()('config.relays.importRelays')}</h3>
|
{(relayUrl: string) => (
|
||||||
|
<li class="flex items-center border-t border-border">
|
||||||
|
<div class="flex-1 truncate">{relayUrl}</div>
|
||||||
|
<button class="h-3 w-3 shrink-0" onClick={() => removeRelay(relayUrl)}>
|
||||||
|
<XMark />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
</Section>
|
||||||
|
<Section title={i18n()('config.relays.importRelays')}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded bg-primary p-2 font-bold text-primary-fg"
|
class="rounded bg-primary p-2 font-bold text-primary-fg"
|
||||||
@@ -144,7 +191,7 @@ const RelayConfig = () => {
|
|||||||
>
|
>
|
||||||
{i18n()('config.relays.importFromExtension')}
|
{i18n()('config.relays.importFromExtension')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</Section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -166,14 +213,13 @@ const ColorThemeConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="py-2">
|
<Section title={i18n()('config.display.colorTheme')}>
|
||||||
<h3 class="font-bold">{i18n()('config.display.colorTheme')}</h3>
|
|
||||||
<div class="scrollbar flex max-h-[25vh] flex-col overflow-y-scroll rounded-md border border-border">
|
<div class="scrollbar flex max-h-[25vh] flex-col overflow-y-scroll rounded-md border border-border">
|
||||||
<For each={Object.values(colorThemes)}>
|
<For each={Object.values(colorThemes)}>
|
||||||
{(colorTheme) => (
|
{(colorTheme) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="border-t border-border px-2 py-1 text-left"
|
class="border-t border-border px-2 py-1 text-left first:border-none"
|
||||||
classList={{
|
classList={{
|
||||||
'bg-primary': isCurrentlyUsing(colorTheme.id),
|
'bg-primary': isCurrentlyUsing(colorTheme.id),
|
||||||
'text-primary-fg': isCurrentlyUsing(colorTheme.id),
|
'text-primary-fg': isCurrentlyUsing(colorTheme.id),
|
||||||
@@ -186,7 +232,7 @@ const ColorThemeConfig = () => {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -221,8 +267,7 @@ const DateFormatConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="py-2">
|
<Section title={i18n()('config.display.timeNotation')}>
|
||||||
<h3 class="font-bold">{i18n()('config.display.timeNotation')}</h3>
|
|
||||||
<div class="flex flex-col justify-evenly gap-2 sm:flex-row">
|
<div class="flex flex-col justify-evenly gap-2 sm:flex-row">
|
||||||
<For each={dateFormats}>
|
<For each={dateFormats}>
|
||||||
{({ id, name, example }) => (
|
{({ id, name, example }) => (
|
||||||
@@ -245,29 +290,10 @@ const DateFormatConfig = () => {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ToggleButton = (props: {
|
|
||||||
value: boolean;
|
|
||||||
onClick: JSX.EventHandler<HTMLButtonElement, MouseEvent>;
|
|
||||||
}) => (
|
|
||||||
<button
|
|
||||||
class="flex h-[24px] w-[48px] items-center rounded-full border border-primary/80 px-1"
|
|
||||||
classList={{
|
|
||||||
'bg-bg-tertiary': !props.value,
|
|
||||||
'justify-start': !props.value,
|
|
||||||
'bg-primary': props.value,
|
|
||||||
'justify-end': props.value,
|
|
||||||
}}
|
|
||||||
area-label={props.value}
|
|
||||||
onClick={(event) => props.onClick(event)}
|
|
||||||
>
|
|
||||||
<span class="m-[-3px] inline-block h-5 w-5 rounded-full border bg-primary-fg shadow" />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
|
|
||||||
const ReactionConfig = () => {
|
const ReactionConfig = () => {
|
||||||
const i18n = useTranslation();
|
const i18n = useTranslation();
|
||||||
const { config, setConfig } = useConfig();
|
const { config, setConfig } = useConfig();
|
||||||
@@ -287,8 +313,7 @@ const ReactionConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="py-2">
|
<Section title={i18n()('config.display.reaction')}>
|
||||||
<h3 class="font-bold">{i18n()('config.display.reaction')}</h3>
|
|
||||||
<div class="flex flex-col justify-evenly gap-2">
|
<div class="flex flex-col justify-evenly gap-2">
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">{i18n()('config.display.enableEmojiReaction')}</div>
|
<div class="flex-1">{i18n()('config.display.enableEmojiReaction')}</div>
|
||||||
@@ -305,7 +330,7 @@ const ReactionConfig = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -325,21 +350,7 @@ const EmojiConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="py-2">
|
<Section title={i18n()('config.customEmoji.customEmoji')}>
|
||||||
<h3 class="font-bold">{i18n()('config.customEmoji.customEmoji')}</h3>
|
|
||||||
<ul class="flex flex-col gap-1 py-2">
|
|
||||||
<For each={Object.values(config().customEmojis)}>
|
|
||||||
{({ shortcode, url }) => (
|
|
||||||
<li class="flex items-center gap-2">
|
|
||||||
<img class="h-7 min-w-7 max-w-[128px]" src={url} alt={shortcode} />
|
|
||||||
<div class="flex-1 truncate">{shortcode}</div>
|
|
||||||
<button class="h-3 w-3 shrink-0" onClick={() => removeEmoji(shortcode)}>
|
|
||||||
<XMark />
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</ul>
|
|
||||||
<form class="flex flex-col gap-2" onSubmit={handleClickSaveEmoji}>
|
<form class="flex flex-col gap-2" onSubmit={handleClickSaveEmoji}>
|
||||||
<label class="flex flex-1 items-center gap-1">
|
<label class="flex flex-1 items-center gap-1">
|
||||||
<div class="w-9">{i18n()('config.customEmoji.shortcode')}</div>
|
<div class="w-9">{i18n()('config.customEmoji.shortcode')}</div>
|
||||||
@@ -374,7 +385,50 @@ const EmojiConfig = () => {
|
|||||||
{i18n()('config.customEmoji.addEmoji')}
|
{i18n()('config.customEmoji.addEmoji')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
<ul class="mt-4 flex max-h-[40vh] flex-wrap overflow-y-scroll border-t border-border">
|
||||||
|
<For each={Object.values(config().customEmojis)}>
|
||||||
|
{({ shortcode, url }) => {
|
||||||
|
const [ref, setRef] = createSignal<HTMLLIElement | undefined>();
|
||||||
|
const popup = usePopup(() => ({
|
||||||
|
target: ref(),
|
||||||
|
popup: (
|
||||||
|
<div class="flex min-w-24 flex-col items-center rounded border border-border bg-bg shadow">
|
||||||
|
<div class="flex items-center p-1">
|
||||||
|
<img class="h-20 max-w-20 object-contain" src={url} alt={shortcode} />
|
||||||
|
</div>
|
||||||
|
<div class="p-1 text-center text-sm">{shortcode}</div>
|
||||||
|
<div class="w-full border-t border-border">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full px-2 py-1 text-danger"
|
||||||
|
onClick={() => removeEmoji(shortcode)}
|
||||||
|
>
|
||||||
|
{i18n()('config.customEmoji.removeEmoji')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li ref={setRef} class="min-w-0 basis-1/2 sm:basis-1/4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex w-full flex-col items-center gap-1 rounded p-2 hover:bg-bg-tertiary/20 hover:shadow"
|
||||||
|
onClick={() => popup.open()}
|
||||||
|
>
|
||||||
|
<div class="flex h-8 max-w-8 items-center">
|
||||||
|
<img class="object-contain" src={url} alt={shortcode} />
|
||||||
|
</div>
|
||||||
|
<div class="w-full truncate text-xs text-fg-secondary">{shortcode}</div>
|
||||||
|
</button>
|
||||||
|
{popup.popup()}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
</Section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -400,8 +454,7 @@ const EmojiImport = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="py-2">
|
<Section title={i18n()('config.customEmoji.emojiImport')}>
|
||||||
<h3 class="font-bold">{i18n()('config.customEmoji.emojiImport')}</h3>
|
|
||||||
<p>{i18n()('config.customEmoji.emojiImportDescription')}</p>
|
<p>{i18n()('config.customEmoji.emojiImportDescription')}</p>
|
||||||
<form class="flex flex-col gap-2" onSubmit={handleClickSaveEmoji}>
|
<form class="flex flex-col gap-2" onSubmit={handleClickSaveEmoji}>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -418,7 +471,7 @@ const EmojiImport = () => {
|
|||||||
{i18n()('config.customEmoji.importEmoji')}
|
{i18n()('config.customEmoji.importEmoji')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</Section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -437,14 +490,13 @@ const MuteConfig = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="py-2">
|
<Section title={i18n()('config.mute.mutedUsers')} initialOpened={false}>
|
||||||
<h3 class="font-bold">{i18n()('config.mute.mutedUsers')}</h3>
|
<ul class="flex max-h-[50vh] flex-col overflow-y-scroll">
|
||||||
<ul class="flex flex-col">
|
|
||||||
<For each={config().mutedPubkeys}>
|
<For each={config().mutedPubkeys}>
|
||||||
{(pubkey) => (
|
{(pubkey) => (
|
||||||
<li class="flex items-center">
|
<li class="flex items-center border-b border-border">
|
||||||
<div class="flex-1 truncate">
|
<div class="flex-1 truncate">
|
||||||
<UserNameDisplay pubkey={pubkey} />
|
<LazyLoad>{() => <UserNameDisplay pubkey={pubkey} />}</LazyLoad>
|
||||||
</div>
|
</div>
|
||||||
<button class="h-3 w-3 shrink-0" onClick={() => removeMutedPubkey(pubkey)}>
|
<button class="h-3 w-3 shrink-0" onClick={() => removeMutedPubkey(pubkey)}>
|
||||||
<XMark />
|
<XMark />
|
||||||
@@ -453,21 +505,8 @@ const MuteConfig = () => {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</Section>
|
||||||
<div class="py-2">
|
<Section title={i18n()('config.mute.mutedKeywords')} initialOpened={false}>
|
||||||
<h3 class="font-bold">{i18n()('config.mute.mutedKeywords')}</h3>
|
|
||||||
<ul class="flex flex-col">
|
|
||||||
<For each={config().mutedKeywords}>
|
|
||||||
{(keyword) => (
|
|
||||||
<li class="flex items-center">
|
|
||||||
<div class="flex-1 truncate">{keyword}</div>
|
|
||||||
<button class="h-3 w-3 shrink-0" onClick={() => removeMutedKeyword(keyword)}>
|
|
||||||
<XMark />
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</ul>
|
|
||||||
<form class="flex gap-2" onSubmit={handleClickAddKeyword}>
|
<form class="flex gap-2" onSubmit={handleClickAddKeyword}>
|
||||||
<input
|
<input
|
||||||
class="flex-1 rounded-md border border-border bg-bg ring-border focus:border-border focus:ring-primary"
|
class="flex-1 rounded-md border border-border bg-bg ring-border focus:border-border focus:ring-primary"
|
||||||
@@ -480,7 +519,19 @@ const MuteConfig = () => {
|
|||||||
{i18n()('config.mute.add')}
|
{i18n()('config.mute.add')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
<ul class="mt-2 flex max-h-[50vh] flex-col overflow-y-scroll border-t border-border">
|
||||||
|
<For each={config().mutedKeywords}>
|
||||||
|
{(keyword) => (
|
||||||
|
<li class="flex items-center border-b border-border">
|
||||||
|
<div class="flex-1 truncate">{keyword}</div>
|
||||||
|
<button class="h-3 w-3 shrink-0" onClick={() => removeMutedKeyword(keyword)}>
|
||||||
|
<XMark />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
</Section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -500,8 +551,7 @@ const EmbeddingConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="py-2">
|
<Section title={i18n()('config.display.embedding')}>
|
||||||
<h3 class="font-bold">{i18n()('config.display.embedding')}</h3>
|
|
||||||
<p>{i18n()('config.display.embeddingDescription')}</p>
|
<p>{i18n()('config.display.embeddingDescription')}</p>
|
||||||
<div class="flex flex-col justify-evenly gap-2">
|
<div class="flex flex-col justify-evenly gap-2">
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
@@ -517,7 +567,7 @@ const EmbeddingConfig = () => {
|
|||||||
<ToggleButton value={config().embedding.ogp} onClick={() => toggle('ogp')} />
|
<ToggleButton value={config().embedding.ogp} onClick={() => toggle('ogp')} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -547,8 +597,7 @@ const OtherConfig = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="py-2">
|
<Section title={i18n()('config.display.others')}>
|
||||||
<h3 class="font-bold">{i18n()('config.display.others')}</h3>
|
|
||||||
<div class="flex flex-col justify-evenly gap-2">
|
<div class="flex flex-col justify-evenly gap-2">
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">{i18n()('config.display.keepOpenPostForm')}</div>
|
<div class="flex-1">{i18n()('config.display.keepOpenPostForm')}</div>
|
||||||
@@ -576,7 +625,7 @@ const OtherConfig = () => {
|
|||||||
</div>
|
</div>
|
||||||
*/}
|
*/}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
112
src/components/utils/usePopup.tsx
Normal file
112
src/components/utils/usePopup.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import {
|
||||||
|
createSignal,
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
onCleanup,
|
||||||
|
children,
|
||||||
|
Show,
|
||||||
|
type JSX,
|
||||||
|
} from 'solid-js';
|
||||||
|
|
||||||
|
import { Portal } from 'solid-js/web';
|
||||||
|
|
||||||
|
export type UsePopupProps = {
|
||||||
|
target?: HTMLElement;
|
||||||
|
popup?: JSX.Element;
|
||||||
|
position?: 'left' | 'bottom' | 'right' | 'top';
|
||||||
|
};
|
||||||
|
|
||||||
|
type UsePopup = {
|
||||||
|
open: () => void;
|
||||||
|
close: () => void;
|
||||||
|
toggle: () => void;
|
||||||
|
popup: () => JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
const usePopup = (propsProvider: () => UsePopupProps): UsePopup => {
|
||||||
|
const props = createMemo(propsProvider);
|
||||||
|
|
||||||
|
const [popupRef, setPopupRef] = createSignal<HTMLDivElement | undefined>();
|
||||||
|
const [style, setStyle] = createSignal<JSX.CSSProperties>({});
|
||||||
|
const [isOpen, setIsOpen] = createSignal(false);
|
||||||
|
|
||||||
|
const resolvedChildren = children(() => props().popup);
|
||||||
|
|
||||||
|
const open = () => setIsOpen(true);
|
||||||
|
const close = () => setIsOpen(false);
|
||||||
|
const toggle = () => setIsOpen((current) => !current);
|
||||||
|
|
||||||
|
const handleClickOutside = (ev: MouseEvent) => {
|
||||||
|
const target = ev.target as HTMLElement;
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopImmediatePropagation();
|
||||||
|
if (target != null && !popupRef()?.contains(target)) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addClickOutsideHandler = () => {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
const removeClickOutsideHandler = () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
|
||||||
|
const adjustPosition = () => {
|
||||||
|
const { target } = props();
|
||||||
|
const popupElem = popupRef();
|
||||||
|
if (target == null || popupElem == null) return;
|
||||||
|
|
||||||
|
const targetRect = target.getBoundingClientRect();
|
||||||
|
const popupRect = popupElem.getBoundingClientRect();
|
||||||
|
|
||||||
|
let { top, left } = targetRect;
|
||||||
|
|
||||||
|
if (props().position === 'left') {
|
||||||
|
left -= targetRect.width;
|
||||||
|
} else if (props().position === 'right') {
|
||||||
|
left += targetRect.width;
|
||||||
|
} else if (props().position === 'top') {
|
||||||
|
top -= targetRect.height;
|
||||||
|
left -= targetRect.left + targetRect.width / 2;
|
||||||
|
} else {
|
||||||
|
top += targetRect.height;
|
||||||
|
left += targetRect.width / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
top = Math.min(top, window.innerHeight - popupRect.height);
|
||||||
|
left = Math.min(left, window.innerWidth - popupRect.width);
|
||||||
|
|
||||||
|
setStyle({ left: `${left}px`, top: `${top}px` });
|
||||||
|
};
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (isOpen()) {
|
||||||
|
addClickOutsideHandler();
|
||||||
|
adjustPosition();
|
||||||
|
} else {
|
||||||
|
removeClickOutsideHandler();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onCleanup(() => removeClickOutsideHandler());
|
||||||
|
|
||||||
|
const popup = () => (
|
||||||
|
<Show when={isOpen()}>
|
||||||
|
<Portal>
|
||||||
|
<div ref={setPopupRef} class="absolute z-20" style={style()}>
|
||||||
|
{resolvedChildren()}
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
toggle,
|
||||||
|
popup,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default usePopup;
|
||||||
@@ -165,6 +165,7 @@ export default {
|
|||||||
shortcode: 'Name',
|
shortcode: 'Name',
|
||||||
url: 'URL',
|
url: 'URL',
|
||||||
addEmoji: 'Add',
|
addEmoji: 'Add',
|
||||||
|
removeEmoji: 'Remove',
|
||||||
emojiImport: 'Emoji import',
|
emojiImport: 'Emoji import',
|
||||||
emojiImportDescription: 'Paste a JSON where the keys are names and the values are image URLs',
|
emojiImportDescription: 'Paste a JSON where the keys are names and the values are image URLs',
|
||||||
importEmoji: 'Import',
|
importEmoji: 'Import',
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ export default {
|
|||||||
shortcode: '名前',
|
shortcode: '名前',
|
||||||
url: 'URL',
|
url: 'URL',
|
||||||
addEmoji: '追加',
|
addEmoji: '追加',
|
||||||
|
removeEmoji: '削除',
|
||||||
emojiImport: '絵文字のインポート',
|
emojiImport: '絵文字のインポート',
|
||||||
emojiImportDescription:
|
emojiImportDescription:
|
||||||
'絵文字の名前をキー、画像のURLを値とするJSONを読み込むことができます。',
|
'絵文字の名前をキー、画像のURLを値とするJSONを読み込むことができます。',
|
||||||
|
|||||||
Reference in New Issue
Block a user