mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2026-04-17 10:28:46 -04:00
refactor: migrate from Biome to oxlint/oxfmt, remove contact form
- Replace Biome with oxlint + oxfmt (OXC toolchain) for linting and formatting - Add .oxlintrc.json and .oxfmtrc.json configuration files - Update VS Code settings and devcontainer to use oxc-vscode extension - Remove contact form, Resend email integration, and related server action/schema - Remove unused UI components (accordion, alert, card, tabs, toggle, etc.)
This commit is contained in:
@@ -5,7 +5,6 @@
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"editor.rulers": [120],
|
||||
"extensions.ignoreRecommendations": true,
|
||||
"git.allowForcePush": true,
|
||||
"git.autofetch": true,
|
||||
@@ -21,16 +20,9 @@
|
||||
"typescript.tsserver.log": "off",
|
||||
"typescript.updateImportsOnFileMove.enabled": "always"
|
||||
},
|
||||
"extensions": [
|
||||
"biomejs.biome",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"unifiedjs.vscode-mdx"
|
||||
]
|
||||
"extensions": ["bradlc.vscode-tailwindcss", "oxc.oxc-vscode", "unifiedjs.vscode-mdx"]
|
||||
}
|
||||
},
|
||||
"tasks": {
|
||||
"build": "pnpm install --frozen-lockfile && pnpm build"
|
||||
},
|
||||
"forwardPorts": [3000],
|
||||
"containerEnv": {
|
||||
"CHECKPOINT_DISABLE": "1",
|
||||
|
||||
@@ -3,9 +3,6 @@ AUTH_GITHUB_CLIENT_SECRET=
|
||||
AUTH_SECRET=
|
||||
DATABASE_URL=
|
||||
GITHUB_TOKEN=
|
||||
RESEND_API_KEY=
|
||||
RESEND_FROM_EMAIL=
|
||||
RESEND_TO_EMAIL=
|
||||
NEXT_PUBLIC_BASE_URL=
|
||||
NEXT_PUBLIC_GITHUB_REPO=
|
||||
NEXT_PUBLIC_ONION_DOMAIN=
|
||||
|
||||
7
.github/workflows/claude-code-review.yml
vendored
7
.github/workflows/claude-code-review.yml
vendored
@@ -36,9 +36,8 @@ jobs:
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
|
||||
plugins: 'code-review@claude-code-plugins'
|
||||
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
|
||||
plugin_marketplaces: "https://github.com/anthropics/claude-code.git"
|
||||
plugins: "code-review@claude-code-plugins"
|
||||
prompt: "/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}"
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
|
||||
|
||||
1
.github/workflows/claude.yml
vendored
1
.github/workflows/claude.yml
vendored
@@ -47,4 +47,3 @@ jobs:
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
||||
|
||||
|
||||
29
.oxfmtrc.json
Normal file
29
.oxfmtrc.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||
"sortImports": {
|
||||
"groups": [
|
||||
"builtin",
|
||||
"external",
|
||||
["internal", "subpath"],
|
||||
["parent", "sibling", "index"],
|
||||
"style",
|
||||
"unknown"
|
||||
],
|
||||
"internalPattern": ["@/"],
|
||||
"newlinesBetween": true,
|
||||
"order": "asc"
|
||||
},
|
||||
"sortTailwindcss": {
|
||||
"attributes": ["className"],
|
||||
"functions": ["cn", "clsx", "cva"]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.json", "**/*.jsonc"],
|
||||
"options": {
|
||||
"trailingComma": "none"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignorePatterns": ["node_modules", ".next", ".vercel", "drizzle"]
|
||||
}
|
||||
30
.oxlintrc.json
Normal file
30
.oxlintrc.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["oxc", "eslint", "typescript", "react", "import", "unicorn", "nextjs", "jsx-a11y"],
|
||||
"env": {
|
||||
"builtin": true
|
||||
},
|
||||
"categories": {
|
||||
"correctness": "error",
|
||||
"suspicious": "warn",
|
||||
"perf": "warn"
|
||||
},
|
||||
"rules": {
|
||||
"import/no-named-as-default-member": "off",
|
||||
"import/no-unassigned-import": "off",
|
||||
"jsx-a11y/anchor-has-content": "off",
|
||||
"jsx-a11y/iframe-has-title": "off",
|
||||
"jsx-a11y/media-has-caption": "off",
|
||||
"no-await-in-loop": "off",
|
||||
"no-new": "off",
|
||||
"oxc/no-barrel-file": ["warn", { "threshold": 0 }],
|
||||
"react/jsx-no-useless-fragment": "error",
|
||||
"react/no-array-index-key": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/rules-of-hooks": "error",
|
||||
"react/style-prop-object": "off",
|
||||
"unicorn/consistent-function-scoping": "off",
|
||||
"unicorn/no-array-sort": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules", ".next", ".vercel", "drizzle"]
|
||||
}
|
||||
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
@@ -1,7 +1,3 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"biomejs.biome",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"unifiedjs.vscode-mdx"
|
||||
]
|
||||
"recommendations": ["bradlc.vscode-tailwindcss", "oxc.oxc-vscode", "unifiedjs.vscode-mdx"]
|
||||
}
|
||||
|
||||
28
.vscode/settings.json
vendored
28
.vscode/settings.json
vendored
@@ -1,33 +1,41 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.biome": "explicit",
|
||||
"source.organizeImports.biome": "explicit"
|
||||
"source.fixAll.oxlint": "explicit"
|
||||
},
|
||||
"files.readonlyInclude": {
|
||||
"**/routeTree.gen.ts": true
|
||||
},
|
||||
"files.watcherExclude": {
|
||||
"**/routeTree.gen.ts": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/routeTree.gen.ts": true
|
||||
},
|
||||
"emmet.showExpandedAbbreviation": "never",
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { toNextJsHandler } from "better-auth/next-js";
|
||||
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
export const { POST, GET } = toNextJsHandler(auth);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { getAllViewCounts } from "@/lib/server/views";
|
||||
|
||||
export const GET = async (): Promise<
|
||||
@@ -18,9 +19,9 @@ export const GET = async (): Promise<
|
||||
const total = {
|
||||
hits: Object.values(views).reduce((acc, curr) => acc + curr, 0),
|
||||
};
|
||||
const pages = Object.entries(views).map(([slug, views]) => ({
|
||||
const pages = Object.entries(views).map(([slug, count]) => ({
|
||||
slug,
|
||||
hits: views,
|
||||
hits: count,
|
||||
}));
|
||||
|
||||
pages.sort((a, b) => b.hits - a.hits);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { JsonLd } from "react-schemaorg";
|
||||
import type { VideoObject } from "schema-dts";
|
||||
|
||||
import { PageTitle } from "@/components/layout/page-title";
|
||||
import { Video } from "@/components/video";
|
||||
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
|
||||
import thumbnail from "./thumbnail.png";
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { ContactForm } from "@/components/contact-form";
|
||||
import { PageTitle } from "@/components/layout/page-title";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Contact Me",
|
||||
description:
|
||||
"Fill out this quick form and I'll get back to you as soon as I can.",
|
||||
canonical: "/contact",
|
||||
});
|
||||
|
||||
const Page = () => (
|
||||
<>
|
||||
<PageTitle canonical="/contact">Contact</PageTitle>
|
||||
|
||||
<div className="mx-auto w-full max-w-2xl">
|
||||
<div className="prose prose-sm prose-neutral dark:prose-invert max-w-none">
|
||||
<p>
|
||||
Fill out this quick form and I’ll get back to you as soon as I
|
||||
can! You can also <a href="mailto:jake@jarv.is">email me directly</a>{" "}
|
||||
or send me a direct message on{" "}
|
||||
<a
|
||||
href="https://bsky.app/profile/jarv.is"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Bluesky
|
||||
</a>{" "}
|
||||
or{" "}
|
||||
<a
|
||||
href="https://fediverse.jarv.is/@jake"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Mastodon
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
You can grab my public key here:{" "}
|
||||
<a
|
||||
href="https://keyoxide.org/hkp/3bc6e5776bf379d36f6714802b0c9cf251e69a39"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
title="3BC6 E577 6BF3 79D3 6F67 1480 2B0C 9CF2 51E6 9A39"
|
||||
className="relative rounded-sm bg-muted px-[0.3rem] py-[0.2rem] font-medium font-mono text-sm tracking-wider [word-spacing:-0.25em]"
|
||||
>
|
||||
2B0C 9CF2 51E6 9A39
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ContactForm />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
export default Page;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { buildFeed } from "@/lib/build-feed";
|
||||
|
||||
export const GET = async () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { buildFeed } from "@/lib/build-feed";
|
||||
|
||||
export const GET = async () => {
|
||||
|
||||
182
app/globals.css
182
app/globals.css
@@ -1,6 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@plugin "@tailwindcss/typography";
|
||||
@import "shadcn/tailwind.css";
|
||||
@import "react-lite-youtube-embed/dist/LiteYouTubeEmbed.css";
|
||||
|
||||
@custom-variant dark (&:where(.dark *));
|
||||
@@ -61,6 +61,8 @@
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
@@ -78,11 +80,11 @@
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
@@ -99,12 +101,10 @@
|
||||
--code-number: oklch(0.56 0 0);
|
||||
--selection: oklch(0.145 0 0);
|
||||
--selection-foreground: oklch(1 0 0);
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--background: oklch(0 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
@@ -123,11 +123,11 @@
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
@@ -185,19 +185,171 @@
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
@apply outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
::selection {
|
||||
@apply bg-selection text-selection-foreground;
|
||||
}
|
||||
|
||||
/* https://ui.shadcn.com/docs/components/button#cursor */
|
||||
button:not(:disabled),
|
||||
[role="button"]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.prose {
|
||||
@apply text-foreground/90 max-w-none text-sm leading-7;
|
||||
|
||||
/* Headings */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply text-primary scroll-m-20 font-semibold tracking-tight;
|
||||
}
|
||||
h1 {
|
||||
@apply mt-8 mb-4 text-2xl leading-tight;
|
||||
}
|
||||
h2 {
|
||||
@apply mt-8 mb-4 border-b pb-2 text-xl leading-tight;
|
||||
}
|
||||
h3 {
|
||||
@apply mt-6 mb-3 text-lg leading-snug;
|
||||
}
|
||||
h4 {
|
||||
@apply mt-6 mb-2 text-base leading-snug;
|
||||
}
|
||||
h5 {
|
||||
@apply mt-4 mb-2 text-base leading-snug;
|
||||
}
|
||||
h6 {
|
||||
@apply mt-4 mb-2 text-sm leading-snug;
|
||||
}
|
||||
|
||||
/* Text blocks */
|
||||
p {
|
||||
@apply text-foreground/90 my-4;
|
||||
}
|
||||
strong {
|
||||
@apply text-primary font-semibold;
|
||||
}
|
||||
em {
|
||||
@apply italic;
|
||||
}
|
||||
|
||||
/* Separators */
|
||||
hr {
|
||||
@apply my-8 border-0 border-t;
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a {
|
||||
@apply text-primary font-medium underline underline-offset-4 transition-colors;
|
||||
}
|
||||
a:hover {
|
||||
@apply opacity-80;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
ul {
|
||||
@apply my-4 ml-6 list-disc;
|
||||
}
|
||||
ol {
|
||||
@apply my-4 ml-6 list-decimal;
|
||||
}
|
||||
li {
|
||||
@apply text-foreground/80;
|
||||
}
|
||||
li + li {
|
||||
@apply mt-1;
|
||||
}
|
||||
li > ul,
|
||||
li > ol {
|
||||
@apply my-2;
|
||||
}
|
||||
|
||||
/* Blockquotes */
|
||||
blockquote {
|
||||
@apply my-6 border-l-4 pl-4 italic;
|
||||
border-color: var(--border);
|
||||
}
|
||||
blockquote p {
|
||||
@apply text-foreground/75;
|
||||
}
|
||||
blockquote *::before,
|
||||
blockquote *::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code {
|
||||
@apply bg-muted text-foreground rounded-sm px-1 py-0.5 text-[0.9em];
|
||||
}
|
||||
:not(pre) > code::before,
|
||||
:not(pre) > code::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre {
|
||||
@apply bg-muted my-6 overflow-x-auto rounded-lg p-4 text-sm leading-6;
|
||||
}
|
||||
pre code {
|
||||
@apply bg-transparent p-0 text-inherit;
|
||||
}
|
||||
pre code::before,
|
||||
pre code::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
@apply my-6 w-full border-collapse text-sm;
|
||||
}
|
||||
thead {
|
||||
@apply border-b;
|
||||
}
|
||||
tr {
|
||||
@apply border-b;
|
||||
}
|
||||
th {
|
||||
@apply text-primary px-3 py-2 text-left align-middle font-semibold;
|
||||
}
|
||||
td {
|
||||
@apply text-foreground/90 px-3 py-2 align-middle;
|
||||
}
|
||||
|
||||
/* Images / media */
|
||||
img {
|
||||
@apply my-6 rounded-lg border;
|
||||
}
|
||||
video {
|
||||
@apply my-6 rounded-lg;
|
||||
}
|
||||
|
||||
/* Misc */
|
||||
kbd {
|
||||
@apply bg-muted rounded border px-1.5 py-0.5 text-xs;
|
||||
}
|
||||
mark {
|
||||
@apply bg-muted rounded px-1;
|
||||
}
|
||||
|
||||
/* First/last child spacing cleanup */
|
||||
> :first-child {
|
||||
@apply mt-0;
|
||||
}
|
||||
> :last-child {
|
||||
@apply mb-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* View Transitions - uses tw-animate-css's `enter` and `exit` keyframes */
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { JsonLd } from "react-schemaorg";
|
||||
import type { VideoObject } from "schema-dts";
|
||||
|
||||
import { PageTitle } from "@/components/layout/page-title";
|
||||
import { Video } from "@/components/video";
|
||||
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
|
||||
import thumbnail from "./thumbnail.png";
|
||||
@@ -50,7 +50,7 @@ const Page = () => (
|
||||
poster={thumbnail.src}
|
||||
/>
|
||||
|
||||
<p className="mx-4 mt-5 mb-0 text-center text-muted-foreground text-sm leading-relaxed">
|
||||
<p className="text-muted-foreground mx-4 mt-5 mb-0 text-center text-sm leading-relaxed">
|
||||
Video is property of{" "}
|
||||
<a
|
||||
href="https://www.hillaryclinton.com/"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ViewTransition } from "react";
|
||||
import { JsonLd } from "react-schemaorg";
|
||||
import type { Person, WebSite } from "schema-dts";
|
||||
|
||||
import { Analytics } from "@/app/analytics";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { Header } from "@/components/layout/header";
|
||||
@@ -8,18 +9,18 @@ import { Providers } from "@/components/providers";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import authorConfig from "@/lib/config/author";
|
||||
import siteConfig from "@/lib/config/site";
|
||||
|
||||
import { Inter, JetBrainsMono } from "@/lib/fonts";
|
||||
import { defaultMetadata } from "@/lib/metadata";
|
||||
|
||||
import "./globals.css";
|
||||
import { defaultMetadata } from "@/lib/metadata";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const metadata = defaultMetadata;
|
||||
|
||||
const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => (
|
||||
<html
|
||||
lang={process.env.NEXT_PUBLIC_SITE_LOCALE}
|
||||
className={`${Inter.variable} ${JetBrainsMono.variable}`}
|
||||
className={cn(Inter.variable, JetBrainsMono.variable)}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<head>
|
||||
@@ -29,11 +30,9 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => (
|
||||
"@type": "Person",
|
||||
"@id": `${process.env.NEXT_PUBLIC_BASE_URL}/#person`,
|
||||
name: authorConfig.name,
|
||||
// biome-ignore lint/style/noNonNullAssertion: expected to be set in env
|
||||
url: process.env.NEXT_PUBLIC_BASE_URL!,
|
||||
image: [`${process.env.NEXT_PUBLIC_BASE_URL}/opengraph-image.jpg`],
|
||||
sameAs: [
|
||||
// biome-ignore lint/style/noNonNullAssertion: expected to be set in env
|
||||
process.env.NEXT_PUBLIC_BASE_URL!,
|
||||
`https://${authorConfig.social?.mastodon}`,
|
||||
`https://github.com/${authorConfig.social?.github}`,
|
||||
@@ -62,19 +61,16 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => (
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body className="bg-background font-sans text-foreground antialiased">
|
||||
<body className="bg-background text-foreground font-sans antialiased">
|
||||
<Providers>
|
||||
<Header />
|
||||
<div className="mx-auto mt-4 w-full max-w-4xl px-5">
|
||||
<main>
|
||||
<ViewTransition>{children}</ViewTransition>
|
||||
</main>
|
||||
</div>
|
||||
<main className="mx-auto mt-4 w-full max-w-[720px] px-5">
|
||||
<ViewTransition>{children}</ViewTransition>
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
<Toaster position="bottom-center" hotkey={[]} />
|
||||
<Analytics />
|
||||
</Providers>
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { JsonLd } from "react-schemaorg";
|
||||
import type { VideoObject } from "schema-dts";
|
||||
|
||||
import { PageTitle } from "@/components/layout/page-title";
|
||||
import { Video } from "@/components/video";
|
||||
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
|
||||
import thumbnail from "./thumbnail.png";
|
||||
@@ -49,7 +49,7 @@ const Page = () => (
|
||||
poster={thumbnail.src}
|
||||
/>
|
||||
|
||||
<p className="mx-4 mt-5 mb-0 text-center text-muted-foreground text-sm leading-relaxed">
|
||||
<p className="text-muted-foreground mx-4 mt-5 mb-0 text-center text-sm leading-relaxed">
|
||||
Video is property of{" "}
|
||||
<a
|
||||
href="https://web.archive.org/web/20070511004304/www.g4techtv.ca"
|
||||
@@ -60,12 +60,7 @@ const Page = () => (
|
||||
G4techTV Canada
|
||||
</a>{" "}
|
||||
&{" "}
|
||||
<a
|
||||
href="https://leo.fm/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-bold"
|
||||
>
|
||||
<a href="https://leo.fm/" target="_blank" rel="noopener noreferrer" className="font-bold">
|
||||
Leo Laporte
|
||||
</a>
|
||||
. © 2007 G4 Media, Inc.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
import siteConfig from "@/lib/config/site";
|
||||
|
||||
const manifest = (): MetadataRoute.Manifest => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Video } from "@/components/video";
|
||||
|
||||
@@ -21,12 +22,15 @@ const Page = () => (
|
||||
/>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<h1 className="my-2 font-semibold text-2xl md:text-3xl">
|
||||
Page Not Found
|
||||
</h1>
|
||||
<h1 className="my-2 text-2xl font-semibold md:text-3xl">Page Not Found</h1>
|
||||
|
||||
<Button className="mt-4 mb-0 text-[15px] leading-none" size="lg" asChild>
|
||||
<Link href="/">Go home?</Link>
|
||||
<Button
|
||||
className="mt-4 mb-0 text-[15px] leading-none"
|
||||
size="lg"
|
||||
nativeButton={false}
|
||||
render={<Link href="/" />}
|
||||
>
|
||||
Go home?
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { notFound } from "next/navigation";
|
||||
import { ImageResponse } from "next/og";
|
||||
import siteConfig from "@/lib/config/site";
|
||||
|
||||
import siteConfig from "@/lib/config/site";
|
||||
import { loadGoogleFont } from "@/lib/og-utils";
|
||||
import { getFrontMatter, getSlugs, POSTS_DIR } from "@/lib/posts";
|
||||
|
||||
@@ -52,11 +53,7 @@ const getLocalImage = async (src: string): Promise<ArrayBuffer | string> => {
|
||||
}
|
||||
};
|
||||
|
||||
const OpenGraphImage = async ({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) => {
|
||||
const OpenGraphImage = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
try {
|
||||
const { slug } = await params;
|
||||
|
||||
@@ -66,9 +63,7 @@ const OpenGraphImage = async ({
|
||||
|
||||
// IMPORTANT: include these exact paths in next.config.ts under "outputFileTracingIncludes"
|
||||
const [postImg, avatarImg] = await Promise.all([
|
||||
frontmatter.image
|
||||
? getLocalImage(`${POSTS_DIR}/${slug}/${frontmatter.image}`)
|
||||
: null,
|
||||
frontmatter.image ? getLocalImage(`${POSTS_DIR}/${slug}/${frontmatter.image}`) : null,
|
||||
getLocalImage("app/avatar.jpg"),
|
||||
]);
|
||||
|
||||
@@ -139,7 +134,7 @@ const OpenGraphImage = async ({
|
||||
}}
|
||||
>
|
||||
{avatarImg && (
|
||||
// biome-ignore lint/performance/noImgElement: Satori/ImageResponse requires raw <img> tags
|
||||
// oxlint-disable-next-line nextjs/no-img-element - Satori/ImageResponse requires raw <img> tags
|
||||
<img
|
||||
// @ts-expect-error
|
||||
src={avatarImg}
|
||||
@@ -213,14 +208,11 @@ const OpenGraphImage = async ({
|
||||
lineHeight: "1.2",
|
||||
}}
|
||||
>
|
||||
{new Date(frontmatter.date).toLocaleDateString(
|
||||
process.env.NEXT_PUBLIC_SITE_LOCALE,
|
||||
{
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
},
|
||||
)}
|
||||
{new Date(frontmatter.date).toLocaleDateString(process.env.NEXT_PUBLIC_SITE_LOCALE, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -232,7 +224,7 @@ const OpenGraphImage = async ({
|
||||
flexGrow: 0,
|
||||
}}
|
||||
>
|
||||
{/* biome-ignore lint/performance/noImgElement: Satori/ImageResponse requires raw <img> tags */}
|
||||
{/* oxlint-disable-next-line nextjs/no-img-element - Satori/ImageResponse requires raw <img> tags */}
|
||||
<img
|
||||
// @ts-expect-error
|
||||
src={postImg}
|
||||
@@ -266,10 +258,7 @@ const OpenGraphImage = async ({
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[/notes/[slug]/opengraph-image] error generating open graph image:",
|
||||
error,
|
||||
);
|
||||
console.error("[/notes/[slug]/opengraph-image] error generating open graph image:", error);
|
||||
notFound();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,15 +11,16 @@ import { notFound } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
import { JsonLd } from "react-schemaorg";
|
||||
import type { BlogPosting } from "schema-dts";
|
||||
|
||||
import { CommentCount } from "@/components/comment-count";
|
||||
import { Comments } from "@/components/comments/comments";
|
||||
import { CommentsSkeleton } from "@/components/comments/comments-skeleton";
|
||||
import { ViewCounter } from "@/components/view-counter";
|
||||
import authorConfig from "@/lib/config/author";
|
||||
import siteConfig from "@/lib/config/site";
|
||||
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
import { getFrontMatter, getSlugs, POSTS_DIR } from "@/lib/posts";
|
||||
|
||||
import { size as ogImageSize } from "./opengraph-image";
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
@@ -79,9 +80,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
}),
|
||||
};
|
||||
|
||||
const { default: MDXContent } = await import(
|
||||
`../../../${POSTS_DIR}/${slug}/index.mdx`
|
||||
);
|
||||
const { default: MDXContent } = await import(`../../../${POSTS_DIR}/${slug}/index.mdx`);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -110,17 +109,14 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap justify-items-start gap-4 text-[13px] text-foreground/70 tracking-wide">
|
||||
<div className="text-foreground/70 flex flex-wrap justify-items-start gap-4 text-[13px] tracking-wide">
|
||||
<Link
|
||||
href={`/${POSTS_DIR}/${frontmatter?.slug}`}
|
||||
className={
|
||||
"flex flex-nowrap items-center gap-1.5 whitespace-nowrap text-foreground/70 hover:no-underline"
|
||||
"text-foreground/70 flex flex-nowrap items-center gap-1.5 whitespace-nowrap hover:no-underline"
|
||||
}
|
||||
>
|
||||
<CalendarDaysIcon
|
||||
className="inline size-3 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CalendarDaysIcon className="inline size-3 shrink-0" aria-hidden="true" />
|
||||
<time
|
||||
dateTime={formattedDates.dateISO}
|
||||
title={formattedDates.dateTitle}
|
||||
@@ -137,7 +133,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
<span
|
||||
key={tag}
|
||||
title={tag}
|
||||
className="mx-px lowercase before:pr-0.5 before:text-foreground/40 before:content-['\0023'] first-of-type:ml-0 last-of-type:mr-0"
|
||||
className="before:text-foreground/40 mx-px lowercase before:pr-0.5 before:content-['#'] first-of-type:ml-0 last-of-type:mr-0"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
@@ -149,24 +145,18 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
href={`https://github.com/${process.env.NEXT_PUBLIC_GITHUB_REPO}/blob/main/${POSTS_DIR}/${frontmatter?.slug}/index.mdx`}
|
||||
title={`Edit "${frontmatter?.title}" on GitHub`}
|
||||
className={
|
||||
"flex flex-nowrap items-center gap-1.5 whitespace-nowrap text-foreground/70 hover:no-underline"
|
||||
"text-foreground/70 flex flex-nowrap items-center gap-1.5 whitespace-nowrap hover:no-underline"
|
||||
}
|
||||
>
|
||||
<SquarePenIcon
|
||||
className="inline size-3 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<SquarePenIcon className="inline size-3 shrink-0" aria-hidden="true" />
|
||||
<span>Improve This Post</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href={`/${POSTS_DIR}/${frontmatter?.slug}#comments`}
|
||||
className="flex flex-nowrap items-center gap-1.5 whitespace-nowrap text-foreground/70 hover:no-underline"
|
||||
className="text-foreground/70 flex flex-nowrap items-center gap-1.5 whitespace-nowrap hover:no-underline"
|
||||
>
|
||||
<MessagesSquareIcon
|
||||
className="inline size-3 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<MessagesSquareIcon className="inline size-3 shrink-0" aria-hidden="true" />
|
||||
<CommentCount slug={`${POSTS_DIR}/${frontmatter?.slug}`} />
|
||||
</Link>
|
||||
|
||||
@@ -177,12 +167,11 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
</div>
|
||||
|
||||
<h1
|
||||
className="my-5 font-medium text-3xl tracking-tight"
|
||||
className="my-5 text-3xl font-medium tracking-tight"
|
||||
style={{ viewTransitionName: `note-title-${frontmatter?.slug}` }}
|
||||
>
|
||||
<Link
|
||||
href={`/${POSTS_DIR}/${frontmatter?.slug}`}
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: htmlTitle is sanitized by rehypeSanitize in lib/posts.ts
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: frontmatter.htmlTitle || frontmatter.title,
|
||||
}}
|
||||
@@ -195,10 +184,8 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
<section id="comments" className="isolate my-8 w-full border-t-2 pt-8">
|
||||
<div className="mx-auto w-full max-w-3xl space-y-6">
|
||||
{frontmatter?.noComments ? (
|
||||
<div className="flex justify-center rounded-lg bg-muted/40 px-6 py-12">
|
||||
<p className="text-center font-medium text-lg">
|
||||
Comments are closed.
|
||||
</p>
|
||||
<div className="bg-muted/40 flex justify-center rounded-lg px-6 py-12">
|
||||
<p className="text-center text-lg font-medium">Comments are closed.</p>
|
||||
</div>
|
||||
) : (
|
||||
<Suspense fallback={<CommentsSkeleton />}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { PageTitle } from "@/components/layout/page-title";
|
||||
import { PostStats, PostStatsProvider } from "@/components/post-stats";
|
||||
import authorConfig from "@/lib/config/author";
|
||||
@@ -16,8 +17,7 @@ const PostsList = async () => {
|
||||
|
||||
const formattedPosts = posts.map((post) => {
|
||||
const d = new Date(post.date);
|
||||
return {
|
||||
...post,
|
||||
return Object.assign(post, {
|
||||
year: d.getUTCFullYear(),
|
||||
dateISO: d.toISOString(),
|
||||
dateTitle: d.toLocaleString("en-US", {
|
||||
@@ -32,7 +32,7 @@ const PostsList = async () => {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const postsByYear: {
|
||||
@@ -53,49 +53,39 @@ const PostsList = async () => {
|
||||
|
||||
const sections: React.ReactNode[] = [];
|
||||
|
||||
Object.entries(postsByYear).forEach(([year, posts]) => {
|
||||
Object.entries(postsByYear).forEach(([year, yearPosts]) => {
|
||||
sections.push(
|
||||
<section className="my-8 first-of-type:mt-0 last-of-type:mb-0" key={year}>
|
||||
<h2
|
||||
id={year}
|
||||
className="mt-0 mb-4 font-semibold text-2xl tracking-tight"
|
||||
>
|
||||
<h2 id={year} className="mt-0 mb-4 text-2xl font-semibold tracking-tight">
|
||||
{year}
|
||||
</h2>
|
||||
<ul className="space-y-4">
|
||||
{posts.map(
|
||||
({ slug, dateISO, dateTitle, dateDisplay, title, htmlTitle }) => (
|
||||
<li className="flex text-base leading-relaxed" key={slug}>
|
||||
<span className="w-18 shrink-0 text-muted-foreground md:w-22">
|
||||
<time
|
||||
dateTime={dateISO}
|
||||
title={dateTitle}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{dateDisplay}
|
||||
</time>
|
||||
</span>
|
||||
<div className="space-x-2">
|
||||
<Link
|
||||
href={`/${POSTS_DIR}/${slug}`}
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: htmlTitle is sanitized by rehypeSanitize in lib/posts.ts
|
||||
dangerouslySetInnerHTML={{ __html: htmlTitle || title }}
|
||||
className="mr-2.5 underline-offset-4 hover:underline"
|
||||
style={{ viewTransitionName: `note-title-${slug}` }}
|
||||
/>
|
||||
{yearPosts.map(({ slug, dateISO, dateTitle, dateDisplay, title, htmlTitle }) => (
|
||||
<li className="flex text-base leading-relaxed" key={slug}>
|
||||
<span className="text-muted-foreground w-18 shrink-0 md:w-22">
|
||||
<time dateTime={dateISO} title={dateTitle} suppressHydrationWarning>
|
||||
{dateDisplay}
|
||||
</time>
|
||||
</span>
|
||||
<div className="space-x-2">
|
||||
<Link
|
||||
href={`/${POSTS_DIR}/${slug}`}
|
||||
dangerouslySetInnerHTML={{ __html: htmlTitle || title }}
|
||||
className="mr-2.5 underline-offset-4 hover:underline"
|
||||
style={{ viewTransitionName: `note-title-${slug}` }}
|
||||
/>
|
||||
|
||||
<PostStats slug={`${POSTS_DIR}/${slug}`} />
|
||||
</div>
|
||||
</li>
|
||||
),
|
||||
)}
|
||||
<PostStats slug={`${POSTS_DIR}/${slug}`} />
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>,
|
||||
);
|
||||
});
|
||||
|
||||
// grouped posts enter this component ordered chronologically -- we want reverse chronological
|
||||
return <>{sections.reverse()}</>;
|
||||
return <>{sections.toReversed()}</>;
|
||||
};
|
||||
|
||||
const Page = async () => (
|
||||
|
||||
129
app/page.tsx
129
app/page.tsx
@@ -1,21 +1,11 @@
|
||||
import { LockIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Page = () => (
|
||||
<div
|
||||
className={cn(
|
||||
"prose prose-neutral dark:prose-invert prose-sm max-w-none",
|
||||
"prose-headings:mt-0 prose-headings:mb-3 prose-headings:font-semibold prose-headings:text-primary prose-headings:tracking-tight",
|
||||
"prose-p:my-3 prose-li:text-foreground/80 prose-p:text-foreground/90 prose-strong:text-primary prose-p:leading-[1.75] md:prose-p:leading-relaxed",
|
||||
"prose-a:font-medium prose-a:text-primary prose-a:underline prose-a:underline-offset-4",
|
||||
"prose-code:rounded-sm prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:text-[0.9em] prose-code:text-foreground prose-code:before:content-none prose-code:after:content-none",
|
||||
"[&_table]:!border-[color:var(--border)] [&_td]:!border-[color:var(--border)] [&_th]:!border-[color:var(--border)]",
|
||||
)}
|
||||
>
|
||||
<h1 className="font-medium text-2xl">
|
||||
<div className="prose">
|
||||
<h1 className="text-2xl font-medium">
|
||||
Hi there! I’m Jake.{" "}
|
||||
<span className="ml-0.5 inline-block origin-[65%_80%] text-2xl motion-safe:animate-wave">
|
||||
<span className="motion-safe:animate-wave ml-0.5 inline-block origin-[65%_80%] text-2xl">
|
||||
👋
|
||||
</span>
|
||||
</h1>
|
||||
@@ -31,125 +21,26 @@ const Page = () => (
|
||||
area.
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
I specialize in using TypeScript, React, and Next.js to make lightweight
|
||||
frontends with dynamic and powerful backends.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Whenever possible, I also apply my experience in{" "}
|
||||
<a
|
||||
href="https://bugcrowd.com/jakejarvis"
|
||||
title="Jake Jarvis on Bugcrowd"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
information security
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a
|
||||
href="https://github.com/jakejarvis?tab=repositories&q=github-actions&type=&language=&sort=stargazers"
|
||||
title='My repositories tagged with "github-actions" on GitHub'
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
devops
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
I fell in love with{" "}
|
||||
<Link
|
||||
href="/previously"
|
||||
title="My Terrible, Horrible, No Good, Very Bad First Websites"
|
||||
>
|
||||
frontend web design
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link
|
||||
href="/notes/my-first-code"
|
||||
title="Jake's Bulletin Board, circa 2003"
|
||||
>
|
||||
backend coding
|
||||
</Link>{" "}
|
||||
when my only source of income was{" "}
|
||||
<Link
|
||||
href="/birthday"
|
||||
title="🎉 Cranky Birthday Boy on VHS Tape 📼"
|
||||
style={{
|
||||
cursor: `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='30' style='font-size:24px'><text y='50%' transform='rotate(-70 0 0) translate(-20, 6)'>🪄</text></svg>") 5 5, auto`,
|
||||
}}
|
||||
className="font-normal no-underline"
|
||||
>
|
||||
the Tooth Fairy
|
||||
</Link>
|
||||
.{" "}
|
||||
<span className="text-muted-foreground">
|
||||
(I’ve improved a bit since then, I think?)
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
I’m currently building{" "}
|
||||
<a
|
||||
href="https://domainstack.io"
|
||||
title="Domainstack: Domain intelligence made easy"
|
||||
className="font-medium"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Domainstack
|
||||
</a>
|
||||
, a beautiful all-in-one domain name intelligence tool, and{" "}
|
||||
<a
|
||||
href="https://snoozle.ai"
|
||||
title="Snoozle: AI-powered bedtime stories for children"
|
||||
className="font-medium"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Snoozle
|
||||
</a>
|
||||
, an AI-powered bedtime story generator.
|
||||
</p>
|
||||
|
||||
<p className="mt-2 mb-0 text-sm leading-normal">
|
||||
You can find my work on{" "}
|
||||
<a
|
||||
href="https://github.com/jakejarvis"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer me"
|
||||
>
|
||||
GitHub
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a
|
||||
href="https://www.linkedin.com/in/jakejarvis/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer me"
|
||||
>
|
||||
LinkedIn
|
||||
</a>
|
||||
. I’m always available to connect over{" "}
|
||||
<Link href="/contact" title="Send an email">
|
||||
I’m always available to connect over{" "}
|
||||
<a href="mailto:jake@jarv.is" title="Send an email">
|
||||
email
|
||||
</Link>{" "}
|
||||
</a>{" "}
|
||||
<sup className="">
|
||||
<a
|
||||
href="https://keyoxide.org/hkp/3bc6e5776bf379d36f6714802b0c9cf251e69a39"
|
||||
target="_blank"
|
||||
rel="noopener pgpkey"
|
||||
title="Download my PGP key"
|
||||
className="not-prose space-x-1 text-nowrap px-0.5 text-muted-foreground no-underline hover:text-primary hover:no-underline"
|
||||
className="not-prose text-muted-foreground hover:text-primary space-x-1 px-0.5 text-nowrap no-underline hover:no-underline"
|
||||
>
|
||||
<LockIcon className="inline size-2.5" aria-hidden="true" />
|
||||
<code className="text-wrap text-[9px] leading-none tracking-wider [word-spacing:-3px]">
|
||||
<code className="text-[9px] leading-none tracking-wider text-wrap [word-spacing:-3px]">
|
||||
2B0C 9CF2 51E6 9A39
|
||||
</code>
|
||||
</a>
|
||||
</sup>{" "}
|
||||
as well.
|
||||
</sup>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { ComicNeue } from "@/lib/fonts";
|
||||
|
||||
export const PageStyles = () => {
|
||||
|
||||
@@ -6,7 +6,8 @@ import { PageStyles } from "./page-styles";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Previously on...",
|
||||
description: "An incredibly embarrassing and somewhat painful trip down this site's memory lane...",
|
||||
description:
|
||||
"An incredibly embarrassing and somewhat painful trip down this site's memory lane...",
|
||||
canonical: "/previously",
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import "server-only";
|
||||
|
||||
import { graphql } from "@octokit/graphql";
|
||||
import type { Repository, User } from "@octokit/graphql-schema";
|
||||
import * as cheerio from "cheerio";
|
||||
@@ -40,7 +39,6 @@ export const getContributions = async (): Promise<
|
||||
|
||||
const dayTooltips = $(".js-calendar-graph tool-tip")
|
||||
.toArray()
|
||||
// biome-ignore lint/suspicious/noExplicitAny: cheerio DOM element map
|
||||
.reduce<Record<string, any>>((map, elem) => {
|
||||
map[elem.attribs.for] = elem;
|
||||
return map;
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { ExternalLinkIcon, GitForkIcon, StarIcon } from "lucide-react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { ActivityCalendar } from "@/components/activity-calendar";
|
||||
import { PageTitle } from "@/components/layout/page-title";
|
||||
import { RelativeTime } from "@/components/relative-time";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { getContributions, getRepos } from "./github";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
@@ -21,23 +22,18 @@ const Page = async () => {
|
||||
// don't fail the entire site build if the required config for this page is missing, just return a 404 since this page
|
||||
// would be mostly blank anyways.
|
||||
if (!process.env.GITHUB_TOKEN) {
|
||||
console.error(
|
||||
"[/projects] I can't fetch anything from GitHub without 'GITHUB_TOKEN' set!",
|
||||
);
|
||||
console.error("[/projects] I can't fetch anything from GitHub without 'GITHUB_TOKEN' set!");
|
||||
notFound();
|
||||
}
|
||||
|
||||
// fetch the repos and contributions in parallel
|
||||
const [contributions, repos] = await Promise.all([
|
||||
getContributions(),
|
||||
getRepos(),
|
||||
]);
|
||||
const [contributions, repos] = await Promise.all([getContributions(), getRepos()]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle canonical="/projects">Projects</PageTitle>
|
||||
|
||||
<h2 className="my-3.5 font-medium text-xl">
|
||||
<h2 className="my-3.5 text-xl font-medium">
|
||||
<a
|
||||
href={`https://github.com/${process.env.NEXT_PUBLIC_GITHUB_USERNAME}`}
|
||||
target="_blank"
|
||||
@@ -54,13 +50,13 @@ const Page = async () => {
|
||||
<ActivityCalendar data={contributions} noun="contribution" />
|
||||
</div>
|
||||
) : (
|
||||
<p className="my-4 text-center text-muted-foreground">
|
||||
<p className="text-muted-foreground my-4 text-center">
|
||||
Unable to load contribution data at this time.
|
||||
</p>
|
||||
)}
|
||||
</Suspense>
|
||||
|
||||
<h2 className="my-3.5 font-medium text-xl">
|
||||
<h2 className="my-3.5 text-xl font-medium">
|
||||
<a
|
||||
href={`https://github.com/${process.env.NEXT_PUBLIC_GITHUB_USERNAME}?tab=repositories&sort=stargazers`}
|
||||
target="_blank"
|
||||
@@ -76,32 +72,31 @@ const Page = async () => {
|
||||
{repos.map((repo) => (
|
||||
<div
|
||||
key={repo?.name}
|
||||
className="h-fit space-y-1.5 rounded-2xl border-1 border-ring/30 px-4 py-3 shadow-xs"
|
||||
className="border-ring/30 h-fit space-y-1.5 rounded-2xl border-1 px-4 py-3 shadow-xs"
|
||||
>
|
||||
<a
|
||||
href={repo?.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-block font-semibold text-[#0969da] text-base leading-relaxed hover:underline dark:text-[#76affa]"
|
||||
className="inline-block text-base leading-relaxed font-semibold text-[#0969da] hover:underline dark:text-[#76affa]"
|
||||
>
|
||||
{repo?.name}
|
||||
</a>
|
||||
|
||||
{repo?.description && (
|
||||
<p className="text-[13px] text-foreground/85 leading-relaxed">
|
||||
<p className="text-foreground/85 text-[13px] leading-relaxed">
|
||||
{repo?.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-x-4 whitespace-nowrap text-xs leading-loose">
|
||||
<div className="flex flex-wrap gap-x-4 text-xs leading-loose whitespace-nowrap">
|
||||
{repo?.primaryLanguage && (
|
||||
<div className="inline-flex flex-nowrap items-center gap-1.5 text-muted-foreground">
|
||||
<div className="text-muted-foreground inline-flex flex-nowrap items-center gap-1.5">
|
||||
{repo?.primaryLanguage.color && (
|
||||
<span
|
||||
className="inline-block size-3 rounded-full bg-[var(--language-color)]"
|
||||
style={{
|
||||
["--language-color" as string]:
|
||||
repo?.primaryLanguage.color,
|
||||
["--language-color" as string]: repo?.primaryLanguage.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -115,16 +110,13 @@ const Page = async () => {
|
||||
title={`${Intl.NumberFormat(process.env.NEXT_PUBLIC_SITE_LOCALE).format(repo?.stargazerCount)} ${repo?.stargazerCount === 1 ? "star" : "stars"}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex flex-nowrap items-center gap-1.5 text-muted-foreground hover:text-primary hover:no-underline"
|
||||
className="text-muted-foreground hover:text-primary inline-flex flex-nowrap items-center gap-1.5 hover:no-underline"
|
||||
>
|
||||
<StarIcon
|
||||
className="inline-block size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<StarIcon className="inline-block size-3.5 shrink-0" aria-hidden="true" />
|
||||
<span>
|
||||
{Intl.NumberFormat(
|
||||
process.env.NEXT_PUBLIC_SITE_LOCALE,
|
||||
).format(repo?.stargazerCount)}
|
||||
{Intl.NumberFormat(process.env.NEXT_PUBLIC_SITE_LOCALE).format(
|
||||
repo?.stargazerCount,
|
||||
)}
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
@@ -135,21 +127,18 @@ const Page = async () => {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={`${Intl.NumberFormat(process.env.NEXT_PUBLIC_SITE_LOCALE).format(repo?.forkCount)} ${repo?.forkCount === 1 ? "fork" : "forks"}`}
|
||||
className="inline-flex flex-nowrap items-center gap-1.5 text-muted-foreground hover:text-primary hover:no-underline"
|
||||
className="text-muted-foreground hover:text-primary inline-flex flex-nowrap items-center gap-1.5 hover:no-underline"
|
||||
>
|
||||
<GitForkIcon
|
||||
className="inline-block size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<GitForkIcon className="inline-block size-3.5 shrink-0" aria-hidden="true" />
|
||||
<span>
|
||||
{Intl.NumberFormat(
|
||||
process.env.NEXT_PUBLIC_SITE_LOCALE,
|
||||
).format(repo?.forkCount)}
|
||||
{Intl.NumberFormat(process.env.NEXT_PUBLIC_SITE_LOCALE).format(
|
||||
repo?.forkCount,
|
||||
)}
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
|
||||
<div className="whitespace-nowrap text-muted-foreground">
|
||||
<div className="text-muted-foreground whitespace-nowrap">
|
||||
<Suspense fallback={null}>
|
||||
<span>
|
||||
Updated <RelativeTime date={repo?.pushedAt} />
|
||||
@@ -161,24 +150,25 @@ const Page = async () => {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="my-4 text-center text-muted-foreground">
|
||||
<p className="text-muted-foreground my-4 text-center">
|
||||
Unable to load repository data at this time.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="mt-6 mb-0 text-center font-medium text-base">
|
||||
<Button variant="link" asChild>
|
||||
<a
|
||||
href={`https://github.com/${process.env.NEXT_PUBLIC_GITHUB_USERNAME}?tab=repositories&type=source&sort=stargazers`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View all
|
||||
<ExternalLinkIcon
|
||||
className="inline-block size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
<p className="mt-6 mb-0 text-center text-base font-medium">
|
||||
<Button
|
||||
variant="link"
|
||||
nativeButton={false}
|
||||
render={
|
||||
<a
|
||||
href={`https://github.com/${process.env.NEXT_PUBLIC_GITHUB_USERNAME}?tab=repositories&type=source&sort=stargazers`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
</a>
|
||||
}
|
||||
>
|
||||
View all
|
||||
<ExternalLinkIcon className="inline-block size-3.5 shrink-0" aria-hidden="true" />
|
||||
</Button>
|
||||
</p>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from "node:path";
|
||||
|
||||
import glob from "fast-glob";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
@@ -19,7 +20,6 @@ const sitemap = async (): Promise<MetadataRoute.Sitemap> => {
|
||||
const routes: MetadataRoute.Sitemap = [
|
||||
{
|
||||
// homepage
|
||||
// biome-ignore lint/style/noNonNullAssertion: expected to be set in env
|
||||
url: process.env.NEXT_PUBLIC_BASE_URL!,
|
||||
priority: 1.0,
|
||||
lastModified: new Date(),
|
||||
|
||||
@@ -17,34 +17,46 @@ export const Terminal = () => (
|
||||
}}
|
||||
>
|
||||
<code className="border-ring block rounded-lg border border-solid bg-black/60 p-4 text-sm break-all text-white/90 backdrop-blur-sm backdrop-saturate-150">
|
||||
<span style={{ color: "#f95757" }}>sundar</span>@<span style={{ color: "#3b9dd2" }}>google</span>:
|
||||
<span style={{ color: "#78df55" }}>~</span>$ <span style={{ color: "#d588fb" }}>mv</span> /root
|
||||
<a href="https://killedbygoogle.com/" style={{ color: "inherit" }} className="hover:no-underline">
|
||||
<span style={{ color: "#f95757" }}>sundar</span>@
|
||||
<span style={{ color: "#3b9dd2" }}>google</span>:<span style={{ color: "#78df55" }}>~</span>${" "}
|
||||
<span style={{ color: "#d588fb" }}>mv</span> /root
|
||||
<a
|
||||
href="https://killedbygoogle.com/"
|
||||
style={{ color: "inherit" }}
|
||||
className="hover:no-underline"
|
||||
>
|
||||
/stable_products_that_people_rely_on/
|
||||
</a>
|
||||
googledomains.zip /tmp/
|
||||
<br />
|
||||
<span style={{ color: "#f95757" }}>sundar</span>@<span style={{ color: "#3b9dd2" }}>google</span>:
|
||||
<span style={{ color: "#78df55" }}>~</span>$ <span style={{ color: "#d588fb" }}>crontab</span>{" "}
|
||||
<span style={{ color: "#fd992a" }}>-l</span>
|
||||
<span style={{ color: "#f95757" }}>sundar</span>@
|
||||
<span style={{ color: "#3b9dd2" }}>google</span>:<span style={{ color: "#78df55" }}>~</span>${" "}
|
||||
<span style={{ color: "#d588fb" }}>crontab</span> <span style={{ color: "#fd992a" }}>-l</span>
|
||||
<br />
|
||||
<br />
|
||||
<span style={{ color: "#929292" }}>
|
||||
# TODO(someone else): make super duper sure this only deletes actual zip files and *NOT* the sketchy domains
|
||||
ending with file extensions released by us & purchased on our registrar (which i just yeeted btw cuz i'm &
|
||||
also my evil superpowers are fueled by my reckless disregard for the greater good of the internet). - xoxo
|
||||
sundar <span style={{ color: "#f95757" }}><3</span>
|
||||
# TODO(someone else): make super duper sure this only deletes actual zip files and *NOT* the
|
||||
sketchy domains ending with file extensions released by us & purchased on our registrar
|
||||
(which i just yeeted btw cuz i'm & also my evil superpowers are fueled by my reckless
|
||||
disregard for the greater good of the internet). - xoxo sundar{" "}
|
||||
<span style={{ color: "#f95757" }}><3</span>
|
||||
</span>
|
||||
<br />
|
||||
<span style={{ color: "#78df55" }}>@monthly</span>
|
||||
<span style={{ color: "#d588fb" }}>rm</span> <span style={{ color: "#fd992a" }}>-f</span> /tmp/
|
||||
<a href="https://fuckyougoogle.zip/" style={{ color: "inherit" }} className="hover:no-underline">
|
||||
<span style={{ color: "#d588fb" }}>rm</span> <span style={{ color: "#fd992a" }}>-f</span>{" "}
|
||||
/tmp/
|
||||
<a
|
||||
href="https://fuckyougoogle.zip/"
|
||||
style={{ color: "inherit" }}
|
||||
className="hover:no-underline"
|
||||
>
|
||||
*.zip
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
<span style={{ color: "#f95757" }}>sundar</span>@<span style={{ color: "#3b9dd2" }}>google</span>:
|
||||
<span style={{ color: "#78df55" }}>~</span>$ <span style={{ color: "#d588fb" }}>reboot</span> 0
|
||||
<span style={{ color: "#f95757" }}>sundar</span>@
|
||||
<span style={{ color: "#3b9dd2" }}>google</span>:<span style={{ color: "#78df55" }}>~</span>${" "}
|
||||
<span style={{ color: "#d588fb" }}>reboot</span> 0
|
||||
</code>
|
||||
</div>
|
||||
);
|
||||
|
||||
81
biome.json
81
biome.json
@@ -1,81 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": true,
|
||||
"includes": [
|
||||
"**/*.js",
|
||||
"**/*.jsx",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.mts",
|
||||
"**/*.mjs",
|
||||
"**/*.json",
|
||||
"!**/node_modules",
|
||||
"!**/.next",
|
||||
"!**/.swc",
|
||||
"!**/.vercel",
|
||||
"!**/drizzle",
|
||||
"!**/public"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 80
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"domains": {
|
||||
"next": "recommended",
|
||||
"react": "recommended"
|
||||
},
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"correctness": {
|
||||
"useExhaustiveDependencies": "warn"
|
||||
},
|
||||
"suspicious": {
|
||||
"noUnknownAtRules": "off"
|
||||
},
|
||||
"a11y": {
|
||||
"noSvgWithoutTitle": "off",
|
||||
"useAnchorContent": "off",
|
||||
"useSemanticElements": "off"
|
||||
},
|
||||
"style": {
|
||||
"useConsistentArrowReturn": "error"
|
||||
},
|
||||
"nursery": {
|
||||
"useSortedClasses": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"attributes": ["classList"],
|
||||
"functions": ["cn", "clsx", "cva"]
|
||||
}
|
||||
},
|
||||
"noFloatingPromises": "error",
|
||||
"noMisusedPromises": "error",
|
||||
"useDestructuring": "error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": "on"
|
||||
}
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"parser": {
|
||||
"tailwindDirectives": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"style": "base-nova",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
@@ -11,6 +11,7 @@
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"rtl": false,
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
@@ -18,5 +19,7 @@
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle",
|
||||
"registries": {}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,8 @@ import {
|
||||
type Activity,
|
||||
ActivityCalendar as ActivityCalendarPrimitive,
|
||||
} from "react-activity-calendar";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ActivityCalendar = ({
|
||||
@@ -50,9 +47,9 @@ const ActivityCalendar = ({
|
||||
maxLevel={4}
|
||||
renderBlock={(block, activity) => (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{block}</TooltipTrigger>
|
||||
<TooltipTrigger render={block} />
|
||||
<TooltipContent>
|
||||
<span className="font-medium text-[0.825rem]">{`${activity.count === 0 ? "No" : activity.count} ${noun}${activity.count === 1 ? "" : "s"} on ${new Date(activity.date).toLocaleDateString("en-US", { month: "short", day: "numeric" })}`}</span>
|
||||
<span className="text-[0.825rem] font-medium">{`${activity.count === 0 ? "No" : activity.count} ${noun}${activity.count === 1 ? "" : "s"} on ${new Date(activity.date).toLocaleDateString("en-US", { month: "short", day: "numeric" })}`}</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { cacheLife } from "next/cache";
|
||||
import { codeToHtml } from "shiki";
|
||||
|
||||
import { CopyButton } from "@/components/copy-button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -13,8 +14,7 @@ const getTextContent = (node: React.ReactNode): string => {
|
||||
if (Array.isArray(node)) return node.map(getTextContent).join("");
|
||||
if (typeof node === "object" && "props" in node) {
|
||||
return getTextContent(
|
||||
(node as React.ReactElement<{ children?: React.ReactNode }>).props
|
||||
.children,
|
||||
(node as React.ReactElement<{ children?: React.ReactNode }>).props.children,
|
||||
);
|
||||
}
|
||||
return "";
|
||||
@@ -66,15 +66,14 @@ const CodeBlock = async ({
|
||||
data-lang={lang}
|
||||
data-line-numbers={showLineNumbers || undefined}
|
||||
className={cn(
|
||||
"overflow-x-auto overflow-y-hidden rounded-xl bg-code text-[13px] text-code-foreground leading-normal outline-none",
|
||||
"bg-code text-code-foreground overflow-x-auto overflow-y-hidden rounded-xl text-[13px] leading-normal outline-none",
|
||||
"[&_span]:!bg-transparent [&_span[style*='color']]:dark:!text-(--shiki-dark)",
|
||||
"[&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:rounded-xl",
|
||||
"[&_pre]:m-0 [&_pre]:rounded-xl [&_pre]:!bg-transparent",
|
||||
"[&_code]:white-space-pre [&_code]:grid [&_code]:min-w-full [&_code]:px-4 [&_code]:py-3.5 [&_code]:[counter-reset:line]",
|
||||
"[&_.line]:inline-block [&_.line]:min-h-1lh [&_.line]:w-full [&_.line]:py-0.5",
|
||||
"data-[line-numbers]:[&_.line]:before:mr-6 data-[line-numbers]:[&_.line]:before:inline-block data-[line-numbers]:[&_.line]:before:w-5 data-[line-numbers]:[&_.line]:before:text-right data-[line-numbers]:[&_.line]:before:text-code-number data-[line-numbers]:[&_.line]:before:content-[counter(line)] data-[line-numbers]:[&_.line]:before:[counter-increment:line]",
|
||||
"[&_.line]:min-h-1lh [&_.line]:inline-block [&_.line]:w-full [&_.line]:py-0.5",
|
||||
"data-[line-numbers]:[&_.line]:before:text-code-number data-[line-numbers]:[&_.line]:before:mr-6 data-[line-numbers]:[&_.line]:before:inline-block data-[line-numbers]:[&_.line]:before:w-5 data-[line-numbers]:[&_.line]:before:text-right data-[line-numbers]:[&_.line]:before:content-[counter(line)] data-[line-numbers]:[&_.line]:before:[counter-increment:line]",
|
||||
className,
|
||||
)}
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: trusted Shiki-generated syntax-highlighted HTML
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
EditIcon,
|
||||
EllipsisIcon,
|
||||
Loader2Icon,
|
||||
ReplyIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react";
|
||||
import { EditIcon, EllipsisIcon, Loader2Icon, ReplyIcon, Trash2Icon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -28,6 +23,7 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useSession } from "@/lib/auth-client";
|
||||
import { type CommentWithUser, deleteComment } from "@/lib/server/comments";
|
||||
|
||||
import { EditCommentForm, ReplyForm } from "./comment-form";
|
||||
|
||||
type ActionMode =
|
||||
@@ -76,11 +72,7 @@ const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setMode(
|
||||
mode.type === "replying"
|
||||
? { type: "idle" }
|
||||
: { type: "replying" },
|
||||
)
|
||||
setMode(mode.type === "replying" ? { type: "idle" } : { type: "replying" })
|
||||
}
|
||||
>
|
||||
<ReplyIcon />
|
||||
@@ -89,11 +81,9 @@ const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
|
||||
|
||||
{session.user.id === comment.user.id && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<EllipsisIcon />
|
||||
<span className="sr-only">Actions Menu</span>
|
||||
</Button>
|
||||
<DropdownMenuTrigger render={<Button variant="outline" size="sm" />}>
|
||||
<EllipsisIcon />
|
||||
<span className="sr-only">Actions Menu</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={() => setMode({ type: "editing" })}>
|
||||
@@ -105,11 +95,7 @@ const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
|
||||
disabled={isDeleting}
|
||||
variant="destructive"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<Loader2Icon className="animate-spin" />
|
||||
) : (
|
||||
<Trash2Icon />
|
||||
)}
|
||||
{isDeleting ? <Loader2Icon className="animate-spin" /> : <Trash2Icon />}
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@@ -136,9 +122,7 @@ const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
|
||||
<AlertDialogContent size="sm">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete comment?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getImageProps } from "next/image";
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { InfoIcon, Loader2Icon } from "lucide-react";
|
||||
import { createContext, useContext, useState, useTransition } from "react";
|
||||
import { createContext, useContext, useMemo, useState, useTransition } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { MarkdownIcon } from "@/components/icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useSession } from "@/lib/auth-client";
|
||||
import { createComment, updateComment } from "@/lib/server/comments";
|
||||
|
||||
import { CommentAvatar } from "./comment-avatar";
|
||||
|
||||
// Context for lifting form state to parent components
|
||||
@@ -36,13 +34,12 @@ const CommentFormProvider = ({
|
||||
const [content, setContent] = useState(initialContent);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
return (
|
||||
<CommentFormContext.Provider
|
||||
value={{ content, setContent, isPending, startTransition }}
|
||||
>
|
||||
{children}
|
||||
</CommentFormContext.Provider>
|
||||
const contextValue = useMemo(
|
||||
() => ({ content, setContent, isPending, startTransition }),
|
||||
[content, setContent, isPending, startTransition],
|
||||
);
|
||||
|
||||
return <CommentFormContext.Provider value={contextValue}>{children}</CommentFormContext.Provider>;
|
||||
};
|
||||
|
||||
// Hook to access form state from context (for sibling components like preview panels)
|
||||
@@ -136,18 +133,20 @@ const SubmitButton = ({
|
||||
|
||||
// Markdown help popover (only shown for new comments)
|
||||
const MarkdownHelp = () => (
|
||||
<p className="text-[0.8rem] text-muted-foreground leading-relaxed">
|
||||
<p className="text-muted-foreground text-[0.8rem] leading-relaxed">
|
||||
<MarkdownIcon className="mr-1.5 inline-block size-4 align-text-top" />
|
||||
<span className="max-md:hidden">Basic </span>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer font-semibold text-primary no-underline decoration-2 decoration-primary/40 underline-offset-4 hover:underline"
|
||||
>
|
||||
<span>Markdown</span>
|
||||
<span className="max-md:hidden"> syntax</span>
|
||||
</button>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<button
|
||||
type="button"
|
||||
className="text-primary decoration-primary/40 cursor-pointer font-semibold no-underline decoration-2 underline-offset-4 hover:underline"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<span>Markdown</span>
|
||||
<span className="max-md:hidden"> syntax</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start">
|
||||
<p className="text-sm leading-loose">
|
||||
@@ -155,7 +154,7 @@ const MarkdownHelp = () => (
|
||||
Examples:
|
||||
</p>
|
||||
|
||||
<ul className="my-2 list-inside list-disc pl-1 text-sm [&>li::marker]:font-normal [&>li::marker]:text-muted-foreground [&>li]:my-1.5 [&>li]:text-nowrap [&>li]:pl-1">
|
||||
<ul className="[&>li::marker]:text-muted-foreground my-2 list-inside list-disc pl-1 text-sm [&>li]:my-1.5 [&>li]:pl-1 [&>li]:text-nowrap [&>li::marker]:font-normal">
|
||||
<li>
|
||||
<span className="font-bold">**bold**</span>
|
||||
</li>
|
||||
@@ -164,18 +163,13 @@ const MarkdownHelp = () => (
|
||||
</li>
|
||||
<li>
|
||||
[
|
||||
<a
|
||||
href="https://jarv.is"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="hover:no-underline"
|
||||
>
|
||||
<a href="https://jarv.is" target="_blank" rel="noopener" className="hover:no-underline">
|
||||
links
|
||||
</a>
|
||||
](https://jarv.is)
|
||||
</li>
|
||||
<li>
|
||||
<span className="rounded-sm bg-muted px-[0.3rem] py-[0.2rem] font-medium font-mono text-sm">
|
||||
<span className="bg-muted rounded-sm px-[0.3rem] py-[0.2rem] font-mono text-sm font-medium">
|
||||
`code`
|
||||
</span>
|
||||
</li>
|
||||
@@ -203,8 +197,7 @@ const MarkdownHelp = () => (
|
||||
|
||||
// New comment form - for creating top-level comments
|
||||
const NewCommentForm = ({ slug }: { slug: string }) => {
|
||||
const { content, setContent, isPending, startTransition } =
|
||||
useCommentFormState();
|
||||
const { content, setContent, isPending, startTransition } = useCommentFormState();
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
@@ -269,8 +262,7 @@ const ReplyForm = ({
|
||||
onCancel: () => void;
|
||||
onSuccess?: () => void;
|
||||
}) => {
|
||||
const { content, setContent, isPending, startTransition } =
|
||||
useCommentFormState();
|
||||
const { content, setContent, isPending, startTransition } = useCommentFormState();
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
@@ -308,12 +300,7 @@ const ReplyForm = ({
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={isPending}
|
||||
>
|
||||
<Button type="button" variant="outline" onClick={onCancel} disabled={isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -345,8 +332,7 @@ const EditCommentForm = ({
|
||||
onCancel: () => void;
|
||||
onSuccess?: () => void;
|
||||
}) => {
|
||||
const { content, setContent, isPending, startTransition } =
|
||||
useCommentFormState(initialContent);
|
||||
const { content, setContent, isPending, startTransition } = useCommentFormState(initialContent);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
@@ -369,12 +355,7 @@ const EditCommentForm = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="space-y-4"
|
||||
data-intent="edit"
|
||||
data-slug={slug}
|
||||
>
|
||||
<form onSubmit={handleSubmit} className="space-y-4" data-intent="edit" data-slug={slug}>
|
||||
<div className="min-w-0 flex-1 space-y-4">
|
||||
<CommentTextarea
|
||||
content={content}
|
||||
@@ -385,20 +366,11 @@ const EditCommentForm = ({
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={isPending}
|
||||
>
|
||||
<Button type="button" variant="outline" onClick={onCancel} disabled={isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<SubmitButton
|
||||
isPending={isPending}
|
||||
disabled={!content.trim()}
|
||||
pendingLabel="Updating..."
|
||||
>
|
||||
<SubmitButton isPending={isPending} disabled={!content.trim()} pendingLabel="Updating...">
|
||||
Edit
|
||||
</SubmitButton>
|
||||
</div>
|
||||
@@ -407,10 +379,4 @@ const EditCommentForm = ({
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
NewCommentForm,
|
||||
ReplyForm,
|
||||
EditCommentForm,
|
||||
CommentFormProvider,
|
||||
useCommentForm,
|
||||
};
|
||||
export { NewCommentForm, ReplyForm, EditCommentForm, CommentFormProvider, useCommentForm };
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import Link from "next/link";
|
||||
import Markdown from "react-markdown";
|
||||
|
||||
import { RelativeTime } from "@/components/relative-time";
|
||||
import { rehypeExternalLinks } from "@/lib/rehype";
|
||||
import { remarkGfm, remarkSmartypants } from "@/lib/remark";
|
||||
import type { CommentWithUser } from "@/lib/server/comments";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { CommentActions } from "./comment-actions";
|
||||
import { CommentAvatar } from "./comment-avatar";
|
||||
|
||||
@@ -44,29 +46,17 @@ const CommentSingle = ({ comment }: { comment: CommentWithUser }) => {
|
||||
className={cn(
|
||||
"isolate max-w-none text-[0.875rem] leading-relaxed",
|
||||
"[&_p]:my-5 [&_p]:first:mt-0 [&_p]:last:mb-0",
|
||||
"[&_a]:text-primary [&_a]:no-underline [&_a]:decoration-2 [&_a]:decoration-primary/40 [&_a]:underline-offset-4 [&_a]:hover:underline",
|
||||
"[&_code]:rounded-sm [&_code]:bg-muted [&_code]:px-[0.3rem] [&_code]:py-[0.2rem] [&_code]:font-medium",
|
||||
"[&_a]:text-primary [&_a]:decoration-primary/40 [&_a]:no-underline [&_a]:decoration-2 [&_a]:underline-offset-4 [&_a]:hover:underline",
|
||||
"[&_code]:bg-muted [&_code]:rounded-sm [&_code]:px-[0.3rem] [&_code]:py-[0.2rem] [&_code]:font-medium",
|
||||
"group-has-data-[intent=edit]:hidden", // hides the rendered comment when its own edit form is active
|
||||
)}
|
||||
>
|
||||
<Markdown
|
||||
remarkPlugins={[remarkGfm, remarkSmartypants]}
|
||||
rehypePlugins={[
|
||||
[
|
||||
rehypeExternalLinks,
|
||||
{ target: "_blank", rel: "noopener noreferrer nofollow" },
|
||||
],
|
||||
]}
|
||||
allowedElements={[
|
||||
"p",
|
||||
"a",
|
||||
"em",
|
||||
"strong",
|
||||
"code",
|
||||
"pre",
|
||||
"blockquote",
|
||||
"del",
|
||||
[rehypeExternalLinks, { target: "_blank", rel: "noopener noreferrer nofollow" }],
|
||||
]}
|
||||
allowedElements={["p", "a", "em", "strong", "code", "pre", "blockquote", "del"]}
|
||||
>
|
||||
{comment.content}
|
||||
</Markdown>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CommentWithUser } from "@/lib/server/comments";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { CommentSingle } from "./comment-single";
|
||||
|
||||
/** Maximum nesting depth for comment threads (0-indexed, so 2 = 3 levels deep) */
|
||||
@@ -20,12 +21,7 @@ const CommentThread = ({
|
||||
<CommentSingle comment={comment} />
|
||||
|
||||
{replies.length > 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
"mt-6 space-y-6",
|
||||
level < MAX_NESTING_LEVEL && "ml-6 border-l-2 pl-6",
|
||||
)}
|
||||
>
|
||||
<div className={cn("mt-6 space-y-6", level < MAX_NESTING_LEVEL && "ml-6 border-l-2 pl-6")}>
|
||||
{replies.map((reply) => (
|
||||
<CommentThread
|
||||
key={reply.id}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { auth } from "@/lib/auth";
|
||||
import { type CommentWithUser, getComments } from "@/lib/server/comments";
|
||||
|
||||
import { NewCommentForm } from "./comment-form";
|
||||
import { CommentThread } from "./comment-thread";
|
||||
import { SignIn } from "./sign-in";
|
||||
@@ -31,10 +33,8 @@ const Comments = async ({ slug }: { slug: string }) => {
|
||||
{session ? (
|
||||
<NewCommentForm slug={slug} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center gap-y-4 rounded-lg bg-muted/40 p-6">
|
||||
<p className="text-center font-medium">
|
||||
Join the discussion by signing in:
|
||||
</p>
|
||||
<div className="bg-muted/40 flex flex-col items-center justify-center gap-y-4 rounded-lg p-6">
|
||||
<p className="text-center font-medium">Join the discussion by signing in:</p>
|
||||
<SignIn callbackPath={`/${slug}#comments`} />
|
||||
</div>
|
||||
)}
|
||||
@@ -51,7 +51,7 @@ const Comments = async ({ slug }: { slug: string }) => {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-8 text-center font-medium text-foreground/80 text-lg tracking-tight">
|
||||
<div className="text-foreground/80 py-8 text-center text-lg font-medium tracking-tight">
|
||||
Be the first to comment!
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { Loader2Icon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { GitHubIcon } from "@/components/icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { signIn } from "@/lib/auth-client";
|
||||
@@ -26,12 +27,7 @@ const SignIn = ({ callbackPath }: { callbackPath?: string }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleSignIn}
|
||||
disabled={isLoading}
|
||||
size="lg"
|
||||
variant="outline"
|
||||
>
|
||||
<Button onClick={handleSignIn} disabled={isLoading} size="lg" variant="outline">
|
||||
{isLoading ? <Loader2Icon className="animate-spin" /> : <GitHubIcon />}
|
||||
Sign in with GitHub
|
||||
</Button>
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useForm } from "@tanstack/react-form";
|
||||
import { CheckIcon, Loader2Icon, SendIcon, XIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { MarkdownIcon } from "@/components/icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ContactSchema } from "@/lib/schemas/contact";
|
||||
import { type ContactResult, sendContactForm } from "@/lib/server/contact";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ContactForm = () => {
|
||||
const [result, setResult] = useState<ContactResult | null>(null);
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
email: "",
|
||||
message: "",
|
||||
},
|
||||
validators: {
|
||||
onBlur: ContactSchema,
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("name", value.name);
|
||||
formData.append("email", value.email);
|
||||
formData.append("message", value.message);
|
||||
|
||||
const response = await sendContactForm(formData);
|
||||
setResult(response);
|
||||
|
||||
if (response.success) {
|
||||
form.reset();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[contact-form] error:", error);
|
||||
setResult({
|
||||
success: false,
|
||||
message: "Something went wrong. Please try again.",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
className="my-5 space-y-4"
|
||||
>
|
||||
<form.Subscribe
|
||||
selector={(state) => state.isSubmitting || result?.success}
|
||||
>
|
||||
{(isDisabled) => (
|
||||
<>
|
||||
<form.Field name="name">
|
||||
{(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched &&
|
||||
field.state.meta.errors.length > 0;
|
||||
return (
|
||||
<Field
|
||||
data-invalid={isInvalid || undefined}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<FieldLabel htmlFor="name">Name</FieldLabel>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Microsoft Bob"
|
||||
autoComplete="name"
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
disabled={!!isDisabled}
|
||||
aria-invalid={isInvalid || undefined}
|
||||
/>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
|
||||
<form.Field name="email">
|
||||
{(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched &&
|
||||
field.state.meta.errors.length > 0;
|
||||
return (
|
||||
<Field
|
||||
data-invalid={isInvalid || undefined}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
inputMode="email"
|
||||
placeholder="robert@hotmail.com"
|
||||
autoComplete="email"
|
||||
spellCheck={false}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
disabled={!!isDisabled}
|
||||
aria-invalid={isInvalid || undefined}
|
||||
/>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
|
||||
<form.Field name="message">
|
||||
{(field) => {
|
||||
const isInvalid =
|
||||
field.state.meta.isTouched &&
|
||||
field.state.meta.errors.length > 0;
|
||||
return (
|
||||
<Field
|
||||
data-invalid={isInvalid || undefined}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<FieldLabel htmlFor="message">Message</FieldLabel>
|
||||
<Textarea
|
||||
id="message"
|
||||
name="message"
|
||||
placeholder="Write something…"
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
disabled={!!isDisabled}
|
||||
aria-invalid={isInvalid || undefined}
|
||||
className="min-h-[6lh] resize-y"
|
||||
/>
|
||||
{isInvalid && (
|
||||
<FieldError errors={field.state.meta.errors} />
|
||||
)}
|
||||
|
||||
<p className="mt-1.5 text-[0.8rem] text-foreground/85 leading-relaxed">
|
||||
<MarkdownIcon className="mr-1.5 inline-block size-4 align-text-top" />
|
||||
Basic{" "}
|
||||
<a
|
||||
href="https://commonmark.org/help/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Markdown reference sheet"
|
||||
className="font-semibold"
|
||||
>
|
||||
Markdown syntax
|
||||
</a>{" "}
|
||||
is allowed, e.g.: <strong>**bold**</strong>,{" "}
|
||||
<em>_italics_</em>, [
|
||||
<a
|
||||
href="https://jarv.is"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="hover:no-underline"
|
||||
>
|
||||
links
|
||||
</a>
|
||||
](https://jarv.is), and <code>`code`</code>.
|
||||
</p>
|
||||
</Field>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
</>
|
||||
)}
|
||||
</form.Subscribe>
|
||||
|
||||
<div className="flex min-h-16 items-center space-x-4">
|
||||
<form.Subscribe selector={(state) => [undefined, state.isSubmitting]}>
|
||||
{([isSubmitting]) => (
|
||||
<>
|
||||
{!result?.success && (
|
||||
<Button type="submit" size="lg" disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2Icon
|
||||
className="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Sending…
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SendIcon aria-hidden="true" />
|
||||
Send
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!isSubmitting && result?.message && (
|
||||
<div
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className={cn(
|
||||
"space-x-0.5 font-semibold text-[0.9rem]",
|
||||
result.success
|
||||
? "text-green-600 dark:text-green-400"
|
||||
: "text-destructive",
|
||||
)}
|
||||
>
|
||||
{result.success ? (
|
||||
<CheckIcon className="inline size-4" aria-hidden="true" />
|
||||
) : (
|
||||
<XIcon className="inline size-4" aria-hidden="true" />
|
||||
)}{" "}
|
||||
<span>{result.message}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</form.Subscribe>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export { ContactForm };
|
||||
@@ -4,6 +4,7 @@ import copy from "copy-to-clipboard";
|
||||
import { CheckIcon, ClipboardCheckIcon, CopyIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -33,12 +34,7 @@ function CopyButton({
|
||||
copy(value);
|
||||
setHasCopied(true);
|
||||
toast.success("Copied!", {
|
||||
icon: (
|
||||
<ClipboardCheckIcon
|
||||
className="size-4 text-foreground/85"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
),
|
||||
icon: <ClipboardCheckIcon className="text-foreground/85 size-4" aria-hidden="true" />,
|
||||
duration: 2000,
|
||||
id: "copy-button-toast-success",
|
||||
});
|
||||
@@ -56,7 +52,7 @@ function CopyButton({
|
||||
size="icon"
|
||||
variant={variant}
|
||||
className={cn(
|
||||
"absolute top-3 right-2 z-10 size-7.5 bg-code hover:bg-accent hover:opacity-100 focus-visible:opacity-100 dark:hover:bg-accent",
|
||||
"bg-code hover:bg-accent dark:hover:bg-accent absolute top-3 right-2 z-10 size-7.5 hover:opacity-100 focus-visible:opacity-100",
|
||||
hasCopied ? "cursor-default" : "cursor-pointer",
|
||||
className,
|
||||
)}
|
||||
@@ -65,10 +61,7 @@ function CopyButton({
|
||||
{...props}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<CheckIcon
|
||||
className="text-green-600 dark:text-green-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CheckIcon className="text-green-600 dark:text-green-400" aria-hidden="true" />
|
||||
) : (
|
||||
<CopyIcon aria-hidden="true" />
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { LinkIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const HeadingAnchor = ({
|
||||
@@ -13,7 +14,7 @@ const HeadingAnchor = ({
|
||||
<a
|
||||
href={`#${id}`}
|
||||
className={cn(
|
||||
"ml-2 inline-block px-2 align-baseline text-muted-foreground hover:text-primary hover:no-underline",
|
||||
"text-muted-foreground hover:text-primary ml-2 inline-block px-2 align-baseline hover:no-underline",
|
||||
className,
|
||||
)}
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -41,17 +41,3 @@ export const GitHubIcon = ({ className }: { className?: string }) => (
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const NextjsIcon = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="0"
|
||||
viewBox="0 0 24 24"
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M18.665 21.978C16.758 23.255 14.465 24 12 24 5.377 24 0 18.623 0 12S5.377 0 12 0s12 5.377 12 12c0 3.583-1.574 6.801-4.067 9.001L9.219 7.2H7.2v9.596h1.615V9.251l9.85 12.727Zm-3.332-8.533 1.6 2.061V7.2h-1.6v6.245Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
import { getImageProps } from "next/image";
|
||||
import { Children } from "react";
|
||||
import {
|
||||
ReactCompareSlider,
|
||||
ReactCompareSliderImage,
|
||||
} from "react-compare-slider";
|
||||
import { ReactCompareSlider, ReactCompareSliderImage } from "react-compare-slider";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ImageDiff = ({
|
||||
@@ -18,16 +16,12 @@ const ImageDiff = ({
|
||||
// Extract the two image children
|
||||
const childrenArray = Children.toArray(children);
|
||||
if (childrenArray.length !== 2) {
|
||||
console.error(
|
||||
"ImageDiff must have exactly two children (before and after images)",
|
||||
);
|
||||
console.error("ImageDiff must have exactly two children (before and after images)");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the original image source to extract dimensions for aspect ratio
|
||||
const firstChildProps = children[0].props as Parameters<
|
||||
typeof getImageProps
|
||||
>[0];
|
||||
const firstChildProps = children[0].props as Parameters<typeof getImageProps>[0];
|
||||
const imageSrc = firstChildProps.src;
|
||||
const aspectRatio =
|
||||
typeof imageSrc === "object" && "width" in imageSrc && "height" in imageSrc
|
||||
@@ -42,23 +36,10 @@ const ImageDiff = ({
|
||||
|
||||
return (
|
||||
<ReactCompareSlider
|
||||
className={cn(
|
||||
"my-8 w-full max-w-full overflow-hidden rounded-sm",
|
||||
className,
|
||||
)}
|
||||
className={cn("my-8 w-full max-w-full overflow-hidden rounded-sm", className)}
|
||||
style={{ aspectRatio }}
|
||||
itemOne={
|
||||
<ReactCompareSliderImage
|
||||
{...beforeImageProps}
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
}
|
||||
itemTwo={
|
||||
<ReactCompareSliderImage
|
||||
{...afterImageProps}
|
||||
className="size-full object-cover"
|
||||
/>
|
||||
}
|
||||
itemOne={<ReactCompareSliderImage {...beforeImageProps} className="size-full object-cover" />}
|
||||
itemTwo={<ReactCompareSliderImage {...afterImageProps} className="size-full object-cover" />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import siteConfig from "@/lib/config/site";
|
||||
|
||||
const Footer = () => (
|
||||
<footer className="mt-8 w-full py-6 text-center text-[13px] text-muted-foreground leading-loose">
|
||||
<footer className="text-muted-foreground mt-8 w-full py-6 text-center text-[13px] leading-loose">
|
||||
All content is licensed under{" "}
|
||||
<Link href="/license" className="underline underline-offset-4">
|
||||
{siteConfig.license}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import { AtSignIcon, MoonIcon, SunIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import avatarImg from "@/app/avatar.jpg";
|
||||
import { GitHubIcon } from "@/components/icons";
|
||||
import { Menu } from "@/components/layout/menu";
|
||||
@@ -42,24 +43,24 @@ const Header = ({ className }: { className?: string }) => {
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<header className="mx-auto flex w-full max-w-4xl items-center justify-between px-5 py-4">
|
||||
<header className="mx-auto mt-2 flex w-full max-w-[720px] items-center justify-between px-5 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
href="/"
|
||||
rel="author"
|
||||
aria-label={siteConfig.name}
|
||||
className="flex shrink-0 items-center gap-2.5 pr-2 hover:text-foreground/85 hover:no-underline"
|
||||
className="hover:text-foreground/85 flex shrink-0 items-center gap-2.5 pr-2 hover:no-underline"
|
||||
>
|
||||
<Image
|
||||
src={avatarImg}
|
||||
alt={`Photo of ${siteConfig.name}`}
|
||||
className="size-[40px] rounded-full border border-ring/30 md:size-[32px]"
|
||||
className="border-ring/30 size-7 rounded-full border"
|
||||
width={40}
|
||||
height={40}
|
||||
quality={75}
|
||||
priority
|
||||
/>
|
||||
<span className="whitespace-nowrap font-medium text-[17.5px] tracking-tight max-md:sr-only">
|
||||
<span className="text-[17.5px] font-medium tracking-tight whitespace-nowrap max-md:sr-only">
|
||||
{siteConfig.name}
|
||||
</span>
|
||||
</Link>
|
||||
@@ -71,16 +72,26 @@ const Header = ({ className }: { className?: string }) => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
aria-label="Open GitHub profile"
|
||||
asChild
|
||||
nativeButton={false}
|
||||
aria-label="Email Me"
|
||||
render={<a href={`mailto:${authorConfig.email}`} />}
|
||||
>
|
||||
<a
|
||||
href={`https://github.com/${authorConfig.social.github}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<GitHubIcon />
|
||||
</a>
|
||||
<AtSignIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
nativeButton={false}
|
||||
aria-label="Open GitHub profile"
|
||||
render={
|
||||
<a
|
||||
href={`https://github.com/${authorConfig.social.github}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<GitHubIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -89,10 +100,7 @@ const Header = ({ className }: { className?: string }) => {
|
||||
aria-label="Toggle theme"
|
||||
className="group"
|
||||
>
|
||||
<SunIcon
|
||||
className="group-hover:stroke-orange-600 dark:hidden"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<SunIcon className="group-hover:stroke-orange-600 dark:hidden" aria-hidden="true" />
|
||||
<MoonIcon
|
||||
className="not-dark:hidden group-hover:stroke-yellow-400"
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useSelectedLayoutSegment } from "next/navigation";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
@@ -20,79 +14,31 @@ const menuItems = [
|
||||
text: "Projects",
|
||||
href: "/projects",
|
||||
},
|
||||
{
|
||||
text: "Contact",
|
||||
href: "/contact",
|
||||
},
|
||||
] as const;
|
||||
|
||||
const Menu = () => {
|
||||
const segment = useSelectedLayoutSegment() || "";
|
||||
|
||||
const currentItem = menuItems.find(
|
||||
(item) => item.href?.split("/")[1] === segment,
|
||||
);
|
||||
const currentLabel = segment === "" ? "Home" : currentItem?.text || "Menu";
|
||||
|
||||
return (
|
||||
<nav data-slot="navigation-menu">
|
||||
{/* Desktop: Show all buttons */}
|
||||
<div className="hidden items-center gap-2 sm:flex">
|
||||
{menuItems.map((item) => {
|
||||
const isCurrent = item.href?.split("/")[1] === segment;
|
||||
<nav data-slot="navigation-menu" className="flex items-center gap-2">
|
||||
{menuItems.map((item) => {
|
||||
const isCurrent = item.href?.split("/")[1] === segment;
|
||||
|
||||
return (
|
||||
<Button
|
||||
asChild
|
||||
key={item.href}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
aria-label={item.text}
|
||||
data-current={isCurrent || undefined}
|
||||
className="text-[15px] leading-none data-current:bg-accent/60 data-current:text-accent-foreground"
|
||||
>
|
||||
<Link href={item.href}>{item.text}</Link>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Mobile: Show dropdown menu */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
return (
|
||||
<Button
|
||||
key={item.href}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex gap-2 text-[17.5px] sm:hidden data-[state=open]:[&_svg]:rotate-180"
|
||||
nativeButton={false}
|
||||
aria-label={item.text}
|
||||
data-current={isCurrent || undefined}
|
||||
className="data-current:bg-accent/60 data-current:text-accent-foreground text-[15px] leading-none"
|
||||
render={<Link href={item.href} />}
|
||||
>
|
||||
{currentLabel}
|
||||
<ChevronDownIcon className="size-3.5 opacity-60 transition-transform duration-200" />
|
||||
{item.text}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="min-w-[140px]">
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
data-current={segment === ""}
|
||||
aria-current={segment === "" ? "page" : undefined}
|
||||
>
|
||||
<Link href="/">Home</Link>
|
||||
</DropdownMenuItem>
|
||||
{menuItems.map((item) => {
|
||||
const isCurrent = item.href?.split("/")[1] === segment;
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
key={item.href}
|
||||
data-current={isCurrent || undefined}
|
||||
aria-current={isCurrent ? "page" : undefined}
|
||||
>
|
||||
<Link href={item.href}>{item.text}</Link>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const PageTitle = ({
|
||||
@@ -11,14 +12,14 @@ const PageTitle = ({
|
||||
}) => (
|
||||
<h1
|
||||
className={cn(
|
||||
"not-prose mt-0 mb-6 text-left font-medium text-3xl lowercase tracking-tight",
|
||||
"not-prose mt-0 mb-6 text-left text-3xl font-medium tracking-tight lowercase",
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<Link
|
||||
href={canonical}
|
||||
className="text-foreground no-underline before:text-muted-foreground before:tracking-wider before:content-['\002E\002F']"
|
||||
className="text-foreground before:text-muted-foreground no-underline before:tracking-wider before:content-['./']"
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
|
||||
@@ -12,19 +12,15 @@ const Marquee = ({
|
||||
repeat?: number;
|
||||
}) => (
|
||||
<div
|
||||
className={cn(
|
||||
"group flex flex-row overflow-hidden [--gap:2rem] [gap:var(--gap)]",
|
||||
className,
|
||||
)}
|
||||
className={cn("group flex flex-row [gap:var(--gap)] overflow-hidden [--gap:2rem]", className)}
|
||||
{...rest}
|
||||
>
|
||||
{Array(repeat)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<div
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: identical clones for animation; no natural unique key exists
|
||||
key={i}
|
||||
className="flex shrink-0 flex-row justify-around [gap:var(--gap)] motion-safe:animate-marquee"
|
||||
className="motion-safe:animate-marquee flex shrink-0 flex-row justify-around [gap:var(--gap)]"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -2,22 +2,14 @@
|
||||
|
||||
import { EyeIcon, MessagesSquareIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { createContext, type ReactNode, useContext, useEffect, useState } from "react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
import { getAllCommentCounts } from "@/lib/server/comments";
|
||||
import { getAllViewCounts } from "@/lib/server/views";
|
||||
|
||||
const numberFormatter = new Intl.NumberFormat(
|
||||
process.env.NEXT_PUBLIC_SITE_LOCALE,
|
||||
);
|
||||
const numberFormatter = new Intl.NumberFormat(process.env.NEXT_PUBLIC_SITE_LOCALE);
|
||||
|
||||
type Stats = {
|
||||
views: Record<string, number>;
|
||||
@@ -53,9 +45,7 @@ export const PostStatsProvider = ({ children }: { children: ReactNode }) => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StatsContext.Provider value={stats}>{children}</StatsContext.Provider>
|
||||
);
|
||||
return <StatsContext.Provider value={stats}>{children}</StatsContext.Provider>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -80,10 +70,7 @@ const PostStats = ({ slug }: { slug: string }) => {
|
||||
return (
|
||||
<>
|
||||
{viewCount > 0 && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="gap-[5px] text-foreground/80 tabular-nums"
|
||||
>
|
||||
<Badge variant="secondary" className="text-foreground/80 gap-[5px] tabular-nums">
|
||||
<EyeIcon className="text-foreground/65" aria-hidden="true" />
|
||||
{numberFormatter.format(viewCount)}
|
||||
</Badge>
|
||||
@@ -92,19 +79,16 @@ const PostStats = ({ slug }: { slug: string }) => {
|
||||
{commentCount > 0 && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="gap-[5px] text-foreground/80 tabular-nums"
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
href={`/${slug}#comments`}
|
||||
title={`${numberFormatter.format(commentCount)} ${commentCount === 1 ? "comment" : "comments"}`}
|
||||
>
|
||||
<MessagesSquareIcon
|
||||
className="text-foreground/65"
|
||||
aria-hidden="true"
|
||||
className="text-foreground/80 gap-[5px] tabular-nums"
|
||||
render={
|
||||
<Link
|
||||
href={`/${slug}#comments`}
|
||||
title={`${numberFormatter.format(commentCount)} ${commentCount === 1 ? "comment" : "comments"}`}
|
||||
/>
|
||||
{numberFormatter.format(commentCount)}
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<MessagesSquareIcon className="text-foreground/65" aria-hidden="true" />
|
||||
{numberFormatter.format(commentCount)}
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -2,14 +2,11 @@
|
||||
|
||||
import { ThemeProvider } from "next-themes";
|
||||
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
|
||||
const Providers = ({ children }: { children: React.ReactNode }) => (
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
|
||||
1
components/third-party/codepen.tsx
vendored
1
components/third-party/codepen.tsx
vendored
@@ -24,6 +24,7 @@ const CodePen = ({
|
||||
editable: `${!!editable}`,
|
||||
})}`}
|
||||
title={title}
|
||||
sandbox="allow-scripts allow-popups allow-forms"
|
||||
className={cn("h-[500px] w-full overflow-hidden border-none", className)}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
6
components/third-party/gist.tsx
vendored
6
components/third-party/gist.tsx
vendored
@@ -1,4 +1,5 @@
|
||||
import { cacheLife, cacheTag } from "next/cache";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Gist = async ({
|
||||
@@ -18,6 +19,8 @@ const Gist = async ({
|
||||
|
||||
const iframeId = `gist-${id}${file ? `-${file}` : ""}`;
|
||||
|
||||
const iframeTitle = title ?? `GitHub Gist ${id}${file ? ` - ${file}` : ""}`;
|
||||
|
||||
const scriptUrl = `https://gist.github.com/${id}.js${file ? `?file=${file}` : ""}`;
|
||||
const scriptResponse = await fetch(scriptUrl);
|
||||
|
||||
@@ -49,7 +52,8 @@ const Gist = async ({
|
||||
scrolling="no"
|
||||
id={iframeId}
|
||||
srcDoc={iframeHtml}
|
||||
title={title || `GitHub Gist ${id}${file ? ` - ${file}` : ""}`}
|
||||
title={iframeTitle}
|
||||
sandbox="allow-scripts"
|
||||
className={cn("overflow-hidden border-none", className)}
|
||||
{...rest}
|
||||
suppressHydrationWarning
|
||||
|
||||
1
components/third-party/tweet.tsx
vendored
1
components/third-party/tweet.tsx
vendored
@@ -3,6 +3,7 @@ import Image from "next/image";
|
||||
import { EmbeddedTweet, TweetNotFound } from "react-tweet";
|
||||
import type { Tweet as TweetType } from "react-tweet/api";
|
||||
import { fetchTweet } from "react-tweet/api";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Tweet = async ({ id, className }: { id: string; className?: string }) => {
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("border-b last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left font-medium text-sm outline-none transition-all hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
);
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
@@ -1,41 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||
import type * as React from "react";
|
||||
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
|
||||
import * as React from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
);
|
||||
function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {
|
||||
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
);
|
||||
function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {
|
||||
return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
function AlertDialogOverlay({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
<AlertDialogPrimitive.Backdrop
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
"data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0 fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -47,17 +35,17 @@ function AlertDialogContent({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
|
||||
}: AlertDialogPrimitive.Popup.Props & {
|
||||
size?: "default" | "sm";
|
||||
}) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
<AlertDialogPrimitive.Popup
|
||||
data-slot="alert-dialog-content"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 group/alert-dialog-content fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[size=sm]:max-w-xs data-[state=closed]:animate-out data-[state=open]:animate-in data-[size=default]:sm:max-w-lg",
|
||||
"group/alert-dialog-content bg-popover text-popover-foreground ring-foreground/10 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 fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl p-4 ring-1 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -66,15 +54,12 @@ function AlertDialogContent({
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn(
|
||||
"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
|
||||
"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -82,15 +67,25 @@ function AlertDialogHeader({
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
|
||||
"bg-muted/50 -mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogMedia({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-media"
|
||||
className={cn(
|
||||
"bg-muted mb-2 inline-flex size-10 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -106,7 +101,7 @@ function AlertDialogTitle({
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn(
|
||||
"font-semibold text-lg sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
|
||||
"font-heading text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -121,21 +116,8 @@ function AlertDialogDescription({
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogMedia({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-media"
|
||||
className={cn(
|
||||
"mb-2 inline-flex size-16 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8",
|
||||
"text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -143,22 +125,8 @@ function AlertDialogMedia({
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
|
||||
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
||||
return (
|
||||
<Button variant={variant} size={size} asChild>
|
||||
<AlertDialogPrimitive.Action
|
||||
data-slot="alert-dialog-action"
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
function AlertDialogAction({ className, ...props }: React.ComponentProps<typeof Button>) {
|
||||
return <Button data-slot="alert-dialog-action" className={cn(className)} {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
@@ -166,16 +134,15 @@ function AlertDialogCancel({
|
||||
variant = "outline",
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
|
||||
}: AlertDialogPrimitive.Close.Props &
|
||||
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
||||
return (
|
||||
<Button variant={variant} size={size} asChild>
|
||||
<AlertDialogPrimitive.Cancel
|
||||
data-slot="alert-dialog-cancel"
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
</Button>
|
||||
<AlertDialogPrimitive.Close
|
||||
data-slot="alert-dialog-cancel"
|
||||
className={cn(className)}
|
||||
render={<Button variant={variant} size={size} />}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"col-start-2 grid justify-items-start gap-1 text-muted-foreground text-sm [&_p]:leading-relaxed",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
@@ -1,11 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
|
||||
|
||||
function AspectRatio({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
|
||||
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
|
||||
}
|
||||
|
||||
export { AspectRatio };
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
import type * as React from "react";
|
||||
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -9,7 +9,7 @@ function Avatar({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
|
||||
}: AvatarPrimitive.Root.Props & {
|
||||
size?: "default" | "sm" | "lg";
|
||||
}) {
|
||||
return (
|
||||
@@ -17,7 +17,7 @@ function Avatar({
|
||||
data-slot="avatar"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/avatar relative flex size-8 shrink-0 select-none overflow-hidden rounded-full data-[size=lg]:size-10 data-[size=sm]:size-6",
|
||||
"group/avatar after:border-border relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -25,28 +25,22 @@ function Avatar({
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
className={cn("aspect-square size-full rounded-full object-cover", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
function AvatarFallback({ className, ...props }: AvatarPrimitive.Fallback.Props) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"flex size-full items-center justify-center rounded-full bg-muted text-muted-foreground text-sm group-data-[size=sm]/avatar:text-xs",
|
||||
"bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -59,7 +53,7 @@ function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
|
||||
<span
|
||||
data-slot="avatar-badge"
|
||||
className={cn(
|
||||
"absolute right-0 bottom-0 z-10 inline-flex select-none items-center justify-center rounded-full bg-primary text-primary-foreground ring-2 ring-background",
|
||||
"bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none",
|
||||
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
|
||||
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
|
||||
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
|
||||
@@ -75,7 +69,7 @@ function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
<div
|
||||
data-slot="avatar-group"
|
||||
className={cn(
|
||||
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
|
||||
"group/avatar-group *:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -83,15 +77,12 @@ function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarGroupCount({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
function AvatarGroupCount({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="avatar-group-count"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground text-sm ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
|
||||
"bg-muted text-muted-foreground ring-background relative flex size-8 shrink-0 items-center justify-center rounded-full text-sm ring-2 group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -99,11 +90,4 @@ function AvatarGroupCount({
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Avatar,
|
||||
AvatarImage,
|
||||
AvatarFallback,
|
||||
AvatarBadge,
|
||||
AvatarGroup,
|
||||
AvatarGroupCount,
|
||||
};
|
||||
export { Avatar, AvatarImage, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarBadge };
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { mergeProps } from "@base-ui/react/merge-props";
|
||||
import { useRender } from "@base-ui/react/use-render";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-full border border-transparent px-2 py-0.5 font-medium text-xs transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
|
||||
"group/badge focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:ring-[3px] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||
destructive:
|
||||
"bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
|
||||
outline:
|
||||
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 [a&]:hover:underline",
|
||||
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
||||
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -29,20 +27,23 @@ const badgeVariants = cva(
|
||||
function Badge({
|
||||
className,
|
||||
variant = "default",
|
||||
asChild = false,
|
||||
render,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
data-variant={variant}
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
||||
return useRender({
|
||||
defaultTagName: "span",
|
||||
props: mergeProps<"span">(
|
||||
{
|
||||
className: cn(badgeVariants({ variant }), className),
|
||||
},
|
||||
props,
|
||||
),
|
||||
render,
|
||||
state: {
|
||||
slot: "badge",
|
||||
variant,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonGroupVariants = cva(
|
||||
"flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
||||
{
|
||||
variants: {
|
||||
orientation: {
|
||||
horizontal:
|
||||
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
|
||||
vertical:
|
||||
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: "horizontal",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function ButtonGroup({
|
||||
className,
|
||||
orientation,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="button-group"
|
||||
data-orientation={orientation}
|
||||
className={cn(buttonGroupVariants({ orientation }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ButtonGroupText({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-md border bg-muted px-4 font-medium text-sm shadow-xs [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ButtonGroupSeparator({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Separator>) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot="button-group-separator"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"!m-0 relative self-stretch bg-input data-[orientation=vertical]:h-auto",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
ButtonGroup,
|
||||
ButtonGroupSeparator,
|
||||
ButtonGroupText,
|
||||
buttonGroupVariants,
|
||||
};
|
||||
@@ -1,34 +1,38 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
"use client";
|
||||
|
||||
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
"group/button focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:ring-3 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:ring-3 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
default:
|
||||
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
icon: "size-8",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm":
|
||||
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||
"icon-lg": "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -42,19 +46,11 @@ function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
||||
return (
|
||||
<Comp
|
||||
<ButtonPrimitive
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("font-semibold leading-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs outline-none transition-shadow focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:data-[state=checked]:bg-primary dark:aria-invalid:ring-destructive/40",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="grid place-content-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Checkbox };
|
||||
@@ -1,33 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible";
|
||||
|
||||
function Collapsible({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleTrigger
|
||||
data-slot="collapsible-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) {
|
||||
return <CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function CollapsibleContent({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
data-slot="collapsible-content"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) {
|
||||
return <CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} />;
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
|
||||
@@ -1,44 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
import * as React from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
function DialogOverlay({ className, ...props }: DialogPrimitive.Backdrop.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
<DialogPrimitive.Backdrop
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
"data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0 fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -51,16 +41,16 @@ function DialogContent({
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
}: DialogPrimitive.Popup.Props & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
<DialogPrimitive.Popup
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg outline-none duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in sm:max-w-lg",
|
||||
"bg-popover text-popover-foreground ring-foreground/10 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 fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl p-4 text-sm ring-1 duration-100 outline-none sm:max-w-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -69,24 +59,20 @@ function DialogContent({
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
||||
render={<Button variant="ghost" className="absolute top-2 right-2" size="icon-sm" />}
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Popup>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
<div data-slot="dialog-header" className={cn("flex flex-col gap-2", className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,42 +88,37 @@ function DialogFooter({
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
"bg-muted/50 -mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t p-4 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</DialogPrimitive.Close>
|
||||
<DialogPrimitive.Close render={<Button variant="outline" />}>Close</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("font-semibold text-lg leading-none", className)}
|
||||
className={cn("font-heading text-base leading-none font-medium", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn(
|
||||
"text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,61 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
|
||||
import { ChevronRightIcon, CheckIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
|
||||
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
);
|
||||
function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
|
||||
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
|
||||
return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
align = "start",
|
||||
alignOffset = 0,
|
||||
side = "bottom",
|
||||
sideOffset = 4,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
}: MenuPrimitive.Popup.Props &
|
||||
Pick<MenuPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
<MenuPrimitive.Portal>
|
||||
<MenuPrimitive.Positioner
|
||||
className="isolate z-50 outline-none"
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
>
|
||||
<MenuPrimitive.Popup
|
||||
data-slot="dropdown-menu-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground ring-foreground/10 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-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 z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-md ring-1 duration-100 outline-none data-closed:overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenuPrimitive.Positioner>
|
||||
</MenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
|
||||
return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
}: MenuPrimitive.GroupLabel.Props & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
<MenuPrimitive.GroupLabel
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,17 +78,17 @@ function DropdownMenuItem({
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
}: MenuPrimitive.Item.Props & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
<MenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"data-[variant=destructive]:*:[svg]:!text-destructive relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[disabled]:opacity-50 data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
"group/dropdown-menu-item focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:*:[svg]:text-destructive relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -82,155 +96,142 @@ function DropdownMenuItem({
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
|
||||
return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: MenuPrimitive.SubmenuTrigger.Props & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.SubmenuTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</MenuPrimitive.SubmenuTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
align = "start",
|
||||
alignOffset = -3,
|
||||
side = "right",
|
||||
sideOffset = 0,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuContent>) {
|
||||
return (
|
||||
<DropdownMenuContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground ring-foreground/10 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 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 w-auto min-w-[96px] rounded-lg p-1 shadow-lg ring-1 duration-100",
|
||||
className,
|
||||
)}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
}: MenuPrimitive.CheckboxItem.Props & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
<MenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
<span
|
||||
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||
>
|
||||
<MenuPrimitive.CheckboxItemIndicator>
|
||||
<CheckIcon />
|
||||
</MenuPrimitive.CheckboxItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
</MenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
|
||||
return <MenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
}: MenuPrimitive.RadioItem.Props & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
<MenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
<span
|
||||
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
||||
data-slot="dropdown-menu-radio-item-indicator"
|
||||
>
|
||||
<MenuPrimitive.RadioItemIndicator>
|
||||
<CheckIcon />
|
||||
</MenuPrimitive.RadioItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
</MenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
function DropdownMenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 font-medium text-sm data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
<MenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"ml-auto text-muted-foreground text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[inset]:pl-8 data-[state=open]:text-accent-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
"text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty"
|
||||
className={cn(
|
||||
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 text-balance rounded-lg border-dashed p-6 text-center md:p-12",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-header"
|
||||
className={cn(
|
||||
"flex max-w-sm flex-col items-center gap-2 text-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMediaVariants = cva(
|
||||
"mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function EmptyMedia({
|
||||
className,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-icon"
|
||||
data-variant={variant}
|
||||
className={cn(emptyMediaVariants({ variant, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-title"
|
||||
className={cn("font-medium text-lg tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-description"
|
||||
className={cn(
|
||||
"text-muted-foreground text-sm/relaxed [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-content"
|
||||
className={cn(
|
||||
"flex w-full min-w-0 max-w-sm flex-col items-center gap-4 text-balance text-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Empty,
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
EmptyDescription,
|
||||
EmptyContent,
|
||||
EmptyMedia,
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -11,8 +12,7 @@ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
||||
<fieldset
|
||||
data-slot="field-set"
|
||||
className={cn(
|
||||
"flex flex-col gap-6",
|
||||
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
||||
"flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -30,9 +30,7 @@ function FieldLegend({
|
||||
data-slot="field-legend"
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"mb-3 font-medium",
|
||||
"data-[variant=legend]:text-base",
|
||||
"data-[variant=label]:text-sm",
|
||||
"mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -45,7 +43,7 @@ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
<div
|
||||
data-slot="field-group"
|
||||
className={cn(
|
||||
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
|
||||
"group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -53,29 +51,20 @@ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
);
|
||||
}
|
||||
|
||||
const fieldVariants = cva(
|
||||
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
|
||||
{
|
||||
variants: {
|
||||
orientation: {
|
||||
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
|
||||
horizontal: [
|
||||
"flex-row items-center",
|
||||
"[&>[data-slot=field-label]]:flex-auto",
|
||||
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||
],
|
||||
responsive: [
|
||||
"@md/field-group:flex-row flex-col @md/field-group:items-center @md/field-group:[&>*]:w-auto [&>*]:w-full [&>.sr-only]:w-auto",
|
||||
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
|
||||
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: "vertical",
|
||||
const fieldVariants = cva("group/field data-[invalid=true]:text-destructive flex w-full gap-2", {
|
||||
variants: {
|
||||
orientation: {
|
||||
vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
|
||||
horizontal:
|
||||
"flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||
responsive:
|
||||
"flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||
},
|
||||
},
|
||||
);
|
||||
defaultVariants: {
|
||||
orientation: "vertical",
|
||||
},
|
||||
});
|
||||
|
||||
function Field({
|
||||
className,
|
||||
@@ -97,26 +86,19 @@ function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-content"
|
||||
className={cn(
|
||||
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
||||
className,
|
||||
)}
|
||||
className={cn("group/field-content flex flex-1 flex-col gap-0.5 leading-snug", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Label>) {
|
||||
function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>) {
|
||||
return (
|
||||
<Label
|
||||
data-slot="field-label"
|
||||
className={cn(
|
||||
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
||||
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
|
||||
"has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5 dark:has-data-[state=checked]:bg-primary/10",
|
||||
"group/field-label peer/field-label has-data-checked:border-primary/30 has-data-checked:bg-primary/5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10 flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5",
|
||||
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -129,7 +111,7 @@ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
<div
|
||||
data-slot="field-label"
|
||||
className={cn(
|
||||
"flex w-fit items-center gap-2 font-medium text-sm leading-snug group-data-[disabled=true]/field:opacity-50",
|
||||
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -142,8 +124,8 @@ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
<p
|
||||
data-slot="field-description"
|
||||
className={cn(
|
||||
"font-normal text-muted-foreground text-sm leading-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
|
||||
"nth-last-2:-mt-1 last:mt-0 [[data-variant=legend]+&]:-mt-1.5",
|
||||
"text-muted-foreground text-left text-sm leading-normal font-normal group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
|
||||
"last:mt-0 nth-last-2:-mt-1",
|
||||
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
||||
className,
|
||||
)}
|
||||
@@ -172,7 +154,7 @@ function FieldSeparator({
|
||||
<Separator className="absolute inset-0 top-1/2" />
|
||||
{children && (
|
||||
<span
|
||||
className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
|
||||
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
|
||||
data-slot="field-separator-content"
|
||||
>
|
||||
{children}
|
||||
@@ -199,20 +181,15 @@ function FieldError({
|
||||
return null;
|
||||
}
|
||||
|
||||
const uniqueErrors = [
|
||||
...new Map(errors.map((error) => [error?.message, error])).values(),
|
||||
];
|
||||
const uniqueErrors = [...new Map(errors.map((error) => [error?.message, error])).values()];
|
||||
|
||||
if (uniqueErrors?.length === 1) {
|
||||
if (uniqueErrors?.length == 1) {
|
||||
return uniqueErrors[0]?.message;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="ml-4 flex list-disc flex-col gap-1">
|
||||
{uniqueErrors.map(
|
||||
(error) =>
|
||||
error?.message && <li key={error.message}>{error.message}</li>,
|
||||
)}
|
||||
{uniqueErrors.map((error, index) => error?.message && <li key={index}>{error.message}</li>)}
|
||||
</ul>
|
||||
);
|
||||
}, [children, errors]);
|
||||
@@ -225,7 +202,7 @@ function FieldError({
|
||||
<div
|
||||
role="alert"
|
||||
data-slot="field-error"
|
||||
className={cn("font-normal text-destructive text-sm", className)}
|
||||
className={cn("text-destructive text-sm font-normal", className)}
|
||||
{...props}
|
||||
>
|
||||
{content}
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="input-group"
|
||||
role="group"
|
||||
className={cn(
|
||||
"group/input-group relative flex w-full items-center rounded-md border border-input shadow-xs outline-none transition-[color,box-shadow] dark:bg-input/30",
|
||||
"h-9 min-w-0 has-[>textarea]:h-auto",
|
||||
|
||||
// Variants based on alignment.
|
||||
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
||||
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
||||
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
||||
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
||||
|
||||
// Focus state.
|
||||
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50",
|
||||
|
||||
// Error state.
|
||||
"has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
||||
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const inputGroupAddonVariants = cva(
|
||||
"flex h-auto cursor-text select-none items-center justify-center gap-2 py-1.5 font-medium text-muted-foreground text-sm group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
align: {
|
||||
"inline-start":
|
||||
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
||||
"inline-end":
|
||||
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
|
||||
"block-start":
|
||||
"order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3",
|
||||
"block-end":
|
||||
"order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
align: "inline-start",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function InputGroupAddon({
|
||||
className,
|
||||
align = "inline-start",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="input-group-addon"
|
||||
data-align={align}
|
||||
className={cn(inputGroupAddonVariants({ align }), className)}
|
||||
onClick={(e) => {
|
||||
if ((e.target as HTMLElement).closest("button")) {
|
||||
return;
|
||||
}
|
||||
e.currentTarget.parentElement?.querySelector("input")?.focus();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
e.currentTarget.parentElement?.querySelector("input")?.focus();
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const inputGroupButtonVariants = cva(
|
||||
"flex items-center gap-2 text-sm shadow-none",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
|
||||
sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
||||
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "xs",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function InputGroupButton({
|
||||
className,
|
||||
type = "button",
|
||||
variant = "ghost",
|
||||
size = "xs",
|
||||
...props
|
||||
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
||||
VariantProps<typeof inputGroupButtonVariants>) {
|
||||
return (
|
||||
<Button
|
||||
type={type}
|
||||
data-size={size}
|
||||
variant={variant}
|
||||
className={cn(inputGroupButtonVariants({ size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-muted-foreground text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputGroupInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<Input
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputGroupTextarea({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<Textarea
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupText,
|
||||
InputGroupInput,
|
||||
InputGroupTextarea,
|
||||
};
|
||||
@@ -1,16 +1,15 @@
|
||||
import type * as React from "react";
|
||||
import { Input as InputPrimitive } from "@base-ui/react/input";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
<InputPrimitive
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs outline-none transition-[color,box-shadow] selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:font-medium file:text-foreground file:text-sm placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
|
||||
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
||||
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
||||
"border-input file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 disabled:bg-input/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 h-8 w-full min-w-0 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
role="list"
|
||||
data-slot="item-group"
|
||||
className={cn("group/item-group flex flex-col", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Separator>) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot="item-separator"
|
||||
orientation="horizontal"
|
||||
className={cn("my-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const itemVariants = cva(
|
||||
"group/item flex flex-wrap items-center rounded-md border border-transparent text-sm outline-none transition-colors duration-100 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [a]:transition-colors [a]:hover:bg-accent/50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline: "border-border",
|
||||
muted: "bg-muted/50",
|
||||
},
|
||||
size: {
|
||||
default: "gap-4 p-4",
|
||||
sm: "gap-2.5 px-4 py-3",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Item({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> &
|
||||
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "div";
|
||||
return (
|
||||
<Comp
|
||||
data-slot="item"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(itemVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const itemMediaVariants = cva(
|
||||
"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:translate-y-0.5 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
icon: "size-8 rounded-sm border bg-muted [&_svg:not([class*='size-'])]:size-4",
|
||||
image:
|
||||
"size-10 overflow-hidden rounded-sm [&_img]:size-full [&_img]:object-cover",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function ItemMedia({
|
||||
className,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="item-media"
|
||||
data-variant={variant}
|
||||
className={cn(itemMediaVariants({ variant, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="item-content"
|
||||
className={cn(
|
||||
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="item-title"
|
||||
className={cn(
|
||||
"flex w-fit items-center gap-2 font-medium text-sm leading-snug",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
return (
|
||||
<p
|
||||
data-slot="item-description"
|
||||
className={cn(
|
||||
"line-clamp-2 text-balance font-normal text-muted-foreground text-sm leading-normal",
|
||||
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="item-actions"
|
||||
className={cn("flex items-center gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="item-header"
|
||||
className={cn(
|
||||
"flex basis-full items-center justify-between gap-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="item-footer"
|
||||
className={cn(
|
||||
"flex basis-full items-center justify-between gap-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Item,
|
||||
ItemMedia,
|
||||
ItemContent,
|
||||
ItemActions,
|
||||
ItemGroup,
|
||||
ItemSeparator,
|
||||
ItemTitle,
|
||||
ItemDescription,
|
||||
ItemHeader,
|
||||
ItemFooter,
|
||||
};
|
||||
@@ -1,19 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import type * as React from "react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
function Label({ className, ...props }: React.ComponentProps<"label">) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
// oxlint-disable-next-line jsx_a11y/label-has-associated-control
|
||||
<label
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex select-none items-center gap-2 font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,76 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import type * as React from "react";
|
||||
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
function Popover({ ...props }: PopoverPrimitive.Root.Props) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
||||
}
|
||||
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
alignOffset = 0,
|
||||
side = "bottom",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
}: PopoverPrimitive.Popup.Props &
|
||||
Pick<PopoverPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
data-slot="popover-content"
|
||||
<PopoverPrimitive.Positioner
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
className="isolate z-50"
|
||||
>
|
||||
<PopoverPrimitive.Popup
|
||||
data-slot="popover-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground ring-foreground/10 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-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 z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-lg p-2.5 text-sm shadow-md ring-1 outline-hidden duration-100",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Positioner>
|
||||
</PopoverPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
||||
}
|
||||
|
||||
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="popover-header"
|
||||
className={cn("flex flex-col gap-1 text-sm", className)}
|
||||
className={cn("flex flex-col gap-0.5 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
|
||||
function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
|
||||
return (
|
||||
<div
|
||||
<PopoverPrimitive.Title
|
||||
data-slot="popover-title"
|
||||
className={cn("font-medium", className)}
|
||||
className={cn("font-heading font-medium", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PopoverDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"p">) {
|
||||
function PopoverDescription({ className, ...props }: PopoverPrimitive.Description.Props) {
|
||||
return (
|
||||
<p
|
||||
<PopoverPrimitive.Description
|
||||
data-slot="popover-description"
|
||||
className={cn("text-muted-foreground", className)}
|
||||
{...props}
|
||||
@@ -78,12 +74,4 @@ function PopoverDescription({
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
PopoverAnchor,
|
||||
PopoverHeader,
|
||||
PopoverTitle,
|
||||
PopoverDescription,
|
||||
};
|
||||
export { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger };
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
import { CircleIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function RadioGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
data-slot="radio-group"
|
||||
className={cn("grid gap-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RadioGroupItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
data-slot="radio-group-item"
|
||||
className={cn(
|
||||
"aspect-square size-4 shrink-0 rounded-full border border-input text-primary shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator
|
||||
data-slot="radio-group-indicator"
|
||||
className="relative flex items-center justify-center"
|
||||
>
|
||||
<CircleIcon className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-primary" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export { RadioGroup, RadioGroupItem };
|
||||
@@ -1,58 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="size-full rounded-[inherit] outline-none transition-[color,box-shadow] focus-visible:outline-1 focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none p-px transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="relative flex-1 rounded-full bg-border"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
);
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar };
|
||||
@@ -1,27 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
import { Select as SelectPrimitive } from "@base-ui/react/select";
|
||||
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Group
|
||||
data-slot="select-group"
|
||||
className={cn("scroll-my-1 p-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Value
|
||||
data-slot="select-value"
|
||||
className={cn("flex flex-1 text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
@@ -29,7 +33,7 @@ function SelectTrigger({
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
}: SelectPrimitive.Trigger.Props & {
|
||||
size?: "sm" | "default";
|
||||
}) {
|
||||
return (
|
||||
@@ -37,15 +41,15 @@ function SelectTrigger({
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"flex w-fit items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[size=default]:h-9 data-[size=sm]:h-8 data-[placeholder]:text-muted-foreground *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:hover:bg-input/50 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
"border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 flex w-fit items-center justify-between gap-1.5 rounded-lg border bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
<SelectPrimitive.Icon
|
||||
render={<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4" />}
|
||||
/>
|
||||
</SelectPrimitive.Trigger>
|
||||
);
|
||||
}
|
||||
@@ -53,88 +57,84 @@ function SelectTrigger({
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "item-aligned",
|
||||
side = "bottom",
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
alignOffset = 0,
|
||||
alignItemWithTrigger = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
}: SelectPrimitive.Popup.Props &
|
||||
Pick<
|
||||
SelectPrimitive.Positioner.Props,
|
||||
"align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
|
||||
>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
position === "popper" &&
|
||||
"data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=bottom]:translate-y-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
<SelectPrimitive.Positioner
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
align={align}
|
||||
{...props}
|
||||
alignOffset={alignOffset}
|
||||
alignItemWithTrigger={alignItemWithTrigger}
|
||||
className="isolate z-50"
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
<SelectPrimitive.Popup
|
||||
data-slot="select-content"
|
||||
data-align-trigger={alignItemWithTrigger}
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
|
||||
"bg-popover text-popover-foreground ring-foreground/10 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-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 relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg shadow-md ring-1 duration-100 data-[align-trigger=true]:animate-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.List>{children}</SelectPrimitive.List>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Popup>
|
||||
</SelectPrimitive.Positioner>
|
||||
</SelectPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
function SelectLabel({ className, ...props }: SelectPrimitive.GroupLabel.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
<SelectPrimitive.GroupLabel
|
||||
data-slot="select-label"
|
||||
className={cn("px-2 py-1.5 text-muted-foreground text-xs", className)}
|
||||
className={cn("text-muted-foreground px-1.5 py-1 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
function SelectItem({ className, children, ...props }: SelectPrimitive.Item.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
data-slot="select-item-indicator"
|
||||
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||
<SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap">
|
||||
{children}
|
||||
</SelectPrimitive.ItemText>
|
||||
<SelectPrimitive.ItemIndicator
|
||||
render={
|
||||
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
|
||||
}
|
||||
>
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
<CheckIcon className="pointer-events-none" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</SelectPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
function SelectSeparator({ className, ...props }: SelectPrimitive.Separator.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -143,36 +143,36 @@ function SelectSeparator({
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
<SelectPrimitive.ScrollUpArrow
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
"bg-popover top-0 z-10 flex w-full cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
<ChevronUpIcon />
|
||||
</SelectPrimitive.ScrollUpArrow>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
<SelectPrimitive.ScrollDownArrow
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
"bg-popover bottom-0 z-10 flex w-full cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
<ChevronDownIcon />
|
||||
</SelectPrimitive.ScrollDownArrow>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
import type * as React from "react";
|
||||
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
function Separator({ className, orientation = "horizontal", ...props }: SeparatorPrimitive.Props) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
<SeparatorPrimitive
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px",
|
||||
"bg-border shrink-0 data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -4,7 +4,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("animate-pulse rounded-md bg-accent", className)}
|
||||
className={cn("bg-muted animate-pulse rounded-md", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import {
|
||||
CircleCheckIcon,
|
||||
InfoIcon,
|
||||
Loader2Icon,
|
||||
OctagonXIcon,
|
||||
TriangleAlertIcon,
|
||||
OctagonXIcon,
|
||||
Loader2Icon,
|
||||
} from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
||||
@@ -32,6 +32,11 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||
"--border-radius": "var(--radius)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast: "cn-toast",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
|
||||
size?: "sm" | "default";
|
||||
}) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=sm]:h-3.5 data-[size=default]:w-8 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"pointer-events-none block rounded-full bg-background ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Switch };
|
||||
@@ -1,91 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
data-orientation={orientation}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const tabsListVariants = cva(
|
||||
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground data-[variant=line]:rounded-none group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-muted",
|
||||
line: "gap-1 bg-transparent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List> &
|
||||
VariantProps<typeof tabsListVariants>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
data-variant={variant}
|
||||
className={cn(tabsListVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-2 py-1 font-medium text-foreground/60 text-sm transition-all hover:text-foreground focus-visible:border-ring focus-visible:outline-1 focus-visible:outline-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
||||
"data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground",
|
||||
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
|
||||
@@ -1,4 +1,4 @@
|
||||
import type * as React from "react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"field-sizing-content flex min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs outline-none transition-[color,box-shadow] placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 disabled:bg-input/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 flex field-sizing-content min-h-16 w-full rounded-lg border bg-transparent px-2.5 py-2 text-base transition-colors outline-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import { toggleVariants } from "@/components/ui/toggle";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants> & {
|
||||
spacing?: number;
|
||||
}
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 0,
|
||||
});
|
||||
|
||||
function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants> & {
|
||||
spacing?: number;
|
||||
}) {
|
||||
return (
|
||||
<ToggleGroupPrimitive.Root
|
||||
data-slot="toggle-group"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
data-spacing={spacing}
|
||||
style={{ "--gap": spacing } as React.CSSProperties}
|
||||
className={cn(
|
||||
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size, spacing }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleGroupItem({
|
||||
className,
|
||||
children,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
const context = React.useContext(ToggleGroupContext);
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
data-slot="toggle-group-item"
|
||||
data-variant={context.variant || variant}
|
||||
data-size={context.size || size}
|
||||
data-spacing={context.spacing}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
|
||||
"data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:first:border-l data-[spacing=0]:first:rounded-l-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem };
|
||||
@@ -1,47 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-[color,box-shadow] hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 min-w-9 px-2",
|
||||
sm: "h-8 min-w-8 px-1.5",
|
||||
lg: "h-10 min-w-10 px-2.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Toggle({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<TogglePrimitive.Root
|
||||
data-slot="toggle"
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Toggle, toggleVariants };
|
||||
@@ -1,59 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import type * as React from "react";
|
||||
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
function TooltipProvider({ delay = 0, ...props }: TooltipPrimitive.Provider.Props) {
|
||||
return <TooltipPrimitive.Provider data-slot="tooltip-provider" delay={delay} {...props} />;
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
);
|
||||
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
|
||||
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
side = "top",
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
alignOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
}: TooltipPrimitive.Popup.Props &
|
||||
Pick<TooltipPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
<TooltipPrimitive.Positioner
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"fade-in-0 zoom-in-95 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in text-balance rounded-md bg-foreground px-3 py-1.5 text-background text-xs data-[state=closed]:animate-out",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
className="isolate z-50"
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
|
||||
</TooltipPrimitive.Content>
|
||||
<TooltipPrimitive.Popup
|
||||
data-slot="tooltip-content"
|
||||
className={cn(
|
||||
"bg-foreground text-background 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-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 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 z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md px-3 py-1.5 text-xs has-data-[slot=kbd]:pr-1.5 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
|
||||
</TooltipPrimitive.Popup>
|
||||
</TooltipPrimitive.Positioner>
|
||||
</TooltipPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,14 +37,7 @@ const Video = ({
|
||||
|
||||
if (extension === "vtt") {
|
||||
return (
|
||||
<track
|
||||
key={file}
|
||||
kind="subtitles"
|
||||
src={file}
|
||||
srcLang="en"
|
||||
label="English"
|
||||
default
|
||||
/>
|
||||
<track key={file} kind="subtitles" src={file} srcLang="en" label="English" default />
|
||||
);
|
||||
} else {
|
||||
return <source key={file} src={file} type={`video/${extension}`} />;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { CountUp } from "@/components/count-up";
|
||||
|
||||
import { CountUp } from "@/components/count-up";
|
||||
import { incrementViews } from "@/lib/server/views";
|
||||
|
||||
const ViewCounter = ({ slug }: { slug: string }) => {
|
||||
@@ -15,8 +15,8 @@ const ViewCounter = ({ slug }: { slug: string }) => {
|
||||
.then((hits) => {
|
||||
setViews(hits);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("[view-counter] error:", error);
|
||||
.catch((err) => {
|
||||
console.error("[view-counter] error:", err);
|
||||
setError(true);
|
||||
});
|
||||
}, [slug]);
|
||||
|
||||
@@ -6,7 +6,6 @@ export default defineConfig({
|
||||
out: "./drizzle",
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
// biome-ignore lint/style/noNonNullAssertion: expected to be set in .env
|
||||
url: process.env.DATABASE_URL!,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { initBotId } from "botid/client/core";
|
||||
|
||||
initBotId({
|
||||
protect: [
|
||||
{
|
||||
path: "/contact",
|
||||
method: "POST",
|
||||
},
|
||||
{
|
||||
path: "/notes/*",
|
||||
method: "POST",
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type BetterAuthOptions, betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { nextCookies } from "better-auth/next-js";
|
||||
|
||||
import { db } from "@/lib/db";
|
||||
import * as schema from "@/lib/db/schema";
|
||||
|
||||
@@ -13,9 +14,7 @@ export const auth = betterAuth({
|
||||
plugins: [nextCookies()],
|
||||
socialProviders: {
|
||||
github: {
|
||||
// biome-ignore lint/style/noNonNullAssertion: expected to be set in env
|
||||
clientId: process.env.AUTH_GITHUB_CLIENT_ID!,
|
||||
// biome-ignore lint/style/noNonNullAssertion: expected to be set in env
|
||||
clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET!,
|
||||
mapProfileToUser: (profile) => ({
|
||||
name: profile.login,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Feed, type Item as FeedItem } from "feed";
|
||||
|
||||
import ogImage from "@/app/opengraph-image.jpg";
|
||||
import authorConfig from "@/lib/config/author";
|
||||
import siteConfig from "@/lib/config/site";
|
||||
|
||||
import { getContent, getFrontMatter } from "@/lib/posts";
|
||||
|
||||
/**
|
||||
@@ -52,10 +52,7 @@ export const buildFeed = async (): Promise<Feed> => {
|
||||
);
|
||||
|
||||
// sort posts reverse chronologically in case the promises resolved out of order
|
||||
posts.sort(
|
||||
(post1, post2) =>
|
||||
new Date(post2.date).getTime() - new Date(post1.date).getTime(),
|
||||
);
|
||||
posts.sort((post1, post2) => new Date(post2.date).getTime() - new Date(post1.date).getTime());
|
||||
|
||||
// officially add each post to the feed
|
||||
posts.forEach((post) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { attachDatabasePool } from "@vercel/functions";
|
||||
import { drizzle } from "drizzle-orm/node-postgres";
|
||||
import { Pool } from "pg";
|
||||
|
||||
import * as schema from "@/lib/db/schema";
|
||||
|
||||
// Create explicit pool instance for better connection management
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import authorConfig from "@/lib/config/author";
|
||||
import siteConfig from "@/lib/config/site";
|
||||
|
||||
export const defaultMetadata: Metadata = {
|
||||
// biome-ignore lint/style/noNonNullAssertion: expected to be set in env
|
||||
metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL!),
|
||||
title: {
|
||||
template: `%s – ${siteConfig.name}`,
|
||||
@@ -49,16 +49,12 @@ export const defaultMetadata: Metadata = {
|
||||
* Helper function to deep merge a page's metadata into the default site metadata
|
||||
* @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata
|
||||
*/
|
||||
export const createMetadata = (
|
||||
metadata: Metadata & { canonical: string },
|
||||
): Metadata => ({
|
||||
export const createMetadata = (metadata: Metadata & { canonical: string }): Metadata => ({
|
||||
...defaultMetadata,
|
||||
...metadata,
|
||||
openGraph: {
|
||||
...defaultMetadata.openGraph,
|
||||
// biome-ignore lint/style/noNonNullAssertion: title is always provided by callers
|
||||
title: metadata.title!,
|
||||
// biome-ignore lint/style/noNonNullAssertion: description is always provided by callers
|
||||
description: metadata.description!,
|
||||
url: metadata.canonical,
|
||||
...metadata.openGraph,
|
||||
|
||||
@@ -2,10 +2,7 @@ import { cacheLife } from "next/cache";
|
||||
|
||||
// Load a Google Font from the Google Fonts API
|
||||
// Adapted from https://github.com/brianlovin/briOS/blob/f72dc33a11194de45c80337b22be4560da62ad7e/src/lib/og-utils.tsx#L32
|
||||
export async function loadGoogleFont(
|
||||
font: string,
|
||||
weight: number,
|
||||
): Promise<ArrayBuffer> {
|
||||
export async function loadGoogleFont(font: string, weight: number): Promise<ArrayBuffer> {
|
||||
"use cache";
|
||||
|
||||
const url = `https://fonts.googleapis.com/css2?family=${font}:wght@${weight}`;
|
||||
@@ -16,9 +13,7 @@ export async function loadGoogleFont(
|
||||
},
|
||||
});
|
||||
const css = await cssResponse.text();
|
||||
const resource = css.match(
|
||||
/src: url\((.+)\) format\('(opentype|truetype)'\)/,
|
||||
);
|
||||
const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/);
|
||||
|
||||
if (resource) {
|
||||
const fontResponse = await fetch(resource[1], {
|
||||
|
||||
24
lib/posts.ts
24
lib/posts.ts
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import glob from "fast-glob";
|
||||
import { decode } from "html-entities";
|
||||
import { unified } from "unified";
|
||||
@@ -40,9 +41,7 @@ export const getSlugs = async (): Promise<string[]> => {
|
||||
});
|
||||
|
||||
// strip the .mdx extensions from filenames
|
||||
const slugs = mdxFiles.map((fileName) =>
|
||||
fileName.replace(/\/index\.mdx$/, ""),
|
||||
);
|
||||
const slugs = mdxFiles.map((fileName) => fileName.replace(/\/index\.mdx$/, ""));
|
||||
|
||||
return slugs;
|
||||
};
|
||||
@@ -92,10 +91,7 @@ export const getFrontMatter: {
|
||||
permalink: `${process.env.NEXT_PUBLIC_BASE_URL}/${POSTS_DIR}/${slug}`,
|
||||
} as FrontMatter;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to load front matter for post with slug "${slug}":`,
|
||||
error,
|
||||
);
|
||||
console.error(`Failed to load front matter for post with slug "${slug}":`, error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -110,8 +106,7 @@ export const getFrontMatter: {
|
||||
|
||||
// sort the results reverse chronologically and return
|
||||
return posts.sort(
|
||||
(post1, post2) =>
|
||||
new Date(post2.date).getTime() - new Date(post1.date).getTime(),
|
||||
(post1, post2) => new Date(post2.date).getTime() - new Date(post1.date).getTime(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,11 +148,7 @@ export const getContent = async (slug: string): Promise<string | undefined> => {
|
||||
],
|
||||
})
|
||||
.use(rehypeStringify)
|
||||
.process(
|
||||
await fs.readFile(
|
||||
path.join(process.cwd(), `${POSTS_DIR}/${slug}/index.mdx`),
|
||||
),
|
||||
);
|
||||
.process(await fs.readFile(path.join(process.cwd(), `${POSTS_DIR}/${slug}/index.mdx`)));
|
||||
|
||||
// convert the parsed content to a string with "safe" HTML
|
||||
return content
|
||||
@@ -166,10 +157,7 @@ export const getContent = async (slug: string): Promise<string | undefined> => {
|
||||
.replaceAll("<p></p>", "")
|
||||
.trim();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to load/parse content for post with slug "${slug}":`,
|
||||
error,
|
||||
);
|
||||
console.error(`Failed to load/parse content for post with slug "${slug}":`, error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,4 +5,3 @@ export { default as rehypeSanitize } from "rehype-sanitize";
|
||||
export { default as rehypeSlug } from "rehype-slug";
|
||||
export { default as rehypeStringify } from "rehype-stringify";
|
||||
export { default as rehypeUnwrapImages } from "rehype-unwrap-images";
|
||||
export { default as rehypeWrapper } from "rehype-wrapper";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user