extremely wip tangled spa

skibivi

aylac.top 7246fe13 c7941b02

verified
+17 -3
LICENSE
···
+
MIT License
+
Copyright (c) 2025 aylac.top
-
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
Permission is hereby granted, free of charge, to any person obtaining a copy
+
of this software and associated documentation files (the "Software"), to deal
+
in the Software without restriction, including without limitation the rights
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+
copies of the Software, and to permit persons to whom the Software is
+
furnished to do so, subject to the following conditions:
-
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
The above copyright notice and this permission notice shall be included in all
+
copies or substantial portions of the Software.
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+
SOFTWARE.
+2 -1
biome.json
···
"style": { "noNonNullAssertion": "off" }
},
"domains": {
-
"solid": "recommended"
+
"solid": "all",
+
"react": "none"
}
},
"javascript": {
+41 -40
package.json
···
{
-
"name": "vite-template-solid",
-
"version": "0.0.0",
-
"description": "",
-
"type": "module",
-
"scripts": {
-
"start": "vite",
-
"dev": "vite",
-
"build": "vite build",
-
"serve": "vite preview"
-
},
-
"license": "MIT",
-
"devDependencies": {
-
"@biomejs/biome": "^2.3.4",
-
"@iconify-json/gravity-ui": "^1.2.10",
-
"@iconify/tailwind4": "^1.1.0",
-
"postcss": "^8.5.6",
-
"solid-devtools": "^0.34.4",
-
"tailwindcss": "^4.1.17",
-
"vite": "^7.2.2",
-
"vite-plugin-solid": "^2.11.10"
-
},
-
"dependencies": {
-
"@atcute/atproto": "^3.1.9",
-
"@atcute/client": "^4.0.5",
-
"@atcute/identity-resolver": "^1.1.4",
-
"@atcute/oauth-browser-client": "^2.0.1",
-
"@atcute/tangled": "^1.0.10",
-
"@gleam-lang/highlight.js-gleam": "^1.5.0",
-
"@solidjs/router": "^0.15.3",
-
"@tailwindcss/vite": "^4.1.17",
-
"@types/highlight.js": "^10.1.0",
-
"highlight.js": "^11.11.1",
-
"highlightjs-line-numbers.js": "^2.9.1",
-
"solid-js": "^1.9.10",
-
"solid-markdown": "^2.0.14"
-
},
-
"patchedDependencies": {
-
"@gleam-lang/highlight.js-gleam@1.5.0": "patches/@gleam-lang%2Fhighlight.js-gleam@1.5.0.patch",
-
"highlight.js@11.11.1": "patches/highlight.js@11.11.1.patch"
-
}
+
"name": "vite-template-solid",
+
"version": "0.0.0",
+
"description": "",
+
"type": "module",
+
"scripts": {
+
"start": "vite",
+
"dev": "vite",
+
"build": "vite build",
+
"serve": "vite preview",
+
"lint": "biome lint"
+
},
+
"license": "MIT",
+
"devDependencies": {
+
"@biomejs/biome": "^2.3.4",
+
"@iconify-json/gravity-ui": "^1.2.10",
+
"@iconify/tailwind4": "^1.1.0",
+
"postcss": "^8.5.6",
+
"solid-devtools": "^0.34.4",
+
"tailwindcss": "^4.1.17",
+
"vite": "^7.2.2",
+
"vite-plugin-solid": "^2.11.10"
+
},
+
"dependencies": {
+
"@atcute/atproto": "^3.1.9",
+
"@atcute/client": "^4.0.5",
+
"@atcute/identity-resolver": "^1.1.4",
+
"@atcute/oauth-browser-client": "^2.0.1",
+
"@atcute/tangled": "^1.0.10",
+
"@gleam-lang/highlight.js-gleam": "^1.5.0",
+
"@solidjs/router": "^0.15.3",
+
"@tailwindcss/vite": "^4.1.17",
+
"@types/highlight.js": "^10.1.0",
+
"highlight.js": "^11.11.1",
+
"highlightjs-line-numbers.js": "^2.9.1",
+
"solid-js": "^1.9.10",
+
"solid-markdown": "^2.0.14"
+
},
+
"patchedDependencies": {
+
"@gleam-lang/highlight.js-gleam@1.5.0": "patches/@gleam-lang%2Fhighlight.js-gleam@1.5.0.patch",
+
"highlight.js@11.11.1": "patches/highlight.js@11.11.1.patch"
+
}
}
-4
shell.nix
···
#in
pkgs.mkShellNoCC {
#inputsFrom = [ defaultPackage ];
-
#
-
env = {
-
RUST_SRC_PATH = pkgs.rustPlatform.rustLibSrc;
-
};
packages = with pkgs; [
bun
-31
src/elements/code_block.tsx
···
-
import hljs from "highlight.js";
-
import { createResource, For } from "solid-js";
-
import "../styles/fileviewer.css";
-
import "../util/highlight.js/index";
-
-
export function CodeBlock(props: { code: string; language: string }) {
-
const [codeBlock] = createResource(
-
() => props,
-
(props) => {
-
const highlit = hljs.getLanguage(props.language)
-
? hljs.highlight(props.code, { language: props.language }).value
-
: props.code;
-
return (
-
<For each={highlit.split("\n")}>
-
{(line, i) => (
-
<span class="line-wrapper">
-
<span class="line-number">{i() + 1}</span>
-
<span class="line-content" innerHTML={line}></span>
-
</span>
-
)}
-
</For>
-
);
-
},
-
);
-
-
return (
-
<pre>
-
<code class="flex flex-col text-wrap p-4">{codeBlock()}</code>
-
</pre>
-
);
-
}
+32
src/elements/code_block/index.tsx
···
+
import hljs from "highlight.js";
+
import { For } from "solid-js";
+
import "./style.css";
+
import "../../util/highlight.js/index";
+
+
export function CodeBlock(props: { code: string; language: string }) {
+
const highlit = (
+
hljs.getLanguage(props.language)
+
? hljs.highlight(props.code, { language: props.language }).value
+
: props.code
+
).split("\n");
+
const numberSize = highlit.length.toString().length;
+
return (
+
<div class="overflow-x-auto">
+
<div class="flex w-min flex-col whitespace-pre text-nowrap font-mono text-gray-500 dark:text-gray-300">
+
<For each={highlit}>
+
{(line, i) => (
+
<div id={`L${i() + 1}`} class="flex flex-row gap-2">
+
<a
+
href={`#L${i() + 1}`}
+
class="sticky left-0 select-none border-gray-200 border-r bg-white px-1.5 text-gray-400 hover:text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-500 hover:dark:text-gray-200"
+
>
+
{(i() + 1).toString().padStart(numberSize, " ")}
+
</a>
+
<span innerHTML={line} />
+
</div>
+
)}
+
</For>
+
</div>
+
</div>
+
);
+
}
-31
src/elements/icon_with_text.tsx
···
-
import { type ComponentProps, splitProps } from "solid-js";
-
import { Dynamic } from "solid-js/web";
-
-
type AllowedTags = "div" | "a" | "span" | "button";
-
-
export function IconWithText<T extends AllowedTags>(
-
props: {
-
type?: T;
-
icon: string;
-
text: string;
-
style?: string;
-
href?: string;
-
} & ComponentProps<T>,
-
) {
-
const [local, dynamic] = splitProps(
-
props,
-
["type", "icon", "text", "style"],
-
["href"],
-
);
-
-
return (
-
<Dynamic
-
component={local.type || "span"}
-
class={`flex flex-row items-center gap-1 ${local.style || ""}`}
-
{...dynamic}
-
>
-
<div class={`iconify ${local.icon}`} />
-
<span>{local.text}</span>
-
</Dynamic>
-
);
-
}
+6 -6
src/errors/404.tsx
···
export default function NotFound() {
-
return (
-
<section class="text-gray-700 p-8">
-
<h1 class="text-2xl font-bold">404: Not Found</h1>
-
<p class="mt-4">It's gone 😞</p>
-
</section>
-
);
+
return (
+
<section class="p-8 text-gray-700">
+
<h1 class="font-bold text-2xl">404: Not Found</h1>
+
<p class="mt-4">It's gone 😞</p>
+
</section>
+
);
}
+7 -3
src/index.tsx
···
import App from "./app";
import NotFound from "./errors/404";
import RepoBlob, { preloadRepoBlob } from "./routes/repo/blob";
-
import RepoCommit from "./routes/repo/commit/commit";
+
import RepoCommit, { preloadRepoCommit } from "./routes/repo/commit";
import { RepoProvider } from "./routes/repo/context";
import RepoTree, { preloadRepoTree } from "./routes/repo/tree";
import User from "./routes/user";
···
component={RepoBlob}
preload={preloadRepoBlob}
/>
-
<Route path="/commit/:ref" component={RepoCommit} />
-
<Route component={RepoTree} />
+
<Route
+
path="/commit/:ref"
+
component={RepoCommit}
+
preload={preloadRepoCommit}
+
/>
+
<Route component={RepoTree} preload={preloadRepoTree} />
</Route>
<Route path="/:user" component={User} />
<Route path="*" component={NotFound} />
+13 -10
src/routes/repo/blob.tsx
···
useNavigate,
useParams,
} from "@solidjs/router";
-
import { Show } from "solid-js";
+
import { createMemo } from "solid-js";
import { CodeBlock } from "../../elements/code_block";
import { getLanguage } from "../../util/get_language";
import { figureOutDid } from "../../util/handle";
···
})(),
);
+
const codeBlock = createMemo(() => {
+
if (!blob()) return;
+
return (
+
<CodeBlock
+
code={blob()!.content}
+
language={getLanguage(blob()!.path.split("/").pop()) || "text"}
+
/>
+
);
+
});
+
return (
<div class="mx-auto max-w-5xl">
<Header user={params.user} repo={params.repo} />
-
<div class="flex flex-col rounded bg-white dark:bg-gray-800">
-
<Show when={blob()} keyed>
-
{(data) => (
-
<CodeBlock
-
code={data.content}
-
language={getLanguage(data.path.split("/").pop()) || "text"}
-
/>
-
)}
-
</Show>
+
<div class="flex flex-col rounded bg-white p-4 dark:bg-gray-800">
+
{codeBlock()}
</div>
</div>
);
src/routes/repo/commit/commit.data.ts src/routes/repo/commit/data.ts
-376
src/routes/repo/commit/commit.tsx
···
-
import { useParams } from "@solidjs/router";
-
import {
-
createMemo,
-
createResource,
-
createSignal,
-
For,
-
Match,
-
onMount,
-
Show,
-
Suspense,
-
Switch,
-
} from "solid-js";
-
import type { Commit, DID, DiffTextFragment } from "../../../util/types";
-
import { useDid } from "../context";
-
import { Header } from "../main";
-
import { getRepoCommit } from "../main.data";
-
import { buildTree, type TreeNode } from "./commit.data";
-
-
function RenderTree(props: { tree: TreeNode; skip?: boolean }) {
-
if (props.skip)
-
return (
-
<For each={props.tree.children}>
-
{(node) => <RenderTree tree={node} />}
-
</For>
-
);
-
const [displayChildren, setDisplayChildren] = createSignal(true);
-
return (
-
<Switch>
-
<Match when={props.tree.type === "file"}>
-
<a
-
class="flex cursor-default select-none flex-row items-center gap-1 rounded p-1 text-xs hover:bg-gray-100 hover:dark:bg-gray-700"
-
href={`#file-${props.tree.fullPath}`}
-
>
-
<div class="iconify gravity-ui--file" />
-
<span class="select-text">{props.tree.name}</span>
-
</a>
-
</Match>
-
<Match when={props.tree.type === "directory"}>
-
<div
-
class="flex select-none flex-row items-center gap-1 rounded p-1 text-xs hover:bg-gray-100 hover:dark:bg-gray-700"
-
onclick={() => setDisplayChildren(!displayChildren())}
-
>
-
<div class="iconify gravity-ui--folder-fill" />
-
<span class="select-text">{props.tree.name}</span>
-
</div>
-
<div
-
class={`ml-1 flex flex-col border-gray-200 border-l pl-1 dark:border-gray-700 ${displayChildren() ? "" : "hidden"}`}
-
>
-
<For each={props.tree.children}>
-
{(node) => <RenderTree tree={node} />}
-
</For>
-
</div>
-
</Match>
-
</Switch>
-
);
-
}
-
-
function Fragment(props: {
-
file: string;
-
data: DiffTextFragment;
-
index: number;
-
numberSize: number;
-
}) {
-
let lineNumber = props.data.NewPosition;
-
let iOld = props.data.OldPosition;
-
let iNew = props.data.NewPosition;
-
-
return (
-
<Show when={!props.data.is_binary} fallback={<div>binary data</div>}>
-
<Show when={props.index !== 0}>
-
<div class="h-5 w-full select-none bg-gray-100 text-center font-mono text-gray-700 dark:bg-gray-700 dark:text-gray-300">
-
···
-
</div>
-
</Show>
-
<div class="w-full whitespace-pre font-mono">
-
<For each={props.data.Lines}>
-
{(line) => {
-
const lineNumberOld = line.Op === 2 ? "" : (iOld++).toString();
-
const lineNumberNew = line.Op === 1 ? "" : (iNew++).toString();
-
const fillerOld = " ".repeat(
-
props.numberSize - lineNumberOld.length,
-
);
-
const fillerNew = " ".repeat(
-
props.numberSize - lineNumberNew.length,
-
);
-
return (
-
<Line
-
file={props.file}
-
index={props.index}
-
line={line}
-
lineNumber={lineNumber++}
-
lineNumberNew={lineNumberNew}
-
lineNumberOld={lineNumberOld}
-
fillerOld={fillerOld}
-
fillerNew={fillerNew}
-
/>
-
);
-
}}
-
</For>
-
</div>
-
</Show>
-
);
-
}
-
-
function Line(props: {
-
file: string;
-
index: number;
-
line: { Op: number; Line: string };
-
lineNumber: number;
-
lineNumberOld: string;
-
lineNumberNew: string;
-
fillerOld: string;
-
fillerNew: string;
-
}) {
-
const id = `line-${props.file}-${props.index}-${props.lineNumber.toString()}`;
-
return (
-
<div
-
class="flex scroll-mt-10 flex-row text-gray-400 *:flex *:flex-row dark:text-gray-500"
-
id={id}
-
>
-
<div class="sticky left-0 select-none border-gray-200 border-r bg-white px-1 *:flex dark:border-gray-700 dark:bg-gray-800">
-
<span class="float-right mr-1 w-1/2 justify-end">
-
<span>{props.fillerOld}</span>
-
<Show when={props.lineNumberOld}>
-
<a
-
class="hover:text-gray-700 hover:underline hover:dark:text-gray-200"
-
href={`#${id}`}
-
>
-
{props.lineNumberOld}
-
</a>
-
</Show>
-
</span>
-
<span class="float-right mr-1 w-1/2 justify-end">
-
{props.fillerNew}
-
<Show when={props.lineNumberNew}>
-
<a
-
class="hover:text-gray-700 hover:underline hover:dark:text-gray-200"
-
href={`#${id}`}
-
>
-
{props.lineNumberNew}
-
</a>
-
</Show>
-
</span>
-
</div>
-
<Switch>
-
<Match when={props.line.Op === 0}>
-
<div class="w-full text-gray-500 dark:text-gray-500">
-
<div class="select-none">{"   "}</div>
-
{props.line.Line}
-
</div>
-
</Match>
-
<Match when={props.line.Op === 2}>
-
<div class="w-full bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400">
-
<div class="select-none">{" + "}</div>
-
{props.line.Line}
-
</div>
-
</Match>
-
<Match when={props.line.Op === 1}>
-
<div class="w-full bg-red-100 text-red-700 dark:bg-red-800/30 dark:text-red-400">
-
<div class="select-none">{" - "}</div>
-
{props.line.Line}
-
</div>
-
</Match>
-
</Switch>
-
</div>
-
);
-
}
-
-
function DiffView(props: { commit: Commit }) {
-
return (
-
<For each={props.commit.diff.diff}>
-
{(diff) => {
-
const [show, setShow] = createSignal(true);
-
const [addedLines, removedLines] = diff.text_fragments.reduce(
-
(acc, v) => [acc[0] + v.NewLines, acc[1] + v.LinesDeleted],
-
[0, 0],
-
);
-
-
const lastFrag = diff.text_fragments[diff.text_fragments.length - 1];
-
const numberSize = Math.max(
-
2,
-
(
-
Math.max(lastFrag.NewPosition, lastFrag.OldPosition) +
-
lastFrag.Lines.length
-
).toString().length,
-
);
-
-
return (
-
<div
-
id={`file-${diff.name.new}`}
-
class="not-last:mb-1 flex w-full flex-col rounded border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
-
>
-
<div
-
class={`sticky top-0 z-10 flex cursor-default select-none flex-row items-center gap-2 bg-white p-2 hover:bg-gray-100 dark:bg-gray-800 hover:dark:bg-gray-700 ${show() ? "rounded-t border-gray-200 border-b dark:border-gray-700" : "rounded"}`}
-
onclick={() => setShow(!show())}
-
>
-
<div
-
class={`iconify ${show() ? "gravity-ui--chevron-down" : "gravity-ui--chevron-right"}`}
-
/>
-
<div class="flex h-6 select-text flex-row items-center overflow-hidden rounded font-mono text-xs *:h-full *:content-center *:px-1">
-
<Show when={addedLines > 0}>
-
<div class="bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400">{`+${addedLines}`}</div>
-
</Show>
-
<Show when={removedLines > 0}>
-
<div class="bg-red-100 text-red-700 dark:bg-red-700/30 dark:text-red-400">{`-${removedLines}`}</div>
-
</Show>
-
</div>
-
<Show
-
when={diff.name.old !== "" && diff.name.new !== diff.name.old}
-
>
-
<div class="select-text">{diff.name.old}</div>
-
<div class="iconify gravity-ui--arrow-right" />
-
</Show>
-
<div class="select-text">{diff.name.new}</div>
-
</div>
-
<div
-
class={`select-text overflow-x-auto rounded-b bg-white dark:bg-gray-800 ${show() ? "" : "hidden"}`}
-
>
-
<div class="min-w-max">
-
<For each={diff.text_fragments}>
-
{(frag, i) => (
-
<Fragment
-
file={diff.name.new}
-
data={frag}
-
index={i()}
-
numberSize={numberSize}
-
/>
-
)}
-
</For>
-
</div>
-
</div>
-
</div>
-
);
-
}}
-
</For>
-
);
-
}
-
-
function CommitHeader(props: {
-
user: string;
-
repo: string;
-
message: { title: string; content: string };
-
commit: Commit;
-
}) {
-
return (
-
<div>
-
<Header user={props.user} repo={props.repo} />
-
<div class="mx-1 flex flex-col gap-2 rounded bg-white p-4 dark:bg-gray-800">
-
<div>{props.message.title}</div>
-
<Show when={props.message.content}>
-
<div class="text-xs">{props.message.content}</div>
-
</Show>
-
<div class="text-gray-500 text-xs dark:text-gray-300">
-
<span>{`${new Date(props.commit.diff.commit.author.When).toLocaleDateString(undefined, { dateStyle: "long" })} at ${new Date(props.commit.diff.commit.author.When).toLocaleTimeString()}`}</span>
-
<span class="select-none px-1 before:content-['\00B7']" />
-
<span>{`${props.commit.diff.commit.author.Name} <${props.commit.diff.commit.author.Email}>`}</span>
-
<span class="select-none px-1 before:content-['\00B7']" />
-
<a
-
class="hover:text-gray-600 hover:underline hover:dark:text-gray-200"
-
href={`/${props.user}/${props.repo}/commit/${props.commit.ref}`}
-
>
-
{props.commit.ref.slice(0, 8)}
-
</a>
-
<Show when={props.commit.diff.commit.parent}>
-
<div class="iconify gravity-ui--arrow-left mx-1 text-[0.6rem]" />
-
<a
-
class="hover:text-gray-600 hover:underline hover:dark:text-gray-200"
-
href={`/${props.user}/${props.repo}/commit/${props.commit.diff.commit.parent}`}
-
>
-
{props.commit.diff.commit.parent.slice(0, 8)}
-
</a>
-
</Show>
-
</div>
-
</div>
-
</div>
-
);
-
}
-
-
export default function RepoCommit() {
-
const params = useParams();
-
const did = useDid();
-
-
const [commit] = createResource(
-
() => {
-
const d = did();
-
if (!d) return;
-
return [d, params.repo, params.ref];
-
},
-
async ([did, repo, ref]) => {
-
const res = await getRepoCommit(did as DID, repo, ref);
-
if (!res.ok) return;
-
return res.data as Commit;
-
},
-
);
-
-
const [sidebar] = createResource(commit, async (commit) => {
-
if (!commit.diff.diff)
-
return { name: "", fullPath: "", type: "directory" } as TreeNode;
-
return buildTree(commit.diff.diff.map((v) => v.name.new));
-
});
-
-
const allData = createMemo(() => {
-
const s = sidebar();
-
const c = commit();
-
if (!(s && c)) return;
-
-
return [s, c] as const;
-
});
-
-
const headerData = createMemo(() => {
-
const c = commit();
-
if (!c) return;
-
-
const titleEnd = c.diff.commit.message.indexOf("\n");
-
const message = {
-
title: c.diff.commit.message.slice(0, titleEnd),
-
content: c.diff.commit.message.slice(titleEnd + 1),
-
};
-
-
return [c, message] as const;
-
});
-
-
onMount(() => {
-
if (window.location.hash) {
-
const element = document.getElementById(window.location.hash.slice(1));
-
if (element)
-
element.scrollIntoView({ behavior: "instant", block: "start" });
-
}
-
});
-
-
return (
-
<div class="mx-auto max-w-10xl">
-
<Suspense>
-
<Show when={headerData()} keyed>
-
{([commit, message]) => (
-
<CommitHeader
-
user={params.user}
-
repo={params.repo}
-
message={message}
-
commit={commit}
-
/>
-
)}
-
</Show>
-
<Show when={allData()} keyed>
-
{([sidebar, commit]) => (
-
<>
-
<div class="flex flex-row gap-1">
-
<div class="sticky top-0 flex max-h-screen min-w-50 overflow-auto p-1 pr-0">
-
<Show when={sidebar.children}>
-
<div class="flex min-h-max w-full grow cursor-default flex-col rounded border border-gray-200 bg-white p-1 dark:border-gray-700 dark:bg-gray-800">
-
<div class="flex flex-row items-center justify-between gap-1 p-1">
-
<div class="font-bold">CHANGED FILES</div>
-
<div class="flex h-6 select-text flex-row items-center overflow-hidden rounded font-mono text-xs *:h-full *:content-center *:px-1">
-
<Show when={commit.diff.stat.insertions > 0}>
-
<div class="bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400">{`+${commit.diff.stat.insertions}`}</div>
-
</Show>
-
<Show when={commit.diff.stat.deletions > 0}>
-
<div class="bg-red-100 text-red-700 dark:bg-red-700/30 dark:text-red-400">{`-${commit.diff.stat.deletions}`}</div>
-
</Show>
-
</div>
-
</div>
-
<RenderTree tree={sidebar} skip={true} />
-
</div>
-
</Show>
-
</div>
-
<div class="min-w-0 flex-1 flex-col gap-1 p-1 pl-0">
-
<DiffView commit={commit} />
-
</div>
-
</div>
-
</>
-
)}
-
</Show>
-
</Suspense>
-
</div>
-
);
-
}
+403
src/routes/repo/commit/index.tsx
···
+
import { type Params, useParams } from "@solidjs/router";
+
import {
+
createMemo,
+
createResource,
+
createSignal,
+
For,
+
Match,
+
onMount,
+
Show,
+
Switch,
+
} from "solid-js";
+
import { figureOutDid } from "../../../util/handle";
+
import type { Commit, DID, DiffTextFragment } from "../../../util/types";
+
import { useDid } from "../context";
+
import { Header } from "../main";
+
import { getRepoCommit } from "../main.data";
+
import { buildTree, type TreeNode } from "./data";
+
+
export async function preloadRepoCommit({ params }: { params: Params }) {
+
const did = await figureOutDid(params.user);
+
if (!did) return;
+
getRepoCommit(did, params.repo, params.ref);
+
}
+
+
function RenderTree(props: { tree: TreeNode; skip?: boolean }) {
+
if (props.skip)
+
return (
+
<For each={props.tree.children}>
+
{(node) => <RenderTree tree={node} />}
+
</For>
+
);
+
const [displayChildren, setDisplayChildren] = createSignal(true);
+
return (
+
<Switch>
+
<Match when={props.tree.type === "file"}>
+
<a
+
class="flex min-w-fit cursor-default select-none flex-row items-center gap-1 rounded p-1 text-xs hover:bg-gray-100 hover:dark:bg-gray-700"
+
href={`#file-${encodeURI(props.tree.fullPath)}`}
+
onClick={() => {
+
const hash = `#file-${encodeURI(props.tree.fullPath)}`;
+
if (window.location.hash === hash) {
+
document
+
.getElementById(`file-${props.tree.fullPath}`)
+
?.scrollIntoView({ behavior: "instant", block: "start" });
+
}
+
}}
+
>
+
<div class="iconify gravity-ui--file" />
+
<span class="select-text">{props.tree.name}</span>
+
</a>
+
</Match>
+
<Match when={props.tree.type === "directory"}>
+
<button
+
type="button"
+
class="flex min-w-fit select-none flex-row items-center gap-1 rounded p-1 text-xs hover:bg-gray-100 hover:dark:bg-gray-700"
+
onClick={() => setDisplayChildren(!displayChildren())}
+
>
+
<div class="iconify gravity-ui--folder-fill" />
+
<span class="select-text">{props.tree.name}</span>
+
</button>
+
<div
+
class={`ml-1 flex flex-col border-gray-200 border-l pl-1 dark:border-gray-700 ${displayChildren() ? "" : "hidden"}`}
+
>
+
<For each={props.tree.children}>
+
{(node) => <RenderTree tree={node} />}
+
</For>
+
</div>
+
</Match>
+
</Switch>
+
);
+
}
+
+
function Line(props: {
+
file: string;
+
index: number;
+
line: { Op: number; Line: string };
+
lineNumber: number;
+
lineNumberOld: string;
+
lineNumberNew: string;
+
filler: string;
+
}) {
+
const id = `line-${encodeURI(props.file)}-${props.index}-${props.lineNumber.toString()}`;
+
return (
+
<div
+
class="flex scroll-mt-10 flex-row text-gray-400 *:flex *:flex-row dark:text-gray-500"
+
id={id}
+
>
+
<div class="sticky left-0 select-none border-gray-200 border-r bg-white *:flex dark:border-gray-700 dark:bg-gray-800">
+
<Show
+
when={props.lineNumberOld}
+
fallback={
+
<span class="float-right w-1/2 justify-end pr-1 pl-1.5">
+
{props.filler}
+
</span>
+
}
+
>
+
<a
+
href={`#${id}`}
+
class="float-right w-1/2 justify-end pr-1 pl-1.5 hover:text-gray-700 hover:dark:text-gray-200"
+
>
+
{props.lineNumberOld}
+
</a>
+
</Show>
+
<Show
+
when={props.lineNumberNew}
+
fallback={
+
<span class="float-right w-1/2 justify-end pr-1.5 pl-1">
+
{props.filler}
+
</span>
+
}
+
>
+
<a
+
href={`#${id}`}
+
class="float-right w-1/2 justify-end pr-1.5 pl-1 hover:text-gray-700 hover:dark:text-gray-200"
+
>
+
{props.lineNumberNew}
+
</a>
+
</Show>
+
</div>
+
<Switch>
+
<Match when={props.line.Op === 0}>
+
<div class="w-full text-gray-500 dark:text-gray-500">
+
<div class="select-none">{"   "}</div>
+
{props.line.Line}
+
</div>
+
</Match>
+
<Match when={props.line.Op === 2}>
+
<div class="w-full bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400">
+
<div class="select-none">{" + "}</div>
+
{props.line.Line}
+
</div>
+
</Match>
+
<Match when={props.line.Op === 1}>
+
<div class="w-full bg-red-100 text-red-700 dark:bg-red-800/30 dark:text-red-400">
+
<div class="select-none">{" - "}</div>
+
{props.line.Line}
+
</div>
+
</Match>
+
</Switch>
+
</div>
+
);
+
}
+
+
function Fragment(props: {
+
file: string;
+
data: DiffTextFragment;
+
index: number;
+
numberSize: number;
+
}) {
+
let lineNumber = props.data.NewPosition;
+
let iOld = props.data.OldPosition;
+
let iNew = props.data.NewPosition;
+
+
return (
+
<>
+
<Show when={props.index !== 0}>
+
<div class="h-5 w-full select-none bg-gray-100 text-center font-mono text-gray-700 dark:bg-gray-700 dark:text-gray-300">
+
···
+
</div>
+
</Show>
+
<div class="w-full whitespace-pre font-mono">
+
<For each={props.data.Lines}>
+
{(line) => {
+
const lineNumberOld =
+
line.Op === 2
+
? ""
+
: (iOld++).toString().padStart(props.numberSize, " ");
+
const lineNumberNew =
+
line.Op === 1
+
? ""
+
: (iNew++).toString().padStart(props.numberSize, " ");
+
const filler = " ".repeat(props.numberSize);
+
return (
+
<Line
+
file={props.file}
+
index={props.index}
+
line={line}
+
lineNumber={lineNumber++}
+
lineNumberNew={lineNumberNew}
+
lineNumberOld={lineNumberOld}
+
filler={filler}
+
/>
+
);
+
}}
+
</For>
+
</div>
+
</>
+
);
+
}
+
+
function DiffView(props: { commit: Commit }) {
+
return (
+
<For each={props.commit.diff.diff}>
+
{(diff) => {
+
const [show, setShow] = createSignal(true);
+
+
const [addedLines, removedLines] = diff.text_fragments
+
? diff.text_fragments.reduce(
+
(acc, v) => [acc[0] + v.NewLines, acc[1] + v.LinesDeleted],
+
[0, 0],
+
)
+
: [0, 0];
+
+
const header = (
+
<button
+
type="button"
+
class={`sticky top-0 z-10 flex cursor-default select-none flex-row items-center gap-2 bg-white p-2 hover:bg-gray-100 dark:bg-gray-800 hover:dark:bg-gray-700 ${show() ? "rounded-t border-gray-200 border-b dark:border-gray-700" : "rounded"}`}
+
onClick={() => setShow(!show())}
+
>
+
<div
+
class={`iconify ${show() ? "gravity-ui--chevron-down" : "gravity-ui--chevron-right"}`}
+
/>
+
<div class="flex h-6 select-text flex-row items-center overflow-hidden rounded font-mono text-xs *:h-full *:content-center *:px-1">
+
<Show when={addedLines > 0}>
+
<div class="bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400">{`+${addedLines}`}</div>
+
</Show>
+
<Show when={removedLines > 0}>
+
<div class="bg-red-100 text-red-700 dark:bg-red-700/30 dark:text-red-400">{`-${removedLines}`}</div>
+
</Show>
+
</div>
+
<Show
+
when={diff.name.old !== "" && diff.name.new !== diff.name.old}
+
>
+
<div class="select-text">{diff.name.old}</div>
+
<div class="iconify gravity-ui--arrow-right" />
+
</Show>
+
<div class="select-text">{diff.name.new}</div>
+
</button>
+
);
+
+
if (!diff.text_fragments)
+
return (
+
<div
+
id={`file-${encodeURI(diff.name.new)}`}
+
class="not-last:mb-1 flex w-full flex-col rounded border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
+
>
+
{header}
+
<div
+
class={`flex select-text justify-center rounded-b bg-white py-2 text-gray-500 dark:bg-gray-800 dark:text-gray-300 ${show() ? "" : "hidden"}`}
+
>
+
This is a binary file and will not be displayed.
+
</div>
+
</div>
+
);
+
+
const lastFrag = diff.text_fragments[diff.text_fragments.length - 1];
+
const numberSize = Math.max(
+
2,
+
(
+
Math.max(lastFrag.NewPosition, lastFrag.OldPosition) +
+
lastFrag.Lines.length
+
).toString().length,
+
);
+
+
return (
+
<div
+
id={`file-${diff.name.new}`}
+
class="not-last:mb-1 flex w-full flex-col rounded border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
+
>
+
{header}
+
<div
+
class={`select-text overflow-x-auto rounded-b bg-white dark:bg-gray-800 ${show() ? "" : "hidden"}`}
+
>
+
<div class="min-w-max">
+
<For each={diff.text_fragments}>
+
{(frag, i) => (
+
<Fragment
+
file={diff.name.new}
+
data={frag}
+
index={i()}
+
numberSize={numberSize}
+
/>
+
)}
+
</For>
+
</div>
+
</div>
+
</div>
+
);
+
}}
+
</For>
+
);
+
}
+
+
function CommitHeader(props: {
+
user: string;
+
repo: string;
+
message: { title: string; content: string };
+
commit: Commit;
+
}) {
+
return (
+
<div>
+
<Header user={props.user} repo={props.repo} />
+
<div class="mx-1 flex flex-col gap-2 rounded bg-white p-4 dark:bg-gray-800">
+
<div>{props.message.title}</div>
+
<Show when={props.message.content}>
+
<div class="text-xs">{props.message.content}</div>
+
</Show>
+
<div class="text-gray-500 text-xs dark:text-gray-300">
+
<span>{`${new Date(props.commit.diff.commit.author.When).toLocaleDateString(undefined, { dateStyle: "long" })} at ${new Date(props.commit.diff.commit.author.When).toLocaleTimeString()}`}</span>
+
<span class="select-none px-1 before:content-['\00B7']" />
+
<span>{`${props.commit.diff.commit.author.Name} <${props.commit.diff.commit.author.Email}>`}</span>
+
<span class="select-none px-1 before:content-['\00B7']" />
+
<a
+
class="hover:text-gray-600 hover:underline hover:dark:text-gray-200"
+
href={`/${props.user}/${props.repo}/commit/${props.commit.ref}`}
+
>
+
{props.commit.ref.slice(0, 8)}
+
</a>
+
<Show when={props.commit.diff.commit.parent}>
+
<div class="iconify gravity-ui--arrow-left mx-1 text-[0.6rem]" />
+
<a
+
class="hover:text-gray-600 hover:underline hover:dark:text-gray-200"
+
href={`/${props.user}/${props.repo}/commit/${props.commit.diff.commit.parent}`}
+
>
+
{props.commit.diff.commit.parent.slice(0, 8)}
+
</a>
+
</Show>
+
</div>
+
</div>
+
</div>
+
);
+
}
+
+
export default function RepoCommit() {
+
const params = useParams();
+
const did = useDid();
+
+
const [commit] = createResource(
+
() => {
+
const d = did();
+
if (!d) return;
+
return [d, params.repo, params.ref];
+
},
+
async ([did, repo, ref]) => {
+
const res = await getRepoCommit(did as DID, repo, ref);
+
if (!res.ok) return;
+
return res.data as Commit;
+
},
+
);
+
+
const sidebar = createMemo(() => {
+
if (!commit()?.diff.diff)
+
return { name: "", fullPath: "", type: "directory" } as TreeNode;
+
return buildTree(commit()!.diff.diff.map((v) => v.name.new));
+
});
+
+
const message = createMemo(() => {
+
const c = commit();
+
if (!c) return;
+
+
const titleEnd = c.diff.commit.message.indexOf("\n");
+
return {
+
title: c.diff.commit.message.slice(0, titleEnd),
+
content: c.diff.commit.message.slice(titleEnd + 1),
+
};
+
});
+
+
onMount(() => {
+
if (window.location.hash) {
+
const element = document.getElementById(window.location.hash.slice(1));
+
if (element)
+
element.scrollIntoView({ behavior: "instant", block: "start" });
+
}
+
});
+
+
return (
+
<div class="mx-auto max-w-10xl">
+
<Show when={commit() && message()}>
+
<CommitHeader
+
user={params.user}
+
repo={params.repo}
+
message={message()!}
+
commit={commit()!}
+
/>
+
</Show>
+
<Show when={sidebar()?.children && commit()}>
+
<div class="flex flex-row gap-1">
+
<div class="sticky top-0 flex max-h-screen w-50 overflow-y-auto p-1 pr-0">
+
<div class="flex min-h-max w-full grow cursor-default flex-col rounded border border-gray-200 bg-white p-1 dark:border-gray-700 dark:bg-gray-800">
+
<div class="flex flex-row items-center justify-between gap-1 p-1">
+
<div class="font-bold">CHANGED FILES</div>
+
<div class="flex h-6 select-text flex-row items-center overflow-hidden rounded font-mono text-xs *:h-full *:content-center *:px-1">
+
<Show when={commit()!.diff.stat.insertions > 0}>
+
<div class="bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400">{`+${commit()!.diff.stat.insertions}`}</div>
+
</Show>
+
<Show when={commit()!.diff.stat.deletions > 0}>
+
<div class="bg-red-100 text-red-700 dark:bg-red-700/30 dark:text-red-400">{`-${commit()!.diff.stat.deletions}`}</div>
+
</Show>
+
</div>
+
</div>
+
<div class="max-w-full overflow-x-auto text-nowrap">
+
<RenderTree tree={sidebar()!} skip={true} />
+
</div>
+
</div>
+
</div>
+
<div class="min-w-0 flex-1 flex-col gap-1 p-1 pl-0">
+
<DiffView commit={commit()!} />
+
</div>
+
</div>
+
</Show>
+
</div>
+
);
+
}
+2 -2
src/routes/repo/main.data.ts
···
}, "RepoDefaultBranch");
export const getRepoTree = query(
-
async (user: DID, repo: string, ref: string, path: string) => {
+
async (user: DID, repo: string, ref: string, path?: string) => {
const rpc = await getKnotRpc(user, repo);
return await rpc.get("sh.tangled.repo.tree", {
params: {
repo: `${user}/${repo}`,
ref,
-
path,
+
path: path ?? "",
},
});
},
+66 -78
src/routes/repo/tree.tsx
···
Switch,
} from "solid-js";
import { SolidMarkdown } from "solid-markdown";
-
import { IconWithText } from "../../elements/icon_with_text";
import { languageColors } from "../../util/get_language";
import type { RepoLog } from "../../util/types";
import { useDid } from "./context";
···
const [languages] = createResource(did, async (did) => {
const res = await getRepoLanguages(did, params.repo, params.ref);
if (!res.ok) return;
-
return res.data.languages.sort((a, b) => b.percentage - a.percentage);
+
return res.data.languages.sort((a, b) =>
+
b.name === "" ? -1 : b.percentage - a.percentage,
+
);
});
const [logs] = createResource(
···
},
);
-
const [readme] = createResource(tree, async (tree) => {
-
if (!tree.readme) return;
+
const readme = createMemo(() => {
+
const readme = tree()?.readme;
+
if (!readme) return;
+
return {
-
contents: tree.readme.contents,
-
type: tree.readme.filename.toLowerCase().endsWith(".md")
+
contents: readme.contents,
+
type: readme.filename.toLowerCase().endsWith(".md")
? "markdown"
: "plaintext",
} as const;
});
-
const [filesInOrder] = createResource(tree, (tree) => {
-
if (!tree.files) return;
-
return tree.files.sort((a, b) => {
-
if (!a.is_file === b.is_file) return !a.is_file ? -1 : 1;
+
const sortedFiles = createMemo(() => {
+
const files = tree()?.files;
+
if (!files) return;
+
+
return files.sort((a, b) => {
+
if (a.is_file !== b.is_file) return a.is_file ? 1 : -1;
const aDot = a.name.startsWith(".");
const bDot = b.name.startsWith(".");
···
});
});
-
const repoData = createMemo(() => {
-
const db = defaultBranch();
-
const l = languages();
-
if (!(db && l)) return;
-
return [db, l] as const;
-
});
-
-
const pathData = createMemo(() => {
-
const t = tree();
-
const f = filesInOrder();
-
const r = readme();
-
const l = logs();
-
if (!(t && f && r && l)) return;
-
return [t, f, r, l] as const;
-
});
-
return (
<div class="mx-auto max-w-5xl">
-
<Show when={repoData()} keyed>
-
{([defaultBranch, languages]) => (
-
<Show when={pathData()} keyed>
-
{([tree, files, readme, logs]) => (
-
<div>
-
<Header user={params.user} repo={params.repo} />
-
<div class="mb-4 flex flex-col rounded bg-white dark:bg-gray-800">
-
<LanguageLine languages={languages} />
-
<div class="flex flex-row">
-
<div class="mr-1 flex w-1/2 flex-col border-gray-300 border-r p-2 pt-1 dark:border-gray-700">
-
<FileDirectory
-
user={params.user}
-
repo={params.repo}
-
files={files}
-
tree={tree}
-
defaultBranch={defaultBranch}
-
/>
-
</div>
-
<div class="ml-1 flex w-1/2 flex-col p-2 pt-1">
-
<LogData
-
user={params.user}
-
repo={params.repo}
-
defaultBranch={defaultBranch}
-
files={files}
-
logs={logs}
-
/>
-
</div>
-
</div>
-
</div>
-
<Show when={readme.contents}>
-
<ReadmeCard
-
path={`/${params.user}/${params.repo}/blob/${tree.ref || defaultBranch}`}
-
readme={readme}
-
/>
-
</Show>
-
</div>
-
)}
+
<div>
+
<Header user={params.user} repo={params.repo} />
+
<div class="mb-4 flex flex-col rounded bg-white dark:bg-gray-800">
+
<Show when={languages()} fallback={<div class="h-4" />}>
+
<LanguageLine languages={languages()!} />
</Show>
-
)}
-
</Show>
+
+
<div class="flex flex-row">
+
<div class="mr-1 flex w-1/2 flex-col border-gray-300 border-r p-2 pt-1 dark:border-gray-700">
+
<Show when={defaultBranch() && tree() && sortedFiles()}>
+
<FileDirectory
+
user={params.user}
+
repo={params.repo}
+
files={sortedFiles()!}
+
tree={tree()!}
+
defaultBranch={defaultBranch()!}
+
/>
+
</Show>
+
</div>
+
<div class="ml-1 flex w-1/2 flex-col p-2 pt-1">
+
<Show when={defaultBranch() && sortedFiles() && logs()}>
+
<LogData
+
user={params.user}
+
repo={params.repo}
+
defaultBranch={defaultBranch()!}
+
files={sortedFiles()!}
+
logs={logs()!}
+
/>
+
</Show>
+
</div>
+
</div>
+
</div>
+
<Show when={readme()?.contents}>
+
<ReadmeCard
+
path={`/${params.user}/${params.repo}/blob/${tree()!.ref || defaultBranch}`}
+
readme={readme()!}
+
/>
+
</Show>
+
</div>
</div>
);
}
···
return (
<div class={languageLineState() ? "h-full" : "h-4"}>
-
<div
+
<button
+
type="button"
class={`flex w-full flex-row overflow-hidden rounded-t duration-75 hover:h-4 ${languageLineState() ? "h-4" : "h-2"}`}
-
onclick={toggleLanguageLineState}
+
onClick={toggleLanguageLineState}
>
<For each={props.languages}>
{(language) => (
<div
-
class="h-full border-gray-50 border-r duration-75 hover:brightness-90 dark:border-gray-950 dark:hover:brightness-110"
-
style={`width: ${language.percentage}%; background-color: ${languageColors.get(language.name.toLowerCase().replaceAll(" ", ""))}`}
+
class="h-full border-gray-50 not-last:border-r duration-75 hover:brightness-90 dark:border-gray-950 dark:hover:brightness-110"
+
style={`width: ${language.percentage}%; background-color: ${languageColors.get(language.name.toLowerCase().replaceAll(" ", "")) ?? "aaa"}`}
title={`${language.name} ${language.percentage}%`}
></div>
)}
</For>
-
</div>
+
</button>
<div
-
class={`flex h-4 flex-row gap-3 border-gray-300 border-r border-b px-6 py-3.5 text-xs dark:border-gray-700 ${languageLineState() ? "" : "hidden"}`}
+
class={`flex h-4 flex-row justify-around gap-3 border-gray-300 border-b px-6 py-3.5 text-xs dark:border-gray-700 ${languageLineState() ? "" : "hidden"}`}
>
<For each={props.languages}>
{(language) => (
<div class="flex flex-row items-center gap-2">
<div
class="h-2 w-2 rounded-full"
-
style={`background-color: ${languageColors.get(language.name.toLowerCase().replaceAll(" ", ""))}`}
+
style={`background-color: ${languageColors.get(language.name.toLowerCase().replaceAll(" ", "")) ?? "#aaa"}`}
/>
<span>
-
<span>{language.name}</span>{" "}
+
<span>{language.name || "Other"}</span>{" "}
<span class="text-gray-600 dark:text-gray-400">
{language.percentage}%
</span>
···
class="mb-2 flex flex-row items-center gap-2 text-black hover:text-gray-600 dark:text-white hover:dark:text-gray-300"
href={`/${props.user}/${props.repo}/commits/${props.logs.ref || props.defaultBranch}`}
>
-
<IconWithText
-
icon="gravity-ui--code-commit"
-
text="commits"
-
style="font-bold"
-
/>
+
<div class="flex select-none flex-row items-center gap-1 font-bold">
+
<div class="iconify gravity-ui--code-commit" />
+
<span>commits</span>
+
</div>
<div class="rounded bg-gray-300 px-1 text-xs dark:bg-gray-700">
{props.logs.total}
</div>
+4 -17
src/styles/fileviewer.css src/elements/code_block/style.css
···
@import "tailwindcss";
-
pre code .line-wrapper {
-
@apply flex flex-row;
-
}
-
-
pre code .line-number {
-
@apply opacity-50 shrink-0 w-12 text-right pr-4 select-none;
-
}
-
-
pre code .line-content {
-
@apply flex-1 pl-4 whitespace-pre-wrap text-sm;
-
}
-
.hljs {
@apply text-gray-900 dark:text-gray-300;
}
···
.hljs-regexp,
.hljs-string,
.hljs-meta .hljs-string {
-
@apply text-cyan-800 dark:text-cyan-300;
+
@apply text-cyan-800 dark:text-cyan-200;
}
.hljs-built_in,
.hljs-symbol {
-
@apply text-amber-700 dark:text-amber-400;
+
@apply text-amber-700 dark:text-amber-300;
}
.hljs-comment,
.hljs-code,
-
.hljs-formula,
-
pre code .line::before {
-
@apply text-neutral-500 dark:text-neutral-500;
+
.hljs-formula {
+
@apply text-neutral-500 dark:text-neutral-400;
}
.hljs-name,
.hljs-quote,
+5 -57
src/styles/index.css
···
}
}
-
.btn {
-
@apply relative z-10 inline-flex min-h-[30px] items-center justify-center bg-transparent pl-2 pr-2 text-sm text-gray-900 outline-none;
-
}
-
-
.btn::before {
-
@apply absolute inset-0 -z-10 rounded border border-gray-200 bg-white duration-150 transition-all ease-in;
-
--tw-shadow:
-
inset 0 -2px 0 0 rgba(0, 0, 0, 0.1), 0 1px 0 0 rgba(0, 0, 0, 0.04);
-
--tw-shadow-colored:
-
inset 0 -2px 0 0 var(--tw-shadow-color), 0 1px 0 0 var(--tw-shadow-color);
-
box-shadow:
-
var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
-
var(--tw-shadow);
-
content: "";
-
}
-
-
.btn:hover::before {
-
@apply bg-gray-50;
-
--tw-shadow:
-
inset 0 -2px 0 0 rgba(0, 0, 0, 0.15), 0 2px 1px 0 rgba(0, 0, 0, 0.06);
-
--tw-shadow-colored:
-
inset 0 -2px 0 0 var(--tw-shadow-color), 0 2px 1px 0 var(--tw-shadow-color);
-
box-shadow:
-
var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
-
var(--tw-shadow);
-
content: "";
-
}
-
-
.btn:focus {
-
@apply outline-2 outline-offset-2;
-
}
-
-
.btn:focus-visible::before {
-
@apply outline-solid outline-2 outline-gray-400;
-
}
-
-
.btn:active::before {
-
--tw-shadow: inset 0 2px 2px 0 rgba(0, 0, 0, 0.1);
-
--tw-shadow-colored: inset 0 2px 2px 0 var(--tw-shadow-color);
-
box-shadow:
-
var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
-
var(--tw-shadow);
-
content: "";
-
}
-
-
.btn:disabled {
-
@apply cursor-not-allowed opacity-50;
-
}
-
-
@media (prefers-color-scheme: dark) {
+
@layer components {
+
/* https://tangled.org/@tangled.org/core/blob/master/input.css */
.btn {
-
@apply text-gray-100;
+
@apply relative z-10 inline-flex min-h-[30px] cursor-pointer items-center justify-center bg-transparent px-2 pb-[0.2rem] text-sm text-gray-900 before:absolute before:inset-0 before:-z-10 before:block before:rounded before:border before:border-gray-200 before:bg-white before:shadow-[inset_0_-2px_0_0_rgba(0,0,0,0.1),0_1px_0_0_rgba(0,0,0,0.04)] before:content-[''] before:transition-all before:duration-150 before:ease-in-out hover:before:shadow-[inset_0_-2px_0_0_rgba(0,0,0,0.15),0_2px_1px_0_rgba(0,0,0,0.06)] hover:before:bg-gray-50 dark:hover:before:bg-gray-700 active:before:shadow-[inset_0_2px_2px_0_rgba(0,0,0,0.1)] focus:outline-none focus-visible:before:outline-2 focus-visible:before:outline-gray-400 disabled:cursor-not-allowed disabled:opacity-50 dark:text-gray-100 dark:before:bg-gray-800 dark:before:border-gray-700;
}
-
.btn::before {
-
@apply border-gray-700 bg-gray-800;
-
}
-
-
.btn:hover::before {
-
@apply bg-gray-700;
+
.btn-create {
+
@apply btn text-white before:bg-green-600 hover:before:bg-green-700 dark:before:bg-green-700 dark:hover:before:bg-green-800 before:border before:border-green-700 hover:before:border-green-800 focus-visible:before:outline-green-500 disabled:before:bg-green-400 dark:disabled:before:bg-green-600;
}
}