howl
This commit is contained in:
commit
67fae70c83
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# dependencies (bun install)
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# output
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# code coverage
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.eslintcache
|
||||||
|
.cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
111
CLAUDE.md
Normal file
111
CLAUDE.md
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
---
|
||||||
|
description: Use Bun instead of Node.js, npm, pnpm, or vite.
|
||||||
|
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
Default to using Bun instead of Node.js.
|
||||||
|
|
||||||
|
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
||||||
|
- Use `bun test` instead of `jest` or `vitest`
|
||||||
|
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
||||||
|
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
||||||
|
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
||||||
|
- Bun automatically loads .env, so don't use dotenv.
|
||||||
|
|
||||||
|
## APIs
|
||||||
|
|
||||||
|
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
||||||
|
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
||||||
|
- `Bun.redis` for Redis. Don't use `ioredis`.
|
||||||
|
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
||||||
|
- `WebSocket` is built-in. Don't use `ws`.
|
||||||
|
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
||||||
|
- Bun.$`ls` instead of execa.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Use `bun test` to run tests.
|
||||||
|
|
||||||
|
```ts#index.test.ts
|
||||||
|
import { test, expect } from "bun:test";
|
||||||
|
|
||||||
|
test("hello world", () => {
|
||||||
|
expect(1).toBe(1);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
||||||
|
|
||||||
|
Server:
|
||||||
|
|
||||||
|
```ts#index.ts
|
||||||
|
import index from "./index.html"
|
||||||
|
|
||||||
|
Bun.serve({
|
||||||
|
routes: {
|
||||||
|
"/": index,
|
||||||
|
"/api/users/:id": {
|
||||||
|
GET: (req) => {
|
||||||
|
return new Response(JSON.stringify({ id: req.params.id }));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// optional websocket support
|
||||||
|
websocket: {
|
||||||
|
open: (ws) => {
|
||||||
|
ws.send("Hello, world!");
|
||||||
|
},
|
||||||
|
message: (ws, message) => {
|
||||||
|
ws.send(message);
|
||||||
|
},
|
||||||
|
close: (ws) => {
|
||||||
|
// handle close
|
||||||
|
}
|
||||||
|
},
|
||||||
|
development: {
|
||||||
|
hmr: true,
|
||||||
|
console: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
||||||
|
|
||||||
|
```html#index.html
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Hello, world!</h1>
|
||||||
|
<script type="module" src="./frontend.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
With the following `frontend.tsx`:
|
||||||
|
|
||||||
|
```tsx#frontend.tsx
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// import .css files directly and it works
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
const root = createRoot(document.body);
|
||||||
|
|
||||||
|
export default function Frontend() {
|
||||||
|
return <h1>Hello, world!</h1>;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.render(<Frontend />);
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, run index.ts
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun --hot ./index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
|
||||||
8
README.md
Normal file
8
README.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# 🐺 howl
|
||||||
|
|
||||||
|
Howl is a fork of `werewolf-ui`, without any Tailwind.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
33
bun.lock
Normal file
33
bun.lock
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "howl",
|
||||||
|
"dependencies": {
|
||||||
|
"hono": "^4.10.7",
|
||||||
|
"lucide-static": "^0.555.0",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
|
||||||
|
|
||||||
|
"hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="],
|
||||||
|
|
||||||
|
"lucide-static": ["lucide-static@0.555.0", "", {}, "sha512-FMMaYYsEYsUA6xlEzIMoKEV3oGnxIIvAN+AtLmYXvlTJptJTveJjVBQwvtA/zZLrD6KLEu89G95dQYlhivw5jQ=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
18
package.json
Normal file
18
package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "howl",
|
||||||
|
"module": "index.tsx",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run --hot test/server.tsx"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"hono": "^4.10.7",
|
||||||
|
"lucide-static": "^0.555.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/avatar.tsx
Normal file
117
src/avatar.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
import "hono/jsx"
|
||||||
|
import type { FC, JSX } from "hono/jsx"
|
||||||
|
import { VStack, HStack } from "./stack"
|
||||||
|
|
||||||
|
export type AvatarProps = {
|
||||||
|
src: string
|
||||||
|
alt?: string
|
||||||
|
size?: number
|
||||||
|
rounded?: boolean
|
||||||
|
style?: JSX.CSSProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Avatar: FC<AvatarProps> = (props) => {
|
||||||
|
const { src, size = 32, rounded, style, alt = "" } = props
|
||||||
|
|
||||||
|
const avatarStyle: JSX.CSSProperties = {
|
||||||
|
width: `${size}px`,
|
||||||
|
height: `${size}px`,
|
||||||
|
borderRadius: rounded ? "9999px" : undefined,
|
||||||
|
...style,
|
||||||
|
}
|
||||||
|
|
||||||
|
return <img src={src} alt={alt} style={avatarStyle} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Test = () => {
|
||||||
|
const sampleImages = [
|
||||||
|
"https://picsum.photos/seed/3/200/200",
|
||||||
|
"https://picsum.photos/seed/2/200/200",
|
||||||
|
"https://picsum.photos/seed/8/200/200",
|
||||||
|
"https://picsum.photos/seed/9/200/200",
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack gap={8} style={{ padding: "24px" }}>
|
||||||
|
{/* Size variations */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Size Variations</h2>
|
||||||
|
<HStack gap={4}>
|
||||||
|
{[24, 32, 48, 64, 96].map((size) => (
|
||||||
|
<VStack key={size} h="center" gap={2}>
|
||||||
|
<Avatar src={sampleImages[0]!} size={size} alt="Sample" />
|
||||||
|
<p style={{ fontSize: "14px" }}>
|
||||||
|
{size}x{size}
|
||||||
|
</p>
|
||||||
|
</VStack>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Rounded vs Square */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Rounded vs Square</h2>
|
||||||
|
<HStack gap={6}>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Avatar src={sampleImages[0]!} size={64} alt="Sample" />
|
||||||
|
<p style={{ fontSize: "14px" }}>Square</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Avatar src={sampleImages[0]!} size={64} rounded alt="Sample" />
|
||||||
|
<p style={{ fontSize: "14px" }}>Rounded</p>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Different images */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Different Images</h2>
|
||||||
|
<HStack gap={4}>
|
||||||
|
{sampleImages.map((src, index) => (
|
||||||
|
<VStack key={src} h="center" gap={2}>
|
||||||
|
<Avatar src={src} size={64} rounded alt={`Sample ${index + 1}`} />
|
||||||
|
<p style={{ fontSize: "14px" }}>Image {index + 1}</p>
|
||||||
|
</VStack>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Custom styles */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>With Custom Styles</h2>
|
||||||
|
<HStack gap={6}>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Avatar
|
||||||
|
src={sampleImages[0]!}
|
||||||
|
size={64}
|
||||||
|
rounded
|
||||||
|
style={{ border: "4px solid #3b82f6" }}
|
||||||
|
alt="With border"
|
||||||
|
/>
|
||||||
|
<p style={{ fontSize: "14px" }}>With Border</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Avatar
|
||||||
|
src={sampleImages[1]!}
|
||||||
|
size={64}
|
||||||
|
rounded
|
||||||
|
style={{ boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)" }}
|
||||||
|
alt="With shadow"
|
||||||
|
/>
|
||||||
|
<p style={{ fontSize: "14px" }}>With Shadow</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Avatar
|
||||||
|
src={sampleImages[2]!}
|
||||||
|
size={64}
|
||||||
|
rounded
|
||||||
|
style={{ border: "4px solid #22c55e", boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)" }}
|
||||||
|
alt="Border + shadow"
|
||||||
|
/>
|
||||||
|
<p style={{ fontSize: "14px" }}>Border + Shadow</p>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
134
src/button.tsx
Normal file
134
src/button.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
import "hono/jsx"
|
||||||
|
import type { JSX, FC } from "hono/jsx"
|
||||||
|
import { VStack, HStack } from "./stack"
|
||||||
|
|
||||||
|
export type ButtonProps = JSX.IntrinsicElements["button"] & {
|
||||||
|
variant?: "primary" | "secondary" | "outline" | "ghost" | "destructive"
|
||||||
|
size?: "sm" | "md" | "lg"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Button: FC<ButtonProps> = (props) => {
|
||||||
|
const { variant = "primary", size = "md", style, ...buttonProps } = props
|
||||||
|
|
||||||
|
const baseStyles: JSX.CSSProperties = {
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontWeight: "500",
|
||||||
|
transition: "all 0.2s",
|
||||||
|
outline: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
borderRadius: "4px",
|
||||||
|
border: "1px solid transparent",
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantStyles: Record<string, JSX.CSSProperties> = {
|
||||||
|
primary: {
|
||||||
|
backgroundColor: "#3b82f6",
|
||||||
|
color: "#ffffff",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
backgroundColor: "#64748b",
|
||||||
|
color: "#ffffff",
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
color: "#000000",
|
||||||
|
borderColor: "#d1d5db",
|
||||||
|
},
|
||||||
|
ghost: {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
color: "#000000",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
backgroundColor: "#ef4444",
|
||||||
|
color: "#ffffff",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeStyles: Record<string, JSX.CSSProperties> = {
|
||||||
|
sm: {
|
||||||
|
height: "32px",
|
||||||
|
padding: "0 12px",
|
||||||
|
fontSize: "14px",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
height: "40px",
|
||||||
|
padding: "0 16px",
|
||||||
|
fontSize: "14px",
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
height: "48px",
|
||||||
|
padding: "0 24px",
|
||||||
|
fontSize: "16px",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedStyles: JSX.CSSProperties = {
|
||||||
|
...baseStyles,
|
||||||
|
...variantStyles[variant],
|
||||||
|
...sizeStyles[size],
|
||||||
|
...(style || {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
return <button {...buttonProps} style={combinedStyles} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Test = () => {
|
||||||
|
return (
|
||||||
|
<VStack gap={8} style={{ padding: "24px" }}>
|
||||||
|
{/* Variants */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Button Variants</h2>
|
||||||
|
<HStack gap={4}>
|
||||||
|
<Button variant="primary">Primary</Button>
|
||||||
|
<Button variant="secondary">Secondary</Button>
|
||||||
|
<Button variant="outline">Outline</Button>
|
||||||
|
<Button variant="ghost">Ghost</Button>
|
||||||
|
<Button variant="destructive">Destructive</Button>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Sizes */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Button Sizes</h2>
|
||||||
|
<HStack gap={4} v="end">
|
||||||
|
<Button size="sm">Small</Button>
|
||||||
|
<Button size="md">Medium</Button>
|
||||||
|
<Button size="lg">Large</Button>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* With custom content */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Custom Content</h2>
|
||||||
|
<HStack gap={4}>
|
||||||
|
<Button variant="primary">
|
||||||
|
<span>🚀</span>
|
||||||
|
<span style={{ marginLeft: "8px" }}>Launch</span>
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" style={{ flexDirection: "column", height: "80px", width: "96px" }}>
|
||||||
|
<span style={{ fontSize: "24px" }}>💳</span>
|
||||||
|
<span style={{ fontSize: "12px", marginTop: "4px" }}>Card</span>
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Native attributes work */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Native Attributes</h2>
|
||||||
|
<HStack gap={4}>
|
||||||
|
<Button onClick={() => alert("Clicked!")} variant="primary">
|
||||||
|
Click Me
|
||||||
|
</Button>
|
||||||
|
<Button disabled variant="secondary" style={{ opacity: 0.5, pointerEvents: "none" }}>
|
||||||
|
Disabled
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" variant="outline">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
61
src/divider.tsx
Normal file
61
src/divider.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import "hono/jsx"
|
||||||
|
import type { FC, PropsWithChildren, JSX } from "hono/jsx"
|
||||||
|
import { VStack } from "./stack"
|
||||||
|
|
||||||
|
type DividerProps = PropsWithChildren & {
|
||||||
|
style?: JSX.CSSProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Divider: FC<DividerProps> = ({ children, style }) => {
|
||||||
|
const containerStyle: JSX.CSSProperties = {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
margin: "16px 0",
|
||||||
|
...style,
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineStyle: JSX.CSSProperties = {
|
||||||
|
flex: 1,
|
||||||
|
borderTop: "1px solid #d1d5db",
|
||||||
|
}
|
||||||
|
|
||||||
|
const textStyle: JSX.CSSProperties = {
|
||||||
|
padding: "0 12px",
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "#6b7280",
|
||||||
|
backgroundColor: "white",
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={containerStyle}>
|
||||||
|
<div style={lineStyle}></div>
|
||||||
|
{children && (
|
||||||
|
<>
|
||||||
|
<span style={textStyle}>{children}</span>
|
||||||
|
<div style={lineStyle}></div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Test = () => {
|
||||||
|
return (
|
||||||
|
<VStack gap={4} style={{ padding: "16px", maxWidth: "448px" }}>
|
||||||
|
<h2 style={{ fontSize: "18px", fontWeight: "bold" }}>Divider Examples</h2>
|
||||||
|
|
||||||
|
<VStack gap={0}>
|
||||||
|
<p>Would you like to continue</p>
|
||||||
|
<Divider>OR WOULD YOU LIKE TO</Divider>
|
||||||
|
<p>Submit to certain death</p>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Just a line */}
|
||||||
|
<VStack gap={0}>
|
||||||
|
<p>Look a line 👇</p>
|
||||||
|
<Divider />
|
||||||
|
<p>So cool, so straight!</p>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
158
src/grid.tsx
Normal file
158
src/grid.tsx
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
import type { TailwindSize } from "./types"
|
||||||
|
import "hono/jsx"
|
||||||
|
import type { FC, PropsWithChildren, JSX } from "hono/jsx"
|
||||||
|
import { VStack } from "./stack"
|
||||||
|
import { Button } from "./button"
|
||||||
|
|
||||||
|
type GridProps = PropsWithChildren & {
|
||||||
|
cols?: GridCols
|
||||||
|
gap?: TailwindSize
|
||||||
|
v?: keyof typeof alignItemsMap
|
||||||
|
h?: keyof typeof justifyItemsMap
|
||||||
|
style?: JSX.CSSProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
type GridCols = number | { sm?: number; md?: number; lg?: number; xl?: number }
|
||||||
|
|
||||||
|
export const Grid: FC<GridProps> = (props) => {
|
||||||
|
const { cols = 2, gap = 4, v, h, style, children } = props
|
||||||
|
|
||||||
|
const gapPx = gap * 4
|
||||||
|
|
||||||
|
const baseStyles: JSX.CSSProperties = {
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: getColumnsValue(cols),
|
||||||
|
gap: `${gapPx}px`,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v) {
|
||||||
|
baseStyles.alignItems = alignItemsMap[v]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (h) {
|
||||||
|
baseStyles.justifyItems = justifyItemsMap[h]
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedStyles = {
|
||||||
|
...baseStyles,
|
||||||
|
...style,
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div style={combinedStyles}>{children}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColumnsValue(cols: GridCols): string {
|
||||||
|
if (typeof cols === "number") {
|
||||||
|
return `repeat(${cols}, minmax(0, 1fr))`
|
||||||
|
}
|
||||||
|
|
||||||
|
// For responsive grids, we'll use the largest value
|
||||||
|
// In a real implementation, you'd want media queries which require CSS
|
||||||
|
// For now, let's use the largest value specified
|
||||||
|
const largestCols = cols.xl || cols.lg || cols.md || cols.sm || 1
|
||||||
|
return `repeat(${largestCols}, minmax(0, 1fr))`
|
||||||
|
}
|
||||||
|
|
||||||
|
const alignItemsMap = {
|
||||||
|
start: "start",
|
||||||
|
center: "center",
|
||||||
|
end: "end",
|
||||||
|
stretch: "stretch",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const justifyItemsMap = {
|
||||||
|
start: "start",
|
||||||
|
center: "center",
|
||||||
|
end: "end",
|
||||||
|
stretch: "stretch",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const Test = () => {
|
||||||
|
return (
|
||||||
|
<VStack gap={4} style={{ padding: "16px" }}>
|
||||||
|
<VStack gap={6}>
|
||||||
|
<h2 style={{ fontSize: "18px", fontWeight: "bold" }}>Grid Examples</h2>
|
||||||
|
|
||||||
|
{/* Simple 3-column grid */}
|
||||||
|
<VStack gap={2}>
|
||||||
|
<h3 style={{ fontSize: "16px", fontWeight: "600" }}>Simple 3 columns: cols=3</h3>
|
||||||
|
<Grid cols={3} gap={4}>
|
||||||
|
<div style={{ backgroundColor: "#fecaca", padding: "16px", textAlign: "center" }}>Item 1</div>
|
||||||
|
<div style={{ backgroundColor: "#bbf7d0", padding: "16px", textAlign: "center" }}>Item 2</div>
|
||||||
|
<div style={{ backgroundColor: "#bfdbfe", padding: "16px", textAlign: "center" }}>Item 3</div>
|
||||||
|
<div style={{ backgroundColor: "#fef08a", padding: "16px", textAlign: "center" }}>Item 4</div>
|
||||||
|
<div style={{ backgroundColor: "#e9d5ff", padding: "16px", textAlign: "center" }}>Item 5</div>
|
||||||
|
<div style={{ backgroundColor: "#fbcfe8", padding: "16px", textAlign: "center" }}>Item 6</div>
|
||||||
|
</Grid>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Responsive grid */}
|
||||||
|
<VStack gap={2}>
|
||||||
|
<h3 style={{ fontSize: "16px", fontWeight: "600" }}>
|
||||||
|
Responsive: cols={sm: 1, md: 2, lg: 3}
|
||||||
|
</h3>
|
||||||
|
<Grid cols={{ sm: 1, md: 2, lg: 3 }} gap={4}>
|
||||||
|
<div style={{ backgroundColor: "#fecaca", padding: "16px", textAlign: "center" }}>Card 1</div>
|
||||||
|
<div style={{ backgroundColor: "#bbf7d0", padding: "16px", textAlign: "center" }}>Card 2</div>
|
||||||
|
<div style={{ backgroundColor: "#bfdbfe", padding: "16px", textAlign: "center" }}>Card 3</div>
|
||||||
|
<div style={{ backgroundColor: "#fef08a", padding: "16px", textAlign: "center" }}>Card 4</div>
|
||||||
|
</Grid>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* More responsive examples */}
|
||||||
|
<VStack gap={2}>
|
||||||
|
<h3 style={{ fontSize: "16px", fontWeight: "600" }}>
|
||||||
|
More responsive: cols={sm: 2, lg: 4, xl: 6}
|
||||||
|
</h3>
|
||||||
|
<Grid cols={{ sm: 2, lg: 4, xl: 6 }} gap={4}>
|
||||||
|
<div style={{ backgroundColor: "#fecaca", padding: "16px", textAlign: "center" }}>Item A</div>
|
||||||
|
<div style={{ backgroundColor: "#bbf7d0", padding: "16px", textAlign: "center" }}>Item B</div>
|
||||||
|
<div style={{ backgroundColor: "#bfdbfe", padding: "16px", textAlign: "center" }}>Item C</div>
|
||||||
|
<div style={{ backgroundColor: "#fef08a", padding: "16px", textAlign: "center" }}>Item D</div>
|
||||||
|
<div style={{ backgroundColor: "#e9d5ff", padding: "16px", textAlign: "center" }}>Item E</div>
|
||||||
|
<div style={{ backgroundColor: "#fbcfe8", padding: "16px", textAlign: "center" }}>Item F</div>
|
||||||
|
</Grid>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Payment method example */}
|
||||||
|
<VStack gap={2}>
|
||||||
|
<h3 style={{ fontSize: "16px", fontWeight: "600" }}>Payment buttons example</h3>
|
||||||
|
<Grid cols={3} gap={4}>
|
||||||
|
<Button variant="outline" style={{ height: "80px", flexDirection: "column" }}>
|
||||||
|
<div style={{ fontSize: "24px" }}>💳</div>
|
||||||
|
<span style={{ fontSize: "12px" }}>Card</span>
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" style={{ height: "80px", flexDirection: "column" }}>
|
||||||
|
<div style={{ fontSize: "24px" }}>🍎</div>
|
||||||
|
<span style={{ fontSize: "12px" }}>Apple</span>
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" style={{ height: "80px", flexDirection: "column" }}>
|
||||||
|
<div style={{ fontSize: "24px" }}>💰</div>
|
||||||
|
<span style={{ fontSize: "12px" }}>PayPal</span>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Alignment examples */}
|
||||||
|
<VStack gap={2}>
|
||||||
|
<h3 style={{ fontSize: "16px", fontWeight: "600" }}>
|
||||||
|
Alignment: v="center" h="center"
|
||||||
|
</h3>
|
||||||
|
<Grid cols={3} gap={4} v="center" h="center" style={{ height: "128px", backgroundColor: "#f3f4f6" }}>
|
||||||
|
<div style={{ backgroundColor: "#fecaca", padding: "8px" }}>Item 1</div>
|
||||||
|
<div style={{ backgroundColor: "#bbf7d0", padding: "8px" }}>Item 2</div>
|
||||||
|
<div style={{ backgroundColor: "#bfdbfe", padding: "8px" }}>Item 3</div>
|
||||||
|
</Grid>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
<VStack gap={2}>
|
||||||
|
<h3 style={{ fontSize: "16px", fontWeight: "600" }}>Alignment: v="start" h="end"</h3>
|
||||||
|
<Grid cols={2} gap={4} v="start" h="end" style={{ height: "96px", backgroundColor: "#f3f4f6" }}>
|
||||||
|
<div style={{ backgroundColor: "#e9d5ff", padding: "8px" }}>Left</div>
|
||||||
|
<div style={{ backgroundColor: "#fed7aa", padding: "8px" }}>Right</div>
|
||||||
|
</Grid>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
210
src/icon.tsx
Normal file
210
src/icon.tsx
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
import "hono/jsx"
|
||||||
|
import type { FC, JSX } from "hono/jsx"
|
||||||
|
import * as icons from "lucide-static"
|
||||||
|
import { Grid } from "./grid"
|
||||||
|
import { VStack, HStack } from "./stack"
|
||||||
|
|
||||||
|
export type IconName = keyof typeof icons
|
||||||
|
|
||||||
|
type IconProps = {
|
||||||
|
name: IconName
|
||||||
|
size?: number
|
||||||
|
class?: string
|
||||||
|
style?: JSX.CSSProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
type IconLinkProps = IconProps & {
|
||||||
|
href?: string
|
||||||
|
target?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Icon: FC<IconProps> = (props) => {
|
||||||
|
const { name, size = 6, class: className, style } = props
|
||||||
|
|
||||||
|
const iconSvg = icons[name]
|
||||||
|
|
||||||
|
if (!iconSvg) {
|
||||||
|
throw new Error(`Icon "${name}" not found in Lucide icons`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pixelSize = sizeToPixels(size)
|
||||||
|
const iconStyle: JSX.CSSProperties = {
|
||||||
|
display: "block",
|
||||||
|
flexShrink: "0",
|
||||||
|
width: `${pixelSize}px`,
|
||||||
|
height: `${pixelSize}px`,
|
||||||
|
...style,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify the SVG string to include our custom attributes
|
||||||
|
const modifiedSvg = iconSvg
|
||||||
|
.replace(/width="[^"]*"/, "")
|
||||||
|
.replace(/height="[^"]*"/, "")
|
||||||
|
.replace(/class="[^"]*"/, "")
|
||||||
|
.replace(
|
||||||
|
/<svg([^>]*)>/,
|
||||||
|
`<svg$1 style="display: block; flex-shrink: 0; width: ${pixelSize}px; height: ${pixelSize}px;" class="${className || ""}">`
|
||||||
|
)
|
||||||
|
|
||||||
|
return <div dangerouslySetInnerHTML={{ __html: modifiedSvg }} style={iconStyle} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IconLink: FC<IconLinkProps> = (props) => {
|
||||||
|
const { href = "#", target, class: className, style, ...iconProps } = props
|
||||||
|
|
||||||
|
const linkStyle: JSX.CSSProperties = {
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
transition: "opacity 0.2s",
|
||||||
|
...style,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a href={href} target={target} class={className} style={linkStyle}>
|
||||||
|
<Icon {...iconProps} />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Test = () => {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: "24px" }}>
|
||||||
|
{/* === ICON TESTS === */}
|
||||||
|
|
||||||
|
{/* Size variations */}
|
||||||
|
<div style={{ marginBottom: "32px" }}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold", marginBottom: "16px" }}>Icon Size Variations</h2>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "16px" }}>
|
||||||
|
{([3, 4, 5, 6, 8, 10, 12, 16] as const).map((size) => (
|
||||||
|
<div key={size} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "8px" }}>
|
||||||
|
<Icon name="Heart" size={size} />
|
||||||
|
<p style={{ fontSize: "14px" }}>{size}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Styling with CSS classes */}
|
||||||
|
<div style={{ marginBottom: "32px" }}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold", marginBottom: "16px" }}>Styling with CSS Classes</h2>
|
||||||
|
<Grid cols={5} gap={6}>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Icon name="Star" size={12} />
|
||||||
|
<p style={{ fontSize: "14px" }}>Default</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Icon name="Star" size={12} style={{ color: "#3b82f6" }} />
|
||||||
|
<p style={{ fontSize: "14px" }}>Blue Color</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Icon name="Star" size={12} class="stroke-1" />
|
||||||
|
<p style={{ fontSize: "14px" }}>Thin Stroke</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Icon name="Star" size={12} style={{ color: "#fbbf24", fill: "currentColor", stroke: "none" }} />
|
||||||
|
<p style={{ fontSize: "14px" }}>Filled</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Icon name="Star" size={12} style={{ color: "#a855f7", transition: "color 0.2s" }} />
|
||||||
|
<p style={{ fontSize: "14px" }}>Hover Effect</p>
|
||||||
|
</VStack>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Advanced styling */}
|
||||||
|
<div style={{ marginBottom: "32px" }}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold", marginBottom: "16px" }}>Advanced Styling</h2>
|
||||||
|
<Grid cols={4} gap={6}>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Icon name="Heart" size={12} style={{ color: "#ef4444", fill: "currentColor", stroke: "none" }} />
|
||||||
|
<p style={{ fontSize: "14px" }}>Filled Heart</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Icon name="Shield" size={12} style={{ color: "#16a34a", strokeWidth: "2" }} />
|
||||||
|
<p style={{ fontSize: "14px" }}>Thick Stroke</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Icon name="Sun" size={12} style={{ color: "#eab308" }} />
|
||||||
|
<p style={{ fontSize: "14px" }}>Sun Icon</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Icon name="Zap" size={12} style={{ color: "#60a5fa", filter: "drop-shadow(0 4px 6px rgba(0,0,0,0.3))" }} />
|
||||||
|
<p style={{ fontSize: "14px" }}>Drop Shadow</p>
|
||||||
|
</VStack>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* === ICON LINK TESTS === */}
|
||||||
|
|
||||||
|
{/* Basic icon links */}
|
||||||
|
<div style={{ marginBottom: "32px" }}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold", marginBottom: "16px" }}>Icon Links</h2>
|
||||||
|
<div style={{ display: "flex", gap: "24px" }}>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "8px" }}>
|
||||||
|
<IconLink name="Home" size={8} href="/" />
|
||||||
|
<p style={{ fontSize: "14px" }}>Home Link</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "8px" }}>
|
||||||
|
<IconLink name="ExternalLink" size={8} href="https://example.com" target="_blank" />
|
||||||
|
<p style={{ fontSize: "14px" }}>External Link</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "8px" }}>
|
||||||
|
<IconLink name="Mail" size={8} href="mailto:hello@example.com" />
|
||||||
|
<p style={{ fontSize: "14px" }}>Email Link</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "8px" }}>
|
||||||
|
<IconLink name="Phone" size={8} href="tel:+1234567890" />
|
||||||
|
<p style={{ fontSize: "14px" }}>Phone Link</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Styled icon links */}
|
||||||
|
<div>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold", marginBottom: "16px" }}>Styled Icon Links</h2>
|
||||||
|
<Grid cols={4} gap={6}>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<IconLink
|
||||||
|
name="Download"
|
||||||
|
size={8}
|
||||||
|
href="#"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#3b82f6",
|
||||||
|
color: "white",
|
||||||
|
padding: "8px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p style={{ fontSize: "14px" }}>Button Style</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<IconLink
|
||||||
|
name="Settings"
|
||||||
|
size={8}
|
||||||
|
href="#"
|
||||||
|
style={{
|
||||||
|
border: "2px solid #d1d5db",
|
||||||
|
padding: "8px",
|
||||||
|
borderRadius: "9999px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p style={{ fontSize: "14px" }}>Circle Border</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<IconLink name="Heart" size={8} href="#" style={{ color: "#ef4444" }} />
|
||||||
|
<p style={{ fontSize: "14px" }}>Red Heart</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<IconLink name="Star" size={8} href="#" style={{ color: "#fbbf24", fill: "currentColor" }} />
|
||||||
|
<p style={{ fontSize: "14px" }}>Filled Star</p>
|
||||||
|
</VStack>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sizeToPixels(size: number): number {
|
||||||
|
return size * 4
|
||||||
|
}
|
||||||
219
src/image.tsx
Normal file
219
src/image.tsx
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
import "hono/jsx"
|
||||||
|
import type { FC, JSX } from "hono/jsx"
|
||||||
|
import { VStack, HStack } from "./stack"
|
||||||
|
import { Grid } from "./grid"
|
||||||
|
|
||||||
|
export type ImageProps = {
|
||||||
|
src: string
|
||||||
|
alt?: string
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
objectFit?: "cover" | "contain" | "fill" | "none" | "scale-down"
|
||||||
|
style?: JSX.CSSProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Image: FC<ImageProps> = ({ src, alt = "", width, height, objectFit, style }) => {
|
||||||
|
const imageStyle: JSX.CSSProperties = {
|
||||||
|
width: width ? `${width}px` : undefined,
|
||||||
|
height: height ? `${height}px` : undefined,
|
||||||
|
objectFit: objectFit,
|
||||||
|
...style,
|
||||||
|
}
|
||||||
|
|
||||||
|
return <img src={src} alt={alt} style={imageStyle} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Test = () => {
|
||||||
|
const sampleImages = [
|
||||||
|
"https://picsum.photos/seed/1/400/600", // Portrait
|
||||||
|
"https://picsum.photos/seed/2/600/400", // Landscape
|
||||||
|
"https://picsum.photos/seed/3/300/300", // Square
|
||||||
|
"https://picsum.photos/seed/4/200/100", // Small image
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack gap={8} style={{ padding: "24px" }}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Image Examples</h2>
|
||||||
|
|
||||||
|
{/* Size variations */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h3 style={{ fontSize: "18px", fontWeight: "600" }}>Size Variations</h3>
|
||||||
|
<HStack gap={4} wrap>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Image src={sampleImages[0]!} width={64} height={64} objectFit="cover" alt="64x64" />
|
||||||
|
<p style={{ fontSize: "14px" }}>64x64</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Image src={sampleImages[0]!} width={96} height={96} objectFit="cover" alt="96x96" />
|
||||||
|
<p style={{ fontSize: "14px" }}>96x96</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Image src={sampleImages[0]!} width={128} height={128} objectFit="cover" alt="128x128" />
|
||||||
|
<p style={{ fontSize: "14px" }}>128x128</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Image src={sampleImages[0]!} width={192} height={128} objectFit="cover" alt="192x128" />
|
||||||
|
<p style={{ fontSize: "14px" }}>192x128</p>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Object fit variations */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h3 style={{ fontSize: "18px", fontWeight: "600" }}>Object Fit Variations</h3>
|
||||||
|
<p style={{ fontSize: "14px", color: "#6b7280" }}>
|
||||||
|
Same image with different object-fit values
|
||||||
|
</p>
|
||||||
|
<HStack gap={6} wrap>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Image
|
||||||
|
src={sampleImages[0]!}
|
||||||
|
width={128}
|
||||||
|
height={128}
|
||||||
|
objectFit="cover"
|
||||||
|
style={{ border: "1px solid black" }}
|
||||||
|
alt="Object cover"
|
||||||
|
/>
|
||||||
|
<p style={{ fontSize: "14px" }}>object-fit: cover</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Image
|
||||||
|
src={sampleImages[0]!}
|
||||||
|
width={128}
|
||||||
|
height={128}
|
||||||
|
objectFit="contain"
|
||||||
|
style={{ border: "1px solid black", backgroundColor: "#f3f4f6" }}
|
||||||
|
alt="Object contain"
|
||||||
|
/>
|
||||||
|
<p style={{ fontSize: "14px" }}>object-fit: contain</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Image
|
||||||
|
src={sampleImages[0]!}
|
||||||
|
width={128}
|
||||||
|
height={128}
|
||||||
|
objectFit="fill"
|
||||||
|
style={{ border: "1px solid black" }}
|
||||||
|
alt="Object fill"
|
||||||
|
/>
|
||||||
|
<p style={{ fontSize: "14px" }}>object-fit: fill</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Image
|
||||||
|
src={sampleImages[0]!}
|
||||||
|
width={128}
|
||||||
|
height={128}
|
||||||
|
objectFit="scale-down"
|
||||||
|
style={{ border: "1px solid black", backgroundColor: "#f3f4f6" }}
|
||||||
|
alt="Object scale-down"
|
||||||
|
/>
|
||||||
|
<p style={{ fontSize: "14px" }}>object-fit: scale-down</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Image
|
||||||
|
src={sampleImages[0]!}
|
||||||
|
width={128}
|
||||||
|
height={128}
|
||||||
|
objectFit="none"
|
||||||
|
style={{ border: "1px solid black", backgroundColor: "#f3f4f6" }}
|
||||||
|
alt="Object none"
|
||||||
|
/>
|
||||||
|
<p style={{ fontSize: "14px" }}>object-fit: none</p>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Styling examples */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h3 style={{ fontSize: "18px", fontWeight: "600" }}>Styling Examples</h3>
|
||||||
|
<HStack gap={6} wrap>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Image
|
||||||
|
src={sampleImages[0]!}
|
||||||
|
width={128}
|
||||||
|
height={128}
|
||||||
|
objectFit="cover"
|
||||||
|
style={{ borderRadius: "8px", border: "4px solid #3b82f6" }}
|
||||||
|
alt="Rounded with border"
|
||||||
|
/>
|
||||||
|
<p style={{ fontSize: "14px" }}>Rounded + Border</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Image
|
||||||
|
src={sampleImages[1]!}
|
||||||
|
width={128}
|
||||||
|
height={128}
|
||||||
|
objectFit="cover"
|
||||||
|
style={{ boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)" }}
|
||||||
|
alt="With shadow"
|
||||||
|
/>
|
||||||
|
<p style={{ fontSize: "14px" }}>With Shadow</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Image
|
||||||
|
src={sampleImages[2]!}
|
||||||
|
width={128}
|
||||||
|
height={128}
|
||||||
|
objectFit="cover"
|
||||||
|
style={{
|
||||||
|
borderRadius: "9999px",
|
||||||
|
border: "4px solid #22c55e",
|
||||||
|
boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)",
|
||||||
|
}}
|
||||||
|
alt="Circular with effects"
|
||||||
|
/>
|
||||||
|
<p style={{ fontSize: "14px" }}>Circular + Effects</p>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Common use cases */}
|
||||||
|
<VStack gap={6}>
|
||||||
|
<h3 style={{ fontSize: "18px", fontWeight: "600" }}>Common Use Cases</h3>
|
||||||
|
|
||||||
|
{/* Avatar */}
|
||||||
|
<VStack gap={2}>
|
||||||
|
<h4 style={{ fontWeight: "500" }}>Avatar</h4>
|
||||||
|
<Image
|
||||||
|
src={sampleImages[0]!}
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
objectFit="cover"
|
||||||
|
style={{ borderRadius: "9999px" }}
|
||||||
|
alt="Avatar"
|
||||||
|
/>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Card image */}
|
||||||
|
<VStack gap={2} style={{ maxWidth: "384px" }}>
|
||||||
|
<h4 style={{ fontWeight: "500" }}>Card Image</h4>
|
||||||
|
<VStack gap={0} style={{ border: "1px solid #d1d5db", borderRadius: "8px", overflow: "hidden" }}>
|
||||||
|
<Image src={sampleImages[1]!} width={384} height={192} objectFit="cover" alt="Card image" />
|
||||||
|
<VStack gap={1} style={{ padding: "16px" }}>
|
||||||
|
<h5 style={{ fontWeight: "500" }}>Card Title</h5>
|
||||||
|
<p style={{ fontSize: "14px", color: "#6b7280" }}>Card description goes here</p>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Gallery grid */}
|
||||||
|
<VStack gap={2}>
|
||||||
|
<h4 style={{ fontWeight: "500" }}>Gallery Grid</h4>
|
||||||
|
<Grid cols={3} gap={2}>
|
||||||
|
{sampleImages.map((src, i) => (
|
||||||
|
<Image
|
||||||
|
key={i}
|
||||||
|
src={src}
|
||||||
|
width={120}
|
||||||
|
height={120}
|
||||||
|
objectFit="cover"
|
||||||
|
style={{ borderRadius: "4px" }}
|
||||||
|
alt={`Gallery ${i}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
28
src/index.tsx
Normal file
28
src/index.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
export { Button } from "./button"
|
||||||
|
export type { ButtonProps } from "./button"
|
||||||
|
|
||||||
|
export { Icon, IconLink } from "./icon"
|
||||||
|
export type { IconName } from "./icon"
|
||||||
|
|
||||||
|
export { VStack, HStack } from "./stack"
|
||||||
|
|
||||||
|
export { Grid } from "./grid"
|
||||||
|
|
||||||
|
export { Divider } from "./divider"
|
||||||
|
|
||||||
|
export { Avatar } from "./avatar"
|
||||||
|
export type { AvatarProps } from "./avatar"
|
||||||
|
|
||||||
|
export { Image } from "./image"
|
||||||
|
export type { ImageProps } from "./image"
|
||||||
|
|
||||||
|
export { Input } from "./input"
|
||||||
|
export type { InputProps } from "./input"
|
||||||
|
|
||||||
|
export { Select } from "./select"
|
||||||
|
export type { SelectProps, SelectOption } from "./select"
|
||||||
|
|
||||||
|
export { Placeholder } from "./placeholder"
|
||||||
|
export { default as PlaceholderDefault } from "./placeholder"
|
||||||
|
|
||||||
|
export type { TailwindSize } from "./types"
|
||||||
181
src/input.tsx
Normal file
181
src/input.tsx
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
import "hono/jsx"
|
||||||
|
import type { JSX, FC } from "hono/jsx"
|
||||||
|
import { VStack, HStack } from "./stack"
|
||||||
|
|
||||||
|
export type InputProps = JSX.IntrinsicElements["input"] & {
|
||||||
|
labelPosition?: "above" | "left" | "right"
|
||||||
|
children?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Input: FC<InputProps> = (props) => {
|
||||||
|
const { labelPosition = "above", children, style, ...inputProps } = props
|
||||||
|
|
||||||
|
const inputStyle: JSX.CSSProperties = {
|
||||||
|
height: "40px",
|
||||||
|
padding: "8px 12px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
border: "1px solid #d1d5db",
|
||||||
|
backgroundColor: "white",
|
||||||
|
fontSize: "14px",
|
||||||
|
outline: "none",
|
||||||
|
...style,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!children) {
|
||||||
|
return <input style={inputStyle} {...inputProps} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelStyle: JSX.CSSProperties = {
|
||||||
|
fontSize: "14px",
|
||||||
|
fontWeight: "500",
|
||||||
|
color: "#111827",
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelElement = (
|
||||||
|
<label for={inputProps.id} style={labelStyle}>
|
||||||
|
{children}
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (labelPosition === "above") {
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "4px", flex: 1, minWidth: 0 }}>
|
||||||
|
{labelElement}
|
||||||
|
<input style={inputStyle} {...inputProps} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelPosition === "left") {
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "4px", flex: 1 }}>
|
||||||
|
{labelElement}
|
||||||
|
<input style={{ ...inputStyle, flex: 1 }} {...inputProps} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelPosition === "right") {
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "4px", flex: 1 }}>
|
||||||
|
<input style={{ ...inputStyle, flex: 1 }} {...inputProps} />
|
||||||
|
{labelElement}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Test = () => {
|
||||||
|
return (
|
||||||
|
<VStack gap={8} style={{ padding: "24px", maxWidth: "448px" }}>
|
||||||
|
{/* Basic inputs */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Basic Inputs</h2>
|
||||||
|
<VStack gap={4}>
|
||||||
|
<Input placeholder="Enter your name" />
|
||||||
|
<Input type="email" placeholder="Enter your email" />
|
||||||
|
<Input type="password" placeholder="Enter your password" />
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Custom styling */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Custom Styling</h2>
|
||||||
|
<VStack gap={4}>
|
||||||
|
<Input style={{ height: "32px", fontSize: "12px" }} placeholder="Small input" />
|
||||||
|
<Input placeholder="Default input" />
|
||||||
|
<Input style={{ height: "48px", fontSize: "16px" }} placeholder="Large input" />
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* With values */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>With Values</h2>
|
||||||
|
<VStack gap={4}>
|
||||||
|
<Input value="John Doe" placeholder="Name" />
|
||||||
|
<Input type="email" value="john@example.com" placeholder="Email" />
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Disabled state */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Disabled State</h2>
|
||||||
|
<VStack gap={4}>
|
||||||
|
<Input disabled placeholder="Disabled input" style={{ opacity: 0.5, cursor: "not-allowed" }} />
|
||||||
|
<Input disabled value="Disabled with value" style={{ opacity: 0.5, cursor: "not-allowed" }} />
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Label above */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Label Above</h2>
|
||||||
|
<VStack gap={4}>
|
||||||
|
<Input placeholder="Enter your name">Name</Input>
|
||||||
|
<Input type="email" placeholder="Enter your email">
|
||||||
|
Email
|
||||||
|
</Input>
|
||||||
|
<Input type="password" placeholder="Enter your password">
|
||||||
|
Password
|
||||||
|
</Input>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Label to the left */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Label Left</h2>
|
||||||
|
<VStack gap={4}>
|
||||||
|
<Input labelPosition="left" placeholder="Enter your name">
|
||||||
|
Name
|
||||||
|
</Input>
|
||||||
|
<Input labelPosition="left" type="email" placeholder="Enter your email">
|
||||||
|
Email
|
||||||
|
</Input>
|
||||||
|
<Input labelPosition="left" type="password" placeholder="Enter your password">
|
||||||
|
Password
|
||||||
|
</Input>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Label to the right */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Label Right</h2>
|
||||||
|
<VStack gap={4}>
|
||||||
|
<Input labelPosition="right" placeholder="Enter your name">
|
||||||
|
Name
|
||||||
|
</Input>
|
||||||
|
<Input labelPosition="right" type="email" placeholder="Enter your email">
|
||||||
|
Email
|
||||||
|
</Input>
|
||||||
|
<Input labelPosition="right" type="password" placeholder="Enter your password">
|
||||||
|
Password
|
||||||
|
</Input>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Horizontal layout */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Horizontal Layout</h2>
|
||||||
|
<HStack gap={4}>
|
||||||
|
<Input placeholder="First name">First</Input>
|
||||||
|
<Input placeholder="Last name">Last</Input>
|
||||||
|
<Input placeholder="Age">Age</Input>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Custom styling */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Custom Input Styling</h2>
|
||||||
|
<VStack gap={4}>
|
||||||
|
<Input style={{ borderColor: "#93c5fd" }} placeholder="Custom styled input">
|
||||||
|
<span style={{ color: "#2563eb", fontWeight: "bold" }}>Custom Label</span>
|
||||||
|
</Input>
|
||||||
|
<Input labelPosition="left" placeholder="Required input">
|
||||||
|
<span style={{ color: "#dc2626", minWidth: "96px" }}>Required Field</span>
|
||||||
|
</Input>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
245
src/placeholder.tsx
Normal file
245
src/placeholder.tsx
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
import "hono/jsx"
|
||||||
|
import { Avatar } from "./avatar"
|
||||||
|
import type { AvatarProps } from "./avatar"
|
||||||
|
import { Image } from "./image"
|
||||||
|
import type { ImageProps } from "./image"
|
||||||
|
import { VStack, HStack } from "./stack"
|
||||||
|
import { Grid } from "./grid"
|
||||||
|
|
||||||
|
export const Placeholder = {
|
||||||
|
Avatar(props: PlaceholderAvatarProps) {
|
||||||
|
const { size = 32, seed = "seed", type = "dylan", transparent, alt, style, rounded } = props
|
||||||
|
|
||||||
|
// Generate DiceBear avatar URL
|
||||||
|
const url = new URL(`https://api.dicebear.com/9.x/${type}/svg`)
|
||||||
|
url.searchParams.set("seed", seed)
|
||||||
|
url.searchParams.set("size", size.toString())
|
||||||
|
|
||||||
|
if (transparent) {
|
||||||
|
url.searchParams.set("backgroundColor", "transparent")
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Avatar src={url.toString()} alt={alt} style={style} size={size} rounded={rounded} />
|
||||||
|
},
|
||||||
|
|
||||||
|
Image(props: PlaceholderImageProps) {
|
||||||
|
const { width = 200, height = 200, seed = 1, alt = "Placeholder image", objectFit, style } = props
|
||||||
|
|
||||||
|
// Generate Picsum Photos URL with seed for consistent images
|
||||||
|
const src = `https://picsum.photos/${width}/${height}?random=${seed}`
|
||||||
|
|
||||||
|
return <Image src={src} alt={alt} width={width} height={height} objectFit={objectFit} style={style} />
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Test = () => {
|
||||||
|
return (
|
||||||
|
<VStack gap={8} style={{ padding: "24px" }}>
|
||||||
|
{/* === AVATAR TESTS === */}
|
||||||
|
|
||||||
|
{/* Show all available avatar styles */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>
|
||||||
|
All Avatar Styles ({allStyles.length} total)
|
||||||
|
</h2>
|
||||||
|
<Grid cols={10} gap={3}>
|
||||||
|
{allStyles.map((style) => (
|
||||||
|
<VStack h="center" gap={1} key={style}>
|
||||||
|
<Placeholder.Avatar type={style} size={48} />
|
||||||
|
<p style={{ fontSize: "12px", fontWeight: "500" }}>{style}</p>
|
||||||
|
</VStack>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Avatar size variations */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Avatar Size Variations</h2>
|
||||||
|
<HStack gap={4}>
|
||||||
|
{[24, 32, 48, 64].map((size) => (
|
||||||
|
<VStack h="center" gap={2} key={size}>
|
||||||
|
<Placeholder.Avatar size={size} />
|
||||||
|
<p style={{ fontSize: "14px" }}>{size}px</p>
|
||||||
|
</VStack>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Avatar styling combinations */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Avatar Styling Options</h2>
|
||||||
|
<HStack gap={6}>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Placeholder.Avatar rounded size={64} />
|
||||||
|
<p style={{ fontSize: "14px" }}>Rounded + Background</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<div style={{ backgroundColor: "#e5e7eb", padding: "8px" }}>
|
||||||
|
<Placeholder.Avatar rounded transparent size={64} />
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: "14px" }}>Rounded + Transparent</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Placeholder.Avatar size={64} />
|
||||||
|
<p style={{ fontSize: "14px" }}>Square + Background</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<div style={{ backgroundColor: "#e5e7eb", padding: "8px" }}>
|
||||||
|
<Placeholder.Avatar transparent size={64} />
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: "14px" }}>Square + Transparent</p>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Avatar seed variations */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>
|
||||||
|
Avatar Seeds (Same Style, Different People)
|
||||||
|
</h2>
|
||||||
|
<HStack gap={4}>
|
||||||
|
{["alice", "bob", "charlie", "diana"].map((seed) => (
|
||||||
|
<VStack h="center" gap={2} key={seed}>
|
||||||
|
<Placeholder.Avatar seed={seed} size={64} />
|
||||||
|
<p style={{ fontSize: "14px" }}>"{seed}"</p>
|
||||||
|
</VStack>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* === IMAGE TESTS === */}
|
||||||
|
<VStack gap={6}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Placeholder Images</h2>
|
||||||
|
|
||||||
|
{/* Size variations */}
|
||||||
|
<VStack gap={3}>
|
||||||
|
<h3 style={{ fontSize: "18px", fontWeight: "600" }}>Size Variations</h3>
|
||||||
|
<HStack gap={4}>
|
||||||
|
{[
|
||||||
|
{ width: 100, height: 100 },
|
||||||
|
{ width: 150, height: 100 },
|
||||||
|
{ width: 200, height: 150 },
|
||||||
|
{ width: 250, height: 200 },
|
||||||
|
].map(({ width, height }) => (
|
||||||
|
<VStack h="center" gap={2} key={`${width}x${height}`}>
|
||||||
|
<Placeholder.Image width={width} height={height} seed={1} />
|
||||||
|
<p style={{ fontSize: "14px" }}>
|
||||||
|
{width}×{height}
|
||||||
|
</p>
|
||||||
|
</VStack>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Different seeds - show variety */}
|
||||||
|
<VStack gap={3}>
|
||||||
|
<h3 style={{ fontSize: "18px", fontWeight: "600" }}>
|
||||||
|
Different Images (Different Seeds)
|
||||||
|
</h3>
|
||||||
|
<HStack gap={4}>
|
||||||
|
{[1, 2, 3, 4, 5].map((seed) => (
|
||||||
|
<VStack h="center" gap={2} key={seed}>
|
||||||
|
<Placeholder.Image width={150} height={150} seed={seed} />
|
||||||
|
<p style={{ fontSize: "14px" }}>Seed {seed}</p>
|
||||||
|
</VStack>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* With custom styles */}
|
||||||
|
<VStack gap={3}>
|
||||||
|
<h3 style={{ fontSize: "18px", fontWeight: "600" }}>With Custom Styles</h3>
|
||||||
|
<HStack gap={6}>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Placeholder.Image
|
||||||
|
width={150}
|
||||||
|
height={150}
|
||||||
|
seed={1}
|
||||||
|
objectFit="cover"
|
||||||
|
style={{ borderRadius: "8px", border: "4px solid #3b82f6" }}
|
||||||
|
/>
|
||||||
|
<p style={{ fontSize: "14px" }}>Rounded + Border</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Placeholder.Image
|
||||||
|
width={150}
|
||||||
|
height={150}
|
||||||
|
seed={2}
|
||||||
|
objectFit="cover"
|
||||||
|
style={{ boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)" }}
|
||||||
|
/>
|
||||||
|
<p style={{ fontSize: "14px" }}>With Shadow</p>
|
||||||
|
</VStack>
|
||||||
|
<VStack h="center" gap={2}>
|
||||||
|
<Placeholder.Image
|
||||||
|
width={150}
|
||||||
|
height={150}
|
||||||
|
seed={3}
|
||||||
|
objectFit="cover"
|
||||||
|
style={{
|
||||||
|
borderRadius: "9999px",
|
||||||
|
border: "4px solid #22c55e",
|
||||||
|
boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p style={{ fontSize: "14px" }}>Circular + Effects</p>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type definitions
|
||||||
|
type PlaceholderAvatarProps = Omit<AvatarProps, "src"> & {
|
||||||
|
seed?: string
|
||||||
|
type?: DicebearStyleName
|
||||||
|
transparent?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlaceholderImageProps = Omit<ImageProps, "src" | "alt"> & {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
seed?: number
|
||||||
|
alt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// All supported DiceBear HTTP styleNames. Source: https://www.dicebear.com/styles
|
||||||
|
const allStyles = [
|
||||||
|
"adventurer",
|
||||||
|
"adventurer-neutral",
|
||||||
|
"avataaars",
|
||||||
|
"avataaars-neutral",
|
||||||
|
"big-ears",
|
||||||
|
"big-ears-neutral",
|
||||||
|
"big-smile",
|
||||||
|
"bottts",
|
||||||
|
"bottts-neutral",
|
||||||
|
"croodles",
|
||||||
|
"croodles-neutral",
|
||||||
|
"dylan",
|
||||||
|
"fun-emoji",
|
||||||
|
"glass",
|
||||||
|
"icons",
|
||||||
|
"identicon",
|
||||||
|
"initials",
|
||||||
|
"lorelei",
|
||||||
|
"lorelei-neutral",
|
||||||
|
"micah",
|
||||||
|
"miniavs",
|
||||||
|
"notionists",
|
||||||
|
"notionists-neutral",
|
||||||
|
"open-peeps",
|
||||||
|
"personas",
|
||||||
|
"pixel-art",
|
||||||
|
"pixel-art-neutral",
|
||||||
|
"rings",
|
||||||
|
"shapes",
|
||||||
|
"thumbs",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
type DicebearStyleName = (typeof allStyles)[number]
|
||||||
|
|
||||||
|
// Default export for convenience
|
||||||
|
export default Placeholder
|
||||||
263
src/select.tsx
Normal file
263
src/select.tsx
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
import "hono/jsx"
|
||||||
|
import type { JSX, FC } from "hono/jsx"
|
||||||
|
import { VStack, HStack } from "./stack"
|
||||||
|
|
||||||
|
export type SelectOption = {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SelectProps = Omit<JSX.IntrinsicElements["select"], "children"> & {
|
||||||
|
options: SelectOption[]
|
||||||
|
placeholder?: string
|
||||||
|
labelPosition?: "above" | "left" | "right"
|
||||||
|
children?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Select: FC<SelectProps> = (props) => {
|
||||||
|
const { options, placeholder, labelPosition = "above", children, style, ...selectProps } = props
|
||||||
|
|
||||||
|
// If a label is provided but no id, generate a random id so the label can be clicked
|
||||||
|
if (children && !selectProps.id) {
|
||||||
|
selectProps.id = `random-${Math.random().toString(36)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectStyle: JSX.CSSProperties = {
|
||||||
|
height: "40px",
|
||||||
|
padding: "8px 32px 8px 12px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
border: "1px solid #d1d5db",
|
||||||
|
backgroundColor: "white",
|
||||||
|
fontSize: "14px",
|
||||||
|
outline: "none",
|
||||||
|
appearance: "none",
|
||||||
|
backgroundImage: `url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQgNkw4IDEwTDEyIDYiIHN0cm9rZT0iIzZCNzI4MCIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+")`,
|
||||||
|
backgroundRepeat: "no-repeat",
|
||||||
|
backgroundPosition: "right 8px center",
|
||||||
|
backgroundSize: "16px 16px",
|
||||||
|
...style,
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectElement = (
|
||||||
|
<select style={selectStyle} {...selectProps}>
|
||||||
|
{placeholder && (
|
||||||
|
<option value="" disabled>
|
||||||
|
{placeholder}
|
||||||
|
</option>
|
||||||
|
)}
|
||||||
|
{options.map((option) => (
|
||||||
|
<option
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
disabled={option.disabled}
|
||||||
|
selected={selectProps.value === option.value}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!children) {
|
||||||
|
return selectElement
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelStyle: JSX.CSSProperties = {
|
||||||
|
fontSize: "14px",
|
||||||
|
fontWeight: "500",
|
||||||
|
color: "#111827",
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelElement = (
|
||||||
|
<label for={selectProps.id} style={labelStyle}>
|
||||||
|
{children}
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (labelPosition === "above") {
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "4px", flex: 1, minWidth: 0 }}>
|
||||||
|
{labelElement}
|
||||||
|
{selectElement}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelPosition === "left") {
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "4px", flex: 1 }}>
|
||||||
|
{labelElement}
|
||||||
|
<select style={{ ...selectStyle, flex: 1 }} {...selectProps}>
|
||||||
|
{placeholder && (
|
||||||
|
<option value="" disabled>
|
||||||
|
{placeholder}
|
||||||
|
</option>
|
||||||
|
)}
|
||||||
|
{options.map((option) => (
|
||||||
|
<option key={option.value} value={option.value} disabled={option.disabled}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelPosition === "right") {
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "4px", flex: 1 }}>
|
||||||
|
<select style={{ ...selectStyle, flex: 1 }} {...selectProps}>
|
||||||
|
{placeholder && (
|
||||||
|
<option value="" disabled>
|
||||||
|
{placeholder}
|
||||||
|
</option>
|
||||||
|
)}
|
||||||
|
{options.map((option) => (
|
||||||
|
<option key={option.value} value={option.value} disabled={option.disabled}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{labelElement}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Test = () => {
|
||||||
|
const months = [
|
||||||
|
{ value: "01", label: "January" },
|
||||||
|
{ value: "02", label: "February" },
|
||||||
|
{ value: "03", label: "March" },
|
||||||
|
{ value: "04", label: "April" },
|
||||||
|
{ value: "05", label: "May" },
|
||||||
|
{ value: "06", label: "June" },
|
||||||
|
{ value: "07", label: "July" },
|
||||||
|
{ value: "08", label: "August" },
|
||||||
|
{ value: "09", label: "September" },
|
||||||
|
{ value: "10", label: "October" },
|
||||||
|
{ value: "11", label: "November" },
|
||||||
|
{ value: "12", label: "December" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const years = Array.from({ length: 10 }, (_, i) => ({
|
||||||
|
value: String(2024 + i),
|
||||||
|
label: String(2024 + i),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const countries = [
|
||||||
|
{ value: "us", label: "United States" },
|
||||||
|
{ value: "ca", label: "Canada" },
|
||||||
|
{ value: "uk", label: "United Kingdom" },
|
||||||
|
{ value: "de", label: "Germany" },
|
||||||
|
{ value: "fr", label: "France" },
|
||||||
|
{ value: "au", label: "Australia", disabled: true },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack gap={8} style={{ padding: "24px", maxWidth: "448px" }}>
|
||||||
|
{/* Basic selects */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Basic Selects</h2>
|
||||||
|
<VStack gap={4}>
|
||||||
|
<Select options={months} placeholder="Select month" />
|
||||||
|
<Select options={years} placeholder="Select year" />
|
||||||
|
<Select options={countries} placeholder="Select country" />
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* With values */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>With Values</h2>
|
||||||
|
<VStack gap={4}>
|
||||||
|
<Select options={months} value="03" />
|
||||||
|
<Select options={years} value="2025" />
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Disabled state */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Disabled State</h2>
|
||||||
|
<VStack gap={4}>
|
||||||
|
<Select
|
||||||
|
options={months}
|
||||||
|
disabled
|
||||||
|
placeholder="Disabled select"
|
||||||
|
style={{ opacity: 0.5, cursor: "not-allowed" }}
|
||||||
|
/>
|
||||||
|
<Select options={years} disabled value="2024" style={{ opacity: 0.5, cursor: "not-allowed" }} />
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Custom styling */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Custom Styling</h2>
|
||||||
|
<VStack gap={4}>
|
||||||
|
<Select options={countries} style={{ borderColor: "#93c5fd" }} placeholder="Custom styled select" />
|
||||||
|
<Select options={months} style={{ height: "32px", fontSize: "12px" }} placeholder="Small select" />
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Label above */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Label Above</h2>
|
||||||
|
<VStack gap={4}>
|
||||||
|
<Select options={months} placeholder="Select month">
|
||||||
|
Birth Month
|
||||||
|
</Select>
|
||||||
|
<Select options={years} placeholder="Select year">
|
||||||
|
Birth Year
|
||||||
|
</Select>
|
||||||
|
<Select options={countries} placeholder="Select country">
|
||||||
|
Country
|
||||||
|
</Select>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Label to the left */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Label Left</h2>
|
||||||
|
<VStack gap={4}>
|
||||||
|
<Select labelPosition="left" options={months} placeholder="Select month">
|
||||||
|
Month
|
||||||
|
</Select>
|
||||||
|
<Select labelPosition="left" options={years} placeholder="Select year">
|
||||||
|
Year
|
||||||
|
</Select>
|
||||||
|
<Select labelPosition="left" options={countries} placeholder="Select country">
|
||||||
|
Country
|
||||||
|
</Select>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Label to the right */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Label Right</h2>
|
||||||
|
<VStack gap={4}>
|
||||||
|
<Select labelPosition="right" options={months} placeholder="Select month">
|
||||||
|
Month
|
||||||
|
</Select>
|
||||||
|
<Select labelPosition="right" options={years} placeholder="Select year">
|
||||||
|
Year
|
||||||
|
</Select>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Horizontal layout (like card form) */}
|
||||||
|
<VStack gap={4}>
|
||||||
|
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Horizontal Layout</h2>
|
||||||
|
<HStack gap={4}>
|
||||||
|
<Select options={months} placeholder="MM">
|
||||||
|
Expires
|
||||||
|
</Select>
|
||||||
|
<Select options={years} placeholder="YYYY">
|
||||||
|
Year
|
||||||
|
</Select>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
193
src/stack.tsx
Normal file
193
src/stack.tsx
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
import type { TailwindSize } from "./types"
|
||||||
|
import "hono/jsx"
|
||||||
|
import type { FC, PropsWithChildren, JSX } from "hono/jsx"
|
||||||
|
import { Grid } from "./grid"
|
||||||
|
|
||||||
|
export const VStack: FC<VStackProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="col"
|
||||||
|
mainAxis={props.v}
|
||||||
|
crossAxis={props.h}
|
||||||
|
wrap={props.wrap}
|
||||||
|
gap={props.gap}
|
||||||
|
style={props.style}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HStack: FC<HStackProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
mainAxis={props.h}
|
||||||
|
crossAxis={props.v}
|
||||||
|
wrap={props.wrap}
|
||||||
|
gap={props.gap}
|
||||||
|
style={props.style}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Stack: FC<StackProps> = (props) => {
|
||||||
|
const gapPx = props.gap ? props.gap * 4 : 0
|
||||||
|
|
||||||
|
const baseStyles: JSX.CSSProperties = {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: props.direction === "row" ? "row" : "column",
|
||||||
|
flexWrap: props.wrap ? "wrap" : "nowrap",
|
||||||
|
gap: `${gapPx}px`,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.mainAxis) {
|
||||||
|
baseStyles.justifyContent = getJustifyContent(props.mainAxis)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.crossAxis) {
|
||||||
|
baseStyles.alignItems = getAlignItems(props.crossAxis)
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedStyles = {
|
||||||
|
...baseStyles,
|
||||||
|
...props.style,
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div style={combinedStyles}>{props.children}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Test = () => {
|
||||||
|
const mainAxisOpts: MainAxisOpts[] = ["start", "center", "end", "between", "around", "evenly"]
|
||||||
|
const crossAxisOpts: CrossAxisOpts[] = ["start", "center", "end", "stretch", "baseline"]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack gap={8} style={{ padding: "16px" }}>
|
||||||
|
{/* HStack layout matrix */}
|
||||||
|
<VStack gap={2}>
|
||||||
|
<h2 style={{ fontSize: "18px", fontWeight: "bold" }}>HStack Layout</h2>
|
||||||
|
<div style={{ overflowX: "auto" }}>
|
||||||
|
<Grid cols={7} gap={1} style={{ gridTemplateColumns: "auto repeat(6, 1fr)" }}>
|
||||||
|
{/* Header row: blank + h labels */}
|
||||||
|
<div></div>
|
||||||
|
{mainAxisOpts.map((h) => (
|
||||||
|
<div key={h} style={{ fontSize: "14px", fontWeight: "500", textAlign: "center" }}>
|
||||||
|
h: {h}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Each row: v label + HStack cells */}
|
||||||
|
{crossAxisOpts.map((v) => [
|
||||||
|
<div key={v} style={{ fontSize: "14px", fontWeight: "500" }}>
|
||||||
|
v: {v}
|
||||||
|
</div>,
|
||||||
|
...mainAxisOpts.map((h) => (
|
||||||
|
<HStack
|
||||||
|
key={`${h}-${v}`}
|
||||||
|
h={h}
|
||||||
|
v={v}
|
||||||
|
style={{ backgroundColor: "#f3f4f6", padding: "8px", height: "96px", border: "1px solid #9ca3af" }}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: "center", padding: "4px", backgroundColor: "#ef4444" }}>Aa</div>
|
||||||
|
<div style={{ textAlign: "center", padding: "4px", backgroundColor: "#22c55e" }}>Aa</div>
|
||||||
|
<div style={{ textAlign: "center", padding: "4px", backgroundColor: "#3b82f6" }}>Aa</div>
|
||||||
|
</HStack>
|
||||||
|
)),
|
||||||
|
])}
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* VStack layout matrix */}
|
||||||
|
<VStack gap={2}>
|
||||||
|
<h2 style={{ fontSize: "18px", fontWeight: "bold" }}>VStack Layout</h2>
|
||||||
|
<div style={{ overflowX: "auto" }}>
|
||||||
|
<Grid cols={6} gap={1} style={{ gridTemplateColumns: "auto repeat(5, 1fr)" }}>
|
||||||
|
{/* Header row: blank + h labels */}
|
||||||
|
<div></div>
|
||||||
|
{crossAxisOpts.map((h) => (
|
||||||
|
<div key={h} style={{ fontSize: "14px", fontWeight: "500", textAlign: "center" }}>
|
||||||
|
h: {h}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Each row: v label + VStack cells */}
|
||||||
|
{mainAxisOpts.map((v) => [
|
||||||
|
<div key={v} style={{ fontSize: "14px", fontWeight: "500" }}>
|
||||||
|
v: {v}
|
||||||
|
</div>,
|
||||||
|
...crossAxisOpts.map((h) => (
|
||||||
|
<VStack
|
||||||
|
key={`${h}-${v}`}
|
||||||
|
v={v}
|
||||||
|
h={h}
|
||||||
|
style={{ backgroundColor: "#f3f4f6", padding: "8px", height: "168px", border: "1px solid #9ca3af" }}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: "center", padding: "4px", backgroundColor: "#ef4444" }}>Aa</div>
|
||||||
|
<div style={{ textAlign: "center", padding: "4px", backgroundColor: "#22c55e" }}>Aa</div>
|
||||||
|
<div style={{ textAlign: "center", padding: "4px", backgroundColor: "#3b82f6" }}>Aa</div>
|
||||||
|
</VStack>
|
||||||
|
)),
|
||||||
|
])}
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type StackDirection = "row" | "col"
|
||||||
|
|
||||||
|
type StackProps = {
|
||||||
|
direction: StackDirection
|
||||||
|
mainAxis?: string
|
||||||
|
crossAxis?: string
|
||||||
|
wrap?: boolean
|
||||||
|
gap?: TailwindSize
|
||||||
|
style?: JSX.CSSProperties
|
||||||
|
children?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
type MainAxisOpts = "start" | "center" | "end" | "between" | "around" | "evenly"
|
||||||
|
type CrossAxisOpts = "start" | "center" | "end" | "stretch" | "baseline"
|
||||||
|
|
||||||
|
type CommonStackProps = PropsWithChildren & {
|
||||||
|
wrap?: boolean
|
||||||
|
gap?: TailwindSize
|
||||||
|
style?: JSX.CSSProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
type VStackProps = CommonStackProps & {
|
||||||
|
v?: MainAxisOpts // main axis for vertical stack
|
||||||
|
h?: CrossAxisOpts // cross axis for vertical stack
|
||||||
|
}
|
||||||
|
|
||||||
|
type HStackProps = CommonStackProps & {
|
||||||
|
h?: MainAxisOpts // main axis for horizontal stack
|
||||||
|
v?: CrossAxisOpts // cross axis for horizontal stack
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJustifyContent(axis: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
start: "flex-start",
|
||||||
|
center: "center",
|
||||||
|
end: "flex-end",
|
||||||
|
between: "space-between",
|
||||||
|
around: "space-around",
|
||||||
|
evenly: "space-evenly",
|
||||||
|
}
|
||||||
|
return map[axis] || "flex-start"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAlignItems(axis: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
start: "flex-start",
|
||||||
|
center: "center",
|
||||||
|
end: "flex-end",
|
||||||
|
stretch: "stretch",
|
||||||
|
baseline: "baseline",
|
||||||
|
}
|
||||||
|
return map[axis] || "stretch"
|
||||||
|
}
|
||||||
1
src/types.ts
Normal file
1
src/types.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export type TailwindSize = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96
|
||||||
37
test/server.tsx
Normal file
37
test/server.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Hono } from 'hono'
|
||||||
|
import { readdirSync } from 'fs'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
const port = process.env.PORT ?? '3100'
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
app.get('/:file', async c => {
|
||||||
|
const file = c.req.param('file') ?? ''
|
||||||
|
const fileName = (file).replace('.', '')
|
||||||
|
const path = join(process.env.PWD ?? '.', `/src/${fileName}.tsx`)
|
||||||
|
|
||||||
|
if (!(await Bun.file(path).exists()))
|
||||||
|
return c.text('404 Not Found', 404)
|
||||||
|
|
||||||
|
const page = await import(path + `?t=${Date.now()}`)
|
||||||
|
return c.html(<><h1>{file}</h1><page.Test req={c.req} /></>)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/', c => {
|
||||||
|
return c.html(<>
|
||||||
|
<h1>Test Files</h1>
|
||||||
|
<ul style='font-size:150%'>{testFiles().map(x => <li><a href={`/${x}`}>{x}</a></li>)}</ul>
|
||||||
|
</>)
|
||||||
|
})
|
||||||
|
|
||||||
|
function testFiles(): string[] {
|
||||||
|
return readdirSync('./test')
|
||||||
|
.filter(x => x.endsWith('.tsx') && !x.startsWith('server'))
|
||||||
|
.map(x => x.replace('.tsx', ''))
|
||||||
|
.sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fetch: app.fetch,
|
||||||
|
port
|
||||||
|
}
|
||||||
36
tsconfig.json
Normal file
36
tsconfig.json
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": [
|
||||||
|
"ESNext",
|
||||||
|
"DOM"
|
||||||
|
],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "hono/jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"#*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user