feat: identity menu

This commit is contained in:
Shibo Lyu 2024-09-03 01:58:04 +08:00
parent 3a76e2f9f8
commit 09b7d24b95
10 changed files with 83 additions and 45 deletions

13
package-lock.json generated
View file

@ -13,6 +13,7 @@
"canonicalize": "^2.0.0",
"svelte-boring-avatars": "^1.2.6",
"svelte-hero-icons": "^5.2.0",
"svelte-persisted-store": "^0.11.0",
"tailwind-merge": "^2.5.2",
"typewriter-editor": "^0.12.6",
"unique-names-generator": "^4.7.1",
@ -5421,6 +5422,18 @@
"svelte": "^3.19.0 || ^4.0.0"
}
},
"node_modules/svelte-persisted-store": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/svelte-persisted-store/-/svelte-persisted-store-0.11.0.tgz",
"integrity": "sha512-9RgJ5DrawGyyfK22A80cfu8Jose3CV8YjEZKz9Tn94rQ0tWyEmYr+XI+wrVF6wjRbW99JMDSVcFRiM3XzVJj/w==",
"license": "MIT",
"engines": {
"node": ">=0.14"
},
"peerDependencies": {
"svelte": "^3.48.0 || ^4.0.0 || ^5.0.0-next.0"
}
},
"node_modules/svelte-preprocess": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz",

View file

@ -41,6 +41,7 @@
"canonicalize": "^2.0.0",
"svelte-boring-avatars": "^1.2.6",
"svelte-hero-icons": "^5.2.0",
"svelte-persisted-store": "^0.11.0",
"tailwind-merge": "^2.5.2",
"typewriter-editor": "^0.12.6",
"unique-names-generator": "^4.7.1",

View file

@ -3,7 +3,9 @@ import { DropdownMenu } from 'bits-ui';
import Content from './DropdownMenu/Content.svelte';
import Trigger from './DropdownMenu/Trigger.svelte';
import Item from './DropdownMenu/Item.svelte';
import RadioItem from './DropdownMenu/RadioItem.svelte';
import Separator from './DropdownMenu/Separator.svelte';
const { Root, RadioGroup, RadioItem, Separator } = DropdownMenu;
const { Root, RadioGroup } = DropdownMenu;
export { Root, Trigger, Content, Item, RadioGroup, RadioItem, Separator };

View file

@ -1,12 +1,22 @@
<script lang="ts">
import { tw } from '$lib/tw';
import { DropdownMenu, type DropdownMenuContentProps } from 'bits-ui';
import { expoOut } from 'svelte/easing';
import { scale } from 'svelte/transition';
interface $$Props extends DropdownMenuContentProps {}
let className: $$Props['class'] = '';
export { className as class };
</script>
<DropdownMenu.Content
class="bg-sb-overlay min-w-32 rounded-lg border border-ss-secondary p-1 shadow-xl"
class={tw(
'bg-sb-overlay group min-w-32 origin-top rounded-lg border border-ss-secondary p-1 shadow-xl',
className
)}
sideOffset={4}
transition={scale}
transitionConfig={{ start: 0.96, duration: 300, easing: expoOut }}
{...$$restProps}
>
<slot />

View file

@ -5,7 +5,7 @@
</script>
<DropdownMenu.Item
class="cursor-default rounded px-1.5 py-0.5 text-sf-primary transition-colors duration-200 hover:bg-accent-50 dark:hover:bg-white/5"
class="cursor-default rounded px-1.5 py-0.5 text-sf-primary transition-colors duration-200 hover:bg-accent-50 group-has-[[data-melt-dropdown-menu-radio-group]]:ps-6 dark:hover:bg-white/5"
on:click
{...$$restProps}
>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import type { DropdownMenuRadioItemProps } from 'bits-ui';
import { DropdownMenu } from 'bits-ui';
import { Check, Icon } from 'svelte-hero-icons';
type $$Props = DropdownMenuRadioItemProps;
export let value: string;
</script>
<DropdownMenu.RadioItem
class="flex cursor-default items-center gap-1 rounded px-1.5 py-0.5 text-sf-primary transition-colors duration-200 hover:bg-accent-50 dark:hover:bg-white/5"
{value}
{...$$props}
>
<DropdownMenu.RadioIndicator class="relative size-4">
<Icon src={Check} class="size-full" micro />
</DropdownMenu.RadioIndicator>
<slot />
</DropdownMenu.RadioItem>

