diff --git a/.claude/skills/improve/SKILL.md b/.claude/skills/improve/SKILL.md index df5b900..892636d 100644 --- a/.claude/skills/improve/SKILL.md +++ b/.claude/skills/improve/SKILL.md @@ -9,13 +9,13 @@ metadata: # Improve -You are a **senior advisor, not an implementer**. Your job is to deeply understand a codebase, find the highest-value improvement opportunities, and write implementation plans good enough that a *different, less capable model with zero context from this session* can execute, test, and maintain them. +You are a **senior advisor, not an implementer**. Your job is to deeply understand a codebase, find the highest-value improvement opportunities, and write implementation plans good enough that a _different, less capable model with zero context from this session_ can execute, test, and maintain them. The economics of this skill: an expensive, high-ceiling model does the part where intelligence compounds (understanding, judging, specifying). Cheaper models do the execution. The plan is the product — its quality determines whether the executor succeeds. ## Hard Rules -1. **Never modify source code yourself.** No edits, no fixes, no "quick wins while you're in there." The ONLY files you may create or modify live under `plans/` in the repo root — or under `advisor-plans/` when `plans/` already exists for an unrelated purpose (create the chosen directory if absent). The `execute` variant dispatches a *separate executor subagent* that edits code in an isolated git worktree — you review its diff and render a verdict; you still never edit code directly, and you never merge, push, or commit to the user's branch. +1. **Never modify source code yourself.** No edits, no fixes, no "quick wins while you're in there." The ONLY files you may create or modify live under `plans/` in the repo root — or under `advisor-plans/` when `plans/` already exists for an unrelated purpose (create the chosen directory if absent). The `execute` variant dispatches a _separate executor subagent_ that edits code in an isolated git worktree — you review its diff and render a verdict; you still never edit code directly, and you never merge, push, or commit to the user's branch. 2. **Never run commands that mutate the user's working tree** — no installs, no builds that write artifacts outside standard ignored dirs, no git commits, no formatters. Read, search, and run read-only analysis only (e.g. `tsc --noEmit`, lint in check mode, `npm audit` / `pnpm audit`, test suite if cheap and side-effect free). Two scoped exceptions: verification commands inside an executor's disposable worktree during `execute` review, and `gh issue create` under an explicit `--issues` flag. 3. **Every plan must be fully self-contained.** The executor has not seen this conversation, this codebase survey, or any other plan. If a plan references "the pattern discussed above," it is broken. 4. **Never reproduce secret values.** If the audit finds credentials, tokens, or `.env` contents, findings and plans reference the `file:line` and credential type only, and recommend rotation. The value itself must never appear in anything you write. @@ -30,7 +30,7 @@ Map the territory before judging it: - Read `README`, `CLAUDE.md`/`AGENTS.md`, `CONTRIBUTING`, root config files (`package.json`, `pyproject.toml`, `go.mod`, etc.), CI config, and the directory structure. - Identify: language(s), framework(s), package manager, **how to build / test / lint / typecheck** (exact commands — these go into every plan as verification gates), test coverage shape, deployment target. -- Note repo conventions: code style, naming, folder layout, error-handling and state-management patterns. Plans must tell the executor to *match* these, with examples. +- Note repo conventions: code style, naming, folder layout, error-handling and state-management patterns. Plans must tell the executor to _match_ these, with examples. - **Ingest intent & design docs where present** — they record decided tradeoffs and product direction the code itself can't tell you. Glob for ADRs (`docs/adr/`, `docs/adrs/`, `docs/decisions/`), PRDs / specs, `CONTEXT.md` (shared domain vocabulary), `DESIGN.md` (design-system spec), and `PRODUCT.md` (product brief). Strictly additive: read what exists, no-op when absent. Carry what you learn forward — into Vet (a tradeoff recorded in an ADR is by-design, not a finding), Direction (ground suggestions in stated product intent), and the plans themselves (match the documented vocabulary and design system). Reading these docs lets `/improve` compose with repos that already maintain them. - Check git signal where useful (`git log --oneline -30`, churn hotspots) for what's actively evolving vs. frozen. @@ -51,15 +51,15 @@ For repos of any real size, fan out with parallel read-only subagents (in Claude Audit depth follows the **effort level** (default `standard`; the user sets it with a `quick` / `deep` keyword anywhere in the invocation): -| | `quick` | `standard` (default) | `deep` | -|---|---|---|---| -| Coverage | Recon hotspots only — highest-churn, highest-criticality code | Hotspot-weighted, key packages | Whole repo, every package | -| Subagents | 0–1 (sweep directly when feasible) | ≤4 concurrent | ≤8 concurrent, one per category | -| Breadth | "medium" | "very thorough" for correctness + security, "medium" rest | "very thorough" everywhere | -| Categories | correctness, security, tests | all nine | all nine | -| Findings | top ~6, HIGH-confidence only | full table | full table incl. LOW-confidence "investigate" items | +| | `quick` | `standard` (default) | `deep` | +| ---------- | ------------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------- | +| Coverage | Recon hotspots only — highest-churn, highest-criticality code | Hotspot-weighted, key packages | Whole repo, every package | +| Subagents | 0–1 (sweep directly when feasible) | ≤4 concurrent | ≤8 concurrent, one per category | +| Breadth | "medium" | "very thorough" for correctness + security, "medium" rest | "very thorough" everywhere | +| Categories | correctness, security, tests | all nine | all nine | +| Findings | top ~6, HIGH-confidence only | full table | full table incl. LOW-confidence "investigate" items | -Whatever the level, say in the final report what was *not* audited. On a large monorepo even `deep` scopes subagents to packages, not the root. +Whatever the level, say in the final report what was _not_ audited. On a large monorepo even `deep` scopes subagents to packages, not the root. Every finding needs: evidence (`file:line` references), impact, effort estimate (S/M/L), risk of the fix itself, and confidence. No vibes-only findings. diff --git a/.claude/skills/improve/references/audit-playbook.md b/.claude/skills/improve/references/audit-playbook.md index 6c2278c..d3e4e0b 100644 --- a/.claude/skills/improve/references/audit-playbook.md +++ b/.claude/skills/improve/references/audit-playbook.md @@ -25,7 +25,7 @@ Review only what is directly supported by code evidence. Keep findings framed as **Handling rule:** never copy a secret value into a finding or plan — those files get committed. Reference the `file:line` and credential type only ("Stripe live key at `config.ts:12`"), and the fix sketch always includes rotation, not just removal (a committed secret is burned even after deletion). -**By-design is not a finding:** standard platform conventions are intentional behavior — honoring `https_proxy`/`NO_PROXY`, reading `~/.netrc`, an explicitly local dev tool shelling out to configured package managers. A tradeoff explicitly recorded in an ADR or decision doc is likewise settled, not a finding. Flag these only when the *implementation* adds risk beyond the convention or the documented decision itself — and note that a **stale ADR is itself a finding**: if the code has drifted from what the decision doc says, report the decision drift (the doc or the code is wrong; either way the team should know), don't use the doc to suppress it. +**By-design is not a finding:** standard platform conventions are intentional behavior — honoring `https_proxy`/`NO_PROXY`, reading `~/.netrc`, an explicitly local dev tool shelling out to configured package managers. A tradeoff explicitly recorded in an ADR or decision doc is likewise settled, not a finding. Flag these only when the _implementation_ adds risk beyond the convention or the documented decision itself — and note that a **stale ADR is itself a finding**: if the code has drifted from what the decision doc says, report the decision drift (the doc or the code is wrong; either way the team should know), don't use the doc to suppress it. - Credential hygiene: hardcoded keys/tokens/passwords, credentials in committed `.env` files, credentials logged or persisted in event/history stores. Findings should name only the credential type and location, then recommend removal, rotation, and a safer configuration path. - Data crossing into interpreters or privileged APIs: SQL or shell operations assembled from request data (SQL/command injection), HTML sinks fed by user-controlled content (XSS), dynamic execution APIs used with runtime input, or filesystem paths derived from request data (path traversal). Describe the safer API or validation boundary; do not provide runnable examples. @@ -42,14 +42,14 @@ Look for the algorithmic and architectural wins, not micro-optimizations. - N+1 patterns: query/fetch per item inside loops or per list-row rendering; missing batching or dataloader. - Wrong complexity: nested scans over the same collection, repeated `find`/`filter` inside hot loops where a Map keyed lookup belongs. - Caching gaps: identical expensive computations or fetches repeated per request/render; missing memoization at clear function boundaries; no HTTP/data-layer caching on stable data. -- Payload size: over-fetching (select *, full objects where IDs suffice), missing pagination on unbounded lists, large JSON shipped to clients. +- Payload size: over-fetching (select \*, full objects where IDs suffice), missing pagination on unbounded lists, large JSON shipped to clients. - Frontend (if applicable): bundle composition (heavyweight deps for trivial use), missing code-splitting on rarely-hit routes, unoptimized images/fonts, client-side fetching for data available at render time, render waterfalls. For React/Next.js, defer to the repo's framework conventions and any installed best-practices guidelines. - Backend: synchronous work that belongs in a queue, missing indexes implied by query patterns (flag for verification — don't claim without schema evidence), connection-per-request patterns where pooling exists. - Build/CI: slow CI from missing caching, redundant pipeline steps, test suites that could parallelize. ## 4. Test Coverage -The goal is not a percentage — it's *which untested code is dangerous*. +The goal is not a percentage — it's _which untested code is dangerous_. - Map the critical paths (money, auth, data mutation, the feature the repo exists for) and check which have zero or trivial coverage. - Modules with high churn (git log) + no tests = top refactor risk; flag as "characterization tests first" candidates. @@ -101,7 +101,7 @@ Forward-looking: not what's broken, but what this codebase wants to become. **Gr - **The adjacent possible**: capabilities the existing architecture makes disproportionately cheap — a plugin system one interface away, a public API one route file from the existing service layer, an integration the data model already supports. - **Friction worth productizing**: things users of this project evidently do by hand around it (visible in docs, examples, issues) that the project could absorb. -Direction findings use the standard format with two adaptations: **Impact** is product/user value (who wants this and why now), and **Confidence** reflects how grounded the evidence is — not certainty that it's the right call. Strategy belongs to the maintainer; the advisor's job is grounded options with honest trade-offs. Effort estimates here are coarser; say so. Plans for selected direction findings are usually a *design/spike plan* (investigate, prototype, define the API, list open questions) rather than a build-everything plan — scope them that way. +Direction findings use the standard format with two adaptations: **Impact** is product/user value (who wants this and why now), and **Confidence** reflects how grounded the evidence is — not certainty that it's the right call. Strategy belongs to the maintainer; the advisor's job is grounded options with honest trade-offs. Effort estimates here are coarser; say so. Plans for selected direction findings are usually a _design/spike plan_ (investigate, prototype, define the API, list open questions) rather than a build-everything plan — scope them that way. --- @@ -114,7 +114,7 @@ Every finding, from every category and every subagent, comes back in this shape: - **Evidence**: `path/file.ts:123` — one-sentence description of what's there. (Repeat per location; 2–5 strongest locations, note "and ~N similar sites" if widespread.) - **Impact**: What goes wrong / what's being paid because of this. Concrete: "every order-list render issues 1+N queries", not "suboptimal". -- **Effort**: S (hours) / M (a day-ish) / L (multi-day) — for the *fix*, including tests. +- **Effort**: S (hours) / M (a day-ish) / L (multi-day) — for the _fix_, including tests. - **Risk**: What the fix could break; LOW/MED/HIGH plus one line why. - **Confidence**: HIGH (read the code, certain) / MED (strong signal, needs verification) / LOW (smell, needs investigation). LOW-confidence findings may be reported but get an "investigate" plan, not a "fix" plan. - **Fix sketch**: 1–3 sentences. Not the plan — just enough to judge effort honestly. diff --git a/.claude/skills/improve/references/closing-the-loop.md b/.claude/skills/improve/references/closing-the-loop.md index f0e524f..81bffb9 100644 --- a/.claude/skills/improve/references/closing-the-loop.md +++ b/.claude/skills/improve/references/closing-the-loop.md @@ -2,7 +2,7 @@ The advisor's job doesn't end at the plan. This file covers the three follow-through flows: dispatching an executor and reviewing its work (`execute`), keeping the plan backlog alive (`reconcile`), and publishing plans where work gets picked up (`--issues`). -The founding rule survives unchanged: **the advisor never edits source code.** In `execute`, a *separate executor subagent* edits code in an isolated git worktree; the advisor dispatches, reviews, and renders a verdict — like a tech lead who doesn't push commits to your branch. +The founding rule survives unchanged: **the advisor never edits source code.** In `execute`, a _separate executor subagent_ edits code in an isolated git worktree; the advisor dispatches, reviews, and renders a verdict — like a tech lead who doesn't push commits to your branch. --- @@ -58,13 +58,13 @@ Review like a tech lead reviewing a PR against the spec — never fix anything y ### Verdict -**Documented deviations are judged on merit, not reflex-blocked.** "Do not improvise" exists to stop silent drift; an executor that hits a real obstacle (e.g. the plan's approach breaks existing test mocks), adapts minimally, and explains it in NOTES has done the right thing. Approve it if the adaptation serves the plan's intent and stays in scope; treat *undocumented* deviations as review failures. +**Documented deviations are judged on merit, not reflex-blocked.** "Do not improvise" exists to stop silent drift; an executor that hits a real obstacle (e.g. the plan's approach breaks existing test mocks), adapts minimally, and explains it in NOTES has done the right thing. Approve it if the adaptation serves the plan's intent and stays in scope; treat _undocumented_ deviations as review failures. -| Verdict | When | Action | -|---|---|---| -| **APPROVE** | Criteria pass, scope clean, quality holds | Update index status to DONE. Present to the user: diff summary, worktree path and branch, anything from NOTES. **Merging is the user's decision — never merge, push, or commit to their branch.** | -| **REVISE** | Fixable gaps | SendMessage to the same executor with specific, actionable feedback ("criterion 3 fails: X; the error handling in `api.ts:90` swallows the error — use the Result pattern per the plan"). **Max 2 revision rounds**, then BLOCK. | -| **BLOCK** | STOP condition hit, scope violated unrecoverably, or revisions exhausted | Mark BLOCKED in the index with the reason. Refine or rewrite the plan with what was learned. Tell the user what happened and what changed in the plan. | +| Verdict | When | Action | +| ----------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **APPROVE** | Criteria pass, scope clean, quality holds | Update index status to DONE. Present to the user: diff summary, worktree path and branch, anything from NOTES. **Merging is the user's decision — never merge, push, or commit to their branch.** | +| **REVISE** | Fixable gaps | SendMessage to the same executor with specific, actionable feedback ("criterion 3 fails: X; the error handling in `api.ts:90` swallows the error — use the Result pattern per the plan"). **Max 2 revision rounds**, then BLOCK. | +| **BLOCK** | STOP condition hit, scope violated unrecoverably, or revisions exhausted | Mark BLOCKED in the index with the reason. Refine or rewrite the plan with what was learned. Tell the user what happened and what changed in the plan. | Running verification commands inside the executor's worktree is fine — it's isolated and disposable. The no-mutating-commands rule protects the user's working tree, not the worktree. diff --git a/.claude/skills/improve/references/plan-template.md b/.claude/skills/improve/references/plan-template.md index f4ff6fc..f164b45 100644 --- a/.claude/skills/improve/references/plan-template.md +++ b/.claude/skills/improve/references/plan-template.md @@ -5,7 +5,7 @@ Every plan is written for an executor model that has **zero context**: it has no Three properties make a plan executable by a weaker model: 1. **Self-contained context** — everything needed is in the file: paths, code excerpts, conventions, commands. -2. **Verification gates** — every step ends with a command and its expected result. The executor never has to *judge* whether it succeeded. +2. **Verification gates** — every step ends with a command and its expected result. The executor never has to _judge_ whether it succeeded. 3. **Hard boundaries and escape hatches** — explicit out-of-scope list, and "STOP and report" conditions instead of letting the model improvise when reality doesn't match the plan. File naming: `plans/NNN-short-slug.md`, numbered in recommended execution order. @@ -34,7 +34,7 @@ File naming: `plans/NNN-short-slug.md`, numbered in recommended execution order. - **Priority**: P1 | P2 | P3 - **Effort**: S | M | L - **Risk**: LOW | MED | HIGH -- **Depends on**: plans/NNN-*.md (or "none") +- **Depends on**: plans/NNN-\*.md (or "none") - **Category**: bug | security | perf | tests | tech-debt | migration | dx | docs | direction - **Planned at**: commit ``, - **Issue**: @@ -64,12 +64,12 @@ The facts the executor needs, inlined — never "as discussed" or "see audit": ## Commands you will need -| Purpose | Command | Expected on success | -|-----------|--------------------------|---------------------| -| Install | `pnpm install` | exit 0 | -| Typecheck | `pnpm typecheck` | exit 0, no errors | -| Tests | `pnpm test -- ` | all pass | -| Lint | `pnpm lint` | exit 0 | +| Purpose | Command | Expected on success | +| --------- | ----------------------- | ------------------- | +| Install | `pnpm install` | exit 0 | +| Typecheck | `pnpm typecheck` | exit 0, no errors | +| Tests | `pnpm test -- ` | all pass | +| Lint | `pnpm lint` | exit 0 | (Exact commands from this repo — verified during recon, not guessed.) @@ -85,10 +85,12 @@ executor's environment. Skip the section otherwise.) ## Scope **In scope** (the only files you should modify): + - `src/orders/api.ts` - `src/orders/api.test.ts` (create) **Out of scope** (do NOT touch, even though they look related): + - `src/orders/legacy-api.ts` — deprecated path, scheduled for deletion; changing it wastes effort and risks the v1 clients still pinned to it. - Any change to the public response shape — clients depend on it. @@ -171,7 +173,7 @@ honor its STOP conditions, and update your row when done. ## Execution order & status | Plan | Title | Priority | Effort | Depends on | Status | -|------|-------|----------|--------|------------|--------| +| ---- | ----- | -------- | ------ | ---------- | ------ | | 001 | ... | P1 | S | — | TODO | | 002 | ... | P1 | M | 001 | TODO | diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dd84ea7..9b77ea7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,10 +1,9 @@ --- name: Bug report about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - +title: "" +labels: "" +assignees: "" --- **Describe the bug** @@ -12,6 +11,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,15 +24,17 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] + +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] **Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md index 48d5f81..babf9b2 100644 --- a/.github/ISSUE_TEMPLATE/custom.md +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -1,10 +1,7 @@ --- name: Custom issue template about: Describe this issue template's purpose here. -title: '' -labels: '' -assignees: '' - +title: "" +labels: "" +assignees: "" --- - - diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bbcbbe7..2bc5d5f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,10 +1,9 @@ --- name: Feature request about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - +title: "" +labels: "" +assignees: "" --- **Is your feature request related to a problem? Please describe.** diff --git a/.github/prompts/prompt-screenshot-to-code.prompts.md b/.github/prompts/prompt-screenshot-to-code.prompts.md index 96fb1ed..63b7701 100644 --- a/.github/prompts/prompt-screenshot-to-code.prompts.md +++ b/.github/prompts/prompt-screenshot-to-code.prompts.md @@ -2,7 +2,7 @@ You can copy and paste the following prompt into any Vision-capable AI model (like Gemini 1.5 Pro, Claude 3.5 Sonnet, or GPT-4o) along with your screenshot. -*** +--- **Copy from below:** @@ -14,7 +14,7 @@ You are an expert Frontend Architect specializing in **Next.js 15, React, Tailwi ### 🚨 CRITICAL RULES (ZERO TOLERANCE FOR VIOLATION) #### 1. NO EXTERNAL OR HARDCODED COLORS -You are STRICTLY FORBIDDEN from using arbitrary hex codes, rgb values, or arbitrary Tailwind values (e.g., NEVER use `bg-[#ffde09]`, `text-[#123456]`, `border-[#eee]`). +You are STRICTLY FORBIDDEN from using arbitrary hex codes, rgb values, or arbitrary Tailwind values (e.g., NEVER use `bg-[#ffde09]`, `text-[#123456]`, `border-[#eee]`). Instead, you MUST map every color in the screenshot to one of our project's built-in CSS variables and Tailwind classes. - **Core Theme Color:** `bg-primary`, `text-primary`. - **SPECIFIC COLOR REPLACEMENTS (DO NOT USE SEMANTIC NAMES):** You MUST use these specific Tailwind-based tokens instead of generic semantic names: @@ -33,14 +33,14 @@ Our project utilizes Shadcn UI (Radix Primitives). All foundational UI elements - **Let the components style themselves:** You do NOT need to write `py-5 px-4 bg-background border-border transition-opacity` on Shadcn primitives like `` or ` - ); - }, -); + ) + } +) -CodeBlock.displayName = "CodeBlock"; +CodeBlock.displayName = "CodeBlock" // ------------------------------ | PAGE - SHADCN CLI | ------------------------------ // @@ -89,13 +86,14 @@ export default function ShadcnCliPage() {
-
+
-

