BlueSky & more on desktop
lazurite.stormlightlabs.org/
tauri
rust
typescript
bluesky
appview
atproto
solid
1import { ActorSuggestionList, useActorSuggestions } from "$/components/actors/ActorSearch";
2import { ActorTypeaheadLoading } from "$/components/actors/ActorTypeaheadLoading";
3import { useActorTypeaheadCombobox } from "$/components/actors/hooks/useActorTypeaheadCombobox";
4import { Icon } from "$/components/shared/Icon";
5import type { ActorSuggestion } from "$/lib/types";
6import * as logger from "@tauri-apps/plugin-log";
7import { createSignal } from "solid-js";
8import type { ProfileSelection } from "../types";
9
10export function ProfilePicker(props: { onSubmit: (selection: ProfileSelection) => void }) {
11 let container: HTMLDivElement | undefined;
12 let input: HTMLInputElement | undefined;
13 const [value, setValue] = createSignal("");
14 const typeahead = useActorSuggestions({
15 container: () => container,
16 input: () => input,
17 onError: (error) => logger.warn(`Failed to load profile suggestions: ${String(error)}`),
18 value,
19 });
20 const combobox = useActorTypeaheadCombobox({
21 ariaControls: "profile-suggestions",
22 onSelect: submitSuggestion,
23 typeahead,
24 });
25
26 function submitManualActor() {
27 const actor = value().trim();
28 if (!actor) {
29 return;
30 }
31
32 typeahead.close();
33 props.onSubmit({ actor });
34 }
35
36 function submitSuggestion(suggestion: ActorSuggestion) {
37 typeahead.close();
38 props.onSubmit({
39 actor: suggestion.handle,
40 did: suggestion.did,
41 displayName: suggestion.displayName ?? null,
42 handle: suggestion.handle,
43 });
44 }
45
46 return (
47 <form
48 class="grid gap-3"
49 onSubmit={(event) => {
50 event.preventDefault();
51 submitManualActor();
52 }}>
53 <label class="grid gap-1.5">
54 <span class="text-xs font-medium uppercase tracking-wide text-on-surface-variant">Handle or DID</span>
55 <div
56 class="relative"
57 ref={(element) => {
58 container = element as HTMLDivElement;
59 }}>
60 <input
61 ref={(element) => {
62 input = element;
63 }}
64 type="text"
65 role="combobox"
66 aria-autocomplete="list"
67 aria-controls={combobox.a11y.controls}
68 aria-activedescendant={combobox.a11y.activeDescendant()}
69 aria-expanded={combobox.a11y.expanded()}
70 class="ui-input ui-input-strong w-full rounded-xl px-4 py-2.5 pr-10"
71 placeholder="alice.bsky.social"
72 spellcheck={false}
73 value={value()}
74 onFocus={() => typeahead.focus()}
75 onInput={(event) => setValue(event.currentTarget.value)}
76 onKeyDown={(event) => combobox.handleKeyDown(event)} />
77
78 <ActorTypeaheadLoading visible={typeahead.loading()} iconClass="animate-spin text-sm" />
79 <ActorSuggestionList
80 activeIndex={typeahead.activeIndex()}
81 id="profile-suggestions"
82 open={typeahead.open()}
83 suggestions={typeahead.suggestions()}
84 title="Suggested profiles"
85 onSelect={submitSuggestion} />
86 </div>
87 </label>
88
89 <button
90 type="submit"
91 disabled={!value().trim()}
92 class="flex items-center justify-center gap-2 rounded-xl border-0 bg-primary/15 px-4 py-2.5 text-sm font-medium text-primary transition duration-150 hover:-translate-y-px hover:bg-primary/25 disabled:cursor-not-allowed disabled:opacity-40">
93 <Icon kind="profile" />
94 Open profile
95 </button>
96 </form>
97 );
98}