Add global things from old app
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@ -39,3 +39,11 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# vscode
|
||||
*.code-workspace
|
||||
|
||||
# prisma
|
||||
.env
|
||||
/src/generated/
|
||||
|
||||
|
8
.prettierrc.json
Normal file
8
.prettierrc.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"plugins": ["prettier-plugin-tailwindcss"],
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@ -0,0 +1,19 @@
|
||||
# Base image
|
||||
FROM node:22
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install
|
||||
|
||||
# Copy the rest of the code
|
||||
COPY . .
|
||||
RUN npx prisma generate
|
||||
|
||||
# Expose the dev port
|
||||
EXPOSE 3000
|
||||
|
||||
# Run dev server
|
||||
CMD ["npm", "run", "dev"]
|
21
components.json
Normal file
21
components.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "gray",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@ -10,7 +10,7 @@ const compat = new FlatCompat({
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
...compat.extends("next/core-web-vitals", "next/typescript", "prettier"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
|
6569
package-lock.json
generated
6569
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
65
package.json
65
package.json
@ -9,19 +9,74 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.848.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.848.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@material/material-color-utilities": "^0.3.0",
|
||||
"@platejs/basic-nodes": "^49.0.0",
|
||||
"@platejs/code-block": "^49.0.0",
|
||||
"@platejs/indent": "^49.0.0",
|
||||
"@platejs/list": "^49.0.0",
|
||||
"@platejs/markdown": "^49.1.9",
|
||||
"@prisma/client": "^6.12.0",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@radix-ui/react-toggle": "^1.1.9",
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-toolbar": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"lowlight": "^3.3.0",
|
||||
"lucide-react": "^0.525.0",
|
||||
"next": "15.4.2",
|
||||
"next-auth": "^4.24.11",
|
||||
"next-themes": "^0.4.6",
|
||||
"node-vibrant": "^4.0.3",
|
||||
"platejs": "^49.1.5",
|
||||
"react": "19.1.0",
|
||||
"react-day-picker": "^9.8.0",
|
||||
"react-dom": "19.1.0",
|
||||
"next": "15.4.2"
|
||||
"react-hook-form": "^7.60.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"sharp": "^0.34.3",
|
||||
"sonner": "^2.0.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-scrollbar-hide": "^4.0.0",
|
||||
"uuid": "^11.1.0",
|
||||
"zod": "^4.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.4.2",
|
||||
"@eslint/eslintrc": "^3"
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.3",
|
||||
"prisma": "^6.12.0",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
307
prisma/schema.prisma
Normal file
307
prisma/schema.prisma
Normal file
@ -0,0 +1,307 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "../src/generated/prisma"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model CommissionType {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
name String
|
||||
|
||||
description String?
|
||||
|
||||
options CommissionTypeOption[]
|
||||
extras CommissionTypeExtra[]
|
||||
customInputs CommissionTypeCustomInput[]
|
||||
requests CommissionRequest[]
|
||||
}
|
||||
|
||||
model CommissionOption {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
name String
|
||||
|
||||
description String?
|
||||
|
||||
types CommissionTypeOption[]
|
||||
requests CommissionRequest[]
|
||||
}
|
||||
|
||||
model CommissionExtra {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
name String
|
||||
|
||||
description String?
|
||||
|
||||
types CommissionTypeExtra[]
|
||||
}
|
||||
|
||||
model CommissionCustomInput {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
name String @unique
|
||||
fieldId String
|
||||
|
||||
types CommissionTypeCustomInput[]
|
||||
}
|
||||
|
||||
model CommissionTypeOption {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
typeId String
|
||||
optionId String
|
||||
|
||||
priceRange String?
|
||||
pricePercent Float?
|
||||
price Float?
|
||||
|
||||
type CommissionType @relation(fields: [typeId], references: [id])
|
||||
option CommissionOption @relation(fields: [optionId], references: [id])
|
||||
|
||||
@@unique([typeId, optionId])
|
||||
}
|
||||
|
||||
model CommissionTypeExtra {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
typeId String
|
||||
extraId String
|
||||
|
||||
priceRange String?
|
||||
pricePercent Float?
|
||||
price Float?
|
||||
|
||||
type CommissionType @relation(fields: [typeId], references: [id])
|
||||
extra CommissionExtra @relation(fields: [extraId], references: [id])
|
||||
|
||||
@@unique([typeId, extraId])
|
||||
}
|
||||
|
||||
model CommissionTypeCustomInput {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
typeId String
|
||||
customInputId String
|
||||
|
||||
inputType String
|
||||
label String
|
||||
required Boolean @default(false)
|
||||
|
||||
type CommissionType @relation(fields: [typeId], references: [id])
|
||||
customInput CommissionCustomInput @relation(fields: [customInputId], references: [id])
|
||||
|
||||
@@unique([typeId, customInputId])
|
||||
}
|
||||
|
||||
model TermsOfService {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
version Int @default(autoincrement())
|
||||
markdown String
|
||||
}
|
||||
|
||||
model CommissionRequest {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
customerName String
|
||||
customerEmail String
|
||||
message String
|
||||
|
||||
optionId String?
|
||||
typeId String?
|
||||
option CommissionOption? @relation(fields: [optionId], references: [id])
|
||||
type CommissionType? @relation(fields: [typeId], references: [id])
|
||||
}
|
||||
|
||||
model PortfolioImage {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
fileKey String @unique
|
||||
originalFile String @unique
|
||||
nsfw Boolean @default(false)
|
||||
published Boolean @default(false)
|
||||
setAsHeader Boolean @default(false)
|
||||
|
||||
altText String?
|
||||
description String?
|
||||
fileType String?
|
||||
group String?
|
||||
kind String?
|
||||
layoutGroup String?
|
||||
name String?
|
||||
series String?
|
||||
slug String?
|
||||
type String?
|
||||
fileSize Int?
|
||||
layoutOrder Int?
|
||||
month Int?
|
||||
year Int?
|
||||
creationDate DateTime?
|
||||
|
||||
artTypeId String?
|
||||
artType PortfolioArtType? @relation(fields: [artTypeId], references: [id])
|
||||
metadata ImageMetadata?
|
||||
categories PortfolioCategory[]
|
||||
colors ImageColor[]
|
||||
tags PortfolioTag[]
|
||||
variants ImageVariant[]
|
||||
}
|
||||
|
||||
model PortfolioArtType {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
name String @unique
|
||||
|
||||
slug String?
|
||||
description String?
|
||||
|
||||
images PortfolioImage[]
|
||||
}
|
||||
|
||||
model PortfolioCategory {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
name String @unique
|
||||
|
||||
slug String?
|
||||
description String?
|
||||
|
||||
images PortfolioImage[]
|
||||
}
|
||||
|
||||
model PortfolioTag {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
name String @unique
|
||||
|
||||
slug String?
|
||||
description String?
|
||||
|
||||
images PortfolioImage[]
|
||||
}
|
||||
|
||||
model Color {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
name String @unique
|
||||
type String
|
||||
|
||||
hex String?
|
||||
blue Int?
|
||||
green Int?
|
||||
red Int?
|
||||
|
||||
images ImageColor[]
|
||||
}
|
||||
|
||||
model ImageColor {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
imageId String
|
||||
colorId String
|
||||
type String
|
||||
|
||||
image PortfolioImage @relation(fields: [imageId], references: [id])
|
||||
color Color @relation(fields: [colorId], references: [id])
|
||||
|
||||
@@unique([imageId, type])
|
||||
}
|
||||
|
||||
model ImageMetadata {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
imageId String @unique
|
||||
depth String
|
||||
format String
|
||||
space String
|
||||
channels Int
|
||||
height Int
|
||||
width Int
|
||||
|
||||
autoOrientH Int?
|
||||
autoOrientW Int?
|
||||
bitsPerSample Int?
|
||||
density Int?
|
||||
hasAlpha Boolean?
|
||||
hasProfile Boolean?
|
||||
isPalette Boolean?
|
||||
isProgressive Boolean?
|
||||
|
||||
image PortfolioImage @relation(fields: [imageId], references: [id])
|
||||
}
|
||||
|
||||
model ImageVariant {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
imageId String
|
||||
s3Key String
|
||||
type String
|
||||
height Int
|
||||
width Int
|
||||
|
||||
fileExtension String?
|
||||
mimeType String?
|
||||
url String?
|
||||
sizeBytes Int?
|
||||
|
||||
image PortfolioImage @relation(fields: [imageId], references: [id])
|
||||
|
||||
@@unique([imageId, type])
|
||||
}
|
24
src/_middleware.ts
Normal file
24
src/_middleware.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// import { auth } from "@/auth"
|
||||
// import type { NextRequest } from "next/server"
|
||||
// import { NextResponse } from "next/server"
|
||||
|
||||
// export async function middleware(request: NextRequest) {
|
||||
// const session = await auth()
|
||||
|
||||
// const isProtectedPath =
|
||||
// !request.nextUrl.pathname.startsWith("/api/auth") &&
|
||||
// !request.nextUrl.pathname.startsWith("/_next") &&
|
||||
// !request.nextUrl.pathname.startsWith("/favicon") &&
|
||||
// !request.nextUrl.pathname.startsWith("/assets")
|
||||
|
||||
// if (isProtectedPath && !session) {
|
||||
// const signInUrl = new URL("/api/auth/signin", request.url)
|
||||
// return NextResponse.redirect(signInUrl)
|
||||
// }
|
||||
|
||||
// return NextResponse.next()
|
||||
// }
|
||||
|
||||
// export const config = {
|
||||
// matcher: ["/((?!api/auth|_next/static|_next/image|favicon.ico).*)"],
|
||||
// }
|
10
src/actions/items/commissions/tos/getTos.ts
Normal file
10
src/actions/items/commissions/tos/getTos.ts
Normal file
@ -0,0 +1,10 @@
|
||||
'use server';
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export async function getLatestTos(): Promise<string | null> {
|
||||
const tos = await prisma.termsOfService.findFirst({
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
return tos?.markdown ?? null;
|
||||
}
|
11
src/actions/items/commissions/tos/saveTosAction.ts
Normal file
11
src/actions/items/commissions/tos/saveTosAction.ts
Normal file
@ -0,0 +1,11 @@
|
||||
'use server';
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export async function saveTosAction(markdown: string) {
|
||||
await prisma.termsOfService.create({
|
||||
data: {
|
||||
markdown,
|
||||
},
|
||||
});
|
||||
}
|
19
src/actions/items/commissions/types/deleteType.ts
Normal file
19
src/actions/items/commissions/types/deleteType.ts
Normal file
@ -0,0 +1,19 @@
|
||||
"use server"
|
||||
|
||||
import prisma from "@/lib/prisma"
|
||||
|
||||
export async function deleteCommissionType(typeId: string) {
|
||||
|
||||
await prisma.commissionTypeOption.deleteMany({
|
||||
where: { typeId },
|
||||
})
|
||||
|
||||
await prisma.commissionTypeExtra.deleteMany({
|
||||
where: { typeId },
|
||||
})
|
||||
|
||||
await prisma.commissionType.delete({
|
||||
where: { id: typeId },
|
||||
})
|
||||
|
||||
}
|
81
src/actions/items/commissions/types/newType.ts
Normal file
81
src/actions/items/commissions/types/newType.ts
Normal file
@ -0,0 +1,81 @@
|
||||
"use server"
|
||||
|
||||
import prisma from "@/lib/prisma"
|
||||
import { commissionTypeSchema } from "@/schemas/commissionType"
|
||||
|
||||
export async function createCommissionOption(data: { name: string }) {
|
||||
return await prisma.commissionOption.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
description: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createCommissionExtra(data: { name: string }) {
|
||||
return await prisma.commissionExtra.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
description: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createCommissionCustomInput(data: {
|
||||
name: string
|
||||
fieldId: string
|
||||
}) {
|
||||
return await prisma.commissionCustomInput.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
fieldId: data.fieldId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createCommissionType(formData: commissionTypeSchema) {
|
||||
const parsed = commissionTypeSchema.safeParse(formData)
|
||||
|
||||
if (!parsed.success) {
|
||||
console.error("Validation failed", parsed.error)
|
||||
throw new Error("Invalid input")
|
||||
}
|
||||
|
||||
const data = parsed.data
|
||||
|
||||
const created = await prisma.commissionType.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
options: {
|
||||
create: data.options?.map((opt, index) => ({
|
||||
option: { connect: { id: opt.optionId } },
|
||||
price: opt.price,
|
||||
pricePercent: opt.pricePercent,
|
||||
priceRange: opt.priceRange,
|
||||
sortIndex: index,
|
||||
})) || [],
|
||||
},
|
||||
extras: {
|
||||
create: data.extras?.map((ext, index) => ({
|
||||
extra: { connect: { id: ext.extraId } },
|
||||
price: ext.price,
|
||||
pricePercent: ext.pricePercent,
|
||||
priceRange: ext.priceRange,
|
||||
sortIndex: index,
|
||||
})) || [],
|
||||
},
|
||||
customInputs: {
|
||||
create: data.customInputs?.map((c, index) => ({
|
||||
customInput: { connect: { id: c.customInputId } },
|
||||
label: c.label,
|
||||
inputType: c.inputType,
|
||||
required: c.required,
|
||||
sortIndex: index,
|
||||
})) || [],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return created
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
"use server"
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export async function updateCommissionTypeSortOrder(
|
||||
ordered: { id: string; sortIndex: number }[]
|
||||
) {
|
||||
const updates = ordered.map(({ id, sortIndex }) =>
|
||||
prisma.commissionType.update({
|
||||
where: { id },
|
||||
data: { sortIndex },
|
||||
})
|
||||
)
|
||||
|
||||
await Promise.all(updates)
|
||||
}
|
57
src/actions/items/commissions/types/updateType.ts
Normal file
57
src/actions/items/commissions/types/updateType.ts
Normal file
@ -0,0 +1,57 @@
|
||||
"use server"
|
||||
|
||||
import prisma from "@/lib/prisma"
|
||||
import { commissionTypeSchema } from "@/schemas/commissionType"
|
||||
import * as z from "zod/v4"
|
||||
|
||||
export async function updateCommissionType(
|
||||
id: string,
|
||||
rawData: z.infer<typeof commissionTypeSchema>
|
||||
) {
|
||||
const data = commissionTypeSchema.parse(rawData)
|
||||
|
||||
const updated = await prisma.commissionType.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
options: {
|
||||
deleteMany: {},
|
||||
create: data.options?.map((opt, index) => ({
|
||||
option: { connect: { id: opt.optionId } },
|
||||
price: opt.price ?? null,
|
||||
pricePercent: opt.pricePercent ?? null,
|
||||
priceRange: opt.priceRange ?? null,
|
||||
sortIndex: index,
|
||||
})),
|
||||
},
|
||||
extras: {
|
||||
deleteMany: {},
|
||||
create: data.extras?.map((ext, index) => ({
|
||||
extra: { connect: { id: ext.extraId } },
|
||||
price: ext.price ?? null,
|
||||
pricePercent: ext.pricePercent ?? null,
|
||||
priceRange: ext.priceRange ?? null,
|
||||
sortIndex: index,
|
||||
})),
|
||||
},
|
||||
customInputs: {
|
||||
deleteMany: {},
|
||||
create: data.customInputs?.map((c, index) => ({
|
||||
customInput: { connect: { id: c.customInputId } },
|
||||
label: c.label,
|
||||
inputType: c.inputType,
|
||||
required: c.required,
|
||||
sortIndex: index,
|
||||
})) || [],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
options: true,
|
||||
extras: true,
|
||||
customInputs: true,
|
||||
},
|
||||
})
|
||||
|
||||
return updated
|
||||
}
|
25
src/actions/portfolio/arttypes/createArtType.ts
Normal file
25
src/actions/portfolio/arttypes/createArtType.ts
Normal file
@ -0,0 +1,25 @@
|
||||
"use server"
|
||||
|
||||
import prisma from '@/lib/prisma';
|
||||
import { artTypeSchema } from '@/schemas/artTypeSchema';
|
||||
|
||||
export async function createArtType(formData: artTypeSchema) {
|
||||
const parsed = artTypeSchema.safeParse(formData)
|
||||
|
||||
if (!parsed.success) {
|
||||
console.error("Validation failed", parsed.error)
|
||||
throw new Error("Invalid input")
|
||||
}
|
||||
|
||||
const data = parsed.data
|
||||
|
||||
const created = await prisma.portfolioArtType.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
slug: data.slug,
|
||||
description: data.description
|
||||
},
|
||||
})
|
||||
|
||||
return created
|
||||
}
|
28
src/actions/portfolio/arttypes/updateArtType.ts
Normal file
28
src/actions/portfolio/arttypes/updateArtType.ts
Normal file
@ -0,0 +1,28 @@
|
||||
"use server"
|
||||
|
||||
import prisma from '@/lib/prisma';
|
||||
import { artTypeSchema } from '@/schemas/artTypeSchema';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
export async function updateArtType(id: string,
|
||||
rawData: z.infer<typeof artTypeSchema>) {
|
||||
const parsed = artTypeSchema.safeParse(rawData)
|
||||
|
||||
if (!parsed.success) {
|
||||
console.error("Validation failed", parsed.error)
|
||||
throw new Error("Invalid input")
|
||||
}
|
||||
|
||||
const data = parsed.data
|
||||
|
||||
const updated = await prisma.portfolioArtType.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name: data.name,
|
||||
slug: data.slug,
|
||||
description: data.description
|
||||
},
|
||||
})
|
||||
|
||||
return updated
|
||||
}
|
15
src/actions/portfolio/arttypes/updateArtTypeSortOrder.ts
Normal file
15
src/actions/portfolio/arttypes/updateArtTypeSortOrder.ts
Normal file
@ -0,0 +1,15 @@
|
||||
'use server';
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
import { SortableItem } from "@/types/SortableItem";
|
||||
|
||||
export async function updateArtTypeSortOrder(items: SortableItem[]) {
|
||||
await Promise.all(
|
||||
items.map(item =>
|
||||
prisma.portfolioArtType.update({
|
||||
where: { id: item.id },
|
||||
data: { sortIndex: item.sortIndex },
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
67
src/actions/portfolio/edit/deleteImage.ts
Normal file
67
src/actions/portfolio/edit/deleteImage.ts
Normal file
@ -0,0 +1,67 @@
|
||||
"use server";
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
import { s3 } from "@/lib/s3";
|
||||
import { DeleteObjectCommand } from "@aws-sdk/client-s3";
|
||||
|
||||
export async function deleteImage(imageId: string) {
|
||||
const image = await prisma.portfolioImage.findUnique({
|
||||
where: { id: imageId },
|
||||
include: {
|
||||
variants: true,
|
||||
colors: true,
|
||||
metadata: true,
|
||||
tags: true,
|
||||
categories: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!image) throw new Error("Image not found");
|
||||
|
||||
// Delete S3 objects
|
||||
for (const variant of image.variants) {
|
||||
try {
|
||||
await s3.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: "gaertan",
|
||||
Key: variant.s3Key,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn("Failed to delete S3 object: " + variant.s3Key + ". " + err);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1: Delete join entries
|
||||
await prisma.imageColor.deleteMany({ where: { imageId } });
|
||||
|
||||
// Colors
|
||||
for (const color of image.colors) {
|
||||
const count = await prisma.imageColor.count({
|
||||
where: { colorId: color.colorId },
|
||||
});
|
||||
if (count === 0) {
|
||||
await prisma.color.delete({ where: { id: color.colorId } });
|
||||
}
|
||||
}
|
||||
|
||||
// Delete variants
|
||||
await prisma.imageVariant.deleteMany({ where: { imageId } });
|
||||
|
||||
// Delete metadata
|
||||
await prisma.imageMetadata.deleteMany({ where: { imageId } });
|
||||
|
||||
// Clean many-to-many tag/category joins
|
||||
await prisma.portfolioImage.update({
|
||||
where: { id: imageId },
|
||||
data: {
|
||||
tags: { set: [] },
|
||||
categories: { set: [] },
|
||||
},
|
||||
});
|
||||
|
||||
// Finally delete the image
|
||||
await prisma.portfolioImage.delete({ where: { id: imageId } });
|
||||
|
||||
return { success: true };
|
||||
}
|
66
src/actions/portfolio/edit/generateImageColors.ts
Normal file
66
src/actions/portfolio/edit/generateImageColors.ts
Normal file
@ -0,0 +1,66 @@
|
||||
"use server"
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
import { VibrantSwatch } from "@/types/VibrantSwatch";
|
||||
import { getImageBufferFromS3 } from "@/utils/getImageBufferFromS3";
|
||||
import { generateColorName, rgbToHex } from "@/utils/uploadHelper";
|
||||
import { Vibrant } from "node-vibrant/node";
|
||||
|
||||
export async function generateImageColors(imageId: string, fileKey: string, fileType?: string) {
|
||||
const buffer = await getImageBufferFromS3(fileKey, fileType);
|
||||
const palette = await Vibrant.from(buffer).getPalette();
|
||||
|
||||
const vibrantHexes = Object.entries(palette).map(([key, swatch]) => {
|
||||
const castSwatch = swatch as VibrantSwatch | null;
|
||||
const rgb = castSwatch?._rgb;
|
||||
const hex = castSwatch?.hex || (rgb ? rgbToHex(rgb) : undefined);
|
||||
return { type: key, hex };
|
||||
});
|
||||
|
||||
for (const { type, hex } of vibrantHexes) {
|
||||
if (!hex) continue;
|
||||
|
||||
const [r, g, b] = hex.match(/\w\w/g)!.map((h) => parseInt(h, 16));
|
||||
const name = generateColorName(hex);
|
||||
|
||||
const color = await prisma.color.upsert({
|
||||
where: { name },
|
||||
create: {
|
||||
name,
|
||||
type,
|
||||
hex,
|
||||
red: r,
|
||||
green: g,
|
||||
blue: b,
|
||||
},
|
||||
update: {
|
||||
hex,
|
||||
red: r,
|
||||
green: g,
|
||||
blue: b,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.imageColor.upsert({
|
||||
where: {
|
||||
imageId_type: {
|
||||
imageId,
|
||||
type,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
imageId,
|
||||
colorId: color.id,
|
||||
type,
|
||||
},
|
||||
update: {
|
||||
colorId: color.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.imageColor.findMany({
|
||||
where: { imageId },
|
||||
include: { color: true },
|
||||
});
|
||||
}
|
74
src/actions/portfolio/edit/updateImage.ts
Normal file
74
src/actions/portfolio/edit/updateImage.ts
Normal file
@ -0,0 +1,74 @@
|
||||
"use server"
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
import { imageSchema } from "@/schemas/imageSchema";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export async function updateImage(
|
||||
values: z.infer<typeof imageSchema>,
|
||||
id: string
|
||||
) {
|
||||
const validated = imageSchema.safeParse(values);
|
||||
if (!validated.success) {
|
||||
throw new Error("Invalid image data");
|
||||
}
|
||||
|
||||
const {
|
||||
fileKey,
|
||||
originalFile,
|
||||
nsfw,
|
||||
published,
|
||||
altText,
|
||||
description,
|
||||
fileType,
|
||||
name,
|
||||
slug,
|
||||
type,
|
||||
fileSize,
|
||||
creationDate,
|
||||
tagIds,
|
||||
categoryIds
|
||||
} = validated.data;
|
||||
|
||||
const updatedImage = await prisma.portfolioImage.update({
|
||||
where: { id: id },
|
||||
data: {
|
||||
fileKey,
|
||||
originalFile,
|
||||
nsfw,
|
||||
published,
|
||||
altText,
|
||||
description,
|
||||
fileType,
|
||||
name,
|
||||
slug,
|
||||
type,
|
||||
fileSize,
|
||||
creationDate,
|
||||
}
|
||||
});
|
||||
|
||||
if (tagIds) {
|
||||
await prisma.portfolioImage.update({
|
||||
where: { id: id },
|
||||
data: {
|
||||
tags: {
|
||||
set: tagIds.map(id => ({ id }))
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (categoryIds) {
|
||||
await prisma.portfolioImage.update({
|
||||
where: { id: id },
|
||||
data: {
|
||||
categories: {
|
||||
set: categoryIds.map(id => ({ id }))
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return updatedImage
|
||||
}
|
15
src/actions/portfolio/updateImageSortOrder.ts
Normal file
15
src/actions/portfolio/updateImageSortOrder.ts
Normal file
@ -0,0 +1,15 @@
|
||||
'use server';
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
import { SortableItem } from "@/types/SortableItem";
|
||||
|
||||
export async function updateImageSortOrder(items: SortableItem[]) {
|
||||
await Promise.all(
|
||||
items.map(item =>
|
||||
prisma.portfolioImage.update({
|
||||
where: { id: item.id },
|
||||
data: { sortIndex: item.sortIndex },
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
190
src/actions/portfolio/upload/uploadImage.ts
Normal file
190
src/actions/portfolio/upload/uploadImage.ts
Normal file
@ -0,0 +1,190 @@
|
||||
"use server"
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
import { s3 } from "@/lib/s3";
|
||||
import { imageUploadSchema } from "@/schemas/imageSchema";
|
||||
import { PutObjectCommand } from "@aws-sdk/client-s3";
|
||||
import sharp from "sharp";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export async function uploadImage(values: z.infer<typeof imageUploadSchema>) {
|
||||
const imageFile = values.file[0];
|
||||
|
||||
if (!(imageFile instanceof File)) {
|
||||
console.log("No image or invalid type");
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileName = imageFile.name;
|
||||
const fileType = imageFile.type;
|
||||
const fileSize = imageFile.size;
|
||||
const lastModified = new Date(imageFile.lastModified);
|
||||
|
||||
const fileKey = uuidv4();
|
||||
|
||||
const arrayBuffer = await imageFile.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const realFileType = fileType.split("/")[1];
|
||||
const originalKey = `original/${fileKey}.${realFileType}`;
|
||||
const modifiedKey = `modified/${fileKey}.webp`;
|
||||
const resizedKey = `resized/${fileKey}.webp`;
|
||||
const thumbnailKey = `thumbnail/${fileKey}.webp`;
|
||||
|
||||
const sharpData = sharp(buffer);
|
||||
const metadata = await sharpData.metadata();
|
||||
|
||||
//--- Original file
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: "gaertan",
|
||||
Key: originalKey,
|
||||
Body: buffer,
|
||||
ContentType: "image/" + metadata.format,
|
||||
})
|
||||
);
|
||||
//--- Modified file
|
||||
const modifiedBuffer = await sharp(buffer)
|
||||
.toFormat('webp')
|
||||
.toBuffer()
|
||||
const modifiedMetadata = await sharp(modifiedBuffer).metadata();
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: "gaertan",
|
||||
Key: modifiedKey,
|
||||
Body: modifiedBuffer,
|
||||
ContentType: "image/" + modifiedMetadata.format,
|
||||
})
|
||||
);
|
||||
//--- Resized file
|
||||
const { width, height } = modifiedMetadata;
|
||||
const targetSize = 400;
|
||||
let resizeOptions;
|
||||
if (width && height) {
|
||||
if (height < width) {
|
||||
resizeOptions = { height: targetSize };
|
||||
} else {
|
||||
resizeOptions = { width: targetSize };
|
||||
}
|
||||
} else {
|
||||
resizeOptions = { height: targetSize };
|
||||
}
|
||||
const resizedBuffer = await sharp(modifiedBuffer)
|
||||
.resize({ ...resizeOptions, withoutEnlargement: true })
|
||||
.toFormat('webp')
|
||||
.toBuffer();
|
||||
const resizedMetadata = await sharp(resizedBuffer).metadata();
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: "gaertan",
|
||||
Key: resizedKey,
|
||||
Body: resizedBuffer,
|
||||
ContentType: "image/" + resizedMetadata.format,
|
||||
})
|
||||
);
|
||||
//--- Thumbnail file
|
||||
// const thumbnailWidth = Math.min(watermarkedMetadata.width || 200, 200);
|
||||
const thumbnailTargetSize = 160;
|
||||
let thumbnailOptions;
|
||||
if (width && height) {
|
||||
if (height < width) {
|
||||
thumbnailOptions = { height: thumbnailTargetSize };
|
||||
} else {
|
||||
thumbnailOptions = { width: thumbnailTargetSize };
|
||||
}
|
||||
} else {
|
||||
thumbnailOptions = { height: thumbnailTargetSize };
|
||||
}
|
||||
const thumbnailBuffer = await sharp(modifiedBuffer)
|
||||
.resize({ ...thumbnailOptions, withoutEnlargement: true })
|
||||
.toFormat('webp')
|
||||
.toBuffer();
|
||||
const thumbnailMetadata = await sharp(thumbnailBuffer).metadata();
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: "gaertan",
|
||||
Key: thumbnailKey,
|
||||
Body: thumbnailBuffer,
|
||||
ContentType: "image/" + thumbnailMetadata.format,
|
||||
})
|
||||
);
|
||||
|
||||
const image = await prisma.portfolioImage.create({
|
||||
data: {
|
||||
name: fileName,
|
||||
fileKey,
|
||||
originalFile: fileName,
|
||||
creationDate: lastModified,
|
||||
fileType: fileType,
|
||||
fileSize: fileSize
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.imageMetadata.create({
|
||||
data: {
|
||||
imageId: image.id,
|
||||
format: metadata.format || "unknown",
|
||||
width: metadata.width || 0,
|
||||
height: metadata.height || 0,
|
||||
space: metadata.space || "unknown",
|
||||
channels: metadata.channels || 0,
|
||||
depth: metadata.depth || "unknown",
|
||||
density: metadata.density ?? undefined,
|
||||
bitsPerSample: metadata.bitsPerSample ?? undefined,
|
||||
isProgressive: metadata.isProgressive ?? undefined,
|
||||
isPalette: metadata.isPalette ?? undefined,
|
||||
hasProfile: metadata.hasProfile ?? undefined,
|
||||
hasAlpha: metadata.hasAlpha ?? undefined,
|
||||
autoOrientW: metadata.autoOrient?.width ?? undefined,
|
||||
autoOrientH: metadata.autoOrient?.height ?? undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.imageVariant.createMany({
|
||||
data: [
|
||||
{
|
||||
s3Key: originalKey,
|
||||
type: "original",
|
||||
height: metadata.height,
|
||||
width: metadata.width,
|
||||
fileExtension: metadata.format,
|
||||
mimeType: "image/" + metadata.format,
|
||||
sizeBytes: metadata.size,
|
||||
imageId: image.id
|
||||
},
|
||||
{
|
||||
s3Key: modifiedKey,
|
||||
type: "modified",
|
||||
height: modifiedMetadata.height,
|
||||
width: modifiedMetadata.width,
|
||||
fileExtension: modifiedMetadata.format,
|
||||
mimeType: "image/" + modifiedMetadata.format,
|
||||
sizeBytes: modifiedMetadata.size,
|
||||
imageId: image.id
|
||||
},
|
||||
{
|
||||
s3Key: resizedKey,
|
||||
type: "resized",
|
||||
height: resizedMetadata.height,
|
||||
width: resizedMetadata.width,
|
||||
fileExtension: resizedMetadata.format,
|
||||
mimeType: "image/" + resizedMetadata.format,
|
||||
sizeBytes: resizedMetadata.size,
|
||||
imageId: image.id
|
||||
},
|
||||
{
|
||||
s3Key: thumbnailKey,
|
||||
type: "thumbnail",
|
||||
height: thumbnailMetadata.height,
|
||||
width: thumbnailMetadata.width,
|
||||
fileExtension: thumbnailMetadata.format,
|
||||
mimeType: "image/" + thumbnailMetadata.format,
|
||||
sizeBytes: thumbnailMetadata.size,
|
||||
imageId: image.id
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
return image
|
||||
}
|
@ -1,26 +1,680 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
@plugin "tailwind-scrollbar-hide";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-brand: var(--brand);
|
||||
--color-highlight: var(--highlight);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
:root {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.606 0.25 292.717);
|
||||
--primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.606 0.25 292.717);
|
||||
--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);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.606 0.25 292.717);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.606 0.25 292.717);
|
||||
--brand: oklch(0.623 0.214 259.815);
|
||||
--highlight: oklch(0.852 0.199 91.936);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.541 0.281 293.009);
|
||||
--primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--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.541 0.281 293.009);
|
||||
--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);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.541 0.281 293.009);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.541 0.281 293.009);
|
||||
--brand: oklch(0.707 0.165 254.624);
|
||||
--highlight: oklch(0.852 0.199 91.936);
|
||||
}
|
||||
|
||||
.light-zinc {
|
||||
--radius: 0.65rem;
|
||||
--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.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);
|
||||
--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-zinc {
|
||||
--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.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);
|
||||
--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);
|
||||
}
|
||||
|
||||
.light-red {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.637 0.237 25.331);
|
||||
--primary-foreground: oklch(0.971 0.013 17.38);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.637 0.237 25.331);
|
||||
--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);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.637 0.237 25.331);
|
||||
--sidebar-primary-foreground: oklch(0.971 0.013 17.38);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.637 0.237 25.331);
|
||||
}
|
||||
|
||||
.dark-red {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.637 0.237 25.331);
|
||||
--primary-foreground: oklch(0.971 0.013 17.38);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--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.637 0.237 25.331);
|
||||
--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);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.637 0.237 25.331);
|
||||
--sidebar-primary-foreground: oklch(0.971 0.013 17.38);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.637 0.237 25.331);
|
||||
}
|
||||
|
||||
.light-rose {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.645 0.246 16.439);
|
||||
--primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.645 0.246 16.439);
|
||||
--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);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.645 0.246 16.439);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.645 0.246 16.439);
|
||||
}
|
||||
|
||||
.dark-rose {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.645 0.246 16.439);
|
||||
--primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--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.645 0.246 16.439);
|
||||
--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);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.645 0.246 16.439);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.645 0.246 16.439);
|
||||
}
|
||||
|
||||
.light-orange {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.705 0.213 47.604);
|
||||
--primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.705 0.213 47.604);
|
||||
--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);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.705 0.213 47.604);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.705 0.213 47.604);
|
||||
}
|
||||
|
||||
.dark-orange {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.646 0.222 41.116);
|
||||
--primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--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.646 0.222 41.116);
|
||||
--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);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.646 0.222 41.116);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.646 0.222 41.116);
|
||||
}
|
||||
|
||||
.light-green {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.723 0.219 149.579);
|
||||
--primary-foreground: oklch(0.982 0.018 155.826);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.723 0.219 149.579);
|
||||
--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);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.723 0.219 149.579);
|
||||
--sidebar-primary-foreground: oklch(0.982 0.018 155.826);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.723 0.219 149.579);
|
||||
}
|
||||
|
||||
.dark-green {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.696 0.17 162.48);
|
||||
--primary-foreground: oklch(0.393 0.095 152.535);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--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.527 0.154 150.069);
|
||||
--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);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.696 0.17 162.48);
|
||||
--sidebar-primary-foreground: oklch(0.393 0.095 152.535);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.527 0.154 150.069);
|
||||
}
|
||||
|
||||
.light-blue {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.623 0.214 259.815);
|
||||
--primary-foreground: oklch(0.97 0.014 254.604);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.623 0.214 259.815);
|
||||
--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);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.623 0.214 259.815);
|
||||
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.623 0.214 259.815);
|
||||
}
|
||||
|
||||
.dark-blue {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.546 0.245 262.881);
|
||||
--primary-foreground: oklch(0.379 0.146 265.522);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--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.488 0.243 264.376);
|
||||
--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);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.546 0.245 262.881);
|
||||
--sidebar-primary-foreground: oklch(0.379 0.146 265.522);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.488 0.243 264.376);
|
||||
}
|
||||
|
||||
.light-yellow {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.795 0.184 86.047);
|
||||
--primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.795 0.184 86.047);
|
||||
--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);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.795 0.184 86.047);
|
||||
--sidebar-primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.795 0.184 86.047);
|
||||
}
|
||||
|
||||
.dark-yellow {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.795 0.184 86.047);
|
||||
--primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--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.554 0.135 66.442);
|
||||
--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);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.795 0.184 86.047);
|
||||
--sidebar-primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.554 0.135 66.442);
|
||||
}
|
||||
|
||||
.light-violet {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.606 0.25 292.717);
|
||||
--primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.606 0.25 292.717);
|
||||
--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);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.606 0.25 292.717);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.606 0.25 292.717);
|
||||
}
|
||||
|
||||
.dark-violet {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.541 0.281 293.009);
|
||||
--primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--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.541 0.281 293.009);
|
||||
--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);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.541 0.281 293.009);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.541 0.281 293.009);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
@ -1,3 +1,7 @@
|
||||
import Footer from "@/components/global/Footer";
|
||||
import Header from "@/components/global/Header";
|
||||
import { ThemeProvider } from "@/components/global/ThemeProvider";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
@ -13,8 +17,8 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Gaertan Art Admin",
|
||||
description: "Admin page for the gaertan.art website",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@ -23,11 +27,47 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark-violet"
|
||||
themes={[
|
||||
'light-zinc',
|
||||
'light-red',
|
||||
'light-rose',
|
||||
'light-orange',
|
||||
'light-green',
|
||||
'light-blue',
|
||||
'light-yellow',
|
||||
'light-violet',
|
||||
'dark-zinc',
|
||||
'dark-red',
|
||||
'dark-rose',
|
||||
'dark-orange',
|
||||
'dark-green',
|
||||
'dark-blue',
|
||||
'dark-yellow',
|
||||
'dark-violet',
|
||||
]}
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<div className="flex flex-col min-h-screen min-w-screen">
|
||||
<header className="sticky top-0 z-50 h-14 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 px-4 py-2">
|
||||
<Header />
|
||||
</header>
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
{children}
|
||||
</main>
|
||||
<footer className="mt-auto px-4 py-2 h-14 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<Footer />
|
||||
</footer>
|
||||
</div>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
101
src/app/page.tsx
101
src/app/page.tsx
@ -1,103 +1,8 @@
|
||||
import Image from "next/image";
|
||||
|
||||
export default function Home() {
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
|
||||
src/app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
<div>
|
||||
ADMIN HOME
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
28
src/auth.ts
Normal file
28
src/auth.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import NextAuth from "next-auth"
|
||||
import GitHub from "next-auth/providers/github"
|
||||
|
||||
export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
providers: [GitHub],
|
||||
callbacks: {
|
||||
async signIn({ profile }) {
|
||||
return profile?.login === "skeltoras"
|
||||
},
|
||||
|
||||
async jwt({ token, profile }) {
|
||||
if (profile) {
|
||||
token.login = profile.login
|
||||
token.id = profile.id
|
||||
}
|
||||
return token
|
||||
},
|
||||
|
||||
async session({ session, token }) {
|
||||
const typedToken = token as { login?: string; id?: string }
|
||||
|
||||
if (typedToken.login) session.user.login = typedToken.login
|
||||
if (typedToken.id) session.user.id = typedToken.id
|
||||
|
||||
return session
|
||||
},
|
||||
},
|
||||
})
|
54
src/components/editor/plate-editor.tsx
Normal file
54
src/components/editor/plate-editor.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import { Plate, usePlateEditor } from 'platejs/react';
|
||||
|
||||
import { BasicNodesKit } from '@/components/editor/plugins/basic-nodes-kit';
|
||||
import { Editor, EditorContainer } from '@/components/ui/editor';
|
||||
|
||||
export function PlateEditor() {
|
||||
const editor = usePlateEditor({
|
||||
plugins: BasicNodesKit,
|
||||
value,
|
||||
});
|
||||
|
||||
return (
|
||||
<Plate editor={editor}>
|
||||
<EditorContainer>
|
||||
<Editor variant="demo" placeholder="Type..." />
|
||||
</EditorContainer>
|
||||
</Plate>
|
||||
);
|
||||
}
|
||||
|
||||
const value = [
|
||||
{
|
||||
children: [{ text: 'Basic Editor' }],
|
||||
type: 'h1',
|
||||
},
|
||||
{
|
||||
children: [{ text: 'Heading 2' }],
|
||||
type: 'h2',
|
||||
},
|
||||
{
|
||||
children: [{ text: 'Heading 3' }],
|
||||
type: 'h3',
|
||||
},
|
||||
{
|
||||
children: [{ text: 'This is a blockquote element' }],
|
||||
type: 'blockquote',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{ text: 'Basic marks: ' },
|
||||
{ bold: true, text: 'bold' },
|
||||
{ text: ', ' },
|
||||
{ italic: true, text: 'italic' },
|
||||
{ text: ', ' },
|
||||
{ text: 'underline', underline: true },
|
||||
{ text: ', ' },
|
||||
{ strikethrough: true, text: 'strikethrough' },
|
||||
{ text: '.' },
|
||||
],
|
||||
type: 'p',
|
||||
},
|
||||
];
|
35
src/components/editor/plugins/basic-blocks-base-kit.tsx
Normal file
35
src/components/editor/plugins/basic-blocks-base-kit.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import {
|
||||
BaseBlockquotePlugin,
|
||||
BaseH1Plugin,
|
||||
BaseH2Plugin,
|
||||
BaseH3Plugin,
|
||||
BaseH4Plugin,
|
||||
BaseH5Plugin,
|
||||
BaseH6Plugin,
|
||||
BaseHorizontalRulePlugin,
|
||||
} from '@platejs/basic-nodes';
|
||||
import { BaseParagraphPlugin } from 'platejs';
|
||||
|
||||
import { BlockquoteElementStatic } from '@/components/ui/blockquote-node-static';
|
||||
import {
|
||||
H1ElementStatic,
|
||||
H2ElementStatic,
|
||||
H3ElementStatic,
|
||||
H4ElementStatic,
|
||||
H5ElementStatic,
|
||||
H6ElementStatic,
|
||||
} from '@/components/ui/heading-node-static';
|
||||
import { HrElementStatic } from '@/components/ui/hr-node-static';
|
||||
import { ParagraphElementStatic } from '@/components/ui/paragraph-node-static';
|
||||
|
||||
export const BaseBasicBlocksKit = [
|
||||
BaseParagraphPlugin.withComponent(ParagraphElementStatic),
|
||||
BaseH1Plugin.withComponent(H1ElementStatic),
|
||||
BaseH2Plugin.withComponent(H2ElementStatic),
|
||||
BaseH3Plugin.withComponent(H3ElementStatic),
|
||||
BaseH4Plugin.withComponent(H4ElementStatic),
|
||||
BaseH5Plugin.withComponent(H5ElementStatic),
|
||||
BaseH6Plugin.withComponent(H6ElementStatic),
|
||||
BaseBlockquotePlugin.withComponent(BlockquoteElementStatic),
|
||||
BaseHorizontalRulePlugin.withComponent(HrElementStatic),
|
||||
];
|
88
src/components/editor/plugins/basic-blocks-kit.tsx
Normal file
88
src/components/editor/plugins/basic-blocks-kit.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
BlockquotePlugin,
|
||||
H1Plugin,
|
||||
H2Plugin,
|
||||
H3Plugin,
|
||||
H4Plugin,
|
||||
H5Plugin,
|
||||
H6Plugin,
|
||||
HorizontalRulePlugin,
|
||||
} from '@platejs/basic-nodes/react';
|
||||
import { ParagraphPlugin } from 'platejs/react';
|
||||
|
||||
import { BlockquoteElement } from '@/components/ui/blockquote-node';
|
||||
import {
|
||||
H1Element,
|
||||
H2Element,
|
||||
H3Element,
|
||||
H4Element,
|
||||
H5Element,
|
||||
H6Element,
|
||||
} from '@/components/ui/heading-node';
|
||||
import { HrElement } from '@/components/ui/hr-node';
|
||||
import { ParagraphElement } from '@/components/ui/paragraph-node';
|
||||
|
||||
export const BasicBlocksKit = [
|
||||
ParagraphPlugin.withComponent(ParagraphElement),
|
||||
H1Plugin.configure({
|
||||
node: {
|
||||
component: H1Element,
|
||||
},
|
||||
rules: {
|
||||
break: { empty: 'reset' },
|
||||
},
|
||||
shortcuts: { toggle: { keys: 'mod+alt+1' } },
|
||||
}),
|
||||
H2Plugin.configure({
|
||||
node: {
|
||||
component: H2Element,
|
||||
},
|
||||
rules: {
|
||||
break: { empty: 'reset' },
|
||||
},
|
||||
shortcuts: { toggle: { keys: 'mod+alt+2' } },
|
||||
}),
|
||||
H3Plugin.configure({
|
||||
node: {
|
||||
component: H3Element,
|
||||
},
|
||||
rules: {
|
||||
break: { empty: 'reset' },
|
||||
},
|
||||
shortcuts: { toggle: { keys: 'mod+alt+3' } },
|
||||
}),
|
||||
H4Plugin.configure({
|
||||
node: {
|
||||
component: H4Element,
|
||||
},
|
||||
rules: {
|
||||
break: { empty: 'reset' },
|
||||
},
|
||||
shortcuts: { toggle: { keys: 'mod+alt+4' } },
|
||||
}),
|
||||
H5Plugin.configure({
|
||||
node: {
|
||||
component: H5Element,
|
||||
},
|
||||
rules: {
|
||||
break: { empty: 'reset' },
|
||||
},
|
||||
shortcuts: { toggle: { keys: 'mod+alt+5' } },
|
||||
}),
|
||||
H6Plugin.configure({
|
||||
node: {
|
||||
component: H6Element,
|
||||
},
|
||||
rules: {
|
||||
break: { empty: 'reset' },
|
||||
},
|
||||
shortcuts: { toggle: { keys: 'mod+alt+6' } },
|
||||
}),
|
||||
BlockquotePlugin.configure({
|
||||
node: { component: BlockquoteElement },
|
||||
shortcuts: { toggle: { keys: 'mod+shift+period' } },
|
||||
}),
|
||||
HorizontalRulePlugin.withComponent(HrElement),
|
||||
];
|
27
src/components/editor/plugins/basic-marks-base-kit.tsx
Normal file
27
src/components/editor/plugins/basic-marks-base-kit.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import {
|
||||
BaseBoldPlugin,
|
||||
BaseCodePlugin,
|
||||
BaseHighlightPlugin,
|
||||
BaseItalicPlugin,
|
||||
BaseKbdPlugin,
|
||||
BaseStrikethroughPlugin,
|
||||
BaseSubscriptPlugin,
|
||||
BaseSuperscriptPlugin,
|
||||
BaseUnderlinePlugin,
|
||||
} from '@platejs/basic-nodes';
|
||||
|
||||
import { CodeLeafStatic } from '@/components/ui/code-node-static';
|
||||
import { HighlightLeafStatic } from '@/components/ui/highlight-node-static';
|
||||
import { KbdLeafStatic } from '@/components/ui/kbd-node-static';
|
||||
|
||||
export const BaseBasicMarksKit = [
|
||||
BaseBoldPlugin,
|
||||
BaseItalicPlugin,
|
||||
BaseUnderlinePlugin,
|
||||
BaseCodePlugin.withComponent(CodeLeafStatic),
|
||||
BaseStrikethroughPlugin,
|
||||
BaseSubscriptPlugin,
|
||||
BaseSuperscriptPlugin,
|
||||
BaseHighlightPlugin.withComponent(HighlightLeafStatic),
|
||||
BaseKbdPlugin.withComponent(KbdLeafStatic),
|
||||
];
|
41
src/components/editor/plugins/basic-marks-kit.tsx
Normal file
41
src/components/editor/plugins/basic-marks-kit.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
BoldPlugin,
|
||||
CodePlugin,
|
||||
HighlightPlugin,
|
||||
ItalicPlugin,
|
||||
KbdPlugin,
|
||||
StrikethroughPlugin,
|
||||
SubscriptPlugin,
|
||||
SuperscriptPlugin,
|
||||
UnderlinePlugin,
|
||||
} from '@platejs/basic-nodes/react';
|
||||
|
||||
import { CodeLeaf } from '@/components/ui/code-node';
|
||||
import { HighlightLeaf } from '@/components/ui/highlight-node';
|
||||
import { KbdLeaf } from '@/components/ui/kbd-node';
|
||||
|
||||
export const BasicMarksKit = [
|
||||
BoldPlugin,
|
||||
ItalicPlugin,
|
||||
UnderlinePlugin,
|
||||
CodePlugin.configure({
|
||||
node: { component: CodeLeaf },
|
||||
shortcuts: { toggle: { keys: 'mod+e' } },
|
||||
}),
|
||||
StrikethroughPlugin.configure({
|
||||
shortcuts: { toggle: { keys: 'mod+shift+x' } },
|
||||
}),
|
||||
SubscriptPlugin.configure({
|
||||
shortcuts: { toggle: { keys: 'mod+comma' } },
|
||||
}),
|
||||
SuperscriptPlugin.configure({
|
||||
shortcuts: { toggle: { keys: 'mod+period' } },
|
||||
}),
|
||||
HighlightPlugin.configure({
|
||||
node: { component: HighlightLeaf },
|
||||
shortcuts: { toggle: { keys: 'mod+shift+h' } },
|
||||
}),
|
||||
KbdPlugin.withComponent(KbdLeaf),
|
||||
];
|
6
src/components/editor/plugins/basic-nodes-kit.tsx
Normal file
6
src/components/editor/plugins/basic-nodes-kit.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { BasicBlocksKit } from './basic-blocks-kit';
|
||||
import { BasicMarksKit } from './basic-marks-kit';
|
||||
|
||||
export const BasicNodesKit = [...BasicBlocksKit, ...BasicMarksKit];
|
23
src/components/editor/plugins/code-block-base-kit.tsx
Normal file
23
src/components/editor/plugins/code-block-base-kit.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import {
|
||||
BaseCodeBlockPlugin,
|
||||
BaseCodeLinePlugin,
|
||||
BaseCodeSyntaxPlugin,
|
||||
} from '@platejs/code-block';
|
||||
import { all, createLowlight } from 'lowlight';
|
||||
|
||||
import {
|
||||
CodeBlockElementStatic,
|
||||
CodeLineElementStatic,
|
||||
CodeSyntaxLeafStatic,
|
||||
} from '@/components/ui/code-block-node-static';
|
||||
|
||||
const lowlight = createLowlight(all);
|
||||
|
||||
export const BaseCodeBlockKit = [
|
||||
BaseCodeBlockPlugin.configure({
|
||||
node: { component: CodeBlockElementStatic },
|
||||
options: { lowlight },
|
||||
}),
|
||||
BaseCodeLinePlugin.withComponent(CodeLineElementStatic),
|
||||
BaseCodeSyntaxPlugin.withComponent(CodeSyntaxLeafStatic),
|
||||
];
|
26
src/components/editor/plugins/code-block-kit.tsx
Normal file
26
src/components/editor/plugins/code-block-kit.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
CodeBlockPlugin,
|
||||
CodeLinePlugin,
|
||||
CodeSyntaxPlugin,
|
||||
} from '@platejs/code-block/react';
|
||||
import { all, createLowlight } from 'lowlight';
|
||||
|
||||
import {
|
||||
CodeBlockElement,
|
||||
CodeLineElement,
|
||||
CodeSyntaxLeaf,
|
||||
} from '@/components/ui/code-block-node';
|
||||
|
||||
const lowlight = createLowlight(all);
|
||||
|
||||
export const CodeBlockKit = [
|
||||
CodeBlockPlugin.configure({
|
||||
node: { component: CodeBlockElement },
|
||||
options: { lowlight },
|
||||
shortcuts: { toggle: { keys: 'mod+alt+8' } },
|
||||
}),
|
||||
CodeLinePlugin.withComponent(CodeLineElement),
|
||||
CodeSyntaxPlugin.withComponent(CodeSyntaxLeaf),
|
||||
];
|
19
src/components/editor/plugins/indent-base-kit.tsx
Normal file
19
src/components/editor/plugins/indent-base-kit.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { BaseIndentPlugin } from '@platejs/indent';
|
||||
import { KEYS } from 'platejs';
|
||||
|
||||
export const BaseIndentKit = [
|
||||
BaseIndentPlugin.configure({
|
||||
inject: {
|
||||
targetPlugins: [
|
||||
...KEYS.heading,
|
||||
KEYS.p,
|
||||
KEYS.blockquote,
|
||||
KEYS.codeBlock,
|
||||
KEYS.toggle,
|
||||
],
|
||||
},
|
||||
options: {
|
||||
offset: 24,
|
||||
},
|
||||
}),
|
||||
];
|
21
src/components/editor/plugins/indent-kit.tsx
Normal file
21
src/components/editor/plugins/indent-kit.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import { IndentPlugin } from '@platejs/indent/react';
|
||||
import { KEYS } from 'platejs';
|
||||
|
||||
export const IndentKit = [
|
||||
IndentPlugin.configure({
|
||||
inject: {
|
||||
targetPlugins: [
|
||||
...KEYS.heading,
|
||||
KEYS.p,
|
||||
KEYS.blockquote,
|
||||
KEYS.codeBlock,
|
||||
KEYS.toggle,
|
||||
],
|
||||
},
|
||||
options: {
|
||||
offset: 24,
|
||||
},
|
||||
}),
|
||||
];
|
23
src/components/editor/plugins/list-base-kit.tsx
Normal file
23
src/components/editor/plugins/list-base-kit.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { BaseListPlugin } from '@platejs/list';
|
||||
import { KEYS } from 'platejs';
|
||||
|
||||
import { BaseIndentKit } from '@/components/editor/plugins/indent-base-kit';
|
||||
import { BlockListStatic } from '@/components/ui/block-list-static';
|
||||
|
||||
export const BaseListKit = [
|
||||
...BaseIndentKit,
|
||||
BaseListPlugin.configure({
|
||||
inject: {
|
||||
targetPlugins: [
|
||||
...KEYS.heading,
|
||||
KEYS.p,
|
||||
KEYS.blockquote,
|
||||
KEYS.codeBlock,
|
||||
KEYS.toggle,
|
||||
],
|
||||
},
|
||||
render: {
|
||||
belowNodes: BlockListStatic,
|
||||
},
|
||||
}),
|
||||
];
|
25
src/components/editor/plugins/list-kit.tsx
Normal file
25
src/components/editor/plugins/list-kit.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { ListPlugin } from '@platejs/list/react';
|
||||
import { KEYS } from 'platejs';
|
||||
|
||||
import { IndentKit } from '@/components/editor/plugins/indent-kit';
|
||||
import { BlockList } from '@/components/ui/block-list';
|
||||
|
||||
export const ListKit = [
|
||||
...IndentKit,
|
||||
ListPlugin.configure({
|
||||
inject: {
|
||||
targetPlugins: [
|
||||
...KEYS.heading,
|
||||
KEYS.p,
|
||||
KEYS.blockquote,
|
||||
KEYS.codeBlock,
|
||||
KEYS.toggle,
|
||||
],
|
||||
},
|
||||
render: {
|
||||
belowNodes: BlockList,
|
||||
},
|
||||
}),
|
||||
];
|
13
src/components/editor/plugins/markdown-kit.tsx
Normal file
13
src/components/editor/plugins/markdown-kit.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { MarkdownPlugin, remarkMdx, remarkMention } from '@platejs/markdown';
|
||||
import { KEYS } from 'platejs';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
|
||||
export const MarkdownKit = [
|
||||
MarkdownPlugin.configure({
|
||||
options: {
|
||||
disallowedNodes: [KEYS.suggestion],
|
||||
remarkPlugins: [remarkMath, remarkGfm, remarkMdx, remarkMention],
|
||||
},
|
||||
}),
|
||||
];
|
5
src/components/global/Footer.tsx
Normal file
5
src/components/global/Footer.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
export default function Footer() {
|
||||
return (
|
||||
<div>Footer</div>
|
||||
);
|
||||
}
|
18
src/components/global/Header.tsx
Normal file
18
src/components/global/Header.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
// import { auth } from "@/auth";
|
||||
// import { SignInOutButton } from "../auth/SignInOutButton";
|
||||
import ModeToggle from "./ModeToggle";
|
||||
import TopNav from "./TopNav";
|
||||
|
||||
export default async function Header() {
|
||||
// const session = await auth()
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<TopNav />
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
{/* <SignInOutButton session={session} /> */}
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
93
src/components/global/ModeToggle.tsx
Normal file
93
src/components/global/ModeToggle.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
"use client"
|
||||
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useEffect, useState } from "react"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
|
||||
|
||||
const modes = ["light", "dark"] as const
|
||||
const accents = ["zinc", "red", "rose", "orange", "green", "blue", "yellow", "violet"] as const
|
||||
|
||||
const modeIcons = {
|
||||
light: <Sun className="h-4 w-4" />,
|
||||
dark: <Moon className="h-4 w-4" />,
|
||||
}
|
||||
|
||||
export default function ModeToggle() {
|
||||
const { setTheme, theme } = useTheme()
|
||||
const [mode, setMode] = useState("dark")
|
||||
const [accent, setAccent] = useState("violet")
|
||||
|
||||
useEffect(() => {
|
||||
const parts = theme?.split("-")
|
||||
if (parts?.length === 2) {
|
||||
setMode(parts[0])
|
||||
setAccent(parts[1])
|
||||
}
|
||||
}, [theme])
|
||||
|
||||
function updateTheme(newMode: string, newAccent: string) {
|
||||
const fullTheme = `${newMode}-${newAccent}`
|
||||
setTheme(fullTheme)
|
||||
}
|
||||
|
||||
const accentColorMap: Record<string, string> = {
|
||||
zinc: "text-zinc-600",
|
||||
red: "text-red-600",
|
||||
rose: "text-rose-600",
|
||||
orange: "text-orange-600",
|
||||
green: "text-green-600",
|
||||
blue: "text-blue-600",
|
||||
yellow: "text-yellow-600",
|
||||
violet: "text-violet-600",
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-4 items-center">
|
||||
<Select
|
||||
value={mode}
|
||||
onValueChange={(value) => {
|
||||
setMode(value)
|
||||
updateTheme(value, accent)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[70px]">
|
||||
<SelectValue
|
||||
placeholder="Mode"
|
||||
className="flex items-center gap-2"
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{modes.map((m) => (
|
||||
<SelectItem key={m} value={m}>
|
||||
<span className="flex items-center gap-2">
|
||||
{modeIcons[m]}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={accent}
|
||||
onValueChange={(value) => {
|
||||
setAccent(value)
|
||||
updateTheme(mode, value)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="Accent" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{accents.map((a) => (
|
||||
<SelectItem key={a} value={a} className={cn(accentColorMap[a])}>
|
||||
{a.charAt(0).toUpperCase() + a.slice(1)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
11
src/components/global/ThemeProvider.tsx
Normal file
11
src/components/global/ThemeProvider.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
import * as React from "react"
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
25
src/components/global/Toaster.tsx
Normal file
25
src/components/global/Toaster.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
65
src/components/global/TopNav.tsx
Normal file
65
src/components/global/TopNav.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
"use client"
|
||||
|
||||
import { NavigationMenu, NavigationMenuContent, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function TopNav() {
|
||||
return (
|
||||
<NavigationMenu viewport={false}>
|
||||
<NavigationMenuList>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||
<Link href="/">Home</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>Portfolio</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<ul className="grid gap-2 p-4 w-48">
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link href="/portfolio" className="block px-2 py-1 rounded hover:bg-muted">
|
||||
All Portfolio Images
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link href="/portfolio/arttypes" className="block px-2 py-1 rounded hover:bg-muted">
|
||||
Art Types
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link href="/portfolio/categories" className="block px-2 py-1 rounded hover:bg-muted">
|
||||
Categories
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link href="/portfolio/tags" className="block px-2 py-1 rounded hover:bg-muted">
|
||||
Tags
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
</ul>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||
<Link href="/items/commissions/types">CommissionTypes</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||
<Link href="/items/commissions/tos">ToS</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
);
|
||||
}
|
46
src/components/ui/badge.tsx
Normal file
46
src/components/ui/badge.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
83
src/components/ui/block-list-static.tsx
Normal file
83
src/components/ui/block-list-static.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type {
|
||||
RenderStaticNodeWrapper,
|
||||
SlateRenderElementProps,
|
||||
TListElement,
|
||||
} from 'platejs';
|
||||
|
||||
import { isOrderedList } from '@platejs/list';
|
||||
import { CheckIcon } from 'lucide-react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const config: Record<
|
||||
string,
|
||||
{
|
||||
Li: React.FC<SlateRenderElementProps>;
|
||||
Marker: React.FC<SlateRenderElementProps>;
|
||||
}
|
||||
> = {
|
||||
todo: {
|
||||
Li: TodoLiStatic,
|
||||
Marker: TodoMarkerStatic,
|
||||
},
|
||||
};
|
||||
|
||||
export const BlockListStatic: RenderStaticNodeWrapper = (props) => {
|
||||
if (!props.element.listStyleType) return;
|
||||
|
||||
return (props) => <List {...props} />;
|
||||
};
|
||||
|
||||
function List(props: SlateRenderElementProps) {
|
||||
const { listStart, listStyleType } = props.element as TListElement;
|
||||
const { Li, Marker } = config[listStyleType] ?? {};
|
||||
const List = isOrderedList(props.element) ? 'ol' : 'ul';
|
||||
|
||||
return (
|
||||
<List
|
||||
className="relative m-0 p-0"
|
||||
style={{ listStyleType }}
|
||||
start={listStart}
|
||||
>
|
||||
{Marker && <Marker {...props} />}
|
||||
{Li ? <Li {...props} /> : <li>{props.children}</li>}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
function TodoMarkerStatic(props: SlateRenderElementProps) {
|
||||
const checked = props.element.checked as boolean;
|
||||
|
||||
return (
|
||||
<div contentEditable={false}>
|
||||
<button
|
||||
className={cn(
|
||||
'peer pointer-events-none absolute top-1 -left-6 size-4 shrink-0 rounded-sm border border-primary bg-background ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||
props.className
|
||||
)}
|
||||
data-state={checked ? 'checked' : 'unchecked'}
|
||||
type="button"
|
||||
>
|
||||
<div className={cn('flex items-center justify-center text-current')}>
|
||||
{checked && <CheckIcon className="size-4" />}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TodoLiStatic(props: SlateRenderElementProps) {
|
||||
return (
|
||||
<li
|
||||
className={cn(
|
||||
'list-none',
|
||||
(props.element.checked as boolean) &&
|
||||
'text-muted-foreground line-through'
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</li>
|
||||
);
|
||||
}
|
87
src/components/ui/block-list.tsx
Normal file
87
src/components/ui/block-list.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type { TListElement } from 'platejs';
|
||||
|
||||
import { isOrderedList } from '@platejs/list';
|
||||
import {
|
||||
useTodoListElement,
|
||||
useTodoListElementState,
|
||||
} from '@platejs/list/react';
|
||||
import {
|
||||
type PlateElementProps,
|
||||
type RenderNodeWrapper,
|
||||
useReadOnly,
|
||||
} from 'platejs/react';
|
||||
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const config: Record<
|
||||
string,
|
||||
{
|
||||
Li: React.FC<PlateElementProps>;
|
||||
Marker: React.FC<PlateElementProps>;
|
||||
}
|
||||
> = {
|
||||
todo: {
|
||||
Li: TodoLi,
|
||||
Marker: TodoMarker,
|
||||
},
|
||||
};
|
||||
|
||||
export const BlockList: RenderNodeWrapper = (props) => {
|
||||
if (!props.element.listStyleType) return;
|
||||
|
||||
return (props) => <List {...props} />;
|
||||
};
|
||||
|
||||
function List(props: PlateElementProps) {
|
||||
const { listStart, listStyleType } = props.element as TListElement;
|
||||
const { Li, Marker } = config[listStyleType] ?? {};
|
||||
const List = isOrderedList(props.element) ? 'ol' : 'ul';
|
||||
|
||||
return (
|
||||
<List
|
||||
className="relative m-0 p-0"
|
||||
style={{ listStyleType }}
|
||||
start={listStart}
|
||||
>
|
||||
{Marker && <Marker {...props} />}
|
||||
{Li ? <Li {...props} /> : <li>{props.children}</li>}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
function TodoMarker(props: PlateElementProps) {
|
||||
const state = useTodoListElementState({ element: props.element });
|
||||
const { checkboxProps } = useTodoListElement(state);
|
||||
const readOnly = useReadOnly();
|
||||
|
||||
return (
|
||||
<div contentEditable={false}>
|
||||
<Checkbox
|
||||
className={cn(
|
||||
'absolute top-1 -left-6',
|
||||
readOnly && 'pointer-events-none'
|
||||
)}
|
||||
{...checkboxProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TodoLi(props: PlateElementProps) {
|
||||
return (
|
||||
<li
|
||||
className={cn(
|
||||
'list-none',
|
||||
(props.element.checked as boolean) &&
|
||||
'text-muted-foreground line-through'
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</li>
|
||||
);
|
||||
}
|
13
src/components/ui/blockquote-node-static.tsx
Normal file
13
src/components/ui/blockquote-node-static.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { type SlateElementProps, SlateElement } from 'platejs';
|
||||
|
||||
export function BlockquoteElementStatic(props: SlateElementProps) {
|
||||
return (
|
||||
<SlateElement
|
||||
as="blockquote"
|
||||
className="my-1 border-l-2 pl-6 italic"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
13
src/components/ui/blockquote-node.tsx
Normal file
13
src/components/ui/blockquote-node.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { type PlateElementProps, PlateElement } from 'platejs/react';
|
||||
|
||||
export function BlockquoteElement(props: PlateElementProps) {
|
||||
return (
|
||||
<PlateElement
|
||||
as="blockquote"
|
||||
className="my-1 border-l-2 pl-6 italic"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
59
src/components/ui/button.tsx
Normal file
59
src/components/ui/button.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
213
src/components/ui/calendar.tsx
Normal file
213
src/components/ui/calendar.tsx
Normal file
@ -0,0 +1,213 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from "lucide-react"
|
||||
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
captionLayout = "label",
|
||||
buttonVariant = "ghost",
|
||||
formatters,
|
||||
components,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayPicker> & {
|
||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||
}) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn(
|
||||
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||
className
|
||||
)}
|
||||
captionLayout={captionLayout}
|
||||
formatters={{
|
||||
formatMonthDropdown: (date) =>
|
||||
date.toLocaleString("default", { month: "short" }),
|
||||
...formatters,
|
||||
}}
|
||||
classNames={{
|
||||
root: cn("w-fit", defaultClassNames.root),
|
||||
months: cn(
|
||||
"flex gap-4 flex-col md:flex-row relative",
|
||||
defaultClassNames.months
|
||||
),
|
||||
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
||||
nav: cn(
|
||||
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
|
||||
defaultClassNames.nav
|
||||
),
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||
defaultClassNames.button_previous
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||
defaultClassNames.button_next
|
||||
),
|
||||
month_caption: cn(
|
||||
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
|
||||
defaultClassNames.month_caption
|
||||
),
|
||||
dropdowns: cn(
|
||||
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
|
||||
defaultClassNames.dropdowns
|
||||
),
|
||||
dropdown_root: cn(
|
||||
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
||||
defaultClassNames.dropdown_root
|
||||
),
|
||||
dropdown: cn(
|
||||
"absolute bg-popover inset-0 opacity-0",
|
||||
defaultClassNames.dropdown
|
||||
),
|
||||
caption_label: cn(
|
||||
"select-none font-medium",
|
||||
captionLayout === "label"
|
||||
? "text-sm"
|
||||
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
|
||||
defaultClassNames.caption_label
|
||||
),
|
||||
table: "w-full border-collapse",
|
||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||
weekday: cn(
|
||||
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
|
||||
defaultClassNames.weekday
|
||||
),
|
||||
week: cn("flex w-full mt-2", defaultClassNames.week),
|
||||
week_number_header: cn(
|
||||
"select-none w-(--cell-size)",
|
||||
defaultClassNames.week_number_header
|
||||
),
|
||||
week_number: cn(
|
||||
"text-[0.8rem] select-none text-muted-foreground",
|
||||
defaultClassNames.week_number
|
||||
),
|
||||
day: cn(
|
||||
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||
defaultClassNames.day
|
||||
),
|
||||
range_start: cn(
|
||||
"rounded-l-md bg-accent",
|
||||
defaultClassNames.range_start
|
||||
),
|
||||
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
||||
today: cn(
|
||||
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||
defaultClassNames.today
|
||||
),
|
||||
outside: cn(
|
||||
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||
defaultClassNames.outside
|
||||
),
|
||||
disabled: cn(
|
||||
"text-muted-foreground opacity-50",
|
||||
defaultClassNames.disabled
|
||||
),
|
||||
hidden: cn("invisible", defaultClassNames.hidden),
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
Root: ({ className, rootRef, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
data-slot="calendar"
|
||||
ref={rootRef}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
Chevron: ({ className, orientation, ...props }) => {
|
||||
if (orientation === "left") {
|
||||
return (
|
||||
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
if (orientation === "right") {
|
||||
return (
|
||||
<ChevronRightIcon
|
||||
className={cn("size-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
},
|
||||
DayButton: CalendarDayButton,
|
||||
WeekNumber: ({ children, ...props }) => {
|
||||
return (
|
||||
<td {...props}>
|
||||
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
},
|
||||
...components,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CalendarDayButton({
|
||||
className,
|
||||
day,
|
||||
modifiers,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayButton>) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
const ref = React.useRef<HTMLButtonElement>(null)
|
||||
React.useEffect(() => {
|
||||
if (modifiers.focused) ref.current?.focus()
|
||||
}, [modifiers.focused])
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-day={day.date.toLocaleDateString()}
|
||||
data-selected-single={
|
||||
modifiers.selected &&
|
||||
!modifiers.range_start &&
|
||||
!modifiers.range_end &&
|
||||
!modifiers.range_middle
|
||||
}
|
||||
data-range-start={modifiers.range_start}
|
||||
data-range-end={modifiers.range_end}
|
||||
data-range-middle={modifiers.range_middle}
|
||||
className={cn(
|
||||
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
|
||||
defaultClassNames.day,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Calendar, CalendarDayButton }
|
92
src/components/ui/card.tsx
Normal file
92
src/components/ui/card.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 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-1.5 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("leading-none font-semibold", 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,
|
||||
}
|
32
src/components/ui/checkbox.tsx
Normal file
32
src/components/ui/checkbox.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
36
src/components/ui/code-block-node-static.tsx
Normal file
36
src/components/ui/code-block-node-static.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
type SlateElementProps,
|
||||
type SlateLeafProps,
|
||||
type TCodeBlockElement,
|
||||
SlateElement,
|
||||
SlateLeaf,
|
||||
} from 'platejs';
|
||||
|
||||
export function CodeBlockElementStatic(
|
||||
props: SlateElementProps<TCodeBlockElement>
|
||||
) {
|
||||
return (
|
||||
<SlateElement
|
||||
className="py-1 **:[.hljs-addition]:bg-[#f0fff4] **:[.hljs-addition]:text-[#22863a] dark:**:[.hljs-addition]:bg-[#3c5743] dark:**:[.hljs-addition]:text-[#ceead5] **:[.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable]:text-[#005cc5] dark:**:[.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable]:text-[#6596cf] **:[.hljs-built\\\\_in,.hljs-symbol]:text-[#e36209] dark:**:[.hljs-built\\\\_in,.hljs-symbol]:text-[#c3854e] **:[.hljs-bullet]:text-[#735c0f] **:[.hljs-comment,.hljs-code,.hljs-formula]:text-[#6a737d] dark:**:[.hljs-comment,.hljs-code,.hljs-formula]:text-[#6a737d] **:[.hljs-deletion]:bg-[#ffeef0] **:[.hljs-deletion]:text-[#b31d28] dark:**:[.hljs-deletion]:bg-[#473235] dark:**:[.hljs-deletion]:text-[#e7c7cb] **:[.hljs-emphasis]:italic **:[.hljs-keyword,.hljs-doctag,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language\\\\_]:text-[#d73a49] dark:**:[.hljs-keyword,.hljs-doctag,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language\\\\_]:text-[#ee6960] **:[.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo]:text-[#22863a] dark:**:[.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo]:text-[#36a84f] **:[.hljs-regexp,.hljs-string,.hljs-meta_.hljs-string]:text-[#032f62] dark:**:[.hljs-regexp,.hljs-string,.hljs-meta_.hljs-string]:text-[#3593ff] **:[.hljs-section]:font-bold **:[.hljs-section]:text-[#005cc5] dark:**:[.hljs-section]:text-[#61a5f2] **:[.hljs-strong]:font-bold **:[.hljs-title,.hljs-title.class\\\\_,.hljs-title.class\\\\_.inherited\\\\_\\\\_,.hljs-title.function\\\\_]:text-[#6f42c1] dark:**:[.hljs-title,.hljs-title.class\\\\_,.hljs-title.class\\\\_.inherited\\\\_\\\\_,.hljs-title.function\\\\_]:text-[#a77bfa]"
|
||||
{...props}
|
||||
>
|
||||
<div className="relative rounded-md bg-muted/50">
|
||||
<pre className="overflow-x-auto p-8 pr-4 font-mono text-sm leading-[normal] [tab-size:2] print:break-inside-avoid">
|
||||
<code>{props.children}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</SlateElement>
|
||||
);
|
||||
}
|
||||
|
||||
export function CodeLineElementStatic(props: SlateElementProps) {
|
||||
return <SlateElement {...props} />;
|
||||
}
|
||||
|
||||
export function CodeSyntaxLeafStatic(props: SlateLeafProps) {
|
||||
const tokenClassName = props.leaf.className as string;
|
||||
|
||||
return <SlateLeaf className={tokenClassName} {...props} />;
|
||||
}
|
289
src/components/ui/code-block-node.tsx
Normal file
289
src/components/ui/code-block-node.tsx
Normal file
@ -0,0 +1,289 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { formatCodeBlock, isLangSupported } from '@platejs/code-block';
|
||||
import { BracesIcon, Check, CheckIcon, CopyIcon } from 'lucide-react';
|
||||
import { type TCodeBlockElement, type TCodeSyntaxLeaf, NodeApi } from 'platejs';
|
||||
import {
|
||||
type PlateElementProps,
|
||||
type PlateLeafProps,
|
||||
PlateElement,
|
||||
PlateLeaf,
|
||||
} from 'platejs/react';
|
||||
import { useEditorRef, useElement, useReadOnly } from 'platejs/react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/ui/command';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function CodeBlockElement(props: PlateElementProps<TCodeBlockElement>) {
|
||||
const { editor, element } = props;
|
||||
|
||||
return (
|
||||
<PlateElement
|
||||
className="py-1 **:[.hljs-addition]:bg-[#f0fff4] **:[.hljs-addition]:text-[#22863a] dark:**:[.hljs-addition]:bg-[#3c5743] dark:**:[.hljs-addition]:text-[#ceead5] **:[.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable]:text-[#005cc5] dark:**:[.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable]:text-[#6596cf] **:[.hljs-built\\\\_in,.hljs-symbol]:text-[#e36209] dark:**:[.hljs-built\\\\_in,.hljs-symbol]:text-[#c3854e] **:[.hljs-bullet]:text-[#735c0f] **:[.hljs-comment,.hljs-code,.hljs-formula]:text-[#6a737d] dark:**:[.hljs-comment,.hljs-code,.hljs-formula]:text-[#6a737d] **:[.hljs-deletion]:bg-[#ffeef0] **:[.hljs-deletion]:text-[#b31d28] dark:**:[.hljs-deletion]:bg-[#473235] dark:**:[.hljs-deletion]:text-[#e7c7cb] **:[.hljs-emphasis]:italic **:[.hljs-keyword,.hljs-doctag,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language\\\\_]:text-[#d73a49] dark:**:[.hljs-keyword,.hljs-doctag,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language\\\\_]:text-[#ee6960] **:[.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo]:text-[#22863a] dark:**:[.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo]:text-[#36a84f] **:[.hljs-regexp,.hljs-string,.hljs-meta_.hljs-string]:text-[#032f62] dark:**:[.hljs-regexp,.hljs-string,.hljs-meta_.hljs-string]:text-[#3593ff] **:[.hljs-section]:font-bold **:[.hljs-section]:text-[#005cc5] dark:**:[.hljs-section]:text-[#61a5f2] **:[.hljs-strong]:font-bold **:[.hljs-title,.hljs-title.class\\\\_,.hljs-title.class\\\\_.inherited\\\\_\\\\_,.hljs-title.function\\\\_]:text-[#6f42c1] dark:**:[.hljs-title,.hljs-title.class\\\\_,.hljs-title.class\\\\_.inherited\\\\_\\\\_,.hljs-title.function\\\\_]:text-[#a77bfa]"
|
||||
{...props}
|
||||
>
|
||||
<div className="relative rounded-md bg-muted/50">
|
||||
<pre className="overflow-x-auto p-8 pr-4 font-mono text-sm leading-[normal] [tab-size:2] print:break-inside-avoid">
|
||||
<code>{props.children}</code>
|
||||
</pre>
|
||||
|
||||
<div
|
||||
className="absolute top-1 right-1 z-10 flex gap-0.5 select-none"
|
||||
contentEditable={false}
|
||||
>
|
||||
{isLangSupported(element.lang) && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="size-6 text-xs"
|
||||
onClick={() => formatCodeBlock(editor, { element })}
|
||||
title="Format code"
|
||||
>
|
||||
<BracesIcon className="!size-3.5 text-muted-foreground" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<CodeBlockCombobox />
|
||||
|
||||
<CopyButton
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="size-6 gap-1 text-xs text-muted-foreground"
|
||||
value={() => NodeApi.string(element)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PlateElement>
|
||||
);
|
||||
}
|
||||
|
||||
function CodeBlockCombobox() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const readOnly = useReadOnly();
|
||||
const editor = useEditorRef();
|
||||
const element = useElement<TCodeBlockElement>();
|
||||
const value = element.lang || 'plaintext';
|
||||
const [searchValue, setSearchValue] = React.useState('');
|
||||
|
||||
const items = React.useMemo(
|
||||
() =>
|
||||
languages.filter(
|
||||
(language) =>
|
||||
!searchValue ||
|
||||
language.label.toLowerCase().includes(searchValue.toLowerCase())
|
||||
),
|
||||
[searchValue]
|
||||
);
|
||||
|
||||
if (readOnly) return null;
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 justify-between gap-1 px-2 text-xs text-muted-foreground select-none"
|
||||
aria-expanded={open}
|
||||
role="combobox"
|
||||
>
|
||||
{languages.find((language) => language.value === value)?.label ??
|
||||
'Plain Text'}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[200px] p-0"
|
||||
onCloseAutoFocus={() => setSearchValue('')}
|
||||
>
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
className="h-9"
|
||||
value={searchValue}
|
||||
onValueChange={(value) => setSearchValue(value)}
|
||||
placeholder="Search language..."
|
||||
/>
|
||||
<CommandEmpty>No language found.</CommandEmpty>
|
||||
|
||||
<CommandList className="h-[344px] overflow-y-auto">
|
||||
<CommandGroup>
|
||||
{items.map((language) => (
|
||||
<CommandItem
|
||||
key={language.label}
|
||||
className="cursor-pointer"
|
||||
value={language.value}
|
||||
onSelect={(value) => {
|
||||
editor.tf.setNodes<TCodeBlockElement>(
|
||||
{ lang: value },
|
||||
{ at: element }
|
||||
);
|
||||
setSearchValue(value);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
value === language.value ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
{language.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function CopyButton({
|
||||
value,
|
||||
...props
|
||||
}: { value: (() => string) | string } & Omit<
|
||||
React.ComponentProps<typeof Button>,
|
||||
'value'
|
||||
>) {
|
||||
const [hasCopied, setHasCopied] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setHasCopied(false);
|
||||
}, 2000);
|
||||
}, [hasCopied]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
void navigator.clipboard.writeText(
|
||||
typeof value === 'function' ? value() : value
|
||||
);
|
||||
setHasCopied(true);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<span className="sr-only">Copy</span>
|
||||
{hasCopied ? (
|
||||
<CheckIcon className="!size-3" />
|
||||
) : (
|
||||
<CopyIcon className="!size-3" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function CodeLineElement(props: PlateElementProps) {
|
||||
return <PlateElement {...props} />;
|
||||
}
|
||||
|
||||
export function CodeSyntaxLeaf(props: PlateLeafProps<TCodeSyntaxLeaf>) {
|
||||
const tokenClassName = props.leaf.className as string;
|
||||
|
||||
return <PlateLeaf className={tokenClassName} {...props} />;
|
||||
}
|
||||
|
||||
const languages: { label: string; value: string }[] = [
|
||||
{ label: 'Auto', value: 'auto' },
|
||||
{ label: 'Plain Text', value: 'plaintext' },
|
||||
{ label: 'ABAP', value: 'abap' },
|
||||
{ label: 'Agda', value: 'agda' },
|
||||
{ label: 'Arduino', value: 'arduino' },
|
||||
{ label: 'ASCII Art', value: 'ascii' },
|
||||
{ label: 'Assembly', value: 'x86asm' },
|
||||
{ label: 'Bash', value: 'bash' },
|
||||
{ label: 'BASIC', value: 'basic' },
|
||||
{ label: 'BNF', value: 'bnf' },
|
||||
{ label: 'C', value: 'c' },
|
||||
{ label: 'C#', value: 'csharp' },
|
||||
{ label: 'C++', value: 'cpp' },
|
||||
{ label: 'Clojure', value: 'clojure' },
|
||||
{ label: 'CoffeeScript', value: 'coffeescript' },
|
||||
{ label: 'Coq', value: 'coq' },
|
||||
{ label: 'CSS', value: 'css' },
|
||||
{ label: 'Dart', value: 'dart' },
|
||||
{ label: 'Dhall', value: 'dhall' },
|
||||
{ label: 'Diff', value: 'diff' },
|
||||
{ label: 'Docker', value: 'dockerfile' },
|
||||
{ label: 'EBNF', value: 'ebnf' },
|
||||
{ label: 'Elixir', value: 'elixir' },
|
||||
{ label: 'Elm', value: 'elm' },
|
||||
{ label: 'Erlang', value: 'erlang' },
|
||||
{ label: 'F#', value: 'fsharp' },
|
||||
{ label: 'Flow', value: 'flow' },
|
||||
{ label: 'Fortran', value: 'fortran' },
|
||||
{ label: 'Gherkin', value: 'gherkin' },
|
||||
{ label: 'GLSL', value: 'glsl' },
|
||||
{ label: 'Go', value: 'go' },
|
||||
{ label: 'GraphQL', value: 'graphql' },
|
||||
{ label: 'Groovy', value: 'groovy' },
|
||||
{ label: 'Haskell', value: 'haskell' },
|
||||
{ label: 'HCL', value: 'hcl' },
|
||||
{ label: 'HTML', value: 'html' },
|
||||
{ label: 'Idris', value: 'idris' },
|
||||
{ label: 'Java', value: 'java' },
|
||||
{ label: 'JavaScript', value: 'javascript' },
|
||||
{ label: 'JSON', value: 'json' },
|
||||
{ label: 'Julia', value: 'julia' },
|
||||
{ label: 'Kotlin', value: 'kotlin' },
|
||||
{ label: 'LaTeX', value: 'latex' },
|
||||
{ label: 'Less', value: 'less' },
|
||||
{ label: 'Lisp', value: 'lisp' },
|
||||
{ label: 'LiveScript', value: 'livescript' },
|
||||
{ label: 'LLVM IR', value: 'llvm' },
|
||||
{ label: 'Lua', value: 'lua' },
|
||||
{ label: 'Makefile', value: 'makefile' },
|
||||
{ label: 'Markdown', value: 'markdown' },
|
||||
{ label: 'Markup', value: 'markup' },
|
||||
{ label: 'MATLAB', value: 'matlab' },
|
||||
{ label: 'Mathematica', value: 'mathematica' },
|
||||
{ label: 'Mermaid', value: 'mermaid' },
|
||||
{ label: 'Nix', value: 'nix' },
|
||||
{ label: 'Notion Formula', value: 'notion' },
|
||||
{ label: 'Objective-C', value: 'objectivec' },
|
||||
{ label: 'OCaml', value: 'ocaml' },
|
||||
{ label: 'Pascal', value: 'pascal' },
|
||||
{ label: 'Perl', value: 'perl' },
|
||||
{ label: 'PHP', value: 'php' },
|
||||
{ label: 'PowerShell', value: 'powershell' },
|
||||
{ label: 'Prolog', value: 'prolog' },
|
||||
{ label: 'Protocol Buffers', value: 'protobuf' },
|
||||
{ label: 'PureScript', value: 'purescript' },
|
||||
{ label: 'Python', value: 'python' },
|
||||
{ label: 'R', value: 'r' },
|
||||
{ label: 'Racket', value: 'racket' },
|
||||
{ label: 'Reason', value: 'reasonml' },
|
||||
{ label: 'Ruby', value: 'ruby' },
|
||||
{ label: 'Rust', value: 'rust' },
|
||||
{ label: 'Sass', value: 'scss' },
|
||||
{ label: 'Scala', value: 'scala' },
|
||||
{ label: 'Scheme', value: 'scheme' },
|
||||
{ label: 'SCSS', value: 'scss' },
|
||||
{ label: 'Shell', value: 'shell' },
|
||||
{ label: 'Smalltalk', value: 'smalltalk' },
|
||||
{ label: 'Solidity', value: 'solidity' },
|
||||
{ label: 'SQL', value: 'sql' },
|
||||
{ label: 'Swift', value: 'swift' },
|
||||
{ label: 'TOML', value: 'toml' },
|
||||
{ label: 'TypeScript', value: 'typescript' },
|
||||
{ label: 'VB.Net', value: 'vbnet' },
|
||||
{ label: 'Verilog', value: 'verilog' },
|
||||
{ label: 'VHDL', value: 'vhdl' },
|
||||
{ label: 'Visual Basic', value: 'vbnet' },
|
||||
{ label: 'WebAssembly', value: 'wasm' },
|
||||
{ label: 'XML', value: 'xml' },
|
||||
{ label: 'YAML', value: 'yaml' },
|
||||
];
|
17
src/components/ui/code-node-static.tsx
Normal file
17
src/components/ui/code-node-static.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SlateLeafProps } from 'platejs';
|
||||
|
||||
import { SlateLeaf } from 'platejs';
|
||||
|
||||
export function CodeLeafStatic(props: SlateLeafProps) {
|
||||
return (
|
||||
<SlateLeaf
|
||||
{...props}
|
||||
as="code"
|
||||
className="rounded-md bg-muted px-[0.3em] py-[0.2em] font-mono text-sm whitespace-pre-wrap"
|
||||
>
|
||||
{props.children}
|
||||
</SlateLeaf>
|
||||
);
|
||||
}
|
19
src/components/ui/code-node.tsx
Normal file
19
src/components/ui/code-node.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import type { PlateLeafProps } from 'platejs/react';
|
||||
|
||||
import { PlateLeaf } from 'platejs/react';
|
||||
|
||||
export function CodeLeaf(props: PlateLeafProps) {
|
||||
return (
|
||||
<PlateLeaf
|
||||
{...props}
|
||||
as="code"
|
||||
className="rounded-md bg-muted px-[0.3em] py-[0.2em] font-mono text-sm whitespace-pre-wrap"
|
||||
>
|
||||
{props.children}
|
||||
</PlateLeaf>
|
||||
);
|
||||
}
|
184
src/components/ui/command.tsx
Normal file
184
src/components/ui/command.tsx
Normal file
@ -0,0 +1,184 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { SearchIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
children,
|
||||
className,
|
||||
showCloseButton = true,
|
||||
...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("overflow-hidden p-0", className)}
|
||||
showCloseButton={showCloseButton}
|
||||
>
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
className="flex h-9 items-center gap-2 border-b px-3"
|
||||
>
|
||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("bg-border -mx-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
143
src/components/ui/dialog.tsx
Normal file
143
src/components/ui/dialog.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
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(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out 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 p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</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 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
257
src/components/ui/dropdown-menu.tsx
Normal file
257
src/components/ui/dropdown-menu.tsx
Normal file
@ -0,0 +1,257 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
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,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out 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-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
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(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
|
||||
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>
|
||||
{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,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
|
||||
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>
|
||||
{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-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto 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(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
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(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out 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 p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
55
src/components/ui/editor-static.tsx
Normal file
55
src/components/ui/editor-static.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { type PlateStaticProps, PlateStatic } from 'platejs';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export const editorVariants = cva(
|
||||
cn(
|
||||
'group/editor',
|
||||
'relative w-full cursor-text overflow-x-hidden break-words whitespace-pre-wrap select-text',
|
||||
'rounded-md ring-offset-background focus-visible:outline-none',
|
||||
'placeholder:text-muted-foreground/80 **:data-slate-placeholder:top-[auto_!important] **:data-slate-placeholder:text-muted-foreground/80 **:data-slate-placeholder:opacity-100!',
|
||||
'[&_strong]:font-bold'
|
||||
),
|
||||
{
|
||||
defaultVariants: {
|
||||
variant: 'none',
|
||||
},
|
||||
variants: {
|
||||
disabled: {
|
||||
true: 'cursor-not-allowed opacity-50',
|
||||
},
|
||||
focused: {
|
||||
true: 'ring-2 ring-ring ring-offset-2',
|
||||
},
|
||||
variant: {
|
||||
ai: 'w-full px-0 text-base md:text-sm',
|
||||
aiChat:
|
||||
'max-h-[min(70vh,320px)] w-full max-w-[700px] overflow-y-auto px-5 py-3 text-base md:text-sm',
|
||||
default:
|
||||
'size-full px-16 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]',
|
||||
demo: 'size-full px-16 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]',
|
||||
fullWidth: 'size-full px-16 pt-4 pb-72 text-base sm:px-24',
|
||||
none: '',
|
||||
select: 'px-3 py-2 text-base data-readonly:w-fit',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export function EditorStatic({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: PlateStaticProps & VariantProps<typeof editorVariants>) {
|
||||
return (
|
||||
<PlateStatic
|
||||
className={cn(editorVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
129
src/components/ui/editor.tsx
Normal file
129
src/components/ui/editor.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import type { PlateContentProps, PlateViewProps } from 'platejs/react';
|
||||
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { PlateContainer, PlateContent, PlateView } from 'platejs/react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const editorContainerVariants = cva(
|
||||
'relative w-full cursor-text overflow-y-auto caret-primary select-text selection:bg-brand/25 focus-visible:outline-none [&_.slate-selection-area]:z-50 [&_.slate-selection-area]:border [&_.slate-selection-area]:border-brand/25 [&_.slate-selection-area]:bg-brand/15',
|
||||
{
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
variants: {
|
||||
variant: {
|
||||
comment: cn(
|
||||
'flex flex-wrap justify-between gap-1 px-1 py-0.5 text-sm',
|
||||
'rounded-md border-[1.5px] border-transparent bg-transparent',
|
||||
'has-[[data-slate-editor]:focus]:border-brand/50 has-[[data-slate-editor]:focus]:ring-2 has-[[data-slate-editor]:focus]:ring-brand/30',
|
||||
'has-aria-disabled:border-input has-aria-disabled:bg-muted'
|
||||
),
|
||||
default: 'h-full',
|
||||
demo: 'h-[650px]',
|
||||
select: cn(
|
||||
'group rounded-md border border-input ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2',
|
||||
'has-data-readonly:w-fit has-data-readonly:cursor-default has-data-readonly:border-transparent has-data-readonly:focus-within:[box-shadow:none]'
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export function EditorContainer({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & VariantProps<typeof editorContainerVariants>) {
|
||||
return (
|
||||
<PlateContainer
|
||||
className={cn(
|
||||
'ignore-click-outside/toolbar',
|
||||
editorContainerVariants({ variant }),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const editorVariants = cva(
|
||||
cn(
|
||||
'group/editor',
|
||||
'relative w-full cursor-text overflow-x-hidden break-words whitespace-pre-wrap select-text',
|
||||
'rounded-md ring-offset-background focus-visible:outline-none',
|
||||
'placeholder:text-muted-foreground/80 **:data-slate-placeholder:!top-1/2 **:data-slate-placeholder:-translate-y-1/2 **:data-slate-placeholder:text-muted-foreground/80 **:data-slate-placeholder:opacity-100!',
|
||||
'[&_strong]:font-bold'
|
||||
),
|
||||
{
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
variants: {
|
||||
disabled: {
|
||||
true: 'cursor-not-allowed opacity-50',
|
||||
},
|
||||
focused: {
|
||||
true: 'ring-2 ring-ring ring-offset-2',
|
||||
},
|
||||
variant: {
|
||||
ai: 'w-full px-0 text-base md:text-sm',
|
||||
aiChat:
|
||||
'max-h-[min(70vh,320px)] w-full max-w-[700px] overflow-y-auto px-3 py-2 text-base md:text-sm',
|
||||
comment: cn('rounded-none border-none bg-transparent text-sm'),
|
||||
default:
|
||||
'size-full px-16 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]',
|
||||
demo: 'size-full px-16 pt-4 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]',
|
||||
fullWidth: 'size-full px-16 pt-4 pb-72 text-base sm:px-24',
|
||||
none: '',
|
||||
select: 'px-3 py-2 text-base data-readonly:w-fit',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export type EditorProps = PlateContentProps &
|
||||
VariantProps<typeof editorVariants>;
|
||||
|
||||
export const Editor = React.forwardRef<HTMLDivElement, EditorProps>(
|
||||
({ className, disabled, focused, variant, ...props }, ref) => {
|
||||
return (
|
||||
<PlateContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
editorVariants({
|
||||
disabled,
|
||||
focused,
|
||||
variant,
|
||||
}),
|
||||
className
|
||||
)}
|
||||
disabled={disabled}
|
||||
disableDefaultStyles
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Editor.displayName = 'Editor';
|
||||
|
||||
export function EditorView({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: PlateViewProps & VariantProps<typeof editorVariants>) {
|
||||
return (
|
||||
<PlateView
|
||||
{...props}
|
||||
className={cn(editorVariants({ variant }), className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
EditorView.displayName = 'EditorView';
|
17
src/components/ui/fixed-toolbar.tsx
Normal file
17
src/components/ui/fixed-toolbar.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { Toolbar } from './toolbar';
|
||||
|
||||
export function FixedToolbar(props: React.ComponentProps<typeof Toolbar>) {
|
||||
return (
|
||||
<Toolbar
|
||||
{...props}
|
||||
className={cn(
|
||||
'sticky top-0 left-0 z-50 scrollbar-hide w-full justify-between overflow-x-auto rounded-t-lg border-b border-b-border bg-background/95 p-1 backdrop-blur-sm supports-backdrop-blur:bg-background/60',
|
||||
props.className
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
167
src/components/ui/form.tsx
Normal file
167
src/components/ui/form.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
useFormState,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
} from "react-hook-form"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState } = useFormContext()
|
||||
const formState = useFormState({ name: fieldContext.name })
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
|
||||
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div
|
||||
data-slot="form-item"
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function FormLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
data-slot="form-label"
|
||||
data-error={!!error}
|
||||
className={cn("data-[error=true]:text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
data-slot="form-control"
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-description"
|
||||
id={formDescriptionId}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message ?? "") : props.children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-message"
|
||||
id={formMessageId}
|
||||
className={cn("text-destructive text-sm", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
68
src/components/ui/heading-node-static.tsx
Normal file
68
src/components/ui/heading-node-static.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SlateElementProps } from 'platejs';
|
||||
|
||||
import { type VariantProps, cva } from 'class-variance-authority';
|
||||
import { SlateElement } from 'platejs';
|
||||
|
||||
const headingVariants = cva('relative mb-1', {
|
||||
variants: {
|
||||
variant: {
|
||||
h1: 'mt-[1.6em] pb-1 font-heading text-4xl font-bold',
|
||||
h2: 'mt-[1.4em] pb-px font-heading text-2xl font-semibold tracking-tight',
|
||||
h3: 'mt-[1em] pb-px font-heading text-xl font-semibold tracking-tight',
|
||||
h4: 'mt-[0.75em] font-heading text-lg font-semibold tracking-tight',
|
||||
h5: 'mt-[0.75em] text-lg font-semibold tracking-tight',
|
||||
h6: 'mt-[0.75em] text-base font-semibold tracking-tight',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function HeadingElementStatic({
|
||||
variant = 'h1',
|
||||
...props
|
||||
}: SlateElementProps & VariantProps<typeof headingVariants>) {
|
||||
return (
|
||||
<SlateElement
|
||||
as={variant!}
|
||||
className={headingVariants({ variant })}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</SlateElement>
|
||||
);
|
||||
}
|
||||
|
||||
export function H1ElementStatic(props: SlateElementProps) {
|
||||
return <HeadingElementStatic variant="h1" {...props} />;
|
||||
}
|
||||
|
||||
export function H2ElementStatic(
|
||||
props: React.ComponentProps<typeof HeadingElementStatic>
|
||||
) {
|
||||
return <HeadingElementStatic variant="h2" {...props} />;
|
||||
}
|
||||
|
||||
export function H3ElementStatic(
|
||||
props: React.ComponentProps<typeof HeadingElementStatic>
|
||||
) {
|
||||
return <HeadingElementStatic variant="h3" {...props} />;
|
||||
}
|
||||
|
||||
export function H4ElementStatic(
|
||||
props: React.ComponentProps<typeof HeadingElementStatic>
|
||||
) {
|
||||
return <HeadingElementStatic variant="h4" {...props} />;
|
||||
}
|
||||
|
||||
export function H5ElementStatic(
|
||||
props: React.ComponentProps<typeof HeadingElementStatic>
|
||||
) {
|
||||
return <HeadingElementStatic variant="h5" {...props} />;
|
||||
}
|
||||
|
||||
export function H6ElementStatic(
|
||||
props: React.ComponentProps<typeof HeadingElementStatic>
|
||||
) {
|
||||
return <HeadingElementStatic variant="h6" {...props} />;
|
||||
}
|
60
src/components/ui/heading-node.tsx
Normal file
60
src/components/ui/heading-node.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import type { PlateElementProps } from 'platejs/react';
|
||||
|
||||
import { type VariantProps, cva } from 'class-variance-authority';
|
||||
import { PlateElement } from 'platejs/react';
|
||||
|
||||
const headingVariants = cva('relative mb-1', {
|
||||
variants: {
|
||||
variant: {
|
||||
h1: 'mt-[1.6em] pb-1 font-heading text-4xl font-bold',
|
||||
h2: 'mt-[1.4em] pb-px font-heading text-2xl font-semibold tracking-tight',
|
||||
h3: 'mt-[1em] pb-px font-heading text-xl font-semibold tracking-tight',
|
||||
h4: 'mt-[0.75em] font-heading text-lg font-semibold tracking-tight',
|
||||
h5: 'mt-[0.75em] text-lg font-semibold tracking-tight',
|
||||
h6: 'mt-[0.75em] text-base font-semibold tracking-tight',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function HeadingElement({
|
||||
variant = 'h1',
|
||||
...props
|
||||
}: PlateElementProps & VariantProps<typeof headingVariants>) {
|
||||
return (
|
||||
<PlateElement
|
||||
as={variant!}
|
||||
className={headingVariants({ variant })}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</PlateElement>
|
||||
);
|
||||
}
|
||||
|
||||
export function H1Element(props: PlateElementProps) {
|
||||
return <HeadingElement variant="h1" {...props} />;
|
||||
}
|
||||
|
||||
export function H2Element(props: PlateElementProps) {
|
||||
return <HeadingElement variant="h2" {...props} />;
|
||||
}
|
||||
|
||||
export function H3Element(props: PlateElementProps) {
|
||||
return <HeadingElement variant="h3" {...props} />;
|
||||
}
|
||||
|
||||
export function H4Element(props: PlateElementProps) {
|
||||
return <HeadingElement variant="h4" {...props} />;
|
||||
}
|
||||
|
||||
export function H5Element(props: PlateElementProps) {
|
||||
return <HeadingElement variant="h5" {...props} />;
|
||||
}
|
||||
|
||||
export function H6Element(props: PlateElementProps) {
|
||||
return <HeadingElement variant="h6" {...props} />;
|
||||
}
|
13
src/components/ui/highlight-node-static.tsx
Normal file
13
src/components/ui/highlight-node-static.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SlateLeafProps } from 'platejs';
|
||||
|
||||
import { SlateLeaf } from 'platejs';
|
||||
|
||||
export function HighlightLeafStatic(props: SlateLeafProps) {
|
||||
return (
|
||||
<SlateLeaf {...props} as="mark" className="bg-highlight/30 text-inherit">
|
||||
{props.children}
|
||||
</SlateLeaf>
|
||||
);
|
||||
}
|
15
src/components/ui/highlight-node.tsx
Normal file
15
src/components/ui/highlight-node.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import type { PlateLeafProps } from 'platejs/react';
|
||||
|
||||
import { PlateLeaf } from 'platejs/react';
|
||||
|
||||
export function HighlightLeaf(props: PlateLeafProps) {
|
||||
return (
|
||||
<PlateLeaf {...props} as="mark" className="bg-highlight/30 text-inherit">
|
||||
{props.children}
|
||||
</PlateLeaf>
|
||||
);
|
||||
}
|
22
src/components/ui/hr-node-static.tsx
Normal file
22
src/components/ui/hr-node-static.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SlateElementProps } from 'platejs';
|
||||
|
||||
import { SlateElement } from 'platejs';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function HrElementStatic(props: SlateElementProps) {
|
||||
return (
|
||||
<SlateElement {...props}>
|
||||
<div className="cursor-text py-6" contentEditable={false}>
|
||||
<hr
|
||||
className={cn(
|
||||
'h-0.5 rounded-sm border-none bg-muted bg-clip-content'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{props.children}
|
||||
</SlateElement>
|
||||
);
|
||||
}
|
35
src/components/ui/hr-node.tsx
Normal file
35
src/components/ui/hr-node.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import type { PlateElementProps } from 'platejs/react';
|
||||
|
||||
import {
|
||||
PlateElement,
|
||||
useFocused,
|
||||
useReadOnly,
|
||||
useSelected,
|
||||
} from 'platejs/react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function HrElement(props: PlateElementProps) {
|
||||
const readOnly = useReadOnly();
|
||||
const selected = useSelected();
|
||||
const focused = useFocused();
|
||||
|
||||
return (
|
||||
<PlateElement {...props}>
|
||||
<div className="py-6" contentEditable={false}>
|
||||
<hr
|
||||
className={cn(
|
||||
'h-0.5 rounded-sm border-none bg-muted bg-clip-content',
|
||||
selected && focused && 'ring-2 ring-ring ring-offset-2',
|
||||
!readOnly && 'cursor-pointer'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{props.children}
|
||||
</PlateElement>
|
||||
);
|
||||
}
|
32
src/components/ui/indent-toolbar-button.tsx
Normal file
32
src/components/ui/indent-toolbar-button.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { useIndentButton, useOutdentButton } from '@platejs/indent/react';
|
||||
import { IndentIcon, OutdentIcon } from 'lucide-react';
|
||||
|
||||
import { ToolbarButton } from './toolbar';
|
||||
|
||||
export function IndentToolbarButton(
|
||||
props: React.ComponentProps<typeof ToolbarButton>
|
||||
) {
|
||||
const { props: buttonProps } = useIndentButton();
|
||||
|
||||
return (
|
||||
<ToolbarButton {...props} {...buttonProps} tooltip="Indent">
|
||||
<IndentIcon />
|
||||
</ToolbarButton>
|
||||
);
|
||||
}
|
||||
|
||||
export function OutdentToolbarButton(
|
||||
props: React.ComponentProps<typeof ToolbarButton>
|
||||
) {
|
||||
const { props: buttonProps } = useOutdentButton();
|
||||
|
||||
return (
|
||||
<ToolbarButton {...props} {...buttonProps} tooltip="Outdent">
|
||||
<OutdentIcon />
|
||||
</ToolbarButton>
|
||||
);
|
||||
}
|
21
src/components/ui/input.tsx
Normal file
21
src/components/ui/input.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
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(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
17
src/components/ui/kbd-node-static.tsx
Normal file
17
src/components/ui/kbd-node-static.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SlateLeafProps } from 'platejs';
|
||||
|
||||
import { SlateLeaf } from 'platejs';
|
||||
|
||||
export function KbdLeafStatic(props: SlateLeafProps) {
|
||||
return (
|
||||
<SlateLeaf
|
||||
{...props}
|
||||
as="kbd"
|
||||
className="rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-sm shadow-[rgba(255,_255,_255,_0.1)_0px_0.5px_0px_0px_inset,_rgb(248,_249,_250)_0px_1px_5px_0px_inset,_rgb(193,_200,_205)_0px_0px_0px_0.5px,_rgb(193,_200,_205)_0px_2px_1px_-1px,_rgb(193,_200,_205)_0px_1px_0px_0px] dark:shadow-[rgba(255,_255,_255,_0.1)_0px_0.5px_0px_0px_inset,_rgb(26,_29,_30)_0px_1px_5px_0px_inset,_rgb(76,_81,_85)_0px_0px_0px_0.5px,_rgb(76,_81,_85)_0px_2px_1px_-1px,_rgb(76,_81,_85)_0px_1px_0px_0px]"
|
||||
>
|
||||
{props.children}
|
||||
</SlateLeaf>
|
||||
);
|
||||
}
|
19
src/components/ui/kbd-node.tsx
Normal file
19
src/components/ui/kbd-node.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import type { PlateLeafProps } from 'platejs/react';
|
||||
|
||||
import { PlateLeaf } from 'platejs/react';
|
||||
|
||||
export function KbdLeaf(props: PlateLeafProps) {
|
||||
return (
|
||||
<PlateLeaf
|
||||
{...props}
|
||||
as="kbd"
|
||||
className="rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-sm shadow-[rgba(255,_255,_255,_0.1)_0px_0.5px_0px_0px_inset,_rgb(248,_249,_250)_0px_1px_5px_0px_inset,_rgb(193,_200,_205)_0px_0px_0px_0.5px,_rgb(193,_200,_205)_0px_2px_1px_-1px,_rgb(193,_200,_205)_0px_1px_0px_0px] dark:shadow-[rgba(255,_255,_255,_0.1)_0px_0.5px_0px_0px_inset,_rgb(26,_29,_30)_0px_1px_5px_0px_inset,_rgb(76,_81,_85)_0px_0px_0px_0.5px,_rgb(76,_81,_85)_0px_2px_1px_-1px,_rgb(76,_81,_85)_0px_1px_0px_0px]"
|
||||
>
|
||||
{props.children}
|
||||
</PlateLeaf>
|
||||
);
|
||||
}
|
24
src/components/ui/label.tsx
Normal file
24
src/components/ui/label.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
206
src/components/ui/list-toolbar-button.tsx
Normal file
206
src/components/ui/list-toolbar-button.tsx
Normal file
@ -0,0 +1,206 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { ListStyleType, someList, toggleList } from '@platejs/list';
|
||||
import {
|
||||
useIndentTodoToolBarButton,
|
||||
useIndentTodoToolBarButtonState,
|
||||
} from '@platejs/list/react';
|
||||
import { List, ListOrdered, ListTodoIcon } from 'lucide-react';
|
||||
import { useEditorRef, useEditorSelector } from 'platejs/react';
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
|
||||
import {
|
||||
ToolbarButton,
|
||||
ToolbarSplitButton,
|
||||
ToolbarSplitButtonPrimary,
|
||||
ToolbarSplitButtonSecondary,
|
||||
} from './toolbar';
|
||||
|
||||
export function BulletedListToolbarButton() {
|
||||
const editor = useEditorRef();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const pressed = useEditorSelector(
|
||||
(editor) =>
|
||||
someList(editor, [
|
||||
ListStyleType.Disc,
|
||||
ListStyleType.Circle,
|
||||
ListStyleType.Square,
|
||||
]),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<ToolbarSplitButton pressed={open}>
|
||||
<ToolbarSplitButtonPrimary
|
||||
className="data-[state=on]:bg-accent data-[state=on]:text-accent-foreground"
|
||||
onClick={() => {
|
||||
toggleList(editor, {
|
||||
listStyleType: ListStyleType.Disc,
|
||||
});
|
||||
}}
|
||||
data-state={pressed ? 'on' : 'off'}
|
||||
>
|
||||
<List className="size-4" />
|
||||
</ToolbarSplitButtonPrimary>
|
||||
|
||||
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ToolbarSplitButtonSecondary />
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="start" alignOffset={-32}>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
toggleList(editor, {
|
||||
listStyleType: ListStyleType.Disc,
|
||||
})
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="size-2 rounded-full border border-current bg-current" />
|
||||
Default
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
toggleList(editor, {
|
||||
listStyleType: ListStyleType.Circle,
|
||||
})
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="size-2 rounded-full border border-current" />
|
||||
Circle
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
toggleList(editor, {
|
||||
listStyleType: ListStyleType.Square,
|
||||
})
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="size-2 border border-current bg-current" />
|
||||
Square
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ToolbarSplitButton>
|
||||
);
|
||||
}
|
||||
|
||||
export function NumberedListToolbarButton() {
|
||||
const editor = useEditorRef();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const pressed = useEditorSelector(
|
||||
(editor) =>
|
||||
someList(editor, [
|
||||
ListStyleType.Decimal,
|
||||
ListStyleType.LowerAlpha,
|
||||
ListStyleType.UpperAlpha,
|
||||
ListStyleType.LowerRoman,
|
||||
ListStyleType.UpperRoman,
|
||||
]),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<ToolbarSplitButton pressed={open}>
|
||||
<ToolbarSplitButtonPrimary
|
||||
className="data-[state=on]:bg-accent data-[state=on]:text-accent-foreground"
|
||||
onClick={() =>
|
||||
toggleList(editor, {
|
||||
listStyleType: ListStyleType.Decimal,
|
||||
})
|
||||
}
|
||||
data-state={pressed ? 'on' : 'off'}
|
||||
>
|
||||
<ListOrdered className="size-4" />
|
||||
</ToolbarSplitButtonPrimary>
|
||||
|
||||
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ToolbarSplitButtonSecondary />
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="start" alignOffset={-32}>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
onSelect={() =>
|
||||
toggleList(editor, {
|
||||
listStyleType: ListStyleType.Decimal,
|
||||
})
|
||||
}
|
||||
>
|
||||
Decimal (1, 2, 3)
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() =>
|
||||
toggleList(editor, {
|
||||
listStyleType: ListStyleType.LowerAlpha,
|
||||
})
|
||||
}
|
||||
>
|
||||
Lower Alpha (a, b, c)
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() =>
|
||||
toggleList(editor, {
|
||||
listStyleType: ListStyleType.UpperAlpha,
|
||||
})
|
||||
}
|
||||
>
|
||||
Upper Alpha (A, B, C)
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() =>
|
||||
toggleList(editor, {
|
||||
listStyleType: ListStyleType.LowerRoman,
|
||||
})
|
||||
}
|
||||
>
|
||||
Lower Roman (i, ii, iii)
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() =>
|
||||
toggleList(editor, {
|
||||
listStyleType: ListStyleType.UpperRoman,
|
||||
})
|
||||
}
|
||||
>
|
||||
Upper Roman (I, II, III)
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ToolbarSplitButton>
|
||||
);
|
||||
}
|
||||
|
||||
export function TodoListToolbarButton(
|
||||
props: React.ComponentProps<typeof ToolbarButton>
|
||||
) {
|
||||
const state = useIndentTodoToolBarButtonState({ nodeType: 'todo' });
|
||||
const { props: buttonProps } = useIndentTodoToolBarButton(state);
|
||||
|
||||
return (
|
||||
<ToolbarButton {...props} {...buttonProps} tooltip="Todo">
|
||||
<ListTodoIcon />
|
||||
</ToolbarButton>
|
||||
);
|
||||
}
|
21
src/components/ui/mark-toolbar-button.tsx
Normal file
21
src/components/ui/mark-toolbar-button.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { useMarkToolbarButton, useMarkToolbarButtonState } from 'platejs/react';
|
||||
|
||||
import { ToolbarButton } from './toolbar';
|
||||
|
||||
export function MarkToolbarButton({
|
||||
clear,
|
||||
nodeType,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToolbarButton> & {
|
||||
nodeType: string;
|
||||
clear?: string[] | string;
|
||||
}) {
|
||||
const state = useMarkToolbarButtonState({ clear, nodeType });
|
||||
const { props: buttonProps } = useMarkToolbarButton(state);
|
||||
|
||||
return <ToolbarButton {...props} {...buttonProps} />;
|
||||
}
|
168
src/components/ui/navigation-menu.tsx
Normal file
168
src/components/ui/navigation-menu.tsx
Normal file
@ -0,0 +1,168 @@
|
||||
import * as React from "react"
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||
import { cva } from "class-variance-authority"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function NavigationMenu({
|
||||
className,
|
||||
children,
|
||||
viewport = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||
viewport?: boolean
|
||||
}) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Root
|
||||
data-slot="navigation-menu"
|
||||
data-viewport={viewport}
|
||||
className={cn(
|
||||
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{viewport && <NavigationMenuViewport />}
|
||||
</NavigationMenuPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.List
|
||||
data-slot="navigation-menu-list"
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center gap-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Item
|
||||
data-slot="navigation-menu-item"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
)
|
||||
|
||||
function NavigationMenuTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
data-slot="navigation-menu-trigger"
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDownIcon
|
||||
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Content
|
||||
data-slot="navigation-menu-content"
|
||||
className={cn(
|
||||
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuViewport({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-full left-0 isolate z-50 flex justify-center"
|
||||
)}
|
||||
>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
data-slot="navigation-menu-viewport"
|
||||
className={cn(
|
||||
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuLink({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Link
|
||||
data-slot="navigation-menu-link"
|
||||
className={cn(
|
||||
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuIndicator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
data-slot="navigation-menu-indicator"
|
||||
className={cn(
|
||||
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
NavigationMenu,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
navigationMenuTriggerStyle,
|
||||
}
|
15
src/components/ui/paragraph-node-static.tsx
Normal file
15
src/components/ui/paragraph-node-static.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SlateElementProps } from 'platejs';
|
||||
|
||||
import { SlateElement } from 'platejs';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function ParagraphElementStatic(props: SlateElementProps) {
|
||||
return (
|
||||
<SlateElement {...props} className={cn('m-0 px-0 py-1')}>
|
||||
{props.children}
|
||||
</SlateElement>
|
||||
);
|
||||
}
|
17
src/components/ui/paragraph-node.tsx
Normal file
17
src/components/ui/paragraph-node.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import type { PlateElementProps } from 'platejs/react';
|
||||
|
||||
import { PlateElement } from 'platejs/react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function ParagraphElement(props: PlateElementProps) {
|
||||
return (
|
||||
<PlateElement {...props} className={cn('m-0 px-0 py-1')}>
|
||||
{props.children}
|
||||
</PlateElement>
|
||||
);
|
||||
}
|
48
src/components/ui/popover.tsx
Normal file
48
src/components/ui/popover.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
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(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out 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 p-4 shadow-md outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||
}
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
31
src/components/ui/progress.tsx
Normal file
31
src/components/ui/progress.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
value,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
data-slot="progress-indicator"
|
||||
className="bg-primary h-full w-full flex-1 transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Progress }
|
185
src/components/ui/select.tsx
Normal file
185
src/components/ui/select.tsx
Normal file
@ -0,0 +1,185 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...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 SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_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.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out 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-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 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 className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
28
src/components/ui/separator.tsx
Normal file
28
src/components/ui/separator.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
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(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
63
src/components/ui/slider.tsx
Normal file
63
src/components/ui/slider.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Slider({
|
||||
className,
|
||||
defaultValue,
|
||||
value,
|
||||
min = 0,
|
||||
max = 100,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||
const _values = React.useMemo(
|
||||
() =>
|
||||
Array.isArray(value)
|
||||
? value
|
||||
: Array.isArray(defaultValue)
|
||||
? defaultValue
|
||||
: [min, max],
|
||||
[value, defaultValue, min, max]
|
||||
)
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Root
|
||||
data-slot="slider"
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track
|
||||
data-slot="slider-track"
|
||||
className={cn(
|
||||
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
|
||||
)}
|
||||
>
|
||||
<SliderPrimitive.Range
|
||||
data-slot="slider-range"
|
||||
className={cn(
|
||||
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
|
||||
)}
|
||||
/>
|
||||
</SliderPrimitive.Track>
|
||||
{Array.from({ length: _values.length }, (_, index) => (
|
||||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
key={index}
|
||||
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||
/>
|
||||
))}
|
||||
</SliderPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Slider }
|
25
src/components/ui/sonner.tsx
Normal file
25
src/components/ui/sonner.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
31
src/components/ui/switch.tsx
Normal file
31
src/components/ui/switch.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Switch }
|
66
src/components/ui/tabs.tsx
Normal file
66
src/components/ui/tabs.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
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 }
|
18
src/components/ui/textarea.tsx
Normal file
18
src/components/ui/textarea.tsx
Normal 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(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
73
src/components/ui/toggle-group.tsx
Normal file
73
src/components/ui/toggle-group.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
||||
import { type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { toggleVariants } from "@/components/ui/toggle"
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants>
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
})
|
||||
|
||||
function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<ToggleGroupPrimitive.Root
|
||||
data-slot="toggle-group"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
{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}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem }
|
47
src/components/ui/toggle.tsx
Normal file
47
src/components/ui/toggle.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-2 min-w-9",
|
||||
sm: "h-8 px-1.5 min-w-8",
|
||||
lg: "h-10 px-2.5 min-w-10",
|
||||
},
|
||||
},
|
||||
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 }
|
389
src/components/ui/toolbar.tsx
Normal file
389
src/components/ui/toolbar.tsx
Normal file
@ -0,0 +1,389 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as ToolbarPrimitive from '@radix-ui/react-toolbar';
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||
import { type VariantProps, cva } from 'class-variance-authority';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuSeparator,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Tooltip, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function Toolbar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToolbarPrimitive.Root>) {
|
||||
return (
|
||||
<ToolbarPrimitive.Root
|
||||
className={cn('relative flex items-center select-none', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToolbarToggleGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToolbarPrimitive.ToolbarToggleGroup>) {
|
||||
return (
|
||||
<ToolbarPrimitive.ToolbarToggleGroup
|
||||
className={cn('flex items-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToolbarLink({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToolbarPrimitive.Link>) {
|
||||
return (
|
||||
<ToolbarPrimitive.Link
|
||||
className={cn('font-medium underline underline-offset-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToolbarSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToolbarPrimitive.Separator>) {
|
||||
return (
|
||||
<ToolbarPrimitive.Separator
|
||||
className={cn('mx-2 my-1 w-px shrink-0 bg-border', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// From toggleVariants
|
||||
const toolbarButtonVariants = cva(
|
||||
"inline-flex cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none 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-checked:bg-accent aria-checked:text-accent-foreground aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
defaultVariants: {
|
||||
size: 'default',
|
||||
variant: 'default',
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
default: 'h-9 min-w-9 px-2',
|
||||
lg: 'h-10 min-w-10 px-2.5',
|
||||
sm: 'h-8 min-w-8 px-1.5',
|
||||
},
|
||||
variant: {
|
||||
default: 'bg-transparent',
|
||||
outline:
|
||||
'border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const dropdownArrowVariants = cva(
|
||||
cn(
|
||||
'inline-flex items-center justify-center rounded-r-md text-sm font-medium text-foreground transition-colors disabled:pointer-events-none disabled:opacity-50'
|
||||
),
|
||||
{
|
||||
defaultVariants: {
|
||||
size: 'sm',
|
||||
variant: 'default',
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
default: 'h-9 w-6',
|
||||
lg: 'h-10 w-8',
|
||||
sm: 'h-8 w-4',
|
||||
},
|
||||
variant: {
|
||||
default:
|
||||
'bg-transparent hover:bg-muted hover:text-muted-foreground aria-checked:bg-accent aria-checked:text-accent-foreground',
|
||||
outline:
|
||||
'border border-l-0 border-input bg-transparent hover:bg-accent hover:text-accent-foreground',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
type ToolbarButtonProps = {
|
||||
isDropdown?: boolean;
|
||||
pressed?: boolean;
|
||||
} & Omit<
|
||||
React.ComponentPropsWithoutRef<typeof ToolbarToggleItem>,
|
||||
'asChild' | 'value'
|
||||
> &
|
||||
VariantProps<typeof toolbarButtonVariants>;
|
||||
|
||||
export const ToolbarButton = withTooltip(function ToolbarButton({
|
||||
children,
|
||||
className,
|
||||
isDropdown,
|
||||
pressed,
|
||||
size = 'sm',
|
||||
variant,
|
||||
...props
|
||||
}: ToolbarButtonProps) {
|
||||
return typeof pressed === 'boolean' ? (
|
||||
<ToolbarToggleGroup disabled={props.disabled} value="single" type="single">
|
||||
<ToolbarToggleItem
|
||||
className={cn(
|
||||
toolbarButtonVariants({
|
||||
size,
|
||||
variant,
|
||||
}),
|
||||
isDropdown && 'justify-between gap-1 pr-1',
|
||||
className
|
||||
)}
|
||||
value={pressed ? 'single' : ''}
|
||||
{...props}
|
||||
>
|
||||
{isDropdown ? (
|
||||
<>
|
||||
<div className="flex flex-1 items-center gap-2 whitespace-nowrap">
|
||||
{children}
|
||||
</div>
|
||||
<div>
|
||||
<ChevronDown
|
||||
className="size-3.5 text-muted-foreground"
|
||||
data-icon
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</ToolbarToggleItem>
|
||||
</ToolbarToggleGroup>
|
||||
) : (
|
||||
<ToolbarPrimitive.Button
|
||||
className={cn(
|
||||
toolbarButtonVariants({
|
||||
size,
|
||||
variant,
|
||||
}),
|
||||
isDropdown && 'pr-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToolbarPrimitive.Button>
|
||||
);
|
||||
});
|
||||
|
||||
export function ToolbarSplitButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof ToolbarButton>) {
|
||||
return (
|
||||
<ToolbarButton
|
||||
className={cn('group flex gap-0 px-0 hover:bg-transparent', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type ToolbarSplitButtonPrimaryProps = Omit<
|
||||
React.ComponentPropsWithoutRef<typeof ToolbarToggleItem>,
|
||||
'value'
|
||||
> &
|
||||
VariantProps<typeof toolbarButtonVariants>;
|
||||
|
||||
export function ToolbarSplitButtonPrimary({
|
||||
children,
|
||||
className,
|
||||
size = 'sm',
|
||||
variant,
|
||||
...props
|
||||
}: ToolbarSplitButtonPrimaryProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
toolbarButtonVariants({
|
||||
size,
|
||||
variant,
|
||||
}),
|
||||
'rounded-r-none',
|
||||
'group-data-[pressed=true]:bg-accent group-data-[pressed=true]:text-accent-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToolbarSplitButtonSecondary({
|
||||
className,
|
||||
size,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<'span'> &
|
||||
VariantProps<typeof dropdownArrowVariants>) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
dropdownArrowVariants({
|
||||
size,
|
||||
variant,
|
||||
}),
|
||||
'group-data-[pressed=true]:bg-accent group-data-[pressed=true]:text-accent-foreground',
|
||||
className
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="button"
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="size-3.5 text-muted-foreground" data-icon />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToolbarToggleItem({
|
||||
className,
|
||||
size = 'sm',
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToolbarPrimitive.ToggleItem> &
|
||||
VariantProps<typeof toolbarButtonVariants>) {
|
||||
return (
|
||||
<ToolbarPrimitive.ToggleItem
|
||||
className={cn(toolbarButtonVariants({ size, variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToolbarGroup({
|
||||
children,
|
||||
className,
|
||||
}: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group/toolbar-group',
|
||||
'relative hidden has-[button]:flex',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center">{children}</div>
|
||||
|
||||
<div className="mx-1.5 py-0.5 group-last/toolbar-group:hidden!">
|
||||
<Separator orientation="vertical" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type TooltipProps<T extends React.ElementType> = {
|
||||
tooltip?: React.ReactNode;
|
||||
tooltipContentProps?: Omit<
|
||||
React.ComponentPropsWithoutRef<typeof TooltipContent>,
|
||||
'children'
|
||||
>;
|
||||
tooltipProps?: Omit<
|
||||
React.ComponentPropsWithoutRef<typeof Tooltip>,
|
||||
'children'
|
||||
>;
|
||||
tooltipTriggerProps?: React.ComponentPropsWithoutRef<typeof TooltipTrigger>;
|
||||
} & React.ComponentProps<T>;
|
||||
|
||||
function withTooltip<T extends React.ElementType>(Component: T) {
|
||||
return function ExtendComponent({
|
||||
tooltip,
|
||||
tooltipContentProps,
|
||||
tooltipProps,
|
||||
tooltipTriggerProps,
|
||||
...props
|
||||
}: TooltipProps<T>) {
|
||||
const [mounted, setMounted] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const component = <Component {...(props as React.ComponentProps<T>)} />;
|
||||
|
||||
if (tooltip && mounted) {
|
||||
return (
|
||||
<Tooltip {...tooltipProps}>
|
||||
<TooltipTrigger asChild {...tooltipTriggerProps}>
|
||||
{component}
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent {...tooltipContentProps}>{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return component;
|
||||
};
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
children,
|
||||
className,
|
||||
// CHANGE
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
className={cn(
|
||||
'z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md bg-primary px-3 py-1.5 text-xs text-balance text-primary-foreground',
|
||||
className
|
||||
)}
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{/* CHANGE */}
|
||||
{/* <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-primary fill-primary" /> */}
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToolbarMenuGroup({
|
||||
children,
|
||||
className,
|
||||
label,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuRadioGroup> & { label?: string }) {
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuSeparator
|
||||
className={cn(
|
||||
'hidden',
|
||||
'mb-0 shrink-0 peer-has-[[role=menuitem]]/menu-group:block peer-has-[[role=menuitemradio]]/menu-group:block peer-has-[[role=option]]/menu-group:block'
|
||||
)}
|
||||
/>
|
||||
|
||||
<DropdownMenuRadioGroup
|
||||
{...props}
|
||||
className={cn(
|
||||
'hidden',
|
||||
'peer/menu-group group/menu-group my-1.5 has-[[role=menuitem]]:block has-[[role=menuitemradio]]:block has-[[role=option]]:block',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{label && (
|
||||
<DropdownMenuLabel className="text-xs font-semibold text-muted-foreground select-none">
|
||||
{label}
|
||||
</DropdownMenuLabel>
|
||||
)}
|
||||
{children}
|
||||
</DropdownMenuRadioGroup>
|
||||
</>
|
||||
);
|
||||
}
|
61
src/components/ui/tooltip.tsx
Normal file
61
src/components/ui/tooltip.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-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 Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out 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) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
14
src/lib/prisma.ts
Normal file
14
src/lib/prisma.ts
Normal file
@ -0,0 +1,14 @@
|
||||
// import { PrismaClient } from '@/types/prisma'
|
||||
// import { withAccelerate } from '@prisma/extension-accelerate'
|
||||
|
||||
import { PrismaClient } from "@/generated/prisma"
|
||||
|
||||
const globalForPrisma = global as unknown as {
|
||||
prisma: PrismaClient
|
||||
}
|
||||
|
||||
const prisma = globalForPrisma.prisma || new PrismaClient()
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
|
||||
|
||||
export default prisma
|
21
src/lib/s3.ts
Normal file
21
src/lib/s3.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
|
||||
export const s3 = new S3Client({
|
||||
region: "us-east-1",
|
||||
endpoint: "http://10.0.20.11:9010",
|
||||
forcePathStyle: true,
|
||||
credentials: {
|
||||
accessKeyId: "fellies",
|
||||
secretAccessKey: "XCJ7spqxWZhVn8tkYnfVBFbz2cRKYxPAfeQeIdPRp1",
|
||||
},
|
||||
});
|
||||
|
||||
export async function getSignedImageUrl(key: string, expiresInSec = 3600) {
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: "gaertan",
|
||||
Key: key,
|
||||
});
|
||||
|
||||
return getSignedUrl(s3, command, { expiresIn: expiresInSec });
|
||||
}
|
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal 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))
|
||||
}
|
9
src/schemas/artTypeSchema.ts
Normal file
9
src/schemas/artTypeSchema.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export const artTypeSchema = z.object({
|
||||
name: z.string().min(3, "Name is required. Min 3 characters."),
|
||||
slug: z.string().min(3, "Slug is required. Min 3 characters.").regex(/^[a-z]+$/, "Only lowercase letters are allowed (no numbers, spaces, or uppercase)"),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
export type artTypeSchema = z.infer<typeof artTypeSchema>
|
34
src/schemas/commissionType.ts
Normal file
34
src/schemas/commissionType.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import * as z from "zod/v4";
|
||||
|
||||
const rangePattern = /^\d{1,3}–\d{1,3}$/;
|
||||
|
||||
const optionField = z.object({
|
||||
optionId: z.string(),
|
||||
price: z.number().optional(),
|
||||
pricePercent: z.number().optional(),
|
||||
priceRange: z.string().regex(rangePattern, "Format must be like '10–80'").optional(),
|
||||
});
|
||||
|
||||
const extraField = z.object({
|
||||
extraId: z.string(),
|
||||
price: z.number().optional(),
|
||||
pricePercent: z.number().optional(),
|
||||
priceRange: z.string().regex(rangePattern, "Format must be like '10–80'").optional(),
|
||||
});
|
||||
|
||||
const customInputsField = z.object({
|
||||
customInputId: z.string(),
|
||||
inputType: z.string(),
|
||||
label: z.string(),
|
||||
required: z.boolean(),
|
||||
});
|
||||
|
||||
export const commissionTypeSchema = z.object({
|
||||
name: z.string().min(1, "Name is required. Min 1 character."),
|
||||
description: z.string().optional(),
|
||||
options: z.array(optionField).optional(),
|
||||
extras: z.array(extraField).optional(),
|
||||
customInputs: z.array(customInputsField).optional(),
|
||||
})
|
||||
|
||||
export type commissionTypeSchema = z.infer<typeof commissionTypeSchema>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user