mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-16 09:13:42 +08:00
feat: implement drag-and-drop tag reordering and introduce a secret page for adult content search.
This commit is contained in:
@@ -74,6 +74,17 @@
|
||||
- **语义化 HTML**:使用语义化标签提升可访问性
|
||||
- **高对比度**:确保 4.5:1 的文字对比度
|
||||
|
||||
## 🔒 隐藏模式
|
||||
|
||||
本项目包含一个隐藏的"成人模式",仅通过特定操作激活:
|
||||
|
||||
1. 在首页点击"管理标签"
|
||||
2. 在输入框中输入 **"色情"** 并添加
|
||||
3. 点击新添加的 **"色情"** 标签
|
||||
4. 系统将自动跳转至隐藏模式页面
|
||||
|
||||
> **注意**:隐藏模式下的内容源与主页完全隔离,互不干扰。
|
||||
|
||||
## 🛠 技术栈
|
||||
|
||||
### 前端核心
|
||||
|
||||
+10
-1
@@ -6,4 +6,13 @@
|
||||
@import './styles/effects.css';
|
||||
@import './styles/glass.css';
|
||||
@import "./styles/video-player.css";
|
||||
@import "./styles/search-history.css";
|
||||
@import "./styles/search-history.css";
|
||||
@keyframes jiggle {
|
||||
0% { transform: rotate(-1deg); }
|
||||
50% { transform: rotate(1deg); }
|
||||
100% { transform: rotate(-1deg); }
|
||||
}
|
||||
|
||||
.animate-jiggle {
|
||||
animation: jiggle 0.2s infinite;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import { SearchForm } from '@/components/search/SearchForm';
|
||||
import { NoResults } from '@/components/search/NoResults';
|
||||
import { Navbar } from '@/components/layout/Navbar';
|
||||
import { SearchResults } from '@/components/home/SearchResults';
|
||||
import { useSecretHomePage } from '@/lib/hooks/useSecretHomePage';
|
||||
|
||||
function SecretHomePage() {
|
||||
const {
|
||||
query,
|
||||
hasSearched,
|
||||
loading,
|
||||
results,
|
||||
availableSources,
|
||||
completedSources,
|
||||
totalSources,
|
||||
handleSearch,
|
||||
handleReset,
|
||||
} = useSecretHomePage();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black">
|
||||
{/* Glass Navbar */}
|
||||
<Navbar onReset={handleReset} />
|
||||
|
||||
{/* Search Form - Separate from navbar */}
|
||||
<div className="max-w-7xl mx-auto px-4 mt-6 mb-8 relative" style={{
|
||||
transform: 'translate3d(0, 0, 0)',
|
||||
zIndex: 1000
|
||||
}}>
|
||||
<SearchForm
|
||||
onSearch={handleSearch}
|
||||
onClear={handleReset}
|
||||
isLoading={loading}
|
||||
initialQuery={query}
|
||||
currentSource=""
|
||||
checkedSources={completedSources}
|
||||
totalSources={totalSources}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
|
||||
{/* Results Section */}
|
||||
{(results.length >= 1 || (!loading && results.length > 0)) && (
|
||||
<SearchResults
|
||||
results={results}
|
||||
availableSources={availableSources}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* No Results */}
|
||||
{!loading && hasSearched && results.length === 0 && (
|
||||
<NoResults onReset={handleReset} />
|
||||
)}
|
||||
|
||||
{/* Empty State - Just show a dark placeholder or nothing for secret mode */}
|
||||
{!loading && !hasSearched && (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-[var(--text-color-secondary)]">
|
||||
<p>Secret Mode Active</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SecretPage() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="min-h-screen flex items-center justify-center bg-black">
|
||||
<div className="animate-spin rounded-full h-16 w-16 border-4 border-[var(--accent-color)] border-t-transparent"></div>
|
||||
</div>
|
||||
}>
|
||||
<SecretHomePage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -26,6 +26,7 @@ export function PopularFeatures({ onSearch }: PopularFeaturesProps) {
|
||||
handleAddTag,
|
||||
handleDeleteTag,
|
||||
handleRestoreDefaults,
|
||||
handleDragEnd,
|
||||
} = useTagManager();
|
||||
|
||||
const {
|
||||
@@ -49,12 +50,19 @@ export function PopularFeatures({ onSearch }: PopularFeaturesProps) {
|
||||
selectedTag={selectedTag}
|
||||
showTagManager={showTagManager}
|
||||
newTagInput={newTagInput}
|
||||
onTagSelect={setSelectedTag}
|
||||
onTagSelect={(tagId) => {
|
||||
if (tagId === 'custom_色情' || tags.find(t => t.id === tagId)?.label === '色情') {
|
||||
window.location.href = '/secret';
|
||||
return;
|
||||
}
|
||||
setSelectedTag(tagId);
|
||||
}}
|
||||
onTagDelete={handleDeleteTag}
|
||||
onToggleManager={() => setShowTagManager(!showTagManager)}
|
||||
onRestoreDefaults={handleRestoreDefaults}
|
||||
onNewTagInputChange={setNewTagInput}
|
||||
onAddTag={handleAddTag}
|
||||
onDragEnd={handleDragEnd}
|
||||
/>
|
||||
|
||||
<MovieGrid
|
||||
|
||||
+161
-34
@@ -1,11 +1,25 @@
|
||||
/**
|
||||
* TagManager - Tag management UI component
|
||||
* Handles custom tag creation, deletion, and filtering
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Icons } from '@/components/ui/Icon';
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent,
|
||||
DragStartEvent,
|
||||
DragOverlay,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
horizontalListSortingStrategy,
|
||||
useSortable,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
|
||||
interface Tag {
|
||||
id: string;
|
||||
@@ -24,6 +38,73 @@ interface TagManagerProps {
|
||||
onRestoreDefaults: () => void;
|
||||
onNewTagInputChange: (value: string) => void;
|
||||
onAddTag: () => void;
|
||||
onDragEnd: (event: DragEndEvent) => void;
|
||||
}
|
||||
|
||||
function SortableTag({
|
||||
tag,
|
||||
selectedTag,
|
||||
showTagManager,
|
||||
onTagSelect,
|
||||
onTagDelete,
|
||||
}: {
|
||||
tag: Tag;
|
||||
selectedTag: string;
|
||||
showTagManager: boolean;
|
||||
onTagSelect: (id: string) => void;
|
||||
onTagDelete: (id: string) => void;
|
||||
}) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: tag.id, disabled: !showTagManager });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
zIndex: isDragging ? 10 : 1,
|
||||
opacity: isDragging ? 0.3 : 1,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="relative flex-shrink-0"
|
||||
>
|
||||
<div className={`${showTagManager && !isDragging ? 'animate-jiggle' : ''}`}>
|
||||
<button
|
||||
onClick={() => !showTagManager && onTagSelect(tag.id)}
|
||||
className={`
|
||||
px-6 py-2.5 text-sm font-semibold transition-all whitespace-nowrap rounded-[var(--radius-full)] cursor-pointer select-none
|
||||
${selectedTag === tag.id
|
||||
? 'bg-[var(--accent-color)] text-white shadow-md scale-105'
|
||||
: 'bg-[var(--glass-bg)] backdrop-blur-xl text-[var(--text-color)] border border-[var(--glass-border)] hover:border-[var(--accent-color)] hover:scale-105'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{tag.label}
|
||||
</button>
|
||||
{showTagManager && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onTagDelete(tag.id);
|
||||
}}
|
||||
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white flex items-center justify-center hover:bg-red-600 transition-colors rounded-[var(--radius-full)] cursor-pointer z-20 shadow-sm"
|
||||
>
|
||||
<Icons.X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TagManager({
|
||||
@@ -37,8 +118,46 @@ export function TagManager({
|
||||
onRestoreDefaults,
|
||||
onNewTagInputChange,
|
||||
onAddTag,
|
||||
onDragEnd,
|
||||
}: TagManagerProps) {
|
||||
const isCustomTag = (tagId: string) => tagId.startsWith('custom_');
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const prevTagsLength = useRef(tags.length);
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
|
||||
// Auto-scroll to end when new tag is added
|
||||
useEffect(() => {
|
||||
if (tags.length > prevTagsLength.current) {
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTo({
|
||||
left: scrollContainerRef.current.scrollWidth,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
prevTagsLength.current = tags.length;
|
||||
}, [tags.length]);
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
setActiveId(event.active.id as string);
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
setActiveId(null);
|
||||
onDragEnd(event);
|
||||
};
|
||||
|
||||
const activeTag = tags.find((t) => t.id === activeId);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -83,35 +202,43 @@ export function TagManager({
|
||||
)}
|
||||
|
||||
{/* Tag Filter */}
|
||||
<div className="mb-8 flex items-center gap-3 overflow-x-auto pb-3 pt-2 px-1 scrollbar-hide">
|
||||
{tags.map((tag) => (
|
||||
<div key={tag.id} className="relative flex-shrink-0">
|
||||
<button
|
||||
onClick={() => onTagSelect(tag.id)}
|
||||
className={`
|
||||
px-6 py-2.5 text-sm font-semibold transition-all whitespace-nowrap rounded-[var(--radius-full)] cursor-pointer
|
||||
${selectedTag === tag.id
|
||||
? 'bg-[var(--accent-color)] text-white shadow-md scale-105'
|
||||
: 'bg-[var(--glass-bg)] backdrop-blur-xl text-[var(--text-color)] border border-[var(--glass-border)] hover:border-[var(--accent-color)] hover:scale-105'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{tag.label}
|
||||
</button>
|
||||
{showTagManager && isCustomTag(tag.id) && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onTagDelete(tag.id);
|
||||
}}
|
||||
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white flex items-center justify-center hover:bg-red-600 transition-colors rounded-[var(--radius-full)] cursor-pointer"
|
||||
>
|
||||
<Icons.X size={14} />
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="mb-8 flex items-center gap-3 overflow-x-auto pb-3 pt-2 px-1 scrollbar-hide"
|
||||
>
|
||||
<SortableContext
|
||||
items={tags.map(t => t.id)}
|
||||
strategy={horizontalListSortingStrategy}
|
||||
>
|
||||
{tags.map((tag) => (
|
||||
<SortableTag
|
||||
key={tag.id}
|
||||
tag={tag}
|
||||
selectedTag={selectedTag}
|
||||
showTagManager={showTagManager}
|
||||
onTagSelect={onTagSelect}
|
||||
onTagDelete={onTagDelete}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</div>
|
||||
|
||||
<DragOverlay>
|
||||
{activeId && activeTag ? (
|
||||
<div className="relative flex-shrink-0 animate-jiggle">
|
||||
<button className="px-6 py-2.5 text-sm font-semibold whitespace-nowrap rounded-[var(--radius-full)] bg-[var(--accent-color)] text-white shadow-xl scale-110 cursor-grabbing border border-transparent">
|
||||
{activeTag.label}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { DragEndEvent } from '@dnd-kit/core';
|
||||
import { arrayMove } from '@dnd-kit/sortable';
|
||||
|
||||
const DEFAULT_TAGS = [
|
||||
{ id: 'popular', label: '热门', value: '热门' },
|
||||
@@ -69,6 +71,16 @@ export function useTagManager() {
|
||||
setShowTagManager(false);
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = tags.findIndex((tag) => tag.id === active.id);
|
||||
const newIndex = tags.findIndex((tag) => tag.id === over.id);
|
||||
saveTags(arrayMove(tags, oldIndex, newIndex));
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
tags,
|
||||
selectedTag,
|
||||
@@ -80,5 +92,6 @@ export function useTagManager() {
|
||||
handleAddTag,
|
||||
handleDeleteTag,
|
||||
handleRestoreDefaults,
|
||||
handleDragEnd,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { VideoSource } from '@/lib/types';
|
||||
|
||||
export const ADULT_SOURCES: VideoSource[] = [
|
||||
{
|
||||
id: 'ck',
|
||||
name: 'CK',
|
||||
baseUrl: 'https://www.ckzy1.com/api.php/provide/vod',
|
||||
searchPath: '',
|
||||
detailPath: '',
|
||||
enabled: true,
|
||||
priority: 1
|
||||
},
|
||||
{
|
||||
id: 'jkun',
|
||||
name: 'jkun',
|
||||
baseUrl: 'https://jkunzyapi.com/api.php/provide/vod',
|
||||
searchPath: '',
|
||||
detailPath: '',
|
||||
enabled: true,
|
||||
priority: 2
|
||||
},
|
||||
{
|
||||
id: 'souav',
|
||||
name: 'souav',
|
||||
baseUrl: 'https://api.souavzy.vip/api.php/provide/vod',
|
||||
searchPath: '',
|
||||
detailPath: '',
|
||||
enabled: true,
|
||||
priority: 3
|
||||
},
|
||||
{
|
||||
id: '155',
|
||||
name: '155',
|
||||
baseUrl: 'https://155api.com/api.php/provide/vod',
|
||||
searchPath: '',
|
||||
detailPath: '',
|
||||
enabled: true,
|
||||
priority: 4
|
||||
},
|
||||
{
|
||||
id: 'lsb',
|
||||
name: 'lsb',
|
||||
baseUrl: 'https://apilsbzy1.com/api.php/provide/vod',
|
||||
searchPath: '',
|
||||
detailPath: '',
|
||||
enabled: true,
|
||||
priority: 5
|
||||
},
|
||||
{
|
||||
id: 'hsck',
|
||||
name: '黄色仓库',
|
||||
baseUrl: 'https://hsckzy.vip/api.php/provide/vod',
|
||||
searchPath: '',
|
||||
detailPath: '',
|
||||
enabled: true,
|
||||
priority: 6
|
||||
},
|
||||
{
|
||||
id: 'yutu',
|
||||
name: '玉兔',
|
||||
baseUrl: 'https://yutuzy10.com/api.php/provide/vod',
|
||||
searchPath: '',
|
||||
detailPath: '',
|
||||
enabled: true,
|
||||
priority: 7
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useSearchCache } from '@/lib/hooks/useSearchCache';
|
||||
import { useParallelSearch } from '@/lib/hooks/useParallelSearch';
|
||||
import { ADULT_SOURCES } from '@/lib/api/adult-sources';
|
||||
|
||||
export function useSecretHomePage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { loadFromCache, saveToCache } = useSearchCache();
|
||||
const hasLoadedCache = useRef(false);
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
const [currentSortBy, setCurrentSortBy] = useState('default');
|
||||
|
||||
// Search stream hook
|
||||
const {
|
||||
loading,
|
||||
results,
|
||||
availableSources,
|
||||
completedSources,
|
||||
totalSources,
|
||||
performSearch,
|
||||
resetSearch,
|
||||
loadCachedResults,
|
||||
applySorting,
|
||||
} = useParallelSearch(
|
||||
saveToCache,
|
||||
(q: string) => router.replace(`/secret?q=${encodeURIComponent(q)}`, { scroll: false })
|
||||
);
|
||||
|
||||
// Re-sort results when sort preference changes
|
||||
useEffect(() => {
|
||||
if (hasSearched && results.length > 0) {
|
||||
applySorting(currentSortBy as any);
|
||||
}
|
||||
}, [currentSortBy, applySorting, hasSearched, results.length]);
|
||||
|
||||
// Load cached results on mount
|
||||
useEffect(() => {
|
||||
if (hasLoadedCache.current) return;
|
||||
hasLoadedCache.current = true;
|
||||
|
||||
const urlQuery = searchParams.get('q');
|
||||
// Note: We might want to separate cache for secret mode, but for now sharing or not using cache might be safer.
|
||||
// However, useSearchCache uses localStorage which is shared.
|
||||
// If we want to avoid leaking secret searches to normal history, we might want to disable cache or use a different key.
|
||||
// For simplicity and "hidden" nature, maybe we don't load cache from normal mode?
|
||||
// But the user asked for "same as original page".
|
||||
|
||||
if (urlQuery) {
|
||||
setQuery(urlQuery);
|
||||
handleSearch(urlQuery);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
const handleSearch = (searchQuery: string) => {
|
||||
setQuery(searchQuery);
|
||||
setHasSearched(true);
|
||||
// Always use ADULT_SOURCES
|
||||
performSearch(searchQuery, ADULT_SOURCES, currentSortBy as any);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setHasSearched(false);
|
||||
setQuery('');
|
||||
resetSearch();
|
||||
router.replace('/secret', { scroll: false });
|
||||
};
|
||||
|
||||
return {
|
||||
query,
|
||||
hasSearched,
|
||||
loading,
|
||||
results,
|
||||
availableSources,
|
||||
completedSources,
|
||||
totalSources,
|
||||
handleSearch,
|
||||
handleReset,
|
||||
};
|
||||
}
|
||||
Generated
+56
@@ -8,6 +8,9 @@
|
||||
"name": "kvideo",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@vercel/analytics": "^1.5.0",
|
||||
"next": "16.0.3",
|
||||
"react": "19.2.0",
|
||||
@@ -279,6 +282,59 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/accessibility": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/core": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/sortable": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dnd-kit/core": "^6.3.0",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/utilities": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@vercel/analytics": "^1.5.0",
|
||||
"next": "16.0.3",
|
||||
"react": "19.2.0",
|
||||
|
||||
Reference in New Issue
Block a user