Merge pull request 'Port howl to forge' (#1) from forge into main
Reviewed-on: #1
This commit is contained in:
commit
1b33637d0e
|
|
@ -1,6 +1,9 @@
|
||||||
# 🐺 howl
|
# 🐺 howl
|
||||||
|
|
||||||
Howl is a fork of `werewolf-ui`, without any Tailwind. A minimal, zero-dependency React component library.
|
Howl is a fork of `werewolf-ui`, without any Tailwind.
|
||||||
|
|
||||||
|
A minimal, single-dependency component library built on forge
|
||||||
|
for hype/hono.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
|
||||||
556
TAGS.md
Normal file
556
TAGS.md
Normal 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'
|
||||||
|
```
|
||||||
12
bun.lock
12
bun.lock
|
|
@ -1,9 +1,11 @@
|
||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "howl",
|
"name": "howl",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"forge": "git+https://git.nose.space/defunkt/forge",
|
||||||
"hono": "^4.10.7",
|
"hono": "^4.10.7",
|
||||||
"lucide-static": "^0.555.0",
|
"lucide-static": "^0.555.0",
|
||||||
},
|
},
|
||||||
|
|
@ -16,13 +18,15 @@
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
|
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
"@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
|
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
||||||
|
|
||||||
"hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="],
|
"forge": ["forge@git+https://git.nose.space/defunkt/forge#e14821c6995e82088efdfe16e53021afe471ac83", { "dependencies": { "hono": "^4.11.3" }, "peerDependencies": { "typescript": "^5" } }, "e14821c6995e82088efdfe16e53021afe471ac83"],
|
||||||
|
|
||||||
|
"hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
|
||||||
|
|
||||||
"lucide-static": ["lucide-static@0.555.0", "", {}, "sha512-FMMaYYsEYsUA6xlEzIMoKEV3oGnxIIvAN+AtLmYXvlTJptJTveJjVBQwvtA/zZLrD6KLEu89G95dQYlhivw5jQ=="],
|
"lucide-static": ["lucide-static@0.555.0", "", {}, "sha512-FMMaYYsEYsUA6xlEzIMoKEV3oGnxIIvAN+AtLmYXvlTJptJTveJjVBQwvtA/zZLrD6KLEu89G95dQYlhivw5jQ=="],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,14 @@
|
||||||
"dev": "bun run --hot test/server.tsx"
|
"dev": "bun run --hot test/server.tsx"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest"
|
"@types/bun": "latest",
|
||||||
|
"hono": "^4.10.7"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hono": "^4.10.7",
|
"forge": "git+https://git.nose.space/defunkt/forge",
|
||||||
"lucide-static": "^0.555.0"
|
"lucide-static": "^0.555.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
104
src/avatar.tsx
104
src/avatar.tsx
|
|
@ -1,61 +1,63 @@
|
||||||
import { Section } from "./section"
|
import { define } from 'forge'
|
||||||
import { H2, Text } from "./text"
|
import { theme } from './theme'
|
||||||
import "hono/jsx"
|
import { Section } from './section'
|
||||||
import type { FC, JSX } from "hono/jsx"
|
import { H2, Text } from './text'
|
||||||
import { VStack, HStack } from "./stack"
|
import { VStack, HStack } from './stack'
|
||||||
import { CodeExamples } from "./code"
|
|
||||||
import { cn } from "./cn"
|
|
||||||
|
|
||||||
export type AvatarProps = JSX.IntrinsicElements["img"] & {
|
export const Avatar = define('Avatar', {
|
||||||
size?: number
|
base: 'img',
|
||||||
rounded?: boolean
|
objectFit: 'cover',
|
||||||
}
|
|
||||||
|
|
||||||
export const Avatar: FC<AvatarProps> = (props) => {
|
variants: {
|
||||||
const { src, size = 32, rounded, class: className, style, id, ref, alt = "", ...rest } = props
|
size: {
|
||||||
|
24: { width: 24, height: 24 },
|
||||||
|
32: { width: 32, height: 32 },
|
||||||
|
48: { width: 48, height: 48 },
|
||||||
|
64: { width: 64, height: 64 },
|
||||||
|
96: { width: 96, height: 96 },
|
||||||
|
},
|
||||||
|
rounded: {
|
||||||
|
borderRadius: theme('radius-full'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const avatarStyle: JSX.CSSProperties = {
|
export type AvatarProps = Parameters<typeof Avatar>[0]
|
||||||
width: `${size}px`,
|
|
||||||
height: `${size}px`,
|
|
||||||
borderRadius: rounded ? "9999px" : undefined,
|
|
||||||
...(style as JSX.CSSProperties),
|
|
||||||
}
|
|
||||||
|
|
||||||
return <img src={src} alt={alt} class={cn("Avatar", className)} style={avatarStyle} id={id} ref={ref} {...rest} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Test = () => {
|
export const Test = () => {
|
||||||
const sampleImages = [
|
const sampleImages = [
|
||||||
"https://picsum.photos/seed/3/200/200",
|
'https://picsum.photos/seed/3/200/200',
|
||||||
"https://picsum.photos/seed/2/200/200",
|
'https://picsum.photos/seed/2/200/200',
|
||||||
"https://picsum.photos/seed/8/200/200",
|
'https://picsum.photos/seed/8/200/200',
|
||||||
"https://picsum.photos/seed/9/200/200",
|
'https://picsum.photos/seed/9/200/200',
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section>
|
<Section>
|
||||||
{/* API Usage Examples */}
|
|
||||||
<CodeExamples
|
|
||||||
examples={[
|
|
||||||
'<Avatar src="/user.jpg" />',
|
|
||||||
'<Avatar src="/user.jpg" size={64} />',
|
|
||||||
'<Avatar src="/user.jpg" size={48} rounded />',
|
|
||||||
'<Avatar src="/user.jpg" style={{ border: "2px solid blue" }} />',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Size variations */}
|
{/* Size variations */}
|
||||||
<VStack gap={4}>
|
<VStack gap={4}>
|
||||||
<H2>Size Variations</H2>
|
<H2>Size Variations</H2>
|
||||||
<HStack gap={4}>
|
<HStack gap={4}>
|
||||||
{[24, 32, 48, 64, 96].map((size) => (
|
<VStack h="center" gap={2}>
|
||||||
<VStack key={size} h="center" gap={2}>
|
<Avatar src={sampleImages[0]} size={24} alt="Sample" />
|
||||||
<Avatar src={sampleImages[0]!} size={size} alt="Sample" />
|
<Text>24x24</Text>
|
||||||
<Text>
|
</VStack>
|
||||||
{size}x{size}
|
<VStack h="center" gap={2}>
|
||||||
</Text>
|
<Avatar src={sampleImages[0]} size={32} alt="Sample" />
|
||||||
|
<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>
|
</VStack>
|
||||||
))}
|
|
||||||
</HStack>
|
</HStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
|
|
@ -64,11 +66,11 @@ export const Test = () => {
|
||||||
<H2>Rounded vs Square</H2>
|
<H2>Rounded vs Square</H2>
|
||||||
<HStack gap={6}>
|
<HStack gap={6}>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Avatar src={sampleImages[0]!} size={64} alt="Sample" />
|
<Avatar src={sampleImages[0]} size={64} alt="Sample" />
|
||||||
<Text>Square</Text>
|
<Text>Square</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Avatar src={sampleImages[0]!} size={64} rounded alt="Sample" />
|
<Avatar src={sampleImages[0]} size={64} rounded alt="Sample" />
|
||||||
<Text>Rounded</Text>
|
<Text>Rounded</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
@ -93,30 +95,30 @@ export const Test = () => {
|
||||||
<HStack gap={6}>
|
<HStack gap={6}>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={sampleImages[0]!}
|
src={sampleImages[0]}
|
||||||
size={64}
|
size={64}
|
||||||
rounded
|
rounded
|
||||||
style={{ border: "4px solid #3b82f6" }}
|
style={{ border: '4px solid #3b82f6' }}
|
||||||
alt="With border"
|
alt="With border"
|
||||||
/>
|
/>
|
||||||
<Text>With Border</Text>
|
<Text>With Border</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={sampleImages[1]!}
|
src={sampleImages[1]}
|
||||||
size={64}
|
size={64}
|
||||||
rounded
|
rounded
|
||||||
style={{ boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)" }}
|
style={{ boxShadow: '0 10px 15px rgba(0, 0, 0, 0.3)' }}
|
||||||
alt="With shadow"
|
alt="With shadow"
|
||||||
/>
|
/>
|
||||||
<Text>With Shadow</Text>
|
<Text>With Shadow</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={sampleImages[2]!}
|
src={sampleImages[2]}
|
||||||
size={64}
|
size={64}
|
||||||
rounded
|
rounded
|
||||||
style={{ border: "4px solid #22c55e", boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)" }}
|
style={{ border: '4px solid #22c55e', boxShadow: '0 10px 15px rgba(0, 0, 0, 0.3)' }}
|
||||||
alt="Border + shadow"
|
alt="Border + shadow"
|
||||||
/>
|
/>
|
||||||
<Text>Border + Shadow</Text>
|
<Text>Border + Shadow</Text>
|
||||||
|
|
|
||||||
102
src/box.tsx
102
src/box.tsx
|
|
@ -1,80 +1,54 @@
|
||||||
import "hono/jsx"
|
import { define } from 'forge'
|
||||||
import type { FC, PropsWithChildren, JSX } from "hono/jsx"
|
import { theme } from './theme'
|
||||||
import { CodeExamples } from "./code"
|
|
||||||
import { cn } from "./cn"
|
|
||||||
|
|
||||||
type BoxProps = JSX.IntrinsicElements["div"] & PropsWithChildren & {
|
export const Box = define('Box', {
|
||||||
bg?: string
|
// Base box - minimal styling, designed for customization via style prop
|
||||||
color?: string
|
})
|
||||||
p?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Box: FC<BoxProps> = (props) => {
|
export const RedBox = define('RedBox', {
|
||||||
const { children, bg, color, p, class: className, style, id, ref, ...rest } = props
|
background: theme('colors-red'),
|
||||||
|
color: theme('colors-bg'),
|
||||||
|
padding: theme('spacing-1'),
|
||||||
|
textAlign: 'center',
|
||||||
|
})
|
||||||
|
|
||||||
const boxStyle: JSX.CSSProperties = {
|
export const GreenBox = define('GreenBox', {
|
||||||
backgroundColor: bg,
|
background: theme('colors-success'),
|
||||||
color: color,
|
color: theme('colors-bg'),
|
||||||
padding: p ? `${p}px` : undefined,
|
padding: theme('spacing-1'),
|
||||||
...(style as JSX.CSSProperties),
|
textAlign: 'center',
|
||||||
}
|
})
|
||||||
|
|
||||||
return <div class={cn("Box", className)} style={boxStyle} id={id} ref={ref} {...rest}>{children}</div>
|
export const BlueBox = define('BlueBox', {
|
||||||
}
|
background: theme('colors-blue'),
|
||||||
|
color: theme('colors-bg'),
|
||||||
|
padding: theme('spacing-1'),
|
||||||
|
textAlign: 'center',
|
||||||
|
})
|
||||||
|
|
||||||
// Common demo box colors
|
export const GrayBox = define('GrayBox', {
|
||||||
export const RedBox: FC<PropsWithChildren> = ({ children }) => (
|
background: theme('colors-bgMuted'),
|
||||||
<Box bg="#ef4444" p={4} style={{ textAlign: "center" }}>
|
padding: theme('spacing-4'),
|
||||||
{children}
|
})
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const GreenBox: FC<PropsWithChildren> = ({ children }) => (
|
|
||||||
<Box bg="#22c55e" p={4} style={{ textAlign: "center" }}>
|
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const BlueBox: FC<PropsWithChildren> = ({ children }) => (
|
|
||||||
<Box bg="#3b82f6" p={4} style={{ textAlign: "center" }}>
|
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const GrayBox: FC<PropsWithChildren & { style?: JSX.CSSProperties }> = ({ children, style }) => (
|
|
||||||
<Box bg="#f3f4f6" p={16} style={style}>
|
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const Test = () => {
|
export const Test = () => {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "32px", padding: "24px" }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '32px', padding: '24px' }}>
|
||||||
{/* API Usage Examples */}
|
|
||||||
<CodeExamples
|
|
||||||
examples={[
|
|
||||||
'<Box>Content</Box>',
|
|
||||||
'<Box bg="#3b82f6" p={16}>Content</Box>',
|
|
||||||
'<RedBox>Content</RedBox>',
|
|
||||||
'<GrayBox>Content</GrayBox>',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 style={{ fontSize: "20px", fontWeight: "bold", marginBottom: "16px" }}>Box Component</h2>
|
<h2 style={{ fontSize: '20px', fontWeight: 'bold', marginBottom: '16px' }}>Box Component</h2>
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||||
<Box bg="#3b82f6" color="#ffffff" p={16}>
|
<Box style={{ background: '#3b82f6', color: '#ffffff', padding: '16px' }}>
|
||||||
Basic Box with custom background and text color
|
Basic Box with custom background and text color
|
||||||
</Box>
|
</Box>
|
||||||
<Box p={8} style={{ border: "2px solid #d1d5db", borderRadius: "8px" }}>
|
<Box style={{ padding: '8px', border: '2px solid #d1d5db', borderRadius: '8px' }}>
|
||||||
Box with padding and border
|
Box with padding and border
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 style={{ fontSize: "20px", fontWeight: "bold", marginBottom: "16px" }}>Color Variants</h2>
|
<h2 style={{ fontSize: '20px', fontWeight: 'bold', marginBottom: '16px' }}>Color Variants</h2>
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
<RedBox>Red Box</RedBox>
|
<RedBox>Red Box</RedBox>
|
||||||
<GreenBox>Green Box</GreenBox>
|
<GreenBox>Green Box</GreenBox>
|
||||||
<BlueBox>Blue Box</BlueBox>
|
<BlueBox>Blue Box</BlueBox>
|
||||||
|
|
@ -83,10 +57,10 @@ export const Test = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 style={{ fontSize: "20px", fontWeight: "bold", marginBottom: "16px" }}>Nested Boxes</h2>
|
<h2 style={{ fontSize: '20px', fontWeight: 'bold', marginBottom: '16px' }}>Nested Boxes</h2>
|
||||||
<Box bg="#f3f4f6" p={16}>
|
<Box style={{ background: '#f3f4f6', padding: '16px' }}>
|
||||||
<Box bg="#e5e7eb" p={12}>
|
<Box style={{ background: '#e5e7eb', padding: '12px' }}>
|
||||||
<Box bg="#d1d5db" p={8}>
|
<Box style={{ background: '#d1d5db', padding: '8px' }}>
|
||||||
Nested boxes demonstration
|
Nested boxes demonstration
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
166
src/button.tsx
166
src/button.tsx
|
|
@ -1,96 +1,114 @@
|
||||||
import "hono/jsx"
|
import { define } from 'forge'
|
||||||
import type { JSX, FC } from "hono/jsx"
|
import { theme } from './theme'
|
||||||
import { VStack, HStack } from "./stack"
|
import { VStack, HStack } from './stack'
|
||||||
import { Section } from "./section"
|
import { Section } from './section'
|
||||||
import { H2 } from "./text"
|
import { H2 } from './text'
|
||||||
import { CodeExamples } from "./code"
|
|
||||||
import { cn } from "./cn"
|
|
||||||
|
|
||||||
export type ButtonProps = JSX.IntrinsicElements["button"] & {
|
export const Button = define('Button', {
|
||||||
variant?: "primary" | "secondary" | "outline" | "ghost" | "destructive"
|
base: 'button',
|
||||||
size?: "sm" | "md" | "lg"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Button: FC<ButtonProps> = (props) => {
|
display: 'inline-flex',
|
||||||
const { variant = "primary", size = "md", style, class: className, id, ref, ...buttonProps } = props
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontWeight: 500,
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderRadius: theme('radius-sm'),
|
||||||
|
border: '1px solid transparent',
|
||||||
|
outline: 'none',
|
||||||
|
|
||||||
const baseStyles: JSX.CSSProperties = {
|
// Default: primary + md
|
||||||
display: "inline-flex",
|
background: theme('colors-primary'),
|
||||||
alignItems: "center",
|
color: theme('colors-bg'),
|
||||||
justifyContent: "center",
|
height: 40,
|
||||||
fontWeight: "500",
|
padding: `0 ${theme('spacing-4')}`,
|
||||||
transition: "all 0.2s",
|
fontSize: theme('fontSize-sm'),
|
||||||
outline: "none",
|
|
||||||
cursor: "pointer",
|
|
||||||
borderRadius: "4px",
|
|
||||||
border: "1px solid transparent",
|
|
||||||
}
|
|
||||||
|
|
||||||
const variantStyles: Record<string, JSX.CSSProperties> = {
|
states: {
|
||||||
|
':not(:disabled):hover': {
|
||||||
|
background: theme('colors-primaryHover'),
|
||||||
|
},
|
||||||
|
':disabled': {
|
||||||
|
opacity: 0.5,
|
||||||
|
cursor: 'not-allowed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
primary: {
|
primary: {
|
||||||
backgroundColor: "#3b82f6",
|
background: theme('colors-primary'),
|
||||||
color: "#ffffff",
|
color: theme('colors-bg'),
|
||||||
|
states: {
|
||||||
|
':not(:disabled):hover': {
|
||||||
|
background: theme('colors-primaryHover'),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
backgroundColor: "#64748b",
|
background: theme('colors-secondary'),
|
||||||
color: "#ffffff",
|
color: theme('colors-bg'),
|
||||||
|
states: {
|
||||||
|
':not(:disabled):hover': {
|
||||||
|
background: theme('colors-secondaryHover'),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
outline: {
|
outline: {
|
||||||
backgroundColor: "transparent",
|
background: 'transparent',
|
||||||
color: "#000000",
|
color: theme('colors-fg'),
|
||||||
borderColor: "#d1d5db",
|
borderColor: theme('colors-border'),
|
||||||
|
states: {
|
||||||
|
':not(:disabled):hover': {
|
||||||
|
borderColor: theme('colors-borderActive'),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
ghost: {
|
ghost: {
|
||||||
backgroundColor: "transparent",
|
background: 'transparent',
|
||||||
color: "#000000",
|
color: theme('colors-fg'),
|
||||||
|
border: 'none',
|
||||||
|
states: {
|
||||||
|
':not(:disabled):hover': {
|
||||||
|
background: theme('colors-bgMuted'),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
destructive: {
|
destructive: {
|
||||||
backgroundColor: "#ef4444",
|
background: theme('colors-destructive'),
|
||||||
color: "#ffffff",
|
color: theme('colors-bg'),
|
||||||
|
states: {
|
||||||
|
':not(:disabled):hover': {
|
||||||
|
background: theme('colors-destructiveHover'),
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
},
|
||||||
const sizeStyles: Record<string, JSX.CSSProperties> = {
|
},
|
||||||
|
size: {
|
||||||
sm: {
|
sm: {
|
||||||
height: "32px",
|
height: 32,
|
||||||
padding: "0 12px",
|
padding: `0 ${theme('spacing-3')}`,
|
||||||
fontSize: "14px",
|
fontSize: theme('fontSize-sm'),
|
||||||
},
|
},
|
||||||
md: {
|
md: {
|
||||||
height: "40px",
|
height: 40,
|
||||||
padding: "0 16px",
|
padding: `0 ${theme('spacing-4')}`,
|
||||||
fontSize: "14px",
|
fontSize: theme('fontSize-sm'),
|
||||||
},
|
},
|
||||||
lg: {
|
lg: {
|
||||||
height: "48px",
|
height: 48,
|
||||||
padding: "0 24px",
|
padding: `0 ${theme('spacing-6')}`,
|
||||||
fontSize: "16px",
|
fontSize: theme('fontSize-base'),
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const combinedStyles: JSX.CSSProperties = {
|
export type ButtonProps = Parameters<typeof Button>[0]
|
||||||
...baseStyles,
|
|
||||||
...variantStyles[variant],
|
|
||||||
...sizeStyles[size],
|
|
||||||
...(style as JSX.CSSProperties),
|
|
||||||
}
|
|
||||||
|
|
||||||
return <button {...buttonProps} class={cn("Button", className)} style={combinedStyles} id={id} ref={ref} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Test = () => {
|
export const Test = () => {
|
||||||
return (
|
return (
|
||||||
<Section>
|
<Section>
|
||||||
{/* API Usage Examples */}
|
|
||||||
<CodeExamples
|
|
||||||
examples={[
|
|
||||||
'<Button>Click Me</Button>',
|
|
||||||
'<Button variant="secondary">Secondary</Button>',
|
|
||||||
'<Button variant="outline" size="lg">Large</Button>',
|
|
||||||
'<Button onClick={handleClick}>Action</Button>',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Variants */}
|
{/* Variants */}
|
||||||
<VStack gap={4}>
|
<VStack gap={4}>
|
||||||
<H2>Button Variants</H2>
|
<H2>Button Variants</H2>
|
||||||
|
|
@ -119,11 +137,11 @@ export const Test = () => {
|
||||||
<HStack gap={4}>
|
<HStack gap={4}>
|
||||||
<Button variant="primary">
|
<Button variant="primary">
|
||||||
<span>🚀</span>
|
<span>🚀</span>
|
||||||
<span style={{ marginLeft: "8px" }}>Launch</span>
|
<span style={{ marginLeft: '8px' }}>Launch</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" style={{ flexDirection: "column", height: "80px", width: "96px" }}>
|
<Button variant="outline" style={{ flexDirection: 'column', height: '80px', width: '96px' }}>
|
||||||
<span style={{ fontSize: "24px" }}>💳</span>
|
<span style={{ fontSize: '24px' }}>💳</span>
|
||||||
<span style={{ fontSize: "12px", marginTop: "4px" }}>Card</span>
|
<span style={{ fontSize: '12px', marginTop: '4px' }}>Card</span>
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
@ -132,10 +150,10 @@ export const Test = () => {
|
||||||
<VStack gap={4}>
|
<VStack gap={4}>
|
||||||
<H2>Native Attributes</H2>
|
<H2>Native Attributes</H2>
|
||||||
<HStack gap={4}>
|
<HStack gap={4}>
|
||||||
<Button onClick={() => alert("Clicked!")} variant="primary">
|
<Button onClick={() => alert('Clicked!')} variant="primary">
|
||||||
Click Me
|
Click Me
|
||||||
</Button>
|
</Button>
|
||||||
<Button disabled variant="secondary" style={{ opacity: 0.5, pointerEvents: "none" }}>
|
<Button disabled variant="secondary">
|
||||||
Disabled
|
Disabled
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" variant="outline">
|
<Button type="submit" variant="outline">
|
||||||
|
|
|
||||||
197
src/code.tsx
197
src/code.tsx
|
|
@ -1,72 +1,87 @@
|
||||||
import "hono/jsx"
|
import { define } from 'forge'
|
||||||
import type { FC } from "hono/jsx"
|
import { theme } from './theme'
|
||||||
import { VStack } from "./stack"
|
|
||||||
|
// Code block container
|
||||||
|
const CodeBlock = define('CodeBlock', {
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '13px',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
color: theme('syntax-text'),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Syntax token spans
|
||||||
|
const TagToken = define('TagToken', {
|
||||||
|
base: 'span',
|
||||||
|
color: theme('syntax-tag'),
|
||||||
|
fontWeight: 600,
|
||||||
|
})
|
||||||
|
|
||||||
|
const AttrToken = define('AttrToken', {
|
||||||
|
base: 'span',
|
||||||
|
color: theme('syntax-attr'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const StringToken = define('StringToken', {
|
||||||
|
base: 'span',
|
||||||
|
color: theme('syntax-string'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const NumberToken = define('NumberToken', {
|
||||||
|
base: 'span',
|
||||||
|
color: theme('syntax-number'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const BraceToken = define('BraceToken', {
|
||||||
|
base: 'span',
|
||||||
|
color: theme('syntax-brace'),
|
||||||
|
fontWeight: 600,
|
||||||
|
})
|
||||||
|
|
||||||
|
const TextToken = define('TextToken', {
|
||||||
|
base: 'span',
|
||||||
|
color: theme('syntax-text'),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Code examples container
|
||||||
|
const CodeExamplesBox = define('CodeExamplesBox', {
|
||||||
|
background: theme('colors-bgElevated'),
|
||||||
|
padding: theme('spacing-4'),
|
||||||
|
borderRadius: theme('radius-lg'),
|
||||||
|
border: `1px solid ${theme('colors-border')}`,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme('spacing-2'),
|
||||||
|
})
|
||||||
|
|
||||||
type CodeProps = {
|
type CodeProps = {
|
||||||
children: string
|
children: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Color scheme
|
|
||||||
const colors = {
|
|
||||||
tag: "#0ea5e9", // cyan-500
|
|
||||||
attr: "#8b5cf6", // violet-500
|
|
||||||
string: "#10b981", // emerald-500
|
|
||||||
number: "#f59e0b", // amber-500
|
|
||||||
brace: "#ef4444", // red-500
|
|
||||||
text: "#374151", // gray-700
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lightweight JSX syntax highlighter
|
// Lightweight JSX syntax highlighter
|
||||||
export const Code: FC<CodeProps> = ({ children }) => {
|
export const Code = ({ children }: CodeProps) => {
|
||||||
const tokens = tokenizeJSX(children)
|
const tokens = tokenizeJSX(children)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<CodeBlock>
|
||||||
style={{
|
|
||||||
fontFamily: "monospace",
|
|
||||||
fontSize: "13px",
|
|
||||||
lineHeight: "1.5",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tokens.map((token, i) => {
|
{tokens.map((token, i) => {
|
||||||
if (token.type === "tag") {
|
if (token.type === 'tag') {
|
||||||
return (
|
return <TagToken key={i}>{token.value}</TagToken>
|
||||||
<span key={i} style={{ color: colors.tag, fontWeight: "600" }}>
|
|
||||||
{token.value}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (token.type === "attr") {
|
if (token.type === 'attr') {
|
||||||
return (
|
return <AttrToken key={i}>{token.value}</AttrToken>
|
||||||
<span key={i} style={{ color: colors.attr }}>
|
|
||||||
{token.value}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (token.type === "string") {
|
if (token.type === 'string') {
|
||||||
return (
|
return <StringToken key={i}>{token.value}</StringToken>
|
||||||
<span key={i} style={{ color: colors.string }}>
|
|
||||||
{token.value}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (token.type === "number") {
|
if (token.type === 'number') {
|
||||||
return (
|
return <NumberToken key={i}>{token.value}</NumberToken>
|
||||||
<span key={i} style={{ color: colors.number }}>
|
|
||||||
{token.value}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (token.type === "brace") {
|
if (token.type === 'brace') {
|
||||||
return (
|
return <BraceToken key={i}>{token.value}</BraceToken>
|
||||||
<span key={i} style={{ color: colors.brace, fontWeight: "600" }}>
|
|
||||||
{token.value}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return <span key={i}>{token.value}</span>
|
return <TextToken key={i}>{token.value}</TextToken>
|
||||||
})}
|
})}
|
||||||
</div>
|
</CodeBlock>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,28 +90,18 @@ type CodeExamplesProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Container for multiple code examples
|
// Container for multiple code examples
|
||||||
export const CodeExamples: FC<CodeExamplesProps> = ({ examples }) => {
|
export const CodeExamples = ({ examples }: CodeExamplesProps) => {
|
||||||
return (
|
return (
|
||||||
<div class="code-examples-container">
|
<CodeExamplesBox class="code-examples-container">
|
||||||
<VStack
|
|
||||||
gap={2}
|
|
||||||
style={{
|
|
||||||
backgroundColor: "#f9fafb",
|
|
||||||
padding: "16px",
|
|
||||||
borderRadius: "8px",
|
|
||||||
border: "1px solid #e5e7eb",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{examples.map((example, i) => (
|
{examples.map((example, i) => (
|
||||||
<Code key={i}>{example}</Code>
|
<Code key={i}>{example}</Code>
|
||||||
))}
|
))}
|
||||||
</VStack>
|
</CodeExamplesBox>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Token = {
|
type Token = {
|
||||||
type: "tag" | "attr" | "string" | "number" | "brace" | "text"
|
type: 'tag' | 'attr' | 'string' | 'number' | 'brace' | 'text'
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,56 +111,56 @@ function tokenizeJSX(code: string): Token[] {
|
||||||
|
|
||||||
while (i < code.length) {
|
while (i < code.length) {
|
||||||
// Match opening/closing tags: < or </
|
// Match opening/closing tags: < or </
|
||||||
if (code[i] === "<") {
|
if (code[i] === '<') {
|
||||||
i++
|
i++
|
||||||
|
|
||||||
// Check for closing tag
|
// Check for closing tag
|
||||||
if (code[i] === "/") {
|
if (code[i] === '/') {
|
||||||
tokens.push({ type: "tag", value: "</" })
|
tokens.push({ type: 'tag', value: '</' })
|
||||||
i++
|
i++
|
||||||
} else {
|
} else {
|
||||||
tokens.push({ type: "tag", value: "<" })
|
tokens.push({ type: 'tag', value: '<' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get tag name
|
// Get tag name
|
||||||
let tagName = ""
|
let tagName = ''
|
||||||
while (i < code.length && /[A-Za-z0-9.]/.test(code[i]!)) {
|
while (i < code.length && /[A-Za-z0-9.]/.test(code[i]!)) {
|
||||||
tagName += code[i]
|
tagName += code[i]
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
if (tagName) {
|
if (tagName) {
|
||||||
tokens.push({ type: "tag", value: tagName })
|
tokens.push({ type: 'tag', value: tagName })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse attributes inside the tag
|
// Parse attributes inside the tag
|
||||||
while (i < code.length && code[i] !== ">") {
|
while (i < code.length && code[i] !== '>') {
|
||||||
// Skip whitespace
|
// Skip whitespace
|
||||||
if (/\s/.test(code[i]!)) {
|
if (/\s/.test(code[i]!)) {
|
||||||
tokens.push({ type: "text", value: code[i]! })
|
tokens.push({ type: 'text', value: code[i]! })
|
||||||
i++
|
i++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for self-closing /
|
// Check for self-closing /
|
||||||
if (code[i] === "/" && code[i + 1] === ">") {
|
if (code[i] === '/' && code[i + 1] === '>') {
|
||||||
tokens.push({ type: "tag", value: " />" })
|
tokens.push({ type: 'tag', value: ' />' })
|
||||||
i += 2
|
i += 2
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse attribute name
|
// Parse attribute name
|
||||||
let attrName = ""
|
let attrName = ''
|
||||||
while (i < code.length && /[a-zA-Z0-9-]/.test(code[i]!)) {
|
while (i < code.length && /[a-zA-Z0-9-]/.test(code[i]!)) {
|
||||||
attrName += code[i]
|
attrName += code[i]
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
if (attrName) {
|
if (attrName) {
|
||||||
tokens.push({ type: "attr", value: attrName })
|
tokens.push({ type: 'attr', value: attrName })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for =
|
// Check for =
|
||||||
if (code[i] === "=") {
|
if (code[i] === '=') {
|
||||||
tokens.push({ type: "text", value: "=" })
|
tokens.push({ type: 'text', value: '=' })
|
||||||
i++
|
i++
|
||||||
|
|
||||||
// Parse attribute value
|
// Parse attribute value
|
||||||
|
|
@ -171,18 +176,18 @@ function tokenizeJSX(code: string): Token[] {
|
||||||
str += '"'
|
str += '"'
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
tokens.push({ type: "string", value: str })
|
tokens.push({ type: 'string', value: str })
|
||||||
} else if (code[i] === "{") {
|
} else if (code[i] === '{') {
|
||||||
// Brace value
|
// Brace value
|
||||||
tokens.push({ type: "brace", value: "{" })
|
tokens.push({ type: 'brace', value: '{' })
|
||||||
i++
|
i++
|
||||||
|
|
||||||
// Get content inside braces
|
// Get content inside braces
|
||||||
let content = ""
|
let content = ''
|
||||||
let depth = 1
|
let depth = 1
|
||||||
while (i < code.length && depth > 0) {
|
while (i < code.length && depth > 0) {
|
||||||
if (code[i] === "{") depth++
|
if (code[i] === '{') depth++
|
||||||
if (code[i] === "}") {
|
if (code[i] === '}') {
|
||||||
depth--
|
depth--
|
||||||
if (depth === 0) break
|
if (depth === 0) break
|
||||||
}
|
}
|
||||||
|
|
@ -192,13 +197,13 @@ function tokenizeJSX(code: string): Token[] {
|
||||||
|
|
||||||
// Check if content is a number
|
// Check if content is a number
|
||||||
if (/^\d+$/.test(content)) {
|
if (/^\d+$/.test(content)) {
|
||||||
tokens.push({ type: "number", value: content })
|
tokens.push({ type: 'number', value: content })
|
||||||
} else {
|
} else {
|
||||||
tokens.push({ type: "text", value: content })
|
tokens.push({ type: 'text', value: content })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code[i] === "}") {
|
if (code[i] === '}') {
|
||||||
tokens.push({ type: "brace", value: "}" })
|
tokens.push({ type: 'brace', value: '}' })
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -206,19 +211,19 @@ function tokenizeJSX(code: string): Token[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Closing >
|
// Closing >
|
||||||
if (code[i] === ">") {
|
if (code[i] === '>') {
|
||||||
tokens.push({ type: "tag", value: ">" })
|
tokens.push({ type: 'tag', value: '>' })
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Regular text
|
// Regular text
|
||||||
let text = ""
|
let text = ''
|
||||||
while (i < code.length && code[i] !== "<") {
|
while (i < code.length && code[i] !== '<') {
|
||||||
text += code[i]
|
text += code[i]
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
if (text) {
|
if (text) {
|
||||||
tokens.push({ type: "text", value: text })
|
tokens.push({ type: 'text', value: text })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,48 @@
|
||||||
import { Section } from "./section"
|
import { define } from 'forge'
|
||||||
import { H2 } from "./text"
|
import { theme } from './theme'
|
||||||
import "hono/jsx"
|
import { Section } from './section'
|
||||||
import type { FC, PropsWithChildren, JSX } from "hono/jsx"
|
import { H2 } from './text'
|
||||||
import { VStack } from "./stack"
|
import { VStack } from './stack'
|
||||||
import { CodeExamples } from "./code"
|
|
||||||
import { cn } from "./cn"
|
|
||||||
|
|
||||||
type DividerProps = JSX.IntrinsicElements["div"] & PropsWithChildren
|
export const Divider = define('Divider', {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
margin: `${theme('spacing-4')} 0`,
|
||||||
|
|
||||||
export const Divider: FC<DividerProps> = (props) => {
|
parts: {
|
||||||
const { children, class: className, style, id, ref, ...rest } = props
|
Line: {
|
||||||
|
|
||||||
const containerStyle: JSX.CSSProperties = {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
margin: "16px 0",
|
|
||||||
...(style as JSX.CSSProperties),
|
|
||||||
}
|
|
||||||
|
|
||||||
const lineStyle: JSX.CSSProperties = {
|
|
||||||
flex: 1,
|
flex: 1,
|
||||||
borderTop: "1px solid #d1d5db",
|
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 textStyle: JSX.CSSProperties = {
|
render({ props, parts: { Root, Line, Text } }) {
|
||||||
padding: "0 12px",
|
const { children, ...rest } = props
|
||||||
fontSize: "14px",
|
|
||||||
color: "#6b7280",
|
|
||||||
backgroundColor: "#ffffff",
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={cn("Divider", className)} style={containerStyle} id={id} ref={ref} {...rest}>
|
<Root {...rest}>
|
||||||
<div style={lineStyle}></div>
|
<Line />
|
||||||
{children && (
|
{children && (
|
||||||
<>
|
<>
|
||||||
<span style={textStyle}>{children}</span>
|
<Text>{children}</Text>
|
||||||
<div style={lineStyle}></div>
|
<Line />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Root>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export const Test = () => {
|
export const Test = () => {
|
||||||
return (
|
return (
|
||||||
<Section gap={4} maxWidth="448px" style={{ padding: "16px" }}>
|
<Section gap={4} style={{ maxWidth: '448px', padding: '16px' }}>
|
||||||
{/* API Usage Examples */}
|
|
||||||
<CodeExamples
|
|
||||||
examples={[
|
|
||||||
'<Divider />',
|
|
||||||
'<Divider>OR</Divider>',
|
|
||||||
'<Divider style={{ margin: "24px 0" }} />',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<H2>Divider Examples</H2>
|
<H2>Divider Examples</H2>
|
||||||
|
|
||||||
<VStack gap={0}>
|
<VStack gap={0}>
|
||||||
|
|
@ -65,7 +53,7 @@ export const Test = () => {
|
||||||
|
|
||||||
{/* Just a line */}
|
{/* Just a line */}
|
||||||
<VStack gap={0}>
|
<VStack gap={0}>
|
||||||
<p>Look a line 👇</p>
|
<p>Look a line</p>
|
||||||
<Divider />
|
<Divider />
|
||||||
<p>So cool, so straight!</p>
|
<p>So cool, so straight!</p>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
|
||||||
196
src/grid.tsx
196
src/grid.tsx
|
|
@ -1,125 +1,75 @@
|
||||||
import type { TailwindSize } from "./types"
|
import { define } from 'forge'
|
||||||
import "hono/jsx"
|
import { theme } from './theme'
|
||||||
import type { FC, PropsWithChildren, JSX } from "hono/jsx"
|
import { VStack, HStack } from './stack'
|
||||||
import { VStack } from "./stack"
|
import { Button } from './button'
|
||||||
import { Button } from "./button"
|
import { Section } from './section'
|
||||||
import { Section } from "./section"
|
import { H2, H3 } from './text'
|
||||||
import { H2, H3 } from "./text"
|
|
||||||
import { CodeExamples } from "./code"
|
|
||||||
import { cn } from "./cn"
|
|
||||||
|
|
||||||
type GridCols = number | { sm?: number; md?: number; lg?: number; xl?: number }
|
export const Grid = define('Grid', {
|
||||||
|
display: 'grid',
|
||||||
|
|
||||||
type GridProps = JSX.IntrinsicElements["div"] & PropsWithChildren & {
|
variants: {
|
||||||
cols?: GridCols
|
cols: {
|
||||||
gap?: TailwindSize
|
1: { gridTemplateColumns: 'repeat(1, minmax(0, 1fr))' },
|
||||||
v?: keyof typeof alignItemsMap
|
2: { gridTemplateColumns: 'repeat(2, minmax(0, 1fr))' },
|
||||||
h?: keyof typeof justifyItemsMap
|
3: { gridTemplateColumns: 'repeat(3, minmax(0, 1fr))' },
|
||||||
}
|
4: { gridTemplateColumns: 'repeat(4, minmax(0, 1fr))' },
|
||||||
|
5: { gridTemplateColumns: 'repeat(5, minmax(0, 1fr))' },
|
||||||
export const Grid: FC<GridProps> = (props) => {
|
6: { gridTemplateColumns: 'repeat(6, minmax(0, 1fr))' },
|
||||||
const { cols = 2, gap = 4, v, h, class: className, style, id, ref, children, ...rest } = props
|
7: { gridTemplateColumns: 'repeat(7, minmax(0, 1fr))' },
|
||||||
|
},
|
||||||
const gapPx = gap * 4
|
gap: {
|
||||||
|
0: { gap: 0 },
|
||||||
const baseStyles: JSX.CSSProperties = {
|
1: { gap: theme('spacing-1') },
|
||||||
display: "grid",
|
2: { gap: theme('spacing-2') },
|
||||||
gridTemplateColumns: getColumnsValue(cols),
|
3: { gap: theme('spacing-3') },
|
||||||
gap: `${gapPx}px`,
|
4: { gap: theme('spacing-4') },
|
||||||
}
|
6: { gap: theme('spacing-6') },
|
||||||
|
8: { gap: theme('spacing-8') },
|
||||||
if (v) {
|
12: { gap: theme('spacing-12') },
|
||||||
baseStyles.alignItems = alignItemsMap[v]
|
},
|
||||||
}
|
v: {
|
||||||
|
start: { alignItems: 'start' },
|
||||||
if (h) {
|
center: { alignItems: 'center' },
|
||||||
baseStyles.justifyItems = justifyItemsMap[h]
|
end: { alignItems: 'end' },
|
||||||
}
|
stretch: { alignItems: 'stretch' },
|
||||||
|
},
|
||||||
const combinedStyles = {
|
h: {
|
||||||
...baseStyles,
|
start: { justifyItems: 'start' },
|
||||||
...(style as JSX.CSSProperties),
|
center: { justifyItems: 'center' },
|
||||||
}
|
end: { justifyItems: 'end' },
|
||||||
|
stretch: { justifyItems: 'stretch' },
|
||||||
return <div class={cn("Grid", className)} style={combinedStyles} id={id} ref={ref} {...rest}>{children}</div>
|
},
|
||||||
}
|
},
|
||||||
|
})
|
||||||
function getColumnsValue(cols: GridCols): string {
|
|
||||||
if (typeof cols === "number") {
|
|
||||||
return `repeat(${cols}, minmax(0, 1fr))`
|
|
||||||
}
|
|
||||||
|
|
||||||
// For responsive grids, we'll use the largest value
|
|
||||||
// In a real implementation, you'd want media queries which require CSS
|
|
||||||
// For now, let's use the largest value specified
|
|
||||||
const largestCols = cols.xl || cols.lg || cols.md || cols.sm || 1
|
|
||||||
return `repeat(${largestCols}, minmax(0, 1fr))`
|
|
||||||
}
|
|
||||||
|
|
||||||
const alignItemsMap = {
|
|
||||||
start: "start",
|
|
||||||
center: "center",
|
|
||||||
end: "end",
|
|
||||||
stretch: "stretch",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
const justifyItemsMap = {
|
|
||||||
start: "start",
|
|
||||||
center: "center",
|
|
||||||
end: "end",
|
|
||||||
stretch: "stretch",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const Test = () => {
|
export const Test = () => {
|
||||||
return (
|
return (
|
||||||
<Section gap={4} style={{ padding: "16px" }}>
|
<Section gap={4} style={{ padding: '16px' }}>
|
||||||
{/* API Usage Examples */}
|
|
||||||
<CodeExamples
|
|
||||||
examples={[
|
|
||||||
'<Grid cols={3}>...</Grid>',
|
|
||||||
'<Grid cols={4} gap={6}>...</Grid>',
|
|
||||||
'<Grid cols={{ sm: 1, md: 2, lg: 3 }}>...</Grid>',
|
|
||||||
'<Grid cols={2} v="center" h="center">...</Grid>',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<VStack gap={6}>
|
<VStack gap={6}>
|
||||||
<H2>Grid Examples</H2>
|
<H2>Grid Examples</H2>
|
||||||
|
|
||||||
{/* Simple 3-column grid */}
|
{/* Simple 3-column grid */}
|
||||||
<VStack gap={2}>
|
<VStack gap={2}>
|
||||||
<H3>Simple 3 columns: cols=3</H3>
|
<H3>Simple 3 columns: cols={3}</H3>
|
||||||
<Grid cols={3} gap={4}>
|
<Grid cols={3} gap={4}>
|
||||||
<div style={{ backgroundColor: "#fecaca", padding: "16px", textAlign: "center" }}>Item 1</div>
|
<div style={{ backgroundColor: '#fecaca', padding: '16px', textAlign: 'center' }}>Item 1</div>
|
||||||
<div style={{ backgroundColor: "#bbf7d0", padding: "16px", textAlign: "center" }}>Item 2</div>
|
<div style={{ backgroundColor: '#bbf7d0', padding: '16px', textAlign: 'center' }}>Item 2</div>
|
||||||
<div style={{ backgroundColor: "#bfdbfe", padding: "16px", textAlign: "center" }}>Item 3</div>
|
<div style={{ backgroundColor: '#bfdbfe', padding: '16px', textAlign: 'center' }}>Item 3</div>
|
||||||
<div style={{ backgroundColor: "#fef08a", padding: "16px", textAlign: "center" }}>Item 4</div>
|
<div style={{ backgroundColor: '#fef08a', padding: '16px', textAlign: 'center' }}>Item 4</div>
|
||||||
<div style={{ backgroundColor: "#e9d5ff", padding: "16px", textAlign: "center" }}>Item 5</div>
|
<div style={{ backgroundColor: '#e9d5ff', padding: '16px', textAlign: 'center' }}>Item 5</div>
|
||||||
<div style={{ backgroundColor: "#fbcfe8", padding: "16px", textAlign: "center" }}>Item 6</div>
|
<div style={{ backgroundColor: '#fbcfe8', padding: '16px', textAlign: 'center' }}>Item 6</div>
|
||||||
</Grid>
|
</Grid>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
{/* Responsive grid */}
|
{/* 4-column grid */}
|
||||||
<VStack gap={2}>
|
<VStack gap={2}>
|
||||||
<H3>Responsive: cols={sm: 1, md: 2, lg: 3}</H3>
|
<H3>4 columns: cols={4}</H3>
|
||||||
<Grid cols={{ sm: 1, md: 2, lg: 3 }} gap={4}>
|
<Grid cols={4} gap={4}>
|
||||||
<div style={{ backgroundColor: "#fecaca", padding: "16px", textAlign: "center" }}>Card 1</div>
|
<div style={{ backgroundColor: '#fecaca', padding: '16px', textAlign: 'center' }}>Card 1</div>
|
||||||
<div style={{ backgroundColor: "#bbf7d0", padding: "16px", textAlign: "center" }}>Card 2</div>
|
<div style={{ backgroundColor: '#bbf7d0', padding: '16px', textAlign: 'center' }}>Card 2</div>
|
||||||
<div style={{ backgroundColor: "#bfdbfe", padding: "16px", textAlign: "center" }}>Card 3</div>
|
<div style={{ backgroundColor: '#bfdbfe', padding: '16px', textAlign: 'center' }}>Card 3</div>
|
||||||
<div style={{ backgroundColor: "#fef08a", padding: "16px", textAlign: "center" }}>Card 4</div>
|
<div style={{ backgroundColor: '#fef08a', padding: '16px', textAlign: 'center' }}>Card 4</div>
|
||||||
</Grid>
|
|
||||||
</VStack>
|
|
||||||
|
|
||||||
{/* More responsive examples */}
|
|
||||||
<VStack gap={2}>
|
|
||||||
<H3>More responsive: cols={sm: 2, lg: 4, xl: 6}</H3>
|
|
||||||
<Grid cols={{ sm: 2, lg: 4, xl: 6 }} gap={4}>
|
|
||||||
<div style={{ backgroundColor: "#fecaca", padding: "16px", textAlign: "center" }}>Item A</div>
|
|
||||||
<div style={{ backgroundColor: "#bbf7d0", padding: "16px", textAlign: "center" }}>Item B</div>
|
|
||||||
<div style={{ backgroundColor: "#bfdbfe", padding: "16px", textAlign: "center" }}>Item C</div>
|
|
||||||
<div style={{ backgroundColor: "#fef08a", padding: "16px", textAlign: "center" }}>Item D</div>
|
|
||||||
<div style={{ backgroundColor: "#e9d5ff", padding: "16px", textAlign: "center" }}>Item E</div>
|
|
||||||
<div style={{ backgroundColor: "#fbcfe8", padding: "16px", textAlign: "center" }}>Item F</div>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
|
|
@ -127,17 +77,17 @@ export const Test = () => {
|
||||||
<VStack gap={2}>
|
<VStack gap={2}>
|
||||||
<H3>Payment buttons example</H3>
|
<H3>Payment buttons example</H3>
|
||||||
<Grid cols={3} gap={4}>
|
<Grid cols={3} gap={4}>
|
||||||
<Button variant="outline" style={{ height: "80px", flexDirection: "column" }}>
|
<Button variant="outline" style={{ height: '80px', flexDirection: 'column' }}>
|
||||||
<div style={{ fontSize: "24px" }}>💳</div>
|
<div style={{ fontSize: '24px' }}>💳</div>
|
||||||
<span style={{ fontSize: "12px" }}>Card</span>
|
<span style={{ fontSize: '12px' }}>Card</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" style={{ height: "80px", flexDirection: "column" }}>
|
<Button variant="outline" style={{ height: '80px', flexDirection: 'column' }}>
|
||||||
<div style={{ fontSize: "24px" }}>🍎</div>
|
<div style={{ fontSize: '24px' }}>🍎</div>
|
||||||
<span style={{ fontSize: "12px" }}>Apple</span>
|
<span style={{ fontSize: '12px' }}>Apple</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" style={{ height: "80px", flexDirection: "column" }}>
|
<Button variant="outline" style={{ height: '80px', flexDirection: 'column' }}>
|
||||||
<div style={{ fontSize: "24px" }}>💰</div>
|
<div style={{ fontSize: '24px' }}>💰</div>
|
||||||
<span style={{ fontSize: "12px" }}>PayPal</span>
|
<span style={{ fontSize: '12px' }}>PayPal</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
@ -145,18 +95,18 @@ export const Test = () => {
|
||||||
{/* Alignment examples */}
|
{/* Alignment examples */}
|
||||||
<VStack gap={2}>
|
<VStack gap={2}>
|
||||||
<H3>Alignment: v="center" h="center"</H3>
|
<H3>Alignment: v="center" h="center"</H3>
|
||||||
<Grid cols={3} gap={4} v="center" h="center" style={{ height: "128px", backgroundColor: "#f3f4f6" }}>
|
<Grid cols={3} gap={4} v="center" h="center" style={{ height: '128px', backgroundColor: '#f3f4f6' }}>
|
||||||
<div style={{ backgroundColor: "#fecaca", padding: "8px" }}>Item 1</div>
|
<div style={{ backgroundColor: '#fecaca', padding: '8px' }}>Item 1</div>
|
||||||
<div style={{ backgroundColor: "#bbf7d0", padding: "8px" }}>Item 2</div>
|
<div style={{ backgroundColor: '#bbf7d0', padding: '8px' }}>Item 2</div>
|
||||||
<div style={{ backgroundColor: "#bfdbfe", padding: "8px" }}>Item 3</div>
|
<div style={{ backgroundColor: '#bfdbfe', padding: '8px' }}>Item 3</div>
|
||||||
</Grid>
|
</Grid>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
<VStack gap={2}>
|
<VStack gap={2}>
|
||||||
<H3>Alignment: v="start" h="end"</H3>
|
<H3>Alignment: v="start" h="end"</H3>
|
||||||
<Grid cols={2} gap={4} v="start" h="end" style={{ height: "96px", backgroundColor: "#f3f4f6" }}>
|
<Grid cols={2} gap={4} v="start" h="end" style={{ height: '96px', backgroundColor: '#f3f4f6' }}>
|
||||||
<div style={{ backgroundColor: "#e9d5ff", padding: "8px" }}>Left</div>
|
<div style={{ backgroundColor: '#e9d5ff', padding: '8px' }}>Left</div>
|
||||||
<div style={{ backgroundColor: "#fed7aa", padding: "8px" }}>Right</div>
|
<div style={{ backgroundColor: '#fed7aa', padding: '8px' }}>Right</div>
|
||||||
</Grid>
|
</Grid>
|
||||||
</VStack>
|
</VStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
|
||||||
137
src/icon.tsx
137
src/icon.tsx
|
|
@ -1,27 +1,50 @@
|
||||||
import "hono/jsx"
|
import { define } from 'forge'
|
||||||
import type { FC, JSX } from "hono/jsx"
|
import { theme } from './theme'
|
||||||
import * as icons from "lucide-static"
|
import * as icons from 'lucide-static'
|
||||||
import { Grid } from "./grid"
|
import { Grid } from './grid'
|
||||||
import { VStack } from "./stack"
|
import { VStack } from './stack'
|
||||||
import { Section } from "./section"
|
import { Section } from './section'
|
||||||
import { H2, Text } from "./text"
|
import { H2, Text } from './text'
|
||||||
import { CodeExamples } from "./code"
|
|
||||||
import { cn } from "./cn"
|
|
||||||
|
|
||||||
export type IconName = keyof typeof icons
|
export type IconName = keyof typeof icons
|
||||||
|
|
||||||
type IconProps = JSX.IntrinsicElements["div"] & {
|
// Icon wrapper - the SVG is injected via dangerouslySetInnerHTML
|
||||||
|
const IconWrapper = define('Icon', {
|
||||||
|
display: 'block',
|
||||||
|
flexShrink: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// IconLink wrapper
|
||||||
|
const IconLinkWrapper = define('IconLink', {
|
||||||
|
base: 'a',
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
transition: 'opacity 0.2s',
|
||||||
|
|
||||||
|
states: {
|
||||||
|
':hover': {
|
||||||
|
opacity: 0.7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
type IconProps = Parameters<typeof IconWrapper>[0] & {
|
||||||
name: IconName
|
name: IconName
|
||||||
size?: number
|
size?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type IconLinkProps = JSX.IntrinsicElements["a"] & {
|
type IconLinkProps = Parameters<typeof IconLinkWrapper>[0] & {
|
||||||
name: IconName
|
name: IconName
|
||||||
size?: number
|
size?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Icon: FC<IconProps> = (props) => {
|
function sizeToPixels(size: number): number {
|
||||||
const { name, size = 6, class: className, style, id, ref, ...rest } = props
|
return size * 4
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Icon = (props: IconProps) => {
|
||||||
|
const { name, size = 6, style, ...rest } = props
|
||||||
|
|
||||||
const iconSvg = icons[name]
|
const iconSvg = icons[name]
|
||||||
|
|
||||||
|
|
@ -30,64 +53,42 @@ export const Icon: FC<IconProps> = (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const pixelSize = sizeToPixels(size)
|
const pixelSize = sizeToPixels(size)
|
||||||
const iconStyle: JSX.CSSProperties = {
|
const iconStyle = {
|
||||||
display: "block",
|
|
||||||
flexShrink: "0",
|
|
||||||
width: `${pixelSize}px`,
|
width: `${pixelSize}px`,
|
||||||
height: `${pixelSize}px`,
|
height: `${pixelSize}px`,
|
||||||
...(style as JSX.CSSProperties),
|
...style,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modify the SVG string to include our custom attributes
|
// Modify the SVG string to include our custom attributes
|
||||||
const modifiedSvg = iconSvg
|
const modifiedSvg = iconSvg
|
||||||
.replace(/width="[^"]*"/, "")
|
.replace(/width="[^"]*"/, '')
|
||||||
.replace(/height="[^"]*"/, "")
|
.replace(/height="[^"]*"/, '')
|
||||||
.replace(/class="[^"]*"/, "")
|
.replace(/class="[^"]*"/, '')
|
||||||
.replace(
|
.replace(
|
||||||
/<svg([^>]*)>/,
|
/<svg([^>]*)>/,
|
||||||
`<svg$1 style="display: block; flex-shrink: 0; width: ${pixelSize}px; height: ${pixelSize}px;" class="${cn("Icon", className)}">`
|
`<svg$1 style="display: block; flex-shrink: 0; width: ${pixelSize}px; height: ${pixelSize}px;">`
|
||||||
)
|
)
|
||||||
|
|
||||||
return <div dangerouslySetInnerHTML={{ __html: modifiedSvg }} style={iconStyle} id={id} ref={ref} {...rest} />
|
return <IconWrapper dangerouslySetInnerHTML={{ __html: modifiedSvg }} style={iconStyle} {...rest} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IconLink: FC<IconLinkProps> = (props) => {
|
export const IconLink = (props: IconLinkProps) => {
|
||||||
const { href = "#", target, class: className, style, id, ref, name, size, ...rest } = props
|
const { href = '#', name, size, ...rest } = props
|
||||||
|
|
||||||
const linkStyle: JSX.CSSProperties = {
|
|
||||||
display: "inline-flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
transition: "opacity 0.2s",
|
|
||||||
...(style as JSX.CSSProperties),
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={href} target={target} class={cn("IconLink", className)} style={linkStyle} id={id} ref={ref} {...rest}>
|
<IconLinkWrapper href={href} {...rest}>
|
||||||
<Icon name={name} size={size} />
|
<Icon name={name} size={size} />
|
||||||
</a>
|
</IconLinkWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Test = () => {
|
export const Test = () => {
|
||||||
return (
|
return (
|
||||||
<Section>
|
<Section>
|
||||||
{/* API Usage Examples */}
|
|
||||||
<CodeExamples
|
|
||||||
examples={[
|
|
||||||
'<Icon name="Heart" />',
|
|
||||||
'<Icon name="Star" size={8} />',
|
|
||||||
'<Icon name="Home" style={{ color: "#3b82f6" }} />',
|
|
||||||
'<IconLink name="ExternalLink" href="/link" />',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* === ICON TESTS === */}
|
|
||||||
|
|
||||||
{/* Size variations */}
|
{/* Size variations */}
|
||||||
<VStack gap={4}>
|
<VStack gap={4}>
|
||||||
<H2>Icon Size Variations</H2>
|
<H2>Icon Size Variations</H2>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "16px" }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||||
{([3, 4, 5, 6, 8, 10, 12, 16] as const).map((size) => (
|
{([3, 4, 5, 6, 8, 10, 12, 16] as const).map((size) => (
|
||||||
<VStack h="center" gap={2} key={size}>
|
<VStack h="center" gap={2} key={size}>
|
||||||
<Icon name="Heart" size={size} />
|
<Icon name="Heart" size={size} />
|
||||||
|
|
@ -106,7 +107,7 @@ export const Test = () => {
|
||||||
<Text>Default</Text>
|
<Text>Default</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Icon name="Star" size={12} style={{ color: "#3b82f6" }} />
|
<Icon name="Star" size={12} style={{ color: '#3b82f6' }} />
|
||||||
<Text>Blue Color</Text>
|
<Text>Blue Color</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
|
|
@ -114,11 +115,11 @@ export const Test = () => {
|
||||||
<Text>Thin Stroke</Text>
|
<Text>Thin Stroke</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Icon name="Star" size={12} style={{ color: "#fbbf24", fill: "currentColor", stroke: "none" }} />
|
<Icon name="Star" size={12} style={{ color: '#fbbf24', fill: 'currentColor', stroke: 'none' }} />
|
||||||
<Text>Filled</Text>
|
<Text>Filled</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Icon name="Star" size={12} style={{ color: "#a855f7", transition: "color 0.2s" }} />
|
<Icon name="Star" size={12} style={{ color: '#a855f7', transition: 'color 0.2s' }} />
|
||||||
<Text>Hover Effect</Text>
|
<Text>Hover Effect</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
@ -129,30 +130,28 @@ export const Test = () => {
|
||||||
<H2>Advanced Styling</H2>
|
<H2>Advanced Styling</H2>
|
||||||
<Grid cols={4} gap={6}>
|
<Grid cols={4} gap={6}>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Icon name="Heart" size={12} style={{ color: "#ef4444", fill: "currentColor", stroke: "none" }} />
|
<Icon name="Heart" size={12} style={{ color: '#ef4444', fill: 'currentColor', stroke: 'none' }} />
|
||||||
<Text>Filled Heart</Text>
|
<Text>Filled Heart</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Icon name="Shield" size={12} style={{ color: "#16a34a", strokeWidth: "2" }} />
|
<Icon name="Shield" size={12} style={{ color: '#16a34a', strokeWidth: '2' }} />
|
||||||
<Text>Thick Stroke</Text>
|
<Text>Thick Stroke</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Icon name="Sun" size={12} style={{ color: "#eab308" }} />
|
<Icon name="Sun" size={12} style={{ color: '#eab308' }} />
|
||||||
<Text>Sun Icon</Text>
|
<Text>Sun Icon</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Icon name="Zap" size={12} style={{ color: "#60a5fa", filter: "drop-shadow(0 4px 6px rgba(0,0,0,0.3))" }} />
|
<Icon name="Zap" size={12} style={{ color: '#60a5fa', filter: 'drop-shadow(0 4px 6px rgba(0,0,0,0.3))' }} />
|
||||||
<Text>Drop Shadow</Text>
|
<Text>Drop Shadow</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Grid>
|
</Grid>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
{/* === ICON LINK TESTS === */}
|
{/* Icon links */}
|
||||||
|
|
||||||
{/* Basic icon links */}
|
|
||||||
<VStack gap={4}>
|
<VStack gap={4}>
|
||||||
<H2>Icon Links</H2>
|
<H2>Icon Links</H2>
|
||||||
<div style={{ display: "flex", gap: "24px" }}>
|
<div style={{ display: 'flex', gap: '24px' }}>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<IconLink name="Home" size={8} href="/" />
|
<IconLink name="Home" size={8} href="/" />
|
||||||
<Text>Home Link</Text>
|
<Text>Home Link</Text>
|
||||||
|
|
@ -182,10 +181,10 @@ export const Test = () => {
|
||||||
size={8}
|
size={8}
|
||||||
href="#"
|
href="#"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#3b82f6",
|
backgroundColor: '#3b82f6',
|
||||||
color: "white",
|
color: 'white',
|
||||||
padding: "8px",
|
padding: '8px',
|
||||||
borderRadius: "8px",
|
borderRadius: '8px',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Text>Button Style</Text>
|
<Text>Button Style</Text>
|
||||||
|
|
@ -196,19 +195,19 @@ export const Test = () => {
|
||||||
size={8}
|
size={8}
|
||||||
href="#"
|
href="#"
|
||||||
style={{
|
style={{
|
||||||
border: "2px solid #d1d5db",
|
border: '2px solid #d1d5db',
|
||||||
padding: "8px",
|
padding: '8px',
|
||||||
borderRadius: "9999px",
|
borderRadius: '9999px',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Text>Circle Border</Text>
|
<Text>Circle Border</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<IconLink name="Heart" size={8} href="#" style={{ color: "#ef4444" }} />
|
<IconLink name="Heart" size={8} href="#" style={{ color: '#ef4444' }} />
|
||||||
<Text>Red Heart</Text>
|
<Text>Red Heart</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<IconLink name="Star" size={8} href="#" style={{ color: "#fbbf24", fill: "currentColor" }} />
|
<IconLink name="Star" size={8} href="#" style={{ color: '#fbbf24', fill: 'currentColor' }} />
|
||||||
<Text>Filled Star</Text>
|
<Text>Filled Star</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
@ -216,7 +215,3 @@ export const Test = () => {
|
||||||
</Section>
|
</Section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function sizeToPixels(size: number): number {
|
|
||||||
return size * 4
|
|
||||||
}
|
|
||||||
|
|
|
||||||
144
src/image.tsx
144
src/image.tsx
|
|
@ -1,51 +1,35 @@
|
||||||
import { Section } from "./section"
|
import { define } from 'forge'
|
||||||
import { H2, H3, H4, H5, Text, SmallText } from "./text"
|
import { Section } from './section'
|
||||||
import "hono/jsx"
|
import { H2, H3, H4, H5, Text } from './text'
|
||||||
import type { FC, JSX } from "hono/jsx"
|
import { VStack, HStack } from './stack'
|
||||||
import { VStack, HStack } from "./stack"
|
import { Grid } from './grid'
|
||||||
import { Grid } from "./grid"
|
|
||||||
import { CodeExamples } from "./code"
|
|
||||||
import { cn } from "./cn"
|
|
||||||
|
|
||||||
export type ImageProps = JSX.IntrinsicElements["img"] & {
|
export const Image = define('Image', {
|
||||||
width?: number
|
base: 'img',
|
||||||
height?: number
|
|
||||||
objectFit?: "cover" | "contain" | "fill" | "none" | "scale-down"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Image: FC<ImageProps> = (props) => {
|
variants: {
|
||||||
const { src, alt = "", width, height, objectFit, class: className, style, id, ref, ...rest } = props
|
objectFit: {
|
||||||
|
cover: { objectFit: 'cover' },
|
||||||
|
contain: { objectFit: 'contain' },
|
||||||
|
fill: { objectFit: 'fill' },
|
||||||
|
none: { objectFit: 'none' },
|
||||||
|
'scale-down': { objectFit: 'scale-down' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const imageStyle: JSX.CSSProperties = {
|
export type ImageProps = Parameters<typeof Image>[0]
|
||||||
width: width ? `${width}px` : undefined,
|
|
||||||
height: height ? `${height}px` : undefined,
|
|
||||||
objectFit: objectFit,
|
|
||||||
...(style as JSX.CSSProperties),
|
|
||||||
}
|
|
||||||
|
|
||||||
return <img src={src} alt={alt} class={cn("Image", className)} style={imageStyle} id={id} ref={ref} {...rest} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Test = () => {
|
export const Test = () => {
|
||||||
const sampleImages = [
|
const sampleImages = [
|
||||||
"https://picsum.photos/seed/1/400/600", // Portrait
|
'https://picsum.photos/seed/1/400/600', // Portrait
|
||||||
"https://picsum.photos/seed/2/600/400", // Landscape
|
'https://picsum.photos/seed/2/600/400', // Landscape
|
||||||
"https://picsum.photos/seed/3/300/300", // Square
|
'https://picsum.photos/seed/3/300/300', // Square
|
||||||
"https://picsum.photos/seed/4/200/100", // Small image
|
'https://picsum.photos/seed/4/200/100', // Small image
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section>
|
<Section>
|
||||||
{/* API Usage Examples */}
|
|
||||||
<CodeExamples
|
|
||||||
examples={[
|
|
||||||
'<Image src="/photo.jpg" />',
|
|
||||||
'<Image src="/photo.jpg" width={200} height={200} />',
|
|
||||||
'<Image src="/photo.jpg" objectFit="cover" />',
|
|
||||||
'<Image src="/photo.jpg" style={{ borderRadius: "8px" }} />',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<H2>Image Examples</H2>
|
<H2>Image Examples</H2>
|
||||||
|
|
||||||
{/* Size variations */}
|
{/* Size variations */}
|
||||||
|
|
@ -53,19 +37,19 @@ export const Test = () => {
|
||||||
<H3>Size Variations</H3>
|
<H3>Size Variations</H3>
|
||||||
<HStack gap={4} wrap>
|
<HStack gap={4} wrap>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Image src={sampleImages[0]!} width={64} height={64} objectFit="cover" alt="64x64" />
|
<Image src={sampleImages[0]} objectFit="cover" style={{ width: 64, height: 64 }} alt="64x64" />
|
||||||
<Text>64x64</Text>
|
<Text>64x64</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Image src={sampleImages[0]!} width={96} height={96} objectFit="cover" alt="96x96" />
|
<Image src={sampleImages[0]} objectFit="cover" style={{ width: 96, height: 96 }} alt="96x96" />
|
||||||
<Text>96x96</Text>
|
<Text>96x96</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Image src={sampleImages[0]!} width={128} height={128} objectFit="cover" alt="128x128" />
|
<Image src={sampleImages[0]} objectFit="cover" style={{ width: 128, height: 128 }} alt="128x128" />
|
||||||
<Text>128x128</Text>
|
<Text>128x128</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Image src={sampleImages[0]!} width={192} height={128} objectFit="cover" alt="192x128" />
|
<Image src={sampleImages[0]} objectFit="cover" style={{ width: 192, height: 128 }} alt="192x128" />
|
||||||
<Text>192x128</Text>
|
<Text>192x128</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
@ -74,61 +58,49 @@ export const Test = () => {
|
||||||
{/* Object fit variations */}
|
{/* Object fit variations */}
|
||||||
<VStack gap={4}>
|
<VStack gap={4}>
|
||||||
<H3>Object Fit Variations</H3>
|
<H3>Object Fit Variations</H3>
|
||||||
<Text>
|
<Text>Same image with different object-fit values</Text>
|
||||||
Same image with different object-fit values
|
|
||||||
</Text>
|
|
||||||
<HStack gap={6} wrap>
|
<HStack gap={6} wrap>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Image
|
<Image
|
||||||
src={sampleImages[0]!}
|
src={sampleImages[0]}
|
||||||
width={128}
|
|
||||||
height={128}
|
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
style={{ border: "1px solid black" }}
|
style={{ width: 128, height: 128, border: '1px solid black' }}
|
||||||
alt="Object cover"
|
alt="Object cover"
|
||||||
/>
|
/>
|
||||||
<Text>object-fit: cover</Text>
|
<Text>object-fit: cover</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Image
|
<Image
|
||||||
src={sampleImages[0]!}
|
src={sampleImages[0]}
|
||||||
width={128}
|
|
||||||
height={128}
|
|
||||||
objectFit="contain"
|
objectFit="contain"
|
||||||
style={{ border: "1px solid black", backgroundColor: "#f3f4f6" }}
|
style={{ width: 128, height: 128, border: '1px solid black', backgroundColor: '#f3f4f6' }}
|
||||||
alt="Object contain"
|
alt="Object contain"
|
||||||
/>
|
/>
|
||||||
<Text>object-fit: contain</Text>
|
<Text>object-fit: contain</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Image
|
<Image
|
||||||
src={sampleImages[0]!}
|
src={sampleImages[0]}
|
||||||
width={128}
|
|
||||||
height={128}
|
|
||||||
objectFit="fill"
|
objectFit="fill"
|
||||||
style={{ border: "1px solid black" }}
|
style={{ width: 128, height: 128, border: '1px solid black' }}
|
||||||
alt="Object fill"
|
alt="Object fill"
|
||||||
/>
|
/>
|
||||||
<Text>object-fit: fill</Text>
|
<Text>object-fit: fill</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Image
|
<Image
|
||||||
src={sampleImages[0]!}
|
src={sampleImages[0]}
|
||||||
width={128}
|
|
||||||
height={128}
|
|
||||||
objectFit="scale-down"
|
objectFit="scale-down"
|
||||||
style={{ border: "1px solid black", backgroundColor: "#f3f4f6" }}
|
style={{ width: 128, height: 128, border: '1px solid black', backgroundColor: '#f3f4f6' }}
|
||||||
alt="Object scale-down"
|
alt="Object scale-down"
|
||||||
/>
|
/>
|
||||||
<Text>object-fit: scale-down</Text>
|
<Text>object-fit: scale-down</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Image
|
<Image
|
||||||
src={sampleImages[0]!}
|
src={sampleImages[0]}
|
||||||
width={128}
|
|
||||||
height={128}
|
|
||||||
objectFit="none"
|
objectFit="none"
|
||||||
style={{ border: "1px solid black", backgroundColor: "#f3f4f6" }}
|
style={{ width: 128, height: 128, border: '1px solid black', backgroundColor: '#f3f4f6' }}
|
||||||
alt="Object none"
|
alt="Object none"
|
||||||
/>
|
/>
|
||||||
<Text>object-fit: none</Text>
|
<Text>object-fit: none</Text>
|
||||||
|
|
@ -142,36 +114,32 @@ export const Test = () => {
|
||||||
<HStack gap={6} wrap>
|
<HStack gap={6} wrap>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Image
|
<Image
|
||||||
src={sampleImages[0]!}
|
src={sampleImages[0]}
|
||||||
width={128}
|
|
||||||
height={128}
|
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
style={{ borderRadius: "8px", border: "4px solid #3b82f6" }}
|
style={{ width: 128, height: 128, borderRadius: '8px', border: '4px solid #3b82f6' }}
|
||||||
alt="Rounded with border"
|
alt="Rounded with border"
|
||||||
/>
|
/>
|
||||||
<Text>Rounded + Border</Text>
|
<Text>Rounded + Border</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Image
|
<Image
|
||||||
src={sampleImages[1]!}
|
src={sampleImages[1]}
|
||||||
width={128}
|
|
||||||
height={128}
|
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
style={{ boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)" }}
|
style={{ width: 128, height: 128, boxShadow: '0 10px 15px rgba(0, 0, 0, 0.3)' }}
|
||||||
alt="With shadow"
|
alt="With shadow"
|
||||||
/>
|
/>
|
||||||
<Text>With Shadow</Text>
|
<Text>With Shadow</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<Image
|
<Image
|
||||||
src={sampleImages[2]!}
|
src={sampleImages[2]}
|
||||||
width={128}
|
|
||||||
height={128}
|
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: "9999px",
|
width: 128,
|
||||||
border: "4px solid #22c55e",
|
height: 128,
|
||||||
boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)",
|
borderRadius: '9999px',
|
||||||
|
border: '4px solid #22c55e',
|
||||||
|
boxShadow: '0 10px 15px rgba(0, 0, 0, 0.3)',
|
||||||
}}
|
}}
|
||||||
alt="Circular with effects"
|
alt="Circular with effects"
|
||||||
/>
|
/>
|
||||||
|
|
@ -188,21 +156,19 @@ export const Test = () => {
|
||||||
<VStack gap={2}>
|
<VStack gap={2}>
|
||||||
<H4>Avatar</H4>
|
<H4>Avatar</H4>
|
||||||
<Image
|
<Image
|
||||||
src={sampleImages[0]!}
|
src={sampleImages[0]}
|
||||||
width={48}
|
|
||||||
height={48}
|
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
style={{ borderRadius: "9999px" }}
|
style={{ width: 48, height: 48, borderRadius: '9999px' }}
|
||||||
alt="Avatar"
|
alt="Avatar"
|
||||||
/>
|
/>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
{/* Card image */}
|
{/* Card image */}
|
||||||
<VStack gap={2} style={{ maxWidth: "384px" }}>
|
<VStack gap={2} style={{ maxWidth: '384px' }}>
|
||||||
<H4>Card Image</H4>
|
<H4>Card Image</H4>
|
||||||
<VStack gap={0} style={{ border: "1px solid #d1d5db", borderRadius: "8px", overflow: "hidden" }}>
|
<VStack gap={0} style={{ border: '1px solid #d1d5db', borderRadius: '8px', overflow: 'hidden' }}>
|
||||||
<Image src={sampleImages[1]!} width={384} height={192} objectFit="cover" alt="Card image" />
|
<Image src={sampleImages[1]} objectFit="cover" style={{ width: 384, height: 192 }} alt="Card image" />
|
||||||
<VStack gap={1} style={{ padding: "16px" }}>
|
<VStack gap={1} style={{ padding: '16px' }}>
|
||||||
<H5>Card Title</H5>
|
<H5>Card Title</H5>
|
||||||
<Text>Card description goes here</Text>
|
<Text>Card description goes here</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
@ -217,10 +183,8 @@ export const Test = () => {
|
||||||
<Image
|
<Image
|
||||||
key={i}
|
key={i}
|
||||||
src={src}
|
src={src}
|
||||||
width={120}
|
|
||||||
height={120}
|
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
style={{ borderRadius: "4px" }}
|
style={{ width: 120, height: 120, borderRadius: '4px' }}
|
||||||
alt={`Gallery ${i}`}
|
alt={`Gallery ${i}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,40 @@
|
||||||
export { cn } from "./cn"
|
// Re-export Forge utilities
|
||||||
|
export { Styles, extendThemes } from 'forge'
|
||||||
|
export { theme } from './theme'
|
||||||
|
|
||||||
export { Button } from "./button"
|
export { cn } from './cn'
|
||||||
export type { ButtonProps } from "./button"
|
|
||||||
|
|
||||||
export { Icon, IconLink } from "./icon"
|
export { Button } from './button'
|
||||||
export type { IconName } from "./icon"
|
export type { ButtonProps } from './button'
|
||||||
|
|
||||||
export { VStack, HStack } from "./stack"
|
export { Icon, IconLink } from './icon'
|
||||||
|
export type { IconName } from './icon'
|
||||||
|
|
||||||
export { Grid } from "./grid"
|
export { VStack, HStack } from './stack'
|
||||||
|
|
||||||
export { Divider } from "./divider"
|
export { Grid } from './grid'
|
||||||
|
|
||||||
export { Avatar } from "./avatar"
|
export { Divider } from './divider'
|
||||||
export type { AvatarProps } from "./avatar"
|
|
||||||
|
|
||||||
export { Image } from "./image"
|
export { Avatar } from './avatar'
|
||||||
export type { ImageProps } from "./image"
|
export type { AvatarProps } from './avatar'
|
||||||
|
|
||||||
export { Input } from "./input"
|
export { Image } from './image'
|
||||||
export type { InputProps } from "./input"
|
export type { ImageProps } from './image'
|
||||||
|
|
||||||
export { Select } from "./select"
|
export { Input } from './input'
|
||||||
export type { SelectProps, SelectOption } from "./select"
|
export type { InputProps } from './input'
|
||||||
|
|
||||||
export { Placeholder } from "./placeholder"
|
export { Select } from './select'
|
||||||
export { default as PlaceholderDefault } from "./placeholder"
|
export type { SelectProps, SelectOption } from './select'
|
||||||
|
|
||||||
export { H1, H2, H3, H4, H5, Text, SmallText } from "./text"
|
export { Placeholder } from './placeholder'
|
||||||
|
export { default as PlaceholderDefault } from './placeholder'
|
||||||
|
|
||||||
export { Box, RedBox, GreenBox, BlueBox, GrayBox } from "./box"
|
export { H1, H2, H3, H4, H5, Text, SmallText } from './text'
|
||||||
|
|
||||||
export { Section } from "./section"
|
export { Box, RedBox, GreenBox, BlueBox, GrayBox } from './box'
|
||||||
|
|
||||||
export type { TailwindSize, CommonHTMLProps } from "./types"
|
export { Section } from './section'
|
||||||
|
|
||||||
|
export type { TailwindSize, CommonHTMLProps } from './types'
|
||||||
|
|
|
||||||
177
src/input.tsx
177
src/input.tsx
|
|
@ -1,89 +1,99 @@
|
||||||
import { Section } from "./section"
|
import { define } from 'forge'
|
||||||
import { H2, H3, H4, H5, Text, SmallText } from "./text"
|
import { theme } from './theme'
|
||||||
import "hono/jsx"
|
import { Section } from './section'
|
||||||
import type { JSX, FC } from "hono/jsx"
|
import { H2 } from './text'
|
||||||
import { VStack, HStack } from "./stack"
|
import { VStack, HStack } from './stack'
|
||||||
import { CodeExamples } from "./code"
|
|
||||||
import { cn } from "./cn"
|
|
||||||
|
|
||||||
export type InputProps = JSX.IntrinsicElements["input"] & {
|
export const Input = define('Input', {
|
||||||
labelPosition?: "above" | "left" | "right"
|
parts: {
|
||||||
children?: any
|
Wrapper: {
|
||||||
}
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme('spacing-1'),
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
},
|
||||||
|
Label: {
|
||||||
|
base: 'label',
|
||||||
|
fontSize: theme('fontSize-sm'),
|
||||||
|
fontWeight: 500,
|
||||||
|
color: theme('colors-fg'),
|
||||||
|
},
|
||||||
|
Field: {
|
||||||
|
base: 'input',
|
||||||
|
height: 40,
|
||||||
|
padding: `${theme('spacing-2')} ${theme('spacing-3')}`,
|
||||||
|
borderRadius: theme('radius-md'),
|
||||||
|
border: `1px solid ${theme('colors-border')}`,
|
||||||
|
background: theme('colors-bg'),
|
||||||
|
fontSize: theme('fontSize-sm'),
|
||||||
|
outline: 'none',
|
||||||
|
flex: 1,
|
||||||
|
|
||||||
export const Input: FC<InputProps> = (props) => {
|
states: {
|
||||||
const { labelPosition = "above", children, style, class: className, id, ref, ...inputProps } = props
|
':focus': {
|
||||||
|
borderColor: theme('colors-borderActive'),
|
||||||
|
},
|
||||||
|
':disabled': {
|
||||||
|
opacity: 0.5,
|
||||||
|
cursor: 'not-allowed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
const inputStyle: JSX.CSSProperties = {
|
variants: {
|
||||||
height: "40px",
|
labelPosition: {
|
||||||
padding: "8px 12px",
|
above: {
|
||||||
borderRadius: "6px",
|
parts: {
|
||||||
border: "1px solid #d1d5db",
|
Wrapper: {
|
||||||
backgroundColor: "white",
|
flexDirection: 'column',
|
||||||
fontSize: "14px",
|
gap: theme('spacing-1'),
|
||||||
outline: "none",
|
},
|
||||||
...(style as JSX.CSSProperties),
|
},
|
||||||
}
|
},
|
||||||
|
left: {
|
||||||
|
parts: {
|
||||||
|
Wrapper: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme('spacing-1'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
parts: {
|
||||||
|
Wrapper: {
|
||||||
|
flexDirection: 'row-reverse',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme('spacing-1'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
render({ props, parts: { Root, Wrapper, Label, Field } }) {
|
||||||
|
const { children, labelPosition = 'above', ...inputProps } = props
|
||||||
|
|
||||||
if (!children) {
|
if (!children) {
|
||||||
return <input style={inputStyle} {...inputProps} class={cn("Input", className)} id={id} ref={ref} />
|
return <Field {...inputProps} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const labelStyle: JSX.CSSProperties = {
|
|
||||||
fontSize: "14px",
|
|
||||||
fontWeight: "500",
|
|
||||||
color: "#111827",
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelElement = (
|
|
||||||
<label for={id} style={labelStyle}>
|
|
||||||
{children}
|
|
||||||
</label>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (labelPosition === "above") {
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "4px", flex: 1, minWidth: 0 }}>
|
<Wrapper labelPosition={labelPosition}>
|
||||||
{labelElement}
|
<Label for={props.id}>{children}</Label>
|
||||||
<input style={inputStyle} {...inputProps} class={cn("Input", className)} id={id} ref={ref} />
|
<Field {...inputProps} />
|
||||||
</div>
|
</Wrapper>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
})
|
||||||
|
|
||||||
if (labelPosition === "left") {
|
export type InputProps = Parameters<typeof Input>[0]
|
||||||
return (
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "4px", flex: 1 }}>
|
|
||||||
{labelElement}
|
|
||||||
<input style={{ ...inputStyle, flex: 1 }} {...inputProps} class={cn("Input", className)} id={id} ref={ref} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (labelPosition === "right") {
|
|
||||||
return (
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "4px", flex: 1 }}>
|
|
||||||
<input style={{ ...inputStyle, flex: 1 }} {...inputProps} class={cn("Input", className)} id={id} ref={ref} />
|
|
||||||
{labelElement}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Test = () => {
|
export const Test = () => {
|
||||||
return (
|
return (
|
||||||
<Section maxWidth="448px">
|
<Section style={{ maxWidth: '448px' }}>
|
||||||
{/* API Usage Examples */}
|
|
||||||
<CodeExamples
|
|
||||||
examples={[
|
|
||||||
'<Input placeholder="Enter name" />',
|
|
||||||
'<Input type="email" placeholder="Email" />',
|
|
||||||
'<Input>Label</Input>',
|
|
||||||
'<Input labelPosition="left">Name</Input>',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Basic inputs */}
|
{/* Basic inputs */}
|
||||||
<VStack gap={4}>
|
<VStack gap={4}>
|
||||||
<H2>Basic Inputs</H2>
|
<H2>Basic Inputs</H2>
|
||||||
|
|
@ -98,9 +108,9 @@ export const Test = () => {
|
||||||
<VStack gap={4}>
|
<VStack gap={4}>
|
||||||
<H2>Custom Styling</H2>
|
<H2>Custom Styling</H2>
|
||||||
<VStack gap={4}>
|
<VStack gap={4}>
|
||||||
<Input style={{ height: "32px", fontSize: "12px" }} placeholder="Small input" />
|
<Input style={{ height: '32px', fontSize: '12px' }} placeholder="Small input" />
|
||||||
<Input placeholder="Default input" />
|
<Input placeholder="Default input" />
|
||||||
<Input style={{ height: "48px", fontSize: "16px" }} placeholder="Large input" />
|
<Input style={{ height: '48px', fontSize: '16px' }} placeholder="Large input" />
|
||||||
</VStack>
|
</VStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
|
|
@ -117,8 +127,8 @@ export const Test = () => {
|
||||||
<VStack gap={4}>
|
<VStack gap={4}>
|
||||||
<H2>Disabled State</H2>
|
<H2>Disabled State</H2>
|
||||||
<VStack gap={4}>
|
<VStack gap={4}>
|
||||||
<Input disabled placeholder="Disabled input" style={{ opacity: 0.5, cursor: "not-allowed" }} />
|
<Input disabled placeholder="Disabled input" />
|
||||||
<Input disabled value="Disabled with value" style={{ opacity: 0.5, cursor: "not-allowed" }} />
|
<Input disabled value="Disabled with value" />
|
||||||
</VStack>
|
</VStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
|
|
@ -177,19 +187,6 @@ export const Test = () => {
|
||||||
<Input placeholder="Age">Age</Input>
|
<Input placeholder="Age">Age</Input>
|
||||||
</HStack>
|
</HStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
{/* Custom styling */}
|
|
||||||
<VStack gap={4}>
|
|
||||||
<H2>Custom Input Styling</H2>
|
|
||||||
<VStack gap={4}>
|
|
||||||
<Input style={{ borderColor: "#93c5fd" }} placeholder="Custom styled input">
|
|
||||||
<span style={{ color: "#2563eb", fontWeight: "bold" }}>Custom Label</span>
|
|
||||||
</Input>
|
|
||||||
<Input labelPosition="left" placeholder="Required input">
|
|
||||||
<span style={{ color: "#dc2626", minWidth: "96px" }}>Required Field</span>
|
|
||||||
</Input>
|
|
||||||
</VStack>
|
|
||||||
</VStack>
|
|
||||||
</Section>
|
</Section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
118
src/layout.tsx
Normal file
118
src/layout.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
import { define } from 'forge'
|
||||||
|
import { theme } from './theme'
|
||||||
|
|
||||||
|
// Dark mode toggle button
|
||||||
|
export const DarkModeToggle = define('DarkModeToggle', {
|
||||||
|
base: 'button',
|
||||||
|
position: 'fixed',
|
||||||
|
top: theme('spacing-4'),
|
||||||
|
right: theme('spacing-4'),
|
||||||
|
background: theme('colors-bgMuted'),
|
||||||
|
border: `1px solid ${theme('colors-border')}`,
|
||||||
|
borderRadius: theme('radius-full'),
|
||||||
|
padding: `${theme('spacing-2')} ${theme('spacing-4')}`,
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme('spacing-2'),
|
||||||
|
fontSize: theme('fontSize-sm'),
|
||||||
|
fontWeight: 500,
|
||||||
|
color: theme('colors-fg'),
|
||||||
|
zIndex: 1000,
|
||||||
|
|
||||||
|
states: {
|
||||||
|
':hover': {
|
||||||
|
background: theme('colors-primary'),
|
||||||
|
color: '#ffffff',
|
||||||
|
borderColor: theme('colors-primary'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Page title bar
|
||||||
|
export const PageTitle = define('PageTitle', {
|
||||||
|
position: 'relative',
|
||||||
|
fontSize: '32px',
|
||||||
|
fontWeight: 700,
|
||||||
|
padding: theme('spacing-6'),
|
||||||
|
borderBottom: `1px solid ${theme('colors-border')}`,
|
||||||
|
background: theme('colors-bg'),
|
||||||
|
color: theme('colors-fg'),
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
hasHomeLink: {
|
||||||
|
paddingLeft: '80px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Home link button
|
||||||
|
export const HomeLink = define('HomeLink', {
|
||||||
|
base: 'a',
|
||||||
|
position: 'absolute',
|
||||||
|
left: theme('spacing-6'),
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
background: theme('colors-bgMuted'),
|
||||||
|
border: `1px solid ${theme('colors-border')}`,
|
||||||
|
borderRadius: theme('radius-lg'),
|
||||||
|
padding: `${theme('spacing-2')} ${theme('spacing-3')}`,
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: theme('colors-fg'),
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme('spacing-2'),
|
||||||
|
fontSize: theme('fontSize-sm'),
|
||||||
|
fontWeight: 500,
|
||||||
|
|
||||||
|
states: {
|
||||||
|
':hover': {
|
||||||
|
background: theme('colors-primary'),
|
||||||
|
color: '#ffffff',
|
||||||
|
borderColor: theme('colors-primary'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Navigation link for component list
|
||||||
|
export const NavLink = define('NavLink', {
|
||||||
|
base: 'a',
|
||||||
|
color: theme('colors-primary'),
|
||||||
|
textDecoration: 'none',
|
||||||
|
fontSize: theme('fontSize-lg'),
|
||||||
|
lineHeight: 2,
|
||||||
|
|
||||||
|
states: {
|
||||||
|
':hover': {
|
||||||
|
textDecoration: 'underline',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Nav list container
|
||||||
|
export const NavList = define('NavList', {
|
||||||
|
base: 'ul',
|
||||||
|
padding: theme('spacing-6'),
|
||||||
|
listStyle: 'none',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Nav list item
|
||||||
|
export const NavItem = define('NavItem', {
|
||||||
|
base: 'li',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Body wrapper that handles theme background
|
||||||
|
export const Body = define('Body', {
|
||||||
|
base: 'body',
|
||||||
|
background: theme('colors-bg'),
|
||||||
|
color: theme('colors-fg'),
|
||||||
|
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Code examples container - override styles for dark mode compatibility
|
||||||
|
export const CodeExamplesWrapper = define('CodeExamplesWrapper', {
|
||||||
|
background: theme('colors-bgElevated'),
|
||||||
|
borderColor: theme('colors-border'),
|
||||||
|
})
|
||||||
|
|
@ -1,65 +1,51 @@
|
||||||
import { Section } from "./section"
|
import { Section } from './section'
|
||||||
import { H2, H3, H4, H5, Text, SmallText } from "./text"
|
import { H2, H3, Text, SmallText } from './text'
|
||||||
import "hono/jsx"
|
import { Avatar } from './avatar'
|
||||||
import { Avatar } from "./avatar"
|
import type { AvatarProps } from './avatar'
|
||||||
import type { AvatarProps } from "./avatar"
|
import { Image } from './image'
|
||||||
import { Image } from "./image"
|
import type { ImageProps } from './image'
|
||||||
import type { ImageProps } from "./image"
|
import { VStack, HStack } from './stack'
|
||||||
import { VStack, HStack } from "./stack"
|
import { Grid } from './grid'
|
||||||
import { Grid } from "./grid"
|
|
||||||
import { CodeExamples } from "./code"
|
|
||||||
|
|
||||||
export const Placeholder = {
|
export const Placeholder = {
|
||||||
Avatar(props: PlaceholderAvatarProps) {
|
Avatar(props: PlaceholderAvatarProps) {
|
||||||
const { size = 32, seed = "seed", type = "dylan", transparent, alt, style, rounded, id, ref, class: className } = props
|
const { size = 32, seed = 'seed', type = 'dylan', transparent, alt, style, rounded, id, ref, class: className } = props
|
||||||
|
|
||||||
// Generate DiceBear avatar URL
|
// Generate DiceBear avatar URL
|
||||||
const url = new URL(`https://api.dicebear.com/9.x/${type}/svg`)
|
const url = new URL(`https://api.dicebear.com/9.x/${type}/svg`)
|
||||||
url.searchParams.set("seed", seed)
|
url.searchParams.set('seed', seed)
|
||||||
url.searchParams.set("size", size.toString())
|
url.searchParams.set('size', size.toString())
|
||||||
|
|
||||||
if (transparent) {
|
if (transparent) {
|
||||||
url.searchParams.set("backgroundColor", "transparent")
|
url.searchParams.set('backgroundColor', 'transparent')
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Avatar src={url.toString()} alt={alt} style={style} size={size} rounded={rounded} id={id} ref={ref} class={className} />
|
return <Avatar src={url.toString()} alt={alt} style={style} size={size as any} rounded={rounded} id={id} ref={ref} class={className} />
|
||||||
},
|
},
|
||||||
|
|
||||||
Image(props: PlaceholderImageProps) {
|
Image(props: PlaceholderImageProps) {
|
||||||
const { width = 200, height = 200, seed = 1, alt = "Placeholder image", objectFit, style, id, ref, class: className } = props
|
const { width = 200, height = 200, seed = 1, alt = 'Placeholder image', objectFit, style, id, ref, class: className } = props
|
||||||
|
|
||||||
// Generate Picsum Photos URL with seed for consistent images
|
// Generate Picsum Photos URL with seed for consistent images
|
||||||
const src = `https://picsum.photos/${width}/${height}?random=${seed}`
|
const src = `https://picsum.photos/${width}/${height}?random=${seed}`
|
||||||
|
|
||||||
return <Image src={src} alt={alt} width={width} height={height} objectFit={objectFit} style={style} id={id} ref={ref} class={className} />
|
return <Image src={src} alt={alt} objectFit={objectFit} width={width} height={height} style={style} id={id} ref={ref} class={className} />
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Test = () => {
|
export const Test = () => {
|
||||||
return (
|
return (
|
||||||
<Section>
|
<Section>
|
||||||
{/* API Usage Examples */}
|
|
||||||
<CodeExamples
|
|
||||||
examples={[
|
|
||||||
'<Placeholder.Avatar />',
|
|
||||||
'<Placeholder.Avatar type="avataaars" size={64} />',
|
|
||||||
'<Placeholder.Image width={200} height={200} />',
|
|
||||||
'<Placeholder.Image seed={42} />',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* === AVATAR TESTS === */}
|
|
||||||
|
|
||||||
{/* Show all available avatar styles */}
|
{/* Show all available avatar styles */}
|
||||||
<VStack gap={4}>
|
<VStack gap={4}>
|
||||||
<H2>
|
<H2>
|
||||||
All Avatar Styles ({allStyles.length} total)
|
All Avatar Styles ({allStyles.length} total)
|
||||||
</H2>
|
</H2>
|
||||||
<Grid cols={10} gap={3}>
|
<Grid cols={6} gap={3}>
|
||||||
{allStyles.map((style) => (
|
{allStyles.slice(0, 12).map((style) => (
|
||||||
<VStack h="center" gap={1} key={style}>
|
<VStack h="center" gap={1} key={style}>
|
||||||
<Placeholder.Avatar type={style} size={48} />
|
<Placeholder.Avatar type={style} size={48} />
|
||||||
<SmallText style={{ fontWeight: "500" }}>{style}</SmallText>
|
<SmallText style={{ fontWeight: '500' }}>{style}</SmallText>
|
||||||
</VStack>
|
</VStack>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
@ -87,7 +73,7 @@ export const Test = () => {
|
||||||
<Text>Rounded + Background</Text>
|
<Text>Rounded + Background</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<div style={{ backgroundColor: "#e5e7eb", padding: "8px" }}>
|
<div style={{ backgroundColor: '#e5e7eb', padding: '8px' }}>
|
||||||
<Placeholder.Avatar rounded transparent size={64} />
|
<Placeholder.Avatar rounded transparent size={64} />
|
||||||
</div>
|
</div>
|
||||||
<Text>Rounded + Transparent</Text>
|
<Text>Rounded + Transparent</Text>
|
||||||
|
|
@ -97,7 +83,7 @@ export const Test = () => {
|
||||||
<Text>Square + Background</Text>
|
<Text>Square + Background</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack h="center" gap={2}>
|
<VStack h="center" gap={2}>
|
||||||
<div style={{ backgroundColor: "#e5e7eb", padding: "8px" }}>
|
<div style={{ backgroundColor: '#e5e7eb', padding: '8px' }}>
|
||||||
<Placeholder.Avatar transparent size={64} />
|
<Placeholder.Avatar transparent size={64} />
|
||||||
</div>
|
</div>
|
||||||
<Text>Square + Transparent</Text>
|
<Text>Square + Transparent</Text>
|
||||||
|
|
@ -111,7 +97,7 @@ export const Test = () => {
|
||||||
Avatar Seeds (Same Style, Different People)
|
Avatar Seeds (Same Style, Different People)
|
||||||
</H2>
|
</H2>
|
||||||
<HStack gap={4}>
|
<HStack gap={4}>
|
||||||
{["alice", "bob", "charlie", "diana"].map((seed) => (
|
{['alice', 'bob', 'charlie', 'diana'].map((seed) => (
|
||||||
<VStack h="center" gap={2} key={seed}>
|
<VStack h="center" gap={2} key={seed}>
|
||||||
<Placeholder.Avatar seed={seed} size={64} />
|
<Placeholder.Avatar seed={seed} size={64} />
|
||||||
<Text>"{seed}"</Text>
|
<Text>"{seed}"</Text>
|
||||||
|
|
@ -120,7 +106,7 @@ export const Test = () => {
|
||||||
</HStack>
|
</HStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
{/* === IMAGE TESTS === */}
|
{/* Placeholder Images */}
|
||||||
<VStack gap={6}>
|
<VStack gap={6}>
|
||||||
<H2>Placeholder Images</H2>
|
<H2>Placeholder Images</H2>
|
||||||
|
|
||||||
|
|
@ -137,7 +123,7 @@ export const Test = () => {
|
||||||
<VStack h="center" gap={2} key={`${width}x${height}`}>
|
<VStack h="center" gap={2} key={`${width}x${height}`}>
|
||||||
<Placeholder.Image width={width} height={height} seed={1} />
|
<Placeholder.Image width={width} height={height} seed={1} />
|
||||||
<Text>
|
<Text>
|
||||||
{width}×{height}
|
{width}x{height}
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
))}
|
))}
|
||||||
|
|
@ -169,7 +155,7 @@ export const Test = () => {
|
||||||
height={150}
|
height={150}
|
||||||
seed={1}
|
seed={1}
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
style={{ borderRadius: "8px", border: "4px solid #3b82f6" }}
|
style={{ borderRadius: '8px', border: '4px solid #3b82f6' }}
|
||||||
/>
|
/>
|
||||||
<Text>Rounded + Border</Text>
|
<Text>Rounded + Border</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
@ -179,7 +165,7 @@ export const Test = () => {
|
||||||
height={150}
|
height={150}
|
||||||
seed={2}
|
seed={2}
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
style={{ boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)" }}
|
style={{ boxShadow: '0 10px 15px rgba(0, 0, 0, 0.3)' }}
|
||||||
/>
|
/>
|
||||||
<Text>With Shadow</Text>
|
<Text>With Shadow</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
@ -190,9 +176,9 @@ export const Test = () => {
|
||||||
seed={3}
|
seed={3}
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: "9999px",
|
borderRadius: '9999px',
|
||||||
border: "4px solid #22c55e",
|
border: '4px solid #22c55e',
|
||||||
boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)",
|
boxShadow: '0 10px 15px rgba(0, 0, 0, 0.3)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Text>Circular + Effects</Text>
|
<Text>Circular + Effects</Text>
|
||||||
|
|
@ -205,13 +191,13 @@ export const Test = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type definitions
|
// Type definitions
|
||||||
type PlaceholderAvatarProps = Omit<AvatarProps, "src"> & {
|
type PlaceholderAvatarProps = Omit<AvatarProps, 'src'> & {
|
||||||
seed?: string
|
seed?: string
|
||||||
type?: DicebearStyleName
|
type?: DicebearStyleName
|
||||||
transparent?: boolean
|
transparent?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlaceholderImageProps = Omit<ImageProps, "src" | "alt"> & {
|
type PlaceholderImageProps = Omit<ImageProps, 'src' | 'alt'> & {
|
||||||
width?: number
|
width?: number
|
||||||
height?: number
|
height?: number
|
||||||
seed?: number
|
seed?: number
|
||||||
|
|
@ -220,36 +206,36 @@ type PlaceholderImageProps = Omit<ImageProps, "src" | "alt"> & {
|
||||||
|
|
||||||
// All supported DiceBear HTTP styleNames. Source: https://www.dicebear.com/styles
|
// All supported DiceBear HTTP styleNames. Source: https://www.dicebear.com/styles
|
||||||
const allStyles = [
|
const allStyles = [
|
||||||
"adventurer",
|
'adventurer',
|
||||||
"adventurer-neutral",
|
'adventurer-neutral',
|
||||||
"avataaars",
|
'avataaars',
|
||||||
"avataaars-neutral",
|
'avataaars-neutral',
|
||||||
"big-ears",
|
'big-ears',
|
||||||
"big-ears-neutral",
|
'big-ears-neutral',
|
||||||
"big-smile",
|
'big-smile',
|
||||||
"bottts",
|
'bottts',
|
||||||
"bottts-neutral",
|
'bottts-neutral',
|
||||||
"croodles",
|
'croodles',
|
||||||
"croodles-neutral",
|
'croodles-neutral',
|
||||||
"dylan",
|
'dylan',
|
||||||
"fun-emoji",
|
'fun-emoji',
|
||||||
"glass",
|
'glass',
|
||||||
"icons",
|
'icons',
|
||||||
"identicon",
|
'identicon',
|
||||||
"initials",
|
'initials',
|
||||||
"lorelei",
|
'lorelei',
|
||||||
"lorelei-neutral",
|
'lorelei-neutral',
|
||||||
"micah",
|
'micah',
|
||||||
"miniavs",
|
'miniavs',
|
||||||
"notionists",
|
'notionists',
|
||||||
"notionists-neutral",
|
'notionists-neutral',
|
||||||
"open-peeps",
|
'open-peeps',
|
||||||
"personas",
|
'personas',
|
||||||
"pixel-art",
|
'pixel-art',
|
||||||
"pixel-art-neutral",
|
'pixel-art-neutral',
|
||||||
"rings",
|
'rings',
|
||||||
"shapes",
|
'shapes',
|
||||||
"thumbs",
|
'thumbs',
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
type DicebearStyleName = (typeof allStyles)[number]
|
type DicebearStyleName = (typeof allStyles)[number]
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,48 @@
|
||||||
import "hono/jsx"
|
import { define } from 'forge'
|
||||||
import type { FC, PropsWithChildren, JSX } from "hono/jsx"
|
import { theme } from './theme'
|
||||||
import { VStack } from "./stack"
|
|
||||||
import type { TailwindSize } from "./types"
|
|
||||||
import { CodeExamples } from "./code"
|
|
||||||
|
|
||||||
type SectionProps = JSX.IntrinsicElements["div"] & PropsWithChildren & {
|
export const Section = define('Section', {
|
||||||
gap?: TailwindSize
|
display: 'flex',
|
||||||
maxWidth?: string
|
flexDirection: 'column',
|
||||||
}
|
padding: theme('spacing-6'),
|
||||||
|
|
||||||
export const Section: FC<SectionProps> = (props) => {
|
variants: {
|
||||||
const { children, gap = 8, maxWidth, class: className, style, id, ref, ...rest } = props
|
gap: {
|
||||||
|
0: { gap: 0 },
|
||||||
return (
|
1: { gap: theme('spacing-1') },
|
||||||
<VStack gap={gap} class={className} style={{ padding: "24px", maxWidth, ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>
|
2: { gap: theme('spacing-2') },
|
||||||
{children}
|
3: { gap: theme('spacing-3') },
|
||||||
</VStack>
|
4: { gap: theme('spacing-4') },
|
||||||
)
|
6: { gap: theme('spacing-6') },
|
||||||
}
|
8: { gap: theme('spacing-8') },
|
||||||
|
12: { gap: theme('spacing-12') },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export const Test = () => {
|
export const Test = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* API Usage Examples */}
|
|
||||||
<div style={{ margin: "24px" }}>
|
|
||||||
<CodeExamples
|
|
||||||
examples={[
|
|
||||||
'<Section>...</Section>',
|
|
||||||
'<Section gap={4}>...</Section>',
|
|
||||||
'<Section maxWidth="600px">...</Section>',
|
|
||||||
'<Section style={{ backgroundColor: "#f3f4f6" }}>...</Section>',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Section>
|
<Section>
|
||||||
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Default Section</h2>
|
<h2 style={{ fontSize: '20px', fontWeight: 'bold' }}>Default Section</h2>
|
||||||
<p>This is a section with default gap (8)</p>
|
<p>This is a section with default styling</p>
|
||||||
<p>It has padding and vertical spacing between children</p>
|
<p>It has padding and vertical spacing between children</p>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section gap={4}>
|
<Section gap={4}>
|
||||||
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Compact Section</h2>
|
<h2 style={{ fontSize: '20px', fontWeight: 'bold' }}>Compact Section</h2>
|
||||||
<p>This section has a smaller gap (4)</p>
|
<p>This section has a smaller gap (4)</p>
|
||||||
<p>Items are closer together</p>
|
<p>Items are closer together</p>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section gap={12}>
|
<Section gap={12}>
|
||||||
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Spacious Section</h2>
|
<h2 style={{ fontSize: '20px', fontWeight: 'bold' }}>Spacious Section</h2>
|
||||||
<p>This section has a larger gap (12)</p>
|
<p>This section has a larger gap (12)</p>
|
||||||
<p>Items have more breathing room</p>
|
<p>Items have more breathing room</p>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section maxWidth="600px" style={{ backgroundColor: "#f3f4f6" }}>
|
<Section style={{ backgroundColor: '#f3f4f6', maxWidth: '600px' }}>
|
||||||
<h2 style={{ fontSize: "20px", fontWeight: "bold" }}>Constrained Width Section</h2>
|
<h2 style={{ fontSize: '20px', fontWeight: 'bold' }}>Constrained Width Section</h2>
|
||||||
<p>This section has a max width of 600px and a gray background</p>
|
<p>This section has a max width of 600px and a gray background</p>
|
||||||
<p>Good for centering content on wide screens</p>
|
<p>Good for centering content on wide screens</p>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
|
||||||
256
src/select.tsx
256
src/select.tsx
|
|
@ -1,10 +1,8 @@
|
||||||
import { Section } from "./section"
|
import { define } from 'forge'
|
||||||
import { H2, H3, H4, H5, Text, SmallText } from "./text"
|
import { theme } from './theme'
|
||||||
import "hono/jsx"
|
import { Section } from './section'
|
||||||
import type { JSX, FC } from "hono/jsx"
|
import { H2 } from './text'
|
||||||
import { VStack, HStack } from "./stack"
|
import { VStack, HStack } from './stack'
|
||||||
import { CodeExamples } from "./code"
|
|
||||||
import { cn } from "./cn"
|
|
||||||
|
|
||||||
export type SelectOption = {
|
export type SelectOption = {
|
||||||
value: string
|
value: string
|
||||||
|
|
@ -12,139 +10,137 @@ export type SelectOption = {
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SelectProps = Omit<JSX.IntrinsicElements["select"], "children"> & {
|
// Custom dropdown arrow as base64 SVG
|
||||||
options: SelectOption[]
|
const dropdownArrow = `url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQgNkw4IDEwTDEyIDYiIHN0cm9rZT0iIzZCNzI4MCIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+")`
|
||||||
placeholder?: string
|
|
||||||
labelPosition?: "above" | "left" | "right"
|
|
||||||
children?: any
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Select: FC<SelectProps> = (props) => {
|
export const Select = define('Select', {
|
||||||
const { options, placeholder, labelPosition = "above", children, style, class: className, id, ref, ...selectProps } = props
|
parts: {
|
||||||
|
Wrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme('spacing-1'),
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
},
|
||||||
|
Label: {
|
||||||
|
base: 'label',
|
||||||
|
fontSize: theme('fontSize-sm'),
|
||||||
|
fontWeight: 500,
|
||||||
|
color: theme('colors-fg'),
|
||||||
|
},
|
||||||
|
Field: {
|
||||||
|
base: 'select',
|
||||||
|
height: 40,
|
||||||
|
padding: `${theme('spacing-2')} 32px ${theme('spacing-2')} ${theme('spacing-3')}`,
|
||||||
|
borderRadius: theme('radius-md'),
|
||||||
|
border: `1px solid ${theme('colors-border')}`,
|
||||||
|
background: theme('colors-bg'),
|
||||||
|
fontSize: theme('fontSize-sm'),
|
||||||
|
outline: 'none',
|
||||||
|
appearance: 'none',
|
||||||
|
backgroundImage: dropdownArrow,
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
backgroundPosition: 'right 8px center',
|
||||||
|
backgroundSize: '16px' as any,
|
||||||
|
flex: 1,
|
||||||
|
|
||||||
// If a label is provided but no id, generate a random id so the label can be clicked
|
states: {
|
||||||
const elementId = id || (children ? `random-${Math.random().toString(36)}` : undefined)
|
':focus': {
|
||||||
|
borderColor: theme('colors-borderActive'),
|
||||||
|
},
|
||||||
|
':disabled': {
|
||||||
|
opacity: 0.5,
|
||||||
|
cursor: 'not-allowed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
const selectStyle: JSX.CSSProperties = {
|
variants: {
|
||||||
height: "40px",
|
labelPosition: {
|
||||||
padding: "8px 32px 8px 12px",
|
above: {
|
||||||
borderRadius: "6px",
|
parts: {
|
||||||
border: "1px solid #d1d5db",
|
Wrapper: {
|
||||||
backgroundColor: "white",
|
flexDirection: 'column',
|
||||||
fontSize: "14px",
|
gap: theme('spacing-1'),
|
||||||
outline: "none",
|
},
|
||||||
appearance: "none",
|
},
|
||||||
backgroundImage: `url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQgNkw4IDEwTDEyIDYiIHN0cm9rZT0iIzZCNzI4MCIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+")`,
|
},
|
||||||
backgroundRepeat: "no-repeat",
|
left: {
|
||||||
backgroundPosition: "right 8px center",
|
parts: {
|
||||||
backgroundSize: "16px 16px",
|
Wrapper: {
|
||||||
...style,
|
flexDirection: 'row',
|
||||||
}
|
alignItems: 'center',
|
||||||
|
gap: theme('spacing-1'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
parts: {
|
||||||
|
Wrapper: {
|
||||||
|
flexDirection: 'row-reverse',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme('spacing-1'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
render({ props, parts: { Root, Wrapper, Label, Field } }) {
|
||||||
|
const { children, options, placeholder, labelPosition = 'above', ...selectProps } = props
|
||||||
|
|
||||||
|
// Generate id for label association if needed
|
||||||
|
const elementId = props.id || (children ? `select-${Math.random().toString(36).slice(2)}` : undefined)
|
||||||
|
|
||||||
const selectElement = (
|
const selectElement = (
|
||||||
<select style={selectStyle} {...selectProps} class={cn("Select", className)} id={elementId} ref={ref}>
|
<Field {...selectProps} id={elementId}>
|
||||||
{placeholder && (
|
{placeholder && (
|
||||||
<option value="" disabled>
|
<option value="" disabled>
|
||||||
{placeholder}
|
{placeholder}
|
||||||
</option>
|
</option>
|
||||||
)}
|
)}
|
||||||
{options.map((option) => (
|
{options?.map((option: SelectOption) => (
|
||||||
<option
|
<option
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
disabled={option.disabled}
|
disabled={option.disabled}
|
||||||
selected={selectProps.value === option.value}
|
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</Field>
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!children) {
|
if (!children) {
|
||||||
return selectElement
|
return selectElement
|
||||||
}
|
}
|
||||||
|
|
||||||
const labelStyle: JSX.CSSProperties = {
|
|
||||||
fontSize: "14px",
|
|
||||||
fontWeight: "500",
|
|
||||||
color: "#111827",
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelElement = (
|
|
||||||
<label for={elementId} style={labelStyle}>
|
|
||||||
{children}
|
|
||||||
</label>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (labelPosition === "above") {
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "4px", flex: 1, minWidth: 0 }}>
|
<Wrapper labelPosition={labelPosition}>
|
||||||
{labelElement}
|
<Label for={elementId}>{children}</Label>
|
||||||
{selectElement}
|
{selectElement}
|
||||||
</div>
|
</Wrapper>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
})
|
||||||
|
|
||||||
if (labelPosition === "left") {
|
export type SelectProps = Parameters<typeof Select>[0]
|
||||||
return (
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "4px", flex: 1 }}>
|
|
||||||
{labelElement}
|
|
||||||
<select style={{ ...selectStyle, flex: 1 }} {...selectProps} class={cn("Select", className)} id={elementId} ref={ref}>
|
|
||||||
{placeholder && (
|
|
||||||
<option value="" disabled>
|
|
||||||
{placeholder}
|
|
||||||
</option>
|
|
||||||
)}
|
|
||||||
{options.map((option) => (
|
|
||||||
<option key={option.value} value={option.value} disabled={option.disabled}>
|
|
||||||
{option.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (labelPosition === "right") {
|
|
||||||
return (
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "4px", flex: 1 }}>
|
|
||||||
<select style={{ ...selectStyle, flex: 1 }} {...selectProps} class={cn("Select", className)} id={elementId} ref={ref}>
|
|
||||||
{placeholder && (
|
|
||||||
<option value="" disabled>
|
|
||||||
{placeholder}
|
|
||||||
</option>
|
|
||||||
)}
|
|
||||||
{options.map((option) => (
|
|
||||||
<option key={option.value} value={option.value} disabled={option.disabled}>
|
|
||||||
{option.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{labelElement}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Test = () => {
|
export const Test = () => {
|
||||||
const options = [{ value: "1", label: "Option 1" }, { value: "2", label: "Option 2" }]
|
|
||||||
|
|
||||||
const months = [
|
const months = [
|
||||||
{ value: "01", label: "January" },
|
{ value: '01', label: 'January' },
|
||||||
{ value: "02", label: "February" },
|
{ value: '02', label: 'February' },
|
||||||
{ value: "03", label: "March" },
|
{ value: '03', label: 'March' },
|
||||||
{ value: "04", label: "April" },
|
{ value: '04', label: 'April' },
|
||||||
{ value: "05", label: "May" },
|
{ value: '05', label: 'May' },
|
||||||
{ value: "06", label: "June" },
|
{ value: '06', label: 'June' },
|
||||||
{ value: "07", label: "July" },
|
{ value: '07', label: 'July' },
|
||||||
{ value: "08", label: "August" },
|
{ value: '08', label: 'August' },
|
||||||
{ value: "09", label: "September" },
|
{ value: '09', label: 'September' },
|
||||||
{ value: "10", label: "October" },
|
{ value: '10', label: 'October' },
|
||||||
{ value: "11", label: "November" },
|
{ value: '11', label: 'November' },
|
||||||
{ value: "12", label: "December" },
|
{ value: '12', label: 'December' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const years = Array.from({ length: 10 }, (_, i) => ({
|
const years = Array.from({ length: 10 }, (_, i) => ({
|
||||||
|
|
@ -153,26 +149,16 @@ export const Test = () => {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const countries = [
|
const countries = [
|
||||||
{ value: "us", label: "United States" },
|
{ value: 'us', label: 'United States' },
|
||||||
{ value: "ca", label: "Canada" },
|
{ value: 'ca', label: 'Canada' },
|
||||||
{ value: "uk", label: "United Kingdom" },
|
{ value: 'uk', label: 'United Kingdom' },
|
||||||
{ value: "de", label: "Germany" },
|
{ value: 'de', label: 'Germany' },
|
||||||
{ value: "fr", label: "France" },
|
{ value: 'fr', label: 'France' },
|
||||||
{ value: "au", label: "Australia", disabled: true },
|
{ value: 'au', label: 'Australia', disabled: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section maxWidth="448px">
|
<Section style={{ maxWidth: '448px' }}>
|
||||||
{/* API Usage Examples */}
|
|
||||||
<CodeExamples
|
|
||||||
examples={[
|
|
||||||
'<Select options={options} />',
|
|
||||||
'<Select options={options} placeholder="Choose" />',
|
|
||||||
'<Select options={options}>Label</Select>',
|
|
||||||
'<Select options={options} labelPosition="left">Label</Select>',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Basic selects */}
|
{/* Basic selects */}
|
||||||
<VStack gap={4}>
|
<VStack gap={4}>
|
||||||
<H2>Basic Selects</H2>
|
<H2>Basic Selects</H2>
|
||||||
|
|
@ -196,22 +182,8 @@ export const Test = () => {
|
||||||
<VStack gap={4}>
|
<VStack gap={4}>
|
||||||
<H2>Disabled State</H2>
|
<H2>Disabled State</H2>
|
||||||
<VStack gap={4}>
|
<VStack gap={4}>
|
||||||
<Select
|
<Select options={months} disabled placeholder="Disabled select" />
|
||||||
options={months}
|
<Select options={years} disabled value="2024" />
|
||||||
disabled
|
|
||||||
placeholder="Disabled select"
|
|
||||||
style={{ opacity: 0.5, cursor: "not-allowed" }}
|
|
||||||
/>
|
|
||||||
<Select options={years} disabled value="2024" style={{ opacity: 0.5, cursor: "not-allowed" }} />
|
|
||||||
</VStack>
|
|
||||||
</VStack>
|
|
||||||
|
|
||||||
{/* Custom styling */}
|
|
||||||
<VStack gap={4}>
|
|
||||||
<H2>Custom Styling</H2>
|
|
||||||
<VStack gap={4}>
|
|
||||||
<Select options={countries} style={{ borderColor: "#93c5fd" }} placeholder="Custom styled select" />
|
|
||||||
<Select options={months} style={{ height: "32px", fontSize: "12px" }} placeholder="Small select" />
|
|
||||||
</VStack>
|
</VStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
|
|
|
||||||
332
src/stack.tsx
332
src/stack.tsx
|
|
@ -1,146 +1,123 @@
|
||||||
import type { TailwindSize } from "./types"
|
import { define } from 'forge'
|
||||||
import "hono/jsx"
|
import { theme } from './theme'
|
||||||
import type { FC, PropsWithChildren, JSX } from "hono/jsx"
|
import { H2 } from './text'
|
||||||
import { Grid } from "./grid"
|
import { RedBox, GreenBox, BlueBox } from './box'
|
||||||
import { Section } from "./section"
|
import { Grid } from './grid'
|
||||||
import { H2 } from "./text"
|
import { Section } from './section'
|
||||||
import { RedBox, GreenBox, BlueBox } from "./box"
|
|
||||||
import { CodeExamples } from "./code"
|
|
||||||
import { cn } from "./cn"
|
|
||||||
|
|
||||||
export const VStack: FC<VStackProps> = (props) => {
|
export const VStack = define('VStack', {
|
||||||
const { v, h, wrap, gap, maxWidth, rows, class: className, style, id, ref, children, ...rest } = props
|
display: 'flex',
|
||||||
return (
|
flexDirection: 'column',
|
||||||
<Stack
|
|
||||||
direction="col"
|
|
||||||
mainAxis={v}
|
|
||||||
crossAxis={h}
|
|
||||||
wrap={wrap}
|
|
||||||
gap={gap}
|
|
||||||
maxWidth={maxWidth}
|
|
||||||
gridSizes={rows}
|
|
||||||
componentName="VStack"
|
|
||||||
class={className}
|
|
||||||
style={style}
|
|
||||||
id={id}
|
|
||||||
ref={ref}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HStack: FC<HStackProps> = (props) => {
|
variants: {
|
||||||
const { h, v, wrap, gap, maxWidth, cols, class: className, style, id, ref, children, ...rest } = props
|
gap: {
|
||||||
return (
|
0: { gap: 0 },
|
||||||
<Stack
|
1: { gap: theme('spacing-1') },
|
||||||
direction="row"
|
2: { gap: theme('spacing-2') },
|
||||||
mainAxis={h}
|
3: { gap: theme('spacing-3') },
|
||||||
crossAxis={v}
|
4: { gap: theme('spacing-4') },
|
||||||
wrap={wrap}
|
6: { gap: theme('spacing-6') },
|
||||||
gap={gap}
|
8: { gap: theme('spacing-8') },
|
||||||
maxWidth={maxWidth}
|
12: { gap: theme('spacing-12') },
|
||||||
gridSizes={cols}
|
},
|
||||||
componentName="HStack"
|
v: {
|
||||||
class={className}
|
start: { justifyContent: 'flex-start' },
|
||||||
style={style}
|
center: { justifyContent: 'center' },
|
||||||
id={id}
|
end: { justifyContent: 'flex-end' },
|
||||||
ref={ref}
|
between: { justifyContent: 'space-between' },
|
||||||
{...rest}
|
around: { justifyContent: 'space-around' },
|
||||||
>
|
evenly: { justifyContent: 'space-evenly' },
|
||||||
{children}
|
},
|
||||||
</Stack>
|
h: {
|
||||||
)
|
start: { alignItems: 'flex-start' },
|
||||||
}
|
center: { alignItems: 'center' },
|
||||||
|
end: { alignItems: 'flex-end' },
|
||||||
|
stretch: { alignItems: 'stretch' },
|
||||||
|
baseline: { alignItems: 'baseline' },
|
||||||
|
},
|
||||||
|
wrap: {
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
const Stack: FC<StackProps> = (props) => {
|
render({ props, parts: { Root } }) {
|
||||||
const { direction, mainAxis, crossAxis, wrap, gap, maxWidth, gridSizes, componentName, class: className, style, id, ref, children, ...rest } = props
|
const { rows, style, ...rest } = props
|
||||||
const gapPx = gap ? gap * 4 : 0
|
const gridStyle = rows
|
||||||
|
? { display: 'grid', gridTemplateRows: rows.map((r: number) => `${r}fr`).join(' '), ...style }
|
||||||
|
: style
|
||||||
|
return <Root style={gridStyle} {...rest}>{props.children}</Root>
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// Use CSS Grid when gridSizes (cols/rows) is provided
|
export const HStack = define('HStack', {
|
||||||
if (gridSizes) {
|
display: 'flex',
|
||||||
const gridTemplate = gridSizes.map(size => `${size}fr`).join(" ")
|
flexDirection: 'row',
|
||||||
|
|
||||||
const gridStyles: JSX.CSSProperties = {
|
variants: {
|
||||||
display: "grid",
|
gap: {
|
||||||
gap: `${gapPx}px`,
|
0: { gap: 0 },
|
||||||
maxWidth: maxWidth,
|
1: { gap: theme('spacing-1') },
|
||||||
}
|
2: { gap: theme('spacing-2') },
|
||||||
|
3: { gap: theme('spacing-3') },
|
||||||
|
4: { gap: theme('spacing-4') },
|
||||||
|
6: { gap: theme('spacing-6') },
|
||||||
|
8: { gap: theme('spacing-8') },
|
||||||
|
12: { gap: theme('spacing-12') },
|
||||||
|
},
|
||||||
|
h: {
|
||||||
|
start: { justifyContent: 'flex-start' },
|
||||||
|
center: { justifyContent: 'center' },
|
||||||
|
end: { justifyContent: 'flex-end' },
|
||||||
|
between: { justifyContent: 'space-between' },
|
||||||
|
around: { justifyContent: 'space-around' },
|
||||||
|
evenly: { justifyContent: 'space-evenly' },
|
||||||
|
},
|
||||||
|
v: {
|
||||||
|
start: { alignItems: 'flex-start' },
|
||||||
|
center: { alignItems: 'center' },
|
||||||
|
end: { alignItems: 'flex-end' },
|
||||||
|
stretch: { alignItems: 'stretch' },
|
||||||
|
baseline: { alignItems: 'baseline' },
|
||||||
|
},
|
||||||
|
wrap: {
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
if (direction === "row") {
|
render({ props, parts: { Root } }) {
|
||||||
gridStyles.gridTemplateColumns = gridTemplate
|
const { cols, style, ...rest } = props
|
||||||
} else {
|
const gridStyle = cols
|
||||||
gridStyles.gridTemplateRows = gridTemplate
|
? { display: 'grid', gridTemplateColumns: cols.map((c: number) => `${c}fr`).join(' '), ...style }
|
||||||
}
|
: style
|
||||||
|
return <Root style={gridStyle} {...rest}>{props.children}</Root>
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const combinedStyles = {
|
type MainAxisOpts = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'
|
||||||
...gridStyles,
|
type CrossAxisOpts = 'start' | 'center' | 'end' | 'stretch' | 'baseline'
|
||||||
...(style as JSX.CSSProperties),
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div class={cn(componentName, className)} style={combinedStyles} id={id} ref={ref} {...rest}>{children}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default flexbox behavior
|
|
||||||
const baseStyles: JSX.CSSProperties = {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: direction === "row" ? "row" : "column",
|
|
||||||
flexWrap: wrap ? "wrap" : "nowrap",
|
|
||||||
gap: `${gapPx}px`,
|
|
||||||
maxWidth: maxWidth,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mainAxis) {
|
|
||||||
baseStyles.justifyContent = getJustifyContent(mainAxis)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (crossAxis) {
|
|
||||||
baseStyles.alignItems = getAlignItems(crossAxis)
|
|
||||||
}
|
|
||||||
|
|
||||||
const combinedStyles = {
|
|
||||||
...baseStyles,
|
|
||||||
...(style as JSX.CSSProperties),
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div class={cn(componentName, className)} style={combinedStyles} id={id} ref={ref} {...rest}>{children}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Test = () => {
|
export const Test = () => {
|
||||||
const mainAxisOpts: MainAxisOpts[] = ["start", "center", "end", "between", "around", "evenly"]
|
const mainAxisOpts: MainAxisOpts[] = ['start', 'center', 'end', 'between', 'around', 'evenly']
|
||||||
const crossAxisOpts: CrossAxisOpts[] = ["start", "center", "end", "stretch", "baseline"]
|
const crossAxisOpts: CrossAxisOpts[] = ['start', 'center', 'end', 'stretch', 'baseline']
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section gap={8} style={{ padding: "16px" }}>
|
<Section gap={8} style={{ padding: '16px' }}>
|
||||||
{/* API Usage Examples */}
|
|
||||||
<CodeExamples
|
|
||||||
examples={[
|
|
||||||
'<VStack>...</VStack>',
|
|
||||||
'<VStack gap={4} v="center">...</VStack>',
|
|
||||||
'<HStack>...</HStack>',
|
|
||||||
'<HStack gap={6} h="between" v="center">...</HStack>',
|
|
||||||
'<HStack cols={[7, 3]} maxWidth="1200px" gap={4}>...</HStack>',
|
|
||||||
'<VStack rows={[2, 1]} maxWidth="800px">...</VStack>',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* HStack layout matrix */}
|
{/* HStack layout matrix */}
|
||||||
<VStack gap={2}>
|
<VStack gap={2}>
|
||||||
<H2>HStack Layout</H2>
|
<H2>HStack Layout</H2>
|
||||||
<div style={{ overflowX: "auto" }}>
|
<div style={{ overflowX: 'auto' }}>
|
||||||
<Grid cols={7} gap={1} style={{ gridTemplateColumns: "auto repeat(6, 1fr)" }}>
|
<Grid cols={7} gap={1} style={{ gridTemplateColumns: 'auto repeat(6, 1fr)' }}>
|
||||||
{/* Header row: blank + h labels */}
|
{/* Header row: blank + h labels */}
|
||||||
<div></div>
|
<div></div>
|
||||||
{mainAxisOpts.map((h) => (
|
{mainAxisOpts.map((h) => (
|
||||||
<div key={h} style={{ fontSize: "14px", fontWeight: "500", textAlign: "center" }}>
|
<div key={h} style={{ fontSize: '14px', fontWeight: '500', textAlign: 'center' }}>
|
||||||
h: {h}
|
h: {h}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Each row: v label + HStack cells */}
|
{/* Each row: v label + HStack cells */}
|
||||||
{crossAxisOpts.map((v) => [
|
{crossAxisOpts.map((v) => [
|
||||||
<div key={v} style={{ fontSize: "14px", fontWeight: "500" }}>
|
<div key={v} style={{ fontSize: '14px', fontWeight: '500' }}>
|
||||||
v: {v}
|
v: {v}
|
||||||
</div>,
|
</div>,
|
||||||
...mainAxisOpts.map((h) => (
|
...mainAxisOpts.map((h) => (
|
||||||
|
|
@ -148,7 +125,7 @@ export const Test = () => {
|
||||||
key={`${h}-${v}`}
|
key={`${h}-${v}`}
|
||||||
h={h}
|
h={h}
|
||||||
v={v}
|
v={v}
|
||||||
style={{ backgroundColor: "#f3f4f6", padding: "8px", height: "96px", border: "1px solid #9ca3af" }}
|
style={{ backgroundColor: '#f3f4f6', padding: '8px', height: '96px', border: '1px solid #9ca3af' }}
|
||||||
>
|
>
|
||||||
<RedBox>Aa</RedBox>
|
<RedBox>Aa</RedBox>
|
||||||
<GreenBox>Aa</GreenBox>
|
<GreenBox>Aa</GreenBox>
|
||||||
|
|
@ -163,19 +140,19 @@ export const Test = () => {
|
||||||
{/* VStack layout matrix */}
|
{/* VStack layout matrix */}
|
||||||
<VStack gap={2}>
|
<VStack gap={2}>
|
||||||
<H2>VStack Layout</H2>
|
<H2>VStack Layout</H2>
|
||||||
<div style={{ overflowX: "auto" }}>
|
<div style={{ overflowX: 'auto' }}>
|
||||||
<Grid cols={6} gap={1} style={{ gridTemplateColumns: "auto repeat(5, 1fr)" }}>
|
<Grid cols={6} gap={1} style={{ gridTemplateColumns: 'auto repeat(5, 1fr)' }}>
|
||||||
{/* Header row: blank + h labels */}
|
{/* Header row: blank + h labels */}
|
||||||
<div></div>
|
<div></div>
|
||||||
{crossAxisOpts.map((h) => (
|
{crossAxisOpts.map((h) => (
|
||||||
<div key={h} style={{ fontSize: "14px", fontWeight: "500", textAlign: "center" }}>
|
<div key={h} style={{ fontSize: '14px', fontWeight: '500', textAlign: 'center' }}>
|
||||||
h: {h}
|
h: {h}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Each row: v label + VStack cells */}
|
{/* Each row: v label + VStack cells */}
|
||||||
{mainAxisOpts.map((v) => [
|
{mainAxisOpts.map((v) => [
|
||||||
<div key={v} style={{ fontSize: "14px", fontWeight: "500" }}>
|
<div key={v} style={{ fontSize: '14px', fontWeight: '500' }}>
|
||||||
v: {v}
|
v: {v}
|
||||||
</div>,
|
</div>,
|
||||||
...crossAxisOpts.map((h) => (
|
...crossAxisOpts.map((h) => (
|
||||||
|
|
@ -183,7 +160,7 @@ export const Test = () => {
|
||||||
key={`${h}-${v}`}
|
key={`${h}-${v}`}
|
||||||
v={v}
|
v={v}
|
||||||
h={h}
|
h={h}
|
||||||
style={{ backgroundColor: "#f3f4f6", padding: "8px", height: "168px", border: "1px solid #9ca3af" }}
|
style={{ backgroundColor: '#f3f4f6', padding: '8px', height: '168px', border: '1px solid #9ca3af' }}
|
||||||
>
|
>
|
||||||
<RedBox>Aa</RedBox>
|
<RedBox>Aa</RedBox>
|
||||||
<GreenBox>Aa</GreenBox>
|
<GreenBox>Aa</GreenBox>
|
||||||
|
|
@ -200,59 +177,59 @@ export const Test = () => {
|
||||||
<H2>HStack with Custom Column Sizing</H2>
|
<H2>HStack with Custom Column Sizing</H2>
|
||||||
<VStack gap={4}>
|
<VStack gap={4}>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: "14px", fontWeight: "500", marginBottom: "8px" }}>
|
<div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
|
||||||
cols=[7, 3] (70%/30% split)
|
cols=[7, 3] (70%/30% split)
|
||||||
</div>
|
</div>
|
||||||
<HStack cols={[7, 3]} gap={4} style={{ backgroundColor: "#f3f4f6", padding: "16px" }}>
|
<HStack gap={4} cols={[7, 3]} style={{ backgroundColor: '#f3f4f6', padding: '16px' }}>
|
||||||
<div style={{ backgroundColor: "#bfdbfe", padding: "16px", textAlign: "center" }}>
|
<div style={{ backgroundColor: '#bfdbfe', padding: '16px', textAlign: 'center' }}>
|
||||||
70% width
|
70% width
|
||||||
</div>
|
</div>
|
||||||
<div style={{ backgroundColor: "#fecaca", padding: "16px", textAlign: "center" }}>
|
<div style={{ backgroundColor: '#fecaca', padding: '16px', textAlign: 'center' }}>
|
||||||
30% width
|
30% width
|
||||||
</div>
|
</div>
|
||||||
</HStack>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: "14px", fontWeight: "500", marginBottom: "8px" }}>
|
<div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
|
||||||
cols=[2, 1] (66%/33% split)
|
cols=[2, 1] (66%/33% split)
|
||||||
</div>
|
</div>
|
||||||
<HStack cols={[2, 1]} gap={4} style={{ backgroundColor: "#f3f4f6", padding: "16px" }}>
|
<HStack gap={4} cols={[2, 1]} style={{ backgroundColor: '#f3f4f6', padding: '16px' }}>
|
||||||
<div style={{ backgroundColor: "#bbf7d0", padding: "16px", textAlign: "center" }}>
|
<div style={{ backgroundColor: '#bbf7d0', padding: '16px', textAlign: 'center' }}>
|
||||||
2/3 width
|
2/3 width
|
||||||
</div>
|
</div>
|
||||||
<div style={{ backgroundColor: "#fef08a", padding: "16px", textAlign: "center" }}>
|
<div style={{ backgroundColor: '#fef08a', padding: '16px', textAlign: 'center' }}>
|
||||||
1/3 width
|
1/3 width
|
||||||
</div>
|
</div>
|
||||||
</HStack>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: "14px", fontWeight: "500", marginBottom: "8px" }}>
|
<div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
|
||||||
cols=[1, 2, 1] (25%/50%/25% split)
|
cols=[1, 2, 1] (25%/50%/25% split)
|
||||||
</div>
|
</div>
|
||||||
<HStack cols={[1, 2, 1]} gap={4} style={{ backgroundColor: "#f3f4f6", padding: "16px" }}>
|
<HStack gap={4} cols={[1, 2, 1]} style={{ backgroundColor: '#f3f4f6', padding: '16px' }}>
|
||||||
<div style={{ backgroundColor: "#e9d5ff", padding: "16px", textAlign: "center" }}>
|
<div style={{ backgroundColor: '#e9d5ff', padding: '16px', textAlign: 'center' }}>
|
||||||
25%
|
25%
|
||||||
</div>
|
</div>
|
||||||
<div style={{ backgroundColor: "#bfdbfe", padding: "16px", textAlign: "center" }}>
|
<div style={{ backgroundColor: '#bfdbfe', padding: '16px', textAlign: 'center' }}>
|
||||||
50%
|
50%
|
||||||
</div>
|
</div>
|
||||||
<div style={{ backgroundColor: "#fbcfe8", padding: "16px", textAlign: "center" }}>
|
<div style={{ backgroundColor: '#fbcfe8', padding: '16px', textAlign: 'center' }}>
|
||||||
25%
|
25%
|
||||||
</div>
|
</div>
|
||||||
</HStack>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: "14px", fontWeight: "500", marginBottom: "8px" }}>
|
<div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
|
||||||
With maxWidth="600px"
|
cols=[7, 3] with maxWidth: 600px
|
||||||
</div>
|
</div>
|
||||||
<HStack cols={[7, 3]} maxWidth="600px" gap={4} style={{ backgroundColor: "#f3f4f6", padding: "16px" }}>
|
<HStack gap={4} cols={[7, 3]} style={{ maxWidth: '600px', backgroundColor: '#f3f4f6', padding: '16px' }}>
|
||||||
<div style={{ backgroundColor: "#bfdbfe", padding: "16px", textAlign: "center" }}>
|
<div style={{ backgroundColor: '#bfdbfe', padding: '16px', textAlign: 'center' }}>
|
||||||
70% of max 600px
|
70% of max 600px
|
||||||
</div>
|
</div>
|
||||||
<div style={{ backgroundColor: "#fecaca", padding: "16px", textAlign: "center" }}>
|
<div style={{ backgroundColor: '#fecaca', padding: '16px', textAlign: 'center' }}>
|
||||||
30% of max 600px
|
30% of max 600px
|
||||||
</div>
|
</div>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
@ -265,18 +242,18 @@ export const Test = () => {
|
||||||
<H2>VStack with Custom Row Sizing</H2>
|
<H2>VStack with Custom Row Sizing</H2>
|
||||||
<VStack gap={4}>
|
<VStack gap={4}>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: "14px", fontWeight: "500", marginBottom: "8px" }}>
|
<div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
|
||||||
rows=[2, 1] (2/3 and 1/3 height)
|
rows=[2, 1] (2/3 and 1/3 height)
|
||||||
</div>
|
</div>
|
||||||
<VStack
|
<VStack
|
||||||
rows={[2, 1]}
|
|
||||||
gap={4}
|
gap={4}
|
||||||
style={{ backgroundColor: "#f3f4f6", padding: "16px", height: "300px" }}
|
rows={[2, 1]}
|
||||||
|
style={{ backgroundColor: '#f3f4f6', padding: '16px', height: '300px' }}
|
||||||
>
|
>
|
||||||
<div style={{ backgroundColor: "#bfdbfe", padding: "16px", textAlign: "center" }}>
|
<div style={{ backgroundColor: '#bfdbfe', padding: '16px', textAlign: 'center' }}>
|
||||||
2/3 height
|
2/3 height
|
||||||
</div>
|
</div>
|
||||||
<div style={{ backgroundColor: "#fecaca", padding: "16px", textAlign: "center" }}>
|
<div style={{ backgroundColor: '#fecaca', padding: '16px', textAlign: 'center' }}>
|
||||||
1/3 height
|
1/3 height
|
||||||
</div>
|
</div>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
@ -286,60 +263,3 @@ export const Test = () => {
|
||||||
</Section>
|
</Section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type StackDirection = "row" | "col"
|
|
||||||
|
|
||||||
type StackProps = JSX.IntrinsicElements["div"] & {
|
|
||||||
direction: StackDirection
|
|
||||||
mainAxis?: string
|
|
||||||
crossAxis?: string
|
|
||||||
wrap?: boolean
|
|
||||||
gap?: TailwindSize
|
|
||||||
maxWidth?: string
|
|
||||||
gridSizes?: number[] // cols for row, rows for col
|
|
||||||
componentName?: string // for data-howl attribute
|
|
||||||
}
|
|
||||||
|
|
||||||
type MainAxisOpts = "start" | "center" | "end" | "between" | "around" | "evenly"
|
|
||||||
type CrossAxisOpts = "start" | "center" | "end" | "stretch" | "baseline"
|
|
||||||
|
|
||||||
type CommonStackProps = JSX.IntrinsicElements["div"] & PropsWithChildren & {
|
|
||||||
wrap?: boolean
|
|
||||||
gap?: TailwindSize
|
|
||||||
maxWidth?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type VStackProps = CommonStackProps & {
|
|
||||||
v?: MainAxisOpts // main axis for vertical stack
|
|
||||||
h?: CrossAxisOpts // cross axis for vertical stack
|
|
||||||
rows?: number[] // custom row sizing (e.g., [7, 3] for 70%/30%)
|
|
||||||
}
|
|
||||||
|
|
||||||
type HStackProps = CommonStackProps & {
|
|
||||||
h?: MainAxisOpts // main axis for horizontal stack
|
|
||||||
v?: CrossAxisOpts // cross axis for horizontal stack
|
|
||||||
cols?: number[] // custom column sizing (e.g., [7, 3] for 70%/30%)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getJustifyContent(axis: string): string {
|
|
||||||
const map: Record<string, string> = {
|
|
||||||
start: "flex-start",
|
|
||||||
center: "center",
|
|
||||||
end: "flex-end",
|
|
||||||
between: "space-between",
|
|
||||||
around: "space-around",
|
|
||||||
evenly: "space-evenly",
|
|
||||||
}
|
|
||||||
return map[axis] || "flex-start"
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAlignItems(axis: string): string {
|
|
||||||
const map: Record<string, string> = {
|
|
||||||
start: "flex-start",
|
|
||||||
center: "center",
|
|
||||||
end: "flex-end",
|
|
||||||
stretch: "stretch",
|
|
||||||
baseline: "baseline",
|
|
||||||
}
|
|
||||||
return map[axis] || "stretch"
|
|
||||||
}
|
|
||||||
|
|
|
||||||
100
src/text.tsx
100
src/text.tsx
|
|
@ -1,57 +1,57 @@
|
||||||
import "hono/jsx"
|
import { define } from 'forge'
|
||||||
import type { FC, PropsWithChildren, JSX } from "hono/jsx"
|
import { theme } from './theme'
|
||||||
import { CodeExamples } from "./code"
|
|
||||||
import { cn } from "./cn"
|
|
||||||
|
|
||||||
export const H1: FC<JSX.IntrinsicElements["h1"]> = (props) => {
|
export const H1 = define('H1', {
|
||||||
const { children, class: className, style, id, ref, ...rest } = props
|
base: 'h1',
|
||||||
return <h1 class={cn("H1", className)} style={{ fontSize: "24px", fontWeight: "bold", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</h1>
|
fontSize: theme('fontSize-2xl'),
|
||||||
}
|
fontWeight: 700,
|
||||||
|
color: theme('colors-fg'),
|
||||||
|
})
|
||||||
|
|
||||||
export const H2: FC<JSX.IntrinsicElements["h2"]> = (props) => {
|
export const H2 = define('H2', {
|
||||||
const { children, class: className, style, id, ref, ...rest } = props
|
base: 'h2',
|
||||||
return <h2 class={cn("H2", className)} style={{ fontSize: "20px", fontWeight: "bold", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</h2>
|
fontSize: theme('fontSize-xl'),
|
||||||
}
|
fontWeight: 700,
|
||||||
|
color: theme('colors-fg'),
|
||||||
|
})
|
||||||
|
|
||||||
export const H3: FC<JSX.IntrinsicElements["h3"]> = (props) => {
|
export const H3 = define('H3', {
|
||||||
const { children, class: className, style, id, ref, ...rest } = props
|
base: 'h3',
|
||||||
return <h3 class={cn("H3", className)} style={{ fontSize: "18px", fontWeight: "600", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</h3>
|
fontSize: theme('fontSize-lg'),
|
||||||
}
|
fontWeight: 600,
|
||||||
|
color: theme('colors-fg'),
|
||||||
|
})
|
||||||
|
|
||||||
export const H4: FC<JSX.IntrinsicElements["h4"]> = (props) => {
|
export const H4 = define('H4', {
|
||||||
const { children, class: className, style, id, ref, ...rest } = props
|
base: 'h4',
|
||||||
return <h4 class={cn("H4", className)} style={{ fontSize: "16px", fontWeight: "600", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</h4>
|
fontSize: theme('fontSize-base'),
|
||||||
}
|
fontWeight: 600,
|
||||||
|
color: theme('colors-fg'),
|
||||||
|
})
|
||||||
|
|
||||||
export const H5: FC<JSX.IntrinsicElements["h5"]> = (props) => {
|
export const H5 = define('H5', {
|
||||||
const { children, class: className, style, id, ref, ...rest } = props
|
base: 'h5',
|
||||||
return <h5 class={cn("H5", className)} style={{ fontSize: "14px", fontWeight: "500", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</h5>
|
fontSize: theme('fontSize-sm'),
|
||||||
}
|
fontWeight: 500,
|
||||||
|
color: theme('colors-fg'),
|
||||||
|
})
|
||||||
|
|
||||||
export const Text: FC<JSX.IntrinsicElements["p"]> = (props) => {
|
export const Text = define('Text', {
|
||||||
const { children, class: className, style, id, ref, ...rest } = props
|
base: 'p',
|
||||||
return <p class={cn("Text", className)} style={{ fontSize: "14px", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</p>
|
fontSize: theme('fontSize-sm'),
|
||||||
}
|
color: theme('colors-fg'),
|
||||||
|
})
|
||||||
|
|
||||||
export const SmallText: FC<JSX.IntrinsicElements["p"]> = (props) => {
|
export const SmallText = define('SmallText', {
|
||||||
const { children, class: className, style, id, ref, ...rest } = props
|
base: 'p',
|
||||||
return <p class={cn("SmallText", className)} style={{ fontSize: "12px", ...(style as JSX.CSSProperties) }} id={id} ref={ref} {...rest}>{children}</p>
|
fontSize: theme('fontSize-xs'),
|
||||||
}
|
color: theme('colors-fgMuted'),
|
||||||
|
})
|
||||||
|
|
||||||
export const Test = () => {
|
export const Test = () => {
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: "24px", display: "flex", flexDirection: "column", gap: "32px" }}>
|
<div style={{ padding: '24px', display: 'flex', flexDirection: 'column', gap: '32px' }}>
|
||||||
{/* API Usage Examples */}
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||||
<CodeExamples
|
|
||||||
examples={[
|
|
||||||
'<H1>Heading 1</H1>',
|
|
||||||
'<H2>Heading 2</H2>',
|
|
||||||
'<Text>Regular text</Text>',
|
|
||||||
'<SmallText>Small text</SmallText>',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
||||||
<H1>Heading 1 (24px, bold)</H1>
|
<H1>Heading 1 (24px, bold)</H1>
|
||||||
<H2>Heading 2 (20px, bold)</H2>
|
<H2>Heading 2 (20px, bold)</H2>
|
||||||
<H3>Heading 3 (18px, semibold)</H3>
|
<H3>Heading 3 (18px, semibold)</H3>
|
||||||
|
|
@ -61,16 +61,16 @@ export const Test = () => {
|
||||||
<SmallText>Small text (12px)</SmallText>
|
<SmallText>Small text (12px)</SmallText>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
<H2>Custom Styling</H2>
|
<H2>Custom Styling</H2>
|
||||||
<Text style={{ color: "#3b82f6" }}>Blue text with custom color</Text>
|
<Text style={{ color: '#3b82f6' }}>Blue text with custom color</Text>
|
||||||
<Text style={{ fontWeight: "bold", fontStyle: "italic" }}>Bold italic text</Text>
|
<Text style={{ fontWeight: 'bold', fontStyle: 'italic' }}>Bold italic text</Text>
|
||||||
<SmallText style={{ color: "#ef4444", textTransform: "uppercase" }}>
|
<SmallText style={{ color: '#ef4444', textTransform: 'uppercase' }}>
|
||||||
Red uppercase small text
|
Red uppercase small text
|
||||||
</SmallText>
|
</SmallText>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px", maxWidth: "600px" }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', maxWidth: '600px' }}>
|
||||||
<H2>Typography Example</H2>
|
<H2>Typography Example</H2>
|
||||||
<H3>Article Title</H3>
|
<H3>Article Title</H3>
|
||||||
<Text>
|
<Text>
|
||||||
|
|
@ -81,7 +81,7 @@ export const Test = () => {
|
||||||
Multiple paragraphs can be stacked together to create readable content with consistent styling
|
Multiple paragraphs can be stacked together to create readable content with consistent styling
|
||||||
throughout your application.
|
throughout your application.
|
||||||
</Text>
|
</Text>
|
||||||
<SmallText style={{ color: "#64748b" }}>Last updated: Today</SmallText>
|
<SmallText style={{ color: '#64748b' }}>Last updated: Today</SmallText>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
98
src/theme.ts
Normal file
98
src/theme.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { createThemes } from 'forge'
|
||||||
|
|
||||||
|
const lightTheme = {
|
||||||
|
// Colors
|
||||||
|
'colors-bg': '#ffffff',
|
||||||
|
'colors-bgElevated': '#f9fafb',
|
||||||
|
'colors-bgMuted': '#f3f4f6',
|
||||||
|
'colors-fg': '#111827',
|
||||||
|
'colors-fgMuted': '#6b7280',
|
||||||
|
'colors-fgDim': '#9ca3af',
|
||||||
|
'colors-border': '#d1d5db',
|
||||||
|
'colors-borderActive': '#3b82f6',
|
||||||
|
|
||||||
|
// Button colors
|
||||||
|
'colors-primary': '#3b82f6',
|
||||||
|
'colors-primaryHover': '#2563eb',
|
||||||
|
'colors-secondary': '#64748b',
|
||||||
|
'colors-secondaryHover': '#475569',
|
||||||
|
'colors-destructive': '#ef4444',
|
||||||
|
'colors-destructiveHover': '#dc2626',
|
||||||
|
|
||||||
|
// Accent colors
|
||||||
|
'colors-success': '#22c55e',
|
||||||
|
'colors-error': '#ef4444',
|
||||||
|
'colors-info': '#3b82f6',
|
||||||
|
|
||||||
|
// Box colors
|
||||||
|
'colors-red': '#ef4444',
|
||||||
|
'colors-green': '#22c55e',
|
||||||
|
'colors-blue': '#3b82f6',
|
||||||
|
'colors-gray': '#6b7280',
|
||||||
|
|
||||||
|
// Syntax highlighting colors
|
||||||
|
'syntax-tag': '#0ea5e9',
|
||||||
|
'syntax-attr': '#8b5cf6',
|
||||||
|
'syntax-string': '#10b981',
|
||||||
|
'syntax-number': '#f59e0b',
|
||||||
|
'syntax-brace': '#ef4444',
|
||||||
|
'syntax-text': '#374151',
|
||||||
|
|
||||||
|
// Spacing (TailwindSize * 4)
|
||||||
|
'spacing-0': '0px',
|
||||||
|
'spacing-1': '4px',
|
||||||
|
'spacing-2': '8px',
|
||||||
|
'spacing-3': '12px',
|
||||||
|
'spacing-4': '16px',
|
||||||
|
'spacing-5': '20px',
|
||||||
|
'spacing-6': '24px',
|
||||||
|
'spacing-8': '32px',
|
||||||
|
'spacing-10': '40px',
|
||||||
|
'spacing-12': '48px',
|
||||||
|
'spacing-16': '64px',
|
||||||
|
|
||||||
|
// Typography
|
||||||
|
'fontSize-xs': '12px',
|
||||||
|
'fontSize-sm': '14px',
|
||||||
|
'fontSize-base': '16px',
|
||||||
|
'fontSize-lg': '18px',
|
||||||
|
'fontSize-xl': '20px',
|
||||||
|
'fontSize-2xl': '24px',
|
||||||
|
|
||||||
|
// Font weights
|
||||||
|
'fontWeight-normal': '400',
|
||||||
|
'fontWeight-medium': '500',
|
||||||
|
'fontWeight-semibold': '600',
|
||||||
|
'fontWeight-bold': '700',
|
||||||
|
|
||||||
|
// Radii
|
||||||
|
'radius-sm': '4px',
|
||||||
|
'radius-md': '6px',
|
||||||
|
'radius-lg': '8px',
|
||||||
|
'radius-full': '9999px',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const darkTheme = {
|
||||||
|
...lightTheme,
|
||||||
|
'colors-bg': '#0a0a0a',
|
||||||
|
'colors-bgElevated': '#111111',
|
||||||
|
'colors-bgMuted': '#1a1a1a',
|
||||||
|
'colors-fg': '#ffffff',
|
||||||
|
'colors-fgMuted': '#a1a1a1',
|
||||||
|
'colors-fgDim': '#6b7280',
|
||||||
|
'colors-border': '#333333',
|
||||||
|
'colors-borderActive': '#60a5fa',
|
||||||
|
|
||||||
|
// Dark mode syntax highlighting
|
||||||
|
'syntax-tag': '#bfdbfe',
|
||||||
|
'syntax-attr': '#e9d5ff',
|
||||||
|
'syntax-string': '#a7f3d0',
|
||||||
|
'syntax-number': '#fde68a',
|
||||||
|
'syntax-brace': '#fecaca',
|
||||||
|
'syntax-text': '#f9fafb',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const theme = createThemes({
|
||||||
|
light: lightTheme,
|
||||||
|
dark: darkTheme,
|
||||||
|
})
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
import "hono/jsx"
|
|
||||||
import type { JSX } from "hono/jsx"
|
|
||||||
|
|
||||||
export type TailwindSize = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96
|
export type TailwindSize = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -9,6 +6,6 @@ export type TailwindSize = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7
|
||||||
export type CommonHTMLProps = {
|
export type CommonHTMLProps = {
|
||||||
class?: string
|
class?: string
|
||||||
id?: string
|
id?: string
|
||||||
style?: JSX.CSSProperties
|
style?: Record<string, unknown>
|
||||||
ref?: any
|
ref?: any
|
||||||
}
|
}
|
||||||
|
|
|
||||||
208
test/layout.tsx
208
test/layout.tsx
|
|
@ -1,5 +1,15 @@
|
||||||
import "hono/jsx"
|
import "hono/jsx"
|
||||||
import { raw } from "hono/html"
|
import { raw } from "hono/html"
|
||||||
|
import { Styles } from "forge"
|
||||||
|
import {
|
||||||
|
DarkModeToggle,
|
||||||
|
PageTitle,
|
||||||
|
HomeLink,
|
||||||
|
NavLink,
|
||||||
|
NavList,
|
||||||
|
NavItem,
|
||||||
|
Body,
|
||||||
|
} from "../src/layout"
|
||||||
|
|
||||||
type LayoutProps = {
|
type LayoutProps = {
|
||||||
title: string
|
title: string
|
||||||
|
|
@ -14,6 +24,7 @@ export const Layout = ({ title, children, showHomeLink = true }: LayoutProps) =>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>{title} - Howl UI</title>
|
<title>{title} - Howl UI</title>
|
||||||
|
<Styles />
|
||||||
<style>
|
<style>
|
||||||
{raw(`
|
{raw(`
|
||||||
* {
|
* {
|
||||||
|
|
@ -22,119 +33,7 @@ export const Layout = ({ title, children, showHomeLink = true }: LayoutProps) =>
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
/* Dark mode color box adjustments */
|
||||||
--bg: #ffffff;
|
|
||||||
--text: #1f2937;
|
|
||||||
--border: #e5e7eb;
|
|
||||||
--code-bg: #f9fafb;
|
|
||||||
--code-border: #e5e7eb;
|
|
||||||
--toggle-bg: #e5e7eb;
|
|
||||||
--toggle-active: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] {
|
|
||||||
--bg: #111827;
|
|
||||||
--text: #f9fafb;
|
|
||||||
--border: #374151;
|
|
||||||
--code-bg: #1f2937;
|
|
||||||
--code-border: #374151;
|
|
||||||
--toggle-bg: #374151;
|
|
||||||
--toggle-active: #60a5fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
transition: background-color 0.3s, color 0.3s;
|
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-mode-toggle {
|
|
||||||
position: fixed;
|
|
||||||
top: 16px;
|
|
||||||
right: 16px;
|
|
||||||
background: var(--toggle-bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 9999px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.2s;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-mode-toggle:hover {
|
|
||||||
background: var(--toggle-active);
|
|
||||||
color: white;
|
|
||||||
border-color: var(--toggle-active);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
position: relative;
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 24px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
background: var(--bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title:has(.home-link) {
|
|
||||||
padding-left: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-link {
|
|
||||||
position: absolute;
|
|
||||||
left: 24px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
background: var(--toggle-bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--text);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-link:hover {
|
|
||||||
background: var(--toggle-active);
|
|
||||||
color: white;
|
|
||||||
border-color: var(--toggle-active);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Update code example styles for dark mode */
|
|
||||||
[data-theme="dark"] .code-examples-container {
|
|
||||||
background-color: var(--code-bg) !important;
|
|
||||||
border-color: var(--code-border) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Syntax highlighting colors for dark mode - even lighter for better readability */
|
|
||||||
[data-theme="dark"] .code-examples-container span[style*="color: #8b5cf6"] {
|
|
||||||
color: #e9d5ff !important; /* much lighter violet */
|
|
||||||
}
|
|
||||||
[data-theme="dark"] .code-examples-container span[style*="color: #0ea5e9"] {
|
|
||||||
color: #bfdbfe !important; /* much lighter cyan */
|
|
||||||
}
|
|
||||||
[data-theme="dark"] .code-examples-container span[style*="color: #10b981"] {
|
|
||||||
color: #a7f3d0 !important; /* much lighter emerald */
|
|
||||||
}
|
|
||||||
[data-theme="dark"] .code-examples-container span[style*="color: #f59e0b"] {
|
|
||||||
color: #fde68a !important; /* much lighter amber */
|
|
||||||
}
|
|
||||||
[data-theme="dark"] .code-examples-container span[style*="color: #ef4444"] {
|
|
||||||
color: #fecaca !important; /* much lighter red */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode for specific demo colors */
|
|
||||||
[data-theme="dark"] div[style*="#fecaca"] { background-color: #7f1d1d !important; }
|
[data-theme="dark"] div[style*="#fecaca"] { background-color: #7f1d1d !important; }
|
||||||
[data-theme="dark"] div[style*="#bbf7d0"] { background-color: #14532d !important; }
|
[data-theme="dark"] div[style*="#bbf7d0"] { background-color: #14532d !important; }
|
||||||
[data-theme="dark"] div[style*="#bfdbfe"] { background-color: #1e3a8a !important; }
|
[data-theme="dark"] div[style*="#bfdbfe"] { background-color: #1e3a8a !important; }
|
||||||
|
|
@ -161,92 +60,37 @@ export const Layout = ({ title, children, showHomeLink = true }: LayoutProps) =>
|
||||||
color: #93c5fd !important;
|
color: #93c5fd !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark mode for button colors */
|
|
||||||
[data-theme="dark"] button[style*="#3b82f6"] { background-color: #1e40af !important; }
|
|
||||||
[data-theme="dark"] button[style*="#64748b"] { background-color: #334155 !important; }
|
|
||||||
[data-theme="dark"] button[style*="#ef4444"] { background-color: #991b1b !important; }
|
|
||||||
|
|
||||||
/* Dark mode for buttons - removed filter to maintain colors */
|
|
||||||
|
|
||||||
/* Dark mode for borders */
|
|
||||||
[data-theme="dark"] *[style*="border"] {
|
|
||||||
border-color: var(--border) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode for images */
|
/* Dark mode for images */
|
||||||
[data-theme="dark"] img {
|
[data-theme="dark"] img {
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark mode for cards and containers */
|
/* Fix white backgrounds in dark mode */
|
||||||
[data-theme="dark"] div[style*="border: 1px solid #d1d5db"] {
|
|
||||||
border-color: #374151 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Improve text visibility - very important! */
|
|
||||||
[data-theme="dark"] p,
|
|
||||||
[data-theme="dark"] h1,
|
|
||||||
[data-theme="dark"] h2,
|
|
||||||
[data-theme="dark"] h3,
|
|
||||||
[data-theme="dark"] h4,
|
|
||||||
[data-theme="dark"] h5,
|
|
||||||
[data-theme="dark"] span,
|
|
||||||
[data-theme="dark"] div,
|
|
||||||
[data-theme="dark"] li {
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure button text is visible */
|
|
||||||
[data-theme="dark"] button {
|
|
||||||
color: white !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Override black text colors */
|
|
||||||
[data-theme="dark"] *[style*="color: #000000"],
|
|
||||||
[data-theme="dark"] *[style*="color:#000000"],
|
|
||||||
[data-theme="dark"] *[style*="color: black"],
|
|
||||||
[data-theme="dark"] *[style*="color:black"] {
|
|
||||||
color: var(--text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Special case for buttons with transparent or outline variants */
|
|
||||||
[data-theme="dark"] button[style*="background: transparent"],
|
|
||||||
[data-theme="dark"] button[style*="backgroundColor: transparent"] {
|
|
||||||
color: var(--text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix divider backgrounds in dark mode */
|
|
||||||
[data-theme="dark"] span[style*="#ffffff"] {
|
|
||||||
background-color: var(--bg) !important;
|
|
||||||
color: #9ca3af !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix any #ffffff backgrounds in dark mode */
|
|
||||||
[data-theme="dark"] *[style*="background-color: rgb(255, 255, 255)"],
|
[data-theme="dark"] *[style*="background-color: rgb(255, 255, 255)"],
|
||||||
[data-theme="dark"] *[style*="#ffffff"] {
|
[data-theme="dark"] *[style*="#ffffff"] {
|
||||||
background-color: var(--bg) !important;
|
background-color: var(--theme-colors-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fix link colors in dark mode */
|
/* Fix link colors */
|
||||||
[data-theme="dark"] a[style*="#3b82f6"] {
|
[data-theme="dark"] a[style*="#3b82f6"] {
|
||||||
color: #60a5fa !important;
|
color: #60a5fa !important;
|
||||||
}
|
}
|
||||||
`)}
|
`)}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<Body>
|
||||||
<button class="dark-mode-toggle" onclick="toggleDarkMode()">
|
<DarkModeToggle onclick="toggleDarkMode()">
|
||||||
<span class="icon">🌙</span>
|
<span class="icon">🌙</span>
|
||||||
</button>
|
</DarkModeToggle>
|
||||||
|
|
||||||
<div class="page-title">
|
<PageTitle hasHomeLink={showHomeLink}>
|
||||||
{showHomeLink && (
|
{showHomeLink && (
|
||||||
<a href="/" class="home-link">
|
<HomeLink href="/">
|
||||||
<span>🏠</span>
|
<span>🏠</span>
|
||||||
</a>
|
</HomeLink>
|
||||||
)}
|
)}
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</PageTitle>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
|
|
@ -277,9 +121,8 @@ export const Layout = ({ title, children, showHomeLink = true }: LayoutProps) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateToggleButton(theme) {
|
function updateToggleButton(theme) {
|
||||||
const button = document.querySelector('.dark-mode-toggle')
|
const button = document.querySelector('.DarkModeToggle')
|
||||||
const icon = button.querySelector('.icon')
|
const icon = button.querySelector('.icon')
|
||||||
const label = button.querySelector('.label')
|
|
||||||
|
|
||||||
if (theme === 'dark') {
|
if (theme === 'dark') {
|
||||||
icon.textContent = '☀️';
|
icon.textContent = '☀️';
|
||||||
|
|
@ -289,7 +132,10 @@ export const Layout = ({ title, children, showHomeLink = true }: LayoutProps) =>
|
||||||
}
|
}
|
||||||
`)}
|
`)}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</Body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export nav components for use in server.tsx
|
||||||
|
export { NavLink, NavList, NavItem }
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Hono } from 'hono'
|
||||||
import { readdirSync } from 'fs'
|
import { readdirSync } from 'fs'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { capitalize } from './utils'
|
import { capitalize } from './utils'
|
||||||
import { Layout } from './layout'
|
import { Layout, NavLink, NavList, NavItem } from './layout'
|
||||||
|
|
||||||
const port = process.env.PORT ?? '3100'
|
const port = process.env.PORT ?? '3100'
|
||||||
const app = new Hono()
|
const app = new Hono()
|
||||||
|
|
@ -26,17 +26,15 @@ app.get('/:file', async c => {
|
||||||
app.get('/', c => {
|
app.get('/', c => {
|
||||||
return c.html(
|
return c.html(
|
||||||
<Layout title="🐺 howl" showHomeLink={false}>
|
<Layout title="🐺 howl" showHomeLink={false}>
|
||||||
<div style="padding: 24px;">
|
<NavList>
|
||||||
<ul style="font-size: 18px; line-height: 2;">
|
|
||||||
{testFiles().map(x => (
|
{testFiles().map(x => (
|
||||||
<li>
|
<NavItem key={x}>
|
||||||
<a href={`/${x}`} style="color: #3b82f6; text-decoration: none;">
|
<NavLink href={`/${x}`}>
|
||||||
{x}
|
{x}
|
||||||
</a>
|
</NavLink>
|
||||||
</li>
|
</NavItem>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</NavList>
|
||||||
</div>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user