Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .eslintrc.json

This file was deleted.

24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: CI

on:
pull_request:
push:
branches:
- main

jobs:
check:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Typecheck and validate registry
run: bun run check
26 changes: 15 additions & 11 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
# Contributing to this repository
# Contributing

### Introduction
beUI is a React, TypeScript, Motion and Tailwind CSS component library.

- UI Components is built with React.js, Typescript, framer motion & TailwindCSS
- Check out the existing issues for ways to contribute
## Before You Open a PR

### Have a new feature request or see a bug?
Run the project checks:

Create a new issue! On the issue we can discuss the problem and assign the work.
```bash
bun install
bun run check
```

### Ready to contribute?
`bun run check` typechecks the app and verifies every registry component can publish its source files.

1. Comment on the issue to claim it
2. Create a fork of the repo
3. Work on your fork, then open a pull request to develop branch. Tag the issue in your pull request
4. Your PR will be reviewed, and if it is approved it will be merged into `main`
## Pull Requests

1. Open or comment on an issue before starting larger work.
2. Create a fork or feature branch.
3. Keep changes focused and include the component source, preview and registry entry together.
4. Open a pull request against `main`.
10 changes: 2 additions & 8 deletions app/components/[category]/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import type { Metadata } from "next";
import { notFound } from "next/navigation";
import Link from "next/link";
import { ChevronRight } from "lucide-react";
import { promises as fs } from "node:fs";
import path from "node:path";
import { findCategory, findComponent, registry, type ComponentExample } from "@/lib/registry";
import { CodeBlock } from "@/components/app/code-block";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/motion/tabs";
import { getPreview, previews } from "@/components/previews";
import { readSourceFile } from "@/lib/source-files";

