atmosphere explorer pds.ls
tool typescript atproto
430
fork

Configure Feed

Select the types of activity you want to include in your feed.

new tag input replacing textarea

Juliet d13b0c7f cc7ed302

+102 -31
+69
src/components/tag-input.tsx
··· 1 + import { createSignal, For, Show } from "solid-js"; 2 + import { TextInput } from "./text-input"; 3 + 4 + export const TagInput = (props: { 5 + name: string; 6 + placeholder?: string; 7 + initialValues?: string[]; 8 + }) => { 9 + const [tags, setTags] = createSignal<string[]>(props.initialValues ?? []); 10 + const [inputValue, setInputValue] = createSignal(""); 11 + 12 + const addTag = () => { 13 + const value = inputValue().trim(); 14 + if (value && !tags().includes(value)) { 15 + setTags([...tags(), value]); 16 + setInputValue(""); 17 + } 18 + }; 19 + 20 + const removeTag = (index: number) => { 21 + setTags(tags().filter((_, i) => i !== index)); 22 + }; 23 + 24 + const onKeyDown = (e: KeyboardEvent) => { 25 + if (e.key === "Enter") { 26 + e.preventDefault(); 27 + addTag(); 28 + } 29 + }; 30 + 31 + return ( 32 + <div class="flex min-w-0 grow flex-col gap-1.5"> 33 + <input type="hidden" name={props.name} value={tags().join(",")} /> 34 + <div class="flex gap-1.5"> 35 + <TextInput 36 + value={inputValue()} 37 + onInput={(e) => setInputValue(e.currentTarget.value)} 38 + onKeyDown={onKeyDown} 39 + placeholder={props.placeholder} 40 + class="min-w-0 grow" 41 + /> 42 + <button 43 + type="button" 44 + onClick={addTag} 45 + class="dark:bg-dark-300 dark:hover:bg-dark-200 dark:active:bg-dark-100 flex shrink-0 items-center gap-1 rounded-md border border-neutral-200 bg-neutral-50 px-2.5 py-1 text-xs text-neutral-700 transition-colors select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:text-neutral-300" 46 + > 47 + <span class="iconify lucide--plus text-xs"></span> 48 + Add 49 + </button> 50 + </div> 51 + <Show when={tags().length > 0}> 52 + <div class="flex flex-wrap gap-1"> 53 + <For each={tags()}> 54 + {(tag, index) => ( 55 + <button 56 + type="button" 57 + onClick={() => removeTag(index())} 58 + class="group dark:bg-dark-200 flex items-center gap-1 rounded-full bg-neutral-200/70 px-2 py-0.5 text-xs text-neutral-700 hover:bg-neutral-300/70 dark:text-neutral-300 dark:hover:bg-neutral-700" 59 + > 60 + <span class="max-w-48 truncate sm:max-w-none">{tag}</span> 61 + <span class="iconify lucide--x text-current opacity-40 group-hover:opacity-100"></span> 62 + </button> 63 + )} 64 + </For> 65 + </div> 66 + </Show> 67 + </div> 68 + ); 69 + };
+2
src/components/text-input.tsx
··· 10 10 spellcheck?: boolean; 11 11 value?: string | string[]; 12 12 onInput?: (ev: InputEvent & { currentTarget: HTMLInputElement }) => void; 13 + onKeyDown?: (ev: KeyboardEvent & { currentTarget: HTMLInputElement }) => void; 13 14 } 14 15 15 16 export const TextInput = (props: TextInputProps) => { ··· 29 30 props.class 30 31 } 31 32 onInput={props.onInput} 33 + onKeyDown={props.onKeyDown} 32 34 /> 33 35 ); 34 36 };
+9 -13
src/views/labels.tsx
··· 7 7 import { Button } from "../components/button.jsx"; 8 8 import DidHoverCard from "../components/hover-card/did.jsx"; 9 9 import RecordHoverCard from "../components/hover-card/record.jsx"; 10 + import { TagInput } from "../components/tag-input.jsx"; 10 11 import { TextInput } from "../components/text-input.jsx"; 11 12 import { canHover } from "../layout.jsx"; 12 13 import { getPDS, labelerCache, resolveHandle } from "../lib/api.js"; ··· 129 130 130 131 const fetchLabels = async (formData: FormData, reset?: boolean) => { 131 132 let did = formData.get("did")?.toString()?.trim() || DEFAULT_LABELER_DID; 132 - const uriPatterns = formData.get("uriPatterns")?.toString()?.trim(); 133 - 134 - if (!uriPatterns) { 135 - setError("Please provide both DID and URI patterns"); 136 - return; 137 - } 133 + const uriPatterns = formData.get("uriPatterns")?.toString()?.trim() || "*"; 138 134 139 135 if (reset) { 140 136 setLabels([]); ··· 219 215 220 216 <label class="flex w-full flex-col gap-y-1"> 221 217 <span class="text-sm font-medium text-neutral-700 dark:text-neutral-300"> 222 - URI patterns (comma-separated) 218 + URI patterns 223 219 </span> 224 - <textarea 225 - id="uriPatterns" 220 + <TagInput 226 221 name="uriPatterns" 227 - spellcheck={false} 228 - rows={2} 229 - value={searchParams.uriPatterns ?? "*"} 230 222 placeholder="at://did:web:example.com/app.bsky.feed.post/*" 231 - class="dark:bg-dark-100 grow rounded-lg bg-white px-2 py-1.5 text-sm outline-1 outline-neutral-200 focus:outline-neutral-400 dark:outline-neutral-600 dark:focus:outline-neutral-400" 223 + initialValues={ 224 + (searchParams.uriPatterns as string) 225 + ?.split(",") 226 + .filter((v) => v.trim().length > 0) ?? [] 227 + } 232 228 /> 233 229 </label> 234 230 </div>
+11 -11
src/views/stream/config.ts
··· 5 5 export type FormField = { 6 6 name: string; 7 7 label: string; 8 - type: "text" | "textarea" | "checkbox"; 8 + type: "text" | "checkbox" | "tags"; 9 9 placeholder?: string; 10 10 searchParam: string; 11 11 }; ··· 45 45 { 46 46 name: "collections", 47 47 label: "Collections", 48 - type: "textarea", 49 - placeholder: "Comma-separated list of collections", 48 + type: "tags", 49 + placeholder: "app.bsky.feed.post", 50 50 searchParam: "collections", 51 51 }, 52 52 { 53 53 name: "dids", 54 54 label: "DIDs", 55 - type: "textarea", 56 - placeholder: "Comma-separated list of DIDs", 55 + type: "tags", 56 + placeholder: "did:plc:xyz...", 57 57 searchParam: "dids", 58 58 }, 59 59 { ··· 145 145 { 146 146 name: "sources", 147 147 label: "Sources", 148 - type: "textarea", 149 - placeholder: "e.g. app.bsky.graph.follow:subject", 148 + type: "tags", 149 + placeholder: "app.bsky.graph.follow:subject", 150 150 searchParam: "sources", 151 151 }, 152 152 { 153 153 name: "subjectDids", 154 154 label: "Subject DIDs", 155 - type: "textarea", 156 - placeholder: "Comma-separated list of DIDs", 155 + type: "tags", 156 + placeholder: "did:plc:xyz...", 157 157 searchParam: "subjectDids", 158 158 }, 159 159 { 160 160 name: "subjects", 161 161 label: "Subjects", 162 - type: "textarea", 163 - placeholder: "Comma-separated list of AT URIs", 162 + type: "tags", 163 + placeholder: "at://did:plc:xyz.../app.bsky.feed.post/abc", 164 164 searchParam: "subjects", 165 165 }, 166 166 {
+11 -7
src/views/stream/index.tsx
··· 4 4 import { Button } from "../../components/button"; 5 5 import DidHoverCard from "../../components/hover-card/did"; 6 6 import { JSONValue } from "../../components/json"; 7 + import { TagInput } from "../../components/tag-input"; 7 8 import { TextInput } from "../../components/text-input"; 8 9 import { addToClipboard } from "../../utils/copy"; 9 10 import { websocketCloseReasons } from "../../utils/websocket"; ··· 366 367 367 368 <For each={config().fields}> 368 369 {(field) => ( 369 - <label class="flex items-center justify-end gap-x-1"> 370 + <label class={`flex justify-end gap-x-1 ${field.type === "tags" ? "items-start" : "items-center"}`}> 370 371 <Show when={field.type === "checkbox"}> 371 372 <input 372 373 type="checkbox" ··· 375 376 checked={searchParams[field.searchParam] === "on"} 376 377 /> 377 378 </Show> 378 - <span class="min-w-21 select-none">{field.label}</span> 379 - <Show when={field.type === "textarea"}> 380 - <textarea 379 + <span class={`min-w-21 select-none ${field.type === "tags" ? "mt-1" : ""}`}>{field.label}</span> 380 + <Show when={field.type === "tags"}> 381 + <TagInput 381 382 name={field.name} 382 - spellcheck={false} 383 383 placeholder={field.placeholder} 384 - value={(searchParams[field.searchParam] as string) ?? ""} 385 - class="dark:bg-dark-100 grow rounded-lg bg-white px-2 py-1 outline-1 outline-neutral-200 focus:outline-neutral-400 dark:outline-neutral-600 dark:focus:outline-neutral-400" 384 + initialValues={ 385 + (searchParams[field.searchParam] as string) 386 + ?.split(",") 387 + .filter((v) => v.trim().length > 0) ?? [] 388 + } 386 389 /> 387 390 </Show> 391 + 388 392 <Show when={field.type === "text"}> 389 393 <TextInput 390 394 name={field.name}