From 6c3b3e80b025c9db9ff44063303b010caf0e58ca Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sat, 29 Nov 2025 22:38:15 -0800 Subject: [PATCH] gucci --- README.md | 42 +++++++++++++++++++- src/avatar.tsx | 30 ++++++++------- src/box.tsx | 84 ++++++++++++++++++++++++++++++++++++++++ src/button.tsx | 16 ++++---- src/divider.tsx | 8 ++-- src/grid.tsx | 26 ++++++------- src/icon.tsx | 94 +++++++++++++++++++++++---------------------- src/image.tsx | 54 +++++++++++++------------- src/index.tsx | 6 +++ src/input.tsx | 24 ++++++------ src/placeholder.tsx | 52 +++++++++++++------------ src/section.tsx | 48 +++++++++++++++++++++++ src/select.tsx | 22 ++++++----- src/stack.tsx | 23 ++++++----- src/text.tsx | 73 +++++++++++++++++++++++++++++++++++ test/server.tsx | 4 +- 16 files changed, 436 insertions(+), 170 deletions(-) create mode 100644 src/box.tsx create mode 100644 src/section.tsx create mode 100644 src/text.tsx diff --git a/README.md b/README.md index a2330f4..22e1571 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,46 @@ # 🐺 howl -Howl is a fork of `werewolf-ui`, without any Tailwind. +Howl is a fork of `werewolf-ui`, without any Tailwind. A minimal, zero-dependency React component library. + +## Installation + +```bash +bun install howl +``` + +## Usage + +```tsx +import { Button, VStack, Text } from "howl" + +function App() { + return ( + + Hello, world! + + + ) +} +``` + +## Components + +- **Avatar** - Profile image component +- **Box** - Container components (Box, RedBox, GreenBox, BlueBox, GrayBox) +- **Button** - Button component +- **Divider** - Horizontal divider +- **Grid** - Grid layout +- **Icon** - Icon display using lucide-static +- **IconLink** - Icon with link functionality +- **Image** - Image component +- **Input** - Text input field +- **Placeholder** - Placeholder component +- **Section** - Section container +- **Select** - Dropdown select input +- **Stack** - Layout components (VStack, HStack) +- **Text** - Text components (H1, H2, H3, H4, H5, Text, SmallText) + +## Development ```bash bun install diff --git a/src/avatar.tsx b/src/avatar.tsx index f02ea6f..457c1f5 100644 --- a/src/avatar.tsx +++ b/src/avatar.tsx @@ -1,3 +1,5 @@ +import { Section } from "./section" +import { H2, Text } from "./text" import "hono/jsx" import type { FC, JSX } from "hono/jsx" import { VStack, HStack } from "./stack" @@ -32,17 +34,17 @@ export const Test = () => { ] return ( - +
{/* Size variations */} -

Size Variations

+

Size Variations

