atproto explorer pdsls.dev
atproto tool

Compare changes

Choose any two refs to compare.

+38 -12
src/components/dropdown.tsx
···
Show,
useContext,
} from "solid-js";
+
import { Portal } from "solid-js/web";
import { addToClipboard } from "../utils/copy";
const MenuContext = createContext<{
···
const ctx = useContext(MenuContext);
const [menu, setMenu] = createSignal<HTMLDivElement>();
const [menuButton, setMenuButton] = createSignal<HTMLButtonElement>();
+
const [buttonRect, setButtonRect] = createSignal<DOMRect>();
const clickEvent = (event: MouseEvent) => {
const target = event.target as Node;
if (!menuButton()?.contains(target) && !menu()?.contains(target)) ctx?.setShowMenu(false);
};
-
onMount(() => window.addEventListener("click", clickEvent));
-
onCleanup(() => window.removeEventListener("click", clickEvent));
+
const updatePosition = () => {
+
const rect = menuButton()?.getBoundingClientRect();
+
if (rect) setButtonRect(rect);
+
};
+
+
onMount(() => {
+
window.addEventListener("click", clickEvent);
+
window.addEventListener("scroll", updatePosition, true);
+
window.addEventListener("resize", updatePosition);
+
});
+
+
onCleanup(() => {
+
window.removeEventListener("click", clickEvent);
+
window.removeEventListener("scroll", updatePosition, true);
+
window.removeEventListener("resize", updatePosition);
+
});
return (
<div class="relative">
···
props.buttonClass
}
ref={setMenuButton}
-
onClick={() => ctx?.setShowMenu(!ctx?.showMenu())}
+
onClick={() => {
+
updatePosition();
+
ctx?.setShowMenu(!ctx?.showMenu());
+
}}
>
<span class={"iconify " + props.icon}></span>
</button>
<Show when={ctx?.showMenu()}>
-
<div
-
ref={setMenu}
-
class={
-
"dark:bg-dark-300 dark:shadow-dark-700 absolute right-0 z-40 flex min-w-40 flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 shadow-md dark:border-neutral-700 " +
-
props.menuClass
-
}
-
>
-
{props.children}
-
</div>
+
<Portal>
+
<div
+
ref={setMenu}
+
style={{
+
position: "fixed",
+
top: `${(buttonRect()?.bottom ?? 0) + 4}px`,
+
left: `${(buttonRect()?.right ?? 0) - 160}px`,
+
}}
+
class={
+
"dark:bg-dark-300 dark:shadow-dark-700 z-50 flex min-w-40 flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-md dark:border-neutral-700 " +
+
props.menuClass
+
}
+
>
+
{props.children}
+
</div>
+
</Portal>
</Show>
</div>
);
+1 -1
src/components/notification.tsx
···
export const NotificationContainer = () => {
return (
-
<div class="pointer-events-none fixed bottom-4 left-4 z-50 flex flex-col gap-2">
+
<div class="pointer-events-none fixed bottom-4 left-4 z-60 flex flex-col gap-2">
<For each={notifications}>
{(notification) => (
<div
+30
src/utils/key.ts
···
+
import { parseDidKey, parsePublicMultikey } from "@atcute/crypto";
+
import { fromBase58Btc } from "@atcute/multibase";
+
+
export const detectKeyType = (key: string): string => {
+
try {
+
return parsePublicMultikey(key).type;
+
} catch (e) {
+
try {
+
const bytes = fromBase58Btc(key.startsWith("z") ? key.slice(1) : key);
+
if (bytes.length >= 2) {
+
const type = (bytes[0] << 8) | bytes[1];
+
if (type === 0xed01) {
+
return "ed25519";
+
}
+
}
+
} catch {}
+
return "unknown";
+
}
+
};
+
+
export const detectDidKeyType = (key: string): string => {
+
try {
+
return parseDidKey(key).type;
+
} catch (e) {
+
if (key.startsWith("did:key:")) {
+
return detectKeyType(key.slice(8));
+
}
+
return "unknown";
+
}
+
};
+1 -1
public/oauth-client-metadata.json
···
"client_uri": "https://pdsls.dev",
"logo_uri": "https://pdsls.dev/favicon.ico",
"redirect_uris": ["https://pdsls.dev/"],
-
"scope": "atproto transition:generic",
+
"scope": "atproto repo:*?action=create repo:*?action=update repo:*?action=delete blob:*/*",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
+13
src/auth/oauth-config.ts
···
+
import { configureOAuth, defaultIdentityResolver } from "@atcute/oauth-browser-client";
+
import { didDocumentResolver, handleResolver } from "../utils/api";
+
+
configureOAuth({
+
metadata: {
+
client_id: import.meta.env.VITE_OAUTH_CLIENT_ID,
+
redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL,
+
},
+
identityResolver: defaultIdentityResolver({
+
handleResolver: handleResolver,
+
didDocumentResolver: didDocumentResolver,
+
}),
+
});
+77
src/auth/scope-flow.ts
···
+
import { isDid, isHandle } from "@atcute/lexicons/syntax";
+
import { createAuthorizationUrl } from "@atcute/oauth-browser-client";
+
import { createSignal } from "solid-js";
+
+
interface UseOAuthScopeFlowOptions {
+
onError?: (error: unknown) => void;
+
onRedirecting?: () => void;
+
beforeRedirect?: (account: string) => Promise<void>;
+
}
+
+
export const useOAuthScopeFlow = (options: UseOAuthScopeFlowOptions = {}) => {
+
const [showScopeSelector, setShowScopeSelector] = createSignal(false);
+
const [pendingAccount, setPendingAccount] = createSignal("");
+
const [shouldForceRedirect, setShouldForceRedirect] = createSignal(false);
+
+
const initiate = (account: string) => {
+
if (!account) return;
+
setPendingAccount(account);
+
setShouldForceRedirect(false);
+
setShowScopeSelector(true);
+
};
+
+
const initiateWithRedirect = (account: string) => {
+
if (!account) return;
+
setPendingAccount(account);
+
setShouldForceRedirect(true);
+
setShowScopeSelector(true);
+
};
+
+
const complete = async (scopeString: string, scopeIds: string) => {
+
try {
+
const account = pendingAccount();
+
+
if (options.beforeRedirect && !shouldForceRedirect()) {
+
try {
+
await options.beforeRedirect(account);
+
setShowScopeSelector(false);
+
return;
+
} catch {}
+
}
+
+
localStorage.setItem("pendingScopes", scopeIds);
+
+
options.onRedirecting?.();
+
+
const authUrl = await createAuthorizationUrl({
+
scope: scopeString,
+
target:
+
isHandle(account) || isDid(account) ?
+
{ type: "account", identifier: account }
+
: { type: "pds", serviceUrl: account },
+
});
+
+
await new Promise((resolve) => setTimeout(resolve, 250));
+
location.assign(authUrl);
+
} catch (e) {
+
console.error(e);
+
options.onError?.(e);
+
setShowScopeSelector(false);
+
}
+
};
+
+
const cancel = () => {
+
setShowScopeSelector(false);
+
setPendingAccount("");
+
setShouldForceRedirect(false);
+
};
+
+
return {
+
showScopeSelector,
+
pendingAccount,
+
initiate,
+
initiateWithRedirect,
+
complete,
+
cancel,
+
};
+
};
+53
src/auth/scope-utils.ts
···
+
import { agent, sessions } from "./state";
+
+
export const GRANULAR_SCOPES = [
+
{
+
id: "create",
+
scope: "repo:*?action=create",
+
label: "Create records",
+
},
+
{
+
id: "update",
+
scope: "repo:*?action=update",
+
label: "Update records",
+
},
+
{
+
id: "delete",
+
scope: "repo:*?action=delete",
+
label: "Delete records",
+
},
+
{
+
id: "blob",
+
scope: "blob:*/*",
+
label: "Upload blobs",
+
},
+
];
+
+
export const BASE_SCOPES = ["atproto"];
+
+
export const buildScopeString = (selected: Set<string>): string => {
+
const granular = GRANULAR_SCOPES.filter((s) => selected.has(s.id)).map((s) => s.scope);
+
return [...BASE_SCOPES, ...granular].join(" ");
+
};
+
+
export const scopeIdsToString = (scopeIds: Set<string>): string => {
+
return ["atproto", ...Array.from(scopeIds)].join(",");
+
};
+
+
export const parseScopeString = (scopeIdsString: string): Set<string> => {
+
if (!scopeIdsString) return new Set();
+
const ids = scopeIdsString.split(",").filter(Boolean);
+
return new Set(ids.filter((id) => id !== "atproto"));
+
};
+
+
export const hasScope = (grantedScopes: string | undefined, scopeId: string): boolean => {
+
if (!grantedScopes) return false;
+
return grantedScopes.split(",").includes(scopeId);
+
};
+
+
export const hasUserScope = (scopeId: string): boolean => {
+
if (!agent()) return false;
+
const grantedScopes = sessions[agent()!.sub]?.grantedScopes;
+
if (!grantedScopes) return true;
+
return hasScope(grantedScopes, scopeId);
+
};
+14
src/auth/state.ts
···
+
import { OAuthUserAgent } from "@atcute/oauth-browser-client";
+
import { createSignal } from "solid-js";
+
import { createStore } from "solid-js/store";
+
+
export type Account = {
+
signedIn: boolean;
+
handle?: string;
+
grantedScopes?: string;
+
};
+
+
export type Sessions = Record<string, Account>;
+
+
export const [agent, setAgent] = createSignal<OAuthUserAgent | undefined>();
+
export const [sessions, setSessions] = createStore<Sessions>();
-143
src/components/login.tsx
···
-
import { Client } from "@atcute/client";
-
import { Did } from "@atcute/lexicons";
-
import { isDid, isHandle } from "@atcute/lexicons/syntax";
-
import {
-
configureOAuth,
-
createAuthorizationUrl,
-
defaultIdentityResolver,
-
finalizeAuthorization,
-
getSession,
-
OAuthUserAgent,
-
type Session,
-
} from "@atcute/oauth-browser-client";
-
import { createSignal, Show } from "solid-js";
-
import { didDocumentResolver, handleResolver } from "../utils/api";
-
-
configureOAuth({
-
metadata: {
-
client_id: import.meta.env.VITE_OAUTH_CLIENT_ID,
-
redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL,
-
},
-
identityResolver: defaultIdentityResolver({
-
handleResolver: handleResolver,
-
didDocumentResolver: didDocumentResolver,
-
}),
-
});
-
-
export const [agent, setAgent] = createSignal<OAuthUserAgent | undefined>();
-
-
type Account = {
-
signedIn: boolean;
-
handle?: string;
-
};
-
-
export type Sessions = Record<string, Account>;
-
-
const Login = () => {
-
const [notice, setNotice] = createSignal("");
-
const [loginInput, setLoginInput] = createSignal("");
-
-
const login = async (handle: string) => {
-
try {
-
setNotice("");
-
if (!handle) return;
-
setNotice(`Contacting your data server...`);
-
const authUrl = await createAuthorizationUrl({
-
scope: import.meta.env.VITE_OAUTH_SCOPE,
-
target:
-
isHandle(handle) || isDid(handle) ?
-
{ type: "account", identifier: handle }
-
: { type: "pds", serviceUrl: handle },
-
});
-
-
setNotice(`Redirecting...`);
-
await new Promise((resolve) => setTimeout(resolve, 250));
-
-
location.assign(authUrl);
-
} catch (e) {
-
console.error(e);
-
setNotice(`${e}`);
-
}
-
};
-
-
return (
-
<form class="flex flex-col gap-y-2 px-1" onsubmit={(e) => e.preventDefault()}>
-
<label for="username" class="hidden">
-
Add account
-
</label>
-
<div class="dark:bg-dark-100 dark:inset-shadow-dark-200 flex grow items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 inset-shadow-xs focus-within:outline-[1px] focus-within:outline-neutral-600 dark:border-neutral-600 dark:focus-within:outline-neutral-400">
-
<label
-
for="username"
-
class="iconify lucide--user-round-plus shrink-0 text-neutral-500 dark:text-neutral-400"
-
></label>
-
<input
-
type="text"
-
spellcheck={false}
-
placeholder="user.bsky.social"
-
id="username"
-
name="username"
-
autocomplete="username"
-
aria-label="Your AT Protocol handle"
-
class="grow py-1 select-none placeholder:text-sm focus:outline-none"
-
onInput={(e) => setLoginInput(e.currentTarget.value)}
-
/>
-
<button
-
onclick={() => login(loginInput())}
-
class="flex items-center rounded-md p-1 hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-600 dark:active:bg-neutral-500"
-
>
-
<span class="iconify lucide--log-in"></span>
-
</button>
-
</div>
-
<Show when={notice()}>
-
<div class="text-sm">{notice()}</div>
-
</Show>
-
</form>
-
);
-
};
-
-
const retrieveSession = async () => {
-
const init = async (): Promise<Session | undefined> => {
-
const params = new URLSearchParams(location.hash.slice(1));
-
-
if (params.has("state") && (params.has("code") || params.has("error"))) {
-
history.replaceState(null, "", location.pathname + location.search);
-
-
const auth = await finalizeAuthorization(params);
-
const did = auth.session.info.sub;
-
-
localStorage.setItem("lastSignedIn", did);
-
-
const sessions = localStorage.getItem("sessions");
-
const newSessions: Sessions = sessions ? JSON.parse(sessions) : { [did]: {} };
-
newSessions[did] = { signedIn: true };
-
localStorage.setItem("sessions", JSON.stringify(newSessions));
-
return auth.session;
-
} else {
-
const lastSignedIn = localStorage.getItem("lastSignedIn");
-
-
if (lastSignedIn) {
-
const sessions = localStorage.getItem("sessions");
-
const newSessions: Sessions = sessions ? JSON.parse(sessions) : {};
-
try {
-
const session = await getSession(lastSignedIn as Did);
-
const rpc = new Client({ handler: new OAuthUserAgent(session) });
-
const res = await rpc.get("com.atproto.server.getSession");
-
newSessions[lastSignedIn].signedIn = true;
-
localStorage.setItem("sessions", JSON.stringify(newSessions));
-
if (!res.ok) throw res.data.error;
-
return session;
-
} catch (err) {
-
newSessions[lastSignedIn].signedIn = false;
-
localStorage.setItem("sessions", JSON.stringify(newSessions));
-
throw err;
-
}
-
}
-
}
-
};
-
-
const session = await init();
-
-
if (session) setAgent(new OAuthUserAgent(session));
-
};
-
-
export { Login, retrieveSession };
+2 -2
src/views/blob.tsx
···
-
import { Client, CredentialManager } from "@atcute/client";
+
import { Client, simpleFetchHandler } from "@atcute/client";
import { createResource, createSignal, For, Show } from "solid-js";
import { Button } from "../components/button";
···
let rpc: Client;
const fetchBlobs = async () => {
-
if (!rpc) rpc = new Client({ handler: new CredentialManager({ service: props.pds }) });
+
if (!rpc) rpc = new Client({ handler: simpleFetchHandler({ service: props.pds }) });
const res = await rpc.get("com.atproto.sync.listBlobs", {
params: {
did: props.repo as `did:${string}:${string}`,
+2 -2
src/views/collection.tsx
···
import { ComAtprotoRepoApplyWrites, ComAtprotoRepoGetRecord } from "@atcute/atproto";
-
import { Client, CredentialManager } from "@atcute/client";
+
import { Client, simpleFetchHandler } from "@atcute/client";
import { $type, ActorIdentifier, InferXRPCBodyOutput } from "@atcute/lexicons";
import * as TID from "@atcute/tid";
import { A, useParams } from "@solidjs/router";
···
const fetchRecords = async () => {
if (!pds) pds = await resolvePDS(did!);
-
if (!rpc) rpc = new Client({ handler: new CredentialManager({ service: pds }) });
+
if (!rpc) rpc = new Client({ handler: simpleFetchHandler({ service: pds }) });
const res = await rpc.get("com.atproto.repo.listRecords", {
params: {
repo: did as ActorIdentifier,
+2 -2
src/views/labels.tsx
···
import { ComAtprotoLabelDefs } from "@atcute/atproto";
-
import { Client, CredentialManager } from "@atcute/client";
+
import { Client, simpleFetchHandler } from "@atcute/client";
import { isAtprotoDid } from "@atcute/identity";
import { Handle } from "@atcute/lexicons";
import { A, useSearchParams } from "@solidjs/router";
···
await resolvePDS(did);
if (!labelerCache[did]) throw new Error("Repository is not a labeler");
rpc = new Client({
-
handler: new CredentialManager({ service: labelerCache[did] }),
+
handler: simpleFetchHandler({ service: labelerCache[did] }),
});
setSearchParams({ did, uriPatterns });
public/favicon.ico

This is a binary file and will not be displayed.

public/fonts/Figtree[wght].woff2

This is a binary file and will not be displayed.

+7 -1
src/styles/index.css
···
@custom-variant dark (&:where(.dark, .dark *));
+
@font-face {
+
font-family: "Figtree";
+
src: url("/fonts/Figtree[wght].woff2") format("woff2");
+
font-display: swap;
+
}
+
@theme {
-
--font-sans: "Inter", sans-serif;
+
--font-sans: "Figtree", sans-serif;
--font-mono: "Roboto Mono", monospace;
--font-pecita: "Pecita", serif;
+5 -1
src/auth/account.tsx
···
return (
<MenuProvider>
<DropdownMenu icon="lucide--ellipsis" buttonClass="rounded-md p-2">
-
<NavMenu href={`/at://${props.did}`} label="Go to repo" icon="lucide--user-round" />
+
<NavMenu
+
href={`/at://${props.did}`}
+
label={agent()?.sub === props.did ? "Go to repo (g)" : "Go to repo"}
+
icon="lucide--user-round"
+
/>
<ActionMenu
icon="lucide--settings"
label="Edit permissions"
+1
src/components/create.tsx
···
onMount(() => {
const keyEvent = (ev: KeyboardEvent) => {
if (ev.target instanceof HTMLInputElement || ev.target instanceof HTMLTextAreaElement) return;
+
if ((ev.target as HTMLElement).closest("[data-modal]")) return;
const key = props.create ? "n" : "e";
if (ev.key === key) {
+109
src/components/create/file-upload.tsx
···
+
import { Client } from "@atcute/client";
+
import { remove } from "@mary/exif-rm";
+
import { createSignal, onCleanup, Show } from "solid-js";
+
import { agent } from "../../auth/state";
+
import { Button } from "../button.jsx";
+
import { TextInput } from "../text-input.jsx";
+
import { editorInstance } from "./state";
+
+
export const FileUpload = (props: {
+
file: File;
+
blobInput: HTMLInputElement;
+
onClose: () => void;
+
}) => {
+
const [uploading, setUploading] = createSignal(false);
+
const [error, setError] = createSignal("");
+
+
onCleanup(() => (props.blobInput.value = ""));
+
+
const formatFileSize = (bytes: number) => {
+
if (bytes === 0) return "0 Bytes";
+
const k = 1024;
+
const sizes = ["Bytes", "KB", "MB", "GB"];
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
+
};
+
+
const uploadBlob = async () => {
+
let blob: Blob;
+
+
const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value;
+
(document.getElementById("mimetype") as HTMLInputElement).value = "";
+
if (mimetype) blob = new Blob([props.file], { type: mimetype });
+
else blob = props.file;
+
+
if ((document.getElementById("exif-rm") as HTMLInputElement).checked) {
+
const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer()));
+
if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type });
+
}
+
+
const rpc = new Client({ handler: agent()! });
+
setUploading(true);
+
const res = await rpc.post("com.atproto.repo.uploadBlob", {
+
input: blob,
+
});
+
setUploading(false);
+
if (!res.ok) {
+
setError(res.data.error);
+
return;
+
}
+
editorInstance.view.dispatch({
+
changes: {
+
from: editorInstance.view.state.selection.main.head,
+
insert: JSON.stringify(res.data.blob, null, 2),
+
},
+
});
+
props.onClose();
+
};
+
+
return (
+
<div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-[20rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0">
+
<h2 class="mb-2 font-semibold">Upload blob</h2>
+
<div class="flex flex-col gap-2 text-sm">
+
<div class="flex flex-col gap-1">
+
<p class="flex gap-1">
+
<span class="truncate">{props.file.name}</span>
+
<span class="shrink-0 text-neutral-600 dark:text-neutral-400">
+
({formatFileSize(props.file.size)})
+
</span>
+
</p>
+
</div>
+
<div class="flex items-center gap-x-2">
+
<label for="mimetype" class="shrink-0 select-none">
+
MIME type
+
</label>
+
<TextInput id="mimetype" placeholder={props.file.type} />
+
</div>
+
<div class="flex items-center gap-1">
+
<input id="exif-rm" type="checkbox" checked />
+
<label for="exif-rm" class="select-none">
+
Remove EXIF data
+
</label>
+
</div>
+
<p class="text-xs text-neutral-600 dark:text-neutral-400">
+
Metadata will be pasted after the cursor
+
</p>
+
<Show when={error()}>
+
<span class="text-red-500 dark:text-red-400">Error: {error()}</span>
+
</Show>
+
<div class="flex justify-between gap-2">
+
<Button onClick={props.onClose}>Cancel</Button>
+
<Show when={uploading()}>
+
<div class="flex items-center gap-1">
+
<span class="iconify lucide--loader-circle animate-spin"></span>
+
<span>Uploading</span>
+
</div>
+
</Show>
+
<Show when={!uploading()}>
+
<Button
+
onClick={uploadBlob}
+
class="dark:shadow-dark-700 flex items-center gap-1 rounded-lg bg-blue-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 dark:active:bg-blue-400"
+
>
+
Upload
+
</Button>
+
</Show>
+
</div>
+
</div>
+
</div>
+
);
+
};
+87
src/components/create/handle-input.tsx
···
+
import { Handle } from "@atcute/lexicons";
+
import { createSignal, Show } from "solid-js";
+
import { resolveHandle } from "../../utils/api";
+
import { Button } from "../button.jsx";
+
import { TextInput } from "../text-input.jsx";
+
import { editorInstance } from "./state";
+
+
export const HandleInput = (props: { onClose: () => void }) => {
+
const [resolving, setResolving] = createSignal(false);
+
const [error, setError] = createSignal("");
+
let handleFormRef!: HTMLFormElement;
+
+
const resolveDid = async (e: SubmitEvent) => {
+
e.preventDefault();
+
const formData = new FormData(handleFormRef);
+
const handleValue = formData.get("handle")?.toString().trim();
+
+
if (!handleValue) {
+
setError("Please enter a handle");
+
return;
+
}
+
+
setResolving(true);
+
setError("");
+
try {
+
const did = await resolveHandle(handleValue as Handle);
+
editorInstance.view.dispatch({
+
changes: {
+
from: editorInstance.view.state.selection.main.head,
+
insert: `"${did}"`,
+
},
+
});
+
props.onClose();
+
handleFormRef.reset();
+
} catch (err: any) {
+
setError(err.message || "Failed to resolve handle");
+
} finally {
+
setResolving(false);
+
}
+
};
+
+
return (
+
<div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-[20rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0">
+
<h2 class="mb-2 font-semibold">Insert DID from handle</h2>
+
<form ref={handleFormRef} onSubmit={resolveDid} class="flex flex-col gap-2 text-sm">
+
<div class="flex flex-col gap-1">
+
<label for="handle-input" class="select-none">
+
Handle
+
</label>
+
<TextInput id="handle-input" name="handle" placeholder="user.bsky.social" />
+
</div>
+
<p class="text-xs text-neutral-600 dark:text-neutral-400">
+
DID will be pasted after the cursor
+
</p>
+
<Show when={error()}>
+
<span class="text-red-500 dark:text-red-400">Error: {error()}</span>
+
</Show>
+
<div class="flex justify-between gap-2">
+
<Button
+
type="button"
+
onClick={() => {
+
props.onClose();
+
handleFormRef.reset();
+
setError("");
+
}}
+
>
+
Cancel
+
</Button>
+
<Show when={resolving()}>
+
<div class="flex items-center gap-1">
+
<span class="iconify lucide--loader-circle animate-spin"></span>
+
<span>Resolving</span>
+
</div>
+
</Show>
+
<Show when={!resolving()}>
+
<Button
+
type="submit"
+
class="dark:shadow-dark-700 flex items-center gap-1 rounded-lg bg-blue-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 dark:active:bg-blue-400"
+
>
+
Insert
+
</Button>
+
</Show>
+
</div>
+
</form>
+
</div>
+
);
+
};
+12
src/components/create/menu-item.tsx
···
+
export const MenuItem = (props: { icon: string; label: string; onClick: () => void }) => {
+
return (
+
<button
+
type="button"
+
class="flex items-center gap-2 rounded-md p-2 text-left text-xs hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
+
onClick={props.onClick}
+
>
+
<span class={`iconify ${props.icon}`}></span>
+
<span>{props.label}</span>
+
</button>
+
);
+
};
+4
src/components/create/state.ts
···
+
import { createSignal } from "solid-js";
+
+
export const editorInstance = { view: null as any };
+
export const [placeholder, setPlaceholder] = createSignal<any>();
+1 -1
src/components/editor.tsx
···
import { basicLight } from "@fsegurai/codemirror-theme-basic-light";
import { basicSetup, EditorView } from "codemirror";
import { onCleanup, onMount } from "solid-js";
-
import { editorInstance } from "./create";
+
import { editorInstance } from "./create/state";
const Editor = (props: { content: string }) => {
let editorDiv!: HTMLDivElement;
+5 -5
package.json
···
"serve": "vite preview"
},
"devDependencies": {
-
"@iconify-json/lucide": "^1.2.79",
+
"@iconify-json/lucide": "^1.2.81",
"@iconify/tailwind4": "^1.2.0",
-
"@tailwindcss/vite": "^4.1.17",
+
"@tailwindcss/vite": "^4.1.18",
"prettier": "^3.7.4",
"prettier-plugin-organize-imports": "^4.3.0",
"prettier-plugin-tailwindcss": "^0.7.2",
-
"tailwindcss": "^4.1.17",
+
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vite": "^7.2.7",
"vite-plugin-solid": "^2.11.10"
···
"@atcute/did-plc": "^0.2.0",
"@atcute/identity": "^1.1.3",
"@atcute/identity-resolver": "^1.2.0",
-
"@atcute/leaflet": "^1.0.13",
+
"@atcute/leaflet": "^1.0.14",
"@atcute/lexicon-doc": "^2.0.5",
"@atcute/lexicon-resolver": "^0.1.5",
"@atcute/lexicons": "^1.2.5",
···
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lint": "^6.9.2",
"@codemirror/state": "^6.5.2",
-
"@codemirror/view": "^6.39.2",
+
"@codemirror/view": "^6.39.4",
"@fsegurai/codemirror-theme-basic-dark": "^6.2.3",
"@fsegurai/codemirror-theme-basic-light": "^6.2.3",
"@mary/exif-rm": "jsr:^0.2.2",
+108 -108
pnpm-lock.yaml
···
specifier: ^1.2.0
version: 1.2.0(@atcute/identity@1.1.3)
'@atcute/leaflet':
-
specifier: ^1.0.13
-
version: 1.0.13
+
specifier: ^1.0.14
+
version: 1.0.14
'@atcute/lexicon-doc':
specifier: ^2.0.5
version: 2.0.5
···
specifier: ^6.5.2
version: 6.5.2
'@codemirror/view':
-
specifier: ^6.39.2
-
version: 6.39.2
+
specifier: ^6.39.4
+
version: 6.39.4
'@fsegurai/codemirror-theme-basic-dark':
specifier: ^6.2.3
-
version: 6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.39.2)(@lezer/highlight@1.2.3)
+
version: 6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.39.4)(@lezer/highlight@1.2.3)
'@fsegurai/codemirror-theme-basic-light':
specifier: ^6.2.3
-
version: 6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.39.2)(@lezer/highlight@1.2.3)
+
version: 6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.39.4)(@lezer/highlight@1.2.3)
'@mary/exif-rm':
specifier: jsr:^0.2.2
version: '@jsr/mary__exif-rm@0.2.2'
···
version: 1.9.10
devDependencies:
'@iconify-json/lucide':
-
specifier: ^1.2.79
-
version: 1.2.79
+
specifier: ^1.2.81
+
version: 1.2.81
'@iconify/tailwind4':
specifier: ^1.2.0
-
version: 1.2.0(tailwindcss@4.1.17)
+
version: 1.2.0(tailwindcss@4.1.18)
'@tailwindcss/vite':
-
specifier: ^4.1.17
-
version: 4.1.17(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))
+
specifier: ^4.1.18
+
version: 4.1.18(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))
prettier:
specifier: ^3.7.4
version: 3.7.4
···
specifier: ^0.7.2
version: 0.7.2(prettier-plugin-organize-imports@4.3.0(prettier@3.7.4)(typescript@5.9.3))(prettier@3.7.4)
tailwindcss:
-
specifier: ^4.1.17
-
version: 4.1.17
+
specifier: ^4.1.18
+
version: 4.1.18
typescript:
specifier: ^5.9.3
version: 5.9.3
···
'@atcute/identity@1.1.3':
resolution: {integrity: sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng==}
-
'@atcute/leaflet@1.0.13':
-
resolution: {integrity: sha512-0/rjFqjXhOHFHsVYxGJVj3OAt40LvlV1VwjVx+T//JDcP9UUkzerhaHkefw2aWfpYnwxDMujuAf8P69xKtIaVg==}
+
'@atcute/leaflet@1.0.14':
+
resolution: {integrity: sha512-TWbtB7b73GChBaYwfd7aWFyGVObZ/DqrRtwkpWGm1GO8zZmQ9eJyKDUnXim7NOAs2hmKQ1u2wk2AM4AYzkF5Gg==}
'@atcute/lexicon-doc@2.0.5':
resolution: {integrity: sha512-fNCp94ehGjWFZMIqP6pWD1F9MOJogNCyqsaMVZluPSIclZ+lDL528iXB56aW4u0eSiD6Y9WJB1OI/lElG39cSA==}
···
'@codemirror/state@6.5.2':
resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==}
-
'@codemirror/view@6.39.2':
-
resolution: {integrity: sha512-YCbOfs4cq49ulN/MVhrUV22rKDJv/fHUs4cR98McAI59/coVwUa2N3RAoNVDgeJNchrQzBxTT3vzto4ZbTYVtw==}
+
'@codemirror/view@6.39.4':
+
resolution: {integrity: sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==}
'@cyberalien/svg-utils@1.0.11':
resolution: {integrity: sha512-qEE9mnyI+avfGT3emKuRs3ucYkITeaV0Xi7VlYN41f+uGnZBecQP3jwz/AF437H9J4Q7qPClHKm4NiTYpNE6hA==}
···
'@codemirror/view': ^6.0.0
'@lezer/highlight': ^1.0.0
-
'@iconify-json/lucide@1.2.79':
-
resolution: {integrity: sha512-CcwoXfC2Y7UVW0PXopmXtB4Do/eUJkhAqQqOnVENEiw3FwU707TK4uyIUqdo9tlvBaFBl95wnJf3smqsTnSyKA==}
+
'@iconify-json/lucide@1.2.81':
+
resolution: {integrity: sha512-6Kz/+SEuD5bkg0KImi0yFem9l6njKp4e1qF1LpQbgRfk7ngsJR/qjlB4y5rM8N1iKiDR/p19cqhmwZxyCWek+w==}
'@iconify/tailwind4@1.2.0':
resolution: {integrity: sha512-+t7XqfojOB0zzZdd8gV7IQZGq1AaIHTlsxMVzagxYR0hAlJCLUD63o3iSlNKRMH3ZR7gZ8y5c9dJ7J431avRbA==}
···
'@standard-schema/spec@1.0.0':
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
-
'@tailwindcss/node@4.1.17':
-
resolution: {integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==}
+
'@tailwindcss/node@4.1.18':
+
resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==}
-
'@tailwindcss/oxide-android-arm64@4.1.17':
-
resolution: {integrity: sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==}
+
'@tailwindcss/oxide-android-arm64@4.1.18':
+
resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
-
'@tailwindcss/oxide-darwin-arm64@4.1.17':
-
resolution: {integrity: sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==}
+
'@tailwindcss/oxide-darwin-arm64@4.1.18':
+
resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
-
'@tailwindcss/oxide-darwin-x64@4.1.17':
-
resolution: {integrity: sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==}
+
'@tailwindcss/oxide-darwin-x64@4.1.18':
+
resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
-
'@tailwindcss/oxide-freebsd-x64@4.1.17':
-
resolution: {integrity: sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==}
+
'@tailwindcss/oxide-freebsd-x64@4.1.18':
+
resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [freebsd]
-
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17':
-
resolution: {integrity: sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==}
+
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
+
resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
-
'@tailwindcss/oxide-linux-arm64-gnu@4.1.17':
-
resolution: {integrity: sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==}
+
'@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
+
resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
-
'@tailwindcss/oxide-linux-arm64-musl@4.1.17':
-
resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==}
+
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
+
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
-
'@tailwindcss/oxide-linux-x64-gnu@4.1.17':
-
resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==}
+
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
+
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
-
'@tailwindcss/oxide-linux-x64-musl@4.1.17':
-
resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==}
+
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
+
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
-
'@tailwindcss/oxide-wasm32-wasi@4.1.17':
-
resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==}
+
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
+
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
bundledDependencies:
···
- '@emnapi/wasi-threads'
- tslib
-
'@tailwindcss/oxide-win32-arm64-msvc@4.1.17':
-
resolution: {integrity: sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==}
+
'@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
+
resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
-
'@tailwindcss/oxide-win32-x64-msvc@4.1.17':
-
resolution: {integrity: sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==}
+
'@tailwindcss/oxide-win32-x64-msvc@4.1.18':
+
resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
-
'@tailwindcss/oxide@4.1.17':
-
resolution: {integrity: sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==}
+
'@tailwindcss/oxide@4.1.18':
+
resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==}
engines: {node: '>= 10'}
-
'@tailwindcss/vite@4.1.17':
-
resolution: {integrity: sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==}
+
'@tailwindcss/vite@4.1.18':
+
resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==}
peerDependencies:
vite: ^5.2.0 || ^6 || ^7
···
solid-js:
optional: true
-
baseline-browser-mapping@2.9.6:
-
resolution: {integrity: sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==}
+
baseline-browser-mapping@2.9.7:
+
resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==}
hasBin: true
boolbase@1.0.0:
···
electron-to-chromium@1.5.267:
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
-
enhanced-resolve@5.18.3:
-
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
+
enhanced-resolve@5.18.4:
+
resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
engines: {node: '>=10.13.0'}
entities@4.5.0:
···
engines: {node: '>=16'}
hasBin: true
-
tailwindcss@4.1.17:
-
resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==}
+
tailwindcss@4.1.18:
+
resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
tapable@2.3.0:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
···
'@atcute/lexicons': 1.2.5
'@badrap/valita': 0.4.6
-
'@atcute/leaflet@1.0.13':
+
'@atcute/leaflet@1.0.14':
dependencies:
'@atcute/atproto': 3.1.9
'@atcute/lexicons': 1.2.5
···
dependencies:
'@codemirror/language': 6.11.3
'@codemirror/state': 6.5.2
-
'@codemirror/view': 6.39.2
+
'@codemirror/view': 6.39.4
'@lezer/common': 1.4.0
'@codemirror/commands@6.10.0':
dependencies:
'@codemirror/language': 6.11.3
'@codemirror/state': 6.5.2
-
'@codemirror/view': 6.39.2
+
'@codemirror/view': 6.39.4
'@lezer/common': 1.4.0
'@codemirror/lang-json@6.0.2':
···
'@codemirror/language@6.11.3':
dependencies:
'@codemirror/state': 6.5.2
-
'@codemirror/view': 6.39.2
+
'@codemirror/view': 6.39.4
'@lezer/common': 1.4.0
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.5
···
'@codemirror/lint@6.9.2':
dependencies:
'@codemirror/state': 6.5.2
-
'@codemirror/view': 6.39.2
+
'@codemirror/view': 6.39.4
crelt: 1.0.6
'@codemirror/search@6.5.11':
dependencies:
'@codemirror/state': 6.5.2
-
'@codemirror/view': 6.39.2
+
'@codemirror/view': 6.39.4
crelt: 1.0.6
'@codemirror/state@6.5.2':
dependencies:
'@marijn/find-cluster-break': 1.0.2
-
'@codemirror/view@6.39.2':
+
'@codemirror/view@6.39.4':
dependencies:
'@codemirror/state': 6.5.2
crelt: 1.0.6
···
'@esbuild/win32-x64@0.25.12':
optional: true
-
'@fsegurai/codemirror-theme-basic-dark@6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.39.2)(@lezer/highlight@1.2.3)':
+
'@fsegurai/codemirror-theme-basic-dark@6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.39.4)(@lezer/highlight@1.2.3)':
dependencies:
'@codemirror/language': 6.11.3
'@codemirror/state': 6.5.2
-
'@codemirror/view': 6.39.2
+
'@codemirror/view': 6.39.4
'@lezer/highlight': 1.2.3
-
'@fsegurai/codemirror-theme-basic-light@6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.39.2)(@lezer/highlight@1.2.3)':
+
'@fsegurai/codemirror-theme-basic-light@6.2.3(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.39.4)(@lezer/highlight@1.2.3)':
dependencies:
'@codemirror/language': 6.11.3
'@codemirror/state': 6.5.2
-
'@codemirror/view': 6.39.2
+
'@codemirror/view': 6.39.4
'@lezer/highlight': 1.2.3
-
'@iconify-json/lucide@1.2.79':
+
'@iconify-json/lucide@1.2.81':
dependencies:
'@iconify/types': 2.0.0
-
'@iconify/tailwind4@1.2.0(tailwindcss@4.1.17)':
+
'@iconify/tailwind4@1.2.0(tailwindcss@4.1.18)':
dependencies:
'@iconify/tools': 5.0.0
'@iconify/types': 2.0.0
'@iconify/utils': 3.1.0
-
tailwindcss: 4.1.17
+
tailwindcss: 4.1.18
'@iconify/tools@5.0.0':
dependencies:
···
'@standard-schema/spec@1.0.0': {}
-
'@tailwindcss/node@4.1.17':
+
'@tailwindcss/node@4.1.18':
dependencies:
'@jridgewell/remapping': 2.3.5
-
enhanced-resolve: 5.18.3
+
enhanced-resolve: 5.18.4
jiti: 2.6.1
lightningcss: 1.30.2
magic-string: 0.30.21
source-map-js: 1.2.1
-
tailwindcss: 4.1.17
+
tailwindcss: 4.1.18
-
'@tailwindcss/oxide-android-arm64@4.1.17':
+
'@tailwindcss/oxide-android-arm64@4.1.18':
optional: true
-
'@tailwindcss/oxide-darwin-arm64@4.1.17':
+
'@tailwindcss/oxide-darwin-arm64@4.1.18':
optional: true
-
'@tailwindcss/oxide-darwin-x64@4.1.17':
+
'@tailwindcss/oxide-darwin-x64@4.1.18':
optional: true
-
'@tailwindcss/oxide-freebsd-x64@4.1.17':
+
'@tailwindcss/oxide-freebsd-x64@4.1.18':
optional: true
-
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17':
+
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
optional: true
-
'@tailwindcss/oxide-linux-arm64-gnu@4.1.17':
+
'@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
optional: true
-
'@tailwindcss/oxide-linux-arm64-musl@4.1.17':
+
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
optional: true
-
'@tailwindcss/oxide-linux-x64-gnu@4.1.17':
+
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
optional: true
-
'@tailwindcss/oxide-linux-x64-musl@4.1.17':
+
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
optional: true
-
'@tailwindcss/oxide-wasm32-wasi@4.1.17':
+
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
optional: true
-
'@tailwindcss/oxide-win32-arm64-msvc@4.1.17':
+
'@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
optional: true
-
'@tailwindcss/oxide-win32-x64-msvc@4.1.17':
+
'@tailwindcss/oxide-win32-x64-msvc@4.1.18':
optional: true
-
'@tailwindcss/oxide@4.1.17':
+
'@tailwindcss/oxide@4.1.18':
optionalDependencies:
-
'@tailwindcss/oxide-android-arm64': 4.1.17
-
'@tailwindcss/oxide-darwin-arm64': 4.1.17
-
'@tailwindcss/oxide-darwin-x64': 4.1.17
-
'@tailwindcss/oxide-freebsd-x64': 4.1.17
-
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.17
-
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.17
-
'@tailwindcss/oxide-linux-arm64-musl': 4.1.17
-
'@tailwindcss/oxide-linux-x64-gnu': 4.1.17
-
'@tailwindcss/oxide-linux-x64-musl': 4.1.17
-
'@tailwindcss/oxide-wasm32-wasi': 4.1.17
-
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.17
-
'@tailwindcss/oxide-win32-x64-msvc': 4.1.17
-
-
'@tailwindcss/vite@4.1.17(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))':
-
dependencies:
-
'@tailwindcss/node': 4.1.17
-
'@tailwindcss/oxide': 4.1.17
-
tailwindcss: 4.1.17
+
'@tailwindcss/oxide-android-arm64': 4.1.18
+
'@tailwindcss/oxide-darwin-arm64': 4.1.18
+
'@tailwindcss/oxide-darwin-x64': 4.1.18
+
'@tailwindcss/oxide-freebsd-x64': 4.1.18
+
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18
+
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.18
+
'@tailwindcss/oxide-linux-arm64-musl': 4.1.18
+
'@tailwindcss/oxide-linux-x64-gnu': 4.1.18
+
'@tailwindcss/oxide-linux-x64-musl': 4.1.18
+
'@tailwindcss/oxide-wasm32-wasi': 4.1.18
+
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.18
+
'@tailwindcss/oxide-win32-x64-msvc': 4.1.18
+
+
'@tailwindcss/vite@4.1.18(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2))':
+
dependencies:
+
'@tailwindcss/node': 4.1.18
+
'@tailwindcss/oxide': 4.1.18
+
tailwindcss: 4.1.18
vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.19.2)
'@types/babel__core@7.20.5':
···
optionalDependencies:
solid-js: 1.9.10
-
baseline-browser-mapping@2.9.6: {}
+
baseline-browser-mapping@2.9.7: {}
boolbase@1.0.0: {}
browserslist@4.28.1:
dependencies:
-
baseline-browser-mapping: 2.9.6
+
baseline-browser-mapping: 2.9.7
caniuse-lite: 1.0.30001760
electron-to-chromium: 1.5.267
node-releases: 2.0.27
···
'@codemirror/lint': 6.9.2
'@codemirror/search': 6.5.11
'@codemirror/state': 6.5.2
-
'@codemirror/view': 6.39.2
+
'@codemirror/view': 6.39.4
commander@11.1.0: {}
···
electron-to-chromium@1.5.267: {}
-
enhanced-resolve@5.18.3:
+
enhanced-resolve@5.18.4:
dependencies:
graceful-fs: 4.2.11
tapable: 2.3.0
···
picocolors: 1.1.1
sax: 1.4.3
-
tailwindcss@4.1.17: {}
+
tailwindcss@4.1.18: {}
tapable@2.3.0: {}