+17
-3
LICENSE
+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
+2
-1
biome.json
+41
-40
package.json
+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
shell.nix
-31
src/elements/code_block.tsx
-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
+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
-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
+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
-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
+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
src/routes/repo/commit/commit.data.ts
src/routes/repo/commit/data.ts
-376
src/routes/repo/commit/commit.tsx
-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
+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
+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
+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
+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
+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
}