{[24, 32, 48, 64, 96].map((size) => ( -

+ {size}x{size} -

+
))}
@@ -50,27 +52,27 @@ export const Test = () => { {/* Rounded vs Square */} -

Rounded vs Square

+

Rounded vs Square

-

Square

+ Square
-

Rounded

+ Rounded
{/* Different images */} -

Different Images

+

Different Images

{sampleImages.map((src, index) => ( -

Image {index + 1}

+ Image {index + 1}
))}
@@ -78,7 +80,7 @@ export const Test = () => { {/* Custom styles */} -

With Custom Styles

+

With Custom Styles

{ style={{ border: "4px solid #3b82f6" }} alt="With border" /> -

With Border

+ With Border
{ style={{ boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)" }} alt="With shadow" /> -

With Shadow

+ With Shadow
{ style={{ border: "4px solid #22c55e", boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)" }} alt="Border + shadow" /> -

Border + Shadow

+ Border + Shadow
-
+
) } diff --git a/src/box.tsx b/src/box.tsx new file mode 100644 index 0000000..0153864 --- /dev/null +++ b/src/box.tsx @@ -0,0 +1,84 @@ +import "hono/jsx" +import type { FC, PropsWithChildren, JSX } from "hono/jsx" + +type BoxProps = PropsWithChildren & { + bg?: string + color?: string + p?: number + style?: JSX.CSSProperties +} + +export const Box: FC = ({ children, bg, color, p, style }) => { + const boxStyle: JSX.CSSProperties = { + backgroundColor: bg, + color: color, + padding: p ? `${p}px` : undefined, + ...style, + } + + return
{children}
+} + +// 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 Test = () => { + return ( +
+
+

Box Component

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

Color Variants

+
+ Red Box + Green Box + Blue Box + Gray Box +
+
+ +
+

Nested Boxes

+ + + + Nested boxes demonstration + + + +
+
+ ) +} diff --git a/src/button.tsx b/src/button.tsx index 7f5efbb..431f093 100644 --- a/src/button.tsx +++ b/src/button.tsx @@ -1,6 +1,8 @@ import "hono/jsx" import type { JSX, FC } from "hono/jsx" 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" @@ -68,7 +70,7 @@ export const Button: FC = (props) => { ...baseStyles, ...variantStyles[variant], ...sizeStyles[size], - ...(style || {}), + ...(style as JSX.CSSProperties), } return @@ -91,7 +93,7 @@ export const Test = () => { {/* Sizes */} -

Button Sizes

+

Button Sizes

@@ -101,7 +103,7 @@ export const Test = () => { {/* With custom content */} -

Custom Content

+

Custom Content

-
+ ) } diff --git a/src/divider.tsx b/src/divider.tsx index decedf4..b6032f1 100644 --- a/src/divider.tsx +++ b/src/divider.tsx @@ -1,3 +1,5 @@ +import { Section } from "./section" +import { H2 } from "./text" import "hono/jsx" import type { FC, PropsWithChildren, JSX } from "hono/jsx" import { VStack } from "./stack" @@ -41,8 +43,8 @@ export const Divider: FC = ({ children, style }) => { export const Test = () => { return ( - -

Divider Examples

+
+

Divider Examples

Would you like to continue

@@ -56,6 +58,6 @@ export const Test = () => {

So cool, so straight!

- +
) } diff --git a/src/grid.tsx b/src/grid.tsx index 5741ee0..9ec3fe6 100644 --- a/src/grid.tsx +++ b/src/grid.tsx @@ -3,6 +3,8 @@ import "hono/jsx" import type { FC, PropsWithChildren, JSX } from "hono/jsx" import { VStack } from "./stack" import { Button } from "./button" +import { Section } from "./section" +import { H2, H3 } from "./text" type GridProps = PropsWithChildren & { cols?: GridCols @@ -69,13 +71,13 @@ const justifyItemsMap = { export const Test = () => { return ( - +
-

Grid Examples

+

Grid Examples

{/* Simple 3-column grid */} -

Simple 3 columns: cols=3

+

Simple 3 columns: cols=3

Item 1
Item 2
@@ -88,9 +90,7 @@ export const Test = () => { {/* Responsive grid */} -

- Responsive: cols={sm: 1, md: 2, lg: 3} -

+

Responsive: cols={sm: 1, md: 2, lg: 3}

Card 1
Card 2
@@ -101,9 +101,7 @@ export const Test = () => { {/* More responsive examples */} -

- More responsive: cols={sm: 2, lg: 4, xl: 6} -

+

More responsive: cols={sm: 2, lg: 4, xl: 6}

Item A
Item B
@@ -116,7 +114,7 @@ export const Test = () => { {/* Payment method example */} -

Payment buttons example

+

Payment buttons example

) } diff --git a/src/icon.tsx b/src/icon.tsx index b485976..c51e81d 100644 --- a/src/icon.tsx +++ b/src/icon.tsx @@ -2,7 +2,9 @@ import "hono/jsx" import type { FC, JSX } from "hono/jsx" import * as icons from "lucide-static" import { Grid } from "./grid" -import { VStack, HStack } from "./stack" +import { VStack } from "./stack" +import { Section } from "./section" +import { H2, Text } from "./text" export type IconName = keyof typeof icons @@ -69,100 +71,100 @@ export const IconLink: FC = (props) => { export const Test = () => { return ( -
+
{/* === ICON TESTS === */} {/* Size variations */} -
-

Icon Size Variations

+ +

Icon Size Variations

{([3, 4, 5, 6, 8, 10, 12, 16] as const).map((size) => ( -
+ -

{size}

-
+ {size} + ))}
-
+ {/* Styling with CSS classes */} -
-

Styling with CSS Classes

+ +

Styling with CSS Classes

-

Default

+ Default
-

Blue Color

+ Blue Color
-

Thin Stroke

+ Thin Stroke
-

Filled

+ Filled
-

Hover Effect

+ Hover Effect
-
+ {/* Advanced styling */} -
-

Advanced Styling

+ +

Advanced Styling

-

Filled Heart

+ Filled Heart
-

Thick Stroke

+ Thick Stroke
-

Sun Icon

+ Sun Icon
-

Drop Shadow

+ Drop Shadow
-
+ {/* === ICON LINK TESTS === */} {/* Basic icon links */} -
-

Icon Links

+ +

Icon Links

-
+ -

Home Link

-
-
+ Home Link + + -

External Link

-
-
+ External Link + + -

Email Link

-
-
+ Email Link + + -

Phone Link

-
+ Phone Link +
-
+ {/* Styled icon links */} -
-

Styled Icon Links

+ +

Styled Icon Links

{ borderRadius: "8px", }} /> -

Button Style

+ Button Style
{ borderRadius: "9999px", }} /> -

Circle Border

+ Circle Border
-

Red Heart

+ Red Heart
-

Filled Star

+ Filled Star
-
-
+
+ ) } diff --git a/src/image.tsx b/src/image.tsx index f4aec23..72b29b7 100644 --- a/src/image.tsx +++ b/src/image.tsx @@ -1,3 +1,5 @@ +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" @@ -32,38 +34,38 @@ export const Test = () => { ] return ( - -

Image Examples

+
+

Image Examples

{/* Size variations */} -

Size Variations

+

Size Variations

64x64 -

64x64

+ 64x64
96x96 -

96x96

+ 96x96
128x128 -

128x128

+ 128x128
192x128 -

192x128

+ 192x128
{/* Object fit variations */} -

Object Fit Variations

-

+

Object Fit Variations

+ Same image with different object-fit values -

+
{ style={{ border: "1px solid black" }} alt="Object cover" /> -

object-fit: cover

+ object-fit: cover
{ style={{ border: "1px solid black", backgroundColor: "#f3f4f6" }} alt="Object contain" /> -

object-fit: contain

+ object-fit: contain
{ style={{ border: "1px solid black" }} alt="Object fill" /> -

object-fit: fill

+ object-fit: fill
{ style={{ border: "1px solid black", backgroundColor: "#f3f4f6" }} alt="Object scale-down" /> -

object-fit: scale-down

+ object-fit: scale-down
{ style={{ border: "1px solid black", backgroundColor: "#f3f4f6" }} alt="Object none" /> -

object-fit: none

+ object-fit: none
{/* Styling examples */} -

Styling Examples

+

Styling Examples

{ style={{ borderRadius: "8px", border: "4px solid #3b82f6" }} alt="Rounded with border" /> -

Rounded + Border

+ Rounded + Border
{ style={{ boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)" }} alt="With shadow" /> -

With Shadow

+ With Shadow
{ }} alt="Circular with effects" /> -

Circular + Effects

+ Circular + Effects
{/* Common use cases */} -

Common Use Cases

+

Common Use Cases

{/* Avatar */} -

Avatar

+

Avatar

{ {/* Card image */} -

Card Image

+

Card Image

Card image -
Card Title
-

Card description goes here

+
Card Title
+ Card description goes here
{/* Gallery grid */} -

Gallery Grid

+

Gallery Grid

{sampleImages.map((src, i) => ( {
-
+
) } diff --git a/src/index.tsx b/src/index.tsx index b2acd23..80bfb5d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -25,4 +25,10 @@ export type { SelectProps, SelectOption } from "./select" export { Placeholder } from "./placeholder" export { default as PlaceholderDefault } from "./placeholder" +export { H1, H2, H3, H4, H5, Text, SmallText } from "./text" + +export { Box, RedBox, GreenBox, BlueBox, GrayBox } from "./box" + +export { Section } from "./section" + export type { TailwindSize } from "./types" diff --git a/src/input.tsx b/src/input.tsx index 15c5713..f96ee3b 100644 --- a/src/input.tsx +++ b/src/input.tsx @@ -1,3 +1,5 @@ +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" @@ -18,7 +20,7 @@ export const Input: FC = (props) => { backgroundColor: "white", fontSize: "14px", outline: "none", - ...style, + ...(style as JSX.CSSProperties), } if (!children) { @@ -69,10 +71,10 @@ export const Input: FC = (props) => { export const Test = () => { return ( - +
{/* Basic inputs */} -

Basic Inputs

+

Basic Inputs

@@ -82,7 +84,7 @@ export const Test = () => { {/* Custom styling */} -

Custom Styling

+

Custom Styling

@@ -92,7 +94,7 @@ export const Test = () => { {/* With values */} -

With Values

+

With Values

@@ -101,7 +103,7 @@ export const Test = () => { {/* Disabled state */} -

Disabled State

+

Disabled State

@@ -110,7 +112,7 @@ export const Test = () => { {/* Label above */} -

Label Above

+

Label Above

Name @@ -124,7 +126,7 @@ export const Test = () => { {/* Label to the left */} -

Label Left

+

Label Left

Name @@ -140,7 +142,7 @@ export const Test = () => { {/* Label to the right */} -

Label Right

+

Label Right

Name @@ -156,7 +158,7 @@ export const Test = () => { {/* Horizontal layout */} -

Horizontal Layout

+

Horizontal Layout

First Last @@ -166,7 +168,7 @@ export const Test = () => { {/* Custom styling */} -

Custom Input Styling

+

Custom Input Styling

Custom Label diff --git a/src/placeholder.tsx b/src/placeholder.tsx index 0c4332b..e5216c1 100644 --- a/src/placeholder.tsx +++ b/src/placeholder.tsx @@ -1,3 +1,5 @@ +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" @@ -34,19 +36,19 @@ export const Placeholder = { export const Test = () => { return ( - +
{/* === AVATAR TESTS === */} {/* Show all available avatar styles */} -

+

All Avatar Styles ({allStyles.length} total) -

+ {allStyles.map((style) => ( -

{style}

+ {style}
))}
@@ -54,12 +56,12 @@ export const Test = () => { {/* Avatar size variations */} -

Avatar Size Variations

+

Avatar Size Variations

{[24, 32, 48, 64].map((size) => ( -

{size}px

+ {size}px
))}
@@ -67,41 +69,41 @@ export const Test = () => { {/* Avatar styling combinations */} -

Avatar Styling Options

+

Avatar Styling Options

-

Rounded + Background

+ Rounded + Background
-

Rounded + Transparent

+ Rounded + Transparent
-

Square + Background

+ Square + Background
-

Square + Transparent

+ Square + Transparent
{/* Avatar seed variations */} -

+

Avatar Seeds (Same Style, Different People) -

+ {["alice", "bob", "charlie", "diana"].map((seed) => ( -

"{seed}"

+ "{seed}"
))}
@@ -109,11 +111,11 @@ export const Test = () => { {/* === IMAGE TESTS === */} -

Placeholder Images

+

Placeholder Images

{/* Size variations */} -

Size Variations

+

Size Variations

{[ { width: 100, height: 100 }, @@ -123,9 +125,9 @@ export const Test = () => { ].map(({ width, height }) => ( -

+ {width}×{height} -

+
))}
@@ -133,14 +135,14 @@ export const Test = () => { {/* Different seeds - show variety */} -

+

Different Images (Different Seeds) -

+ {[1, 2, 3, 4, 5].map((seed) => ( -

Seed {seed}

+ Seed {seed}
))}
@@ -148,7 +150,7 @@ export const Test = () => { {/* With custom styles */} -

With Custom Styles

+

With Custom Styles

{ objectFit="cover" style={{ borderRadius: "8px", border: "4px solid #3b82f6" }} /> -

Rounded + Border

+ Rounded + Border
{ objectFit="cover" style={{ boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)" }} /> -

With Shadow

+ With Shadow
{ boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)", }} /> -

Circular + Effects

+ Circular + Effects
diff --git a/src/section.tsx b/src/section.tsx new file mode 100644 index 0000000..670219f --- /dev/null +++ b/src/section.tsx @@ -0,0 +1,48 @@ +import "hono/jsx" +import type { FC, PropsWithChildren, JSX } from "hono/jsx" +import { VStack } from "./stack" +import type { TailwindSize } from "./types" + +type SectionProps = PropsWithChildren & { + gap?: TailwindSize + maxWidth?: string + style?: JSX.CSSProperties +} + +export const Section: FC = ({ children, gap = 8, maxWidth, style }) => { + return ( + + {children} + + ) +} + +export const Test = () => { + return ( +
+
+

Default Section

+

This is a section with default gap (8)

+

It has padding and vertical spacing between children

+
+ +
+

Compact Section

+

This section has a smaller gap (4)

+

Items are closer together

+
+ +
+

Spacious Section

+

This section has a larger gap (12)

+

Items have more breathing room

+
+ +
+

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 085c634..5e9a9e2 100644 --- a/src/select.tsx +++ b/src/select.tsx @@ -1,3 +1,5 @@ +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" @@ -158,10 +160,10 @@ export const Test = () => { ] return ( - +
{/* Basic selects */} -

Basic Selects

+

Basic Selects

@@ -171,7 +173,7 @@ export const Test = () => { {/* With values */} -

With Values

+

With Values

@@ -180,7 +182,7 @@ export const Test = () => { {/* Disabled state */} -

Disabled State

+

Disabled State

Birth Month @@ -219,7 +221,7 @@ export const Test = () => { {/* Label to the left */} -

Label Left

+

Label Left

Month @@ -248,7 +250,7 @@ export const Test = () => { {/* Horizontal layout (like card form) */} -

Horizontal Layout

+

Horizontal Layout

-
+
) } diff --git a/src/stack.tsx b/src/stack.tsx index ed3bc53..f631b0f 100644 --- a/src/stack.tsx +++ b/src/stack.tsx @@ -2,6 +2,9 @@ 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" export const VStack: FC = (props) => { return ( @@ -64,10 +67,10 @@ export const Test = () => { const crossAxisOpts: CrossAxisOpts[] = ["start", "center", "end", "stretch", "baseline"] return ( - +
{/* HStack layout matrix */} -

HStack Layout

+

HStack Layout

{/* Header row: blank + h labels */} @@ -90,9 +93,9 @@ export const Test = () => { v={v} style={{ backgroundColor: "#f3f4f6", padding: "8px", height: "96px", border: "1px solid #9ca3af" }} > -
Aa
-
Aa
-
Aa
+ Aa + Aa + Aa )), ])} @@ -102,7 +105,7 @@ export const Test = () => { {/* VStack layout matrix */} -

VStack Layout

+

VStack Layout

{/* Header row: blank + h labels */} @@ -125,16 +128,16 @@ export const Test = () => { h={h} style={{ backgroundColor: "#f3f4f6", padding: "8px", height: "168px", border: "1px solid #9ca3af" }} > -
Aa
-
Aa
-
Aa
+ Aa + Aa + Aa )), ])}
- +
) } diff --git a/src/text.tsx b/src/text.tsx new file mode 100644 index 0000000..a089f20 --- /dev/null +++ b/src/text.tsx @@ -0,0 +1,73 @@ +import "hono/jsx" +import type { FC, PropsWithChildren, JSX } from "hono/jsx" + +type TextProps = PropsWithChildren & { + style?: JSX.CSSProperties +} + +export const H1: FC = ({ children, style }) => ( +

{children}

+) + +export const H2: FC = ({ children, style }) => ( +

{children}

+) + +export const H3: FC = ({ children, style }) => ( +

{children}

+) + +export const H4: FC = ({ children, style }) => ( +

{children}

+) + +export const H5: FC = ({ children, style }) => ( +
{children}
+) + +export const Text: FC = ({ children, style }) => ( +

{children}

+) + +export const SmallText: FC = ({ children, style }) => ( +

{children}

+) + +export const Test = () => { + return ( +
+
+

Heading 1 (24px, bold)

+

Heading 2 (20px, bold)

+

Heading 3 (18px, semibold)

+

Heading 4 (16px, semibold)

+
Heading 5 (14px, medium)
+ Regular text (14px) + Small text (12px) +
+ +
+

Custom Styling

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

Typography Example

+

Article Title

+ + This is a paragraph of regular text. It demonstrates how the Text component can be used for body + content in articles, blog posts, or any other long-form content. + + + Multiple paragraphs can be stacked together to create readable content with consistent styling + throughout your application. + + Last updated: Today +
+
+ ) +} diff --git a/test/server.tsx b/test/server.tsx index 5596087..190516c 100644 --- a/test/server.tsx +++ b/test/server.tsx @@ -25,8 +25,8 @@ app.get('/', c => { }) function testFiles(): string[] { - return readdirSync('./test') - .filter(x => x.endsWith('.tsx') && !x.startsWith('server')) + return readdirSync('./src') + .filter(x => x.endsWith('.tsx') && !x.startsWith('index')) .map(x => x.replace('.tsx', '')) .sort() }