This commit is contained in:
2026-07-10 22:38:58 +08:00
commit 34c7cf34fd
56 changed files with 16425 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
View File
+10
View File
@@ -0,0 +1,10 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# 已忽略包含查询文件的默认文件夹
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+57
View File
@@ -0,0 +1,57 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/koring-space.iml" filepath="$PROJECT_DIR$/.idea/koring-space.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+4
View File
@@ -0,0 +1,4 @@
singleQuote: false
semi: true
printWidth: 100
trailingComma: none
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+36
View File
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+148
View File
@@ -0,0 +1,148 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
@theme {
--animate-warning-scroll: warning-scroll 1.5s linear infinite;
@keyframes warning-scroll {
from { background-position: 0 0; }
to { background-position: 56.56px 0; }
}
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
--font-heading: var(--font-sans);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
--animate-meteor: meteor 5s linear infinite;
@keyframes meteor {
0% {
transform: rotate(var(--angle)) translateX(0);
opacity: 1;
}
70% {
opacity: 1;
}
100% {
transform: rotate(var(--angle)) translateX(-500px);
opacity: 0;
}
}
}
:root {
--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);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--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);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--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);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
+52
View File
@@ -0,0 +1,52 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Header } from "@/layout/header";
import { Footer } from "@/layout/footer";
import Script from "next/script";
import { cn } from "@/lib/utils";
const alimamaFangYuan = localFont({
src: "../public/font/Alimama.ttf",
variable: "--font-sans",
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "RivFox",
description: "Generated by create next app",
icons: {
icon: "/favicon.svg",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="en"
className={cn("h-full", "antialiased", alimamaFangYuan.variable, geistMono.variable, "font-sans")}
>
<body className="min-h-full flex flex-col">
<Header />
<main className="flex-1 pt-14 px-4 sm:px-10 lg:px-48">
{children}
<Script
src="https://analysis.rivfox.com/api/script.js"
data-site-id="aaedd41de497"
strategy="afterInteractive"
/>
</main>
<Footer/>
</body>
</html>
);
}
+30
View File
@@ -0,0 +1,30 @@
import { useNewsStore } from "@/lib/database";
export default async function NewsPage() {
const newsList = await useNewsStore.findAsync({});
return (
<div className="flex flex-col gap-8">
<h1 className="text-2xl font-bold"></h1>
<div className="grid gap-4">
{newsList.length > 0 ? (
newsList.map((item: any) => (
<div
key={item._id}
className="p-4 border rounded-lg shadow-sm hover:shadow-md transition-shadow"
>
<h2 className="text-lg font-semibold">{item.title || "无标题"}</h2>
<p className="text-gray-500 text-sm mt-2">{item.content || "暂无内容..."}</p>
<div className="text-xs text-gray-400 mt-4">
: {item.createdAt ? new Date(item.createdAt).toLocaleDateString() : "未知"}
</div>
</div>
))
) : (
<p className="text-muted-foreground"></p>
)}
</div>
</div>
);
}
+186
View File
@@ -0,0 +1,186 @@
import { FlickeringGrid } from "@/components/ui/flickering-grid";
import { ArrowUpRight, HammerIcon } from "lucide-react";
import { Marquee, MarqueeContent, MarqueeFade, MarqueeItem } from "@/components/kibo-ui/marquee";
import { Status, StatusIndicator } from "@/components/kibo-ui/status";
import { Meteors } from "@/components/ui/meteors";
import Link from "next/link";
export default function HomePage() {
return (
<>
<div className="flex flex-col w-full h-full gap-4 md:gap-8">
<div className="relative overflow-hidden rounded-xl border border-amber-500/50 group">
{/* 滚动条纹层 */}
<div
className="absolute inset-0 z-0 animate-warning-scroll opacity-100"
style={{
backgroundImage:
"repeating-linear-gradient(-45deg, #f59e0b, #f59e0b 20px, #000 20px, #000 40px)",
backgroundSize: "56.56px 56.56px"
}}
/>
{/* 内容层 */}
<div className="relative z-10 flex items-center justify-center gap-4 p-3 bg-amber-500/80">
<div className="flex items-center gap-2 px-3 py-1 bg-black rounded-full text-amber-500">
<HammerIcon className="size-4" />
<span className="text-[10px] font-black tracking-widest"></span>
</div>
<p className="text-black font-bold text-sm tracking-tight">Koring Launcher UI </p>
</div>
</div>
<div className="relative w-full h-120 overflow-hidden">
<div className="relative rounded-xl border w-full h-full">
<div className="absolute left-12 top-12 flex flex-col gap-2">
<span className="font-bold text-5xl">Koring Team</span>
<span className="text-xl"></span>
</div>
<span className="absolute right-12 bottom-12 text-xl text-muted-foreground">
<span className="text-sm">
<br />
</span>
</span>
<FlickeringGrid
className="relative inset-0 z-0 mask-[radial-gradient(700px_circle_at_center,white,transparent)]"
squareSize={6}
gridGap={14}
color="#6b9eb6"
maxOpacity={0.7}
flickerChance={0.1}
/>
</div>
</div>
<Link
href="/launcher"
className="relative flex w-full flex-col overflow-hidden rounded-md border p-6 group dark text-white bg-[#0A0A0AFF]"
>
<div className="flex items-center justify-between gap-4">
...
<ArrowUpRight className="size-4 -translate-x-2 translate-y-2 opacity-0 transition-all group-hover:translate-x-0 group-hover:translate-y-0 group-hover:opacity-100" />
</div>
<div className="mt-4">
<h3 className="mb-1 font-semibold">Koring Launcher</h3>
<p className="text-sm text-muted-foreground"> electron Minecraft </p>
</div>
<Meteors number={28} />
</Link>
<div className="py-8 font-bold text-xl">
<Marquee>
<MarqueeFade side="left" />
<MarqueeFade side="right" />
<MarqueeContent>
<MarqueeItem className="w-32">Launcher</MarqueeItem>
<MarqueeItem className="w-32">Account</MarqueeItem>
<MarqueeItem className="w-32">Play服务</MarqueeItem>
<MarqueeItem className="w-32">.</MarqueeItem>
<MarqueeItem className="w-32">SkinEditer</MarqueeItem>
<MarqueeItem className="w-32"></MarqueeItem>
</MarqueeContent>
</Marquee>
</div>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
<a
href="https://docs.play.lenjing.work"
target="_blank"
className="group rounded-md border border-border p-6"
>
<div className="flex items-center justify-between gap-4">
<b></b>
<ArrowUpRight className="size-4 -translate-x-2 translate-y-2 opacity-0 transition-all group-hover:translate-x-0 group-hover:translate-y-0 group-hover:opacity-100" />
</div>
<div className="mt-4">
<h3 className="mb-1 font-semibold">Sanshe Play</h3>
<p className="text-sm text-muted-foreground">Minecraft </p>
</div>
</a>
<a
href="/launcher"
className="group rounded-md border border-border p-6"
>
<div className="flex items-center justify-between gap-4">
<b></b>
<Status status="maintenance">
<StatusIndicator />
UI
</Status>
{/*<ArrowUpRight className="size-4 -translate-x-2 translate-y-2 opacity-0 transition-all group-hover:translate-x-0 group-hover:translate-y-0 group-hover:opacity-100" />*/}
</div>
<div className="mt-4">
<h3 className="mb-1 font-semibold">Koring Launcher</h3>
<p className="text-sm text-muted-foreground"> electron Minecraft </p>
</div>
</a>
<a className="group rounded-md border border-border p-6">
<div className="flex items-center justify-between gap-4">
<b></b>
<Status status="degraded">
<StatusIndicator />
</Status>
{/*<ArrowUpRight className="size-4 -translate-x-2 translate-y-2 opacity-0 transition-all group-hover:translate-x-0 group-hover:translate-y-0 group-hover:opacity-100" />*/}
</div>
<div className="mt-4">
<h3 className="mb-1 font-semibold">SkinEditer</h3>
<p className="text-sm text-muted-foreground"> Tauri Minecraft </p>
</div>
</a>
<a className="group rounded-md border border-border p-6">
<div className="flex items-center justify-between gap-4">
<b></b>
<Status status="degraded">
<StatusIndicator />
</Status>
{/*<ArrowUpRight className="size-4 -translate-x-2 translate-y-2 opacity-0 transition-all group-hover:translate-x-0 group-hover:translate-y-0 group-hover:opacity-100" />*/}
</div>
<div className="mt-4">
<h3 className="mb-1 font-semibold">SkinEditer</h3>
<p className="text-sm text-muted-foreground"> Tauri Minecraft </p>
</div>
</a>
<a className="group rounded-md border border-border p-6">
<div className="flex items-center justify-between gap-4">
<b></b>
<Status status="degraded">
<StatusIndicator />
</Status>
{/*<ArrowUpRight className="size-4 -translate-x-2 translate-y-2 opacity-0 transition-all group-hover:translate-x-0 group-hover:translate-y-0 group-hover:opacity-100" />*/}
</div>
<div className="mt-4">
<h3 className="mb-1 font-semibold">Koring Foreign Exchange Fund</h3>
<p className="text-sm text-muted-foreground">100 RMB </p>
</div>
</a>
<a
href="https://mchine.space"
target="_blank"
className="group rounded-md border border-border p-6"
>
<div className="flex items-center justify-between gap-4">
<b>.</b>
<ArrowUpRight className="size-4 -translate-x-2 translate-y-2 opacity-0 transition-all group-hover:translate-x-0 group-hover:translate-y-0 group-hover:opacity-100" />
</div>
<div className="mt-4">
<h3 className="mb-1 font-semibold">MChine Space</h3>
<p className="text-sm text-muted-foreground">Minecraft </p>
</div>
</a>
</div>
</div>
</>
);
}
+14
View File
@@ -0,0 +1,14 @@
import { useNewsStore } from "@/lib/database";
import NewsEditor from "@/components/news-editor";
export default async function NewsPage() {
const newsList = await useNewsStore.findAsync({});
return (
<div className="flex flex-col h-[calc(100vh-25vh)]">
<div className="flex-1">213</div>
<NewsEditor className="max-h-52" />
</div>
);
}
+2028
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "radix-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"menuColor": "default",
"menuAccent": "subtle",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@magicui": "https://magicui.design/r/{name}",
"@kibo-ui": "https://www.kibo-ui.com/r/{name}.json"
}
}
+142
View File
@@ -0,0 +1,142 @@
"use client";
import { useControllableState } from "@radix-ui/react-use-controllable-state";
import { type LucideIcon, XIcon } from "lucide-react";
import {
type ComponentProps,
createContext,
type HTMLAttributes,
type MouseEventHandler,
useContext,
} from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
type BannerContextProps = {
show: boolean;
setShow: (show: boolean) => void;
};
export const BannerContext = createContext<BannerContextProps>({
show: true,
setShow: () => {},
});
export type BannerProps = HTMLAttributes<HTMLDivElement> & {
visible?: boolean;
defaultVisible?: boolean;
onClose?: () => void;
inset?: boolean;
};
export const Banner = ({
children,
visible,
defaultVisible = true,
onClose,
className,
inset = false,
...props
}: BannerProps) => {
const [show, setShow] = useControllableState({
defaultProp: defaultVisible,
prop: visible,
onChange: onClose,
});
if (!show) {
return null;
}
return (
<BannerContext.Provider value={{ show, setShow }}>
<div
className={cn(
"flex w-full items-center justify-between gap-2 bg-primary px-4 py-2 text-primary-foreground",
inset && "rounded-lg",
className
)}
{...props}
>
{children}
</div>
</BannerContext.Provider>
);
};
export type BannerIconProps = HTMLAttributes<HTMLDivElement> & {
icon: LucideIcon;
};
export const BannerIcon = ({
icon: Icon,
className,
...props
}: BannerIconProps) => (
<div
className={cn(
"rounded-full border border-background/20 bg-background/10 p-1 shadow-sm",
className
)}
{...props}
>
<Icon size={16} />
</div>
);
export type BannerTitleProps = HTMLAttributes<HTMLParagraphElement>;
export const BannerTitle = ({ className, ...props }: BannerTitleProps) => (
<p className={cn("flex-1 text-sm", className)} {...props} />
);
export type BannerActionProps = ComponentProps<typeof Button>;
export const BannerAction = ({
variant = "outline",
size = "sm",
className,
...props
}: BannerActionProps) => (
<Button
className={cn(
"shrink-0 bg-transparent hover:bg-background/10 hover:text-background",
className
)}
size={size}
variant={variant}
{...props}
/>
);
export type BannerCloseProps = ComponentProps<typeof Button>;
export const BannerClose = ({
variant = "ghost",
size = "icon",
onClick,
className,
...props
}: BannerCloseProps) => {
const { setShow } = useContext(BannerContext);
const handleClick: MouseEventHandler<HTMLButtonElement> = (e) => {
setShow(false);
onClick?.(e);
};
return (
<Button
className={cn(
"shrink-0 bg-transparent hover:bg-background/10 hover:text-background",
className
)}
onClick={handleClick}
size={size}
variant={variant}
{...props}
>
<XIcon size={18} />
</Button>
);
};
File diff suppressed because it is too large Load Diff
+59
View File
@@ -0,0 +1,59 @@
"use client";
import type { HTMLAttributes } from "react";
import type { MarqueeProps as FastMarqueeProps } from "react-fast-marquee";
import FastMarquee from "react-fast-marquee";
import { cn } from "@/lib/utils";
export type MarqueeProps = HTMLAttributes<HTMLDivElement>;
export const Marquee = ({ className, ...props }: MarqueeProps) => (
<div
className={cn("relative w-full overflow-hidden", className)}
{...props}
/>
);
export type MarqueeContentProps = FastMarqueeProps;
export const MarqueeContent = ({
loop = 0,
autoFill = true,
pauseOnHover = true,
...props
}: MarqueeContentProps) => (
<FastMarquee
autoFill={autoFill}
loop={loop}
pauseOnHover={pauseOnHover}
{...props}
/>
);
export type MarqueeFadeProps = HTMLAttributes<HTMLDivElement> & {
side: "left" | "right";
};
export const MarqueeFade = ({
className,
side,
...props
}: MarqueeFadeProps) => (
<div
className={cn(
"absolute top-0 bottom-0 z-10 h-full w-24 from-background to-transparent",
side === "left" ? "left-0 bg-gradient-to-r" : "right-0 bg-gradient-to-l",
className
)}
{...props}
/>
);
export type MarqueeItemProps = HTMLAttributes<HTMLDivElement>;
export const MarqueeItem = ({ className, ...props }: MarqueeItemProps) => (
<div
className={cn("mx-2 flex-shrink-0 object-contain", className)}
{...props}
/>
);
+62
View File
@@ -0,0 +1,62 @@
import type { ComponentProps, HTMLAttributes } from "react";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
export type StatusProps = ComponentProps<typeof Badge> & {
status: "online" | "offline" | "maintenance" | "degraded";
};
export const Status = ({ className, status, ...props }: StatusProps) => (
<Badge
className={cn("flex items-center gap-2", "group", status, className)}
variant="secondary"
{...props}
/>
);
export type StatusIndicatorProps = HTMLAttributes<HTMLSpanElement>;
export const StatusIndicator = ({
className,
...props
}: StatusIndicatorProps) => (
<span className="relative flex h-2 w-2" {...props}>
<span
className={cn(
"absolute inline-flex h-full w-full animate-ping rounded-full opacity-75",
"group-[.online]:bg-emerald-500",
"group-[.offline]:bg-red-500",
"group-[.maintenance]:bg-blue-500",
"group-[.degraded]:bg-amber-500"
)}
/>
<span
className={cn(
"relative inline-flex h-2 w-2 rounded-full",
"group-[.online]:bg-emerald-500",
"group-[.offline]:bg-red-500",
"group-[.maintenance]:bg-blue-500",
"group-[.degraded]:bg-amber-500"
)}
/>
</span>
);
export type StatusLabelProps = HTMLAttributes<HTMLSpanElement>;
export const StatusLabel = ({
className,
children,
...props
}: StatusLabelProps) => (
<span className={cn("text-muted-foreground", className)} {...props}>
{children ?? (
<>
<span className="hidden group-[.online]:block">Online</span>
<span className="hidden group-[.offline]:block">Offline</span>
<span className="hidden group-[.maintenance]:block">Maintenance</span>
<span className="hidden group-[.degraded]:block">Degraded</span>
</>
)}
</span>
);
+127
View File
@@ -0,0 +1,127 @@
"use client";
import type { Editor, JSONContent } from "@/components/kibo-ui/editor";
import {
EditorBubbleMenu,
EditorCharacterCount,
EditorClearFormatting,
EditorFloatingMenu,
EditorFormatBold,
EditorFormatCode,
EditorFormatItalic,
EditorFormatStrike,
EditorFormatSubscript,
EditorFormatSuperscript,
EditorFormatUnderline,
EditorLinkSelector,
EditorNodeBulletList,
EditorNodeCode,
EditorNodeHeading1,
EditorNodeHeading2,
EditorNodeHeading3,
EditorNodeOrderedList,
EditorNodeQuote,
EditorNodeTable,
EditorNodeTaskList,
EditorNodeText,
EditorProvider,
EditorSelector,
EditorTableColumnAfter,
EditorTableColumnBefore,
EditorTableColumnDelete,
EditorTableColumnMenu,
EditorTableDelete,
EditorTableFix,
EditorTableGlobalMenu,
EditorTableHeaderColumnToggle,
EditorTableHeaderRowToggle,
EditorTableMenu,
EditorTableMergeCells,
EditorTableRowAfter,
EditorTableRowBefore,
EditorTableRowDelete,
EditorTableRowMenu,
EditorTableSplitCell
} from "@/components/kibo-ui/editor";
import { useState } from "react";
import { cn } from "@/lib/utils";
interface NewsEditorProps {
className?: string
}
export default function NewsEditor({ className }: NewsEditorProps) {
const [content, setContent] = useState<JSONContent>({
type: "doc",
content: []
});
const handleUpdate = ({ editor }: { editor: Editor }) => {
const json = editor.getJSON();
setContent(json);
console.log(JSON.stringify(json));
};
return (
<EditorProvider
className={cn("h-full w-full overflow-y-auto rounded-lg border bg-background p-4", className)}
content={content}
onUpdate={handleUpdate}
placeholder="Start typing..."
>
<EditorFloatingMenu>
<EditorNodeHeading1 hideName />
<EditorNodeBulletList hideName />
<EditorNodeQuote hideName />
<EditorNodeCode hideName />
<EditorNodeTable hideName />
</EditorFloatingMenu>
<EditorBubbleMenu>
<EditorSelector title="Text">
<EditorNodeText />
<EditorNodeHeading1 />
<EditorNodeHeading2 />
<EditorNodeHeading3 />
<EditorNodeBulletList />
<EditorNodeOrderedList />
<EditorNodeTaskList />
<EditorNodeQuote />
<EditorNodeCode />
</EditorSelector>
<EditorSelector title="Format">
<EditorFormatBold />
<EditorFormatItalic />
<EditorFormatUnderline />
<EditorFormatStrike />
<EditorFormatCode />
<EditorFormatSuperscript />
<EditorFormatSubscript />
</EditorSelector>
<EditorLinkSelector />
<EditorClearFormatting />
</EditorBubbleMenu>
<EditorTableMenu>
<EditorTableColumnMenu>
<EditorTableColumnBefore />
<EditorTableColumnAfter />
<EditorTableColumnDelete />
</EditorTableColumnMenu>
<EditorTableRowMenu>
<EditorTableRowBefore />
<EditorTableRowAfter />
<EditorTableRowDelete />
</EditorTableRowMenu>
<EditorTableGlobalMenu>
<EditorTableHeaderColumnToggle />
<EditorTableHeaderRowToggle />
<EditorTableDelete />
<EditorTableMergeCells />
<EditorTableSplitCell />
<EditorTableFix />
</EditorTableGlobalMenu>
</EditorTableMenu>
<EditorCharacterCount.Words>Words: </EditorCharacterCount.Words>
</EditorProvider>
);
};
+49
View File
@@ -0,0 +1,49 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"group/badge 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:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
{
variants: {
variant: {
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/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: {
variant: "default",
},
}
)
function Badge({
className,
variant = "default",
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span"
return (
<Comp
data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }
+67
View File
@@ -0,0 +1,67 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"group/button 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:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline:
"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 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"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-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: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }
+195
View File
@@ -0,0 +1,195 @@
"use client"
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { cn } from "@/lib/utils"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
InputGroup,
InputGroupAddon,
} from "@/components/ui/input-group"
import { SearchIcon, CheckIcon } from "lucide-react"
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"flex size-full flex-col overflow-hidden rounded-xl! bg-popover p-1 text-popover-foreground",
className
)}
{...props}
/>
)
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = false,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
className?: string
showCloseButton?: boolean
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn(
"top-1/3 translate-y-0 overflow-hidden rounded-xl! p-0",
className
)}
showCloseButton={showCloseButton}
>
{children}
</DialogContent>
</Dialog>
)
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div data-slot="command-input-wrapper" className="p-1 pb-0">
<InputGroup className="h-8! rounded-lg! border-input/30 bg-input/30 shadow-none! *:data-[slot=input-group-addon]:pl-2!">
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
<InputGroupAddon>
<SearchIcon className="size-4 shrink-0 opacity-50" />
</InputGroupAddon>
</InputGroup>
</div>
)
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"no-scrollbar max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto outline-none",
className
)}
{...props}
/>
)
}
function CommandEmpty({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className={cn("py-6 text-center text-sm", className)}
{...props}
/>
)
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
)
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
)
}
function CommandItem({
className,
children,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"group/command-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-selected:bg-muted data-selected:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-selected:*:[svg]:text-foreground",
className
)}
{...props}
>
{children}
<CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
</CommandPrimitive.Item>
)
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground group-data-selected/command-item:text-foreground",
className
)}
{...props}
/>
)
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}
+168
View File
@@ -0,0 +1,168 @@
"use client"
import * as React from "react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"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 bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close data-slot="dialog-close" asChild>
<Button
variant="ghost"
className="absolute top-2 right-2"
size="icon-sm"
>
<XIcon
/>
<span className="sr-only">Close</span>
</Button>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
</DialogPrimitive.Close>
)}
</div>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn(
"font-heading text-base leading-none font-medium",
className
)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn(
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className
)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}
+269
View File
@@ -0,0 +1,269 @@
"use client"
import * as React from "react"
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { CheckIcon, ChevronRightIcon } from "lucide-react"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.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 DropdownMenuContent({
className,
align = "start",
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
align={align}
className={cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 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-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 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-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-checkbox-item-indicator"
>
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon
/>
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-radio-item-indicator"
>
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon
/>
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
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 items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn("z-50 min-w-[96px] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 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", className )}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}
+198
View File
@@ -0,0 +1,198 @@
"use client"
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { cn } from "@/lib/utils"
interface FlickeringGridProps extends React.HTMLAttributes<HTMLDivElement> {
squareSize?: number
gridGap?: number
flickerChance?: number
color?: string
width?: number
height?: number
className?: string
maxOpacity?: number
}
export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
squareSize = 4,
gridGap = 6,
flickerChance = 0.3,
color = "rgb(0, 0, 0)",
width,
height,
className,
maxOpacity = 0.3,
...props
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const [isInView, setIsInView] = useState(false)
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 })
const memoizedColor = useMemo(() => {
const toRGBA = (color: string) => {
if (typeof window === "undefined") {
return `rgba(0, 0, 0,`
}
const canvas = document.createElement("canvas")
canvas.width = canvas.height = 1
const ctx = canvas.getContext("2d")
if (!ctx) return "rgba(255, 0, 0,"
ctx.fillStyle = color
ctx.fillRect(0, 0, 1, 1)
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data)
return `rgba(${r}, ${g}, ${b},`
}
return toRGBA(color)
}, [color])
const setupCanvas = useCallback(
(canvas: HTMLCanvasElement, width: number, height: number) => {
const dpr = window.devicePixelRatio || 1
canvas.width = width * dpr
canvas.height = height * dpr
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`
const cols = Math.ceil(width / (squareSize + gridGap))
const rows = Math.ceil(height / (squareSize + gridGap))
const squares = new Float32Array(cols * rows)
for (let i = 0; i < squares.length; i++) {
squares[i] = Math.random() * maxOpacity
}
return { cols, rows, squares, dpr }
},
[squareSize, gridGap, maxOpacity]
)
const updateSquares = useCallback(
(squares: Float32Array, deltaTime: number) => {
for (let i = 0; i < squares.length; i++) {
if (Math.random() < flickerChance * deltaTime) {
squares[i] = Math.random() * maxOpacity
}
}
},
[flickerChance, maxOpacity]
)
const drawGrid = useCallback(
(
ctx: CanvasRenderingContext2D,
width: number,
height: number,
cols: number,
rows: number,
squares: Float32Array,
dpr: number
) => {
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = "transparent"
ctx.fillRect(0, 0, width, height)
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const opacity = squares[i * rows + j]
ctx.fillStyle = `${memoizedColor}${opacity})`
ctx.fillRect(
i * (squareSize + gridGap) * dpr,
j * (squareSize + gridGap) * dpr,
squareSize * dpr,
squareSize * dpr
)
}
}
},
[memoizedColor, squareSize, gridGap]
)
useEffect(() => {
const canvas = canvasRef.current
const container = containerRef.current
const ctx = canvas?.getContext("2d") ?? null
let animationFrameId: number | null = null
let resizeObserver: ResizeObserver | null = null
let intersectionObserver: IntersectionObserver | null = null
let gridParams: ReturnType<typeof setupCanvas> | null = null
if (canvas && container && ctx) {
const updateCanvasSize = () => {
const newWidth = width || container.clientWidth
const newHeight = height || container.clientHeight
setCanvasSize({ width: newWidth, height: newHeight })
gridParams = setupCanvas(canvas, newWidth, newHeight)
}
updateCanvasSize()
let lastTime = 0
const animate = (time: number) => {
if (!isInView || !gridParams) return
const deltaTime = (time - lastTime) / 1000
lastTime = time
updateSquares(gridParams.squares, deltaTime)
drawGrid(
ctx,
canvas.width,
canvas.height,
gridParams.cols,
gridParams.rows,
gridParams.squares,
gridParams.dpr
)
animationFrameId = requestAnimationFrame(animate)
}
resizeObserver = new ResizeObserver(() => {
updateCanvasSize()
})
resizeObserver.observe(container)
intersectionObserver = new IntersectionObserver(
([entry]) => {
setIsInView(entry.isIntersecting)
},
{ threshold: 0 }
)
intersectionObserver.observe(canvas)
if (isInView) {
animationFrameId = requestAnimationFrame(animate)
}
}
return () => {
if (animationFrameId !== null) {
cancelAnimationFrame(animationFrameId)
}
if (resizeObserver) {
resizeObserver.disconnect()
}
if (intersectionObserver) {
intersectionObserver.disconnect()
}
}
}, [setupCanvas, updateSquares, drawGrid, width, height, isInView])
return (
<div
ref={containerRef}
className={cn(`h-full w-full ${className}`)}
{...props}
>
<canvas
ref={canvasRef}
className="pointer-events-none"
style={{
width: canvasSize.width,
height: canvasSize.height,
}}
/>
</div>
)
}
+102
View File
@@ -0,0 +1,102 @@
"use client"
import { useLayoutEffect, useRef } from "react"
import type React from "react"
import { useInView } from "motion/react"
import { annotate } from "rough-notation"
import { type RoughAnnotation } from "rough-notation/lib/model"
type AnnotationAction =
| "highlight"
| "underline"
| "box"
| "circle"
| "strike-through"
| "crossed-off"
| "bracket"
interface HighlighterProps {
children: React.ReactNode
action?: AnnotationAction
color?: string
strokeWidth?: number
animationDuration?: number
iterations?: number
padding?: number
multiline?: boolean
isView?: boolean
}
export function Highlighter({
children,
action = "highlight",
color = "#ffd1dc",
strokeWidth = 1.5,
animationDuration = 600,
iterations = 2,
padding = 2,
multiline = true,
isView = false,
}: HighlighterProps) {
const elementRef = useRef<HTMLSpanElement>(null)
const isInView = useInView(elementRef, {
once: true,
margin: "-10%",
})
// If isView is false, always show. If isView is true, wait for inView
const shouldShow = !isView || isInView
useLayoutEffect(() => {
const element = elementRef.current
let annotation: RoughAnnotation | null = null
let resizeObserver: ResizeObserver | null = null
if (shouldShow && element) {
const annotationConfig = {
type: action,
color,
strokeWidth,
animationDuration,
iterations,
padding,
multiline,
}
const currentAnnotation = annotate(element, annotationConfig)
annotation = currentAnnotation
currentAnnotation.show()
resizeObserver = new ResizeObserver(() => {
currentAnnotation.hide()
currentAnnotation.show()
})
resizeObserver.observe(element)
resizeObserver.observe(document.body)
}
return () => {
annotation?.remove()
if (resizeObserver) {
resizeObserver.disconnect()
}
}
}, [
shouldShow,
action,
color,
strokeWidth,
animationDuration,
iterations,
padding,
multiline,
])
return (
<span ref={elementRef} className="relative inline-block bg-transparent">
{children}
</span>
)
}
+156
View File
@@ -0,0 +1,156 @@
"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
className
)}
{...props}
/>
)
}
const inputGroupAddonVariants = cva(
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
{
variants: {
align: {
"inline-start":
"order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
"inline-end":
"order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
"block-start":
"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
"block-end":
"order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
},
},
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()
}}
{...props}
/>
)
}
const inputGroupButtonVariants = cva(
"flex items-center gap-2 text-sm shadow-none",
{
variants: {
size: {
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: "",
"icon-xs":
"size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
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-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
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 ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled: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-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
className
)}
{...props}
/>
)
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
}
+19
View File
@@ -0,0 +1,19 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input 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 file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }
+61
View File
@@ -0,0 +1,61 @@
"use client"
import React, { useEffect, useState } from "react"
import { cn } from "@/lib/utils"
interface MeteorsProps {
number?: number
minDelay?: number
maxDelay?: number
minDuration?: number
maxDuration?: number
angle?: number
className?: string
}
export const Meteors = ({
number = 20,
minDelay = 0.2,
maxDelay = 1.2,
minDuration = 2,
maxDuration = 10,
angle = 215,
className,
}: MeteorsProps) => {
const [meteorStyles, setMeteorStyles] = useState<Array<React.CSSProperties>>(
[]
)
useEffect(() => {
const styles = [...new Array(number)].map(() => ({
"--angle": -angle + "deg",
top: "-5%",
left: `calc(0% + ${Math.floor(Math.random() * window.innerWidth)}px)`,
animationDelay: Math.random() * (maxDelay - minDelay) + minDelay + "s",
animationDuration:
Math.floor(Math.random() * (maxDuration - minDuration) + minDuration) +
"s",
}))
setMeteorStyles(styles)
}, [number, minDelay, maxDelay, minDuration, maxDuration, angle])
return (
<>
{[...meteorStyles].map((style, idx) => (
// Meteor Head
<span
key={idx}
style={{ ...style }}
className={cn(
"animate-meteor pointer-events-none absolute size-0.5 rotate-(--angle) rounded-full bg-zinc-500 shadow-[0_0_0_1px_#ffffff10]",
className
)}
>
{/* Meteor Tail */}
<div className="pointer-events-none absolute top-1/2 -z-10 h-px w-12.5 -translate-y-1/2 bg-linear-to-r from-zinc-500 to-transparent" />
</span>
))}
</>
)
}
+89
View File
@@ -0,0 +1,89 @@
"use client"
import * as React from "react"
import { Popover as PopoverPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 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",
className
)}
{...props}
/>
</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-0.5 text-sm", className)}
{...props}
/>
)
}
function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
return (
<div
data-slot="popover-title"
className={cn("font-heading font-medium", className)}
{...props}
/>
)
}
function PopoverDescription({
className,
...props
}: React.ComponentProps<"p">) {
return (
<p
data-slot="popover-description"
className={cn("text-muted-foreground", className)}
{...props}
/>
)
}
export {
Popover,
PopoverAnchor,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
}
+28
View File
@@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import { Separator as SeparatorPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className
)}
{...props}
/>
)
}
export { Separator }
+445
View File
@@ -0,0 +1,445 @@
"use client"
import { memo } from "react"
import {
AnimatePresence,
motion,
Variants,
type DOMMotionComponents,
type MotionProps,
} from "motion/react"
import { cn } from "@/lib/utils"
type AnimationType = "text" | "word" | "character" | "line"
type AnimationVariant =
| "fadeIn"
| "blurIn"
| "blurInUp"
| "blurInDown"
| "slideUp"
| "slideDown"
| "slideLeft"
| "slideRight"
| "scaleUp"
| "scaleDown"
const motionElements = {
article: motion.article,
div: motion.div,
h1: motion.h1,
h2: motion.h2,
h3: motion.h3,
h4: motion.h4,
h5: motion.h5,
h6: motion.h6,
li: motion.li,
p: motion.p,
section: motion.section,
span: motion.span,
} as const
type MotionElementType = Extract<
keyof DOMMotionComponents,
keyof typeof motionElements
>
interface TextAnimateProps extends Omit<MotionProps, "children"> {
/**
* The text content to animate
*/
children: string
/**
* The class name to be applied to the component
*/
className?: string
/**
* The class name to be applied to each segment
*/
segmentClassName?: string
/**
* The delay before the animation starts
*/
delay?: number
/**
* The duration of the animation
*/
duration?: number
/**
* Custom motion variants for the animation
*/
variants?: Variants
/**
* The element type to render
*/
as?: MotionElementType
/**
* How to split the text ("text", "word", "character")
*/
by?: AnimationType
/**
* Whether to start animation when component enters viewport
*/
startOnView?: boolean
/**
* Whether to animate only once
*/
once?: boolean
/**
* The animation preset to use
*/
animation?: AnimationVariant
/**
* Whether to enable accessibility features (default: true)
*/
accessible?: boolean
}
const staggerTimings: Record<AnimationType, number> = {
text: 0.06,
word: 0.05,
character: 0.03,
line: 0.06,
}
const defaultContainerVariants = {
hidden: { opacity: 1 },
show: {
opacity: 1,
transition: {
delayChildren: 0,
staggerChildren: 0.05,
},
},
exit: {
opacity: 0,
transition: {
staggerChildren: 0.05,
staggerDirection: -1,
},
},
}
const defaultItemVariants: Variants = {
hidden: { opacity: 0 },
show: {
opacity: 1,
},
exit: {
opacity: 0,
},
}
const defaultItemAnimationVariants: Record<
AnimationVariant,
{ container: Variants; item: Variants }
> = {
fadeIn: {
container: defaultContainerVariants,
item: {
hidden: { opacity: 0, y: 20 },
show: {
opacity: 1,
y: 0,
transition: {
duration: 0.3,
},
},
exit: {
opacity: 0,
y: 20,
transition: { duration: 0.3 },
},
},
},
blurIn: {
container: defaultContainerVariants,
item: {
hidden: { opacity: 0, filter: "blur(10px)" },
show: {
opacity: 1,
filter: "blur(0px)",
transition: {
duration: 0.3,
},
},
exit: {
opacity: 0,
filter: "blur(10px)",
transition: { duration: 0.3 },
},
},
},
blurInUp: {
container: defaultContainerVariants,
item: {
hidden: { opacity: 0, filter: "blur(10px)", y: 20 },
show: {
opacity: 1,
filter: "blur(0px)",
y: 0,
transition: {
y: { duration: 0.3 },
opacity: { duration: 0.4 },
filter: { duration: 0.3 },
},
},
exit: {
opacity: 0,
filter: "blur(10px)",
y: 20,
transition: {
y: { duration: 0.3 },
opacity: { duration: 0.4 },
filter: { duration: 0.3 },
},
},
},
},
blurInDown: {
container: defaultContainerVariants,
item: {
hidden: { opacity: 0, filter: "blur(10px)", y: -20 },
show: {
opacity: 1,
filter: "blur(0px)",
y: 0,
transition: {
y: { duration: 0.3 },
opacity: { duration: 0.4 },
filter: { duration: 0.3 },
},
},
},
},
slideUp: {
container: defaultContainerVariants,
item: {
hidden: { y: 20, opacity: 0 },
show: {
y: 0,
opacity: 1,
transition: {
duration: 0.3,
},
},
exit: {
y: -20,
opacity: 0,
transition: {
duration: 0.3,
},
},
},
},
slideDown: {
container: defaultContainerVariants,
item: {
hidden: { y: -20, opacity: 0 },
show: {
y: 0,
opacity: 1,
transition: { duration: 0.3 },
},
exit: {
y: 20,
opacity: 0,
transition: { duration: 0.3 },
},
},
},
slideLeft: {
container: defaultContainerVariants,
item: {
hidden: { x: 20, opacity: 0 },
show: {
x: 0,
opacity: 1,
transition: { duration: 0.3 },
},
exit: {
x: -20,
opacity: 0,
transition: { duration: 0.3 },
},
},
},
slideRight: {
container: defaultContainerVariants,
item: {
hidden: { x: -20, opacity: 0 },
show: {
x: 0,
opacity: 1,
transition: { duration: 0.3 },
},
exit: {
x: 20,
opacity: 0,
transition: { duration: 0.3 },
},
},
},
scaleUp: {
container: defaultContainerVariants,
item: {
hidden: { scale: 0.5, opacity: 0 },
show: {
scale: 1,
opacity: 1,
transition: {
duration: 0.3,
scale: {
type: "spring",
damping: 15,
stiffness: 300,
},
},
},
exit: {
scale: 0.5,
opacity: 0,
transition: { duration: 0.3 },
},
},
},
scaleDown: {
container: defaultContainerVariants,
item: {
hidden: { scale: 1.5, opacity: 0 },
show: {
scale: 1,
opacity: 1,
transition: {
duration: 0.3,
scale: {
type: "spring",
damping: 15,
stiffness: 300,
},
},
},
exit: {
scale: 1.5,
opacity: 0,
transition: { duration: 0.3 },
},
},
},
}
const TextAnimateBase = ({
children,
delay = 0,
duration = 0.3,
variants,
className,
segmentClassName,
as: Component = "p",
startOnView = true,
once = false,
by = "word",
animation = "fadeIn",
accessible = true,
...props
}: TextAnimateProps) => {
const MotionComponent = motionElements[Component]
let segments: string[] = []
switch (by) {
case "word":
segments = children.split(/(\s+)/)
break
case "character":
segments = children.split("")
break
case "line":
segments = children.split("\n")
break
case "text":
default:
segments = [children]
break
}
const finalVariants = variants
? {
container: {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
opacity: { duration: 0.01, delay },
delayChildren: delay,
staggerChildren: duration / segments.length,
},
},
exit: {
opacity: 0,
transition: {
staggerChildren: duration / segments.length,
staggerDirection: -1,
},
},
},
item: variants,
}
: animation
? {
container: {
...defaultItemAnimationVariants[animation].container,
show: {
...defaultItemAnimationVariants[animation].container.show,
transition: {
delayChildren: delay,
staggerChildren: duration / segments.length,
},
},
exit: {
...defaultItemAnimationVariants[animation].container.exit,
transition: {
staggerChildren: duration / segments.length,
staggerDirection: -1,
},
},
},
item: defaultItemAnimationVariants[animation].item,
}
: { container: defaultContainerVariants, item: defaultItemVariants }
return (
<AnimatePresence mode="popLayout">
<MotionComponent
variants={finalVariants.container as Variants}
initial="hidden"
whileInView={startOnView ? "show" : undefined}
animate={startOnView ? undefined : "show"}
exit="exit"
className={cn("whitespace-pre-wrap", className)}
viewport={{ once }}
aria-label={accessible ? children : undefined}
{...props}
>
{accessible && <span className="sr-only">{children}</span>}
{segments.map((segment, i) => (
<motion.span
key={`${by}-${segment}-${i}`}
variants={finalVariants.item}
custom={i * staggerTimings[by]}
className={cn(
by === "line" ? "block" : "inline-block whitespace-pre",
by === "character" && "",
segmentClassName
)}
aria-hidden={accessible ? true : undefined}
>
{segment}
</motion.span>
))}
</MotionComponent>
</AnimatePresence>
)
}
// Export the memoized version
export const TextAnimate = memo(TextAnimateBase)
+18
View File
@@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Textarea }
+57
View File
@@ -0,0 +1,57 @@
"use client"
import * as React from "react"
import { Tooltip as TooltipPrimitive } from "radix-ui"
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 Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 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-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm 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",
className
)}
{...props}
>
{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.Portal>
)
}
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
View File
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
+36
View File
@@ -0,0 +1,36 @@
import { Button } from "@/components/ui/button";
import Link from "next/link";
function Footer() {
return (
<div className="py-10 px-4 mt-10 mx-4 md:mx-6 lg:mx-44 flex items-start md:items-center flex-col md:flex-row sm:justify-between gap-4 md:gap-0 border-t">
<Link href="/">
<Button className="font-bold" variant="ghost">
<span>Koring Team Space</span>
</Button>
</Link>
<div className="flex items-center gap-1 flex-wrap">
<a href="mailto:hi@lenjing.email?subject=联系支持" target="_blank">
<Button variant="ghost">hi@lenjing.email</Button>
</a>
<a href="https://beian.miit.gov.cn/" target="_blank">
<Button variant="ghost">ICP备2024089677号-2</Button>
</a>
<a href="https://rivfox.com/" target="_blank">
<Button variant="ghost">RIVFOX.COM</Button>
</a>
<a href="https://lingke.ink/" target="_blank">
<Button variant="ghost"></Button>
</a>
<a href="https://dream-pep.cn/" target="_blank">
<Button variant="ghost"></Button>
</a>
<a href="https://lenjing.cn/" target="_blank">
<Button variant="ghost"></Button>
</a>
</div>
</div>
);
}
export { Footer };
+24
View File
@@ -0,0 +1,24 @@
import { Button } from "@/components/ui/button";
import Link from "next/link";
function Header() {
return (
<div className="fixed w-full backdrop-blur-sm bg-background/70 p-3 px-4 md:px-10 lg:px-48 flex items-center justify-between z-20">
<Link href="/">
<Button className="font-bold" variant="ghost">
Koring Team Space
</Button>
</Link>
<div className="flex items-center gap-1">
<a href="https://support.lingke.ink">
<Button variant="ghost"></Button>
</a>
<a href="https://learn.rivfox.com">
<Button variant="ghost"></Button>
</a>
</div>
</div>
);
}
export { Header };
+8
View File
@@ -0,0 +1,8 @@
import Datastore from "@seald-io/nedb";
const useNewsStore = new Datastore({
filename: "./database/news.db",
autoload: true
});
export { useNewsStore };
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+20
View File
@@ -0,0 +1,20 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
if (pathname.startsWith("/rivfox-admin")) {
const isAdmin = request.cookies.get("rivfox-admin")?.value === "a2zFc9YTHpgiOxRH4"
if (!isAdmin) {
return NextResponse.rewrite(new URL("/404", request.url))
}
}
return NextResponse.next()
}
export const config = {
matcher: "/rivfox-admin/:path*",
}
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
+67
View File
@@ -0,0 +1,67 @@
{
"name": "rivfox.website",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbo",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@floating-ui/dom": "^1.7.6",
"@radix-ui/react-use-controllable-state": "^1.2.2",
"@seald-io/nedb": "^4.1.2",
"@tiptap/core": "^3.22.1",
"@tiptap/extension-code-block-lowlight": "^3.22.1",
"@tiptap/extension-list": "^3.22.1",
"@tiptap/extension-subscript": "^3.22.1",
"@tiptap/extension-superscript": "^3.22.1",
"@tiptap/extension-table": "^3.22.1",
"@tiptap/extension-text-style": "^3.22.1",
"@tiptap/extension-typography": "^3.22.1",
"@tiptap/extensions": "^3.22.1",
"@tiptap/pm": "^3.22.1",
"@tiptap/react": "^3.22.1",
"@tiptap/starter-kit": "^3.22.1",
"@tiptap/suggestion": "^3.22.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"fuse.js": "^7.2.0",
"lowlight": "^3.3.0",
"lucide-react": "^1.7.0",
"motion": "^12.38.0",
"next": "16.2.2",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-fast-marquee": "^1.6.5",
"rough-notation": "^0.5.1",
"tailwind-merge": "^3.5.0",
"tippy.js": "^6.3.7",
"tw-animate-css": "^1.4.0",
"zustand": "^5.0.12"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.2",
"prettierrc": "^0.0.0-5",
"shadcn": "^4.1.2",
"tailwindcss": "^4",
"typescript": "^5"
},
"ignoreScripts": [
"sharp",
"unrs-resolver"
],
"trustedDependencies": [
"sharp",
"unrs-resolver"
]
}
+9230
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512" viewBox="0 0 512 512" fill="none"><path fill="#FFFFFF" d="M0 412L0 100C0 44.7715 44.7715 0 100 0L412 0C467.228 0 512 44.7715 512 100L512 412C512 467.228 467.228 512 412 512L100 512C44.7715 512 0 467.228 0 412Z"></path><path fill="#000000" d="M74.0896 267.948L212.983 365.709C221.558 371.744 233.337 370.133 239.976 362.016L347.509 230.543C354.863 221.553 353.037 208.211 343.54 201.526L204.646 103.765C196.072 97.73 184.292 99.3415 177.653 107.458L70.1199 238.931C62.7666 247.921 64.5919 261.263 74.0896 267.948Z"></path><path fill="#000000" d="M438.649 267.728L299.756 365.488C291.181 371.524 279.402 369.912 272.763 361.796L165.23 230.323C157.876 221.332 159.702 207.99 169.199 201.305L308.093 103.545C316.667 97.5093 328.447 99.1209 335.086 107.237L442.619 238.71C449.972 247.701 448.147 261.043 438.649 267.728Z"></path><path fill="#2B2B2B" d="M127.238 306.279L241.71 420.841C249.521 428.659 262.193 428.659 270.005 420.841L384.477 306.279C392.28 298.47 392.28 285.815 384.477 278.006L270.005 163.444C262.193 155.627 249.521 155.627 241.71 163.444L127.238 278.006C119.435 285.815 119.435 298.47 127.238 306.279Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="438.488037109375" height="362.9998474121094" viewBox="0 0 438.488037109375 362.9998474121094" fill="none"><path fill="#FFFFFF" d="M36.9641 195.948L175.858 293.709C184.432 299.744 196.212 298.133 202.85 290.016L310.384 158.543C317.737 149.553 315.912 136.211 306.414 129.526L167.521 31.7651C158.946 25.73 147.166 27.3415 140.528 35.4578L32.9944 166.931C25.6411 175.921 27.4664 189.263 36.9641 195.948Z"></path><path fill="#FFFFFF" d="M401.524 195.728L262.63 293.488C254.056 299.524 242.276 297.912 235.638 289.796L128.104 158.323C120.751 149.332 122.576 135.99 132.074 129.305L270.967 31.5445C279.542 25.5093 291.322 27.1209 297.96 35.2372L405.494 166.71C412.847 175.701 411.022 189.043 401.524 195.728Z"></path><path fill="#F7F7F7" d="M90.1122 234.279L204.584 348.841C212.396 356.659 225.068 356.659 232.879 348.841L347.351 234.279C355.154 226.47 355.154 213.815 347.351 206.006L232.879 91.4445C225.068 83.6266 212.396 83.6266 204.584 91.4445L90.1122 206.006C82.3091 213.815 82.3091 226.47 90.1122 234.279Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="438.488037109375" height="362.9998474121094" viewBox="0 0 438.488037109375 362.9998474121094" fill="none"><path fill="#000000" d="M36.9641 195.948L175.858 293.709C184.432 299.744 196.212 298.133 202.85 290.016L310.384 158.543C317.737 149.553 315.912 136.211 306.414 129.526L167.521 31.7651C158.946 25.73 147.166 27.3415 140.528 35.4578L32.9944 166.931C25.6411 175.921 27.4664 189.263 36.9641 195.948Z"></path><path fill="#000000" d="M401.524 195.728L262.63 293.488C254.056 299.524 242.276 297.912 235.638 289.796L128.104 158.323C120.751 149.332 122.576 135.99 132.074 129.305L270.967 31.5445C279.542 25.5093 291.322 27.1209 297.96 35.2372L405.494 166.71C412.847 175.701 411.022 189.043 401.524 195.728Z"></path><path fill="#2B2B2B" d="M90.1122 234.279L204.584 348.841C212.396 356.659 225.068 356.659 232.879 348.841L347.351 234.279C355.154 226.47 355.154 213.815 347.351 206.006L232.879 91.4445C225.068 83.6266 212.396 83.6266 204.584 91.4445L90.1122 206.006C82.3091 213.815 82.3091 226.47 90.1122 234.279Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+34
View File
@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}