feat[wip]: domain setup dialog
Some checks are pending
Build & Test / build (20.x) (push) Waiting to run
Build & Test / build (22.x) (push) Waiting to run

This commit is contained in:
Shibo Lyu 2025-04-27 02:39:07 +08:00
parent 1880726a8c
commit b3e16f2063
6 changed files with 133 additions and 21 deletions

View file

@ -12,13 +12,13 @@
const className = $derived(
tw(
'text-sf-secondary bg-sb-primary ring-ss-secondary inline-flex cursor-default items-center justify-center rounded-md px-2.5 py-1 shadow-xs ring-1',
'text-sf-secondary bg-sb-primary inset-ring-ss-secondary inline-flex cursor-default items-center justify-center rounded-md px-2.5 py-1 shadow-xs inset-ring',
'hover:ring-ss-primary font-normal transition-shadow duration-200 active:shadow-inner',
variant === 'primary' && [
'relative text-slate-50 ring-0 duration-200',
'before:absolute before:-inset-px before:rounded-[7px]',
'before:from-accent-400 before:to-accent-500 before:bg-linear-to-b before:from-40% before:ring-1 before:ring-black/10 before:ring-inset',
'dark:before:from-accent-500 dark:before:to-accent-600 before:transition-shadow active:before:shadow-inner'
'dark:before:from-accent-600 dark:before:to-accent-700 before:transition-shadow active:before:shadow-inner'
],
externalClass
)

View file

@ -0,0 +1,43 @@
<script lang="ts">
import { tw } from '$lib/tw';
import { Dialog } from 'bits-ui';
import { fade, fly } from 'svelte/transition';
const { Root, Overlay, Portal, Content } = Dialog;
interface Props {
open: boolean;
children?: import('svelte').Snippet;
class?: string;
}
let { open = $bindable(false), children, class: className }: Props = $props();
</script>
<Root bind:open>
<Portal>
<Overlay forceMount>
{#snippet child({ props, open })}
{#if open}
<div {...props} class="fixed inset-0 z-50 bg-black/50" transition:fade></div>
{/if}
{/snippet}
</Overlay>
<Content forceMount>
{#snippet child({ props, open })}
{#if open}
<div
{...props}
class={tw(
'bg-sb-secondary border-ss-secondary shadow-3xl fixed inset-1/2 z-50 -translate-1/2 overflow-hidden rounded-lg border sm:min-h-64 sm:min-w-lg',
className
)}
transition:fly={{ y: -25 }}
>
{@render children?.()}
</div>
{/if}
{/snippet}
</Content>
</Portal>
</Root>

View file

@ -1,9 +1,15 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
import { tw } from '$lib/tw';
import type { HTMLAttributes } from 'svelte/elements';
let { children }: Props = $props();
type Props = {
children?: import('svelte').Snippet;
class?: string;
} & Omit<HTMLAttributes<HTMLDivElement>, 'class'>;
let { children, class: className, ...rest }: Props = $props();
</script>
<div class="mx-auto max-w-3xl">{@render children?.()}</div>
<div class={tw('mx-auto max-w-3xl', className)} {...rest}>
{@render children?.()}
</div>

View file

@ -11,7 +11,10 @@
class?: string;
} & (
| ({ href: string } & Omit<HTMLAnchorAttributes, 'class' | 'href'>)
| ({ onclick: Required<HTMLButtonAttributes['onclick']> } & Omit<HTMLButtonAttributes, 'class'>)
| ({ onclick: Exclude<HTMLButtonAttributes['onclick'], null | undefined> } & Omit<
HTMLButtonAttributes,
'class' | 'onclick'
>)
| Omit<HTMLAttributes<HTMLDivElement>, 'onclick'>
);

View file

@ -1,10 +1,10 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte';
import Dialog from '$lib/components/Dialog.svelte';
import { GroupedListItem } from '$lib/components/GroupedList';
import LoadingIndicator from '$lib/components/LoadingIndicator.svelte';
import { idURLToUsername, validateIDURL } from '$lib/idURL';
import { idURLToUsername, validateIDURL, type IDURLValidity } from '$lib/idURL';
import type { BlahIdentity } from '@blah-im/core/identity';
import { AtSymbol, ExclamationCircle, ExclamationTriangle, Icon } from 'svelte-hero-icons';
import { AtSymbol, ExclamationCircle, Icon } from 'svelte-hero-icons';
import UsernameSetupSelfhostDialog from './UsernameSetupSelfhostDialog.svelte';
interface Props {
url: string;
@ -12,27 +12,36 @@
}
let { url, identity }: Props = $props();
let validationResult = $state<IDURLValidity | null>(null);
let showFixDialog = $state(false);
async function validate() {
if (!identity) return null;
return await validateIDURL(url, identity);
validationResult = await validateIDURL(url, identity);
}
$effect.pre(() => {
if (identity && !showFixDialog) {
validate();
}
});
const isInvalid = $derived(validationResult && !validationResult.valid);
</script>
<GroupedListItem>
<GroupedListItem onclick={isInvalid ? () => (showFixDialog = true) : undefined}>
<div class="flex items-center gap-0.5">
<Icon micro src={AtSymbol} class="size-3.5 opacity-90" />
<span>{idURLToUsername(url)}</span>
</div>
{#snippet badge()}
{#await validate()}
<LoadingIndicator />
{:then result}
{#if result && !result.valid}
<Icon mini src={ExclamationCircle} class="size-5 fill-red-500 dark:fill-red-400" />
<Button>Fix</Button>
{#if isInvalid}
<Icon mini src={ExclamationCircle} class="size-5 fill-red-500 dark:fill-red-400" />
{#if url && identity}
<UsernameSetupSelfhostDialog bind:open={showFixDialog} {url} {identity} />
{/if}
{/await}
{/if}
{/snippet}
</GroupedListItem>

View file

@ -0,0 +1,51 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte';
import Dialog from '$lib/components/Dialog.svelte';
import { GroupedListContainer, GroupedListSection } from '$lib/components/GroupedList';
import GroupedListContent from '$lib/components/GroupedList/GroupedListContent.svelte';
import PageHeader from '$lib/components/PageHeader.svelte';
import { idURLToUsername } from '$lib/idURL';
import type { BlahIdentity } from '@blah-im/core/identity';
interface Props {
open: boolean;
url: string;
identity: BlahIdentity;
}
let { open = $bindable(), url, identity }: Props = $props();
const username = $derived(idURLToUsername(url));
const profileDescriptionString = $derived(
JSON.stringify(identity.generateIdentityDescription(), null, 2)
);
</script>
<Dialog bind:open class="flex h-2/3 flex-col">
<PageHeader>
<h3 class="flex-1">Setup Domain {username}</h3>
<Button variant="primary" onclick={() => (open = false)}>Done</Button>
</PageHeader>
<GroupedListContainer class="w-full grow overflow-x-auto">
<GroupedListSection>
{#snippet header()}
<div class="-me-4 flex min-w-0 items-end gap-2 text-base normal-case">
<p class="text-sf-primary">
For others to validate your domain as your username, put the content below at
<code>/.well-known/blah/profile.json</code>
under your domain.
</p>
<Button>Copy</Button>
</div>
{/snippet}
<GroupedListContent class="p-0">
<textarea
readonly
class="text-sf-primary block h-100 w-full resize-none overflow-x-auto px-4 py-3 font-mono"
value={profileDescriptionString}
></textarea>
</GroupedListContent>
</GroupedListSection>
</GroupedListContainer>
</Dialog>