mirror of
https://github.com/SrIzan10/spongebin.git
synced 2026-05-01 11:05:09 +00:00
feat: initial commit
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
4
.vscode/extensions.json
vendored
Normal file
4
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# sponge bin
|
||||
|
||||
a pastebin for code snippets; rendered with shiki
|
||||
9
astro.config.mjs
Normal file
9
astro.config.mjs
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import tailwind from "@astrojs/tailwind";
|
||||
import preact from "@astrojs/preact";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [tailwind(), preact()],
|
||||
output: 'server'
|
||||
});
|
||||
26
package.json
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "pastebin",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.2",
|
||||
"@astrojs/preact": "^3.5.1",
|
||||
"@astrojs/tailwind": "^5.1.0",
|
||||
"@nanostores/preact": "^0.5.2",
|
||||
"astro": "^4.14.2",
|
||||
"nanostores": "^0.11.2",
|
||||
"preact": "^10.23.2",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"typescript": "^5.5.4"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro check && astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"prettier-plugin-astro": "^0.14.1"
|
||||
}
|
||||
}
|
||||
120
public/favicon.svg
Normal file
120
public/favicon.svg
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img"
|
||||
class="iconify iconify--twemoji" preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<path fill="#F4900C"
|
||||
d="M35.676 18.625c.057.794.011 5.907.006 6.093a6.821 6.821 0 0 1-.026.422c-.618 6.445-5.937 10.342-12.187 9.375c-6.062-.938-8.938-5.5-11.938-8.5S1.656 22.827.641 19.312a7.065 7.065 0 0 1-.294-2.267c.011-.256-.141-5.031.035-6.169c.338-2.189 35.008 3.795 35.294 7.749z">
|
||||
|
||||
</path>
|
||||
|
||||
<path fill="#FFCC4D"
|
||||
d="M4.718 4.856c3.5-2.125 8.964-2.286 12.5 1.25c3 3 4.243 3.843 9.243 4.843s9.632 3.532 9.194 8.094c-.618 6.445-5.937 10.342-12.187 9.375c-6.062-.938-8.938-5.5-11.938-8.5S1.655 16.73.64 13.215c-1.109-3.842.996-6.487 4.078-8.359z">
|
||||
|
||||
</path>
|
||||
|
||||
<g fill="#FFAC33">
|
||||
|
||||
<circle cx="12.513" cy="14.106" r="1.5">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="29.013" cy="25.794" r="1">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="17.013" cy="24.106" r="1">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="24.513" cy="24.794" r="1.5">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="31.825" cy="21.606" r="1.5">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="29.013" cy="18.419" r="1">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="25.013" cy="20.356" r="1">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="19.513" cy="21.356" r="1">
|
||||
|
||||
</circle>
|
||||
|
||||
<path
|
||||
d="M14.013 20.606a1.5 1.5 0 0 0-1.5-1.5c-.496 0-.933.244-1.206.616c.074.067.155.127.224.197c.598.598 1.192 1.258 1.806 1.939c.406-.268.676-.728.676-1.252z">
|
||||
|
||||
</path>
|
||||
|
||||
<circle cx="9.575" cy="16.419" r="1">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="16.013" cy="17.419" r="1">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="24.513" cy="13.606" r="1.5">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="21.013" cy="17.419" r="1.5">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="12.013" cy="10.106" r="1">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="20.513" cy="11.106" r="1">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="14.513" cy="7.856" r="1">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="7.575" cy="12.606" r="1">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="6.013" cy="6.856" r="1">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="28.575" cy="14.106" r="1">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="33.325" cy="17.419" r="1">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="3.263" cy="10.606" r="1.5">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="10.575" cy="5.856" r="1.5">
|
||||
|
||||
</circle>
|
||||
|
||||
<circle cx="16.513" cy="11.106" r="1.5">
|
||||
|
||||
</circle>
|
||||
|
||||
<path
|
||||
d="M19.513 26.106c-.408 0-.778.164-1.048.429c.77.486 1.61.908 2.531 1.243c.007-.057.017-.113.017-.172a1.5 1.5 0 0 0-1.5-1.5zm-14.75-10.5a.996.996 0 0 0-1.994-.029c.491.288 1.034.549 1.615.795a.987.987 0 0 0 .379-.766z">
|
||||
|
||||
</path>
|
||||
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
BIN
public/sponge.png
Normal file
BIN
public/sponge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
47
src/components/CodeBlock.tsx
Normal file
47
src/components/CodeBlock.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useStore } from "@nanostores/preact";
|
||||
import {
|
||||
$codeStore,
|
||||
$langStore,
|
||||
$themeStore,
|
||||
highlighter,
|
||||
} from "../utils/theme";
|
||||
|
||||
export default function CodeBlock() {
|
||||
const lang = useStore($langStore);
|
||||
const theme = useStore($themeStore);
|
||||
const code = useStore($codeStore);
|
||||
|
||||
const handleCodeChange = (event: Event) => {
|
||||
const newCode = (event.target as HTMLTextAreaElement).value;
|
||||
$codeStore.set(newCode);
|
||||
};
|
||||
|
||||
const __html = highlighter.codeToHtml($codeStore.get(), {
|
||||
lang,
|
||||
theme,
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="relative">
|
||||
<textarea
|
||||
class="ml-4 w-full p-2 z-0 rounded absolute top-0 left-0 bg-transparent caret-transparent text-transparent resize-none"
|
||||
value={code}
|
||||
onInput={handleCodeChange}
|
||||
rows={10}
|
||||
spellCheck={false}
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
class="ml-4 shiki pointer-events-none"
|
||||
dangerouslySetInnerHTML={{ __html }}
|
||||
style={{
|
||||
whiteSpace: "pre-wrap",
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
src/components/Editor.tsx
Normal file
32
src/components/Editor.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import LanguageSelect from "./LanguageSelect";
|
||||
import SaveButton from "./SaveButton";
|
||||
import { $themeColorsStore } from "../utils/theme";
|
||||
import ThemeSelect from "./ThemeSelect";
|
||||
import { useStore } from "@nanostores/preact";
|
||||
import CodeBlock from "./CodeBlock";
|
||||
|
||||
export default function Editor() {
|
||||
const colors = useStore($themeColorsStore);
|
||||
|
||||
return (
|
||||
<div style={{ backgroundColor: colors.background }} class="h-screen">
|
||||
<div class="mb-4 p-2" style={{ backgroundColor: colors.navbar }}>
|
||||
<nav
|
||||
class="flex justify-between mx-4"
|
||||
style={{ color: colors.primary }}
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<SaveButton />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<LanguageSelect />
|
||||
<ThemeSelect />
|
||||
<a href="github.com/durocodes/spongebin">[source]</a>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<CodeBlock />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
src/components/LanguageSelect.tsx
Normal file
13
src/components/LanguageSelect.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { bundledLanguages } from "shiki";
|
||||
import MenuButton from "./MenuButton";
|
||||
import { $langStore } from "../utils/theme";
|
||||
|
||||
export default function LanguageSelect() {
|
||||
return (
|
||||
<MenuButton
|
||||
label="language"
|
||||
ids={Object.keys(bundledLanguages)}
|
||||
$store={$langStore}
|
||||
/>
|
||||
);
|
||||
}
|
||||
127
src/components/MenuButton.tsx
Normal file
127
src/components/MenuButton.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import type { PreinitializedWritableAtom } from "nanostores";
|
||||
import { $themeColorsStore } from "../utils/theme";
|
||||
import { useStore } from "@nanostores/preact";
|
||||
|
||||
export interface MenuButtonProps {
|
||||
label: string;
|
||||
ids: string[];
|
||||
minWidth?: string;
|
||||
$store: PreinitializedWritableAtom<string>;
|
||||
}
|
||||
|
||||
export default function MenuButton({
|
||||
label,
|
||||
ids,
|
||||
minWidth,
|
||||
$store,
|
||||
}: MenuButtonProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const colors = useStore($themeColorsStore);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = () => setOpen(false);
|
||||
window.addEventListener("click", listener);
|
||||
return () => window.removeEventListener("click", listener);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
function toggleOpen(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
setOpen(!open);
|
||||
}
|
||||
|
||||
function select(e: MouseEvent | KeyboardEvent, id: string) {
|
||||
e.stopPropagation();
|
||||
setOpen(false);
|
||||
$store.set(id);
|
||||
}
|
||||
|
||||
function handleInputClick(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (filteredIds.length > 0) {
|
||||
select(e, filteredIds[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filteredIds = ids.filter((id) =>
|
||||
id.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
|
||||
return (
|
||||
<div class="relative inline-block">
|
||||
<button
|
||||
class="rounded focus:outline-none"
|
||||
style={{ color: colors.primary }}
|
||||
onClick={toggleOpen}
|
||||
>
|
||||
<span class="hidden md:inline">
|
||||
[{label}: {$store.get()}]
|
||||
</span>
|
||||
<span class="md:hidden">[{$store.get()}]</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div
|
||||
class="absolute right-0 z-10 mt-4 rounded-xl max-h-60 overflow-auto w-full"
|
||||
style={{
|
||||
backgroundColor: colors.navbar,
|
||||
minWidth: minWidth ?? "13rem",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full px-4 py-2 border-b focus:outline-none"
|
||||
style={{
|
||||
backgroundColor: colors.navbar,
|
||||
borderColor: `${colors.primary}aa`,
|
||||
color: colors.primary,
|
||||
}}
|
||||
placeholder="type to search..."
|
||||
value={searchTerm}
|
||||
onInput={(e) => setSearchTerm((e.target as HTMLInputElement).value)}
|
||||
onClick={handleInputClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<ul>
|
||||
{filteredIds.length > 0 ? (
|
||||
filteredIds.map((id) => (
|
||||
<li
|
||||
key={id}
|
||||
class={`px-4 py-2 cursor-pointer ${id === $store.get() ? "font-bold" : ""}`}
|
||||
style={{
|
||||
backgroundColor:
|
||||
id === $store.get()
|
||||
? `${colors.background}77`
|
||||
: `${colors.navbar}`,
|
||||
}}
|
||||
onClick={(e) => select(e, id)}
|
||||
>
|
||||
{id === $store.get() ? `*${id}` : id}
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
<li class="px-4 py-2" style={{ color: colors.primary }}>
|
||||
No results
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
50
src/components/SaveButton.tsx
Normal file
50
src/components/SaveButton.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useStore } from "@nanostores/preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { unwrapOr } from "../utils/result";
|
||||
import { encode } from "../utils/encode";
|
||||
import { $codeStore, $langStore, $themeStore } from "../utils/theme";
|
||||
|
||||
export default function SaveButton() {
|
||||
const code = useStore($codeStore);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [buttonText, setButtonText] = useState("[save]");
|
||||
|
||||
const save = async () => {
|
||||
setSaving(true);
|
||||
|
||||
console.log({ code });
|
||||
|
||||
const result = await unwrapOr(encode(code), "");
|
||||
const url = `${window.location.origin}/?l=${$langStore.get()}&t=${$themeStore.get()}&c=${result}`;
|
||||
|
||||
navigator.clipboard.writeText(url);
|
||||
|
||||
setButtonText("[link copied!]");
|
||||
|
||||
setTimeout(() => {
|
||||
setButtonText("[save]");
|
||||
setSaving(false);
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.ctrlKey && event.key === "s") {
|
||||
event.preventDefault();
|
||||
save();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [code, saving]);
|
||||
|
||||
return (
|
||||
<button class="rounded focus:outline-none" disabled={saving} onClick={save}>
|
||||
{buttonText}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
13
src/components/ThemeSelect.tsx
Normal file
13
src/components/ThemeSelect.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { bundledThemes } from "shiki";
|
||||
import MenuButton from "./MenuButton";
|
||||
import { $themeStore } from "../utils/theme";
|
||||
|
||||
export default function ThemeSelect() {
|
||||
return (
|
||||
<MenuButton
|
||||
label="theme"
|
||||
ids={Object.keys(bundledThemes)}
|
||||
$store={$themeStore}
|
||||
/>
|
||||
);
|
||||
}
|
||||
1
src/env.d.ts
vendored
Normal file
1
src/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
48
src/pages/index.astro
Normal file
48
src/pages/index.astro
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
import "../styles/global.css";
|
||||
import { type BundledTheme } from "shiki";
|
||||
import Editor from "../components/Editor";
|
||||
import { $codeStore, $langStore, $themeStore, parseLang } from "../utils/theme";
|
||||
import { decode } from "../utils/encode";
|
||||
import { unwrapOr } from "../utils/result";
|
||||
|
||||
const langParam = Astro.url.searchParams.get("l") ?? "txt";
|
||||
const themeParam = Astro.url.searchParams.get("t") ?? "github-dark-default";
|
||||
const codeParam =
|
||||
Astro.url.searchParams.get("c") ??
|
||||
"H4sIAAAAAAAAE_PLV3DOT0lVCCjKL8tMSU0BAGFoYswQAAAA";
|
||||
|
||||
const code = await unwrapOr(
|
||||
decode(codeParam),
|
||||
"H4sIAAAAAAAAE_PLV3DOT0lVCCjKL8tMSU0BAGFoYswQAAAA",
|
||||
);
|
||||
|
||||
$codeStore.set(code);
|
||||
$langStore.set(parseLang(langParam));
|
||||
$themeStore.set(themeParam as BundledTheme);
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<title>sponge bin</title>
|
||||
<meta name="theme-color" content="#FFCC4D" />
|
||||
<meta name="description" content="a pastebin made of sponge" />
|
||||
|
||||
<meta property="og:title" content="sponge bin" />
|
||||
<meta property="og:description" content="a pastebin made of sponge" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content={new URL("/images/sponge.png", Astro.url)}
|
||||
/>
|
||||
<meta property="og:url" content={Astro.url} />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Editor client:load />
|
||||
</body>
|
||||
</html>
|
||||
19
src/styles/global.css
Normal file
19
src/styles/global.css
Normal file
@@ -0,0 +1,19 @@
|
||||
* {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
"Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
code {
|
||||
counter-reset: step;
|
||||
counter-increment: step 0;
|
||||
}
|
||||
|
||||
code .line::before {
|
||||
content: counter(step);
|
||||
counter-increment: step;
|
||||
width: 1rem;
|
||||
margin-right: 1.5rem;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
color: rgba(115, 138, 148, 0.4);
|
||||
}
|
||||
35
src/utils/encode.ts
Normal file
35
src/utils/encode.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { tryCatch } from "./result";
|
||||
|
||||
const processStream = async (
|
||||
str: string,
|
||||
format: CompressionFormat,
|
||||
StreamConstructor: typeof CompressionStream | typeof DecompressionStream,
|
||||
encode = true,
|
||||
) => {
|
||||
const input = encode
|
||||
? new TextEncoder().encode(str)
|
||||
: Uint8Array.from(atob(str.replace(/-/g, "+").replace(/_/g, "/")), (c) =>
|
||||
c.charCodeAt(0),
|
||||
);
|
||||
|
||||
const stream = new StreamConstructor(format);
|
||||
const writer = stream.writable.getWriter();
|
||||
|
||||
writer.write(input);
|
||||
writer.close();
|
||||
|
||||
const buffer = await new Response(stream.readable).arrayBuffer();
|
||||
const base64 = encode
|
||||
? btoa(String.fromCharCode(...new Uint8Array(buffer)))
|
||||
: new TextDecoder().decode(buffer);
|
||||
|
||||
return encode
|
||||
? base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
|
||||
: base64;
|
||||
};
|
||||
|
||||
export const encode = (str: string, format: CompressionFormat = "gzip") =>
|
||||
tryCatch(() => processStream(str, format, CompressionStream, true));
|
||||
|
||||
export const decode = (str: string, format: CompressionFormat = "gzip") =>
|
||||
tryCatch(() => processStream(str, format, DecompressionStream, false));
|
||||
16
src/utils/result.ts
Normal file
16
src/utils/result.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export type Result<Ok, Err> =
|
||||
| { ok: true; value: Ok }
|
||||
| { ok: false; error: Err };
|
||||
export const Ok = <Ok>(value: Ok) => ({ ok: true, value } as const);
|
||||
export const Err = <Err>(error: Err) => ({ ok: false, error } as const);
|
||||
|
||||
export const tryCatch = <T>(fn: () => T): Result<T, unknown> => {
|
||||
try {
|
||||
return Ok(fn());
|
||||
} catch (error) {
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const unwrapOr = <Ok, Err, T>(result: Result<Ok, Err>, fallback: T) =>
|
||||
result.ok ? result.value : fallback;
|
||||
53
src/utils/theme.ts
Normal file
53
src/utils/theme.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { atom, computed, task } from "nanostores";
|
||||
import {
|
||||
bundledLanguages,
|
||||
type BundledLanguage,
|
||||
type BundledTheme,
|
||||
createHighlighter,
|
||||
bundledThemes,
|
||||
} from "shiki";
|
||||
|
||||
export const highlighter = await createHighlighter({
|
||||
themes: Object.keys(bundledThemes),
|
||||
langs: Object.keys(bundledLanguages),
|
||||
});
|
||||
|
||||
export const $langStore = atom<BundledLanguage | "txt">("txt");
|
||||
export const $codeStore = atom<string>("");
|
||||
export const $themeStore = atom<BundledTheme>("github-dark-default");
|
||||
export const $themeColorsStore = computed($themeStore, (theme) =>
|
||||
themeColors(theme),
|
||||
);
|
||||
|
||||
export const parseLang = (lang: string | null = "txt") =>
|
||||
Object.keys(bundledLanguages).includes(lang ?? "txt")
|
||||
? (lang as BundledLanguage)
|
||||
: "txt";
|
||||
|
||||
const navbarColor = (backgroundColor: string): string => {
|
||||
const expandHex = (hex: string) =>
|
||||
hex.length === 4 ? `#${[...hex.slice(1)].map((c) => c + c).join("")}` : hex;
|
||||
|
||||
const isLight = (hex: string) => parseInt(hex.slice(1), 16) > 0xffffff / 2;
|
||||
|
||||
const adjustColor = (hex: string, amt: number) => {
|
||||
const hexCode = (parseInt(hex.slice(1), 16) + amt * 0x010101)
|
||||
.toString(16)
|
||||
.padStart(6, "0");
|
||||
|
||||
return `#${hexCode}`;
|
||||
};
|
||||
|
||||
const expandedColor = expandHex(backgroundColor);
|
||||
return adjustColor(expandedColor, isLight(expandedColor) ? -10 : 10);
|
||||
};
|
||||
|
||||
export const themeColors = (theme: BundledTheme) => {
|
||||
const shikiTheme = highlighter.getTheme(theme);
|
||||
|
||||
return {
|
||||
background: shikiTheme.bg,
|
||||
primary: shikiTheme.fg,
|
||||
navbar: navbarColor(shikiTheme.bg),
|
||||
};
|
||||
};
|
||||
8
tailwind.config.mjs
Normal file
8
tailwind.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "preact"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user