View file

@ -0,0 +1,5 @@
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
</script>
<DropdownMenu.Separator class="my-1 border-t border-ss-secondary" />

View file

@ -1,5 +1,5 @@
import { persisted } from 'svelte-persisted-store';
import type { EncodedBlahKeyPair } from './blah/crypto';
import { localStore } from './localstore';
export const keyStore = localStore<EncodedBlahKeyPair[]>('weblah-keypairs', []);
export const currentKeyIndex = localStore<number>('weblah-current-key-index', 0);
export const keyStore = persisted<EncodedBlahKeyPair[]>('weblah-keypairs', []);
export const currentKeyIndex = persisted<number>('weblah-current-key-index', 0);

View file

@ -1,28 +0,0 @@
import { browser } from '$app/environment';
import { get, writable, type Writable } from 'svelte/store';
export function localStore<V>(key: string, initialData: V): Writable<V> {
const store = writable(initialData);
const { subscribe, set } = store;
if (browser) {
const storedValue = localStorage.getItem(key);
if (storedValue) set(JSON.parse(storedValue));
}
return {
subscribe,
set: (v) => {
if (browser) {
localStorage.setItem(key, JSON.stringify(v));
}
set(v);
},
update: (cb) => {
const updatedStore = cb(get(store));
if (browser) localStorage.setItem(key, JSON.stringify(updatedStore));
set(updatedStore);
}
};
}

View file

@ -5,8 +5,11 @@
import { BlahKeyPair, generateName } from '$lib/blah/crypto';
let currentKeyId: string | undefined;
$: currentKeyId = $keyStore[$currentKeyIndex]?.id;
$: currentKeyName = currentKeyId ? generateName(currentKeyId) : null;
let currentKeyName: string | null;
$: {
currentKeyId = $keyStore[$currentKeyIndex]?.id;
currentKeyName = currentKeyId ? generateName(currentKeyId) : null;
}
async function createKeyPair() {
const newKeyPair = await BlahKeyPair.generate();
@ -14,12 +17,18 @@
$keyStore = [...$keyStore, encoded];
$currentKeyIndex = $keyStore.length - 1;
}
function setCurrentKeyIndex(idx: string | undefined | null) {
$currentKeyIndex = parseInt(idx ?? '0', 10);
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Root closeOnItemClick={false}>
<DropdownMenu.Trigger>
{#if currentKeyId}
<AvatarBeam size={30} name={currentKeyId} />
{#key currentKeyId}
<AvatarBeam size={30} name={currentKeyId} />
{/key}
<span class="sr-only">Using identity {currentKeyName}</span>
{:else}
<div
@ -29,19 +38,26 @@
<span class="sr-only">Using no identity</span>
{/if}
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Content class="origin-top-left">
{#if $keyStore.length > 0}
<DropdownMenu.RadioGroup bind:value={currentKeyId}>
{#each $keyStore as { id }}
<DropdownMenu.RadioGroup
value={$currentKeyIndex.toString()}
onValueChange={setCurrentKeyIndex}
>
{#each $keyStore as { id }, idx}
{@const name = generateName(id)}
<DropdownMenu.RadioItem value={id}>
<AvatarBeam size={30} {name} />
<span>{name}</span>
<DropdownMenu.RadioItem value={idx.toString()}>
<div class="flex items-center gap-2 py-0.5">
<AvatarBeam size={24} name={id} />
<span>{name}</span>
</div>
</DropdownMenu.RadioItem>
{/each}
</DropdownMenu.RadioGroup>
<DropdownMenu.Separator />
<DropdownMenu.Item>Manage identities</DropdownMenu.Item>
{:else}
<DropdownMenu.Item on:click={createKeyPair}>Create new identity</DropdownMenu.Item>
{/if}
<DropdownMenu.Item on:click={createKeyPair}>Create new identity</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>