Compare commits
9 Commits
main
...
wip/changa
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a54bb29fb | |||
| 0e11c4153d | |||
| 6e921e82fb | |||
| 7a4578c16d | |||
| 9cd93e60df | |||
| 111b6fbadd | |||
| a2ef989b75 | |||
| 9ad083b014 | |||
| 8d3c805985 |
16
lib/client/storage.ts
Normal file
16
lib/client/storage.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// lib/client/storage.ts
|
||||
export const CLIENT_STORAGE_KEYS = {
|
||||
THEME: "theme",
|
||||
LANG: "lang"
|
||||
} as const;
|
||||
|
||||
export const getInitialState = () => {
|
||||
if (typeof window === "undefined") return {};
|
||||
|
||||
return {
|
||||
uiStore: {
|
||||
theme: localStorage.getItem(CLIENT_STORAGE_KEYS.THEME) || "light",
|
||||
language: localStorage.getItem(CLIENT_STORAGE_KEYS.LANG) || "zh-CN"
|
||||
}
|
||||
};
|
||||
};
|
||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"plugins": ["prettier-plugin-tailwindcss"],
|
||||
"tailwindStylesheet": "./app/globals.css"
|
||||
}
|
||||
@@ -1,26 +1,2 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
@plugin "daisyui";
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { StoreProvider } from "../stores/storeContext";
|
||||
import { getInitialState } from "@/ lib/client/storage";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -22,12 +24,16 @@ export default function RootLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const initialState = getInitialState();
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased overflow-hidden`}
|
||||
>
|
||||
{children}
|
||||
<StoreProvider initialData={initialState}>
|
||||
{children}
|
||||
</StoreProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
194
app/page.tsx
194
app/page.tsx
@@ -1,103 +1,103 @@
|
||||
import Image from "next/image";
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import DraggablePanel from "@/components/DraggablePanel";
|
||||
import Logo from "@/widgets/Logo";
|
||||
import Preview from "@/components/Draggable/Preview";
|
||||
import Draggable from "@/components/Draggable/Draggable";
|
||||
import PreviewStore from "@/stores/previewStore";
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
import ComponentsStore from "@/stores/componentStore";
|
||||
import ComponentPaletteDrawer from "@/components/ComponentPaletteDrawer";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
|
||||
app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
</ol>
|
||||
const [isChangeSize, setIsSizeChangeSize] = useState(false);
|
||||
const [isDraggable, setIsDraggable] = useState(false);
|
||||
const { components } = ComponentsStore;
|
||||
// useEffect(() => {
|
||||
// ComponentsStore.initComponent([
|
||||
// {
|
||||
// id: "1",
|
||||
// x: 0,
|
||||
// y: 0,
|
||||
// width: 320,
|
||||
// height: 160,
|
||||
// component: () => <Logo />
|
||||
// },
|
||||
// {
|
||||
// id: "2",
|
||||
// x: 336,
|
||||
// y: 0,
|
||||
// width: 160,
|
||||
// height: 320,
|
||||
// component: () => <Logo />
|
||||
// },
|
||||
// {
|
||||
// id: "3",
|
||||
// x: 336,
|
||||
// y: 0,
|
||||
// width: 160,
|
||||
// height: 320,
|
||||
// component: () => <Logo />
|
||||
// },
|
||||
// {
|
||||
// id: "4",
|
||||
// x: 336,
|
||||
// y: 0,
|
||||
// width: 160,
|
||||
// height: 320,
|
||||
// component: () => <Logo />
|
||||
// },
|
||||
// {
|
||||
// id: "5",
|
||||
// x: 336,
|
||||
// y: 0,
|
||||
// width: 160,
|
||||
// height: 320,
|
||||
// component: () => <Logo />
|
||||
// },
|
||||
// {
|
||||
// id: "6",
|
||||
// x: 336,
|
||||
// y: 0,
|
||||
// width: 160,
|
||||
// height: 320,
|
||||
// component: () => <Logo />
|
||||
// },
|
||||
// ]);
|
||||
// }, [])
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
return useObserver(() => (
|
||||
<div className="h-screen w-screen p-4">
|
||||
<label className="swap">
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={(e) => setIsSizeChangeSize(e.target.checked)}
|
||||
/>
|
||||
<div className="swap-on">编辑</div>
|
||||
<div className="swap-off">锁定</div>
|
||||
</label>
|
||||
<label className="swap">
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={(e) => setIsDraggable(e.target.checked)}
|
||||
/>
|
||||
<div className="swap-on">拖拽</div>
|
||||
<div className="swap-off">锁定</div>
|
||||
</label>
|
||||
<ComponentPaletteDrawer />
|
||||
<DraggablePanel draggable={isDraggable}>
|
||||
{ComponentsStore.components.map((item) => (
|
||||
<Draggable
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
x={item.x}
|
||||
y={item.y}
|
||||
width={item.width}
|
||||
height={item.height}
|
||||
widgetsId={item.widgetsId}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
))}
|
||||
</DraggablePanel>
|
||||
</div>
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
0
app/settings/page.tsx
Normal file
0
app/settings/page.tsx
Normal file
42
app/signin/page.tsx
Normal file
42
app/signin/page.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import "@/app/globals.css";
|
||||
import Logo from "@/widgets/Logo";
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
{/* Logo Section */}
|
||||
<div className="text-center">
|
||||
<Logo />
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900">
|
||||
登录您的账户
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Login Form */}
|
||||
<form className="mt-8 space-y-6" action="#" method="POST">
|
||||
{/* Email Input */}
|
||||
<div>
|
||||
<input type="text" placeholder="帐号" className="input w-full" />
|
||||
</div>
|
||||
|
||||
{/* Password Input */}
|
||||
<div>
|
||||
<input type="password" placeholder="密码" className="input w-full" />
|
||||
</div>
|
||||
|
||||
{/* Remember Me & Forgot Password */}
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="fieldset-label">
|
||||
<input type="checkbox" defaultChecked className="checkbox" />
|
||||
记住我
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button className="btn w-full">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
117
components/ComponentPaletteDrawer.tsx
Normal file
117
components/ComponentPaletteDrawer.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
"use client";
|
||||
import "@/app/globals.css";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import ComponentsStore from "@/stores/componentStore";
|
||||
import { useWidgets } from "@/hooks/useWidgets";
|
||||
|
||||
export default function ComponentPaletteDrawer() {
|
||||
const checkboxRef = useRef<HTMLInputElement>(null);
|
||||
const [widgetsId, setwidgetsId] = useState("logo");
|
||||
const [data, setData] = useState("");
|
||||
const {widgets, widgetsLibrary} = useWidgets();
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const widget = widgets.find(item => item.id === widgetsId);
|
||||
const defaultConfig = widget?.defaultConfig({})
|
||||
const stringData = JSON.stringify(defaultConfig, undefined, 4);
|
||||
setData(stringData);
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
}, [widgetsId]);
|
||||
|
||||
const onSubmit = (e: SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
ComponentsStore.addComponent(widgetsId, data && JSON.parse(data));
|
||||
checkboxRef.current?.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="drawer drawer-end">
|
||||
<input
|
||||
ref={checkboxRef}
|
||||
id="ComponentPaletteDrawer"
|
||||
type="checkbox"
|
||||
className="drawer-toggle"
|
||||
/>
|
||||
<div className="drawer-content">
|
||||
{/* Page content here */}
|
||||
<label
|
||||
htmlFor="ComponentPaletteDrawer"
|
||||
className="drawer-button btn btn-primary"
|
||||
>
|
||||
Add Component
|
||||
</label>
|
||||
</div>
|
||||
<div className="drawer-side z-50">
|
||||
<label
|
||||
htmlFor="ComponentPaletteDrawer"
|
||||
aria-label="close sidebar"
|
||||
className="drawer-overlay"
|
||||
></label>
|
||||
<div className="menu min-h-full w-2/5 bg-base-200 p-0 text-base-content">
|
||||
<div className="navbar bg-base-100 shadow-sm">
|
||||
<a className="btn text-xl btn-ghost">daisyUI</a>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div role="alert" className="alert alert-warning mb-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Warning: 内容处于开发阶段仅供参考!</span>
|
||||
</div>
|
||||
|
||||
<div className="resize-y border overflow-hidden max-w-full h-64 ">
|
||||
{
|
||||
9 }
|
||||
</div>
|
||||
|
||||
<form onSubmit={onSubmit} className="w-full">
|
||||
<fieldset className="fieldset w-full">
|
||||
<legend className="fieldset-legend">小部件</legend>
|
||||
<select
|
||||
value={widgetsId}
|
||||
onChange={(e) => setwidgetsId(e.target.value)}
|
||||
className="select w-full"
|
||||
>
|
||||
<option disabled={true}>请选择小部件</option>
|
||||
{
|
||||
widgets.map(item => {
|
||||
return (<option value={item.id}>{item.name}</option>)
|
||||
})
|
||||
}
|
||||
</select>
|
||||
<p className="fieldset-label">你可以选择一款你喜欢的小部件</p>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="fieldset w-full">
|
||||
<legend className="fieldset-legend">配置信息</legend>
|
||||
<textarea
|
||||
value={data}
|
||||
onChange={(e) => setData(e.target.value)}
|
||||
className="textarea h-24 w-full"
|
||||
placeholder="请输入 JSON 格式的配置信息"
|
||||
></textarea>
|
||||
|
||||
<p className="fieldset-label">JSON格式的配置信息</p>
|
||||
</fieldset>
|
||||
<button className="btn w-full btn-primary">确认</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
114
components/Draggable/Draggable.tsx
Normal file
114
components/Draggable/Draggable.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
import "@/app/globals.css";
|
||||
import _ from "lodash";
|
||||
|
||||
import { useDraggable } from "@dnd-kit/core";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { nearestMultiple } from "./utils";
|
||||
import PreviewStore from "@/stores/previewStore";
|
||||
import { useWidgets } from "@/hooks/useWidgets";
|
||||
|
||||
export default function Draggable(props: DraggablePropsType) {
|
||||
const targetRef = useRef<HTMLDivElement>(null);
|
||||
const timerRef = useRef<NodeJS.Timeout>(null);
|
||||
const [size, setSize] = useState({ width: 16, height: 16 });
|
||||
const { id, widgetsId, data, x, y, width:_width, height:_height } = props;
|
||||
const [width, setWidth] = useState(_width);
|
||||
const [height, setHeight] = useState(_height);
|
||||
const {widgets, widgetsLibrary} = useWidgets();
|
||||
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
||||
id,
|
||||
data: props
|
||||
});
|
||||
|
||||
const style = transform
|
||||
? {
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
opacity: 0.6,
|
||||
}
|
||||
: {};
|
||||
useEffect(() => {
|
||||
const element = targetRef.current;
|
||||
if (!element) return;
|
||||
|
||||
const observer = new ResizeObserver(
|
||||
_.throttle((entries: any) => {
|
||||
for (const entry of entries) {
|
||||
const { width, height } = entry.contentRect;
|
||||
setSize({
|
||||
width: nearestMultiple(width),
|
||||
height: nearestMultiple(height),
|
||||
});
|
||||
const x = nearestMultiple(element.offsetLeft);
|
||||
const y = nearestMultiple(element.offsetTop);
|
||||
PreviewStore.changePreviewX(x);
|
||||
PreviewStore.changePreviewY(y);
|
||||
PreviewStore.changePreviewWidth(nearestMultiple(width));
|
||||
PreviewStore.changePreviewHeight(nearestMultiple(height));
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
timerRef.current = setTimeout(() => {
|
||||
setWidth(nearestMultiple(width));
|
||||
setHeight(nearestMultiple(height));
|
||||
PreviewStore.clearPreview();
|
||||
}, 500);
|
||||
}
|
||||
}, 300),
|
||||
);
|
||||
|
||||
observer.observe(element);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const className = transform ? "shadow-xl absolute w-min min-w-[128px] min-h-[128px]" : "absolute w-min resize overflow-hidden min-w-[128px] min-h-[128px]";
|
||||
|
||||
return (
|
||||
<div className={className} ref={(el) => {
|
||||
setNodeRef(el); // DnD 的 ref
|
||||
(targetRef as React.MutableRefObject<HTMLDivElement | null>).current = el;
|
||||
}} style={{
|
||||
top: y,
|
||||
left: x,
|
||||
width: width,
|
||||
height: height,
|
||||
...style,
|
||||
}} {...attributes}>
|
||||
<button
|
||||
className="btn absolute top-1 right-1 z-40 btn-square btn-soft"
|
||||
{...listeners}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-6 pointer-events-none"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3.75 9h16.5m-16.5 6.75h16.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{widgetsLibrary[widgetsId] && widgetsLibrary[widgetsId](data ?? {})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type DraggablePropsType = {
|
||||
id: string;
|
||||
widgetsId: string;
|
||||
data?: Record<string, unknown>;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
14
components/Draggable/Droppable.tsx
Normal file
14
components/Draggable/Droppable.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
import "@/app/globals.css";
|
||||
|
||||
export default function Droppable() {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-gray-300 text-5xl font-bold text-gray-900">
|
||||
<span className="whitespace-nowrap md:whitespace-normal">NEXUSHUB</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type DroppablePropsType = {
|
||||
|
||||
}
|
||||
47
components/Draggable/Preview.tsx
Normal file
47
components/Draggable/Preview.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
import "@/app/globals.css";
|
||||
import { CSSProperties, useEffect, useState } from "react";
|
||||
|
||||
export default function Preview(props: PreviewPropsType) {
|
||||
const {x, y, width, height} = props;
|
||||
const [style, setStyle] = useState<CSSProperties>({
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setStyle({
|
||||
top: y,
|
||||
left: x,
|
||||
width: width,
|
||||
height: height,
|
||||
display: width + height + x + y === 0 ? 'none' : 'block'
|
||||
});
|
||||
}, [props.height, props.width, props.x, props.y])
|
||||
return (
|
||||
<div
|
||||
className="absolute border-2 z-50 border-emerald-500 bg-gradient-to-br from-emerald-100/30 to-cyan-100/30 backdrop-blur-[2px] rounded-lg shadow-lg shadow-emerald-200/50 animate-pulse"
|
||||
style={style}
|
||||
>
|
||||
<span
|
||||
className="absolute text-2xl bg-emerald-500 border-emerald-500 text-white top-0 p-0.5"
|
||||
>
|
||||
{width + height === 0 || `${width} * ${height}`}
|
||||
</span>
|
||||
<span
|
||||
className="absolute text-2xl bg-emerald-500 border-emerald-500 text-white bottom-0 p-0.5"
|
||||
>
|
||||
{x + y === 0 || `${x}, ${y}`}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type PreviewPropsType = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
0
components/Draggable/hooks/use
Normal file
0
components/Draggable/hooks/use
Normal file
0
components/Draggable/index.tsx
Normal file
0
components/Draggable/index.tsx
Normal file
9
components/Draggable/utils/index.ts
Normal file
9
components/Draggable/utils/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 计算最接近的 x 的倍数
|
||||
* @param n 输入的数字
|
||||
* @param [x=16] 推荐使用偶数
|
||||
* @returns 最近的 x 的倍数
|
||||
*/
|
||||
export function nearestMultiple(n: number, x: number = 18): number {
|
||||
return Math.floor((n + x/2) / x) * x;
|
||||
}
|
||||
43
components/DraggableOld.tsx
Normal file
43
components/DraggableOld.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
import { useDraggable } from "@dnd-kit/core";
|
||||
|
||||
export function Draggable(props) {
|
||||
const { id } = props;
|
||||
const { attributes, listeners, setNodeRef, transform, isDragging } =
|
||||
useDraggable({
|
||||
id,
|
||||
});
|
||||
const style = transform
|
||||
? {
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
opacity: 0.6,
|
||||
}
|
||||
: undefined;
|
||||
const className = transform ? "z-30 shadow-xl relative w-min " : "z-10 relative w-min";
|
||||
|
||||
return (
|
||||
<div className={className} ref={setNodeRef} style={style} {...attributes}>
|
||||
<button
|
||||
className="btn absolute top-1 right-1 z-20 btn-square btn-soft"
|
||||
{...listeners}
|
||||
onMouseDown={(e) => e.stopPropagation()} // 阻止事件冒泡
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3.75 9h16.5m-16.5 6.75h16.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
69
components/DraggablePanel.tsx
Normal file
69
components/DraggablePanel.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
import classnames from "classnames";
|
||||
import { DndContext } from "@dnd-kit/core";
|
||||
import { Droppable } from "./Droppable";
|
||||
import PreviewStore from "@/stores/previewStore";
|
||||
import ComponentStore from '@/stores/componentStore';
|
||||
import Preview from "./Draggable/Preview";
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
import { nearestMultiple } from "./Draggable/utils";
|
||||
import { ReactElement } from "react";
|
||||
|
||||
export default function DraggablePanel(props: DraggablePanelType) {
|
||||
const { draggable, wdith = "full", height = "full", children } = props;
|
||||
|
||||
const draggablePanelClass = classnames(
|
||||
`relative h-${height} w-${wdith}`,
|
||||
{
|
||||
"h-full": height === "full" || height === "100%",
|
||||
"w-full": wdith === "full" || wdith === "100%",
|
||||
"base-200": draggable, // 当 active 为 true 时添加
|
||||
"base-100": !draggable, // 当 disabled 为 true 时添加
|
||||
},
|
||||
);
|
||||
return useObserver(() => (
|
||||
<DndContext
|
||||
onDragMove={(event) => {
|
||||
const node = event?.activatorEvent?.target?.parentNode;
|
||||
const rect = node.getBoundingClientRect();
|
||||
const { width, height } = rect;
|
||||
const x = nearestMultiple(node.offsetLeft + event.delta.x);
|
||||
const y = nearestMultiple(node.offsetTop + event.delta.y);
|
||||
PreviewStore.changePreviewX(x);
|
||||
PreviewStore.changePreviewY(y);
|
||||
PreviewStore.changePreviewWidth(nearestMultiple(width));
|
||||
PreviewStore.changePreviewHeight(nearestMultiple(height));
|
||||
}}
|
||||
onDragEnd={(event) => {
|
||||
const node = event?.activatorEvent?.target?.parentNode;
|
||||
const rect = node.getBoundingClientRect();
|
||||
const { width, height } = rect;
|
||||
const x = nearestMultiple(node.offsetLeft + event.delta.x);
|
||||
const y = nearestMultiple(node.offsetTop + event.delta.y);
|
||||
PreviewStore.clearPreview();
|
||||
ComponentStore.changeComponent({
|
||||
id: event?.active?.data?.current?.id,
|
||||
widgetsId: event?.active?.data?.current?.widgetsId,
|
||||
x,
|
||||
y,
|
||||
width: nearestMultiple(width),
|
||||
height: nearestMultiple(height),
|
||||
});
|
||||
}}>
|
||||
<Droppable>
|
||||
<div className={draggablePanelClass}>
|
||||
<Preview x={PreviewStore.x} y={PreviewStore.y} width={PreviewStore.width} height={PreviewStore.height} />
|
||||
{children}
|
||||
</div>
|
||||
</Droppable>
|
||||
</DndContext>
|
||||
));
|
||||
}
|
||||
|
||||
export type DraggablePanelType = {
|
||||
draggable: boolean;
|
||||
wdith?: number | string;
|
||||
height?: number | string;
|
||||
children: any;
|
||||
// grid: boolean;
|
||||
};
|
||||
124
components/DraggableWidget.tsx
Normal file
124
components/DraggableWidget.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
import { ReactElement, useEffect, useRef, useState } from "react";
|
||||
import classnames from "classnames";
|
||||
import _ from "lodash";
|
||||
import { Draggable } from "./Draggable";
|
||||
|
||||
export default function DraggableWidget(props: DraggableWidgetType) {
|
||||
const { id, draggable, children, x, y, w, h } = props;
|
||||
const targetRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [size, setSize] = useState({ width: 16, height: 16 });
|
||||
const [isResize, setIsResize] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const element = targetRef.current;
|
||||
if (!element) return;
|
||||
|
||||
// 创建 ResizeObserver 实例
|
||||
let timer: any;
|
||||
const observer = new ResizeObserver(
|
||||
_.throttle((entries: any) => {
|
||||
for (const entry of entries) {
|
||||
const { width, height } = entry.contentRect;
|
||||
setSize({
|
||||
width: nearestMultipleOf16(width),
|
||||
height: nearestMultipleOf16(height),
|
||||
});
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
setIsResize(true);
|
||||
timer = setTimeout(() => {
|
||||
entry.target.style.width = nearestMultipleOf16(width) + "px";
|
||||
entry.target.style.height = nearestMultipleOf16(height) + "px";
|
||||
// syncSize(entry.contentRect, containerRef.current);
|
||||
setIsResize(false);
|
||||
}, 150);
|
||||
}
|
||||
}, 30),
|
||||
);
|
||||
|
||||
const syncSize = (contentRect, containerNode) => {
|
||||
const { x, y, height, width } = contentRect;
|
||||
const area = {
|
||||
rowStart: x / 16 + 1,
|
||||
columnStart: y / 16 + 1,
|
||||
rowEnd: width / 16 + x / 16 + 1,
|
||||
columnEnd: height / 16 + y / 16 + 1,
|
||||
};
|
||||
containerNode.style.gridArea = `${area.rowStart} / ${area.columnStart} / ${area.rowEnd} / ${area.columnEnd}`;
|
||||
};
|
||||
|
||||
// 开始观察元素
|
||||
observer.observe(element);
|
||||
|
||||
// 组件卸载时断开连接
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []); // 空依赖数组确保只运行一次
|
||||
|
||||
const draggableWidgetClass = classnames(
|
||||
"block w-full h-full max-h-[1048px] max-w-[1888px] cursor-pointer overflow-hidden",
|
||||
{
|
||||
"hover:bg-red-200 hover:outline-2 hover:outline-dashed hover:outline-gray-300 hover:opacity-30 resize":
|
||||
draggable,
|
||||
},
|
||||
);
|
||||
|
||||
const draggableWidgetIndicatorBoxClass = classnames(
|
||||
"absolute top-0 left-0 outline-2 outline-dashed outline-red-500 z-50 pointer-events-none",
|
||||
{
|
||||
invisible: !isResize,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<Draggable id={id}>
|
||||
<div
|
||||
className="relative w-min"
|
||||
ref={containerRef}
|
||||
style={{
|
||||
width: w,
|
||||
height: h,
|
||||
top: y,
|
||||
left: x
|
||||
}}
|
||||
>
|
||||
<div className={draggableWidgetClass} ref={targetRef}>
|
||||
{children}
|
||||
</div>
|
||||
<div
|
||||
className={draggableWidgetIndicatorBoxClass}
|
||||
style={{
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
}}
|
||||
>{`${size.width}x${size.height}`}</div>
|
||||
</div>
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
export type DraggableWidgetType = {
|
||||
id: string;
|
||||
draggable: boolean;
|
||||
wdith?: number | string;
|
||||
height?: number | string;
|
||||
children: ReactElement;
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算最接近的 16 的倍数
|
||||
* @param n 输入的数字
|
||||
* @returns 最近的 16 的倍数
|
||||
*/
|
||||
function nearestMultipleOf16(n: number): number {
|
||||
return Math.floor((n + 8) / 16) * 16;
|
||||
}
|
||||
18
components/Droppable.tsx
Normal file
18
components/Droppable.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import {useDroppable} from '@dnd-kit/core';
|
||||
|
||||
export function Droppable(props) {
|
||||
const {isOver, setNodeRef} = useDroppable({
|
||||
id: 'droppable',
|
||||
});
|
||||
const style = {
|
||||
color: isOver ? 'green' : undefined,
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
68
hooks/useDynamicWidgets.ts
Normal file
68
hooks/useDynamicWidgets.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
export interface WidgetModule {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
component: ReturnType<typeof dynamic>;
|
||||
}
|
||||
|
||||
export const useDynamicWidgets = (): {
|
||||
widgets: WidgetModule[];
|
||||
refreshWidgets: () => Promise<void>;
|
||||
} => {
|
||||
const [widgets, setWidgets] = useState<WidgetModule[]>([]);
|
||||
|
||||
const loadWidgets = useCallback(async () => {
|
||||
try {
|
||||
// Webpack 的 require.context 匹配 widgets 目录
|
||||
const context = require.context('../widgets', true, /\/index\.tsx$/);
|
||||
console.log("[DEBUG] Matched files:", context.keys());
|
||||
|
||||
|
||||
|
||||
const modules = await Promise.all(
|
||||
context.keys().map(async (key) => {
|
||||
const folderName = key.split('/')[1];
|
||||
|
||||
// 动态导入元数据
|
||||
console.log("[DEBUG] import files:", `../widgets/${folderName}/index.tsx`);
|
||||
|
||||
const meta = await import(`../widgets/${folderName}/index.tsx`);
|
||||
|
||||
// 创建动态组件(单独导入确保 Tree Shaking)
|
||||
const Component = dynamic(
|
||||
() => import(`../widgets/${folderName}/index.tsx`),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
return {
|
||||
id: meta.id,
|
||||
name: meta.name,
|
||||
version: meta.version,
|
||||
component: Component
|
||||
};
|
||||
})
|
||||
);
|
||||
console.log(modules);
|
||||
|
||||
setWidgets(modules);
|
||||
} catch (error) {
|
||||
console.error('Failed to load widgets:', error);
|
||||
setWidgets([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 初始化加载
|
||||
useEffect(() => {
|
||||
loadWidgets();
|
||||
}, [loadWidgets]);
|
||||
|
||||
// 暴露刷新方法
|
||||
const refreshWidgets = useCallback(async () => {
|
||||
await loadWidgets();
|
||||
}, [loadWidgets]);
|
||||
|
||||
return { widgets, refreshWidgets };
|
||||
};
|
||||
110
hooks/useWidgets.tsx
Normal file
110
hooks/useWidgets.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
import { useState, useCallback, useEffect, ReactNode } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { DraggablePropsType } from '@/components/Draggable/Draggable';
|
||||
import * as Logo from '@/widgets/Logo/index';
|
||||
import * as Text from '@/widgets/Text/index';
|
||||
import * as Image from '@/widgets/Image/index';
|
||||
export interface WidgetModule {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
defaultConfig: (data: Record<string, unknown>) => Record<string, unknown>
|
||||
}
|
||||
|
||||
export type widgetsLibraryType = Record<string, (data?:DraggablePropsType['data']) => ReactNode>;
|
||||
|
||||
const widgetPaths = [
|
||||
'@/widgets/Logo/index',
|
||||
'@/widgets/Text/index',
|
||||
'@/widgets/Image/index',
|
||||
];
|
||||
|
||||
export const useWidgets = (): {
|
||||
widgets: WidgetModule[];
|
||||
widgetsLibrary: widgetsLibraryType;
|
||||
refreshWidgets: () => Promise<void>;
|
||||
} => {
|
||||
const [widgets, setWidgets] = useState<WidgetModule[]>([
|
||||
{
|
||||
id: 'logo',
|
||||
name: 'Logo',
|
||||
version: '1.0.0',
|
||||
defaultConfig: Logo.defaultConfig
|
||||
},
|
||||
{
|
||||
id: 'text',
|
||||
name: '文本',
|
||||
version: '1.0.0',
|
||||
defaultConfig: Text.defaultConfig
|
||||
},
|
||||
{
|
||||
id: 'image',
|
||||
name: '图片',
|
||||
version: '1.0.0',
|
||||
defaultConfig: Image.defaultConfig
|
||||
|
||||
},
|
||||
]);
|
||||
const [widgetsLibrary, setWidgetsLibrary] = useState<widgetsLibraryType>({
|
||||
'logo': (data) => {
|
||||
const { default:Component, defaultConfig } = Logo;
|
||||
|
||||
return <Component {...defaultConfig(data)} />
|
||||
},
|
||||
'text': (data) => {
|
||||
const { default:Component, defaultConfig } = Text;
|
||||
return <Component data={defaultConfig(data)} />
|
||||
},
|
||||
'image': (data) => {
|
||||
const { default:Component, defaultConfig } = Image;
|
||||
return <Component data={defaultConfig(data)} />
|
||||
}
|
||||
});
|
||||
|
||||
const loadWidgets = useCallback(async () => {
|
||||
try {
|
||||
// Webpack 的 require.context 匹配 widgets 目录
|
||||
console.log(window);
|
||||
// const modules = await Promise.all(
|
||||
// widgetPaths.map(async (key) => {
|
||||
// console.log(key);
|
||||
// debugger
|
||||
// const meta = await require(key);
|
||||
|
||||
|
||||
// // 创建动态组件(单独导入确保 Tree Shaking)
|
||||
// // const Component = dynamic(
|
||||
// // () => import(key),
|
||||
// // { ssr: false }
|
||||
// // );
|
||||
|
||||
// return {
|
||||
// id: meta.id,
|
||||
// name: meta.name,
|
||||
// version: meta.version,
|
||||
// // component: Component
|
||||
// };
|
||||
// })
|
||||
// );
|
||||
// console.log(modules);
|
||||
|
||||
// setWidgets(modules);
|
||||
} catch (error) {
|
||||
console.error('Failed to load widgets:', error);
|
||||
setWidgets([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 初始化加载
|
||||
// useEffect(() => {
|
||||
// loadWidgets();
|
||||
// }, [loadWidgets]);
|
||||
|
||||
// 暴露刷新方法
|
||||
const refreshWidgets = useCallback(async () => {
|
||||
await loadWidgets();
|
||||
}, [loadWidgets]);
|
||||
|
||||
return { widgets, widgetsLibrary, refreshWidgets };
|
||||
};
|
||||
@@ -1,7 +1,17 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
// next.config.js
|
||||
const nextConfig:NextConfig = {
|
||||
// 移除自定义 webpack 配置,Next.js 已内置 TypeScript 支持
|
||||
// 保留以下配置即可
|
||||
experimental: {
|
||||
externalDir: true, // 如果需要引用外部目录
|
||||
},
|
||||
webpack: (config) => {
|
||||
// 保留其他必要配置
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
23
package.json
23
package.json
@@ -9,19 +9,30 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"classnames": "^2.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"mobx": "^6.13.7",
|
||||
"mobx-react-lite": "^4.1.0",
|
||||
"next": "15.2.3",
|
||||
"next-auth": "5.0.0-beta.25",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"next": "15.2.3"
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4.0.14",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4",
|
||||
"daisyui": "^5.0.6",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.2.3",
|
||||
"@eslint/eslintrc": "^3"
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"tailwindcss": "^4.0.14",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
293
pnpm-lock.yaml
generated
293
pnpm-lock.yaml
generated
@@ -8,9 +8,30 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@dnd-kit/core':
|
||||
specifier: ^6.3.1
|
||||
version: 6.3.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@heroicons/react':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0(react@19.0.0)
|
||||
classnames:
|
||||
specifier: ^2.5.1
|
||||
version: 2.5.1
|
||||
lodash:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
mobx:
|
||||
specifier: ^6.13.7
|
||||
version: 6.13.7
|
||||
mobx-react-lite:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0(mobx@6.13.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
next:
|
||||
specifier: 15.2.3
|
||||
version: 15.2.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
next-auth:
|
||||
specifier: 5.0.0-beta.25
|
||||
version: 5.0.0-beta.25(next@15.2.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.0.0
|
||||
@@ -22,8 +43,11 @@ importers:
|
||||
specifier: ^3
|
||||
version: 3.3.0
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4
|
||||
specifier: ^4.0.14
|
||||
version: 4.0.14
|
||||
'@types/lodash':
|
||||
specifier: ^4.17.16
|
||||
version: 4.17.16
|
||||
'@types/node':
|
||||
specifier: ^20
|
||||
version: 20.17.24
|
||||
@@ -33,14 +57,23 @@ importers:
|
||||
'@types/react-dom':
|
||||
specifier: ^19
|
||||
version: 19.0.4(@types/react@19.0.11)
|
||||
daisyui:
|
||||
specifier: ^5.0.6
|
||||
version: 5.0.6
|
||||
eslint:
|
||||
specifier: ^9
|
||||
version: 9.22.0(jiti@2.4.2)
|
||||
eslint-config-next:
|
||||
specifier: 15.2.3
|
||||
version: 15.2.3(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
|
||||
prettier:
|
||||
specifier: ^3.5.3
|
||||
version: 3.5.3
|
||||
prettier-plugin-tailwindcss:
|
||||
specifier: ^0.6.11
|
||||
version: 0.6.11(prettier@3.5.3)
|
||||
tailwindcss:
|
||||
specifier: ^4
|
||||
specifier: ^4.0.14
|
||||
version: 4.0.14
|
||||
typescript:
|
||||
specifier: ^5
|
||||
@@ -52,6 +85,36 @@ packages:
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@auth/core@0.37.2':
|
||||
resolution: {integrity: sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw==}
|
||||
peerDependencies:
|
||||
'@simplewebauthn/browser': ^9.0.1
|
||||
'@simplewebauthn/server': ^9.0.2
|
||||
nodemailer: ^6.8.0
|
||||
peerDependenciesMeta:
|
||||
'@simplewebauthn/browser':
|
||||
optional: true
|
||||
'@simplewebauthn/server':
|
||||
optional: true
|
||||
nodemailer:
|
||||
optional: true
|
||||
|
||||
'@dnd-kit/accessibility@3.1.1':
|
||||
resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
|
||||
'@dnd-kit/core@6.3.1':
|
||||
resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@dnd-kit/utilities@3.2.2':
|
||||
resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
|
||||
'@emnapi/core@1.3.1':
|
||||
resolution: {integrity: sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==}
|
||||
|
||||
@@ -99,6 +162,11 @@ packages:
|
||||
resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@heroicons/react@2.2.0':
|
||||
resolution: {integrity: sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==}
|
||||
peerDependencies:
|
||||
react: '>= 16 || ^19.0.0-rc'
|
||||
|
||||
'@humanfs/core@0.19.1':
|
||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
@@ -297,6 +365,9 @@ packages:
|
||||
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
|
||||
engines: {node: '>=12.4.0'}
|
||||
|
||||
'@panva/hkdf@1.2.1':
|
||||
resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
|
||||
|
||||
'@rtsao/scc@1.1.0':
|
||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||
|
||||
@@ -388,6 +459,9 @@ packages:
|
||||
'@tybys/wasm-util@0.9.0':
|
||||
resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==}
|
||||
|
||||
'@types/cookie@0.6.0':
|
||||
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||
|
||||
'@types/estree@1.0.6':
|
||||
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
|
||||
|
||||
@@ -397,6 +471,9 @@ packages:
|
||||
'@types/json5@0.0.29':
|
||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||
|
||||
'@types/lodash@4.17.16':
|
||||
resolution: {integrity: sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==}
|
||||
|
||||
'@types/node@20.17.24':
|
||||
resolution: {integrity: sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==}
|
||||
|
||||
@@ -625,6 +702,9 @@ packages:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
classnames@2.5.1:
|
||||
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
|
||||
|
||||
client-only@0.0.1:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
|
||||
@@ -645,6 +725,10 @@ packages:
|
||||
concat-map@0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
cookie@0.7.1:
|
||||
resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -652,6 +736,9 @@ packages:
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
daisyui@5.0.6:
|
||||
resolution: {integrity: sha512-/e/9Gw/2y9oawBJlWkJMSEhRXdmfOLvcPl+6q/x2rPEdIVOtebs1t3ex2vwySl9vCRs1GGNBKCiL+P60Ps/wUw==}
|
||||
|
||||
damerau-levenshtein@1.0.8:
|
||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||
|
||||
@@ -1127,6 +1214,9 @@ packages:
|
||||
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
||||
hasBin: true
|
||||
|
||||
jose@5.10.0:
|
||||
resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==}
|
||||
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
@@ -1236,6 +1326,9 @@ packages:
|
||||
lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
|
||||
lodash@4.17.21:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
hasBin: true
|
||||
@@ -1262,6 +1355,22 @@ packages:
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
mobx-react-lite@4.1.0:
|
||||
resolution: {integrity: sha512-QEP10dpHHBeQNv1pks3WnHRCem2Zp636lq54M2nKO2Sarr13pL4u6diQXf65yzXUn0mkk18SyIDCm9UOJYTi1w==}
|
||||
peerDependencies:
|
||||
mobx: ^6.9.0
|
||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
|
||||
mobx@6.13.7:
|
||||
resolution: {integrity: sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
@@ -1273,6 +1382,22 @@ packages:
|
||||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
next-auth@5.0.0-beta.25:
|
||||
resolution: {integrity: sha512-2dJJw1sHQl2qxCrRk+KTQbeH+izFbGFPuJj5eGgBZFYyiYYtvlrBeUw1E/OJJxTRjuxbSYGnCTkUIRsIIW0bog==}
|
||||
peerDependencies:
|
||||
'@simplewebauthn/browser': ^9.0.1
|
||||
'@simplewebauthn/server': ^9.0.2
|
||||
next: ^14.0.0-0 || ^15.0.0-0
|
||||
nodemailer: ^6.6.5
|
||||
react: ^18.2.0 || ^19.0.0-0
|
||||
peerDependenciesMeta:
|
||||
'@simplewebauthn/browser':
|
||||
optional: true
|
||||
'@simplewebauthn/server':
|
||||
optional: true
|
||||
nodemailer:
|
||||
optional: true
|
||||
|
||||
next@15.2.3:
|
||||
resolution: {integrity: sha512-x6eDkZxk2rPpu46E1ZVUWIBhYCLszmUY6fvHBFcbzJ9dD+qRX6vcHusaqqDlnY+VngKzKbAiG2iRCkPbmi8f7w==}
|
||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||
@@ -1294,6 +1419,9 @@ packages:
|
||||
sass:
|
||||
optional: true
|
||||
|
||||
oauth4webapi@3.3.1:
|
||||
resolution: {integrity: sha512-ZwX7UqYrP3Lr+Glhca3a1/nF2jqf7VVyJfhGuW5JtrfDUxt0u+IoBPzFjZ2dd7PJGkdM6CFPVVYzuDYKHv101A==}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1380,10 +1508,81 @@ packages:
|
||||
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
preact-render-to-string@5.2.3:
|
||||
resolution: {integrity: sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==}
|
||||
peerDependencies:
|
||||
preact: '>=10'
|
||||
|
||||
preact@10.11.3:
|
||||
resolution: {integrity: sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==}
|
||||
|
||||
prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
prettier-plugin-tailwindcss@0.6.11:
|
||||
resolution: {integrity: sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
peerDependencies:
|
||||
'@ianvs/prettier-plugin-sort-imports': '*'
|
||||
'@prettier/plugin-pug': '*'
|
||||
'@shopify/prettier-plugin-liquid': '*'
|
||||
'@trivago/prettier-plugin-sort-imports': '*'
|
||||
'@zackad/prettier-plugin-twig': '*'
|
||||
prettier: ^3.0
|
||||
prettier-plugin-astro: '*'
|
||||
prettier-plugin-css-order: '*'
|
||||
prettier-plugin-import-sort: '*'
|
||||
prettier-plugin-jsdoc: '*'
|
||||
prettier-plugin-marko: '*'
|
||||
prettier-plugin-multiline-arrays: '*'
|
||||
prettier-plugin-organize-attributes: '*'
|
||||
prettier-plugin-organize-imports: '*'
|
||||
prettier-plugin-sort-imports: '*'
|
||||
prettier-plugin-style-order: '*'
|
||||
prettier-plugin-svelte: '*'
|
||||
peerDependenciesMeta:
|
||||
'@ianvs/prettier-plugin-sort-imports':
|
||||
optional: true
|
||||
'@prettier/plugin-pug':
|
||||
optional: true
|
||||
'@shopify/prettier-plugin-liquid':
|
||||
optional: true
|
||||
'@trivago/prettier-plugin-sort-imports':
|
||||
optional: true
|
||||
'@zackad/prettier-plugin-twig':
|
||||
optional: true
|
||||
prettier-plugin-astro:
|
||||
optional: true
|
||||
prettier-plugin-css-order:
|
||||
optional: true
|
||||
prettier-plugin-import-sort:
|
||||
optional: true
|
||||
prettier-plugin-jsdoc:
|
||||
optional: true
|
||||
prettier-plugin-marko:
|
||||
optional: true
|
||||
prettier-plugin-multiline-arrays:
|
||||
optional: true
|
||||
prettier-plugin-organize-attributes:
|
||||
optional: true
|
||||
prettier-plugin-organize-imports:
|
||||
optional: true
|
||||
prettier-plugin-sort-imports:
|
||||
optional: true
|
||||
prettier-plugin-style-order:
|
||||
optional: true
|
||||
prettier-plugin-svelte:
|
||||
optional: true
|
||||
|
||||
prettier@3.5.3:
|
||||
resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
pretty-format@3.8.0:
|
||||
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
|
||||
|
||||
prop-types@15.8.1:
|
||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||
|
||||
@@ -1632,6 +1831,11 @@ packages:
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
use-sync-external-store@1.4.0:
|
||||
resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1665,6 +1869,34 @@ snapshots:
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@auth/core@0.37.2':
|
||||
dependencies:
|
||||
'@panva/hkdf': 1.2.1
|
||||
'@types/cookie': 0.6.0
|
||||
cookie: 0.7.1
|
||||
jose: 5.10.0
|
||||
oauth4webapi: 3.3.1
|
||||
preact: 10.11.3
|
||||
preact-render-to-string: 5.2.3(preact@10.11.3)
|
||||
|
||||
'@dnd-kit/accessibility@3.1.1(react@19.0.0)':
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/core@6.3.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@dnd-kit/accessibility': 3.1.1(react@19.0.0)
|
||||
'@dnd-kit/utilities': 3.2.2(react@19.0.0)
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/utilities@3.2.2(react@19.0.0)':
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@emnapi/core@1.3.1':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.0.1
|
||||
@@ -1725,6 +1957,10 @@ snapshots:
|
||||
'@eslint/core': 0.12.0
|
||||
levn: 0.4.1
|
||||
|
||||
'@heroicons/react@2.2.0(react@19.0.0)':
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@humanfs/node@0.16.6':
|
||||
@@ -1864,6 +2100,8 @@ snapshots:
|
||||
|
||||
'@nolyfill/is-core-module@1.0.39': {}
|
||||
|
||||
'@panva/hkdf@1.2.1': {}
|
||||
|
||||
'@rtsao/scc@1.1.0': {}
|
||||
|
||||
'@rushstack/eslint-patch@1.11.0': {}
|
||||
@@ -1941,12 +2179,16 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@types/cookie@0.6.0': {}
|
||||
|
||||
'@types/estree@1.0.6': {}
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/json5@0.0.29': {}
|
||||
|
||||
'@types/lodash@4.17.16': {}
|
||||
|
||||
'@types/node@20.17.24':
|
||||
dependencies:
|
||||
undici-types: 6.19.8
|
||||
@@ -2214,6 +2456,8 @@ snapshots:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
classnames@2.5.1: {}
|
||||
|
||||
client-only@0.0.1: {}
|
||||
|
||||
color-convert@2.0.1:
|
||||
@@ -2236,6 +2480,8 @@ snapshots:
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
|
||||
cookie@0.7.1: {}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
@@ -2244,6 +2490,8 @@ snapshots:
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
daisyui@5.0.6: {}
|
||||
|
||||
damerau-levenshtein@1.0.8: {}
|
||||
|
||||
data-view-buffer@1.0.2:
|
||||
@@ -2882,6 +3130,8 @@ snapshots:
|
||||
|
||||
jiti@2.4.2: {}
|
||||
|
||||
jose@5.10.0: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-yaml@4.1.0:
|
||||
@@ -2971,6 +3221,8 @@ snapshots:
|
||||
|
||||
lodash.merge@4.6.2: {}
|
||||
|
||||
lodash@4.17.21: {}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
dependencies:
|
||||
js-tokens: 4.0.0
|
||||
@@ -2994,12 +3246,28 @@ snapshots:
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
mobx-react-lite@4.1.0(mobx@6.13.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
mobx: 6.13.7
|
||||
react: 19.0.0
|
||||
use-sync-external-store: 1.4.0(react@19.0.0)
|
||||
optionalDependencies:
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
|
||||
mobx@6.13.7: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
nanoid@3.3.10: {}
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
next-auth@5.0.0-beta.25(next@15.2.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
'@auth/core': 0.37.2
|
||||
next: 15.2.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
react: 19.0.0
|
||||
|
||||
next@15.2.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
'@next/env': 15.2.3
|
||||
@@ -3025,6 +3293,8 @@ snapshots:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
oauth4webapi@3.3.1: {}
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
object-inspect@1.13.4: {}
|
||||
@@ -3120,8 +3390,23 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
preact-render-to-string@5.2.3(preact@10.11.3):
|
||||
dependencies:
|
||||
preact: 10.11.3
|
||||
pretty-format: 3.8.0
|
||||
|
||||
preact@10.11.3: {}
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier-plugin-tailwindcss@0.6.11(prettier@3.5.3):
|
||||
dependencies:
|
||||
prettier: 3.5.3
|
||||
|
||||
prettier@3.5.3: {}
|
||||
|
||||
pretty-format@3.8.0: {}
|
||||
|
||||
prop-types@15.8.1:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
@@ -3459,6 +3744,10 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
use-sync-external-store@1.4.0(react@19.0.0):
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
dependencies:
|
||||
is-bigint: 1.1.0
|
||||
|
||||
69
stores/componentStore.ts
Normal file
69
stores/componentStore.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { DraggablePropsType } from "@/components/Draggable/Draggable";
|
||||
import { makeAutoObservable, reaction } from "mobx";
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
class ComponentsStore {
|
||||
components: DraggablePropsType[] = [];
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
this.loadFromLocalStorage(); // 初始化加载
|
||||
|
||||
// 监听 components 变化自动保存(防抖版)
|
||||
reaction(
|
||||
() => this.components.slice(), // 深度监听变化
|
||||
debounce(() => this.saveToLocalStorage(), 500)
|
||||
);
|
||||
}
|
||||
|
||||
// 初始化时从本地存储加载
|
||||
private loadFromLocalStorage() {
|
||||
if (window === undefined) {
|
||||
return
|
||||
}
|
||||
const data = localStorage.getItem("componentsStore");
|
||||
if (data) {
|
||||
try {
|
||||
this.components = JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse stored components", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 防抖保存到本地存储
|
||||
private saveToLocalStorage = debounce(() => {
|
||||
localStorage.setItem("componentsStore", JSON.stringify(this.components));
|
||||
}, 100);
|
||||
|
||||
// 以下方法会触发自动保存
|
||||
initComponent(componentsList: DraggablePropsType[]) {
|
||||
this.components = componentsList || [];
|
||||
}
|
||||
|
||||
changeComponent(updatedComponent: DraggablePropsType) {
|
||||
this.components = this.components.map(item =>
|
||||
item.id === updatedComponent.id ? updatedComponent : item
|
||||
);
|
||||
}
|
||||
|
||||
deleteComponent(targetComponent: DraggablePropsType) {
|
||||
this.components = this.components.filter(
|
||||
item => item.id !== targetComponent.id
|
||||
);
|
||||
}
|
||||
|
||||
addComponent(widgetsId: string, data:Record<string, unknown>) {
|
||||
this.components.push({
|
||||
id: String(this.components.length),
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
widgetsId,
|
||||
data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default new ComponentsStore();
|
||||
17
stores/counterStore.ts
Normal file
17
stores/counterStore.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
export class CounterStore {
|
||||
count = 0;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.count += 1;
|
||||
}
|
||||
|
||||
decrement() {
|
||||
this.count -= 1;
|
||||
}
|
||||
}
|
||||
43
stores/previewStore.ts
Normal file
43
stores/previewStore.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { DraggablePropsType } from "@/components/Draggable/Draggable";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
class PreviewStore {
|
||||
width: number = 0;
|
||||
height: number = 0;
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
changePreview(x: number, y: number, width: number, height: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
changePreviewX(x: number) {
|
||||
this.x = x;
|
||||
}
|
||||
changePreviewY(y: number) {
|
||||
this.y = y;
|
||||
}
|
||||
changePreviewWidth(width: number) {
|
||||
this.width = width;
|
||||
}
|
||||
changePreviewHeight(height: number) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
clearPreview() {
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default new PreviewStore();
|
||||
29
stores/storeContext.tsx
Normal file
29
stores/storeContext.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
// stores/storeContext.ts
|
||||
import { createContext, useContext } from "react";
|
||||
import { CounterStore } from "./counterStore";
|
||||
|
||||
const StoreContext = createContext<{
|
||||
counterStore: CounterStore;
|
||||
}>(null!);
|
||||
|
||||
export const useStores = () => useContext(StoreContext);
|
||||
|
||||
export const initializeStores = (initialData = {}) => {
|
||||
const counterStore = new CounterStore();
|
||||
|
||||
// 服务端预取数据注入
|
||||
if (initialData?.counterStore) {
|
||||
counterStore.count = initialData.counterStore.count;
|
||||
}
|
||||
|
||||
return { counterStore };
|
||||
};
|
||||
|
||||
export const StoreProvider = ({ children, initialData }) => {
|
||||
const stores = initializeStores(initialData);
|
||||
const { Provider } = StoreContext;
|
||||
return (
|
||||
<StoreContext.Provider value={stores}>{children}</StoreContext.Provider>
|
||||
);
|
||||
};
|
||||
7
types/plugin.d.ts
vendored
Normal file
7
types/plugin.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface CardPlugin {
|
||||
id: string; // 唯一标识
|
||||
displayName: string; // 显示名称
|
||||
component: React.ComponentType<{ config?: any }>; // 卡片组件
|
||||
configSchema?: any; // 配置的JSON Schema(用于后台配置界面)
|
||||
|
||||
}
|
||||
37
widgets/Image/index.tsx
Normal file
37
widgets/Image/index.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
import "@/app/globals.css";
|
||||
import { DraggablePropsType } from "@/components/Draggable/Draggable";
|
||||
// import Image from "next/Image";
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export const id = "image";
|
||||
export const name = "Image";
|
||||
export const version = "1.0.0";
|
||||
export const defaultConfig = (config: ImageData) => {
|
||||
return {
|
||||
src: 'https://www.todaybing.com/api/today',
|
||||
...config,
|
||||
}
|
||||
}
|
||||
|
||||
type ImageData = {
|
||||
src: string;
|
||||
customStyles?: CSSProperties; // 自定义CSS扩展
|
||||
};
|
||||
|
||||
interface ResponsiveImageProps {
|
||||
data: DraggablePropsType["data"] & ImageData;
|
||||
className?: string; // 外部容器类名
|
||||
}
|
||||
|
||||
|
||||
const WidgetsImage: React.FC<ResponsiveImageProps> = ({
|
||||
data,
|
||||
className
|
||||
}) => {
|
||||
return (
|
||||
<img className="w-full h-full object-cover" src={data.src} style={data.customStyles} />
|
||||
);
|
||||
}
|
||||
|
||||
export default WidgetsImage;
|
||||
21
widgets/Logo/index.tsx
Normal file
21
widgets/Logo/index.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
import "@/app/globals.css";
|
||||
import { DraggablePropsType } from "@/components/Draggable/Draggable";
|
||||
|
||||
export const id = "logo";
|
||||
export const name = "Logo";
|
||||
export const version = "1.0.0";
|
||||
export const defaultConfig = (config: DraggablePropsType['data']) => {
|
||||
return {
|
||||
...config
|
||||
}
|
||||
}
|
||||
export default function WidgetsLogo() {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center text-5xl bg-base-200 font-bold">
|
||||
<span className="whitespace-nowrap text-primary">
|
||||
NEXUSHUB
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
86
widgets/Text/index.tsx
Normal file
86
widgets/Text/index.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { DraggablePropsType } from '../../components/Draggable/Draggable';
|
||||
|
||||
export const id = "text";
|
||||
export const name = "文字";
|
||||
export const version = "1.0.0";
|
||||
export const defaultConfig = (config: FontData) => {
|
||||
return {
|
||||
textAlign: 'center',
|
||||
content: '默认文本',
|
||||
...config,
|
||||
}
|
||||
}
|
||||
type FontData = {
|
||||
content: string;
|
||||
fontSize?: number; // 字体大小(px)
|
||||
fontFamily?: string; // 字体类型
|
||||
color?: string; // 字体颜色
|
||||
fontWeight?: number; // 字重
|
||||
lineHeight?: number; // 行高比例
|
||||
textAlign?: 'left' | 'center' | 'right'; // 对齐方式
|
||||
letterSpacing?: number; // 字间距(px)
|
||||
customStyles?: CSSProperties; // 自定义CSS扩展
|
||||
};
|
||||
|
||||
interface ResponsiveTextProps {
|
||||
data: DraggablePropsType["data"] & FontData;
|
||||
className?: string; // 外部容器类名
|
||||
}
|
||||
|
||||
const WidgetsText: React.FC<ResponsiveTextProps> = ({
|
||||
data,
|
||||
className
|
||||
}) => {
|
||||
// 合并默认样式与传入样式
|
||||
const textStyles: CSSProperties = {
|
||||
fontSize: `${data.fontSize || 16}px`,
|
||||
fontFamily: data.fontFamily || 'Arial, sans-serif',
|
||||
color: data.color || '#333',
|
||||
fontWeight: data.fontWeight || 400,
|
||||
lineHeight: data.lineHeight ? `${data.lineHeight}em` : '1.5',
|
||||
textAlign: data.textAlign || 'left',
|
||||
letterSpacing: `${data.letterSpacing || 0}px`,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: data.textAlign === 'center' ? 'center' :
|
||||
data.textAlign === 'right' ? 'flex-end' : 'flex-start',
|
||||
...data.customStyles
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
minWidth: '50px', // 防止容器坍缩
|
||||
minHeight: '1em' // 最小高度保障
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
...textStyles,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
padding: '0.2em' // 防止文本贴边
|
||||
}}
|
||||
title={data.content} // 添加tooltip
|
||||
>
|
||||
{data.content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WidgetsText;
|
||||
Reference in New Issue
Block a user