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 type { ActorSuggestion } from "$/lib/types";
5import { createEffect, Show } from "solid-js";
6import { Motion } from "solid-motionone";
7import { Icon } from "./shared/Icon";
8import { LazuriteLogo } from "./Wordmark";
9
10function LoginSubmitButton(props: { pending: boolean }) {
11 return (
12 <button class="pill-action border-0 bg-primary text-on-primary-fixed" type="submit" disabled={props.pending}>
13 <Show
14 when={props.pending}
15 fallback={
16 <>
17 <Icon kind="ext-link" name="ext-link" aria-hidden class="mr-1" />
18 <span>Continue</span>
19 </>
20 }>
21 <Icon kind="loader" name="loader" aria-hidden class="mr-1" />
22 <span>Opening sign-in...</span>
23 </Show>
24 </button>
25 );
26}
27
28type LoginPanelProps = {
29 value: string;
30 pending: boolean;
31 shakeCount: number;
32 onInput: (value: string) => void;
33 onSubmit: () => void;
34};
35
36export function LoginPanel(props: LoginPanelProps) {
37 let container: HTMLDivElement | undefined;
38 let input: HTMLInputElement | undefined;
39 const typeahead = useActorSuggestions({
40 container: () => container,
41 disabled: () => props.pending,
42 input: () => input,
43 value: () => props.value,
44 });
45 const combobox = useActorTypeaheadCombobox({
46 ariaControls: "login-suggestions",
47 onSelect: applySuggestion,
48 typeahead,
49 });
50
51 createEffect(() => {
52 if (props.shakeCount > 0) {
53 input?.focus();
54 input?.select();
55 }
56 });
57
58 function applySuggestion(suggestion: ActorSuggestion) {
59 props.onInput(suggestion.handle);
60 typeahead.close();
61 input?.focus();
62 }
63
64 return (
65 <article
66 class="panel-surface grid gap-5 p-5"
67 ref={(element) => {
68 container = element as HTMLDivElement;
69 }}>
70 <div class="grid place-items-center gap-3 py-2">
71 <span class="grid place-items-center text-primary">
72 <LazuriteLogo class="h-14 w-14" />
73 </span>
74 <div class="grid place-items-center gap-0.5">
75 <p class="m-0 text-[1.25rem] font-semibold tracking-[-0.02em]">Lazurite</p>
76 <p class="m-0 text-xs text-on-surface-variant">Powered by Bluesky</p>
77 </div>
78 </div>
79
80 <Motion.form
81 class="grid gap-4"
82 initial={{ opacity: 0, y: 18 }}
83 animate={{ opacity: 1, y: 0, x: props.shakeCount > 0 ? [0, -16, 10, -8, 0] : 0 }}
84 transition={{ duration: props.shakeCount > 0 ? 0.42 : 0.24, easing: [0.22, 1, 0.36, 1] }}
85 onSubmit={(event) => {
86 event.preventDefault();
87 props.onSubmit();
88 }}>
89 <label class="grid gap-3">
90 <span class="overline-copy text-xs tracking-[0.08em] text-on-surface-variant">
91 {/* TODO: use tauri opener */}
92 Sign in with your <a href="https://internethandle.org" class="text-primary underline">Internet Handle</a>
93 {" "}
94 or DID
95 </span>
96 <div class="relative">
97 <input
98 ref={(element) => {
99 input = element;
100 }}
101 class="min-h-[3.4rem] w-full rounded-xl border-0 bg-white/4 px-[1.15rem] pr-11 text-on-surface shadow-[inset_0_0_0_1px_rgba(125,175,255,0.16)] focus:outline focus:outline-primary/50 focus:shadow-[inset_0_0_0_1px_rgba(125,175,255,0.35),0_0_28px_rgba(125,175,255,0.12)]"
102 type="text"
103 role="combobox"
104 aria-autocomplete="list"
105 aria-controls={combobox.a11y.controls}
106 aria-activedescendant={combobox.a11y.activeDescendant()}
107 aria-expanded={combobox.a11y.expanded()}
108 autocomplete="username"
109 spellcheck={false}
110 value={props.value}
111 placeholder="alice.bsky.social"
112 onFocus={() => typeahead.focus()}
113 onInput={(event) => props.onInput(event.currentTarget.value)}
114 onKeyDown={(event) => combobox.handleKeyDown(event)} />
115 <ActorTypeaheadLoading visible={typeahead.loading()} class="right-4" />
116 <ActorSuggestionList
117 activeIndex={typeahead.activeIndex()}
118 id="login-suggestions"
119 open={typeahead.open()}
120 suggestions={typeahead.suggestions()}
121 title="Suggested handles"
122 onSelect={applySuggestion} />
123 </div>
124 </label>
125 <LoginSubmitButton pending={props.pending} />
126 </Motion.form>
127 </article>
128 );
129}