diff --git a/README.md b/README.md index a15823f..8fa36d8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # 🐺 howl -Howl is a fork of `werewolf-ui`, without any Tailwind. A minimal, zero-dependency React component library. +Howl is a fork of `werewolf-ui`, without any Tailwind. + +A minimal, single-dependency component library built on forge +for hype/hono. ## Installation diff --git a/TAGS.md b/TAGS.md new file mode 100644 index 0000000..aed257f --- /dev/null +++ b/TAGS.md @@ -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 in your document head to inject CSS + + + + + + + + + + +``` + +## 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 +Content +Content +Content +``` + +--- + +### 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 +Content +Content +Content +``` + +--- + +### 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 +Items +Items +``` + +--- + +### 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 +
Content
+
Content
+``` + +--- + +## 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 +Content +Red content +Blue content +``` + +--- + +## 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 +

Heading 1

+

Heading 2

+

Heading 3

+``` + +--- + +### 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 +Regular text +``` + +--- + +### SmallText + +Small paragraph text component. + +**Props:** Same as Text + +**Sizing:** 12px + +**Examples:** +```tsx +Small text +``` + +--- + +## 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 + + + + + +``` + +--- + +### 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 + + +Label +Name +Label +``` + +--- + +### 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 + + + +``` + +--- + +## 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 + + + +``` + +--- + +### 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 + + + +``` + +--- + +### 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 + + + +``` + +--- + +### 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 + + + +``` + +--- + +## 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 + +OR +``` + +--- + +### 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.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 + + + +``` + +--- + +## 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 + 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' +``` diff --git a/bun.lock b/bun.lock index 9bf8c8f..fde4fbe 100644 --- a/bun.lock +++ b/bun.lock @@ -1,9 +1,11 @@ { "lockfileVersion": 1, + "configVersion": 1, "workspaces": { "": { "name": "howl", "dependencies": { + "forge": "git+https://git.nose.space/defunkt/forge", "hono": "^4.10.7", "lucide-static": "^0.555.0", }, @@ -16,13 +18,15 @@ }, }, "packages": { - "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], + "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], - "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="], - "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], + "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], - "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], + "forge": ["forge@git+https://git.nose.space/defunkt/forge#e14821c6995e82088efdfe16e53021afe471ac83", { "dependencies": { "hono": "^4.11.3" }, "peerDependencies": { "typescript": "^5" } }, "e14821c6995e82088efdfe16e53021afe471ac83"], + + "hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="], "lucide-static": ["lucide-static@0.555.0", "", {}, "sha512-FMMaYYsEYsUA6xlEzIMoKEV3oGnxIIvAN+AtLmYXvlTJptJTveJjVBQwvtA/zZLrD6KLEu89G95dQYlhivw5jQ=="], diff --git a/package.json b/package.json index b0e2f96..a10c87b 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,14 @@ "dev": "bun run --hot test/server.tsx" }, "devDependencies": { - "@types/bun": "latest" + "@types/bun": "latest", + "hono": "^4.10.7" }, "peerDependencies": { "typescript": "^5" }, "dependencies": { - "hono": "^4.10.7", + "forge": "git+https://git.nose.space/defunkt/forge", "lucide-static": "^0.555.0" } } \ No newline at end of file diff --git a/src/avatar.tsx b/src/avatar.tsx index fd56c02..6bb0601 100644 --- a/src/avatar.tsx +++ b/src/avatar.tsx @@ -1,61 +1,63 @@ -import { Section } from "./section" -import { H2, Text } from "./text" -import "hono/jsx" -import type { FC, JSX } from "hono/jsx" -import { VStack, HStack } from "./stack" -import { CodeExamples } from "./code" -import { cn } from "./cn" +import { define } from 'forge' +import { theme } from './theme' +import { Section } from './section' +import { H2, Text } from './text' +import { VStack, HStack } from './stack' -export type AvatarProps = JSX.IntrinsicElements["img"] & { - size?: number - rounded?: boolean -} +export const Avatar = define('Avatar', { + base: 'img', + objectFit: 'cover', -export const Avatar: FC = (props) => { - const { src, size = 32, rounded, class: className, style, id, ref, alt = "", ...rest } = props + variants: { + size: { + 24: { width: 24, height: 24 }, + 32: { width: 32, height: 32 }, + 48: { width: 48, height: 48 }, + 64: { width: 64, height: 64 }, + 96: { width: 96, height: 96 }, + }, + rounded: { + borderRadius: theme('radius-full'), + }, + }, +}) - const avatarStyle: JSX.CSSProperties = { - width: `${size}px`, - height: `${size}px`, - borderRadius: rounded ? "9999px" : undefined, - ...(style as JSX.CSSProperties), - } - - return {alt} -} +export type AvatarProps = Parameters[0] export const Test = () => { const sampleImages = [ - "https://picsum.photos/seed/3/200/200", - "https://picsum.photos/seed/2/200/200", - "https://picsum.photos/seed/8/200/200", - "https://picsum.photos/seed/9/200/200", + 'https://picsum.photos/seed/3/200/200', + 'https://picsum.photos/seed/2/200/200', + 'https://picsum.photos/seed/8/200/200', + 'https://picsum.photos/seed/9/200/200', ] return (
- {/* API Usage Examples */} - ', - '', - '', - '', - ]} - /> - {/* Size variations */}

Size Variations

