werewolf
This commit is contained in:
parent
56262f1f6f
commit
49a8471628
|
|
@ -1 +1 @@
|
|||
1.2.16
|
||||
1.2.18
|
||||
2
packages/werewolf-ui/.gitattributes
vendored
Normal file
2
packages/werewolf-ui/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
34
packages/werewolf-ui/.gitignore
vendored
Normal file
34
packages/werewolf-ui/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
5
packages/werewolf-ui/README.md
Normal file
5
packages/werewolf-ui/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# werewolfUI <howl>
|
||||
|
||||
- h1, h2, h3, etc... are kind of annoying. They are too generic.
|
||||
- I didn't make "card" beccause it didn't feel like it added enough.
|
||||
- Importing all these components always annoys me. Import them all as a single object (like $ or W or just one letter).
|
||||
24
packages/werewolf-ui/package.json
Normal file
24
packages/werewolf-ui/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "werewolfUI",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.tsx",
|
||||
"module": "src/index.tsx",
|
||||
"scripts": {
|
||||
"dev": "bun --hot src/server.tsx",
|
||||
"start": "NODE_ENV=production bun src/server.tsx"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"printWidth": 110
|
||||
},
|
||||
"dependencies": {
|
||||
"hono": "^4.8.3",
|
||||
"lucide-static": "^0.525.0",
|
||||
"tailwindcss": "^4.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
}
|
||||
}
|
||||
315
packages/werewolf-ui/src/examples/cards.tsx
Normal file
315
packages/werewolf-ui/src/examples/cards.tsx
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
import { HStack, VStack } from "@/lib/stack"
|
||||
import Placeholder from "@/lib/placeholder"
|
||||
import { Icon, IconLink, IconName } from "@/lib/icon"
|
||||
import { Button } from "@/lib/button"
|
||||
import { Input } from "@/lib/input"
|
||||
import { Select } from "@/lib/select"
|
||||
import { Grid } from "@/lib/grid"
|
||||
import { Break } from "@/lib/break"
|
||||
|
||||
export const Cards = () => {
|
||||
return (
|
||||
<Grid>
|
||||
<div>
|
||||
<PaymentMethod />
|
||||
<CreateAccount />
|
||||
<TeamCard />
|
||||
</div>
|
||||
<div>
|
||||
<TeamMembers />
|
||||
<ShareDocument />
|
||||
<DateCard />
|
||||
<Notification />
|
||||
</div>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
const TeamMembers = () => {
|
||||
/*
|
||||
team_members = [("Sofia Davis", "m@example.com", "Owner"),("Jackson Lee", "p@example.com", "Member"),]
|
||||
def TeamMemberRow(name, email, role):
|
||||
return DivFullySpaced(
|
||||
DivLAligned(
|
||||
DiceBearAvatar(name, 10,10),
|
||||
Div(P(name, cls=(TextT.sm, TextT.medium)),
|
||||
P(email, cls=TextPresets.muted_sm))),
|
||||
Button(role, UkIcon('chevron-down', cls='ml-4')),
|
||||
DropDownNavContainer(map(NavCloseLi, [
|
||||
A(Div('Viewer', NavSubtitle('Can view and comment.'))),
|
||||
A(Div('Developer', NavSubtitle('Can view, comment and edit.'))),
|
||||
A(Div('Billing', NavSubtitle('Can view, comment and manage billing.'))),
|
||||
A(Div('Owner', NavSubtitle('Admin-level access to all resources.')))])))
|
||||
|
||||
TeamMembers = Card(*[TeamMemberRow(*member) for member in team_members],
|
||||
header = (H4('Team Members'),Subtitle('Invite your team members to collaborate.')))
|
||||
*/
|
||||
|
||||
const teamMembers = [
|
||||
{ name: "Sofia Davis", email: "m@example.com", role: "owner" },
|
||||
{ name: "Jackson Lee", email: "p@example.com", role: "member" },
|
||||
]
|
||||
|
||||
const roleOptions = [
|
||||
{ value: "owner", label: "Owner" },
|
||||
{ value: "developer", label: "Developer" },
|
||||
{ value: "billing", label: "Billing" },
|
||||
{ value: "member", label: "Member" },
|
||||
]
|
||||
|
||||
return (
|
||||
<VStack gap="1" class="m-3 p-4 rounded-xs border border-gray-300">
|
||||
<h2>Team Members</h2>
|
||||
<h5 class="text-gray-500">Invite your team members to collaborate.</h5>
|
||||
{teamMembers.map((member) => (
|
||||
<HStack h="between" gap="3" class="w-full p-2 hover:bg-gray-50 rounded">
|
||||
<Placeholder.Avatar size="10" rounded seed={member.name} />
|
||||
<div class="flex-1">
|
||||
<p class="font-medium">{member.name}</p>
|
||||
<p class="text-sm text-gray-500">{member.email}</p>
|
||||
</div>
|
||||
<Select options={roleOptions} value={member.role} />
|
||||
</HStack>
|
||||
))}
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
const CreateAccount = () => {
|
||||
/*
|
||||
CreateAccount = Card(
|
||||
Grid(Button(DivLAligned(UkIcon('github'),Div('Github'))),Button('Google')),
|
||||
DividerSplit("OR CONTINUE WITH", text_cls=TextPresets.muted_sm),
|
||||
LabelInput('Email', id='email', placeholder='m@example.com'),
|
||||
LabelInput('Password', id='password',placeholder='Password', type='Password'),
|
||||
header=(H3('Create an Account'),Subtitle('Enter your email below to create your account')),
|
||||
footer=Button('Create Account',cls=(ButtonT.primary,'w-full')))
|
||||
*/
|
||||
|
||||
return (
|
||||
<>
|
||||
<VStack gap="4" class="m-3 p-4 rounded-xs border border-gray-300">
|
||||
<h2>Create an Account</h2>
|
||||
<h5>Enter your email below to create your account</h5>
|
||||
<HStack h="evenly" gap="4">
|
||||
<Button variant="outline" class="grow">
|
||||
<Icon name="Github" />
|
||||
Github
|
||||
</Button>
|
||||
<Button variant="outline" class="grow">
|
||||
<Icon name="Mail" />
|
||||
Google
|
||||
</Button>
|
||||
</HStack>
|
||||
<Break>OR CONTINUE WITH</Break>
|
||||
<Input id="email" placeholder="m@example.com">
|
||||
Email
|
||||
</Input>
|
||||
<Input id="password" placeholder="Password" type="Password">
|
||||
Password
|
||||
</Input>
|
||||
<Button variant="primary">Create Account</Button>
|
||||
</VStack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const PaymentMethod = () => {
|
||||
/*
|
||||
Grid(Button(DivCentered(Card1Svg, "Card"), cls='h-20 border-2 border-primary'),
|
||||
Button(DivCentered(PaypalSvg, "PayPal"), cls='h-20'),
|
||||
Button(DivCentered(AppleSvg, "Apple"), cls='h-20')),
|
||||
Form(LabelInput('Name', id='name', placeholder='John Doe'),
|
||||
LabelInput('Card Number', id='card_number', placeholder='m@example.com'),
|
||||
Grid(LabelSelect(*Options(*calendar.month_name[1:],selected_idx=0),label='Expires',id='expire_month'),
|
||||
LabelSelect(*Options(*range(2024,2030),selected_idx=0), label='Year', id='expire_year'),
|
||||
LabelInput('CVV', id='cvv',placeholder='CVV', cls='mt-0'))),
|
||||
header=(H3('Payment Method'),Subtitle('Add a new payment method to your account.')))
|
||||
*/
|
||||
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" },
|
||||
]
|
||||
|
||||
const years = Array.from({ length: 10 }, (_, i) => ({
|
||||
value: String(2024 + i),
|
||||
label: String(2024 + i),
|
||||
}))
|
||||
|
||||
return (
|
||||
<VStack gap="1" class="m-3 p-4 rounded-xs border border-gray-300">
|
||||
<h2>Payment Method</h2>
|
||||
<h5 class="text-gray-500">Add a new payment method to your account.</h5>
|
||||
<HStack h="evenly" gap="4" class="mt-3">
|
||||
<Button variant="outline" class="grow h-20 flex flex-col">
|
||||
<Icon name="CreditCard" />
|
||||
<p class="text-xs">Card</p>
|
||||
</Button>
|
||||
<Button variant="outline" class="grow h-20 flex flex-col">
|
||||
<Icon name="Apple" />
|
||||
<p class="text-xs">Apple</p>
|
||||
</Button>
|
||||
<Button variant="outline" class="grow h-20 flex flex-col">
|
||||
<Icon name="DollarSign" />
|
||||
<p class="text-xs">PayPal</p>
|
||||
</Button>
|
||||
</HStack>
|
||||
<VStack gap="4">
|
||||
<Input id="name" placeholder="John Doe">
|
||||
Name
|
||||
</Input>
|
||||
<Input id="card_number" placeholder="1234 5678 9012 3456">
|
||||
Card Number
|
||||
</Input>
|
||||
<HStack gap="4">
|
||||
<Select id="expire_month" options={months} placeholder="MM">
|
||||
Expires
|
||||
</Select>
|
||||
<Select id="expire_year" options={years} placeholder="YYYY">
|
||||
Year
|
||||
</Select>
|
||||
<Input id="cvv" placeholder="CVV">
|
||||
CVV
|
||||
</Input>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
const TeamCard = () => {
|
||||
/*
|
||||
TeamCard = Card(
|
||||
DivLAligned(
|
||||
DiceBearAvatar("Isaac Flath", h=24, w=24),
|
||||
Div(H3("Isaac Flath"), P("Library Creator"))),
|
||||
footer=DivFullySpaced(
|
||||
DivHStacked(UkIcon("map-pin", height=16), P("Alexandria, VA")),
|
||||
DivHStacked(*(UkIconLink(icon, height=16) for icon in ("mail", "linkedin", "github")))),
|
||||
cls=CardT.hover)
|
||||
*/
|
||||
return (
|
||||
<VStack gap="3" class="m-3 p-4 rounded-xs border border-gray-300">
|
||||
<HStack gap="2" v="center">
|
||||
<Placeholder.Avatar size="20" rounded seed="Isaac Flath" />
|
||||
<div>
|
||||
<h2>Isaac Flath</h2>
|
||||
<h4>Library Creator</h4>
|
||||
</div>
|
||||
</HStack>
|
||||
<HStack h="between" gap="3" v="center">
|
||||
<Icon name="MapPin" size={16} />
|
||||
<p class="grow">Alexandria, VA</p>
|
||||
<IconLink name="Mail" size={16} />
|
||||
<IconLink name="Linkedin" size={16} />
|
||||
<IconLink name="Github" size={16} />
|
||||
</HStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
const ShareDocument = () => {
|
||||
/*
|
||||
ShareDocument = Card(
|
||||
DivLAligned(Input(value='http://example.com/link/to/document'),Button('Copy link', cls='whitespace-nowrap')),
|
||||
Divider(),
|
||||
H4('People with access', cls=TextPresets.bold_sm),
|
||||
*[TeamMemberRow(*member) for member in team_members],
|
||||
header = (H4('Share this document'),Subtitle('Anyone with the link can view this document.')))
|
||||
|
||||
DateCard = Card(Button('Jan 20, 2024 - Feb 09, 2024'))
|
||||
|
||||
section_content =(('bell','Everything',"Email digest, mentions & all activity."),
|
||||
('user',"Available","Only mentions and comments"),
|
||||
('ban', "Ignoring","Turn of all notifications"))
|
||||
*/
|
||||
|
||||
const teamMembers = [
|
||||
{ name: "Olivia Martin", email: "m@example.com", role: "r+w" },
|
||||
{ name: "Isabella Nguyen", email: "b@example.com", role: "r" },
|
||||
{ name: "Sofia Davis", email: "p@example.com", role: "r" },
|
||||
]
|
||||
|
||||
const options = [
|
||||
{ value: "r+w", label: "Read and write access" },
|
||||
{ value: "r", label: "Read-only access" },
|
||||
{ value: "p", label: "Pending access", disabled: true },
|
||||
]
|
||||
|
||||
return (
|
||||
<VStack gap="1" class="m-3 p-4 rounded-xs border border-gray-300">
|
||||
<HStack h="start" gap="3">
|
||||
<Input value="http://example.com/link/to/document" class="grow" />
|
||||
<Button variant="outline">Copy link</Button>
|
||||
</HStack>
|
||||
<Break>
|
||||
<Icon name="Circle" size={16} class="stroke-gray-300" />
|
||||
</Break>
|
||||
<h4 class="font-bold">People with access</h4>
|
||||
{teamMembers.map((member) => (
|
||||
<HStack h="between" gap="3" class="w-full p-2 hover:bg-gray-50 rounded">
|
||||
<Placeholder.Avatar size="10" rounded seed={member.name} />
|
||||
<div class="flex-1">
|
||||
<p class="font-medium">{member.name}</p>
|
||||
<p class="text-sm text-gray-500">{member.email}</p>
|
||||
</div>
|
||||
<Select options={options} value={member.role} />
|
||||
</HStack>
|
||||
))}
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
const DateCard = () => {
|
||||
const date: string = new Date().toISOString().slice(0, 16)
|
||||
return (
|
||||
<VStack gap="3" class="m-3 p-4 rounded-xs border border-gray-300">
|
||||
<Input type="datetime-local" value={date}>
|
||||
Select Date
|
||||
</Input>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
const Notification = () => {
|
||||
/*
|
||||
Notifications = Card(
|
||||
NavContainer(
|
||||
*[NotificationRow(*row) for row in section_content],
|
||||
cls=NavT.secondary),
|
||||
header = (H4('Notification'),Subtitle('Choose what you want to be notified about.')),
|
||||
body_cls='pt-0')
|
||||
*/
|
||||
|
||||
const sectionContent: { icon: IconName; title: string; description: string }[] = [
|
||||
{ icon: "Bell", title: "Everything", description: "Email digest, mentions & all activity." },
|
||||
{ icon: "User", title: "Available", description: "Only mentions and comments" },
|
||||
{ icon: "Ban", title: "Ignoring", description: "Turn off all notifications" },
|
||||
]
|
||||
|
||||
return (
|
||||
<VStack gap="3" class="m-3 p-4 rounded-xs border border-gray-300">
|
||||
<h2>Notification</h2>
|
||||
<h4>Choose what you want to be notified about.</h4>
|
||||
{sectionContent.map((item) => (
|
||||
<HStack gap="3" v="center">
|
||||
<Icon name={item.icon} size={16} />
|
||||
<div class="flex-1">
|
||||
<p class="text-sm">{item.title}</p>
|
||||
<p class="text-sm text-gray-400">{item.description}</p>
|
||||
</div>
|
||||
</HStack>
|
||||
))}
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
42
packages/werewolf-ui/src/index.css
Normal file
42
packages/werewolf-ui/src/index.css
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-background: #ffffff;
|
||||
--color-foreground: #09090b;
|
||||
|
||||
--color-primary: #18181b;
|
||||
--color-primary-foreground: #fafafa;
|
||||
|
||||
--color-secondary: #f4f4f5;
|
||||
--color-secondary-foreground: #18181b;
|
||||
|
||||
--color-muted: #f4f4f5;
|
||||
--color-muted-foreground: #71717a;
|
||||
|
||||
--color-destructive: #ef4444;
|
||||
--color-destructive-foreground: #fafafa;
|
||||
|
||||
--color-border: #e4e4e7;
|
||||
--color-input: #e4e4e7;
|
||||
--color-ring: #18181b;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-2xl font-bold;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-xl font-bold;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-lg font-bold;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@apply text-xs;
|
||||
}
|
||||
109
packages/werewolf-ui/src/lib/avatar.tsx
Normal file
109
packages/werewolf-ui/src/lib/avatar.tsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import "hono/jsx"
|
||||
import { FC } from "hono/jsx"
|
||||
|
||||
export type AvatarProps = {
|
||||
src: string
|
||||
alt?: string
|
||||
class?: string
|
||||
size?: string // Tailwind size class (e.g., "8", "12", "16")
|
||||
rounded?: boolean
|
||||
}
|
||||
|
||||
export const Avatar: FC<AvatarProps> = (props) => {
|
||||
let { size = "8", rounded, class: className } = props
|
||||
|
||||
// Build class names
|
||||
const sizeClasses = [`w-${size}`, `h-${size}`]
|
||||
const roundedClass = rounded ? "rounded-full" : ""
|
||||
|
||||
const combinedClassName = [className, ...sizeClasses, roundedClass].filter(Boolean).join(" ")
|
||||
|
||||
return <img src={props.src} alt={props.alt} class={combinedClassName} />
|
||||
}
|
||||
|
||||
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",
|
||||
]
|
||||
|
||||
return (
|
||||
<div class="p-6 space-y-8">
|
||||
{/* Size variations */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Size Variations</h2>
|
||||
<div class="flex gap-4">
|
||||
{["6", "8", "12", "16", "24"].map((size) => (
|
||||
<div key={size} class="flex flex-col space-y-2">
|
||||
<Avatar src={sampleImages[0]} size={size} alt="Sample" />
|
||||
<p class="text-sm">
|
||||
w-{size} h-{size}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rounded vs Square */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Rounded vs Square</h2>
|
||||
<div class="flex gap-6">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Avatar src={sampleImages[0]} size="16" alt="Sample" />
|
||||
<p class="text-sm">Square</p>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Avatar src={sampleImages[0]} size="16" rounded alt="Sample" />
|
||||
<p class="text-sm">Rounded</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Different images */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Different Images</h2>
|
||||
<div class="flex gap-4">
|
||||
{sampleImages.map((src, index) => (
|
||||
<div key={src} class="flex flex-col space-y-2">
|
||||
<Avatar src={src} size="16" rounded alt={`Sample ${index + 1}`} />
|
||||
<p class="text-sm">Image {index + 1}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom classes */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">With Custom Classes</h2>
|
||||
<div class="flex gap-6">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Avatar
|
||||
src={sampleImages[0]}
|
||||
size="16"
|
||||
rounded
|
||||
class="border-4 border-blue-500"
|
||||
alt="With border"
|
||||
/>
|
||||
<p class="text-sm">With Border</p>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Avatar src={sampleImages[1]} size="16" rounded class="shadow-lg" alt="With shadow" />
|
||||
<p class="text-sm">With Shadow</p>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Avatar
|
||||
src={sampleImages[2]}
|
||||
size="16"
|
||||
rounded
|
||||
class="border-4 border-green-500 shadow-lg"
|
||||
alt="Border + shadow"
|
||||
/>
|
||||
<p class="text-sm">Border + Shadow</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
41
packages/werewolf-ui/src/lib/break.tsx
Normal file
41
packages/werewolf-ui/src/lib/break.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import "hono/jsx"
|
||||
import { FC, PropsWithChildren } from "hono/jsx"
|
||||
|
||||
type BreakProps = PropsWithChildren & {
|
||||
class?: string
|
||||
}
|
||||
|
||||
export const Break: FC<BreakProps> = ({ children, class: className }) => {
|
||||
return (
|
||||
<div class={`flex items-center my-4 ${className || ""}`}>
|
||||
<div class="flex-1 border-t border-gray-300"></div>
|
||||
{children && (
|
||||
<>
|
||||
<span class="px-3 text-sm text-gray-500 bg-white">{children}</span>
|
||||
<div class="flex-1 border-t border-gray-300"></div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Test = () => {
|
||||
return (
|
||||
<div class="p-4 space-y-8 max-w-md">
|
||||
<h2 class="text-lg font-bold mb-4">Break Examples</h2>
|
||||
|
||||
<div>
|
||||
<p>Would you like to continue</p>
|
||||
<Break>OR WOULD YOU LIKE TO</Break>
|
||||
<p>Submit to certain dealth</p>
|
||||
</div>
|
||||
|
||||
{/* Just a line */}
|
||||
<div>
|
||||
<p>Look a line 👇</p>
|
||||
<Break />
|
||||
<p>So cool, so straight!</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
105
packages/werewolf-ui/src/lib/button.tsx
Normal file
105
packages/werewolf-ui/src/lib/button.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import "hono/jsx"
|
||||
import { JSX, FC } from "hono/jsx"
|
||||
|
||||
export type ButtonProps = JSX.IntrinsicElements["button"] & {
|
||||
variant?: "primary" | "secondary" | "outline" | "ghost" | "destructive"
|
||||
size?: "sm" | "md" | "lg"
|
||||
}
|
||||
|
||||
export const Button: FC<ButtonProps> = (props) => {
|
||||
const { variant = "primary", size = "md", class: className, ...buttonProps } = props
|
||||
|
||||
const baseClasses = [
|
||||
"inline-flex",
|
||||
"items-center",
|
||||
"justify-center",
|
||||
"font-medium",
|
||||
"transition-colors",
|
||||
"focus-visible:outline-none",
|
||||
"focus-visible:ring-2",
|
||||
"focus-visible:ring-offset-2",
|
||||
"disabled:pointer-events-none",
|
||||
"disabled:opacity-50",
|
||||
"cursor-pointer",
|
||||
"rounded-sm",
|
||||
]
|
||||
|
||||
const variantClasses = {
|
||||
primary: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
outline: "border border-border bg-background text-foreground hover:bg-secondary",
|
||||
ghost: "text-foreground hover:bg-secondary",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "h-8 px-3 text-sm",
|
||||
md: "h-10 px-4 text-sm",
|
||||
lg: "h-12 px-6 text-base",
|
||||
}
|
||||
|
||||
const classes = [...baseClasses, variantClasses[variant], sizeClasses[size], className]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
|
||||
return <button {...buttonProps} class={classes} />
|
||||
}
|
||||
|
||||
export const Test = () => {
|
||||
return (
|
||||
<div class="p-6 space-y-8">
|
||||
{/* Variants */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Button Variants</h2>
|
||||
<div class="flex gap-4">
|
||||
<Button variant="primary">Primary</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Button variant="destructive">Destructive</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sizes */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Button Sizes</h2>
|
||||
<div class="flex items-end gap-4">
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="md">Medium</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* With custom content */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Custom Content</h2>
|
||||
<div class="flex gap-4">
|
||||
<Button variant="primary">
|
||||
<span>🚀</span>
|
||||
<span class="ml-2">Launch</span>
|
||||
</Button>
|
||||
<Button variant="outline" class="flex-col h-20 w-24">
|
||||
<span class="text-2xl">💳</span>
|
||||
<span class="text-xs mt-1">Card</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Native attributes work */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Native Attributes</h2>
|
||||
<div class="flex gap-4">
|
||||
<Button onClick={() => alert("Clicked!")} variant="primary">
|
||||
Click Me
|
||||
</Button>
|
||||
<Button disabled variant="secondary">
|
||||
Disabled
|
||||
</Button>
|
||||
<Button type="submit" variant="outline">
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
185
packages/werewolf-ui/src/lib/grid.tsx
Normal file
185
packages/werewolf-ui/src/lib/grid.tsx
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
import { TailwindSize } from "@/types"
|
||||
import "hono/jsx"
|
||||
import { FC, PropsWithChildren } from "hono/jsx"
|
||||
|
||||
type GridProps = PropsWithChildren & {
|
||||
cols?: GridCols
|
||||
gap?: TailwindSize
|
||||
v?: keyof typeof alignItemsClasses
|
||||
h?: keyof typeof justifyItemsClasses
|
||||
class?: string
|
||||
}
|
||||
|
||||
type GridCols = number | { sm?: number; md?: number; lg?: number; xl?: number }
|
||||
|
||||
export const Grid: FC<GridProps> = (props) => {
|
||||
const { cols = 2, gap = 4, v, h, class: className, children } = props
|
||||
const classes = [
|
||||
"grid",
|
||||
getColumnsClass(cols),
|
||||
gap && `gap-${gap}`,
|
||||
v && alignItemsClasses[v],
|
||||
h && justifyItemsClasses[h],
|
||||
className,
|
||||
]
|
||||
return <div class={classes.join(" ")}>{children}</div>
|
||||
}
|
||||
|
||||
function getColumnsClass(cols: GridCols): string {
|
||||
if (typeof cols === "number") {
|
||||
return `grid-cols-${cols}`
|
||||
}
|
||||
|
||||
const classes = []
|
||||
if (cols.sm) classes.push(`grid-cols-${cols.sm}`)
|
||||
if (cols.md) classes.push(`md:grid-cols-${cols.md}`)
|
||||
if (cols.lg) classes.push(`lg:grid-cols-${cols.lg}`)
|
||||
if (cols.xl) classes.push(`xl:grid-cols-${cols.xl}`)
|
||||
|
||||
return classes.length ? classes.join(" ") : "grid-cols-1"
|
||||
}
|
||||
|
||||
const alignItemsClasses = {
|
||||
start: "items-start",
|
||||
center: "items-center",
|
||||
end: "items-end",
|
||||
stretch: "items-stretch",
|
||||
} as const
|
||||
|
||||
const justifyItemsClasses = {
|
||||
start: "justify-items-start",
|
||||
center: "justify-items-center",
|
||||
end: "justify-items-end",
|
||||
stretch: "justify-items-stretch",
|
||||
} as const
|
||||
|
||||
export const Test = () => {
|
||||
return (
|
||||
<div class="p-4 space-y-8">
|
||||
<div>
|
||||
<h2 class="text-lg font-bold mb-4">Grid Examples</h2>
|
||||
|
||||
{/* Simple 3-column grid */}
|
||||
<div class="mb-6">
|
||||
<h3 class="text-md font-semibold mb-2">Simple 3 columns: cols=3</h3>
|
||||
<Grid cols={3} gap={4}>
|
||||
<div class="bg-red-200 p-4 text-center">Item 1</div>
|
||||
<div class="bg-green-200 p-4 text-center">Item 2</div>
|
||||
<div class="bg-blue-200 p-4 text-center">Item 3</div>
|
||||
<div class="bg-yellow-200 p-4 text-center">Item 4</div>
|
||||
<div class="bg-purple-200 p-4 text-center">Item 5</div>
|
||||
<div class="bg-pink-200 p-4 text-center">Item 6</div>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
{/* Responsive grid */}
|
||||
<div class="mb-6">
|
||||
<h3 class="text-md font-semibold mb-2">Responsive: cols={sm: 1, md: 2, lg: 3}</h3>
|
||||
<Grid cols={{ sm: 1, md: 2, lg: 3 }} gap={4}>
|
||||
<div class="bg-red-200 p-4 text-center">Card 1</div>
|
||||
<div class="bg-green-200 p-4 text-center">Card 2</div>
|
||||
<div class="bg-blue-200 p-4 text-center">Card 3</div>
|
||||
<div class="bg-yellow-200 p-4 text-center">Card 4</div>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
{/* More responsive examples */}
|
||||
<div class="mb-6">
|
||||
<h3 class="text-md font-semibold mb-2">More responsive: cols={sm: 2, lg: 4, xl: 6}</h3>
|
||||
<Grid cols={{ sm: 2, lg: 4, xl: 6 }} gap={4}>
|
||||
<div class="bg-red-200 p-4 text-center">Item A</div>
|
||||
<div class="bg-green-200 p-4 text-center">Item B</div>
|
||||
<div class="bg-blue-200 p-4 text-center">Item C</div>
|
||||
<div class="bg-yellow-200 p-4 text-center">Item D</div>
|
||||
<div class="bg-purple-200 p-4 text-center">Item E</div>
|
||||
<div class="bg-pink-200 p-4 text-center">Item F</div>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
{/* Payment method example */}
|
||||
<div class="mb-6">
|
||||
<h3 class="text-md font-semibold mb-2">Payment buttons example</h3>
|
||||
<Grid cols={3} gap={4}>
|
||||
<button class="h-20 border-2 border-gray-300 rounded p-2 flex flex-col items-center justify-center hover:border-blue-500">
|
||||
<div class="text-2xl">💳</div>
|
||||
<span class="text-xs">Card</span>
|
||||
</button>
|
||||
<button class="h-20 border-2 border-gray-300 rounded p-2 flex flex-col items-center justify-center hover:border-blue-500">
|
||||
<div class="text-2xl">🍎</div>
|
||||
<span class="text-xs">Apple</span>
|
||||
</button>
|
||||
<button class="h-20 border-2 border-gray-300 rounded p-2 flex flex-col items-center justify-center hover:border-blue-500">
|
||||
<div class="text-2xl">💰</div>
|
||||
<span class="text-xs">PayPal</span>
|
||||
</button>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
{/* Alignment examples */}
|
||||
<div class="mb-6">
|
||||
<h3 class="text-md font-semibold mb-2">Alignment: v="center" h="center"</h3>
|
||||
<Grid cols={3} gap={4} v="center" h="center" class="h-32 bg-gray-100">
|
||||
<div class="bg-red-200 p-2">Item 1</div>
|
||||
<div class="bg-green-200 p-2">Item 2</div>
|
||||
<div class="bg-blue-200 p-2">Item 3</div>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h3 class="text-md font-semibold mb-2">Alignment: v="start" h="end"</h3>
|
||||
<Grid cols={2} gap={4} v="start" h="end" class="h-24 bg-gray-100">
|
||||
<div class="bg-purple-200 p-2">Left</div>
|
||||
<div class="bg-orange-200 p-2">Right</div>
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Tailwind safelist for dynamic classes
|
||||
const _tailwindSafelist = [
|
||||
"grid",
|
||||
"grid-cols-1",
|
||||
"grid-cols-2",
|
||||
"grid-cols-3",
|
||||
"grid-cols-4",
|
||||
"grid-cols-5",
|
||||
"grid-cols-6",
|
||||
"sm:grid-cols-1",
|
||||
"sm:grid-cols-2",
|
||||
"sm:grid-cols-3",
|
||||
"sm:grid-cols-4",
|
||||
"md:grid-cols-1",
|
||||
"md:grid-cols-2",
|
||||
"md:grid-cols-3",
|
||||
"md:grid-cols-4",
|
||||
"lg:grid-cols-1",
|
||||
"lg:grid-cols-2",
|
||||
"lg:grid-cols-3",
|
||||
"lg:grid-cols-4",
|
||||
"lg:grid-cols-5",
|
||||
"lg:grid-cols-6",
|
||||
"xl:grid-cols-1",
|
||||
"xl:grid-cols-2",
|
||||
"xl:grid-cols-3",
|
||||
"xl:grid-cols-4",
|
||||
"xl:grid-cols-5",
|
||||
"xl:grid-cols-6",
|
||||
"gap-0",
|
||||
"gap-1",
|
||||
"gap-2",
|
||||
"gap-3",
|
||||
"gap-4",
|
||||
"gap-5",
|
||||
"gap-6",
|
||||
"gap-8",
|
||||
"items-start",
|
||||
"items-center",
|
||||
"items-end",
|
||||
"items-stretch",
|
||||
"justify-items-start",
|
||||
"justify-items-center",
|
||||
"justify-items-end",
|
||||
"justify-items-stretch",
|
||||
]
|
||||
204
packages/werewolf-ui/src/lib/icon.tsx
Normal file
204
packages/werewolf-ui/src/lib/icon.tsx
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import "hono/jsx"
|
||||
import { FC } from "hono/jsx"
|
||||
import * as icons from "lucide-static"
|
||||
|
||||
export type IconName = keyof typeof icons
|
||||
|
||||
type IconProps = {
|
||||
name: IconName
|
||||
size?: number | string
|
||||
class?: string
|
||||
}
|
||||
|
||||
type IconLinkProps = IconProps & {
|
||||
href?: string
|
||||
target?: string
|
||||
}
|
||||
|
||||
export const Icon: FC<IconProps> = (props) => {
|
||||
const { name, size = 24, class: className } = props
|
||||
|
||||
const iconSvg = icons[name]
|
||||
|
||||
if (!iconSvg) {
|
||||
throw new Error(`Icon "${name}" not found in Lucide icons`)
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
width={size}
|
||||
height={size}
|
||||
class={className}
|
||||
style={{
|
||||
display: "block",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: getIconContent(iconSvg) }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const IconLink: FC<IconLinkProps> = (props) => {
|
||||
const { href = "#", target, class: className, ...iconProps } = props
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target={target}
|
||||
class={`inline-flex items-center justify-center hover:opacity-70 transition-opacity ${className || ""}`}
|
||||
>
|
||||
<Icon {...iconProps} />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export const Test = () => {
|
||||
return (
|
||||
<div class="p-6 space-y-8">
|
||||
{/* === ICON TESTS === */}
|
||||
|
||||
{/* Size variations */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Icon Size Variations</h2>
|
||||
<div class="flex items-center gap-4">
|
||||
{([3, 4, 5, 6, 8, 10, 12, 16] as const).map((size) => (
|
||||
<div key={size} class="flex flex-col items-center space-y-2">
|
||||
<Icon name="Heart" size={size} />
|
||||
<p class="text-sm">{size}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Styling with CSS classes */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Styling with CSS Classes</h2>
|
||||
<div class="grid grid-cols-5 gap-6">
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<Icon name="Star" size={8} />
|
||||
<p class="text-sm">Default</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<Icon name="Star" size={8} class="text-blue-500" />
|
||||
<p class="text-sm">Blue Color</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<Icon name="Star" size={8} class="stroke-1" />
|
||||
<p class="text-sm">Thin Stroke</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<Icon name="Star" size={8} class="text-yellow-400 stroke-0 fill-current" />
|
||||
<p class="text-sm">Filled</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<Icon name="Star" size={8} class="text-purple-500 hover:text-purple-700 transition-colors" />
|
||||
<p class="text-sm">Hover Effect</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Advanced styling */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Advanced Styling</h2>
|
||||
<div class="grid grid-cols-4 gap-6">
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<Icon name="Heart" size={10} class="text-red-500 stroke-0 fill-current" />
|
||||
<p class="text-sm">Filled Heart</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<Icon name="Shield" size={10} class="text-green-600 stroke-2" />
|
||||
<p class="text-sm">Thick Stroke</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<Icon name="Sun" size={10} class="text-yellow-500 animate-spin" />
|
||||
<p class="text-sm">Animated</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<Icon name="Zap" size={10} class="text-blue-400 drop-shadow-lg" />
|
||||
<p class="text-sm">Drop Shadow</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* === ICON LINK TESTS === */}
|
||||
|
||||
{/* Basic icon links */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Icon Links</h2>
|
||||
<div class="flex gap-6">
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<IconLink name="Home" size={32} href="/" />
|
||||
<p class="text-sm">Home Link</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<IconLink name="ExternalLink" size={32} href="https://example.com" target="_blank" />
|
||||
<p class="text-sm">External Link</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<IconLink name="Mail" size={32} href="mailto:hello@example.com" />
|
||||
<p class="text-sm">Email Link</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<IconLink name="Phone" size={32} href="tel:+1234567890" />
|
||||
<p class="text-sm">Phone Link</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Styled icon links */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Styled Icon Links</h2>
|
||||
<div class="grid grid-cols-4 gap-6">
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<IconLink
|
||||
name="Download"
|
||||
size={32}
|
||||
href="#"
|
||||
class="bg-blue-500 text-white p-2 rounded-lg hover:bg-blue-600 transition-colors"
|
||||
/>
|
||||
<p class="text-sm">Button Style</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<IconLink
|
||||
name="Settings"
|
||||
size={32}
|
||||
href="#"
|
||||
class="border-2 border-gray-300 p-2 rounded-full hover:border-gray-500 hover:bg-gray-50 transition-all"
|
||||
/>
|
||||
<p class="text-sm">Circle Border</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<IconLink
|
||||
name="Heart"
|
||||
size={32}
|
||||
href="#"
|
||||
class="text-red-500 hover:scale-125 transition-transform stroke-[3]"
|
||||
/>
|
||||
<p class="text-sm">Scale + Thick</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<IconLink
|
||||
name="Star"
|
||||
size={32}
|
||||
href="#"
|
||||
class="text-yellow-400 fill-current stroke-1 hover:rotate-12 transition-transform"
|
||||
/>
|
||||
<p class="text-sm">Filled + Rotate</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const getIconContent = (svgString: string): string => {
|
||||
const match = svgString.match(/<svg[^>]*>(.*?)<\/svg>/s)
|
||||
return match ? match[1] : ""
|
||||
}
|
||||
174
packages/werewolf-ui/src/lib/image.tsx
Normal file
174
packages/werewolf-ui/src/lib/image.tsx
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import "hono/jsx"
|
||||
import { FC } from "hono/jsx"
|
||||
|
||||
export type ImageProps = {
|
||||
src: string
|
||||
alt?: string
|
||||
class?: string
|
||||
}
|
||||
|
||||
export const Image: FC<ImageProps> = ({ src, alt, class: className }) => {
|
||||
return <img src={src} alt={alt} class={className} />
|
||||
}
|
||||
|
||||
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
|
||||
]
|
||||
|
||||
return (
|
||||
<div class="p-6 space-y-8">
|
||||
<h2 class="text-xl font-bold mb-4">Image Examples with Tailwind Classes</h2>
|
||||
|
||||
{/* Size variations */}
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-4">Size Variations</h3>
|
||||
<div class="flex gap-4 flex-wrap">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Image src={sampleImages[0]} class="w-16 h-16 object-cover" alt="16x16" />
|
||||
<p class="text-sm">w-16 h-16</p>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Image src={sampleImages[0]} class="w-24 h-24 object-cover" alt="24x24" />
|
||||
<p class="text-sm">w-24 h-24</p>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Image src={sampleImages[0]} class="w-32 h-32 object-cover" alt="32x32" />
|
||||
<p class="text-sm">w-32 h-32</p>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Image src={sampleImages[0]} class="w-48 h-32 object-cover" alt="48x32" />
|
||||
<p class="text-sm">w-48 h-32</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Object fit variations */}
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-4">Object Fit Variations</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Same image with different object-fit classes</p>
|
||||
<div class="flex gap-6 flex-wrap">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Image src={sampleImages[0]} class="w-32 h-32 object-cover border" alt="Object cover" />
|
||||
<p class="text-sm">object-cover</p>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Image
|
||||
src={sampleImages[0]}
|
||||
class="w-32 h-32 object-contain border bg-gray-100"
|
||||
alt="Object contain"
|
||||
/>
|
||||
<p class="text-sm">object-contain</p>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Image src={sampleImages[0]} class="w-32 h-32 object-fill border" alt="Object fill" />
|
||||
<p class="text-sm">object-fill</p>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Image
|
||||
src={sampleImages[0]}
|
||||
class="w-32 h-32 object-scale-down border bg-gray-100"
|
||||
alt="Object scale-down"
|
||||
/>
|
||||
<p class="text-sm">object-scale-down</p>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Image src={sampleImages[0]} class="w-32 h-32 object-none border bg-gray-100" alt="Object none" />
|
||||
<p class="text-sm">object-none</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Styling examples */}
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-4">Styling Examples</h3>
|
||||
<div class="flex gap-6 flex-wrap">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Image
|
||||
src={sampleImages[0]}
|
||||
class="w-32 h-32 object-cover rounded-lg border-4 border-blue-500"
|
||||
alt="Rounded with border"
|
||||
/>
|
||||
<p class="text-sm">Rounded + Border</p>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Image src={sampleImages[1]} class="w-32 h-32 object-cover shadow-lg" alt="With shadow" />
|
||||
<p class="text-sm">With Shadow</p>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Image
|
||||
src={sampleImages[2]}
|
||||
class="w-32 h-32 object-cover rounded-full border-4 border-green-500 shadow-lg"
|
||||
alt="Circular with effects"
|
||||
/>
|
||||
<p class="text-sm">Circular + Effects</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Responsive sizing */}
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-4">Responsive Sizing</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<Image src={sampleImages[0]} class="w-full h-48 object-cover rounded" alt="Full width" />
|
||||
<p class="text-sm mt-2">w-full h-48 (responsive width)</p>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<Image
|
||||
src={sampleImages[1]}
|
||||
class="w-16 sm:w-24 md:w-32 lg:w-48 h-24 object-cover rounded"
|
||||
alt="Responsive sizes"
|
||||
/>
|
||||
<div>
|
||||
<p class="text-sm">w-16 sm:w-24 md:w-32 lg:w-48</p>
|
||||
<p class="text-xs text-gray-600">Resize window to see changes</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Common use cases */}
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-4">Common Use Cases</h3>
|
||||
<div class="space-y-6">
|
||||
{/* Avatar */}
|
||||
<div>
|
||||
<h4 class="font-medium mb-2">Avatar</h4>
|
||||
<Image src={sampleImages[0]} class="w-12 h-12 rounded-full object-cover" alt="Avatar" />
|
||||
</div>
|
||||
|
||||
{/* Card image */}
|
||||
<div class="max-w-sm">
|
||||
<h4 class="font-medium mb-2">Card Image</h4>
|
||||
<div class="border rounded-lg overflow-hidden">
|
||||
<Image src={sampleImages[1]} class="w-full h-48 object-cover" alt="Card image" />
|
||||
<div class="p-4">
|
||||
<h5 class="font-medium">Card Title</h5>
|
||||
<p class="text-sm text-gray-600">Card description goes here</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gallery grid */}
|
||||
<div>
|
||||
<h4 class="font-medium mb-2">Gallery Grid</h4>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
{sampleImages.map((src, i) => (
|
||||
<Image
|
||||
key={i}
|
||||
src={src}
|
||||
class="w-full aspect-square object-cover rounded"
|
||||
alt={`Gallery ${i}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
185
packages/werewolf-ui/src/lib/input.tsx
Normal file
185
packages/werewolf-ui/src/lib/input.tsx
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
import "hono/jsx"
|
||||
import { JSX, FC } from "hono/jsx"
|
||||
|
||||
export type InputProps = JSX.IntrinsicElements["input"] & {
|
||||
labelPosition?: "above" | "left" | "right"
|
||||
children?: any
|
||||
}
|
||||
|
||||
export const Input: FC<InputProps> = (props) => {
|
||||
const { labelPosition = "above", children, class: className, ...inputProps } = props
|
||||
|
||||
const classes = [
|
||||
"h-10 px-3 py-2 rounded-md border border-input bg-background text-sm",
|
||||
"placeholder:text-muted-foreground",
|
||||
"focus-visible:outline-none focus-visible:ring-2",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
]
|
||||
|
||||
if (!children) {
|
||||
return <input class={classes.join(" ")} {...inputProps} />
|
||||
}
|
||||
|
||||
const labelElement = (
|
||||
<label for={inputProps.id} class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{children}
|
||||
</label>
|
||||
)
|
||||
|
||||
if (labelPosition === "above") {
|
||||
return (
|
||||
<div class="space-y-1 flex flex-col flex-1 min-w-0">
|
||||
{labelElement}
|
||||
<input class={classes.join(" ")} {...inputProps} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (labelPosition === "left") {
|
||||
return (
|
||||
<div class="flex items-center space-x-1 flex-1">
|
||||
{labelElement}
|
||||
<input class={[...classes, "flex-1"].join(" ")} {...inputProps} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (labelPosition === "right") {
|
||||
return (
|
||||
<div class="flex items-center space-x-1 flex-1">
|
||||
<input class={[...classes, "flex-1"].join(" ")} {...inputProps} />
|
||||
{labelElement}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const Test = () => {
|
||||
return (
|
||||
<div class="p-6 space-y-8 max-w-md">
|
||||
{/* Basic inputs */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Basic Inputs</h2>
|
||||
<div class="space-y-4">
|
||||
<Input placeholder="Enter your name" />
|
||||
<Input type="email" placeholder="Enter your email" />
|
||||
<Input type="password" placeholder="Enter your password" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom styling */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Custom Styling</h2>
|
||||
<div class="space-y-4">
|
||||
<Input class="h-8 text-xs" placeholder="Small input" />
|
||||
<Input placeholder="Default input" />
|
||||
<Input class="h-12 text-base" placeholder="Large input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* With values */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">With Values</h2>
|
||||
<div class="space-y-4">
|
||||
<Input value="John Doe" placeholder="Name" />
|
||||
<Input type="email" value="john@example.com" placeholder="Email" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Disabled state */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Disabled State</h2>
|
||||
<div class="space-y-4">
|
||||
<Input disabled placeholder="Disabled input" />
|
||||
<Input disabled value="Disabled with value" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Label above */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Label Above</h2>
|
||||
<div class="space-y-4">
|
||||
<Input placeholder="Enter your name">Name</Input>
|
||||
<Input type="email" placeholder="Enter your email">
|
||||
Email
|
||||
</Input>
|
||||
<Input type="password" placeholder="Enter your password">
|
||||
Password
|
||||
</Input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Label to the left */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Label Left</h2>
|
||||
<div class="space-y-4">
|
||||
<Input labelPosition="left" placeholder="Enter your name">
|
||||
Name
|
||||
</Input>
|
||||
<Input labelPosition="left" type="email" placeholder="Enter your email">
|
||||
Email
|
||||
</Input>
|
||||
<Input labelPosition="left" type="password" placeholder="Enter your password">
|
||||
Password
|
||||
</Input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Label to the right */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Label Right</h2>
|
||||
<div class="space-y-4">
|
||||
<Input labelPosition="right" placeholder="Enter your name">
|
||||
Name
|
||||
</Input>
|
||||
<Input labelPosition="right" type="email" placeholder="Enter your email">
|
||||
Email
|
||||
</Input>
|
||||
<Input labelPosition="right" type="password" placeholder="Enter your password">
|
||||
Password
|
||||
</Input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Horizontal layout */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Horizontal Layout</h2>
|
||||
<div class="flex gap-4">
|
||||
<Input placeholder="First name">First</Input>
|
||||
<Input placeholder="Last name">Last</Input>
|
||||
<Input placeholder="Age">Age</Input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom styling */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Custom Input Styling</h2>
|
||||
<div class="space-y-4">
|
||||
<Input class="border-blue-300 focus-visible:ring-blue-500" placeholder="Custom styled input">
|
||||
<span class="text-blue-600 font-bold">Custom Label</span>
|
||||
</Input>
|
||||
<Input labelPosition="left" placeholder="Required input">
|
||||
<span class="text-red-600 min-w-24">Required Field</span>
|
||||
</Input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* With values and disabled states */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Input States</h2>
|
||||
<div class="space-y-4">
|
||||
<Input value="John Doe">With Value</Input>
|
||||
<Input disabled value="Disabled field">
|
||||
Disabled
|
||||
</Input>
|
||||
<Input class="h-8 text-xs" placeholder="Small input">
|
||||
Small
|
||||
</Input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
55
packages/werewolf-ui/src/lib/linebreak.tsx
Normal file
55
packages/werewolf-ui/src/lib/linebreak.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import "hono/jsx"
|
||||
import { FC, PropsWithChildren } from "hono/jsx"
|
||||
|
||||
type BreakProps = PropsWithChildren & {
|
||||
class?: string
|
||||
}
|
||||
|
||||
export const Break: FC<BreakProps> = ({ children, class: className }) => {
|
||||
return (
|
||||
<div class={`flex items-center my-4 ${className || ""}`}>
|
||||
<div class="flex-1 border-t border-gray-300"></div>
|
||||
{children && (
|
||||
<>
|
||||
<span class="px-3 text-sm text-gray-500 bg-white">{children}</span>
|
||||
<div class="flex-1 border-t border-gray-300"></div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Test = () => {
|
||||
return (
|
||||
<div class="p-4 space-y-8 max-w-md">
|
||||
<h2 class="text-lg font-bold mb-4">Break Examples</h2>
|
||||
|
||||
{/* With text */}
|
||||
<div>
|
||||
<p>Would you like to sign in?</p>
|
||||
<Break>OR DO YOU SUBMIT TO</Break>
|
||||
<p>Certain death</p>
|
||||
</div>
|
||||
|
||||
{/* Simple divider */}
|
||||
<div>
|
||||
<p>Payment methods</p>
|
||||
<Break />
|
||||
<p>Billing information</p>
|
||||
</div>
|
||||
|
||||
{/* Different text examples */}
|
||||
<div>
|
||||
<p>Existing user?</p>
|
||||
<Break>OR</Break>
|
||||
<p>Create new account</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Personal info</p>
|
||||
<Break>STEP 2</Break>
|
||||
<p>Payment details</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
232
packages/werewolf-ui/src/lib/placeholder.tsx
Normal file
232
packages/werewolf-ui/src/lib/placeholder.tsx
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
import "hono/jsx"
|
||||
import { Avatar, AvatarProps } from "@/lib/avatar"
|
||||
import { Image, ImageProps } from "@/lib/image"
|
||||
|
||||
export const Placeholder = {
|
||||
Avatar(props: PlaceholderAvatarProps) {
|
||||
const { size = 32, seed = "seed", type = "dylan", transparent, alt, class: className, rounded } = 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())
|
||||
|
||||
if (transparent) {
|
||||
url.searchParams.set("backgroundColor", "transparent")
|
||||
}
|
||||
|
||||
return <Avatar src={url.toString()} alt={alt} class={className} size={size} rounded={rounded} />
|
||||
},
|
||||
|
||||
Image(props: PlaceholderImageProps) {
|
||||
const {
|
||||
width = 200,
|
||||
height = 200,
|
||||
seed = 1,
|
||||
alt = "Placeholder image",
|
||||
objectFit,
|
||||
class: className,
|
||||
} = props
|
||||
|
||||
// Generate Picsum Photos URL with seed for consistent images
|
||||
const src = `https://picsum.photos/${width}/${height}?random=${seed}`
|
||||
|
||||
return <Image src={src} alt={alt} width={width} height={height} objectFit={objectFit} class={className} />
|
||||
},
|
||||
}
|
||||
|
||||
export const Test = () => {
|
||||
return (
|
||||
<div class="p-6 space-y-8">
|
||||
{/* === AVATAR TESTS === */}
|
||||
|
||||
{/* Show all available avatar styles */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">All Avatar Styles ({allStyles.length} total)</h2>
|
||||
<div class="grid grid-cols-10 gap-3">
|
||||
{allStyles.map((style) => (
|
||||
<div key={style} class="flex flex-col space-y-1">
|
||||
<Placeholder.Avatar type={style} size={48} />
|
||||
<p class="text-xs font-medium">{style}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Avatar size variations */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Avatar Size Variations</h2>
|
||||
<div class="flex gap-4">
|
||||
{[24, 32, 48, 64].map((size) => (
|
||||
<div key={size} class="flex flex-col space-y-2">
|
||||
<Placeholder.Avatar size={size} />
|
||||
<p class="text-sm">{size}px</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Avatar styling combinations */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Avatar Styling Options</h2>
|
||||
<div class="flex gap-6">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Placeholder.Avatar rounded size={64} />
|
||||
<p class="text-sm">Rounded + Background</p>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="bg-gray-200 p-2">
|
||||
<Placeholder.Avatar rounded transparent size={64} />
|
||||
</div>
|
||||
<p class="text-sm">Rounded + Transparent</p>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Placeholder.Avatar size={64} />
|
||||
<p class="text-sm">Square + Background</p>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="bg-gray-200 p-2">
|
||||
<Placeholder.Avatar transparent size={64} />
|
||||
</div>
|
||||
<p class="text-sm">Square + Transparent</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Avatar seed variations */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Avatar Seeds (Same Style, Different People)</h2>
|
||||
<div class="flex gap-4">
|
||||
{["alice", "bob", "charlie", "diana"].map((seed) => (
|
||||
<div key={seed} class="flex flex-col space-y-2">
|
||||
<Placeholder.Avatar seed={seed} size={64} />
|
||||
<p class="text-sm">"{seed}"</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* === IMAGE TESTS === */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Placeholder Images</h2>
|
||||
|
||||
{/* Size variations */}
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-semibold mb-3">Size Variations</h3>
|
||||
<div class="flex gap-4">
|
||||
{[
|
||||
{ width: 100, height: 100 },
|
||||
{ width: 150, height: 100 },
|
||||
{ width: 200, height: 150 },
|
||||
{ width: 250, height: 200 },
|
||||
].map(({ width, height }) => (
|
||||
<div key={`${width}x${height}`} class="flex flex-col space-y-2">
|
||||
<Placeholder.Image width={width} height={height} seed={1} />
|
||||
<p class="text-sm">
|
||||
{width}×{height}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Different seeds - show variety */}
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-semibold mb-3">Different Images (Different Seeds)</h3>
|
||||
<div class="flex gap-4">
|
||||
{[1, 2, 3, 4, 5].map((seed) => (
|
||||
<div key={seed} class="flex flex-col space-y-2">
|
||||
<Placeholder.Image width={150} height={150} seed={seed} />
|
||||
<p class="text-sm">Seed {seed}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* With custom classes */}
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-3">With Custom Classes</h3>
|
||||
<div class="flex gap-6">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Placeholder.Image
|
||||
width={150}
|
||||
height={150}
|
||||
seed={1}
|
||||
objectFit="cover"
|
||||
class="rounded-lg border-4 border-blue-500"
|
||||
/>
|
||||
<p class="text-sm">Rounded + Border</p>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Placeholder.Image width={150} height={150} seed={2} objectFit="cover" class="shadow-lg" />
|
||||
<p class="text-sm">With Shadow</p>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<Placeholder.Image
|
||||
width={150}
|
||||
height={150}
|
||||
seed={3}
|
||||
objectFit="cover"
|
||||
class="rounded-full border-4 border-green-500 shadow-lg"
|
||||
/>
|
||||
<p class="text-sm">Circular + Effects</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Type definitions
|
||||
type PlaceholderAvatarProps = Omit<AvatarProps, "src"> & {
|
||||
seed?: string
|
||||
type?: DicebearStyleName
|
||||
transparent?: boolean
|
||||
}
|
||||
|
||||
type PlaceholderImageProps = Omit<ImageProps, "src" | "alt"> & {
|
||||
width?: number
|
||||
height?: number
|
||||
seed?: number
|
||||
alt?: string
|
||||
}
|
||||
|
||||
// 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",
|
||||
] as const
|
||||
|
||||
type DicebearStyleName = (typeof allStyles)[number]
|
||||
|
||||
// Default export for convenience
|
||||
export default Placeholder
|
||||
291
packages/werewolf-ui/src/lib/select.tsx
Normal file
291
packages/werewolf-ui/src/lib/select.tsx
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
import "hono/jsx"
|
||||
import { JSX, FC } from "hono/jsx"
|
||||
|
||||
export type SelectOption = {
|
||||
value: string
|
||||
label: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export type SelectProps = Omit<JSX.IntrinsicElements["select"], "children"> & {
|
||||
options: SelectOption[]
|
||||
placeholder?: string
|
||||
labelPosition?: "above" | "left" | "right"
|
||||
children?: any
|
||||
}
|
||||
|
||||
export const Select: FC<SelectProps> = (props) => {
|
||||
const { options, placeholder, labelPosition = "above", children, class: className, ...selectProps } = props
|
||||
|
||||
// If a label is provided but no id, generate a random id so the label can be clicked
|
||||
if (children && !selectProps.id) {
|
||||
selectProps.id = `random-${Math.random().toString(36)}`
|
||||
}
|
||||
|
||||
const classes = [
|
||||
"h-10 px-3 py-2 rounded-md border border-input bg-background text-sm",
|
||||
"placeholder:text-muted-foreground",
|
||||
"focus-visible:outline-none focus-visible:ring-2",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"appearance-none bg-no-repeat bg-[right_8px_center] bg-[length:16px_16px] pr-8",
|
||||
"bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQgNkw4IDEwTDEyIDYiIHN0cm9rZT0iIzZCNzI4MCIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+')]",
|
||||
className,
|
||||
]
|
||||
|
||||
if (!children) {
|
||||
return (
|
||||
<select class={classes.join(" ")} {...selectProps}>
|
||||
{placeholder && (
|
||||
<option value="" disabled>
|
||||
{placeholder}
|
||||
</option>
|
||||
)}
|
||||
{options.map((option) => (
|
||||
<option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
selected={selectProps.value === option.value}
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
const labelElement = (
|
||||
<label for={selectProps.id} class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{children}
|
||||
</label>
|
||||
)
|
||||
|
||||
if (labelPosition === "above") {
|
||||
return (
|
||||
<div class="space-y-1 flex flex-col flex-1 min-w-0">
|
||||
{labelElement}
|
||||
<select class={classes.join(" ")} {...selectProps}>
|
||||
{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 === "left") {
|
||||
return (
|
||||
<div class="flex items-center space-x-1 flex-1">
|
||||
{labelElement}
|
||||
<select class={[...classes, "flex-1"].join(" ")} {...selectProps}>
|
||||
{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 class="flex items-center space-x-1 flex-1">
|
||||
<select class={[...classes, "flex-1"].join(" ")} {...selectProps}>
|
||||
{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 = () => {
|
||||
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" },
|
||||
]
|
||||
|
||||
const years = Array.from({ length: 10 }, (_, i) => ({
|
||||
value: String(2024 + i),
|
||||
label: String(2024 + i),
|
||||
}))
|
||||
|
||||
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 },
|
||||
]
|
||||
|
||||
return (
|
||||
<div class="p-6 space-y-8 max-w-md">
|
||||
{/* Basic selects */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Basic Selects</h2>
|
||||
<div class="space-y-4">
|
||||
<Select options={months} placeholder="Select month" />
|
||||
<Select options={years} placeholder="Select year" />
|
||||
<Select options={countries} placeholder="Select country" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* With values */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">With Values</h2>
|
||||
<div class="space-y-4">
|
||||
<Select options={months} value="03" />
|
||||
<Select options={years} value="2025" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Disabled state */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Disabled State</h2>
|
||||
<div class="space-y-4">
|
||||
<Select options={months} disabled placeholder="Disabled select" />
|
||||
<Select options={years} disabled value="2024" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom styling */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Custom Styling</h2>
|
||||
<div class="space-y-4">
|
||||
<Select
|
||||
options={countries}
|
||||
class="border-blue-300 focus-visible:ring-blue-500"
|
||||
placeholder="Custom styled select"
|
||||
/>
|
||||
<Select options={months} class="h-8 text-xs" placeholder="Small select" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Label above */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Label Above</h2>
|
||||
<div class="space-y-4">
|
||||
<Select options={months} placeholder="Select month">
|
||||
Birth Month
|
||||
</Select>
|
||||
<Select options={years} placeholder="Select year">
|
||||
Birth Year
|
||||
</Select>
|
||||
<Select options={countries} placeholder="Select country">
|
||||
Country
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Label to the left */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Label Left</h2>
|
||||
<div class="space-y-4">
|
||||
<Select labelPosition="left" options={months} placeholder="Select month">
|
||||
Month
|
||||
</Select>
|
||||
<Select labelPosition="left" options={years} placeholder="Select year">
|
||||
Year
|
||||
</Select>
|
||||
<Select labelPosition="left" options={countries} placeholder="Select country">
|
||||
Country
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Label to the right */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Label Right</h2>
|
||||
<div class="space-y-4">
|
||||
<Select labelPosition="right" options={months} placeholder="Select month">
|
||||
Month
|
||||
</Select>
|
||||
<Select labelPosition="right" options={years} placeholder="Select year">
|
||||
Year
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Horizontal layout (like card form) */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Horizontal Layout</h2>
|
||||
<div class="flex gap-4">
|
||||
<Select options={months} placeholder="MM">
|
||||
Expires
|
||||
</Select>
|
||||
<Select options={years} placeholder="YYYY">
|
||||
Year
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom styling */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Custom Select Styling</h2>
|
||||
<div class="space-y-4">
|
||||
<Select
|
||||
class="border-blue-300 focus-visible:ring-blue-500"
|
||||
options={countries}
|
||||
placeholder="Custom styled select"
|
||||
>
|
||||
<span class="text-blue-600 font-bold">Custom Label</span>
|
||||
</Select>
|
||||
<Select labelPosition="left" options={months} placeholder="Required select">
|
||||
<span class="text-red-600 min-w-24">Required Field</span>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* With values and disabled states */}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-4">Select States</h2>
|
||||
<div class="space-y-4">
|
||||
<Select options={months} value="03">
|
||||
With Value
|
||||
</Select>
|
||||
<Select disabled options={years} value="2024">
|
||||
Disabled
|
||||
</Select>
|
||||
<Select class="h-8 text-xs" options={countries} placeholder="Small select">
|
||||
Small
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
177
packages/werewolf-ui/src/lib/stack.tsx
Normal file
177
packages/werewolf-ui/src/lib/stack.tsx
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import { TailwindSize } from "@/types"
|
||||
import "hono/jsx"
|
||||
import { FC, PropsWithChildren } from "hono/jsx"
|
||||
|
||||
export const VStack: FC<VStackProps> = (props) => {
|
||||
return (
|
||||
<Stack
|
||||
direction="col"
|
||||
mainAxis={props.v}
|
||||
crossAxis={props.h}
|
||||
wrap={props.wrap}
|
||||
gap={props.gap}
|
||||
class={props.class}
|
||||
>
|
||||
{props.children}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export const HStack: FC<HStackProps> = (props) => {
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
mainAxis={props.h}
|
||||
crossAxis={props.v}
|
||||
wrap={props.wrap}
|
||||
gap={props.gap}
|
||||
class={props.class}
|
||||
>
|
||||
{props.children}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const Stack: FC<StackProps> = (props) => {
|
||||
const classes = [
|
||||
"flex",
|
||||
`flex-${props.direction}`,
|
||||
props.wrap ? "flex-wrap" : "",
|
||||
props.gap ? `gap-${props.gap}` : "",
|
||||
props.mainAxis ? `justify-${props.mainAxis}` : "",
|
||||
props.crossAxis ? `items-${props.crossAxis}` : "",
|
||||
props.class ?? "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
|
||||
return <div class={classes}>{props.children}</div>
|
||||
}
|
||||
|
||||
export const Test = () => {
|
||||
const mainAxisOpts: MainAxisOpts[] = ["start", "center", "end", "between", "around", "evenly"]
|
||||
const crossAxisOpts: CrossAxisOpts[] = ["start", "center", "end", "stretch", "baseline"]
|
||||
|
||||
return (
|
||||
<div class="p-4 space-y-8">
|
||||
{/* HStack layout matrix */}
|
||||
<div>
|
||||
<h2 class="text-lg font-bold mb-2">HStack Layout</h2>
|
||||
<div class="overflow-auto">
|
||||
<div class="grid grid-cols-7 gap-1">
|
||||
{/* Header row: blank + h labels */}
|
||||
<div></div>
|
||||
{mainAxisOpts.map((h) => (
|
||||
<div key={h} class="text-sm font-medium text-center">
|
||||
h: {h}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Each row: v label + HStack cells */}
|
||||
{crossAxisOpts.map((v) => [
|
||||
<div key={v} class="text-sm font-medium">
|
||||
v: {v}
|
||||
</div>,
|
||||
...mainAxisOpts.map((h) => (
|
||||
<HStack key={`${h}-${v}`} h={h} v={v} class="bg-gray-100 p-2 h-24 border border-gray-400">
|
||||
<div class="text-center p-1 bg-red-500">Aa</div>
|
||||
<div class="text-center p-1 bg-green-500">Aa</div>
|
||||
<div class="text-center p-1 bg-blue-500">Aa</div>
|
||||
</HStack>
|
||||
)),
|
||||
])}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* VStack layout matrix */}
|
||||
<div>
|
||||
<h2 class="text-lg font-bold mb-2">VStack Layout</h2>
|
||||
<div class="overflow-auto">
|
||||
<div class="grid grid-cols-6 gap-1">
|
||||
{/* Header row: blank + h labels */}
|
||||
<div></div>
|
||||
{crossAxisOpts.map((h) => (
|
||||
<div key={h} class="text-sm font-medium text-center">
|
||||
h: {h}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Each row: v label + VStack cells */}
|
||||
{mainAxisOpts.map((v) => [
|
||||
<div key={v} class="text-sm font-medium">
|
||||
v: {v}
|
||||
</div>,
|
||||
...crossAxisOpts.map((h) => (
|
||||
<VStack key={`${h}-${v}`} v={v} h={h} class="bg-gray-100 p-2 h-42 border border-gray-400">
|
||||
<div class="text-center p-1 bg-red-500">Aa</div>
|
||||
<div class="text-center p-1 bg-green-500">Aa</div>
|
||||
<div class="text-center p-1 bg-blue-500">Aa</div>
|
||||
</VStack>
|
||||
)),
|
||||
])}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type StackDirection = "row" | "col"
|
||||
|
||||
type StackProps = {
|
||||
direction: StackDirection
|
||||
mainAxis?: string
|
||||
crossAxis?: string
|
||||
wrap?: boolean
|
||||
gap?: TailwindSize
|
||||
class?: string
|
||||
children?: any
|
||||
}
|
||||
|
||||
type MainAxisOpts = "start" | "center" | "end" | "between" | "around" | "evenly"
|
||||
type CrossAxisOpts = "start" | "center" | "end" | "stretch" | "baseline"
|
||||
|
||||
type CommonStackProps = PropsWithChildren & {
|
||||
wrap?: boolean
|
||||
gap?: TailwindSize
|
||||
class?: string
|
||||
}
|
||||
|
||||
type VStackProps = CommonStackProps & {
|
||||
v?: MainAxisOpts // main axis for vertical stack
|
||||
h?: CrossAxisOpts // cross axis for vertical stack
|
||||
}
|
||||
|
||||
type HStackProps = CommonStackProps & {
|
||||
h?: MainAxisOpts // main axis for horizontal stack
|
||||
v?: CrossAxisOpts // cross axis for horizontal stack
|
||||
}
|
||||
|
||||
// Tailwind will purge any unused styles. Since we use dynamic class names, we need to safelist the classes we use in this file.
|
||||
const _tailwindSafelist = [
|
||||
"flex-row",
|
||||
"flex-col",
|
||||
"flex-wrap",
|
||||
"gap-0",
|
||||
"gap-1",
|
||||
"gap-2",
|
||||
"gap-3",
|
||||
"gap-4",
|
||||
"gap-5",
|
||||
"gap-6",
|
||||
"gap-8",
|
||||
"gap-10",
|
||||
"gap-12",
|
||||
"items-start",
|
||||
"items-center",
|
||||
"items-end",
|
||||
"items-stretch",
|
||||
"items-baseline",
|
||||
"justify-start",
|
||||
"justify-center",
|
||||
"justify-end",
|
||||
"justify-between",
|
||||
"justify-around",
|
||||
"justify-evenly",
|
||||
]
|
||||
28
packages/werewolf-ui/src/routes/examples/[example].tsx
Normal file
28
packages/werewolf-ui/src/routes/examples/[example].tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { LoaderProps } from "@workshop/nano-remix"
|
||||
import "@/index.css"
|
||||
import { Cards } from "@/examples/cards"
|
||||
|
||||
export const loader = async (req: Request) => {
|
||||
const exampleName = req.url.split("/").pop() || ""
|
||||
return { exampleName }
|
||||
}
|
||||
|
||||
export default function Example({ exampleName }: LoaderProps<typeof loader>) {
|
||||
let component = (
|
||||
<>
|
||||
<h1>Example {exampleName} not Found</h1>
|
||||
<p>Available examples: cards</p>
|
||||
</>
|
||||
)
|
||||
|
||||
if (exampleName === "cards") {
|
||||
component = <Cards />
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 class="p-4">{exampleName}</h1>
|
||||
{component}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
41
packages/werewolf-ui/src/routes/index.tsx
Normal file
41
packages/werewolf-ui/src/routes/index.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { LoaderProps } from "@workshop/nano-remix"
|
||||
import "../index.css"
|
||||
import { readdir } from "node:fs/promises"
|
||||
import { join, basename } from "node:path"
|
||||
import { VStack } from "@/lib/stack"
|
||||
|
||||
export const loader = async (req: Request) => {
|
||||
const examples = await readdir(join(import.meta.dir, "../examples"))
|
||||
const tests = await readdir(join(import.meta.dir, "../lib"))
|
||||
return {
|
||||
examples: examples.map((file) => basename(file, ".tsx")),
|
||||
tests: tests.map((file) => basename(file, ".tsx")),
|
||||
}
|
||||
}
|
||||
|
||||
export function App({ examples, tests }: LoaderProps<typeof loader>) {
|
||||
return (
|
||||
<div class="m-8">
|
||||
<h1 class="text-3xl font-bold mb-4">Werewolf UI</h1>
|
||||
<h2>Tests</h2>
|
||||
<VStack>
|
||||
{tests.sort().map((test) => (
|
||||
<a class="underline text-blue-500" key={test} href={`/tests/${test}`}>
|
||||
{test}
|
||||
</a>
|
||||
))}
|
||||
</VStack>
|
||||
|
||||
<h2 class="mt-8">Examples</h2>
|
||||
<VStack>
|
||||
{examples.map((example) => (
|
||||
<a class="underline text-blue-500" key={example} href={`/examples/${example}`}>
|
||||
{example}
|
||||
</a>
|
||||
))}
|
||||
</VStack>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
77
packages/werewolf-ui/src/routes/tests/[test].tsx
Normal file
77
packages/werewolf-ui/src/routes/tests/[test].tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { LoaderProps } from "@workshop/nano-remix"
|
||||
import "@/index.css"
|
||||
/*
|
||||
break
|
||||
button
|
||||
grid
|
||||
icon
|
||||
image
|
||||
input
|
||||
linebreak
|
||||
placeholder
|
||||
select
|
||||
stack
|
||||
*/
|
||||
import { Test as AvatarTest } from "@/lib/avatar"
|
||||
import { Test as BreakTest } from "@/lib/break"
|
||||
import { Test as ButtonTest } from "@/lib/button"
|
||||
import { Test as GridTest } from "@/lib/grid"
|
||||
import { Test as IconTest } from "@/lib/icon"
|
||||
import { Test as ImageTest } from "@/lib/image"
|
||||
import { Test as InputTest } from "@/lib/input"
|
||||
import { Test as LinebreakTest } from "@/lib/linebreak"
|
||||
import { Test as PlaceholderTest } from "@/lib/placeholder"
|
||||
import { Test as SelectTest } from "@/lib/select"
|
||||
import { Test as StackTest } from "@/lib/stack"
|
||||
|
||||
export const loader = async (req: Request) => {
|
||||
const testName = req.url.split("/").pop() || ""
|
||||
return { testName }
|
||||
}
|
||||
|
||||
export default function Test({ testName }: LoaderProps<typeof loader>) {
|
||||
let component = (
|
||||
<>
|
||||
<h1>Test {testName} not Found</h1>
|
||||
<p>Available tests: cards</p>
|
||||
</>
|
||||
)
|
||||
|
||||
if (testName === "avatar") {
|
||||
component = <AvatarTest />
|
||||
} else if (testName === "break") {
|
||||
component = <BreakTest />
|
||||
} else if (testName === "button") {
|
||||
component = <ButtonTest />
|
||||
} else if (testName === "grid") {
|
||||
component = <GridTest />
|
||||
} else if (testName === "icon") {
|
||||
component = <IconTest />
|
||||
} else if (testName === "image") {
|
||||
component = <ImageTest />
|
||||
} else if (testName === "input") {
|
||||
component = <InputTest />
|
||||
} else if (testName === "linebreak") {
|
||||
component = <LinebreakTest />
|
||||
} else if (testName === "placeholder") {
|
||||
component = <PlaceholderTest />
|
||||
} else if (testName === "select") {
|
||||
component = <SelectTest />
|
||||
} else if (testName === "stack") {
|
||||
component = <StackTest />
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 class="p-4">
|
||||
{testName}
|
||||
{" : "}
|
||||
<a class="underline text-blue-500 italic" href="/">
|
||||
back
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
{component}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
19
packages/werewolf-ui/src/server.tsx
Normal file
19
packages/werewolf-ui/src/server.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { serve } from "bun"
|
||||
import { nanoRemix } from "@workshop/nano-remix"
|
||||
import { join } from "node:path"
|
||||
|
||||
const server = serve({
|
||||
routes: {
|
||||
"/*": (req) => {
|
||||
const routePath = join(import.meta.dir, "routes")
|
||||
return nanoRemix(req, { routePath })
|
||||
},
|
||||
},
|
||||
|
||||
development: process.env.NODE_ENV !== "production" && {
|
||||
hmr: true,
|
||||
console: true,
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`🚀 Server running at ${server.url}`)
|
||||
33
packages/werewolf-ui/src/types.ts
Normal file
33
packages/werewolf-ui/src/types.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
type TailwindSizeNumber =
|
||||
| 0
|
||||
| 1
|
||||
| 2
|
||||
| 3
|
||||
| 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 = TailwindSizeNumber | `${TailwindSizeNumber}`
|
||||
18
packages/werewolf-ui/tsconfig.json
Normal file
18
packages/werewolf-ui/tsconfig.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "hono/jsx",
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user