Merge pull request 'Port howl to forge' (#1) from forge into main

Reviewed-on: #1
This commit is contained in:
defunkt 2026-01-21 04:52:54 +00:00
commit 1b33637d0e
24 changed files with 1863 additions and 1478 deletions

View File

@ -1,6 +1,9 @@
# 🐺 howl # 🐺 howl
Howl is a fork of `werewolf-ui`, without any Tailwind. A minimal, zero-dependency React component library. Howl is a fork of `werewolf-ui`, without any Tailwind.
A minimal, single-dependency component library built on forge
for hype/hono.
## Installation ## Installation

556
TAGS.md Normal file
View File

@ -0,0 +1,556 @@
# HOWL Component Library - Complete Tags Reference
Built with [Forge](https://github.com/user/forge) - a CSS-in-JS framework with theme support.
## Quick Start
```tsx
import { Styles, Button, VStack } from 'howl'
// Include <Styles /> in your document head to inject CSS
<html>
<head>
<Styles />
</head>
<body data-theme="light">
<VStack gap={4}>
<Button>Click Me</Button>
</VStack>
</body>
</html>
```
## Theming
Howl includes light and dark themes. Set `data-theme="light"` or `data-theme="dark"` on your root element.
### Customizing the Theme
```tsx
import { extendThemes } from 'howl'
extendThemes({
light: { 'colors-primary': '#8b5cf6' },
dark: { 'colors-primary': '#a78bfa' }
})
```
---
## Table of Contents
- [Layout Components](#layout-components)
- [VStack](#vstack)
- [HStack](#hstack)
- [Grid](#grid)
- [Section](#section)
- [Container Components](#container-components)
- [Box](#box)
- [Typography Components](#typography-components)
- [H1, H2, H3, H4, H5](#h1-h2-h3-h4-h5)
- [Text](#text)
- [SmallText](#smalltext)
- [Interactive Components](#interactive-components)
- [Button](#button)
- [Input](#input)
- [Select](#select)
- [Media Components](#media-components)
- [Image](#image)
- [Avatar](#avatar)
- [Icon](#icon)
- [IconLink](#iconlink)
- [Utility Components](#utility-components)
- [Divider](#divider)
- [Placeholder](#placeholder)
- [Type Definitions](#type-definitions)
---
## Layout Components
### VStack
Vertical stack layout component using flexbox or CSS Grid for flexible vertical layouts.
**Props:**
- `v?: "start" | "center" | "end" | "between" | "around" | "evenly"` - Main axis alignment (vertical)
- `h?: "start" | "center" | "end" | "stretch" | "baseline"` - Cross axis alignment (horizontal)
- `gap?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 12` - Gap between items (multiplied by 4px)
- `rows?: number[]` - Custom row sizing fractions (e.g., [2, 1] for 2/3 and 1/3 height)
- `wrap?: boolean` - Enable flex-wrap
- `class?: string` - CSS class name
- `style?: JSX.CSSProperties` - Inline styles
- `id?: string` - HTML id attribute
- `ref?: any` - React ref
- `children?: any` - Child elements
**Examples:**
```tsx
<VStack>Content</VStack>
<VStack gap={4} v="center">Content</VStack>
<VStack rows={[2, 1]}>Content</VStack>
```
---
### HStack
Horizontal stack layout component using flexbox or CSS Grid for flexible horizontal layouts.
**Props:**
- `h?: "start" | "center" | "end" | "between" | "around" | "evenly"` - Main axis alignment (horizontal)
- `v?: "start" | "center" | "end" | "stretch" | "baseline"` - Cross axis alignment (vertical)
- `gap?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 12` - Gap between items (multiplied by 4px)
- `cols?: number[]` - Custom column sizing fractions (e.g., [7, 3] for 70% and 30% width)
- `wrap?: boolean` - Enable flex-wrap
- `class?: string` - CSS class name
- `style?: JSX.CSSProperties` - Inline styles
- `id?: string` - HTML id attribute
- `ref?: any` - React ref
- `children?: any` - Child elements
**Examples:**
```tsx
<HStack>Content</HStack>
<HStack gap={6} h="between" v="center">Content</HStack>
<HStack cols={[7, 3]} gap={4}>Content</HStack>
```
---
### Grid
CSS Grid component for creating multi-column layouts.
**Props:**
- `cols?: 1 | 2 | 3 | 4 | 5 | 6 | 7` - Number of columns. Default: 2
- `gap?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 12` - Gap between items (multiplied by 4px). Default: 4
- `class?: string` - CSS class name
- `style?: JSX.CSSProperties` - Inline styles
- `id?: string` - HTML id attribute
- `ref?: any` - React ref
- `children?: any` - Child elements
**Examples:**
```tsx
<Grid cols={3}>Items</Grid>
<Grid cols={4} gap={6}>Items</Grid>
```
---
### Section
Wrapper component that adds padding and vertical stacking with gap control.
**Props:**
- `gap?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 12` - Gap between children (multiplied by 4px). Default: 8
- `class?: string` - CSS class name
- `style?: JSX.CSSProperties` - Inline styles
- `id?: string` - HTML id attribute
- `ref?: any` - React ref
- `children?: any` - Child elements
**Examples:**
```tsx
<Section>Content</Section>
<Section gap={4}>Content</Section>
```
---
## Container Components
### Box
Generic container component. Use color variants for common patterns.
**Variants:**
- `Box` - Plain container (no default styles)
- `RedBox` - Red background, white text, 4px padding
- `GreenBox` - Green background, white text, 4px padding
- `BlueBox` - Blue background, white text, 4px padding
- `GrayBox` - Gray background, 16px padding
**Props:**
- `class?: string` - CSS class name
- `style?: JSX.CSSProperties` - Inline styles
- `id?: string` - HTML id attribute
- `ref?: any` - React ref
- `children?: any` - Child elements
**Examples:**
```tsx
<Box>Content</Box>
<RedBox>Red content</RedBox>
<BlueBox>Blue content</BlueBox>
```
---
## Typography Components
### H1, H2, H3, H4, H5
Heading components with preset font sizes and weights.
**Props (all heading components):**
- `class?: string` - CSS class name
- `style?: JSX.CSSProperties` - Inline styles
- `id?: string` - HTML id attribute
- `ref?: any` - React ref
- `children?: any` - Child elements
**Sizing:**
- `H1` - 24px, bold
- `H2` - 20px, bold
- `H3` - 18px, semibold (600)
- `H4` - 16px, semibold (600)
- `H5` - 14px, medium (500)
**Examples:**
```tsx
<H1>Heading 1</H1>
<H2>Heading 2</H2>
<H3>Heading 3</H3>
```
---
### Text
Standard paragraph text component.
**Props:**
- `class?: string` - CSS class name
- `style?: JSX.CSSProperties` - Inline styles
- `id?: string` - HTML id attribute
- `ref?: any` - React ref
- `children?: any` - Child elements
**Sizing:** 14px
**Examples:**
```tsx
<Text>Regular text</Text>
```
---
### SmallText
Small paragraph text component.
**Props:** Same as Text
**Sizing:** 12px
**Examples:**
```tsx
<SmallText>Small text</SmallText>
```
---
## Interactive Components
### Button
Clickable button component with multiple variants and sizes.
**Props (extends HTML button attributes):**
- `variant?: "primary" | "secondary" | "outline" | "ghost" | "destructive"` - Button style. Default: "primary"
- `size?: "sm" | "md" | "lg"` - Button size. Default: "md"
- `class?: string` - CSS class name
- `style?: JSX.CSSProperties` - Inline styles
- `id?: string` - HTML id attribute
- `ref?: any` - React ref
- Plus all native button attributes (onClick, disabled, type, etc.)
**Variants:**
- `primary` - Primary color background, white text
- `secondary` - Secondary color background, white text
- `outline` - Transparent background, border
- `ghost` - Transparent background, no border
- `destructive` - Destructive color background, white text
**Sizes:**
- `sm` - 32px height
- `md` - 40px height
- `lg` - 48px height
**Examples:**
```tsx
<Button>Click Me</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline" size="lg">Large</Button>
<Button onClick={handleClick}>Action</Button>
<Button disabled>Disabled</Button>
```
---
### Input
Text input component with optional label positioning.
**Props (extends HTML input attributes):**
- `labelPosition?: "above" | "left" | "right"` - Label position relative to input. Default: "above"
- `children?: any` - Label text (passed as children)
- `class?: string` - CSS class name
- `style?: JSX.CSSProperties` - Inline styles
- `id?: string` - HTML id attribute
- `ref?: any` - React ref
- Plus all native input attributes (type, placeholder, value, disabled, etc.)
**Examples:**
```tsx
<Input placeholder="Enter name" />
<Input type="email" placeholder="Email" />
<Input>Label</Input>
<Input labelPosition="left">Name</Input>
<Input labelPosition="right">Label</Input>
```
---
### Select
Dropdown select component with optional label positioning.
**Props:**
- `options: SelectOption[]` - Array of options (required). SelectOption = { value: string, label: string, disabled?: boolean }
- `placeholder?: string` - Placeholder text
- `labelPosition?: "above" | "left" | "right"` - Label position relative to select. Default: "above"
- `children?: any` - Label text (passed as children)
- `class?: string` - CSS class name
- `style?: JSX.CSSProperties` - Inline styles
- `id?: string` - HTML id attribute
- `ref?: any` - React ref
- Plus all native select attributes (value, onChange, disabled, etc.)
**Examples:**
```tsx
<Select options={options} />
<Select options={options} placeholder="Choose" />
<Select options={options}>Label</Select>
<Select options={options} labelPosition="left">Label</Select>
```
---
## Media Components
### Image
Image component with object-fit support.
**Props:**
- `src: string` - Image URL (required)
- `alt?: string` - Alt text for accessibility
- `width?: number` - Width in pixels
- `height?: number` - Height in pixels
- `objectFit?: "cover" | "contain" | "fill" | "none" | "scale-down"` - CSS object-fit property
- `class?: string` - CSS class name
- `style?: JSX.CSSProperties` - Inline styles
- `id?: string` - HTML id attribute
- `ref?: any` - React ref
**Examples:**
```tsx
<Image src="/photo.jpg" />
<Image src="/photo.jpg" width={200} height={200} />
<Image src="/photo.jpg" objectFit="cover" />
```
---
### Avatar
Avatar image component with size and rounded variants.
**Props:**
- `src: string` - Image URL (required)
- `alt?: string` - Alt text for accessibility
- `size?: 24 | 32 | 48 | 64 | 96 | 128` - Avatar size in pixels. Default: 32
- `rounded?: boolean` - Make avatar circular
- `class?: string` - CSS class name
- `style?: JSX.CSSProperties` - Inline styles
- `id?: string` - HTML id attribute
- `ref?: any` - React ref
**Examples:**
```tsx
<Avatar src="/user.jpg" />
<Avatar src="/user.jpg" size={64} />
<Avatar src="/user.jpg" size={48} rounded />
```
---
### Icon
Icon component using Lucide static SVG icons.
**Props:**
- `name: IconName` - Icon name from Lucide (required). Available: Heart, Star, Home, ExternalLink, Mail, Phone, Download, Settings, Shield, Sun, Zap, and many more
- `size?: 4 | 5 | 6 | 8 | 10 | 12` - Icon size in Tailwind units (multiplied by 4px). Default: 6 (24px)
- `class?: string` - CSS class name
- `style?: JSX.CSSProperties` - Inline styles
- `id?: string` - HTML id attribute
- `ref?: any` - React ref
**Examples:**
```tsx
<Icon name="Heart" />
<Icon name="Star" size={8} />
<Icon name="Home" style={{ color: "#3b82f6" }} />
```
---
### IconLink
Icon wrapped in an anchor link.
**Props (extends Icon props):**
- All Icon props plus:
- `href?: string` - Link URL. Default: "#"
- `target?: string` - Link target (e.g., "_blank")
**Examples:**
```tsx
<IconLink name="ExternalLink" href="/link" />
<IconLink name="Home" href="/" />
<IconLink name="ExternalLink" href="https://example.com" target="_blank" />
```
---
## Utility Components
### Divider
Horizontal divider line with optional text.
**Props:**
- `class?: string` - CSS class name
- `style?: JSX.CSSProperties` - Inline styles
- `id?: string` - HTML id attribute
- `ref?: any` - React ref
- `children?: any` - Text to display on the line
**Examples:**
```tsx
<Divider />
<Divider>OR</Divider>
```
---
### Placeholder
Object containing placeholder generator utilities for Avatar and Image components.
#### Placeholder.Avatar
Generates placeholder avatars using DiceBear API.
**Props:**
- `seed?: string` - Seed for consistent avatar generation. Default: "seed"
- `type?: DicebearStyleName` - Avatar style. Default: "dylan"
- `size?: number` - Avatar size in pixels. Default: 32
- `transparent?: boolean` - Use transparent background
- `rounded?: boolean` - Make circular
- `class?: string` - CSS class name
- `style?: JSX.CSSProperties` - Inline styles
- `id?: string` - HTML id attribute
- `ref?: any` - React ref
- `alt?: string` - Alt text
**Available DiceBear Styles:**
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
**Examples:**
```tsx
<Placeholder.Avatar />
<Placeholder.Avatar type="avataaars" size={64} />
<Placeholder.Avatar seed="alice" rounded />
```
#### Placeholder.Image
Generates placeholder images using Picsum Photos API.
**Props:**
- `width?: number` - Image width in pixels. Default: 200
- `height?: number` - Image height in pixels. Default: 200
- `seed?: number` - Seed for consistent image generation. Default: 1
- `alt?: string` - Alt text. Default: "Placeholder image"
- `objectFit?: "cover" | "contain" | "fill" | "none" | "scale-down"` - CSS object-fit
- `class?: string` - CSS class name
- `style?: JSX.CSSProperties` - Inline styles
- `id?: string` - HTML id attribute
- `ref?: any` - React ref
**Examples:**
```tsx
<Placeholder.Image />
<Placeholder.Image width={200} height={200} />
<Placeholder.Image seed={42} />
```
---
## Type Definitions
### SelectOption
Type for Select component options:
```tsx
{
value: string // Option value
label: string // Display text
disabled?: boolean // Disable option
}
```
---
## Export Summary
```tsx
import {
// Utilities
Styles, // CSS injection component - include in <head>
theme, // Theme function for accessing CSS variables
extendThemes, // Override theme values globally
cn, // Class name helper
// Layout
VStack, HStack, Grid, Section,
// Container
Box, RedBox, GreenBox, BlueBox, GrayBox,
// Typography
H1, H2, H3, H4, H5, Text, SmallText,
// Interactive
Button, Input, Select,
// Media
Image, Avatar, Icon, IconLink,
// Utility
Divider, Placeholder,
// Types
type ButtonProps,
type ImageProps,
type AvatarProps,
type InputProps,
type SelectProps,
type SelectOption,
type IconName,
} from 'howl'
```

View File

@ -1,9 +1,11 @@
{ {
"lockfileVersion": 1, "lockfileVersion": 1,
"configVersion": 1,
"workspaces": { "workspaces": {
"": { "": {
"name": "howl", "name": "howl",
"dependencies": { "dependencies": {
"forge": "git+https://git.nose.space/defunkt/forge",
"hono": "^4.10.7", "hono": "^4.10.7",
"lucide-static": "^0.555.0", "lucide-static": "^0.555.0",
}, },
@ -16,13 +18,15 @@
}, },
}, },
"packages": { "packages": {
"@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], "@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
"hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], "forge": ["forge@git+https://git.nose.space/defunkt/forge#e14821c6995e82088efdfe16e53021afe471ac83", { "dependencies": { "hono": "^4.11.3" }, "peerDependencies": { "typescript": "^5" } }, "e14821c6995e82088efdfe16e53021afe471ac83"],
"hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
"lucide-static": ["lucide-static@0.555.0", "", {}, "sha512-FMMaYYsEYsUA6xlEzIMoKEV3oGnxIIvAN+AtLmYXvlTJptJTveJjVBQwvtA/zZLrD6KLEu89G95dQYlhivw5jQ=="], "lucide-static": ["lucide-static@0.555.0", "", {}, "sha512-FMMaYYsEYsUA6xlEzIMoKEV3oGnxIIvAN+AtLmYXvlTJptJTveJjVBQwvtA/zZLrD6KLEu89G95dQYlhivw5jQ=="],

View File

@ -7,13 +7,14 @@
"dev": "bun run --hot test/server.tsx" "dev": "bun run --hot test/server.tsx"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest" "@types/bun": "latest",
"hono": "^4.10.7"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5" "typescript": "^5"
}, },
"dependencies": { "dependencies": {
"hono": "^4.10.7", "forge": "git+https://git.nose.space/defunkt/forge",
"lucide-static": "^0.555.0" "lucide-static": "^0.555.0"
} }
} }

View File

@ -1,61 +1,63 @@
import { Section } from "./section" import { define } from 'forge'
import { H2, Text } from "./text" import { theme } from './theme'
import "hono/jsx" import { Section } from './section'
import type { FC, JSX } from "hono/jsx" import { H2, Text } from './text'
import { VStack, HStack } from "./stack" import { VStack, HStack } from './stack'
import { CodeExamples } from "./code"
import { cn } from "./cn"
export type AvatarProps = JSX.IntrinsicElements["img"] & { export const Avatar = define('Avatar', {
size?: number base: 'img',
rounded?: boolean objectFit: 'cover',
}
export const Avatar: FC<AvatarProps> = (props) => { variants: {
const { src, size = 32, rounded, class: className, style, id, ref, alt = "", ...rest } = props size: {
24: { width: 24, height: 24 },
32: { width: 32, height: 32 },
48: { width: 48, height: 48 },
64: { width: 64, height: 64 },
96: { width: 96, height: 96 },
},
rounded: {
borderRadius: theme('radius-full'),
},
},
})
const avatarStyle: JSX.CSSProperties = { export type AvatarProps = Parameters<typeof Avatar>[0]
width: `${size}px`,
height: `${size}px`,
borderRadius: rounded ? "9999px" : undefined,
...(style as JSX.CSSProperties),
}
return <img src={src} alt={alt} class={cn("Avatar", className)} style={avatarStyle} id={id} ref={ref} {...rest} />
}
export const Test = () => { export const Test = () => {
const sampleImages = [ const sampleImages = [
"https://picsum.photos/seed/3/200/200", 'https://picsum.photos/seed/3/200/200',
"https://picsum.photos/seed/2/200/200", 'https://picsum.photos/seed/2/200/200',
"https://picsum.photos/seed/8/200/200", 'https://picsum.photos/seed/8/200/200',
"https://picsum.photos/seed/9/200/200", 'https://picsum.photos/seed/9/200/200',
] ]
return ( return (
<Section> <Section>
{/* API Usage Examples */}
<CodeExamples
examples={[
'<Avatar src="/user.jpg" />',
'<Avatar src="/user.jpg" size={64} />',
'<Avatar src="/user.jpg" size={48} rounded />',
'<Avatar src="/user.jpg" style={{ border: "2px solid blue" }} />',
]}
/>
{/* Size variations */} {/* Size variations */}
<VStack gap={4}> <VStack gap={4}>
<H2>Size Variations</H2> <H2>Size Variations</H2>
<HStack gap={4}> <HStack gap={4}>
{[24, 32, 48, 64, 96].map((size) => ( <VStack h="center" gap={2}>
<VStack key={size} h="center" gap={2}> <Avatar src={sampleImages[0]} size={24} alt="Sample" />
<Avatar src={sampleImages[0]!} size={size} alt="Sample" /> <Text>24x24</Text>
<Text> </VStack>
{size}x{size} <VStack h="center" gap={2}>
</Text> <Avatar src={sampleImages[0]} size={32} alt="Sample" />
</VStack> <Text>32x32</Text>
))} </VStack>
<VStack h="center" gap={2}>
<Avatar src={sampleImages[0]} size={48} alt="Sample" />
<Text>48x48</Text>
</VStack>
<VStack h="center" gap={2}>
<Avatar src={sampleImages[0]} size={64} alt="Sample" />
<Text>64x64</Text>
</VStack>
<VStack h="center" gap={2}>
<Avatar src={sampleImages[0]} size={96} alt="Sample" />
<Text>96x96</Text>
</VStack>
</HStack> </HStack>
</VStack> </VStack>
@ -64,11 +66,11 @@ export const Test = () => {
<H2>Rounded vs Square</H2> <H2>Rounded vs Square</H2>
<HStack gap={6}> <HStack gap={6}>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Avatar src={sampleImages[0]!} size={64} alt="Sample" /> <Avatar src={sampleImages[0]} size={64} alt="Sample" />
<Text>Square</Text> <Text>Square</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Avatar src={sampleImages[0]!} size={64} rounded alt="Sample" /> <Avatar src={sampleImages[0]} size={64} rounded alt="Sample" />
<Text>Rounded</Text> <Text>Rounded</Text>
</VStack> </VStack>
</HStack> </HStack>
@ -93,30 +95,30 @@ export const Test = () => {
<HStack gap={6}> <HStack gap={6}>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Avatar <Avatar
src={sampleImages[0]!} src={sampleImages[0]}
size={64} size={64}
rounded rounded
style={{ border: "4px solid #3b82f6" }} style={{ border: '4px solid #3b82f6' }}
alt="With border" alt="With border"
/> />
<Text>With Border</Text> <Text>With Border</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Avatar <Avatar
src={sampleImages[1]!} src={sampleImages[1]}
size={64} size={64}
rounded rounded
style={{ boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)" }} style={{ boxShadow: '0 10px 15px rgba(0, 0, 0, 0.3)' }}
alt="With shadow" alt="With shadow"
/> />
<Text>With Shadow</Text> <Text>With Shadow</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Avatar <Avatar
src={sampleImages[2]!} src={sampleImages[2]}
size={64} size={64}
rounded rounded
style={{ border: "4px solid #22c55e", boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)" }} style={{ border: '4px solid #22c55e', boxShadow: '0 10px 15px rgba(0, 0, 0, 0.3)' }}
alt="Border + shadow" alt="Border + shadow"
/> />
<Text>Border + Shadow</Text> <Text>Border + Shadow</Text>

View File

@ -1,80 +1,54 @@
import "hono/jsx" import { define } from 'forge'
import type { FC, PropsWithChildren, JSX } from "hono/jsx" import { theme } from './theme'
import { CodeExamples } from "./code"
import { cn } from "./cn"
type BoxProps = JSX.IntrinsicElements["div"] & PropsWithChildren & { export const Box = define('Box', {
bg?: string // Base box - minimal styling, designed for customization via style prop
color?: string })
p?: number
}
export const Box: FC<BoxProps> = (props) => { export const RedBox = define('RedBox', {
const { children, bg, color, p, class: className, style, id, ref, ...rest } = props background: theme('colors-red'),
color: theme('colors-bg'),
padding: theme('spacing-1'),
textAlign: 'center',
})
const boxStyle: JSX.CSSProperties = { export const GreenBox = define('GreenBox', {
backgroundColor: bg, background: theme('colors-success'),
color: color, color: theme('colors-bg'),
padding: p ? `${p}px` : undefined, padding: theme('spacing-1'),
...(style as JSX.CSSProperties), textAlign: 'center',
} })
return <div class={cn("Box", className)} style={boxStyle} id={id} ref={ref} {...rest}>{children}</div> export const BlueBox = define('BlueBox', {
} background: theme('colors-blue'),
color: theme('colors-bg'),
padding: theme('spacing-1'),
textAlign: 'center',
})
// Common demo box colors export const GrayBox = define('GrayBox', {
export const RedBox: FC<PropsWithChildren> = ({ children }) => ( background: theme('colors-bgMuted'),
<Box bg="#ef4444" p={4} style={{ textAlign: "center" }}> padding: theme('spacing-4'),
{children} })
</Box>
)
export const GreenBox: FC<PropsWithChildren> = ({ children }) => (
<Box bg="#22c55e" p={4} style={{ textAlign: "center" }}>
{children}
</Box>
)
export const BlueBox: FC<PropsWithChildren> = ({ children }) => (
<Box bg="#3b82f6" p={4} style={{ textAlign: "center" }}>
{children}
</Box>
)
export const GrayBox: FC<PropsWithChildren & { style?: JSX.CSSProperties }> = ({ children, style }) => (
<Box bg="#f3f4f6" p={16} style={style}>
{children}
</Box>
)
export const Test = () => { export const Test = () => {
return ( return (
<div style={{ display: "flex", flexDirection: "column", gap: "32px", padding: "24px" }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '32px', padding: '24px' }}>
{/* API Usage Examples */}
<CodeExamples
examples={[
'<Box>Content</Box>',
'<Box bg="#3b82f6" p={16}>Content</Box>',
'<RedBox>Content</RedBox>',
'<GrayBox>Content</GrayBox>',
]}
/>
<div> <div>
<h2 style={{ fontSize: "20px", fontWeight: "bold", marginBottom: "16px" }}>Box Component</h2> <h2 style={{ fontSize: '20px', fontWeight: 'bold', marginBottom: '16px' }}>Box Component</h2>
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<Box bg="#3b82f6" color="#ffffff" p={16}> <Box style={{ background: '#3b82f6', color: '#ffffff', padding: '16px' }}>
Basic Box with custom background and text color Basic Box with custom background and text color
</Box> </Box>
<Box p={8} style={{ border: "2px solid #d1d5db", borderRadius: "8px" }}> <Box style={{ padding: '8px', border: '2px solid #d1d5db', borderRadius: '8px' }}>
Box with padding and border Box with padding and border
</Box> </Box>
</div> </div>
</div> </div>
<div> <div>
<h2 style={{ fontSize: "20px", fontWeight: "bold", marginBottom: "16px" }}>Color Variants</h2> <h2 style={{ fontSize: '20px', fontWeight: 'bold', marginBottom: '16px' }}>Color Variants</h2>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<RedBox>Red Box</RedBox> <RedBox>Red Box</RedBox>
<GreenBox>Green Box</GreenBox> <GreenBox>Green Box</GreenBox>
<BlueBox>Blue Box</BlueBox> <BlueBox>Blue Box</BlueBox>
@ -83,10 +57,10 @@ export const Test = () => {
</div> </div>
<div> <div>
<h2 style={{ fontSize: "20px", fontWeight: "bold", marginBottom: "16px" }}>Nested Boxes</h2> <h2 style={{ fontSize: '20px', fontWeight: 'bold', marginBottom: '16px' }}>Nested Boxes</h2>
<Box bg="#f3f4f6" p={16}> <Box style={{ background: '#f3f4f6', padding: '16px' }}>
<Box bg="#e5e7eb" p={12}> <Box style={{ background: '#e5e7eb', padding: '12px' }}>
<Box bg="#d1d5db" p={8}> <Box style={{ background: '#d1d5db', padding: '8px' }}>
Nested boxes demonstration Nested boxes demonstration
</Box> </Box>
</Box> </Box>

View File

@ -1,96 +1,114 @@
import "hono/jsx" import { define } from 'forge'
import type { JSX, FC } from "hono/jsx" import { theme } from './theme'
import { VStack, HStack } from "./stack" import { VStack, HStack } from './stack'
import { Section } from "./section" import { Section } from './section'
import { H2 } from "./text" import { H2 } from './text'
import { CodeExamples } from "./code"
import { cn } from "./cn"
export type ButtonProps = JSX.IntrinsicElements["button"] & { export const Button = define('Button', {
variant?: "primary" | "secondary" | "outline" | "ghost" | "destructive" base: 'button',
size?: "sm" | "md" | "lg"
}
export const Button: FC<ButtonProps> = (props) => { display: 'inline-flex',
const { variant = "primary", size = "md", style, class: className, id, ref, ...buttonProps } = props alignItems: 'center',
justifyContent: 'center',
fontWeight: 500,
transition: 'all 0.2s',
cursor: 'pointer',
borderRadius: theme('radius-sm'),
border: '1px solid transparent',
outline: 'none',
const baseStyles: JSX.CSSProperties = { // Default: primary + md
display: "inline-flex", background: theme('colors-primary'),
alignItems: "center", color: theme('colors-bg'),
justifyContent: "center", height: 40,
fontWeight: "500", padding: `0 ${theme('spacing-4')}`,
transition: "all 0.2s", fontSize: theme('fontSize-sm'),
outline: "none",
cursor: "pointer",
borderRadius: "4px",
border: "1px solid transparent",
}
const variantStyles: Record<string, JSX.CSSProperties> = { states: {
primary: { ':not(:disabled):hover': {
backgroundColor: "#3b82f6", background: theme('colors-primaryHover'),
color: "#ffffff",
}, },
secondary: { ':disabled': {
backgroundColor: "#64748b", opacity: 0.5,
color: "#ffffff", cursor: 'not-allowed',
}, },
outline: { },
backgroundColor: "transparent",
color: "#000000",
borderColor: "#d1d5db",
},
ghost: {
backgroundColor: "transparent",
color: "#000000",
},
destructive: {
backgroundColor: "#ef4444",
color: "#ffffff",
},
}
const sizeStyles: Record<string, JSX.CSSProperties> = { variants: {
sm: { variant: {
height: "32px", primary: {
padding: "0 12px", background: theme('colors-primary'),
fontSize: "14px", color: theme('colors-bg'),
states: {
':not(:disabled):hover': {
background: theme('colors-primaryHover'),
},
},
},
secondary: {
background: theme('colors-secondary'),
color: theme('colors-bg'),
states: {
':not(:disabled):hover': {
background: theme('colors-secondaryHover'),
},
},
},
outline: {
background: 'transparent',
color: theme('colors-fg'),
borderColor: theme('colors-border'),
states: {
':not(:disabled):hover': {
borderColor: theme('colors-borderActive'),
},
},
},
ghost: {
background: 'transparent',
color: theme('colors-fg'),
border: 'none',
states: {
':not(:disabled):hover': {
background: theme('colors-bgMuted'),
},
},
},
destructive: {
background: theme('colors-destructive'),
color: theme('colors-bg'),
states: {
':not(:disabled):hover': {
background: theme('colors-destructiveHover'),
},
},
},
}, },
md: { size: {
height: "40px", sm: {
padding: "0 16px", height: 32,
fontSize: "14px", padding: `0 ${theme('spacing-3')}`,
fontSize: theme('fontSize-sm'),
},
md: {
height: 40,
padding: `0 ${theme('spacing-4')}`,
fontSize: theme('fontSize-sm'),
},
lg: {
height: 48,
padding: `0 ${theme('spacing-6')}`,
fontSize: theme('fontSize-base'),
},
}, },
lg: { },
height: "48px", })
padding: "0 24px",
fontSize: "16px",
},
}
const combinedStyles: JSX.CSSProperties = { export type ButtonProps = Parameters<typeof Button>[0]
...baseStyles,
...variantStyles[variant],
...sizeStyles[size],
...(style as JSX.CSSProperties),
}
return <button {...buttonProps} class={cn("Button", className)} style={combinedStyles} id={id} ref={ref} />
}
export const Test = () => { export const Test = () => {
return ( return (
<Section> <Section>
{/* API Usage Examples */}
<CodeExamples
examples={[
'<Button>Click Me</Button>',
'<Button variant="secondary">Secondary</Button>',
'<Button variant="outline" size="lg">Large</Button>',
'<Button onClick={handleClick}>Action</Button>',
]}
/>
{/* Variants */} {/* Variants */}
<VStack gap={4}> <VStack gap={4}>
<H2>Button Variants</H2> <H2>Button Variants</H2>
@ -119,11 +137,11 @@ export const Test = () => {
<HStack gap={4}> <HStack gap={4}>
<Button variant="primary"> <Button variant="primary">
<span>🚀</span> <span>🚀</span>
<span style={{ marginLeft: "8px" }}>Launch</span> <span style={{ marginLeft: '8px' }}>Launch</span>
</Button> </Button>
<Button variant="outline" style={{ flexDirection: "column", height: "80px", width: "96px" }}> <Button variant="outline" style={{ flexDirection: 'column', height: '80px', width: '96px' }}>
<span style={{ fontSize: "24px" }}>💳</span> <span style={{ fontSize: '24px' }}>💳</span>
<span style={{ fontSize: "12px", marginTop: "4px" }}>Card</span> <span style={{ fontSize: '12px', marginTop: '4px' }}>Card</span>
</Button> </Button>
</HStack> </HStack>
</VStack> </VStack>
@ -132,10 +150,10 @@ export const Test = () => {
<VStack gap={4}> <VStack gap={4}>
<H2>Native Attributes</H2> <H2>Native Attributes</H2>
<HStack gap={4}> <HStack gap={4}>
<Button onClick={() => alert("Clicked!")} variant="primary"> <Button onClick={() => alert('Clicked!')} variant="primary">
Click Me Click Me
</Button> </Button>
<Button disabled variant="secondary" style={{ opacity: 0.5, pointerEvents: "none" }}> <Button disabled variant="secondary">
Disabled Disabled
</Button> </Button>
<Button type="submit" variant="outline"> <Button type="submit" variant="outline">

View File

@ -1,72 +1,87 @@
import "hono/jsx" import { define } from 'forge'
import type { FC } from "hono/jsx" import { theme } from './theme'
import { VStack } from "./stack"
// Code block container
const CodeBlock = define('CodeBlock', {
fontFamily: 'monospace',
fontSize: '13px',
lineHeight: 1.5,
color: theme('syntax-text'),
})
// Syntax token spans
const TagToken = define('TagToken', {
base: 'span',
color: theme('syntax-tag'),
fontWeight: 600,
})
const AttrToken = define('AttrToken', {
base: 'span',
color: theme('syntax-attr'),
})
const StringToken = define('StringToken', {
base: 'span',
color: theme('syntax-string'),
})
const NumberToken = define('NumberToken', {
base: 'span',
color: theme('syntax-number'),
})
const BraceToken = define('BraceToken', {
base: 'span',
color: theme('syntax-brace'),
fontWeight: 600,
})
const TextToken = define('TextToken', {
base: 'span',
color: theme('syntax-text'),
})
// Code examples container
const CodeExamplesBox = define('CodeExamplesBox', {
background: theme('colors-bgElevated'),
padding: theme('spacing-4'),
borderRadius: theme('radius-lg'),
border: `1px solid ${theme('colors-border')}`,
display: 'flex',
flexDirection: 'column',
gap: theme('spacing-2'),
})
type CodeProps = { type CodeProps = {
children: string children: string
} }
// Color scheme
const colors = {
tag: "#0ea5e9", // cyan-500
attr: "#8b5cf6", // violet-500
string: "#10b981", // emerald-500
number: "#f59e0b", // amber-500
brace: "#ef4444", // red-500
text: "#374151", // gray-700
}
// Lightweight JSX syntax highlighter // Lightweight JSX syntax highlighter
export const Code: FC<CodeProps> = ({ children }) => { export const Code = ({ children }: CodeProps) => {
const tokens = tokenizeJSX(children) const tokens = tokenizeJSX(children)
return ( return (
<div <CodeBlock>
style={{
fontFamily: "monospace",
fontSize: "13px",
lineHeight: "1.5",
}}
>
{tokens.map((token, i) => { {tokens.map((token, i) => {
if (token.type === "tag") { if (token.type === 'tag') {
return ( return <TagToken key={i}>{token.value}</TagToken>
<span key={i} style={{ color: colors.tag, fontWeight: "600" }}>
{token.value}
</span>
)
} }
if (token.type === "attr") { if (token.type === 'attr') {
return ( return <AttrToken key={i}>{token.value}</AttrToken>
<span key={i} style={{ color: colors.attr }}>
{token.value}
</span>
)
} }
if (token.type === "string") { if (token.type === 'string') {
return ( return <StringToken key={i}>{token.value}</StringToken>
<span key={i} style={{ color: colors.string }}>
{token.value}
</span>
)
} }
if (token.type === "number") { if (token.type === 'number') {
return ( return <NumberToken key={i}>{token.value}</NumberToken>
<span key={i} style={{ color: colors.number }}>
{token.value}
</span>
)
} }
if (token.type === "brace") { if (token.type === 'brace') {
return ( return <BraceToken key={i}>{token.value}</BraceToken>
<span key={i} style={{ color: colors.brace, fontWeight: "600" }}>
{token.value}
</span>
)
} }
return <span key={i}>{token.value}</span> return <TextToken key={i}>{token.value}</TextToken>
})} })}
</div> </CodeBlock>
) )
} }
@ -75,28 +90,18 @@ type CodeExamplesProps = {
} }
// Container for multiple code examples // Container for multiple code examples
export const CodeExamples: FC<CodeExamplesProps> = ({ examples }) => { export const CodeExamples = ({ examples }: CodeExamplesProps) => {
return ( return (
<div class="code-examples-container"> <CodeExamplesBox class="code-examples-container">
<VStack {examples.map((example, i) => (
gap={2} <Code key={i}>{example}</Code>
style={{ ))}
backgroundColor: "#f9fafb", </CodeExamplesBox>
padding: "16px",
borderRadius: "8px",
border: "1px solid #e5e7eb",
}}
>
{examples.map((example, i) => (
<Code key={i}>{example}</Code>
))}
</VStack>
</div>
) )
} }
type Token = { type Token = {
type: "tag" | "attr" | "string" | "number" | "brace" | "text" type: 'tag' | 'attr' | 'string' | 'number' | 'brace' | 'text'
value: string value: string
} }
@ -106,56 +111,56 @@ function tokenizeJSX(code: string): Token[] {
while (i < code.length) { while (i < code.length) {
// Match opening/closing tags: < or </ // Match opening/closing tags: < or </
if (code[i] === "<") { if (code[i] === '<') {
i++ i++
// Check for closing tag // Check for closing tag
if (code[i] === "/") { if (code[i] === '/') {
tokens.push({ type: "tag", value: "</" }) tokens.push({ type: 'tag', value: '</' })
i++ i++
} else { } else {
tokens.push({ type: "tag", value: "<" }) tokens.push({ type: 'tag', value: '<' })
} }
// Get tag name // Get tag name
let tagName = "" let tagName = ''
while (i < code.length && /[A-Za-z0-9.]/.test(code[i]!)) { while (i < code.length && /[A-Za-z0-9.]/.test(code[i]!)) {
tagName += code[i] tagName += code[i]
i++ i++
} }
if (tagName) { if (tagName) {
tokens.push({ type: "tag", value: tagName }) tokens.push({ type: 'tag', value: tagName })
} }
// Parse attributes inside the tag // Parse attributes inside the tag
while (i < code.length && code[i] !== ">") { while (i < code.length && code[i] !== '>') {
// Skip whitespace // Skip whitespace
if (/\s/.test(code[i]!)) { if (/\s/.test(code[i]!)) {
tokens.push({ type: "text", value: code[i]! }) tokens.push({ type: 'text', value: code[i]! })
i++ i++
continue continue
} }
// Check for self-closing / // Check for self-closing /
if (code[i] === "/" && code[i + 1] === ">") { if (code[i] === '/' && code[i + 1] === '>') {
tokens.push({ type: "tag", value: " />" }) tokens.push({ type: 'tag', value: ' />' })
i += 2 i += 2
break break
} }
// Parse attribute name // Parse attribute name
let attrName = "" let attrName = ''
while (i < code.length && /[a-zA-Z0-9-]/.test(code[i]!)) { while (i < code.length && /[a-zA-Z0-9-]/.test(code[i]!)) {
attrName += code[i] attrName += code[i]
i++ i++
} }
if (attrName) { if (attrName) {
tokens.push({ type: "attr", value: attrName }) tokens.push({ type: 'attr', value: attrName })
} }
// Check for = // Check for =
if (code[i] === "=") { if (code[i] === '=') {
tokens.push({ type: "text", value: "=" }) tokens.push({ type: 'text', value: '=' })
i++ i++
// Parse attribute value // Parse attribute value
@ -171,18 +176,18 @@ function tokenizeJSX(code: string): Token[] {
str += '"' str += '"'
i++ i++
} }
tokens.push({ type: "string", value: str }) tokens.push({ type: 'string', value: str })
} else if (code[i] === "{") { } else if (code[i] === '{') {
// Brace value // Brace value
tokens.push({ type: "brace", value: "{" }) tokens.push({ type: 'brace', value: '{' })
i++ i++
// Get content inside braces // Get content inside braces
let content = "" let content = ''
let depth = 1 let depth = 1
while (i < code.length && depth > 0) { while (i < code.length && depth > 0) {
if (code[i] === "{") depth++ if (code[i] === '{') depth++
if (code[i] === "}") { if (code[i] === '}') {
depth-- depth--
if (depth === 0) break if (depth === 0) break
} }
@ -192,13 +197,13 @@ function tokenizeJSX(code: string): Token[] {
// Check if content is a number // Check if content is a number
if (/^\d+$/.test(content)) { if (/^\d+$/.test(content)) {
tokens.push({ type: "number", value: content }) tokens.push({ type: 'number', value: content })
} else { } else {
tokens.push({ type: "text", value: content }) tokens.push({ type: 'text', value: content })
} }
if (code[i] === "}") { if (code[i] === '}') {
tokens.push({ type: "brace", value: "}" }) tokens.push({ type: 'brace', value: '}' })
i++ i++
} }
} }
@ -206,19 +211,19 @@ function tokenizeJSX(code: string): Token[] {
} }
// Closing > // Closing >
if (code[i] === ">") { if (code[i] === '>') {
tokens.push({ type: "tag", value: ">" }) tokens.push({ type: 'tag', value: '>' })
i++ i++
} }
} else { } else {
// Regular text // Regular text
let text = "" let text = ''
while (i < code.length && code[i] !== "<") { while (i < code.length && code[i] !== '<') {
text += code[i] text += code[i]
i++ i++
} }
if (text) { if (text) {
tokens.push({ type: "text", value: text }) tokens.push({ type: 'text', value: text })
} }
} }
} }

View File

@ -1,60 +1,48 @@
import { Section } from "./section" import { define } from 'forge'
import { H2 } from "./text" import { theme } from './theme'
import "hono/jsx" import { Section } from './section'
import type { FC, PropsWithChildren, JSX } from "hono/jsx" import { H2 } from './text'
import { VStack } from "./stack" import { VStack } from './stack'
import { CodeExamples } from "./code"
import { cn } from "./cn"
type DividerProps = JSX.IntrinsicElements["div"] & PropsWithChildren export const Divider = define('Divider', {
display: 'flex',
alignItems: 'center',
margin: `${theme('spacing-4')} 0`,
export const Divider: FC<DividerProps> = (props) => { parts: {
const { children, class: className, style, id, ref, ...rest } = props Line: {
flex: 1,
borderTop: `1px solid ${theme('colors-border')}`,
},
Text: {
base: 'span',
padding: `0 ${theme('spacing-3')}`,
fontSize: theme('fontSize-sm'),
color: theme('colors-fgMuted'),
background: theme('colors-bg'),
},
},
const containerStyle: JSX.CSSProperties = { render({ props, parts: { Root, Line, Text } }) {
display: "flex", const { children, ...rest } = props
alignItems: "center",
margin: "16px 0",
...(style as JSX.CSSProperties),
}
const lineStyle: JSX.CSSProperties = { return (
flex: 1, <Root {...rest}>
borderTop: "1px solid #d1d5db", <Line />
} {children && (
<>
const textStyle: JSX.CSSProperties = { <Text>{children}</Text>
padding: "0 12px", <Line />
fontSize: "14px", </>
color: "#6b7280", )}
backgroundColor: "#ffffff", </Root>
} )
},
return ( })
<div class={cn("Divider", className)} style={containerStyle} id={id} ref={ref} {...rest}>
<div style={lineStyle}></div>
{children && (
<>
<span style={textStyle}>{children}</span>
<div style={lineStyle}></div>
</>
)}
</div>
)
}
export const Test = () => { export const Test = () => {
return ( return (
<Section gap={4} maxWidth="448px" style={{ padding: "16px" }}> <Section gap={4} style={{ maxWidth: '448px', padding: '16px' }}>
{/* API Usage Examples */}
<CodeExamples
examples={[
'<Divider />',
'<Divider>OR</Divider>',
'<Divider style={{ margin: "24px 0" }} />',
]}
/>
<H2>Divider Examples</H2> <H2>Divider Examples</H2>
<VStack gap={0}> <VStack gap={0}>
@ -65,7 +53,7 @@ export const Test = () => {
{/* Just a line */} {/* Just a line */}
<VStack gap={0}> <VStack gap={0}>
<p>Look a line 👇</p> <p>Look a line</p>
<Divider /> <Divider />
<p>So cool, so straight!</p> <p>So cool, so straight!</p>
</VStack> </VStack>

View File

@ -1,125 +1,75 @@
import type { TailwindSize } from "./types" import { define } from 'forge'
import "hono/jsx" import { theme } from './theme'
import type { FC, PropsWithChildren, JSX } from "hono/jsx" import { VStack, HStack } from './stack'
import { VStack } from "./stack" import { Button } from './button'
import { Button } from "./button" import { Section } from './section'
import { Section } from "./section" import { H2, H3 } from './text'
import { H2, H3 } from "./text"
import { CodeExamples } from "./code"
import { cn } from "./cn"
type GridCols = number | { sm?: number; md?: number; lg?: number; xl?: number } export const Grid = define('Grid', {
display: 'grid',
type GridProps = JSX.IntrinsicElements["div"] & PropsWithChildren & { variants: {
cols?: GridCols cols: {
gap?: TailwindSize 1: { gridTemplateColumns: 'repeat(1, minmax(0, 1fr))' },
v?: keyof typeof alignItemsMap 2: { gridTemplateColumns: 'repeat(2, minmax(0, 1fr))' },
h?: keyof typeof justifyItemsMap 3: { gridTemplateColumns: 'repeat(3, minmax(0, 1fr))' },
} 4: { gridTemplateColumns: 'repeat(4, minmax(0, 1fr))' },
5: { gridTemplateColumns: 'repeat(5, minmax(0, 1fr))' },
export const Grid: FC<GridProps> = (props) => { 6: { gridTemplateColumns: 'repeat(6, minmax(0, 1fr))' },
const { cols = 2, gap = 4, v, h, class: className, style, id, ref, children, ...rest } = props 7: { gridTemplateColumns: 'repeat(7, minmax(0, 1fr))' },
},
const gapPx = gap * 4 gap: {
0: { gap: 0 },
const baseStyles: JSX.CSSProperties = { 1: { gap: theme('spacing-1') },
display: "grid", 2: { gap: theme('spacing-2') },
gridTemplateColumns: getColumnsValue(cols), 3: { gap: theme('spacing-3') },
gap: `${gapPx}px`, 4: { gap: theme('spacing-4') },
} 6: { gap: theme('spacing-6') },
8: { gap: theme('spacing-8') },
if (v) { 12: { gap: theme('spacing-12') },
baseStyles.alignItems = alignItemsMap[v] },
} v: {
start: { alignItems: 'start' },
if (h) { center: { alignItems: 'center' },
baseStyles.justifyItems = justifyItemsMap[h] end: { alignItems: 'end' },
} stretch: { alignItems: 'stretch' },
},
const combinedStyles = { h: {
...baseStyles, start: { justifyItems: 'start' },
...(style as JSX.CSSProperties), center: { justifyItems: 'center' },
} end: { justifyItems: 'end' },
stretch: { justifyItems: 'stretch' },
return <div class={cn("Grid", className)} style={combinedStyles} id={id} ref={ref} {...rest}>{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 = () => { export const Test = () => {
return ( return (
<Section gap={4} style={{ padding: "16px" }}> <Section gap={4} style={{ padding: '16px' }}>
{/* API Usage Examples */}
<CodeExamples
examples={[
'<Grid cols={3}>...</Grid>',
'<Grid cols={4} gap={6}>...</Grid>',
'<Grid cols={{ sm: 1, md: 2, lg: 3 }}>...</Grid>',
'<Grid cols={2} v="center" h="center">...</Grid>',
]}
/>
<VStack gap={6}> <VStack gap={6}>
<H2>Grid Examples</H2> <H2>Grid Examples</H2>
{/* Simple 3-column grid */} {/* Simple 3-column grid */}
<VStack gap={2}> <VStack gap={2}>
<H3>Simple 3 columns: cols=3</H3> <H3>Simple 3 columns: cols={3}</H3>
<Grid cols={3} gap={4}> <Grid cols={3} gap={4}>
<div style={{ backgroundColor: "#fecaca", padding: "16px", textAlign: "center" }}>Item 1</div> <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: '#bbf7d0', padding: '16px', textAlign: 'center' }}>Item 2</div>
<div style={{ backgroundColor: "#bfdbfe", padding: "16px", textAlign: "center" }}>Item 3</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: '#fef08a', padding: '16px', textAlign: 'center' }}>Item 4</div>
<div style={{ backgroundColor: "#e9d5ff", padding: "16px", textAlign: "center" }}>Item 5</div> <div style={{ backgroundColor: '#e9d5ff', padding: '16px', textAlign: 'center' }}>Item 5</div>
<div style={{ backgroundColor: "#fbcfe8", padding: "16px", textAlign: "center" }}>Item 6</div> <div style={{ backgroundColor: '#fbcfe8', padding: '16px', textAlign: 'center' }}>Item 6</div>
</Grid> </Grid>
</VStack> </VStack>
{/* Responsive grid */} {/* 4-column grid */}
<VStack gap={2}> <VStack gap={2}>
<H3>Responsive: cols=&#123;sm: 1, md: 2, lg: 3&#125;</H3> <H3>4 columns: cols={4}</H3>
<Grid cols={{ sm: 1, md: 2, lg: 3 }} gap={4}> <Grid cols={4} gap={4}>
<div style={{ backgroundColor: "#fecaca", padding: "16px", textAlign: "center" }}>Card 1</div> <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: '#bbf7d0', padding: '16px', textAlign: 'center' }}>Card 2</div>
<div style={{ backgroundColor: "#bfdbfe", padding: "16px", textAlign: "center" }}>Card 3</div> <div style={{ backgroundColor: '#bfdbfe', padding: '16px', textAlign: 'center' }}>Card 3</div>
<div style={{ backgroundColor: "#fef08a", padding: "16px", textAlign: "center" }}>Card 4</div> <div style={{ backgroundColor: '#fef08a', padding: '16px', textAlign: 'center' }}>Card 4</div>
</Grid>
</VStack>
{/* More responsive examples */}
<VStack gap={2}>
<H3>More responsive: cols=&#123;sm: 2, lg: 4, xl: 6&#125;</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> </Grid>
</VStack> </VStack>
@ -127,17 +77,17 @@ export const Test = () => {
<VStack gap={2}> <VStack gap={2}>
<H3>Payment buttons example</H3> <H3>Payment buttons example</H3>
<Grid cols={3} gap={4}> <Grid cols={3} gap={4}>
<Button variant="outline" style={{ height: "80px", flexDirection: "column" }}> <Button variant="outline" style={{ height: '80px', flexDirection: 'column' }}>
<div style={{ fontSize: "24px" }}>💳</div> <div style={{ fontSize: '24px' }}>💳</div>
<span style={{ fontSize: "12px" }}>Card</span> <span style={{ fontSize: '12px' }}>Card</span>
</Button> </Button>
<Button variant="outline" style={{ height: "80px", flexDirection: "column" }}> <Button variant="outline" style={{ height: '80px', flexDirection: 'column' }}>
<div style={{ fontSize: "24px" }}>🍎</div> <div style={{ fontSize: '24px' }}>🍎</div>
<span style={{ fontSize: "12px" }}>Apple</span> <span style={{ fontSize: '12px' }}>Apple</span>
</Button> </Button>
<Button variant="outline" style={{ height: "80px", flexDirection: "column" }}> <Button variant="outline" style={{ height: '80px', flexDirection: 'column' }}>
<div style={{ fontSize: "24px" }}>💰</div> <div style={{ fontSize: '24px' }}>💰</div>
<span style={{ fontSize: "12px" }}>PayPal</span> <span style={{ fontSize: '12px' }}>PayPal</span>
</Button> </Button>
</Grid> </Grid>
</VStack> </VStack>
@ -145,18 +95,18 @@ export const Test = () => {
{/* Alignment examples */} {/* Alignment examples */}
<VStack gap={2}> <VStack gap={2}>
<H3>Alignment: v="center" h="center"</H3> <H3>Alignment: v="center" h="center"</H3>
<Grid cols={3} gap={4} v="center" h="center" style={{ height: "128px", backgroundColor: "#f3f4f6" }}> <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: '#fecaca', padding: '8px' }}>Item 1</div>
<div style={{ backgroundColor: "#bbf7d0", padding: "8px" }}>Item 2</div> <div style={{ backgroundColor: '#bbf7d0', padding: '8px' }}>Item 2</div>
<div style={{ backgroundColor: "#bfdbfe", padding: "8px" }}>Item 3</div> <div style={{ backgroundColor: '#bfdbfe', padding: '8px' }}>Item 3</div>
</Grid> </Grid>
</VStack> </VStack>
<VStack gap={2}> <VStack gap={2}>
<H3>Alignment: v="start" h="end"</H3> <H3>Alignment: v="start" h="end"</H3>
<Grid cols={2} gap={4} v="start" h="end" style={{ height: "96px", backgroundColor: "#f3f4f6" }}> <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: '#e9d5ff', padding: '8px' }}>Left</div>
<div style={{ backgroundColor: "#fed7aa", padding: "8px" }}>Right</div> <div style={{ backgroundColor: '#fed7aa', padding: '8px' }}>Right</div>
</Grid> </Grid>
</VStack> </VStack>
</VStack> </VStack>

View File

@ -1,27 +1,50 @@
import "hono/jsx" import { define } from 'forge'
import type { FC, JSX } from "hono/jsx" import { theme } from './theme'
import * as icons from "lucide-static" import * as icons from 'lucide-static'
import { Grid } from "./grid" import { Grid } from './grid'
import { VStack } from "./stack" import { VStack } from './stack'
import { Section } from "./section" import { Section } from './section'
import { H2, Text } from "./text" import { H2, Text } from './text'
import { CodeExamples } from "./code"
import { cn } from "./cn"
export type IconName = keyof typeof icons export type IconName = keyof typeof icons
type IconProps = JSX.IntrinsicElements["div"] & { // Icon wrapper - the SVG is injected via dangerouslySetInnerHTML
const IconWrapper = define('Icon', {
display: 'block',
flexShrink: 0,
})
// IconLink wrapper
const IconLinkWrapper = define('IconLink', {
base: 'a',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'opacity 0.2s',
states: {
':hover': {
opacity: 0.7,
},
},
})
type IconProps = Parameters<typeof IconWrapper>[0] & {
name: IconName name: IconName
size?: number size?: number
} }
type IconLinkProps = JSX.IntrinsicElements["a"] & { type IconLinkProps = Parameters<typeof IconLinkWrapper>[0] & {
name: IconName name: IconName
size?: number size?: number
} }
export const Icon: FC<IconProps> = (props) => { function sizeToPixels(size: number): number {
const { name, size = 6, class: className, style, id, ref, ...rest } = props return size * 4
}
export const Icon = (props: IconProps) => {
const { name, size = 6, style, ...rest } = props
const iconSvg = icons[name] const iconSvg = icons[name]
@ -30,64 +53,42 @@ export const Icon: FC<IconProps> = (props) => {
} }
const pixelSize = sizeToPixels(size) const pixelSize = sizeToPixels(size)
const iconStyle: JSX.CSSProperties = { const iconStyle = {
display: "block",
flexShrink: "0",
width: `${pixelSize}px`, width: `${pixelSize}px`,
height: `${pixelSize}px`, height: `${pixelSize}px`,
...(style as JSX.CSSProperties), ...style,
} }
// Modify the SVG string to include our custom attributes // Modify the SVG string to include our custom attributes
const modifiedSvg = iconSvg const modifiedSvg = iconSvg
.replace(/width="[^"]*"/, "") .replace(/width="[^"]*"/, '')
.replace(/height="[^"]*"/, "") .replace(/height="[^"]*"/, '')
.replace(/class="[^"]*"/, "") .replace(/class="[^"]*"/, '')
.replace( .replace(
/<svg([^>]*)>/, /<svg([^>]*)>/,
`<svg$1 style="display: block; flex-shrink: 0; width: ${pixelSize}px; height: ${pixelSize}px;" class="${cn("Icon", className)}">` `<svg$1 style="display: block; flex-shrink: 0; width: ${pixelSize}px; height: ${pixelSize}px;">`
) )
return <div dangerouslySetInnerHTML={{ __html: modifiedSvg }} style={iconStyle} id={id} ref={ref} {...rest} /> return <IconWrapper dangerouslySetInnerHTML={{ __html: modifiedSvg }} style={iconStyle} {...rest} />
} }
export const IconLink: FC<IconLinkProps> = (props) => { export const IconLink = (props: IconLinkProps) => {
const { href = "#", target, class: className, style, id, ref, name, size, ...rest } = props const { href = '#', name, size, ...rest } = props
const linkStyle: JSX.CSSProperties = {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
transition: "opacity 0.2s",
...(style as JSX.CSSProperties),
}
return ( return (
<a href={href} target={target} class={cn("IconLink", className)} style={linkStyle} id={id} ref={ref} {...rest}> <IconLinkWrapper href={href} {...rest}>
<Icon name={name} size={size} /> <Icon name={name} size={size} />
</a> </IconLinkWrapper>
) )
} }
export const Test = () => { export const Test = () => {
return ( return (
<Section> <Section>
{/* API Usage Examples */}
<CodeExamples
examples={[
'<Icon name="Heart" />',
'<Icon name="Star" size={8} />',
'<Icon name="Home" style={{ color: "#3b82f6" }} />',
'<IconLink name="ExternalLink" href="/link" />',
]}
/>
{/* === ICON TESTS === */}
{/* Size variations */} {/* Size variations */}
<VStack gap={4}> <VStack gap={4}>
<H2>Icon Size Variations</H2> <H2>Icon Size Variations</H2>
<div style={{ display: "flex", alignItems: "center", gap: "16px" }}> <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
{([3, 4, 5, 6, 8, 10, 12, 16] as const).map((size) => ( {([3, 4, 5, 6, 8, 10, 12, 16] as const).map((size) => (
<VStack h="center" gap={2} key={size}> <VStack h="center" gap={2} key={size}>
<Icon name="Heart" size={size} /> <Icon name="Heart" size={size} />
@ -106,7 +107,7 @@ export const Test = () => {
<Text>Default</Text> <Text>Default</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Icon name="Star" size={12} style={{ color: "#3b82f6" }} /> <Icon name="Star" size={12} style={{ color: '#3b82f6' }} />
<Text>Blue Color</Text> <Text>Blue Color</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
@ -114,11 +115,11 @@ export const Test = () => {
<Text>Thin Stroke</Text> <Text>Thin Stroke</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Icon name="Star" size={12} style={{ color: "#fbbf24", fill: "currentColor", stroke: "none" }} /> <Icon name="Star" size={12} style={{ color: '#fbbf24', fill: 'currentColor', stroke: 'none' }} />
<Text>Filled</Text> <Text>Filled</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Icon name="Star" size={12} style={{ color: "#a855f7", transition: "color 0.2s" }} /> <Icon name="Star" size={12} style={{ color: '#a855f7', transition: 'color 0.2s' }} />
<Text>Hover Effect</Text> <Text>Hover Effect</Text>
</VStack> </VStack>
</Grid> </Grid>
@ -129,30 +130,28 @@ export const Test = () => {
<H2>Advanced Styling</H2> <H2>Advanced Styling</H2>
<Grid cols={4} gap={6}> <Grid cols={4} gap={6}>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Icon name="Heart" size={12} style={{ color: "#ef4444", fill: "currentColor", stroke: "none" }} /> <Icon name="Heart" size={12} style={{ color: '#ef4444', fill: 'currentColor', stroke: 'none' }} />
<Text>Filled Heart</Text> <Text>Filled Heart</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Icon name="Shield" size={12} style={{ color: "#16a34a", strokeWidth: "2" }} /> <Icon name="Shield" size={12} style={{ color: '#16a34a', strokeWidth: '2' }} />
<Text>Thick Stroke</Text> <Text>Thick Stroke</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Icon name="Sun" size={12} style={{ color: "#eab308" }} /> <Icon name="Sun" size={12} style={{ color: '#eab308' }} />
<Text>Sun Icon</Text> <Text>Sun Icon</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <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))" }} /> <Icon name="Zap" size={12} style={{ color: '#60a5fa', filter: 'drop-shadow(0 4px 6px rgba(0,0,0,0.3))' }} />
<Text>Drop Shadow</Text> <Text>Drop Shadow</Text>
</VStack> </VStack>
</Grid> </Grid>
</VStack> </VStack>
{/* === ICON LINK TESTS === */} {/* Icon links */}
{/* Basic icon links */}
<VStack gap={4}> <VStack gap={4}>
<H2>Icon Links</H2> <H2>Icon Links</H2>
<div style={{ display: "flex", gap: "24px" }}> <div style={{ display: 'flex', gap: '24px' }}>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<IconLink name="Home" size={8} href="/" /> <IconLink name="Home" size={8} href="/" />
<Text>Home Link</Text> <Text>Home Link</Text>
@ -182,10 +181,10 @@ export const Test = () => {
size={8} size={8}
href="#" href="#"
style={{ style={{
backgroundColor: "#3b82f6", backgroundColor: '#3b82f6',
color: "white", color: 'white',
padding: "8px", padding: '8px',
borderRadius: "8px", borderRadius: '8px',
}} }}
/> />
<Text>Button Style</Text> <Text>Button Style</Text>
@ -196,19 +195,19 @@ export const Test = () => {
size={8} size={8}
href="#" href="#"
style={{ style={{
border: "2px solid #d1d5db", border: '2px solid #d1d5db',
padding: "8px", padding: '8px',
borderRadius: "9999px", borderRadius: '9999px',
}} }}
/> />
<Text>Circle Border</Text> <Text>Circle Border</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<IconLink name="Heart" size={8} href="#" style={{ color: "#ef4444" }} /> <IconLink name="Heart" size={8} href="#" style={{ color: '#ef4444' }} />
<Text>Red Heart</Text> <Text>Red Heart</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<IconLink name="Star" size={8} href="#" style={{ color: "#fbbf24", fill: "currentColor" }} /> <IconLink name="Star" size={8} href="#" style={{ color: '#fbbf24', fill: 'currentColor' }} />
<Text>Filled Star</Text> <Text>Filled Star</Text>
</VStack> </VStack>
</Grid> </Grid>
@ -216,7 +215,3 @@ export const Test = () => {
</Section> </Section>
) )
} }
function sizeToPixels(size: number): number {
return size * 4
}

View File

@ -1,51 +1,35 @@
import { Section } from "./section" import { define } from 'forge'
import { H2, H3, H4, H5, Text, SmallText } from "./text" import { Section } from './section'
import "hono/jsx" import { H2, H3, H4, H5, Text } from './text'
import type { FC, JSX } from "hono/jsx" import { VStack, HStack } from './stack'
import { VStack, HStack } from "./stack" import { Grid } from './grid'
import { Grid } from "./grid"
import { CodeExamples } from "./code"
import { cn } from "./cn"
export type ImageProps = JSX.IntrinsicElements["img"] & { export const Image = define('Image', {
width?: number base: 'img',
height?: number
objectFit?: "cover" | "contain" | "fill" | "none" | "scale-down"
}
export const Image: FC<ImageProps> = (props) => { variants: {
const { src, alt = "", width, height, objectFit, class: className, style, id, ref, ...rest } = props objectFit: {
cover: { objectFit: 'cover' },
contain: { objectFit: 'contain' },
fill: { objectFit: 'fill' },
none: { objectFit: 'none' },
'scale-down': { objectFit: 'scale-down' },
},
},
})
const imageStyle: JSX.CSSProperties = { export type ImageProps = Parameters<typeof Image>[0]
width: width ? `${width}px` : undefined,
height: height ? `${height}px` : undefined,
objectFit: objectFit,
...(style as JSX.CSSProperties),
}
return <img src={src} alt={alt} class={cn("Image", className)} style={imageStyle} id={id} ref={ref} {...rest} />
}
export const Test = () => { export const Test = () => {
const sampleImages = [ const sampleImages = [
"https://picsum.photos/seed/1/400/600", // Portrait 'https://picsum.photos/seed/1/400/600', // Portrait
"https://picsum.photos/seed/2/600/400", // Landscape 'https://picsum.photos/seed/2/600/400', // Landscape
"https://picsum.photos/seed/3/300/300", // Square 'https://picsum.photos/seed/3/300/300', // Square
"https://picsum.photos/seed/4/200/100", // Small image 'https://picsum.photos/seed/4/200/100', // Small image
] ]
return ( return (
<Section> <Section>
{/* API Usage Examples */}
<CodeExamples
examples={[
'<Image src="/photo.jpg" />',
'<Image src="/photo.jpg" width={200} height={200} />',
'<Image src="/photo.jpg" objectFit="cover" />',
'<Image src="/photo.jpg" style={{ borderRadius: "8px" }} />',
]}
/>
<H2>Image Examples</H2> <H2>Image Examples</H2>
{/* Size variations */} {/* Size variations */}
@ -53,19 +37,19 @@ export const Test = () => {
<H3>Size Variations</H3> <H3>Size Variations</H3>
<HStack gap={4} wrap> <HStack gap={4} wrap>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Image src={sampleImages[0]!} width={64} height={64} objectFit="cover" alt="64x64" /> <Image src={sampleImages[0]} objectFit="cover" style={{ width: 64, height: 64 }} alt="64x64" />
<Text>64x64</Text> <Text>64x64</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Image src={sampleImages[0]!} width={96} height={96} objectFit="cover" alt="96x96" /> <Image src={sampleImages[0]} objectFit="cover" style={{ width: 96, height: 96 }} alt="96x96" />
<Text>96x96</Text> <Text>96x96</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Image src={sampleImages[0]!} width={128} height={128} objectFit="cover" alt="128x128" /> <Image src={sampleImages[0]} objectFit="cover" style={{ width: 128, height: 128 }} alt="128x128" />
<Text>128x128</Text> <Text>128x128</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Image src={sampleImages[0]!} width={192} height={128} objectFit="cover" alt="192x128" /> <Image src={sampleImages[0]} objectFit="cover" style={{ width: 192, height: 128 }} alt="192x128" />
<Text>192x128</Text> <Text>192x128</Text>
</VStack> </VStack>
</HStack> </HStack>
@ -74,61 +58,49 @@ export const Test = () => {
{/* Object fit variations */} {/* Object fit variations */}
<VStack gap={4}> <VStack gap={4}>
<H3>Object Fit Variations</H3> <H3>Object Fit Variations</H3>
<Text> <Text>Same image with different object-fit values</Text>
Same image with different object-fit values
</Text>
<HStack gap={6} wrap> <HStack gap={6} wrap>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Image <Image
src={sampleImages[0]!} src={sampleImages[0]}
width={128}
height={128}
objectFit="cover" objectFit="cover"
style={{ border: "1px solid black" }} style={{ width: 128, height: 128, border: '1px solid black' }}
alt="Object cover" alt="Object cover"
/> />
<Text>object-fit: cover</Text> <Text>object-fit: cover</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Image <Image
src={sampleImages[0]!} src={sampleImages[0]}
width={128}
height={128}
objectFit="contain" objectFit="contain"
style={{ border: "1px solid black", backgroundColor: "#f3f4f6" }} style={{ width: 128, height: 128, border: '1px solid black', backgroundColor: '#f3f4f6' }}
alt="Object contain" alt="Object contain"
/> />
<Text>object-fit: contain</Text> <Text>object-fit: contain</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Image <Image
src={sampleImages[0]!} src={sampleImages[0]}
width={128}
height={128}
objectFit="fill" objectFit="fill"
style={{ border: "1px solid black" }} style={{ width: 128, height: 128, border: '1px solid black' }}
alt="Object fill" alt="Object fill"
/> />
<Text>object-fit: fill</Text> <Text>object-fit: fill</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Image <Image
src={sampleImages[0]!} src={sampleImages[0]}
width={128}
height={128}
objectFit="scale-down" objectFit="scale-down"
style={{ border: "1px solid black", backgroundColor: "#f3f4f6" }} style={{ width: 128, height: 128, border: '1px solid black', backgroundColor: '#f3f4f6' }}
alt="Object scale-down" alt="Object scale-down"
/> />
<Text>object-fit: scale-down</Text> <Text>object-fit: scale-down</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Image <Image
src={sampleImages[0]!} src={sampleImages[0]}
width={128}
height={128}
objectFit="none" objectFit="none"
style={{ border: "1px solid black", backgroundColor: "#f3f4f6" }} style={{ width: 128, height: 128, border: '1px solid black', backgroundColor: '#f3f4f6' }}
alt="Object none" alt="Object none"
/> />
<Text>object-fit: none</Text> <Text>object-fit: none</Text>
@ -142,36 +114,32 @@ export const Test = () => {
<HStack gap={6} wrap> <HStack gap={6} wrap>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Image <Image
src={sampleImages[0]!} src={sampleImages[0]}
width={128}
height={128}
objectFit="cover" objectFit="cover"
style={{ borderRadius: "8px", border: "4px solid #3b82f6" }} style={{ width: 128, height: 128, borderRadius: '8px', border: '4px solid #3b82f6' }}
alt="Rounded with border" alt="Rounded with border"
/> />
<Text>Rounded + Border</Text> <Text>Rounded + Border</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Image <Image
src={sampleImages[1]!} src={sampleImages[1]}
width={128}
height={128}
objectFit="cover" objectFit="cover"
style={{ boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)" }} style={{ width: 128, height: 128, boxShadow: '0 10px 15px rgba(0, 0, 0, 0.3)' }}
alt="With shadow" alt="With shadow"
/> />
<Text>With Shadow</Text> <Text>With Shadow</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<Image <Image
src={sampleImages[2]!} src={sampleImages[2]}
width={128}
height={128}
objectFit="cover" objectFit="cover"
style={{ style={{
borderRadius: "9999px", width: 128,
border: "4px solid #22c55e", height: 128,
boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)", borderRadius: '9999px',
border: '4px solid #22c55e',
boxShadow: '0 10px 15px rgba(0, 0, 0, 0.3)',
}} }}
alt="Circular with effects" alt="Circular with effects"
/> />
@ -188,21 +156,19 @@ export const Test = () => {
<VStack gap={2}> <VStack gap={2}>
<H4>Avatar</H4> <H4>Avatar</H4>
<Image <Image
src={sampleImages[0]!} src={sampleImages[0]}
width={48}
height={48}
objectFit="cover" objectFit="cover"
style={{ borderRadius: "9999px" }} style={{ width: 48, height: 48, borderRadius: '9999px' }}
alt="Avatar" alt="Avatar"
/> />
</VStack> </VStack>
{/* Card image */} {/* Card image */}
<VStack gap={2} style={{ maxWidth: "384px" }}> <VStack gap={2} style={{ maxWidth: '384px' }}>
<H4>Card Image</H4> <H4>Card Image</H4>
<VStack gap={0} style={{ border: "1px solid #d1d5db", borderRadius: "8px", overflow: "hidden" }}> <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" /> <Image src={sampleImages[1]} objectFit="cover" style={{ width: 384, height: 192 }} alt="Card image" />
<VStack gap={1} style={{ padding: "16px" }}> <VStack gap={1} style={{ padding: '16px' }}>
<H5>Card Title</H5> <H5>Card Title</H5>
<Text>Card description goes here</Text> <Text>Card description goes here</Text>
</VStack> </VStack>
@ -217,10 +183,8 @@ export const Test = () => {
<Image <Image
key={i} key={i}
src={src} src={src}
width={120}
height={120}
objectFit="cover" objectFit="cover"
style={{ borderRadius: "4px" }} style={{ width: 120, height: 120, borderRadius: '4px' }}
alt={`Gallery ${i}`} alt={`Gallery ${i}`}
/> />
))} ))}

View File

@ -1,36 +1,40 @@
export { cn } from "./cn" // Re-export Forge utilities
export { Styles, extendThemes } from 'forge'
export { theme } from './theme'
export { Button } from "./button" export { cn } from './cn'
export type { ButtonProps } from "./button"
export { Icon, IconLink } from "./icon" export { Button } from './button'
export type { IconName } from "./icon" export type { ButtonProps } from './button'
export { VStack, HStack } from "./stack" export { Icon, IconLink } from './icon'
export type { IconName } from './icon'
export { Grid } from "./grid" export { VStack, HStack } from './stack'
export { Divider } from "./divider" export { Grid } from './grid'
export { Avatar } from "./avatar" export { Divider } from './divider'
export type { AvatarProps } from "./avatar"
export { Image } from "./image" export { Avatar } from './avatar'
export type { ImageProps } from "./image" export type { AvatarProps } from './avatar'
export { Input } from "./input" export { Image } from './image'
export type { InputProps } from "./input" export type { ImageProps } from './image'
export { Select } from "./select" export { Input } from './input'
export type { SelectProps, SelectOption } from "./select" export type { InputProps } from './input'
export { Placeholder } from "./placeholder" export { Select } from './select'
export { default as PlaceholderDefault } from "./placeholder" export type { SelectProps, SelectOption } from './select'
export { H1, H2, H3, H4, H5, Text, SmallText } from "./text" export { Placeholder } from './placeholder'
export { default as PlaceholderDefault } from './placeholder'
export { Box, RedBox, GreenBox, BlueBox, GrayBox } from "./box" export { H1, H2, H3, H4, H5, Text, SmallText } from './text'
export { Section } from "./section" export { Box, RedBox, GreenBox, BlueBox, GrayBox } from './box'
export type { TailwindSize, CommonHTMLProps } from "./types" export { Section } from './section'
export type { TailwindSize, CommonHTMLProps } from './types'

View File

@ -1,89 +1,99 @@
import { Section } from "./section" import { define } from 'forge'
import { H2, H3, H4, H5, Text, SmallText } from "./text" import { theme } from './theme'
import "hono/jsx" import { Section } from './section'
import type { JSX, FC } from "hono/jsx" import { H2 } from './text'
import { VStack, HStack } from "./stack" import { VStack, HStack } from './stack'
import { CodeExamples } from "./code"
import { cn } from "./cn"
export type InputProps = JSX.IntrinsicElements["input"] & { export const Input = define('Input', {
labelPosition?: "above" | "left" | "right" parts: {
children?: any Wrapper: {
} display: 'flex',
flexDirection: 'column',
gap: theme('spacing-1'),
flex: 1,
minWidth: 0,
},
Label: {
base: 'label',
fontSize: theme('fontSize-sm'),
fontWeight: 500,
color: theme('colors-fg'),
},
Field: {
base: 'input',
height: 40,
padding: `${theme('spacing-2')} ${theme('spacing-3')}`,
borderRadius: theme('radius-md'),
border: `1px solid ${theme('colors-border')}`,
background: theme('colors-bg'),
fontSize: theme('fontSize-sm'),
outline: 'none',
flex: 1,
export const Input: FC<InputProps> = (props) => { states: {
const { labelPosition = "above", children, style, class: className, id, ref, ...inputProps } = props ':focus': {
borderColor: theme('colors-borderActive'),
},
':disabled': {
opacity: 0.5,
cursor: 'not-allowed',
},
},
},
},
const inputStyle: JSX.CSSProperties = { variants: {
height: "40px", labelPosition: {
padding: "8px 12px", above: {
borderRadius: "6px", parts: {
border: "1px solid #d1d5db", Wrapper: {
backgroundColor: "white", flexDirection: 'column',
fontSize: "14px", gap: theme('spacing-1'),
outline: "none", },
...(style as JSX.CSSProperties), },
} },
left: {
parts: {
Wrapper: {
flexDirection: 'row',
alignItems: 'center',
gap: theme('spacing-1'),
},
},
},
right: {
parts: {
Wrapper: {
flexDirection: 'row-reverse',
alignItems: 'center',
gap: theme('spacing-1'),
},
},
},
},
},
if (!children) { render({ props, parts: { Root, Wrapper, Label, Field } }) {
return <input style={inputStyle} {...inputProps} class={cn("Input", className)} id={id} ref={ref} /> const { children, labelPosition = 'above', ...inputProps } = props
}
const labelStyle: JSX.CSSProperties = { if (!children) {
fontSize: "14px", return <Field {...inputProps} />
fontWeight: "500", }
color: "#111827",
}
const labelElement = (
<label for={id} style={labelStyle}>
{children}
</label>
)
if (labelPosition === "above") {
return ( return (
<div style={{ display: "flex", flexDirection: "column", gap: "4px", flex: 1, minWidth: 0 }}> <Wrapper labelPosition={labelPosition}>
{labelElement} <Label for={props.id}>{children}</Label>
<input style={inputStyle} {...inputProps} class={cn("Input", className)} id={id} ref={ref} /> <Field {...inputProps} />
</div> </Wrapper>
) )
} },
})
if (labelPosition === "left") { export type InputProps = Parameters<typeof Input>[0]
return (
<div style={{ display: "flex", alignItems: "center", gap: "4px", flex: 1 }}>
{labelElement}
<input style={{ ...inputStyle, flex: 1 }} {...inputProps} class={cn("Input", className)} id={id} ref={ref} />
</div>
)
}
if (labelPosition === "right") {
return (
<div style={{ display: "flex", alignItems: "center", gap: "4px", flex: 1 }}>
<input style={{ ...inputStyle, flex: 1 }} {...inputProps} class={cn("Input", className)} id={id} ref={ref} />
{labelElement}
</div>
)
}
return null
}
export const Test = () => { export const Test = () => {
return ( return (
<Section maxWidth="448px"> <Section style={{ maxWidth: '448px' }}>
{/* API Usage Examples */}
<CodeExamples
examples={[
'<Input placeholder="Enter name" />',
'<Input type="email" placeholder="Email" />',
'<Input>Label</Input>',
'<Input labelPosition="left">Name</Input>',
]}
/>
{/* Basic inputs */} {/* Basic inputs */}
<VStack gap={4}> <VStack gap={4}>
<H2>Basic Inputs</H2> <H2>Basic Inputs</H2>
@ -98,9 +108,9 @@ export const Test = () => {
<VStack gap={4}> <VStack gap={4}>
<H2>Custom Styling</H2> <H2>Custom Styling</H2>
<VStack gap={4}> <VStack gap={4}>
<Input style={{ height: "32px", fontSize: "12px" }} placeholder="Small input" /> <Input style={{ height: '32px', fontSize: '12px' }} placeholder="Small input" />
<Input placeholder="Default input" /> <Input placeholder="Default input" />
<Input style={{ height: "48px", fontSize: "16px" }} placeholder="Large input" /> <Input style={{ height: '48px', fontSize: '16px' }} placeholder="Large input" />
</VStack> </VStack>
</VStack> </VStack>
@ -117,8 +127,8 @@ export const Test = () => {
<VStack gap={4}> <VStack gap={4}>
<H2>Disabled State</H2> <H2>Disabled State</H2>
<VStack gap={4}> <VStack gap={4}>
<Input disabled placeholder="Disabled input" style={{ opacity: 0.5, cursor: "not-allowed" }} /> <Input disabled placeholder="Disabled input" />
<Input disabled value="Disabled with value" style={{ opacity: 0.5, cursor: "not-allowed" }} /> <Input disabled value="Disabled with value" />
</VStack> </VStack>
</VStack> </VStack>
@ -177,19 +187,6 @@ export const Test = () => {
<Input placeholder="Age">Age</Input> <Input placeholder="Age">Age</Input>
</HStack> </HStack>
</VStack> </VStack>
{/* Custom styling */}
<VStack gap={4}>
<H2>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>
</Section> </Section>
) )
} }

118
src/layout.tsx Normal file
View File

@ -0,0 +1,118 @@
import { define } from 'forge'
import { theme } from './theme'
// Dark mode toggle button
export const DarkModeToggle = define('DarkModeToggle', {
base: 'button',
position: 'fixed',
top: theme('spacing-4'),
right: theme('spacing-4'),
background: theme('colors-bgMuted'),
border: `1px solid ${theme('colors-border')}`,
borderRadius: theme('radius-full'),
padding: `${theme('spacing-2')} ${theme('spacing-4')}`,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: theme('spacing-2'),
fontSize: theme('fontSize-sm'),
fontWeight: 500,
color: theme('colors-fg'),
zIndex: 1000,
states: {
':hover': {
background: theme('colors-primary'),
color: '#ffffff',
borderColor: theme('colors-primary'),
},
},
})
// Page title bar
export const PageTitle = define('PageTitle', {
position: 'relative',
fontSize: '32px',
fontWeight: 700,
padding: theme('spacing-6'),
borderBottom: `1px solid ${theme('colors-border')}`,
background: theme('colors-bg'),
color: theme('colors-fg'),
variants: {
hasHomeLink: {
paddingLeft: '80px',
},
},
})
// Home link button
export const HomeLink = define('HomeLink', {
base: 'a',
position: 'absolute',
left: theme('spacing-6'),
top: '50%',
transform: 'translateY(-50%)',
background: theme('colors-bgMuted'),
border: `1px solid ${theme('colors-border')}`,
borderRadius: theme('radius-lg'),
padding: `${theme('spacing-2')} ${theme('spacing-3')}`,
textDecoration: 'none',
color: theme('colors-fg'),
display: 'flex',
alignItems: 'center',
gap: theme('spacing-2'),
fontSize: theme('fontSize-sm'),
fontWeight: 500,
states: {
':hover': {
background: theme('colors-primary'),
color: '#ffffff',
borderColor: theme('colors-primary'),
},
},
})
// Navigation link for component list
export const NavLink = define('NavLink', {
base: 'a',
color: theme('colors-primary'),
textDecoration: 'none',
fontSize: theme('fontSize-lg'),
lineHeight: 2,
states: {
':hover': {
textDecoration: 'underline',
},
},
})
// Nav list container
export const NavList = define('NavList', {
base: 'ul',
padding: theme('spacing-6'),
listStyle: 'none',
})
// Nav list item
export const NavItem = define('NavItem', {
base: 'li',
})
// Body wrapper that handles theme background
export const Body = define('Body', {
base: 'body',
background: theme('colors-bg'),
color: theme('colors-fg'),
fontFamily: 'system-ui, -apple-system, sans-serif',
margin: 0,
padding: 0,
})
// Code examples container - override styles for dark mode compatibility
export const CodeExamplesWrapper = define('CodeExamplesWrapper', {
background: theme('colors-bgElevated'),
borderColor: theme('colors-border'),
})

View File

@ -1,65 +1,51 @@
import { Section } from "./section" import { Section } from './section'
import { H2, H3, H4, H5, Text, SmallText } from "./text" import { H2, H3, Text, SmallText } from './text'
import "hono/jsx" import { Avatar } from './avatar'
import { Avatar } from "./avatar" import type { AvatarProps } from './avatar'
import type { AvatarProps } from "./avatar" import { Image } from './image'
import { Image } from "./image" import type { ImageProps } from './image'
import type { ImageProps } from "./image" import { VStack, HStack } from './stack'
import { VStack, HStack } from "./stack" import { Grid } from './grid'
import { Grid } from "./grid"
import { CodeExamples } from "./code"
export const Placeholder = { export const Placeholder = {
Avatar(props: PlaceholderAvatarProps) { Avatar(props: PlaceholderAvatarProps) {
const { size = 32, seed = "seed", type = "dylan", transparent, alt, style, rounded, id, ref, class: className } = props const { size = 32, seed = 'seed', type = 'dylan', transparent, alt, style, rounded, id, ref, class: className } = props
// Generate DiceBear avatar URL // Generate DiceBear avatar URL
const url = new URL(`https://api.dicebear.com/9.x/${type}/svg`) const url = new URL(`https://api.dicebear.com/9.x/${type}/svg`)
url.searchParams.set("seed", seed) url.searchParams.set('seed', seed)
url.searchParams.set("size", size.toString()) url.searchParams.set('size', size.toString())
if (transparent) { if (transparent) {
url.searchParams.set("backgroundColor", "transparent") url.searchParams.set('backgroundColor', 'transparent')
} }
return <Avatar src={url.toString()} alt={alt} style={style} size={size} rounded={rounded} id={id} ref={ref} class={className} /> return <Avatar src={url.toString()} alt={alt} style={style} size={size as any} rounded={rounded} id={id} ref={ref} class={className} />
}, },
Image(props: PlaceholderImageProps) { Image(props: PlaceholderImageProps) {
const { width = 200, height = 200, seed = 1, alt = "Placeholder image", objectFit, style, id, ref, class: className } = props const { width = 200, height = 200, seed = 1, alt = 'Placeholder image', objectFit, style, id, ref, class: className } = props
// Generate Picsum Photos URL with seed for consistent images // Generate Picsum Photos URL with seed for consistent images
const src = `https://picsum.photos/${width}/${height}?random=${seed}` const src = `https://picsum.photos/${width}/${height}?random=${seed}`
return <Image src={src} alt={alt} width={width} height={height} objectFit={objectFit} style={style} id={id} ref={ref} class={className} /> return <Image src={src} alt={alt} objectFit={objectFit} width={width} height={height} style={style} id={id} ref={ref} class={className} />
}, },
} }
export const Test = () => { export const Test = () => {
return ( return (
<Section> <Section>
{/* API Usage Examples */}
<CodeExamples
examples={[
'<Placeholder.Avatar />',
'<Placeholder.Avatar type="avataaars" size={64} />',
'<Placeholder.Image width={200} height={200} />',
'<Placeholder.Image seed={42} />',
]}
/>
{/* === AVATAR TESTS === */}
{/* Show all available avatar styles */} {/* Show all available avatar styles */}
<VStack gap={4}> <VStack gap={4}>
<H2> <H2>
All Avatar Styles ({allStyles.length} total) All Avatar Styles ({allStyles.length} total)
</H2> </H2>
<Grid cols={10} gap={3}> <Grid cols={6} gap={3}>
{allStyles.map((style) => ( {allStyles.slice(0, 12).map((style) => (
<VStack h="center" gap={1} key={style}> <VStack h="center" gap={1} key={style}>
<Placeholder.Avatar type={style} size={48} /> <Placeholder.Avatar type={style} size={48} />
<SmallText style={{ fontWeight: "500" }}>{style}</SmallText> <SmallText style={{ fontWeight: '500' }}>{style}</SmallText>
</VStack> </VStack>
))} ))}
</Grid> </Grid>
@ -87,7 +73,7 @@ export const Test = () => {
<Text>Rounded + Background</Text> <Text>Rounded + Background</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<div style={{ backgroundColor: "#e5e7eb", padding: "8px" }}> <div style={{ backgroundColor: '#e5e7eb', padding: '8px' }}>
<Placeholder.Avatar rounded transparent size={64} /> <Placeholder.Avatar rounded transparent size={64} />
</div> </div>
<Text>Rounded + Transparent</Text> <Text>Rounded + Transparent</Text>
@ -97,7 +83,7 @@ export const Test = () => {
<Text>Square + Background</Text> <Text>Square + Background</Text>
</VStack> </VStack>
<VStack h="center" gap={2}> <VStack h="center" gap={2}>
<div style={{ backgroundColor: "#e5e7eb", padding: "8px" }}> <div style={{ backgroundColor: '#e5e7eb', padding: '8px' }}>
<Placeholder.Avatar transparent size={64} /> <Placeholder.Avatar transparent size={64} />
</div> </div>
<Text>Square + Transparent</Text> <Text>Square + Transparent</Text>
@ -111,7 +97,7 @@ export const Test = () => {
Avatar Seeds (Same Style, Different People) Avatar Seeds (Same Style, Different People)
</H2> </H2>
<HStack gap={4}> <HStack gap={4}>
{["alice", "bob", "charlie", "diana"].map((seed) => ( {['alice', 'bob', 'charlie', 'diana'].map((seed) => (
<VStack h="center" gap={2} key={seed}> <VStack h="center" gap={2} key={seed}>
<Placeholder.Avatar seed={seed} size={64} /> <Placeholder.Avatar seed={seed} size={64} />
<Text>"{seed}"</Text> <Text>"{seed}"</Text>
@ -120,7 +106,7 @@ export const Test = () => {
</HStack> </HStack>
</VStack> </VStack>
{/* === IMAGE TESTS === */} {/* Placeholder Images */}
<VStack gap={6}> <VStack gap={6}>
<H2>Placeholder Images</H2> <H2>Placeholder Images</H2>
@ -137,7 +123,7 @@ export const Test = () => {
<VStack h="center" gap={2} key={`${width}x${height}`}> <VStack h="center" gap={2} key={`${width}x${height}`}>
<Placeholder.Image width={width} height={height} seed={1} /> <Placeholder.Image width={width} height={height} seed={1} />
<Text> <Text>
{width}×{height} {width}x{height}
</Text> </Text>
</VStack> </VStack>
))} ))}
@ -169,7 +155,7 @@ export const Test = () => {
height={150} height={150}
seed={1} seed={1}
objectFit="cover" objectFit="cover"
style={{ borderRadius: "8px", border: "4px solid #3b82f6" }} style={{ borderRadius: '8px', border: '4px solid #3b82f6' }}
/> />
<Text>Rounded + Border</Text> <Text>Rounded + Border</Text>
</VStack> </VStack>
@ -179,7 +165,7 @@ export const Test = () => {
height={150} height={150}
seed={2} seed={2}
objectFit="cover" objectFit="cover"
style={{ boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)" }} style={{ boxShadow: '0 10px 15px rgba(0, 0, 0, 0.3)' }}
/> />
<Text>With Shadow</Text> <Text>With Shadow</Text>
</VStack> </VStack>
@ -190,9 +176,9 @@ export const Test = () => {
seed={3} seed={3}
objectFit="cover" objectFit="cover"
style={{ style={{
borderRadius: "9999px", borderRadius: '9999px',
border: "4px solid #22c55e", border: '4px solid #22c55e',
boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)", boxShadow: '0 10px 15px rgba(0, 0, 0, 0.3)',
}} }}
/> />
<Text>Circular + Effects</Text> <Text>Circular + Effects</Text>
@ -205,13 +191,13 @@ export const Test = () => {
} }
// Type definitions // Type definitions
type PlaceholderAvatarProps = Omit<AvatarProps, "src"> & { type PlaceholderAvatarProps = Omit<AvatarProps, 'src'> & {
seed?: string seed?: string
type?: DicebearStyleName type?: DicebearStyleName
transparent?: boolean transparent?: boolean
} }
type PlaceholderImageProps = Omit<ImageProps, "src" | "alt"> & { type PlaceholderImageProps = Omit<ImageProps, 'src' | 'alt'> & {
width?: number width?: number
height?: number height?: number
seed?: number seed?: number
@ -220,36 +206,36 @@ type PlaceholderImageProps = Omit<ImageProps, "src" | "alt"> & {
// All supported DiceBear HTTP styleNames. Source: https://www.dicebear.com/styles // All supported DiceBear HTTP styleNames. Source: https://www.dicebear.com/styles
const allStyles = [ const allStyles = [
"adventurer", 'adventurer',
"adventurer-neutral", 'adventurer-neutral',
"avataaars", 'avataaars',
"avataaars-neutral", 'avataaars-neutral',
"big-ears", 'big-ears',
"big-ears-neutral", 'big-ears-neutral',
"big-smile", 'big-smile',
"bottts", 'bottts',
"bottts-neutral", 'bottts-neutral',
"croodles", 'croodles',
"croodles-neutral", 'croodles-neutral',
"dylan", 'dylan',
"fun-emoji", 'fun-emoji',
"glass", 'glass',
"icons", 'icons',
"identicon", 'identicon',
"initials", 'initials',
"lorelei", 'lorelei',
"lorelei-neutral", 'lorelei-neutral',
"micah", 'micah',
"miniavs", 'miniavs',
"notionists", 'notionists',
"notionists-neutral", 'notionists-neutral',
"open-peeps", 'open-peeps',
"personas", 'personas',
"pixel-art", 'pixel-art',
"pixel-art-neutral", 'pixel-art-neutral',
"rings", 'rings',
"shapes", 'shapes',
"thumbs", 'thumbs',
] as const ] as const
type DicebearStyleName = (typeof allStyles)[number] type DicebearStyleName = (typeof allStyles)[number]

View File

@ -1,59 +1,48 @@
import "hono/jsx" import { define } from 'forge'
import type { FC, PropsWithChildren, JSX } from "hono/jsx" import { theme } from './theme'
import { VStack } from "./stack"
import type { TailwindSize } from "./types"
import { CodeExamples } from "./code"
type SectionProps = JSX.IntrinsicElements["div"] & PropsWithChildren & { export const Section = define('Section', {
gap?: TailwindSize display: 'flex',
maxWidth?: string flexDirection: 'column',
} padding: theme('spacing-6'),
export const Section: FC<SectionProps> = (props) => { variants: {
const { children, gap = 8, maxWidth, class: className, style, id, ref, ...rest } = props gap: {
0: { gap: 0 },
return ( 1: { gap: theme('spacing-1') },
<VStack gap={gap} class={className} style={{ padding: "24px", maxWidth, ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}> 2: { gap: theme('spacing-2') },
{children} 3: { gap: theme('spacing-3') },
</VStack> 4: { gap: theme('spacing-4') },
) 6: { gap: theme('spacing-6') },
} 8: { gap: theme('spacing-8') },
12: { gap: theme('spacing-12') },
},
},
})
export const Test = () => { export const Test = () => {
return ( return (
<div> <div>
{/* API Usage Examples */}
<div style={{ margin: "24px" }}>
<CodeExamples
examples={[
'<Section>...</Section>',
'<Section gap={4}>...</Section>',
'<Section maxWidth="600px">...</Section>',
'<Section style={{ backgroundColor: "#f3f4f6" }}>...</Section>',
]}
/>
</div>
<Section> <Section>
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Default Section</h2> <h2 style={{ fontSize: '20px', fontWeight: 'bold' }}>Default Section</h2>
<p>This is a section with default gap (8)</p> <p>This is a section with default styling</p>
<p>It has padding and vertical spacing between children</p> <p>It has padding and vertical spacing between children</p>
</Section> </Section>
<Section gap={4}> <Section gap={4}>
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Compact Section</h2> <h2 style={{ fontSize: '20px', fontWeight: 'bold' }}>Compact Section</h2>
<p>This section has a smaller gap (4)</p> <p>This section has a smaller gap (4)</p>
<p>Items are closer together</p> <p>Items are closer together</p>
</Section> </Section>
<Section gap={12}> <Section gap={12}>
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Spacious Section</h2> <h2 style={{ fontSize: '20px', fontWeight: 'bold' }}>Spacious Section</h2>
<p>This section has a larger gap (12)</p> <p>This section has a larger gap (12)</p>
<p>Items have more breathing room</p> <p>Items have more breathing room</p>
</Section> </Section>
<Section maxWidth="600px" style={{ backgroundColor: "#f3f4f6" }}> <Section style={{ backgroundColor: '#f3f4f6', maxWidth: '600px' }}>
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Constrained Width Section</h2> <h2 style={{ fontSize: '20px', fontWeight: 'bold' }}>Constrained Width Section</h2>
<p>This section has a max width of 600px and a gray background</p> <p>This section has a max width of 600px and a gray background</p>
<p>Good for centering content on wide screens</p> <p>Good for centering content on wide screens</p>
</Section> </Section>

View File

@ -1,10 +1,8 @@
import { Section } from "./section" import { define } from 'forge'
import { H2, H3, H4, H5, Text, SmallText } from "./text" import { theme } from './theme'
import "hono/jsx" import { Section } from './section'
import type { JSX, FC } from "hono/jsx" import { H2 } from './text'
import { VStack, HStack } from "./stack" import { VStack, HStack } from './stack'
import { CodeExamples } from "./code"
import { cn } from "./cn"
export type SelectOption = { export type SelectOption = {
value: string value: string
@ -12,139 +10,137 @@ export type SelectOption = {
disabled?: boolean disabled?: boolean
} }
export type SelectProps = Omit<JSX.IntrinsicElements["select"], "children"> & { // Custom dropdown arrow as base64 SVG
options: SelectOption[] const dropdownArrow = `url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQgNkw4IDEwTDEyIDYiIHN0cm9rZT0iIzZCNzI4MCIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+")`
placeholder?: string
labelPosition?: "above" | "left" | "right"
children?: any
}
export const Select: FC<SelectProps> = (props) => { export const Select = define('Select', {
const { options, placeholder, labelPosition = "above", children, style, class: className, id, ref, ...selectProps } = props parts: {
Wrapper: {
display: 'flex',
flexDirection: 'column',
gap: theme('spacing-1'),
flex: 1,
minWidth: 0,
},
Label: {
base: 'label',
fontSize: theme('fontSize-sm'),
fontWeight: 500,
color: theme('colors-fg'),
},
Field: {
base: 'select',
height: 40,
padding: `${theme('spacing-2')} 32px ${theme('spacing-2')} ${theme('spacing-3')}`,
borderRadius: theme('radius-md'),
border: `1px solid ${theme('colors-border')}`,
background: theme('colors-bg'),
fontSize: theme('fontSize-sm'),
outline: 'none',
appearance: 'none',
backgroundImage: dropdownArrow,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'right 8px center',
backgroundSize: '16px' as any,
flex: 1,
// If a label is provided but no id, generate a random id so the label can be clicked states: {
const elementId = id || (children ? `random-${Math.random().toString(36)}` : undefined) ':focus': {
borderColor: theme('colors-borderActive'),
},
':disabled': {
opacity: 0.5,
cursor: 'not-allowed',
},
},
},
},
const selectStyle: JSX.CSSProperties = { variants: {
height: "40px", labelPosition: {
padding: "8px 32px 8px 12px", above: {
borderRadius: "6px", parts: {
border: "1px solid #d1d5db", Wrapper: {
backgroundColor: "white", flexDirection: 'column',
fontSize: "14px", gap: theme('spacing-1'),
outline: "none", },
appearance: "none", },
backgroundImage: `url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQgNkw4IDEwTDEyIDYiIHN0cm9rZT0iIzZCNzI4MCIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+")`, },
backgroundRepeat: "no-repeat", left: {
backgroundPosition: "right 8px center", parts: {
backgroundSize: "16px 16px", Wrapper: {
...style, flexDirection: 'row',
} alignItems: 'center',
gap: theme('spacing-1'),
},
},
},
right: {
parts: {
Wrapper: {
flexDirection: 'row-reverse',
alignItems: 'center',
gap: theme('spacing-1'),
},
},
},
},
},
const selectElement = ( render({ props, parts: { Root, Wrapper, Label, Field } }) {
<select style={selectStyle} {...selectProps} class={cn("Select", className)} id={elementId} ref={ref}> const { children, options, placeholder, labelPosition = 'above', ...selectProps } = props
{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) { // Generate id for label association if needed
return selectElement const elementId = props.id || (children ? `select-${Math.random().toString(36).slice(2)}` : undefined)
}
const labelStyle: JSX.CSSProperties = { const selectElement = (
fontSize: "14px", <Field {...selectProps} id={elementId}>
fontWeight: "500", {placeholder && (
color: "#111827", <option value="" disabled>
} {placeholder}
</option>
)}
{options?.map((option: SelectOption) => (
<option
key={option.value}
value={option.value}
disabled={option.disabled}
>
{option.label}
</option>
))}
</Field>
)
const labelElement = ( if (!children) {
<label for={elementId} style={labelStyle}> return selectElement
{children} }
</label>
)
if (labelPosition === "above") {
return ( return (
<div style={{ display: "flex", flexDirection: "column", gap: "4px", flex: 1, minWidth: 0 }}> <Wrapper labelPosition={labelPosition}>
{labelElement} <Label for={elementId}>{children}</Label>
{selectElement} {selectElement}
</div> </Wrapper>
) )
} },
})
if (labelPosition === "left") { export type SelectProps = Parameters<typeof Select>[0]
return (
<div style={{ display: "flex", alignItems: "center", gap: "4px", flex: 1 }}>
{labelElement}
<select style={{ ...selectStyle, flex: 1 }} {...selectProps} class={cn("Select", className)} id={elementId} ref={ref}>
{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} class={cn("Select", className)} id={elementId} ref={ref}>
{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 = () => { export const Test = () => {
const options = [{ value: "1", label: "Option 1" }, { value: "2", label: "Option 2" }]
const months = [ const months = [
{ value: "01", label: "January" }, { value: '01', label: 'January' },
{ value: "02", label: "February" }, { value: '02', label: 'February' },
{ value: "03", label: "March" }, { value: '03', label: 'March' },
{ value: "04", label: "April" }, { value: '04', label: 'April' },
{ value: "05", label: "May" }, { value: '05', label: 'May' },
{ value: "06", label: "June" }, { value: '06', label: 'June' },
{ value: "07", label: "July" }, { value: '07', label: 'July' },
{ value: "08", label: "August" }, { value: '08', label: 'August' },
{ value: "09", label: "September" }, { value: '09', label: 'September' },
{ value: "10", label: "October" }, { value: '10', label: 'October' },
{ value: "11", label: "November" }, { value: '11', label: 'November' },
{ value: "12", label: "December" }, { value: '12', label: 'December' },
] ]
const years = Array.from({ length: 10 }, (_, i) => ({ const years = Array.from({ length: 10 }, (_, i) => ({
@ -153,26 +149,16 @@ export const Test = () => {
})) }))
const countries = [ const countries = [
{ value: "us", label: "United States" }, { value: 'us', label: 'United States' },
{ value: "ca", label: "Canada" }, { value: 'ca', label: 'Canada' },
{ value: "uk", label: "United Kingdom" }, { value: 'uk', label: 'United Kingdom' },
{ value: "de", label: "Germany" }, { value: 'de', label: 'Germany' },
{ value: "fr", label: "France" }, { value: 'fr', label: 'France' },
{ value: "au", label: "Australia", disabled: true }, { value: 'au', label: 'Australia', disabled: true },
] ]
return ( return (
<Section maxWidth="448px"> <Section style={{ maxWidth: '448px' }}>
{/* API Usage Examples */}
<CodeExamples
examples={[
'<Select options={options} />',
'<Select options={options} placeholder="Choose" />',
'<Select options={options}>Label</Select>',
'<Select options={options} labelPosition="left">Label</Select>',
]}
/>
{/* Basic selects */} {/* Basic selects */}
<VStack gap={4}> <VStack gap={4}>
<H2>Basic Selects</H2> <H2>Basic Selects</H2>
@ -196,22 +182,8 @@ export const Test = () => {
<VStack gap={4}> <VStack gap={4}>
<H2>Disabled State</H2> <H2>Disabled State</H2>
<VStack gap={4}> <VStack gap={4}>
<Select <Select options={months} disabled placeholder="Disabled select" />
options={months} <Select options={years} disabled value="2024" />
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>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>
</VStack> </VStack>

View File

@ -1,146 +1,123 @@
import type { TailwindSize } from "./types" import { define } from 'forge'
import "hono/jsx" import { theme } from './theme'
import type { FC, PropsWithChildren, JSX } from "hono/jsx" import { H2 } from './text'
import { Grid } from "./grid" import { RedBox, GreenBox, BlueBox } from './box'
import { Section } from "./section" import { Grid } from './grid'
import { H2 } from "./text" import { Section } from './section'
import { RedBox, GreenBox, BlueBox } from "./box"
import { CodeExamples } from "./code"
import { cn } from "./cn"
export const VStack: FC<VStackProps> = (props) => { export const VStack = define('VStack', {
const { v, h, wrap, gap, maxWidth, rows, class: className, style, id, ref, children, ...rest } = props display: 'flex',
return ( flexDirection: 'column',
<Stack
direction="col"
mainAxis={v}
crossAxis={h}
wrap={wrap}
gap={gap}
maxWidth={maxWidth}
gridSizes={rows}
componentName="VStack"
class={className}
style={style}
id={id}
ref={ref}
{...rest}
>
{children}
</Stack>
)
}
export const HStack: FC<HStackProps> = (props) => { variants: {
const { h, v, wrap, gap, maxWidth, cols, class: className, style, id, ref, children, ...rest } = props gap: {
return ( 0: { gap: 0 },
<Stack 1: { gap: theme('spacing-1') },
direction="row" 2: { gap: theme('spacing-2') },
mainAxis={h} 3: { gap: theme('spacing-3') },
crossAxis={v} 4: { gap: theme('spacing-4') },
wrap={wrap} 6: { gap: theme('spacing-6') },
gap={gap} 8: { gap: theme('spacing-8') },
maxWidth={maxWidth} 12: { gap: theme('spacing-12') },
gridSizes={cols} },
componentName="HStack" v: {
class={className} start: { justifyContent: 'flex-start' },
style={style} center: { justifyContent: 'center' },
id={id} end: { justifyContent: 'flex-end' },
ref={ref} between: { justifyContent: 'space-between' },
{...rest} around: { justifyContent: 'space-around' },
> evenly: { justifyContent: 'space-evenly' },
{children} },
</Stack> h: {
) start: { alignItems: 'flex-start' },
} center: { alignItems: 'center' },
end: { alignItems: 'flex-end' },
stretch: { alignItems: 'stretch' },
baseline: { alignItems: 'baseline' },
},
wrap: {
flexWrap: 'wrap',
},
},
const Stack: FC<StackProps> = (props) => { render({ props, parts: { Root } }) {
const { direction, mainAxis, crossAxis, wrap, gap, maxWidth, gridSizes, componentName, class: className, style, id, ref, children, ...rest } = props const { rows, style, ...rest } = props
const gapPx = gap ? gap * 4 : 0 const gridStyle = rows
? { display: 'grid', gridTemplateRows: rows.map((r: number) => `${r}fr`).join(' '), ...style }
: style
return <Root style={gridStyle} {...rest}>{props.children}</Root>
},
})
// Use CSS Grid when gridSizes (cols/rows) is provided export const HStack = define('HStack', {
if (gridSizes) { display: 'flex',
const gridTemplate = gridSizes.map(size => `${size}fr`).join(" ") flexDirection: 'row',
const gridStyles: JSX.CSSProperties = { variants: {
display: "grid", gap: {
gap: `${gapPx}px`, 0: { gap: 0 },
maxWidth: maxWidth, 1: { gap: theme('spacing-1') },
} 2: { gap: theme('spacing-2') },
3: { gap: theme('spacing-3') },
4: { gap: theme('spacing-4') },
6: { gap: theme('spacing-6') },
8: { gap: theme('spacing-8') },
12: { gap: theme('spacing-12') },
},
h: {
start: { justifyContent: 'flex-start' },
center: { justifyContent: 'center' },
end: { justifyContent: 'flex-end' },
between: { justifyContent: 'space-between' },
around: { justifyContent: 'space-around' },
evenly: { justifyContent: 'space-evenly' },
},
v: {
start: { alignItems: 'flex-start' },
center: { alignItems: 'center' },
end: { alignItems: 'flex-end' },
stretch: { alignItems: 'stretch' },
baseline: { alignItems: 'baseline' },
},
wrap: {
flexWrap: 'wrap',
},
},
if (direction === "row") { render({ props, parts: { Root } }) {
gridStyles.gridTemplateColumns = gridTemplate const { cols, style, ...rest } = props
} else { const gridStyle = cols
gridStyles.gridTemplateRows = gridTemplate ? { display: 'grid', gridTemplateColumns: cols.map((c: number) => `${c}fr`).join(' '), ...style }
} : style
return <Root style={gridStyle} {...rest}>{props.children}</Root>
},
})
const combinedStyles = { type MainAxisOpts = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'
...gridStyles, type CrossAxisOpts = 'start' | 'center' | 'end' | 'stretch' | 'baseline'
...(style as JSX.CSSProperties),
}
return <div class={cn(componentName, className)} style={combinedStyles} id={id} ref={ref} {...rest}>{children}</div>
}
// Default flexbox behavior
const baseStyles: JSX.CSSProperties = {
display: "flex",
flexDirection: direction === "row" ? "row" : "column",
flexWrap: wrap ? "wrap" : "nowrap",
gap: `${gapPx}px`,
maxWidth: maxWidth,
}
if (mainAxis) {
baseStyles.justifyContent = getJustifyContent(mainAxis)
}
if (crossAxis) {
baseStyles.alignItems = getAlignItems(crossAxis)
}
const combinedStyles = {
...baseStyles,
...(style as JSX.CSSProperties),
}
return <div class={cn(componentName, className)} style={combinedStyles} id={id} ref={ref} {...rest}>{children}</div>
}
export const Test = () => { export const Test = () => {
const mainAxisOpts: MainAxisOpts[] = ["start", "center", "end", "between", "around", "evenly"] const mainAxisOpts: MainAxisOpts[] = ['start', 'center', 'end', 'between', 'around', 'evenly']
const crossAxisOpts: CrossAxisOpts[] = ["start", "center", "end", "stretch", "baseline"] const crossAxisOpts: CrossAxisOpts[] = ['start', 'center', 'end', 'stretch', 'baseline']
return ( return (
<Section gap={8} style={{ padding: "16px" }}> <Section gap={8} style={{ padding: '16px' }}>
{/* API Usage Examples */}
<CodeExamples
examples={[
'<VStack>...</VStack>',
'<VStack gap={4} v="center">...</VStack>',
'<HStack>...</HStack>',
'<HStack gap={6} h="between" v="center">...</HStack>',
'<HStack cols={[7, 3]} maxWidth="1200px" gap={4}>...</HStack>',
'<VStack rows={[2, 1]} maxWidth="800px">...</VStack>',
]}
/>
{/* HStack layout matrix */} {/* HStack layout matrix */}
<VStack gap={2}> <VStack gap={2}>
<H2>HStack Layout</H2> <H2>HStack Layout</H2>
<div style={{ overflowX: "auto" }}> <div style={{ overflowX: 'auto' }}>
<Grid cols={7} gap={1} style={{ gridTemplateColumns: "auto repeat(6, 1fr)" }}> <Grid cols={7} gap={1} style={{ gridTemplateColumns: 'auto repeat(6, 1fr)' }}>
{/* Header row: blank + h labels */} {/* Header row: blank + h labels */}
<div></div> <div></div>
{mainAxisOpts.map((h) => ( {mainAxisOpts.map((h) => (
<div key={h} style={{ fontSize: "14px", fontWeight: "500", textAlign: "center" }}> <div key={h} style={{ fontSize: '14px', fontWeight: '500', textAlign: 'center' }}>
h: {h} h: {h}
</div> </div>
))} ))}
{/* Each row: v label + HStack cells */} {/* Each row: v label + HStack cells */}
{crossAxisOpts.map((v) => [ {crossAxisOpts.map((v) => [
<div key={v} style={{ fontSize: "14px", fontWeight: "500" }}> <div key={v} style={{ fontSize: '14px', fontWeight: '500' }}>
v: {v} v: {v}
</div>, </div>,
...mainAxisOpts.map((h) => ( ...mainAxisOpts.map((h) => (
@ -148,7 +125,7 @@ export const Test = () => {
key={`${h}-${v}`} key={`${h}-${v}`}
h={h} h={h}
v={v} v={v}
style={{ backgroundColor: "#f3f4f6", padding: "8px", height: "96px", border: "1px solid #9ca3af" }} style={{ backgroundColor: '#f3f4f6', padding: '8px', height: '96px', border: '1px solid #9ca3af' }}
> >
<RedBox>Aa</RedBox> <RedBox>Aa</RedBox>
<GreenBox>Aa</GreenBox> <GreenBox>Aa</GreenBox>
@ -163,19 +140,19 @@ export const Test = () => {
{/* VStack layout matrix */} {/* VStack layout matrix */}
<VStack gap={2}> <VStack gap={2}>
<H2>VStack Layout</H2> <H2>VStack Layout</H2>
<div style={{ overflowX: "auto" }}> <div style={{ overflowX: 'auto' }}>
<Grid cols={6} gap={1} style={{ gridTemplateColumns: "auto repeat(5, 1fr)" }}> <Grid cols={6} gap={1} style={{ gridTemplateColumns: 'auto repeat(5, 1fr)' }}>
{/* Header row: blank + h labels */} {/* Header row: blank + h labels */}
<div></div> <div></div>
{crossAxisOpts.map((h) => ( {crossAxisOpts.map((h) => (
<div key={h} style={{ fontSize: "14px", fontWeight: "500", textAlign: "center" }}> <div key={h} style={{ fontSize: '14px', fontWeight: '500', textAlign: 'center' }}>
h: {h} h: {h}
</div> </div>
))} ))}
{/* Each row: v label + VStack cells */} {/* Each row: v label + VStack cells */}
{mainAxisOpts.map((v) => [ {mainAxisOpts.map((v) => [
<div key={v} style={{ fontSize: "14px", fontWeight: "500" }}> <div key={v} style={{ fontSize: '14px', fontWeight: '500' }}>
v: {v} v: {v}
</div>, </div>,
...crossAxisOpts.map((h) => ( ...crossAxisOpts.map((h) => (
@ -183,7 +160,7 @@ export const Test = () => {
key={`${h}-${v}`} key={`${h}-${v}`}
v={v} v={v}
h={h} h={h}
style={{ backgroundColor: "#f3f4f6", padding: "8px", height: "168px", border: "1px solid #9ca3af" }} style={{ backgroundColor: '#f3f4f6', padding: '8px', height: '168px', border: '1px solid #9ca3af' }}
> >
<RedBox>Aa</RedBox> <RedBox>Aa</RedBox>
<GreenBox>Aa</GreenBox> <GreenBox>Aa</GreenBox>
@ -200,59 +177,59 @@ export const Test = () => {
<H2>HStack with Custom Column Sizing</H2> <H2>HStack with Custom Column Sizing</H2>
<VStack gap={4}> <VStack gap={4}>
<div> <div>
<div style={{ fontSize: "14px", fontWeight: "500", marginBottom: "8px" }}> <div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
cols=[7, 3] (70%/30% split) cols=[7, 3] (70%/30% split)
</div> </div>
<HStack cols={[7, 3]} gap={4} style={{ backgroundColor: "#f3f4f6", padding: "16px" }}> <HStack gap={4} cols={[7, 3]} style={{ backgroundColor: '#f3f4f6', padding: '16px' }}>
<div style={{ backgroundColor: "#bfdbfe", padding: "16px", textAlign: "center" }}> <div style={{ backgroundColor: '#bfdbfe', padding: '16px', textAlign: 'center' }}>
70% width 70% width
</div> </div>
<div style={{ backgroundColor: "#fecaca", padding: "16px", textAlign: "center" }}> <div style={{ backgroundColor: '#fecaca', padding: '16px', textAlign: 'center' }}>
30% width 30% width
</div> </div>
</HStack> </HStack>
</div> </div>
<div> <div>
<div style={{ fontSize: "14px", fontWeight: "500", marginBottom: "8px" }}> <div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
cols=[2, 1] (66%/33% split) cols=[2, 1] (66%/33% split)
</div> </div>
<HStack cols={[2, 1]} gap={4} style={{ backgroundColor: "#f3f4f6", padding: "16px" }}> <HStack gap={4} cols={[2, 1]} style={{ backgroundColor: '#f3f4f6', padding: '16px' }}>
<div style={{ backgroundColor: "#bbf7d0", padding: "16px", textAlign: "center" }}> <div style={{ backgroundColor: '#bbf7d0', padding: '16px', textAlign: 'center' }}>
2/3 width 2/3 width
</div> </div>
<div style={{ backgroundColor: "#fef08a", padding: "16px", textAlign: "center" }}> <div style={{ backgroundColor: '#fef08a', padding: '16px', textAlign: 'center' }}>
1/3 width 1/3 width
</div> </div>
</HStack> </HStack>
</div> </div>
<div> <div>
<div style={{ fontSize: "14px", fontWeight: "500", marginBottom: "8px" }}> <div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
cols=[1, 2, 1] (25%/50%/25% split) cols=[1, 2, 1] (25%/50%/25% split)
</div> </div>
<HStack cols={[1, 2, 1]} gap={4} style={{ backgroundColor: "#f3f4f6", padding: "16px" }}> <HStack gap={4} cols={[1, 2, 1]} style={{ backgroundColor: '#f3f4f6', padding: '16px' }}>
<div style={{ backgroundColor: "#e9d5ff", padding: "16px", textAlign: "center" }}> <div style={{ backgroundColor: '#e9d5ff', padding: '16px', textAlign: 'center' }}>
25% 25%
</div> </div>
<div style={{ backgroundColor: "#bfdbfe", padding: "16px", textAlign: "center" }}> <div style={{ backgroundColor: '#bfdbfe', padding: '16px', textAlign: 'center' }}>
50% 50%
</div> </div>
<div style={{ backgroundColor: "#fbcfe8", padding: "16px", textAlign: "center" }}> <div style={{ backgroundColor: '#fbcfe8', padding: '16px', textAlign: 'center' }}>
25% 25%
</div> </div>
</HStack> </HStack>
</div> </div>
<div> <div>
<div style={{ fontSize: "14px", fontWeight: "500", marginBottom: "8px" }}> <div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
With maxWidth="600px" cols=[7, 3] with maxWidth: 600px
</div> </div>
<HStack cols={[7, 3]} maxWidth="600px" gap={4} style={{ backgroundColor: "#f3f4f6", padding: "16px" }}> <HStack gap={4} cols={[7, 3]} style={{ maxWidth: '600px', backgroundColor: '#f3f4f6', padding: '16px' }}>
<div style={{ backgroundColor: "#bfdbfe", padding: "16px", textAlign: "center" }}> <div style={{ backgroundColor: '#bfdbfe', padding: '16px', textAlign: 'center' }}>
70% of max 600px 70% of max 600px
</div> </div>
<div style={{ backgroundColor: "#fecaca", padding: "16px", textAlign: "center" }}> <div style={{ backgroundColor: '#fecaca', padding: '16px', textAlign: 'center' }}>
30% of max 600px 30% of max 600px
</div> </div>
</HStack> </HStack>
@ -265,18 +242,18 @@ export const Test = () => {
<H2>VStack with Custom Row Sizing</H2> <H2>VStack with Custom Row Sizing</H2>
<VStack gap={4}> <VStack gap={4}>
<div> <div>
<div style={{ fontSize: "14px", fontWeight: "500", marginBottom: "8px" }}> <div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
rows=[2, 1] (2/3 and 1/3 height) rows=[2, 1] (2/3 and 1/3 height)
</div> </div>
<VStack <VStack
rows={[2, 1]}
gap={4} gap={4}
style={{ backgroundColor: "#f3f4f6", padding: "16px", height: "300px" }} rows={[2, 1]}
style={{ backgroundColor: '#f3f4f6', padding: '16px', height: '300px' }}
> >
<div style={{ backgroundColor: "#bfdbfe", padding: "16px", textAlign: "center" }}> <div style={{ backgroundColor: '#bfdbfe', padding: '16px', textAlign: 'center' }}>
2/3 height 2/3 height
</div> </div>
<div style={{ backgroundColor: "#fecaca", padding: "16px", textAlign: "center" }}> <div style={{ backgroundColor: '#fecaca', padding: '16px', textAlign: 'center' }}>
1/3 height 1/3 height
</div> </div>
</VStack> </VStack>
@ -286,60 +263,3 @@ export const Test = () => {
</Section> </Section>
) )
} }
type StackDirection = "row" | "col"
type StackProps = JSX.IntrinsicElements["div"] & {
direction: StackDirection
mainAxis?: string
crossAxis?: string
wrap?: boolean
gap?: TailwindSize
maxWidth?: string
gridSizes?: number[] // cols for row, rows for col
componentName?: string // for data-howl attribute
}
type MainAxisOpts = "start" | "center" | "end" | "between" | "around" | "evenly"
type CrossAxisOpts = "start" | "center" | "end" | "stretch" | "baseline"
type CommonStackProps = JSX.IntrinsicElements["div"] & PropsWithChildren & {
wrap?: boolean
gap?: TailwindSize
maxWidth?: string
}
type VStackProps = CommonStackProps & {
v?: MainAxisOpts // main axis for vertical stack
h?: CrossAxisOpts // cross axis for vertical stack
rows?: number[] // custom row sizing (e.g., [7, 3] for 70%/30%)
}
type HStackProps = CommonStackProps & {
h?: MainAxisOpts // main axis for horizontal stack
v?: CrossAxisOpts // cross axis for horizontal stack
cols?: number[] // custom column sizing (e.g., [7, 3] for 70%/30%)
}
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"
}

View File

@ -1,57 +1,57 @@
import "hono/jsx" import { define } from 'forge'
import type { FC, PropsWithChildren, JSX } from "hono/jsx" import { theme } from './theme'
import { CodeExamples } from "./code"
import { cn } from "./cn"
export const H1: FC<JSX.IntrinsicElements["h1"]> = (props) => { export const H1 = define('H1', {
const { children, class: className, style, id, ref, ...rest } = props base: 'h1',
return <h1 class={cn("H1", className)} style={{ fontSize: "24px", fontWeight: "bold", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</h1> fontSize: theme('fontSize-2xl'),
} fontWeight: 700,
color: theme('colors-fg'),
})
export const H2: FC<JSX.IntrinsicElements["h2"]> = (props) => { export const H2 = define('H2', {
const { children, class: className, style, id, ref, ...rest } = props base: 'h2',
return <h2 class={cn("H2", className)} style={{ fontSize: "20px", fontWeight: "bold", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</h2> fontSize: theme('fontSize-xl'),
} fontWeight: 700,
color: theme('colors-fg'),
})
export const H3: FC<JSX.IntrinsicElements["h3"]> = (props) => { export const H3 = define('H3', {
const { children, class: className, style, id, ref, ...rest } = props base: 'h3',
return <h3 class={cn("H3", className)} style={{ fontSize: "18px", fontWeight: "600", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</h3> fontSize: theme('fontSize-lg'),
} fontWeight: 600,
color: theme('colors-fg'),
})
export const H4: FC<JSX.IntrinsicElements["h4"]> = (props) => { export const H4 = define('H4', {
const { children, class: className, style, id, ref, ...rest } = props base: 'h4',
return <h4 class={cn("H4", className)} style={{ fontSize: "16px", fontWeight: "600", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</h4> fontSize: theme('fontSize-base'),
} fontWeight: 600,
color: theme('colors-fg'),
})
export const H5: FC<JSX.IntrinsicElements["h5"]> = (props) => { export const H5 = define('H5', {
const { children, class: className, style, id, ref, ...rest } = props base: 'h5',
return <h5 class={cn("H5", className)} style={{ fontSize: "14px", fontWeight: "500", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</h5> fontSize: theme('fontSize-sm'),
} fontWeight: 500,
color: theme('colors-fg'),
})
export const Text: FC<JSX.IntrinsicElements["p"]> = (props) => { export const Text = define('Text', {
const { children, class: className, style, id, ref, ...rest } = props base: 'p',
return <p class={cn("Text", className)} style={{ fontSize: "14px", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</p> fontSize: theme('fontSize-sm'),
} color: theme('colors-fg'),
})
export const SmallText: FC<JSX.IntrinsicElements["p"]> = (props) => { export const SmallText = define('SmallText', {
const { children, class: className, style, id, ref, ...rest } = props base: 'p',
return <p class={cn("SmallText", className)} style={{ fontSize: "12px", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</p> fontSize: theme('fontSize-xs'),
} color: theme('colors-fgMuted'),
})
export const Test = () => { export const Test = () => {
return ( return (
<div style={{ padding: "24px", display: "flex", flexDirection: "column", gap: "32px" }}> <div style={{ padding: '24px', display: 'flex', flexDirection: 'column', gap: '32px' }}>
{/* API Usage Examples */} <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<CodeExamples
examples={[
'<H1>Heading 1</H1>',
'<H2>Heading 2</H2>',
'<Text>Regular text</Text>',
'<SmallText>Small text</SmallText>',
]}
/>
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
<H1>Heading 1 (24px, bold)</H1> <H1>Heading 1 (24px, bold)</H1>
<H2>Heading 2 (20px, bold)</H2> <H2>Heading 2 (20px, bold)</H2>
<H3>Heading 3 (18px, semibold)</H3> <H3>Heading 3 (18px, semibold)</H3>
@ -61,16 +61,16 @@ export const Test = () => {
<SmallText>Small text (12px)</SmallText> <SmallText>Small text (12px)</SmallText>
</div> </div>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<H2>Custom Styling</H2> <H2>Custom Styling</H2>
<Text style={{ color: "#3b82f6" }}>Blue text with custom color</Text> <Text style={{ color: '#3b82f6' }}>Blue text with custom color</Text>
<Text style={{ fontWeight: "bold", fontStyle: "italic" }}>Bold italic text</Text> <Text style={{ fontWeight: 'bold', fontStyle: 'italic' }}>Bold italic text</Text>
<SmallText style={{ color: "#ef4444", textTransform: "uppercase" }}> <SmallText style={{ color: '#ef4444', textTransform: 'uppercase' }}>
Red uppercase small text Red uppercase small text
</SmallText> </SmallText>
</div> </div>
<div style={{ display: "flex", flexDirection: "column", gap: "8px", maxWidth: "600px" }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', maxWidth: '600px' }}>
<H2>Typography Example</H2> <H2>Typography Example</H2>
<H3>Article Title</H3> <H3>Article Title</H3>
<Text> <Text>
@ -81,7 +81,7 @@ export const Test = () => {
Multiple paragraphs can be stacked together to create readable content with consistent styling Multiple paragraphs can be stacked together to create readable content with consistent styling
throughout your application. throughout your application.
</Text> </Text>
<SmallText style={{ color: "#64748b" }}>Last updated: Today</SmallText> <SmallText style={{ color: '#64748b' }}>Last updated: Today</SmallText>
</div> </div>
</div> </div>
) )

98
src/theme.ts Normal file
View File

@ -0,0 +1,98 @@
import { createThemes } from 'forge'
const lightTheme = {
// Colors
'colors-bg': '#ffffff',
'colors-bgElevated': '#f9fafb',
'colors-bgMuted': '#f3f4f6',
'colors-fg': '#111827',
'colors-fgMuted': '#6b7280',
'colors-fgDim': '#9ca3af',
'colors-border': '#d1d5db',
'colors-borderActive': '#3b82f6',
// Button colors
'colors-primary': '#3b82f6',
'colors-primaryHover': '#2563eb',
'colors-secondary': '#64748b',
'colors-secondaryHover': '#475569',
'colors-destructive': '#ef4444',
'colors-destructiveHover': '#dc2626',
// Accent colors
'colors-success': '#22c55e',
'colors-error': '#ef4444',
'colors-info': '#3b82f6',
// Box colors
'colors-red': '#ef4444',
'colors-green': '#22c55e',
'colors-blue': '#3b82f6',
'colors-gray': '#6b7280',
// Syntax highlighting colors
'syntax-tag': '#0ea5e9',
'syntax-attr': '#8b5cf6',
'syntax-string': '#10b981',
'syntax-number': '#f59e0b',
'syntax-brace': '#ef4444',
'syntax-text': '#374151',
// Spacing (TailwindSize * 4)
'spacing-0': '0px',
'spacing-1': '4px',
'spacing-2': '8px',
'spacing-3': '12px',
'spacing-4': '16px',
'spacing-5': '20px',
'spacing-6': '24px',
'spacing-8': '32px',
'spacing-10': '40px',
'spacing-12': '48px',
'spacing-16': '64px',
// Typography
'fontSize-xs': '12px',
'fontSize-sm': '14px',
'fontSize-base': '16px',
'fontSize-lg': '18px',
'fontSize-xl': '20px',
'fontSize-2xl': '24px',
// Font weights
'fontWeight-normal': '400',
'fontWeight-medium': '500',
'fontWeight-semibold': '600',
'fontWeight-bold': '700',
// Radii
'radius-sm': '4px',
'radius-md': '6px',
'radius-lg': '8px',
'radius-full': '9999px',
} as const
const darkTheme = {
...lightTheme,
'colors-bg': '#0a0a0a',
'colors-bgElevated': '#111111',
'colors-bgMuted': '#1a1a1a',
'colors-fg': '#ffffff',
'colors-fgMuted': '#a1a1a1',
'colors-fgDim': '#6b7280',
'colors-border': '#333333',
'colors-borderActive': '#60a5fa',
// Dark mode syntax highlighting
'syntax-tag': '#bfdbfe',
'syntax-attr': '#e9d5ff',
'syntax-string': '#a7f3d0',
'syntax-number': '#fde68a',
'syntax-brace': '#fecaca',
'syntax-text': '#f9fafb',
} as const
export const theme = createThemes({
light: lightTheme,
dark: darkTheme,
})

View File

@ -1,6 +1,3 @@
import "hono/jsx"
import type { JSX } from "hono/jsx"
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 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
/** /**
@ -9,6 +6,6 @@ export type TailwindSize = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7
export type CommonHTMLProps = { export type CommonHTMLProps = {
class?: string class?: string
id?: string id?: string
style?: JSX.CSSProperties style?: Record<string, unknown>
ref?: any ref?: any
} }

View File

@ -1,5 +1,15 @@
import "hono/jsx" import "hono/jsx"
import { raw } from "hono/html" import { raw } from "hono/html"
import { Styles } from "forge"
import {
DarkModeToggle,
PageTitle,
HomeLink,
NavLink,
NavList,
NavItem,
Body,
} from "../src/layout"
type LayoutProps = { type LayoutProps = {
title: string title: string
@ -14,6 +24,7 @@ export const Layout = ({ title, children, showHomeLink = true }: LayoutProps) =>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title} - Howl UI</title> <title>{title} - Howl UI</title>
<Styles />
<style> <style>
{raw(` {raw(`
* { * {
@ -22,119 +33,7 @@ export const Layout = ({ title, children, showHomeLink = true }: LayoutProps) =>
box-sizing: border-box; box-sizing: border-box;
} }
:root { /* Dark mode color box adjustments */
--bg: #ffffff;
--text: #1f2937;
--border: #e5e7eb;
--code-bg: #f9fafb;
--code-border: #e5e7eb;
--toggle-bg: #e5e7eb;
--toggle-active: #3b82f6;
}
[data-theme="dark"] {
--bg: #111827;
--text: #f9fafb;
--border: #374151;
--code-bg: #1f2937;
--code-border: #374151;
--toggle-bg: #374151;
--toggle-active: #60a5fa;
}
body {
background-color: var(--bg);
color: var(--text);
transition: background-color 0.3s, color 0.3s;
font-family: system-ui, -apple-system, sans-serif;
}
.dark-mode-toggle {
position: fixed;
top: 16px;
right: 16px;
background: var(--toggle-bg);
border: 1px solid var(--border);
border-radius: 9999px;
padding: 8px 16px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
z-index: 1000;
}
.dark-mode-toggle:hover {
background: var(--toggle-active);
color: white;
border-color: var(--toggle-active);
}
.page-title {
position: relative;
font-size: 32px;
font-weight: bold;
padding: 24px;
border-bottom: 1px solid var(--border);
background: var(--bg);
}
.page-title:has(.home-link) {
padding-left: 80px;
}
.home-link {
position: absolute;
left: 24px;
top: 50%;
transform: translateY(-50%);
background: var(--toggle-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 12px;
text-decoration: none;
color: var(--text);
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
}
.home-link:hover {
background: var(--toggle-active);
color: white;
border-color: var(--toggle-active);
}
/* Update code example styles for dark mode */
[data-theme="dark"] .code-examples-container {
background-color: var(--code-bg) !important;
border-color: var(--code-border) !important;
}
/* Syntax highlighting colors for dark mode - even lighter for better readability */
[data-theme="dark"] .code-examples-container span[style*="color: #8b5cf6"] {
color: #e9d5ff !important; /* much lighter violet */
}
[data-theme="dark"] .code-examples-container span[style*="color: #0ea5e9"] {
color: #bfdbfe !important; /* much lighter cyan */
}
[data-theme="dark"] .code-examples-container span[style*="color: #10b981"] {
color: #a7f3d0 !important; /* much lighter emerald */
}
[data-theme="dark"] .code-examples-container span[style*="color: #f59e0b"] {
color: #fde68a !important; /* much lighter amber */
}
[data-theme="dark"] .code-examples-container span[style*="color: #ef4444"] {
color: #fecaca !important; /* much lighter red */
}
/* Dark mode for specific demo colors */
[data-theme="dark"] div[style*="#fecaca"] { background-color: #7f1d1d !important; } [data-theme="dark"] div[style*="#fecaca"] { background-color: #7f1d1d !important; }
[data-theme="dark"] div[style*="#bbf7d0"] { background-color: #14532d !important; } [data-theme="dark"] div[style*="#bbf7d0"] { background-color: #14532d !important; }
[data-theme="dark"] div[style*="#bfdbfe"] { background-color: #1e3a8a !important; } [data-theme="dark"] div[style*="#bfdbfe"] { background-color: #1e3a8a !important; }
@ -161,92 +60,37 @@ export const Layout = ({ title, children, showHomeLink = true }: LayoutProps) =>
color: #93c5fd !important; color: #93c5fd !important;
} }
/* Dark mode for button colors */
[data-theme="dark"] button[style*="#3b82f6"] { background-color: #1e40af !important; }
[data-theme="dark"] button[style*="#64748b"] { background-color: #334155 !important; }
[data-theme="dark"] button[style*="#ef4444"] { background-color: #991b1b !important; }
/* Dark mode for buttons - removed filter to maintain colors */
/* Dark mode for borders */
[data-theme="dark"] *[style*="border"] {
border-color: var(--border) !important;
}
/* Dark mode for images */ /* Dark mode for images */
[data-theme="dark"] img { [data-theme="dark"] img {
opacity: 0.9; opacity: 0.9;
} }
/* Dark mode for cards and containers */ /* Fix white backgrounds in dark mode */
[data-theme="dark"] div[style*="border: 1px solid #d1d5db"] {
border-color: #374151 !important;
}
/* Improve text visibility - very important! */
[data-theme="dark"] p,
[data-theme="dark"] h1,
[data-theme="dark"] h2,
[data-theme="dark"] h3,
[data-theme="dark"] h4,
[data-theme="dark"] h5,
[data-theme="dark"] span,
[data-theme="dark"] div,
[data-theme="dark"] li {
color: var(--text);
}
/* Ensure button text is visible */
[data-theme="dark"] button {
color: white !important;
}
/* Override black text colors */
[data-theme="dark"] *[style*="color: #000000"],
[data-theme="dark"] *[style*="color:#000000"],
[data-theme="dark"] *[style*="color: black"],
[data-theme="dark"] *[style*="color:black"] {
color: var(--text) !important;
}
/* Special case for buttons with transparent or outline variants */
[data-theme="dark"] button[style*="background: transparent"],
[data-theme="dark"] button[style*="backgroundColor: transparent"] {
color: var(--text) !important;
}
/* Fix divider backgrounds in dark mode */
[data-theme="dark"] span[style*="#ffffff"] {
background-color: var(--bg) !important;
color: #9ca3af !important;
}
/* Fix any #ffffff backgrounds in dark mode */
[data-theme="dark"] *[style*="background-color: rgb(255, 255, 255)"], [data-theme="dark"] *[style*="background-color: rgb(255, 255, 255)"],
[data-theme="dark"] *[style*="#ffffff"] { [data-theme="dark"] *[style*="#ffffff"] {
background-color: var(--bg) !important; background-color: var(--theme-colors-bg) !important;
} }
/* Fix link colors in dark mode */ /* Fix link colors */
[data-theme="dark"] a[style*="#3b82f6"] { [data-theme="dark"] a[style*="#3b82f6"] {
color: #60a5fa !important; color: #60a5fa !important;
} }
`)} `)}
</style> </style>
</head> </head>
<body> <Body>
<button class="dark-mode-toggle" onclick="toggleDarkMode()"> <DarkModeToggle onclick="toggleDarkMode()">
<span class="icon">🌙</span> <span class="icon">🌙</span>
</button> </DarkModeToggle>
<div class="page-title"> <PageTitle hasHomeLink={showHomeLink}>
{showHomeLink && ( {showHomeLink && (
<a href="/" class="home-link"> <HomeLink href="/">
<span>🏠</span> <span>🏠</span>
</a> </HomeLink>
)} )}
{title} {title}
</div> </PageTitle>
{children} {children}
@ -277,9 +121,8 @@ export const Layout = ({ title, children, showHomeLink = true }: LayoutProps) =>
} }
function updateToggleButton(theme) { function updateToggleButton(theme) {
const button = document.querySelector('.dark-mode-toggle') const button = document.querySelector('.DarkModeToggle')
const icon = button.querySelector('.icon') const icon = button.querySelector('.icon')
const label = button.querySelector('.label')
if (theme === 'dark') { if (theme === 'dark') {
icon.textContent = '☀️'; icon.textContent = '☀️';
@ -289,7 +132,10 @@ export const Layout = ({ title, children, showHomeLink = true }: LayoutProps) =>
} }
`)} `)}
</script> </script>
</body> </Body>
</html> </html>
) )
} }
// Export nav components for use in server.tsx
export { NavLink, NavList, NavItem }

View File

@ -2,7 +2,7 @@ import { Hono } from 'hono'
import { readdirSync } from 'fs' import { readdirSync } from 'fs'
import { join } from 'path' import { join } from 'path'
import { capitalize } from './utils' import { capitalize } from './utils'
import { Layout } from './layout' import { Layout, NavLink, NavList, NavItem } from './layout'
const port = process.env.PORT ?? '3100' const port = process.env.PORT ?? '3100'
const app = new Hono() const app = new Hono()
@ -26,17 +26,15 @@ app.get('/:file', async c => {
app.get('/', c => { app.get('/', c => {
return c.html( return c.html(
<Layout title="🐺 howl" showHomeLink={false}> <Layout title="🐺 howl" showHomeLink={false}>
<div style="padding: 24px;"> <NavList>
<ul style="font-size: 18px; line-height: 2;"> {testFiles().map(x => (
{testFiles().map(x => ( <NavItem key={x}>
<li> <NavLink href={`/${x}`}>
<a href={`/${x}`} style="color: #3b82f6; text-decoration: none;"> {x}
{x} </NavLink>
</a> </NavItem>
</li> ))}
))} </NavList>
</ul>
</div>
</Layout> </Layout>
) )
}) })