Compare commits

..

12 Commits
main ... main

Author SHA1 Message Date
4eecc0e778 3 2026-02-18 10:51:53 -08:00
44cc1ac10f 0.0.2 2026-02-18 10:48:08 -08:00
d64b855a90 update deps 2026-02-18 10:48:01 -08:00
14e44d1592 wrong forge 2026-01-29 21:04:10 -08:00
bf2a5b4976 @because/howl 2026-01-29 19:38:16 -08:00
Chris Wanstrath
50af55c1b7 v0.1.1 2026-01-29 18:35:31 -08:00
Chris Wanstrath
9d0391beaa npm 2026-01-29 18:35:22 -08:00
3954663270 make forge a peerDependency
fixes duplicate module instances when bundled

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 20:54:59 -08:00
1b33637d0e Merge pull request 'Port howl to forge' (#1) from forge into main
Reviewed-on: defunkt/howl#1
2026-01-21 04:52:54 +00:00
6f99e4b08e don't depend on hono 2026-01-16 13:40:31 -08:00
bec56d8815 be honest 2026-01-16 08:40:20 -08:00
0cad100197 ________ ______ _______ ______ ________
/        |/      \ /       \  /      \ /        |
$$$$$$$$//$$$$$$  |$$$$$$$  |/$$$$$$  |$$$$$$$$/
$$ |__   $$ |  $$ |$$ |__$$ |$$ | _$$/ $$ |__
$$    |  $$ |  $$ |$$    $$< $$ |/    |$$    |
$$$$$/   $$ |  $$ |$$$$$$$  |$$ |$$$$ |$$$$$/
$$ |     $$ \__$$ |$$ |  $$ |$$ \__$$ |$$ |_____
$$ |     $$    $$/ $$ |  $$ |$$    $$/ $$       |
$$/       $$$$$$/  $$/   $$/  $$$$$$/  $$$$$$$$/
2026-01-16 08:33:38 -08:00
25 changed files with 1874 additions and 1482 deletions

1
.npmrc Normal file
View File

@ -0,0 +1 @@
registry=https://npm.nose.space

View File

@ -1,6 +1,9 @@
# 🐺 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

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,
"configVersion": 1,
"workspaces": {
"": {
"name": "howl",
"dependencies": {
"forge": "git+https://git.nose.space/defunkt/forge",
"hono": "^4.10.7",
"lucide-static": "^0.555.0",
},
@ -16,13 +18,15 @@
},
},
"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=="],

View File

@ -1,19 +1,26 @@
{
"name": "howl",
"module": "index.tsx",
"name": "@because/howl",
"version": "0.0.3",
"module": "src/index.tsx",
"type": "module",
"exports": "./src/index.tsx",
"files": [
"src",
"!src/**/*.test.*",
"README.md"
],
"scripts": {
"dev": "bun run --hot test/server.tsx"
},
"devDependencies": {
"@types/bun": "latest"
"@types/bun": "latest",
"hono": "^4.10.7"
},
"peerDependencies": {
"typescript": "^5"
"typescript": "^5",
"@because/forge": "*"
},
"dependencies": {
"hono": "^4.10.7",
"lucide-static": "^0.555.0"
}
}
}

View File

