mirror of
https://github.com/Blah-IM/Weblah.git
synced 2025-05-01 00:31:08 +00:00
feat: identity menu
This commit is contained in:
parent
3a76e2f9f8
commit
09b7d24b95
10 changed files with 83 additions and 45 deletions
13
package-lock.json
generated
13
package-lock.json
generated
|
@ -13,6 +13,7 @@
|
||||||
"canonicalize": "^2.0.0",
|
"canonicalize": "^2.0.0",
|
||||||
"svelte-boring-avatars": "^1.2.6",
|
"svelte-boring-avatars": "^1.2.6",
|
||||||
"svelte-hero-icons": "^5.2.0",
|
"svelte-hero-icons": "^5.2.0",
|
||||||
|
"svelte-persisted-store": "^0.11.0",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"typewriter-editor": "^0.12.6",
|
"typewriter-editor": "^0.12.6",
|
||||||
"unique-names-generator": "^4.7.1",
|
"unique-names-generator": "^4.7.1",
|
||||||
|
@ -5421,6 +5422,18 @@
|
||||||
"svelte": "^3.19.0 || ^4.0.0"
|
"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": {
|
"node_modules/svelte-preprocess": {
|
||||||
"version": "5.1.4",
|
"version": "5.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz",
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
"canonicalize": "^2.0.0",
|
"canonicalize": "^2.0.0",
|
||||||
"svelte-boring-avatars": "^1.2.6",
|
"svelte-boring-avatars": "^1.2.6",
|
||||||
"svelte-hero-icons": "^5.2.0",
|
"svelte-hero-icons": "^5.2.0",
|
||||||
|
"svelte-persisted-store": "^0.11.0",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"typewriter-editor": "^0.12.6",
|
"typewriter-editor": "^0.12.6",
|
||||||
"unique-names-generator": "^4.7.1",
|
"unique-names-generator": "^4.7.1",
|
||||||
|
|
|
@ -3,7 +3,9 @@ import { DropdownMenu } from 'bits-ui';
|
||||||
import Content from './DropdownMenu/Content.svelte';
|
import Content from './DropdownMenu/Content.svelte';
|
||||||
import Trigger from './DropdownMenu/Trigger.svelte';
|
import Trigger from './DropdownMenu/Trigger.svelte';
|
||||||
import Item from './DropdownMenu/Item.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 };
|
export { Root, Trigger, Content, Item, RadioGroup, RadioItem, Separator };
|
||||||
|
|
|
@ -1,12 +1,22 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { tw } from '$lib/tw';
|
||||||
import { DropdownMenu, type DropdownMenuContentProps } from 'bits-ui';
|
import { DropdownMenu, type DropdownMenuContentProps } from 'bits-ui';
|
||||||
|
import { expoOut } from 'svelte/easing';
|
||||||
|
import { scale } from 'svelte/transition';
|
||||||
|
|
||||||
interface $$Props extends DropdownMenuContentProps {}
|
interface $$Props extends DropdownMenuContentProps {}
|
||||||
|
let className: $$Props['class'] = '';
|
||||||
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenu.Content
|
<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}
|
sideOffset={4}
|
||||||
|
transition={scale}
|
||||||
|
transitionConfig={{ start: 0.96, duration: 300, easing: expoOut }}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenu.Item
|
<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
|
on:click
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
>
|
>
|
||||||
|
|
19
src/lib/components/DropdownMenu/RadioItem.svelte
Normal file
19
src/lib/components/DropdownMenu/RadioItem.svelte
Normal 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>
|
5
src/lib/components/DropdownMenu/Separator.svelte
Normal file
5
src/lib/components/DropdownMenu/Separator.svelte
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu } from 'bits-ui';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenu.Separator class="my-1 border-t border-ss-secondary" />
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { persisted } from 'svelte-persisted-store';
|
||||||
import type { EncodedBlahKeyPair } from './blah/crypto';
|
import type { EncodedBlahKeyPair } from './blah/crypto';
|
||||||
import { localStore } from './localstore';
|
|
||||||
|
|
||||||
export const keyStore = localStore<EncodedBlahKeyPair[]>('weblah-keypairs', []);
|
export const keyStore = persisted<EncodedBlahKeyPair[]>('weblah-keypairs', []);
|
||||||
export const currentKeyIndex = localStore<number>('weblah-current-key-index', 0);
|
export const currentKeyIndex = persisted<number>('weblah-current-key-index', 0);
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -5,8 +5,11 @@
|
||||||
import { BlahKeyPair, generateName } from '$lib/blah/crypto';
|
import { BlahKeyPair, generateName } from '$lib/blah/crypto';
|
||||||
|
|
||||||
let currentKeyId: string | undefined;
|
let currentKeyId: string | undefined;
|
||||||
$: currentKeyId = $keyStore[$currentKeyIndex]?.id;
|
let currentKeyName: string | null;
|
||||||
$: currentKeyName = currentKeyId ? generateName(currentKeyId) : null;
|
$: {
|
||||||
|
currentKeyId = $keyStore[$currentKeyIndex]?.id;
|
||||||
|
currentKeyName = currentKeyId ? generateName(currentKeyId) : null;
|
||||||
|
}
|
||||||
|
|
||||||
async function createKeyPair() {
|
async function createKeyPair() {
|
||||||
const newKeyPair = await BlahKeyPair.generate();
|
const newKeyPair = await BlahKeyPair.generate();
|
||||||
|
@ -14,12 +17,18 @@
|
||||||
$keyStore = [...$keyStore, encoded];
|
$keyStore = [...$keyStore, encoded];
|
||||||
$currentKeyIndex = $keyStore.length - 1;
|
$currentKeyIndex = $keyStore.length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setCurrentKeyIndex(idx: string | undefined | null) {
|
||||||
|
$currentKeyIndex = parseInt(idx ?? '0', 10);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root closeOnItemClick={false}>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
{#if currentKeyId}
|
{#if currentKeyId}
|
||||||
<AvatarBeam size={30} name={currentKeyId} />
|
{#key currentKeyId}
|
||||||
|
<AvatarBeam size={30} name={currentKeyId} />
|
||||||
|
{/key}
|
||||||
<span class="sr-only">Using identity {currentKeyName}</span>
|
<span class="sr-only">Using identity {currentKeyName}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
|
@ -29,19 +38,26 @@
|
||||||
<span class="sr-only">Using no identity</span>
|
<span class="sr-only">Using no identity</span>
|
||||||
{/if}
|
{/if}
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content class="origin-top-left">
|
||||||
{#if $keyStore.length > 0}
|
{#if $keyStore.length > 0}
|
||||||
<DropdownMenu.RadioGroup bind:value={currentKeyId}>
|
<DropdownMenu.RadioGroup
|
||||||
{#each $keyStore as { id }}
|
value={$currentKeyIndex.toString()}
|
||||||
|
onValueChange={setCurrentKeyIndex}
|
||||||
|
>
|
||||||
|
{#each $keyStore as { id }, idx}
|
||||||
{@const name = generateName(id)}
|
{@const name = generateName(id)}
|
||||||
<DropdownMenu.RadioItem value={id}>
|
<DropdownMenu.RadioItem value={idx.toString()}>
|
||||||
<AvatarBeam size={30} {name} />
|
<div class="flex items-center gap-2 py-0.5">
|
||||||
<span>{name}</span>
|
<AvatarBeam size={24} name={id} />
|
||||||
|
<span>{name}</span>
|
||||||
|
</div>
|
||||||
</DropdownMenu.RadioItem>
|
</DropdownMenu.RadioItem>
|
||||||
{/each}
|
{/each}
|
||||||
</DropdownMenu.RadioGroup>
|
</DropdownMenu.RadioGroup>
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item>Manage identities</DropdownMenu.Item>
|
||||||
|
{:else}
|
||||||
|
<DropdownMenu.Item on:click={createKeyPair}>Create new identity</DropdownMenu.Item>
|
||||||
{/if}
|
{/if}
|
||||||
<DropdownMenu.Item on:click={createKeyPair}>Create new identity</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
|
|
Loading…
Add table
Reference in a new issue