- {[24, 32, 48, 64, 96].map((size) => ( - - - - {size}x{size} - - - ))} + + + 24x24 + + + + 32x32 + + + + 48x48 + + + + 64x64 + + + + 96x96 +
@@ -64,11 +66,11 @@ export const Test = () => {

Rounded vs Square

- + Square - + Rounded @@ -93,30 +95,30 @@ export const Test = () => { With Border With Shadow Border + Shadow diff --git a/src/box.tsx b/src/box.tsx index 6e93b5d..5d3a6b1 100644 --- a/src/box.tsx +++ b/src/box.tsx @@ -1,80 +1,54 @@ -import "hono/jsx" -import type { FC, PropsWithChildren, JSX } from "hono/jsx" -import { CodeExamples } from "./code" -import { cn } from "./cn" +import { define } from 'forge' +import { theme } from './theme' -type BoxProps = JSX.IntrinsicElements["div"] & PropsWithChildren & { - bg?: string - color?: string - p?: number -} +export const Box = define('Box', { + // Base box - minimal styling, designed for customization via style prop +}) -export const Box: FC = (props) => { - const { children, bg, color, p, class: className, style, id, ref, ...rest } = props +export const RedBox = define('RedBox', { + background: theme('colors-red'), + color: theme('colors-bg'), + padding: theme('spacing-1'), + textAlign: 'center', +}) - const boxStyle: JSX.CSSProperties = { - backgroundColor: bg, - color: color, - padding: p ? `${p}px` : undefined, - ...(style as JSX.CSSProperties), - } +export const GreenBox = define('GreenBox', { + background: theme('colors-success'), + color: theme('colors-bg'), + padding: theme('spacing-1'), + textAlign: 'center', +}) - return
{children}
-} +export const BlueBox = define('BlueBox', { + background: theme('colors-blue'), + color: theme('colors-bg'), + padding: theme('spacing-1'), + textAlign: 'center', +}) -// Common demo box colors -export const RedBox: FC = ({ children }) => ( - - {children} - -) - -export const GreenBox: FC = ({ children }) => ( - - {children} - -) - -export const BlueBox: FC = ({ children }) => ( - - {children} - -) - -export const GrayBox: FC = ({ children, style }) => ( - - {children} - -) +export const GrayBox = define('GrayBox', { + background: theme('colors-bgMuted'), + padding: theme('spacing-4'), +}) export const Test = () => { return ( -
- {/* API Usage Examples */} - Content', - 'Content', - 'Content', - 'Content', - ]} - /> - +
-

Box Component

-
- +

Box Component

+
+ Basic Box with custom background and text color - + Box with padding and border
-

Color Variants

-
+

Color Variants

+
Red Box Green Box Blue Box @@ -83,10 +57,10 @@ export const Test = () => {
-

Nested Boxes

- - - +

Nested Boxes

+ + + Nested boxes demonstration diff --git a/src/button.tsx b/src/button.tsx index 9e7bb6a..adce573 100644 --- a/src/button.tsx +++ b/src/button.tsx @@ -1,96 +1,114 @@ -import "hono/jsx" -import type { JSX, FC } from "hono/jsx" -import { VStack, HStack } from "./stack" -import { Section } from "./section" -import { H2 } from "./text" -import { CodeExamples } from "./code" -import { cn } from "./cn" +import { define } from 'forge' +import { theme } from './theme' +import { VStack, HStack } from './stack' +import { Section } from './section' +import { H2 } from './text' -export type ButtonProps = JSX.IntrinsicElements["button"] & { - variant?: "primary" | "secondary" | "outline" | "ghost" | "destructive" - size?: "sm" | "md" | "lg" -} +export const Button = define('Button', { + base: 'button', -export const Button: FC = (props) => { - const { variant = "primary", size = "md", style, class: className, id, ref, ...buttonProps } = props + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + fontWeight: 500, + transition: 'all 0.2s', + cursor: 'pointer', + borderRadius: theme('radius-sm'), + border: '1px solid transparent', + outline: 'none', - const baseStyles: JSX.CSSProperties = { - display: "inline-flex", - alignItems: "center", - justifyContent: "center", - fontWeight: "500", - transition: "all 0.2s", - outline: "none", - cursor: "pointer", - borderRadius: "4px", - border: "1px solid transparent", - } + // Default: primary + md + background: theme('colors-primary'), + color: theme('colors-bg'), + height: 40, + padding: `0 ${theme('spacing-4')}`, + fontSize: theme('fontSize-sm'), - const variantStyles: Record = { - primary: { - backgroundColor: "#3b82f6", - color: "#ffffff", + states: { + ':not(:disabled):hover': { + background: theme('colors-primaryHover'), }, - secondary: { - backgroundColor: "#64748b", - color: "#ffffff", + ':disabled': { + opacity: 0.5, + cursor: 'not-allowed', }, - outline: { - backgroundColor: "transparent", - color: "#000000", - borderColor: "#d1d5db", - }, - ghost: { - backgroundColor: "transparent", - color: "#000000", - }, - destructive: { - backgroundColor: "#ef4444", - color: "#ffffff", - }, - } + }, - const sizeStyles: Record = { - sm: { - height: "32px", - padding: "0 12px", - fontSize: "14px", + variants: { + variant: { + primary: { + background: theme('colors-primary'), + color: theme('colors-bg'), + states: { + ':not(:disabled):hover': { + background: theme('colors-primaryHover'), + }, + }, + }, + secondary: { + background: theme('colors-secondary'), + color: theme('colors-bg'), + states: { + ':not(:disabled):hover': { + background: theme('colors-secondaryHover'), + }, + }, + }, + outline: { + background: 'transparent', + color: theme('colors-fg'), + borderColor: theme('colors-border'), + states: { + ':not(:disabled):hover': { + borderColor: theme('colors-borderActive'), + }, + }, + }, + ghost: { + background: 'transparent', + color: theme('colors-fg'), + border: 'none', + states: { + ':not(:disabled):hover': { + background: theme('colors-bgMuted'), + }, + }, + }, + destructive: { + background: theme('colors-destructive'), + color: theme('colors-bg'), + states: { + ':not(:disabled):hover': { + background: theme('colors-destructiveHover'), + }, + }, + }, }, - md: { - height: "40px", - padding: "0 16px", - fontSize: "14px", + size: { + sm: { + height: 32, + padding: `0 ${theme('spacing-3')}`, + fontSize: theme('fontSize-sm'), + }, + md: { + height: 40, + padding: `0 ${theme('spacing-4')}`, + fontSize: theme('fontSize-sm'), + }, + lg: { + height: 48, + padding: `0 ${theme('spacing-6')}`, + fontSize: theme('fontSize-base'), + }, }, - lg: { - height: "48px", - padding: "0 24px", - fontSize: "16px", - }, - } + }, +}) - const combinedStyles: JSX.CSSProperties = { - ...baseStyles, - ...variantStyles[variant], - ...sizeStyles[size], - ...(style as JSX.CSSProperties), - } - - return ', - '', - '', - '', - ]} - /> - {/* Variants */}

Button Variants

@@ -119,11 +137,11 @@ export const Test = () => { -
@@ -132,10 +150,10 @@ export const Test = () => {

Native Attributes

- - - -
@@ -145,18 +95,18 @@ export const Test = () => { {/* Alignment examples */}

Alignment: v="center" h="center"

- -
Item 1
-
Item 2
-
Item 3
+ +
Item 1
+
Item 2
+
Item 3

Alignment: v="start" h="end"

- -
Left
-
Right
+ +
Left
+
Right
diff --git a/src/icon.tsx b/src/icon.tsx index 0cad67f..1fa7189 100644 --- a/src/icon.tsx +++ b/src/icon.tsx @@ -1,27 +1,50 @@ -import "hono/jsx" -import type { FC, JSX } from "hono/jsx" -import * as icons from "lucide-static" -import { Grid } from "./grid" -import { VStack } from "./stack" -import { Section } from "./section" -import { H2, Text } from "./text" -import { CodeExamples } from "./code" -import { cn } from "./cn" +import { define } from 'forge' +import { theme } from './theme' +import * as icons from 'lucide-static' +import { Grid } from './grid' +import { VStack } from './stack' +import { Section } from './section' +import { H2, Text } from './text' export type IconName = keyof typeof icons -type IconProps = JSX.IntrinsicElements["div"] & { +// Icon wrapper - the SVG is injected via dangerouslySetInnerHTML +const IconWrapper = define('Icon', { + display: 'block', + flexShrink: 0, +}) + +// IconLink wrapper +const IconLinkWrapper = define('IconLink', { + base: 'a', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + transition: 'opacity 0.2s', + + states: { + ':hover': { + opacity: 0.7, + }, + }, +}) + +type IconProps = Parameters[0] & { name: IconName size?: number } -type IconLinkProps = JSX.IntrinsicElements["a"] & { +type IconLinkProps = Parameters[0] & { name: IconName size?: number } -export const Icon: FC = (props) => { - const { name, size = 6, class: className, style, id, ref, ...rest } = props +function sizeToPixels(size: number): number { + return size * 4 +} + +export const Icon = (props: IconProps) => { + const { name, size = 6, style, ...rest } = props const iconSvg = icons[name] @@ -30,64 +53,42 @@ export const Icon: FC = (props) => { } const pixelSize = sizeToPixels(size) - const iconStyle: JSX.CSSProperties = { - display: "block", - flexShrink: "0", + const iconStyle = { width: `${pixelSize}px`, height: `${pixelSize}px`, - ...(style as JSX.CSSProperties), + ...style, } // Modify the SVG string to include our custom attributes const modifiedSvg = iconSvg - .replace(/width="[^"]*"/, "") - .replace(/height="[^"]*"/, "") - .replace(/class="[^"]*"/, "") + .replace(/width="[^"]*"/, '') + .replace(/height="[^"]*"/, '') + .replace(/class="[^"]*"/, '') .replace( /]*)>/, - `` + `` ) - return
+ return } -export const IconLink: FC = (props) => { - const { href = "#", target, class: className, style, id, ref, name, size, ...rest } = props - - const linkStyle: JSX.CSSProperties = { - display: "inline-flex", - alignItems: "center", - justifyContent: "center", - transition: "opacity 0.2s", - ...(style as JSX.CSSProperties), - } +export const IconLink = (props: IconLinkProps) => { + const { href = '#', name, size, ...rest } = props return ( - + - + ) } export const Test = () => { return (
- {/* API Usage Examples */} - ', - '', - '', - '', - ]} - /> - - {/* === ICON TESTS === */} - {/* Size variations */}

Icon Size Variations

-
+
{([3, 4, 5, 6, 8, 10, 12, 16] as const).map((size) => ( @@ -106,7 +107,7 @@ export const Test = () => { Default - + Blue Color @@ -114,11 +115,11 @@ export const Test = () => { Thin Stroke - + Filled - + Hover Effect @@ -129,30 +130,28 @@ export const Test = () => {

Advanced Styling

- + Filled Heart - + Thick Stroke - + Sun Icon - + Drop Shadow - {/* === ICON LINK TESTS === */} - - {/* Basic icon links */} + {/* Icon links */}

Icon Links

-
+
Home Link @@ -182,10 +181,10 @@ export const Test = () => { size={8} href="#" style={{ - backgroundColor: "#3b82f6", - color: "white", - padding: "8px", - borderRadius: "8px", + backgroundColor: '#3b82f6', + color: 'white', + padding: '8px', + borderRadius: '8px', }} /> Button Style @@ -196,19 +195,19 @@ export const Test = () => { size={8} href="#" style={{ - border: "2px solid #d1d5db", - padding: "8px", - borderRadius: "9999px", + border: '2px solid #d1d5db', + padding: '8px', + borderRadius: '9999px', }} /> Circle Border - + Red Heart - + Filled Star @@ -216,7 +215,3 @@ export const Test = () => {
) } - -function sizeToPixels(size: number): number { - return size * 4 -} diff --git a/src/image.tsx b/src/image.tsx index 3c2a1d2..79b6be8 100644 --- a/src/image.tsx +++ b/src/image.tsx @@ -1,51 +1,35 @@ -import { Section } from "./section" -import { H2, H3, H4, H5, Text, SmallText } from "./text" -import "hono/jsx" -import type { FC, JSX } from "hono/jsx" -import { VStack, HStack } from "./stack" -import { Grid } from "./grid" -import { CodeExamples } from "./code" -import { cn } from "./cn" +import { define } from 'forge' +import { Section } from './section' +import { H2, H3, H4, H5, Text } from './text' +import { VStack, HStack } from './stack' +import { Grid } from './grid' -export type ImageProps = JSX.IntrinsicElements["img"] & { - width?: number - height?: number - objectFit?: "cover" | "contain" | "fill" | "none" | "scale-down" -} +export const Image = define('Image', { + base: 'img', -export const Image: FC = (props) => { - const { src, alt = "", width, height, objectFit, class: className, style, id, ref, ...rest } = props + variants: { + objectFit: { + cover: { objectFit: 'cover' }, + contain: { objectFit: 'contain' }, + fill: { objectFit: 'fill' }, + none: { objectFit: 'none' }, + 'scale-down': { objectFit: 'scale-down' }, + }, + }, +}) - const imageStyle: JSX.CSSProperties = { - width: width ? `${width}px` : undefined, - height: height ? `${height}px` : undefined, - objectFit: objectFit, - ...(style as JSX.CSSProperties), - } - - return {alt} -} +export type ImageProps = Parameters[0] export const Test = () => { const sampleImages = [ - "https://picsum.photos/seed/1/400/600", // Portrait - "https://picsum.photos/seed/2/600/400", // Landscape - "https://picsum.photos/seed/3/300/300", // Square - "https://picsum.photos/seed/4/200/100", // Small image + 'https://picsum.photos/seed/1/400/600', // Portrait + 'https://picsum.photos/seed/2/600/400', // Landscape + 'https://picsum.photos/seed/3/300/300', // Square + 'https://picsum.photos/seed/4/200/100', // Small image ] return (
- {/* API Usage Examples */} - ', - '', - '', - '', - ]} - /> -

Image Examples

{/* Size variations */} @@ -53,19 +37,19 @@ export const Test = () => {

Size Variations

- 64x64 + 64x64 64x64 - 96x96 + 96x96 96x96 - 128x128 + 128x128 128x128 - 192x128 + 192x128 192x128 @@ -74,61 +58,49 @@ export const Test = () => { {/* Object fit variations */}

Object Fit Variations

- - Same image with different object-fit values - + Same image with different object-fit values Object cover object-fit: cover Object contain object-fit: contain Object fill object-fit: fill Object scale-down object-fit: scale-down Object none object-fit: none @@ -142,36 +114,32 @@ export const Test = () => { Rounded with border Rounded + Border With shadow With Shadow Circular with effects @@ -188,21 +156,19 @@ export const Test = () => {

Avatar

Avatar
{/* Card image */} - +

Card Image

- - Card image - + + Card image +
Card Title
Card description goes here
@@ -217,10 +183,8 @@ export const Test = () => { {`Gallery ))} diff --git a/src/index.tsx b/src/index.tsx index d991a6d..19608de 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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 type { ButtonProps } from "./button" +export { cn } from './cn' -export { Icon, IconLink } from "./icon" -export type { IconName } from "./icon" +export { Button } from './button' +export type { ButtonProps } from './button' -export { VStack, HStack } from "./stack" +export { Icon, IconLink } from './icon' +export type { IconName } from './icon' -export { Grid } from "./grid" +export { VStack, HStack } from './stack' -export { Divider } from "./divider" +export { Grid } from './grid' -export { Avatar } from "./avatar" -export type { AvatarProps } from "./avatar" +export { Divider } from './divider' -export { Image } from "./image" -export type { ImageProps } from "./image" +export { Avatar } from './avatar' +export type { AvatarProps } from './avatar' -export { Input } from "./input" -export type { InputProps } from "./input" +export { Image } from './image' +export type { ImageProps } from './image' -export { Select } from "./select" -export type { SelectProps, SelectOption } from "./select" +export { Input } from './input' +export type { InputProps } from './input' -export { Placeholder } from "./placeholder" -export { default as PlaceholderDefault } from "./placeholder" +export { Select } from './select' +export type { SelectProps, SelectOption } from './select' -export { H1, H2, H3, H4, H5, Text, SmallText } from "./text" +export { Placeholder } from './placeholder' +export { default as PlaceholderDefault } from './placeholder' -export { Box, RedBox, GreenBox, BlueBox, GrayBox } from "./box" +export { H1, H2, H3, H4, H5, Text, SmallText } from './text' -export { Section } from "./section" +export { Box, RedBox, GreenBox, BlueBox, GrayBox } from './box' -export type { TailwindSize, CommonHTMLProps } from "./types" +export { Section } from './section' + +export type { TailwindSize, CommonHTMLProps } from './types' diff --git a/src/input.tsx b/src/input.tsx index fdae09d..a260db6 100644 --- a/src/input.tsx +++ b/src/input.tsx @@ -1,89 +1,99 @@ -import { Section } from "./section" -import { H2, H3, H4, H5, Text, SmallText } from "./text" -import "hono/jsx" -import type { JSX, FC } from "hono/jsx" -import { VStack, HStack } from "./stack" -import { CodeExamples } from "./code" -import { cn } from "./cn" +import { define } from 'forge' +import { theme } from './theme' +import { Section } from './section' +import { H2 } from './text' +import { VStack, HStack } from './stack' -export type InputProps = JSX.IntrinsicElements["input"] & { - labelPosition?: "above" | "left" | "right" - children?: any -} +export const Input = define('Input', { + parts: { + Wrapper: { + display: 'flex', + flexDirection: 'column', + gap: theme('spacing-1'), + flex: 1, + minWidth: 0, + }, + Label: { + base: 'label', + fontSize: theme('fontSize-sm'), + fontWeight: 500, + color: theme('colors-fg'), + }, + Field: { + base: 'input', + height: 40, + padding: `${theme('spacing-2')} ${theme('spacing-3')}`, + borderRadius: theme('radius-md'), + border: `1px solid ${theme('colors-border')}`, + background: theme('colors-bg'), + fontSize: theme('fontSize-sm'), + outline: 'none', + flex: 1, -export const Input: FC = (props) => { - const { labelPosition = "above", children, style, class: className, id, ref, ...inputProps } = props + states: { + ':focus': { + borderColor: theme('colors-borderActive'), + }, + ':disabled': { + opacity: 0.5, + cursor: 'not-allowed', + }, + }, + }, + }, - const inputStyle: JSX.CSSProperties = { - height: "40px", - padding: "8px 12px", - borderRadius: "6px", - border: "1px solid #d1d5db", - backgroundColor: "white", - fontSize: "14px", - outline: "none", - ...(style as JSX.CSSProperties), - } + variants: { + labelPosition: { + above: { + parts: { + Wrapper: { + flexDirection: 'column', + gap: theme('spacing-1'), + }, + }, + }, + left: { + parts: { + Wrapper: { + flexDirection: 'row', + alignItems: 'center', + gap: theme('spacing-1'), + }, + }, + }, + right: { + parts: { + Wrapper: { + flexDirection: 'row-reverse', + alignItems: 'center', + gap: theme('spacing-1'), + }, + }, + }, + }, + }, - if (!children) { - return - } + render({ props, parts: { Root, Wrapper, Label, Field } }) { + const { children, labelPosition = 'above', ...inputProps } = props - const labelStyle: JSX.CSSProperties = { - fontSize: "14px", - fontWeight: "500", - color: "#111827", - } + if (!children) { + return + } - const labelElement = ( - - ) - - if (labelPosition === "above") { return ( -
- {labelElement} - -
+ + + + ) - } + }, +}) - if (labelPosition === "left") { - return ( -
- {labelElement} - -
- ) - } - - if (labelPosition === "right") { - return ( -
- - {labelElement} -
- ) - } - - return null -} +export type InputProps = Parameters[0] export const Test = () => { return ( -
- {/* API Usage Examples */} - ', - '', - 'Label', - 'Name', - ]} - /> - +
{/* Basic inputs */}

Basic Inputs

@@ -98,9 +108,9 @@ export const Test = () => {

Custom Styling

- + - +
@@ -117,8 +127,8 @@ export const Test = () => {

Disabled State

- - + +
@@ -177,19 +187,6 @@ export const Test = () => { Age
- - {/* Custom styling */} - -

Custom Input Styling

- - - Custom Label - - - Required Field - - -
) } diff --git a/src/layout.tsx b/src/layout.tsx new file mode 100644 index 0000000..9a4f9c1 --- /dev/null +++ b/src/layout.tsx @@ -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'), +}) diff --git a/src/placeholder.tsx b/src/placeholder.tsx index f6ea0c6..2413fe4 100644 --- a/src/placeholder.tsx +++ b/src/placeholder.tsx @@ -1,65 +1,51 @@ -import { Section } from "./section" -import { H2, H3, H4, H5, Text, SmallText } from "./text" -import "hono/jsx" -import { Avatar } from "./avatar" -import type { AvatarProps } from "./avatar" -import { Image } from "./image" -import type { ImageProps } from "./image" -import { VStack, HStack } from "./stack" -import { Grid } from "./grid" -import { CodeExamples } from "./code" +import { Section } from './section' +import { H2, H3, Text, SmallText } from './text' +import { Avatar } from './avatar' +import type { AvatarProps } from './avatar' +import { Image } from './image' +import type { ImageProps } from './image' +import { VStack, HStack } from './stack' +import { Grid } from './grid' export const Placeholder = { Avatar(props: PlaceholderAvatarProps) { - const { size = 32, seed = "seed", type = "dylan", transparent, alt, style, rounded, id, ref, class: className } = props + const { size = 32, seed = 'seed', type = 'dylan', transparent, alt, style, rounded, id, ref, class: className } = props // Generate DiceBear avatar URL const url = new URL(`https://api.dicebear.com/9.x/${type}/svg`) - url.searchParams.set("seed", seed) - url.searchParams.set("size", size.toString()) + url.searchParams.set('seed', seed) + url.searchParams.set('size', size.toString()) if (transparent) { - url.searchParams.set("backgroundColor", "transparent") + url.searchParams.set('backgroundColor', 'transparent') } - return + return }, Image(props: PlaceholderImageProps) { - const { width = 200, height = 200, seed = 1, alt = "Placeholder image", objectFit, style, id, ref, class: className } = props + const { width = 200, height = 200, seed = 1, alt = 'Placeholder image', objectFit, style, id, ref, class: className } = props // Generate Picsum Photos URL with seed for consistent images const src = `https://picsum.photos/${width}/${height}?random=${seed}` - return {alt} + return {alt} }, } export const Test = () => { return (
- {/* API Usage Examples */} - ', - '', - '', - '', - ]} - /> - - {/* === AVATAR TESTS === */} - {/* Show all available avatar styles */}

All Avatar Styles ({allStyles.length} total)

- - {allStyles.map((style) => ( + + {allStyles.slice(0, 12).map((style) => ( - {style} + {style} ))} @@ -87,7 +73,7 @@ export const Test = () => { Rounded + Background
-
+
Rounded + Transparent @@ -97,7 +83,7 @@ export const Test = () => { Square + Background -
+
Square + Transparent @@ -111,7 +97,7 @@ export const Test = () => { Avatar Seeds (Same Style, Different People) - {["alice", "bob", "charlie", "diana"].map((seed) => ( + {['alice', 'bob', 'charlie', 'diana'].map((seed) => ( "{seed}" @@ -120,7 +106,7 @@ export const Test = () => { - {/* === IMAGE TESTS === */} + {/* Placeholder Images */}

Placeholder Images

@@ -137,7 +123,7 @@ export const Test = () => { - {width}×{height} + {width}x{height} ))} @@ -169,7 +155,7 @@ export const Test = () => { height={150} seed={1} objectFit="cover" - style={{ borderRadius: "8px", border: "4px solid #3b82f6" }} + style={{ borderRadius: '8px', border: '4px solid #3b82f6' }} /> Rounded + Border
@@ -179,7 +165,7 @@ export const Test = () => { height={150} seed={2} objectFit="cover" - style={{ boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)" }} + style={{ boxShadow: '0 10px 15px rgba(0, 0, 0, 0.3)' }} /> With Shadow @@ -190,9 +176,9 @@ export const Test = () => { seed={3} objectFit="cover" style={{ - borderRadius: "9999px", - border: "4px solid #22c55e", - boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)", + borderRadius: '9999px', + border: '4px solid #22c55e', + boxShadow: '0 10px 15px rgba(0, 0, 0, 0.3)', }} /> Circular + Effects @@ -205,13 +191,13 @@ export const Test = () => { } // Type definitions -type PlaceholderAvatarProps = Omit & { +type PlaceholderAvatarProps = Omit & { seed?: string type?: DicebearStyleName transparent?: boolean } -type PlaceholderImageProps = Omit & { +type PlaceholderImageProps = Omit & { width?: number height?: number seed?: number @@ -220,36 +206,36 @@ type PlaceholderImageProps = Omit & { // All supported DiceBear HTTP styleNames. Source: https://www.dicebear.com/styles const allStyles = [ - "adventurer", - "adventurer-neutral", - "avataaars", - "avataaars-neutral", - "big-ears", - "big-ears-neutral", - "big-smile", - "bottts", - "bottts-neutral", - "croodles", - "croodles-neutral", - "dylan", - "fun-emoji", - "glass", - "icons", - "identicon", - "initials", - "lorelei", - "lorelei-neutral", - "micah", - "miniavs", - "notionists", - "notionists-neutral", - "open-peeps", - "personas", - "pixel-art", - "pixel-art-neutral", - "rings", - "shapes", - "thumbs", + 'adventurer', + 'adventurer-neutral', + 'avataaars', + 'avataaars-neutral', + 'big-ears', + 'big-ears-neutral', + 'big-smile', + 'bottts', + 'bottts-neutral', + 'croodles', + 'croodles-neutral', + 'dylan', + 'fun-emoji', + 'glass', + 'icons', + 'identicon', + 'initials', + 'lorelei', + 'lorelei-neutral', + 'micah', + 'miniavs', + 'notionists', + 'notionists-neutral', + 'open-peeps', + 'personas', + 'pixel-art', + 'pixel-art-neutral', + 'rings', + 'shapes', + 'thumbs', ] as const type DicebearStyleName = (typeof allStyles)[number] diff --git a/src/section.tsx b/src/section.tsx index 02159c6..2e7a94e 100644 --- a/src/section.tsx +++ b/src/section.tsx @@ -1,59 +1,48 @@ -import "hono/jsx" -import type { FC, PropsWithChildren, JSX } from "hono/jsx" -import { VStack } from "./stack" -import type { TailwindSize } from "./types" -import { CodeExamples } from "./code" +import { define } from 'forge' +import { theme } from './theme' -type SectionProps = JSX.IntrinsicElements["div"] & PropsWithChildren & { - gap?: TailwindSize - maxWidth?: string -} +export const Section = define('Section', { + display: 'flex', + flexDirection: 'column', + padding: theme('spacing-6'), -export const Section: FC = (props) => { - const { children, gap = 8, maxWidth, class: className, style, id, ref, ...rest } = props - - return ( - - {children} - - ) -} + variants: { + gap: { + 0: { gap: 0 }, + 1: { gap: theme('spacing-1') }, + 2: { gap: theme('spacing-2') }, + 3: { gap: theme('spacing-3') }, + 4: { gap: theme('spacing-4') }, + 6: { gap: theme('spacing-6') }, + 8: { gap: theme('spacing-8') }, + 12: { gap: theme('spacing-12') }, + }, + }, +}) export const Test = () => { return (
- {/* API Usage Examples */} -
- ...
', - '
...
', - '
...
', - '
...
', - ]} - /> -
-
-

Default Section

-

This is a section with default gap (8)

+

Default Section

+

This is a section with default styling

It has padding and vertical spacing between children

-

Compact Section

+

Compact Section

This section has a smaller gap (4)

Items are closer together

-

Spacious Section

+

Spacious Section

This section has a larger gap (12)

Items have more breathing room

-
-

Constrained Width Section

+
+

Constrained Width Section

This section has a max width of 600px and a gray background

Good for centering content on wide screens

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

Basic Selects

@@ -196,22 +182,8 @@ export const Test = () => {

Disabled State

- - -
- - {/* Custom styling */} - -

Custom Styling

- - +
diff --git a/src/stack.tsx b/src/stack.tsx index d683e23..dfd92d9 100644 --- a/src/stack.tsx +++ b/src/stack.tsx @@ -1,146 +1,123 @@ -import type { TailwindSize } from "./types" -import "hono/jsx" -import type { FC, PropsWithChildren, JSX } from "hono/jsx" -import { Grid } from "./grid" -import { Section } from "./section" -import { H2 } from "./text" -import { RedBox, GreenBox, BlueBox } from "./box" -import { CodeExamples } from "./code" -import { cn } from "./cn" +import { define } from 'forge' +import { theme } from './theme' +import { H2 } from './text' +import { RedBox, GreenBox, BlueBox } from './box' +import { Grid } from './grid' +import { Section } from './section' -export const VStack: FC = (props) => { - const { v, h, wrap, gap, maxWidth, rows, class: className, style, id, ref, children, ...rest } = props - return ( - - {children} - - ) -} +export const VStack = define('VStack', { + display: 'flex', + flexDirection: 'column', -export const HStack: FC = (props) => { - const { h, v, wrap, gap, maxWidth, cols, class: className, style, id, ref, children, ...rest } = props - return ( - - {children} - - ) -} + variants: { + gap: { + 0: { gap: 0 }, + 1: { gap: theme('spacing-1') }, + 2: { gap: theme('spacing-2') }, + 3: { gap: theme('spacing-3') }, + 4: { gap: theme('spacing-4') }, + 6: { gap: theme('spacing-6') }, + 8: { gap: theme('spacing-8') }, + 12: { gap: theme('spacing-12') }, + }, + v: { + start: { justifyContent: 'flex-start' }, + center: { justifyContent: 'center' }, + end: { justifyContent: 'flex-end' }, + between: { justifyContent: 'space-between' }, + around: { justifyContent: 'space-around' }, + evenly: { justifyContent: 'space-evenly' }, + }, + h: { + start: { alignItems: 'flex-start' }, + center: { alignItems: 'center' }, + end: { alignItems: 'flex-end' }, + stretch: { alignItems: 'stretch' }, + baseline: { alignItems: 'baseline' }, + }, + wrap: { + flexWrap: 'wrap', + }, + }, -const Stack: FC = (props) => { - const { direction, mainAxis, crossAxis, wrap, gap, maxWidth, gridSizes, componentName, class: className, style, id, ref, children, ...rest } = props - const gapPx = gap ? gap * 4 : 0 + render({ props, parts: { Root } }) { + const { rows, style, ...rest } = props + const gridStyle = rows + ? { display: 'grid', gridTemplateRows: rows.map((r: number) => `${r}fr`).join(' '), ...style } + : style + return {props.children} + }, +}) - // Use CSS Grid when gridSizes (cols/rows) is provided - if (gridSizes) { - const gridTemplate = gridSizes.map(size => `${size}fr`).join(" ") +export const HStack = define('HStack', { + display: 'flex', + flexDirection: 'row', - const gridStyles: JSX.CSSProperties = { - display: "grid", - gap: `${gapPx}px`, - maxWidth: maxWidth, - } + variants: { + gap: { + 0: { gap: 0 }, + 1: { gap: theme('spacing-1') }, + 2: { gap: theme('spacing-2') }, + 3: { gap: theme('spacing-3') }, + 4: { gap: theme('spacing-4') }, + 6: { gap: theme('spacing-6') }, + 8: { gap: theme('spacing-8') }, + 12: { gap: theme('spacing-12') }, + }, + h: { + start: { justifyContent: 'flex-start' }, + center: { justifyContent: 'center' }, + end: { justifyContent: 'flex-end' }, + between: { justifyContent: 'space-between' }, + around: { justifyContent: 'space-around' }, + evenly: { justifyContent: 'space-evenly' }, + }, + v: { + start: { alignItems: 'flex-start' }, + center: { alignItems: 'center' }, + end: { alignItems: 'flex-end' }, + stretch: { alignItems: 'stretch' }, + baseline: { alignItems: 'baseline' }, + }, + wrap: { + flexWrap: 'wrap', + }, + }, - if (direction === "row") { - gridStyles.gridTemplateColumns = gridTemplate - } else { - gridStyles.gridTemplateRows = gridTemplate - } + render({ props, parts: { Root } }) { + const { cols, style, ...rest } = props + const gridStyle = cols + ? { display: 'grid', gridTemplateColumns: cols.map((c: number) => `${c}fr`).join(' '), ...style } + : style + return {props.children} + }, +}) - const combinedStyles = { - ...gridStyles, - ...(style as JSX.CSSProperties), - } - - return
{children}
- } - - // 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
{children}
-} +type MainAxisOpts = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly' +type CrossAxisOpts = 'start' | 'center' | 'end' | 'stretch' | 'baseline' export const Test = () => { - const mainAxisOpts: MainAxisOpts[] = ["start", "center", "end", "between", "around", "evenly"] - const crossAxisOpts: CrossAxisOpts[] = ["start", "center", "end", "stretch", "baseline"] + const mainAxisOpts: MainAxisOpts[] = ['start', 'center', 'end', 'between', 'around', 'evenly'] + const crossAxisOpts: CrossAxisOpts[] = ['start', 'center', 'end', 'stretch', 'baseline'] return ( -
- {/* API Usage Examples */} - ...', - '...', - '...', - '...', - '...', - '...', - ]} - /> - +
{/* HStack layout matrix */}

HStack Layout

-
- +
+ {/* Header row: blank + h labels */}
{mainAxisOpts.map((h) => ( -
+
h: {h}
))} {/* Each row: v label + HStack cells */} {crossAxisOpts.map((v) => [ -
+
v: {v}
, ...mainAxisOpts.map((h) => ( @@ -148,7 +125,7 @@ export const Test = () => { key={`${h}-${v}`} h={h} v={v} - style={{ backgroundColor: "#f3f4f6", padding: "8px", height: "96px", border: "1px solid #9ca3af" }} + style={{ backgroundColor: '#f3f4f6', padding: '8px', height: '96px', border: '1px solid #9ca3af' }} > Aa Aa @@ -163,19 +140,19 @@ export const Test = () => { {/* VStack layout matrix */}

VStack Layout

-
- +
+ {/* Header row: blank + h labels */}
{crossAxisOpts.map((h) => ( -
+
h: {h}
))} {/* Each row: v label + VStack cells */} {mainAxisOpts.map((v) => [ -
+
v: {v}
, ...crossAxisOpts.map((h) => ( @@ -183,7 +160,7 @@ export const Test = () => { key={`${h}-${v}`} v={v} h={h} - style={{ backgroundColor: "#f3f4f6", padding: "8px", height: "168px", border: "1px solid #9ca3af" }} + style={{ backgroundColor: '#f3f4f6', padding: '8px', height: '168px', border: '1px solid #9ca3af' }} > Aa Aa @@ -200,59 +177,59 @@ export const Test = () => {

HStack with Custom Column Sizing

-
+
cols=[7, 3] (70%/30% split)
- -
+ +
70% width
-
+
30% width
-
+
cols=[2, 1] (66%/33% split)
- -
+ +
2/3 width
-
+
1/3 width
-
+
cols=[1, 2, 1] (25%/50%/25% split)
- -
+ +
25%
-
+
50%
-
+
25%
-
- With maxWidth="600px" +
+ cols=[7, 3] with maxWidth: 600px
- -
+ +
70% of max 600px
-
+
30% of max 600px
@@ -265,18 +242,18 @@ export const Test = () => {

VStack with Custom Row Sizing

-
+
rows=[2, 1] (2/3 and 1/3 height)
-
+
2/3 height
-
+
1/3 height
@@ -286,60 +263,3 @@ export const Test = () => {
) } - -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 = { - 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 = { - start: "flex-start", - center: "center", - end: "flex-end", - stretch: "stretch", - baseline: "baseline", - } - return map[axis] || "stretch" -} diff --git a/src/text.tsx b/src/text.tsx index bbf9d2d..377681a 100644 --- a/src/text.tsx +++ b/src/text.tsx @@ -1,57 +1,57 @@ -import "hono/jsx" -import type { FC, PropsWithChildren, JSX } from "hono/jsx" -import { CodeExamples } from "./code" -import { cn } from "./cn" +import { define } from 'forge' +import { theme } from './theme' -export const H1: FC = (props) => { - const { children, class: className, style, id, ref, ...rest } = props - return

{children}

-} +export const H1 = define('H1', { + base: 'h1', + fontSize: theme('fontSize-2xl'), + fontWeight: 700, + color: theme('colors-fg'), +}) -export const H2: FC = (props) => { - const { children, class: className, style, id, ref, ...rest } = props - return

{children}

-} +export const H2 = define('H2', { + base: 'h2', + fontSize: theme('fontSize-xl'), + fontWeight: 700, + color: theme('colors-fg'), +}) -export const H3: FC = (props) => { - const { children, class: className, style, id, ref, ...rest } = props - return

{children}

-} +export const H3 = define('H3', { + base: 'h3', + fontSize: theme('fontSize-lg'), + fontWeight: 600, + color: theme('colors-fg'), +}) -export const H4: FC = (props) => { - const { children, class: className, style, id, ref, ...rest } = props - return

{children}

-} +export const H4 = define('H4', { + base: 'h4', + fontSize: theme('fontSize-base'), + fontWeight: 600, + color: theme('colors-fg'), +}) -export const H5: FC = (props) => { - const { children, class: className, style, id, ref, ...rest } = props - return
{children}
-} +export const H5 = define('H5', { + base: 'h5', + fontSize: theme('fontSize-sm'), + fontWeight: 500, + color: theme('colors-fg'), +}) -export const Text: FC = (props) => { - const { children, class: className, style, id, ref, ...rest } = props - return

{children}

-} +export const Text = define('Text', { + base: 'p', + fontSize: theme('fontSize-sm'), + color: theme('colors-fg'), +}) -export const SmallText: FC = (props) => { - const { children, class: className, style, id, ref, ...rest } = props - return

{children}

-} +export const SmallText = define('SmallText', { + base: 'p', + fontSize: theme('fontSize-xs'), + color: theme('colors-fgMuted'), +}) export const Test = () => { return ( -
- {/* API Usage Examples */} - Heading 1', - '

Heading 2

', - 'Regular text', - 'Small text', - ]} - /> - -
+
+

Heading 1 (24px, bold)

Heading 2 (20px, bold)

Heading 3 (18px, semibold)

@@ -61,16 +61,16 @@ export const Test = () => { Small text (12px)
-
+

Custom Styling

- Blue text with custom color - Bold italic text - + Blue text with custom color + Bold italic text + Red uppercase small text
-
+

Typography Example

Article Title

@@ -81,7 +81,7 @@ export const Test = () => { Multiple paragraphs can be stacked together to create readable content with consistent styling throughout your application. - Last updated: Today + Last updated: Today
) diff --git a/src/theme.ts b/src/theme.ts new file mode 100644 index 0000000..b9e4148 --- /dev/null +++ b/src/theme.ts @@ -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, +}) diff --git a/src/types.ts b/src/types.ts index aea1847..aa3b657 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,3 @@ -import "hono/jsx" -import type { JSX } from "hono/jsx" - export type TailwindSize = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96 /** @@ -9,6 +6,6 @@ export type TailwindSize = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 export type CommonHTMLProps = { class?: string id?: string - style?: JSX.CSSProperties + style?: Record ref?: any } diff --git a/test/layout.tsx b/test/layout.tsx index d10980f..315524a 100644 --- a/test/layout.tsx +++ b/test/layout.tsx @@ -1,5 +1,15 @@ import "hono/jsx" import { raw } from "hono/html" +import { Styles } from "forge" +import { + DarkModeToggle, + PageTitle, + HomeLink, + NavLink, + NavList, + NavItem, + Body, +} from "../src/layout" type LayoutProps = { title: string @@ -14,6 +24,7 @@ export const Layout = ({ title, children, showHomeLink = true }: LayoutProps) => {title} - Howl UI + - - + -
+ {showHomeLink && ( - + 🏠 - + )} {title} -
+ {children} - + ) } + +// Export nav components for use in server.tsx +export { NavLink, NavList, NavItem } diff --git a/test/server.tsx b/test/server.tsx index a7d69c2..d7a62fb 100644 --- a/test/server.tsx +++ b/test/server.tsx @@ -2,7 +2,7 @@ import { Hono } from 'hono' import { readdirSync } from 'fs' import { join } from 'path' import { capitalize } from './utils' -import { Layout } from './layout' +import { Layout, NavLink, NavList, NavItem } from './layout' const port = process.env.PORT ?? '3100' const app = new Hono() @@ -26,17 +26,15 @@ app.get('/:file', async c => { app.get('/', c => { return c.html( -
-
    - {testFiles().map(x => ( -
  • - - {x} - -
  • - ))} -
-
+ + {testFiles().map(x => ( + + + {x} + + + ))} +
) }) @@ -51,4 +49,4 @@ function testFiles(): string[] { export default { fetch: app.fetch, port -} \ No newline at end of file +}