extremely wip tangled spa

skibivi

aylac.top 7246fe13 c7941b02

verified
+17 -3
LICENSE
··· 1 + MIT License 2 + 1 3 Copyright (c) 2025 aylac.top 2 4 3 - 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: 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 4 11 5 - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 6 14 7 - 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. 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+2 -1
biome.json
··· 20 20 "style": { "noNonNullAssertion": "off" } 21 21 }, 22 22 "domains": { 23 - "solid": "recommended" 23 + "solid": "all", 24 + "react": "none" 24 25 } 25 26 }, 26 27 "javascript": {
+41 -40
package.json
··· 1 1 { 2 - "name": "vite-template-solid", 3 - "version": "0.0.0", 4 - "description": "", 5 - "type": "module", 6 - "scripts": { 7 - "start": "vite", 8 - "dev": "vite", 9 - "build": "vite build", 10 - "serve": "vite preview" 11 - }, 12 - "license": "MIT", 13 - "devDependencies": { 14 - "@biomejs/biome": "^2.3.4", 15 - "@iconify-json/gravity-ui": "^1.2.10", 16 - "@iconify/tailwind4": "^1.1.0", 17 - "postcss": "^8.5.6", 18 - "solid-devtools": "^0.34.4", 19 - "tailwindcss": "^4.1.17", 20 - "vite": "^7.2.2", 21 - "vite-plugin-solid": "^2.11.10" 22 - }, 23 - "dependencies": { 24 - "@atcute/atproto": "^3.1.9", 25 - "@atcute/client": "^4.0.5", 26 - "@atcute/identity-resolver": "^1.1.4", 27 - "@atcute/oauth-browser-client": "^2.0.1", 28 - "@atcute/tangled": "^1.0.10", 29 - "@gleam-lang/highlight.js-gleam": "^1.5.0", 30 - "@solidjs/router": "^0.15.3", 31 - "@tailwindcss/vite": "^4.1.17", 32 - "@types/highlight.js": "^10.1.0", 33 - "highlight.js": "^11.11.1", 34 - "highlightjs-line-numbers.js": "^2.9.1", 35 - "solid-js": "^1.9.10", 36 - "solid-markdown": "^2.0.14" 37 - }, 38 - "patchedDependencies": { 39 - "@gleam-lang/highlight.js-gleam@1.5.0": "patches/@gleam-lang%2Fhighlight.js-gleam@1.5.0.patch", 40 - "highlight.js@11.11.1": "patches/highlight.js@11.11.1.patch" 41 - } 2 + "name": "vite-template-solid", 3 + "version": "0.0.0", 4 + "description": "", 5 + "type": "module", 6 + "scripts": { 7 + "start": "vite", 8 + "dev": "vite", 9 + "build": "vite build", 10 + "serve": "vite preview", 11 + "lint": "biome lint" 12 + }, 13 + "license": "MIT", 14 + "devDependencies": { 15 + "@biomejs/biome": "^2.3.4", 16 + "@iconify-json/gravity-ui": "^1.2.10", 17 + "@iconify/tailwind4": "^1.1.0", 18 + "postcss": "^8.5.6", 19 + "solid-devtools": "^0.34.4", 20 + "tailwindcss": "^4.1.17", 21 + "vite": "^7.2.2", 22 + "vite-plugin-solid": "^2.11.10" 23 + }, 24 + "dependencies": { 25 + "@atcute/atproto": "^3.1.9", 26 + "@atcute/client": "^4.0.5", 27 + "@atcute/identity-resolver": "^1.1.4", 28 + "@atcute/oauth-browser-client": "^2.0.1", 29 + "@atcute/tangled": "^1.0.10", 30 + "@gleam-lang/highlight.js-gleam": "^1.5.0", 31 + "@solidjs/router": "^0.15.3", 32 + "@tailwindcss/vite": "^4.1.17", 33 + "@types/highlight.js": "^10.1.0", 34 + "highlight.js": "^11.11.1", 35 + "highlightjs-line-numbers.js": "^2.9.1", 36 + "solid-js": "^1.9.10", 37 + "solid-markdown": "^2.0.14" 38 + }, 39 + "patchedDependencies": { 40 + "@gleam-lang/highlight.js-gleam@1.5.0": "patches/@gleam-lang%2Fhighlight.js-gleam@1.5.0.patch", 41 + "highlight.js@11.11.1": "patches/highlight.js@11.11.1.patch" 42 + } 42 43 }
-4
shell.nix
··· 4 4 #in 5 5 pkgs.mkShellNoCC { 6 6 #inputsFrom = [ defaultPackage ]; 7 - # 8 - env = { 9 - RUST_SRC_PATH = pkgs.rustPlatform.rustLibSrc; 10 - }; 11 7 12 8 packages = with pkgs; [ 13 9 bun
-31
src/elements/code_block.tsx
··· 1 - import hljs from "highlight.js"; 2 - import { createResource, For } from "solid-js"; 3 - import "../styles/fileviewer.css"; 4 - import "../util/highlight.js/index"; 5 - 6 - export function CodeBlock(props: { code: string; language: string }) { 7 - const [codeBlock] = createResource( 8 - () => props, 9 - (props) => { 10 - const highlit = hljs.getLanguage(props.language) 11 - ? hljs.highlight(props.code, { language: props.language }).value 12 - : props.code; 13 - return ( 14 - <For each={highlit.split("\n")}> 15 - {(line, i) => ( 16 - <span class="line-wrapper"> 17 - <span class="line-number">{i() + 1}</span> 18 - <span class="line-content" innerHTML={line}></span> 19 - </span> 20 - )} 21 - </For> 22 - ); 23 - }, 24 - ); 25 - 26 - return ( 27 - <pre> 28 - <code class="flex flex-col text-wrap p-4">{codeBlock()}</code> 29 - </pre> 30 - ); 31 - }
+32
src/elements/code_block/index.tsx
··· 1 + import hljs from "highlight.js"; 2 + import { For } from "solid-js"; 3 + import "./style.css"; 4 + import "../../util/highlight.js/index"; 5 + 6 + export function CodeBlock(props: { code: string; language: string }) { 7 + const highlit = ( 8 + hljs.getLanguage(props.language) 9 + ? hljs.highlight(props.code, { language: props.language }).value 10 + : props.code 11 + ).split("\n"); 12 + const numberSize = highlit.length.toString().length; 13 + return ( 14 + <div class="overflow-x-auto"> 15 + <div class="flex w-min flex-col whitespace-pre text-nowrap font-mono text-gray-500 dark:text-gray-300"> 16 + <For each={highlit}> 17 + {(line, i) => ( 18 + <div id={`L${i() + 1}`} class="flex flex-row gap-2"> 19 + <a 20 + href={`#L${i() + 1}`} 21 + 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" 22 + > 23 + {(i() + 1).toString().padStart(numberSize, " ")} 24 + </a> 25 + <span innerHTML={line} /> 26 + </div> 27 + )} 28 + </For> 29 + </div> 30 + </div> 31 + ); 32 + }
-31
src/elements/icon_with_text.tsx
··· 1 - import { type ComponentProps, splitProps } from "solid-js"; 2 - import { Dynamic } from "solid-js/web"; 3 - 4 - type AllowedTags = "div" | "a" | "span" | "button"; 5 - 6 - export function IconWithText<T extends AllowedTags>( 7 - props: { 8 - type?: T; 9 - icon: string; 10 - text: string; 11 - style?: string; 12 - href?: string; 13 - } & ComponentProps<T>, 14 - ) { 15 - const [local, dynamic] = splitProps( 16 - props, 17 - ["type", "icon", "text", "style"], 18 - ["href"], 19 - ); 20 - 21 - return ( 22 - <Dynamic 23 - component={local.type || "span"} 24 - class={`flex flex-row items-center gap-1 ${local.style || ""}`} 25 - {...dynamic} 26 - > 27 - <div class={`iconify ${local.icon}`} /> 28 - <span>{local.text}</span> 29 - </Dynamic> 30 - ); 31 - }
+6 -6
src/errors/404.tsx
··· 1 1 export default function NotFound() { 2 - return ( 3 - <section class="text-gray-700 p-8"> 4 - <h1 class="text-2xl font-bold">404: Not Found</h1> 5 - <p class="mt-4">It's gone 😞</p> 6 - </section> 7 - ); 2 + return ( 3 + <section class="p-8 text-gray-700"> 4 + <h1 class="font-bold text-2xl">404: Not Found</h1> 5 + <p class="mt-4">It's gone 😞</p> 6 + </section> 7 + ); 8 8 }
+7 -3
src/index.tsx
··· 7 7 import App from "./app"; 8 8 import NotFound from "./errors/404"; 9 9 import RepoBlob, { preloadRepoBlob } from "./routes/repo/blob"; 10 - import RepoCommit from "./routes/repo/commit/commit"; 10 + import RepoCommit, { preloadRepoCommit } from "./routes/repo/commit"; 11 11 import { RepoProvider } from "./routes/repo/context"; 12 12 import RepoTree, { preloadRepoTree } from "./routes/repo/tree"; 13 13 import User from "./routes/user"; ··· 32 32 component={RepoBlob} 33 33 preload={preloadRepoBlob} 34 34 /> 35 - <Route path="/commit/:ref" component={RepoCommit} /> 36 - <Route component={RepoTree} /> 35 + <Route 36 + path="/commit/:ref" 37 + component={RepoCommit} 38 + preload={preloadRepoCommit} 39 + /> 40 + <Route component={RepoTree} preload={preloadRepoTree} /> 37 41 </Route> 38 42 <Route path="/:user" component={User} /> 39 43 <Route path="*" component={NotFound} />
+13 -10
src/routes/repo/blob.tsx
··· 4 4 useNavigate, 5 5 useParams, 6 6 } from "@solidjs/router"; 7 - import { Show } from "solid-js"; 7 + import { createMemo } from "solid-js"; 8 8 import { CodeBlock } from "../../elements/code_block"; 9 9 import { getLanguage } from "../../util/get_language"; 10 10 import { figureOutDid } from "../../util/handle"; ··· 38 38 })(), 39 39 ); 40 40 41 + const codeBlock = createMemo(() => { 42 + if (!blob()) return; 43 + return ( 44 + <CodeBlock 45 + code={blob()!.content} 46 + language={getLanguage(blob()!.path.split("/").pop()) || "text"} 47 + /> 48 + ); 49 + }); 50 + 41 51 return ( 42 52 <div class="mx-auto max-w-5xl"> 43 53 <Header user={params.user} repo={params.repo} /> 44 - <div class="flex flex-col rounded bg-white dark:bg-gray-800"> 45 - <Show when={blob()} keyed> 46 - {(data) => ( 47 - <CodeBlock 48 - code={data.content} 49 - language={getLanguage(data.path.split("/").pop()) || "text"} 50 - /> 51 - )} 52 - </Show> 54 + <div class="flex flex-col rounded bg-white p-4 dark:bg-gray-800"> 55 + {codeBlock()} 53 56 </div> 54 57 </div> 55 58 );
src/routes/repo/commit/commit.data.ts src/routes/repo/commit/data.ts
-376
src/routes/repo/commit/commit.tsx
··· 1 - import { useParams } from "@solidjs/router"; 2 - import { 3 - createMemo, 4 - createResource, 5 - createSignal, 6 - For, 7 - Match, 8 - onMount, 9 - Show, 10 - Suspense, 11 - Switch, 12 - } from "solid-js"; 13 - import type { Commit, DID, DiffTextFragment } from "../../../util/types"; 14 - import { useDid } from "../context"; 15 - import { Header } from "../main"; 16 - import { getRepoCommit } from "../main.data"; 17 - import { buildTree, type TreeNode } from "./commit.data"; 18 - 19 - function RenderTree(props: { tree: TreeNode; skip?: boolean }) { 20 - if (props.skip) 21 - return ( 22 - <For each={props.tree.children}> 23 - {(node) => <RenderTree tree={node} />} 24 - </For> 25 - ); 26 - const [displayChildren, setDisplayChildren] = createSignal(true); 27 - return ( 28 - <Switch> 29 - <Match when={props.tree.type === "file"}> 30 - <a 31 - 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" 32 - href={`#file-${props.tree.fullPath}`} 33 - > 34 - <div class="iconify gravity-ui--file" /> 35 - <span class="select-text">{props.tree.name}</span> 36 - </a> 37 - </Match> 38 - <Match when={props.tree.type === "directory"}> 39 - <div 40 - class="flex select-none flex-row items-center gap-1 rounded p-1 text-xs hover:bg-gray-100 hover:dark:bg-gray-700" 41 - onclick={() => setDisplayChildren(!displayChildren())} 42 - > 43 - <div class="iconify gravity-ui--folder-fill" /> 44 - <span class="select-text">{props.tree.name}</span> 45 - </div> 46 - <div 47 - class={`ml-1 flex flex-col border-gray-200 border-l pl-1 dark:border-gray-700 ${displayChildren() ? "" : "hidden"}`} 48 - > 49 - <For each={props.tree.children}> 50 - {(node) => <RenderTree tree={node} />} 51 - </For> 52 - </div> 53 - </Match> 54 - </Switch> 55 - ); 56 - } 57 - 58 - function Fragment(props: { 59 - file: string; 60 - data: DiffTextFragment; 61 - index: number; 62 - numberSize: number; 63 - }) { 64 - let lineNumber = props.data.NewPosition; 65 - let iOld = props.data.OldPosition; 66 - let iNew = props.data.NewPosition; 67 - 68 - return ( 69 - <Show when={!props.data.is_binary} fallback={<div>binary data</div>}> 70 - <Show when={props.index !== 0}> 71 - <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"> 72 - ··· 73 - </div> 74 - </Show> 75 - <div class="w-full whitespace-pre font-mono"> 76 - <For each={props.data.Lines}> 77 - {(line) => { 78 - const lineNumberOld = line.Op === 2 ? "" : (iOld++).toString(); 79 - const lineNumberNew = line.Op === 1 ? "" : (iNew++).toString(); 80 - const fillerOld = " ".repeat( 81 - props.numberSize - lineNumberOld.length, 82 - ); 83 - const fillerNew = " ".repeat( 84 - props.numberSize - lineNumberNew.length, 85 - ); 86 - return ( 87 - <Line 88 - file={props.file} 89 - index={props.index} 90 - line={line} 91 - lineNumber={lineNumber++} 92 - lineNumberNew={lineNumberNew} 93 - lineNumberOld={lineNumberOld} 94 - fillerOld={fillerOld} 95 - fillerNew={fillerNew} 96 - /> 97 - ); 98 - }} 99 - </For> 100 - </div> 101 - </Show> 102 - ); 103 - } 104 - 105 - function Line(props: { 106 - file: string; 107 - index: number; 108 - line: { Op: number; Line: string }; 109 - lineNumber: number; 110 - lineNumberOld: string; 111 - lineNumberNew: string; 112 - fillerOld: string; 113 - fillerNew: string; 114 - }) { 115 - const id = `line-${props.file}-${props.index}-${props.lineNumber.toString()}`; 116 - return ( 117 - <div 118 - class="flex scroll-mt-10 flex-row text-gray-400 *:flex *:flex-row dark:text-gray-500" 119 - id={id} 120 - > 121 - <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"> 122 - <span class="float-right mr-1 w-1/2 justify-end"> 123 - <span>{props.fillerOld}</span> 124 - <Show when={props.lineNumberOld}> 125 - <a 126 - class="hover:text-gray-700 hover:underline hover:dark:text-gray-200" 127 - href={`#${id}`} 128 - > 129 - {props.lineNumberOld} 130 - </a> 131 - </Show> 132 - </span> 133 - <span class="float-right mr-1 w-1/2 justify-end"> 134 - {props.fillerNew} 135 - <Show when={props.lineNumberNew}> 136 - <a 137 - class="hover:text-gray-700 hover:underline hover:dark:text-gray-200" 138 - href={`#${id}`} 139 - > 140 - {props.lineNumberNew} 141 - </a> 142 - </Show> 143 - </span> 144 - </div> 145 - <Switch> 146 - <Match when={props.line.Op === 0}> 147 - <div class="w-full text-gray-500 dark:text-gray-500"> 148 - <div class="select-none">{"   "}</div> 149 - {props.line.Line} 150 - </div> 151 - </Match> 152 - <Match when={props.line.Op === 2}> 153 - <div class="w-full bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400"> 154 - <div class="select-none">{" + "}</div> 155 - {props.line.Line} 156 - </div> 157 - </Match> 158 - <Match when={props.line.Op === 1}> 159 - <div class="w-full bg-red-100 text-red-700 dark:bg-red-800/30 dark:text-red-400"> 160 - <div class="select-none">{" - "}</div> 161 - {props.line.Line} 162 - </div> 163 - </Match> 164 - </Switch> 165 - </div> 166 - ); 167 - } 168 - 169 - function DiffView(props: { commit: Commit }) { 170 - return ( 171 - <For each={props.commit.diff.diff}> 172 - {(diff) => { 173 - const [show, setShow] = createSignal(true); 174 - const [addedLines, removedLines] = diff.text_fragments.reduce( 175 - (acc, v) => [acc[0] + v.NewLines, acc[1] + v.LinesDeleted], 176 - [0, 0], 177 - ); 178 - 179 - const lastFrag = diff.text_fragments[diff.text_fragments.length - 1]; 180 - const numberSize = Math.max( 181 - 2, 182 - ( 183 - Math.max(lastFrag.NewPosition, lastFrag.OldPosition) + 184 - lastFrag.Lines.length 185 - ).toString().length, 186 - ); 187 - 188 - return ( 189 - <div 190 - id={`file-${diff.name.new}`} 191 - 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" 192 - > 193 - <div 194 - 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"}`} 195 - onclick={() => setShow(!show())} 196 - > 197 - <div 198 - class={`iconify ${show() ? "gravity-ui--chevron-down" : "gravity-ui--chevron-right"}`} 199 - /> 200 - <div class="flex h-6 select-text flex-row items-center overflow-hidden rounded font-mono text-xs *:h-full *:content-center *:px-1"> 201 - <Show when={addedLines > 0}> 202 - <div class="bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400">{`+${addedLines}`}</div> 203 - </Show> 204 - <Show when={removedLines > 0}> 205 - <div class="bg-red-100 text-red-700 dark:bg-red-700/30 dark:text-red-400">{`-${removedLines}`}</div> 206 - </Show> 207 - </div> 208 - <Show 209 - when={diff.name.old !== "" && diff.name.new !== diff.name.old} 210 - > 211 - <div class="select-text">{diff.name.old}</div> 212 - <div class="iconify gravity-ui--arrow-right" /> 213 - </Show> 214 - <div class="select-text">{diff.name.new}</div> 215 - </div> 216 - <div 217 - class={`select-text overflow-x-auto rounded-b bg-white dark:bg-gray-800 ${show() ? "" : "hidden"}`} 218 - > 219 - <div class="min-w-max"> 220 - <For each={diff.text_fragments}> 221 - {(frag, i) => ( 222 - <Fragment 223 - file={diff.name.new} 224 - data={frag} 225 - index={i()} 226 - numberSize={numberSize} 227 - /> 228 - )} 229 - </For> 230 - </div> 231 - </div> 232 - </div> 233 - ); 234 - }} 235 - </For> 236 - ); 237 - } 238 - 239 - function CommitHeader(props: { 240 - user: string; 241 - repo: string; 242 - message: { title: string; content: string }; 243 - commit: Commit; 244 - }) { 245 - return ( 246 - <div> 247 - <Header user={props.user} repo={props.repo} /> 248 - <div class="mx-1 flex flex-col gap-2 rounded bg-white p-4 dark:bg-gray-800"> 249 - <div>{props.message.title}</div> 250 - <Show when={props.message.content}> 251 - <div class="text-xs">{props.message.content}</div> 252 - </Show> 253 - <div class="text-gray-500 text-xs dark:text-gray-300"> 254 - <span>{`${new Date(props.commit.diff.commit.author.When).toLocaleDateString(undefined, { dateStyle: "long" })} at ${new Date(props.commit.diff.commit.author.When).toLocaleTimeString()}`}</span> 255 - <span class="select-none px-1 before:content-['\00B7']" /> 256 - <span>{`${props.commit.diff.commit.author.Name} <${props.commit.diff.commit.author.Email}>`}</span> 257 - <span class="select-none px-1 before:content-['\00B7']" /> 258 - <a 259 - class="hover:text-gray-600 hover:underline hover:dark:text-gray-200" 260 - href={`/${props.user}/${props.repo}/commit/${props.commit.ref}`} 261 - > 262 - {props.commit.ref.slice(0, 8)} 263 - </a> 264 - <Show when={props.commit.diff.commit.parent}> 265 - <div class="iconify gravity-ui--arrow-left mx-1 text-[0.6rem]" /> 266 - <a 267 - class="hover:text-gray-600 hover:underline hover:dark:text-gray-200" 268 - href={`/${props.user}/${props.repo}/commit/${props.commit.diff.commit.parent}`} 269 - > 270 - {props.commit.diff.commit.parent.slice(0, 8)} 271 - </a> 272 - </Show> 273 - </div> 274 - </div> 275 - </div> 276 - ); 277 - } 278 - 279 - export default function RepoCommit() { 280 - const params = useParams(); 281 - const did = useDid(); 282 - 283 - const [commit] = createResource( 284 - () => { 285 - const d = did(); 286 - if (!d) return; 287 - return [d, params.repo, params.ref]; 288 - }, 289 - async ([did, repo, ref]) => { 290 - const res = await getRepoCommit(did as DID, repo, ref); 291 - if (!res.ok) return; 292 - return res.data as Commit; 293 - }, 294 - ); 295 - 296 - const [sidebar] = createResource(commit, async (commit) => { 297 - if (!commit.diff.diff) 298 - return { name: "", fullPath: "", type: "directory" } as TreeNode; 299 - return buildTree(commit.diff.diff.map((v) => v.name.new)); 300 - }); 301 - 302 - const allData = createMemo(() => { 303 - const s = sidebar(); 304 - const c = commit(); 305 - if (!(s && c)) return; 306 - 307 - return [s, c] as const; 308 - }); 309 - 310 - const headerData = createMemo(() => { 311 - const c = commit(); 312 - if (!c) return; 313 - 314 - const titleEnd = c.diff.commit.message.indexOf("\n"); 315 - const message = { 316 - title: c.diff.commit.message.slice(0, titleEnd), 317 - content: c.diff.commit.message.slice(titleEnd + 1), 318 - }; 319 - 320 - return [c, message] as const; 321 - }); 322 - 323 - onMount(() => { 324 - if (window.location.hash) { 325 - const element = document.getElementById(window.location.hash.slice(1)); 326 - if (element) 327 - element.scrollIntoView({ behavior: "instant", block: "start" }); 328 - } 329 - }); 330 - 331 - return ( 332 - <div class="mx-auto max-w-10xl"> 333 - <Suspense> 334 - <Show when={headerData()} keyed> 335 - {([commit, message]) => ( 336 - <CommitHeader 337 - user={params.user} 338 - repo={params.repo} 339 - message={message} 340 - commit={commit} 341 - /> 342 - )} 343 - </Show> 344 - <Show when={allData()} keyed> 345 - {([sidebar, commit]) => ( 346 - <> 347 - <div class="flex flex-row gap-1"> 348 - <div class="sticky top-0 flex max-h-screen min-w-50 overflow-auto p-1 pr-0"> 349 - <Show when={sidebar.children}> 350 - <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"> 351 - <div class="flex flex-row items-center justify-between gap-1 p-1"> 352 - <div class="font-bold">CHANGED FILES</div> 353 - <div class="flex h-6 select-text flex-row items-center overflow-hidden rounded font-mono text-xs *:h-full *:content-center *:px-1"> 354 - <Show when={commit.diff.stat.insertions > 0}> 355 - <div class="bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400">{`+${commit.diff.stat.insertions}`}</div> 356 - </Show> 357 - <Show when={commit.diff.stat.deletions > 0}> 358 - <div class="bg-red-100 text-red-700 dark:bg-red-700/30 dark:text-red-400">{`-${commit.diff.stat.deletions}`}</div> 359 - </Show> 360 - </div> 361 - </div> 362 - <RenderTree tree={sidebar} skip={true} /> 363 - </div> 364 - </Show> 365 - </div> 366 - <div class="min-w-0 flex-1 flex-col gap-1 p-1 pl-0"> 367 - <DiffView commit={commit} /> 368 - </div> 369 - </div> 370 - </> 371 - )} 372 - </Show> 373 - </Suspense> 374 - </div> 375 - ); 376 - }
+403
src/routes/repo/commit/index.tsx
··· 1 + import { type Params, useParams } from "@solidjs/router"; 2 + import { 3 + createMemo, 4 + createResource, 5 + createSignal, 6 + For, 7 + Match, 8 + onMount, 9 + Show, 10 + Switch, 11 + } from "solid-js"; 12 + import { figureOutDid } from "../../../util/handle"; 13 + import type { Commit, DID, DiffTextFragment } from "../../../util/types"; 14 + import { useDid } from "../context"; 15 + import { Header } from "../main"; 16 + import { getRepoCommit } from "../main.data"; 17 + import { buildTree, type TreeNode } from "./data"; 18 + 19 + export async function preloadRepoCommit({ params }: { params: Params }) { 20 + const did = await figureOutDid(params.user); 21 + if (!did) return; 22 + getRepoCommit(did, params.repo, params.ref); 23 + } 24 + 25 + function RenderTree(props: { tree: TreeNode; skip?: boolean }) { 26 + if (props.skip) 27 + return ( 28 + <For each={props.tree.children}> 29 + {(node) => <RenderTree tree={node} />} 30 + </For> 31 + ); 32 + const [displayChildren, setDisplayChildren] = createSignal(true); 33 + return ( 34 + <Switch> 35 + <Match when={props.tree.type === "file"}> 36 + <a 37 + 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" 38 + href={`#file-${encodeURI(props.tree.fullPath)}`} 39 + onClick={() => { 40 + const hash = `#file-${encodeURI(props.tree.fullPath)}`; 41 + if (window.location.hash === hash) { 42 + document 43 + .getElementById(`file-${props.tree.fullPath}`) 44 + ?.scrollIntoView({ behavior: "instant", block: "start" }); 45 + } 46 + }} 47 + > 48 + <div class="iconify gravity-ui--file" /> 49 + <span class="select-text">{props.tree.name}</span> 50 + </a> 51 + </Match> 52 + <Match when={props.tree.type === "directory"}> 53 + <button 54 + type="button" 55 + 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" 56 + onClick={() => setDisplayChildren(!displayChildren())} 57 + > 58 + <div class="iconify gravity-ui--folder-fill" /> 59 + <span class="select-text">{props.tree.name}</span> 60 + </button> 61 + <div 62 + class={`ml-1 flex flex-col border-gray-200 border-l pl-1 dark:border-gray-700 ${displayChildren() ? "" : "hidden"}`} 63 + > 64 + <For each={props.tree.children}> 65 + {(node) => <RenderTree tree={node} />} 66 + </For> 67 + </div> 68 + </Match> 69 + </Switch> 70 + ); 71 + } 72 + 73 + function Line(props: { 74 + file: string; 75 + index: number; 76 + line: { Op: number; Line: string }; 77 + lineNumber: number; 78 + lineNumberOld: string; 79 + lineNumberNew: string; 80 + filler: string; 81 + }) { 82 + const id = `line-${encodeURI(props.file)}-${props.index}-${props.lineNumber.toString()}`; 83 + return ( 84 + <div 85 + class="flex scroll-mt-10 flex-row text-gray-400 *:flex *:flex-row dark:text-gray-500" 86 + id={id} 87 + > 88 + <div class="sticky left-0 select-none border-gray-200 border-r bg-white *:flex dark:border-gray-700 dark:bg-gray-800"> 89 + <Show 90 + when={props.lineNumberOld} 91 + fallback={ 92 + <span class="float-right w-1/2 justify-end pr-1 pl-1.5"> 93 + {props.filler} 94 + </span> 95 + } 96 + > 97 + <a 98 + href={`#${id}`} 99 + class="float-right w-1/2 justify-end pr-1 pl-1.5 hover:text-gray-700 hover:dark:text-gray-200" 100 + > 101 + {props.lineNumberOld} 102 + </a> 103 + </Show> 104 + <Show 105 + when={props.lineNumberNew} 106 + fallback={ 107 + <span class="float-right w-1/2 justify-end pr-1.5 pl-1"> 108 + {props.filler} 109 + </span> 110 + } 111 + > 112 + <a 113 + href={`#${id}`} 114 + class="float-right w-1/2 justify-end pr-1.5 pl-1 hover:text-gray-700 hover:dark:text-gray-200" 115 + > 116 + {props.lineNumberNew} 117 + </a> 118 + </Show> 119 + </div> 120 + <Switch> 121 + <Match when={props.line.Op === 0}> 122 + <div class="w-full text-gray-500 dark:text-gray-500"> 123 + <div class="select-none">{"   "}</div> 124 + {props.line.Line} 125 + </div> 126 + </Match> 127 + <Match when={props.line.Op === 2}> 128 + <div class="w-full bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400"> 129 + <div class="select-none">{" + "}</div> 130 + {props.line.Line} 131 + </div> 132 + </Match> 133 + <Match when={props.line.Op === 1}> 134 + <div class="w-full bg-red-100 text-red-700 dark:bg-red-800/30 dark:text-red-400"> 135 + <div class="select-none">{" - "}</div> 136 + {props.line.Line} 137 + </div> 138 + </Match> 139 + </Switch> 140 + </div> 141 + ); 142 + } 143 + 144 + function Fragment(props: { 145 + file: string; 146 + data: DiffTextFragment; 147 + index: number; 148 + numberSize: number; 149 + }) { 150 + let lineNumber = props.data.NewPosition; 151 + let iOld = props.data.OldPosition; 152 + let iNew = props.data.NewPosition; 153 + 154 + return ( 155 + <> 156 + <Show when={props.index !== 0}> 157 + <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"> 158 + ··· 159 + </div> 160 + </Show> 161 + <div class="w-full whitespace-pre font-mono"> 162 + <For each={props.data.Lines}> 163 + {(line) => { 164 + const lineNumberOld = 165 + line.Op === 2 166 + ? "" 167 + : (iOld++).toString().padStart(props.numberSize, " "); 168 + const lineNumberNew = 169 + line.Op === 1 170 + ? "" 171 + : (iNew++).toString().padStart(props.numberSize, " "); 172 + const filler = " ".repeat(props.numberSize); 173 + return ( 174 + <Line 175 + file={props.file} 176 + index={props.index} 177 + line={line} 178 + lineNumber={lineNumber++} 179 + lineNumberNew={lineNumberNew} 180 + lineNumberOld={lineNumberOld} 181 + filler={filler} 182 + /> 183 + ); 184 + }} 185 + </For> 186 + </div> 187 + </> 188 + ); 189 + } 190 + 191 + function DiffView(props: { commit: Commit }) { 192 + return ( 193 + <For each={props.commit.diff.diff}> 194 + {(diff) => { 195 + const [show, setShow] = createSignal(true); 196 + 197 + const [addedLines, removedLines] = diff.text_fragments 198 + ? diff.text_fragments.reduce( 199 + (acc, v) => [acc[0] + v.NewLines, acc[1] + v.LinesDeleted], 200 + [0, 0], 201 + ) 202 + : [0, 0]; 203 + 204 + const header = ( 205 + <button 206 + type="button" 207 + 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"}`} 208 + onClick={() => setShow(!show())} 209 + > 210 + <div 211 + class={`iconify ${show() ? "gravity-ui--chevron-down" : "gravity-ui--chevron-right"}`} 212 + /> 213 + <div class="flex h-6 select-text flex-row items-center overflow-hidden rounded font-mono text-xs *:h-full *:content-center *:px-1"> 214 + <Show when={addedLines > 0}> 215 + <div class="bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400">{`+${addedLines}`}</div> 216 + </Show> 217 + <Show when={removedLines > 0}> 218 + <div class="bg-red-100 text-red-700 dark:bg-red-700/30 dark:text-red-400">{`-${removedLines}`}</div> 219 + </Show> 220 + </div> 221 + <Show 222 + when={diff.name.old !== "" && diff.name.new !== diff.name.old} 223 + > 224 + <div class="select-text">{diff.name.old}</div> 225 + <div class="iconify gravity-ui--arrow-right" /> 226 + </Show> 227 + <div class="select-text">{diff.name.new}</div> 228 + </button> 229 + ); 230 + 231 + if (!diff.text_fragments) 232 + return ( 233 + <div 234 + id={`file-${encodeURI(diff.name.new)}`} 235 + 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" 236 + > 237 + {header} 238 + <div 239 + 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"}`} 240 + > 241 + This is a binary file and will not be displayed. 242 + </div> 243 + </div> 244 + ); 245 + 246 + const lastFrag = diff.text_fragments[diff.text_fragments.length - 1]; 247 + const numberSize = Math.max( 248 + 2, 249 + ( 250 + Math.max(lastFrag.NewPosition, lastFrag.OldPosition) + 251 + lastFrag.Lines.length 252 + ).toString().length, 253 + ); 254 + 255 + return ( 256 + <div 257 + id={`file-${diff.name.new}`} 258 + 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" 259 + > 260 + {header} 261 + <div 262 + class={`select-text overflow-x-auto rounded-b bg-white dark:bg-gray-800 ${show() ? "" : "hidden"}`} 263 + > 264 + <div class="min-w-max"> 265 + <For each={diff.text_fragments}> 266 + {(frag, i) => ( 267 + <Fragment 268 + file={diff.name.new} 269 + data={frag} 270 + index={i()} 271 + numberSize={numberSize} 272 + /> 273 + )} 274 + </For> 275 + </div> 276 + </div> 277 + </div> 278 + ); 279 + }} 280 + </For> 281 + ); 282 + } 283 + 284 + function CommitHeader(props: { 285 + user: string; 286 + repo: string; 287 + message: { title: string; content: string }; 288 + commit: Commit; 289 + }) { 290 + return ( 291 + <div> 292 + <Header user={props.user} repo={props.repo} /> 293 + <div class="mx-1 flex flex-col gap-2 rounded bg-white p-4 dark:bg-gray-800"> 294 + <div>{props.message.title}</div> 295 + <Show when={props.message.content}> 296 + <div class="text-xs">{props.message.content}</div> 297 + </Show> 298 + <div class="text-gray-500 text-xs dark:text-gray-300"> 299 + <span>{`${new Date(props.commit.diff.commit.author.When).toLocaleDateString(undefined, { dateStyle: "long" })} at ${new Date(props.commit.diff.commit.author.When).toLocaleTimeString()}`}</span> 300 + <span class="select-none px-1 before:content-['\00B7']" /> 301 + <span>{`${props.commit.diff.commit.author.Name} <${props.commit.diff.commit.author.Email}>`}</span> 302 + <span class="select-none px-1 before:content-['\00B7']" /> 303 + <a 304 + class="hover:text-gray-600 hover:underline hover:dark:text-gray-200" 305 + href={`/${props.user}/${props.repo}/commit/${props.commit.ref}`} 306 + > 307 + {props.commit.ref.slice(0, 8)} 308 + </a> 309 + <Show when={props.commit.diff.commit.parent}> 310 + <div class="iconify gravity-ui--arrow-left mx-1 text-[0.6rem]" /> 311 + <a 312 + class="hover:text-gray-600 hover:underline hover:dark:text-gray-200" 313 + href={`/${props.user}/${props.repo}/commit/${props.commit.diff.commit.parent}`} 314 + > 315 + {props.commit.diff.commit.parent.slice(0, 8)} 316 + </a> 317 + </Show> 318 + </div> 319 + </div> 320 + </div> 321 + ); 322 + } 323 + 324 + export default function RepoCommit() { 325 + const params = useParams(); 326 + const did = useDid(); 327 + 328 + const [commit] = createResource( 329 + () => { 330 + const d = did(); 331 + if (!d) return; 332 + return [d, params.repo, params.ref]; 333 + }, 334 + async ([did, repo, ref]) => { 335 + const res = await getRepoCommit(did as DID, repo, ref); 336 + if (!res.ok) return; 337 + return res.data as Commit; 338 + }, 339 + ); 340 + 341 + const sidebar = createMemo(() => { 342 + if (!commit()?.diff.diff) 343 + return { name: "", fullPath: "", type: "directory" } as TreeNode; 344 + return buildTree(commit()!.diff.diff.map((v) => v.name.new)); 345 + }); 346 + 347 + const message = createMemo(() => { 348 + const c = commit(); 349 + if (!c) return; 350 + 351 + const titleEnd = c.diff.commit.message.indexOf("\n"); 352 + return { 353 + title: c.diff.commit.message.slice(0, titleEnd), 354 + content: c.diff.commit.message.slice(titleEnd + 1), 355 + }; 356 + }); 357 + 358 + onMount(() => { 359 + if (window.location.hash) { 360 + const element = document.getElementById(window.location.hash.slice(1)); 361 + if (element) 362 + element.scrollIntoView({ behavior: "instant", block: "start" }); 363 + } 364 + }); 365 + 366 + return ( 367 + <div class="mx-auto max-w-10xl"> 368 + <Show when={commit() && message()}> 369 + <CommitHeader 370 + user={params.user} 371 + repo={params.repo} 372 + message={message()!} 373 + commit={commit()!} 374 + /> 375 + </Show> 376 + <Show when={sidebar()?.children && commit()}> 377 + <div class="flex flex-row gap-1"> 378 + <div class="sticky top-0 flex max-h-screen w-50 overflow-y-auto p-1 pr-0"> 379 + <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"> 380 + <div class="flex flex-row items-center justify-between gap-1 p-1"> 381 + <div class="font-bold">CHANGED FILES</div> 382 + <div class="flex h-6 select-text flex-row items-center overflow-hidden rounded font-mono text-xs *:h-full *:content-center *:px-1"> 383 + <Show when={commit()!.diff.stat.insertions > 0}> 384 + <div class="bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400">{`+${commit()!.diff.stat.insertions}`}</div> 385 + </Show> 386 + <Show when={commit()!.diff.stat.deletions > 0}> 387 + <div class="bg-red-100 text-red-700 dark:bg-red-700/30 dark:text-red-400">{`-${commit()!.diff.stat.deletions}`}</div> 388 + </Show> 389 + </div> 390 + </div> 391 + <div class="max-w-full overflow-x-auto text-nowrap"> 392 + <RenderTree tree={sidebar()!} skip={true} /> 393 + </div> 394 + </div> 395 + </div> 396 + <div class="min-w-0 flex-1 flex-col gap-1 p-1 pl-0"> 397 + <DiffView commit={commit()!} /> 398 + </div> 399 + </div> 400 + </Show> 401 + </div> 402 + ); 403 + }
+2 -2
src/routes/repo/main.data.ts
··· 23 23 }, "RepoDefaultBranch"); 24 24 25 25 export const getRepoTree = query( 26 - async (user: DID, repo: string, ref: string, path: string) => { 26 + async (user: DID, repo: string, ref: string, path?: string) => { 27 27 const rpc = await getKnotRpc(user, repo); 28 28 29 29 return await rpc.get("sh.tangled.repo.tree", { 30 30 params: { 31 31 repo: `${user}/${repo}`, 32 32 ref, 33 - path, 33 + path: path ?? "", 34 34 }, 35 35 }); 36 36 },
+66 -78
src/routes/repo/tree.tsx
··· 13 13 Switch, 14 14 } from "solid-js"; 15 15 import { SolidMarkdown } from "solid-markdown"; 16 - import { IconWithText } from "../../elements/icon_with_text"; 17 16 import { languageColors } from "../../util/get_language"; 18 17 import type { RepoLog } from "../../util/types"; 19 18 import { useDid } from "./context"; ··· 59 58 const [languages] = createResource(did, async (did) => { 60 59 const res = await getRepoLanguages(did, params.repo, params.ref); 61 60 if (!res.ok) return; 62 - return res.data.languages.sort((a, b) => b.percentage - a.percentage); 61 + return res.data.languages.sort((a, b) => 62 + b.name === "" ? -1 : b.percentage - a.percentage, 63 + ); 63 64 }); 64 65 65 66 const [logs] = createResource( ··· 86 87 }, 87 88 ); 88 89 89 - const [readme] = createResource(tree, async (tree) => { 90 - if (!tree.readme) return; 90 + const readme = createMemo(() => { 91 + const readme = tree()?.readme; 92 + if (!readme) return; 93 + 91 94 return { 92 - contents: tree.readme.contents, 93 - type: tree.readme.filename.toLowerCase().endsWith(".md") 95 + contents: readme.contents, 96 + type: readme.filename.toLowerCase().endsWith(".md") 94 97 ? "markdown" 95 98 : "plaintext", 96 99 } as const; 97 100 }); 98 101 99 - const [filesInOrder] = createResource(tree, (tree) => { 100 - if (!tree.files) return; 101 - return tree.files.sort((a, b) => { 102 - if (!a.is_file === b.is_file) return !a.is_file ? -1 : 1; 102 + const sortedFiles = createMemo(() => { 103 + const files = tree()?.files; 104 + if (!files) return; 105 + 106 + return files.sort((a, b) => { 107 + if (a.is_file !== b.is_file) return a.is_file ? 1 : -1; 103 108 104 109 const aDot = a.name.startsWith("."); 105 110 const bDot = b.name.startsWith("."); ··· 109 114 }); 110 115 }); 111 116 112 - const repoData = createMemo(() => { 113 - const db = defaultBranch(); 114 - const l = languages(); 115 - if (!(db && l)) return; 116 - return [db, l] as const; 117 - }); 118 - 119 - const pathData = createMemo(() => { 120 - const t = tree(); 121 - const f = filesInOrder(); 122 - const r = readme(); 123 - const l = logs(); 124 - if (!(t && f && r && l)) return; 125 - return [t, f, r, l] as const; 126 - }); 127 - 128 117 return ( 129 118 <div class="mx-auto max-w-5xl"> 130 - <Show when={repoData()} keyed> 131 - {([defaultBranch, languages]) => ( 132 - <Show when={pathData()} keyed> 133 - {([tree, files, readme, logs]) => ( 134 - <div> 135 - <Header user={params.user} repo={params.repo} /> 136 - <div class="mb-4 flex flex-col rounded bg-white dark:bg-gray-800"> 137 - <LanguageLine languages={languages} /> 138 - <div class="flex flex-row"> 139 - <div class="mr-1 flex w-1/2 flex-col border-gray-300 border-r p-2 pt-1 dark:border-gray-700"> 140 - <FileDirectory 141 - user={params.user} 142 - repo={params.repo} 143 - files={files} 144 - tree={tree} 145 - defaultBranch={defaultBranch} 146 - /> 147 - </div> 148 - <div class="ml-1 flex w-1/2 flex-col p-2 pt-1"> 149 - <LogData 150 - user={params.user} 151 - repo={params.repo} 152 - defaultBranch={defaultBranch} 153 - files={files} 154 - logs={logs} 155 - /> 156 - </div> 157 - </div> 158 - </div> 159 - <Show when={readme.contents}> 160 - <ReadmeCard 161 - path={`/${params.user}/${params.repo}/blob/${tree.ref || defaultBranch}`} 162 - readme={readme} 163 - /> 164 - </Show> 165 - </div> 166 - )} 119 + <div> 120 + <Header user={params.user} repo={params.repo} /> 121 + <div class="mb-4 flex flex-col rounded bg-white dark:bg-gray-800"> 122 + <Show when={languages()} fallback={<div class="h-4" />}> 123 + <LanguageLine languages={languages()!} /> 167 124 </Show> 168 - )} 169 - </Show> 125 + 126 + <div class="flex flex-row"> 127 + <div class="mr-1 flex w-1/2 flex-col border-gray-300 border-r p-2 pt-1 dark:border-gray-700"> 128 + <Show when={defaultBranch() && tree() && sortedFiles()}> 129 + <FileDirectory 130 + user={params.user} 131 + repo={params.repo} 132 + files={sortedFiles()!} 133 + tree={tree()!} 134 + defaultBranch={defaultBranch()!} 135 + /> 136 + </Show> 137 + </div> 138 + <div class="ml-1 flex w-1/2 flex-col p-2 pt-1"> 139 + <Show when={defaultBranch() && sortedFiles() && logs()}> 140 + <LogData 141 + user={params.user} 142 + repo={params.repo} 143 + defaultBranch={defaultBranch()!} 144 + files={sortedFiles()!} 145 + logs={logs()!} 146 + /> 147 + </Show> 148 + </div> 149 + </div> 150 + </div> 151 + <Show when={readme()?.contents}> 152 + <ReadmeCard 153 + path={`/${params.user}/${params.repo}/blob/${tree()!.ref || defaultBranch}`} 154 + readme={readme()!} 155 + /> 156 + </Show> 157 + </div> 170 158 </div> 171 159 ); 172 160 } ··· 205 193 206 194 return ( 207 195 <div class={languageLineState() ? "h-full" : "h-4"}> 208 - <div 196 + <button 197 + type="button" 209 198 class={`flex w-full flex-row overflow-hidden rounded-t duration-75 hover:h-4 ${languageLineState() ? "h-4" : "h-2"}`} 210 - onclick={toggleLanguageLineState} 199 + onClick={toggleLanguageLineState} 211 200 > 212 201 <For each={props.languages}> 213 202 {(language) => ( 214 203 <div 215 - class="h-full border-gray-50 border-r duration-75 hover:brightness-90 dark:border-gray-950 dark:hover:brightness-110" 216 - style={`width: ${language.percentage}%; background-color: ${languageColors.get(language.name.toLowerCase().replaceAll(" ", ""))}`} 204 + class="h-full border-gray-50 not-last:border-r duration-75 hover:brightness-90 dark:border-gray-950 dark:hover:brightness-110" 205 + style={`width: ${language.percentage}%; background-color: ${languageColors.get(language.name.toLowerCase().replaceAll(" ", "")) ?? "aaa"}`} 217 206 title={`${language.name} ${language.percentage}%`} 218 207 ></div> 219 208 )} 220 209 </For> 221 - </div> 210 + </button> 222 211 <div 223 - 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"}`} 212 + 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"}`} 224 213 > 225 214 <For each={props.languages}> 226 215 {(language) => ( 227 216 <div class="flex flex-row items-center gap-2"> 228 217 <div 229 218 class="h-2 w-2 rounded-full" 230 - style={`background-color: ${languageColors.get(language.name.toLowerCase().replaceAll(" ", ""))}`} 219 + style={`background-color: ${languageColors.get(language.name.toLowerCase().replaceAll(" ", "")) ?? "#aaa"}`} 231 220 /> 232 221 <span> 233 - <span>{language.name}</span>{" "} 222 + <span>{language.name || "Other"}</span>{" "} 234 223 <span class="text-gray-600 dark:text-gray-400"> 235 224 {language.percentage}% 236 225 </span> ··· 292 281 class="mb-2 flex flex-row items-center gap-2 text-black hover:text-gray-600 dark:text-white hover:dark:text-gray-300" 293 282 href={`/${props.user}/${props.repo}/commits/${props.logs.ref || props.defaultBranch}`} 294 283 > 295 - <IconWithText 296 - icon="gravity-ui--code-commit" 297 - text="commits" 298 - style="font-bold" 299 - /> 284 + <div class="flex select-none flex-row items-center gap-1 font-bold"> 285 + <div class="iconify gravity-ui--code-commit" /> 286 + <span>commits</span> 287 + </div> 300 288 <div class="rounded bg-gray-300 px-1 text-xs dark:bg-gray-700"> 301 289 {props.logs.total} 302 290 </div>
+4 -17
src/styles/fileviewer.css src/elements/code_block/style.css
··· 1 1 @import "tailwindcss"; 2 2 3 - pre code .line-wrapper { 4 - @apply flex flex-row; 5 - } 6 - 7 - pre code .line-number { 8 - @apply opacity-50 shrink-0 w-12 text-right pr-4 select-none; 9 - } 10 - 11 - pre code .line-content { 12 - @apply flex-1 pl-4 whitespace-pre-wrap text-sm; 13 - } 14 - 15 3 .hljs { 16 4 @apply text-gray-900 dark:text-gray-300; 17 5 } ··· 45 33 .hljs-regexp, 46 34 .hljs-string, 47 35 .hljs-meta .hljs-string { 48 - @apply text-cyan-800 dark:text-cyan-300; 36 + @apply text-cyan-800 dark:text-cyan-200; 49 37 } 50 38 .hljs-built_in, 51 39 .hljs-symbol { 52 - @apply text-amber-700 dark:text-amber-400; 40 + @apply text-amber-700 dark:text-amber-300; 53 41 } 54 42 .hljs-comment, 55 43 .hljs-code, 56 - .hljs-formula, 57 - pre code .line::before { 58 - @apply text-neutral-500 dark:text-neutral-500; 44 + .hljs-formula { 45 + @apply text-neutral-500 dark:text-neutral-400; 59 46 } 60 47 .hljs-name, 61 48 .hljs-quote,
+5 -57
src/styles/index.css
··· 22 22 } 23 23 } 24 24 25 - .btn { 26 - @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; 27 - } 28 - 29 - .btn::before { 30 - @apply absolute inset-0 -z-10 rounded border border-gray-200 bg-white duration-150 transition-all ease-in; 31 - --tw-shadow: 32 - inset 0 -2px 0 0 rgba(0, 0, 0, 0.1), 0 1px 0 0 rgba(0, 0, 0, 0.04); 33 - --tw-shadow-colored: 34 - inset 0 -2px 0 0 var(--tw-shadow-color), 0 1px 0 0 var(--tw-shadow-color); 35 - box-shadow: 36 - var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), 37 - var(--tw-shadow); 38 - content: ""; 39 - } 40 - 41 - .btn:hover::before { 42 - @apply bg-gray-50; 43 - --tw-shadow: 44 - inset 0 -2px 0 0 rgba(0, 0, 0, 0.15), 0 2px 1px 0 rgba(0, 0, 0, 0.06); 45 - --tw-shadow-colored: 46 - inset 0 -2px 0 0 var(--tw-shadow-color), 0 2px 1px 0 var(--tw-shadow-color); 47 - box-shadow: 48 - var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), 49 - var(--tw-shadow); 50 - content: ""; 51 - } 52 - 53 - .btn:focus { 54 - @apply outline-2 outline-offset-2; 55 - } 56 - 57 - .btn:focus-visible::before { 58 - @apply outline-solid outline-2 outline-gray-400; 59 - } 60 - 61 - .btn:active::before { 62 - --tw-shadow: inset 0 2px 2px 0 rgba(0, 0, 0, 0.1); 63 - --tw-shadow-colored: inset 0 2px 2px 0 var(--tw-shadow-color); 64 - box-shadow: 65 - var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), 66 - var(--tw-shadow); 67 - content: ""; 68 - } 69 - 70 - .btn:disabled { 71 - @apply cursor-not-allowed opacity-50; 72 - } 73 - 74 - @media (prefers-color-scheme: dark) { 25 + @layer components { 26 + /* https://tangled.org/@tangled.org/core/blob/master/input.css */ 75 27 .btn { 76 - @apply text-gray-100; 28 + @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; 77 29 } 78 30 79 - .btn::before { 80 - @apply border-gray-700 bg-gray-800; 81 - } 82 - 83 - .btn:hover::before { 84 - @apply bg-gray-700; 31 + .btn-create { 32 + @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; 85 33 } 86 34 }