diff --git a/web/src/lib/api/lexicons/index.ts b/web/src/lib/api/lexicons/index.ts index 2f9b36dc..f940142a 100644 --- a/web/src/lib/api/lexicons/index.ts +++ b/web/src/lib/api/lexicons/index.ts @@ -34,6 +34,7 @@ export * as ShTangledActorGetProfile from "./types/sh/tangled/actor/getProfile.j export * as ShTangledActorGetProfiles from "./types/sh/tangled/actor/getProfiles.js"; export * as ShTangledActorGetTrending from "./types/sh/tangled/actor/getTrending.js"; export * as ShTangledActorProfile from "./types/sh/tangled/actor/profile.js"; +export * as ShTangledActorSearchActorsTypeahead from "./types/sh/tangled/actor/searchActorsTypeahead.js"; export * as ShTangledBobbinAwaitRecord from "./types/sh/tangled/bobbin/awaitRecord.js"; export * as ShTangledCiCancelPipeline from "./types/sh/tangled/ci/cancelPipeline.js"; export * as ShTangledCiDescribeWorkflowDefinition from "./types/sh/tangled/ci/describeWorkflowDefinition.js"; diff --git a/web/src/lib/api/lexicons/types/sh/tangled/actor/searchActorsTypeahead.ts b/web/src/lib/api/lexicons/types/sh/tangled/actor/searchActorsTypeahead.ts new file mode 100644 index 00000000..bff3c56c --- /dev/null +++ b/web/src/lib/api/lexicons/types/sh/tangled/actor/searchActorsTypeahead.ts @@ -0,0 +1,69 @@ +import type {} from "@atcute/lexicons"; +import * as v from "@atcute/lexicons/validations"; +import type {} from "@atcute/lexicons/ambient"; + +const _actorSchema = /*#__PURE__*/ v.object({ + $type: /*#__PURE__*/ v.optional( + /*#__PURE__*/ v.literal("sh.tangled.actor.searchActorsTypeahead#actor"), + ), + did: /*#__PURE__*/ v.didString(), + handle: /*#__PURE__*/ v.handleString(), + /** + * AT-URI for the actor's Tangled profile. + */ + uri: /*#__PURE__*/ v.resourceUriString(), +}); +const _mainSchema = /*#__PURE__*/ v.query( + "sh.tangled.actor.searchActorsTypeahead", + { + params: /*#__PURE__*/ v.object({ + /** + * @minimum 1 + * @maximum 100 + * @default 10 + */ + limit: /*#__PURE__*/ v.optional( + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.integer(), [ + /*#__PURE__*/ v.integerRange(1, 100), + ]), + 10, + ), + /** + * The beginning of a handle. A leading \@ is optional. + * @minLength 1 + * @maxLength 254 + */ + q: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ + /*#__PURE__*/ v.stringLength(1, 254), + ]), + }), + output: { + type: "lex", + schema: /*#__PURE__*/ v.object({ + get actors() { + return /*#__PURE__*/ v.array(actorSchema); + }, + }), + }, + }, +); + +type actor$schematype = typeof _actorSchema; +type main$schematype = typeof _mainSchema; + +export interface actorSchema extends actor$schematype {} +export interface mainSchema extends main$schematype {} + +export const actorSchema = _actorSchema as actorSchema; +export const mainSchema = _mainSchema as mainSchema; + +export interface Actor extends v.InferInput {} + +export interface $params extends v.InferInput {} +export interface $output extends v.InferXRPCBodyInput {} + +declare module "@atcute/lexicons/ambient" { + interface XRPCQueries { + "sh.tangled.actor.searchActorsTypeahead": mainSchema; + } +} diff --git a/web/src/lib/api/search.ts b/web/src/lib/api/search.ts index dfca7a00..b41b0c7b 100644 --- a/web/src/lib/api/search.ts +++ b/web/src/lib/api/search.ts @@ -1,3 +1,4 @@ +import type * as ShTangledActorSearchActorsTypeahead from "./lexicons/types/sh/tangled/actor/searchActorsTypeahead"; import type { BobbinContext, XrpcRequestInit } from "./client"; import { jsonGet } from "./_request"; import { paginateBy } from "./pagination"; @@ -31,6 +32,20 @@ export interface SearchParams { export const search = (ctx: BobbinContext, params: SearchParams, init?: XrpcRequestInit) => jsonGet(ctx, "sh.tangled.search.query", { ...params }, init); +export type ActorSuggestion = ShTangledActorSearchActorsTypeahead.Actor; + +export const searchActorsTypeahead = ( + ctx: BobbinContext, + params: ShTangledActorSearchActorsTypeahead.$params, + init?: XrpcRequestInit +) => + jsonGet( + ctx, + "sh.tangled.actor.searchActorsTypeahead", + params, + init + ); + export interface SearchAllOptions extends XrpcRequestInit { maxPages?: number; } diff --git a/web/src/lib/components/ui/ActorInput.svelte b/web/src/lib/components/ui/ActorInput.svelte new file mode 100644 index 00000000..7ed62576 --- /dev/null +++ b/web/src/lib/components/ui/ActorInput.svelte @@ -0,0 +1,242 @@ + + +
+ = 0 ? `${resultListId}-${activeIndex}` : undefined} + /> + +
+
+ {#if actors.length > 0} + {#each actors as actor, index (actor.did)} + + {/each} + {:else if status === "loading"} +

Searching…

+ {:else if status === "failed"} +

Couldn’t load actors

+ {:else} +

No actors found

+ {/if} +
+
+
+ + diff --git a/web/src/routes/[handle]/[repo]/settings/access/new/+page.svelte b/web/src/routes/[handle]/[repo]/settings/access/new/+page.svelte index 95690b14..d6ad2e45 100644 --- a/web/src/routes/[handle]/[repo]/settings/access/new/+page.svelte +++ b/web/src/routes/[handle]/[repo]/settings/access/new/+page.svelte @@ -2,7 +2,7 @@ import { goto } from "$app/navigation"; import { resolve } from "$app/paths"; import Button from "$lib/components/ui/Button.svelte"; - import Input from "$lib/components/ui/Input.svelte"; + import ActorInput from "$lib/components/ui/ActorInput.svelte"; import Pending from "$lib/components/ui/Pending.svelte"; import Bones from "$lib/components/ui/Bones.svelte"; import settingsSettingspanelContentSkeletonFixture from "$lib/bones/settings-settingspanel--content-skeleton-fixture.bones.json"; @@ -13,10 +13,11 @@ import Plus from "$icon/plus"; import X from "$icon/x"; + import type { ActorSuggestion } from "$lib/api/search"; let { data } = $props(); - let user = $state(""); - const valid = $derived(user.trim().length > 0); + let actor = $state(); + const valid = $derived(actor !== undefined); // mocked — collaborator management is not wired up yet const submit = () => { @@ -41,7 +42,12 @@ > - + diff --git a/web/src/routes/login/+page.svelte b/web/src/routes/login/+page.svelte index ca0bceff..0d83210e 100644 --- a/web/src/routes/login/+page.svelte +++ b/web/src/routes/login/+page.svelte @@ -5,10 +5,12 @@ import { getAuth } from "$lib/auth.svelte"; import { createAction } from "$lib/action.svelte"; import Button from "$lib/components/ui/Button.svelte"; + import ActorInput from "$lib/components/ui/ActorInput.svelte"; import CircleAlert from "$icon/circle-alert"; import { onMount } from "svelte"; import { SvelteURL } from "svelte/reactivity"; + let { data } = $props(); const appPath = (path: string) => resolve(path as "/"); const auth = getAuth(); const returnTo = $derived(page.url.searchParams.get("return_url") ?? "/"); @@ -55,8 +57,9 @@ -

Use your AT Protocol handle to log in.