@ -1,61 +1,63 @@
import { Section } from "./section"
import { H2, Text } from "./text"
import "hono/jsx"
import type { FC, JSX } from "hono/jsx"
import { VStack, HStack } from "./stack"
import { CodeExamples } from "./code"
import { cn } from "./cn"
import { define } from '@because/forge'
import { theme } from './theme'
import { Section } from './section'
import { H2, Text } from './text'
import { VStack, HStack } from './stack'
export type AvatarProps = JSX.IntrinsicElements["img"] & {
size?: number
rounded?: boolean
}
export const Avatar = define('Avatar', {
base: 'img',
objectFit: 'cover',
export const Avatar: FC<AvatarProps> = (props) => {
const { src, size = 32, rounded, class: className, style, id, ref, alt = "", ...rest } = props
variants: {
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 = {
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 type AvatarProps = Parameters<typeof Avatar>[0]
export const Test = () => {
const sampleImages = [
"https://picsum.photos/seed/3/200/200",
"https://picsum.photos/seed/2/200/200",
"https://picsum.photos/seed/8/200/200",
"https://picsum.photos/seed/9/200/200",
'https://picsum.photos/seed/3/200/200',
'https://picsum.photos/seed/2/200/200',
'https://picsum.photos/seed/8/200/200',
'https://picsum.photos/seed/9/200/200',
]
return (
<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 */}
<VStack gap={4}>
<H2>Size Variations</H2>
<HStack gap={4}>
{[24, 32, 48, 64, 96].map((size) => (
<VStack key={size} h="center" gap={2}>
<Avatar src={sampleImages[0]!} size={size} alt="Sample" />
<Text>
{size}x{size}
</Text>
</VStack>
))}
<VStack h="center" gap={2}>
<Avatar src={sampleImages[0]} size={24} alt="Sample" />
<Text>24x24</Text>
</VStack>
<VStack h="center" gap={2}>
<Avatar src={sampleImages[0]} size={32} alt="Sample" />
<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>
</VStack>
@ -64,11 +66,11 @@ export const Test = () => {
<H2>Rounded vs Square</H2>
<HStack gap={6}>
<VStack h="center" gap={2}>
<Avatar src={sampleImages[0]!} size={64} alt="Sample" />
<Avatar src={sampleImages[0]} size={64} alt="Sample" />
<Text>Square</Text>
</VStack>
<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>
</VStack>
</HStack>
@ -93,30 +95,30 @@ export const Test = () => {
<HStack gap={6}>
<VStack h="center" gap={2}>
<Avatar
src={sampleImages[0]!}
src={sampleImages[0]}
size={64}
rounded
style={{ border: "4px solid #3b82f6" }}
style={{ border: '4px solid #3b82f6' }}
alt="With border"
/>
<Text>With Border</Text>
</VStack>
<VStack h="center" gap={2}>
<Avatar
src={sampleImages[1]!}
src={sampleImages[1]}
size={64}
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"
/>
<Text>With Shadow</Text>
</VStack>
<VStack h="center" gap={2}>
<Avatar
src={sampleImages[2]!}
src={sampleImages[2]}
size={64}
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"
/>
<Text>Border + Shadow</Text>

View File

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

View File

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

View File

@ -1,72 +1,87 @@
import "hono/jsx"
import type { FC } from "hono/jsx"
import { VStack } from "./stack"
import { define } from '@because/forge'
import { theme } from './theme'
// 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 = {
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
export const Code: FC<CodeProps> = ({ children }) => {
export const Code = ({ children }: CodeProps) => {
const tokens = tokenizeJSX(children)
return (
<div
style={{
fontFamily: "monospace",
fontSize: "13px",
lineHeight: "1.5",
}}
>
<CodeBlock>
{tokens.map((token, i) => {
if (token.type === "tag") {
return (
<span key={i} style={{ color: colors.tag, fontWeight: "600" }}>
{token.value}
</span>
)
if (token.type === 'tag') {
return <TagToken key={i}>{token.value}</TagToken>
}
if (token.type === "attr") {
return (
<span key={i} style={{ color: colors.attr }}>
{token.value}
</span>
)
if (token.type === 'attr') {
return <AttrToken key={i}>{token.value}</AttrToken>
}
if (token.type === "string") {
return (
<span key={i} style={{ color: colors.string }}>
{token.value}
</span>
)
if (token.type === 'string') {
return <StringToken key={i}>{token.value}</StringToken>
}
if (token.type === "number") {
return (
<span key={i} style={{ color: colors.number }}>
{token.value}
</span>
)
if (token.type === 'number') {
return <NumberToken key={i}>{token.value}</NumberToken>
}
if (token.type === "brace") {
return (
<span key={i} style={{ color: colors.brace, fontWeight: "600" }}>
{token.value}
</span>
)
if (token.type === 'brace') {
return <BraceToken key={i}>{token.value}</BraceToken>
}
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
export const CodeExamples: FC<CodeExamplesProps> = ({ examples }) => {
export const CodeExamples = ({ examples }: CodeExamplesProps) => {
return (
<div class="code-examples-container">
<VStack
gap={2}
style={{
backgroundColor: "#f9fafb",
padding: "16px",
borderRadius: "8px",
border: "1px solid #e5e7eb",
}}
>
{examples.map((example, i) => (
<Code key={i}>{example}</Code>
))}
</VStack>
</div>
<CodeExamplesBox class="code-examples-container">
{examples.map((example, i) => (
<Code key={i}>{example}</Code>
))}
</CodeExamplesBox>
)
}
type Token = {
type: "tag" | "attr" | "string" | "number" | "brace" | "text"
type: 'tag' | 'attr' | 'string' | 'number' | 'brace' | 'text'
value: string
}
@ -106,56 +111,56 @@ function tokenizeJSX(code: string): Token[] {
while (i < code.length) {
// Match opening/closing tags: < or </
if (code[i] === "<") {
if (code[i] === '<') {
i++
// Check for closing tag
if (code[i] === "/") {
tokens.push({ type: "tag", value: "</" })
if (code[i] === '/') {
tokens.push({ type: 'tag', value: '</' })
i++
} else {
tokens.push({ type: "tag", value: "<" })
tokens.push({ type: 'tag', value: '<' })
}
// Get tag name
let tagName = ""
let tagName = ''
while (i < code.length && /[A-Za-z0-9.]/.test(code[i]!)) {
tagName += code[i]
i++
}
if (tagName) {
tokens.push({ type: "tag", value: tagName })
tokens.push({ type: 'tag', value: tagName })
}
// Parse attributes inside the tag
while (i < code.length && code[i] !== ">") {
while (i < code.length && code[i] !== '>') {
// Skip whitespace
if (/\s/.test(code[i]!)) {
tokens.push({ type: "text", value: code[i]! })
tokens.push({ type: 'text', value: code[i]! })
i++
continue
}
// Check for self-closing /
if (code[i] === "/" && code[i + 1] === ">") {
tokens.push({ type: "tag", value: " />" })
if (code[i] === '/' && code[i + 1] === '>') {
tokens.push({ type: 'tag', value: ' />' })
i += 2
break
}
// Parse attribute name
let attrName = ""
let attrName = ''
while (i < code.length && /[a-zA-Z0-9-]/.test(code[i]!)) {
attrName += code[i]
i++
}
if (attrName) {
tokens.push({ type: "attr", value: attrName })
tokens.push({ type: 'attr', value: attrName })
}
// Check for =
if (code[i] === "=") {
tokens.push({ type: "text", value: "=" })
if (code[i] === '=') {
tokens.push({ type: 'text', value: '=' })
i++
// Parse attribute value
@ -171,18 +176,18 @@ function tokenizeJSX(code: string): Token[] {
str += '"'
i++
}
tokens.push({ type: "string", value: str })
} else if (code[i] === "{") {
tokens.push({ type: 'string', value: str })
} else if (code[i] === '{') {
// Brace value
tokens.push({ type: "brace", value: "{" })
tokens.push({ type: 'brace', value: '{' })
i++
// Get content inside braces
let content = ""
let content = ''
let depth = 1
while (i < code.length && depth > 0) {
if (code[i] === "{") depth++
if (code[i] === "}") {
if (code[i] === '{') depth++
if (code[i] === '}') {
depth--
if (depth === 0) break
}
@ -192,13 +197,13 @@ function tokenizeJSX(code: string): Token[] {
// Check if content is a number
if (/^\d+$/.test(content)) {
tokens.push({ type: "number", value: content })
tokens.push({ type: 'number', value: content })
} else {
tokens.push({ type: "text", value: content })
tokens.push({ type: 'text', value: content })
}
if (code[i] === "}") {
tokens.push({ type: "brace", value: "}" })
if (code[i] === '}') {
tokens.push({ type: 'brace', value: '}' })
i++
}
}
@ -206,19 +211,19 @@ function tokenizeJSX(code: string): Token[] {
}
// Closing >
if (code[i] === ">") {
tokens.push({ type: "tag", value: ">" })
if (code[i] === '>') {
tokens.push({ type: 'tag', value: '>' })
i++
}
} else {
// Regular text
let text = ""
while (i < code.length && code[i] !== "<") {
let text = ''
while (i < code.length && code[i] !== '<') {
text += code[i]
i++
}
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 { H2 } from "./text"
import "hono/jsx"
import type { FC, PropsWithChildren, JSX } from "hono/jsx"
import { VStack } from "./stack"
import { CodeExamples } from "./code"
import { cn } from "./cn"
import { define } from '@because/forge'
import { theme } from './theme'
import { Section } from './section'
import { H2 } from './text'
import { VStack } from './stack'
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) => {
const { children, class: className, style, id, ref, ...rest } = props
parts: {
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 = {
display: "flex",
alignItems: "center",
margin: "16px 0",
...(style as JSX.CSSProperties),
}
render({ props, parts: { Root, Line, Text } }) {
const { children, ...rest } = props
const lineStyle: JSX.CSSProperties = {
flex: 1,
borderTop: "1px solid #d1d5db",
}
const textStyle: JSX.CSSProperties = {
padding: "0 12px",
fontSize: "14px",
color: "#6b7280",
backgroundColor: "#ffffff",
}
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>
)
}
return (
<Root {...rest}>
<Line />
{children && (
<>
<Text>{children}</Text>
<Line />
</>
)}
</Root>
)
},
})
export const Test = () => {
return (
<Section gap={4} maxWidth="448px" style={{ padding: "16px" }}>
{/* API Usage Examples */}
<CodeExamples
examples={[
'<Divider />',
'<Divider>OR</Divider>',
'<Divider style={{ margin: "24px 0" }} />',
]}
/>
<Section gap={4} style={{ maxWidth: '448px', padding: '16px' }}>
<H2>Divider Examples</H2>
<VStack gap={0}>
@ -65,7 +53,7 @@ export const Test = () => {
{/* Just a line */}
<VStack gap={0}>
<p>Look a line 👇</p>
<p>Look a line</p>
<Divider />
<p>So cool, so straight!</p>
</VStack>

View File

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

View File

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

View File

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

View File

@ -1,36 +1,40 @@
export { cn } from "./cn"
// Re-export Forge utilities
export { Styles, extendThemes } from '@because/forge'
export { theme } from './theme'
export { Button } from "./button"
export type { ButtonProps } from "./button"
export { cn } from './cn'
export { Icon, IconLink } from "./icon"
export type { IconName } from "./icon"
export { Button } from './button'
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 type { AvatarProps } from "./avatar"
export { Divider } from './divider'
export { Image } from "./image"
export type { ImageProps } from "./image"
export { Avatar } from './avatar'
export type { AvatarProps } from './avatar'
export { Input } from "./input"
export type { InputProps } from "./input"
export { Image } from './image'
export type { ImageProps } from './image'
export { Select } from "./select"
export type { SelectProps, SelectOption } from "./select"
export { Input } from './input'
export type { InputProps } from './input'
export { Placeholder } from "./placeholder"
export { default as PlaceholderDefault } from "./placeholder"
export { Select } from './select'
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 { H2, H3, H4, H5, Text, SmallText } from "./text"
import "hono/jsx"
import type { JSX, FC } from "hono/jsx"
import { VStack, HStack } from "./stack"
import { CodeExamples } from "./code"
import { cn } from "./cn"
import { define } from '@because/forge'
import { theme } from './theme'
import { Section } from './section'
import { H2 } from './text'
import { VStack, HStack } from './stack'
export type InputProps = JSX.IntrinsicElements["input"] & {
labelPosition?: "above" | "left" | "right"
children?: any
}
export const Input = define('Input', {
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: '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) => {
const { labelPosition = "above", children, style, class: className, id, ref, ...inputProps } = props
states: {
':focus': {
borderColor: theme('colors-borderActive'),
},
':disabled': {
opacity: 0.5,
cursor: 'not-allowed',
},
},
},
},
const inputStyle: JSX.CSSProperties = {
height: "40px",
padding: "8px 12px",
borderRadius: "6px",
border: "1px solid #d1d5db",
backgroundColor: "white",
fontSize: "14px",
outline: "none",
...(style as JSX.CSSProperties),
}
variants: {
labelPosition: {
above: {
parts: {
Wrapper: {
flexDirection: 'column',
gap: theme('spacing-1'),
},
},
},
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) {
return <input style={inputStyle} {...inputProps} class={cn("Input", className)} id={id} ref={ref} />
}
render({ props, parts: { Root, Wrapper, Label, Field } }) {
const { children, labelPosition = 'above', ...inputProps } = props
const labelStyle: JSX.CSSProperties = {
fontSize: "14px",
fontWeight: "500",
color: "#111827",
}
if (!children) {
return <Field {...inputProps} />
}
const labelElement = (
<label for={id} style={labelStyle}>
{children}
</label>
)
if (labelPosition === "above") {
return (
<div style={{ display: "flex", flexDirection: "column", gap: "4px", flex: 1, minWidth: 0 }}>
{labelElement}
<input style={inputStyle} {...inputProps} class={cn("Input", className)} id={id} ref={ref} />
</div>
<Wrapper labelPosition={labelPosition}>
<Label for={props.id}>{children}</Label>
<Field {...inputProps} />
</Wrapper>
)
}
},
})
if (labelPosition === "left") {
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 type InputProps = Parameters<typeof Input>[0]
export const Test = () => {
return (
<Section maxWidth="448px">
{/* API Usage Examples */}
<CodeExamples
examples={[
'<Input placeholder="Enter name" />',
'<Input type="email" placeholder="Email" />',
'<Input>Label</Input>',
'<Input labelPosition="left">Name</Input>',
]}
/>
<Section style={{ maxWidth: '448px' }}>
{/* Basic inputs */}
<VStack gap={4}>
<H2>Basic Inputs</H2>
@ -98,9 +108,9 @@ export const Test = () => {
<VStack gap={4}>
<H2>Custom Styling</H2>
<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 style={{ height: "48px", fontSize: "16px" }} placeholder="Large input" />
<Input style={{ height: '48px', fontSize: '16px' }} placeholder="Large input" />
</VStack>
</VStack>
@ -117,8 +127,8 @@ export const Test = () => {
<VStack gap={4}>
<H2>Disabled State</H2>
<VStack gap={4}>
<Input disabled placeholder="Disabled input" style={{ opacity: 0.5, cursor: "not-allowed" }} />
<Input disabled value="Disabled with value" style={{ opacity: 0.5, cursor: "not-allowed" }} />
<Input disabled placeholder="Disabled input" />
<Input disabled value="Disabled with value" />
</VStack>
</VStack>
@ -177,19 +187,6 @@ export const Test = () => {
<Input placeholder="Age">Age</Input>
</HStack>
</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>
)
}

118
src/layout.tsx Normal file
View File

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

View File

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

View File

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

View File

@ -1,146 +1,123 @@
import type { TailwindSize } from "./types"
import "hono/jsx"
import type { FC, PropsWithChildren, JSX } from "hono/jsx"
import { Grid } from "./grid"
import { Section } from "./section"
import { H2 } from "./text"
import { RedBox, GreenBox, BlueBox } from "./box"
import { CodeExamples } from "./code"
import { cn } from "./cn"
import { define } from '@because/forge'
import { theme } from './theme'
import { H2 } from './text'
import { RedBox, GreenBox, BlueBox } from './box'
import { Grid } from './grid'
import { Section } from './section'
export const VStack: FC<VStackProps> = (props) => {
const { v, h, wrap, gap, maxWidth, rows, class: className, style, id, ref, children, ...rest } = props
return (
<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 VStack = define('VStack', {
display: 'flex',
flexDirection: 'column',
export const HStack: FC<HStackProps> = (props) => {
const { h, v, wrap, gap, maxWidth, cols, class: className, style, id, ref, children, ...rest } = props
return (
<Stack
direction="row"
mainAxis={h}
crossAxis={v}
wrap={wrap}
gap={gap}
maxWidth={maxWidth}
gridSizes={cols}
componentName="HStack"
class={className}
style={style}
id={id}
ref={ref}
{...rest}
>
{children}
</Stack>
)
}
variants: {
gap: {
0: { gap: 0 },
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') },
},
v: {
start: { justifyContent: 'flex-start' },
center: { justifyContent: 'center' },
end: { justifyContent: 'flex-end' },
between: { justifyContent: 'space-between' },
around: { justifyContent: 'space-around' },
evenly: { justifyContent: 'space-evenly' },
},
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) => {
const { direction, mainAxis, crossAxis, wrap, gap, maxWidth, gridSizes, componentName, class: className, style, id, ref, children, ...rest } = props
const gapPx = gap ? gap * 4 : 0
render({ props, parts: { Root } }) {
const { rows, style, ...rest } = props
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
if (gridSizes) {
const gridTemplate = gridSizes.map(size => `${size}fr`).join(" ")
export const HStack = define('HStack', {
display: 'flex',
flexDirection: 'row',
const gridStyles: JSX.CSSProperties = {
display: "grid",
gap: `${gapPx}px`,
maxWidth: maxWidth,
}
variants: {
gap: {
0: { gap: 0 },
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") {
gridStyles.gridTemplateColumns = gridTemplate
} else {
gridStyles.gridTemplateRows = gridTemplate
}
render({ props, parts: { Root } }) {
const { cols, style, ...rest } = props
const gridStyle = cols
? { display: 'grid', gridTemplateColumns: cols.map((c: number) => `${c}fr`).join(' '), ...style }
: style
return <Root style={gridStyle} {...rest}>{props.children}</Root>
},
})
const combinedStyles = {
...gridStyles,
...(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>
}
type MainAxisOpts = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'
type CrossAxisOpts = 'start' | 'center' | 'end' | 'stretch' | 'baseline'
export const Test = () => {
const mainAxisOpts: MainAxisOpts[] = ["start", "center", "end", "between", "around", "evenly"]
const crossAxisOpts: CrossAxisOpts[] = ["start", "center", "end", "stretch", "baseline"]
const mainAxisOpts: MainAxisOpts[] = ['start', 'center', 'end', 'between', 'around', 'evenly']
const crossAxisOpts: CrossAxisOpts[] = ['start', 'center', 'end', 'stretch', 'baseline']
return (
<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>',
]}
/>
<Section gap={8} style={{ padding: '16px' }}>
{/* HStack layout matrix */}
<VStack gap={2}>
<H2>HStack Layout</H2>
<div style={{ overflowX: "auto" }}>
<Grid cols={7} gap={1} style={{ gridTemplateColumns: "auto repeat(6, 1fr)" }}>
<div style={{ overflowX: 'auto' }}>
<Grid cols={7} gap={1} style={{ gridTemplateColumns: 'auto repeat(6, 1fr)' }}>
{/* Header row: blank + h labels */}
<div></div>
{mainAxisOpts.map((h) => (
<div key={h} style={{ fontSize: "14px", fontWeight: "500", textAlign: "center" }}>
<div key={h} style={{ fontSize: '14px', fontWeight: '500', textAlign: 'center' }}>
h: {h}
</div>
))}
{/* Each row: v label + HStack cells */}
{crossAxisOpts.map((v) => [
<div key={v} style={{ fontSize: "14px", fontWeight: "500" }}>
<div key={v} style={{ fontSize: '14px', fontWeight: '500' }}>
v: {v}
</div>,
...mainAxisOpts.map((h) => (
@ -148,7 +125,7 @@ export const Test = () => {
key={`${h}-${v}`}
h={h}
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>
<GreenBox>Aa</GreenBox>
@ -163,19 +140,19 @@ export const Test = () => {
{/* VStack layout matrix */}
<VStack gap={2}>
<H2>VStack Layout</H2>
<div style={{ overflowX: "auto" }}>
<Grid cols={6} gap={1} style={{ gridTemplateColumns: "auto repeat(5, 1fr)" }}>
<div style={{ overflowX: 'auto' }}>
<Grid cols={6} gap={1} style={{ gridTemplateColumns: 'auto repeat(5, 1fr)' }}>
{/* Header row: blank + h labels */}
<div></div>
{crossAxisOpts.map((h) => (
<div key={h} style={{ fontSize: "14px", fontWeight: "500", textAlign: "center" }}>
<div key={h} style={{ fontSize: '14px', fontWeight: '500', textAlign: 'center' }}>
h: {h}
</div>
))}
{/* Each row: v label + VStack cells */}
{mainAxisOpts.map((v) => [
<div key={v} style={{ fontSize: "14px", fontWeight: "500" }}>
<div key={v} style={{ fontSize: '14px', fontWeight: '500' }}>
v: {v}
</div>,
...crossAxisOpts.map((h) => (
@ -183,7 +160,7 @@ export const Test = () => {
key={`${h}-${v}`}
v={v}
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>
<GreenBox>Aa</GreenBox>
@ -200,59 +177,59 @@ export const Test = () => {
<H2>HStack with Custom Column Sizing</H2>
<VStack gap={4}>
<div>
<div style={{ fontSize: "14px", fontWeight: "500", marginBottom: "8px" }}>
<div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
cols=[7, 3] (70%/30% split)
</div>
<HStack cols={[7, 3]} gap={4} style={{ backgroundColor: "#f3f4f6", padding: "16px" }}>
<div style={{ backgroundColor: "#bfdbfe", padding: "16px", textAlign: "center" }}>
<HStack gap={4} cols={[7, 3]} style={{ backgroundColor: '#f3f4f6', padding: '16px' }}>
<div style={{ backgroundColor: '#bfdbfe', padding: '16px', textAlign: 'center' }}>
70% width
</div>
<div style={{ backgroundColor: "#fecaca", padding: "16px", textAlign: "center" }}>
<div style={{ backgroundColor: '#fecaca', padding: '16px', textAlign: 'center' }}>
30% width
</div>
</HStack>
</div>
<div>
<div style={{ fontSize: "14px", fontWeight: "500", marginBottom: "8px" }}>
<div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
cols=[2, 1] (66%/33% split)
</div>
<HStack cols={[2, 1]} gap={4} style={{ backgroundColor: "#f3f4f6", padding: "16px" }}>
<div style={{ backgroundColor: "#bbf7d0", padding: "16px", textAlign: "center" }}>
<HStack gap={4} cols={[2, 1]} style={{ backgroundColor: '#f3f4f6', padding: '16px' }}>
<div style={{ backgroundColor: '#bbf7d0', padding: '16px', textAlign: 'center' }}>
2/3 width
</div>
<div style={{ backgroundColor: "#fef08a", padding: "16px", textAlign: "center" }}>
<div style={{ backgroundColor: '#fef08a', padding: '16px', textAlign: 'center' }}>
1/3 width
</div>
</HStack>
</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)
</div>
<HStack cols={[1, 2, 1]} gap={4} style={{ backgroundColor: "#f3f4f6", padding: "16px" }}>
<div style={{ backgroundColor: "#e9d5ff", padding: "16px", textAlign: "center" }}>
<HStack gap={4} cols={[1, 2, 1]} style={{ backgroundColor: '#f3f4f6', padding: '16px' }}>
<div style={{ backgroundColor: '#e9d5ff', padding: '16px', textAlign: 'center' }}>
25%
</div>
<div style={{ backgroundColor: "#bfdbfe", padding: "16px", textAlign: "center" }}>
<div style={{ backgroundColor: '#bfdbfe', padding: '16px', textAlign: 'center' }}>
50%
</div>
<div style={{ backgroundColor: "#fbcfe8", padding: "16px", textAlign: "center" }}>
<div style={{ backgroundColor: '#fbcfe8', padding: '16px', textAlign: 'center' }}>
25%
</div>
</HStack>
</div>
<div>
<div style={{ fontSize: "14px", fontWeight: "500", marginBottom: "8px" }}>
With maxWidth="600px"
<div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
cols=[7, 3] with maxWidth: 600px
</div>
<HStack cols={[7, 3]} maxWidth="600px" gap={4} style={{ backgroundColor: "#f3f4f6", padding: "16px" }}>
<div style={{ backgroundColor: "#bfdbfe", padding: "16px", textAlign: "center" }}>
<HStack gap={4} cols={[7, 3]} style={{ maxWidth: '600px', backgroundColor: '#f3f4f6', padding: '16px' }}>
<div style={{ backgroundColor: '#bfdbfe', padding: '16px', textAlign: 'center' }}>
70% of max 600px
</div>
<div style={{ backgroundColor: "#fecaca", padding: "16px", textAlign: "center" }}>
<div style={{ backgroundColor: '#fecaca', padding: '16px', textAlign: 'center' }}>
30% of max 600px
</div>
</HStack>
@ -265,18 +242,18 @@ export const Test = () => {
<H2>VStack with Custom Row Sizing</H2>
<VStack gap={4}>
<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)
</div>
<VStack
rows={[2, 1]}
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
</div>
<div style={{ backgroundColor: "#fecaca", padding: "16px", textAlign: "center" }}>
<div style={{ backgroundColor: '#fecaca', padding: '16px', textAlign: 'center' }}>
1/3 height
</div>
</VStack>
@ -286,60 +263,3 @@ export const Test = () => {
</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 type { FC, PropsWithChildren, JSX } from "hono/jsx"
import { CodeExamples } from "./code"
import { cn } from "./cn"
import { define } from '@because/forge'
import { theme } from './theme'
export const H1: FC<JSX.IntrinsicElements["h1"]> = (props) => {
const { children, class: className, style, id, ref, ...rest } = props
return <h1 class={cn("H1", className)} style={{ fontSize: "24px", fontWeight: "bold", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</h1>
}
export const H1 = define('H1', {
base: 'h1',
fontSize: theme('fontSize-2xl'),
fontWeight: 700,
color: theme('colors-fg'),
})
export const H2: FC<JSX.IntrinsicElements["h2"]> = (props) => {
const { children, class: className, style, id, ref, ...rest } = props
return <h2 class={cn("H2", className)} style={{ fontSize: "20px", fontWeight: "bold", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</h2>
}
export const H2 = define('H2', {
base: 'h2',
fontSize: theme('fontSize-xl'),
fontWeight: 700,
color: theme('colors-fg'),
})
export const H3: FC<JSX.IntrinsicElements["h3"]> = (props) => {
const { children, class: className, style, id, ref, ...rest } = props
return <h3 class={cn("H3", className)} style={{ fontSize: "18px", fontWeight: "600", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</h3>
}
export const H3 = define('H3', {
base: 'h3',
fontSize: theme('fontSize-lg'),
fontWeight: 600,
color: theme('colors-fg'),
})
export const H4: FC<JSX.IntrinsicElements["h4"]> = (props) => {
const { children, class: className, style, id, ref, ...rest } = props
return <h4 class={cn("H4", className)} style={{ fontSize: "16px", fontWeight: "600", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</h4>
}
export const H4 = define('H4', {
base: 'h4',
fontSize: theme('fontSize-base'),
fontWeight: 600,
color: theme('colors-fg'),
})
export const H5: FC<JSX.IntrinsicElements["h5"]> = (props) => {
const { children, class: className, style, id, ref, ...rest } = props
return <h5 class={cn("H5", className)} style={{ fontSize: "14px", fontWeight: "500", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</h5>
}
export const H5 = define('H5', {
base: 'h5',
fontSize: theme('fontSize-sm'),
fontWeight: 500,
color: theme('colors-fg'),
})
export const Text: FC<JSX.IntrinsicElements["p"]> = (props) => {
const { children, class: className, style, id, ref, ...rest } = props
return <p class={cn("Text", className)} style={{ fontSize: "14px", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</p>
}
export const Text = define('Text', {
base: 'p',
fontSize: theme('fontSize-sm'),
color: theme('colors-fg'),
})
export const SmallText: FC<JSX.IntrinsicElements["p"]> = (props) => {
const { children, class: className, style, id, ref, ...rest } = props
return <p class={cn("SmallText", className)} style={{ fontSize: "12px", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</p>
}
export const SmallText = define('SmallText', {
base: 'p',
fontSize: theme('fontSize-xs'),
color: theme('colors-fgMuted'),
})
export const Test = () => {
return (
<div style={{ padding: "24px", display: "flex", flexDirection: "column", gap: "32px" }}>
{/* API Usage Examples */}
<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" }}>
<div style={{ padding: '24px', display: 'flex', flexDirection: 'column', gap: '32px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<H1>Heading 1 (24px, bold)</H1>
<H2>Heading 2 (20px, bold)</H2>
<H3>Heading 3 (18px, semibold)</H3>
@ -61,16 +61,16 @@ export const Test = () => {
<SmallText>Small text (12px)</SmallText>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<H2>Custom Styling</H2>
<Text style={{ color: "#3b82f6" }}>Blue text with custom color</Text>
<Text style={{ fontWeight: "bold", fontStyle: "italic" }}>Bold italic text</Text>
<SmallText style={{ color: "#ef4444", textTransform: "uppercase" }}>
<Text style={{ color: '#3b82f6' }}>Blue text with custom color</Text>
<Text style={{ fontWeight: 'bold', fontStyle: 'italic' }}>Bold italic text</Text>
<SmallText style={{ color: '#ef4444', textTransform: 'uppercase' }}>
Red uppercase small text
</SmallText>
</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>
<H3>Article Title</H3>
<Text>
@ -81,7 +81,7 @@ export const Test = () => {
Multiple paragraphs can be stacked together to create readable content with consistent styling
throughout your application.
</Text>
<SmallText style={{ color: "#64748b" }}>Last updated: Today</SmallText>
<SmallText style={{ color: '#64748b' }}>Last updated: Today</SmallText>
</div>
</div>
)

98
src/theme.ts Normal file
View File

@ -0,0 +1,98 @@
import { createThemes } from '@because/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
/**
@ -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 = {
class?: string
id?: string
style?: JSX.CSSProperties
style?: Record<string, unknown>
ref?: any
}

View File

@ -1,5 +1,15 @@
import "hono/jsx"
import { raw } from "hono/html"
import { Styles } from '@because/forge'
import {
DarkModeToggle,
PageTitle,
HomeLink,
NavLink,
NavList,
NavItem,
Body,
} from "../src/layout"
type LayoutProps = {
title: string
@ -14,6 +24,7 @@ export const Layout = ({ title, children, showHomeLink = true }: LayoutProps) =>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title} - Howl UI</title>
<Styles />
<style>
{raw(`
* {
@ -22,119 +33,7 @@ export const Layout = ({ title, children, showHomeLink = true }: LayoutProps) =>
box-sizing: border-box;
}
:root {
--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 */
/* Dark mode color box adjustments */
[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*="#bfdbfe"] { background-color: #1e3a8a !important; }
@ -161,98 +60,43 @@ export const Layout = ({ title, children, showHomeLink = true }: LayoutProps) =>
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 */
[data-theme="dark"] img {
opacity: 0.9;
}
/* Dark mode for cards and containers */
[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 */
/* Fix white backgrounds in dark mode */
[data-theme="dark"] *[style*="background-color: rgb(255, 255, 255)"],
[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"] {
color: #60a5fa !important;
}
`)}
</style>
</head>
<body>
<button class="dark-mode-toggle" onclick="toggleDarkMode()">
<Body>
<DarkModeToggle onclick="toggleDarkMode()">
<span class="icon">🌙</span>
</button>
</DarkModeToggle>
<div class="page-title">
<PageTitle hasHomeLink={showHomeLink}>
{showHomeLink && (
<a href="/" class="home-link">
<HomeLink href="/">
<span>🏠</span>
</a>
</HomeLink>
)}
{title}
</div>
</PageTitle>
{children}
<script>
{raw(`
const getTheme = () =>
const getTheme = () =>
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ?
'dark' :
'light'
@ -277,9 +121,8 @@ export const Layout = ({ title, children, showHomeLink = true }: LayoutProps) =>
}
function updateToggleButton(theme) {
const button = document.querySelector('.dark-mode-toggle')
const button = document.querySelector('.DarkModeToggle')
const icon = button.querySelector('.icon')
const label = button.querySelector('.label')
if (theme === 'dark') {
icon.textContent = '☀️';
@ -289,7 +132,10 @@ export const Layout = ({ title, children, showHomeLink = true }: LayoutProps) =>
}
`)}
</script>
</body>
</Body>
</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 { join } from 'path'
import { capitalize } from './utils'
import { Layout } from './layout'
import { Layout, NavLink, NavList, NavItem } from './layout'
const port = process.env.PORT ?? '3100'
const app = new Hono()
@ -26,17 +26,15 @@ app.get('/:file', async c => {
app.get('/', c => {
return c.html(
<Layout title="🐺 howl" showHomeLink={false}>
<div style="padding: 24px;">
<ul style="font-size: 18px; line-height: 2;">
{testFiles().map(x => (
<li>
<a href={`/${x}`} style="color: #3b82f6; text-decoration: none;">
{x}
</a>
</li>
))}
</ul>
</div>
<NavList>
{testFiles().map(x => (
<NavItem key={x}>
<NavLink href={`/${x}`}>
{x}
</NavLink>
</NavItem>
))}
</NavList>
</Layout>
)
})
@ -51,4 +49,4 @@ function testFiles(): string[] {
export default {
fetch: app.fetch,
port
}
}