Remove css, js, and fe() inline helpers; update docs and examples to use client scripts
This commit is contained in:
parent
1333deb5e9
commit
78eef47f46
43
README.md
43
README.md
|
|
@ -17,7 +17,6 @@
|
||||||
- Static files served from `pub/`
|
- Static files served from `pub/`
|
||||||
- Page-based routing to `.tsx` files that export a `JSX` function in `./src/pages`
|
- Page-based routing to `.tsx` files that export a `JSX` function in `./src/pages`
|
||||||
- Transpile `.ts` files in `src/client/blah.ts` via `website.com/client/blah.ts`
|
- Transpile `.ts` files in `src/client/blah.ts` via `website.com/client/blah.ts`
|
||||||
- Helpers like `css` and `js` template tags.
|
|
||||||
- Default, simple HTML5 layout with working frontend transpilation/bundling, or supply your own.
|
- Default, simple HTML5 layout with working frontend transpilation/bundling, or supply your own.
|
||||||
- Optional CSS reset.
|
- Optional CSS reset.
|
||||||
- Optional pico.css.
|
- Optional pico.css.
|
||||||
|
|
@ -115,25 +114,6 @@ CSS can be accessed via `/css/main.css`:
|
||||||
<link href="/css/main.css" rel="stylesheet" />
|
<link href="/css/main.css" rel="stylesheet" />
|
||||||
```
|
```
|
||||||
|
|
||||||
Or written inline using the `css` template tag:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { css } from "hype"
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<div>
|
|
||||||
{css`
|
|
||||||
* {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
<h1>Hello</h1>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Install the `vscode-styled-components` VSCode extension to highlight inline `css` tags!
|
|
||||||
|
|
||||||
### css reset
|
### css reset
|
||||||
|
|
||||||
To use reset.css with the default layout, create `Hype` with `{ reset: true }`:
|
To use reset.css with the default layout, create `Hype` with `{ reset: true }`:
|
||||||
|
|
@ -165,27 +145,6 @@ import { initAbout } from "./about"
|
||||||
import utils from "./shared/utils"
|
import utils from "./shared/utils"
|
||||||
```
|
```
|
||||||
|
|
||||||
Or written inline as transpiled typescript using the `js` template tag:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { js } from "hype"
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<div>
|
|
||||||
{js`
|
|
||||||
window.onload = () => alert(welcomeMsg(Date.now()))
|
|
||||||
|
|
||||||
function welcomeMsg(time: number): string {
|
|
||||||
return "Welcome to my website!"
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
<h1>Hello!</h1>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Install the `vscode-styled-components` VSCode extension to highlight inline `js` tags!
|
|
||||||
|
|
||||||
### pub
|
### pub
|
||||||
|
|
||||||
Anything in `pub/` is served as-is. Simple stuff.
|
Anything in `pub/` is served as-is. Simple stuff.
|
||||||
|
|
@ -249,10 +208,8 @@ curl -N http://localhost:3000/api/time
|
||||||
`hype` includes helpful utils for your webapp:
|
`hype` includes helpful utils for your webapp:
|
||||||
|
|
||||||
- `capitalize(str: string): string` - Capitalizes a word
|
- `capitalize(str: string): string` - Capitalizes a word
|
||||||
- `css` Template Tag - Lets you inline CSS in your TSX. Returns a `<style>` tag
|
|
||||||
- `darkenColor(hex: string, opacity: number): string` - Darken a hex color by blending with black. opacity 1 = original, 0 = black
|
- `darkenColor(hex: string, opacity: number): string` - Darken a hex color by blending with black. opacity 1 = original, 0 = black
|
||||||
- `isDarkMode(): boolean` - Check if the user prefers dark mode
|
- `isDarkMode(): boolean` - Check if the user prefers dark mode
|
||||||
- `js` Template Tag - Lets you inline JavaScript in your TSX. Transpiles and returns a `<script>` tag
|
|
||||||
- `lightenColor(hex: string, opacity: number): string` - Lighten a hex color by blending with white. opacity 1 = original, 0 = white
|
- `lightenColor(hex: string, opacity: number): string` - Lighten a hex color by blending with white. opacity 1 = original, 0 = white
|
||||||
- `rand(end = 2, startAtZero = false): number` - Generate random integer. `rand(2)` flips a coin, `rand(6)` rolls a die, `rand(20)` rolls d20
|
- `rand(end = 2, startAtZero = false): number` - Generate random integer. `rand(2)` flips a coin, `rand(6)` rolls a die, `rand(20)` rolls d20
|
||||||
- `randIndex<T>(list: T[]): number | undefined` - Get a random index from an array. `randIndex([5, 7, 9])` returns `0`, `1`, or `2`
|
- `randIndex<T>(list: T[]): number | undefined` - Get a random index from an array. `randIndex([5, 7, 9])` returns `0`, `1`, or `2`
|
||||||
|
|
|
||||||
204
docs/GUIDE.md
204
docs/GUIDE.md
|
|
@ -1,6 +1,6 @@
|
||||||
# Guide to Writing Hype Apps
|
# Guide to Writing Hype Apps
|
||||||
|
|
||||||
Hype is a thin, opinionated wrapper around [Hono](https://hono.dev) for fast prototyping with Bun. It gives you file-based routing, automatic TypeScript transpilation, SSE, inline CSS/JS helpers, and a default HTML5 layout — all without a build step.
|
Hype is a thin, opinionated wrapper around [Hono](https://hono.dev) for fast prototyping with Bun. It gives you file-based routing, automatic TypeScript transpilation, SSE, and a default HTML5 layout — all without a build step.
|
||||||
|
|
||||||
Since `Hype extends Hono`, every Hono API (`get`, `post`, `use`, `on`, etc.) works out of the box.
|
Since `Hype extends Hono`, every Hono API (`get`, `post`, `use`, `on`, etc.) works out of the box.
|
||||||
|
|
||||||
|
|
@ -16,8 +16,6 @@ Since `Hype extends Hono`, every Hono API (`get`, `post`, `use`, `on`, etc.) wor
|
||||||
- [Client-Side JavaScript](#client-side-javascript)
|
- [Client-Side JavaScript](#client-side-javascript)
|
||||||
- [Styling](#styling)
|
- [Styling](#styling)
|
||||||
- [Server-Sent Events (SSE)](#server-sent-events-sse)
|
- [Server-Sent Events (SSE)](#server-sent-events-sse)
|
||||||
- [The `fe()` Helper](#the-fe-helper)
|
|
||||||
- [Inline `css` and `js` Tags](#inline-css-and-js-tags)
|
|
||||||
- [Custom API Routes](#custom-api-routes)
|
- [Custom API Routes](#custom-api-routes)
|
||||||
- [Sub-Routers](#sub-routers)
|
- [Sub-Routers](#sub-routers)
|
||||||
- [Static Files](#static-files)
|
- [Static Files](#static-files)
|
||||||
|
|
@ -593,32 +591,6 @@ Or include it in a custom layout:
|
||||||
const app = new Hype({ pico: true, reset: true })
|
const app = new Hype({ pico: true, reset: true })
|
||||||
```
|
```
|
||||||
|
|
||||||
### Inline CSS
|
|
||||||
|
|
||||||
Use the `css` template tag for scoped inline styles in any page:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { css } from '@because/hype'
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<section>
|
|
||||||
{css`
|
|
||||||
.hero {
|
|
||||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
||||||
color: white;
|
|
||||||
padding: 4rem 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
<div class="hero">
|
|
||||||
<h1>Welcome</h1>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
This renders a `<style>` tag inline. Install the `vscode-styled-components` extension for syntax highlighting.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Server-Sent Events (SSE)
|
## Server-Sent Events (SSE)
|
||||||
|
|
@ -662,30 +634,31 @@ Return a cleanup function to handle client disconnection.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// src/pages/sse.tsx
|
// src/pages/sse.tsx
|
||||||
import { js } from '@because/hype'
|
|
||||||
|
|
||||||
export default () => (
|
export default () => (
|
||||||
<section>
|
<section>
|
||||||
<h1>SSE Demo</h1>
|
<h1>SSE Demo</h1>
|
||||||
<div id="time" style="font-size: 2em; font-family: monospace;"></div>
|
<div id="time" style="font-size: 2em; font-family: monospace;"></div>
|
||||||
|
|
||||||
{js`
|
<script src="/client/main.ts" type="module"></script>
|
||||||
const timeEl = document.getElementById('time')
|
|
||||||
const events = new EventSource('/api/time')
|
|
||||||
|
|
||||||
events.onmessage = (e) => {
|
|
||||||
const data = JSON.parse(e.data)
|
|
||||||
timeEl.textContent = new Date(data.time).toLocaleTimeString()
|
|
||||||
}
|
|
||||||
|
|
||||||
events.onerror = () => {
|
|
||||||
timeEl.textContent = 'Disconnected'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/client/main.ts
|
||||||
|
const timeEl = document.getElementById('time')
|
||||||
|
const events = new EventSource('/api/time')
|
||||||
|
|
||||||
|
events.onmessage = (e) => {
|
||||||
|
const data = JSON.parse(e.data)
|
||||||
|
timeEl!.textContent = new Date(data.time).toLocaleTimeString()
|
||||||
|
}
|
||||||
|
|
||||||
|
events.onerror = () => {
|
||||||
|
timeEl!.textContent = 'Disconnected'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Named events
|
### Named events
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|
@ -717,149 +690,6 @@ curl -N http://localhost:3000/api/time
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## The `fe()` Helper
|
|
||||||
|
|
||||||
`fe()` (short for "frontend") lets you define JavaScript functions on the server and use them as inline event handlers in SSR pages. The function bodies are automatically extracted and injected as a `<script>` before `</body>`.
|
|
||||||
|
|
||||||
### Basic usage
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { fe } from '@because/hype'
|
|
||||||
|
|
||||||
const showAlert = fe(() => {
|
|
||||||
alert('ding dong')
|
|
||||||
})
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<section>
|
|
||||||
<button onclick={showAlert}>Click me</button>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
`fe()` returns a string like `"frontendFn0()"` which works as an `onclick` attribute value. The actual function is injected once in a `<script>` tag at the end of the page.
|
|
||||||
|
|
||||||
### Passing arguments
|
|
||||||
|
|
||||||
Pass server data to frontend functions:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { fe } from '@because/hype'
|
|
||||||
|
|
||||||
const greet = (name: string) => fe((args) => {
|
|
||||||
alert(`Hello, ${args.name}!`)
|
|
||||||
}, { name })
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<section>
|
|
||||||
<button onclick={greet('Chris')}>Greet Chris</button>
|
|
||||||
<button onclick={greet('Alex')}>Greet Alex</button>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Arguments are JSON-serialized and passed at call time. The function body is deduplicated — if the same function is used with different arguments, only one copy of the function is injected.
|
|
||||||
|
|
||||||
### Deduplication
|
|
||||||
|
|
||||||
`fe()` deduplicates by function body. If you call `fe()` with the same function multiple times, it only injects one copy:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { fe } from '@because/hype'
|
|
||||||
|
|
||||||
const handleClick = fe(() => {
|
|
||||||
console.log('clicked!')
|
|
||||||
})
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<section>
|
|
||||||
{/* Both buttons share the same injected function */}
|
|
||||||
<button onclick={handleClick}>Button 1</button>
|
|
||||||
<button onclick={handleClick}>Button 2</button>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Inline `css` and `js` Tags
|
|
||||||
|
|
||||||
### `css` template tag
|
|
||||||
|
|
||||||
Returns a `<style>` element. Use it anywhere in your JSX:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { css } from '@because/hype'
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<section>
|
|
||||||
{css`
|
|
||||||
.card {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
<div class="card">
|
|
||||||
<h2>Card Title</h2>
|
|
||||||
<p>Card content</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### `js` template tag
|
|
||||||
|
|
||||||
Returns a `<script>` element. The content is **transpiled from TypeScript** on the server using Bun's transpiler, so you can write TypeScript directly:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { js } from '@because/hype'
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<section>
|
|
||||||
<div id="output"></div>
|
|
||||||
|
|
||||||
{js`
|
|
||||||
interface TimeData {
|
|
||||||
formatted: string
|
|
||||||
timestamp: number
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTime(ts: number): TimeData {
|
|
||||||
return {
|
|
||||||
formatted: new Date(ts).toLocaleTimeString(),
|
|
||||||
timestamp: ts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const el = document.getElementById('output')!
|
|
||||||
setInterval(() => {
|
|
||||||
el.textContent = formatTime(Date.now()).formatted
|
|
||||||
}, 1000)
|
|
||||||
`}
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Interpolation
|
|
||||||
|
|
||||||
Both tags support template literal interpolation:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
const primaryColor = '#3498db'
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<section>
|
|
||||||
{css`
|
|
||||||
h1 { color: ${primaryColor}; }
|
|
||||||
`}
|
|
||||||
<h1>Styled heading</h1>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Custom API Routes
|
## Custom API Routes
|
||||||
|
|
||||||
Since Hype extends Hono, use any Hono routing method:
|
Since Hype extends Hono, use any Hono routing method:
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
A complete guide to building web apps with Hype and Forge.
|
A complete guide to building web apps with Hype and Forge.
|
||||||
|
|
||||||
**Hype** is a thin wrapper around [Hono](https://hono.dev) for building web apps with Bun. It provides file-based routing, automatic TypeScript transpilation, SSE, inline CSS/JS helpers, and a default HTML5 layout — all without a build step. Since `Hype extends Hono`, every Hono API works out of the box.
|
**Hype** is a thin wrapper around [Hono](https://hono.dev) for building web apps with Bun. It provides file-based routing, automatic TypeScript transpilation, SSE, and a default HTML5 layout — all without a build step. Since `Hype extends Hono`, every Hono API works out of the box.
|
||||||
|
|
||||||
**Forge** is a component library that generates real CSS from JSX definitions. Each `define()` call produces a typed component with scoped CSS classes. It works on both server and client — on the server it collects CSS for `<Styles />` to inline, in the browser it auto-injects a `<style>` tag.
|
**Forge** is a component library that generates real CSS from JSX definitions. Each `define()` call produces a typed component with scoped CSS classes. It works on both server and client — on the server it collects CSS for `<Styles />` to inline, in the browser it auto-injects a `<style>` tag.
|
||||||
|
|
||||||
|
|
@ -20,8 +20,6 @@ A complete guide to building web apps with Hype and Forge.
|
||||||
- [Client-Side JavaScript](#client-side-javascript)
|
- [Client-Side JavaScript](#client-side-javascript)
|
||||||
- [Styling](#styling)
|
- [Styling](#styling)
|
||||||
- [Server-Sent Events (SSE)](#server-sent-events-sse)
|
- [Server-Sent Events (SSE)](#server-sent-events-sse)
|
||||||
- [The `fe()` Helper](#the-fe-helper)
|
|
||||||
- [Inline `css` and `js` Tags](#inline-css-and-js-tags)
|
|
||||||
- [Custom API Routes](#custom-api-routes)
|
- [Custom API Routes](#custom-api-routes)
|
||||||
- [Sub-Routers](#sub-routers)
|
- [Sub-Routers](#sub-routers)
|
||||||
- [Static Files](#static-files)
|
- [Static Files](#static-files)
|
||||||
|
|
@ -538,30 +536,31 @@ Return a cleanup function to handle client disconnection.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// src/pages/sse.tsx
|
// src/pages/sse.tsx
|
||||||
import { js } from '@because/hype'
|
|
||||||
|
|
||||||
export default () => (
|
export default () => (
|
||||||
<section>
|
<section>
|
||||||
<h1>SSE Demo</h1>
|
<h1>SSE Demo</h1>
|
||||||
<div id="time" style="font-size: 2em; font-family: monospace;"></div>
|
<div id="time" style="font-size: 2em; font-family: monospace;"></div>
|
||||||
|
|
||||||
{js`
|
<script src="/client/main.ts" type="module"></script>
|
||||||
const timeEl = document.getElementById('time')
|
|
||||||
const events = new EventSource('/api/time')
|
|
||||||
|
|
||||||
events.onmessage = (e) => {
|
|
||||||
const data = JSON.parse(e.data)
|
|
||||||
timeEl.textContent = new Date(data.time).toLocaleTimeString()
|
|
||||||
}
|
|
||||||
|
|
||||||
events.onerror = () => {
|
|
||||||
timeEl.textContent = 'Disconnected'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/client/main.ts
|
||||||
|
const timeEl = document.getElementById('time')
|
||||||
|
const events = new EventSource('/api/time')
|
||||||
|
|
||||||
|
events.onmessage = (e) => {
|
||||||
|
const data = JSON.parse(e.data)
|
||||||
|
timeEl!.textContent = new Date(data.time).toLocaleTimeString()
|
||||||
|
}
|
||||||
|
|
||||||
|
events.onerror = () => {
|
||||||
|
timeEl!.textContent = 'Disconnected'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Named events
|
### Named events
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|
@ -593,149 +592,6 @@ curl -N http://localhost:3000/api/time
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## The `fe()` Helper
|
|
||||||
|
|
||||||
`fe()` (short for "frontend") lets you define JavaScript functions on the server and use them as inline event handlers in SSR pages. The function bodies are automatically extracted and injected as a `<script>` before `</body>`.
|
|
||||||
|
|
||||||
### Basic usage
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { fe } from '@because/hype'
|
|
||||||
|
|
||||||
const showAlert = fe(() => {
|
|
||||||
alert('ding dong')
|
|
||||||
})
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<section>
|
|
||||||
<button onclick={showAlert}>Click me</button>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
`fe()` returns a string like `"frontendFn0()"` which works as an `onclick` attribute value. The actual function is injected once in a `<script>` tag at the end of the page.
|
|
||||||
|
|
||||||
### Passing arguments
|
|
||||||
|
|
||||||
Pass server data to frontend functions:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { fe } from '@because/hype'
|
|
||||||
|
|
||||||
const greet = (name: string) => fe((args) => {
|
|
||||||
alert(`Hello, ${args.name}!`)
|
|
||||||
}, { name })
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<section>
|
|
||||||
<button onclick={greet('Chris')}>Greet Chris</button>
|
|
||||||
<button onclick={greet('Alex')}>Greet Alex</button>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Arguments are JSON-serialized and passed at call time. The function body is deduplicated — if the same function is used with different arguments, only one copy of the function is injected.
|
|
||||||
|
|
||||||
### Deduplication
|
|
||||||
|
|
||||||
`fe()` deduplicates by function body. If you call `fe()` with the same function multiple times, it only injects one copy:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { fe } from '@because/hype'
|
|
||||||
|
|
||||||
const handleClick = fe(() => {
|
|
||||||
console.log('clicked!')
|
|
||||||
})
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<section>
|
|
||||||
{/* Both buttons share the same injected function */}
|
|
||||||
<button onclick={handleClick}>Button 1</button>
|
|
||||||
<button onclick={handleClick}>Button 2</button>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Inline `css` and `js` Tags
|
|
||||||
|
|
||||||
### `css` template tag
|
|
||||||
|
|
||||||
Returns a `<style>` element. Use it anywhere in your JSX:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { css } from '@because/hype'
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<section>
|
|
||||||
{css`
|
|
||||||
.card {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
<div class="card">
|
|
||||||
<h2>Card Title</h2>
|
|
||||||
<p>Card content</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### `js` template tag
|
|
||||||
|
|
||||||
Returns a `<script>` element. The content is **transpiled from TypeScript** on the server using Bun's transpiler, so you can write TypeScript directly:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { js } from '@because/hype'
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<section>
|
|
||||||
<div id="output"></div>
|
|
||||||
|
|
||||||
{js`
|
|
||||||
interface TimeData {
|
|
||||||
formatted: string
|
|
||||||
timestamp: number
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTime(ts: number): TimeData {
|
|
||||||
return {
|
|
||||||
formatted: new Date(ts).toLocaleTimeString(),
|
|
||||||
timestamp: ts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const el = document.getElementById('output')!
|
|
||||||
setInterval(() => {
|
|
||||||
el.textContent = formatTime(Date.now()).formatted
|
|
||||||
}, 1000)
|
|
||||||
`}
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Interpolation
|
|
||||||
|
|
||||||
Both tags support template literal interpolation:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
const primaryColor = '#3498db'
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<section>
|
|
||||||
{css`
|
|
||||||
h1 { color: ${primaryColor}; }
|
|
||||||
`}
|
|
||||||
<h1>Styled heading</h1>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Custom API Routes
|
## Custom API Routes
|
||||||
|
|
||||||
Since Hype extends Hono, use any Hono routing method:
|
Since Hype extends Hono, use any Hono routing method:
|
||||||
|
|
@ -1852,43 +1708,25 @@ export default () => (
|
||||||
|
|
||||||
### Adding client interactivity to SSR
|
### Adding client interactivity to SSR
|
||||||
|
|
||||||
For light interactivity on SSR pages, use Hype's `fe()` helper alongside Forge components:
|
For interactivity on SSR pages, use a client-side script alongside Forge components:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// src/pages/index.tsx
|
// src/pages/index.tsx
|
||||||
import { fe } from '@because/hype'
|
|
||||||
import { Button } from '@components'
|
import { Button } from '@components'
|
||||||
|
|
||||||
const handleClick = fe(() => {
|
|
||||||
alert('Clicked!')
|
|
||||||
})
|
|
||||||
|
|
||||||
export default () => (
|
export default () => (
|
||||||
<section>
|
<section>
|
||||||
<Button intent="primary" onclick={handleClick}>Click me</Button>
|
<Button intent="primary" id="my-btn">Click me</Button>
|
||||||
|
<script src="/client/main.ts" type="module"></script>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Or use Hype's `js` tag for inline scripts:
|
```ts
|
||||||
|
// src/client/main.ts
|
||||||
```tsx
|
document.getElementById('my-btn')?.addEventListener('click', () => {
|
||||||
import { js } from '@because/hype'
|
alert('Clicked!')
|
||||||
import { Card } from '@components'
|
})
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<section>
|
|
||||||
<Card title="Live Clock">
|
|
||||||
<span id="clock">--:--:--</span>
|
|
||||||
</Card>
|
|
||||||
{js`
|
|
||||||
setInterval(() => {
|
|
||||||
document.getElementById('clock')!.textContent =
|
|
||||||
new Date().toLocaleTimeString()
|
|
||||||
}, 1000)
|
|
||||||
`}
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### SSR with Forge + SSE
|
### SSR with Forge + SSE
|
||||||
|
|
@ -1914,7 +1752,6 @@ export default app.defaults
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// src/pages/index.tsx
|
// src/pages/index.tsx
|
||||||
import { js } from '@because/hype'
|
|
||||||
import { Card, Badge } from '@components'
|
import { Card, Badge } from '@components'
|
||||||
|
|
||||||
export default () => (
|
export default () => (
|
||||||
|
|
@ -1922,17 +1759,20 @@ export default () => (
|
||||||
<Card title="Live Counter">
|
<Card title="Live Counter">
|
||||||
Count: <Badge><span id="count">--</span></Badge>
|
Count: <Badge><span id="count">--</span></Badge>
|
||||||
</Card>
|
</Card>
|
||||||
{js`
|
<script src="/client/main.ts" type="module"></script>
|
||||||
const el = document.getElementById('count')!
|
|
||||||
const events = new EventSource('/api/counter')
|
|
||||||
events.onmessage = (e) => {
|
|
||||||
el.textContent = JSON.parse(e.data).count
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/client/main.ts
|
||||||
|
const el = document.getElementById('count')!
|
||||||
|
const events = new EventSource('/api/counter')
|
||||||
|
events.onmessage = (e) => {
|
||||||
|
el.textContent = JSON.parse(e.data).count
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## SPA with Forge
|
## SPA with Forge
|
||||||
|
|
@ -2316,7 +2156,7 @@ Hype's bundler handles the import in both cases. On the server, `define()` regis
|
||||||
| | SSR | SPA |
|
| | SSR | SPA |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Styles** | `<Styles />` in the layout head | Auto-injected by Forge in browser |
|
| **Styles** | `<Styles />` in the layout head | Auto-injected by Forge in browser |
|
||||||
| **Interactivity** | `fe()`, `js` tag, or link to client scripts | Full `hono/jsx/dom` with hooks |
|
| **Interactivity** | Client scripts via `/client/main.ts` | Full `hono/jsx/dom` with hooks |
|
||||||
| **Routing** | File-based, one page per route | Client-side, single HTML shell |
|
| **Routing** | File-based, one page per route | Client-side, single HTML shell |
|
||||||
| **Data** | Fetch in page components (`c`, `req`) | Fetch in `useEffect`, SSE, etc. |
|
| **Data** | Fetch in page components (`c`, `req`) | Fetch in `useEffect`, SSE, etc. |
|
||||||
| **Best for** | Content sites, dashboards, forms | Interactive apps, real-time UIs |
|
| **Best for** | Content sites, dashboards, forms | Interactive apps, real-time UIs |
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
import { fe } from '@because/hype'
|
|
||||||
|
|
||||||
const test = fe(() => {
|
|
||||||
alert('ding dong')
|
|
||||||
})
|
|
||||||
|
|
||||||
export default () => (
|
export default () => (
|
||||||
<section>
|
<section>
|
||||||
<a href="#" onclick={test}>test</a>
|
<a href="#" onclick="alert('ding dong')">test</a>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { js } from '@because/hype'
|
|
||||||
|
|
||||||
export default () => (
|
export default () => (
|
||||||
<section>
|
<section>
|
||||||
<h1>SSE Demo</h1>
|
<h1>SSE Demo</h1>
|
||||||
|
|
@ -12,7 +10,8 @@ export default () => (
|
||||||
<a href="/">Back to Home</a>
|
<a href="/">Back to Home</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{js`
|
<script dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
const statusEl = document.getElementById('status')
|
const statusEl = document.getElementById('status')
|
||||||
const timeEl = document.getElementById('time')
|
const timeEl = document.getElementById('time')
|
||||||
const countEl = document.getElementById('count')
|
const countEl = document.getElementById('count')
|
||||||
|
|
@ -36,6 +35,6 @@ export default () => (
|
||||||
statusEl.textContent = 'Disconnected'
|
statusEl.textContent = 'Disconnected'
|
||||||
statusEl.style.color = 'red'
|
statusEl.style.color = 'red'
|
||||||
}
|
}
|
||||||
`}
|
`}} />
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import { AsyncLocalStorage } from 'async_hooks'
|
|
||||||
|
|
||||||
export const fnStorage = new AsyncLocalStorage<{
|
|
||||||
fns: Map<string, string>
|
|
||||||
counter: number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
export function fe<T extends Record<string, unknown>>(
|
|
||||||
fn: (args?: T) => void,
|
|
||||||
args?: T
|
|
||||||
): string {
|
|
||||||
const store = fnStorage.getStore()
|
|
||||||
if (!store) {
|
|
||||||
// Fallback to IIFE if outside request context
|
|
||||||
return args
|
|
||||||
? `(${fn.toString()})(${JSON.stringify(args)})`
|
|
||||||
: `(${fn.toString()})()`
|
|
||||||
}
|
|
||||||
|
|
||||||
const fnStr = fn.toString()
|
|
||||||
|
|
||||||
// Dedupe by function body
|
|
||||||
for (const [name, body] of store.fns)
|
|
||||||
if (body === fnStr)
|
|
||||||
return args ? `${name}(${JSON.stringify(args)})` : `${name}()`
|
|
||||||
|
|
||||||
const name = `frontendFn${store.counter++}`
|
|
||||||
store.fns.set(name, fnStr)
|
|
||||||
|
|
||||||
return args ? `${name}(${JSON.stringify(args)})` : `${name}()`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function feFunctions(): string[] {
|
|
||||||
const store = fnStorage.getStore()
|
|
||||||
if (!store?.fns.size) return []
|
|
||||||
|
|
||||||
return [...store.fns.entries()].map(([name, body]) => {
|
|
||||||
// Handle arrow functions vs regular functions
|
|
||||||
if (body.startsWith('(') || body.startsWith('async ('))
|
|
||||||
return `const ${name} = ${body};`
|
|
||||||
|
|
||||||
// Named function - rename it
|
|
||||||
return body.replace(/^(async\s+)?function\s*\w*/, `$1function ${name}`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep for backwards compat
|
|
||||||
export const frontend = fe
|
|
||||||
|
|
@ -7,14 +7,12 @@ import color from 'kleur'
|
||||||
|
|
||||||
import { transpile } from './utils'
|
import { transpile } from './utils'
|
||||||
import defaultLayout from './layout'
|
import defaultLayout from './layout'
|
||||||
import { feFunctions, fnStorage } from './frontend'
|
|
||||||
|
|
||||||
const SHOW_HTTP_LOG = true
|
const SHOW_HTTP_LOG = true
|
||||||
const CSS_RESET = await Bun.file(join(import.meta.dir, '/css/reset.css')).text()
|
const CSS_RESET = await Bun.file(join(import.meta.dir, '/css/reset.css')).text()
|
||||||
const PICO_CSS = await Bun.file(join(import.meta.dir, '/css/pico.css')).text()
|
const PICO_CSS = await Bun.file(join(import.meta.dir, '/css/pico.css')).text()
|
||||||
|
|
||||||
export * from './utils'
|
export * from './utils'
|
||||||
export { frontend, frontend as fe } from './frontend'
|
|
||||||
export type { Context } from 'hono'
|
export type { Context } from 'hono'
|
||||||
|
|
||||||
const pageCache = new Map()
|
const pageCache = new Map()
|
||||||
|
|
@ -146,26 +144,6 @@ export class Hype<
|
||||||
if (this.props.ok)
|
if (this.props.ok)
|
||||||
this.get('/ok', c => c.text('ok'))
|
this.get('/ok', c => c.text('ok'))
|
||||||
|
|
||||||
// serve frontend js
|
|
||||||
this.use('*', async (c, next) => {
|
|
||||||
await fnStorage.run({ fns: new Map(), counter: 0 }, async () => {
|
|
||||||
await next()
|
|
||||||
|
|
||||||
const contentType = c.res.headers.get('content-type')
|
|
||||||
if (!contentType?.includes('text/html')) return
|
|
||||||
|
|
||||||
const fns = feFunctions()
|
|
||||||
if (!fns.length) return
|
|
||||||
|
|
||||||
const res = c.res.clone()
|
|
||||||
const html = await res.text()
|
|
||||||
const newHtml = html.replace('</body>', `<script>${fns.join('\n')}</script></body>`)
|
|
||||||
const headers = new Headers(c.res.headers)
|
|
||||||
headers.delete('content-length')
|
|
||||||
c.res = new Response(newHtml, { status: c.res.status, headers })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// css reset
|
// css reset
|
||||||
this.get('/css/reset.css', async c => new Response(CSS_RESET, { headers: { 'Content-Type': 'text/css' } }))
|
this.get('/css/reset.css', async c => new Response(CSS_RESET, { headers: { 'Content-Type': 'text/css' } }))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,6 @@
|
||||||
import { type Context } from 'hono'
|
import { type Context } from 'hono'
|
||||||
import { stat } from 'fs/promises'
|
import { stat } from 'fs/promises'
|
||||||
|
|
||||||
// template literal tag for inline CSS. returns a <style> tag
|
|
||||||
export function css(strings: TemplateStringsArray, ...values: any[]) {
|
|
||||||
return <style dangerouslySetInnerHTML={{
|
|
||||||
__html: String.raw({ raw: strings }, ...values)
|
|
||||||
}} />
|
|
||||||
}
|
|
||||||
|
|
||||||
const transpiler = new Bun.Transpiler({ loader: 'tsx' })
|
|
||||||
|
|
||||||
// template literal tag for inline JS. transpiles and returns a <script> tag
|
|
||||||
export function js(strings: TemplateStringsArray, ...values: any[]) {
|
|
||||||
return <script dangerouslySetInnerHTML={{
|
|
||||||
__html: transpiler.transformSync(String.raw({ raw: strings }, ...values))
|
|
||||||
}} />
|
|
||||||
}
|
|
||||||
|
|
||||||
// lighten a hex color by blending with white. opacity 1 = original, 0 = white
|
// lighten a hex color by blending with white. opacity 1 = original, 0 = white
|
||||||
export function lightenColor(hex: string, opacity: number): string {
|
export function lightenColor(hex: string, opacity: number): string {
|
||||||
// Remove # if present
|
// Remove # if present
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user