refactor: remove unused monorepo tooling
Delete packages/ui and packages/utils (zero imports from apps/web), remove Turborepo, inline tsconfig.base.json into web's tsconfig, and simplify root scripts to use pnpm --filter directly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8684156ffc
commit
d1f73bf7fc
60 changed files with 49 additions and 5340 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -7,7 +7,6 @@ dist
|
||||||
# build outputs
|
# build outputs
|
||||||
.next
|
.next
|
||||||
out
|
out
|
||||||
.turbo
|
|
||||||
build
|
build
|
||||||
bin
|
bin
|
||||||
dist-electron
|
dist-electron
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,49 @@
|
||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"plugins": [{ "name": "next" }],
|
"target": "ESNext",
|
||||||
"paths": {
|
|
||||||
"@/*": ["./*"]
|
|
||||||
},
|
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"noEmit": true
|
"lib": [
|
||||||
},
|
"ESNext",
|
||||||
"include": ["next-env.d.ts", "src", "app", "**/*.ts", "**/*.tsx"],
|
"DOM",
|
||||||
"exclude": ["node_modules"]
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"noEmit": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"incremental": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"src",
|
||||||
|
"app",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
package.json
13
package.json
|
|
@ -4,12 +4,12 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:web": "turbo run dev --filter=@multica/web",
|
"dev:web": "pnpm --filter @multica/web dev",
|
||||||
"build": "turbo run build",
|
"build": "pnpm --filter @multica/web build",
|
||||||
"typecheck": "turbo run typecheck",
|
"typecheck": "pnpm --filter @multica/web typecheck",
|
||||||
"test": "turbo run test",
|
"test": "pnpm --filter @multica/web test",
|
||||||
"lint": "turbo run lint",
|
"lint": "pnpm --filter @multica/web lint",
|
||||||
"clean": "turbo run clean && rm -rf node_modules"
|
"clean": "pnpm --filter @multica/web clean && rm -rf node_modules"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.28.2",
|
"packageManager": "pnpm@10.28.2",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
|
|
@ -26,7 +26,6 @@
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@types/pg": "^8.20.0",
|
"@types/pg": "^8.20.0",
|
||||||
"pg": "^8.20.0",
|
"pg": "^8.20.0",
|
||||||
"turbo": "^2.5.0",
|
|
||||||
"typescript": "catalog:"
|
"typescript": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
|
||||||
"style": "base-nova",
|
|
||||||
"rsc": true,
|
|
||||||
"tsx": true,
|
|
||||||
"tailwind": {
|
|
||||||
"config": "",
|
|
||||||
"css": "src/styles/globals.css",
|
|
||||||
"baseColor": "zinc",
|
|
||||||
"cssVariables": true
|
|
||||||
},
|
|
||||||
"iconLibrary": "lucide",
|
|
||||||
"aliases": {
|
|
||||||
"components": "@multica/ui/components",
|
|
||||||
"utils": "@multica/ui/lib/utils",
|
|
||||||
"hooks": "@multica/ui/hooks",
|
|
||||||
"lib": "@multica/ui/lib",
|
|
||||||
"ui": "@multica/ui/components/ui"
|
|
||||||
},
|
|
||||||
"rtl": false,
|
|
||||||
"menuColor": "inverted-translucent",
|
|
||||||
"menuAccent": "subtle"
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@multica/ui",
|
|
||||||
"version": "0.2.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"exports": {
|
|
||||||
"./globals.css": "./src/styles/globals.css",
|
|
||||||
"./postcss.config": "./postcss.config.mjs",
|
|
||||||
"./lib/*": "./src/lib/*.ts",
|
|
||||||
"./components/*": "./src/components/*.tsx",
|
|
||||||
"./components/ui/*": "./src/components/ui/*.tsx",
|
|
||||||
"./components/common/*": "./src/components/common/*.tsx",
|
|
||||||
"./components/markdown": "./src/components/markdown/index.ts",
|
|
||||||
"./hooks/*": "./src/hooks/*.ts"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@base-ui/react": "^1.3.0",
|
|
||||||
"class-variance-authority": "catalog:",
|
|
||||||
"clsx": "catalog:",
|
|
||||||
"cmdk": "^1.1.1",
|
|
||||||
"date-fns": "^4.1.0",
|
|
||||||
"lucide-react": "catalog:",
|
|
||||||
"next-themes": "^0.4.6",
|
|
||||||
"react": "catalog:",
|
|
||||||
"react-day-picker": "^9.14.0",
|
|
||||||
"react-dom": "catalog:",
|
|
||||||
"react-markdown": "^10.1.0",
|
|
||||||
"shiki": "^3.21.0",
|
|
||||||
"sonner": "^2.0.7",
|
|
||||||
"tailwind-merge": "catalog:",
|
|
||||||
"tailwindcss": "catalog:"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/react": "catalog:",
|
|
||||||
"@types/react-dom": "catalog:",
|
|
||||||
"shadcn": "^4.1.0",
|
|
||||||
"tw-animate-css": "^1.4.0",
|
|
||||||
"typescript": "catalog:"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
/** @type {import('postcss-load-config').Config} */
|
|
||||||
const config = {
|
|
||||||
plugins: { "@tailwindcss/postcss": {} },
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
import { Bot } from "lucide-react";
|
|
||||||
import { cn } from "@multica/ui/lib/utils";
|
|
||||||
|
|
||||||
interface ActorAvatarProps {
|
|
||||||
actorType: string;
|
|
||||||
actorId: string;
|
|
||||||
size?: number;
|
|
||||||
getName?: (type: string, id: string) => string;
|
|
||||||
getInitials?: (type: string, id: string) => string;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ActorAvatar({
|
|
||||||
actorType,
|
|
||||||
actorId,
|
|
||||||
size = 20,
|
|
||||||
getName,
|
|
||||||
getInitials,
|
|
||||||
className,
|
|
||||||
}: ActorAvatarProps) {
|
|
||||||
const name = getName?.(actorType, actorId);
|
|
||||||
const initials = getInitials?.(actorType, actorId);
|
|
||||||
const isAgent = actorType === "agent";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"inline-flex shrink-0 items-center justify-center rounded-full font-medium",
|
|
||||||
isAgent ? "bg-info/10 text-info" : "bg-muted text-muted-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
style={{ width: size, height: size, fontSize: size * 0.45 }}
|
|
||||||
title={name}
|
|
||||||
>
|
|
||||||
{isAgent ? (
|
|
||||||
<Bot style={{ width: size * 0.55, height: size * 0.55 }} />
|
|
||||||
) : (
|
|
||||||
initials
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ActorAvatar, type ActorAvatarProps };
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { Spinner } from "@multica/ui/components/spinner";
|
|
||||||
import { cn } from "@multica/ui/lib/utils";
|
|
||||||
|
|
||||||
export type LoadingVariant = "generating" | "streaming";
|
|
||||||
|
|
||||||
interface LoadingIndicatorProps {
|
|
||||||
variant: LoadingVariant;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const VARIANT_TEXT: Record<LoadingVariant, string> = {
|
|
||||||
generating: "Generating...",
|
|
||||||
streaming: "Streaming...",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unified loading indicator for chat.
|
|
||||||
* Use "generating" when waiting for AI response (no content yet).
|
|
||||||
* Use "streaming" when content is actively being received.
|
|
||||||
*/
|
|
||||||
export function LoadingIndicator({ variant, className }: LoadingIndicatorProps) {
|
|
||||||
return (
|
|
||||||
<div className={cn("flex items-center gap-2 py-1 text-muted-foreground", className)}>
|
|
||||||
<Spinner className="text-xs" />
|
|
||||||
<span className="text-xs">{VARIANT_TEXT[variant]}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,250 +0,0 @@
|
||||||
import * as React from 'react'
|
|
||||||
import { codeToHtml, bundledLanguages, type BundledLanguage } from 'shiki'
|
|
||||||
import { cn } from '@multica/ui/lib/utils'
|
|
||||||
|
|
||||||
export interface CodeBlockProps {
|
|
||||||
code: string
|
|
||||||
language?: string
|
|
||||||
className?: string
|
|
||||||
/**
|
|
||||||
* Render mode affects code block styling:
|
|
||||||
* - 'terminal': Minimal, keeps control chars visible
|
|
||||||
* - 'minimal': Clean code, basic styling
|
|
||||||
* - 'full': Rich styling with background, copy button, etc.
|
|
||||||
*/
|
|
||||||
mode?: 'terminal' | 'minimal' | 'full'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map common aliases to Shiki language names
|
|
||||||
const LANGUAGE_ALIASES: Record<string, BundledLanguage> = {
|
|
||||||
js: 'javascript',
|
|
||||||
ts: 'typescript',
|
|
||||||
py: 'python',
|
|
||||||
sh: 'bash',
|
|
||||||
zsh: 'bash',
|
|
||||||
yml: 'yaml',
|
|
||||||
rb: 'ruby',
|
|
||||||
rs: 'rust',
|
|
||||||
kt: 'kotlin',
|
|
||||||
'objective-c': 'objc',
|
|
||||||
objc: 'objc'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple LRU cache for highlighted code
|
|
||||||
const highlightCache = new Map<string, string>()
|
|
||||||
const CACHE_MAX_SIZE = 200
|
|
||||||
|
|
||||||
function getCacheKey(code: string, lang: string): string {
|
|
||||||
return `${lang}:${code}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidLanguage(lang: string): lang is BundledLanguage {
|
|
||||||
const normalized = LANGUAGE_ALIASES[lang] || lang
|
|
||||||
return normalized in bundledLanguages
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CodeBlock - Syntax highlighted code block using Shiki
|
|
||||||
*
|
|
||||||
* Uses Shiki dual themes with CSS variables for light/dark switching.
|
|
||||||
* No JS-based dark mode detection needed — theme switching is handled
|
|
||||||
* entirely via CSS (see globals.css for .shiki/.dark .shiki rules).
|
|
||||||
*
|
|
||||||
* @see https://shiki.style/guide/dual-themes
|
|
||||||
*/
|
|
||||||
export function CodeBlock({
|
|
||||||
code,
|
|
||||||
language = 'text',
|
|
||||||
className,
|
|
||||||
mode = 'full'
|
|
||||||
}: CodeBlockProps): React.JSX.Element {
|
|
||||||
const [highlighted, setHighlighted] = React.useState<string | null>(null)
|
|
||||||
const [isLoading, setIsLoading] = React.useState(true)
|
|
||||||
const [copied, setCopied] = React.useState(false)
|
|
||||||
|
|
||||||
// Resolve language alias - keep as string to allow 'text' fallback
|
|
||||||
const langLower = language.toLowerCase()
|
|
||||||
const resolvedLang: string = LANGUAGE_ALIASES[langLower] || langLower
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
let cancelled = false
|
|
||||||
|
|
||||||
async function highlight(): Promise<void> {
|
|
||||||
const cacheKey = getCacheKey(code, resolvedLang)
|
|
||||||
|
|
||||||
const cached = highlightCache.get(cacheKey)
|
|
||||||
if (cached) {
|
|
||||||
if (!cancelled) {
|
|
||||||
setHighlighted(cached)
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Use valid language or fallback to plaintext
|
|
||||||
const lang = isValidLanguage(resolvedLang) ? resolvedLang : 'text'
|
|
||||||
|
|
||||||
// Dual themes: Shiki outputs CSS variables for both themes in one pass.
|
|
||||||
// CSS handles switching via .dark selector (see globals.css).
|
|
||||||
const html = await codeToHtml(code, {
|
|
||||||
lang,
|
|
||||||
themes: {
|
|
||||||
light: 'github-light',
|
|
||||||
dark: 'github-dark',
|
|
||||||
},
|
|
||||||
defaultColor: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Cache the result
|
|
||||||
if (highlightCache.size >= CACHE_MAX_SIZE) {
|
|
||||||
const firstKey = highlightCache.keys().next().value
|
|
||||||
if (firstKey) highlightCache.delete(firstKey)
|
|
||||||
}
|
|
||||||
highlightCache.set(cacheKey, html)
|
|
||||||
|
|
||||||
if (!cancelled) {
|
|
||||||
setHighlighted(html)
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Fallback to plain text on error
|
|
||||||
console.warn(`Shiki highlighting failed for language "${resolvedLang}":`, error)
|
|
||||||
if (!cancelled) {
|
|
||||||
setHighlighted(null)
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
highlight()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true
|
|
||||||
}
|
|
||||||
}, [code, resolvedLang])
|
|
||||||
|
|
||||||
const handleCopy = React.useCallback(async () => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(code)
|
|
||||||
setCopied(true)
|
|
||||||
setTimeout(() => setCopied(false), 2000)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to copy code:', err)
|
|
||||||
}
|
|
||||||
}, [code])
|
|
||||||
|
|
||||||
// Terminal mode: raw monospace with minimal styling
|
|
||||||
if (mode === 'terminal') {
|
|
||||||
return (
|
|
||||||
<pre className={cn('font-mono text-sm whitespace-pre-wrap', className)}>
|
|
||||||
<code>{code}</code>
|
|
||||||
</pre>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Minimal mode: just syntax highlighting, no chrome
|
|
||||||
if (mode === 'minimal') {
|
|
||||||
if (isLoading || !highlighted) {
|
|
||||||
return (
|
|
||||||
<pre className={cn('font-mono text-sm whitespace-pre-wrap', className)}>
|
|
||||||
<code>{code}</code>
|
|
||||||
</pre>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'font-mono text-sm [&_pre]:!bg-transparent [&_pre]:!p-0 [&_pre]:whitespace-pre-wrap [&_pre]:break-all [&_code]:!bg-transparent',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
dangerouslySetInnerHTML={{ __html: highlighted }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Full mode: rich styling with header and copy button
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'relative group rounded-lg overflow-hidden border bg-muted/30 mb-4 last:mb-0',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{/* Language label + copy button */}
|
|
||||||
<div className="flex items-center justify-between px-3 py-1.5 bg-muted/50 border-b text-xs">
|
|
||||||
<span className="text-muted-foreground font-medium uppercase tracking-wide">
|
|
||||||
{resolvedLang !== 'text' ? resolvedLang : 'plain text'}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={handleCopy}
|
|
||||||
className="opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-foreground"
|
|
||||||
aria-label="Copy code"
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<svg
|
|
||||||
className="w-4 h-4 text-green-500"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M5 13l4 4L19 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Code content */}
|
|
||||||
<div className="p-3 overflow-x-auto">
|
|
||||||
{isLoading || !highlighted ? (
|
|
||||||
<pre className="font-mono text-sm whitespace-pre-wrap break-all">
|
|
||||||
<code>{code}</code>
|
|
||||||
</pre>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="font-mono text-sm [&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:!p-0 [&_pre]:whitespace-pre-wrap [&_pre]:break-all [&_code]:!bg-transparent"
|
|
||||||
dangerouslySetInnerHTML={{ __html: highlighted }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* InlineCode - Styled inline code span
|
|
||||||
* Features: subtle background (3%), subtle border (5%), 75% opacity text
|
|
||||||
*/
|
|
||||||
export function InlineCode({
|
|
||||||
children,
|
|
||||||
className
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
className?: string
|
|
||||||
}): React.JSX.Element {
|
|
||||||
return (
|
|
||||||
<code
|
|
||||||
className={cn(
|
|
||||||
'px-1.5 py-0.5 rounded bg-foreground/[0.03] border border-foreground/[0.05] font-mono text-sm text-foreground/75',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</code>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,301 +0,0 @@
|
||||||
import * as React from 'react'
|
|
||||||
import ReactMarkdown, { type Components } from 'react-markdown'
|
|
||||||
import rehypeRaw from 'rehype-raw'
|
|
||||||
import remarkGfm from 'remark-gfm'
|
|
||||||
import { cn } from '@multica/ui/lib/utils'
|
|
||||||
import { CodeBlock, InlineCode } from './CodeBlock'
|
|
||||||
import { preprocessLinks } from './linkify'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render modes for markdown content:
|
|
||||||
*
|
|
||||||
* - 'terminal': Raw output with minimal formatting, control chars visible
|
|
||||||
* Best for: Debug output, raw logs, when you want to see exactly what's there
|
|
||||||
*
|
|
||||||
* - 'minimal': Clean rendering with syntax highlighting but no extra chrome
|
|
||||||
* Best for: Chat messages, inline content, when you want readability without clutter
|
|
||||||
*
|
|
||||||
* - 'full': Rich rendering with beautiful tables, styled code blocks, proper typography
|
|
||||||
* Best for: Documentation, long-form content, when presentation matters
|
|
||||||
*/
|
|
||||||
export type RenderMode = 'terminal' | 'minimal' | 'full'
|
|
||||||
|
|
||||||
export interface MarkdownProps {
|
|
||||||
children: string
|
|
||||||
/**
|
|
||||||
* Render mode controlling formatting level
|
|
||||||
* @default 'minimal'
|
|
||||||
*/
|
|
||||||
mode?: RenderMode
|
|
||||||
className?: string
|
|
||||||
/**
|
|
||||||
* Message ID for memoization (optional)
|
|
||||||
* When provided, memoizes parsed blocks to avoid re-parsing during streaming
|
|
||||||
*/
|
|
||||||
id?: string
|
|
||||||
/**
|
|
||||||
* Callback when a URL is clicked
|
|
||||||
*/
|
|
||||||
onUrlClick?: (url: string) => void
|
|
||||||
/**
|
|
||||||
* Callback when a file path is clicked
|
|
||||||
*/
|
|
||||||
onFileClick?: (path: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
// File path detection regex - matches paths starting with /, ~/, or ./
|
|
||||||
const FILE_PATH_REGEX =
|
|
||||||
/^(?:\/|~\/|\.\/)[\w\-./@]+\.(?:ts|tsx|js|jsx|mjs|cjs|md|json|yaml|yml|py|go|rs|css|scss|less|html|htm|txt|log|sh|bash|zsh|swift|kt|java|c|cpp|h|hpp|rb|php|xml|toml|ini|cfg|conf|env|sql|graphql|vue|svelte|astro|prisma)$/i
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create custom components based on render mode
|
|
||||||
*/
|
|
||||||
function createComponents(
|
|
||||||
mode: RenderMode,
|
|
||||||
onUrlClick?: (url: string) => void,
|
|
||||||
onFileClick?: (path: string) => void
|
|
||||||
): Partial<Components> {
|
|
||||||
const baseComponents: Partial<Components> = {
|
|
||||||
// Links: Make clickable with callbacks
|
|
||||||
a: ({ href, children }) => {
|
|
||||||
const handleClick = (e: React.MouseEvent): void => {
|
|
||||||
e.preventDefault()
|
|
||||||
if (href) {
|
|
||||||
// Check if it's a file path
|
|
||||||
if (FILE_PATH_REGEX.test(href) && onFileClick) {
|
|
||||||
onFileClick(href)
|
|
||||||
} else if (onUrlClick) {
|
|
||||||
onUrlClick(href)
|
|
||||||
} else {
|
|
||||||
// Default: open in new window
|
|
||||||
window.open(href, '_blank', 'noopener,noreferrer')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={href}
|
|
||||||
onClick={handleClick}
|
|
||||||
className="text-primary hover:underline cursor-pointer"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Terminal mode: minimal formatting
|
|
||||||
if (mode === 'terminal') {
|
|
||||||
return {
|
|
||||||
...baseComponents,
|
|
||||||
// No special code handling - just monospace
|
|
||||||
code: ({ children }) => <code className="font-mono">{children}</code>,
|
|
||||||
pre: ({ children }) => <pre className="font-mono whitespace-pre-wrap my-2">{children}</pre>,
|
|
||||||
// Minimal paragraph spacing
|
|
||||||
p: ({ children }) => <p className="my-1">{children}</p>,
|
|
||||||
// Simple lists
|
|
||||||
ul: ({ children }) => <ul className="list-disc list-inside my-1">{children}</ul>,
|
|
||||||
ol: ({ children }) => <ol className="list-decimal list-inside my-1">{children}</ol>,
|
|
||||||
li: ({ children }) => <li className="my-0.5">{children}</li>,
|
|
||||||
// Plain tables
|
|
||||||
table: ({ children }) => <table className="my-2 font-mono text-sm">{children}</table>,
|
|
||||||
th: ({ children }) => <th className="text-left pr-4">{children}</th>,
|
|
||||||
td: ({ children }) => <td className="pr-4">{children}</td>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Minimal mode: clean with syntax highlighting
|
|
||||||
if (mode === 'minimal') {
|
|
||||||
return {
|
|
||||||
...baseComponents,
|
|
||||||
// Inline code
|
|
||||||
code: ({ className, children, ...props }) => {
|
|
||||||
const match = /language-(\w+)/.exec(className || '')
|
|
||||||
const isBlock =
|
|
||||||
'node' in props && props.node?.position?.start.line !== props.node?.position?.end.line
|
|
||||||
|
|
||||||
// Block code - use CodeBlock with full mode
|
|
||||||
if (match || isBlock) {
|
|
||||||
const code = String(children).replace(/\n$/, '')
|
|
||||||
return <CodeBlock code={code} language={match?.[1]} mode="full" className="my-1" />
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inline code
|
|
||||||
return <InlineCode>{children}</InlineCode>
|
|
||||||
},
|
|
||||||
pre: ({ children }) => <>{children}</>,
|
|
||||||
// Comfortable paragraph spacing
|
|
||||||
p: ({ children }) => <p className="my-2 leading-relaxed">{children}</p>,
|
|
||||||
// Styled lists
|
|
||||||
ul: ({ children }) => (
|
|
||||||
<ul className="my-2 space-y-1 ps-[16px] pe-2 list-disc marker:text-muted-foreground">
|
|
||||||
{children}
|
|
||||||
</ul>
|
|
||||||
),
|
|
||||||
ol: ({ children }) => <ol className="my-2 space-y-1 pl-6 list-decimal">{children}</ol>,
|
|
||||||
li: ({ children }) => <li>{children}</li>,
|
|
||||||
// Clean tables
|
|
||||||
table: ({ children }) => (
|
|
||||||
<div className="my-3 overflow-x-auto">
|
|
||||||
<table className="min-w-full text-sm">{children}</table>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
thead: ({ children }) => <thead className="border-b">{children}</thead>,
|
|
||||||
th: ({ children }) => (
|
|
||||||
<th className="text-left py-2 px-3 font-semibold text-muted-foreground">{children}</th>
|
|
||||||
),
|
|
||||||
td: ({ children }) => <td className="py-2 px-3 border-b border-border/50">{children}</td>,
|
|
||||||
// Headings - H1/H2 same size, differentiated by weight
|
|
||||||
h1: ({ children }) => <h1 className="font-sans text-base font-bold mt-5 mb-3">{children}</h1>,
|
|
||||||
h2: ({ children }) => (
|
|
||||||
<h2 className="font-sans text-base font-semibold mt-4 mb-3">{children}</h2>
|
|
||||||
),
|
|
||||||
h3: ({ children }) => (
|
|
||||||
<h3 className="font-sans text-sm font-semibold mt-4 mb-2">{children}</h3>
|
|
||||||
),
|
|
||||||
// Blockquotes
|
|
||||||
blockquote: ({ children }) => (
|
|
||||||
<blockquote className="border-l-2 border-muted-foreground/30 pl-3 my-2 text-muted-foreground italic">
|
|
||||||
{children}
|
|
||||||
</blockquote>
|
|
||||||
),
|
|
||||||
// Horizontal rules
|
|
||||||
hr: () => <hr className="my-4 border-border" />,
|
|
||||||
// Strong/emphasis
|
|
||||||
strong: ({ children }) => <strong className="font-semibold">{children}</strong>,
|
|
||||||
em: ({ children }) => <em className="italic">{children}</em>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Full mode: rich styling
|
|
||||||
return {
|
|
||||||
...baseComponents,
|
|
||||||
// Full code blocks with copy button
|
|
||||||
code: ({ className, children, ...props }) => {
|
|
||||||
const match = /language-(\w+)/.exec(className || '')
|
|
||||||
const isBlock =
|
|
||||||
'node' in props && props.node?.position?.start.line !== props.node?.position?.end.line
|
|
||||||
|
|
||||||
if (match || isBlock) {
|
|
||||||
const code = String(children).replace(/\n$/, '')
|
|
||||||
return <CodeBlock code={code} language={match?.[1]} mode="full" className="my-1" />
|
|
||||||
}
|
|
||||||
|
|
||||||
return <InlineCode>{children}</InlineCode>
|
|
||||||
},
|
|
||||||
pre: ({ children }) => <>{children}</>,
|
|
||||||
// Rich paragraph spacing
|
|
||||||
p: ({ children }) => <p className="my-3 leading-relaxed">{children}</p>,
|
|
||||||
// Styled lists
|
|
||||||
ul: ({ children }) => (
|
|
||||||
<ul className="my-3 space-y-1.5 ps-[16px] pe-2 list-disc marker:text-muted-foreground">
|
|
||||||
{children}
|
|
||||||
</ul>
|
|
||||||
),
|
|
||||||
ol: ({ children }) => <ol className="my-3 space-y-1.5 pl-6 list-decimal">{children}</ol>,
|
|
||||||
li: ({ children }) => <li className="leading-relaxed">{children}</li>,
|
|
||||||
// Beautiful tables
|
|
||||||
table: ({ children }) => (
|
|
||||||
<div className="my-4 overflow-x-auto rounded-md border">
|
|
||||||
<table className="min-w-full divide-y divide-border">{children}</table>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
thead: ({ children }) => <thead className="bg-muted/50">{children}</thead>,
|
|
||||||
tbody: ({ children }) => <tbody className="divide-y divide-border">{children}</tbody>,
|
|
||||||
th: ({ children }) => <th className="text-left py-3 px-4 font-semibold text-sm">{children}</th>,
|
|
||||||
td: ({ children }) => <td className="py-3 px-4 text-sm">{children}</td>,
|
|
||||||
tr: ({ children }) => <tr className="hover:bg-muted/30 transition-colors">{children}</tr>,
|
|
||||||
// Rich headings
|
|
||||||
h1: ({ children }) => <h1 className="font-sans text-base font-bold mt-7 mb-4">{children}</h1>,
|
|
||||||
h2: ({ children }) => (
|
|
||||||
<h2 className="font-sans text-base font-semibold mt-6 mb-3">{children}</h2>
|
|
||||||
),
|
|
||||||
h3: ({ children }) => <h3 className="font-sans text-sm font-semibold mt-5 mb-3">{children}</h3>,
|
|
||||||
h4: ({ children }) => <h4 className="text-sm font-semibold mt-3 mb-1">{children}</h4>,
|
|
||||||
// Styled blockquotes
|
|
||||||
blockquote: ({ children }) => (
|
|
||||||
<blockquote className="border-l-4 border-foreground/30 bg-muted/30 pl-4 pr-3 py-2 my-3 rounded-r-md">
|
|
||||||
{children}
|
|
||||||
</blockquote>
|
|
||||||
),
|
|
||||||
// Task lists (GFM)
|
|
||||||
input: ({ type, checked }) => {
|
|
||||||
if (type === 'checkbox') {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={checked}
|
|
||||||
readOnly
|
|
||||||
className="mr-2 rounded border-muted-foreground"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return <input type={type} />
|
|
||||||
},
|
|
||||||
// Horizontal rules
|
|
||||||
hr: () => <hr className="my-6 border-border" />,
|
|
||||||
// Strong/emphasis
|
|
||||||
strong: ({ children }) => <strong className="font-semibold">{children}</strong>,
|
|
||||||
em: ({ children }) => <em className="italic">{children}</em>,
|
|
||||||
del: ({ children }) => <del className="line-through text-muted-foreground">{children}</del>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Markdown - Customizable markdown renderer with multiple render modes
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Three render modes: terminal, minimal, full
|
|
||||||
* - Syntax highlighting via Shiki
|
|
||||||
* - GFM support (tables, task lists, strikethrough)
|
|
||||||
* - Clickable links and file paths
|
|
||||||
* - Memoization for streaming performance
|
|
||||||
*/
|
|
||||||
export function Markdown({
|
|
||||||
children,
|
|
||||||
mode = 'minimal',
|
|
||||||
className,
|
|
||||||
onUrlClick,
|
|
||||||
onFileClick
|
|
||||||
}: MarkdownProps): React.JSX.Element {
|
|
||||||
const components = React.useMemo(
|
|
||||||
() => createComponents(mode, onUrlClick, onFileClick),
|
|
||||||
[mode, onUrlClick, onFileClick]
|
|
||||||
)
|
|
||||||
|
|
||||||
// Preprocess to convert raw URLs and file paths to markdown links
|
|
||||||
const processedContent = React.useMemo(() => preprocessLinks(children), [children])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn('markdown-content break-words', className)}>
|
|
||||||
<ReactMarkdown
|
|
||||||
remarkPlugins={[[remarkGfm, { singleTilde: false }]]}
|
|
||||||
rehypePlugins={[rehypeRaw]}
|
|
||||||
components={components}
|
|
||||||
>
|
|
||||||
{processedContent}
|
|
||||||
</ReactMarkdown>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MemoizedMarkdown - Optimized for streaming scenarios
|
|
||||||
*
|
|
||||||
* Splits content into blocks and memoizes each block separately,
|
|
||||||
* so only new/changed blocks re-render during streaming.
|
|
||||||
*/
|
|
||||||
export const MemoizedMarkdown = React.memo(Markdown, (prevProps, nextProps) => {
|
|
||||||
// If id is provided, use it for memoization
|
|
||||||
if (prevProps.id && nextProps.id) {
|
|
||||||
return (
|
|
||||||
prevProps.id === nextProps.id &&
|
|
||||||
prevProps.children === nextProps.children &&
|
|
||||||
prevProps.mode === nextProps.mode
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// Otherwise compare content and mode
|
|
||||||
return prevProps.children === nextProps.children && prevProps.mode === nextProps.mode
|
|
||||||
})
|
|
||||||
MemoizedMarkdown.displayName = 'MemoizedMarkdown'
|
|
||||||
|
|
@ -1,225 +0,0 @@
|
||||||
import * as React from 'react'
|
|
||||||
import { Markdown, type RenderMode } from './Markdown'
|
|
||||||
|
|
||||||
export interface StreamingMarkdownProps {
|
|
||||||
content: string
|
|
||||||
isStreaming: boolean
|
|
||||||
mode?: RenderMode
|
|
||||||
className?: string
|
|
||||||
onUrlClick?: (url: string) => void
|
|
||||||
onFileClick?: (path: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Block {
|
|
||||||
content: string
|
|
||||||
isCodeBlock: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* djb2 hash (XOR variant) by Daniel J. Bernstein.
|
|
||||||
* Used to generate stable React keys for completed content blocks.
|
|
||||||
*
|
|
||||||
* - 5381: empirically chosen initial value that produces fewer collisions
|
|
||||||
* - (hash << 5) + hash: equivalent to hash * 33
|
|
||||||
* - ^ charCode: XOR variant, favored by Bernstein over additive version
|
|
||||||
* - >>> 0: convert to unsigned 32-bit integer
|
|
||||||
*
|
|
||||||
* Not cryptographic — just fast with good distribution for short strings.
|
|
||||||
* @see http://www.cse.yorku.ca/~oz/hash.html
|
|
||||||
*/
|
|
||||||
function simpleHash(str: string): string {
|
|
||||||
let hash = 5381
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
hash = ((hash << 5) + hash) ^ str.charCodeAt(i)
|
|
||||||
}
|
|
||||||
return (hash >>> 0).toString(36)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Split content into blocks (paragraphs and code blocks)
|
|
||||||
*
|
|
||||||
* Block boundaries:
|
|
||||||
* - Double newlines (paragraph separators)
|
|
||||||
* - Code fences (```)
|
|
||||||
*
|
|
||||||
* This is intentionally simple - just string scanning, no regex per line.
|
|
||||||
*/
|
|
||||||
function splitIntoBlocks(content: string): Block[] {
|
|
||||||
const blocks: Block[] = []
|
|
||||||
const lines = content.split('\n')
|
|
||||||
let currentBlock = ''
|
|
||||||
let inCodeBlock = false
|
|
||||||
let inMathBlock = false
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = lines[i] ?? ''
|
|
||||||
|
|
||||||
// Check for code fence (``` at start of line, optionally followed by language)
|
|
||||||
if (line.startsWith('```')) {
|
|
||||||
if (!inCodeBlock) {
|
|
||||||
// Starting a code block - flush current paragraph first
|
|
||||||
if (currentBlock.trim()) {
|
|
||||||
blocks.push({ content: currentBlock.trim(), isCodeBlock: false })
|
|
||||||
currentBlock = ''
|
|
||||||
}
|
|
||||||
inCodeBlock = true
|
|
||||||
currentBlock = line + '\n'
|
|
||||||
} else {
|
|
||||||
// Ending a code block
|
|
||||||
currentBlock += line
|
|
||||||
blocks.push({ content: currentBlock, isCodeBlock: true })
|
|
||||||
currentBlock = ''
|
|
||||||
inCodeBlock = false
|
|
||||||
}
|
|
||||||
} else if (inCodeBlock) {
|
|
||||||
// Inside code block - append line
|
|
||||||
currentBlock += line + '\n'
|
|
||||||
// Check for display math fence ($$)
|
|
||||||
} else if (line.trim() === '$$') {
|
|
||||||
if (!inMathBlock) {
|
|
||||||
// Starting a math block - flush current paragraph first
|
|
||||||
if (currentBlock.trim()) {
|
|
||||||
blocks.push({ content: currentBlock.trim(), isCodeBlock: false })
|
|
||||||
currentBlock = ''
|
|
||||||
}
|
|
||||||
inMathBlock = true
|
|
||||||
currentBlock = line + '\n'
|
|
||||||
} else {
|
|
||||||
// Ending a math block
|
|
||||||
currentBlock += line
|
|
||||||
blocks.push({ content: currentBlock, isCodeBlock: false })
|
|
||||||
currentBlock = ''
|
|
||||||
inMathBlock = false
|
|
||||||
}
|
|
||||||
} else if (inMathBlock) {
|
|
||||||
// Inside math block - append line (don't split on blank lines)
|
|
||||||
currentBlock += line + '\n'
|
|
||||||
} else if (line === '') {
|
|
||||||
// Empty line outside code block = paragraph boundary
|
|
||||||
if (currentBlock.trim()) {
|
|
||||||
blocks.push({ content: currentBlock.trim(), isCodeBlock: false })
|
|
||||||
currentBlock = ''
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Regular text line
|
|
||||||
if (currentBlock) {
|
|
||||||
currentBlock += '\n' + line
|
|
||||||
} else {
|
|
||||||
currentBlock = line
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush remaining content
|
|
||||||
if (currentBlock) {
|
|
||||||
blocks.push({
|
|
||||||
content: inCodeBlock || inMathBlock ? currentBlock : currentBlock.trim(),
|
|
||||||
isCodeBlock: inCodeBlock
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return blocks
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Memoized block component
|
|
||||||
*
|
|
||||||
* Only re-renders if content or mode changes.
|
|
||||||
* The key is assigned by the parent based on content hash,
|
|
||||||
* so identical content won't even attempt to render.
|
|
||||||
*/
|
|
||||||
const MemoizedBlock = React.memo(
|
|
||||||
function Block({
|
|
||||||
content,
|
|
||||||
mode,
|
|
||||||
className,
|
|
||||||
onUrlClick,
|
|
||||||
onFileClick
|
|
||||||
}: {
|
|
||||||
content: string
|
|
||||||
mode: RenderMode
|
|
||||||
className?: string
|
|
||||||
onUrlClick?: (url: string) => void
|
|
||||||
onFileClick?: (path: string) => void
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Markdown mode={mode} className={className} onUrlClick={onUrlClick} onFileClick={onFileClick}>
|
|
||||||
{content}
|
|
||||||
</Markdown>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
(prev, next) => {
|
|
||||||
// Only re-render if content actually changed
|
|
||||||
return prev.content === next.content && prev.mode === next.mode && prev.className === next.className
|
|
||||||
}
|
|
||||||
)
|
|
||||||
MemoizedBlock.displayName = 'MemoizedBlock'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* StreamingMarkdown - Optimized markdown renderer for streaming content
|
|
||||||
*
|
|
||||||
* Splits content into blocks (paragraphs, code blocks) and memoizes each block
|
|
||||||
* independently. Only the last (active) block re-renders during streaming.
|
|
||||||
*
|
|
||||||
* Key insight: Completed blocks get a content-hash as their React key.
|
|
||||||
* Same content = same key = React skips re-render entirely.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* Content: "Hello\n\n```js\ncode\n```\n\nMore..."
|
|
||||||
*
|
|
||||||
* Block 1: "Hello" -> key="block-abc123" -> memoized
|
|
||||||
* Block 2: "```js\ncode\n```" -> key="block-xyz789" -> memoized
|
|
||||||
* Block 3: "More..." -> key="active-2" -> re-renders
|
|
||||||
*/
|
|
||||||
export function StreamingMarkdown({
|
|
||||||
content,
|
|
||||||
isStreaming,
|
|
||||||
mode = 'minimal',
|
|
||||||
className,
|
|
||||||
onUrlClick,
|
|
||||||
onFileClick
|
|
||||||
}: StreamingMarkdownProps): React.JSX.Element {
|
|
||||||
// Split into blocks - memoized to avoid recomputation
|
|
||||||
// Must be called unconditionally to satisfy Rules of Hooks
|
|
||||||
const blocks = React.useMemo(
|
|
||||||
() => (isStreaming ? splitIntoBlocks(content) : []),
|
|
||||||
[content, isStreaming]
|
|
||||||
)
|
|
||||||
|
|
||||||
// Not streaming - use simple Markdown (no block splitting needed)
|
|
||||||
if (!isStreaming) {
|
|
||||||
return (
|
|
||||||
<Markdown mode={mode} className={className} onUrlClick={onUrlClick} onFileClick={onFileClick}>
|
|
||||||
{content}
|
|
||||||
</Markdown>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Empty content - return null, let parent handle loading indicator
|
|
||||||
if (blocks.length === 0) {
|
|
||||||
return <></>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{blocks.map((block, i) => {
|
|
||||||
const isLastBlock = i === blocks.length - 1
|
|
||||||
|
|
||||||
// Complete blocks use content hash as key -> stable identity -> memoized
|
|
||||||
// Last block uses "active" prefix -> always re-renders on content change
|
|
||||||
const key = isLastBlock ? `active-${i}` : `block-${i}-${simpleHash(block.content)}`
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MemoizedBlock
|
|
||||||
key={key}
|
|
||||||
content={block.content}
|
|
||||||
mode={mode}
|
|
||||||
className={className}
|
|
||||||
onUrlClick={onUrlClick}
|
|
||||||
onFileClick={onFileClick}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export { Markdown, MemoizedMarkdown, type MarkdownProps, type RenderMode } from './Markdown'
|
|
||||||
export { CodeBlock, InlineCode, type CodeBlockProps } from './CodeBlock'
|
|
||||||
export { StreamingMarkdown, type StreamingMarkdownProps } from './StreamingMarkdown'
|
|
||||||
export { preprocessLinks, detectLinks, hasLinks } from './linkify'
|
|
||||||
|
|
@ -1,215 +0,0 @@
|
||||||
import LinkifyIt from 'linkify-it'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Linkify - URL and file path detection for markdown preprocessing
|
|
||||||
*
|
|
||||||
* Uses linkify-it (12M downloads/week) for battle-tested URL detection,
|
|
||||||
* plus custom regex for local file paths.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Initialize linkify-it with default settings (fuzzy URLs, emails enabled)
|
|
||||||
const linkify = new LinkifyIt()
|
|
||||||
|
|
||||||
// File path regex - detects /path, ~/path, ./path with common extensions
|
|
||||||
// Matches paths that start with /, ~/, or ./ followed by path chars and a file extension
|
|
||||||
const FILE_PATH_REGEX =
|
|
||||||
/(?:^|[\s([{<])((\/|~\/|\.\/)[\w\-./@]+\.(?:ts|tsx|js|jsx|mjs|cjs|md|json|yaml|yml|py|go|rs|css|scss|less|html|htm|txt|log|sh|bash|zsh|swift|kt|java|c|cpp|h|hpp|rb|php|xml|toml|ini|cfg|conf|env|sql|graphql|vue|svelte|astro|prisma|dockerfile|makefile|gitignore))(?=[\s)\]}.,;:!?>]|$)/gi
|
|
||||||
|
|
||||||
interface DetectedLink {
|
|
||||||
type: 'url' | 'email' | 'file'
|
|
||||||
text: string
|
|
||||||
url: string
|
|
||||||
start: number
|
|
||||||
end: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CodeRange {
|
|
||||||
start: number
|
|
||||||
end: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all code block and inline code ranges in text
|
|
||||||
* These ranges should be excluded from link detection
|
|
||||||
*/
|
|
||||||
function findCodeRanges(text: string): CodeRange[] {
|
|
||||||
const ranges: CodeRange[] = []
|
|
||||||
|
|
||||||
// Find fenced code blocks (```...```)
|
|
||||||
const fencedRegex = /```[\s\S]*?```/g
|
|
||||||
let match
|
|
||||||
while ((match = fencedRegex.exec(text)) !== null) {
|
|
||||||
ranges.push({ start: match.index, end: match.index + match[0].length })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find display math blocks ($$...$$)
|
|
||||||
const displayMathRegex = /\$\$[\s\S]*?\$\$/g
|
|
||||||
while ((match = displayMathRegex.exec(text)) !== null) {
|
|
||||||
const pos = match.index
|
|
||||||
const insideOther = ranges.some((r) => pos >= r.start && pos < r.end)
|
|
||||||
if (!insideOther) {
|
|
||||||
ranges.push({ start: pos, end: pos + match[0].length })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find inline math ($...$)
|
|
||||||
const inlineMathRegex = /(?<!\$)\$(?!\$)([^\$\n]+)\$(?!\$)/g
|
|
||||||
while ((match = inlineMathRegex.exec(text)) !== null) {
|
|
||||||
const pos = match.index
|
|
||||||
const insideOther = ranges.some((r) => pos >= r.start && pos < r.end)
|
|
||||||
if (!insideOther) {
|
|
||||||
ranges.push({ start: pos, end: pos + match[0].length })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find inline code (`...`)
|
|
||||||
// But skip escaped backticks and code inside fenced blocks
|
|
||||||
const inlineRegex = /(?<!`)`(?!`)([^`\n]+)`(?!`)/g
|
|
||||||
while ((match = inlineRegex.exec(text)) !== null) {
|
|
||||||
const pos = match.index
|
|
||||||
// Check if this is inside a fenced block or math block
|
|
||||||
const insideOther = ranges.some((r) => pos >= r.start && pos < r.end)
|
|
||||||
if (!insideOther) {
|
|
||||||
ranges.push({ start: pos, end: pos + match[0].length })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ranges
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a position is inside any code range
|
|
||||||
*/
|
|
||||||
function isInsideCode(pos: number, ranges: CodeRange[]): boolean {
|
|
||||||
return ranges.some((r) => pos >= r.start && pos < r.end)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a link at given position is already a markdown link
|
|
||||||
* Looks for patterns like [text](url) or [text][ref]
|
|
||||||
*/
|
|
||||||
function isAlreadyLinked(text: string, linkStart: number, linkEnd: number): boolean {
|
|
||||||
// Check if preceded by ]( which indicates we're inside a markdown link href
|
|
||||||
// Pattern: [text](URL) - we're checking if URL is our link
|
|
||||||
const before = text.slice(Math.max(0, linkStart - 2), linkStart)
|
|
||||||
if (before.endsWith('](')) return true
|
|
||||||
|
|
||||||
// Check if preceded by ][ for reference links
|
|
||||||
if (before.endsWith('][')) return true
|
|
||||||
|
|
||||||
// Check if the link text is wrapped in []
|
|
||||||
// Pattern: [URL](href) - URL is being used as link text
|
|
||||||
const charBefore = text[linkStart - 1]
|
|
||||||
const charAfter = text[linkEnd]
|
|
||||||
if (charBefore === '[' && charAfter === ']') return true
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if ranges overlap
|
|
||||||
*/
|
|
||||||
function rangesOverlap(
|
|
||||||
a: { start: number; end: number },
|
|
||||||
b: { start: number; end: number }
|
|
||||||
): boolean {
|
|
||||||
return a.start < b.end && b.start < a.end
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect all links (URLs, emails, file paths) in text
|
|
||||||
*/
|
|
||||||
export function detectLinks(text: string): DetectedLink[] {
|
|
||||||
const links: DetectedLink[] = []
|
|
||||||
|
|
||||||
// 1. Detect URLs and emails with linkify-it
|
|
||||||
const urlMatches = linkify.match(text) || []
|
|
||||||
for (const match of urlMatches) {
|
|
||||||
links.push({
|
|
||||||
type: match.schema === 'mailto:' ? 'email' : 'url',
|
|
||||||
text: match.text,
|
|
||||||
url: match.url,
|
|
||||||
start: match.index,
|
|
||||||
end: match.lastIndex
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Detect file paths with custom regex
|
|
||||||
// Reset regex state
|
|
||||||
FILE_PATH_REGEX.lastIndex = 0
|
|
||||||
let fileMatch
|
|
||||||
while ((fileMatch = FILE_PATH_REGEX.exec(text)) !== null) {
|
|
||||||
const path = fileMatch[1]
|
|
||||||
if (!path) continue // Skip if no capture group
|
|
||||||
|
|
||||||
// Calculate actual start position (after any leading whitespace/punctuation)
|
|
||||||
const fullMatch = fileMatch[0]
|
|
||||||
const pathOffset = fullMatch.indexOf(path)
|
|
||||||
const start = fileMatch.index + pathOffset
|
|
||||||
|
|
||||||
// Check for overlaps with URL matches (URLs take precedence)
|
|
||||||
const pathRange = { start, end: start + path.length }
|
|
||||||
const overlapsUrl = links.some((link) => rangesOverlap(pathRange, link))
|
|
||||||
if (overlapsUrl) continue
|
|
||||||
|
|
||||||
links.push({
|
|
||||||
type: 'file',
|
|
||||||
text: path,
|
|
||||||
url: path, // File paths are passed as-is to onFileClick handler
|
|
||||||
start,
|
|
||||||
end: start + path.length
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by position
|
|
||||||
return links.sort((a, b) => a.start - b.start)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preprocess text to convert raw URLs and file paths into markdown links
|
|
||||||
* Skips code blocks and already-linked content
|
|
||||||
*/
|
|
||||||
export function preprocessLinks(text: string): string {
|
|
||||||
// Quick check - if no potential links, return early
|
|
||||||
if (!linkify.pretest(text) && !/[~/.]\//.test(text)) {
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
const codeRanges = findCodeRanges(text)
|
|
||||||
const links = detectLinks(text)
|
|
||||||
|
|
||||||
if (links.length === 0) return text
|
|
||||||
|
|
||||||
// Build result, converting raw links to markdown links
|
|
||||||
let result = ''
|
|
||||||
let lastIndex = 0
|
|
||||||
|
|
||||||
for (const link of links) {
|
|
||||||
// Skip if inside code block
|
|
||||||
if (isInsideCode(link.start, codeRanges)) continue
|
|
||||||
|
|
||||||
// Skip if already a markdown link
|
|
||||||
if (isAlreadyLinked(text, link.start, link.end)) continue
|
|
||||||
|
|
||||||
// Add text before this link
|
|
||||||
result += text.slice(lastIndex, link.start)
|
|
||||||
|
|
||||||
// Convert to markdown link
|
|
||||||
result += `[${link.text}](${link.url})`
|
|
||||||
|
|
||||||
lastIndex = link.end
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add remaining text
|
|
||||||
result += text.slice(lastIndex)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test if text contains any detectable links
|
|
||||||
* Useful for optimization - skip preprocessing if no links present
|
|
||||||
*/
|
|
||||||
export function hasLinks(text: string): boolean {
|
|
||||||
return linkify.pretest(text) || /[~/.]\/[\w]/.test(text)
|
|
||||||
}
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { cn } from "@multica/ui/lib/utils";
|
|
||||||
|
|
||||||
interface MulticaIconProps extends React.ComponentProps<"span"> {
|
|
||||||
/**
|
|
||||||
* If true, play a one-time entrance spin animation.
|
|
||||||
*/
|
|
||||||
animate?: boolean;
|
|
||||||
/**
|
|
||||||
* If true, disable hover spin animation.
|
|
||||||
*/
|
|
||||||
noSpin?: boolean;
|
|
||||||
/**
|
|
||||||
* If true, show a border around the icon.
|
|
||||||
*/
|
|
||||||
bordered?: boolean;
|
|
||||||
/**
|
|
||||||
* Size of the bordered icon: "sm" (default), "md", "lg"
|
|
||||||
*/
|
|
||||||
size?: "sm" | "md" | "lg";
|
|
||||||
}
|
|
||||||
|
|
||||||
const borderedSizes = {
|
|
||||||
sm: { wrapper: "p-1.5", icon: "size-3.5" },
|
|
||||||
md: { wrapper: "p-2", icon: "size-4" },
|
|
||||||
lg: { wrapper: "p-2.5", icon: "size-5" },
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pure CSS 8-pointed asterisk icon matching the Multica logo.
|
|
||||||
* Uses currentColor so it adapts to light/dark themes automatically.
|
|
||||||
* Clip-path polygon traced from the original SVG path coordinates.
|
|
||||||
*/
|
|
||||||
export function MulticaIcon({
|
|
||||||
className,
|
|
||||||
animate = false,
|
|
||||||
noSpin = false,
|
|
||||||
bordered = false,
|
|
||||||
size = "sm",
|
|
||||||
...props
|
|
||||||
}: MulticaIconProps) {
|
|
||||||
const [entranceDone, setEntranceDone] = useState(!animate);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!animate) return;
|
|
||||||
const timer = setTimeout(() => setEntranceDone(true), 600);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [animate]);
|
|
||||||
|
|
||||||
const clipPath = `polygon(
|
|
||||||
45% 62.1%, 45% 100%, 55% 100%, 55% 62.1%,
|
|
||||||
81.8% 88.9%, 88.9% 81.8%, 62.1% 55%, 100% 55%,
|
|
||||||
100% 45%, 62.1% 45%, 88.9% 18.2%, 81.8% 11.1%,
|
|
||||||
55% 37.9%, 55% 0%, 45% 0%, 45% 37.9%,
|
|
||||||
18.2% 11.1%, 11.1% 18.2%, 37.9% 45%, 0% 45%,
|
|
||||||
0% 55%, 37.9% 55%, 11.1% 81.8%, 18.2% 88.9%
|
|
||||||
)`;
|
|
||||||
|
|
||||||
if (bordered) {
|
|
||||||
const sizeConfig = borderedSizes[size];
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"inline-flex items-center justify-center border border-border rounded-md",
|
|
||||||
sizeConfig.wrapper,
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"block",
|
|
||||||
sizeConfig.icon,
|
|
||||||
!entranceDone && "animate-entrance-spin",
|
|
||||||
entranceDone && !noSpin && "hover:animate-spin"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="block size-full bg-current"
|
|
||||||
style={{ clipPath }}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"inline-block size-[1em]",
|
|
||||||
!entranceDone && "animate-entrance-spin",
|
|
||||||
entranceDone && !noSpin && "hover:animate-spin",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="block size-full bg-current"
|
|
||||||
style={{ clipPath }}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
/**
|
|
||||||
* Spinner — 3x3 grid pulse for **active processing / execution** states.
|
|
||||||
*
|
|
||||||
* Use when the system is actively doing work or waiting for human action
|
|
||||||
* (streaming content, generating responses, awaiting approval).
|
|
||||||
* For passive content-loading states, use `<Loading />` instead.
|
|
||||||
*
|
|
||||||
* Inherits color from `currentColor` (use Tailwind `text-*`).
|
|
||||||
* Scales with font-size (use Tailwind `text-*` for size).
|
|
||||||
*/
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
|
|
||||||
export interface SpinnerProps {
|
|
||||||
/** Additional className for styling (color via text-*, size via Tailwind text-*) */
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const DELAYS = [0.2, 0.3, 0.4, 0.1, 0.2, 0.3, 0, 0.1, 0.2]
|
|
||||||
|
|
||||||
const cubeStyle: React.CSSProperties = {
|
|
||||||
backgroundColor: "currentColor",
|
|
||||||
animation: "spinner-grid 1.3s infinite ease-in-out",
|
|
||||||
transform: "scale3d(0.5, 0.5, 1)",
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Spinner({ className }: SpinnerProps) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(className)}
|
|
||||||
role="status"
|
|
||||||
aria-label="Loading"
|
|
||||||
style={{
|
|
||||||
display: "inline-grid",
|
|
||||||
gridTemplateColumns: "repeat(3, 1fr)",
|
|
||||||
width: "1em",
|
|
||||||
height: "1em",
|
|
||||||
gap: "0.08em",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{DELAYS.map((delay, i) => (
|
|
||||||
<span key={i} style={{ ...cubeStyle, animationDelay: `${delay}s` }} />
|
|
||||||
))}
|
|
||||||
|
|
||||||
<style>{`@keyframes spinner-grid{0%,70%,100%{transform:scale3d(.5,.5,1)}35%{transform:scale3d(0,0,1)}}`}</style>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
|
||||||
|
|
||||||
export function ThemeProvider({
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
|
||||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import { useTheme } from "next-themes"
|
|
||||||
import { Sun, Moon, Monitor } from "lucide-react"
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
} from "@multica/ui/components/ui/dropdown-menu"
|
|
||||||
import { SidebarMenuButton } from "@multica/ui/components/ui/sidebar"
|
|
||||||
|
|
||||||
export function ThemeToggle() {
|
|
||||||
const { setTheme } = useTheme()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger
|
|
||||||
render={
|
|
||||||
<SidebarMenuButton>
|
|
||||||
<Sun className="dark:hidden" />
|
|
||||||
<Moon className="hidden dark:block" />
|
|
||||||
<span>Theme</span>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<DropdownMenuContent side="top" align="start">
|
|
||||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
|
||||||
<Sun /> Light
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
|
||||||
<Moon /> Dark
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
|
||||||
<Monitor /> System
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,187 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
import { Button } from "@multica/ui/components/ui/button"
|
|
||||||
|
|
||||||
function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
|
|
||||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {
|
|
||||||
return (
|
|
||||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {
|
|
||||||
return (
|
|
||||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogOverlay({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: AlertDialogPrimitive.Backdrop.Props) {
|
|
||||||
return (
|
|
||||||
<AlertDialogPrimitive.Backdrop
|
|
||||||
data-slot="alert-dialog-overlay"
|
|
||||||
className={cn(
|
|
||||||
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogContent({
|
|
||||||
className,
|
|
||||||
size = "default",
|
|
||||||
...props
|
|
||||||
}: AlertDialogPrimitive.Popup.Props & {
|
|
||||||
size?: "default" | "sm"
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<AlertDialogPortal>
|
|
||||||
<AlertDialogOverlay />
|
|
||||||
<AlertDialogPrimitive.Popup
|
|
||||||
data-slot="alert-dialog-content"
|
|
||||||
data-size={size}
|
|
||||||
className={cn(
|
|
||||||
"group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</AlertDialogPortal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogHeader({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="alert-dialog-header"
|
|
||||||
className={cn(
|
|
||||||
"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogFooter({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="alert-dialog-footer"
|
|
||||||
className={cn(
|
|
||||||
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogMedia({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="alert-dialog-media"
|
|
||||||
className={cn(
|
|
||||||
"mb-2 inline-flex size-10 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogTitle({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
|
||||||
return (
|
|
||||||
<AlertDialogPrimitive.Title
|
|
||||||
data-slot="alert-dialog-title"
|
|
||||||
className={cn(
|
|
||||||
"font-heading text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogDescription({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
|
||||||
return (
|
|
||||||
<AlertDialogPrimitive.Description
|
|
||||||
data-slot="alert-dialog-description"
|
|
||||||
className={cn(
|
|
||||||
"text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogAction({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Button>) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
data-slot="alert-dialog-action"
|
|
||||||
className={cn(className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AlertDialogCancel({
|
|
||||||
className,
|
|
||||||
variant = "outline",
|
|
||||||
size = "default",
|
|
||||||
...props
|
|
||||||
}: AlertDialogPrimitive.Close.Props &
|
|
||||||
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
|
||||||
return (
|
|
||||||
<AlertDialogPrimitive.Close
|
|
||||||
data-slot="alert-dialog-cancel"
|
|
||||||
className={cn(className)}
|
|
||||||
render={<Button variant={variant} size={size} />}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogMedia,
|
|
||||||
AlertDialogOverlay,
|
|
||||||
AlertDialogPortal,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
import { mergeProps } from "@base-ui/react/merge-props"
|
|
||||||
import { useRender } from "@base-ui/react/use-render"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
|
|
||||||
const badgeVariants = cva(
|
|
||||||
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
||||||
secondary:
|
|
||||||
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
||||||
destructive:
|
|
||||||
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
|
||||||
outline:
|
|
||||||
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
|
||||||
ghost:
|
|
||||||
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function Badge({
|
|
||||||
className,
|
|
||||||
variant = "default",
|
|
||||||
render,
|
|
||||||
...props
|
|
||||||
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
|
||||||
return useRender({
|
|
||||||
defaultTagName: "span",
|
|
||||||
props: mergeProps<"span">(
|
|
||||||
{
|
|
||||||
className: cn(badgeVariants({ variant }), className),
|
|
||||||
},
|
|
||||||
props
|
|
||||||
),
|
|
||||||
render,
|
|
||||||
state: {
|
|
||||||
slot: "badge",
|
|
||||||
variant,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Badge, badgeVariants }
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
|
|
||||||
const buttonVariants = cva(
|
|
||||||
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
|
|
||||||
outline:
|
|
||||||
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
|
||||||
secondary:
|
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
||||||
ghost:
|
|
||||||
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
|
||||||
destructive:
|
|
||||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default:
|
|
||||||
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
||||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
||||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
||||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
|
||||||
icon: "size-8",
|
|
||||||
"icon-xs":
|
|
||||||
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
||||||
"icon-sm":
|
|
||||||
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
|
||||||
"icon-lg": "size-9",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function Button({
|
|
||||||
className,
|
|
||||||
variant = "default",
|
|
||||||
size = "default",
|
|
||||||
...props
|
|
||||||
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
|
||||||
return (
|
|
||||||
<ButtonPrimitive
|
|
||||||
data-slot="button"
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
|
||||||
|
|
@ -1,221 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import {
|
|
||||||
DayPicker,
|
|
||||||
getDefaultClassNames,
|
|
||||||
type DayButton,
|
|
||||||
type Locale,
|
|
||||||
} from "react-day-picker"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
import { Button, buttonVariants } from "@multica/ui/components/ui/button"
|
|
||||||
import { ChevronLeftIcon, ChevronRightIcon, ChevronDownIcon } from "lucide-react"
|
|
||||||
|
|
||||||
function Calendar({
|
|
||||||
className,
|
|
||||||
classNames,
|
|
||||||
showOutsideDays = true,
|
|
||||||
captionLayout = "label",
|
|
||||||
buttonVariant = "ghost",
|
|
||||||
locale,
|
|
||||||
formatters,
|
|
||||||
components,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DayPicker> & {
|
|
||||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
|
||||||
}) {
|
|
||||||
const defaultClassNames = getDefaultClassNames()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DayPicker
|
|
||||||
showOutsideDays={showOutsideDays}
|
|
||||||
className={cn(
|
|
||||||
"group/calendar bg-background p-2 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(7)] in-data-[slot=card-content]:bg-transparent in-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}
|
|
||||||
locale={locale}
|
|
||||||
formatters={{
|
|
||||||
formatMonthDropdown: (date) =>
|
|
||||||
date.toLocaleString(locale?.code, { month: "short" }),
|
|
||||||
...formatters,
|
|
||||||
}}
|
|
||||||
classNames={{
|
|
||||||
root: cn("w-fit", defaultClassNames.root),
|
|
||||||
months: cn(
|
|
||||||
"relative flex flex-col gap-4 md:flex-row",
|
|
||||||
defaultClassNames.months
|
|
||||||
),
|
|
||||||
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
|
|
||||||
nav: cn(
|
|
||||||
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
|
|
||||||
defaultClassNames.nav
|
|
||||||
),
|
|
||||||
button_previous: cn(
|
|
||||||
buttonVariants({ variant: buttonVariant }),
|
|
||||||
"size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
|
|
||||||
defaultClassNames.button_previous
|
|
||||||
),
|
|
||||||
button_next: cn(
|
|
||||||
buttonVariants({ variant: buttonVariant }),
|
|
||||||
"size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
|
|
||||||
defaultClassNames.button_next
|
|
||||||
),
|
|
||||||
month_caption: cn(
|
|
||||||
"flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
|
|
||||||
defaultClassNames.month_caption
|
|
||||||
),
|
|
||||||
dropdowns: cn(
|
|
||||||
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
|
|
||||||
defaultClassNames.dropdowns
|
|
||||||
),
|
|
||||||
dropdown_root: cn(
|
|
||||||
"relative rounded-(--cell-radius)",
|
|
||||||
defaultClassNames.dropdown_root
|
|
||||||
),
|
|
||||||
dropdown: cn(
|
|
||||||
"absolute inset-0 bg-popover opacity-0",
|
|
||||||
defaultClassNames.dropdown
|
|
||||||
),
|
|
||||||
caption_label: cn(
|
|
||||||
"font-medium select-none",
|
|
||||||
captionLayout === "label"
|
|
||||||
? "text-sm"
|
|
||||||
: "flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
|
|
||||||
defaultClassNames.caption_label
|
|
||||||
),
|
|
||||||
table: "w-full border-collapse",
|
|
||||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
|
||||||
weekday: cn(
|
|
||||||
"flex-1 rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none",
|
|
||||||
defaultClassNames.weekday
|
|
||||||
),
|
|
||||||
week: cn("mt-2 flex w-full", defaultClassNames.week),
|
|
||||||
week_number_header: cn(
|
|
||||||
"w-(--cell-size) select-none",
|
|
||||||
defaultClassNames.week_number_header
|
|
||||||
),
|
|
||||||
week_number: cn(
|
|
||||||
"text-[0.8rem] text-muted-foreground select-none",
|
|
||||||
defaultClassNames.week_number
|
|
||||||
),
|
|
||||||
day: cn(
|
|
||||||
"group/day relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)",
|
|
||||||
props.showWeekNumber
|
|
||||||
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)"
|
|
||||||
: "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)",
|
|
||||||
defaultClassNames.day
|
|
||||||
),
|
|
||||||
range_start: cn(
|
|
||||||
"relative isolate z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted",
|
|
||||||
defaultClassNames.range_start
|
|
||||||
),
|
|
||||||
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
|
||||||
range_end: cn(
|
|
||||||
"relative isolate z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted",
|
|
||||||
defaultClassNames.range_end
|
|
||||||
),
|
|
||||||
today: cn(
|
|
||||||
"rounded-(--cell-radius) bg-muted text-foreground 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: ({ ...props }) => (
|
|
||||||
<CalendarDayButton locale={locale} {...props} />
|
|
||||||
),
|
|
||||||
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,
|
|
||||||
locale,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
|
|
||||||
const defaultClassNames = getDefaultClassNames()
|
|
||||||
|
|
||||||
const ref = React.useRef<HTMLButtonElement>(null)
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (modifiers.focused) ref.current?.focus()
|
|
||||||
}, [modifiers.focused])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
data-day={day.date.toLocaleDateString(locale?.code)}
|
|
||||||
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(
|
|
||||||
"relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70",
|
|
||||||
defaultClassNames.day,
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Calendar, CalendarDayButton }
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
|
|
||||||
function Card({
|
|
||||||
className,
|
|
||||||
size = "default",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card"
|
|
||||||
data-size={size}
|
|
||||||
className={cn(
|
|
||||||
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-header"
|
|
||||||
className={cn(
|
|
||||||
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-title"
|
|
||||||
className={cn(
|
|
||||||
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-description"
|
|
||||||
className={cn("text-sm text-muted-foreground", 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-4 group-data-[size=sm]/card:px-3", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-footer"
|
|
||||||
className={cn(
|
|
||||||
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardFooter,
|
|
||||||
CardTitle,
|
|
||||||
CardAction,
|
|
||||||
CardDescription,
|
|
||||||
CardContent,
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
import { CheckIcon } from "lucide-react"
|
|
||||||
|
|
||||||
function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
|
|
||||||
return (
|
|
||||||
<CheckboxPrimitive.Root
|
|
||||||
data-slot="checkbox"
|
|
||||||
className={cn(
|
|
||||||
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<CheckboxPrimitive.Indicator
|
|
||||||
data-slot="checkbox-indicator"
|
|
||||||
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
|
|
||||||
>
|
|
||||||
<CheckIcon
|
|
||||||
/>
|
|
||||||
</CheckboxPrimitive.Indicator>
|
|
||||||
</CheckboxPrimitive.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Checkbox }
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible"
|
|
||||||
|
|
||||||
function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
|
|
||||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) {
|
|
||||||
return (
|
|
||||||
<CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) {
|
|
||||||
return (
|
|
||||||
<CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
|
||||||
|
|
@ -1,300 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { Combobox as ComboboxPrimitive } from "@base-ui/react"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
import { Button } from "@multica/ui/components/ui/button"
|
|
||||||
import {
|
|
||||||
InputGroup,
|
|
||||||
InputGroupAddon,
|
|
||||||
InputGroupButton,
|
|
||||||
InputGroupInput,
|
|
||||||
} from "@multica/ui/components/ui/input-group"
|
|
||||||
import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react"
|
|
||||||
|
|
||||||
const Combobox = ComboboxPrimitive.Root
|
|
||||||
|
|
||||||
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
|
|
||||||
return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxTrigger({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.Trigger.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Trigger
|
|
||||||
data-slot="combobox-trigger"
|
|
||||||
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
|
|
||||||
</ComboboxPrimitive.Trigger>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Clear
|
|
||||||
data-slot="combobox-clear"
|
|
||||||
render={<InputGroupButton variant="ghost" size="icon-xs" />}
|
|
||||||
className={cn(className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<XIcon className="pointer-events-none" />
|
|
||||||
</ComboboxPrimitive.Clear>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxInput({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
disabled = false,
|
|
||||||
showTrigger = true,
|
|
||||||
showClear = false,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.Input.Props & {
|
|
||||||
showTrigger?: boolean
|
|
||||||
showClear?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<InputGroup className={cn("w-auto", className)}>
|
|
||||||
<ComboboxPrimitive.Input
|
|
||||||
render={<InputGroupInput disabled={disabled} />}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
<InputGroupAddon align="inline-end">
|
|
||||||
{showTrigger && (
|
|
||||||
<InputGroupButton
|
|
||||||
size="icon-xs"
|
|
||||||
variant="ghost"
|
|
||||||
render={<ComboboxTrigger />}
|
|
||||||
data-slot="input-group-button"
|
|
||||||
className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{showClear && <ComboboxClear disabled={disabled} />}
|
|
||||||
</InputGroupAddon>
|
|
||||||
{children}
|
|
||||||
</InputGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxContent({
|
|
||||||
className,
|
|
||||||
side = "bottom",
|
|
||||||
sideOffset = 6,
|
|
||||||
align = "start",
|
|
||||||
alignOffset = 0,
|
|
||||||
anchor,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.Popup.Props &
|
|
||||||
Pick<
|
|
||||||
ComboboxPrimitive.Positioner.Props,
|
|
||||||
"side" | "align" | "sideOffset" | "alignOffset" | "anchor"
|
|
||||||
>) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Portal>
|
|
||||||
<ComboboxPrimitive.Positioner
|
|
||||||
side={side}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
align={align}
|
|
||||||
alignOffset={alignOffset}
|
|
||||||
anchor={anchor}
|
|
||||||
className="isolate z-50"
|
|
||||||
>
|
|
||||||
<ComboboxPrimitive.Popup
|
|
||||||
data-slot="combobox-content"
|
|
||||||
data-chips={!!anchor}
|
|
||||||
className={cn(
|
|
||||||
"dark group/combobox-content max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</ComboboxPrimitive.Positioner>
|
|
||||||
</ComboboxPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.List
|
|
||||||
data-slot="combobox-list"
|
|
||||||
className={cn(
|
|
||||||
"no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.Item.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Item
|
|
||||||
data-slot="combobox-item"
|
|
||||||
className={cn(
|
|
||||||
"relative flex w-full cursor-default items-center gap-2 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ComboboxPrimitive.ItemIndicator
|
|
||||||
render={
|
|
||||||
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CheckIcon className="pointer-events-none" />
|
|
||||||
</ComboboxPrimitive.ItemIndicator>
|
|
||||||
</ComboboxPrimitive.Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Group
|
|
||||||
data-slot="combobox-group"
|
|
||||||
className={cn(className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxLabel({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.GroupLabel.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.GroupLabel
|
|
||||||
data-slot="combobox-label"
|
|
||||||
className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Empty
|
|
||||||
data-slot="combobox-empty"
|
|
||||||
className={cn(
|
|
||||||
"hidden w-full justify-center py-2 text-center text-sm text-muted-foreground group-data-empty/combobox-content:flex",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.Separator.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Separator
|
|
||||||
data-slot="combobox-separator"
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxChips({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
|
|
||||||
ComboboxPrimitive.Chips.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Chips
|
|
||||||
data-slot="combobox-chips"
|
|
||||||
className={cn(
|
|
||||||
"flex min-h-8 flex-wrap items-center gap-1 rounded-lg border border-input bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxChip({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
showRemove = true,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.Chip.Props & {
|
|
||||||
showRemove?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Chip
|
|
||||||
data-slot="combobox-chip"
|
|
||||||
className={cn(
|
|
||||||
"flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
{showRemove && (
|
|
||||||
<ComboboxPrimitive.ChipRemove
|
|
||||||
render={<Button variant="ghost" size="icon-xs" />}
|
|
||||||
className="-ml-1 opacity-50 hover:opacity-100"
|
|
||||||
data-slot="combobox-chip-remove"
|
|
||||||
>
|
|
||||||
<XIcon className="pointer-events-none" />
|
|
||||||
</ComboboxPrimitive.ChipRemove>
|
|
||||||
)}
|
|
||||||
</ComboboxPrimitive.Chip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxChipsInput({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.Input.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Input
|
|
||||||
data-slot="combobox-chip-input"
|
|
||||||
className={cn("min-w-16 flex-1 outline-none", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function useComboboxAnchor() {
|
|
||||||
return React.useRef<HTMLDivElement | null>(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Combobox,
|
|
||||||
ComboboxInput,
|
|
||||||
ComboboxContent,
|
|
||||||
ComboboxList,
|
|
||||||
ComboboxItem,
|
|
||||||
ComboboxGroup,
|
|
||||||
ComboboxLabel,
|
|
||||||
ComboboxCollection,
|
|
||||||
ComboboxEmpty,
|
|
||||||
ComboboxSeparator,
|
|
||||||
ComboboxChips,
|
|
||||||
ComboboxChip,
|
|
||||||
ComboboxChipsInput,
|
|
||||||
ComboboxTrigger,
|
|
||||||
ComboboxValue,
|
|
||||||
useComboboxAnchor,
|
|
||||||
}
|
|
||||||
|
|
@ -1,160 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
import { Button } from "@multica/ui/components/ui/button"
|
|
||||||
import { XIcon } from "lucide-react"
|
|
||||||
|
|
||||||
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
|
|
||||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
|
|
||||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
|
|
||||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
|
|
||||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogOverlay({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: DialogPrimitive.Backdrop.Props) {
|
|
||||||
return (
|
|
||||||
<DialogPrimitive.Backdrop
|
|
||||||
data-slot="dialog-overlay"
|
|
||||||
className={cn(
|
|
||||||
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogContent({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
showCloseButton = true,
|
|
||||||
...props
|
|
||||||
}: DialogPrimitive.Popup.Props & {
|
|
||||||
showCloseButton?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DialogPortal>
|
|
||||||
<DialogOverlay />
|
|
||||||
<DialogPrimitive.Popup
|
|
||||||
data-slot="dialog-content"
|
|
||||||
className={cn(
|
|
||||||
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
{showCloseButton && (
|
|
||||||
<DialogPrimitive.Close
|
|
||||||
data-slot="dialog-close"
|
|
||||||
render={
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="absolute top-2 right-2"
|
|
||||||
size="icon-sm"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<XIcon
|
|
||||||
/>
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</DialogPrimitive.Close>
|
|
||||||
)}
|
|
||||||
</DialogPrimitive.Popup>
|
|
||||||
</DialogPortal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="dialog-header"
|
|
||||||
className={cn("flex flex-col gap-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogFooter({
|
|
||||||
className,
|
|
||||||
showCloseButton = false,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
showCloseButton?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="dialog-footer"
|
|
||||||
className={cn(
|
|
||||||
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
{showCloseButton && (
|
|
||||||
<DialogPrimitive.Close render={<Button variant="outline" />}>
|
|
||||||
Close
|
|
||||||
</DialogPrimitive.Close>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
|
|
||||||
return (
|
|
||||||
<DialogPrimitive.Title
|
|
||||||
data-slot="dialog-title"
|
|
||||||
className={cn(
|
|
||||||
"font-heading text-base leading-none font-medium",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogDescription({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: DialogPrimitive.Description.Props) {
|
|
||||||
return (
|
|
||||||
<DialogPrimitive.Description
|
|
||||||
data-slot="dialog-description"
|
|
||||||
className={cn(
|
|
||||||
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogPortal,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
}
|
|
||||||
|
|
@ -1,274 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
import { ChevronRightIcon, CheckIcon } from "lucide-react"
|
|
||||||
|
|
||||||
function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
|
|
||||||
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
|
|
||||||
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
|
|
||||||
return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuContent({
|
|
||||||
align = "start",
|
|
||||||
alignOffset = 0,
|
|
||||||
side = "bottom",
|
|
||||||
sideOffset = 4,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: MenuPrimitive.Popup.Props &
|
|
||||||
Pick<
|
|
||||||
MenuPrimitive.Positioner.Props,
|
|
||||||
"align" | "alignOffset" | "side" | "sideOffset"
|
|
||||||
>) {
|
|
||||||
return (
|
|
||||||
<MenuPrimitive.Portal>
|
|
||||||
<MenuPrimitive.Positioner
|
|
||||||
className="isolate z-50 outline-none"
|
|
||||||
align={align}
|
|
||||||
alignOffset={alignOffset}
|
|
||||||
side={side}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
>
|
|
||||||
<MenuPrimitive.Popup
|
|
||||||
data-slot="dropdown-menu-content"
|
|
||||||
className={cn(
|
|
||||||
"dark z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95 animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</MenuPrimitive.Positioner>
|
|
||||||
</MenuPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
|
|
||||||
return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuLabel({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
...props
|
|
||||||
}: MenuPrimitive.GroupLabel.Props & {
|
|
||||||
inset?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<MenuPrimitive.GroupLabel
|
|
||||||
data-slot="dropdown-menu-label"
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
"px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuItem({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
variant = "default",
|
|
||||||
...props
|
|
||||||
}: MenuPrimitive.Item.Props & {
|
|
||||||
inset?: boolean
|
|
||||||
variant?: "default" | "destructive"
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<MenuPrimitive.Item
|
|
||||||
data-slot="dropdown-menu-item"
|
|
||||||
data-inset={inset}
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(
|
|
||||||
"group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
|
|
||||||
return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSubTrigger({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: MenuPrimitive.SubmenuTrigger.Props & {
|
|
||||||
inset?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<MenuPrimitive.SubmenuTrigger
|
|
||||||
data-slot="dropdown-menu-sub-trigger"
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRightIcon className="ml-auto" />
|
|
||||||
</MenuPrimitive.SubmenuTrigger>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSubContent({
|
|
||||||
align = "start",
|
|
||||||
alignOffset = -3,
|
|
||||||
side = "right",
|
|
||||||
sideOffset = 0,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuContent>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuContent
|
|
||||||
data-slot="dropdown-menu-sub-content"
|
|
||||||
className={cn(
|
|
||||||
"dark w-auto min-w-[96px] rounded-lg p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
align={align}
|
|
||||||
alignOffset={alignOffset}
|
|
||||||
side={side}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuCheckboxItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
checked,
|
|
||||||
inset,
|
|
||||||
...props
|
|
||||||
}: MenuPrimitive.CheckboxItem.Props & {
|
|
||||||
inset?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<MenuPrimitive.CheckboxItem
|
|
||||||
data-slot="dropdown-menu-checkbox-item"
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
|
||||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
|
||||||
>
|
|
||||||
<MenuPrimitive.CheckboxItemIndicator>
|
|
||||||
<CheckIcon
|
|
||||||
/>
|
|
||||||
</MenuPrimitive.CheckboxItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</MenuPrimitive.CheckboxItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
|
|
||||||
return (
|
|
||||||
<MenuPrimitive.RadioGroup
|
|
||||||
data-slot="dropdown-menu-radio-group"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuRadioItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
inset,
|
|
||||||
...props
|
|
||||||
}: MenuPrimitive.RadioItem.Props & {
|
|
||||||
inset?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<MenuPrimitive.RadioItem
|
|
||||||
data-slot="dropdown-menu-radio-item"
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
|
||||||
data-slot="dropdown-menu-radio-item-indicator"
|
|
||||||
>
|
|
||||||
<MenuPrimitive.RadioItemIndicator>
|
|
||||||
<CheckIcon
|
|
||||||
/>
|
|
||||||
</MenuPrimitive.RadioItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</MenuPrimitive.RadioItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: MenuPrimitive.Separator.Props) {
|
|
||||||
return (
|
|
||||||
<MenuPrimitive.Separator
|
|
||||||
data-slot="dropdown-menu-separator"
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuShortcut({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
data-slot="dropdown-menu-shortcut"
|
|
||||||
className={cn(
|
|
||||||
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
}
|
|
||||||
|
|
@ -1,238 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import { useMemo } from "react"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
import { Label } from "@multica/ui/components/ui/label"
|
|
||||||
import { Separator } from "@multica/ui/components/ui/separator"
|
|
||||||
|
|
||||||
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
|
||||||
return (
|
|
||||||
<fieldset
|
|
||||||
data-slot="field-set"
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldLegend({
|
|
||||||
className,
|
|
||||||
variant = "legend",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
|
|
||||||
return (
|
|
||||||
<legend
|
|
||||||
data-slot="field-legend"
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(
|
|
||||||
"mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-group"
|
|
||||||
className={cn(
|
|
||||||
"group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldVariants = cva(
|
|
||||||
"group/field flex w-full gap-2 data-[invalid=true]:text-destructive",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
orientation: {
|
|
||||||
vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
|
|
||||||
horizontal:
|
|
||||||
"flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
||||||
responsive:
|
|
||||||
"flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
orientation: "vertical",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function Field({
|
|
||||||
className,
|
|
||||||
orientation = "vertical",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="group"
|
|
||||||
data-slot="field"
|
|
||||||
data-orientation={orientation}
|
|
||||||
className={cn(fieldVariants({ orientation }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-content"
|
|
||||||
className={cn(
|
|
||||||
"group/field-content flex flex-1 flex-col gap-0.5 leading-snug",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldLabel({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Label>) {
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
data-slot="field-label"
|
|
||||||
className={cn(
|
|
||||||
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10",
|
|
||||||
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-label"
|
|
||||||
className={cn(
|
|
||||||
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
data-slot="field-description"
|
|
||||||
className={cn(
|
|
||||||
"text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
|
|
||||||
"last:mt-0 nth-last-2:-mt-1",
|
|
||||||
"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldSeparator({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
children?: React.ReactNode
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-separator"
|
|
||||||
data-content={!!children}
|
|
||||||
className={cn(
|
|
||||||
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Separator className="absolute inset-0 top-1/2" />
|
|
||||||
{children && (
|
|
||||||
<span
|
|
||||||
className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
|
|
||||||
data-slot="field-separator-content"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldError({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
errors,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
errors?: Array<{ message?: string } | undefined>
|
|
||||||
}) {
|
|
||||||
const content = useMemo(() => {
|
|
||||||
if (children) {
|
|
||||||
return children
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!errors?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueErrors = [
|
|
||||||
...new Map(errors.map((error) => [error?.message, error])).values(),
|
|
||||||
]
|
|
||||||
|
|
||||||
if (uniqueErrors?.length == 1) {
|
|
||||||
return uniqueErrors[0]?.message
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul className="ml-4 flex list-disc flex-col gap-1">
|
|
||||||
{uniqueErrors.map(
|
|
||||||
(error, index) =>
|
|
||||||
error?.message && <li key={index}>{error.message}</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
)
|
|
||||||
}, [children, errors])
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="alert"
|
|
||||||
data-slot="field-error"
|
|
||||||
className={cn("text-sm font-normal text-destructive", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Field,
|
|
||||||
FieldLabel,
|
|
||||||
FieldDescription,
|
|
||||||
FieldError,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLegend,
|
|
||||||
FieldSeparator,
|
|
||||||
FieldSet,
|
|
||||||
FieldContent,
|
|
||||||
FieldTitle,
|
|
||||||
}
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
|
|
||||||
function HoverCard({ ...props }: PreviewCardPrimitive.Root.Props) {
|
|
||||||
return <PreviewCardPrimitive.Root data-slot="hover-card" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function HoverCardTrigger({ ...props }: PreviewCardPrimitive.Trigger.Props) {
|
|
||||||
return (
|
|
||||||
<PreviewCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function HoverCardContent({
|
|
||||||
className,
|
|
||||||
side = "bottom",
|
|
||||||
sideOffset = 4,
|
|
||||||
align = "center",
|
|
||||||
alignOffset = 4,
|
|
||||||
...props
|
|
||||||
}: PreviewCardPrimitive.Popup.Props &
|
|
||||||
Pick<
|
|
||||||
PreviewCardPrimitive.Positioner.Props,
|
|
||||||
"align" | "alignOffset" | "side" | "sideOffset"
|
|
||||||
>) {
|
|
||||||
return (
|
|
||||||
<PreviewCardPrimitive.Portal data-slot="hover-card-portal">
|
|
||||||
<PreviewCardPrimitive.Positioner
|
|
||||||
align={align}
|
|
||||||
alignOffset={alignOffset}
|
|
||||||
side={side}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className="isolate z-50"
|
|
||||||
>
|
|
||||||
<PreviewCardPrimitive.Popup
|
|
||||||
data-slot="hover-card-content"
|
|
||||||
className={cn(
|
|
||||||
"z-50 w-64 origin-(--transform-origin) rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</PreviewCardPrimitive.Positioner>
|
|
||||||
</PreviewCardPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
import { Button } from "@multica/ui/components/ui/button"
|
|
||||||
import { Input } from "@multica/ui/components/ui/input"
|
|
||||||
import { Textarea } from "@multica/ui/components/ui/textarea"
|
|
||||||
|
|
||||||
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="input-group"
|
|
||||||
role="group"
|
|
||||||
className={cn(
|
|
||||||
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputGroupAddonVariants = cva(
|
|
||||||
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
align: {
|
|
||||||
"inline-start":
|
|
||||||
"order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
|
|
||||||
"inline-end":
|
|
||||||
"order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
|
|
||||||
"block-start":
|
|
||||||
"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
|
|
||||||
"block-end":
|
|
||||||
"order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
align: "inline-start",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function InputGroupAddon({
|
|
||||||
className,
|
|
||||||
align = "inline-start",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="group"
|
|
||||||
data-slot="input-group-addon"
|
|
||||||
data-align={align}
|
|
||||||
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
||||||
onClick={(e) => {
|
|
||||||
if ((e.target as HTMLElement).closest("button")) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e.currentTarget.parentElement?.querySelector("input")?.focus()
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputGroupButtonVariants = cva(
|
|
||||||
"flex items-center gap-2 text-sm shadow-none",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
size: {
|
|
||||||
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
|
|
||||||
sm: "",
|
|
||||||
"icon-xs":
|
|
||||||
"size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
|
|
||||||
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
size: "xs",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function InputGroupButton({
|
|
||||||
className,
|
|
||||||
type = "button",
|
|
||||||
variant = "ghost",
|
|
||||||
size = "xs",
|
|
||||||
...props
|
|
||||||
}: Omit<React.ComponentProps<typeof Button>, "size" | "type"> &
|
|
||||||
VariantProps<typeof inputGroupButtonVariants> & {
|
|
||||||
type?: "button" | "submit" | "reset"
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
type={type}
|
|
||||||
data-size={size}
|
|
||||||
variant={variant}
|
|
||||||
className={cn(inputGroupButtonVariants({ size }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputGroupInput({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"input">) {
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
data-slot="input-group-control"
|
|
||||||
className={cn(
|
|
||||||
"flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputGroupTextarea({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"textarea">) {
|
|
||||||
return (
|
|
||||||
<Textarea
|
|
||||||
data-slot="input-group-control"
|
|
||||||
className={cn(
|
|
||||||
"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
InputGroup,
|
|
||||||
InputGroupAddon,
|
|
||||||
InputGroupButton,
|
|
||||||
InputGroupText,
|
|
||||||
InputGroupInput,
|
|
||||||
InputGroupTextarea,
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import { Input as InputPrimitive } from "@base-ui/react/input"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
|
|
||||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
||||||
return (
|
|
||||||
<InputPrimitive
|
|
||||||
type={type}
|
|
||||||
data-slot="input"
|
|
||||||
className={cn(
|
|
||||||
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Input }
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
|
|
||||||
function Label({ className, ...props }: React.ComponentProps<"label">) {
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
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 }
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
|
|
||||||
interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
||||||
external?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
|
|
||||||
({ className, external = true, children, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"text-primary underline-offset-4 hover:underline cursor-pointer transition-colors",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...(external && {
|
|
||||||
target: "_blank",
|
|
||||||
rel: "noopener noreferrer",
|
|
||||||
})}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Link.displayName = "Link"
|
|
||||||
|
|
||||||
export { Link }
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
|
|
||||||
const BAR_COUNT = 8
|
|
||||||
const DURATION = 1.2
|
|
||||||
|
|
||||||
const bars = Array.from({ length: BAR_COUNT }, (_, i) => ({
|
|
||||||
rotate: `${i * 45}deg`,
|
|
||||||
delay: `${-DURATION + (i * DURATION) / BAR_COUNT}s`,
|
|
||||||
}))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loading — Apple-style radiating-line spinner for **passive waiting** states.
|
|
||||||
*
|
|
||||||
* Use when the user is waiting for content to arrive (page init, data fetching).
|
|
||||||
* For active processing / execution states, use `<Spinner />` instead.
|
|
||||||
*
|
|
||||||
* Inherits color from `currentColor` (use Tailwind `text-*`).
|
|
||||||
* Scales with font-size (use Tailwind `text-*` for size).
|
|
||||||
*/
|
|
||||||
function Loading({ className, ...props }: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn("text-muted-foreground", className)}
|
|
||||||
role="status"
|
|
||||||
aria-label="Loading"
|
|
||||||
style={{ display: "inline-block", position: "relative", width: "1em", height: "1em" }}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{bars.map((bar, i) => (
|
|
||||||
<span
|
|
||||||
key={i}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: "calc(50% - 0.04em)",
|
|
||||||
top: "0.1em",
|
|
||||||
width: "0.08em",
|
|
||||||
height: "0.24em",
|
|
||||||
borderRadius: "1em",
|
|
||||||
backgroundColor: "currentColor",
|
|
||||||
transformOrigin: "50% 0.4em",
|
|
||||||
transform: `rotate(${bar.rotate})`,
|
|
||||||
animation: `loading-fade ${DURATION}s linear infinite`,
|
|
||||||
animationDelay: bar.delay,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* keyframes injected once via <style> — React deduplicates identical tags */}
|
|
||||||
<style>{`@keyframes loading-fade{0%{opacity:1}100%{opacity:.15}}`}</style>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Loading }
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { Popover as PopoverPrimitive } from "@base-ui/react/popover"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
|
|
||||||
function Popover({ ...props }: PopoverPrimitive.Root.Props) {
|
|
||||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
|
|
||||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function PopoverContent({
|
|
||||||
className,
|
|
||||||
align = "center",
|
|
||||||
alignOffset = 0,
|
|
||||||
side = "bottom",
|
|
||||||
sideOffset = 4,
|
|
||||||
...props
|
|
||||||
}: PopoverPrimitive.Popup.Props &
|
|
||||||
Pick<
|
|
||||||
PopoverPrimitive.Positioner.Props,
|
|
||||||
"align" | "alignOffset" | "side" | "sideOffset"
|
|
||||||
>) {
|
|
||||||
return (
|
|
||||||
<PopoverPrimitive.Portal>
|
|
||||||
<PopoverPrimitive.Positioner
|
|
||||||
align={align}
|
|
||||||
alignOffset={alignOffset}
|
|
||||||
side={side}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className="isolate z-50"
|
|
||||||
>
|
|
||||||
<PopoverPrimitive.Popup
|
|
||||||
data-slot="popover-content"
|
|
||||||
className={cn(
|
|
||||||
"z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</PopoverPrimitive.Positioner>
|
|
||||||
</PopoverPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="popover-header"
|
|
||||||
className={cn("flex flex-col gap-0.5 text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
|
|
||||||
return (
|
|
||||||
<PopoverPrimitive.Title
|
|
||||||
data-slot="popover-title"
|
|
||||||
className={cn("font-heading font-medium", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function PopoverDescription({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: PopoverPrimitive.Description.Props) {
|
|
||||||
return (
|
|
||||||
<PopoverPrimitive.Description
|
|
||||||
data-slot="popover-description"
|
|
||||||
className={cn("text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverDescription,
|
|
||||||
PopoverHeader,
|
|
||||||
PopoverTitle,
|
|
||||||
PopoverTrigger,
|
|
||||||
}
|
|
||||||
|
|
@ -1,204 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { Select as SelectPrimitive } from "@base-ui/react/select"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
|
|
||||||
|
|
||||||
const Select = SelectPrimitive.Root
|
|
||||||
|
|
||||||
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Group
|
|
||||||
data-slot="select-group"
|
|
||||||
className={cn("scroll-my-1 p-1", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Value
|
|
||||||
data-slot="select-value"
|
|
||||||
className={cn("flex flex-1 text-left", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectTrigger({
|
|
||||||
className,
|
|
||||||
size = "default",
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: SelectPrimitive.Trigger.Props & {
|
|
||||||
size?: "sm" | "default"
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Trigger
|
|
||||||
data-slot="select-trigger"
|
|
||||||
data-size={size}
|
|
||||||
className={cn(
|
|
||||||
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<SelectPrimitive.Icon
|
|
||||||
render={
|
|
||||||
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SelectPrimitive.Trigger>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectContent({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
side = "bottom",
|
|
||||||
sideOffset = 4,
|
|
||||||
align = "center",
|
|
||||||
alignOffset = 0,
|
|
||||||
alignItemWithTrigger = true,
|
|
||||||
...props
|
|
||||||
}: SelectPrimitive.Popup.Props &
|
|
||||||
Pick<
|
|
||||||
SelectPrimitive.Positioner.Props,
|
|
||||||
"align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
|
|
||||||
>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Portal>
|
|
||||||
<SelectPrimitive.Positioner
|
|
||||||
side={side}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
align={align}
|
|
||||||
alignOffset={alignOffset}
|
|
||||||
alignItemWithTrigger={alignItemWithTrigger}
|
|
||||||
className="isolate z-50"
|
|
||||||
>
|
|
||||||
<SelectPrimitive.Popup
|
|
||||||
data-slot="select-content"
|
|
||||||
data-align-trigger={alignItemWithTrigger}
|
|
||||||
className={cn(
|
|
||||||
"dark isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SelectScrollUpButton />
|
|
||||||
<SelectPrimitive.List>{children}</SelectPrimitive.List>
|
|
||||||
<SelectScrollDownButton />
|
|
||||||
</SelectPrimitive.Popup>
|
|
||||||
</SelectPrimitive.Positioner>
|
|
||||||
</SelectPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectLabel({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: SelectPrimitive.GroupLabel.Props) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.GroupLabel
|
|
||||||
data-slot="select-label"
|
|
||||||
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: SelectPrimitive.Item.Props) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Item
|
|
||||||
data-slot="select-item"
|
|
||||||
className={cn(
|
|
||||||
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground 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}
|
|
||||||
>
|
|
||||||
<SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap">
|
|
||||||
{children}
|
|
||||||
</SelectPrimitive.ItemText>
|
|
||||||
<SelectPrimitive.ItemIndicator
|
|
||||||
render={
|
|
||||||
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CheckIcon className="pointer-events-none" />
|
|
||||||
</SelectPrimitive.ItemIndicator>
|
|
||||||
</SelectPrimitive.Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: SelectPrimitive.Separator.Props) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Separator
|
|
||||||
data-slot="select-separator"
|
|
||||||
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectScrollUpButton({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.ScrollUpArrow
|
|
||||||
data-slot="select-scroll-up-button"
|
|
||||||
className={cn(
|
|
||||||
"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronUpIcon
|
|
||||||
/>
|
|
||||||
</SelectPrimitive.ScrollUpArrow>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectScrollDownButton({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.ScrollDownArrow
|
|
||||||
data-slot="select-scroll-down-button"
|
|
||||||
className={cn(
|
|
||||||
"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronDownIcon
|
|
||||||
/>
|
|
||||||
</SelectPrimitive.ScrollDownArrow>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectLabel,
|
|
||||||
SelectScrollDownButton,
|
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectSeparator,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
|
|
||||||
function Separator({
|
|
||||||
className,
|
|
||||||
orientation = "horizontal",
|
|
||||||
...props
|
|
||||||
}: SeparatorPrimitive.Props) {
|
|
||||||
return (
|
|
||||||
<SeparatorPrimitive
|
|
||||||
data-slot="separator"
|
|
||||||
orientation={orientation}
|
|
||||||
className={cn(
|
|
||||||
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Separator }
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
import { Button } from "@multica/ui/components/ui/button"
|
|
||||||
import { XIcon } from "lucide-react"
|
|
||||||
|
|
||||||
function Sheet({ ...props }: SheetPrimitive.Root.Props) {
|
|
||||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
|
|
||||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
|
|
||||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
|
|
||||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
|
|
||||||
return (
|
|
||||||
<SheetPrimitive.Backdrop
|
|
||||||
data-slot="sheet-overlay"
|
|
||||||
className={cn(
|
|
||||||
"fixed inset-0 z-50 bg-black/10 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetContent({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
side = "right",
|
|
||||||
showCloseButton = true,
|
|
||||||
...props
|
|
||||||
}: SheetPrimitive.Popup.Props & {
|
|
||||||
side?: "top" | "right" | "bottom" | "left"
|
|
||||||
showCloseButton?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<SheetPortal>
|
|
||||||
<SheetOverlay />
|
|
||||||
<SheetPrimitive.Popup
|
|
||||||
data-slot="sheet-content"
|
|
||||||
data-side={side}
|
|
||||||
className={cn(
|
|
||||||
"fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
{showCloseButton && (
|
|
||||||
<SheetPrimitive.Close
|
|
||||||
data-slot="sheet-close"
|
|
||||||
render={
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="absolute top-3 right-3"
|
|
||||||
size="icon-sm"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<XIcon
|
|
||||||
/>
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</SheetPrimitive.Close>
|
|
||||||
)}
|
|
||||||
</SheetPrimitive.Popup>
|
|
||||||
</SheetPortal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sheet-header"
|
|
||||||
className={cn("flex flex-col gap-0.5 p-4", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sheet-footer"
|
|
||||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
|
|
||||||
return (
|
|
||||||
<SheetPrimitive.Title
|
|
||||||
data-slot="sheet-title"
|
|
||||||
className={cn(
|
|
||||||
"font-heading text-base font-medium text-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetDescription({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: SheetPrimitive.Description.Props) {
|
|
||||||
return (
|
|
||||||
<SheetPrimitive.Description
|
|
||||||
data-slot="sheet-description"
|
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Sheet,
|
|
||||||
SheetTrigger,
|
|
||||||
SheetClose,
|
|
||||||
SheetContent,
|
|
||||||
SheetHeader,
|
|
||||||
SheetFooter,
|
|
||||||
SheetTitle,
|
|
||||||
SheetDescription,
|
|
||||||
}
|
|
||||||
|
|
@ -1,723 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { mergeProps } from "@base-ui/react/merge-props"
|
|
||||||
import { useRender } from "@base-ui/react/use-render"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { useIsMobile } from "@multica/ui/hooks/use-mobile"
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
import { Button } from "@multica/ui/components/ui/button"
|
|
||||||
import { Input } from "@multica/ui/components/ui/input"
|
|
||||||
import { Separator } from "@multica/ui/components/ui/separator"
|
|
||||||
import {
|
|
||||||
Sheet,
|
|
||||||
SheetContent,
|
|
||||||
SheetDescription,
|
|
||||||
SheetHeader,
|
|
||||||
SheetTitle,
|
|
||||||
} from "@multica/ui/components/ui/sheet"
|
|
||||||
import { Skeleton } from "@multica/ui/components/ui/skeleton"
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@multica/ui/components/ui/tooltip"
|
|
||||||
import { PanelLeftIcon } from "lucide-react"
|
|
||||||
|
|
||||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
|
||||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
|
||||||
const SIDEBAR_WIDTH = "16rem"
|
|
||||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
|
||||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
|
||||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
|
||||||
|
|
||||||
type SidebarContextProps = {
|
|
||||||
state: "expanded" | "collapsed"
|
|
||||||
open: boolean
|
|
||||||
setOpen: (open: boolean) => void
|
|
||||||
openMobile: boolean
|
|
||||||
setOpenMobile: (open: boolean) => void
|
|
||||||
isMobile: boolean
|
|
||||||
toggleSidebar: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
|
|
||||||
|
|
||||||
function useSidebar() {
|
|
||||||
const context = React.useContext(SidebarContext)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useSidebar must be used within a SidebarProvider.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarProvider({
|
|
||||||
defaultOpen = true,
|
|
||||||
open: openProp,
|
|
||||||
onOpenChange: setOpenProp,
|
|
||||||
className,
|
|
||||||
style,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
defaultOpen?: boolean
|
|
||||||
open?: boolean
|
|
||||||
onOpenChange?: (open: boolean) => void
|
|
||||||
}) {
|
|
||||||
const isMobile = useIsMobile()
|
|
||||||
const [openMobile, setOpenMobile] = React.useState(false)
|
|
||||||
|
|
||||||
// This is the internal state of the sidebar.
|
|
||||||
// We use openProp and setOpenProp for control from outside the component.
|
|
||||||
const [_open, _setOpen] = React.useState(defaultOpen)
|
|
||||||
const open = openProp ?? _open
|
|
||||||
const setOpen = React.useCallback(
|
|
||||||
(value: boolean | ((value: boolean) => boolean)) => {
|
|
||||||
const openState = typeof value === "function" ? value(open) : value
|
|
||||||
if (setOpenProp) {
|
|
||||||
setOpenProp(openState)
|
|
||||||
} else {
|
|
||||||
_setOpen(openState)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This sets the cookie to keep the sidebar state.
|
|
||||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
|
||||||
},
|
|
||||||
[setOpenProp, open]
|
|
||||||
)
|
|
||||||
|
|
||||||
// Helper to toggle the sidebar.
|
|
||||||
const toggleSidebar = React.useCallback(() => {
|
|
||||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
|
|
||||||
}, [isMobile, setOpen, setOpenMobile])
|
|
||||||
|
|
||||||
// Adds a keyboard shortcut to toggle the sidebar.
|
|
||||||
React.useEffect(() => {
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if (
|
|
||||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
|
||||||
(event.metaKey || event.ctrlKey)
|
|
||||||
) {
|
|
||||||
event.preventDefault()
|
|
||||||
toggleSidebar()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown)
|
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
|
||||||
}, [toggleSidebar])
|
|
||||||
|
|
||||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
|
||||||
// This makes it easier to style the sidebar with Tailwind classes.
|
|
||||||
const state = open ? "expanded" : "collapsed"
|
|
||||||
|
|
||||||
const contextValue = React.useMemo<SidebarContextProps>(
|
|
||||||
() => ({
|
|
||||||
state,
|
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
isMobile,
|
|
||||||
openMobile,
|
|
||||||
setOpenMobile,
|
|
||||||
toggleSidebar,
|
|
||||||
}),
|
|
||||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SidebarContext.Provider value={contextValue}>
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-wrapper"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--sidebar-width": SIDEBAR_WIDTH,
|
|
||||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
|
||||||
...style,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
"group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</SidebarContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Sidebar({
|
|
||||||
side = "left",
|
|
||||||
variant = "sidebar",
|
|
||||||
collapsible = "offcanvas",
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
dir,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
side?: "left" | "right"
|
|
||||||
variant?: "sidebar" | "floating" | "inset"
|
|
||||||
collapsible?: "offcanvas" | "icon" | "none"
|
|
||||||
}) {
|
|
||||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
|
||||||
|
|
||||||
if (collapsible === "none") {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar"
|
|
||||||
className={cn(
|
|
||||||
"flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
return (
|
|
||||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
|
||||||
<SheetContent
|
|
||||||
dir={dir}
|
|
||||||
data-sidebar="sidebar"
|
|
||||||
data-slot="sidebar"
|
|
||||||
data-mobile="true"
|
|
||||||
className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
side={side}
|
|
||||||
>
|
|
||||||
<SheetHeader className="sr-only">
|
|
||||||
<SheetTitle>Sidebar</SheetTitle>
|
|
||||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
|
||||||
</SheetHeader>
|
|
||||||
<div className="flex h-full w-full flex-col">{children}</div>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="group peer hidden text-sidebar-foreground md:block"
|
|
||||||
data-state={state}
|
|
||||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
|
||||||
data-variant={variant}
|
|
||||||
data-side={side}
|
|
||||||
data-slot="sidebar"
|
|
||||||
>
|
|
||||||
{/* This is what handles the sidebar gap on desktop */}
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-gap"
|
|
||||||
className={cn(
|
|
||||||
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
|
|
||||||
"group-data-[collapsible=offcanvas]:w-0",
|
|
||||||
"group-data-[side=right]:rotate-180",
|
|
||||||
variant === "floating" || variant === "inset"
|
|
||||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
|
||||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-container"
|
|
||||||
data-side={side}
|
|
||||||
className={cn(
|
|
||||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex",
|
|
||||||
// Adjust the padding for floating and inset variants.
|
|
||||||
variant === "floating" || variant === "inset"
|
|
||||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
|
||||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-sidebar="sidebar"
|
|
||||||
data-slot="sidebar-inner"
|
|
||||||
className="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 group-data-[variant=floating]:ring-sidebar-border"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarTrigger({
|
|
||||||
className,
|
|
||||||
onClick,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Button>) {
|
|
||||||
const { toggleSidebar } = useSidebar()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
data-sidebar="trigger"
|
|
||||||
data-slot="sidebar-trigger"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon-sm"
|
|
||||||
className={cn(className)}
|
|
||||||
onClick={(event) => {
|
|
||||||
onClick?.(event)
|
|
||||||
toggleSidebar()
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<PanelLeftIcon />
|
|
||||||
<span className="sr-only">Toggle Sidebar</span>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
|
||||||
const { toggleSidebar } = useSidebar()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
data-sidebar="rail"
|
|
||||||
data-slot="sidebar-rail"
|
|
||||||
aria-label="Toggle Sidebar"
|
|
||||||
tabIndex={-1}
|
|
||||||
onClick={toggleSidebar}
|
|
||||||
title="Toggle Sidebar"
|
|
||||||
className={cn(
|
|
||||||
"absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
|
|
||||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
|
||||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
|
||||||
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar",
|
|
||||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
|
||||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
|
||||||
return (
|
|
||||||
<main
|
|
||||||
data-slot="sidebar-inset"
|
|
||||||
className={cn(
|
|
||||||
"relative flex w-full flex-1 flex-col bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarInput({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Input>) {
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
data-slot="sidebar-input"
|
|
||||||
data-sidebar="input"
|
|
||||||
className={cn("h-8 w-full bg-background shadow-none", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-header"
|
|
||||||
data-sidebar="header"
|
|
||||||
className={cn("flex flex-col gap-2 p-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-footer"
|
|
||||||
data-sidebar="footer"
|
|
||||||
className={cn("flex flex-col gap-2 p-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Separator>) {
|
|
||||||
return (
|
|
||||||
<Separator
|
|
||||||
data-slot="sidebar-separator"
|
|
||||||
data-sidebar="separator"
|
|
||||||
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-content"
|
|
||||||
data-sidebar="content"
|
|
||||||
className={cn(
|
|
||||||
"no-scrollbar flex min-h-0 flex-1 flex-col gap-0 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-group"
|
|
||||||
data-sidebar="group"
|
|
||||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarGroupLabel({
|
|
||||||
className,
|
|
||||||
render,
|
|
||||||
...props
|
|
||||||
}: useRender.ComponentProps<"div"> & React.ComponentProps<"div">) {
|
|
||||||
return useRender({
|
|
||||||
defaultTagName: "div",
|
|
||||||
props: mergeProps<"div">(
|
|
||||||
{
|
|
||||||
className: cn(
|
|
||||||
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
className
|
|
||||||
),
|
|
||||||
},
|
|
||||||
props
|
|
||||||
),
|
|
||||||
render,
|
|
||||||
state: {
|
|
||||||
slot: "sidebar-group-label",
|
|
||||||
sidebar: "group-label",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarGroupAction({
|
|
||||||
className,
|
|
||||||
render,
|
|
||||||
...props
|
|
||||||
}: useRender.ComponentProps<"button"> & React.ComponentProps<"button">) {
|
|
||||||
return useRender({
|
|
||||||
defaultTagName: "button",
|
|
||||||
props: mergeProps<"button">(
|
|
||||||
{
|
|
||||||
className: cn(
|
|
||||||
"absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
className
|
|
||||||
),
|
|
||||||
},
|
|
||||||
props
|
|
||||||
),
|
|
||||||
render,
|
|
||||||
state: {
|
|
||||||
slot: "sidebar-group-action",
|
|
||||||
sidebar: "group-action",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarGroupContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-group-content"
|
|
||||||
data-sidebar="group-content"
|
|
||||||
className={cn("w-full text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
|
||||||
return (
|
|
||||||
<ul
|
|
||||||
data-slot="sidebar-menu"
|
|
||||||
data-sidebar="menu"
|
|
||||||
className={cn("flex w-full min-w-0 flex-col gap-0", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
data-slot="sidebar-menu-item"
|
|
||||||
data-sidebar="menu-item"
|
|
||||||
className={cn("group/menu-item relative", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const sidebarMenuButtonVariants = cva(
|
|
||||||
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
|
||||||
outline:
|
|
||||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-8 text-sm",
|
|
||||||
sm: "h-7 text-xs",
|
|
||||||
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function SidebarMenuButton({
|
|
||||||
render,
|
|
||||||
isActive = false,
|
|
||||||
variant = "default",
|
|
||||||
size = "default",
|
|
||||||
tooltip,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: useRender.ComponentProps<"button"> &
|
|
||||||
React.ComponentProps<"button"> & {
|
|
||||||
isActive?: boolean
|
|
||||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
|
||||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
|
||||||
const { isMobile, state } = useSidebar()
|
|
||||||
const comp = useRender({
|
|
||||||
defaultTagName: "button",
|
|
||||||
props: mergeProps<"button">(
|
|
||||||
{
|
|
||||||
className: cn(sidebarMenuButtonVariants({ variant, size }), className),
|
|
||||||
},
|
|
||||||
props
|
|
||||||
),
|
|
||||||
render: !tooltip ? render : <TooltipTrigger render={render} />,
|
|
||||||
state: {
|
|
||||||
slot: "sidebar-menu-button",
|
|
||||||
sidebar: "menu-button",
|
|
||||||
size,
|
|
||||||
active: isActive,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!tooltip) {
|
|
||||||
return comp
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof tooltip === "string") {
|
|
||||||
tooltip = {
|
|
||||||
children: tooltip,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip>
|
|
||||||
{comp}
|
|
||||||
<TooltipContent
|
|
||||||
side="right"
|
|
||||||
align="center"
|
|
||||||
hidden={state !== "collapsed" || isMobile}
|
|
||||||
{...tooltip}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuAction({
|
|
||||||
className,
|
|
||||||
render,
|
|
||||||
showOnHover = false,
|
|
||||||
...props
|
|
||||||
}: useRender.ComponentProps<"button"> &
|
|
||||||
React.ComponentProps<"button"> & {
|
|
||||||
showOnHover?: boolean
|
|
||||||
}) {
|
|
||||||
return useRender({
|
|
||||||
defaultTagName: "button",
|
|
||||||
props: mergeProps<"button">(
|
|
||||||
{
|
|
||||||
className: cn(
|
|
||||||
"absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
showOnHover &&
|
|
||||||
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0",
|
|
||||||
className
|
|
||||||
),
|
|
||||||
},
|
|
||||||
props
|
|
||||||
),
|
|
||||||
render,
|
|
||||||
state: {
|
|
||||||
slot: "sidebar-menu-action",
|
|
||||||
sidebar: "menu-action",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuBadge({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-menu-badge"
|
|
||||||
data-sidebar="menu-badge"
|
|
||||||
className={cn(
|
|
||||||
"pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium text-sidebar-foreground tabular-nums select-none group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 peer-data-active/menu-button:text-sidebar-accent-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuSkeleton({
|
|
||||||
className,
|
|
||||||
showIcon = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
showIcon?: boolean
|
|
||||||
}) {
|
|
||||||
// Random width between 50 to 90%.
|
|
||||||
const [width] = React.useState(() => {
|
|
||||||
return `${Math.floor(Math.random() * 40) + 50}%`
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-menu-skeleton"
|
|
||||||
data-sidebar="menu-skeleton"
|
|
||||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{showIcon && (
|
|
||||||
<Skeleton
|
|
||||||
className="size-4 rounded-md"
|
|
||||||
data-sidebar="menu-skeleton-icon"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Skeleton
|
|
||||||
className="h-4 max-w-(--skeleton-width) flex-1"
|
|
||||||
data-sidebar="menu-skeleton-text"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--skeleton-width": width,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
|
||||||
return (
|
|
||||||
<ul
|
|
||||||
data-slot="sidebar-menu-sub"
|
|
||||||
data-sidebar="menu-sub"
|
|
||||||
className={cn(
|
|
||||||
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuSubItem({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"li">) {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
data-slot="sidebar-menu-sub-item"
|
|
||||||
data-sidebar="menu-sub-item"
|
|
||||||
className={cn("group/menu-sub-item relative", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuSubButton({
|
|
||||||
render,
|
|
||||||
size = "md",
|
|
||||||
isActive = false,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: useRender.ComponentProps<"a"> &
|
|
||||||
React.ComponentProps<"a"> & {
|
|
||||||
size?: "sm" | "md"
|
|
||||||
isActive?: boolean
|
|
||||||
}) {
|
|
||||||
return useRender({
|
|
||||||
defaultTagName: "a",
|
|
||||||
props: mergeProps<"a">(
|
|
||||||
{
|
|
||||||
className: cn(
|
|
||||||
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
|
||||||
className
|
|
||||||
),
|
|
||||||
},
|
|
||||||
props
|
|
||||||
),
|
|
||||||
render,
|
|
||||||
state: {
|
|
||||||
slot: "sidebar-menu-sub-button",
|
|
||||||
sidebar: "menu-sub-button",
|
|
||||||
size,
|
|
||||||
active: isActive,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
|
||||||
SidebarFooter,
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupAction,
|
|
||||||
SidebarGroupContent,
|
|
||||||
SidebarGroupLabel,
|
|
||||||
SidebarHeader,
|
|
||||||
SidebarInput,
|
|
||||||
SidebarInset,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuAction,
|
|
||||||
SidebarMenuBadge,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
SidebarMenuSkeleton,
|
|
||||||
SidebarMenuSub,
|
|
||||||
SidebarMenuSubButton,
|
|
||||||
SidebarMenuSubItem,
|
|
||||||
SidebarProvider,
|
|
||||||
SidebarRail,
|
|
||||||
SidebarSeparator,
|
|
||||||
SidebarTrigger,
|
|
||||||
useSidebar,
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
|
|
||||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="skeleton"
|
|
||||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Skeleton }
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import { useTheme } from "next-themes"
|
|
||||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
|
||||||
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
|
|
||||||
|
|
||||||
const Toaster = ({ ...props }: ToasterProps) => {
|
|
||||||
const { theme = "system" } = useTheme()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Sonner
|
|
||||||
theme={theme as ToasterProps["theme"]}
|
|
||||||
className="toaster group"
|
|
||||||
icons={{
|
|
||||||
success: (
|
|
||||||
<CircleCheckIcon className="size-4" />
|
|
||||||
),
|
|
||||||
info: (
|
|
||||||
<InfoIcon className="size-4" />
|
|
||||||
),
|
|
||||||
warning: (
|
|
||||||
<TriangleAlertIcon className="size-4" />
|
|
||||||
),
|
|
||||||
error: (
|
|
||||||
<OctagonXIcon className="size-4" />
|
|
||||||
),
|
|
||||||
loading: (
|
|
||||||
<Loader2Icon className="size-4 animate-spin" />
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--normal-bg": "var(--popover)",
|
|
||||||
"--normal-text": "var(--popover-foreground)",
|
|
||||||
"--normal-border": "var(--border)",
|
|
||||||
"--border-radius": "var(--radius)",
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
toastOptions={{
|
|
||||||
classNames: {
|
|
||||||
toast: "cn-toast",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Toaster }
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
|
|
||||||
function Switch({
|
|
||||||
className,
|
|
||||||
size = "default",
|
|
||||||
...props
|
|
||||||
}: SwitchPrimitive.Root.Props & {
|
|
||||||
size?: "sm" | "default"
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<SwitchPrimitive.Root
|
|
||||||
data-slot="switch"
|
|
||||||
data-size={size}
|
|
||||||
className={cn(
|
|
||||||
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SwitchPrimitive.Thumb
|
|
||||||
data-slot="switch-thumb"
|
|
||||||
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
|
|
||||||
/>
|
|
||||||
</SwitchPrimitive.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Switch }
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
|
|
||||||
function Tabs({
|
|
||||||
className,
|
|
||||||
orientation = "horizontal",
|
|
||||||
...props
|
|
||||||
}: TabsPrimitive.Root.Props) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Root
|
|
||||||
data-slot="tabs"
|
|
||||||
data-orientation={orientation}
|
|
||||||
className={cn(
|
|
||||||
"group/tabs flex gap-2 data-horizontal:flex-col",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabsListVariants = cva(
|
|
||||||
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-muted",
|
|
||||||
line: "gap-1 bg-transparent",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function TabsList({
|
|
||||||
className,
|
|
||||||
variant = "default",
|
|
||||||
...props
|
|
||||||
}: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.List
|
|
||||||
data-slot="tabs-list"
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(tabsListVariants({ variant }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Tab
|
|
||||||
data-slot="tabs-trigger"
|
|
||||||
className={cn(
|
|
||||||
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
|
||||||
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
|
|
||||||
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Panel
|
|
||||||
data-slot="tabs-content"
|
|
||||||
className={cn("flex-1 text-sm outline-none", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
|
|
||||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
||||||
return (
|
|
||||||
<textarea
|
|
||||||
data-slot="textarea"
|
|
||||||
className={cn(
|
|
||||||
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Textarea }
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
|
|
||||||
|
|
||||||
import { cn } from "@multica/ui/lib/utils"
|
|
||||||
|
|
||||||
function TooltipProvider({
|
|
||||||
delay = 0,
|
|
||||||
...props
|
|
||||||
}: TooltipPrimitive.Provider.Props) {
|
|
||||||
return (
|
|
||||||
<TooltipPrimitive.Provider
|
|
||||||
data-slot="tooltip-provider"
|
|
||||||
delay={delay}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
|
|
||||||
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
|
|
||||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function TooltipContent({
|
|
||||||
className,
|
|
||||||
side = "top",
|
|
||||||
sideOffset = 4,
|
|
||||||
align = "center",
|
|
||||||
alignOffset = 0,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: TooltipPrimitive.Popup.Props &
|
|
||||||
Pick<
|
|
||||||
TooltipPrimitive.Positioner.Props,
|
|
||||||
"align" | "alignOffset" | "side" | "sideOffset"
|
|
||||||
>) {
|
|
||||||
return (
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipPrimitive.Positioner
|
|
||||||
align={align}
|
|
||||||
alignOffset={alignOffset}
|
|
||||||
side={side}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className="isolate z-50"
|
|
||||||
>
|
|
||||||
<TooltipPrimitive.Popup
|
|
||||||
data-slot="tooltip-content"
|
|
||||||
className={cn(
|
|
||||||
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
|
|
||||||
</TooltipPrimitive.Popup>
|
|
||||||
</TooltipPrimitive.Positioner>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
import { type RefObject, useEffect, useRef, useCallback } from "react"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Auto-scrolls a scroll container to the bottom when its inner content grows,
|
|
||||||
* as long as the user hasn't scrolled up to read older content.
|
|
||||||
*
|
|
||||||
* Returns a `lockRef` that can be set to `true` to temporarily suppress
|
|
||||||
* auto-scroll (e.g. during history prepend operations).
|
|
||||||
*/
|
|
||||||
export function useAutoScroll(ref: RefObject<HTMLElement | null>) {
|
|
||||||
const stickRef = useRef(true)
|
|
||||||
const lockRef = useRef(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const el = ref.current
|
|
||||||
if (!el) return
|
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
|
||||||
el.scrollTo({ top: el.scrollHeight })
|
|
||||||
}
|
|
||||||
|
|
||||||
const onScroll = () => {
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = el
|
|
||||||
stickRef.current = scrollHeight - scrollTop - clientHeight < 50
|
|
||||||
}
|
|
||||||
|
|
||||||
const onContentChange = () => {
|
|
||||||
if (lockRef.current) return
|
|
||||||
if (stickRef.current) {
|
|
||||||
scrollToBottom()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watch child element resizes (content growth, image loads, streaming)
|
|
||||||
const ro = new ResizeObserver(onContentChange)
|
|
||||||
for (const child of el.children) {
|
|
||||||
ro.observe(child)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watch for added/removed child nodes (new messages rendered)
|
|
||||||
const mo = new MutationObserver((mutations) => {
|
|
||||||
// Also observe newly added elements
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
for (const node of mutation.addedNodes) {
|
|
||||||
if (node instanceof Element) {
|
|
||||||
ro.observe(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onContentChange()
|
|
||||||
})
|
|
||||||
mo.observe(el, { childList: true, subtree: true })
|
|
||||||
|
|
||||||
el.addEventListener("scroll", onScroll, { passive: true })
|
|
||||||
|
|
||||||
// Initial scroll to bottom
|
|
||||||
scrollToBottom()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
el.removeEventListener("scroll", onScroll)
|
|
||||||
ro.disconnect()
|
|
||||||
mo.disconnect()
|
|
||||||
}
|
|
||||||
}, [ref])
|
|
||||||
|
|
||||||
/** Temporarily suppress auto-scroll during prepend operations */
|
|
||||||
const suppressAutoScroll = useCallback(() => {
|
|
||||||
lockRef.current = true
|
|
||||||
return () => { lockRef.current = false }
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return { suppressAutoScroll }
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
const MOBILE_BREAKPOINT = 768
|
|
||||||
|
|
||||||
export function useIsMobile() {
|
|
||||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
|
||||||
const onChange = () => {
|
|
||||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
||||||
}
|
|
||||||
mql.addEventListener("change", onChange)
|
|
||||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
||||||
return () => mql.removeEventListener("change", onChange)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return !!isMobile
|
|
||||||
}
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
import { type RefObject, type CSSProperties, useEffect, useState, useCallback } from "react";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a dynamic maskImage style based on scroll position.
|
|
||||||
* - At top → fade bottom only
|
|
||||||
* - At bottom → fade top only
|
|
||||||
* - In middle → fade both
|
|
||||||
* - No overflow → undefined (no mask)
|
|
||||||
*/
|
|
||||||
export function useScrollFade(
|
|
||||||
ref: RefObject<HTMLElement | null>,
|
|
||||||
fadeSize = 32
|
|
||||||
): CSSProperties | undefined {
|
|
||||||
const [fade, setFade] = useState<"none" | "top" | "bottom" | "both">("none");
|
|
||||||
|
|
||||||
const update = useCallback(() => {
|
|
||||||
const el = ref.current;
|
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = el;
|
|
||||||
const scrollable = scrollHeight - clientHeight;
|
|
||||||
|
|
||||||
if (scrollable <= 0) {
|
|
||||||
setFade("none");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const atTop = scrollTop <= 1;
|
|
||||||
const atBottom = scrollTop >= scrollable - 1;
|
|
||||||
|
|
||||||
if (atTop && atBottom) setFade("none");
|
|
||||||
else if (atTop) setFade("bottom");
|
|
||||||
else if (atBottom) setFade("top");
|
|
||||||
else setFade("both");
|
|
||||||
}, [ref]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const el = ref.current;
|
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
const frame = requestAnimationFrame(update);
|
|
||||||
|
|
||||||
el.addEventListener("scroll", update, { passive: true });
|
|
||||||
const ro = new ResizeObserver(update);
|
|
||||||
ro.observe(el);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelAnimationFrame(frame);
|
|
||||||
el.removeEventListener("scroll", update);
|
|
||||||
ro.disconnect();
|
|
||||||
};
|
|
||||||
}, [ref, update]);
|
|
||||||
|
|
||||||
if (fade === "none") return undefined;
|
|
||||||
|
|
||||||
const top = fade === "top" || fade === "both" ? `transparent 0%, black ${fadeSize}px` : "black 0%";
|
|
||||||
const bottom =
|
|
||||||
fade === "bottom" || fade === "both"
|
|
||||||
? `black calc(100% - ${fadeSize}px), transparent 100%`
|
|
||||||
: "black 100%";
|
|
||||||
|
|
||||||
const gradient = `linear-gradient(to bottom, ${top}, ${bottom})`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
maskImage: gradient,
|
|
||||||
WebkitMaskImage: gradient,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import { clsx, type ClassValue } from "clsx"
|
|
||||||
import { twMerge } from "tailwind-merge"
|
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
|
||||||
return twMerge(clsx(inputs))
|
|
||||||
}
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
/**
|
|
||||||
* Multica-specific design tokens and utilities.
|
|
||||||
* Extends the base shadcn theme with project-level customizations.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@theme inline {
|
|
||||||
--font-mono: var(--font-mono);
|
|
||||||
--color-brand: var(--brand);
|
|
||||||
--color-brand-foreground: var(--brand-foreground);
|
|
||||||
--color-canvas: var(--canvas);
|
|
||||||
--color-success: var(--success);
|
|
||||||
--color-warning: var(--warning);
|
|
||||||
--color-info: var(--info);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--brand: oklch(0.55 0.16 255);
|
|
||||||
--brand-foreground: oklch(0.985 0 0);
|
|
||||||
--canvas: oklch(0.95 0.002 286);
|
|
||||||
|
|
||||||
/* Scrollbar */
|
|
||||||
--scrollbar-thumb: oklch(0.82 0.003 286);
|
|
||||||
--scrollbar-thumb-hover: oklch(0.705 0.015 286.067);
|
|
||||||
--scrollbar-track: transparent;
|
|
||||||
|
|
||||||
/* Tool execution states */
|
|
||||||
--tool-running: oklch(0.6 0.18 250);
|
|
||||||
--tool-success: oklch(0.72 0.12 145);
|
|
||||||
--tool-error: oklch(0.65 0.2 25);
|
|
||||||
|
|
||||||
/* Semantic status colors */
|
|
||||||
--success: oklch(0.55 0.16 145);
|
|
||||||
--warning: oklch(0.75 0.16 85);
|
|
||||||
--info: oklch(0.55 0.18 250);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--brand: oklch(0.65 0.16 255);
|
|
||||||
--brand-foreground: oklch(0.985 0 0);
|
|
||||||
--canvas: oklch(0.2 0.005 286);
|
|
||||||
|
|
||||||
--scrollbar-thumb: oklch(1 0 0 / 15%);
|
|
||||||
--scrollbar-thumb-hover: oklch(1 0 0 / 30%);
|
|
||||||
--scrollbar-track: transparent;
|
|
||||||
|
|
||||||
--tool-running: oklch(0.65 0.2 250);
|
|
||||||
--tool-success: oklch(0.65 0.15 145);
|
|
||||||
--tool-error: oklch(0.7 0.2 22);
|
|
||||||
|
|
||||||
--success: oklch(0.65 0.15 145);
|
|
||||||
--warning: oklch(0.70 0.16 85);
|
|
||||||
--info: oklch(0.65 0.18 250);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar styling */
|
|
||||||
@layer base {
|
|
||||||
* {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
|
|
||||||
}
|
|
||||||
*::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
*::-webkit-scrollbar-track {
|
|
||||||
background: var(--scrollbar-track);
|
|
||||||
}
|
|
||||||
*::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--scrollbar-thumb);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
*::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--scrollbar-thumb-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Shiki dual themes: CSS-only light/dark switching */
|
|
||||||
.shiki,
|
|
||||||
.shiki span {
|
|
||||||
color: var(--shiki-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .shiki,
|
|
||||||
.dark .shiki span {
|
|
||||||
color: var(--shiki-dark) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* KaTeX math rendering */
|
|
||||||
.katex {
|
|
||||||
color: inherit;
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.katex-display {
|
|
||||||
margin: 1em 0;
|
|
||||||
overflow-x: auto;
|
|
||||||
overflow-y: hidden;
|
|
||||||
padding: 0.5em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scroll fade mask utilities */
|
|
||||||
.mask-fade-y {
|
|
||||||
mask-image: linear-gradient(to bottom, transparent 0%, black 32px, black calc(100% - 32px), transparent 100%);
|
|
||||||
-webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 32px, black calc(100% - 32px), transparent 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mask-fade-bottom {
|
|
||||||
mask-image: linear-gradient(to bottom, black 0%, black calc(100% - 32px), transparent 100%);
|
|
||||||
-webkit-mask-image: linear-gradient(to bottom, black 0%, black calc(100% - 32px), transparent 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
@utility container {
|
|
||||||
@apply w-full max-w-4xl mx-auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tool status: running glow pulse */
|
|
||||||
@keyframes glow-pulse {
|
|
||||||
0%, 100% { box-shadow: 0 0 0 0 var(--tool-running); }
|
|
||||||
50% { box-shadow: 0 0 0 3px oklch(0.6 0.2 250 / 0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Multica icon: entrance spin */
|
|
||||||
@keyframes entrance-spin {
|
|
||||||
0% { transform: rotate(0deg); opacity: 0; }
|
|
||||||
50% { opacity: 1; }
|
|
||||||
100% { transform: rotate(360deg); opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-entrance-spin {
|
|
||||||
animation: entrance-spin 0.6s ease-out forwards;
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
// Font imports for Desktop (Electron)
|
|
||||||
// Web uses next/font/google instead
|
|
||||||
|
|
||||||
// Geist Mono - Code font
|
|
||||||
import '@fontsource/geist-mono/400.css'
|
|
||||||
import '@fontsource/geist-mono/500.css'
|
|
||||||
import '@fontsource/geist-mono/600.css'
|
|
||||||
import '@fontsource/geist-mono/700.css'
|
|
||||||
|
|
||||||
// Note: Geist Sans removed - Desktop uses system fonts for CJK support
|
|
||||||
// Note: Playfair Display loaded via Google Fonts in index.html
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
@import "tailwindcss";
|
|
||||||
@import "tw-animate-css";
|
|
||||||
@import "shadcn/tailwind.css";
|
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
|
||||||
@source "../../../apps/**/*.{ts,tsx}";
|
|
||||||
@source "../**/*.{ts,tsx}";
|
|
||||||
|
|
||||||
@theme inline {
|
|
||||||
--font-heading: var(--font-sans);
|
|
||||||
--font-sans: var(--font-sans);
|
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
|
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
||||||
--color-sidebar-primary: var(--sidebar-primary);
|
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
||||||
--color-sidebar: var(--sidebar);
|
|
||||||
--color-chart-5: var(--chart-5);
|
|
||||||
--color-chart-4: var(--chart-4);
|
|
||||||
--color-chart-3: var(--chart-3);
|
|
||||||
--color-chart-2: var(--chart-2);
|
|
||||||
--color-chart-1: var(--chart-1);
|
|
||||||
--color-ring: var(--ring);
|
|
||||||
--color-input: var(--input);
|
|
||||||
--color-border: var(--border);
|
|
||||||
--color-destructive: var(--destructive);
|
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
|
||||||
--color-accent: var(--accent);
|
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
|
||||||
--color-muted: var(--muted);
|
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
|
||||||
--color-secondary: var(--secondary);
|
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
|
||||||
--color-primary: var(--primary);
|
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
|
||||||
--color-popover: var(--popover);
|
|
||||||
--color-card-foreground: var(--card-foreground);
|
|
||||||
--color-card: var(--card);
|
|
||||||
--color-foreground: var(--foreground);
|
|
||||||
--color-background: var(--background);
|
|
||||||
--radius-sm: calc(var(--radius) * 0.6);
|
|
||||||
--radius-md: calc(var(--radius) * 0.8);
|
|
||||||
--radius-lg: var(--radius);
|
|
||||||
--radius-xl: calc(var(--radius) * 1.4);
|
|
||||||
--radius-2xl: calc(var(--radius) * 1.8);
|
|
||||||
--radius-3xl: calc(var(--radius) * 2.2);
|
|
||||||
--radius-4xl: calc(var(--radius) * 2.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--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.21 0.006 285.885);
|
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
|
||||||
--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.015 286.067);
|
|
||||||
--chart-1: oklch(0.871 0.006 286.286);
|
|
||||||
--chart-2: oklch(0.552 0.016 285.938);
|
|
||||||
--chart-3: oklch(0.442 0.017 285.786);
|
|
||||||
--chart-4: oklch(0.37 0.013 285.805);
|
|
||||||
--chart-5: oklch(0.274 0.006 286.033);
|
|
||||||
--radius: 0.625rem;
|
|
||||||
--sidebar: oklch(0.985 0 0);
|
|
||||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
|
||||||
--sidebar-primary: oklch(0.21 0.006 285.885);
|
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
||||||
--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.015 286.067);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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.92 0.004 286.32);
|
|
||||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
|
||||||
--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.552 0.016 285.938);
|
|
||||||
--chart-1: oklch(0.871 0.006 286.286);
|
|
||||||
--chart-2: oklch(0.552 0.016 285.938);
|
|
||||||
--chart-3: oklch(0.442 0.017 285.786);
|
|
||||||
--chart-4: oklch(0.37 0.013 285.805);
|
|
||||||
--chart-5: oklch(0.274 0.006 286.033);
|
|
||||||
--sidebar: oklch(0.21 0.006 285.885);
|
|
||||||
--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.274 0.006 286.033);
|
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
|
||||||
--sidebar-ring: oklch(0.552 0.016 285.938);
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
* {
|
|
||||||
@apply border-border outline-ring/50;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
@apply bg-background text-foreground;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Project customizations (separate file for organization) */
|
|
||||||
@import "./custom.css";
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../../tsconfig.base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@multica/ui/*": ["./src/*"]
|
|
||||||
},
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@multica/utils",
|
|
||||||
"version": "0.2.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"main": "./src/index.ts",
|
|
||||||
"exports": {
|
|
||||||
".": "./src/index.ts"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"typecheck": "tsc --noEmit"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"typescript": "catalog:"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
export function formatDate(date: string | Date): string {
|
|
||||||
return new Intl.DateTimeFormat("en-US", {
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
year: "numeric",
|
|
||||||
}).format(new Date(date));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function relativeTime(date: string | Date): string {
|
|
||||||
const now = Date.now();
|
|
||||||
const then = new Date(date).getTime();
|
|
||||||
const diff = now - then;
|
|
||||||
|
|
||||||
const seconds = Math.floor(diff / 1000);
|
|
||||||
if (seconds < 60) return "just now";
|
|
||||||
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
if (minutes < 60) return `${minutes}m ago`;
|
|
||||||
|
|
||||||
const hours = Math.floor(minutes / 60);
|
|
||||||
if (hours < 24) return `${hours}h ago`;
|
|
||||||
|
|
||||||
const days = Math.floor(hours / 24);
|
|
||||||
if (days < 7) return `${days}d ago`;
|
|
||||||
|
|
||||||
return formatDate(date);
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { formatDate, relativeTime } from "./date";
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../../tsconfig.base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
packages:
|
packages:
|
||||||
- "apps/*"
|
- "apps/*"
|
||||||
- "packages/*"
|
|
||||||
|
|
||||||
catalog:
|
catalog:
|
||||||
# Core React
|
# Core React
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ESNext",
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"jsx": "react-jsx"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
42
turbo.json
42
turbo.json
|
|
@ -1,42 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://turbo.build/schema.json",
|
|
||||||
"globalDependencies": ["tsconfig.base.json"],
|
|
||||||
"globalEnv": [
|
|
||||||
"DATABASE_URL",
|
|
||||||
"PORT",
|
|
||||||
"FRONTEND_PORT",
|
|
||||||
"FRONTEND_ORIGIN",
|
|
||||||
"NEXT_PUBLIC_API_URL",
|
|
||||||
"NEXT_PUBLIC_WS_URL",
|
|
||||||
"MULTICA_SERVER_URL",
|
|
||||||
"COMPOSE_PROJECT_NAME",
|
|
||||||
"POSTGRES_DB",
|
|
||||||
"POSTGRES_PORT"
|
|
||||||
],
|
|
||||||
"tasks": {
|
|
||||||
"build": {
|
|
||||||
"dependsOn": ["^build"],
|
|
||||||
"inputs": ["src/**", "package.json", "tsconfig.json"],
|
|
||||||
"outputs": ["dist/**", ".next/**"]
|
|
||||||
},
|
|
||||||
"dev": {
|
|
||||||
"dependsOn": ["^build"],
|
|
||||||
"cache": false,
|
|
||||||
"persistent": true
|
|
||||||
},
|
|
||||||
"typecheck": {
|
|
||||||
"dependsOn": ["^build"],
|
|
||||||
"inputs": ["src/**", "package.json", "tsconfig.json"]
|
|
||||||
},
|
|
||||||
"test": {
|
|
||||||
"dependsOn": ["^build"],
|
|
||||||
"inputs": ["src/**", "package.json", "tsconfig.json", "vitest.config.*"]
|
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"inputs": ["src/**", "package.json"]
|
|
||||||
},
|
|
||||||
"clean": {
|
|
||||||
"cache": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue