Compare commits

...

4 Commits

Author SHA1 Message Date
Rami Bitar
98dd7741a0 update 2026-06-09 17:06:15 -04:00
Rami Bitar
1ebe68efeb Wrap delegated Button/Image in DOM element, remove data-slot attributes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 16:06:15 -04:00
Rami Bitar
791a257294 Remove elements barrel file and next/image, add image/number field types
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:59:47 -04:00
Rami Bitar
3ed7e027c2 add elements components 2026-06-09 14:43:25 -04:00
12 changed files with 528 additions and 3 deletions

View File

@@ -1,9 +1,14 @@
import { Typography } from "@/components/elements/Typography"
const Home: React.FC = () => {
return (
<div className="flex items-center justify-center w-full h-screen">
<h1 className="text-6xl font-bold font-heading text-black text-center">
Start prompting
</h1>
<Typography
variant="h1"
textAlign="center"
text="Start prompting"
className="text-6xl font-bold font-heading text-black"
/>
</div>
)
}

View File

@@ -0,0 +1,38 @@
import type { ElementConfig } from "./types"
export const ButtonConfig: ElementConfig = {
Button: {
label: "Button",
fields: {
children: {
type: "text",
label: "Label",
},
variant: {
type: "select",
label: "Variant",
options: [
{ label: "Default", value: "default" },
{ label: "Destructive", value: "destructive" },
{ label: "Outline", value: "outline" },
{ label: "Secondary", value: "secondary" },
{ label: "Ghost", value: "ghost" },
{ label: "Link", value: "link" },
],
},
size: {
type: "select",
label: "Size",
options: [
{ label: "Default", value: "default" },
{ label: "Extra Small", value: "xs" },
{ label: "Small", value: "sm" },
{ label: "Large", value: "lg" },
{ label: "Icon", value: "icon" },
],
},
},
},
}
export default ButtonConfig

View File

@@ -0,0 +1,30 @@
import * as React from "react"
import {
Button as ShadcnButton,
type buttonVariants,
} from "@/components/ui/button"
import type { VariantProps } from "class-variance-authority"
export type ButtonVariant = NonNullable<
VariantProps<typeof buttonVariants>["variant"]
>
export type ButtonSize = NonNullable<
VariantProps<typeof buttonVariants>["size"]
>
export interface ButtonProps
extends React.ComponentProps<typeof ShadcnButton> {
variant?: ButtonVariant
size?: ButtonSize
}
function Button({ variant = "default", size = "default", ...props }: ButtonProps) {
return (
<span className="inline-block">
<ShadcnButton variant={variant} size={size} {...props} />
</span>
)
}
export { Button }

View File

@@ -0,0 +1,27 @@
import type { ElementConfig } from "./types"
export const CardConfig: ElementConfig = {
Card: {
label: "Card",
fields: {
image: {
type: "text",
label: "Image URL",
},
title: {
type: "text",
label: "Title",
},
subtitle: {
type: "text",
label: "Subtitle",
},
tags: {
type: "tags",
label: "Tags",
},
},
},
}
export default CardConfig

View File

@@ -0,0 +1,68 @@
import * as React from "react"
import {
Card as ShadcnCard,
CardContent,
CardHeader,
} from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { cn } from "@/lib/utils"
import { Image } from "./Image"
import { Typography } from "./Typography"
export interface CardProps extends React.ComponentProps<typeof ShadcnCard> {
image?: string
title?: string
subtitle?: string
tags?: string[]
}
function Card({
image,
title,
subtitle,
tags,
className,
...props
}: CardProps) {
return (
<ShadcnCard
className={cn("overflow-hidden pt-0", className)}
{...props}
>
{image ? (
<Image
src={image}
alt={title ?? ""}
objectFit="cover"
width={600}
height={300}
className="h-48 w-full rounded-none"
/>
) : null}
<CardHeader>
{title ? <Typography variant="h5" text={title} /> : null}
{subtitle ? (
<Typography
variant="body2"
text={subtitle}
className="text-muted-foreground"
/>
) : null}
</CardHeader>
{tags && tags.length > 0 ? (
<CardContent className="flex flex-wrap gap-2">
{tags.map((tag, index) => (
<Badge key={`${tag}-${index}`} variant="secondary">
{tag}
</Badge>
))}
</CardContent>
) : null}
</ShadcnCard>
)
}
export { Card }

View File

@@ -0,0 +1,23 @@
import type { ElementConfig } from "./types"
export const IconConfig: ElementConfig = {
Icon: {
label: "Icon",
fields: {
name: {
type: "icon",
label: "Icon",
},
size: {
type: "text",
label: "Size",
},
className: {
type: "text",
label: "Class Name",
},
},
},
}
export default IconConfig

View File

@@ -0,0 +1,25 @@
import * as React from "react"
import { DynamicIcon, type IconName } from "lucide-react/dynamic"
import { cn } from "@/lib/utils"
export interface IconProps
extends Omit<React.ComponentProps<typeof DynamicIcon>, "name"> {
name: IconName
size?: number
className?: string
}
function Icon({ name, size = 24, className, ...props }: IconProps) {
return (
<DynamicIcon
name={name}
size={size}
className={cn(className)}
{...props}
/>
)
}
export { Icon }
export type { IconName }

View File

@@ -0,0 +1,42 @@
import type { ElementConfig } from "./types"
export const ImageConfig: ElementConfig = {
Image: {
label: "Image",
fields: {
src: {
type: "image",
label: "Source URL",
},
alt: {
type: "text",
label: "Alt Text",
},
objectFit: {
type: "select",
label: "Object Fit",
options: [
{ label: "Cover", value: "cover" },
{ label: "Contain", value: "contain" },
{ label: "Fill", value: "fill" },
{ label: "None", value: "none" },
{ label: "Scale Down", value: "scale-down" },
],
},
circle: {
type: "boolean",
label: "Circle",
},
width: {
type: "number",
label: "Width",
},
height: {
type: "number",
label: "Height",
},
},
},
}
export default ImageConfig

View File

@@ -0,0 +1,57 @@
import * as React from "react"
import NextImage from "next/image"
import { cn } from "@/lib/utils"
export type ObjectFit =
| "contain"
| "cover"
| "fill"
| "none"
| "scale-down"
const objectFitStyles: Record<ObjectFit, string> = {
contain: "object-contain",
cover: "object-cover",
fill: "object-fill",
none: "object-none",
"scale-down": "object-scale-down",
}
export interface ImageProps {
src: string
alt?: string
objectFit?: ObjectFit
circle?: boolean
height?: number
width?: number
className?: string
}
function Image({
src,
alt = "",
objectFit = "cover",
circle = false,
height = 300,
width = 300,
className,
}: ImageProps) {
return (
<span className="inline-block">
<NextImage
src={src}
alt={alt}
height={height}
width={width}
className={cn(
objectFitStyles[objectFit],
circle ? "rounded-full" : "rounded-md",
className
)}
/>
</span>
)
}
export { Image }

View File

@@ -0,0 +1,59 @@
import type { ElementConfig } from "./types"
export const TypographyConfig: ElementConfig = {
Typography: {
label: "Typography",
fields: {
text: {
type: "textarea",
label: "Text",
},
variant: {
type: "select",
label: "Variant",
options: [
{ label: "Heading 1", value: "h1" },
{ label: "Heading 2", value: "h2" },
{ label: "Heading 3", value: "h3" },
{ label: "Heading 4", value: "h4" },
{ label: "Heading 5", value: "h5" },
{ label: "Heading 6", value: "h6" },
{ label: "Body 1", value: "body1" },
{ label: "Body 2", value: "body2" },
{ label: "Caption", value: "caption" },
],
},
textAlign: {
type: "select",
label: "Text Align",
options: [
{ label: "Left", value: "left" },
{ label: "Center", value: "center" },
{ label: "Right", value: "right" },
{ label: "Justify", value: "justify" },
],
},
fontWeight: {
type: "select",
label: "Font Weight",
options: [
{ label: "Thin", value: "thin" },
{ label: "Extra Light", value: "extralight" },
{ label: "Light", value: "light" },
{ label: "Normal", value: "normal" },
{ label: "Medium", value: "medium" },
{ label: "Semibold", value: "semibold" },
{ label: "Bold", value: "bold" },
{ label: "Extra Bold", value: "extrabold" },
{ label: "Black", value: "black" },
],
},
className: {
type: "text",
label: "Class Name",
},
},
},
}
export default TypographyConfig

View File

@@ -0,0 +1,119 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export type TypographyVariant =
| "h1"
| "h2"
| "h3"
| "h4"
| "h5"
| "h6"
| "body1"
| "body2"
| "caption"
export type TypographyAlign = "left" | "center" | "right" | "justify"
export type TypographyWeight =
| "thin"
| "extralight"
| "light"
| "normal"
| "medium"
| "semibold"
| "bold"
| "extrabold"
| "black"
const variantStyles: Record<TypographyVariant, string> = {
h1: "font-heading scroll-m-20 text-4xl tracking-tight text-balance lg:text-5xl",
h2: "font-heading scroll-m-20 text-3xl tracking-tight",
h3: "font-heading scroll-m-20 text-2xl tracking-tight",
h4: "font-heading scroll-m-20 text-xl tracking-tight",
h5: "font-heading scroll-m-20 text-lg tracking-tight",
h6: "font-heading scroll-m-20 text-base tracking-tight",
body1: "leading-7 text-base",
body2: "leading-6 text-sm",
caption: "text-xs text-muted-foreground",
}
const variantElement: Record<TypographyVariant, React.ElementType> = {
h1: "h1",
h2: "h2",
h3: "h3",
h4: "h4",
h5: "h5",
h6: "h6",
body1: "p",
body2: "p",
caption: "span",
}
const weightStyles: Record<TypographyWeight, string> = {
thin: "font-thin",
extralight: "font-extralight",
light: "font-light",
normal: "font-normal",
medium: "font-medium",
semibold: "font-semibold",
bold: "font-bold",
extrabold: "font-extrabold",
black: "font-black",
}
const alignStyles: Record<TypographyAlign, string> = {
left: "text-left",
center: "text-center",
right: "text-right",
justify: "text-justify",
}
const defaultWeight: Record<TypographyVariant, TypographyWeight> = {
h1: "bold",
h2: "bold",
h3: "bold",
h4: "bold",
h5: "bold",
h6: "bold",
body1: "normal",
body2: "normal",
caption: "normal",
}
export interface TypographyProps
extends Omit<React.HTMLAttributes<HTMLElement>, "children"> {
text?: React.ReactNode
variant?: TypographyVariant
textAlign?: TypographyAlign
fontWeight?: TypographyWeight
className?: string
}
function Typography({
text,
variant = "body1",
textAlign,
fontWeight,
className,
...props
}: TypographyProps) {
const Comp = variantElement[variant]
return (
<Comp
data-variant={variant}
className={cn(
variantStyles[variant],
weightStyles[fontWeight ?? defaultWeight[variant]],
textAlign && alignStyles[textAlign],
className
)}
{...props}
>
{text}
</Comp>
)
}
export { Typography }

View File

@@ -0,0 +1,32 @@
export type FieldType =
| "text"
| "textarea"
| "color"
| "icon"
| "array"
| "object"
| "boolean"
| "select"
| "image"
| "number"
| "tags"
export interface FieldOption {
label: string
value: string
}
export interface FieldConfig {
type: FieldType
label?: string
options?: FieldOption[]
arrayFields?: Record<string, FieldConfig>
objectFields?: Record<string, FieldConfig>
}
export interface ComponentConfig {
label: string
fields: Record<string, FieldConfig>
}
export type ElementConfig = Record<string, ComponentConfig>