export const dynamic = "force-static";
export const dynamicParams = false;
Expand Down Expand Up @@ -85,12 +84,7 @@ export async function generateMetadata({
}

async function loadSource(file: string) {
try {
const p = path.join(process.cwd(), file);
return await fs.readFile(p, "utf8");
} catch {
return "// source unavailable";
}
return readSourceFile(file);
}

export default async function ComponentPage({
Expand Down
5 changes: 2 additions & 3 deletions app/r/[slug]/raw/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import { allComponents, findComponent } from "@/lib/registry";
import { findCategoryBySlug } from "@/lib/registry-server";
import { readSourceFile } from "@/lib/source-files";

export const dynamic = "force-static";

Expand All @@ -19,7 +18,7 @@ export async function GET(
const comp = findComponent(cat.slug, slug);
if (!comp) return new Response("not_found", { status: 404 });
try {
const src = await fs.readFile(path.join(process.cwd(), comp.file), "utf8");
const src = await readSourceFile(comp.file);
return new Response(src, {
headers: {
"content-type": "text/plain; charset=utf-8",
Expand Down
34 changes: 34 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
"files": {
"includes": [
"**",
"!node_modules",
"!.next",
"!build",
"!out",
"!coverage",
"!next-env.d.ts"
]
},
"formatter": {
"enabled": false
},
"css": {
"parser": {
"tailwindDirectives": true
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": {
"noImportantStyles": "off"
},
"security": {
"noDangerouslySetInnerHtml": "off"
}
}
}
}
603 changes: 12 additions & 591 deletions bun.lock

Large diffs are not rendered by default.

23 changes: 1 addition & 22 deletions components/app/code-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,8 @@ type Props = {
lang?: string;
filename?: string;
className?: string;
/** Zero-indexed line numbers to highlight, e.g. [0, 2, 5] */
highlightLines?: number[];
showLineNumbers?: boolean;
};

// Maps user-facing aliases → shiki language IDs
const LANG_MAP: Record<string, string> = {
tsx: "tsx",
ts: "typescript",
Expand All @@ -32,7 +28,6 @@ const LANG_MAP: Record<string, string> = {
sh: "bash",
};

// Short labels shown in the badge
const LANG_LABELS: Record<string, string> = {
tsx: "TSX",
typescript: "TS",
Expand All @@ -48,7 +43,6 @@ export async function CodeBlock({
lang = "tsx",
filename,
className,
showLineNumbers = false,
}: Props) {
const shikiLang = LANG_MAP[lang.toLowerCase()] ?? lang;
const langLabel =
Expand All @@ -65,11 +59,7 @@ export async function CodeBlock({
transformerNotationHighlight(),
transformerNotationDiff(),
transformerNotationFocus(),
] as any,
// Line numbers via CSS counter
...(showLineNumbers && {
structure: "inline",
}),
],
});

const fileDir = filename ? filename.split("/").slice(0, -1).join("/") : null;
Expand All @@ -83,16 +73,13 @@ export async function CodeBlock({
className,
)}
>
{/* ── Header ── */}
{filename ? (
<div className="flex items-center justify-between gap-3 border-b border-(--color-border) bg-(--color-bg)/60 px-4 py-2.5">
<div className="flex min-w-0 items-center gap-2 text-xs">
{/* Lang badge */}
<span className="inline-flex h-5 shrink-0 items-center rounded border border-(--color-border) bg-(--color-bg-elev) px-1.5 font-mono text-[10px] font-semibold tracking-wider text-(--color-fg-muted) uppercase">
{langLabel}
</span>

{/* File icon + path */}
<FileIcon className="h-3.5 w-3.5 shrink-0 text-(--color-fg-muted)" />
<span className="truncate font-mono text-(--color-fg-muted)">
{fileDir && (
Expand All @@ -105,31 +92,23 @@ export async function CodeBlock({
<CopyButton text={code} />
</div>
) : (
/* Floating copy button when no filename */
<div className="absolute right-3 top-3 z-10 opacity-0 transition-opacity group-hover:opacity-100">
<CopyButton text={code} />
</div>
)}

{/* ── Code body ── */}
<ExpandableCode>
<div
className={cn(
"relative",
"px-0 py-4 text-[13px] leading-relaxed",
// Shiki resets
"[&_pre]:!bg-transparent [&_pre]:!p-0",
"[&_code]:font-mono [&_code]:text-[13px]",
// Shiki dual-theme classes
"[&_.shiki]:bg-transparent",
// Per-line padding
"[&_.line]:px-5",
// Highlighted lines (from transformerNotationHighlight)
"[&_.highlighted]:bg-(--color-fg)/[0.07] [&_.highlighted]:border-l-2 [&_.highlighted]:border-blue-500 [&_.highlighted]:!pl-[18px]",
// Diff lines
"[&_.diff.add]:bg-green-500/10 [&_.diff.add]:border-l-2 [&_.diff.add]:border-green-500 [&_.diff.add]:!pl-[18px]",
"[&_.diff.remove]:bg-red-500/10 [&_.diff.remove]:border-l-2 [&_.diff.remove]:border-red-500 [&_.diff.remove]:!pl-[18px]",
// Focus dimming
"[&_.focused]:opacity-100 [&_pre:has(.focused)_.line:not(.focused)]:opacity-30",
"[&_pre:has(.focused)_.line:not(.focused)]:transition-opacity",
)}
Expand Down
2 changes: 1 addition & 1 deletion components/app/expandable-code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function ExpandableCode({ children }: { children: React.ReactNode }) {
<div
ref={contentRef}
className={cn(
"transition-all duration-300",
"transition-[max-height] duration-300 ease-[cubic-bezier(0.23,1,0.32,1)]",
expanded
? "max-h-[640px] overflow-auto"
: canExpand === false
Expand Down
6 changes: 3 additions & 3 deletions components/app/hero-preview-dock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,10 @@ function MainPanel() {
</div>
</div>
<div className="flex items-center gap-2">
<button className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-(--color-border) bg-(--color-bg-elev) text-(--color-fg-muted) press hover:text-(--color-fg)">
<button type="button" className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-(--color-border) bg-(--color-bg-elev) text-(--color-fg-muted) press hover:text-(--color-fg)">
<Bell className="h-3.5 w-3.5" />
</button>
<button className="inline-flex h-8 items-center gap-1.5 rounded-md border border-(--color-border) bg-(--color-bg-elev) px-2.5 text-xs text-(--color-fg) press">
<button type="button" className="inline-flex h-8 items-center gap-1.5 rounded-md border border-(--color-border) bg-(--color-bg-elev) px-2.5 text-xs text-(--color-fg) press">
<Search className="h-3.5 w-3.5 text-(--color-fg-muted)" />
Search
</button>
Expand Down Expand Up @@ -237,7 +237,7 @@ function Spark() {
<p className="text-xs text-(--color-fg-muted)">Last 14 days</p>
<p className="text-xs font-medium text-(--color-fg)">+24%</p>
</div>
<svg viewBox={`0 0 ${w} ${h}`} className="mt-3 h-16 w-full" preserveAspectRatio="none">
<svg viewBox={`0 0 ${w} ${h}`} className="mt-3 h-16 w-full" preserveAspectRatio="none" aria-hidden="true">
<defs>
<linearGradient id="spark" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stopColor="var(--fg)" stopOpacity="0.25" />
Expand Down
12 changes: 3 additions & 9 deletions components/app/landing-component-card.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
"use client";

import Link from "next/link";
import { ArrowUpRight } from "lucide-react";
import { motion } from "motion/react";
import type { ComponentEntry } from "@/lib/registry";
import { getPreview } from "@/components/previews";

Expand All @@ -16,11 +13,8 @@ export function LandingComponentCard({ component }: { component: ComponentEntry
aria-label={`View ${component.name}`}
className="absolute inset-0 z-20 rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-2 focus-visible:ring-offset-(--color-bg)"
/>
<motion.div
initial={false}
whileHover={{ y: -8 }}
transition={{ type: "spring", stiffness: 260, damping: 24, mass: 0.7 }}
className="flex min-h-[318px] flex-col overflow-hidden rounded-lg border border-(--color-border) bg-(--color-bg-elev) transition-colors duration-200 will-change-transform [contain:paint] group-hover/card:border-(--color-border-strong) group-focus-within/card:-translate-y-2 group-focus-within/card:border-(--color-border-strong)"
<div
className="flex min-h-[318px] flex-col overflow-hidden rounded-lg border border-(--color-border) bg-(--color-bg-elev) transition-colors duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] [contain:paint] group-hover/card:border-(--color-border-strong) group-focus-within/card:border-(--color-border-strong)"
>
<div className="relative flex h-52 items-center justify-center overflow-hidden bg-(--color-bg) px-5 py-6 [contain:paint] mask-b-fade">
<div className="pointer-events-none flex w-full max-w-full origin-center scale-75 items-center justify-center overflow-hidden transition-transform duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] [contain:paint] group-hover/card:scale-[0.78] group-focus-within/card:scale-[0.78] [&_*]:!cursor-default">
Expand All @@ -38,7 +32,7 @@ export function LandingComponentCard({ component }: { component: ComponentEntry
</div>
<ArrowUpRight className="mt-0.5 h-3.5 w-3.5 shrink-0 text-(--color-fg-muted) transition-transform duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] group-hover/card:translate-x-0.5 group-hover/card:-translate-y-0.5 group-focus-within/card:translate-x-0.5 group-focus-within/card:-translate-y-0.5" />
</div>
</motion.div>
</div>
</article>
);
}
2 changes: 1 addition & 1 deletion components/motion/animated-badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
type HTMLMotionProps,
type Variants,
} from "motion/react";
import { type ReactNode } from "react";
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";

export type AnimatedBadgeStatus =
Expand Down
4 changes: 3 additions & 1 deletion components/motion/animated-toast-stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,9 @@ export function useAnimatedToastStack({
const timers = toastTimers.current;

return () => {
timers.forEach((entry) => window.clearTimeout(entry.timer));
timers.forEach((entry) => {
window.clearTimeout(entry.timer);
});
timers.clear();
};
}, []);
Expand Down
25 changes: 14 additions & 11 deletions components/motion/command-palette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { motion } from "motion/react";
import { Search, type LucideIcon } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { cn } from "@/lib/utils";

export type CommandItem = {
Expand Down Expand Up @@ -50,13 +50,17 @@ export function CommandPalette({
const [internalOpen, setInternalOpen] = useState(false);
const controlled = controlledOpen !== undefined;
const open = controlled ? controlledOpen : internalOpen;
const setOpen = (v: boolean) => {
const setOpen = useCallback((v: boolean) => {
if (!controlled) setInternalOpen(v);
onOpenChange?.(v);
};
}, [controlled, onOpenChange]);

const [query, setQuery] = useState("");
const [active, setActive] = useState(0);
const updateQuery = useCallback((value: string) => {
setQuery(value);
setActive(0);
}, []);
const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLDivElement>(null);

Expand All @@ -77,15 +81,15 @@ export function CommandPalette({
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open, shortcut]); // eslint-disable-line react-hooks/exhaustive-deps
}, [open, shortcut, setOpen]);

useEffect(() => {
if (open) {
setQuery("");
updateQuery("");
setActive(0);
requestAnimationFrame(() => inputRef.current?.focus());
}
}, [open]);
}, [open, updateQuery]);

useEffect(() => {
if (!open) return;
Expand All @@ -104,14 +108,13 @@ export function CommandPalette({
});
}, [items, query]);

useEffect(() => setActive(0), [query]);

const grouped = useMemo(() => {
const map = new Map<string, CommandItem[]>();
filtered.forEach((it) => {
const g = it.group ?? "Results";
if (!map.has(g)) map.set(g, []);
map.get(g)!.push(it);
const groupItems = map.get(g) ?? [];
groupItems.push(it);
map.set(g, groupItems);
});
return Array.from(map.entries());
}, [filtered]);
Expand Down Expand Up @@ -189,7 +192,7 @@ export function CommandPalette({
<input
ref={inputRef}
value={query}
onChange={(e) => setQuery(e.target.value)}
onChange={(e) => updateQuery(e.target.value)}
placeholder={placeholder}
tabIndex={open ? 0 : -1}
className="h-12 flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground outline-none"
Expand Down
1 change: 1 addition & 0 deletions components/motion/marquee.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export function Marquee({
)}
>
{items.map((child, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: Marquee duplicates static child slots; item order is not mutated.
<div key={i} className="shrink-0">
{child}
</div>
Expand Down
Loading
Loading