update
This commit is contained in:
parent
ddb6f41eec
commit
b8107c8f04
|
|
@ -173,3 +173,4 @@ dist
|
||||||
|
|
||||||
# Finder (MacOS) folder config
|
# Finder (MacOS) folder config
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
target
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
preload = ["./src/myPlugin.ts"]
|
||||||
10
package.json
10
package.json
|
|
@ -4,17 +4,21 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "bun run src/index.tsx",
|
"start": "bun run src/index.tsx",
|
||||||
"dev": "bun run --hot src/index.tsx"
|
"dev": "bun run --watch --hot src/index.tsx"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"csstype": "^3.1.3"
|
"csstype": "^3.1.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.7.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.29.1",
|
"lightningcss": "^1.29.1",
|
||||||
"serve-static-bun": "^0.5.3"
|
"pokedex-promise-v2": "^4.2.1",
|
||||||
|
"serve-static-bun": "^0.5.3",
|
||||||
|
"tree-sitter-cli": "^0.25.2",
|
||||||
|
"tree-sitter-typescript": "^0.23.2",
|
||||||
|
"web-tree-sitter": "^0.25.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { createState } from "~/lib/state";
|
||||||
|
|
||||||
|
export default createState(
|
||||||
|
{ value: 0 },
|
||||||
|
{
|
||||||
|
add: (ctx) => ({
|
||||||
|
onClick: () => {
|
||||||
|
const { value } = ctx.get();
|
||||||
|
ctx.set({ value: value + 1 });
|
||||||
|
ctx.$counter.innerText = String(value + 1);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
counter: (_ctx) => ({}),
|
||||||
|
sub: (ctx) => ({
|
||||||
|
onClick: () => {
|
||||||
|
const { value } = ctx.get();
|
||||||
|
ctx.set({ value: value - 1 });
|
||||||
|
ctx.$counter.innerText = String(value - 1);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { useState } from "~/lib/hooks";
|
||||||
|
import counterState from "./Counter.state";
|
||||||
|
|
||||||
|
export const Counter = () => {
|
||||||
|
const { sub, counter, add } = useState(counterState);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button {...sub}>-</button>
|
||||||
|
<span {...counter}>0</span>
|
||||||
|
<button {...add}>+</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import type { Styles } from "~/types";
|
||||||
|
|
||||||
|
interface HeadingProps {
|
||||||
|
children: JSX.Children;
|
||||||
|
as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "div" | "span";
|
||||||
|
size?:
|
||||||
|
| "xs"
|
||||||
|
| "sm"
|
||||||
|
| "base"
|
||||||
|
| "lg"
|
||||||
|
| "xl"
|
||||||
|
| "2xl"
|
||||||
|
| "3xl"
|
||||||
|
| "4xl"
|
||||||
|
| "5xl"
|
||||||
|
| "6xl"
|
||||||
|
| "7xl"
|
||||||
|
| "8xl"
|
||||||
|
| "9xl";
|
||||||
|
style?: Styles;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Heading = ({
|
||||||
|
children,
|
||||||
|
as = "span",
|
||||||
|
size = "base",
|
||||||
|
style,
|
||||||
|
}: HeadingProps) => {
|
||||||
|
const Tag = as;
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
style={{
|
||||||
|
fontSize: "$" + size,
|
||||||
|
fontWeight: "$extrabold",
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Heading;
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import Global from "~/lib/Global";
|
import Global from "~/lib/Global";
|
||||||
import Navigation from "./Navigation";
|
import Navigation from "./Navigation";
|
||||||
|
import Heading from "./Heading";
|
||||||
|
import { Link } from "./Link";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: JSX.Element | JSX.Element[];
|
children: JSX.Element | JSX.Element[];
|
||||||
|
|
@ -7,29 +9,72 @@ interface LayoutProps {
|
||||||
|
|
||||||
export default function Layout({ children }: LayoutProps) {
|
export default function Layout({ children }: LayoutProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
marginX: "auto",
|
|
||||||
paddingX: "$4",
|
|
||||||
marginTop: "$2",
|
|
||||||
maxWidth: "1100px",
|
|
||||||
gap: "$4",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Global
|
<Global
|
||||||
css={{
|
css={{
|
||||||
body: {
|
body: {
|
||||||
backgroundColor: "$gray.950",
|
backgroundColor: "$gray.900",
|
||||||
color: "$gray.50",
|
color: "$gray.50",
|
||||||
fontFamily: "$sans",
|
fontFamily: "$sans",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<header>Header</header>
|
<div>
|
||||||
<Navigation />
|
<header
|
||||||
{children}
|
style={{
|
||||||
|
pY: "$4",
|
||||||
|
pX: "$6",
|
||||||
|
gap: "$2",
|
||||||
|
display: "grid",
|
||||||
|
maxWidth: "1280px",
|
||||||
|
gridTemplateColumns: "auto 1fr auto",
|
||||||
|
mX: "auto",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Link>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xml:space="preserve"
|
||||||
|
fillRule="evenodd"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeMiterlimit="2"
|
||||||
|
clipRule="evenodd"
|
||||||
|
viewbox="0 0 3307 593"
|
||||||
|
style={{
|
||||||
|
height: "$7",
|
||||||
|
width: "auto",
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
style={{
|
||||||
|
color: "$white",
|
||||||
|
}}
|
||||||
|
fill="currentColor"
|
||||||
|
fill-rule="nonzero"
|
||||||
|
d="M1053.02 205.51c35.59 0 64.27 10.1 84.98 30.81 20.72 21.25 31.34 52.05 31.34 93.48v162.53h-66.4V338.3c0-24.96-5.3-43.55-16.46-56.3-11.15-12.22-26.55-18.6-47.27-18.6-22.3 0-40.37 7.45-53.65 21.79-13.27 14.87-20.18 36.11-20.18 63.2v143.94h-66.4V338.3c0-24.96-5.3-43.55-16.46-56.3-11.15-12.22-26.56-18.6-47.27-18.6-22.84 0-40.37 7.45-53.65 21.79-13.27 14.34-20.18 35.58-20.18 63.2v143.94h-66.4V208.7h63.21v36.12c10.63-12.75 23.9-22.3 39.84-29.21 15.93-6.9 33.46-10.1 53.11-10.1 21.25 0 40.37 3.72 56.84 11.69 16.46 8.5 29.21 20.18 38.77 35.59 11.69-14.88 26.56-26.56 45.15-35.06 18.59-7.97 38.77-12.22 61.08-12.22Zm329.84 290.54c-28.68 0-54.7-6.37-77.54-18.59a133.19 133.19 0 0 1-53.65-52.05c-13.28-21.78-19.65-46.74-19.65-74.9 0-28.14 6.37-53.1 19.65-74.88a135.4 135.4 0 0 1 53.65-51.53c22.84-12.21 48.86-18.59 77.54-18.59 29.22 0 55.24 6.38 78.08 18.6 22.84 12.21 40.9 29.74 54.18 51.52 12.75 21.77 19.12 46.74 19.12 74.89s-6.37 53.11-19.12 74.89c-13.28 22.3-31.34 39.83-54.18 52.05-22.84 12.22-48.86 18.6-78.08 18.6Zm0-56.83c24.44 0 44.62-7.97 60.55-24.43 15.94-16.47 23.9-37.72 23.9-64.27 0-26.56-7.96-47.8-23.9-64.27-15.93-16.47-36.11-24.43-60.55-24.43-24.43 0-44.61 7.96-60.02 24.43-15.93 16.46-23.9 37.71-23.9 64.27 0 26.55 7.97 47.8 23.9 64.27 15.4 16.46 35.6 24.43 60.02 24.43Zm491.32-341v394.11h-63.74v-36.65a108.02 108.02 0 0 1-40.37 30.28c-16.46 6.9-34 10.1-53.65 10.1-27.08 0-51.52-5.85-73.3-18.07-21.77-12.21-39.3-29.21-51.52-51.52-12.21-21.78-18.59-47.27-18.59-75.95s6.38-54.18 18.6-75.96c12.21-21.77 29.74-38.77 51.52-50.99 21.77-12.21 46.2-18.06 73.3-18.06 18.59 0 36.11 3.2 51.52 9.56a106.35 106.35 0 0 1 39.83 28.69V98.22h66.4Zm-149.79 341c15.94 0 30.28-3.72 43.03-11.16 12.74-6.9 22.83-17.52 30.27-30.8 7.44-13.28 11.15-29.21 11.15-46.74s-3.71-33.46-11.15-46.74c-7.44-13.28-17.53-23.9-30.27-31.34-12.75-6.9-27.1-10.62-43.03-10.62s-30.27 3.71-43.02 10.62c-12.75 7.43-22.84 18.06-30.28 31.34-7.43 13.28-11.15 29.2-11.15 46.74 0 17.53 3.72 33.46 11.15 46.74 7.44 13.28 17.53 23.9 30.28 30.8 12.75 7.44 27.09 11.16 43.02 11.16Zm298.51-189.09c19.12-29.74 52.58-44.62 100.92-44.62v63.21a84.29 84.29 0 0 0-15.4-1.6c-26.03 0-46.22 7.44-60.56 22.32-14.34 15.4-21.78 37.18-21.78 65.33v137.56h-66.39V208.7h63.2v41.43Zm155.63-41.43h66.39v283.63h-66.4V208.7Zm33.46-46.74c-12.22 0-22.31-3.72-30.28-11.68a37.36 37.36 0 0 1-12.21-28.16c0-11.15 4.25-20.71 12.21-28.68 7.97-7.43 18.06-11.15 30.28-11.15 12.21 0 22.3 3.72 30.27 10.62 7.97 7.44 12.22 16.47 12.22 27.62 0 11.69-3.72 21.25-11.69 29.21-7.96 7.97-18.59 12.22-30.8 12.22Zm279.38 43.55c35.59 0 64.27 10.63 86.05 31.34 21.78 20.72 32.4 52.05 32.4 92.95v162.53h-66.4V338.3c0-24.96-5.84-43.55-17.52-56.3-11.69-12.22-28.15-18.6-49.93-18.6-24.43 0-43.55 7.45-57.9 21.79-14.34 14.87-21.24 36.11-21.24 63.73v143.41h-66.4V208.7h63.21v36.65c11.16-13.28 24.97-22.84 41.43-29.74 16.47-6.9 35.59-10.1 56.3-10.1Zm371.81 271.42a78.34 78.34 0 0 1-28.15 14.34 130.83 130.83 0 0 1-35.6 4.78c-31.33 0-55.23-7.97-72.23-24.43-17-16.47-25.5-39.84-25.5-71.17V263.94h-46.73v-53.11h46.74v-64.8h66.4v64.8h75.95v53.11h-75.96v134.91c0 13.81 3.19 24.43 10.1 31.34 6.9 7.44 16.46 11.15 29.2 11.15 14.88 0 27.1-3.71 37.19-11.68l18.59 47.27Zm214.05-271.42c35.59 0 64.27 10.63 86.05 31.34 21.77 20.72 32.4 52.05 32.4 92.95v162.53h-66.4V338.3c0-24.96-5.84-43.55-17.53-56.3-11.68-12.22-28.15-18.6-49.92-18.6-24.44 0-43.56 7.45-57.9 21.79-14.34 14.87-21.24 36.11-21.24 63.73v143.41h-66.4V98.23h66.4v143.4c11.15-11.68 24.43-20.71 40.9-27.09 15.93-5.84 33.99-9.03 53.64-9.03Z"
|
||||||
|
></path>
|
||||||
|
<g
|
||||||
|
fill="currentColor"
|
||||||
|
style={{
|
||||||
|
color: "$green.500",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<path d="m29 424.4 188.2-112.95-17.15-45.48 53.75-55.21 67.93-14.64 19.67 24.21-31.32 31.72-27.3 8.6-19.52 20.05 9.56 26.6 19.4 20.6 27.36-7.28 19.47-21.38 42.51-13.47 12.67 28.5-43.87 53.78-73.5 23.27-32.97-36.7L55.06 467.94C46.1 456.41 35.67 440.08 29 424.4Zm543.03-230.25-149.5 40.32c8.24 21.92 10.95 34.8 13.23 49l149.23-40.26c-2.38-15.94-6.65-32.17-12.96-49.06Z"></path>
|
||||||
|
<path d="M51.28 316.13c10.59 125 115.54 223.3 243.27 223.3 96.51 0 180.02-56.12 219.63-137.46l48.61 16.83c-46.78 101.34-149.35 171.75-268.24 171.75C138.6 590.55 10.71 469.38 0 316.13h51.28ZM.78 265.24C15.86 116.36 141.73 0 294.56 0c162.97 0 295.28 132.31 295.28 295.28 0 26.14-3.4 51.49-9.8 75.63l-48.48-16.78a244.28 244.28 0 0 0 7.15-58.85c0-134.75-109.4-244.15-244.15-244.15-124.58 0-227.49 93.5-242.32 214.11H.8Z"></path>
|
||||||
|
<path d="M293.77 153.17c-78.49.07-142.2 63.83-142.2 142.34 0 78.56 63.79 142.34 142.35 142.34 3.98 0 7.93-.16 11.83-.49l14.22 49.76a194.65 194.65 0 0 1-26.05 1.74c-106.72 0-193.36-86.64-193.36-193.35 0-106.72 86.64-193.35 193.36-193.35 2.64 0 5.28.05 7.9.16l-8.05 50.85Zm58.2-42.13c78.39 24.67 135.3 97.98 135.3 184.47 0 80.07-48.77 148.83-118.2 178.18l-14.17-49.55c48.08-22.85 81.36-71.89 81.36-128.63 0-60.99-38.44-113.07-92.39-133.32l8.1-51.15Z"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
<Navigation />
|
||||||
|
<div></div>
|
||||||
|
</header>
|
||||||
|
<main>{children}</main>
|
||||||
|
<footer></footer>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
type Props = JSX.IntrinsicElements["a"];
|
||||||
|
|
||||||
|
export const Link = (props: Props) => {
|
||||||
|
return <a {...props} />;
|
||||||
|
};
|
||||||
|
|
@ -13,13 +13,21 @@ export default function Navigation() {
|
||||||
<nav
|
<nav
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: "1rem",
|
alignItems: "center",
|
||||||
|
margin: "auto",
|
||||||
|
gap: "$2",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<a
|
<a
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "$gray.700",
|
_hover: {
|
||||||
|
backgroundColor: "$gray.900",
|
||||||
|
},
|
||||||
|
fontWeight: "$semibold",
|
||||||
|
paddingY: "$2",
|
||||||
|
paddingX: "$4",
|
||||||
|
borderRadius: "$md",
|
||||||
}}
|
}}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
import { myPlugin } from "./myPlugin";
|
||||||
import { fillTemplate, render } from "./lib/fast-web";
|
import { fillTemplate, render } from "./lib/fast-web";
|
||||||
|
import { requestLog } from "./middlewares/request-log";
|
||||||
import template from "./template.html" with { type: "text" };
|
import template from "./template.html" with { type: "text" };
|
||||||
import theme from "./theme.css" with { type: "text" };
|
import theme from "./theme.css" with { type: "text" };
|
||||||
import staticDir from "serve-static-bun";
|
import staticDir from "serve-static-bun";
|
||||||
|
|
@ -10,11 +12,11 @@ const router = new Bun.FileSystemRouter({
|
||||||
|
|
||||||
const staticPublic = staticDir("./public");
|
const staticPublic = staticDir("./public");
|
||||||
|
|
||||||
const glob = new Bun.Glob(__dirname + "/scripts/*");
|
const scriptsGlob = new Bun.Glob(__dirname + "/scripts/*");
|
||||||
|
|
||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
port: 8080,
|
port: 8080,
|
||||||
fetch: async (req): Promise<Response> => {
|
fetch: requestLog(async (req): Promise<Response> => {
|
||||||
if (req.url.endsWith("ws")) {
|
if (req.url.endsWith("ws")) {
|
||||||
if (server.upgrade(req)) return new Response("ok");
|
if (server.upgrade(req)) return new Response("ok");
|
||||||
}
|
}
|
||||||
|
|
@ -23,30 +25,37 @@ const server = Bun.serve({
|
||||||
if (!match) return staticPublic(req);
|
if (!match) return staticPublic(req);
|
||||||
const Comp = (await import(__dirname + "/pages/" + match?.pathname))
|
const Comp = (await import(__dirname + "/pages/" + match?.pathname))
|
||||||
.default;
|
.default;
|
||||||
const renderResult = render(<Comp />, [theme]);
|
const renderResult = await render(<Comp />, [theme], new URL(req.url));
|
||||||
|
|
||||||
const systemScripts = await Bun.build({
|
const systemScripts = await Bun.build({
|
||||||
entrypoints: [...glob.scanSync()],
|
entrypoints: [
|
||||||
|
...scriptsGlob.scanSync(),
|
||||||
|
"./src/components/Counter.state.ts",
|
||||||
|
],
|
||||||
minify: true,
|
minify: true,
|
||||||
|
plugins: [myPlugin],
|
||||||
});
|
});
|
||||||
const scripts = await Promise.all(
|
const scripts = await Promise.all(
|
||||||
systemScripts.outputs.map(
|
systemScripts.outputs.map(
|
||||||
async (script) => `<script>${await script.text()}</script>`,
|
async (script) =>
|
||||||
|
`<script type="module">${await script.text()}</script>`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return new Response(
|
const responseData = fillTemplate(template, {
|
||||||
fillTemplate(template, {
|
head: renderResult.head + scripts.join(""),
|
||||||
head: renderResult.head + scripts,
|
|
||||||
body: renderResult.html,
|
body: renderResult.html,
|
||||||
}),
|
});
|
||||||
{
|
|
||||||
|
const gzipResponseData = Bun.gzipSync(responseData);
|
||||||
|
|
||||||
|
return new Response(gzipResponseData, {
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "text/html",
|
"content-type": "text/html",
|
||||||
|
"Content-Encoding": "gzip",
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
);
|
}),
|
||||||
},
|
|
||||||
websocket: {
|
websocket: {
|
||||||
message: () => {},
|
message: () => {},
|
||||||
open: async (ws) => {
|
open: async (ws) => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
namespace JSX {
|
namespace JSX {
|
||||||
interface FC {
|
interface FC {
|
||||||
(props: any): JSX.Element | string | null;
|
(
|
||||||
|
props: any,
|
||||||
|
):
|
||||||
|
| JSX.Element
|
||||||
|
| string
|
||||||
|
| null
|
||||||
|
| Promise<JSX.Element>
|
||||||
|
| Promise<string>
|
||||||
|
| Promsie<null>;
|
||||||
}
|
}
|
||||||
type Children =
|
type Children =
|
||||||
| JSX.Element
|
| JSX.Element
|
||||||
|
|
@ -8,20 +16,102 @@ namespace JSX {
|
||||||
| string
|
| string
|
||||||
| null
|
| null
|
||||||
| undefined;
|
| undefined;
|
||||||
type CSSTypeProperties = import("csstype").Properties;
|
|
||||||
type CSSProperties = import("csstype").Properties & {
|
|
||||||
marginX?: CSSTypeProperties["marginLeft"];
|
|
||||||
marginY?: CSSTypeProperties["marginTop"];
|
|
||||||
paddingX?: CSSTypeProperties["paddingLeft"];
|
|
||||||
paddingY?: CSSTypeProperties["paddingTop"];
|
|
||||||
};
|
|
||||||
type BaseElementPropsWithoutChildren = {
|
type BaseElementPropsWithoutChildren = {
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: CSSProperties;
|
id?: string;
|
||||||
|
tabindex?: number | string;
|
||||||
|
style?: import("./types").Styles;
|
||||||
|
[`data-${string}`]?: string | boolean;
|
||||||
} & Partial<ARIAMixin>;
|
} & Partial<ARIAMixin>;
|
||||||
type BaseElementProps = BaseElementPropsWithoutChildren & {
|
type BaseElementProps = BaseElementPropsWithoutChildren & {
|
||||||
children?: JSX.Children;
|
children?: JSX.Children;
|
||||||
};
|
};
|
||||||
|
type SvgProps = BaseElementProps & {
|
||||||
|
["xml:lang"]?: string;
|
||||||
|
["xml:space"]?: string;
|
||||||
|
xmlns?: string;
|
||||||
|
// XLink attributes
|
||||||
|
["xlink:hrefDeprecated"]?: string;
|
||||||
|
["xlink:type"]?: string;
|
||||||
|
["xlink:role"]?: string;
|
||||||
|
["xlink:arcrole"]?: string;
|
||||||
|
["xlink:title"]?: string;
|
||||||
|
["xlink:show"]?: string;
|
||||||
|
["xlink:actuate"]?: string;
|
||||||
|
// Presentation attributes
|
||||||
|
["alignment-baseline"]?: string;
|
||||||
|
["baseline-shift"]?: string;
|
||||||
|
["clip"]?: string;
|
||||||
|
["clipPath"]?: string;
|
||||||
|
["clipRule"]?: string;
|
||||||
|
["color"]?: string;
|
||||||
|
["colorInterpolation"]?: string;
|
||||||
|
["colorInterpolationFilters"]?: string;
|
||||||
|
["cursor"]?: string;
|
||||||
|
["cx"]?: string;
|
||||||
|
["cy"]?: string;
|
||||||
|
["d"]?: string;
|
||||||
|
["direction"]?: string;
|
||||||
|
["display"]?: string;
|
||||||
|
["dominantBaseline"]?: string;
|
||||||
|
["fill"]?: string;
|
||||||
|
["fillOpacity"]?: string;
|
||||||
|
["fillRule"]?: string;
|
||||||
|
["filter"]?: string;
|
||||||
|
["floodColor"]?: string;
|
||||||
|
["floodOpacity"]?: string;
|
||||||
|
["fontFamily"]?: string;
|
||||||
|
["fontSize"]?: string;
|
||||||
|
["fontSize-adjust"]?: string;
|
||||||
|
["fontStretch"]?: string;
|
||||||
|
["fontStyle"]?: string;
|
||||||
|
["fontVariant"]?: string;
|
||||||
|
["fontWeight"]?: string;
|
||||||
|
["glyphOrientation-horizontal"]?: string;
|
||||||
|
["glyphOrientation-vertical"]?: string;
|
||||||
|
["height"]?: string;
|
||||||
|
["imageRendering"]?: string;
|
||||||
|
["letterSpacing"]?: string;
|
||||||
|
["lightingColor"]?: string;
|
||||||
|
["markerEnd"]?: string;
|
||||||
|
["markerMid"]?: string;
|
||||||
|
["markerStart"]?: string;
|
||||||
|
["mask"]?: string;
|
||||||
|
["maskType"]?: string;
|
||||||
|
["opacity"]?: string;
|
||||||
|
["overflow"]?: string;
|
||||||
|
["pointerEvents"]?: string;
|
||||||
|
["r"]?: string;
|
||||||
|
["rx"]?: string;
|
||||||
|
["ry"]?: string;
|
||||||
|
["shapeRendering"]?: string;
|
||||||
|
["stopColor"]?: string;
|
||||||
|
["stopOpacity"]?: string;
|
||||||
|
["stroke"]?: string;
|
||||||
|
["strokeDasharray"]?: string;
|
||||||
|
["strokeDashoffset"]?: string;
|
||||||
|
["strokeLinecap"]?: string;
|
||||||
|
["strokeLinejoin"]?: string;
|
||||||
|
["strokeMiterlimit"]?: string;
|
||||||
|
["strokeOpacity"]?: string;
|
||||||
|
["strokeWidth"]?: string;
|
||||||
|
["textAnchor"]?: string;
|
||||||
|
["textDecoration"]?: string;
|
||||||
|
["textOverflow"]?: string;
|
||||||
|
["textRendering"]?: string;
|
||||||
|
["transform"]?: string;
|
||||||
|
["transformOrigin"]?: string;
|
||||||
|
["unicodeBidi"]?: string;
|
||||||
|
["vectorEffect"]?: string;
|
||||||
|
["visibility"]?: string;
|
||||||
|
["whiteSpace"]?: string;
|
||||||
|
["width"]?: string;
|
||||||
|
["wordSpacing"]?: string;
|
||||||
|
["writingMode"]?: string;
|
||||||
|
["x"]?: string;
|
||||||
|
["y"]?: string;
|
||||||
|
viewbox?: string;
|
||||||
|
};
|
||||||
interface IntrinsicElements {
|
interface IntrinsicElements {
|
||||||
a: BaseElementProps & {
|
a: BaseElementProps & {
|
||||||
href?: string;
|
href?: string;
|
||||||
|
|
@ -50,6 +140,9 @@ namespace JSX {
|
||||||
source: BaseElementPropsWithoutChildren;
|
source: BaseElementPropsWithoutChildren;
|
||||||
track: BaseElementPropsWithoutChildren;
|
track: BaseElementPropsWithoutChildren;
|
||||||
wbr: BaseElementPropsWithoutChildren;
|
wbr: BaseElementPropsWithoutChildren;
|
||||||
|
svg: SvgProps;
|
||||||
|
path: SvgProps;
|
||||||
|
g: SvgProps;
|
||||||
[tagName: string]: BaseElementProps;
|
[tagName: string]: BaseElementProps;
|
||||||
}
|
}
|
||||||
interface Element {
|
interface Element {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,15 @@
|
||||||
import { getStyleRules } from "~/system/style-rules";
|
import { renderStyle } from "~/system/style-rules";
|
||||||
import { pushStyle } from "./fast-web";
|
import { pushStyle } from "./fast-web";
|
||||||
|
import type { Styles } from "~/types";
|
||||||
|
|
||||||
interface GlobalProps {
|
interface GlobalProps {
|
||||||
css?: Record<string, JSX.CSSProperties>;
|
css?: Record<string, Styles>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Global({ css = {} }: GlobalProps) {
|
export default function Global({ css = {} }: GlobalProps) {
|
||||||
const stylesArray = Object.entries(css).map<[string, string[]]>(
|
Object.entries(css).forEach(([key, value]) => {
|
||||||
([key, value]) => {
|
const style = renderStyle(value, key);
|
||||||
return [key, getStyleRules(value)];
|
pushStyle(style.css);
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
stylesArray.forEach(([key, value]) => {
|
|
||||||
pushStyle(`${key} {${value.join(" ")}}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,23 @@
|
||||||
import { camelToKebab, renderStyle } from "~/system/style-rules";
|
import { camelToKebab, renderStyle } from "~/system/style-rules";
|
||||||
import { transform } from "lightningcss";
|
import { transform } from "lightningcss";
|
||||||
import reset from "./reset.css" with { type: "text" };
|
import reset from "./reset.css" with { type: "text" };
|
||||||
|
import { AsyncLocalStorage } from "async_hooks";
|
||||||
|
|
||||||
const renderChildren = (elements: JSX.Children): string => {
|
interface RenderContext {
|
||||||
|
styles: string[];
|
||||||
|
url: URL;
|
||||||
|
states: Record<string, { state: any; handlerId: string }>;
|
||||||
|
// Mapping handlerHash to file
|
||||||
|
handlers: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const asyncLocalStorage = new AsyncLocalStorage<RenderContext>();
|
||||||
|
|
||||||
|
const renderChildren = async (elements: JSX.Children): Promise<string> => {
|
||||||
if (Array.isArray(elements)) {
|
if (Array.isArray(elements)) {
|
||||||
return elements.map((element) => renderChildren(element)).join("");
|
return (
|
||||||
|
await Promise.all(elements.map((element) => renderChildren(element)))
|
||||||
|
).join("");
|
||||||
}
|
}
|
||||||
if (typeof elements === "string") {
|
if (typeof elements === "string") {
|
||||||
return elements;
|
return elements;
|
||||||
|
|
@ -12,15 +25,18 @@ const renderChildren = (elements: JSX.Children): string => {
|
||||||
return renderElement(elements);
|
return renderElement(elements);
|
||||||
};
|
};
|
||||||
|
|
||||||
let styles: string[] = [];
|
const getStyles = () => asyncLocalStorage.getStore()!.styles;
|
||||||
|
export const getUrl = () => asyncLocalStorage.getStore()!.url;
|
||||||
|
export const getHandlers = () => asyncLocalStorage.getStore()!.handlers;
|
||||||
|
export const getStates = () => asyncLocalStorage.getStore()!.states;
|
||||||
|
|
||||||
export const pushStyle = (style: string) => {
|
export const pushStyle = (...styles: string[]) => {
|
||||||
styles.push(style);
|
getStyles().push(...styles);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderElement = (
|
const renderElement = async (
|
||||||
element: JSX.Element | string | null | undefined,
|
element: JSX.Element | string | null | undefined,
|
||||||
): string => {
|
): Promise<string> => {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
@ -34,7 +50,7 @@ const renderElement = (
|
||||||
let { className, ...otherProps } = element.props;
|
let { className, ...otherProps } = element.props;
|
||||||
if ("style" in element.props) {
|
if ("style" in element.props) {
|
||||||
const res = renderStyle(element.props.style);
|
const res = renderStyle(element.props.style);
|
||||||
styles.push(res.css);
|
pushStyle(res.css);
|
||||||
className = className ? `${className} ${res.className}` : res.className;
|
className = className ? `${className} ${res.className}` : res.className;
|
||||||
}
|
}
|
||||||
const props = { ...otherProps, class: className };
|
const props = { ...otherProps, class: className };
|
||||||
|
|
@ -50,32 +66,54 @@ const renderElement = (
|
||||||
}
|
}
|
||||||
return ` ${camelToKebab(key)}="${value}"`;
|
return ` ${camelToKebab(key)}="${value}"`;
|
||||||
});
|
});
|
||||||
return `<${element.type}${attrs.join("")}>${element.children ? renderChildren(element.children) : ""}</${element.type}>`;
|
return `<${element.type}${attrs.join("")}>${element.children ? await renderChildren(element.children) : ""}</${element.type}>`;
|
||||||
}
|
}
|
||||||
if (typeof element.type === "function") {
|
if (typeof element.type === "function") {
|
||||||
return renderElement(
|
return renderElement(
|
||||||
element.type({ ...element.props, children: element.children }),
|
await element.type({ ...element.props, children: element.children }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const render = (
|
const render = async (
|
||||||
element: JSX.Element | string | null,
|
element: JSX.Element | string | null,
|
||||||
extraCss: string[] = [],
|
extraCss: string[] = [],
|
||||||
) => {
|
) => {
|
||||||
styles = [reset, ...extraCss];
|
pushStyle(reset, ...extraCss);
|
||||||
const html = renderElement(element);
|
const html = await renderElement(element);
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const css = transform({
|
const css = transform({
|
||||||
code: encoder.encode(styles.join("\n")),
|
code: encoder.encode(getStyles().join("\n")),
|
||||||
minify: true,
|
minify: true,
|
||||||
filename: "render-style.ts",
|
filename: "render-style.ts",
|
||||||
}).code;
|
}).code;
|
||||||
const head = `<style>${css.toString()}</style>`;
|
const head = `<style>${css.toString()}</style>
|
||||||
|
<script>
|
||||||
|
window.stateManager ??= {
|
||||||
|
init: false,
|
||||||
|
states: {},
|
||||||
|
stateRegistry: new Map()
|
||||||
|
}
|
||||||
|
window.stateManager.states = {...window.stateManager.states,...${JSON.stringify(getStates())}}
|
||||||
|
</script>
|
||||||
|
`;
|
||||||
return { html, head };
|
return { html, head };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const runRender = (
|
||||||
|
element: JSX.Element | string | null,
|
||||||
|
extraCss: string[] = [],
|
||||||
|
url: URL,
|
||||||
|
) => {
|
||||||
|
return asyncLocalStorage.run(
|
||||||
|
{ styles: [], url, states: {}, handlers: {} },
|
||||||
|
() => render(element, extraCss),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { runRender as render };
|
||||||
|
|
||||||
export const fillTemplate = (
|
export const fillTemplate = (
|
||||||
template: string,
|
template: string,
|
||||||
data: Record<string, string>,
|
data: Record<string, string>,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { crazyHash } from "~/system/crazyHash";
|
||||||
|
import { getHandlers, getStates, getUrl } from "./fast-web";
|
||||||
|
import type { CreateStateResult } from "./state";
|
||||||
|
|
||||||
|
export const useUrl = (): URL => {
|
||||||
|
return getUrl();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useParams = (): URLSearchParams => {
|
||||||
|
return getUrl().searchParams;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePath = (): string => {
|
||||||
|
return getUrl().pathname;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useState = <S, T extends string>(
|
||||||
|
state: CreateStateResult<S, T>,
|
||||||
|
) => {
|
||||||
|
const handlerId = state.meta.hash;
|
||||||
|
const handlers = getHandlers();
|
||||||
|
handlers[handlerId] = state.meta.path;
|
||||||
|
const stateId = crazyHash(Bun.randomUUIDv7());
|
||||||
|
const parts = Object.keys(state.handler);
|
||||||
|
getStates()[stateId] = {
|
||||||
|
handlerId,
|
||||||
|
state: state.initial,
|
||||||
|
};
|
||||||
|
return Object.fromEntries(
|
||||||
|
parts.map((part) => [
|
||||||
|
part,
|
||||||
|
{
|
||||||
|
"data-c": part + "-" + stateId,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
) as unknown as Record<T, Record<`data-${string}`, string>>;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import Pokedex from "pokedex-promise-v2";
|
||||||
|
|
||||||
|
export const pokeapi = new Pokedex();
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
type StateContext<S extends any, T extends string> = Record<
|
||||||
|
`$${T}`,
|
||||||
|
HTMLElement
|
||||||
|
> & {
|
||||||
|
get: () => S;
|
||||||
|
set: (state: Partial<S>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateHandler<S extends any, T extends string> = Record<
|
||||||
|
T,
|
||||||
|
(ctx: StateContext<S, T>) => {
|
||||||
|
onClick?: (event: MouseEvent) => void;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
interface StateMeta {
|
||||||
|
path: string;
|
||||||
|
hash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StateRegistryEntry<S, T extends string> {
|
||||||
|
handler: StateHandler<S, T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StateManager {
|
||||||
|
init: boolean;
|
||||||
|
states: Record<string, { state: any; handlerId: string }>;
|
||||||
|
stateRegistry: Map<string, StateRegistryEntry<any, string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MyWindow extends Window {
|
||||||
|
stateManager: StateManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare let window: MyWindow;
|
||||||
|
|
||||||
|
const stateManager: StateManager =
|
||||||
|
typeof window !== "undefined" && "stateManager" in window
|
||||||
|
? window.stateManager
|
||||||
|
: {
|
||||||
|
init: false,
|
||||||
|
states: {},
|
||||||
|
stateRegistry: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateStateResult<S, T extends string> = {
|
||||||
|
meta: StateMeta;
|
||||||
|
initial: S;
|
||||||
|
handler: StateHandler<S, T>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createState = <S, T extends string>(
|
||||||
|
initialState: S,
|
||||||
|
stateHandler: StateHandler<S, T>,
|
||||||
|
meta?: StateMeta,
|
||||||
|
): CreateStateResult<S, T> => {
|
||||||
|
if (!meta) {
|
||||||
|
throw new Error("Missing meta please check if bun plugin is loaded");
|
||||||
|
}
|
||||||
|
if (!stateManager.stateRegistry.has(meta.hash)) {
|
||||||
|
stateManager.stateRegistry.set(meta.hash, {
|
||||||
|
handler: stateHandler,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
meta,
|
||||||
|
initial: initialState,
|
||||||
|
handler: stateHandler,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!stateManager.init && typeof window != "undefined") {
|
||||||
|
stateManager.init = true;
|
||||||
|
window.stateManager = stateManager;
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
const target = e.target;
|
||||||
|
if (target instanceof HTMLElement && target.dataset["c"]) {
|
||||||
|
const [part, stateId] = target.dataset["c"].split("-");
|
||||||
|
const { handler } = stateManager.stateRegistry.get(
|
||||||
|
stateManager.states[stateId].handlerId,
|
||||||
|
)!;
|
||||||
|
const parts = Object.keys(handler);
|
||||||
|
const { state } = stateManager.states[stateId];
|
||||||
|
const context = Object.fromEntries([
|
||||||
|
["get", () => state],
|
||||||
|
[
|
||||||
|
"set",
|
||||||
|
(newState: any) => {
|
||||||
|
stateManager.states[stateId].state = { ...state, ...newState };
|
||||||
|
},
|
||||||
|
],
|
||||||
|
...parts.map((part) => [
|
||||||
|
`$$${part}`,
|
||||||
|
document.querySelector(`[data-c=${part}-${stateId}]`),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
handler[part](context).onClick?.(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
const formatBytes = (bytes: number) => {
|
||||||
|
if (bytes > 1024 * 1024 * 1024) {
|
||||||
|
return Math.floor((bytes * 100) / (1024 * 1024 * 1024)) / 100 + "GB";
|
||||||
|
}
|
||||||
|
if (bytes > 1024 * 1024) {
|
||||||
|
return Math.floor((bytes * 100) / (1024 * 1024)) / 100 + "MB";
|
||||||
|
}
|
||||||
|
if (bytes > 1024) {
|
||||||
|
return Math.floor((bytes * 100) / 1024) / 100 + "KB";
|
||||||
|
}
|
||||||
|
return bytes + "B";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requestLog = (handler: (req: Request) => Promise<Response>) => {
|
||||||
|
return async (req: Request) => {
|
||||||
|
const start = performance.now();
|
||||||
|
const response = await handler(req);
|
||||||
|
const bytes = await response.clone().bytes();
|
||||||
|
const delta = performance.now() - start;
|
||||||
|
console.log(
|
||||||
|
`[${req.method}] ${req.url} -> ${response.headers.get("Content-Type")} ${formatBytes(bytes.length)} in ${delta}ms`,
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { plugin, type BunPlugin } from "bun";
|
||||||
|
import { crazyHash } from "./system/crazyHash";
|
||||||
|
|
||||||
|
const createStateString = "createState(";
|
||||||
|
const createStateStringRegex = /createState\(/g;
|
||||||
|
export const myPlugin: BunPlugin = {
|
||||||
|
name: "Custom loader",
|
||||||
|
setup(build) {
|
||||||
|
build.onLoad(
|
||||||
|
{
|
||||||
|
filter: /\.tsx?$/,
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
console.log("loading", args.path);
|
||||||
|
const path = require.resolve(args.path);
|
||||||
|
let contents = await Bun.file(path).text();
|
||||||
|
const createStateCalls = contents.matchAll(createStateStringRegex);
|
||||||
|
createStateCalls.forEach((match) => {
|
||||||
|
console.log(match);
|
||||||
|
let braces = 1;
|
||||||
|
let index = match.index + createStateString.length;
|
||||||
|
let needsComma = false;
|
||||||
|
console.log(contents[index]);
|
||||||
|
do {
|
||||||
|
index++;
|
||||||
|
if (contents[index] == "(") braces++;
|
||||||
|
else if (contents[index] == ")") braces--;
|
||||||
|
else if (contents[index] == ",") needsComma = false;
|
||||||
|
else if (contents[index].trim().length) needsComma = true;
|
||||||
|
} while (braces > 0);
|
||||||
|
console.log(contents[index]);
|
||||||
|
const meta = {
|
||||||
|
path,
|
||||||
|
hash: crazyHash(contents),
|
||||||
|
};
|
||||||
|
contents =
|
||||||
|
contents.substring(0, index) +
|
||||||
|
(needsComma ? ", " : "") +
|
||||||
|
JSON.stringify(meta) +
|
||||||
|
contents.substring(index);
|
||||||
|
console.log(contents);
|
||||||
|
});
|
||||||
|
// console.log(contents);
|
||||||
|
return {
|
||||||
|
contents,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
plugin(myPlugin);
|
||||||
|
|
@ -1,9 +1,30 @@
|
||||||
|
import { Counter } from "~/components/Counter";
|
||||||
import Layout from "~/components/Layout";
|
import Layout from "~/components/Layout";
|
||||||
|
import { Link } from "~/components/Link";
|
||||||
|
import { useParams } from "~/lib/hooks";
|
||||||
|
import { pokeapi } from "~/lib/pokeapi";
|
||||||
|
|
||||||
export default function Index() {
|
export default async function Index() {
|
||||||
|
const params = useParams();
|
||||||
|
const page = Number(params.get("page") ?? 0);
|
||||||
|
const pokemon = await pokeapi.getPokemonsList({
|
||||||
|
limit: 20,
|
||||||
|
offset: page * 20,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
|
<>
|
||||||
<div>Hello World</div>
|
<div>Hello World</div>
|
||||||
|
<Counter />
|
||||||
|
<Counter />
|
||||||
|
<ul>
|
||||||
|
{pokemon.results.map((r) => (
|
||||||
|
<li>{r.name}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<Link href={`/?page=${page - 1}`}>Prev</Link>
|
||||||
|
<Link href={`/?page=${page + 1}`}>Next</Link>
|
||||||
|
</>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
document.addEventListener("pointerdown", (e) => {
|
||||||
|
const target = e.target;
|
||||||
|
if (target instanceof HTMLElement) {
|
||||||
|
if (target.tagName == "A" && !target.getAttribute("target")) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const href = target.getAttribute("href")!;
|
||||||
|
window.history.pushState({}, "", href);
|
||||||
|
fetch(href).then(async (res) => {
|
||||||
|
window.document.documentElement.innerHTML = await res.text();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
export function crazyHash(data: string) {
|
||||||
|
let num = Bun.hash.murmur32v3(data);
|
||||||
|
// const chars =
|
||||||
|
// "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
const chars =
|
||||||
|
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ";
|
||||||
|
let result = "";
|
||||||
|
do {
|
||||||
|
result = chars[num % chars.length] + result;
|
||||||
|
num = Math.floor(num / chars.length);
|
||||||
|
} while (num > 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import type { BaseCSSProperty, CSSDuration, EasingFunction } from "~/types";
|
||||||
|
|
||||||
|
type Transition =
|
||||||
|
| [BaseCSSProperty]
|
||||||
|
| [BaseCSSProperty, CSSDuration]
|
||||||
|
| [BaseCSSProperty, CSSDuration, EasingFunction]
|
||||||
|
| [BaseCSSProperty, CSSDuration, EasingFunction, CSSDuration];
|
||||||
|
|
||||||
|
export const transition = (transitions: Transition[]) => {
|
||||||
|
return transitions.map((t) => t.join(" ")).join(",");
|
||||||
|
};
|
||||||
|
|
@ -1,29 +1,12 @@
|
||||||
|
import { pseudoClasses, type Styles } from "~/types";
|
||||||
import { getCategory, shorthand } from "./token-categories";
|
import { getCategory, shorthand } from "./token-categories";
|
||||||
|
import { crazyHash } from "./crazyHash";
|
||||||
|
|
||||||
export const camelToKebab = (str: string) => {
|
export const camelToKebab = (str: string) => {
|
||||||
return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2").toLowerCase();
|
return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2").toLowerCase();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStyleRules = (style: JSX.CSSProperties) => {
|
const resolveVars = (key: string, value: string) => {
|
||||||
const styles = Object.entries(style).map(([key, value]) => {
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const rules = [];
|
|
||||||
while (styles.length) {
|
|
||||||
const { key, value } = styles.shift()!;
|
|
||||||
if (key in shorthand) {
|
|
||||||
const shorthandRules = shorthand[key as keyof typeof shorthand](value);
|
|
||||||
styles.unshift(...shorthandRules);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const kKey = camelToKebab(key);
|
|
||||||
if (typeof value === "number") {
|
|
||||||
rules.push(`${kKey}: ${value}px;`);
|
|
||||||
} else if (typeof value === "string") {
|
|
||||||
const match = value.match(/\$([a-zA-Z0-9\.\-]+)/g);
|
const match = value.match(/\$([a-zA-Z0-9\.\-]+)/g);
|
||||||
if (match) {
|
if (match) {
|
||||||
const category = getCategory(key);
|
const category = getCategory(key);
|
||||||
|
|
@ -33,23 +16,84 @@ export const getStyleRules = (style: JSX.CSSProperties) => {
|
||||||
`var(--${category}${m.replaceAll("$", "").replaceAll(".", "-")})`,
|
`var(--${category}${m.replaceAll("$", "").replaceAll(".", "-")})`,
|
||||||
);
|
);
|
||||||
}, value);
|
}, value);
|
||||||
rules.push(`${kKey}: ${replaceValue};`);
|
return replaceValue;
|
||||||
} else {
|
|
||||||
rules.push(`${kKey}: ${value};`);
|
|
||||||
}
|
}
|
||||||
} else {
|
return value;
|
||||||
rules.push(`${kKey}: ${value.join(" ")};`);
|
};
|
||||||
|
|
||||||
|
const renderRule = (key: string, value: string | number | string[]): string => {
|
||||||
|
const kKey = camelToKebab(key);
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return `${kKey}: ${value}px;`;
|
||||||
}
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return `${kKey}: ${resolveVars(key, value)};`;
|
||||||
|
}
|
||||||
|
return `${kKey}: ${value.join(" ")};`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const partitionStyles = (style: Styles) => {
|
||||||
|
const selectorStylesMapping: Record<
|
||||||
|
string,
|
||||||
|
Record<string, string | number | string[]>
|
||||||
|
> = {
|
||||||
|
"&": {},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(style)) {
|
||||||
|
if (typeof value === "object" && !Array.isArray(value)) {
|
||||||
|
const selector =
|
||||||
|
pseudoClasses[key.substring(1) as keyof typeof pseudoClasses] ?? key;
|
||||||
|
selectorStylesMapping[selector] = value;
|
||||||
|
} else {
|
||||||
|
selectorStylesMapping["&"][key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selectorStylesMapping;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveShorthand = (
|
||||||
|
key: string,
|
||||||
|
value: string | number | string[],
|
||||||
|
): { key: string; value: string | number | string[] }[] => {
|
||||||
|
if (key in shorthand) {
|
||||||
|
return shorthand[key as keyof typeof shorthand](value);
|
||||||
|
}
|
||||||
|
return [{ key, value }];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStyleRules = (style: Styles) => {
|
||||||
|
const selectorStylesMapping = partitionStyles(style);
|
||||||
|
const rules: Record<string, string[]> = {};
|
||||||
|
for (const [selector, styles] of Object.entries(selectorStylesMapping)) {
|
||||||
|
const stylePairs = Object.entries(styles).reduce(
|
||||||
|
(acc, [key, value]) => {
|
||||||
|
const resolvedShorthand = resolveShorthand(key, value);
|
||||||
|
return [...acc, ...resolvedShorthand];
|
||||||
|
},
|
||||||
|
[] as { key: string; value: string | number | string[] }[],
|
||||||
|
);
|
||||||
|
rules[selector] = stylePairs.map(({ key, value }) => {
|
||||||
|
return renderRule(key, value);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const renderStyle = (style: JSX.CSSProperties) => {
|
const getRulesClassName = (rules: Record<string, string[]>) => {
|
||||||
const rules = getStyleRules(style);
|
const flatRules = Object.values(rules).flat();
|
||||||
const sortedRules = rules.sort();
|
flatRules.push(...Object.keys(rules));
|
||||||
const rulesString = sortedRules.join(" ");
|
const sortedRules = flatRules.sort();
|
||||||
const hash = Bun.hash(rulesString);
|
const rulesString = sortedRules.join(";");
|
||||||
const className = `style-${hash.toString(36)}`;
|
return `f${crazyHash(rulesString)}`;
|
||||||
const css = `.${className} {${rulesString}}`;
|
};
|
||||||
|
|
||||||
|
export const renderStyle = (style: Styles, selectorOverride?: string) => {
|
||||||
|
const rulesMapping = getStyleRules(style);
|
||||||
|
const className = selectorOverride ?? getRulesClassName(rulesMapping);
|
||||||
|
let css = "";
|
||||||
|
for (const [selector, rules] of Object.entries(rulesMapping)) {
|
||||||
|
css += `${selector.replaceAll("&", selectorOverride ?? "." + className)} {${rules.join(" ")}}`;
|
||||||
|
}
|
||||||
return { className, css };
|
return { className, css };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,17 @@
|
||||||
const tokenTypes = ["color", "space", "font"] as const;
|
import type { CSSProperties } from "~/types";
|
||||||
|
|
||||||
|
const tokenTypes = [
|
||||||
|
"color",
|
||||||
|
"space",
|
||||||
|
"font",
|
||||||
|
"font-size",
|
||||||
|
"font-weight",
|
||||||
|
"border-radius",
|
||||||
|
] as const;
|
||||||
type TokenType = (typeof tokenTypes)[number];
|
type TokenType = (typeof tokenTypes)[number];
|
||||||
|
|
||||||
export const shorthand = {
|
export const shorthand = {
|
||||||
marginX: (value: string) => [
|
marginX: <T>(value: T) => [
|
||||||
{
|
{
|
||||||
key: "marginLeft",
|
key: "marginLeft",
|
||||||
value,
|
value,
|
||||||
|
|
@ -12,7 +21,7 @@ export const shorthand = {
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
marginY: (value: string) => [
|
marginY: <T>(value: T) => [
|
||||||
{
|
{
|
||||||
key: "marginTop",
|
key: "marginTop",
|
||||||
value,
|
value,
|
||||||
|
|
@ -22,7 +31,27 @@ export const shorthand = {
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
paddingX: (value: string) => [
|
mX: <T>(value: T) => [
|
||||||
|
{
|
||||||
|
key: "marginLeft",
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "marginRight",
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mY: <T>(value: T) => [
|
||||||
|
{
|
||||||
|
key: "marginTop",
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "marginBottom",
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
paddingX: <T>(value: T) => [
|
||||||
{
|
{
|
||||||
key: "paddingLeft",
|
key: "paddingLeft",
|
||||||
value,
|
value,
|
||||||
|
|
@ -32,7 +61,27 @@ export const shorthand = {
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
paddingY: (value: string) => [
|
paddingY: <T>(value: T) => [
|
||||||
|
{
|
||||||
|
key: "paddingTop",
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "paddingBottom",
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pX: <T>(value: T) => [
|
||||||
|
{
|
||||||
|
key: "paddingLeft",
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "paddingRight",
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pY: <T>(value: T) => [
|
||||||
{
|
{
|
||||||
key: "paddingTop",
|
key: "paddingTop",
|
||||||
value,
|
value,
|
||||||
|
|
@ -63,8 +112,13 @@ export const categoryMap = {
|
||||||
paddingX: "space",
|
paddingX: "space",
|
||||||
paddingY: "space",
|
paddingY: "space",
|
||||||
gap: "space",
|
gap: "space",
|
||||||
|
height: "space",
|
||||||
|
width: "space",
|
||||||
fontFamily: "font",
|
fontFamily: "font",
|
||||||
} as const satisfies Partial<Record<keyof JSX.CSSProperties, TokenType>>;
|
fontSize: "font-size",
|
||||||
|
fontWeight: "font-weight",
|
||||||
|
borderRadius: "border-radius",
|
||||||
|
} as const satisfies Partial<Record<keyof CSSProperties, TokenType>>;
|
||||||
|
|
||||||
export const getCategory = (value: string) => {
|
export const getCategory = (value: string) => {
|
||||||
return value in categoryMap
|
return value in categoryMap
|
||||||
|
|
|
||||||
|
|
@ -280,5 +280,38 @@
|
||||||
--space-72: 18rem;
|
--space-72: 18rem;
|
||||||
--space-80: 20rem;
|
--space-80: 20rem;
|
||||||
|
|
||||||
--font-sans: "Rubik", sans-serif;
|
--font-sans: sans-serif;
|
||||||
|
|
||||||
|
--font-size-xs: 0.75rem;
|
||||||
|
--font-size-sm: 0.875rem;
|
||||||
|
--font-size-base: 1rem;
|
||||||
|
--font-size-lg: 1.125rem;
|
||||||
|
--font-size-xl: 1.25rem;
|
||||||
|
--font-size-2xl: 1.5rem;
|
||||||
|
--font-size-3xl: 1.875rem;
|
||||||
|
--font-size-4xl: 2.25rem;
|
||||||
|
--font-size-5xl: 3rem;
|
||||||
|
--font-size-6xl: 3.75rem;
|
||||||
|
--font-size-7xl: 4.5rem;
|
||||||
|
--font-size-8xl: 6rem;
|
||||||
|
--font-size-9xl: 8rem;
|
||||||
|
|
||||||
|
--font-weight-thin: 100;
|
||||||
|
--font-weight-extralight: 200;
|
||||||
|
--font-weight-light: 300;
|
||||||
|
--font-weight-normal: 400;
|
||||||
|
--font-weight-medium: 500;
|
||||||
|
--font-weight-semibold: 600;
|
||||||
|
--font-weight-bold: 700;
|
||||||
|
--font-weight-extrabold: 800;
|
||||||
|
--font-weight-black: 900;
|
||||||
|
|
||||||
|
--border-radius-xs: 0.125rem;
|
||||||
|
--border-radius-sm: 0.25rem;
|
||||||
|
--border-radius-md: 0.375rem;
|
||||||
|
--border-radius-lg: 0.5rem;
|
||||||
|
--border-radius-xl: 0.75rem;
|
||||||
|
--border-radius-2xl: 1rem;
|
||||||
|
--border-radius-3xl: 1.5rem;
|
||||||
|
--border-radius-4xl: 2rem;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import * as CSS from "csstype";
|
||||||
|
|
||||||
|
export const pseudoClasses = {
|
||||||
|
hover: "&:hover",
|
||||||
|
focus: "&:focus",
|
||||||
|
active: "&:active",
|
||||||
|
visited: "&:visited",
|
||||||
|
first: "&:first-child",
|
||||||
|
last: "&:last-child",
|
||||||
|
odd: "&:nth-child(odd)",
|
||||||
|
even: "&:nth-child(even)",
|
||||||
|
disabled: "&:disabled",
|
||||||
|
checked: "&:checked",
|
||||||
|
selected: "&:selected",
|
||||||
|
readOnly: "&:read-only",
|
||||||
|
empty: "&:empty",
|
||||||
|
before: "&:before",
|
||||||
|
after: "&:after",
|
||||||
|
link: "&:link",
|
||||||
|
visitedLink: "&:visited",
|
||||||
|
activeLink: "&:active",
|
||||||
|
focusWithin: "&:focus-within",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type PseudoClassProperty = `_${keyof typeof pseudoClasses}`;
|
||||||
|
|
||||||
|
export type BaseCSSProperty = CSS.Properties;
|
||||||
|
export type CSSProperties = CSS.Properties & {
|
||||||
|
marginX?: CSS.Properties["marginLeft"];
|
||||||
|
marginY?: CSS.Properties["marginTop"];
|
||||||
|
paddingX?: CSS.Properties["paddingLeft"];
|
||||||
|
paddingY?: CSS.Properties["paddingTop"];
|
||||||
|
mX?: CSS.Properties["marginLeft"];
|
||||||
|
mY?: CSS.Properties["marginTop"];
|
||||||
|
pX?: CSS.Properties["paddingLeft"];
|
||||||
|
pY?: CSS.Properties["paddingTop"];
|
||||||
|
};
|
||||||
|
export type CSSDuration = `${number}s` | `${number}ms`;
|
||||||
|
export type EasingFunction =
|
||||||
|
| (string & {})
|
||||||
|
| "linear"
|
||||||
|
| "ease"
|
||||||
|
| "ease-in"
|
||||||
|
| "ease-in-out"
|
||||||
|
| "ease-out";
|
||||||
|
|
||||||
|
type ArbitrarySelectors = Record<`&${string}`, CSSProperties>;
|
||||||
|
|
||||||
|
type PseudoClassSelectors = Partial<Record<PseudoClassProperty, CSSProperties>>;
|
||||||
|
|
||||||
|
export type Styles = ArbitrarySelectors & PseudoClassSelectors & CSSProperties;
|
||||||
Loading…
Reference in New Issue