diff --git a/.gitignore b/.gitignore index bc20fec..5c005f9 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,4 @@ package-lock.json server -*.sqlite +db.sqlite diff --git a/README.md b/README.md index 8a7fa16..bedbe3d 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,26 @@ # hyperwave 🌊 -hyperwave is a server-side framework for building web applications. +https://hyperwave.codes/ -* fast: Bun and Hono for best-in-class performance -* lightweight: ~20kb payload, loads in < 5 seconds on slow 3G -* productive: use the best tools for the job: Tailwind, HTMX, and TypeScript -* portable: compile a binary to deploy anywhere +hyperwave is a server-side framework for building web applications. ---- +- fast: Bun and Hono for best-in-class performance +- lightweight: ~20kb payload. Demo loads in a couple seconds even while throttled to 2G. +- productive: use the best tools for the job: Tailwind, HTMX, and TypeScript +- portable: compile a binary to deploy anywhere ### Setup -`bun setup` +`bun install && bun run src/db.ts && bun dev` Visit port 3000 and edit `server.tsx` --- ### Example + This is the endpoint serving our initial landing page: + ```typescript app.get("/", ({ html }) => html( @@ -38,6 +40,7 @@ app.get("/", ({ html }) => ), ); ``` + - The API serves a full HTML document to the client, which includes Tailwind classes and HTMX attributes - The response is wrapped in a `` tag, a server-rendered functional component, which takes a `title` prop - The button, when clicked, will issue a `GET` request to `/instructions` and replace the content of its parent div with the response. diff --git a/assets/icons/calendar.tsx b/assets/icons/calendar.tsx new file mode 100644 index 0000000..cbe3a94 --- /dev/null +++ b/assets/icons/calendar.tsx @@ -0,0 +1,12 @@ +export default function Calendar() { + return ( + + + + ); +} diff --git a/assets/icons/chart.tsx b/assets/icons/chart.tsx new file mode 100644 index 0000000..b67302e --- /dev/null +++ b/assets/icons/chart.tsx @@ -0,0 +1,12 @@ +export default function Chart() { + return ( + + + + ); +} diff --git a/assets/icons/check.tsx b/assets/icons/check.tsx new file mode 100644 index 0000000..f0320f6 --- /dev/null +++ b/assets/icons/check.tsx @@ -0,0 +1,12 @@ +export default function Check() { + return ( + + + + ); +} diff --git a/assets/icons/credit_card.tsx b/assets/icons/credit_card.tsx new file mode 100644 index 0000000..f323cd8 --- /dev/null +++ b/assets/icons/credit_card.tsx @@ -0,0 +1,12 @@ +export default function CreditCard() { + return ( + + + + ); +} diff --git a/assets/icons/file.tsx b/assets/icons/file.tsx new file mode 100644 index 0000000..b51da64 --- /dev/null +++ b/assets/icons/file.tsx @@ -0,0 +1,12 @@ +export default function File() { + return ( + + + + ); +} diff --git a/assets/icons/gear.tsx b/assets/icons/gear.tsx new file mode 100644 index 0000000..c577e21 --- /dev/null +++ b/assets/icons/gear.tsx @@ -0,0 +1,12 @@ +export default function Gear() { + return ( + + + + ); +} diff --git a/assets/icons/house.tsx b/assets/icons/house.tsx new file mode 100644 index 0000000..8b15499 --- /dev/null +++ b/assets/icons/house.tsx @@ -0,0 +1,12 @@ +export default function House() { + return ( + + + + ); +} diff --git a/assets/icons/magnify.tsx b/assets/icons/magnify.tsx new file mode 100644 index 0000000..90a56d9 --- /dev/null +++ b/assets/icons/magnify.tsx @@ -0,0 +1,15 @@ +export default function Magnify() { + return ( + + + + ); +} diff --git a/assets/icons/question.tsx b/assets/icons/question.tsx new file mode 100644 index 0000000..a17c4cf --- /dev/null +++ b/assets/icons/question.tsx @@ -0,0 +1,12 @@ +export default function Question() { + return ( + + + + ); +} diff --git a/assets/icons/user.tsx b/assets/icons/user.tsx new file mode 100644 index 0000000..88b9e65 --- /dev/null +++ b/assets/icons/user.tsx @@ -0,0 +1,12 @@ +export default function User() { + return ( + + + + ); +} diff --git a/bun.lockb b/bun.lockb index 314191e..1b1906b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index cf77241..d186173 100644 --- a/package.json +++ b/package.json @@ -2,23 +2,25 @@ "name": "hyperwave", "version": "1.0.50", "scripts": { + "build": "bun css && bun build --compile ./src/server.tsx", "css": "unocss \"src/**/*.tsx\" -o public/styles/uno.css", "css:watch": "unocss \"src/**/*.tsx\" -o public/styles/uno.css --watch", - "server:watch": "bun --watch run src/server.tsx", - "dev": "concurrently \"bun css:watch\" \"bun server:watch\"", "db": "bun run src/db.ts", - "setup": "bun install && bun db && bun dev", - "test": "bun run test", + "dev": "concurrently \"bun css:watch\" \"bun server:watch\"", "prettier": "bunx prettier --write src/ test/ --plugin prettier-plugin-tailwindcss", - "build": "bun css && bun build --compile ./src/server.tsx" + "server:watch": "bun --watch run src/server.tsx", + "test": "bun run test" }, "dependencies": { - "hono": "^3.6.3" + "@unocss/preset-web-fonts": "^0.58.0", + "hono": "^3.6.3", + "unocss": "^0.58.0" }, "devDependencies": { "@unocss/cli": "^0.56.5", "bun-types": "latest", "concurrently": "^8.2.1", + "prettier": "^3.1.0", "prettier-plugin-tailwindcss": "^0.5.9" }, "module": "src/server.tsx" diff --git a/public/styles/uno.css b/public/styles/uno.css index f60b114..600e1d6 100644 --- a/public/styles/uno.css +++ b/public/styles/uno.css @@ -1,27 +1,128 @@ /* layer: preflights */ -*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgba(0,0,0,0);--un-ring-shadow:0 0 rgba(0,0,0,0);--un-shadow-inset: ;--un-shadow:0 0 rgba(0,0,0,0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgba(0,0,0,0);--un-ring-shadow:0 0 rgba(0,0,0,0);--un-shadow-inset: ;--un-shadow:0 0 rgba(0,0,0,0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;} +*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;} +/* latin-ext */ +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(https://fonts.gstatic.com/s/lato/v24/S6uyw4BMUTPHjxAwXjeu.woff2) format('woff2'); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(https://fonts.gstatic.com/s/lato/v24/S6uyw4BMUTPHjx4wXg.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + /* layer: default */ +.fixed{position:fixed;} +.relative{position:relative;} +.sticky{position:sticky;} +.left-10{left:2.5rem;} +.right-8{right:2rem;} +.top-0\.5{top:0.125rem;} +.top-14{top:3.5rem;} +.m-0{margin:0;} .m-auto{margin:auto;} +.my-4{margin-top:1rem;margin-bottom:1rem;} +.me{margin-inline-end:1rem;} +.block{display:block;} +.hidden{display:none;} +.h-8{height:2rem;} +.h-full{height:100%;} +.max-h-14{max-height:3.5rem;} +.min-h-14{min-height:3.5rem;} +.w-16{width:4rem;} +.w-40{width:10rem;} +.w-5\/6{width:83.3333333333%;} +.w-80{width:20rem;} +.w-96{width:24rem;} .w-full{width:100%;} +.hover\:w-56:hover{width:14rem;} .flex{display:flex;} .flex-col{flex-direction:column;} +.cursor-pointer{cursor:pointer;} +.list-none{list-style-type:none;} +.items-start\!{align-items:flex-start !important;} +.items-center{align-items:center;} .self-start{align-self:flex-start;} +.justify-start\!{justify-content:flex-start !important;} .justify-center{justify-content:center;} +.justify-between{justify-content:space-between;} +.gap-16{gap:4rem;} +.gap-3{gap:0.75rem;} .gap-4{gap:1rem;} .gap-8{gap:2rem;} -.border-b-2{border-bottom-width:2px;} -.border-blue-200{--un-border-opacity:1;border-color:rgba(191,219,254,var(--un-border-opacity));} +.border{border-width:1px;} +.border-b-1{border-bottom-width:1px;} +.border-gray-2{--un-border-opacity:1;border-color:rgb(229 231 235 / var(--un-border-opacity));} +.border-slate-200{--un-border-opacity:1;border-color:rgb(226 232 240 / var(--un-border-opacity));} +.focus\:border-blue-200:focus{--un-border-opacity:1;border-color:rgb(191 219 254 / var(--un-border-opacity));} .rounded-md{border-radius:0.375rem;} -.bg-blue-100{--un-bg-opacity:1;background-color:rgba(219,234,254,var(--un-bg-opacity));} -.bg-blue-50{--un-bg-opacity:1;background-color:rgba(239,246,255,var(--un-bg-opacity));} -.p-10{padding:2.5rem;} +.border-none{border-style:none;} +.border-solid{border-style:solid;} +.border-b-solid{border-bottom-style:solid;} +.bg-blue-100{--un-bg-opacity:1;background-color:rgb(219 234 254 / var(--un-bg-opacity));} +.bg-gray-100{--un-bg-opacity:1;background-color:rgb(243 244 246 / var(--un-bg-opacity));} +.bg-red-400{--un-bg-opacity:1;background-color:rgb(248 113 113 / var(--un-bg-opacity));} +.bg-slate-700{--un-bg-opacity:1;background-color:rgb(51 65 85 / var(--un-bg-opacity));} +.bg-slate-800{--un-bg-opacity:1;background-color:rgb(30 41 59 / var(--un-bg-opacity));} +.bg-transparent{background-color:transparent;} +.bg-white{--un-bg-opacity:1;background-color:rgb(255 255 255 / var(--un-bg-opacity));} +.hover\:bg-red-400:hover{--un-bg-opacity:1;background-color:rgb(248 113 113 / var(--un-bg-opacity));} +.hover\:bg-slate-700:hover{--un-bg-opacity:1;background-color:rgb(51 65 85 / var(--un-bg-opacity));} +.fill-neutral-500{--un-fill-opacity:1;fill:rgb(115 115 115 / var(--un-fill-opacity));} +.fill-white{--un-fill-opacity:1;fill:rgb(255 255 255 / var(--un-fill-opacity));} +.p-0{padding:0;} .p-4{padding:1rem;} -.p-8{padding:2rem;} .px-10{padding-left:2.5rem;padding-right:2.5rem;} +.px-4{padding-left:1rem;padding-right:1rem;} +.py-1{padding-top:0.25rem;padding-bottom:0.25rem;} +.py-2{padding-top:0.5rem;padding-bottom:0.5rem;} +.py-3{padding-top:0.75rem;padding-bottom:0.75rem;} .py-4{padding-top:1rem;padding-bottom:1rem;} -.text-2xl{font-size:1.5rem;line-height:2rem;} +.pl-0\.5{padding-left:0.125rem;} +.pl-10{padding-left:2.5rem;} +.pl-20{padding-left:5rem;} +.pl-3{padding-left:0.75rem;} +.pl-5{padding-left:1.25rem;} +.pl-6{padding-left:1.5rem;} +.pr-10{padding-right:2.5rem;} +.pr-4{padding-right:1rem;} +.text-left{text-align:left;} +.text-base{font-size:1rem;line-height:1.5rem;} .text-sm{font-size:0.875rem;line-height:1.25rem;} +.text-neutral-500{--un-text-opacity:1;color:rgb(115 115 115 / var(--un-text-opacity));} +.text-red-100{--un-text-opacity:1;color:rgb(254 226 226 / var(--un-text-opacity));} +.text-slate-400{--un-text-opacity:1;color:rgb(148 163 184 / var(--un-text-opacity));} +.text-white{--un-text-opacity:1;color:rgb(255 255 255 / var(--un-text-opacity));} +.hover\:text-white:hover{--un-text-opacity:1;color:rgb(255 255 255 / var(--un-text-opacity));} .font-bold{font-weight:700;} -.shadow-md{--un-shadow:var(--un-shadow-inset) 0 4px 6px -1px var(--un-shadow-color, rgba(0,0,0,0.1)),var(--un-shadow-inset) 0 2px 4px -2px var(--un-shadow-color, rgba(0,0,0,0.1));box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);} -.shadow-sm{--un-shadow:var(--un-shadow-inset) 0 1px 2px 0 var(--un-shadow-color, rgba(0,0,0,0.05));box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);} -.outline{outline-style:solid;} \ No newline at end of file +.leading-5{line-height:1.25rem;} +.font-lato{font-family:"Lato";} +.uppercase{text-transform:uppercase;} +.no-underline{text-decoration:none;} +.opacity-0{opacity:0;} +.group:hover .group-hover\:opacity-100{opacity:1;} +.shadow-md{--un-shadow:var(--un-shadow-inset) 0 4px 6px -1px var(--un-shadow-color, rgb(0 0 0 / 0.1)),var(--un-shadow-inset) 0 2px 4px -2px var(--un-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);} +.shadow-sm{--un-shadow:var(--un-shadow-inset) 0 1px 2px 0 var(--un-shadow-color, rgb(0 0 0 / 0.05));box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);} +.outline{outline-style:solid;} +.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;} +.transition-width{transition-property:width;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;} +.duration-200{transition-duration:200ms;} +.duration-300{transition-duration:300ms;} +.duration-50{transition-duration:50ms;} +@media (min-width: 640px){ +.sm\:ml-4{margin-left:1rem;} +} +@media (min-width: 768px){ +.md\:block{display:block;} +.md\:w-56{width:14rem;} +.md\:pl-60{padding-left:15rem;} +.md\:opacity-100{opacity:1;} +} \ No newline at end of file diff --git a/src/Layout.tsx b/src/Layout.tsx deleted file mode 100644 index 31b84b9..0000000 --- a/src/Layout.tsx +++ /dev/null @@ -1,35 +0,0 @@ -export default function Layout({ title, children }) { - return ( - - - - - - {title} - - - - - -
- -

hyperwave 🌊

-
-
-
- {children} -
- - - ); -} diff --git a/src/components/Input.tsx b/src/components/Input.tsx new file mode 100644 index 0000000..84702a3 --- /dev/null +++ b/src/components/Input.tsx @@ -0,0 +1,15 @@ +export default function Input({ class: className, placeholder, ...rest }) { + return ( + + ); +} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx new file mode 100644 index 0000000..6b3531f --- /dev/null +++ b/src/components/Layout.tsx @@ -0,0 +1,54 @@ +import Magnify from "../../assets/icons/magnify"; +import Input from "./Input"; +import Nav from "./Nav"; + +type Props = { + title: string; + currentPath?: string; + children: any; +}; + +export default function Layout({ title, children, currentPath }: Props) { + return ( + + + + + + {title} + + + + + + + +
+ +

Hyperwave 🌊

+
+ +
+ +