+

Shadcn CLI Integration

- Learn how to configure your project to copy and paste {branding.brandName} + Learn how to configure your project to copy and paste{" "} + {branding.brandName} components directly via the command line.

@@ -104,45 +102,48 @@ export default function ShadcnCliPage() { CLI Support - {branding.brandName} provides a fully compatible Shadcn registry endpoint - that integrates natively with the new Shadcn CLI. + {branding.brandName} provides a fully compatible Shadcn + registry endpoint that integrates natively with the new + Shadcn CLI.
-
+
-
+
Overview

Rather than manually copying files and installing imports, - you can configure the Shadcn CLI to resolve {branding.brandName} + you can configure the Shadcn CLI to resolve{" "} + {branding.brandName} components. Using custom registries allows you to bring - {branding.brandName} variants into your project with a single command. + {branding.brandName} variants into your project with a + single command.

-
+
-
+
1. Registry Configuration

- To point the Shadcn CLI to the {branding.brandName} registry, add the{" "} - @uiable namespace under the registries field - in your components.json file at the root of your - project: + To point the Shadcn CLI to the {branding.brandName}{" "} + registry, add the @uiable namespace under the{" "} + registries field in your components.json file + at the root of your project:

-
-
+
+
{`{ "registries": { @@ -157,19 +158,19 @@ export default function ShadcnCliPage() {
-
+
-
+
2. Usage & Installation

- Once configured, you can install any {branding.brandName} component by - referencing the @uiable namespace. For example, to - add the basic accordion, run: + Once configured, you can install any {branding.brandName}{" "} + component by referencing the @uiable namespace. For + example, to add the basic accordion, run:

-
-
+
+
{`npx shadcn add @uiable/accordion-basic`}
@@ -185,13 +186,13 @@ export default function ShadcnCliPage() {
-
+
-
+
Benefits of using the CLI
-
    +
    • Dependency Auto-Resolution: The CLI automatically detects and installs required packages (like{" "} @@ -223,9 +224,9 @@ export default function ShadcnCliPage() { />
-
+
- +
On this page
@@ -239,5 +240,5 @@ export default function ShadcnCliPage() {
- ); + ) } diff --git a/src/app/(dashboard-layout)/doc/components/page.tsx b/src/app/(dashboard-layout)/doc/components/page.tsx index 946a8fa..1e31eb0 100644 --- a/src/app/(dashboard-layout)/doc/components/page.tsx +++ b/src/app/(dashboard-layout)/doc/components/page.tsx @@ -1,31 +1,28 @@ -"use client"; -import { memo, useEffect, useState } from "react"; - -// shadcn -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { Card, CardContent, CardHeader } from "@/components/ui/card"; -import { Separator } from "@/components/ui/separator"; +"use client" +import { memo, useEffect, useState } from "react" // project -import branding from "@/branding.json"; -import DocsNavigation from "@/components/doc-bottom-nav"; -import TableOfContents from "@/components/uiable/layout/table-of-contents"; - +import branding from "@/branding.json" // assets import { - Code, - Terminal, - ClipboardCopy, - Sparkles, BookOpen, + ClipboardCopy, + Code, Copy, + Sparkles, SquareCheckBig, -} from "lucide-react"; - -import { Button } from "@/components/ui/button"; - + Terminal, +} from "lucide-react" // third party -import { type BundledLanguage, codeToHtml } from "shiki"; +import { codeToHtml, type BundledLanguage } from "shiki" + +// shadcn +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader } from "@/components/ui/card" +import { Separator } from "@/components/ui/separator" +import DocsNavigation from "@/components/doc-bottom-nav" +import TableOfContents from "@/components/uiable/layout/table-of-contents" // constants const tocItems = [ @@ -34,39 +31,39 @@ const tocItems = [ { title: "Manual Copy-Paste Method", url: "#manual-method" }, { title: "Usage Example", url: "#usage" }, { title: "Customization", url: "#customization" }, -]; +] const CodeBlock = memo( ({ children, lang }: { children: string; lang: BundledLanguage }) => { - const [html, setHtml] = useState(""); - const [copied, setCopied] = useState(false); + const [html, setHtml] = useState("") + const [copied, setCopied] = useState(false) useEffect(() => { - let mounted = true; + let mounted = true codeToHtml(children, { lang, theme: "slack-dark" }).then((res) => { - if (mounted) setHtml(res); - }); + if (mounted) setHtml(res) + }) return () => { - mounted = false; - }; - }, [children, lang]); + mounted = false + } + }, [children, lang]) const handleCopy = () => { - navigator.clipboard.writeText(children); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; + navigator.clipboard.writeText(children) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } return ( -
+
- ); - }, -); + ) + } +) -CodeBlock.displayName = "CodeBlock"; +CodeBlock.displayName = "CodeBlock" // ------------------------------ | PAGE - COMPONENTS | ------------------------------ // @@ -91,15 +88,15 @@ export default function ComponentsPage() {
-
+
-

+

Using {branding.brandName} Components

- {branding.brandName} components are exported directly into your project's - codebase as raw source files. Learn how to install and - integrate them. + {branding.brandName} components are exported directly into + your project's codebase as raw source files. Learn how to + install and integrate them.

@@ -107,26 +104,26 @@ export default function ComponentsPage() { Developer Freedom - Because {branding.brandName} components are not locked inside an NPM - package, you have 100% control over the DOM structure, React - logic, and styling from day one. + Because {branding.brandName} components are not locked + inside an NPM package, you have 100% control over the DOM + structure, React logic, and styling from day one.
-
+
-
+
Overview

- There are two main ways to integrate {branding.brandName} components into - your application: + There are two main ways to integrate {branding.brandName}{" "} + components into your application:

-
    +
    1. Using the Shadcn CLI (Recommended): Installs components and their dependencies automatically with one @@ -143,9 +140,9 @@ export default function ComponentsPage() {
      -
      +
      -
      +
      Method 1: Shadcn CLI (Recommended)
      @@ -158,8 +155,8 @@ export default function ComponentsPage() { For example, to install the borders variant of the accordion, run:

      -
      -
      +
      +
      {`npx shadcn add @uiable/accordion-borders`}
      @@ -176,9 +173,9 @@ export default function ComponentsPage() {
      -
      +
      -
      +
      Method 2: Manual Copy & Paste
      @@ -186,9 +183,10 @@ export default function ComponentsPage() { If you prefer not to use the CLI, you can add components manually:

      -
        +
        • - Browse to the component in the {branding.brandName} Catalog. + Browse to the component in the {branding.brandName}{" "} + Catalog.
        • Click the Copy Code button on the variant page. @@ -210,9 +208,9 @@ export default function ComponentsPage() {
          -
          +
          -
          +
          Importing and Usage
          @@ -220,8 +218,8 @@ export default function ComponentsPage() { Once a component is placed in your workspace, you can import and use it in your React pages:

          -
          -
          +
          +
          {`import AccordionBorders from "@/components/uiable/accordion/accordion-borders"; @@ -240,7 +238,7 @@ export default function MyPage() {
          -

          +

          Customization

          @@ -264,9 +262,9 @@ export default function MyPage() { />

          -
          +
          - +
          On this page
          @@ -280,5 +278,5 @@ export default function MyPage() {
          - ); + ) } diff --git a/src/app/(dashboard-layout)/doc/installation/page.tsx b/src/app/(dashboard-layout)/doc/installation/page.tsx index 7a6e6a4..3cdc55e 100644 --- a/src/app/(dashboard-layout)/doc/installation/page.tsx +++ b/src/app/(dashboard-layout)/doc/installation/page.tsx @@ -1,24 +1,22 @@ "use client" + import { memo, useEffect, useState } from "react" +// project +import branding from "@/branding.json" +// assets +import { Book, InfoCircle, Magicpen, Setting2 } from "iconsax-reactjs" +import { Copy, SquareCheckBig } from "lucide-react" +// third party +import { codeToHtml, type BundledLanguage } from "shiki" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" // shadcn import { Button } from "@/components/ui/button" -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Card, CardContent, CardHeader } from "@/components/ui/card" import { Separator } from "@/components/ui/separator" - -// project -import branding from "@/branding.json" import DocsNavigation from "@/components/doc-bottom-nav" import TableOfContents from "@/components/uiable/layout/table-of-contents" -// assets -import { Book, InfoCircle, Magicpen, Setting2 } from "iconsax-reactjs" - -// third party -import { codeToHtml, type BundledLanguage } from "shiki" -import { Copy, SquareCheckBig } from "lucide-react" - // constants const tocItems = [ { title: "Prerequisites", url: "#prerequisites" }, diff --git a/src/app/(dashboard-layout)/doc/introduction/page.tsx b/src/app/(dashboard-layout)/doc/introduction/page.tsx index a2420e7..58013ac 100644 --- a/src/app/(dashboard-layout)/doc/introduction/page.tsx +++ b/src/app/(dashboard-layout)/doc/introduction/page.tsx @@ -1,10 +1,10 @@ // shadcn -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { Card, CardContent, CardHeader } from "@/components/ui/card" -import { Separator } from "@/components/ui/separator" // project imports import branding from "@/branding.json" + +import { Card, CardContent, CardHeader } from "@/components/ui/card" +import { Separator } from "@/components/ui/separator" import DocsNavigation from "@/components/doc-bottom-nav" import TableOfContents from "@/components/uiable/layout/table-of-contents" diff --git a/src/app/(dashboard-layout)/doc/license/page.tsx b/src/app/(dashboard-layout)/doc/license/page.tsx index 04a2860..2b85d04 100644 --- a/src/app/(dashboard-layout)/doc/license/page.tsx +++ b/src/app/(dashboard-layout)/doc/license/page.tsx @@ -1,16 +1,16 @@ // shadcn -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { Card, CardContent, CardHeader } from "@/components/ui/card" -import { Separator } from "@/components/ui/separator" // project imports import branding from "@/branding.json" -import DocsNavigation from "@/components/doc-bottom-nav" -import TableOfContents from "@/components/uiable/layout/table-of-contents" - // assets import { DocumentText, Global, SecuritySafe, Shield } from "iconsax-reactjs" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { Card, CardContent, CardHeader } from "@/components/ui/card" +import { Separator } from "@/components/ui/separator" +import DocsNavigation from "@/components/doc-bottom-nav" +import TableOfContents from "@/components/uiable/layout/table-of-contents" + // constants const tocItems = [ { title: "Standard License", url: "#standard" }, diff --git a/src/app/(dashboard-layout)/layout.tsx b/src/app/(dashboard-layout)/layout.tsx index 5ad958a..6efb479 100644 --- a/src/app/(dashboard-layout)/layout.tsx +++ b/src/app/(dashboard-layout)/layout.tsx @@ -10,14 +10,12 @@ import { SidebarTrigger, } from "@/components/ui/sidebar" -// third party -import { Toaster } from "sonner" - // project import branding from "@/branding.json" import { AppSidebar } from "@/components/app-sidebar" import { ThemeToggle } from "@/components/customizer" import Footer from "@/components/uiable/blocks/landing/footer/footer" +// import SearchBar from "@/components/uiable/layout/search-bar"; // assets import { @@ -26,7 +24,8 @@ import { IconBrandX, } from "@tabler/icons-react" -// import SearchBar from "@/components/uiable/layout/search-bar"; +// third party +import { Toaster } from "sonner" function Divider() { return @@ -36,78 +35,81 @@ function Divider() { export default function DashboardLayout({ children }: { children: ReactNode }) { return ( - - - -
          - - {/* */} -
          -
          - - +
          + + + +
          + + {/* */} +
          +
          + + - - + + - - - - {/*
          */} - {/* */} - {/*
          */} - {/* */} -
          -
          -
          - {children} - -
          -
          -
          -
          + + + + {/*
          */} + {/* */} + {/*
          */} + {/* */} +
          +
          +
          + {children} + +
          + +
          + + +
          ) } diff --git a/src/app/(dashboard-layout)/loading.tsx b/src/app/(dashboard-layout)/loading.tsx index 2fbc9c0..9aa2ad3 100644 --- a/src/app/(dashboard-layout)/loading.tsx +++ b/src/app/(dashboard-layout)/loading.tsx @@ -2,11 +2,11 @@ export default function Loading() { return ( -
          +
          -
          +

          Loading...

          - ); + ) } diff --git a/src/app/error.tsx b/src/app/error.tsx index 0dc92ec..e84ef8f 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -1,25 +1,25 @@ -"use client"; +"use client" -import { useEffect } from "react"; +import { useEffect } from "react" // shadcn -import { Button } from "@/components/ui/button"; +import { Button } from "@/components/ui/button" interface ErrorProps { - error: Error & { digest?: string }; - reset: () => void; + error: Error & { digest?: string } + reset: () => void } // ------------------------------ | PAGE - 500 ERROR | ------------------------------ // export default function Error({ error, reset }: ErrorProps) { useEffect(() => { - console.error(error); - }, [error]); + console.error(error) + }, [error]) return ( -
          -
          +
          +

          500

          Something Went Wrong

          @@ -31,11 +31,11 @@ export default function Error({ error, reset }: ErrorProps) {
          - ); + ) } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5fc2668..1f05a5d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,17 +1,15 @@ import { ReactNode } from "react" import { Metadata } from "next" - -// shadcn -import { TooltipProvider } from "@/components/ui/tooltip" - +// project +import branding from "@/branding.json" +import Metrics from "@/metrics" // third-party import { Toaster } from "sonner" -// project -import branding from "@/branding.json" +// shadcn +import { TooltipProvider } from "@/components/ui/tooltip" import { ThemePresetStyles } from "@/components/customizer/ThemePresetStyles" import { ThemeProvider } from "@/components/theme-provider" -import Metrics from "@/metrics" // assets import "./globals.css" diff --git a/src/app/loading.tsx b/src/app/loading.tsx index 2fbc9c0..9aa2ad3 100644 --- a/src/app/loading.tsx +++ b/src/app/loading.tsx @@ -2,11 +2,11 @@ export default function Loading() { return ( -
          +
          -
          +

          Loading...

          - ); + ) } diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index a44e565..5658d05 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,11 +1,11 @@ -import Link from "next/link"; +import Link from "next/link" // ------------------------------ | PAGE - 404 NOT FOUND | ------------------------------ // export default function NotFound() { return ( -
          -
          +
          +

          404

          Page Not Found

          @@ -15,11 +15,11 @@ export default function NotFound() {
          Back to Home
          - ); + ) } diff --git a/src/app/page.tsx b/src/app/page.tsx index 1f5aaed..c060fc8 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,22 +1,21 @@ import { ReactNode } from "react" import { Metadata } from "next" -import { cn } from "@/lib/utils" - // project imports import branding from "@/branding.json" +// assets +import { Star } from "@/images/svg/icons" +import { cn } from "@/lib/utils" // project import Component1 from "@/components/uiable/blocks/landing/component-1/component-1" import Component2 from "@/components/uiable/blocks/landing/component-2/component-2" import Contact from "@/components/uiable/blocks/landing/contact/Contact" +import FAQ from "@/components/uiable/blocks/landing/faq/faq" import Featured from "@/components/uiable/blocks/landing/feature/Feature" import Footer from "@/components/uiable/blocks/landing/footer/footer" import Hero from "@/components/uiable/blocks/landing/hero/hero" import Navbar from "@/components/uiable/blocks/landing/navbar/navbar" -// assets -import { Star } from "@/images/svg/icons" - export const metadata: Metadata = { title: `${branding.brandName} - Component Library`, alternates: { @@ -85,6 +84,9 @@ export default function LandingPage() { + + + diff --git a/src/app/preview/[...slug]/page.tsx b/src/app/preview/[...slug]/page.tsx index e256b72..a7a1620 100644 --- a/src/app/preview/[...slug]/page.tsx +++ b/src/app/preview/[...slug]/page.tsx @@ -1,64 +1,64 @@ -"use client"; +"use client" -import { use, useEffect, useState } from "react"; +import { use, useEffect, useState } from "react" // ------------------------------ | PAGE - PREVIEW | ------------------------------ // export default function PreviewPage({ - params: paramsPromise + params: paramsPromise, }: { - params: Promise<{ slug: string[] }>; + params: Promise<{ slug: string[] }> }) { - const params = use(paramsPromise); - let filePath = params.slug.join("/"); + const params = use(paramsPromise) + let filePath = params.slug.join("/") if (filePath.startsWith("src/")) { - filePath = filePath.substring(4); + filePath = filePath.substring(4) } - const [Comp, setComp] = useState(null); + const [Comp, setComp] = useState(null) useEffect(() => { - let mounted = true; + let mounted = true const load = async () => { try { - let mod; + let mod try { - mod = await import(`@/components/uiable/blocks/${filePath}.tsx`); + mod = await import(`@/components/uiable/blocks/${filePath}.tsx`) // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (_err) { - mod = await import(`@/components/uiable/${filePath}.tsx`); + mod = await import(`@/components/uiable/${filePath}.tsx`) } if (mounted) { const component = mod.default || mod.Component || - Object.values(mod).find((v) => typeof v === "function"); - setComp(() => component); + Object.values(mod).find((v) => typeof v === "function") + setComp(() => component) } } catch (_err) { - console.error("Preview import failed for", filePath, _err); + console.error("Preview import failed for", filePath, _err) if (mounted) { // eslint-disable-next-line react/display-name setComp(() => () => ( -
          +
          Component not found: {filePath}
          - )); + )) } } - }; - load(); + } + load() return () => { - mounted = false; - }; - }, [filePath]); + mounted = false + } + }, [filePath]) if (!Comp) { - return
          ; + return
          } return (
          - ); + ) } diff --git a/src/category-counts.json b/src/category-counts.json index 4e3a295..bf0804e 100644 --- a/src/category-counts.json +++ b/src/category-counts.json @@ -59,4 +59,4 @@ "scroll-area": 1, "data-table": 1, "landing": 7 -} \ No newline at end of file +} diff --git a/src/components-grid.ts b/src/components-grid.ts index d18bee7..5734db1 100644 --- a/src/components-grid.ts +++ b/src/components-grid.ts @@ -1,19 +1,19 @@ export interface CategoryItem { - title: string; - slug: string; + title: string + slug: string breakpoints?: { - xs?: number; - sm?: number; - md?: number; - lg?: number; - xl?: number; - xxl?: number; - }; + xs?: number + sm?: number + md?: number + lg?: number + xl?: number + xxl?: number + } } export interface NavSection { - title: string; - items: CategoryItem[]; + title: string + items: CategoryItem[] } export const NAV_CATEGORIES: NavSection[] = [ @@ -24,79 +24,79 @@ export const NAV_CATEGORIES: NavSection[] = [ { title: "Button Group", slug: "button-group", - breakpoints: { xxl: 3, md: 2, sm: 1, xs: 1 } + breakpoints: { xxl: 3, md: 2, sm: 1, xs: 1 }, }, { title: "Checkbox", slug: "checkbox", - breakpoints: { md: 2, sm: 2, xs: 1 } + breakpoints: { md: 2, sm: 2, xs: 1 }, }, { title: "Combobox", slug: "combobox", - breakpoints: { xl: 3, lg: 3, md: 2, sm: 1, xs: 1 } + breakpoints: { xl: 3, lg: 3, md: 2, sm: 1, xs: 1 }, }, { title: "Command", slug: "command", - breakpoints: { xl: 3, lg: 3, md: 2, sm: 1, xs: 1 } + breakpoints: { xl: 3, lg: 3, md: 2, sm: 1, xs: 1 }, }, { title: "Date Picker", slug: "date-picker", - breakpoints: { xl: 3, lg: 3, md: 2, sm: 1, xs: 1 } + breakpoints: { xl: 3, lg: 3, md: 2, sm: 1, xs: 1 }, }, { title: "Input", slug: "input", - breakpoints: { lg: 2, md: 2, sm: 1, xs: 1 } + breakpoints: { lg: 2, md: 2, sm: 1, xs: 1 }, }, { title: "Input Group", slug: "input-group", - breakpoints: { md: 2, sm: 1, xs: 1 } + breakpoints: { md: 2, sm: 1, xs: 1 }, }, { title: "Input OTP", slug: "input-otp", breakpoints: { md: 2, xs: 1 } }, { title: "Item", slug: "item", breakpoints: { md: 2, xs: 1 } }, { title: "Native Select", slug: "native-select", - breakpoints: { lg: 3, md: 2, sm: 1, xs: 1 } + breakpoints: { lg: 3, md: 2, sm: 1, xs: 1 }, }, { title: "Radio", slug: "radio", breakpoints: { xs: 1 } }, { title: "Radio Group", slug: "radio-group", - breakpoints: { lg: 3, md: 2, sm: 1, xs: 1 } + breakpoints: { lg: 3, md: 2, sm: 1, xs: 1 }, }, { title: "Select", slug: "select", - breakpoints: { lg: 3, md: 2, sm: 1, xs: 1 } + breakpoints: { lg: 3, md: 2, sm: 1, xs: 1 }, }, { title: "Slider", slug: "slider", - breakpoints: { xl: 3, lg: 2, md: 2, sm: 1, xs: 1 } + breakpoints: { xl: 3, lg: 2, md: 2, sm: 1, xs: 1 }, }, { title: "Switch", slug: "switch", breakpoints: { md: 2, sm: 2, xs: 1 } }, { title: "Textarea", slug: "textarea", - breakpoints: { lg: 2, md: 2, sm: 1, xs: 1 } + breakpoints: { lg: 2, md: 2, sm: 1, xs: 1 }, }, { title: "Toggle", slug: "toggle", breakpoints: { sm: 2, xs: 1 } }, { title: "Toggle Group", slug: "toggle-group", - breakpoints: { md: 2, sm: 1, xs: 1 } + breakpoints: { md: 2, sm: 1, xs: 1 }, }, { title: "Calendar", slug: "calendar", - breakpoints: { lg: 2, sm: 1, xs: 1 } - } - ] + breakpoints: { lg: 2, sm: 1, xs: 1 }, + }, + ], }, { title: "Data Display", @@ -106,32 +106,32 @@ export const NAV_CATEGORIES: NavSection[] = [ { title: "Card", slug: "card", - breakpoints: { lg: 2, md: 2, sm: 1, xs: 1 } + breakpoints: { lg: 2, md: 2, sm: 1, xs: 1 }, }, { title: "Carousel", slug: "carousel", - breakpoints: { xl: 2, lg: 2, md: 1, sm: 1, xs: 1 } + breakpoints: { xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }, }, { title: "Chart", slug: "chart", - breakpoints: { xl: 2, lg: 2, md: 1, sm: 1, xs: 1 } + breakpoints: { xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }, }, { title: "Data Table", slug: "data-table", - breakpoints: { xl: 1, lg: 1, md: 1, sm: 1, xs: 1 } + breakpoints: { xl: 1, lg: 1, md: 1, sm: 1, xs: 1 }, }, { title: "Empty", slug: "empty", - breakpoints: { xl: 3, lg: 2, md: 2, sm: 1, xs: 1 } + breakpoints: { xl: 3, lg: 2, md: 2, sm: 1, xs: 1 }, }, { title: "Field", slug: "field", - breakpoints: { lg: 2, md: 2, sm: 1, xs: 1 } + breakpoints: { lg: 2, md: 2, sm: 1, xs: 1 }, }, { title: "Hover Card", slug: "hover-card", breakpoints: { xs: 1 } }, { title: "Kbd", slug: "kbd", breakpoints: { md: 2, sm: 2, xs: 1 } }, @@ -139,40 +139,40 @@ export const NAV_CATEGORIES: NavSection[] = [ { title: "List Group", slug: "list-group", - breakpoints: { lg: 2, md: 2, sm: 1, xs: 1 } + breakpoints: { lg: 2, md: 2, sm: 1, xs: 1 }, }, { title: "Scroll Area", slug: "scroll-area", breakpoints: { xs: 1 } }, { title: "Separator", slug: "separator", - breakpoints: { md: 2, sm: 1, xs: 1 } + breakpoints: { md: 2, sm: 1, xs: 1 }, }, { title: "Table", slug: "table", - breakpoints: { xl: 2, md: 1, sm: 1, xs: 1 } + breakpoints: { xl: 2, md: 1, sm: 1, xs: 1 }, }, { title: "Tooltip", slug: "tooltip", - breakpoints: { md: 3, sm: 2, xs: 1 } + breakpoints: { md: 3, sm: 2, xs: 1 }, }, { title: "Typography", slug: "typography", - breakpoints: { xl: 2, lg: 2, md: 1, sm: 1, xs: 1 } + breakpoints: { xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }, }, { title: "Aspect Ratio", slug: "aspect-ratio", - breakpoints: { md: 2, sm: 1, xs: 1 } + breakpoints: { md: 2, sm: 1, xs: 1 }, }, { title: "Skeleton", slug: "skeleton", - breakpoints: { lg: 3, md: 2, sm: 1, xs: 1 } - } - ] + breakpoints: { lg: 3, md: 2, sm: 1, xs: 1 }, + }, + ], }, { title: "Feedback", @@ -180,36 +180,36 @@ export const NAV_CATEGORIES: NavSection[] = [ { title: "Alert", slug: "alert", - breakpoints: { xl: 2, lg: 2, md: 1, sm: 1, xs: 1 } + breakpoints: { xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }, }, { title: "Alert Dialog", slug: "alert-dialog", - breakpoints: { xl: 3, lg: 3, md: 2, sm: 1, xs: 1 } + breakpoints: { xl: 3, lg: 3, md: 2, sm: 1, xs: 1 }, }, { title: "Dialog", slug: "dialog", - breakpoints: { xl: 3, lg: 3, md: 2, sm: 1, xs: 1 } + breakpoints: { xl: 3, lg: 3, md: 2, sm: 1, xs: 1 }, }, { title: "Drawer", slug: "drawer", - breakpoints: { xl: 3, lg: 3, md: 2, sm: 1, xs: 1 } + breakpoints: { xl: 3, lg: 3, md: 2, sm: 1, xs: 1 }, }, { title: "Progress", slug: "progress", - breakpoints: { xl: 3, lg: 2, md: 2, sm: 1, xs: 1 } + breakpoints: { xl: 3, lg: 2, md: 2, sm: 1, xs: 1 }, }, { title: "Sheet", slug: "sheet", breakpoints: { md: 2, xs: 1 } }, { title: "Sonner", slug: "sonner", breakpoints: { md: 2, sm: 1, xs: 1 } }, { title: "Spinner", slug: "spinner", - breakpoints: { md: 3, sm: 2, xs: 1 } - } - ] + breakpoints: { md: 3, sm: 2, xs: 1 }, + }, + ], }, { title: "Navigation", @@ -217,44 +217,44 @@ export const NAV_CATEGORIES: NavSection[] = [ { title: "Breadcrumb", slug: "breadcrumb", - breakpoints: { xl: 2, lg: 2, md: 1, sm: 1, xs: 1 } + breakpoints: { xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }, }, { title: "Dropdown Menu", slug: "dropdown-menu", - breakpoints: { lg: 3, md: 2, sm: 1, xs: 1 } + breakpoints: { lg: 3, md: 2, sm: 1, xs: 1 }, }, { title: "Menubar", slug: "menubar", - breakpoints: { lg: 2, md: 2, sm: 1, xs: 1 } + breakpoints: { lg: 2, md: 2, sm: 1, xs: 1 }, }, { title: "Navbar", slug: "navbar", - breakpoints: { xl: 1, lg: 1, md: 1, sm: 1, xs: 1 } + breakpoints: { xl: 1, lg: 1, md: 1, sm: 1, xs: 1 }, }, { title: "Navigation Menu", slug: "navigation-menu", - breakpoints: { xs: 1 } + breakpoints: { xs: 1 }, }, { title: "Pagination", slug: "pagination", - breakpoints: { xl: 2, lg: 2, md: 1, sm: 1, xs: 1 } + breakpoints: { xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }, }, { title: "Tabs", slug: "tabs", - breakpoints: { lg: 2, md: 2, sm: 1, xs: 1 } + breakpoints: { lg: 2, md: 2, sm: 1, xs: 1 }, }, { title: "Sidebar", slug: "sidebar", - breakpoints: { xl: 1, lg: 1, md: 1, sm: 1, xs: 1 } - } - ] + breakpoints: { xl: 1, lg: 1, md: 1, sm: 1, xs: 1 }, + }, + ], }, { title: "Surfaces", @@ -262,14 +262,14 @@ export const NAV_CATEGORIES: NavSection[] = [ { title: "Accordion", slug: "accordion", - breakpoints: { xl: 2, lg: 2, md: 1, sm: 1, xs: 1 } + breakpoints: { xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }, }, { title: "Collapsible", slug: "collapsible", - breakpoints: { lg: 2, md: 2, sm: 1, xs: 1 } - } - ] + breakpoints: { lg: 2, md: 2, sm: 1, xs: 1 }, + }, + ], }, { title: "Utils", @@ -277,21 +277,21 @@ export const NAV_CATEGORIES: NavSection[] = [ { title: "Context Menu", slug: "context-menu", - breakpoints: { md: 2, sm: 1, xs: 1 } + breakpoints: { md: 2, sm: 1, xs: 1 }, }, { title: "Popover", slug: "popover", - breakpoints: { lg: 3, md: 2, sm: 1 } + breakpoints: { lg: 3, md: 2, sm: 1 }, }, { title: "Resizable", slug: "resizable", - breakpoints: { xl: 2, lg: 2, md: 1, sm: 1, xs: 1 } - } - ] - } -]; + breakpoints: { xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }, + }, + ], + }, +] export const NAV_BLOCKS: NavSection[] = [ { @@ -300,12 +300,12 @@ export const NAV_BLOCKS: NavSection[] = [ { title: "Landing", slug: "landing", - breakpoints: { xl: 1, lg: 1, md: 1, sm: 1, xs: 1 } - } - ] - } -]; + breakpoints: { xl: 1, lg: 1, md: 1, sm: 1, xs: 1 }, + }, + ], + }, +] export const categories = [...NAV_CATEGORIES, ...NAV_BLOCKS].flatMap( (section) => section.items -); +) diff --git a/src/components/LazySection.tsx b/src/components/LazySection.tsx index e9f9a11..bb170cb 100644 --- a/src/components/LazySection.tsx +++ b/src/components/LazySection.tsx @@ -1,4 +1,4 @@ -"use client"; +"use client" import { ComponentType, @@ -7,23 +7,23 @@ import { useEffect, useMemo, useRef, - useState -} from "react"; + useState, +} from "react" // project -import Loader from "@/components/Loader"; +import Loader from "@/components/Loader" interface SectionConfig { - importFunc: () => Promise<{ default: ComponentType }>; + importFunc: () => Promise<{ default: ComponentType }> - props?: Record; + props?: Record } interface LazySectionProps { - sections: SectionConfig | SectionConfig[]; - fallback?: ReactNode; - offset?: string; - placeholderHeight?: number; + sections: SectionConfig | SectionConfig[] + fallback?: ReactNode + offset?: string + placeholderHeight?: number } // ------------------------------ | COMPONENT - LAZY SECTION | ------------------------------ // @@ -32,37 +32,37 @@ export default function LazySection({ sections, fallback = , offset = "0px", - placeholderHeight = 400 + placeholderHeight = 400, }: LazySectionProps) { const sectionList = useMemo( () => (Array.isArray(sections) ? sections : [sections]), [sections] - ); - const [isVisible, setIsVisible] = useState(false); + ) + const [isVisible, setIsVisible] = useState(false) const [loadedComponents, setLoadedComponents] = useState< (ComponentType | null)[] - >(Array(sectionList.length).fill(null)); - const ref = useRef(null); + >(Array(sectionList.length).fill(null)) + const ref = useRef(null) useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting && !isVisible) { - setIsVisible(true); + setIsVisible(true) Promise.all( sectionList.map((section) => section.importFunc().then((module) => module.default) ) - ).then((components) => setLoadedComponents(components)); + ).then((components) => setLoadedComponents(components)) } }, { rootMargin: offset, threshold: 0.1 } - ); + ) - if (ref.current) observer.observe(ref.current); + if (ref.current) observer.observe(ref.current) - return () => observer.disconnect(); - }, [sectionList, offset, isVisible]); + return () => observer.disconnect() + }, [sectionList, offset, isVisible]) return (
          @@ -70,10 +70,10 @@ export default function LazySection({ ? sectionList.map((section, index) => createElement(loadedComponents[index]!, { key: index, - ...section.props + ...section.props, }) ) : isVisible && fallback}
          - ); + ) } diff --git a/src/components/Loader.tsx b/src/components/Loader.tsx index 967a6ae..65a9791 100644 --- a/src/components/Loader.tsx +++ b/src/components/Loader.tsx @@ -2,8 +2,8 @@ export default function Loader() { return ( -
          -
          +
          +
          - ); + ) } diff --git a/src/components/animation/BorderGlow.tsx b/src/components/animation/BorderGlow.tsx index abd86b1..52f8cbd 100644 --- a/src/components/animation/BorderGlow.tsx +++ b/src/components/animation/BorderGlow.tsx @@ -1,6 +1,14 @@ "use client" -import { ReactNode, useCallback, useEffect, useRef, useState } from "react" +import { + ReactNode, + useCallback, + useEffect, + useRef, + useState, + type PointerEvent, + type CSSProperties, +} from "react" interface BorderGlowProps { children?: ReactNode @@ -182,18 +190,16 @@ export default function BorderGlow({ const [cursorAngle, setCursorAngle] = useState(45) const [edgeProximity, setEdgeProximity] = useState(0) const [sweepActive, setSweepActive] = useState(false) - const [resolvedGlowColor, setResolvedGlowColor] = useState("221 100 64") + const [internalGlowColor, setInternalGlowColor] = useState("221 100 64") + const resolvedGlowColor = glowColor || internalGlowColor useEffect(() => { - if (glowColor) { - setResolvedGlowColor(glowColor) - return - } + if (glowColor) return const update = () => { requestAnimationFrame(() => { const hsl = readPrimaryAsHSL() - if (hsl) setResolvedGlowColor(hsl) + if (hsl) setInternalGlowColor(hsl) }) } @@ -262,7 +268,7 @@ export default function BorderGlow({ ) const handlePointerMove = useCallback( - (e: React.PointerEvent) => { + (e: PointerEvent) => { const card = cardRef.current if (!card) return const rect = card.getBoundingClientRect() @@ -395,7 +401,7 @@ export default function BorderGlow({ transition: isVisible ? "opacity 0.25s ease-out" : "opacity 0.75s ease-in-out", - } as React.CSSProperties + } as CSSProperties } /> @@ -411,7 +417,7 @@ export default function BorderGlow({ transition: isVisible ? "opacity 0.25s ease-out" : "opacity 0.75s ease-in-out", - } as React.CSSProperties + } as CSSProperties } > { + badges?: unknown[] + className?: string + initialDelay?: number + forwardDelay?: number + reverseDelay?: number + holdOpenTime?: number + holdClosedTime?: number + backgroundCode?: string +} + +const badgeVariants = { + hidden: { + width: 0, + marginLeft: 0, + marginRight: 0, + opacity: 0, + x: -40, + scale: 0.3, + rotate: -28, + }, + visible: { + width: "auto", + marginLeft: "0.5rem", + marginRight: "0.5rem", + opacity: 1, + x: 0, + scale: 1, + rotate: 0, + transition: { + type: "spring" as const, + stiffness: 80, + damping: 15, + }, + }, +} + +const slashVariants = { + hidden: { + width: 0, + marginLeft: 0, + marginRight: 0, + opacity: 0, + x: -44, + scale: 0.3, + rotate: 18, + }, + visible: { + width: "auto", + marginLeft: "0.375rem", + marginRight: "0.375rem", + opacity: 1, + x: 0, + scale: 1, + rotate: 0, + transition: { + type: "spring" as const, + stiffness: 80, + damping: 15, + }, + }, +} + +const bracketVariants = { + hidden: { + scale: 0.98, + opacity: 0.8, + }, + visible: { + scale: 1.03, + opacity: 1, + transition: { + type: "spring" as const, + stiffness: 70, + damping: 16, + }, + }, +} + +interface AnimatedBadgeProps { + visible: boolean + index: number +} + +function AnimatedBadge({ visible, index }: AnimatedBadgeProps) { + return ( + +
          + + +
          +
          + ) +} + +export default function CodeAnimation({ + badges = [null, null, null], + className, + initialDelay = 400, + forwardDelay = 800, + reverseDelay = 500, + holdOpenTime = 1200, + holdClosedTime = 600, + backgroundCode, +}: CodeAnimationProps) { + const totalElements = badges.length + 1 + const [visibleCount, setVisibleCount] = useState(0) + const [bracketsExpanded, setBracketsExpanded] = useState(false) + const stepRef = useRef(0) + const reversingRef = useRef(false) + + useEffect(() => { + let timer: ReturnType + + function tick() { + if (!reversingRef.current) { + if (stepRef.current === 0) setBracketsExpanded(true) + + if (stepRef.current < totalElements) { + stepRef.current += 1 + setVisibleCount(stepRef.current) + timer = setTimeout(tick, forwardDelay) + } else { + reversingRef.current = true + timer = setTimeout(tick, holdOpenTime) + } + } else { + if (stepRef.current > 0) { + stepRef.current -= 1 + setVisibleCount(stepRef.current) + timer = setTimeout(tick, reverseDelay) + } else { + setBracketsExpanded(false) + reversingRef.current = false + timer = setTimeout(tick, holdClosedTime) + } + } + } + + timer = setTimeout(tick, initialDelay) + return () => clearTimeout(timer) + }, [ + totalElements, + forwardDelay, + reverseDelay, + holdOpenTime, + holdClosedTime, + initialDelay, + ]) + + const codeTopRight = `// use the JFrame type until support for the + JFrame frame = new JFrame("Hello Button"); + Container pane = frame.getContentPane(); + frame.pack(); + frame.show(); + }` + + const codeBottomLeft = ` + HelloButton() + { + JButton hello = new JButton("hello!"); + JButton Animation = new JButton("Designers!"); + }` + + return ( +
          + + +
          +
          +          {backgroundCode || codeTopRight}
          +        
          +
          + +
          +
          +          {backgroundCode || codeBottomLeft}
          +        
          +
          + + + + + + + + {badges.map((_, i) => ( + i} index={i} /> + ))} + + badges.length ? "visible" : "hidden"} + className="flex shrink-0 items-center justify-center text-slate-900 dark:text-slate-100" + > + + + + + + + + +
          + ) +} diff --git a/src/components/animation/DiagonalScroll.tsx b/src/components/animation/DiagonalScroll.tsx new file mode 100644 index 0000000..b196dec --- /dev/null +++ b/src/components/animation/DiagonalScroll.tsx @@ -0,0 +1,129 @@ +"use client" + +import { useEffect, useRef, type ReactNode, type RefObject } from "react" + +import { cn } from "@/lib/utils" + +interface DiagonalScrollProps { + badge: ReactNode + src: string + darkSrc?: string + duration?: number + opacity?: number + className?: string +} + +function ScrollImage({ + innerRef, + src, + darkSrc, + opacity, +}: { + innerRef: RefObject + src: string + darkSrc?: string + opacity: number +}) { + return ( +
          + + {darkSrc && ( + + )} +
          + ) +} + +export default function DiagonalScroll({ + badge, + src, + darkSrc, + duration = 20, + opacity = 0.9, + className, +}: DiagonalScrollProps) { + const img1Ref = useRef(null) + const img2Ref = useRef(null) + const anim1Ref = useRef(null) + const anim2Ref = useRef(null) + + useEffect(() => { + if (!img1Ref.current || !img2Ref.current) return + + anim1Ref.current = img1Ref.current.animate( + [ + { transform: "translate(-50%, -50%) translate(0%, 0%)" }, + { transform: "translate(-50%, -50%) translate(-100%, -100%)" }, + ], + { + duration: duration * 1000, + iterations: Infinity, + easing: "linear", + } + ) + + anim2Ref.current = img2Ref.current.animate( + [ + { transform: "translate(-50%, -50%) translate(100%, 100%)" }, + { transform: "translate(-50%, -50%) translate(0%, 0%)" }, + ], + { + duration: duration * 1000, + iterations: Infinity, + easing: "linear", + } + ) + + return () => { + anim1Ref.current?.cancel() + anim2Ref.current?.cancel() + } + }, [duration]) + + return ( +
          +
          + {/* Image 1 — starts visible at center, exits top-left */} + + + {/* Image 2 — starts at bottom-right (outside), enters as Image 1 exits */} + +
          + + {/* Center badge */} +
          + {badge} +
          +
          + ) +} diff --git a/src/components/animation/EchoStack.tsx b/src/components/animation/EchoStack.tsx index e8b9017..36222a7 100644 --- a/src/components/animation/EchoStack.tsx +++ b/src/components/animation/EchoStack.tsx @@ -1,7 +1,6 @@ "use client" -import { useEffect, useState, ReactNode, Children } from "react" - +import { Children, ReactNode, useEffect, useState } from "react" // project imports import { AnimationBg } from "@/images/svg/landing" @@ -51,7 +50,7 @@ export default function EchoStack({ }, pauseBeforeExit) } } else { - if (visibleCount > 1) { + if (visibleCount > 2) { timeoutId = setTimeout(() => { setVisibleCount((prev) => prev - 1) }, exitInterval) @@ -76,35 +75,37 @@ export default function EchoStack({ if (totalCards === 0) return null return ( -
          + <> - {cards.map((child, index) => { - const isVisible = index < visibleCount +
          + {cards.map((child, index) => { + const isVisible = index < visibleCount - // The newest card is full size. Older cards shrink. - const stackIndex = isVisible ? visibleCount - 1 - index : 0 - // Each subsequent card is 40px lower than the previous one - const offset = index * 40 - const scale = 1 - stackIndex * 0.04 + // The newest card is full size. Older cards shrink. + const stackIndex = isVisible ? visibleCount - 1 - index : 0 + // Each subsequent card is 50px lower than the previous one + const offset = index * 50 + const scale = 1 - stackIndex * 0.04 - return ( -
          - {child} -
          - ) - })} -
          + return ( +
          + {child} +
          + ) + })} +
          + ) } diff --git a/src/components/animation/HoverBg.tsx b/src/components/animation/HoverBg.tsx index c7d4e6c..fc09264 100644 --- a/src/components/animation/HoverBg.tsx +++ b/src/components/animation/HoverBg.tsx @@ -1,113 +1,130 @@ -"use client"; +"use client" -import { useEffect, useRef, useState } from "react"; -import { SparkleGrid } from "@/images/svg/landing"; +import { useEffect, useRef, useState } from "react" +import { SparkleGrid } from "@/images/svg/landing" interface HoverBgProps { - className?: string; + className?: string } -/** - * HoverBg Component - * Creates a mouse-following background grid SVG spotlight effect. - * Uses requestAnimationFrame interpolation for a buttery-smooth mask reveal. - */ -export default function HoverBg({ className = "" }: HoverBgProps) { - const glowRef = useRef(null); +// ------------------------------ | HOVER BG | ------------------------------ // - // Track visibility to fade out when the mouse leaves the window - const [isVisible, setIsVisible] = useState(false); +export default function HoverBg({ className = "" }: HoverBgProps) { + const glowRef = useRef(null) + const [isVisible, setIsVisible] = useState(false) - const positionRef = useRef({ targetX: 0, targetY: 0, currentX: 0, currentY: 0, initialized: false }); + const positionRef = useRef({ + targetX: 0, + targetY: 0, + currentX: 0, + currentY: 0, + initialized: false, + }) useEffect(() => { - let rafId: number; - const el = glowRef.current; - const parent = el?.parentElement; + let rafId: number + const el = glowRef.current + const parent = el?.parentElement const handleMouseMove = (e: MouseEvent) => { - if (!el || !parent) return; - const rect = parent.getBoundingClientRect(); + if (!el || !parent) return + const rect = parent.getBoundingClientRect() // Update targeted coordinates relative to the parent's bounding box - positionRef.current.targetX = e.clientX - rect.left; - positionRef.current.targetY = e.clientY - rect.top; + positionRef.current.targetX = e.clientX - rect.left + positionRef.current.targetY = e.clientY - rect.top // Snap immediately to mouse on first movement if (!positionRef.current.initialized) { - positionRef.current.currentX = positionRef.current.targetX; - positionRef.current.currentY = positionRef.current.targetY; - positionRef.current.initialized = true; - setIsVisible(true); + positionRef.current.currentX = positionRef.current.targetX + positionRef.current.currentY = positionRef.current.targetY + positionRef.current.initialized = true + setIsVisible(true) } - }; + } - const handleMouseLeave = () => setIsVisible(false); - const handleMouseEnter = () => setIsVisible(true); + const handleMouseLeave = () => setIsVisible(false) + const handleMouseEnter = () => setIsVisible(true) const animate = () => { if (el && positionRef.current.initialized) { - const { targetX, targetY, currentX, currentY } = positionRef.current; + const { targetX, targetY, currentX, currentY } = positionRef.current // Buttery-smooth easing interpolation (factor: 0.08) - const nextX = currentX + (targetX - currentX) * 0.08; - const nextY = currentY + (targetY - currentY) * 0.08; + const nextX = currentX + (targetX - currentX) * 0.08 + const nextY = currentY + (targetY - currentY) * 0.08 - positionRef.current.currentX = nextX; - positionRef.current.currentY = nextY; + positionRef.current.currentX = nextX + positionRef.current.currentY = nextY // Update the mask-image directly in JS for bulletproof browser compatibility - const maskStyle = `radial-gradient(circle 180px at ${nextX}px ${nextY}px, black, transparent)`; - el.style.setProperty("-webkit-mask-image", maskStyle); - el.style.setProperty("mask-image", maskStyle); + const maskStyle = `radial-gradient(circle 180px at ${nextX}px ${nextY}px, black, transparent)` + el.style.setProperty("-webkit-mask-image", maskStyle) + el.style.setProperty("mask-image", maskStyle) } - rafId = requestAnimationFrame(animate); - }; + rafId = requestAnimationFrame(animate) + } if (parent) { - parent.addEventListener("mousemove", handleMouseMove); - parent.addEventListener("mouseleave", handleMouseLeave); - parent.addEventListener("mouseenter", handleMouseEnter); + parent.addEventListener("mousemove", handleMouseMove) + parent.addEventListener("mouseleave", handleMouseLeave) + parent.addEventListener("mouseenter", handleMouseEnter) } - rafId = requestAnimationFrame(animate); + rafId = requestAnimationFrame(animate) return () => { if (parent) { - parent.removeEventListener("mousemove", handleMouseMove); - parent.removeEventListener("mouseleave", handleMouseLeave); - parent.removeEventListener("mouseenter", handleMouseEnter); + parent.removeEventListener("mousemove", handleMouseMove) + parent.removeEventListener("mouseleave", handleMouseLeave) + parent.removeEventListener("mouseenter", handleMouseEnter) } - cancelAnimationFrame(rafId); - }; - }, []); + cancelAnimationFrame(rafId) + } + }, []) const hasOpacityClass = className .split(" ") - .some((c) => c.startsWith("opacity-") || c.startsWith("dark:opacity-") || c.includes(":opacity-")); + .some( + (c) => + c.startsWith("opacity-") || + c.startsWith("dark:opacity-") || + c.includes(":opacity-") + ) return (
          {/* The SVG grid pattern repeated across the screen and revealed by the mask */} - + - +
          - ); + ) } diff --git a/src/components/animation/MotionVisual.tsx b/src/components/animation/MotionVisual.tsx index 42641d3..2b9a636 100644 --- a/src/components/animation/MotionVisual.tsx +++ b/src/components/animation/MotionVisual.tsx @@ -1,10 +1,12 @@ "use client" -// third party -import { motion } from "framer-motion" +import { CSSProperties } from "react" +// third party // project imports -import { AnimationLogo, AnimationBg } from "@/images/svg/landing" +import { DarkFav, LightFav } from "@/images/brand" +import { AnimationBg } from "@/images/svg/landing" +import { motion } from "framer-motion" // ------------------------------ | MOTION VISUAL | ------------------------------ // @@ -13,7 +15,7 @@ export default function MotionVisual() { "M0 17.2207C7.82264 12.1804 16.564 8.57792 25.5551 6.04752C106.333 -15.0859 180.464 43.3414 226.918 104.937C229.638 108.479 232.371 111.88 235.117 115.146C285.46 184.808 392.423 206.118 460.93 152.12C468.597 146.733 475.878 140.769 482.5 134.221C475.694 140.576 468.272 146.324 460.501 151.493C390.622 203.134 288.015 180.915 238.179 112.572C235.471 109.352 232.772 105.997 230.082 102.504C183.36 40.3955 106.317 -18.2241 25.3407 5.3184C16.3571 8.09598 7.67585 11.9568 0 17.2207Z" return ( -
          +
          {/* SVG Background */} @@ -21,6 +23,11 @@ export default function MotionVisual() { viewBox="-20 -60 520 320" className="pointer-events-none absolute inset-0 h-full w-full overflow-visible px-4 md:px-12" > + + + + + - - + + + diff --git a/src/components/animation/SpotlightCard.tsx b/src/components/animation/SpotlightCard.tsx index 6b0cdf3..f7cf7c8 100644 --- a/src/components/animation/SpotlightCard.tsx +++ b/src/components/animation/SpotlightCard.tsx @@ -1,51 +1,51 @@ -"use client"; +"use client" -import React, { useRef, useState } from "react"; +import { useRef, useState, type PropsWithChildren, type FC, type MouseEventHandler } from "react" interface Position { - x: number; - y: number; + x: number + y: number } -interface SpotlightCardProps extends React.PropsWithChildren { - className?: string; - spotlightColor?: `rgba(${number}, ${number}, ${number}, ${number})`; +interface SpotlightCardProps extends PropsWithChildren { + className?: string + spotlightColor?: `rgba(${number}, ${number}, ${number}, ${number})` } -const SpotlightCard: React.FC = ({ +const SpotlightCard: FC = ({ children, className = "", - spotlightColor = "rgba(255, 255, 255, 0.25)" + spotlightColor = "rgba(255, 255, 255, 0.25)", }) => { - const divRef = useRef(null); - const [isFocused, setIsFocused] = useState(false); - const [position, setPosition] = useState({ x: 0, y: 0 }); - const [opacity, setOpacity] = useState(0); + const divRef = useRef(null) + const [isFocused, setIsFocused] = useState(false) + const [position, setPosition] = useState({ x: 0, y: 0 }) + const [opacity, setOpacity] = useState(0) - const handleMouseMove: React.MouseEventHandler = (e) => { - if (!divRef.current || isFocused) return; + const handleMouseMove: MouseEventHandler = (e) => { + if (!divRef.current || isFocused) return - const rect = divRef.current.getBoundingClientRect(); - setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top }); - }; + const rect = divRef.current.getBoundingClientRect() + setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top }) + } const handleFocus = () => { - setIsFocused(true); - setOpacity(0.6); - }; + setIsFocused(true) + setOpacity(0.6) + } const handleBlur = () => { - setIsFocused(false); - setOpacity(0); - }; + setIsFocused(false) + setOpacity(0) + } const handleMouseEnter = () => { - setOpacity(0.6); - }; + setOpacity(0.6) + } const handleMouseLeave = () => { - setOpacity(0); - }; + setOpacity(0) + } return (
          = ({ onBlur={handleBlur} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} - className={`relative border border-neutral-800 bg-neutral-900 overflow-hidden p-8 ${className}`} + className={`relative overflow-hidden border border-neutral-800 bg-neutral-900 p-8 ${className}`} >
          {children}
          - ); -}; + ) +} -export default SpotlightCard; +export default SpotlightCard diff --git a/src/components/animation/TechOrbit.tsx b/src/components/animation/TechOrbit.tsx index fd6853c..8a0d019 100644 --- a/src/components/animation/TechOrbit.tsx +++ b/src/components/animation/TechOrbit.tsx @@ -1,197 +1,236 @@ -"use client"; +"use client" -// third party -import { motion } from "framer-motion"; +import { ReactNode } from "react" +// third party +import { DarkFav, LightFav } from "@/images/brand" +import { BaseUi, Shadcn, Tailwind } from "@/images/svg/icons" +// project imports +import { AnimationBg } from "@/images/svg/landing" // assets -import { BaseUi, Shadcn, Tailwind } from "@/images/svg/icons"; -import { IconBrandNextjs, IconBrandReact } from "@tabler/icons-react"; -import { AnimationLogo, AnimationBg } from "@/images/svg/landing"; - +import { IconBrandNextjs, IconBrandReact } from "@tabler/icons-react" +import { motion } from "framer-motion" // Duration and ease definitions for orbital movement -const ORBIT_1_DURATION = 25; // Clockwise inner orbit -const ORBIT_2_DURATION = 35; // Counter-clockwise middle orbit -const ORBIT_3_DURATION = 45; // Clockwise outer orbit +const ORBIT_1_DURATION = 25 // Clockwise inner orbit +const ORBIT_2_DURATION = 35 // Counter-clockwise middle orbit +const ORBIT_3_DURATION = 45 // Clockwise outer orbit type TechNodeProps = { - icon: React.ReactNode; - startAngle: number; - orbitDuration: number; - orbitDirection: "clockwise" | "counter-clockwise"; -}; - -function TechNode({ icon, startAngle, orbitDuration, orbitDirection }: TechNodeProps) { - const isClockwise = orbitDirection === "clockwise"; - - // The parent orbit container will rotate by 360 or -360. - // To keep the child upright, it needs to counter-rotate in the opposite direction. - const initialRotation = -startAngle; - const animateRotation = isClockwise - ? -startAngle - 360 - : -startAngle + 360; - - return ( -
          -
          - - {icon} - -
          -
          - ); + icon: ReactNode + startAngle: number + orbitDuration: number + orbitDirection: "clockwise" | "counter-clockwise" } - +function TechNode({ + icon, + startAngle, + orbitDuration, + orbitDirection, +}: TechNodeProps) { + const isClockwise = orbitDirection === "clockwise" + + // The parent orbit container will rotate by 360 or -360. + // To keep the child upright, it needs to counter-rotate in the opposite direction. + const initialRotation = -startAngle + const animateRotation = isClockwise ? -startAngle - 360 : -startAngle + 360 + + return ( +
          +
          + + {icon} + +
          +
          + ) +} // ------------------------------ | COMPONENTS TECH ORBITS | ------------------------------ // export default function TechOrbit() { - return ( -
          - - {/* SVG Background */} - - - {/* Orbit Container Centered in the Bottom-Right Area */} -
          - - {/* Orbit 3 (Outer - radius ~360px) */} - - - - - {/* shadcn */} - } - /> - - {/* Tailwind 2 */} - } - /> - - {/* Code/Slash (//) */} - } - /> - - - {/* Orbit 2 (Middle - radius ~240px) */} - - - - - {/* React */} - } - /> - - {/* TypeScript */} - } - /> - - - {/* Orbit 1 (Inner - radius ~140px) */} - - - - - {/* Next.js */} - } - /> - - } - /> - - } - /> - - } - /> - - - - {/* Center Blue Pulse Logo */} - - - -
          -
          - ); + return ( +
          + {/* SVG Background */} + + + {/* Orbit Container Centered in the Bottom-Right Area */} +
          + {/* Orbit 3 (Outer - radius ~360px) */} + + + + + {/* shadcn */} + } + /> + + {/* Tailwind 2 */} + } + /> + + {/* Code/Slash (//) */} + } + /> + + + {/* Orbit 2 (Middle - radius ~240px) */} + + + + + {/* React */} + } + /> + + {/* TypeScript */} + } + /> + + + {/* Orbit 1 (Inner - radius ~140px) */} + + + + + {/* Next.js */} + } + /> + + } + /> + + } + /> + + } + /> + + + {/* Center Blue Pulse Logo */} + + + + +
          +
          + ) } diff --git a/src/components/animation/code-animation.tsx b/src/components/animation/code-animation.tsx deleted file mode 100644 index 5e89518..0000000 --- a/src/components/animation/code-animation.tsx +++ /dev/null @@ -1,203 +0,0 @@ -"use client" - -import React, { useEffect, useState } from "react" - -// third party -import { cn } from "@/lib/utils" -import { motion } from "framer-motion" - -// project imports -import { AnimationBg, AnimationLogo } from "@/images/svg/landing" - -// assets -import { ChevronLeft, ChevronRight, Slash } from "lucide-react" - -interface CodeAnimationProps extends React.HTMLAttributes { - backgroundCode?: string -} - -// Staggered dot animation variant factory -const dotVariants = (delay: number) => ({ - hidden: { margin: "0px", opacity: 0, scale: 0 }, - visible: { - margin: "0 10px", - opacity: 1, - scale: 1, - transition: { delay, type: "spring" as const, stiffness: 40, damping: 20 }, - }, - exit: { - margin: "0px", - opacity: 0, - scale: 0, - transition: { duration: 0.5, ease: [0.4, 0, 0.2, 1] as const }, - }, -}) - -// Slash animation variant -const slashVariants = { - hidden: { width: 0, opacity: 0, scale: 0.5 }, - visible: { - width: "auto", - opacity: 1, - scale: 1, - transition: { - delay: 3.2, - type: "spring" as const, - stiffness: 40, - damping: 20, - }, - }, - exit: { - width: 0, - opacity: 0, - scale: 0.5, - transition: { duration: 0.5, ease: [0.4, 0, 0.2, 1] as const }, - }, -} - -// ------------------------------ | CODE ANIMATION | ------------------------------ // - -export default function CodeAnimation({ - className, - backgroundCode, -}: CodeAnimationProps) { - const [animKey, setAnimKey] = useState(0) - - // Re-trigger the animation loop - useEffect(() => { - const totalCycle = 6000 // 6s per cycle - const interval = setInterval(() => { - setAnimKey((k) => k + 1) - }, totalCycle) - return () => clearInterval(interval) - }, []) - - const codeTopRight = `// use the JFrame type until support for the - JFrame frame = new JFrame("Hello Button"); - Container pane = frame.getContentPane(); - frame.pack(); - frame.show(); - }` - - const codeBottomLeft = ` - HelloButton() - { - JButton hello = new JButton("hello!"); - JButton Animation = new JButton("Designers!"); - }` - - return ( -
          - {/* SVG Background Layer */} - - - {/* Faint Background Code - Top Right */} -
          -
          -          {backgroundCode || codeTopRight}
          -        
          -
          - - {/* Faint Background Code - Bottom Left */} -
          -
          -          {backgroundCode || codeBottomLeft}
          -        
          -
          - - {/* Center animated graphic */} -
          - - - - - - {/* Dots */} - - - - - - - - - - - {/* Slash */} - - - - - - - - -
          -
          - ) -} diff --git a/src/components/animation/index.ts b/src/components/animation/index.ts index 31f3366..55d2ae3 100644 --- a/src/components/animation/index.ts +++ b/src/components/animation/index.ts @@ -1,5 +1,6 @@ export { default as HoverBg } from "./HoverBg" export { default as TechOrbit } from "./TechOrbit" export { default as MotionVisual } from "./MotionVisual" -export { default as CodeAnimation } from "./code-animation" +export { default as CodeAnimation } from "./CodeAnimation" export { default as EchoStack } from "./EchoStack" +export { default as DiagonalScroll } from "./DiagonalScroll" diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index 9be5fa1..79cef5a 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -1,8 +1,12 @@ "use client" +import { ComponentProps, useState } from "react" import Link from "next/link" import { usePathname } from "next/navigation" -import { useState, ComponentProps } from "react" +import CATEGORY_COUNTS from "@/category-counts.json" +import { NAV_CATEGORIES } from "@/components-grid" +// assets +import { ChevronDownIcon, Component, FileText } from "lucide-react" // shadcn import { Button } from "@/components/ui/button" @@ -14,25 +18,20 @@ import { import { Sidebar, SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, - SidebarGroup, - SidebarGroupLabel, - SidebarGroupContent, SidebarRail, } from "@/components/ui/sidebar" - -// project -import Logo from "./uiable/layout/shared/logo" -import CATEGORY_COUNTS from "@/category-counts.json" -import { NAV_CATEGORIES } from "@/components-grid" import ComponentList from "@/components/uiable/layout/component-list" import ComponentSearch from "@/components/uiable/layout/shared/component-search" -// assets -import { Component, FileText, ChevronDownIcon } from "lucide-react" +// project +import Logo from "./uiable/layout/shared/logo" // ------------------------------ | COMPONENT - APP SIDEBAR | ------------------------------ // diff --git a/src/components/block-item.tsx b/src/components/block-item.tsx index 1c2804e..7dd5d69 100644 --- a/src/components/block-item.tsx +++ b/src/components/block-item.tsx @@ -1,82 +1,79 @@ -"use client"; +"use client" -import { memo, useCallback, useEffect, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useRef, useState, type SyntheticEvent } from "react" +// assets +import { + ArrowUpRight, + Code, + Copy, + Monitor, + RotateCw, + Smartphone, + SquareCheckBig, + Tablet, + Terminal, +} from "lucide-react" +// third party +import { codeToHtml, type BundledLanguage } from "shiki" +import { cn } from "@/lib/utils" // shadcn -import { Button, buttonVariants } from "@/components/ui/button"; -import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Button, buttonVariants } from "@/components/ui/button" +import { Card, CardContent, CardHeader } from "@/components/ui/card" import { Dialog, DialogContent, DialogHeader, DialogTitle, - DialogTrigger -} from "@/components/ui/dialog"; -import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; + DialogTrigger, +} from "@/components/ui/dialog" +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" import { Tooltip, TooltipContent, - TooltipTrigger -} from "@/components/ui/tooltip"; - -// third party -import { type BundledLanguage, codeToHtml } from "shiki"; - + TooltipTrigger, +} from "@/components/ui/tooltip" // project -import Loader from "@/components/Loader"; -import { cn } from "@/lib/utils"; - -// assets -import { - ArrowUpRight, - Code, - Copy, - Monitor, - RotateCw, - Smartphone, - SquareCheckBig, - Terminal, - Tablet -} from "lucide-react"; +import Loader from "@/components/Loader" export interface Item { - name: string; - title: string; - description: string; - files: { path: string }[]; - categories: string[]; - rawCode?: string; + name: string + title: string + description: string + files: { path: string }[] + categories: string[] + rawCode?: string } const CodeBlock = memo( ({ children, lang }: { children: string; lang: BundledLanguage }) => { - const [html, setHtml] = useState(""); + const [html, setHtml] = useState("") useEffect(() => { - let mounted = true; + let mounted = true codeToHtml(children, { lang, theme: "one-dark-pro" }).then((res) => { - if (mounted) setHtml(res); - }); + if (mounted) setHtml(res) + }) return () => { - mounted = false; - }; - }, [children, lang]); + mounted = false + } + }, [children, lang]) return (
          - ); + ) } -); +) -CodeBlock.displayName = "CodeBlock"; +CodeBlock.displayName = "CodeBlock" interface BlockItemProps { - item: Item; - index: number; - isLast: boolean; - handleCopy: (index: number, code: string) => void; - copiedIndex: number | null; + item: Item + index: number + isLast: boolean + handleCopy: (index: number, code: string) => void + copiedIndex: number | null } // ------------------------------ | BLOCK ITEM | ------------------------------ // @@ -87,131 +84,133 @@ export default function BlockItem({ // eslint-disable-next-line @typescript-eslint/no-unused-vars isLast, handleCopy, - copiedIndex + copiedIndex, }: BlockItemProps) { - const [viewport, setViewport] = useState("desktop"); + const [viewport, setViewport] = useState("desktop") const filePath = item.files[0].path .replace(/^src\//, "") .replace(/^components\/uiable\//, "") - .replace(".tsx", ""); + .replace(".tsx", "") - const iframeEl = useRef(null); - const [viewportHeight, setViewportHeight] = useState(0); - const [isIframeLoading, setIsIframeLoading] = useState(true); - const [copiedCommand, setCopiedCommand] = useState(false); + const iframeEl = useRef(null) + const [viewportHeight, setViewportHeight] = useState(0) + const [isIframeLoading, setIsIframeLoading] = useState(true) + const [copiedCommand, setCopiedCommand] = useState(false) // Set iframe height based on its content const setIframeHeight = useCallback(() => { - const iframe = iframeEl.current; + const iframe = iframeEl.current if (iframe && iframe.contentWindow?.document?.body) { - const doc = iframe.contentWindow.document; - const height = doc.body.scrollHeight; + const doc = iframe.contentWindow.document + const height = doc.body.scrollHeight if (height > 0) { - setViewportHeight(height); + setViewportHeight(height) } } - }, []); + }, []) useEffect(() => { - const iframe = iframeEl.current; - if (!iframe) return; + const iframe = iframeEl.current + if (!iframe) return const handleIframeLoad = () => { // Small delay to ensure styles are applied - setTimeout(setIframeHeight, 150); + setTimeout(setIframeHeight, 150) - const iframeDoc = iframe.contentWindow?.document?.documentElement; + const iframeDoc = iframe.contentWindow?.document?.documentElement if (iframeDoc && typeof ResizeObserver !== "undefined") { - const resizeObserver = new ResizeObserver(() => setIframeHeight()); - resizeObserver.observe(iframeDoc); - return () => resizeObserver.disconnect(); + const resizeObserver = new ResizeObserver(() => setIframeHeight()) + resizeObserver.observe(iframeDoc) + return () => resizeObserver.disconnect() } - }; + } - iframe.addEventListener("load", handleIframeLoad); - window.addEventListener("resize", setIframeHeight); + iframe.addEventListener("load", handleIframeLoad) + window.addEventListener("resize", setIframeHeight) // Initial check in case it's already loaded if (iframe.contentWindow?.document?.readyState === "complete") { - handleIframeLoad(); + handleIframeLoad() } return () => { - iframe.removeEventListener("load", handleIframeLoad); - window.removeEventListener("resize", setIframeHeight); - }; - }, [setIframeHeight]); + iframe.removeEventListener("load", handleIframeLoad) + window.removeEventListener("resize", setIframeHeight) + } + }, [setIframeHeight]) // Handle screen size change const onScreenChange = () => { // Reset height to allow accurate measurement of shrinking content - setViewportHeight(0); + setViewportHeight(0) // wait for the iframe to reflow then recalculate height - setTimeout(setIframeHeight, 200); - }; + setTimeout(setIframeHeight, 200) + } // Handle iframe reload const handleReload = () => { if (iframeEl.current) { - setIsIframeLoading(true); + setIsIframeLoading(true) try { if (iframeEl.current.contentWindow) { - iframeEl.current.contentWindow.location.reload(); + iframeEl.current.contentWindow.location.reload() } else { - iframeEl.current.src = iframeEl.current.src; + iframeEl.current.src = iframeEl.current.src } // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (_error) { - iframeEl.current.src = iframeEl.current.src; + iframeEl.current.src = iframeEl.current.src } } - }; + } // Handle copying shadcn CLI command const handleCopyCommand = () => { - const commandText = `npx shadcn add @uiable/${item.name.replace(/^uiable-/, "")}`; - navigator.clipboard.writeText(commandText); - setCopiedCommand(true); - setTimeout(() => setCopiedCommand(false), 2000); - }; + const commandText = `npx shadcn add @uiable/${item.name.replace(/^uiable-/, "")}` + navigator.clipboard.writeText(commandText) + setCopiedCommand(true) + setTimeout(() => setCopiedCommand(false), 2000) + } const handleViewportChange = (value: string[]) => { if (value && value.length > 0) { - setViewport(value[value.length - 1]); - onScreenChange(); + setViewport(value[value.length - 1]) + onScreenChange() } - }; + } const handleOpenPreview = () => { - window.open(`/preview/${filePath}`, "_blank"); - }; + window.open(`/preview/${filePath}`, "_blank") + } const handleCopyClick = () => { - handleCopy(index, item.rawCode || ""); - }; + handleCopy(index, item.rawCode || "") + } - const handleIframeLoadEvent = (event: React.SyntheticEvent) => { - const iframe = event.currentTarget; + const handleIframeLoadEvent = ( + event: SyntheticEvent + ) => { + const iframe = event.currentTarget if (iframe) { - iframe.removeAttribute("srcdoc"); - setIframeHeight(); - setIsIframeLoading(false); + iframe.removeAttribute("srcdoc") + setIframeHeight() + setIsIframeLoading(false) } - }; + } return ( -
          -
          -
          - +
          +
          +
          + -
          +
          -
          +
          {item.title}
          -
          -
          +
          +
          -
          +
          } @@ -308,7 +307,7 @@ export default function BlockItem({
          -
          +
          {item.rawCode ? ( {item.rawCode} ) : ( -
          +
          No code available.
          )} @@ -400,11 +399,11 @@ export default function BlockItem({
          -
          -
          +
          +
          @@ -444,5 +443,5 @@ export default function BlockItem({ >
          */}
          - ); + ) } diff --git a/src/components/block-view.tsx b/src/components/block-view.tsx index fe2fba2..f63b3a7 100644 --- a/src/components/block-view.tsx +++ b/src/components/block-view.tsx @@ -1,65 +1,66 @@ -"use client"; +"use client" -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react" +import { categories } from "@/components-grid" + +import LazySection from "@/components/LazySection" // project -import { Item } from "./block-item"; -import { categories } from "@/components-grid"; -import LazySection from "@/components/LazySection"; +import { Item } from "./block-item" interface CategoryViewProps { - category: string; - items: Item[]; + category: string + items: Item[] } // ------------------------------ | COMPONENT - BLOCK VIEW | ------------------------------ // export default function CategoryView({ category, items }: CategoryViewProps) { - const [mounted, setMounted] = useState(false); - const [copiedIndex, setCopiedIndex] = useState(null); + const [mounted, setMounted] = useState(false) + const [copiedIndex, setCopiedIndex] = useState(null) useEffect(() => { - setTimeout(() => setMounted(true), 0); - }, []); + setTimeout(() => setMounted(true), 0) + }, []) const activeCategory = useMemo( () => categories.find((c) => c.slug === category), [category] - ); - const breakpoints = activeCategory?.breakpoints || {}; - const { xs = 1, sm, md, lg, xl, xxl } = breakpoints; + ) + const breakpoints = activeCategory?.breakpoints || {} + const { xs = 1, sm, md, lg, xl, xxl } = breakpoints const [columns, setColumns] = useState( xxl ?? xl ?? lg ?? md ?? sm ?? xs - ); + ) useEffect(() => { const handleResize = () => { - const width = window.innerWidth; - if (width >= 1536) setColumns(xxl ?? xl ?? lg ?? md ?? sm ?? xs); - else if (width >= 1280) setColumns(lg ?? md ?? sm ?? xs); - else if (width >= 1024) setColumns(lg ?? md ?? sm ?? xs); - else if (width >= 768) setColumns(md ?? sm ?? xs); - else if (width >= 640) setColumns(sm ?? xs); - else setColumns(xs); - }; - handleResize(); - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); + const width = window.innerWidth + if (width >= 1536) setColumns(xxl ?? xl ?? lg ?? md ?? sm ?? xs) + else if (width >= 1280) setColumns(lg ?? md ?? sm ?? xs) + else if (width >= 1024) setColumns(lg ?? md ?? sm ?? xs) + else if (width >= 768) setColumns(md ?? sm ?? xs) + else if (width >= 640) setColumns(sm ?? xs) + else setColumns(xs) + } + handleResize() + window.addEventListener("resize", handleResize) + return () => window.removeEventListener("resize", handleResize) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [breakpoints, xs, sm, md, lg, xl]); + }, [breakpoints, xs, sm, md, lg, xl]) const handleCopy = (index: number, code: string) => { - navigator.clipboard.writeText(code); - setCopiedIndex(index); - setTimeout(() => setCopiedIndex(null), 2000); - }; + navigator.clipboard.writeText(code) + setCopiedIndex(index) + setTimeout(() => setCopiedIndex(null), 2000) + } - if (!mounted) return null; + if (!mounted) return null return (
          {items.map((item, index) => ( @@ -72,12 +73,12 @@ export default function CategoryView({ category, items }: CategoryViewProps) { index, isLast: index === items.length - 1, handleCopy, - copiedIndex - } + copiedIndex, + }, }} offset="200px" /> ))}
          - ); + ) } diff --git a/src/components/category-description.tsx b/src/components/category-description.tsx index cd70768..a3bb69f 100644 --- a/src/components/category-description.tsx +++ b/src/components/category-description.tsx @@ -1,4 +1,9 @@ // shadcn + +// project +import branding from "@/branding.json" +import { categoryInfoMap as componentCategoryInfoMap } from "@/data/components" + import { Accordion, AccordionContent, @@ -7,10 +12,6 @@ import { } from "@/components/ui/accordion" import { Card, CardContent } from "@/components/ui/card" -// project -import branding from "@/branding.json" -import { categoryInfoMap as componentCategoryInfoMap } from "@/data/components" - const categoryInfoMap = { ...componentCategoryInfoMap, } diff --git a/src/components/category-view.tsx b/src/components/category-view.tsx index d99bd3d..cb9b6a2 100644 --- a/src/components/category-view.tsx +++ b/src/components/category-view.tsx @@ -1,205 +1,202 @@ -"use client"; - -import { memo, useEffect, useMemo, useState } from "react"; - -// shadcn -import { Button, buttonVariants } from "@/components/ui/button"; -import { Card, CardContent, CardHeader } from "@/components/ui/card"; -import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; - -// third party -import { type BundledLanguage, codeToHtml } from "shiki"; +"use client" +import { memo, useEffect, useMemo, useState } from "react" // project -import { categories } from "@/components-grid"; -import { cn } from "@/lib/utils"; - +import { categories } from "@/components-grid" // assets -import { Code, Copy, SquareCheckBig } from "lucide-react"; +import { Code, Copy, SquareCheckBig } from "lucide-react" +// third party +import { codeToHtml, type BundledLanguage } from "shiki" + +import { cn } from "@/lib/utils" +// shadcn +import { Button, buttonVariants } from "@/components/ui/button" +import { Card, CardContent, CardHeader } from "@/components/ui/card" +import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" interface Item { - name: string; - title: string; - description: string; - files: { path: string }[]; - categories: string[]; - rawCode?: string; - type?: string; + name: string + title: string + description: string + files: { path: string }[] + categories: string[] + rawCode?: string + type?: string } interface CategoryViewProps { - category: string; - items: Item[]; + category: string + items: Item[] } const CodeBlock = memo( ({ children, lang }: { children: string; lang: BundledLanguage }) => { - const [html, setHtml] = useState(""); + const [html, setHtml] = useState("") useEffect(() => { - let mounted = true; + let mounted = true codeToHtml(children, { lang, theme: "one-dark-pro" }).then((res) => { - if (mounted) setHtml(res); - }); + if (mounted) setHtml(res) + }) return () => { - mounted = false; - }; - }, [children, lang]); + mounted = false + } + }, [children, lang]) return (
          - ); + ) } -); +) -CodeBlock.displayName = "CodeBlock"; +CodeBlock.displayName = "CodeBlock" // eslint-disable-next-line @typescript-eslint/no-unused-vars -const componentCache: Record = {}; +const componentCache: Record = {} function DynamicItem({ filePath, - isBlock + isBlock, }: { - filePath: string; - isBlock: boolean; + filePath: string + isBlock: boolean }) { - const [Comp, setComp] = useState(null); + const [Comp, setComp] = useState(null) useEffect(() => { - let mounted = true; + let mounted = true const load = async () => { try { const mod = isBlock ? await import(`./uiable/blocks/${filePath}.tsx`) - : await import(`./uiable/${filePath}.tsx`); + : await import(`./uiable/${filePath}.tsx`) if (mounted) { // Explicitly handle default and named exports const component = mod.default || mod.Component || - Object.values(mod).find((v) => typeof v === "function"); - setComp(() => component); + Object.values(mod).find((v) => typeof v === "function") + setComp(() => component) } } catch (err) { - console.error("Dynamic import failed for", filePath, err); + console.error("Dynamic import failed for", filePath, err) if (mounted) { // eslint-disable-next-line react/display-name setComp(() => () => ( -
          +
          Component not found: {filePath}
          - )); + )) } } - }; - load(); + } + load() return () => { - mounted = false; - }; - }, [filePath, isBlock]); + mounted = false + } + }, [filePath, isBlock]) if (!Comp) { return ( -
          - ); +
          + ) } - return ; + return } // ------------------------------ | COMPONENT - CATEGORY VIEW | ------------------------------ // export default function CategoryView({ category, items }: CategoryViewProps) { - const [mounted, setMounted] = useState(false); - const [copiedIndex, setCopiedIndex] = useState(null); + const [mounted, setMounted] = useState(false) + const [copiedIndex, setCopiedIndex] = useState(null) const [copiedCommandIndex, setCopiedCommandIndex] = useState( null - ); + ) // Vars useEffect(() => { - setTimeout(() => setMounted(true), 0); - }, []); + setTimeout(() => setMounted(true), 0) + }, []) const activeCategory = useMemo( () => categories.find((c) => c.slug === category), [category] - ); - const breakpoints = activeCategory?.breakpoints || {}; - const { xs = 1, sm, md, lg, xl, xxl } = breakpoints; + ) + const breakpoints = activeCategory?.breakpoints || {} + const { xs = 1, sm, md, lg, xl, xxl } = breakpoints const [columns, setColumns] = useState( xxl ?? xl ?? lg ?? md ?? sm ?? xs - ); + ) useEffect(() => { const handleResize = () => { - const width = window.innerWidth; - if (width >= 1536) setColumns(xxl ?? xl ?? lg ?? md ?? sm ?? xs); - else if (width >= 1280) setColumns(lg ?? md ?? sm ?? xs); - else if (width >= 1024) setColumns(lg ?? md ?? sm ?? xs); - else if (width >= 768) setColumns(md ?? sm ?? xs); - else if (width >= 640) setColumns(sm ?? xs); - else setColumns(xs); - }; - handleResize(); - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); + const width = window.innerWidth + if (width >= 1536) setColumns(xxl ?? xl ?? lg ?? md ?? sm ?? xs) + else if (width >= 1280) setColumns(lg ?? md ?? sm ?? xs) + else if (width >= 1024) setColumns(lg ?? md ?? sm ?? xs) + else if (width >= 768) setColumns(md ?? sm ?? xs) + else if (width >= 640) setColumns(sm ?? xs) + else setColumns(xs) + } + handleResize() + window.addEventListener("resize", handleResize) + return () => window.removeEventListener("resize", handleResize) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [breakpoints, xs, sm, md, lg, xl]); + }, [breakpoints, xs, sm, md, lg, xl]) const handleCopy = (index: number, code: string) => { - navigator.clipboard.writeText(code); - setCopiedIndex(index); - setTimeout(() => setCopiedIndex(null), 2000); - }; + navigator.clipboard.writeText(code) + setCopiedIndex(index) + setTimeout(() => setCopiedIndex(null), 2000) + } - if (!mounted) return null; + if (!mounted) return null return (
          {items.map((item, index) => { const filePath = item.files[0].path .replace(/^src\//, "") .replace(/^components\/uiable\//, "") - .replace(".tsx", ""); + .replace(".tsx", "") - const baseUrl = process.env.NEXT_PUBLIC_APP_URL; + const baseUrl = process.env.NEXT_PUBLIC_APP_URL const commands = { pnpm: { cli: "pnpm", - command: `pnpm dlx shadcn@latest add ${baseUrl}/r/${item.name}.json` + command: `pnpm dlx shadcn@latest add ${baseUrl}/r/${item.name}.json`, }, npm: { cli: "npm", - command: `npx shadcn@latest add ${baseUrl}/r/${item.name}.json` + command: `npx shadcn@latest add ${baseUrl}/r/${item.name}.json`, }, yarn: { cli: "yarn", - command: `yarn shadcn@latest add ${baseUrl}/r/${item.name}.json` + command: `yarn shadcn@latest add ${baseUrl}/r/${item.name}.json`, }, bun: { cli: "bun", - command: `bunx --bun shadcn@latest add ${baseUrl}/r/${item.name}.json` - } - }; + command: `bunx --bun shadcn@latest add ${baseUrl}/r/${item.name}.json`, + }, + } function handleCopyCommand(index: number, arg1: string): void { - navigator.clipboard.writeText(arg1); - setCopiedCommandIndex(index); - setTimeout(() => setCopiedCommandIndex(null), 2000); + navigator.clipboard.writeText(arg1) + setCopiedCommandIndex(index) + setTimeout(() => setCopiedCommandIndex(null), 2000) } return ( - +
          -
          +
          {item.title}
          @@ -208,7 +205,7 @@ export default function CategoryView({ category, items }: CategoryViewProps) { onClick={() => handleCopy(index, item.rawCode || "")} variant="outline" size="icon" - className="bg-transparent border-none gap-1.5" + className="gap-1.5 border-none bg-transparent" > {copiedIndex === index ? ( @@ -220,13 +217,13 @@ export default function CategoryView({ category, items }: CategoryViewProps) { - -
          + +
          CLI Command
          {Object.entries(commands).map(([key, value]) => ( {value.cli} @@ -250,8 +247,8 @@ export default function CategoryView({ category, items }: CategoryViewProps) {
          {Object.entries(commands).map(([key, value]) => ( -
          -
          +
          +
          {value.command ? (
          @@ -260,7 +257,7 @@ export default function CategoryView({ category, items }: CategoryViewProps) {
          ) : ( -
          +
          No code available.
          )} @@ -286,18 +283,18 @@ export default function CategoryView({ category, items }: CategoryViewProps) { ))}
          -
          +
          Code
          -
          -
          - +
          +
          + TSX
          -
          +
          {item.rawCode ? ( {item.rawCode} ) : ( -
          +
          No code available.
          )} @@ -325,12 +322,12 @@ export default function CategoryView({ category, items }: CategoryViewProps) {
          - +
          - ); + ) })}
          - ); + ) } diff --git a/src/components/component-example.tsx b/src/components/component-example.tsx index df15ab2..8ca19d7 100644 --- a/src/components/component-example.tsx +++ b/src/components/component-example.tsx @@ -1,6 +1,37 @@ -"use client"; +"use client" -import { useState } from "react"; +import { useState } from "react" +// assets +import { + BellIcon, + BluetoothIcon, + CreditCardIcon, + DownloadIcon, + EyeIcon, + FileCodeIcon, + FileIcon, + FileTextIcon, + FolderIcon, + FolderOpenIcon, + FolderSearchIcon, + HelpCircleIcon, + KeyboardIcon, + LanguagesIcon, + LayoutIcon, + LogOutIcon, + MailIcon, + MonitorIcon, + MoonIcon, + MoreHorizontalIcon, + MoreVerticalIcon, + PaletteIcon, + PlusIcon, + SaveIcon, + SettingsIcon, + ShieldIcon, + SunIcon, + UserIcon, +} from "lucide-react" // shadcn import { @@ -13,10 +44,10 @@ import { AlertDialogHeader, AlertDialogMedia, AlertDialogTitle, - AlertDialogTrigger -} from "@/components/ui/alert-dialog"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" import { Card, CardAction, @@ -24,16 +55,16 @@ import { CardDescription, CardFooter, CardHeader, - CardTitle -} from "@/components/ui/card"; + CardTitle, +} from "@/components/ui/card" import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, - ComboboxList -} from "@/components/ui/combobox"; + ComboboxList, +} from "@/components/ui/combobox" import { DropdownMenu, DropdownMenuCheckboxItem, @@ -49,54 +80,21 @@ import { DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, - DropdownMenuTrigger -} from "@/components/ui/dropdown-menu"; -import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; -import { Input } from "@/components/ui/input"; + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Field, FieldGroup, FieldLabel } from "@/components/ui/field" +import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, - SelectValue -} from "@/components/ui/select"; -import { Textarea } from "@/components/ui/textarea"; - + SelectValue, +} from "@/components/ui/select" +import { Textarea } from "@/components/ui/textarea" // project -import { Example, ExampleWrapper } from "@/components/example"; - -// assets -import { - BellIcon, - BluetoothIcon, - CreditCardIcon, - DownloadIcon, - EyeIcon, - FileCodeIcon, - FileIcon, - FileTextIcon, - FolderIcon, - FolderOpenIcon, - FolderSearchIcon, - HelpCircleIcon, - KeyboardIcon, - LanguagesIcon, - LayoutIcon, - LogOutIcon, - MailIcon, - MonitorIcon, - MoonIcon, - MoreHorizontalIcon, - MoreVerticalIcon, - PaletteIcon, - PlusIcon, - SaveIcon, - SettingsIcon, - ShieldIcon, - SunIcon, - UserIcon -} from "lucide-react"; +import { Example, ExampleWrapper } from "@/components/example" // ------------------------------ | COMPONENT - EXAMPLE | ------------------------------ // @@ -106,14 +104,14 @@ export function ComponentExample() { - ); + ) } function CardExample() { return ( -
          +
          Photo by mymind on Unsplash - ); + ) } const frameworks = [ @@ -165,44 +163,46 @@ const frameworks = [ "SvelteKit", "Nuxt.js", "Remix", - "Astro" -] as const; + "Astro", +] as const const roleItems = [ { label: "Developer", value: "developer" }, { label: "Designer", value: "designer" }, { label: "Manager", value: "manager" }, - { label: "Other", value: "other" } -]; + { label: "Other", value: "other" }, +] function FormExample() { const [notifications, setNotifications] = useState({ email: true, sms: false, - push: true - }); - const [theme, setTheme] = useState("light"); + push: true, + }) + const [theme, setTheme] = useState("light") - const handleEmailNotificationChange = (checked: boolean | "indeterminate") => { + const handleEmailNotificationChange = ( + checked: boolean | "indeterminate" + ) => { setNotifications((prev) => ({ ...prev, - email: checked === true - })); - }; + email: checked === true, + })) + } const handleSmsNotificationChange = (checked: boolean | "indeterminate") => { setNotifications((prev) => ({ ...prev, - sms: checked === true - })); - }; + sms: checked === true, + })) + } const handlePushNotificationChange = (checked: boolean | "indeterminate") => { setNotifications((prev) => ({ ...prev, - push: checked === true - })); - }; + push: checked === true, + })) + } return ( @@ -379,14 +379,18 @@ function FormExample() { Push Notifications Email Notifications @@ -500,5 +504,5 @@ function FormExample() { - ); + ) } diff --git a/src/components/customizer/RadiusControl.tsx b/src/components/customizer/RadiusControl.tsx index cf2e6de..b99012b 100644 --- a/src/components/customizer/RadiusControl.tsx +++ b/src/components/customizer/RadiusControl.tsx @@ -1,13 +1,13 @@ -"use client"; +"use client" // shadcn -import { Input } from "@/components/ui/input"; -import { Slider } from "@/components/ui/slider"; +import { Input } from "@/components/ui/input" +import { Slider } from "@/components/ui/slider" type RadiusControlProps = { - radius: number; - onRadiusChange: (value: number) => void; -}; + radius: number + onRadiusChange: (value: number) => void +} // ------------------------------ | CUSTOMIZER - RADIUS CONTROL | ------------------------------ // @@ -36,8 +36,8 @@ export function RadiusControl({ radius, onRadiusChange }: RadiusControlProps) { type="number" value={radius} onChange={(e) => { - const value = parseFloat(e.target.value); - onRadiusChange(isNaN(value) ? 0 : value); + const value = parseFloat(e.target.value) + onRadiusChange(isNaN(value) ? 0 : value) }} min={0} max={1.25} @@ -49,5 +49,5 @@ export function RadiusControl({ radius, onRadiusChange }: RadiusControlProps) {
          - ); + ) } diff --git a/src/components/customizer/ThemePresetButtons.tsx b/src/components/customizer/ThemePresetButtons.tsx index ce8cae4..e377ba0 100644 --- a/src/components/customizer/ThemePresetButtons.tsx +++ b/src/components/customizer/ThemePresetButtons.tsx @@ -1,7 +1,7 @@ -"use client"; +"use client" // shadcn -import { Button } from "@/components/ui/button"; +import { Button } from "@/components/ui/button" // constants const PRESET_BUTTONS = [ @@ -14,17 +14,17 @@ const PRESET_BUTTONS = [ { label: "Datta", value: "datta" }, { label: "Flat", value: "flat" }, { label: "Guru", value: "guru" }, - { label: "Berry", value: "berry" } -]; + { label: "Berry", value: "berry" }, +] type ThemePresetButtonsProps = { - onSelectPreset: (className: string) => void; -}; + onSelectPreset: (className: string) => void +} // ------------------------------ | CUSTOMIZER - THEME PRESET BUTTONS | ------------------------------ // export function ThemePresetButtons({ - onSelectPreset + onSelectPreset, }: ThemePresetButtonsProps) { return (
          @@ -38,5 +38,5 @@ export function ThemePresetButtons({ ))}
          - ); + ) } diff --git a/src/components/customizer/ThemePresetStyles.tsx b/src/components/customizer/ThemePresetStyles.tsx index 41ea08e..c13f68e 100644 --- a/src/components/customizer/ThemePresetStyles.tsx +++ b/src/components/customizer/ThemePresetStyles.tsx @@ -42,13 +42,7 @@ const PRESET_DATA: Record = { "0 130 54", "13 84 43", ], - "preset-8": [ - "208 250 229", - "94 233 181", - "0 188 125", - "0 122 85", - "0 79 59", - ], + "preset-8": ["208 250 229", "94 233 181", "0 188 125", "0 122 85", "0 79 59"], "preset-9": [ "203 251 241", "70 236 213", diff --git a/src/components/customizer/index.tsx b/src/components/customizer/index.tsx index 223c385..7355720 100644 --- a/src/components/customizer/index.tsx +++ b/src/components/customizer/index.tsx @@ -1,7 +1,13 @@ "use client" import { MouseEvent, useCallback, useEffect, useState } from "react" +// assets +import { CircleCheckBig, Cpu, Moon, Settings2, Sun } from "lucide-react" +// third party +import { useTheme } from "next-themes" +// project +import { useThemeRadius } from "@/hooks/use-theme-radius" // shadcn import { Button } from "@/components/ui/button" import { Separator } from "@/components/ui/separator" @@ -14,18 +20,10 @@ import { SheetTrigger, } from "@/components/ui/sheet" -// third party -import { useTheme } from "next-themes" - -// project -import { useThemeRadius } from "@/hooks/use-theme-radius" import { RadiusControl } from "./RadiusControl" // import { ThemePresetButtons } from "./ThemePresetButtons"; import { ThemePresetStyles } from "./ThemePresetStyles" -// assets -import { CircleCheckBig, Moon, Settings2, Sun } from "lucide-react" - // constants const THEME_PRESET_KEY = "theme-preset" @@ -183,7 +181,7 @@ export function ThemeToggle() { ) const resetDefault = useCallback(() => { - setTheme("light") + setTheme("system") themeClasses.forEach((cls) => document.body.classList.remove(cls)) document.body.classList.add("default") localStorage.removeItem(THEME_PRESET_KEY) @@ -226,9 +224,11 @@ export function ThemeToggle() {
          Theme Mode
          -

          Choose light or dark mode

          +

          + Choose light, dark, or system mode +

          -
          +
          +
          {/*
          diff --git a/src/components/doc-bottom-nav.tsx b/src/components/doc-bottom-nav.tsx index db11e8b..c11bd79 100644 --- a/src/components/doc-bottom-nav.tsx +++ b/src/components/doc-bottom-nav.tsx @@ -1,39 +1,38 @@ -import Link from "next/link"; +import Link from "next/link" +// assets +import { ChevronLeft, ChevronRight } from "lucide-react" // shadcn -import { Card, CardContent } from "@/components/ui/card"; - -// assets -import { ChevronLeft, ChevronRight } from "lucide-react"; +import { Card, CardContent } from "@/components/ui/card" type NavItem = { - name: string; - url: string; -}; + name: string + url: string +} type ComponentNavigationProps = { - previousItem: NavItem | null; - nextItem: NavItem | null; -}; + previousItem: NavItem | null + nextItem: NavItem | null +} // ------------------------------ | COMPONENT - DOC BOTTOM NAV | ------------------------------ // export default function DocBottomNav({ previousItem, - nextItem + nextItem, }: ComponentNavigationProps) { return ( -
          +
          {previousItem && ( - +
          - +
          -
          -

          +

          +

          Previous

          {previousItem.name}

          @@ -45,17 +44,17 @@ export default function DocBottomNav({ )} {nextItem && ( - +
          -
          -

          +

          +

          Next

          {nextItem.name}

          - +
          @@ -63,5 +62,5 @@ export default function DocBottomNav({ )}
          - ); + ) } diff --git a/src/components/example.tsx b/src/components/example.tsx index 09f7e06..f2f164e 100644 --- a/src/components/example.tsx +++ b/src/components/example.tsx @@ -1,13 +1,13 @@ -import { ComponentProps } from "react"; +import { ComponentProps } from "react" // project -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils" // ------------------------------ | COMPONENT - EXAMPLE | ------------------------------ // function ExampleWrapper({ className, ...props }: ComponentProps<"div">) { return ( -
          +
          ) { {...props} />
          - ); + ) } function Example({ @@ -28,8 +28,8 @@ function Example({ containerClassName, ...props }: ComponentProps<"div"> & { - title?: string; - containerClassName?: string; + title?: string + containerClassName?: string }) { return (
          {title && ( -
          +
          {title}
          )}
          {children}
          - ); + ) } -export { ExampleWrapper, Example }; +export { ExampleWrapper, Example } diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx index 6436bed..2c33ea9 100644 --- a/src/components/theme-provider.tsx +++ b/src/components/theme-provider.tsx @@ -1,7 +1,6 @@ -import { ComponentProps } from "react"; - +import { ComponentProps } from "react" // third party -import { ThemeProvider as NextThemesProvider } from "next-themes"; +import { ThemeProvider as NextThemesProvider } from "next-themes" // ------------------------------ | COMPONENT - THEME PROVIDER | ------------------------------ // @@ -9,5 +8,5 @@ export function ThemeProvider({ children, ...props }: ComponentProps) { - return {children}; + return {children} } diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx index d756265..b6053f7 100644 --- a/src/components/ui/accordion.tsx +++ b/src/components/ui/accordion.tsx @@ -1,9 +1,9 @@ -"use client"; +"use client" -import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"; +import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion" +import { ChevronDownIcon, ChevronUpIcon } from "lucide-react" -import { cn } from "@/lib/utils"; -import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; +import { cn } from "@/lib/utils" function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) { return ( @@ -12,17 +12,17 @@ function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) { className={cn("flex w-full flex-col", className)} {...props} /> - ); + ) } function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) { return ( - ); + ) } function AccordionTrigger({ @@ -35,7 +35,7 @@ function AccordionTrigger({ - ); + ) } function AccordionContent({ @@ -62,19 +62,19 @@ function AccordionContent({ return (
          {children}
          - ); + ) } -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx index ba06db4..7047c29 100644 --- a/src/components/ui/alert-dialog.tsx +++ b/src/components/ui/alert-dialog.tsx @@ -1,26 +1,25 @@ -"use client"; +"use client" -import { ComponentProps } from "react"; +import { ComponentProps } from "react" +import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog" -import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"; - -import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) { - return ; + return } function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) { return ( - ); + ) } function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) { return ( - ); + ) } function AlertDialogOverlay({ @@ -31,12 +30,12 @@ function AlertDialogOverlay({ - ); + ) } function AlertDialogContent({ @@ -44,7 +43,7 @@ function AlertDialogContent({ size = "default", ...props }: AlertDialogPrimitive.Popup.Props & { - size?: "default" | "sm"; + size?: "default" | "sm" }) { return ( @@ -53,19 +52,16 @@ function AlertDialogContent({ data-slot="alert-dialog-content" data-size={size} className={cn( - "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-card ring-foreground/10 group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-5 rounded-lg p-5 ring-0 shadow-[0_0_15px_-3px_rgb(0,0,0,0.1)] duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm", + "group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-5 rounded-lg bg-card p-5 shadow-[0_0_15px_-3px_rgb(0,0,0,0.1)] ring-0 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )} {...props} /> - ); + ) } -function AlertDialogHeader({ - className, - ...props -}: ComponentProps<"div">) { +function AlertDialogHeader({ className, ...props }: ComponentProps<"div">) { return (
          - ); + ) } -function AlertDialogFooter({ - className, - ...props -}: ComponentProps<"div">) { +function AlertDialogFooter({ className, ...props }: ComponentProps<"div">) { return (
          - ); + ) } -function AlertDialogMedia({ - className, - ...props -}: ComponentProps<"div">) { +function AlertDialogMedia({ className, ...props }: ComponentProps<"div">) { return (
          - ); + ) } function AlertDialogTitle({ @@ -123,7 +113,7 @@ function AlertDialogTitle({ )} {...props} /> - ); + ) } function AlertDialogDescription({ @@ -134,12 +124,12 @@ function AlertDialogDescription({ - ); + ) } function AlertDialogAction({ @@ -152,7 +142,7 @@ function AlertDialogAction({ className={cn(className)} {...props} /> - ); + ) } function AlertDialogCancel({ @@ -169,7 +159,7 @@ function AlertDialogCancel({ render={ - ); + ) } function CarouselNext({ @@ -208,7 +215,7 @@ function CarouselNext({ size = "icon-sm", ...props }: ComponentProps) { - const { orientation, scrollNext, canScrollNext } = useCarousel(); + const { orientation, scrollNext, canScrollNext } = useCarousel() return ( - ); + ) } export { @@ -239,5 +246,5 @@ export { CarouselItem, CarouselPrevious, CarouselNext, - useCarousel -}; + useCarousel, +} diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx index 79ebb8f..966ec61 100644 --- a/src/components/ui/chart.tsx +++ b/src/components/ui/chart.tsx @@ -1,43 +1,51 @@ -"use client"; - -import { CSSProperties, ComponentProps, ComponentType, ReactNode, createContext, useContext, useId, useMemo } from "react"; - -import * as RechartsPrimitive from "recharts"; -import type { TooltipValueType } from "recharts"; - -import { cn } from "@/lib/utils"; +"use client" + +import { + ComponentProps, + ComponentType, + createContext, + CSSProperties, + ReactNode, + useContext, + useId, + useMemo, +} from "react" +import * as RechartsPrimitive from "recharts" +import type { TooltipValueType } from "recharts" + +import { cn } from "@/lib/utils" // Format: { THEME_NAME: CSS_SELECTOR } -const THEMES = { light: "", dark: ".dark" } as const; +const THEMES = { light: "", dark: ".dark" } as const -const INITIAL_DIMENSION = { width: 320, height: 200 } as const; -type TooltipNameType = number | string; +const INITIAL_DIMENSION = { width: 320, height: 200 } as const +type TooltipNameType = number | string export type ChartConfig = Record< string, { - label?: ReactNode; - icon?: ComponentType; + label?: ReactNode + icon?: ComponentType } & ( | { color?: string; theme?: never } | { color?: never; theme: Record } ) ->; +> type ChartContextProps = { - config: ChartConfig; -}; + config: ChartConfig +} -const ChartContext = createContext(null); +const ChartContext = createContext(null) function useChart() { - const context = useContext(ChartContext); + const context = useContext(ChartContext) if (!context) { - throw new Error("useChart must be used within a "); + throw new Error("useChart must be used within a ") } - return context; + return context } function ChartContainer({ @@ -48,17 +56,17 @@ function ChartContainer({ initialDimension = INITIAL_DIMENSION, ...props }: ComponentProps<"div"> & { - config: ChartConfig; + config: ChartConfig children: ComponentProps< typeof RechartsPrimitive.ResponsiveContainer - >["children"]; + >["children"] initialDimension?: { - width: number; - height: number; - }; + width: number + height: number + } }) { - const uniqueId = useId(); - const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}`; + const uniqueId = useId() + const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}` return ( @@ -79,16 +87,16 @@ function ChartContainer({
          - ); + ) } const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { const colorConfig = Object.entries(config).filter( ([, config]) => config.theme ?? config.color - ); + ) if (!colorConfig.length) { - return null; + return null } return ( @@ -102,20 +110,20 @@ ${colorConfig .map(([key, itemConfig]) => { const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ?? - itemConfig.color; - return color ? ` --color-${key}: ${color};` : null; + itemConfig.color + return color ? ` --color-${key}: ${color};` : null }) .join("\n")} } ` ) - .join("\n") + .join("\n"), }} /> - ); -}; + ) +} -const ChartTooltip = RechartsPrimitive.Tooltip; +const ChartTooltip = RechartsPrimitive.Tooltip function ChartTooltipContent({ active, @@ -130,14 +138,14 @@ function ChartTooltipContent({ formatter, color, nameKey, - labelKey + labelKey, }: ComponentProps & ComponentProps<"div"> & { - hideLabel?: boolean; - hideIndicator?: boolean; - indicator?: "line" | "dot" | "dashed"; - nameKey?: string; - labelKey?: string; + hideLabel?: boolean + hideIndicator?: boolean + indicator?: "line" | "dot" | "dashed" + nameKey?: string + labelKey?: string } & Omit< RechartsPrimitive.DefaultTooltipContentProps< TooltipValueType, @@ -145,34 +153,34 @@ function ChartTooltipContent({ >, "accessibilityLayer" >) { - const { config } = useChart(); + const { config } = useChart() const tooltipLabel = useMemo(() => { if (hideLabel || !payload?.length) { - return null; + return null } - const [item] = payload; - const key = `${labelKey ?? item?.dataKey ?? item?.name ?? "value"}`; - const itemConfig = getPayloadConfigFromPayload(config, item, key); + const [item] = payload + const key = `${labelKey ?? item?.dataKey ?? item?.name ?? "value"}` + const itemConfig = getPayloadConfigFromPayload(config, item, key) const value = !labelKey && typeof label === "string" ? (config[label]?.label ?? label) - : itemConfig?.label; + : itemConfig?.label if (labelFormatter) { return (
          {labelFormatter(value, payload)}
          - ); + ) } if (!value) { - return null; + return null } - return
          {value}
          ; + return
          {value}
          }, [ label, labelFormatter, @@ -180,14 +188,14 @@ function ChartTooltipContent({ hideLabel, labelClassName, config, - labelKey - ]); + labelKey, + ]) if (!active || !payload?.length) { - return null; + return null } - const nestLabel = payload.length === 1 && indicator !== "dot"; + const nestLabel = payload.length === 1 && indicator !== "dot" return (
          item.type !== "none") .map((item, index) => { - const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`; - const itemConfig = getPayloadConfigFromPayload(config, item, key); - const indicatorColor = color ?? item.payload?.fill ?? item.color; + const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}` + const itemConfig = getPayloadConfigFromPayload(config, item, key) + const indicatorColor = color ?? item.payload?.fill ?? item.color return (
          @@ -264,29 +272,29 @@ function ChartTooltipContent({ )}
          - ); + ) })}
          - ); + ) } -const ChartLegend = RechartsPrimitive.Legend; +const ChartLegend = RechartsPrimitive.Legend function ChartLegendContent({ className, hideIcon = false, payload, verticalAlign = "bottom", - nameKey + nameKey, }: ComponentProps<"div"> & { - hideIcon?: boolean; - nameKey?: string; + hideIcon?: boolean + nameKey?: string } & RechartsPrimitive.DefaultLegendContentProps) { - const { config } = useChart(); + const { config } = useChart() if (!payload?.length) { - return null; + return null } return ( @@ -300,8 +308,8 @@ function ChartLegendContent({ {payload .filter((item) => item.type !== "none") .map((item, index) => { - const key = `${nameKey ?? item.dataKey ?? "value"}`; - const itemConfig = getPayloadConfigFromPayload(config, item, key); + const key = `${nameKey ?? item.dataKey ?? "value"}` + const itemConfig = getPayloadConfigFromPayload(config, item, key) return (
          )} {itemConfig?.label}
          - ); + ) })}
          - ); + ) } function getPayloadConfigFromPayload( @@ -334,7 +342,7 @@ function getPayloadConfigFromPayload( key: string ) { if (typeof payload !== "object" || payload === null) { - return undefined; + return undefined } const payloadPayload = @@ -342,15 +350,15 @@ function getPayloadConfigFromPayload( typeof payload.payload === "object" && payload.payload !== null ? payload.payload - : undefined; + : undefined - let configLabelKey: string = key; + let configLabelKey: string = key if ( key in payload && typeof payload[key as keyof typeof payload] === "string" ) { - configLabelKey = payload[key as keyof typeof payload] as string; + configLabelKey = payload[key as keyof typeof payload] as string } else if ( payloadPayload && key in payloadPayload && @@ -358,10 +366,10 @@ function getPayloadConfigFromPayload( ) { configLabelKey = payloadPayload[ key as keyof typeof payloadPayload - ] as string; + ] as string } - return configLabelKey in config ? config[configLabelKey] : config[key]; + return configLabelKey in config ? config[configLabelKey] : config[key] } export { @@ -370,5 +378,5 @@ export { ChartTooltipContent, ChartLegend, ChartLegendContent, - ChartStyle -}; + ChartStyle, +} diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx index 1be7382..28b5cd6 100644 --- a/src/components/ui/checkbox.tsx +++ b/src/components/ui/checkbox.tsx @@ -1,16 +1,16 @@ -"use client"; +"use client" -import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"; +import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox" +import { CheckIcon } from "lucide-react" -import { cn } from "@/lib/utils"; -import { CheckIcon } from "lucide-react"; +import { cn } from "@/lib/utils" function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { return ( - ); + ) } -export { Checkbox }; +export { Checkbox } diff --git a/src/components/ui/collapsible.tsx b/src/components/ui/collapsible.tsx index 2e44190..488fb33 100644 --- a/src/components/ui/collapsible.tsx +++ b/src/components/ui/collapsible.tsx @@ -1,21 +1,21 @@ -"use client"; +"use client" -import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible"; +import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible" function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) { - return ; + return } function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) { return ( - ); + ) } function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) { return ( - ); + ) } -export { Collapsible, CollapsibleTrigger, CollapsibleContent }; +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx index ff7e44a..b56f864 100644 --- a/src/components/ui/combobox.tsx +++ b/src/components/ui/combobox.tsx @@ -1,23 +1,22 @@ -"use client"; +"use client" -import { ComponentPropsWithRef, useRef } from "react"; +import { ComponentPropsWithRef, useRef } from "react" +import { Combobox as ComboboxPrimitive } from "@base-ui/react" +import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react" -import { Combobox as ComboboxPrimitive } from "@base-ui/react"; - -import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" import { InputGroup, InputGroupAddon, InputGroupButton, - InputGroupInput -} from "@/components/ui/input-group"; -import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react"; + InputGroupInput, +} from "@/components/ui/input-group" -const Combobox = ComboboxPrimitive.Root; +const Combobox = ComboboxPrimitive.Root function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { - return ; + return } function ComboboxTrigger({ @@ -34,7 +33,7 @@ function ComboboxTrigger({ {children} - ); + ) } function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { @@ -47,7 +46,7 @@ function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { > - ); + ) } function ComboboxInput({ @@ -58,8 +57,8 @@ function ComboboxInput({ showClear = false, ...props }: ComboboxPrimitive.Input.Props & { - showTrigger?: boolean; - showClear?: boolean; + showTrigger?: boolean + showClear?: boolean }) { return ( @@ -82,7 +81,7 @@ function ComboboxInput({ {children} - ); + ) } function ComboboxContent({ @@ -112,14 +111,14 @@ function ComboboxContent({ data-slot="combobox-content" data-chips={!!anchor} className={cn( - "bg-card text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:border-input/30 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg p-2 shadow-[0_4px_24px_0_rgba(62,57,107,.18)] ring-0 duration-100 data-[chips=true]:min-w-(--anchor-width) *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:shadow-none", + "group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg bg-card p-2 text-popover-foreground shadow-[0_4px_24px_0_rgba(62,57,107,.18)] ring-0 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )} {...props} /> - ); + ) } function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { @@ -132,7 +131,7 @@ function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { )} {...props} /> - ); + ) } function ComboboxItem({ @@ -144,7 +143,7 @@ function ComboboxItem({ - ); + ) } function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) { @@ -168,7 +167,7 @@ function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) { className={cn(className)} {...props} /> - ); + ) } function ComboboxLabel({ @@ -178,16 +177,16 @@ function ComboboxLabel({ return ( - ); + ) } function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) { return ( - ); + ) } function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { @@ -195,12 +194,12 @@ function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
        - ); - }, [children, errors]); + ) + }, [children, errors]) if (!content) { - return null; + return null } return (
        {content}
        - ); + ) } export { @@ -234,5 +234,5 @@ export { FieldSeparator, FieldSet, FieldContent, - FieldTitle -}; + FieldTitle, +} diff --git a/src/components/ui/hover-card.tsx b/src/components/ui/hover-card.tsx index 444cef3..987a396 100644 --- a/src/components/ui/hover-card.tsx +++ b/src/components/ui/hover-card.tsx @@ -1,17 +1,17 @@ -"use client"; +"use client" -import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card"; +import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card" -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils" function HoverCard({ ...props }: PreviewCardPrimitive.Root.Props) { - return ; + return } function HoverCardTrigger({ ...props }: PreviewCardPrimitive.Trigger.Props) { return ( - ); + ) } function HoverCardContent({ @@ -38,14 +38,14 @@ function HoverCardContent({ - ); + ) } -export { HoverCard, HoverCardTrigger, HoverCardContent }; +export { HoverCard, HoverCardTrigger, HoverCardContent } diff --git a/src/components/ui/input-group.tsx b/src/components/ui/input-group.tsx index e2b62a8..3cd1d9a 100644 --- a/src/components/ui/input-group.tsx +++ b/src/components/ui/input-group.tsx @@ -1,13 +1,12 @@ -"use client"; +"use client" -import { ComponentProps } from "react"; +import { ComponentProps } from "react" +import { cva, type VariantProps } from "class-variance-authority" -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" function InputGroup({ className, ...props }: ComponentProps<"div">) { return ( @@ -15,34 +14,34 @@ function InputGroup({ className, ...props }: ComponentProps<"div">) { data-slot="input-group" role="group" className={cn( - "border-border dark:bg-input/30 has-[[data-slot=input-group-control]:focus-visible]:border-primary has-[[data-slot=input-group-control]:focus-visible]:ring-primary has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-disabled:bg-input/50 dark:has-disabled:bg-input/80 group/input-group relative flex w-full min-w-0 items-center rounded-lg border transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:ring-0 has-[[data-slot][aria-invalid=true]]:ring-0 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5", + "group/input-group relative flex w-full min-w-0 items-center rounded-lg border border-border transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-primary has-[[data-slot=input-group-control]:focus-visible]:ring-0 has-[[data-slot=input-group-control]:focus-visible]:ring-primary has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-0 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5", className )} {...props} /> - ); + ) } const inputGroupAddonVariants = cva( - "text-muted-foreground h-auto gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4 flex cursor-text items-center justify-center select-none", + "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4", { variants: { align: { "inline-start": - "pl-3 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem] order-first", + "order-first pl-3 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]", "inline-end": - "pr-3 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem] order-last", + "order-last pr-3 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]", "block-start": - "px-2.5 pt-3 group-has-[>input]/input-group:pt-2 [.border-b]:pb-3 order-first w-full justify-start", + "order-first w-full justify-start px-2.5 pt-3 group-has-[>input]/input-group:pt-2 [.border-b]:pb-3", "block-end": - "px-2.5 pb-3 group-has-[>input]/input-group:pb-2 [.border-t]:pt-3 order-last w-full justify-start" - } + "order-last w-full justify-start px-2.5 pb-3 group-has-[>input]/input-group:pb-2 [.border-t]:pt-3", + }, }, defaultVariants: { - align: "inline-start" - } + align: "inline-start", + }, } -); +) function InputGroupAddon({ className, @@ -57,17 +56,17 @@ function InputGroupAddon({ className={cn(inputGroupAddonVariants({ align }), className)} onClick={(e) => { if ((e.target as HTMLElement).closest("button")) { - return; + return } - e.currentTarget.parentElement?.querySelector("input")?.focus(); + e.currentTarget.parentElement?.querySelector("input")?.focus() }} {...props} /> - ); + ) } const inputGroupButtonVariants = cva( - "gap-2 text-sm shadow-none flex items-center", + "flex items-center gap-2 text-sm shadow-none", { variants: { size: { @@ -75,14 +74,14 @@ const inputGroupButtonVariants = cva( sm: "", "icon-xs": "size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0", - "icon-sm": "size-8 p-0 has-[>svg]:p-0" - } + "icon-sm": "size-8 p-0 has-[>svg]:p-0", + }, }, defaultVariants: { - size: "xs" - } + size: "xs", + }, } -); +) function InputGroupButton({ className, @@ -92,7 +91,7 @@ function InputGroupButton({ ...props }: Omit, "size" | "type"> & VariantProps & { - type?: "button" | "submit" | "reset"; + type?: "button" | "submit" | "reset" }) { return (