feat: Added frontend for The Catalyst tab #10
4176
app/dist/assets/index-Cs3AYztb.js
vendored
4176
app/dist/assets/index-Cs3AYztb.js
vendored
File diff suppressed because one or more lines are too long
1
app/dist/assets/index-J4PPDOa9.css
vendored
1
app/dist/assets/index-J4PPDOa9.css
vendored
File diff suppressed because one or more lines are too long
4
app/dist/index.html
vendored
4
app/dist/index.html
vendored
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Velocity WebOS</title>
|
||||
<script type="module" crossorigin src="./assets/index-Cs3AYztb.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-J4PPDOa9.css">
|
||||
<script type="module" crossorigin src="./assets/index-DWXNQJq4.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-y9vEdTGy.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
0
app/node_modules/.bin/tsc
generated
vendored
Normal file → Executable file
0
app/node_modules/.bin/tsc
generated
vendored
Normal file → Executable file
0
app/node_modules/.bin/vite
generated
vendored
Normal file → Executable file
0
app/node_modules/.bin/vite
generated
vendored
Normal file → Executable file
106
app/node_modules/.package-lock.json
generated
vendored
106
app/node_modules/.package-lock.json
generated
vendored
@@ -1835,18 +1835,18 @@
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -3922,32 +3922,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz",
|
||||
"integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz",
|
||||
"integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz",
|
||||
"integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@standard-schema/utils": {
|
||||
@@ -5008,6 +4994,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.48.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz",
|
||||
@@ -5777,6 +5776,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -6961,6 +6975,44 @@
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz",
|
||||
"integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz",
|
||||
"integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-smooth": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
|
||||
@@ -7291,6 +7343,12 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
||||
2
app/node_modules/.tmp/tsconfig.app.tsbuildinfo
generated
vendored
2
app/node_modules/.tmp/tsconfig.app.tsbuildinfo
generated
vendored
@@ -1 +1 @@
|
||||
{"root":["../../src/app.tsx","../../src/main.tsx","../../src/app/oracle/page.tsx","../../src/components/layout/loginscreen.tsx","../../src/components/layout/sidebar.tsx","../../src/components/modules/dashboard.tsx","../../src/components/modules/inventory.tsx","../../src/components/modules/oracle.tsx","../../src/components/modules/sentinel.tsx","../../src/components/modules/settings.tsx","../../src/components/oracle/leadinspector.tsx","../../src/components/oracle/pipelineview.tsx","../../src/components/oracle/mockleads.ts","../../src/components/sentinel/journeyriver/inspectorpanel.tsx","../../src/components/sentinel/journeyriver/riverpath.tsx","../../src/components/sentinel/journeyriver/index.tsx","../../src/components/ui/accordion.tsx","../../src/components/ui/alert-dialog.tsx","../../src/components/ui/alert.tsx","../../src/components/ui/aspect-ratio.tsx","../../src/components/ui/avatar.tsx","../../src/components/ui/badge.tsx","../../src/components/ui/breadcrumb.tsx","../../src/components/ui/button-group.tsx","../../src/components/ui/button.tsx","../../src/components/ui/calendar.tsx","../../src/components/ui/card.tsx","../../src/components/ui/carousel.tsx","../../src/components/ui/chart.tsx","../../src/components/ui/checkbox.tsx","../../src/components/ui/collapsible.tsx","../../src/components/ui/command.tsx","../../src/components/ui/context-menu.tsx","../../src/components/ui/dialog.tsx","../../src/components/ui/drawer.tsx","../../src/components/ui/dropdown-menu.tsx","../../src/components/ui/empty.tsx","../../src/components/ui/field.tsx","../../src/components/ui/form.tsx","../../src/components/ui/hover-card.tsx","../../src/components/ui/input-group.tsx","../../src/components/ui/input-otp.tsx","../../src/components/ui/input.tsx","../../src/components/ui/item.tsx","../../src/components/ui/kbd.tsx","../../src/components/ui/label.tsx","../../src/components/ui/menubar.tsx","../../src/components/ui/navigation-menu.tsx","../../src/components/ui/pagination.tsx","../../src/components/ui/popover.tsx","../../src/components/ui/progress.tsx","../../src/components/ui/radio-group.tsx","../../src/components/ui/resizable.tsx","../../src/components/ui/scroll-area.tsx","../../src/components/ui/select.tsx","../../src/components/ui/separator.tsx","../../src/components/ui/sheet.tsx","../../src/components/ui/sidebar.tsx","../../src/components/ui/skeleton.tsx","../../src/components/ui/slider.tsx","../../src/components/ui/sonner.tsx","../../src/components/ui/spinner.tsx","../../src/components/ui/switch.tsx","../../src/components/ui/table.tsx","../../src/components/ui/tabs.tsx","../../src/components/ui/textarea.tsx","../../src/components/ui/toggle-group.tsx","../../src/components/ui/toggle.tsx","../../src/components/ui/tooltip.tsx","../../src/hooks/use-mobile.ts","../../src/lib/oraclequeryclient.ts","../../src/lib/utils.ts","../../src/store/usestore.ts","../../src/types/crm.ts","../../src/types/index.ts","../../src/utils/curvegenerator.ts"],"version":"5.9.3"}
|
||||
{"root":["../../src/app.tsx","../../src/main.tsx","../../src/app/oracle/page.tsx","../../src/components/layout/loginscreen.tsx","../../src/components/layout/sidebar.tsx","../../src/components/modules/catalyst.tsx","../../src/components/modules/dashboard.tsx","../../src/components/modules/inventory.tsx","../../src/components/modules/oracle.tsx","../../src/components/modules/sentinel.tsx","../../src/components/modules/settings.tsx","../../src/components/oracle/leadinspector.tsx","../../src/components/oracle/pipelineview.tsx","../../src/components/oracle/mockleads.ts","../../src/components/sentinel/journeyriver/inspectorpanel.tsx","../../src/components/sentinel/journeyriver/riverpath.tsx","../../src/components/sentinel/journeyriver/index.tsx","../../src/components/ui/accordion.tsx","../../src/components/ui/alert-dialog.tsx","../../src/components/ui/alert.tsx","../../src/components/ui/aspect-ratio.tsx","../../src/components/ui/avatar.tsx","../../src/components/ui/badge.tsx","../../src/components/ui/breadcrumb.tsx","../../src/components/ui/button-group.tsx","../../src/components/ui/button.tsx","../../src/components/ui/calendar.tsx","../../src/components/ui/card.tsx","../../src/components/ui/carousel.tsx","../../src/components/ui/chart.tsx","../../src/components/ui/checkbox.tsx","../../src/components/ui/collapsible.tsx","../../src/components/ui/command.tsx","../../src/components/ui/context-menu.tsx","../../src/components/ui/dialog.tsx","../../src/components/ui/drawer.tsx","../../src/components/ui/dropdown-menu.tsx","../../src/components/ui/empty.tsx","../../src/components/ui/field.tsx","../../src/components/ui/form.tsx","../../src/components/ui/hover-card.tsx","../../src/components/ui/input-group.tsx","../../src/components/ui/input-otp.tsx","../../src/components/ui/input.tsx","../../src/components/ui/item.tsx","../../src/components/ui/kbd.tsx","../../src/components/ui/label.tsx","../../src/components/ui/menubar.tsx","../../src/components/ui/navigation-menu.tsx","../../src/components/ui/pagination.tsx","../../src/components/ui/popover.tsx","../../src/components/ui/progress.tsx","../../src/components/ui/radio-group.tsx","../../src/components/ui/resizable.tsx","../../src/components/ui/scroll-area.tsx","../../src/components/ui/select.tsx","../../src/components/ui/separator.tsx","../../src/components/ui/sheet.tsx","../../src/components/ui/sidebar.tsx","../../src/components/ui/skeleton.tsx","../../src/components/ui/slider.tsx","../../src/components/ui/sonner.tsx","../../src/components/ui/spinner.tsx","../../src/components/ui/switch.tsx","../../src/components/ui/table.tsx","../../src/components/ui/tabs.tsx","../../src/components/ui/textarea.tsx","../../src/components/ui/toggle-group.tsx","../../src/components/ui/toggle.tsx","../../src/components/ui/tooltip.tsx","../../src/hooks/use-mobile.ts","../../src/lib/oraclequeryclient.ts","../../src/lib/utils.ts","../../src/store/usemarketingstore.ts","../../src/store/usestore.ts","../../src/types/crm.ts","../../src/types/index.ts","../../src/utils/curvegenerator.ts"],"version":"5.9.3"}
|
||||
3591
app/node_modules/.vite/deps/@dnd-kit_core.js
generated
vendored
3591
app/node_modules/.vite/deps/@dnd-kit_core.js
generated
vendored
File diff suppressed because it is too large
Load Diff
7
app/node_modules/.vite/deps/@dnd-kit_core.js.map
generated
vendored
7
app/node_modules/.vite/deps/@dnd-kit_core.js.map
generated
vendored
File diff suppressed because one or more lines are too long
56
app/node_modules/.vite/deps/@dnd-kit_utilities.js
generated
vendored
56
app/node_modules/.vite/deps/@dnd-kit_utilities.js
generated
vendored
@@ -1,56 +0,0 @@
|
||||
import {
|
||||
CSS,
|
||||
add,
|
||||
canUseDOM,
|
||||
findFirstFocusableNode,
|
||||
getEventCoordinates,
|
||||
getOwnerDocument,
|
||||
getWindow,
|
||||
hasViewportRelativeCoordinates,
|
||||
isDocument,
|
||||
isHTMLElement,
|
||||
isKeyboardEvent,
|
||||
isNode,
|
||||
isSVGElement,
|
||||
isTouchEvent,
|
||||
isWindow,
|
||||
subtract,
|
||||
useCombinedRefs,
|
||||
useEvent,
|
||||
useInterval,
|
||||
useIsomorphicLayoutEffect,
|
||||
useLatestValue,
|
||||
useLazyMemo,
|
||||
useNodeRef,
|
||||
usePrevious,
|
||||
useUniqueId
|
||||
} from "./chunk-YJZCGBGU.js";
|
||||
import "./chunk-WUR7D6NS.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export {
|
||||
CSS,
|
||||
add,
|
||||
canUseDOM,
|
||||
findFirstFocusableNode,
|
||||
getEventCoordinates,
|
||||
getOwnerDocument,
|
||||
getWindow,
|
||||
hasViewportRelativeCoordinates,
|
||||
isDocument,
|
||||
isHTMLElement,
|
||||
isKeyboardEvent,
|
||||
isNode,
|
||||
isSVGElement,
|
||||
isTouchEvent,
|
||||
isWindow,
|
||||
subtract,
|
||||
useCombinedRefs,
|
||||
useEvent,
|
||||
useInterval,
|
||||
useIsomorphicLayoutEffect,
|
||||
useLatestValue,
|
||||
useLazyMemo,
|
||||
useNodeRef,
|
||||
usePrevious,
|
||||
useUniqueId
|
||||
};
|
||||
7
app/node_modules/.vite/deps/@dnd-kit_utilities.js.map
generated
vendored
7
app/node_modules/.vite/deps/@dnd-kit_utilities.js.map
generated
vendored
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
12
app/node_modules/.vite/deps/@radix-ui_react-avatar.js
generated
vendored
12
app/node_modules/.vite/deps/@radix-ui_react-avatar.js
generated
vendored
@@ -1,18 +1,18 @@
|
||||
"use client";
|
||||
import {
|
||||
require_shim
|
||||
} from "./chunk-TXHHHGR3.js";
|
||||
createSlot
|
||||
} from "./chunk-YWBEB5PG.js";
|
||||
import {
|
||||
useCallbackRef,
|
||||
useLayoutEffect2
|
||||
} from "./chunk-23FVUG5N.js";
|
||||
import "./chunk-2VUH7NEY.js";
|
||||
import {
|
||||
require_shim
|
||||
} from "./chunk-TXHHHGR3.js";
|
||||
import {
|
||||
require_react_dom
|
||||
} from "./chunk-YF4B4G2L.js";
|
||||
import {
|
||||
createSlot
|
||||
} from "./chunk-YWBEB5PG.js";
|
||||
import "./chunk-2VUH7NEY.js";
|
||||
import {
|
||||
require_jsx_runtime
|
||||
} from "./chunk-2YVA4HRZ.js";
|
||||
|
||||
4929
app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js
generated
vendored
4929
app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js
generated
vendored
File diff suppressed because it is too large
Load Diff
8
app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js.map
generated
vendored
8
app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js.map
generated
vendored
File diff suppressed because one or more lines are too long
755
app/node_modules/.vite/deps/@radix-ui_react-scroll-area.js
generated
vendored
755
app/node_modules/.vite/deps/@radix-ui_react-scroll-area.js
generated
vendored
@@ -1,755 +0,0 @@
|
||||
"use client";
|
||||
import {
|
||||
clamp
|
||||
} from "./chunk-6ZMM2PAV.js";
|
||||
import {
|
||||
Presence
|
||||
} from "./chunk-SMEXDMMQ.js";
|
||||
import {
|
||||
Primitive,
|
||||
composeEventHandlers,
|
||||
createContextScope,
|
||||
useDirection
|
||||
} from "./chunk-VPWBNV4W.js";
|
||||
import {
|
||||
useCallbackRef,
|
||||
useLayoutEffect2
|
||||
} from "./chunk-23FVUG5N.js";
|
||||
import "./chunk-YF4B4G2L.js";
|
||||
import {
|
||||
useComposedRefs
|
||||
} from "./chunk-2VUH7NEY.js";
|
||||
import {
|
||||
require_jsx_runtime
|
||||
} from "./chunk-2YVA4HRZ.js";
|
||||
import {
|
||||
require_react
|
||||
} from "./chunk-WUR7D6NS.js";
|
||||
import {
|
||||
__toESM
|
||||
} from "./chunk-G3PMV62Z.js";
|
||||
|
||||
// node_modules/@radix-ui/react-scroll-area/dist/index.mjs
|
||||
var React2 = __toESM(require_react(), 1);
|
||||
var React = __toESM(require_react(), 1);
|
||||
var import_jsx_runtime = __toESM(require_jsx_runtime(), 1);
|
||||
function useStateMachine(initialState, machine) {
|
||||
return React.useReducer((state, event) => {
|
||||
const nextState = machine[state][event];
|
||||
return nextState ?? state;
|
||||
}, initialState);
|
||||
}
|
||||
var SCROLL_AREA_NAME = "ScrollArea";
|
||||
var [createScrollAreaContext, createScrollAreaScope] = createContextScope(SCROLL_AREA_NAME);
|
||||
var [ScrollAreaProvider, useScrollAreaContext] = createScrollAreaContext(SCROLL_AREA_NAME);
|
||||
var ScrollArea = React2.forwardRef(
|
||||
(props, forwardedRef) => {
|
||||
const {
|
||||
__scopeScrollArea,
|
||||
type = "hover",
|
||||
dir,
|
||||
scrollHideDelay = 600,
|
||||
...scrollAreaProps
|
||||
} = props;
|
||||
const [scrollArea, setScrollArea] = React2.useState(null);
|
||||
const [viewport, setViewport] = React2.useState(null);
|
||||
const [content, setContent] = React2.useState(null);
|
||||
const [scrollbarX, setScrollbarX] = React2.useState(null);
|
||||
const [scrollbarY, setScrollbarY] = React2.useState(null);
|
||||
const [cornerWidth, setCornerWidth] = React2.useState(0);
|
||||
const [cornerHeight, setCornerHeight] = React2.useState(0);
|
||||
const [scrollbarXEnabled, setScrollbarXEnabled] = React2.useState(false);
|
||||
const [scrollbarYEnabled, setScrollbarYEnabled] = React2.useState(false);
|
||||
const composedRefs = useComposedRefs(forwardedRef, (node) => setScrollArea(node));
|
||||
const direction = useDirection(dir);
|
||||
return (0, import_jsx_runtime.jsx)(
|
||||
ScrollAreaProvider,
|
||||
{
|
||||
scope: __scopeScrollArea,
|
||||
type,
|
||||
dir: direction,
|
||||
scrollHideDelay,
|
||||
scrollArea,
|
||||
viewport,
|
||||
onViewportChange: setViewport,
|
||||
content,
|
||||
onContentChange: setContent,
|
||||
scrollbarX,
|
||||
onScrollbarXChange: setScrollbarX,
|
||||
scrollbarXEnabled,
|
||||
onScrollbarXEnabledChange: setScrollbarXEnabled,
|
||||
scrollbarY,
|
||||
onScrollbarYChange: setScrollbarY,
|
||||
scrollbarYEnabled,
|
||||
onScrollbarYEnabledChange: setScrollbarYEnabled,
|
||||
onCornerWidthChange: setCornerWidth,
|
||||
onCornerHeightChange: setCornerHeight,
|
||||
children: (0, import_jsx_runtime.jsx)(
|
||||
Primitive.div,
|
||||
{
|
||||
dir: direction,
|
||||
...scrollAreaProps,
|
||||
ref: composedRefs,
|
||||
style: {
|
||||
position: "relative",
|
||||
// Pass corner sizes as CSS vars to reduce re-renders of context consumers
|
||||
["--radix-scroll-area-corner-width"]: cornerWidth + "px",
|
||||
["--radix-scroll-area-corner-height"]: cornerHeight + "px",
|
||||
...props.style
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
ScrollArea.displayName = SCROLL_AREA_NAME;
|
||||
var VIEWPORT_NAME = "ScrollAreaViewport";
|
||||
var ScrollAreaViewport = React2.forwardRef(
|
||||
(props, forwardedRef) => {
|
||||
const { __scopeScrollArea, children, nonce, ...viewportProps } = props;
|
||||
const context = useScrollAreaContext(VIEWPORT_NAME, __scopeScrollArea);
|
||||
const ref = React2.useRef(null);
|
||||
const composedRefs = useComposedRefs(forwardedRef, ref, context.onViewportChange);
|
||||
return (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
||||
(0, import_jsx_runtime.jsx)(
|
||||
"style",
|
||||
{
|
||||
dangerouslySetInnerHTML: {
|
||||
__html: `[data-radix-scroll-area-viewport]{scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;}[data-radix-scroll-area-viewport]::-webkit-scrollbar{display:none}`
|
||||
},
|
||||
nonce
|
||||
}
|
||||
),
|
||||
(0, import_jsx_runtime.jsx)(
|
||||
Primitive.div,
|
||||
{
|
||||
"data-radix-scroll-area-viewport": "",
|
||||
...viewportProps,
|
||||
ref: composedRefs,
|
||||
style: {
|
||||
/**
|
||||
* We don't support `visible` because the intention is to have at least one scrollbar
|
||||
* if this component is used and `visible` will behave like `auto` in that case
|
||||
* https://developer.mozilla.org/en-US/docs/Web/CSS/overflow#description
|
||||
*
|
||||
* We don't handle `auto` because the intention is for the native implementation
|
||||
* to be hidden if using this component. We just want to ensure the node is scrollable
|
||||
* so could have used either `scroll` or `auto` here. We picked `scroll` to prevent
|
||||
* the browser from having to work out whether to render native scrollbars or not,
|
||||
* we tell it to with the intention of hiding them in CSS.
|
||||
*/
|
||||
overflowX: context.scrollbarXEnabled ? "scroll" : "hidden",
|
||||
overflowY: context.scrollbarYEnabled ? "scroll" : "hidden",
|
||||
...props.style
|
||||
},
|
||||
children: (0, import_jsx_runtime.jsx)("div", { ref: context.onContentChange, style: { minWidth: "100%", display: "table" }, children })
|
||||
}
|
||||
)
|
||||
] });
|
||||
}
|
||||
);
|
||||
ScrollAreaViewport.displayName = VIEWPORT_NAME;
|
||||
var SCROLLBAR_NAME = "ScrollAreaScrollbar";
|
||||
var ScrollAreaScrollbar = React2.forwardRef(
|
||||
(props, forwardedRef) => {
|
||||
const { forceMount, ...scrollbarProps } = props;
|
||||
const context = useScrollAreaContext(SCROLLBAR_NAME, props.__scopeScrollArea);
|
||||
const { onScrollbarXEnabledChange, onScrollbarYEnabledChange } = context;
|
||||
const isHorizontal = props.orientation === "horizontal";
|
||||
React2.useEffect(() => {
|
||||
isHorizontal ? onScrollbarXEnabledChange(true) : onScrollbarYEnabledChange(true);
|
||||
return () => {
|
||||
isHorizontal ? onScrollbarXEnabledChange(false) : onScrollbarYEnabledChange(false);
|
||||
};
|
||||
}, [isHorizontal, onScrollbarXEnabledChange, onScrollbarYEnabledChange]);
|
||||
return context.type === "hover" ? (0, import_jsx_runtime.jsx)(ScrollAreaScrollbarHover, { ...scrollbarProps, ref: forwardedRef, forceMount }) : context.type === "scroll" ? (0, import_jsx_runtime.jsx)(ScrollAreaScrollbarScroll, { ...scrollbarProps, ref: forwardedRef, forceMount }) : context.type === "auto" ? (0, import_jsx_runtime.jsx)(ScrollAreaScrollbarAuto, { ...scrollbarProps, ref: forwardedRef, forceMount }) : context.type === "always" ? (0, import_jsx_runtime.jsx)(ScrollAreaScrollbarVisible, { ...scrollbarProps, ref: forwardedRef }) : null;
|
||||
}
|
||||
);
|
||||
ScrollAreaScrollbar.displayName = SCROLLBAR_NAME;
|
||||
var ScrollAreaScrollbarHover = React2.forwardRef((props, forwardedRef) => {
|
||||
const { forceMount, ...scrollbarProps } = props;
|
||||
const context = useScrollAreaContext(SCROLLBAR_NAME, props.__scopeScrollArea);
|
||||
const [visible, setVisible] = React2.useState(false);
|
||||
React2.useEffect(() => {
|
||||
const scrollArea = context.scrollArea;
|
||||
let hideTimer = 0;
|
||||
if (scrollArea) {
|
||||
const handlePointerEnter = () => {
|
||||
window.clearTimeout(hideTimer);
|
||||
setVisible(true);
|
||||
};
|
||||
const handlePointerLeave = () => {
|
||||
hideTimer = window.setTimeout(() => setVisible(false), context.scrollHideDelay);
|
||||
};
|
||||
scrollArea.addEventListener("pointerenter", handlePointerEnter);
|
||||
scrollArea.addEventListener("pointerleave", handlePointerLeave);
|
||||
return () => {
|
||||
window.clearTimeout(hideTimer);
|
||||
scrollArea.removeEventListener("pointerenter", handlePointerEnter);
|
||||
scrollArea.removeEventListener("pointerleave", handlePointerLeave);
|
||||
};
|
||||
}
|
||||
}, [context.scrollArea, context.scrollHideDelay]);
|
||||
return (0, import_jsx_runtime.jsx)(Presence, { present: forceMount || visible, children: (0, import_jsx_runtime.jsx)(
|
||||
ScrollAreaScrollbarAuto,
|
||||
{
|
||||
"data-state": visible ? "visible" : "hidden",
|
||||
...scrollbarProps,
|
||||
ref: forwardedRef
|
||||
}
|
||||
) });
|
||||
});
|
||||
var ScrollAreaScrollbarScroll = React2.forwardRef((props, forwardedRef) => {
|
||||
const { forceMount, ...scrollbarProps } = props;
|
||||
const context = useScrollAreaContext(SCROLLBAR_NAME, props.__scopeScrollArea);
|
||||
const isHorizontal = props.orientation === "horizontal";
|
||||
const debounceScrollEnd = useDebounceCallback(() => send("SCROLL_END"), 100);
|
||||
const [state, send] = useStateMachine("hidden", {
|
||||
hidden: {
|
||||
SCROLL: "scrolling"
|
||||
},
|
||||
scrolling: {
|
||||
SCROLL_END: "idle",
|
||||
POINTER_ENTER: "interacting"
|
||||
},
|
||||
interacting: {
|
||||
SCROLL: "interacting",
|
||||
POINTER_LEAVE: "idle"
|
||||
},
|
||||
idle: {
|
||||
HIDE: "hidden",
|
||||
SCROLL: "scrolling",
|
||||
POINTER_ENTER: "interacting"
|
||||
}
|
||||
});
|
||||
React2.useEffect(() => {
|
||||
if (state === "idle") {
|
||||
const hideTimer = window.setTimeout(() => send("HIDE"), context.scrollHideDelay);
|
||||
return () => window.clearTimeout(hideTimer);
|
||||
}
|
||||
}, [state, context.scrollHideDelay, send]);
|
||||
React2.useEffect(() => {
|
||||
const viewport = context.viewport;
|
||||
const scrollDirection = isHorizontal ? "scrollLeft" : "scrollTop";
|
||||
if (viewport) {
|
||||
let prevScrollPos = viewport[scrollDirection];
|
||||
const handleScroll = () => {
|
||||
const scrollPos = viewport[scrollDirection];
|
||||
const hasScrollInDirectionChanged = prevScrollPos !== scrollPos;
|
||||
if (hasScrollInDirectionChanged) {
|
||||
send("SCROLL");
|
||||
debounceScrollEnd();
|
||||
}
|
||||
prevScrollPos = scrollPos;
|
||||
};
|
||||
viewport.addEventListener("scroll", handleScroll);
|
||||
return () => viewport.removeEventListener("scroll", handleScroll);
|
||||
}
|
||||
}, [context.viewport, isHorizontal, send, debounceScrollEnd]);
|
||||
return (0, import_jsx_runtime.jsx)(Presence, { present: forceMount || state !== "hidden", children: (0, import_jsx_runtime.jsx)(
|
||||
ScrollAreaScrollbarVisible,
|
||||
{
|
||||
"data-state": state === "hidden" ? "hidden" : "visible",
|
||||
...scrollbarProps,
|
||||
ref: forwardedRef,
|
||||
onPointerEnter: composeEventHandlers(props.onPointerEnter, () => send("POINTER_ENTER")),
|
||||
onPointerLeave: composeEventHandlers(props.onPointerLeave, () => send("POINTER_LEAVE"))
|
||||
}
|
||||
) });
|
||||
});
|
||||
var ScrollAreaScrollbarAuto = React2.forwardRef((props, forwardedRef) => {
|
||||
const context = useScrollAreaContext(SCROLLBAR_NAME, props.__scopeScrollArea);
|
||||
const { forceMount, ...scrollbarProps } = props;
|
||||
const [visible, setVisible] = React2.useState(false);
|
||||
const isHorizontal = props.orientation === "horizontal";
|
||||
const handleResize = useDebounceCallback(() => {
|
||||
if (context.viewport) {
|
||||
const isOverflowX = context.viewport.offsetWidth < context.viewport.scrollWidth;
|
||||
const isOverflowY = context.viewport.offsetHeight < context.viewport.scrollHeight;
|
||||
setVisible(isHorizontal ? isOverflowX : isOverflowY);
|
||||
}
|
||||
}, 10);
|
||||
useResizeObserver(context.viewport, handleResize);
|
||||
useResizeObserver(context.content, handleResize);
|
||||
return (0, import_jsx_runtime.jsx)(Presence, { present: forceMount || visible, children: (0, import_jsx_runtime.jsx)(
|
||||
ScrollAreaScrollbarVisible,
|
||||
{
|
||||
"data-state": visible ? "visible" : "hidden",
|
||||
...scrollbarProps,
|
||||
ref: forwardedRef
|
||||
}
|
||||
) });
|
||||
});
|
||||
var ScrollAreaScrollbarVisible = React2.forwardRef((props, forwardedRef) => {
|
||||
const { orientation = "vertical", ...scrollbarProps } = props;
|
||||
const context = useScrollAreaContext(SCROLLBAR_NAME, props.__scopeScrollArea);
|
||||
const thumbRef = React2.useRef(null);
|
||||
const pointerOffsetRef = React2.useRef(0);
|
||||
const [sizes, setSizes] = React2.useState({
|
||||
content: 0,
|
||||
viewport: 0,
|
||||
scrollbar: { size: 0, paddingStart: 0, paddingEnd: 0 }
|
||||
});
|
||||
const thumbRatio = getThumbRatio(sizes.viewport, sizes.content);
|
||||
const commonProps = {
|
||||
...scrollbarProps,
|
||||
sizes,
|
||||
onSizesChange: setSizes,
|
||||
hasThumb: Boolean(thumbRatio > 0 && thumbRatio < 1),
|
||||
onThumbChange: (thumb) => thumbRef.current = thumb,
|
||||
onThumbPointerUp: () => pointerOffsetRef.current = 0,
|
||||
onThumbPointerDown: (pointerPos) => pointerOffsetRef.current = pointerPos
|
||||
};
|
||||
function getScrollPosition(pointerPos, dir) {
|
||||
return getScrollPositionFromPointer(pointerPos, pointerOffsetRef.current, sizes, dir);
|
||||
}
|
||||
if (orientation === "horizontal") {
|
||||
return (0, import_jsx_runtime.jsx)(
|
||||
ScrollAreaScrollbarX,
|
||||
{
|
||||
...commonProps,
|
||||
ref: forwardedRef,
|
||||
onThumbPositionChange: () => {
|
||||
if (context.viewport && thumbRef.current) {
|
||||
const scrollPos = context.viewport.scrollLeft;
|
||||
const offset = getThumbOffsetFromScroll(scrollPos, sizes, context.dir);
|
||||
thumbRef.current.style.transform = `translate3d(${offset}px, 0, 0)`;
|
||||
}
|
||||
},
|
||||
onWheelScroll: (scrollPos) => {
|
||||
if (context.viewport) context.viewport.scrollLeft = scrollPos;
|
||||
},
|
||||
onDragScroll: (pointerPos) => {
|
||||
if (context.viewport) {
|
||||
context.viewport.scrollLeft = getScrollPosition(pointerPos, context.dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (orientation === "vertical") {
|
||||
return (0, import_jsx_runtime.jsx)(
|
||||
ScrollAreaScrollbarY,
|
||||
{
|
||||
...commonProps,
|
||||
ref: forwardedRef,
|
||||
onThumbPositionChange: () => {
|
||||
if (context.viewport && thumbRef.current) {
|
||||
const scrollPos = context.viewport.scrollTop;
|
||||
const offset = getThumbOffsetFromScroll(scrollPos, sizes);
|
||||
thumbRef.current.style.transform = `translate3d(0, ${offset}px, 0)`;
|
||||
}
|
||||
},
|
||||
onWheelScroll: (scrollPos) => {
|
||||
if (context.viewport) context.viewport.scrollTop = scrollPos;
|
||||
},
|
||||
onDragScroll: (pointerPos) => {
|
||||
if (context.viewport) context.viewport.scrollTop = getScrollPosition(pointerPos);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
var ScrollAreaScrollbarX = React2.forwardRef((props, forwardedRef) => {
|
||||
const { sizes, onSizesChange, ...scrollbarProps } = props;
|
||||
const context = useScrollAreaContext(SCROLLBAR_NAME, props.__scopeScrollArea);
|
||||
const [computedStyle, setComputedStyle] = React2.useState();
|
||||
const ref = React2.useRef(null);
|
||||
const composeRefs = useComposedRefs(forwardedRef, ref, context.onScrollbarXChange);
|
||||
React2.useEffect(() => {
|
||||
if (ref.current) setComputedStyle(getComputedStyle(ref.current));
|
||||
}, [ref]);
|
||||
return (0, import_jsx_runtime.jsx)(
|
||||
ScrollAreaScrollbarImpl,
|
||||
{
|
||||
"data-orientation": "horizontal",
|
||||
...scrollbarProps,
|
||||
ref: composeRefs,
|
||||
sizes,
|
||||
style: {
|
||||
bottom: 0,
|
||||
left: context.dir === "rtl" ? "var(--radix-scroll-area-corner-width)" : 0,
|
||||
right: context.dir === "ltr" ? "var(--radix-scroll-area-corner-width)" : 0,
|
||||
["--radix-scroll-area-thumb-width"]: getThumbSize(sizes) + "px",
|
||||
...props.style
|
||||
},
|
||||
onThumbPointerDown: (pointerPos) => props.onThumbPointerDown(pointerPos.x),
|
||||
onDragScroll: (pointerPos) => props.onDragScroll(pointerPos.x),
|
||||
onWheelScroll: (event, maxScrollPos) => {
|
||||
if (context.viewport) {
|
||||
const scrollPos = context.viewport.scrollLeft + event.deltaX;
|
||||
props.onWheelScroll(scrollPos);
|
||||
if (isScrollingWithinScrollbarBounds(scrollPos, maxScrollPos)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
onResize: () => {
|
||||
if (ref.current && context.viewport && computedStyle) {
|
||||
onSizesChange({
|
||||
content: context.viewport.scrollWidth,
|
||||
viewport: context.viewport.offsetWidth,
|
||||
scrollbar: {
|
||||
size: ref.current.clientWidth,
|
||||
paddingStart: toInt(computedStyle.paddingLeft),
|
||||
paddingEnd: toInt(computedStyle.paddingRight)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
var ScrollAreaScrollbarY = React2.forwardRef((props, forwardedRef) => {
|
||||
const { sizes, onSizesChange, ...scrollbarProps } = props;
|
||||
const context = useScrollAreaContext(SCROLLBAR_NAME, props.__scopeScrollArea);
|
||||
const [computedStyle, setComputedStyle] = React2.useState();
|
||||
const ref = React2.useRef(null);
|
||||
const composeRefs = useComposedRefs(forwardedRef, ref, context.onScrollbarYChange);
|
||||
React2.useEffect(() => {
|
||||
if (ref.current) setComputedStyle(getComputedStyle(ref.current));
|
||||
}, [ref]);
|
||||
return (0, import_jsx_runtime.jsx)(
|
||||
ScrollAreaScrollbarImpl,
|
||||
{
|
||||
"data-orientation": "vertical",
|
||||
...scrollbarProps,
|
||||
ref: composeRefs,
|
||||
sizes,
|
||||
style: {
|
||||
top: 0,
|
||||
right: context.dir === "ltr" ? 0 : void 0,
|
||||
left: context.dir === "rtl" ? 0 : void 0,
|
||||
bottom: "var(--radix-scroll-area-corner-height)",
|
||||
["--radix-scroll-area-thumb-height"]: getThumbSize(sizes) + "px",
|
||||
...props.style
|
||||
},
|
||||
onThumbPointerDown: (pointerPos) => props.onThumbPointerDown(pointerPos.y),
|
||||
onDragScroll: (pointerPos) => props.onDragScroll(pointerPos.y),
|
||||
onWheelScroll: (event, maxScrollPos) => {
|
||||
if (context.viewport) {
|
||||
const scrollPos = context.viewport.scrollTop + event.deltaY;
|
||||
props.onWheelScroll(scrollPos);
|
||||
if (isScrollingWithinScrollbarBounds(scrollPos, maxScrollPos)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
onResize: () => {
|
||||
if (ref.current && context.viewport && computedStyle) {
|
||||
onSizesChange({
|
||||
content: context.viewport.scrollHeight,
|
||||
viewport: context.viewport.offsetHeight,
|
||||
scrollbar: {
|
||||
size: ref.current.clientHeight,
|
||||
paddingStart: toInt(computedStyle.paddingTop),
|
||||
paddingEnd: toInt(computedStyle.paddingBottom)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
var [ScrollbarProvider, useScrollbarContext] = createScrollAreaContext(SCROLLBAR_NAME);
|
||||
var ScrollAreaScrollbarImpl = React2.forwardRef((props, forwardedRef) => {
|
||||
const {
|
||||
__scopeScrollArea,
|
||||
sizes,
|
||||
hasThumb,
|
||||
onThumbChange,
|
||||
onThumbPointerUp,
|
||||
onThumbPointerDown,
|
||||
onThumbPositionChange,
|
||||
onDragScroll,
|
||||
onWheelScroll,
|
||||
onResize,
|
||||
...scrollbarProps
|
||||
} = props;
|
||||
const context = useScrollAreaContext(SCROLLBAR_NAME, __scopeScrollArea);
|
||||
const [scrollbar, setScrollbar] = React2.useState(null);
|
||||
const composeRefs = useComposedRefs(forwardedRef, (node) => setScrollbar(node));
|
||||
const rectRef = React2.useRef(null);
|
||||
const prevWebkitUserSelectRef = React2.useRef("");
|
||||
const viewport = context.viewport;
|
||||
const maxScrollPos = sizes.content - sizes.viewport;
|
||||
const handleWheelScroll = useCallbackRef(onWheelScroll);
|
||||
const handleThumbPositionChange = useCallbackRef(onThumbPositionChange);
|
||||
const handleResize = useDebounceCallback(onResize, 10);
|
||||
function handleDragScroll(event) {
|
||||
if (rectRef.current) {
|
||||
const x = event.clientX - rectRef.current.left;
|
||||
const y = event.clientY - rectRef.current.top;
|
||||
onDragScroll({ x, y });
|
||||
}
|
||||
}
|
||||
React2.useEffect(() => {
|
||||
const handleWheel = (event) => {
|
||||
const element = event.target;
|
||||
const isScrollbarWheel = scrollbar?.contains(element);
|
||||
if (isScrollbarWheel) handleWheelScroll(event, maxScrollPos);
|
||||
};
|
||||
document.addEventListener("wheel", handleWheel, { passive: false });
|
||||
return () => document.removeEventListener("wheel", handleWheel, { passive: false });
|
||||
}, [viewport, scrollbar, maxScrollPos, handleWheelScroll]);
|
||||
React2.useEffect(handleThumbPositionChange, [sizes, handleThumbPositionChange]);
|
||||
useResizeObserver(scrollbar, handleResize);
|
||||
useResizeObserver(context.content, handleResize);
|
||||
return (0, import_jsx_runtime.jsx)(
|
||||
ScrollbarProvider,
|
||||
{
|
||||
scope: __scopeScrollArea,
|
||||
scrollbar,
|
||||
hasThumb,
|
||||
onThumbChange: useCallbackRef(onThumbChange),
|
||||
onThumbPointerUp: useCallbackRef(onThumbPointerUp),
|
||||
onThumbPositionChange: handleThumbPositionChange,
|
||||
onThumbPointerDown: useCallbackRef(onThumbPointerDown),
|
||||
children: (0, import_jsx_runtime.jsx)(
|
||||
Primitive.div,
|
||||
{
|
||||
...scrollbarProps,
|
||||
ref: composeRefs,
|
||||
style: { position: "absolute", ...scrollbarProps.style },
|
||||
onPointerDown: composeEventHandlers(props.onPointerDown, (event) => {
|
||||
const mainPointer = 0;
|
||||
if (event.button === mainPointer) {
|
||||
const element = event.target;
|
||||
element.setPointerCapture(event.pointerId);
|
||||
rectRef.current = scrollbar.getBoundingClientRect();
|
||||
prevWebkitUserSelectRef.current = document.body.style.webkitUserSelect;
|
||||
document.body.style.webkitUserSelect = "none";
|
||||
if (context.viewport) context.viewport.style.scrollBehavior = "auto";
|
||||
handleDragScroll(event);
|
||||
}
|
||||
}),
|
||||
onPointerMove: composeEventHandlers(props.onPointerMove, handleDragScroll),
|
||||
onPointerUp: composeEventHandlers(props.onPointerUp, (event) => {
|
||||
const element = event.target;
|
||||
if (element.hasPointerCapture(event.pointerId)) {
|
||||
element.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
document.body.style.webkitUserSelect = prevWebkitUserSelectRef.current;
|
||||
if (context.viewport) context.viewport.style.scrollBehavior = "";
|
||||
rectRef.current = null;
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
);
|
||||
});
|
||||
var THUMB_NAME = "ScrollAreaThumb";
|
||||
var ScrollAreaThumb = React2.forwardRef(
|
||||
(props, forwardedRef) => {
|
||||
const { forceMount, ...thumbProps } = props;
|
||||
const scrollbarContext = useScrollbarContext(THUMB_NAME, props.__scopeScrollArea);
|
||||
return (0, import_jsx_runtime.jsx)(Presence, { present: forceMount || scrollbarContext.hasThumb, children: (0, import_jsx_runtime.jsx)(ScrollAreaThumbImpl, { ref: forwardedRef, ...thumbProps }) });
|
||||
}
|
||||
);
|
||||
var ScrollAreaThumbImpl = React2.forwardRef(
|
||||
(props, forwardedRef) => {
|
||||
const { __scopeScrollArea, style, ...thumbProps } = props;
|
||||
const scrollAreaContext = useScrollAreaContext(THUMB_NAME, __scopeScrollArea);
|
||||
const scrollbarContext = useScrollbarContext(THUMB_NAME, __scopeScrollArea);
|
||||
const { onThumbPositionChange } = scrollbarContext;
|
||||
const composedRef = useComposedRefs(
|
||||
forwardedRef,
|
||||
(node) => scrollbarContext.onThumbChange(node)
|
||||
);
|
||||
const removeUnlinkedScrollListenerRef = React2.useRef(void 0);
|
||||
const debounceScrollEnd = useDebounceCallback(() => {
|
||||
if (removeUnlinkedScrollListenerRef.current) {
|
||||
removeUnlinkedScrollListenerRef.current();
|
||||
removeUnlinkedScrollListenerRef.current = void 0;
|
||||
}
|
||||
}, 100);
|
||||
React2.useEffect(() => {
|
||||
const viewport = scrollAreaContext.viewport;
|
||||
if (viewport) {
|
||||
const handleScroll = () => {
|
||||
debounceScrollEnd();
|
||||
if (!removeUnlinkedScrollListenerRef.current) {
|
||||
const listener = addUnlinkedScrollListener(viewport, onThumbPositionChange);
|
||||
removeUnlinkedScrollListenerRef.current = listener;
|
||||
onThumbPositionChange();
|
||||
}
|
||||
};
|
||||
onThumbPositionChange();
|
||||
viewport.addEventListener("scroll", handleScroll);
|
||||
return () => viewport.removeEventListener("scroll", handleScroll);
|
||||
}
|
||||
}, [scrollAreaContext.viewport, debounceScrollEnd, onThumbPositionChange]);
|
||||
return (0, import_jsx_runtime.jsx)(
|
||||
Primitive.div,
|
||||
{
|
||||
"data-state": scrollbarContext.hasThumb ? "visible" : "hidden",
|
||||
...thumbProps,
|
||||
ref: composedRef,
|
||||
style: {
|
||||
width: "var(--radix-scroll-area-thumb-width)",
|
||||
height: "var(--radix-scroll-area-thumb-height)",
|
||||
...style
|
||||
},
|
||||
onPointerDownCapture: composeEventHandlers(props.onPointerDownCapture, (event) => {
|
||||
const thumb = event.target;
|
||||
const thumbRect = thumb.getBoundingClientRect();
|
||||
const x = event.clientX - thumbRect.left;
|
||||
const y = event.clientY - thumbRect.top;
|
||||
scrollbarContext.onThumbPointerDown({ x, y });
|
||||
}),
|
||||
onPointerUp: composeEventHandlers(props.onPointerUp, scrollbarContext.onThumbPointerUp)
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
ScrollAreaThumb.displayName = THUMB_NAME;
|
||||
var CORNER_NAME = "ScrollAreaCorner";
|
||||
var ScrollAreaCorner = React2.forwardRef(
|
||||
(props, forwardedRef) => {
|
||||
const context = useScrollAreaContext(CORNER_NAME, props.__scopeScrollArea);
|
||||
const hasBothScrollbarsVisible = Boolean(context.scrollbarX && context.scrollbarY);
|
||||
const hasCorner = context.type !== "scroll" && hasBothScrollbarsVisible;
|
||||
return hasCorner ? (0, import_jsx_runtime.jsx)(ScrollAreaCornerImpl, { ...props, ref: forwardedRef }) : null;
|
||||
}
|
||||
);
|
||||
ScrollAreaCorner.displayName = CORNER_NAME;
|
||||
var ScrollAreaCornerImpl = React2.forwardRef((props, forwardedRef) => {
|
||||
const { __scopeScrollArea, ...cornerProps } = props;
|
||||
const context = useScrollAreaContext(CORNER_NAME, __scopeScrollArea);
|
||||
const [width, setWidth] = React2.useState(0);
|
||||
const [height, setHeight] = React2.useState(0);
|
||||
const hasSize = Boolean(width && height);
|
||||
useResizeObserver(context.scrollbarX, () => {
|
||||
const height2 = context.scrollbarX?.offsetHeight || 0;
|
||||
context.onCornerHeightChange(height2);
|
||||
setHeight(height2);
|
||||
});
|
||||
useResizeObserver(context.scrollbarY, () => {
|
||||
const width2 = context.scrollbarY?.offsetWidth || 0;
|
||||
context.onCornerWidthChange(width2);
|
||||
setWidth(width2);
|
||||
});
|
||||
return hasSize ? (0, import_jsx_runtime.jsx)(
|
||||
Primitive.div,
|
||||
{
|
||||
...cornerProps,
|
||||
ref: forwardedRef,
|
||||
style: {
|
||||
width,
|
||||
height,
|
||||
position: "absolute",
|
||||
right: context.dir === "ltr" ? 0 : void 0,
|
||||
left: context.dir === "rtl" ? 0 : void 0,
|
||||
bottom: 0,
|
||||
...props.style
|
||||
}
|
||||
}
|
||||
) : null;
|
||||
});
|
||||
function toInt(value) {
|
||||
return value ? parseInt(value, 10) : 0;
|
||||
}
|
||||
function getThumbRatio(viewportSize, contentSize) {
|
||||
const ratio = viewportSize / contentSize;
|
||||
return isNaN(ratio) ? 0 : ratio;
|
||||
}
|
||||
function getThumbSize(sizes) {
|
||||
const ratio = getThumbRatio(sizes.viewport, sizes.content);
|
||||
const scrollbarPadding = sizes.scrollbar.paddingStart + sizes.scrollbar.paddingEnd;
|
||||
const thumbSize = (sizes.scrollbar.size - scrollbarPadding) * ratio;
|
||||
return Math.max(thumbSize, 18);
|
||||
}
|
||||
function getScrollPositionFromPointer(pointerPos, pointerOffset, sizes, dir = "ltr") {
|
||||
const thumbSizePx = getThumbSize(sizes);
|
||||
const thumbCenter = thumbSizePx / 2;
|
||||
const offset = pointerOffset || thumbCenter;
|
||||
const thumbOffsetFromEnd = thumbSizePx - offset;
|
||||
const minPointerPos = sizes.scrollbar.paddingStart + offset;
|
||||
const maxPointerPos = sizes.scrollbar.size - sizes.scrollbar.paddingEnd - thumbOffsetFromEnd;
|
||||
const maxScrollPos = sizes.content - sizes.viewport;
|
||||
const scrollRange = dir === "ltr" ? [0, maxScrollPos] : [maxScrollPos * -1, 0];
|
||||
const interpolate = linearScale([minPointerPos, maxPointerPos], scrollRange);
|
||||
return interpolate(pointerPos);
|
||||
}
|
||||
function getThumbOffsetFromScroll(scrollPos, sizes, dir = "ltr") {
|
||||
const thumbSizePx = getThumbSize(sizes);
|
||||
const scrollbarPadding = sizes.scrollbar.paddingStart + sizes.scrollbar.paddingEnd;
|
||||
const scrollbar = sizes.scrollbar.size - scrollbarPadding;
|
||||
const maxScrollPos = sizes.content - sizes.viewport;
|
||||
const maxThumbPos = scrollbar - thumbSizePx;
|
||||
const scrollClampRange = dir === "ltr" ? [0, maxScrollPos] : [maxScrollPos * -1, 0];
|
||||
const scrollWithoutMomentum = clamp(scrollPos, scrollClampRange);
|
||||
const interpolate = linearScale([0, maxScrollPos], [0, maxThumbPos]);
|
||||
return interpolate(scrollWithoutMomentum);
|
||||
}
|
||||
function linearScale(input, output) {
|
||||
return (value) => {
|
||||
if (input[0] === input[1] || output[0] === output[1]) return output[0];
|
||||
const ratio = (output[1] - output[0]) / (input[1] - input[0]);
|
||||
return output[0] + ratio * (value - input[0]);
|
||||
};
|
||||
}
|
||||
function isScrollingWithinScrollbarBounds(scrollPos, maxScrollPos) {
|
||||
return scrollPos > 0 && scrollPos < maxScrollPos;
|
||||
}
|
||||
var addUnlinkedScrollListener = (node, handler = () => {
|
||||
}) => {
|
||||
let prevPosition = { left: node.scrollLeft, top: node.scrollTop };
|
||||
let rAF = 0;
|
||||
(function loop() {
|
||||
const position = { left: node.scrollLeft, top: node.scrollTop };
|
||||
const isHorizontalScroll = prevPosition.left !== position.left;
|
||||
const isVerticalScroll = prevPosition.top !== position.top;
|
||||
if (isHorizontalScroll || isVerticalScroll) handler();
|
||||
prevPosition = position;
|
||||
rAF = window.requestAnimationFrame(loop);
|
||||
})();
|
||||
return () => window.cancelAnimationFrame(rAF);
|
||||
};
|
||||
function useDebounceCallback(callback, delay) {
|
||||
const handleCallback = useCallbackRef(callback);
|
||||
const debounceTimerRef = React2.useRef(0);
|
||||
React2.useEffect(() => () => window.clearTimeout(debounceTimerRef.current), []);
|
||||
return React2.useCallback(() => {
|
||||
window.clearTimeout(debounceTimerRef.current);
|
||||
debounceTimerRef.current = window.setTimeout(handleCallback, delay);
|
||||
}, [handleCallback, delay]);
|
||||
}
|
||||
function useResizeObserver(element, onResize) {
|
||||
const handleResize = useCallbackRef(onResize);
|
||||
useLayoutEffect2(() => {
|
||||
let rAF = 0;
|
||||
if (element) {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
cancelAnimationFrame(rAF);
|
||||
rAF = window.requestAnimationFrame(handleResize);
|
||||
});
|
||||
resizeObserver.observe(element);
|
||||
return () => {
|
||||
window.cancelAnimationFrame(rAF);
|
||||
resizeObserver.unobserve(element);
|
||||
};
|
||||
}
|
||||
}, [element, handleResize]);
|
||||
}
|
||||
var Root = ScrollArea;
|
||||
var Viewport = ScrollAreaViewport;
|
||||
var Scrollbar = ScrollAreaScrollbar;
|
||||
var Thumb = ScrollAreaThumb;
|
||||
var Corner = ScrollAreaCorner;
|
||||
export {
|
||||
Corner,
|
||||
Root,
|
||||
ScrollArea,
|
||||
ScrollAreaCorner,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
Scrollbar,
|
||||
Thumb,
|
||||
Viewport,
|
||||
createScrollAreaScope
|
||||
};
|
||||
//# sourceMappingURL=@radix-ui_react-scroll-area.js.map
|
||||
7
app/node_modules/.vite/deps/@radix-ui_react-scroll-area.js.map
generated
vendored
7
app/node_modules/.vite/deps/@radix-ui_react-scroll-area.js.map
generated
vendored
File diff suppressed because one or more lines are too long
1359
app/node_modules/.vite/deps/@radix-ui_react-select.js
generated
vendored
1359
app/node_modules/.vite/deps/@radix-ui_react-select.js
generated
vendored
File diff suppressed because it is too large
Load Diff
7
app/node_modules/.vite/deps/@radix-ui_react-select.js.map
generated
vendored
7
app/node_modules/.vite/deps/@radix-ui_react-select.js.map
generated
vendored
File diff suppressed because one or more lines are too long
76836
app/node_modules/.vite/deps/@react-three_drei.js
generated
vendored
76836
app/node_modules/.vite/deps/@react-three_drei.js
generated
vendored
File diff suppressed because one or more lines are too long
8
app/node_modules/.vite/deps/@react-three_drei.js.map
generated
vendored
8
app/node_modules/.vite/deps/@react-three_drei.js.map
generated
vendored
File diff suppressed because one or more lines are too long
4
app/node_modules/.vite/deps/@react-three_fiber.js
generated
vendored
4
app/node_modules/.vite/deps/@react-three_fiber.js
generated
vendored
@@ -28,9 +28,9 @@ import {
|
||||
useLoader,
|
||||
useStore,
|
||||
useThree
|
||||
} from "./chunk-KOYBCGV3.js";
|
||||
import "./chunk-L3Z576C2.js";
|
||||
} from "./chunk-O5V7GNMB.js";
|
||||
import "./chunk-GUQHL3N7.js";
|
||||
import "./chunk-L3Z576C2.js";
|
||||
import "./chunk-NJ4V5H3P.js";
|
||||
import "./chunk-TXHHHGR3.js";
|
||||
import "./chunk-2YVA4HRZ.js";
|
||||
|
||||
272
app/node_modules/.vite/deps/_metadata.json
generated
vendored
272
app/node_modules/.vite/deps/_metadata.json
generated
vendored
@@ -1,151 +1,127 @@
|
||||
{
|
||||
"hash": "947d6a4d",
|
||||
"configHash": "dc08391c",
|
||||
"lockfileHash": "de25a101",
|
||||
"browserHash": "961f5b3a",
|
||||
"hash": "b2c5007d",
|
||||
"configHash": "d9a82a01",
|
||||
"lockfileHash": "8a04eea8",
|
||||
"browserHash": "5d6343ae",
|
||||
"optimized": {
|
||||
"react": {
|
||||
"src": "../../react/index.js",
|
||||
"file": "react.js",
|
||||
"fileHash": "9864076c",
|
||||
"fileHash": "4ea9824e",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-dom": {
|
||||
"src": "../../react-dom/index.js",
|
||||
"file": "react-dom.js",
|
||||
"fileHash": "2f8641cc",
|
||||
"fileHash": "5b549105",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-dev-runtime": {
|
||||
"src": "../../react/jsx-dev-runtime.js",
|
||||
"file": "react_jsx-dev-runtime.js",
|
||||
"fileHash": "0d6d4c83",
|
||||
"fileHash": "41193e59",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-runtime": {
|
||||
"src": "../../react/jsx-runtime.js",
|
||||
"file": "react_jsx-runtime.js",
|
||||
"fileHash": "a2c255cd",
|
||||
"fileHash": "ad8008f8",
|
||||
"needsInterop": true
|
||||
},
|
||||
"@dnd-kit/core": {
|
||||
"src": "../../@dnd-kit/core/dist/core.esm.js",
|
||||
"file": "@dnd-kit_core.js",
|
||||
"fileHash": "c90d47ee",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@dnd-kit/utilities": {
|
||||
"src": "../../@dnd-kit/utilities/dist/utilities.esm.js",
|
||||
"file": "@dnd-kit_utilities.js",
|
||||
"fileHash": "434ef86c",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@radix-ui/react-avatar": {
|
||||
"src": "../../@radix-ui/react-avatar/dist/index.mjs",
|
||||
"file": "@radix-ui_react-avatar.js",
|
||||
"fileHash": "2805441c",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@radix-ui/react-scroll-area": {
|
||||
"src": "../../@radix-ui/react-scroll-area/dist/index.mjs",
|
||||
"file": "@radix-ui_react-scroll-area.js",
|
||||
"fileHash": "6c708018",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@radix-ui/react-slot": {
|
||||
"src": "../../@radix-ui/react-slot/dist/index.mjs",
|
||||
"file": "@radix-ui_react-slot.js",
|
||||
"fileHash": "5329088f",
|
||||
"needsInterop": false
|
||||
},
|
||||
"class-variance-authority": {
|
||||
"src": "../../class-variance-authority/dist/index.mjs",
|
||||
"file": "class-variance-authority.js",
|
||||
"fileHash": "ec62709c",
|
||||
"needsInterop": false
|
||||
},
|
||||
"clsx": {
|
||||
"src": "../../clsx/dist/clsx.mjs",
|
||||
"file": "clsx.js",
|
||||
"fileHash": "5559ec83",
|
||||
"needsInterop": false
|
||||
},
|
||||
"framer-motion": {
|
||||
"src": "../../framer-motion/dist/es/index.mjs",
|
||||
"file": "framer-motion.js",
|
||||
"fileHash": "2b7c361b",
|
||||
"needsInterop": false
|
||||
},
|
||||
"lucide-react": {
|
||||
"src": "../../lucide-react/dist/esm/lucide-react.js",
|
||||
"file": "lucide-react.js",
|
||||
"fileHash": "51875cd4",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-dom/client": {
|
||||
"src": "../../react-dom/client.js",
|
||||
"file": "react-dom_client.js",
|
||||
"fileHash": "45de9104",
|
||||
"needsInterop": true
|
||||
},
|
||||
"recharts": {
|
||||
"src": "../../recharts/es6/index.js",
|
||||
"file": "recharts.js",
|
||||
"fileHash": "64a83b89",
|
||||
"needsInterop": false
|
||||
},
|
||||
"tailwind-merge": {
|
||||
"src": "../../tailwind-merge/dist/bundle-mjs.mjs",
|
||||
"file": "tailwind-merge.js",
|
||||
"fileHash": "f691afe8",
|
||||
"needsInterop": false
|
||||
},
|
||||
"zustand": {
|
||||
"src": "../../zustand/esm/index.mjs",
|
||||
"file": "zustand.js",
|
||||
"fileHash": "fe0603c2",
|
||||
"needsInterop": false
|
||||
},
|
||||
"zustand/middleware": {
|
||||
"src": "../../zustand/esm/middleware.mjs",
|
||||
"file": "zustand_middleware.js",
|
||||
"fileHash": "66783eb7",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@radix-ui/react-select": {
|
||||
"src": "../../@radix-ui/react-select/dist/index.mjs",
|
||||
"file": "@radix-ui_react-select.js",
|
||||
"fileHash": "36896aae",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@react-three/fiber": {
|
||||
"src": "../../@react-three/fiber/dist/react-three-fiber.esm.js",
|
||||
"file": "@react-three_fiber.js",
|
||||
"fileHash": "12c15e20",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@react-three/drei": {
|
||||
"src": "../../@react-three/drei/index.js",
|
||||
"file": "@react-three_drei.js",
|
||||
"fileHash": "cb36fa64",
|
||||
"needsInterop": false
|
||||
},
|
||||
"three-stdlib": {
|
||||
"src": "../../three-stdlib/index.js",
|
||||
"file": "three-stdlib.js",
|
||||
"fileHash": "6c880e7a",
|
||||
"needsInterop": false
|
||||
},
|
||||
"three": {
|
||||
"src": "../../three/build/three.module.js",
|
||||
"file": "three.js",
|
||||
"fileHash": "bcfd9037",
|
||||
"fileHash": "e6d8d406",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@radix-ui/react-dropdown-menu": {
|
||||
"src": "../../@radix-ui/react-dropdown-menu/dist/index.mjs",
|
||||
"file": "@radix-ui_react-dropdown-menu.js",
|
||||
"fileHash": "44e29247",
|
||||
"fileHash": "26caaf75",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@radix-ui/react-slot": {
|
||||
"src": "../../@radix-ui/react-slot/dist/index.mjs",
|
||||
"file": "@radix-ui_react-slot.js",
|
||||
"fileHash": "e034d698",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@react-three/drei": {
|
||||
"src": "../../@react-three/drei/index.js",
|
||||
"file": "@react-three_drei.js",
|
||||
"fileHash": "d81d1332",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@react-three/fiber": {
|
||||
"src": "../../@react-three/fiber/dist/react-three-fiber.esm.js",
|
||||
"file": "@react-three_fiber.js",
|
||||
"fileHash": "dcc73392",
|
||||
"needsInterop": false
|
||||
},
|
||||
"class-variance-authority": {
|
||||
"src": "../../class-variance-authority/dist/index.mjs",
|
||||
"file": "class-variance-authority.js",
|
||||
"fileHash": "5fa3c3f8",
|
||||
"needsInterop": false
|
||||
},
|
||||
"clsx": {
|
||||
"src": "../../clsx/dist/clsx.mjs",
|
||||
"file": "clsx.js",
|
||||
"fileHash": "1428051e",
|
||||
"needsInterop": false
|
||||
},
|
||||
"framer-motion": {
|
||||
"src": "../../framer-motion/dist/es/index.mjs",
|
||||
"file": "framer-motion.js",
|
||||
"fileHash": "dd6ef86d",
|
||||
"needsInterop": false
|
||||
},
|
||||
"lucide-react": {
|
||||
"src": "../../lucide-react/dist/esm/lucide-react.js",
|
||||
"file": "lucide-react.js",
|
||||
"fileHash": "df6c668f",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-dom/client": {
|
||||
"src": "../../react-dom/client.js",
|
||||
"file": "react-dom_client.js",
|
||||
"fileHash": "23087df0",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-router-dom": {
|
||||
"src": "../../react-router-dom/dist/index.mjs",
|
||||
"file": "react-router-dom.js",
|
||||
"fileHash": "48fbca8f",
|
||||
"needsInterop": false
|
||||
},
|
||||
"recharts": {
|
||||
"src": "../../recharts/es6/index.js",
|
||||
"file": "recharts.js",
|
||||
"fileHash": "9faf094d",
|
||||
"needsInterop": false
|
||||
},
|
||||
"tailwind-merge": {
|
||||
"src": "../../tailwind-merge/dist/bundle-mjs.mjs",
|
||||
"file": "tailwind-merge.js",
|
||||
"fileHash": "fc48e7d8",
|
||||
"needsInterop": false
|
||||
},
|
||||
"three": {
|
||||
"src": "../../three/build/three.module.js",
|
||||
"file": "three.js",
|
||||
"fileHash": "a0b83871",
|
||||
"needsInterop": false
|
||||
},
|
||||
"zustand": {
|
||||
"src": "../../zustand/esm/index.mjs",
|
||||
"file": "zustand.js",
|
||||
"fileHash": "ad25a55f",
|
||||
"needsInterop": false
|
||||
},
|
||||
"zustand/middleware": {
|
||||
"src": "../../zustand/esm/middleware.mjs",
|
||||
"file": "zustand_middleware.js",
|
||||
"fileHash": "b84fa2e5",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
@@ -156,35 +132,35 @@
|
||||
"vision_bundle-CRIBZNUI": {
|
||||
"file": "vision_bundle-CRIBZNUI.js"
|
||||
},
|
||||
"chunk-OAEA5FZL": {
|
||||
"file": "chunk-OAEA5FZL.js"
|
||||
"chunk-U7P2NEEE": {
|
||||
"file": "chunk-U7P2NEEE.js"
|
||||
},
|
||||
"chunk-KOYBCGV3": {
|
||||
"file": "chunk-KOYBCGV3.js"
|
||||
"chunk-YWBEB5PG": {
|
||||
"file": "chunk-YWBEB5PG.js"
|
||||
},
|
||||
"chunk-TGAC7IAX": {
|
||||
"file": "chunk-TGAC7IAX.js"
|
||||
"chunk-23FVUG5N": {
|
||||
"file": "chunk-23FVUG5N.js"
|
||||
},
|
||||
"chunk-YJZCGBGU": {
|
||||
"file": "chunk-YJZCGBGU.js"
|
||||
},
|
||||
"chunk-6ZMM2PAV": {
|
||||
"file": "chunk-6ZMM2PAV.js"
|
||||
},
|
||||
"chunk-L3Z576C2": {
|
||||
"file": "chunk-L3Z576C2.js"
|
||||
"chunk-2VUH7NEY": {
|
||||
"file": "chunk-2VUH7NEY.js"
|
||||
},
|
||||
"chunk-6MXH2QM6": {
|
||||
"file": "chunk-6MXH2QM6.js"
|
||||
},
|
||||
"chunk-O4L7C4YS": {
|
||||
"file": "chunk-O4L7C4YS.js"
|
||||
},
|
||||
"chunk-OAEA5FZL": {
|
||||
"file": "chunk-OAEA5FZL.js"
|
||||
},
|
||||
"chunk-O5V7GNMB": {
|
||||
"file": "chunk-O5V7GNMB.js"
|
||||
},
|
||||
"chunk-GUQHL3N7": {
|
||||
"file": "chunk-GUQHL3N7.js"
|
||||
},
|
||||
"chunk-EQCCHGRT": {
|
||||
"file": "chunk-EQCCHGRT.js"
|
||||
},
|
||||
"chunk-O4L7C4YS": {
|
||||
"file": "chunk-O4L7C4YS.js"
|
||||
"chunk-L3Z576C2": {
|
||||
"file": "chunk-L3Z576C2.js"
|
||||
},
|
||||
"chunk-7GZ4CI6Q": {
|
||||
"file": "chunk-7GZ4CI6Q.js"
|
||||
@@ -192,39 +168,21 @@
|
||||
"chunk-NJ4V5H3P": {
|
||||
"file": "chunk-NJ4V5H3P.js"
|
||||
},
|
||||
"chunk-SMEXDMMQ": {
|
||||
"file": "chunk-SMEXDMMQ.js"
|
||||
},
|
||||
"chunk-M3NXY72O": {
|
||||
"file": "chunk-M3NXY72O.js"
|
||||
},
|
||||
"chunk-VPWBNV4W": {
|
||||
"file": "chunk-VPWBNV4W.js"
|
||||
"chunk-EQCCHGRT": {
|
||||
"file": "chunk-EQCCHGRT.js"
|
||||
},
|
||||
"chunk-TXHHHGR3": {
|
||||
"file": "chunk-TXHHHGR3.js"
|
||||
},
|
||||
"chunk-23FVUG5N": {
|
||||
"file": "chunk-23FVUG5N.js"
|
||||
},
|
||||
"chunk-YF4B4G2L": {
|
||||
"file": "chunk-YF4B4G2L.js"
|
||||
},
|
||||
"chunk-YWBEB5PG": {
|
||||
"file": "chunk-YWBEB5PG.js"
|
||||
},
|
||||
"chunk-2VUH7NEY": {
|
||||
"file": "chunk-2VUH7NEY.js"
|
||||
},
|
||||
"chunk-2YVA4HRZ": {
|
||||
"file": "chunk-2YVA4HRZ.js"
|
||||
},
|
||||
"chunk-WUR7D6NS": {
|
||||
"file": "chunk-WUR7D6NS.js"
|
||||
},
|
||||
"chunk-U7P2NEEE": {
|
||||
"file": "chunk-U7P2NEEE.js"
|
||||
},
|
||||
"chunk-G3PMV62Z": {
|
||||
"file": "chunk-G3PMV62Z.js"
|
||||
}
|
||||
|
||||
9
app/node_modules/.vite/deps/chunk-6ZMM2PAV.js
generated
vendored
9
app/node_modules/.vite/deps/chunk-6ZMM2PAV.js
generated
vendored
@@ -1,9 +0,0 @@
|
||||
// node_modules/@radix-ui/number/dist/index.mjs
|
||||
function clamp(value, [min, max]) {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
export {
|
||||
clamp
|
||||
};
|
||||
//# sourceMappingURL=chunk-6ZMM2PAV.js.map
|
||||
7
app/node_modules/.vite/deps/chunk-6ZMM2PAV.js.map
generated
vendored
7
app/node_modules/.vite/deps/chunk-6ZMM2PAV.js.map
generated
vendored
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../@radix-ui/number/src/number.ts"],
|
||||
"sourcesContent": ["function clamp(value: number, [min, max]: [number, number]): number {\n return Math.min(max, Math.max(min, value));\n}\n\nexport { clamp };\n"],
|
||||
"mappings": ";AAAA,SAAS,MAAM,OAAe,CAAC,KAAK,GAAG,GAA6B;AAClE,SAAO,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,CAAC;AAC3C;",
|
||||
"names": []
|
||||
}
|
||||
10153
app/node_modules/.vite/deps/chunk-KOYBCGV3.js
generated
vendored
10153
app/node_modules/.vite/deps/chunk-KOYBCGV3.js
generated
vendored
File diff suppressed because one or more lines are too long
7
app/node_modules/.vite/deps/chunk-KOYBCGV3.js.map
generated
vendored
7
app/node_modules/.vite/deps/chunk-KOYBCGV3.js.map
generated
vendored
File diff suppressed because one or more lines are too long
3841
app/node_modules/.vite/deps/chunk-M3NXY72O.js
generated
vendored
3841
app/node_modules/.vite/deps/chunk-M3NXY72O.js
generated
vendored
File diff suppressed because it is too large
Load Diff
7
app/node_modules/.vite/deps/chunk-M3NXY72O.js.map
generated
vendored
7
app/node_modules/.vite/deps/chunk-M3NXY72O.js.map
generated
vendored
File diff suppressed because one or more lines are too long
143
app/node_modules/.vite/deps/chunk-SMEXDMMQ.js
generated
vendored
143
app/node_modules/.vite/deps/chunk-SMEXDMMQ.js
generated
vendored
@@ -1,143 +0,0 @@
|
||||
import {
|
||||
useLayoutEffect2
|
||||
} from "./chunk-23FVUG5N.js";
|
||||
import {
|
||||
useComposedRefs
|
||||
} from "./chunk-2VUH7NEY.js";
|
||||
import {
|
||||
require_react
|
||||
} from "./chunk-WUR7D6NS.js";
|
||||
import {
|
||||
__toESM
|
||||
} from "./chunk-G3PMV62Z.js";
|
||||
|
||||
// node_modules/@radix-ui/react-presence/dist/index.mjs
|
||||
var React2 = __toESM(require_react(), 1);
|
||||
var React = __toESM(require_react(), 1);
|
||||
function useStateMachine(initialState, machine) {
|
||||
return React.useReducer((state, event) => {
|
||||
const nextState = machine[state][event];
|
||||
return nextState ?? state;
|
||||
}, initialState);
|
||||
}
|
||||
var Presence = (props) => {
|
||||
const { present, children } = props;
|
||||
const presence = usePresence(present);
|
||||
const child = typeof children === "function" ? children({ present: presence.isPresent }) : React2.Children.only(children);
|
||||
const ref = useComposedRefs(presence.ref, getElementRef(child));
|
||||
const forceMount = typeof children === "function";
|
||||
return forceMount || presence.isPresent ? React2.cloneElement(child, { ref }) : null;
|
||||
};
|
||||
Presence.displayName = "Presence";
|
||||
function usePresence(present) {
|
||||
const [node, setNode] = React2.useState();
|
||||
const stylesRef = React2.useRef(null);
|
||||
const prevPresentRef = React2.useRef(present);
|
||||
const prevAnimationNameRef = React2.useRef("none");
|
||||
const initialState = present ? "mounted" : "unmounted";
|
||||
const [state, send] = useStateMachine(initialState, {
|
||||
mounted: {
|
||||
UNMOUNT: "unmounted",
|
||||
ANIMATION_OUT: "unmountSuspended"
|
||||
},
|
||||
unmountSuspended: {
|
||||
MOUNT: "mounted",
|
||||
ANIMATION_END: "unmounted"
|
||||
},
|
||||
unmounted: {
|
||||
MOUNT: "mounted"
|
||||
}
|
||||
});
|
||||
React2.useEffect(() => {
|
||||
const currentAnimationName = getAnimationName(stylesRef.current);
|
||||
prevAnimationNameRef.current = state === "mounted" ? currentAnimationName : "none";
|
||||
}, [state]);
|
||||
useLayoutEffect2(() => {
|
||||
const styles = stylesRef.current;
|
||||
const wasPresent = prevPresentRef.current;
|
||||
const hasPresentChanged = wasPresent !== present;
|
||||
if (hasPresentChanged) {
|
||||
const prevAnimationName = prevAnimationNameRef.current;
|
||||
const currentAnimationName = getAnimationName(styles);
|
||||
if (present) {
|
||||
send("MOUNT");
|
||||
} else if (currentAnimationName === "none" || styles?.display === "none") {
|
||||
send("UNMOUNT");
|
||||
} else {
|
||||
const isAnimating = prevAnimationName !== currentAnimationName;
|
||||
if (wasPresent && isAnimating) {
|
||||
send("ANIMATION_OUT");
|
||||
} else {
|
||||
send("UNMOUNT");
|
||||
}
|
||||
}
|
||||
prevPresentRef.current = present;
|
||||
}
|
||||
}, [present, send]);
|
||||
useLayoutEffect2(() => {
|
||||
if (node) {
|
||||
let timeoutId;
|
||||
const ownerWindow = node.ownerDocument.defaultView ?? window;
|
||||
const handleAnimationEnd = (event) => {
|
||||
const currentAnimationName = getAnimationName(stylesRef.current);
|
||||
const isCurrentAnimation = currentAnimationName.includes(CSS.escape(event.animationName));
|
||||
if (event.target === node && isCurrentAnimation) {
|
||||
send("ANIMATION_END");
|
||||
if (!prevPresentRef.current) {
|
||||
const currentFillMode = node.style.animationFillMode;
|
||||
node.style.animationFillMode = "forwards";
|
||||
timeoutId = ownerWindow.setTimeout(() => {
|
||||
if (node.style.animationFillMode === "forwards") {
|
||||
node.style.animationFillMode = currentFillMode;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleAnimationStart = (event) => {
|
||||
if (event.target === node) {
|
||||
prevAnimationNameRef.current = getAnimationName(stylesRef.current);
|
||||
}
|
||||
};
|
||||
node.addEventListener("animationstart", handleAnimationStart);
|
||||
node.addEventListener("animationcancel", handleAnimationEnd);
|
||||
node.addEventListener("animationend", handleAnimationEnd);
|
||||
return () => {
|
||||
ownerWindow.clearTimeout(timeoutId);
|
||||
node.removeEventListener("animationstart", handleAnimationStart);
|
||||
node.removeEventListener("animationcancel", handleAnimationEnd);
|
||||
node.removeEventListener("animationend", handleAnimationEnd);
|
||||
};
|
||||
} else {
|
||||
send("ANIMATION_END");
|
||||
}
|
||||
}, [node, send]);
|
||||
return {
|
||||
isPresent: ["mounted", "unmountSuspended"].includes(state),
|
||||
ref: React2.useCallback((node2) => {
|
||||
stylesRef.current = node2 ? getComputedStyle(node2) : null;
|
||||
setNode(node2);
|
||||
}, [])
|
||||
};
|
||||
}
|
||||
function getAnimationName(styles) {
|
||||
return styles?.animationName || "none";
|
||||
}
|
||||
function getElementRef(element) {
|
||||
let getter = Object.getOwnPropertyDescriptor(element.props, "ref")?.get;
|
||||
let mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
|
||||
if (mayWarn) {
|
||||
return element.ref;
|
||||
}
|
||||
getter = Object.getOwnPropertyDescriptor(element, "ref")?.get;
|
||||
mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
|
||||
if (mayWarn) {
|
||||
return element.props.ref;
|
||||
}
|
||||
return element.props.ref || element.ref;
|
||||
}
|
||||
|
||||
export {
|
||||
Presence
|
||||
};
|
||||
//# sourceMappingURL=chunk-SMEXDMMQ.js.map
|
||||
7
app/node_modules/.vite/deps/chunk-SMEXDMMQ.js.map
generated
vendored
7
app/node_modules/.vite/deps/chunk-SMEXDMMQ.js.map
generated
vendored
File diff suppressed because one or more lines are too long
158486
app/node_modules/.vite/deps/chunk-TGAC7IAX.js
generated
vendored
158486
app/node_modules/.vite/deps/chunk-TGAC7IAX.js
generated
vendored
File diff suppressed because one or more lines are too long
7
app/node_modules/.vite/deps/chunk-TGAC7IAX.js.map
generated
vendored
7
app/node_modules/.vite/deps/chunk-TGAC7IAX.js.map
generated
vendored
File diff suppressed because one or more lines are too long
241
app/node_modules/.vite/deps/chunk-VPWBNV4W.js
generated
vendored
241
app/node_modules/.vite/deps/chunk-VPWBNV4W.js
generated
vendored
@@ -1,241 +0,0 @@
|
||||
import {
|
||||
require_react_dom
|
||||
} from "./chunk-YF4B4G2L.js";
|
||||
import {
|
||||
composeRefs
|
||||
} from "./chunk-2VUH7NEY.js";
|
||||
import {
|
||||
require_jsx_runtime
|
||||
} from "./chunk-2YVA4HRZ.js";
|
||||
import {
|
||||
require_react
|
||||
} from "./chunk-WUR7D6NS.js";
|
||||
import {
|
||||
__toESM
|
||||
} from "./chunk-G3PMV62Z.js";
|
||||
|
||||
// node_modules/@radix-ui/primitive/dist/index.mjs
|
||||
var canUseDOM = !!(typeof window !== "undefined" && window.document && window.document.createElement);
|
||||
function composeEventHandlers(originalEventHandler, ourEventHandler, { checkForDefaultPrevented = true } = {}) {
|
||||
return function handleEvent(event) {
|
||||
originalEventHandler?.(event);
|
||||
if (checkForDefaultPrevented === false || !event.defaultPrevented) {
|
||||
return ourEventHandler?.(event);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// node_modules/@radix-ui/react-context/dist/index.mjs
|
||||
var React = __toESM(require_react(), 1);
|
||||
var import_jsx_runtime = __toESM(require_jsx_runtime(), 1);
|
||||
function createContextScope(scopeName, createContextScopeDeps = []) {
|
||||
let defaultContexts = [];
|
||||
function createContext3(rootComponentName, defaultContext) {
|
||||
const BaseContext = React.createContext(defaultContext);
|
||||
const index = defaultContexts.length;
|
||||
defaultContexts = [...defaultContexts, defaultContext];
|
||||
const Provider = (props) => {
|
||||
const { scope, children, ...context } = props;
|
||||
const Context = scope?.[scopeName]?.[index] || BaseContext;
|
||||
const value = React.useMemo(() => context, Object.values(context));
|
||||
return (0, import_jsx_runtime.jsx)(Context.Provider, { value, children });
|
||||
};
|
||||
Provider.displayName = rootComponentName + "Provider";
|
||||
function useContext22(consumerName, scope) {
|
||||
const Context = scope?.[scopeName]?.[index] || BaseContext;
|
||||
const context = React.useContext(Context);
|
||||
if (context) return context;
|
||||
if (defaultContext !== void 0) return defaultContext;
|
||||
throw new Error(`\`${consumerName}\` must be used within \`${rootComponentName}\``);
|
||||
}
|
||||
return [Provider, useContext22];
|
||||
}
|
||||
const createScope = () => {
|
||||
const scopeContexts = defaultContexts.map((defaultContext) => {
|
||||
return React.createContext(defaultContext);
|
||||
});
|
||||
return function useScope(scope) {
|
||||
const contexts = scope?.[scopeName] || scopeContexts;
|
||||
return React.useMemo(
|
||||
() => ({ [`__scope${scopeName}`]: { ...scope, [scopeName]: contexts } }),
|
||||
[scope, contexts]
|
||||
);
|
||||
};
|
||||
};
|
||||
createScope.scopeName = scopeName;
|
||||
return [createContext3, composeContextScopes(createScope, ...createContextScopeDeps)];
|
||||
}
|
||||
function composeContextScopes(...scopes) {
|
||||
const baseScope = scopes[0];
|
||||
if (scopes.length === 1) return baseScope;
|
||||
const createScope = () => {
|
||||
const scopeHooks = scopes.map((createScope2) => ({
|
||||
useScope: createScope2(),
|
||||
scopeName: createScope2.scopeName
|
||||
}));
|
||||
return function useComposedScopes(overrideScopes) {
|
||||
const nextScopes = scopeHooks.reduce((nextScopes2, { useScope, scopeName }) => {
|
||||
const scopeProps = useScope(overrideScopes);
|
||||
const currentScope = scopeProps[`__scope${scopeName}`];
|
||||
return { ...nextScopes2, ...currentScope };
|
||||
}, {});
|
||||
return React.useMemo(() => ({ [`__scope${baseScope.scopeName}`]: nextScopes }), [nextScopes]);
|
||||
};
|
||||
};
|
||||
createScope.scopeName = baseScope.scopeName;
|
||||
return createScope;
|
||||
}
|
||||
|
||||
// node_modules/@radix-ui/react-primitive/dist/index.mjs
|
||||
var React3 = __toESM(require_react(), 1);
|
||||
var ReactDOM = __toESM(require_react_dom(), 1);
|
||||
|
||||
// node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot/dist/index.mjs
|
||||
var React2 = __toESM(require_react(), 1);
|
||||
var import_jsx_runtime2 = __toESM(require_jsx_runtime(), 1);
|
||||
function createSlot(ownerName) {
|
||||
const SlotClone = createSlotClone(ownerName);
|
||||
const Slot2 = React2.forwardRef((props, forwardedRef) => {
|
||||
const { children, ...slotProps } = props;
|
||||
const childrenArray = React2.Children.toArray(children);
|
||||
const slottable = childrenArray.find(isSlottable);
|
||||
if (slottable) {
|
||||
const newElement = slottable.props.children;
|
||||
const newChildren = childrenArray.map((child) => {
|
||||
if (child === slottable) {
|
||||
if (React2.Children.count(newElement) > 1) return React2.Children.only(null);
|
||||
return React2.isValidElement(newElement) ? newElement.props.children : null;
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
});
|
||||
return (0, import_jsx_runtime2.jsx)(SlotClone, { ...slotProps, ref: forwardedRef, children: React2.isValidElement(newElement) ? React2.cloneElement(newElement, void 0, newChildren) : null });
|
||||
}
|
||||
return (0, import_jsx_runtime2.jsx)(SlotClone, { ...slotProps, ref: forwardedRef, children });
|
||||
});
|
||||
Slot2.displayName = `${ownerName}.Slot`;
|
||||
return Slot2;
|
||||
}
|
||||
var Slot = createSlot("Slot");
|
||||
function createSlotClone(ownerName) {
|
||||
const SlotClone = React2.forwardRef((props, forwardedRef) => {
|
||||
const { children, ...slotProps } = props;
|
||||
if (React2.isValidElement(children)) {
|
||||
const childrenRef = getElementRef(children);
|
||||
const props2 = mergeProps(slotProps, children.props);
|
||||
if (children.type !== React2.Fragment) {
|
||||
props2.ref = forwardedRef ? composeRefs(forwardedRef, childrenRef) : childrenRef;
|
||||
}
|
||||
return React2.cloneElement(children, props2);
|
||||
}
|
||||
return React2.Children.count(children) > 1 ? React2.Children.only(null) : null;
|
||||
});
|
||||
SlotClone.displayName = `${ownerName}.SlotClone`;
|
||||
return SlotClone;
|
||||
}
|
||||
var SLOTTABLE_IDENTIFIER = /* @__PURE__ */ Symbol("radix.slottable");
|
||||
function createSlottable(ownerName) {
|
||||
const Slottable2 = ({ children }) => {
|
||||
return (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children });
|
||||
};
|
||||
Slottable2.displayName = `${ownerName}.Slottable`;
|
||||
Slottable2.__radixId = SLOTTABLE_IDENTIFIER;
|
||||
return Slottable2;
|
||||
}
|
||||
var Slottable = createSlottable("Slottable");
|
||||
function isSlottable(child) {
|
||||
return React2.isValidElement(child) && typeof child.type === "function" && "__radixId" in child.type && child.type.__radixId === SLOTTABLE_IDENTIFIER;
|
||||
}
|
||||
function mergeProps(slotProps, childProps) {
|
||||
const overrideProps = { ...childProps };
|
||||
for (const propName in childProps) {
|
||||
const slotPropValue = slotProps[propName];
|
||||
const childPropValue = childProps[propName];
|
||||
const isHandler = /^on[A-Z]/.test(propName);
|
||||
if (isHandler) {
|
||||
if (slotPropValue && childPropValue) {
|
||||
overrideProps[propName] = (...args) => {
|
||||
const result = childPropValue(...args);
|
||||
slotPropValue(...args);
|
||||
return result;
|
||||
};
|
||||
} else if (slotPropValue) {
|
||||
overrideProps[propName] = slotPropValue;
|
||||
}
|
||||
} else if (propName === "style") {
|
||||
overrideProps[propName] = { ...slotPropValue, ...childPropValue };
|
||||
} else if (propName === "className") {
|
||||
overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(" ");
|
||||
}
|
||||
}
|
||||
return { ...slotProps, ...overrideProps };
|
||||
}
|
||||
function getElementRef(element) {
|
||||
let getter = Object.getOwnPropertyDescriptor(element.props, "ref")?.get;
|
||||
let mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
|
||||
if (mayWarn) {
|
||||
return element.ref;
|
||||
}
|
||||
getter = Object.getOwnPropertyDescriptor(element, "ref")?.get;
|
||||
mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
|
||||
if (mayWarn) {
|
||||
return element.props.ref;
|
||||
}
|
||||
return element.props.ref || element.ref;
|
||||
}
|
||||
|
||||
// node_modules/@radix-ui/react-primitive/dist/index.mjs
|
||||
var import_jsx_runtime3 = __toESM(require_jsx_runtime(), 1);
|
||||
var NODES = [
|
||||
"a",
|
||||
"button",
|
||||
"div",
|
||||
"form",
|
||||
"h2",
|
||||
"h3",
|
||||
"img",
|
||||
"input",
|
||||
"label",
|
||||
"li",
|
||||
"nav",
|
||||
"ol",
|
||||
"p",
|
||||
"select",
|
||||
"span",
|
||||
"svg",
|
||||
"ul"
|
||||
];
|
||||
var Primitive = NODES.reduce((primitive, node) => {
|
||||
const Slot2 = createSlot(`Primitive.${node}`);
|
||||
const Node = React3.forwardRef((props, forwardedRef) => {
|
||||
const { asChild, ...primitiveProps } = props;
|
||||
const Comp = asChild ? Slot2 : node;
|
||||
if (typeof window !== "undefined") {
|
||||
window[/* @__PURE__ */ Symbol.for("radix-ui")] = true;
|
||||
}
|
||||
return (0, import_jsx_runtime3.jsx)(Comp, { ...primitiveProps, ref: forwardedRef });
|
||||
});
|
||||
Node.displayName = `Primitive.${node}`;
|
||||
return { ...primitive, [node]: Node };
|
||||
}, {});
|
||||
function dispatchDiscreteCustomEvent(target, event) {
|
||||
if (target) ReactDOM.flushSync(() => target.dispatchEvent(event));
|
||||
}
|
||||
|
||||
// node_modules/@radix-ui/react-direction/dist/index.mjs
|
||||
var React4 = __toESM(require_react(), 1);
|
||||
var import_jsx_runtime4 = __toESM(require_jsx_runtime(), 1);
|
||||
var DirectionContext = React4.createContext(void 0);
|
||||
function useDirection(localDir) {
|
||||
const globalDir = React4.useContext(DirectionContext);
|
||||
return localDir || globalDir || "ltr";
|
||||
}
|
||||
|
||||
export {
|
||||
composeEventHandlers,
|
||||
createContextScope,
|
||||
Primitive,
|
||||
dispatchDiscreteCustomEvent,
|
||||
useDirection
|
||||
};
|
||||
//# sourceMappingURL=chunk-VPWBNV4W.js.map
|
||||
7
app/node_modules/.vite/deps/chunk-VPWBNV4W.js.map
generated
vendored
7
app/node_modules/.vite/deps/chunk-VPWBNV4W.js.map
generated
vendored
File diff suppressed because one or more lines are too long
311
app/node_modules/.vite/deps/chunk-YJZCGBGU.js
generated
vendored
311
app/node_modules/.vite/deps/chunk-YJZCGBGU.js
generated
vendored
@@ -1,311 +0,0 @@
|
||||
import {
|
||||
require_react
|
||||
} from "./chunk-WUR7D6NS.js";
|
||||
import {
|
||||
__toESM
|
||||
} from "./chunk-G3PMV62Z.js";
|
||||
|
||||
// node_modules/@dnd-kit/utilities/dist/utilities.esm.js
|
||||
var import_react = __toESM(require_react());
|
||||
function useCombinedRefs() {
|
||||
for (var _len = arguments.length, refs = new Array(_len), _key = 0; _key < _len; _key++) {
|
||||
refs[_key] = arguments[_key];
|
||||
}
|
||||
return (0, import_react.useMemo)(
|
||||
() => (node) => {
|
||||
refs.forEach((ref) => ref(node));
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
refs
|
||||
);
|
||||
}
|
||||
var canUseDOM = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined";
|
||||
function isWindow(element) {
|
||||
const elementString = Object.prototype.toString.call(element);
|
||||
return elementString === "[object Window]" || // In Electron context the Window object serializes to [object global]
|
||||
elementString === "[object global]";
|
||||
}
|
||||
function isNode(node) {
|
||||
return "nodeType" in node;
|
||||
}
|
||||
function getWindow(target) {
|
||||
var _target$ownerDocument, _target$ownerDocument2;
|
||||
if (!target) {
|
||||
return window;
|
||||
}
|
||||
if (isWindow(target)) {
|
||||
return target;
|
||||
}
|
||||
if (!isNode(target)) {
|
||||
return window;
|
||||
}
|
||||
return (_target$ownerDocument = (_target$ownerDocument2 = target.ownerDocument) == null ? void 0 : _target$ownerDocument2.defaultView) != null ? _target$ownerDocument : window;
|
||||
}
|
||||
function isDocument(node) {
|
||||
const {
|
||||
Document
|
||||
} = getWindow(node);
|
||||
return node instanceof Document;
|
||||
}
|
||||
function isHTMLElement(node) {
|
||||
if (isWindow(node)) {
|
||||
return false;
|
||||
}
|
||||
return node instanceof getWindow(node).HTMLElement;
|
||||
}
|
||||
function isSVGElement(node) {
|
||||
return node instanceof getWindow(node).SVGElement;
|
||||
}
|
||||
function getOwnerDocument(target) {
|
||||
if (!target) {
|
||||
return document;
|
||||
}
|
||||
if (isWindow(target)) {
|
||||
return target.document;
|
||||
}
|
||||
if (!isNode(target)) {
|
||||
return document;
|
||||
}
|
||||
if (isDocument(target)) {
|
||||
return target;
|
||||
}
|
||||
if (isHTMLElement(target) || isSVGElement(target)) {
|
||||
return target.ownerDocument;
|
||||
}
|
||||
return document;
|
||||
}
|
||||
var useIsomorphicLayoutEffect = canUseDOM ? import_react.useLayoutEffect : import_react.useEffect;
|
||||
function useEvent(handler) {
|
||||
const handlerRef = (0, import_react.useRef)(handler);
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
handlerRef.current = handler;
|
||||
});
|
||||
return (0, import_react.useCallback)(function() {
|
||||
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
||||
args[_key] = arguments[_key];
|
||||
}
|
||||
return handlerRef.current == null ? void 0 : handlerRef.current(...args);
|
||||
}, []);
|
||||
}
|
||||
function useInterval() {
|
||||
const intervalRef = (0, import_react.useRef)(null);
|
||||
const set = (0, import_react.useCallback)((listener, duration) => {
|
||||
intervalRef.current = setInterval(listener, duration);
|
||||
}, []);
|
||||
const clear = (0, import_react.useCallback)(() => {
|
||||
if (intervalRef.current !== null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
return [set, clear];
|
||||
}
|
||||
function useLatestValue(value, dependencies) {
|
||||
if (dependencies === void 0) {
|
||||
dependencies = [value];
|
||||
}
|
||||
const valueRef = (0, import_react.useRef)(value);
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (valueRef.current !== value) {
|
||||
valueRef.current = value;
|
||||
}
|
||||
}, dependencies);
|
||||
return valueRef;
|
||||
}
|
||||
function useLazyMemo(callback, dependencies) {
|
||||
const valueRef = (0, import_react.useRef)();
|
||||
return (0, import_react.useMemo)(
|
||||
() => {
|
||||
const newValue = callback(valueRef.current);
|
||||
valueRef.current = newValue;
|
||||
return newValue;
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[...dependencies]
|
||||
);
|
||||
}
|
||||
function useNodeRef(onChange) {
|
||||
const onChangeHandler = useEvent(onChange);
|
||||
const node = (0, import_react.useRef)(null);
|
||||
const setNodeRef = (0, import_react.useCallback)(
|
||||
(element) => {
|
||||
if (element !== node.current) {
|
||||
onChangeHandler == null ? void 0 : onChangeHandler(element, node.current);
|
||||
}
|
||||
node.current = element;
|
||||
},
|
||||
//eslint-disable-next-line
|
||||
[]
|
||||
);
|
||||
return [node, setNodeRef];
|
||||
}
|
||||
function usePrevious(value) {
|
||||
const ref = (0, import_react.useRef)();
|
||||
(0, import_react.useEffect)(() => {
|
||||
ref.current = value;
|
||||
}, [value]);
|
||||
return ref.current;
|
||||
}
|
||||
var ids = {};
|
||||
function useUniqueId(prefix, value) {
|
||||
return (0, import_react.useMemo)(() => {
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
const id = ids[prefix] == null ? 0 : ids[prefix] + 1;
|
||||
ids[prefix] = id;
|
||||
return prefix + "-" + id;
|
||||
}, [prefix, value]);
|
||||
}
|
||||
function createAdjustmentFn(modifier) {
|
||||
return function(object) {
|
||||
for (var _len = arguments.length, adjustments = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
||||
adjustments[_key - 1] = arguments[_key];
|
||||
}
|
||||
return adjustments.reduce((accumulator, adjustment) => {
|
||||
const entries = Object.entries(adjustment);
|
||||
for (const [key, valueAdjustment] of entries) {
|
||||
const value = accumulator[key];
|
||||
if (value != null) {
|
||||
accumulator[key] = value + modifier * valueAdjustment;
|
||||
}
|
||||
}
|
||||
return accumulator;
|
||||
}, {
|
||||
...object
|
||||
});
|
||||
};
|
||||
}
|
||||
var add = createAdjustmentFn(1);
|
||||
var subtract = createAdjustmentFn(-1);
|
||||
function hasViewportRelativeCoordinates(event) {
|
||||
return "clientX" in event && "clientY" in event;
|
||||
}
|
||||
function isKeyboardEvent(event) {
|
||||
if (!event) {
|
||||
return false;
|
||||
}
|
||||
const {
|
||||
KeyboardEvent
|
||||
} = getWindow(event.target);
|
||||
return KeyboardEvent && event instanceof KeyboardEvent;
|
||||
}
|
||||
function isTouchEvent(event) {
|
||||
if (!event) {
|
||||
return false;
|
||||
}
|
||||
const {
|
||||
TouchEvent
|
||||
} = getWindow(event.target);
|
||||
return TouchEvent && event instanceof TouchEvent;
|
||||
}
|
||||
function getEventCoordinates(event) {
|
||||
if (isTouchEvent(event)) {
|
||||
if (event.touches && event.touches.length) {
|
||||
const {
|
||||
clientX: x,
|
||||
clientY: y
|
||||
} = event.touches[0];
|
||||
return {
|
||||
x,
|
||||
y
|
||||
};
|
||||
} else if (event.changedTouches && event.changedTouches.length) {
|
||||
const {
|
||||
clientX: x,
|
||||
clientY: y
|
||||
} = event.changedTouches[0];
|
||||
return {
|
||||
x,
|
||||
y
|
||||
};
|
||||
}
|
||||
}
|
||||
if (hasViewportRelativeCoordinates(event)) {
|
||||
return {
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
var CSS = Object.freeze({
|
||||
Translate: {
|
||||
toString(transform) {
|
||||
if (!transform) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
x,
|
||||
y
|
||||
} = transform;
|
||||
return "translate3d(" + (x ? Math.round(x) : 0) + "px, " + (y ? Math.round(y) : 0) + "px, 0)";
|
||||
}
|
||||
},
|
||||
Scale: {
|
||||
toString(transform) {
|
||||
if (!transform) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
scaleX,
|
||||
scaleY
|
||||
} = transform;
|
||||
return "scaleX(" + scaleX + ") scaleY(" + scaleY + ")";
|
||||
}
|
||||
},
|
||||
Transform: {
|
||||
toString(transform) {
|
||||
if (!transform) {
|
||||
return;
|
||||
}
|
||||
return [CSS.Translate.toString(transform), CSS.Scale.toString(transform)].join(" ");
|
||||
}
|
||||
},
|
||||
Transition: {
|
||||
toString(_ref) {
|
||||
let {
|
||||
property,
|
||||
duration,
|
||||
easing
|
||||
} = _ref;
|
||||
return property + " " + duration + "ms " + easing;
|
||||
}
|
||||
}
|
||||
});
|
||||
var SELECTOR = "a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]";
|
||||
function findFirstFocusableNode(element) {
|
||||
if (element.matches(SELECTOR)) {
|
||||
return element;
|
||||
}
|
||||
return element.querySelector(SELECTOR);
|
||||
}
|
||||
|
||||
export {
|
||||
useCombinedRefs,
|
||||
canUseDOM,
|
||||
isWindow,
|
||||
isNode,
|
||||
getWindow,
|
||||
isDocument,
|
||||
isHTMLElement,
|
||||
isSVGElement,
|
||||
getOwnerDocument,
|
||||
useIsomorphicLayoutEffect,
|
||||
useEvent,
|
||||
useInterval,
|
||||
useLatestValue,
|
||||
useLazyMemo,
|
||||
useNodeRef,
|
||||
usePrevious,
|
||||
useUniqueId,
|
||||
add,
|
||||
subtract,
|
||||
hasViewportRelativeCoordinates,
|
||||
isKeyboardEvent,
|
||||
isTouchEvent,
|
||||
getEventCoordinates,
|
||||
CSS,
|
||||
findFirstFocusableNode
|
||||
};
|
||||
//# sourceMappingURL=chunk-YJZCGBGU.js.map
|
||||
7
app/node_modules/.vite/deps/chunk-YJZCGBGU.js.map
generated
vendored
7
app/node_modules/.vite/deps/chunk-YJZCGBGU.js.map
generated
vendored
File diff suppressed because one or more lines are too long
6
app/node_modules/.vite/deps/recharts.js
generated
vendored
6
app/node_modules/.vite/deps/recharts.js
generated
vendored
@@ -1,3 +1,6 @@
|
||||
import {
|
||||
clsx_default
|
||||
} from "./chunk-U7P2NEEE.js";
|
||||
import {
|
||||
_extends
|
||||
} from "./chunk-EQCCHGRT.js";
|
||||
@@ -7,9 +10,6 @@ import {
|
||||
import {
|
||||
require_react
|
||||
} from "./chunk-WUR7D6NS.js";
|
||||
import {
|
||||
clsx_default
|
||||
} from "./chunk-U7P2NEEE.js";
|
||||
import {
|
||||
__commonJS,
|
||||
__export,
|
||||
|
||||
711
app/node_modules/.vite/deps/three-stdlib.js
generated
vendored
711
app/node_modules/.vite/deps/three-stdlib.js
generated
vendored
@@ -1,711 +0,0 @@
|
||||
import {
|
||||
ACESFilmicToneMappingShader,
|
||||
AMFLoader,
|
||||
ARButton,
|
||||
AdaptiveToneMappingPass,
|
||||
AfterimagePass,
|
||||
AfterimageShader,
|
||||
AmmoPhysics,
|
||||
AnaglyphEffect,
|
||||
AnimationClipCreator,
|
||||
ArcballControls,
|
||||
AsciiEffect,
|
||||
AssimpLoader,
|
||||
BVHLoader,
|
||||
BasicShader,
|
||||
BasisTextureLoader,
|
||||
BatchedMesh,
|
||||
BleachBypassShader,
|
||||
BlendShader,
|
||||
BloomPass,
|
||||
BlurShaderUtils,
|
||||
BokehDepthShader,
|
||||
BokehPass,
|
||||
BokehShader,
|
||||
BokehShader2,
|
||||
BoxLineGeometry,
|
||||
BrightnessContrastShader,
|
||||
CCDIKHelper,
|
||||
CCDIKSolver,
|
||||
CSM,
|
||||
CSMFrustum,
|
||||
CSMHelper,
|
||||
CSMShader,
|
||||
CSS2DObject,
|
||||
CSS2DRenderer,
|
||||
CSS3DObject,
|
||||
CSS3DRenderer,
|
||||
CSS3DSprite,
|
||||
CameraControls,
|
||||
Capsule,
|
||||
CinematicCamera,
|
||||
CinquefoilKnot,
|
||||
ClearMaskPass,
|
||||
ClearPass,
|
||||
ColladaExporter,
|
||||
ColladaLoader,
|
||||
ColorConverter,
|
||||
ColorCorrectionShader,
|
||||
ColorMapKeywords,
|
||||
ColorifyShader,
|
||||
ConvexGeometry,
|
||||
ConvexHull,
|
||||
ConvexObjectBreaker,
|
||||
ConvolutionShader,
|
||||
CopyShader,
|
||||
CubeTexturePass,
|
||||
DDSLoader,
|
||||
DOFMipMapShader,
|
||||
DRACOExporter,
|
||||
DRACOLoader,
|
||||
DecalGeometry,
|
||||
DecalVertex,
|
||||
DecoratedTorusKnot4a,
|
||||
DecoratedTorusKnot4b,
|
||||
DecoratedTorusKnot5a,
|
||||
DecoratedTorusKnot5c,
|
||||
DepthLimitedBlurShader,
|
||||
DeviceOrientationControls,
|
||||
DigitalGlitch,
|
||||
DotScreenPass,
|
||||
DotScreenShader,
|
||||
DragControls,
|
||||
EXRLoader,
|
||||
EdgeSplitModifier,
|
||||
EffectComposer,
|
||||
FBXLoader,
|
||||
FXAAShader,
|
||||
Face,
|
||||
Face3,
|
||||
FigureEightPolynomialKnot,
|
||||
FilmPass,
|
||||
FilmShader,
|
||||
FirstPersonControls,
|
||||
FlakesTexture,
|
||||
Flow,
|
||||
FlyControls,
|
||||
FocusShader,
|
||||
Font,
|
||||
FontLoader,
|
||||
FreiChenShader,
|
||||
FresnelShader,
|
||||
FullScreenQuad,
|
||||
GCodeLoader,
|
||||
GLTFExporter,
|
||||
GLTFLoader,
|
||||
GPUComputationRenderer,
|
||||
GammaCorrectionShader,
|
||||
Geometry,
|
||||
GeometryCompressionUtils,
|
||||
GeometryUtils,
|
||||
GlitchPass,
|
||||
GodRaysCombineShader,
|
||||
GodRaysDepthMaskShader,
|
||||
GodRaysFakeSunShader,
|
||||
GodRaysGenerateShader,
|
||||
GrannyKnot,
|
||||
GroundProjectedEnv,
|
||||
Gyroscope,
|
||||
HDRCubeTextureLoader,
|
||||
HTMLMesh,
|
||||
HalfEdge,
|
||||
HalftonePass,
|
||||
HalftoneShader,
|
||||
HeartCurve,
|
||||
HelixCurve,
|
||||
HorizontalBlurShader,
|
||||
HorizontalTiltShiftShader,
|
||||
HueSaturationShader,
|
||||
ImprovedNoise,
|
||||
InstancedFlow,
|
||||
InteractiveGroup,
|
||||
KMZLoader,
|
||||
KTX2Loader,
|
||||
KTXLoader,
|
||||
KaleidoShader,
|
||||
KnotCurve,
|
||||
LDrawLoader,
|
||||
LUT3dlLoader,
|
||||
LUTCubeLoader,
|
||||
LUTPass,
|
||||
LWOLoader,
|
||||
Lensflare,
|
||||
LensflareElement,
|
||||
LightProbeGenerator,
|
||||
LightProbeHelper,
|
||||
LightningStorm,
|
||||
LightningStrike,
|
||||
Line2,
|
||||
LineGeometry,
|
||||
LineMaterial,
|
||||
LineSegments2,
|
||||
LineSegmentsGeometry,
|
||||
LottieLoader,
|
||||
LuminosityHighPassShader,
|
||||
LuminosityShader,
|
||||
Lut,
|
||||
MD2Character,
|
||||
MD2CharacterComplex,
|
||||
MD2Loader,
|
||||
MDDLoader,
|
||||
MMDAnimationHelper,
|
||||
MMDExporter,
|
||||
MMDLoader,
|
||||
MMDPhysics,
|
||||
MTLLoader,
|
||||
MapControls,
|
||||
MapControlsExp,
|
||||
MarchingCubes,
|
||||
MaskPass,
|
||||
MeshSurfaceSampler,
|
||||
MeshoptDecoder,
|
||||
MirrorShader,
|
||||
MorphAnimMesh,
|
||||
MorphBlendMesh,
|
||||
MotionController,
|
||||
MotionControllerConstants,
|
||||
NRRDLoader,
|
||||
NURBSCurve,
|
||||
NURBSSurface,
|
||||
NormalMapShader,
|
||||
OBB,
|
||||
OBJExporter,
|
||||
OBJLoader,
|
||||
Octree,
|
||||
OculusHandModel,
|
||||
OculusHandPointerModel,
|
||||
OrbitControls,
|
||||
OrbitControlsExp,
|
||||
OutlineEffect,
|
||||
OutlinePass,
|
||||
PCDLoader,
|
||||
PDBLoader,
|
||||
PLYExporter,
|
||||
PLYLoader,
|
||||
PRWMLoader,
|
||||
PVRLoader,
|
||||
PackedPhongMaterial,
|
||||
ParallaxBarrierEffect,
|
||||
ParallaxShader,
|
||||
ParametricGeometries,
|
||||
ParametricGeometry,
|
||||
Pass,
|
||||
PeppersGhostEffect,
|
||||
PixelShader,
|
||||
PointerLockControls,
|
||||
PositionalAudioHelper,
|
||||
ProgressiveLightMap,
|
||||
Projector,
|
||||
RGBELoader,
|
||||
RGBMLoader,
|
||||
RGBShiftShader,
|
||||
RaycasterHelper,
|
||||
RectAreaLightHelper,
|
||||
RectAreaLightUniformsLib,
|
||||
Reflector,
|
||||
ReflectorForSSRPass,
|
||||
ReflectorRTT,
|
||||
Refractor,
|
||||
RenderPass,
|
||||
RenderPixelatedPass,
|
||||
RenderableFace,
|
||||
RenderableLine,
|
||||
RenderableObject,
|
||||
RenderableSprite,
|
||||
RenderableVertex,
|
||||
Rhino3dmLoader,
|
||||
RollerCoasterGeometry,
|
||||
RollerCoasterLiftersGeometry,
|
||||
RollerCoasterShadowGeometry,
|
||||
RoomEnvironment,
|
||||
RoughnessMipmapper,
|
||||
RoundedBoxGeometry,
|
||||
SAOPass,
|
||||
SAOShader,
|
||||
SMAABlendShader,
|
||||
SMAAEdgesShader,
|
||||
SMAAPass,
|
||||
SMAAWeightsShader,
|
||||
SSAARenderPass,
|
||||
SSAOBlurShader,
|
||||
SSAODepthShader,
|
||||
SSAOPass,
|
||||
SSAOShader,
|
||||
SSRBlurShader,
|
||||
SSRDepthShader,
|
||||
SSRPass,
|
||||
SSRShader,
|
||||
STATE,
|
||||
STLExporter,
|
||||
STLLoader,
|
||||
SVGLoader,
|
||||
SVGObject,
|
||||
SVGRenderer,
|
||||
SavePass,
|
||||
SceneUtils,
|
||||
SelectionBox,
|
||||
SelectionHelper,
|
||||
SepiaShader,
|
||||
ShaderPass,
|
||||
ShadowMapViewer,
|
||||
ShadowMesh,
|
||||
SimplexNoise,
|
||||
SimplifyModifier,
|
||||
SkeletonUtils,
|
||||
Sky,
|
||||
SkyGeometry,
|
||||
SobelOperatorShader,
|
||||
StereoEffect,
|
||||
SubsurfaceScatteringShader,
|
||||
TAARenderPass,
|
||||
TDSLoader,
|
||||
TGALoader,
|
||||
TTFLoader,
|
||||
TeapotGeometry,
|
||||
TechnicolorShader,
|
||||
TessellateModifier,
|
||||
TextGeometry,
|
||||
TexturePass,
|
||||
ThreeMFLoader,
|
||||
TiltLoader,
|
||||
Timer,
|
||||
ToneMapShader,
|
||||
ToonShader1,
|
||||
ToonShader2,
|
||||
ToonShaderDotted,
|
||||
ToonShaderHatching,
|
||||
TorusKnot,
|
||||
TrackballControls,
|
||||
TrackballControlsExp,
|
||||
TransformControls,
|
||||
TransformControlsGizmo,
|
||||
TransformControlsPlane,
|
||||
TreesGeometry,
|
||||
TrefoilKnot,
|
||||
TrefoilPolynomialKnot,
|
||||
TriangleBlurShader,
|
||||
TubePainter,
|
||||
USDZExporter,
|
||||
UVsDebug,
|
||||
UnpackDepthRGBAShader,
|
||||
UnrealBloomPass,
|
||||
VOXData3DTexture,
|
||||
VOXLoader,
|
||||
VOXMesh,
|
||||
VRButton,
|
||||
VRMLLoader,
|
||||
VRMLoader,
|
||||
VTKLoader,
|
||||
VertexList,
|
||||
VertexNode,
|
||||
VertexNormalsHelper,
|
||||
VertexTangentsHelper,
|
||||
VerticalBlurShader,
|
||||
VerticalTiltShiftShader,
|
||||
VignetteShader,
|
||||
VivianiCurve,
|
||||
Volume,
|
||||
VolumeRenderShader1,
|
||||
VolumeSlice,
|
||||
Water,
|
||||
Water2,
|
||||
WaterPass,
|
||||
WaterRefractionShader,
|
||||
Wireframe,
|
||||
WireframeGeometry2,
|
||||
XLoader,
|
||||
XRControllerModelFactory,
|
||||
XREstimatedLight,
|
||||
XRHandMeshModel,
|
||||
XRHandModelFactory,
|
||||
XRHandPrimitiveModel,
|
||||
XYZLoader,
|
||||
calcBSplineDerivatives,
|
||||
calcBSplinePoint,
|
||||
calcBasisFunctionDerivatives,
|
||||
calcBasisFunctions,
|
||||
calcKoverI,
|
||||
calcNURBSDerivatives,
|
||||
calcRationalCurveDerivatives,
|
||||
calcSurfacePoint,
|
||||
computeMorphedAttributes,
|
||||
createText,
|
||||
edgeTable,
|
||||
estimateBytesUsed,
|
||||
fetchProfile,
|
||||
fetchProfilesList,
|
||||
findSpan,
|
||||
getErrorMessage,
|
||||
getUniforms,
|
||||
getWebGL2ErrorMessage,
|
||||
getWebGLErrorMessage,
|
||||
initSplineTexture,
|
||||
interleaveAttributes,
|
||||
isWebGL2Available,
|
||||
isWebGLAvailable,
|
||||
mergeBufferAttributes,
|
||||
mergeBufferGeometries,
|
||||
mergeVertices,
|
||||
modifyShader,
|
||||
toCreasedNormals,
|
||||
toTrianglesDrawMode,
|
||||
triTable,
|
||||
updateSplineTexture
|
||||
} from "./chunk-TGAC7IAX.js";
|
||||
import "./chunk-L3Z576C2.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export {
|
||||
ACESFilmicToneMappingShader,
|
||||
AMFLoader,
|
||||
ARButton,
|
||||
AdaptiveToneMappingPass,
|
||||
AfterimagePass,
|
||||
AfterimageShader,
|
||||
AmmoPhysics,
|
||||
AnaglyphEffect,
|
||||
AnimationClipCreator,
|
||||
ArcballControls,
|
||||
AsciiEffect,
|
||||
AssimpLoader,
|
||||
BVHLoader,
|
||||
BasicShader,
|
||||
BasisTextureLoader,
|
||||
BatchedMesh,
|
||||
BleachBypassShader,
|
||||
BlendShader,
|
||||
BloomPass,
|
||||
BlurShaderUtils,
|
||||
BokehDepthShader,
|
||||
BokehPass,
|
||||
BokehShader,
|
||||
BokehShader2,
|
||||
BoxLineGeometry,
|
||||
BrightnessContrastShader,
|
||||
CCDIKHelper,
|
||||
CCDIKSolver,
|
||||
CSM,
|
||||
CSMFrustum,
|
||||
CSMHelper,
|
||||
CSMShader,
|
||||
CSS2DObject,
|
||||
CSS2DRenderer,
|
||||
CSS3DObject,
|
||||
CSS3DRenderer,
|
||||
CSS3DSprite,
|
||||
CameraControls,
|
||||
Capsule,
|
||||
CinematicCamera,
|
||||
CinquefoilKnot,
|
||||
ClearMaskPass,
|
||||
ClearPass,
|
||||
ColladaExporter,
|
||||
ColladaLoader,
|
||||
ColorConverter,
|
||||
ColorCorrectionShader,
|
||||
ColorMapKeywords,
|
||||
ColorifyShader,
|
||||
ConvexGeometry,
|
||||
ConvexHull,
|
||||
ConvexObjectBreaker,
|
||||
ConvolutionShader,
|
||||
CopyShader,
|
||||
CubeTexturePass,
|
||||
DDSLoader,
|
||||
DOFMipMapShader,
|
||||
DRACOExporter,
|
||||
DRACOLoader,
|
||||
DecalGeometry,
|
||||
DecalVertex,
|
||||
DecoratedTorusKnot4a,
|
||||
DecoratedTorusKnot4b,
|
||||
DecoratedTorusKnot5a,
|
||||
DecoratedTorusKnot5c,
|
||||
DepthLimitedBlurShader,
|
||||
DeviceOrientationControls,
|
||||
DigitalGlitch,
|
||||
DotScreenPass,
|
||||
DotScreenShader,
|
||||
DragControls,
|
||||
EXRLoader,
|
||||
EdgeSplitModifier,
|
||||
EffectComposer,
|
||||
FBXLoader,
|
||||
FXAAShader,
|
||||
Face,
|
||||
Face3,
|
||||
FigureEightPolynomialKnot,
|
||||
FilmPass,
|
||||
FilmShader,
|
||||
FirstPersonControls,
|
||||
FlakesTexture,
|
||||
Flow,
|
||||
FlyControls,
|
||||
FocusShader,
|
||||
Font,
|
||||
FontLoader,
|
||||
FreiChenShader,
|
||||
FresnelShader,
|
||||
FullScreenQuad,
|
||||
GCodeLoader,
|
||||
GLTFExporter,
|
||||
GLTFLoader,
|
||||
GPUComputationRenderer,
|
||||
GammaCorrectionShader,
|
||||
Geometry,
|
||||
GeometryCompressionUtils,
|
||||
GeometryUtils,
|
||||
GlitchPass,
|
||||
GodRaysCombineShader,
|
||||
GodRaysDepthMaskShader,
|
||||
GodRaysFakeSunShader,
|
||||
GodRaysGenerateShader,
|
||||
GrannyKnot,
|
||||
GroundProjectedEnv,
|
||||
Gyroscope,
|
||||
HDRCubeTextureLoader,
|
||||
HTMLMesh,
|
||||
HalfEdge,
|
||||
HalftonePass,
|
||||
HalftoneShader,
|
||||
HeartCurve,
|
||||
HelixCurve,
|
||||
HorizontalBlurShader,
|
||||
HorizontalTiltShiftShader,
|
||||
HueSaturationShader,
|
||||
ImprovedNoise,
|
||||
InstancedFlow,
|
||||
InteractiveGroup,
|
||||
KMZLoader,
|
||||
KTX2Loader,
|
||||
KTXLoader,
|
||||
KaleidoShader,
|
||||
KnotCurve,
|
||||
LDrawLoader,
|
||||
LUT3dlLoader,
|
||||
LUTCubeLoader,
|
||||
LUTPass,
|
||||
LWOLoader,
|
||||
Lensflare,
|
||||
LensflareElement,
|
||||
LightProbeGenerator,
|
||||
LightProbeHelper,
|
||||
LightningStorm,
|
||||
LightningStrike,
|
||||
Line2,
|
||||
LineGeometry,
|
||||
LineMaterial,
|
||||
LineSegments2,
|
||||
LineSegmentsGeometry,
|
||||
LottieLoader,
|
||||
LuminosityHighPassShader,
|
||||
LuminosityShader,
|
||||
Lut,
|
||||
MD2Character,
|
||||
MD2CharacterComplex,
|
||||
MD2Loader,
|
||||
MDDLoader,
|
||||
MMDAnimationHelper,
|
||||
MMDExporter,
|
||||
MMDLoader,
|
||||
MMDPhysics,
|
||||
MTLLoader,
|
||||
MapControls,
|
||||
MapControlsExp,
|
||||
MarchingCubes,
|
||||
MaskPass,
|
||||
MeshSurfaceSampler,
|
||||
MeshoptDecoder,
|
||||
MirrorShader,
|
||||
MorphAnimMesh,
|
||||
MorphBlendMesh,
|
||||
MotionController,
|
||||
MotionControllerConstants,
|
||||
NRRDLoader,
|
||||
NURBSCurve,
|
||||
NURBSSurface,
|
||||
NormalMapShader,
|
||||
OBB,
|
||||
OBJExporter,
|
||||
OBJLoader,
|
||||
Octree,
|
||||
OculusHandModel,
|
||||
OculusHandPointerModel,
|
||||
OrbitControls,
|
||||
OrbitControlsExp,
|
||||
OutlineEffect,
|
||||
OutlinePass,
|
||||
PCDLoader,
|
||||
PDBLoader,
|
||||
PLYExporter,
|
||||
PLYLoader,
|
||||
PRWMLoader,
|
||||
PVRLoader,
|
||||
PackedPhongMaterial,
|
||||
ParallaxBarrierEffect,
|
||||
ParallaxShader,
|
||||
ParametricGeometries,
|
||||
ParametricGeometry,
|
||||
Pass,
|
||||
PeppersGhostEffect,
|
||||
PixelShader,
|
||||
PointerLockControls,
|
||||
PositionalAudioHelper,
|
||||
ProgressiveLightMap,
|
||||
Projector,
|
||||
RGBELoader,
|
||||
RGBMLoader,
|
||||
RGBShiftShader,
|
||||
RaycasterHelper,
|
||||
RectAreaLightHelper,
|
||||
RectAreaLightUniformsLib,
|
||||
Reflector,
|
||||
ReflectorForSSRPass,
|
||||
ReflectorRTT,
|
||||
Refractor,
|
||||
RenderPass,
|
||||
RenderPixelatedPass,
|
||||
RenderableFace,
|
||||
RenderableLine,
|
||||
RenderableObject,
|
||||
RenderableSprite,
|
||||
RenderableVertex,
|
||||
Rhino3dmLoader,
|
||||
RollerCoasterGeometry,
|
||||
RollerCoasterLiftersGeometry,
|
||||
RollerCoasterShadowGeometry,
|
||||
RoomEnvironment,
|
||||
RoughnessMipmapper,
|
||||
RoundedBoxGeometry,
|
||||
SAOPass,
|
||||
SAOShader,
|
||||
SMAABlendShader,
|
||||
SMAAEdgesShader,
|
||||
SMAAPass,
|
||||
SMAAWeightsShader,
|
||||
SSAARenderPass,
|
||||
SSAOBlurShader,
|
||||
SSAODepthShader,
|
||||
SSAOPass,
|
||||
SSAOShader,
|
||||
SSRBlurShader,
|
||||
SSRDepthShader,
|
||||
SSRPass,
|
||||
SSRShader,
|
||||
STATE,
|
||||
STLExporter,
|
||||
STLLoader,
|
||||
SVGLoader,
|
||||
SVGObject,
|
||||
SVGRenderer,
|
||||
SavePass,
|
||||
SceneUtils,
|
||||
SelectionBox,
|
||||
SelectionHelper,
|
||||
SepiaShader,
|
||||
ShaderPass,
|
||||
ShadowMapViewer,
|
||||
ShadowMesh,
|
||||
SimplexNoise,
|
||||
SimplifyModifier,
|
||||
SkeletonUtils,
|
||||
Sky,
|
||||
SkyGeometry,
|
||||
SobelOperatorShader,
|
||||
StereoEffect,
|
||||
SubsurfaceScatteringShader,
|
||||
TAARenderPass,
|
||||
TDSLoader,
|
||||
TGALoader,
|
||||
TTFLoader,
|
||||
TeapotGeometry,
|
||||
TechnicolorShader,
|
||||
TessellateModifier,
|
||||
TextGeometry as TextBufferGeometry,
|
||||
TextGeometry,
|
||||
TexturePass,
|
||||
ThreeMFLoader,
|
||||
TiltLoader,
|
||||
Timer,
|
||||
ToneMapShader,
|
||||
ToonShader1,
|
||||
ToonShader2,
|
||||
ToonShaderDotted,
|
||||
ToonShaderHatching,
|
||||
TorusKnot,
|
||||
TrackballControls,
|
||||
TrackballControlsExp,
|
||||
TransformControls,
|
||||
TransformControlsGizmo,
|
||||
TransformControlsPlane,
|
||||
TreesGeometry,
|
||||
TrefoilKnot,
|
||||
TrefoilPolynomialKnot,
|
||||
TriangleBlurShader,
|
||||
TubePainter,
|
||||
USDZExporter,
|
||||
UVsDebug,
|
||||
UnpackDepthRGBAShader,
|
||||
UnrealBloomPass,
|
||||
VOXData3DTexture,
|
||||
VOXLoader,
|
||||
VOXMesh,
|
||||
VRButton,
|
||||
VRMLLoader,
|
||||
VRMLoader,
|
||||
VTKLoader,
|
||||
VertexList,
|
||||
VertexNode,
|
||||
VertexNormalsHelper,
|
||||
VertexTangentsHelper,
|
||||
VerticalBlurShader,
|
||||
VerticalTiltShiftShader,
|
||||
VignetteShader,
|
||||
VivianiCurve,
|
||||
Volume,
|
||||
VolumeRenderShader1,
|
||||
VolumeSlice,
|
||||
Water,
|
||||
Water2,
|
||||
WaterPass,
|
||||
WaterRefractionShader,
|
||||
Wireframe,
|
||||
WireframeGeometry2,
|
||||
XLoader,
|
||||
XRControllerModelFactory,
|
||||
XREstimatedLight,
|
||||
XRHandMeshModel,
|
||||
XRHandModelFactory,
|
||||
XRHandPrimitiveModel,
|
||||
XYZLoader,
|
||||
calcBSplineDerivatives,
|
||||
calcBSplinePoint,
|
||||
calcBasisFunctionDerivatives,
|
||||
calcBasisFunctions,
|
||||
calcKoverI,
|
||||
calcNURBSDerivatives,
|
||||
calcRationalCurveDerivatives,
|
||||
calcSurfacePoint,
|
||||
computeMorphedAttributes,
|
||||
createText,
|
||||
edgeTable,
|
||||
estimateBytesUsed,
|
||||
fetchProfile,
|
||||
fetchProfilesList,
|
||||
findSpan,
|
||||
getErrorMessage,
|
||||
getUniforms,
|
||||
getWebGL2ErrorMessage,
|
||||
getWebGLErrorMessage,
|
||||
initSplineTexture,
|
||||
interleaveAttributes,
|
||||
isWebGL2Available,
|
||||
isWebGLAvailable,
|
||||
mergeBufferAttributes,
|
||||
mergeBufferGeometries,
|
||||
mergeVertices,
|
||||
modifyShader,
|
||||
toCreasedNormals,
|
||||
toTrianglesDrawMode,
|
||||
triTable,
|
||||
updateSplineTexture
|
||||
};
|
||||
7
app/node_modules/.vite/deps/three-stdlib.js.map
generated
vendored
7
app/node_modules/.vite/deps/three-stdlib.js.map
generated
vendored
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
3
app/node_modules/@esbuild/win32-x64/README.md
generated
vendored
3
app/node_modules/@esbuild/win32-x64/README.md
generated
vendored
@@ -1,3 +0,0 @@
|
||||
# esbuild
|
||||
|
||||
This is the Windows 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details.
|
||||
BIN
app/node_modules/@esbuild/win32-x64/esbuild.exe
generated
vendored
BIN
app/node_modules/@esbuild/win32-x64/esbuild.exe
generated
vendored
Binary file not shown.
20
app/node_modules/@esbuild/win32-x64/package.json
generated
vendored
20
app/node_modules/@esbuild/win32-x64/package.json
generated
vendored
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "@esbuild/win32-x64",
|
||||
"version": "0.27.2",
|
||||
"description": "The Windows 64-bit binary for esbuild, a JavaScript bundler.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/evanw/esbuild.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"preferUnplugged": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
3
app/node_modules/@rollup/rollup-win32-x64-gnu/README.md
generated
vendored
3
app/node_modules/@rollup/rollup-win32-x64-gnu/README.md
generated
vendored
@@ -1,3 +0,0 @@
|
||||
# `@rollup/rollup-win32-x64-gnu`
|
||||
|
||||
This is the **x86_64-pc-windows-gnu** binary for `rollup`
|
||||
22
app/node_modules/@rollup/rollup-win32-x64-gnu/package.json
generated
vendored
22
app/node_modules/@rollup/rollup-win32-x64-gnu/package.json
generated
vendored
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "@rollup/rollup-win32-x64-gnu",
|
||||
"version": "4.55.1",
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"files": [
|
||||
"rollup.win32-x64-gnu.node"
|
||||
],
|
||||
"description": "Native bindings for Rollup",
|
||||
"author": "Lukas Taegert-Atkinson",
|
||||
"homepage": "https://rollupjs.org/",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/rollup/rollup.git"
|
||||
},
|
||||
"main": "./rollup.win32-x64-gnu.node"
|
||||
}
|
||||
BIN
app/node_modules/@rollup/rollup-win32-x64-gnu/rollup.win32-x64-gnu.node
generated
vendored
BIN
app/node_modules/@rollup/rollup-win32-x64-gnu/rollup.win32-x64-gnu.node
generated
vendored
Binary file not shown.
3
app/node_modules/@rollup/rollup-win32-x64-msvc/README.md
generated
vendored
3
app/node_modules/@rollup/rollup-win32-x64-msvc/README.md
generated
vendored
@@ -1,3 +0,0 @@
|
||||
# `@rollup/rollup-win32-x64-msvc`
|
||||
|
||||
This is the **x86_64-pc-windows-msvc** binary for `rollup`
|
||||
22
app/node_modules/@rollup/rollup-win32-x64-msvc/package.json
generated
vendored
22
app/node_modules/@rollup/rollup-win32-x64-msvc/package.json
generated
vendored
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "@rollup/rollup-win32-x64-msvc",
|
||||
"version": "4.55.1",
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"files": [
|
||||
"rollup.win32-x64-msvc.node"
|
||||
],
|
||||
"description": "Native bindings for Rollup",
|
||||
"author": "Lukas Taegert-Atkinson",
|
||||
"homepage": "https://rollupjs.org/",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/rollup/rollup.git"
|
||||
},
|
||||
"main": "./rollup.win32-x64-msvc.node"
|
||||
}
|
||||
BIN
app/node_modules/@rollup/rollup-win32-x64-msvc/rollup.win32-x64-msvc.node
generated
vendored
BIN
app/node_modules/@rollup/rollup-win32-x64-msvc/rollup.win32-x64-msvc.node
generated
vendored
Binary file not shown.
58
app/package-lock.json
generated
58
app/package-lock.json
generated
@@ -54,6 +54,7 @@
|
||||
"react-dom": "^19.2.0",
|
||||
"react-hook-form": "^7.70.0",
|
||||
"react-resizable-panels": "^4.2.2",
|
||||
"react-router-dom": "^7.13.1",
|
||||
"recharts": "^2.15.4",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
@@ -5834,6 +5835,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.48.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz",
|
||||
@@ -7802,6 +7816,44 @@
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz",
|
||||
"integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz",
|
||||
"integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-smooth": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
|
||||
@@ -8132,6 +8184,12 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"react-dom": "^19.2.0",
|
||||
"react-hook-form": "^7.70.0",
|
||||
"react-resizable-panels": "^4.2.2",
|
||||
"react-router-dom": "^7.13.1",
|
||||
"recharts": "^2.15.4",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
|
||||
117
app/src/App.tsx
117
app/src/App.tsx
@@ -1,3 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useStore } from '@/store/useStore';
|
||||
import { Sidebar } from '@/components/layout/Sidebar';
|
||||
@@ -7,6 +9,7 @@ import { Oracle } from '@/components/modules/Oracle';
|
||||
import { Sentinel } from '@/components/modules/Sentinel';
|
||||
import { Inventory } from '@/components/modules/Inventory';
|
||||
import { Settings } from '@/components/modules/Settings';
|
||||
import { Catalyst } from '@/components/modules/Catalyst';
|
||||
import type { ModuleId } from '@/types';
|
||||
import {
|
||||
MoreVertical,
|
||||
@@ -19,30 +22,75 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
const moduleComponents: Record<ModuleId, React.ComponentType> = {
|
||||
dashboard: Dashboard,
|
||||
oracle: Oracle,
|
||||
sentinel: Sentinel,
|
||||
inventory: Inventory,
|
||||
settings: Settings,
|
||||
};
|
||||
// ── Route map ─────────────────────────────────────────────────────────────────
|
||||
// Single source of truth: module id → URL path → page title → component
|
||||
|
||||
const moduleTitles: Record<ModuleId, string> = {
|
||||
dashboard: 'Dashboard',
|
||||
oracle: 'The Oracle',
|
||||
sentinel: 'The Sentinel',
|
||||
inventory: 'Inventory',
|
||||
settings: 'Settings',
|
||||
};
|
||||
export const MODULE_ROUTES: Array<{
|
||||
id: ModuleId;
|
||||
path: string;
|
||||
title: string;
|
||||
component: React.ComponentType;
|
||||
}> = [
|
||||
{ id: 'dashboard', path: '/dashboard', title: 'Dashboard', component: Dashboard },
|
||||
{ id: 'oracle', path: '/oracle', title: 'The Oracle', component: Oracle },
|
||||
{ id: 'sentinel', path: '/sentinel', title: 'The Sentinel', component: Sentinel },
|
||||
{ id: 'inventory', path: '/inventory', title: 'Inventory', component: Inventory },
|
||||
{ id: 'catalyst', path: '/catalyst', title: 'The Catalyst', component: Catalyst },
|
||||
{ id: 'settings', path: '/settings', title: 'Settings', component: Settings },
|
||||
];
|
||||
|
||||
export const PATH_TO_MODULE = Object.fromEntries(
|
||||
MODULE_ROUTES.map((r) => [r.path, r.id])
|
||||
) as Record<string, ModuleId>;
|
||||
|
||||
// ── Protected Route guard ─────────────────────────────────────────────────────
|
||||
|
||||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
const { isAuthenticated } = useStore();
|
||||
if (!isAuthenticated) return <Navigate to="/login" replace />;
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// ── Sync URL → Zustand activeModule ──────────────────────────────────────────
|
||||
// This keeps the Zustand store in sync with the URL so existing module
|
||||
// components that read `activeModule` from the store continue to work.
|
||||
|
||||
function RouteModuleSync() {
|
||||
const { pathname } = useLocation();
|
||||
const { setActiveModule } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
const moduleId = PATH_TO_MODULE[pathname];
|
||||
if (moduleId) setActiveModule(moduleId);
|
||||
}, [pathname, setActiveModule]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Main authenticated layout ─────────────────────────────────────────────────
|
||||
|
||||
function MainLayout() {
|
||||
const { activeModule, setActiveModule, sidebarExpanded, logout } = useStore();
|
||||
const ActiveComponent = moduleComponents[activeModule];
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// Current route title
|
||||
const currentRoute = MODULE_ROUTES.find((r) => r.path === location.pathname);
|
||||
const pageTitle = currentRoute?.title ?? 'Velocity';
|
||||
|
||||
// Navigate to settings from dropdown (keeps router in sync)
|
||||
const goToSettings = () => {
|
||||
setActiveModule('settings');
|
||||
navigate('/settings');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex" style={{ background: '#000' }}>
|
||||
{/* Sync URL → store */}
|
||||
<RouteModuleSync />
|
||||
|
||||
{/* Sidebar */}
|
||||
<Sidebar />
|
||||
|
||||
@@ -73,7 +121,7 @@ function MainLayout() {
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<h1 className="text-base font-semibold text-white tracking-tight">{moduleTitles[activeModule]}</h1>
|
||||
<h1 className="text-base font-semibold text-white tracking-tight">{pageTitle}</h1>
|
||||
<p className="text-xs" style={{ color: 'hsl(var(--muted-fg))' }}>Project Velocity · v.1.1</p>
|
||||
</motion.div>
|
||||
|
||||
@@ -101,7 +149,7 @@ function MainLayout() {
|
||||
<DropdownMenuContent align="end" className="w-48 bg-[#0A0B10] border-white/10 text-zinc-200">
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer focus:bg-white/5"
|
||||
onClick={() => setActiveModule('settings')}
|
||||
onClick={goToSettings}
|
||||
>
|
||||
<Settings2 className="mr-2 h-4 w-4 text-zinc-400" />
|
||||
<span>Settings</span>
|
||||
@@ -117,11 +165,11 @@ function MainLayout() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Module Content */}
|
||||
{/* Module Content — animated on route change */}
|
||||
<div className="px-8 pb-8">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeModule}
|
||||
key={location.pathname}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
@@ -130,7 +178,16 @@ function MainLayout() {
|
||||
ease: [0.4, 0, 0.2, 1]
|
||||
}}
|
||||
>
|
||||
<ActiveComponent />
|
||||
{/* Nested module routes rendered here */}
|
||||
<Routes>
|
||||
{MODULE_ROUTES.map(({ path, component: Component }) => (
|
||||
<Route key={path} path={path} element={<Component />} />
|
||||
))}
|
||||
{/* Default: redirect / → /dashboard */}
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
{/* Catch-all: any unknown path → dashboard */}
|
||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||
</Routes>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
@@ -139,6 +196,8 @@ function MainLayout() {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Root App ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function App() {
|
||||
const { isAuthenticated } = useStore();
|
||||
|
||||
@@ -152,7 +211,10 @@ function App() {
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<LoginScreen />
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginScreen />} />
|
||||
<Route path="*" element={<LoginScreen />} />
|
||||
</Routes>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
@@ -162,7 +224,16 @@ function App() {
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<MainLayout />
|
||||
<Routes>
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<MainLayout />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
LayoutGrid,
|
||||
@@ -5,21 +6,23 @@ import {
|
||||
ScanFace,
|
||||
Building2,
|
||||
Sliders,
|
||||
Megaphone,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { useStore } from '@/store/useStore';
|
||||
import type { ModuleId } from '@/types';
|
||||
import { MODULE_ROUTES } from '@/App';
|
||||
|
||||
const navItems: { id: ModuleId; label: string; icon: LucideIcon }[] = [
|
||||
{ id: 'dashboard', label: 'Dashboard', icon: LayoutGrid },
|
||||
{ id: 'oracle', label: 'The Oracle', icon: MessageSquarePlus },
|
||||
{ id: 'sentinel', label: 'The Sentinel', icon: ScanFace },
|
||||
{ id: 'inventory', label: 'Inventory', icon: Building2 },
|
||||
{ id: 'settings', label: 'Settings', icon: Sliders },
|
||||
];
|
||||
const NAV_ICONS: Record<string, LucideIcon> = {
|
||||
'/dashboard': LayoutGrid,
|
||||
'/oracle': MessageSquarePlus,
|
||||
'/sentinel': ScanFace,
|
||||
'/inventory': Building2,
|
||||
'/catalyst': Megaphone,
|
||||
'/settings': Sliders,
|
||||
};
|
||||
|
||||
export function Sidebar() {
|
||||
const { activeModule, setActiveModule, sidebarExpanded, setSidebarExpanded, status } = useStore();
|
||||
const { sidebarExpanded, setSidebarExpanded, status } = useStore();
|
||||
|
||||
return (
|
||||
<motion.aside
|
||||
@@ -46,7 +49,6 @@ export function Sidebar() {
|
||||
|
||||
{/* Brand Name */}
|
||||
<div className="flex items-center px-4 mb-8 overflow-hidden" style={{ height: 36 }}>
|
||||
{/* Expanded: full name */}
|
||||
<motion.div
|
||||
className="overflow-hidden whitespace-nowrap"
|
||||
initial={{ opacity: 0, width: 0 }}
|
||||
@@ -58,55 +60,59 @@ export function Sidebar() {
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Nav */}
|
||||
<nav className="flex-1 px-3 space-y-1">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = activeModule === item.id;
|
||||
{MODULE_ROUTES.map((route) => {
|
||||
const Icon = NAV_ICONS[route.path] ?? LayoutGrid;
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
key={item.id}
|
||||
onClick={() => setActiveModule(item.id)}
|
||||
className="relative w-full flex items-center rounded-xl transition-colors duration-150"
|
||||
style={{
|
||||
height: 44,
|
||||
background: isActive ? 'hsl(var(--accent) / 0.12)' : 'transparent',
|
||||
color: isActive ? 'hsl(var(--accent))' : 'hsl(var(--muted-fg))',
|
||||
}}
|
||||
whileHover={{ x: 2 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onHoverStart={(e) => {
|
||||
if (!isActive) (e.target as HTMLElement).style.background = 'hsl(var(--surface-2))';
|
||||
}}
|
||||
onHoverEnd={(e) => {
|
||||
if (!isActive) (e.target as HTMLElement).style.background = 'transparent';
|
||||
}}
|
||||
<NavLink
|
||||
key={route.path}
|
||||
to={route.path}
|
||||
className="block"
|
||||
>
|
||||
{/* Active left bar */}
|
||||
{isActive && (
|
||||
{({ isActive }) => (
|
||||
<motion.div
|
||||
layoutId="activeBar"
|
||||
className="absolute left-0 top-2 bottom-2 w-0.5 rounded-full"
|
||||
style={{ background: 'hsl(var(--accent))' }}
|
||||
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
|
||||
/>
|
||||
className="relative w-full flex items-center rounded-xl transition-colors duration-150 cursor-pointer"
|
||||
style={{
|
||||
height: 44,
|
||||
background: isActive ? 'hsl(var(--accent) / 0.12)' : 'transparent',
|
||||
color: isActive ? 'hsl(var(--accent))' : 'hsl(var(--muted-fg))',
|
||||
}}
|
||||
whileHover={{ x: 2 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onHoverStart={(e) => {
|
||||
if (!isActive) (e.target as HTMLElement).style.background = 'hsl(var(--surface-2))';
|
||||
}}
|
||||
onHoverEnd={(e) => {
|
||||
if (!isActive) (e.target as HTMLElement).style.background = 'transparent';
|
||||
}}
|
||||
>
|
||||
{/* Active left bar */}
|
||||
{isActive && (
|
||||
<motion.div
|
||||
layoutId="activeBar"
|
||||
className="absolute left-0 top-2 bottom-2 w-0.5 rounded-full"
|
||||
style={{ background: 'hsl(var(--accent))' }}
|
||||
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="w-11 flex justify-center flex-shrink-0">
|
||||
<Icon className="w-[18px] h-[18px]" />
|
||||
</div>
|
||||
|
||||
<motion.span
|
||||
className="whitespace-nowrap font-medium text-sm overflow-hidden"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: sidebarExpanded ? 1 : 0 }}
|
||||
transition={{ duration: 0.15, delay: sidebarExpanded ? 0.08 : 0 }}
|
||||
>
|
||||
{route.title}
|
||||
</motion.span>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<div className="w-11 flex justify-center flex-shrink-0">
|
||||
<Icon className="w-[18px] h-[18px]" />
|
||||
</div>
|
||||
|
||||
<motion.span
|
||||
className="whitespace-nowrap font-medium text-sm overflow-hidden"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: sidebarExpanded ? 1 : 0 }}
|
||||
transition={{ duration: 0.15, delay: sidebarExpanded ? 0.08 : 0 }}
|
||||
>
|
||||
{item.label}
|
||||
</motion.span>
|
||||
</motion.button>
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
873
app/src/components/modules/Catalyst.tsx
Normal file
873
app/src/components/modules/Catalyst.tsx
Normal file
@@ -0,0 +1,873 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Megaphone, Clapperboard, BarChart3, Globe, Settings2,
|
||||
Zap, TrendingUp, Eye, MousePointerClick, DollarSign,
|
||||
Upload, Play, Image, Film, RefreshCw, ArrowRight, Plus,
|
||||
AlertTriangle, ArrowRightLeft, PlusCircle, SlidersHorizontal,
|
||||
Activity, Check, Link2,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
AreaChart, Area, BarChart, Bar,
|
||||
XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
import { useMarketingStore } from '@/store/useMarketingStore';
|
||||
import type { Campaign, MarketingAsset, LiveOptimizationEvent, LiveEventType } from '@/types';
|
||||
|
||||
// ── Design tokens ─────────────────────────────────────────────────────────────
|
||||
const GLASS = {
|
||||
background: 'rgba(8, 10, 18, 0.82)',
|
||||
border: '1px solid rgba(59,130,246,0.14)',
|
||||
backdropFilter: 'blur(24px)',
|
||||
WebkitBackdropFilter: 'blur(24px)',
|
||||
boxShadow: '0 0 0 1px rgba(255,255,255,0.04), 0 4px 32px rgba(0,0,0,0.55)',
|
||||
} as const;
|
||||
|
||||
const INNER = {
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
border: '1px solid rgba(255,255,255,0.07)',
|
||||
} as const;
|
||||
|
||||
// ── Shared primitives ─────────────────────────────────────────────────────────
|
||||
|
||||
function Widget({ children, className = '', delay = 0, colSpan = 1, rowSpan = 1, style }: {
|
||||
children: React.ReactNode; className?: string; delay?: number; colSpan?: number; rowSpan?: number; style?: React.CSSProperties;
|
||||
}) {
|
||||
return (
|
||||
<motion.div
|
||||
className={`relative rounded-2xl p-5 overflow-hidden ${className}`}
|
||||
style={{ gridColumn: `span ${colSpan}`, gridRow: `span ${rowSpan}`, ...GLASS, ...style }}
|
||||
initial={{ opacity: 0, y: 16, scale: 0.97 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{ duration: 0.35, delay, ease: [0.4, 0, 0.2, 1] }}
|
||||
>
|
||||
<div className="absolute inset-x-0 top-0 h-px pointer-events-none"
|
||||
style={{ background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.10), transparent)' }} />
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function GhostButton({ children, onClick, danger = false, className = '' }: {
|
||||
children: React.ReactNode; onClick?: () => void; danger?: boolean; className?: string;
|
||||
}) {
|
||||
return (
|
||||
<motion.button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`px-4 py-2 rounded-xl text-sm font-medium transition-colors ${className}`}
|
||||
style={danger
|
||||
? { background: 'rgba(239,68,68,0.12)', color: '#f87171', border: '1px solid rgba(239,68,68,0.2)' }
|
||||
: { ...INNER, color: 'rgba(255,255,255,0.8)' }
|
||||
}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
>
|
||||
{children}
|
||||
</motion.button>
|
||||
);
|
||||
}
|
||||
|
||||
function DarkInput({ value, onChange, placeholder, type = 'text', readOnly = false }: {
|
||||
value: string; onChange?: (v: string) => void; placeholder?: string; type?: string; readOnly?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
readOnly={readOnly}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="rounded-xl px-3 py-2 text-sm text-white w-full focus:outline-none transition-all"
|
||||
style={{ ...INNER, caretColor: 'hsl(var(--accent))' }}
|
||||
onFocus={(e) => { if (!readOnly) e.currentTarget.style.border = '1px solid rgba(59,130,246,0.4)'; }}
|
||||
onBlur={(e) => { e.currentTarget.style.border = '1px solid rgba(255,255,255,0.07)'; }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function LiveBadge() {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 text-xs font-medium" style={{ color: '#4ade80' }}>
|
||||
<motion.div className="w-1.5 h-1.5 rounded-full bg-green-400"
|
||||
animate={{ opacity: [1, 0.3, 1] }} transition={{ repeat: Infinity, duration: 1.6 }} />
|
||||
Live
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── KPI Stat card ─────────────────────────────────────────────────────────────
|
||||
function StatCard({ icon: Icon, label, value, sub, glowColor = 'rgba(59,130,246,0.2)', delay = 0 }: {
|
||||
icon: LucideIcon; label: string; value: string; sub: string; glowColor?: string; delay?: number;
|
||||
}) {
|
||||
return (
|
||||
<Widget delay={delay} className="flex flex-col justify-between min-h-[130px]">
|
||||
<div className="absolute -bottom-6 -right-6 w-44 h-44 pointer-events-none"
|
||||
style={{ background: `radial-gradient(ellipse at 70% 80%, ${glowColor} 0%, transparent 65%)`, filter: 'blur(18px)' }} />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="w-8 h-8 rounded-xl flex items-center justify-center"
|
||||
style={{ background: `${glowColor.replace('0.2', '0.12')}`, border: `1px solid ${glowColor.replace('0.2', '0.25')}` }}>
|
||||
<Icon className="w-4 h-4" style={{ color: glowColor.includes('59,130,246') ? '#60a5fa' : glowColor.includes('34,211,238') ? '#22d3ee' : glowColor.includes('251,191,36') ? '#fbbf24' : '#a78bfa' }} />
|
||||
</div>
|
||||
<LiveBadge />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-medium uppercase tracking-widest mb-1" style={{ color: 'rgba(148,163,184,0.65)' }}>{label}</p>
|
||||
<motion.p className="text-3xl font-semibold text-white leading-none"
|
||||
initial={{ opacity: 0, scale: 0.7 }} animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.4, delay: delay + 0.12, type: 'spring', stiffness: 200 }}>
|
||||
{value}
|
||||
</motion.p>
|
||||
<p className="text-xs mt-1" style={{ color: 'rgba(148,163,184,0.5)' }}>{sub}</p>
|
||||
</div>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Campaign status badge ─────────────────────────────────────────────────────
|
||||
function CampaignStatusBadge({ status }: { status: Campaign['status'] }) {
|
||||
const map: Record<Campaign['status'], { label: string; color: string; bg: string }> = {
|
||||
ACTIVE: { label: 'Active', color: '#4ade80', bg: 'rgba(74,222,128,0.12)' },
|
||||
PAUSED: { label: 'Paused', color: '#fbbf24', bg: 'rgba(251,191,36,0.12)' },
|
||||
DELETED: { label: 'Deleted', color: '#f87171', bg: 'rgba(248,113,113,0.12)' },
|
||||
ARCHIVED: { label: 'Archived', color: '#94a3b8', bg: 'rgba(148,163,184,0.12)' },
|
||||
};
|
||||
const { label, color, bg } = map[status];
|
||||
return (
|
||||
<span className="px-2 py-0.5 rounded-full text-[11px] font-medium" style={{ color, background: bg }}>{label}</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// TAB 1 — The Studio
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const RENDER_MESSAGES: Record<string, string[]> = {
|
||||
rendering: [
|
||||
'Wan 2.2 is rendering cinematic lighting...',
|
||||
'Wan 2.2 is compositing the infinity pool reflection...',
|
||||
'Qwen-Image 2512 is synthesizing Arabic typography...',
|
||||
'Flux-Schnell is upscaling to 4K resolution...',
|
||||
],
|
||||
queued: [
|
||||
'Qwen-Image 2512 queued for cinematic poster render...',
|
||||
'Wan 2.2 14B queued — GPU resources freeing up...',
|
||||
],
|
||||
};
|
||||
|
||||
function AssetCard({ asset }: { asset: MarketingAsset }) {
|
||||
const isProcessing = asset.status === 'rendering' || asset.status === 'queued';
|
||||
const [msgIdx, setMsgIdx] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isProcessing) return;
|
||||
const msgs = RENDER_MESSAGES[asset.status] ?? [];
|
||||
if (msgs.length === 0) return;
|
||||
const t = setInterval(() => setMsgIdx((i) => (i + 1) % msgs.length), 2800);
|
||||
return () => clearInterval(t);
|
||||
}, [isProcessing, asset.status]);
|
||||
|
||||
const statusColor: Record<typeof asset.status, string> = {
|
||||
queued: 'rgba(148,163,184,0.8)',
|
||||
rendering: '#fbbf24',
|
||||
ready: '#4ade80',
|
||||
uploaded: '#60a5fa',
|
||||
failed: '#f87171',
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="relative rounded-2xl overflow-hidden flex flex-col"
|
||||
style={{ ...INNER, minHeight: 200, border: `1px solid ${statusColor[asset.status]}22` }}
|
||||
whileHover={{ scale: 1.015 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{/* Thumbnail / shimmer area */}
|
||||
<div className="relative flex-1 flex items-center justify-center min-h-[120px]"
|
||||
style={{ background: 'rgba(255,255,255,0.02)' }}>
|
||||
{isProcessing ? (
|
||||
<div className="w-full h-full absolute inset-0">
|
||||
<motion.div className="absolute inset-0"
|
||||
style={{ background: 'linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.06) 50%, transparent 100%)' }}
|
||||
animate={{ x: ['-100%', '200%'] }}
|
||||
transition={{ repeat: Infinity, duration: 1.6, ease: 'linear' }} />
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2 p-3 text-center">
|
||||
{asset.type === 'video'
|
||||
? <Film className="w-8 h-8 text-amber-400 opacity-60" />
|
||||
: <Image className="w-8 h-8 text-blue-400 opacity-60" />}
|
||||
<motion.p className="text-xs font-medium leading-relaxed"
|
||||
style={{ color: statusColor[asset.status] }}
|
||||
key={msgIdx}
|
||||
initial={{ opacity: 0, y: 4 }} animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}>
|
||||
{(RENDER_MESSAGES[asset.status] ?? [])[msgIdx] ?? asset.renderMessage}
|
||||
</motion.p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-6">
|
||||
{asset.type === 'video'
|
||||
? <Film className="w-10 h-10 text-blue-400" />
|
||||
: <Image className="w-10 h-10 text-blue-400" />}
|
||||
<span className="text-xs" style={{ color: 'rgba(148,163,184,0.5)' }}>
|
||||
{asset.metaAssetId ? `Meta ID: ${asset.metaAssetId}` : 'Ready for upload'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info footer */}
|
||||
<div className="px-3 py-2.5" style={{ borderTop: '1px solid rgba(255,255,255,0.05)' }}>
|
||||
<div className="flex items-start justify-between gap-2 mb-1.5">
|
||||
<p className="text-xs font-medium text-white leading-snug line-clamp-2">{asset.name}</p>
|
||||
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full flex-shrink-0"
|
||||
style={{ color: statusColor[asset.status], background: `${statusColor[asset.status]}18` }}>
|
||||
{asset.status.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded"
|
||||
style={{ background: 'rgba(255,255,255,0.06)', color: 'rgba(148,163,184,0.6)' }}>
|
||||
{asset.type === 'video' ? 'Wan 2.2' : 'Qwen-2512'}
|
||||
</span>
|
||||
{asset.language && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded uppercase"
|
||||
style={{ background: 'rgba(255,255,255,0.06)', color: 'rgba(148,163,184,0.6)' }}>
|
||||
{asset.language}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkflowInput() {
|
||||
const [mode, setMode] = useState<'image' | 'video'>('image');
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [keywords, setKeywords] = useState('');
|
||||
const [textCopy, setTextCopy] = useState('');
|
||||
|
||||
return (
|
||||
<Widget delay={0.04} colSpan={1} className="!p-6" style={{ background: '#111216', borderRadius: '28px' }}>
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Top Section: Ground Truth & References */}
|
||||
<div className="flex items-end gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<button title="Select Image from Gallery or Dream Weaver / Click Image from iPad" className="w-[60px] h-[60px] rounded-2xl flex items-center justify-center transition-colors hover:bg-white/5" style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)' }}>
|
||||
<span className="text-xs font-medium" style={{ color: 'rgba(255,255,255,0.2)' }}>Start</span>
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-white tracking-wide">Ground Truth</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center h-[60px] mb-[26px]">
|
||||
<Plus className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<button title="Add reference image" className="w-[60px] h-[60px] rounded-2xl flex items-center justify-center transition-colors hover:bg-white/5" style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)' }} />
|
||||
<button title="Add reference image" className="w-[60px] h-[60px] rounded-2xl flex items-center justify-center transition-colors hover:bg-white/5" style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)' }} />
|
||||
<button title="Add more references" className="w-[60px] h-[60px] rounded-2xl flex items-center justify-center transition-colors hover:bg-white/5" style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)' }}>
|
||||
<Plus className="w-5 h-5 text-white/40" />
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-white tracking-wide">References</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Section: Input & Controls */}
|
||||
<div className="flex items-end justify-between">
|
||||
<div className="flex-1 flex flex-col gap-3">
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder="What do you want to create?"
|
||||
className="w-full bg-transparent text-xl font-medium text-white placeholder-white/20 resize-none outline-none leading-relaxed"
|
||||
rows={1}
|
||||
/>
|
||||
{/* Extra inputs for keywords and text copy as requested */}
|
||||
<div className="flex items-center gap-3 pt-2" style={{ borderTop: '1px solid rgba(255,255,255,0.05)' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={keywords}
|
||||
onChange={(e) => setKeywords(e.target.value)}
|
||||
placeholder="Keywords (comma separated)"
|
||||
className="bg-transparent text-sm text-white/80 placeholder-white/20 outline-none flex-1 min-w-0"
|
||||
/>
|
||||
<span className="text-white/10">|</span>
|
||||
<input
|
||||
type="text"
|
||||
value={textCopy}
|
||||
onChange={(e) => setTextCopy(e.target.value)}
|
||||
placeholder='Text copy inside ""'
|
||||
className="bg-transparent text-sm text-white/80 placeholder-white/20 outline-none flex-1 min-w-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 ml-6 pl-4">
|
||||
{/* Toggle */}
|
||||
<div className="flex items-center rounded-full p-1" style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)' }}>
|
||||
<button
|
||||
onClick={() => setMode('image')}
|
||||
className={`px-5 py-2.5 rounded-full text-sm font-semibold transition-all ${mode === 'image' ? 'text-black shadow-lg' : 'text-white/60'}`}
|
||||
style={{ background: mode === 'image' ? '#22c55e' : 'transparent' }}
|
||||
>
|
||||
Image
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('video')}
|
||||
className={`px-5 py-2.5 rounded-full text-sm font-semibold transition-all ${mode === 'video' ? 'text-black shadow-lg' : 'text-white/60'}`}
|
||||
style={{ background: mode === 'video' ? '#22c55e' : 'transparent' }}
|
||||
>
|
||||
Video
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Send Button */}
|
||||
<button className="w-[44px] h-[44px] rounded-full bg-zinc-200 flex items-center justify-center hover:bg-white transition-colors flex-shrink-0">
|
||||
<ArrowRight className="w-5 h-5 text-black" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
function TheStudio() {
|
||||
const { activeAssets } = useMarketingStore();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header row */}
|
||||
<Widget delay={0} colSpan={1}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white mb-0.5 flex items-center gap-2">
|
||||
<Clapperboard className="w-4 h-4 text-blue-400" /> Visual AI Engine
|
||||
</h3>
|
||||
<p className="text-xs" style={{ color: 'rgba(148,163,184,0.55)' }}>
|
||||
ComfyUI workflows (Wan 2.2 & Qwen-Image 2512) feed directly into the Meta Asset Library
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<GhostButton>
|
||||
<span className="flex items-center gap-1.5"><Upload className="w-3.5 h-3.5" /> Sync to Meta</span>
|
||||
</GhostButton>
|
||||
<GhostButton>
|
||||
<span className="flex items-center gap-1.5"><Play className="w-3.5 h-3.5 text-blue-400" /> Trigger Render</span>
|
||||
</GhostButton>
|
||||
</div>
|
||||
</div>
|
||||
</Widget>
|
||||
|
||||
{/* ComfyUI Workflow Input Panel */}
|
||||
<WorkflowInput />
|
||||
|
||||
{/* Asset grid */}
|
||||
<div className="grid grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
{activeAssets.map((asset, i) => (
|
||||
<motion.div key={asset.id}
|
||||
initial={{ opacity: 0, y: 14 }} animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: i * 0.07 }}>
|
||||
<AssetCard asset={asset} />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// TAB 2 — Campaign Command
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function CampaignCommand() {
|
||||
const { campaigns, adInsights } = useMarketingStore();
|
||||
|
||||
const totalSpend = campaigns.reduce((s, c) => s + c.lifetimeSpend / 100, 0);
|
||||
const totalImpr = campaigns.reduce((s, c) => s + c.impressions, 0);
|
||||
const avgCtr = campaigns.reduce((s, c) => s + c.ctr, 0) / campaigns.length;
|
||||
const avgCpa = campaigns.reduce((s, c) => s + c.cpa, 0) / campaigns.length;
|
||||
|
||||
// Build spend-over-time from insights (last 14 days)
|
||||
const spendData = adInsights.slice(0, 14).map((d) => ({
|
||||
date: d.date.slice(5), // MM-DD
|
||||
spend: d.spend,
|
||||
impressions: Math.round(d.impressions / 1000),
|
||||
})).reverse();
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* KPI row */}
|
||||
<div className="grid grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
<StatCard icon={Megaphone} label="Active Campaigns" value={String(campaigns.filter(c => c.status === 'ACTIVE').length)} sub={`${campaigns.length} total campaigns`} glowColor="rgba(59,130,246,0.2)" delay={0} />
|
||||
<StatCard icon={DollarSign} label="Total Spend" value={`AED ${(totalSpend / 1000).toFixed(1)}K`} sub="Lifetime across all campaigns" glowColor="rgba(34,211,238,0.2)" delay={0.06} />
|
||||
<StatCard icon={Eye} label="Impressions" value={`${(totalImpr / 1000).toFixed(0)}K`} sub="Total ad impressions" glowColor="rgba(251,191,36,0.2)" delay={0.12} />
|
||||
<StatCard icon={MousePointerClick} label="Avg CTR" value={`${avgCtr.toFixed(2)}%`} sub={`Avg CPA: AED ${avgCpa.toFixed(0)}`} glowColor="rgba(167,139,250,0.2)" delay={0.18} />
|
||||
</div>
|
||||
|
||||
{/* Spend chart + campaign list */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
{/* Spend over time chart */}
|
||||
<Widget delay={0.24} colSpan={1}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-sm font-semibold text-white flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4 text-blue-400" /> Spend vs Impressions
|
||||
</h3>
|
||||
<LiveBadge />
|
||||
</div>
|
||||
<div className="h-52">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={spendData} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="gSpend" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.35} />
|
||||
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient id="gImpr" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#22d3ee" stopOpacity={0.28} />
|
||||
<stop offset="95%" stopColor="#22d3ee" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.04)" />
|
||||
<XAxis dataKey="date" stroke="rgba(148,163,184,0.3)" fontSize={10} tickLine={false} axisLine={false} />
|
||||
<YAxis stroke="rgba(148,163,184,0.3)" fontSize={10} tickLine={false} axisLine={false} />
|
||||
<Tooltip contentStyle={{ backgroundColor: 'rgba(8,10,18,0.92)', border: '1px solid rgba(59,130,246,0.2)', borderRadius: 10 }} labelStyle={{ color: 'rgba(148,163,184,0.8)', fontSize: 11 }} itemStyle={{ color: '#93c5fd', fontSize: 12 }} />
|
||||
<Area type="monotone" dataKey="spend" stroke="#3b82f6" strokeWidth={2} fillOpacity={1} fill="url(#gSpend)" />
|
||||
<Area type="monotone" dataKey="impressions" stroke="#22d3ee" strokeWidth={2} fillOpacity={1} fill="url(#gImpr)" />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Widget>
|
||||
|
||||
{/* Campaign list */}
|
||||
<Widget delay={0.28} colSpan={1}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-sm font-semibold text-white flex items-center gap-2">
|
||||
<SlidersHorizontal className="w-4 h-4 text-blue-400" /> Campaigns
|
||||
</h3>
|
||||
<GhostButton>
|
||||
<span className="flex items-center gap-1.5 text-xs"><PlusCircle className="w-3.5 h-3.5" /> New</span>
|
||||
</GhostButton>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{campaigns.map((c) => (
|
||||
<motion.div key={c.id} className="flex items-center justify-between p-3 rounded-xl" style={INNER}
|
||||
whileHover={{ scale: 1.01 }} transition={{ duration: 0.15 }}>
|
||||
<div className="flex-1 min-w-0 mr-3">
|
||||
<p className="text-sm font-medium text-white truncate">{c.name}</p>
|
||||
<p className="text-xs mt-0.5" style={{ color: 'rgba(148,163,184,0.5)' }}>
|
||||
AED {(c.lifetimeSpend / 100).toLocaleString()} · {c.impressions.toLocaleString()} impr.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<span className="text-xs font-medium" style={{ color: c.roi > 20 ? '#4ade80' : '#fbbf24' }}>
|
||||
{c.roi.toFixed(1)}% ROI
|
||||
</span>
|
||||
<CampaignStatusBadge status={c.status} />
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// TAB 3 — Intelligence & ROI
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function IntelligenceROI() {
|
||||
const { campaigns, adInsights } = useMarketingStore();
|
||||
|
||||
const cpaData = adInsights.slice(0, 7).map((d) => ({
|
||||
date: d.date.slice(5),
|
||||
cpa: d.cpa,
|
||||
roi: d.roi,
|
||||
})).reverse();
|
||||
|
||||
const adSetPerf = [
|
||||
{ name: '3BHK Dubai', ctr: 2.1, cpa: 210, spend: 3400 },
|
||||
{ name: 'PH Retarget', ctr: 3.2, cpa: 2100, spend: 5800 },
|
||||
{ name: '1BHK Lookalike', ctr: 1.8, cpa: 270, spend: 980 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* CPA / ROI KPIs */}
|
||||
<div className="grid grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
<StatCard icon={DollarSign} label="Avg CPA" value={`AED ${(campaigns.reduce((s,c)=>s+c.cpa,0)/campaigns.length).toFixed(0)}`} sub="Cost per acquisition" glowColor="rgba(34,211,238,0.2)" delay={0} />
|
||||
<StatCard icon={TrendingUp} label="Portfolio ROI" value={`${(campaigns.reduce((s,c)=>s+c.roi,0)/campaigns.length).toFixed(1)}%`} sub="Blended across all ad sets" glowColor="rgba(74,222,128,0.2)" delay={0.06} />
|
||||
<StatCard icon={Activity} label="Avg CTR" value={`${(campaigns.reduce((s,c)=>s+c.ctr,0)/campaigns.length).toFixed(2)}%`} sub="Click-through rate" glowColor="rgba(167,139,250,0.2)" delay={0.12} />
|
||||
<StatCard icon={Zap} label="Optimization Runs" value="47" sub="Agent actions today" glowColor="rgba(251,191,36,0.2)" delay={0.18} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
{/* CPA trend */}
|
||||
<Widget delay={0.24}>
|
||||
<h3 className="text-sm font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<BarChart3 className="w-4 h-4 text-cyan-400" /> CPA Trend (7 Days)
|
||||
</h3>
|
||||
<div className="h-52">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={cpaData} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="gCpa" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#22d3ee" stopOpacity={0.35} />
|
||||
<stop offset="95%" stopColor="#22d3ee" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.04)" />
|
||||
<XAxis dataKey="date" stroke="rgba(148,163,184,0.3)" fontSize={10} tickLine={false} axisLine={false} />
|
||||
<YAxis stroke="rgba(148,163,184,0.3)" fontSize={10} tickLine={false} axisLine={false} />
|
||||
<Tooltip contentStyle={{ backgroundColor: 'rgba(8,10,18,0.92)', border: '1px solid rgba(34,211,238,0.2)', borderRadius: 10 }} labelStyle={{ color: 'rgba(148,163,184,0.8)', fontSize: 11 }} itemStyle={{ color: '#67e8f9', fontSize: 12 }} />
|
||||
<Area type="monotone" dataKey="cpa" stroke="#22d3ee" strokeWidth={2} fillOpacity={1} fill="url(#gCpa)" name="CPA (AED)" />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Widget>
|
||||
|
||||
{/* Ad-set CPA bar chart */}
|
||||
<Widget delay={0.28}>
|
||||
<h3 className="text-sm font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<BarChart3 className="w-4 h-4 text-blue-400" /> Ad Set Performance
|
||||
</h3>
|
||||
<div className="h-52">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={adSetPerf} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.04)" />
|
||||
<XAxis dataKey="name" stroke="rgba(148,163,184,0.3)" fontSize={10} tickLine={false} axisLine={false} />
|
||||
<YAxis stroke="rgba(148,163,184,0.3)" fontSize={10} tickLine={false} axisLine={false} />
|
||||
<Tooltip contentStyle={{ backgroundColor: 'rgba(8,10,18,0.92)', border: '1px solid rgba(59,130,246,0.2)', borderRadius: 10 }} labelStyle={{ color: 'rgba(148,163,184,0.8)', fontSize: 11 }} itemStyle={{ color: '#93c5fd', fontSize: 12 }} />
|
||||
<Bar dataKey="spend" fill="#3b82f6" radius={[4, 4, 0, 0]} name="Spend (AED)" opacity={0.8} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Live Optimization Feed (shared across tabs)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const EVENT_ICON: Record<LiveEventType, { icon: LucideIcon; color: string; bg: string; label: string }> = {
|
||||
pause: { icon: AlertTriangle, color: '#fbbf24', bg: 'rgba(251,191,36,0.12)', label: 'Paused' },
|
||||
shift: { icon: ArrowRightLeft, color: '#60a5fa', bg: 'rgba(96,165,250,0.12)', label: 'Budget Shift' },
|
||||
create: { icon: PlusCircle, color: '#4ade80', bg: 'rgba(74,222,128,0.12)', label: 'Created' },
|
||||
optimize: { icon: Zap, color: '#a78bfa', bg: 'rgba(167,139,250,0.12)', label: 'Optimized' },
|
||||
alert: { icon: AlertTriangle, color: '#f87171', bg: 'rgba(248,113,113,0.12)', label: 'Alert' },
|
||||
rotate: { icon: RefreshCw, color: '#22d3ee', bg: 'rgba(34,211,238,0.12)', label: 'Rotated' },
|
||||
};
|
||||
|
||||
function LiveEventItem({ event }: { event: LiveOptimizationEvent }) {
|
||||
const cfg = EVENT_ICON[event.type];
|
||||
const Icon = cfg.icon;
|
||||
return (
|
||||
<motion.div
|
||||
className="flex items-start gap-3 p-3 rounded-xl"
|
||||
style={INNER}
|
||||
initial={{ opacity: 0, x: -12, scale: 0.97 }}
|
||||
animate={{ opacity: 1, x: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, x: 12, scale: 0.95 }}
|
||||
transition={{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }}
|
||||
layout
|
||||
>
|
||||
<div className="w-7 h-7 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||
style={{ background: cfg.bg }}>
|
||||
<Icon className="w-3.5 h-3.5" style={{ color: cfg.color }} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-0.5">
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider" style={{ color: cfg.color }}>{cfg.label}</span>
|
||||
{event.campaignName && (
|
||||
<span className="text-[10px] px-1.5 py-px rounded" style={{ background: 'rgba(255,255,255,0.05)', color: 'rgba(148,163,184,0.6)' }}>
|
||||
{event.campaignName}
|
||||
</span>
|
||||
)}
|
||||
{event.value && (
|
||||
<span className="text-[10px] font-medium ml-auto" style={{ color: cfg.color }}>{event.value}</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs leading-relaxed" style={{ color: 'rgba(148,163,184,0.75)' }}>{event.message}</p>
|
||||
<p className="text-[10px] mt-1" style={{ color: 'rgba(148,163,184,0.35)' }}>
|
||||
{event.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
const MOCK_STREAM: Array<{ type: LiveEventType; message: string; campaign: string; value?: string }> = [
|
||||
{ type: 'optimize', message: 'Expanded 3BHK audience targeting — added "Property Investment" interest layer.', campaign: '3BHK Prestige Launch', value: '+22k reach' },
|
||||
{ type: 'rotate', message: 'Rotated in Arabic Poster (Qwen-2512) as new creative variant for A/B test.', campaign: 'Penthouse Whale Retarget' },
|
||||
{ type: 'shift', message: 'Shifted AED 150 from underperforming Ad Set C to Ad Set A (CTR 3.2%).', campaign: '1BHK Investment', value: '+AED 150' },
|
||||
{ type: 'pause', message: 'Paused Ad Set D — CPA crossed AED 480 threshold (target: AED 400).', campaign: 'Penthouse Whale Retarget', value: 'CPA: AED 481' },
|
||||
{ type: 'create', message: 'Created new Custom Audience from 18 Closed/Won CRM leads (hashed emails).', campaign: '3BHK Prestige Launch' },
|
||||
];
|
||||
|
||||
function LiveOptimizationFeed() {
|
||||
const { liveEvents, pushLiveEvent } = useMarketingStore();
|
||||
const streamIdx = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => {
|
||||
const item = MOCK_STREAM[streamIdx.current % MOCK_STREAM.length];
|
||||
streamIdx.current++;
|
||||
pushLiveEvent({
|
||||
id: `ev_${Date.now()}`,
|
||||
type: item.type,
|
||||
message: item.message,
|
||||
campaignName: item.campaign,
|
||||
timestamp: new Date(),
|
||||
value: item.value,
|
||||
});
|
||||
}, 4000);
|
||||
return () => clearInterval(t);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Widget delay={0.4} colSpan={1}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-sm font-semibold text-white flex items-center gap-2">
|
||||
<Activity className="w-4 h-4 text-blue-400" /> Live Optimization Feed
|
||||
</h3>
|
||||
<LiveBadge />
|
||||
</div>
|
||||
<div className="space-y-2 max-h-72 overflow-y-auto custom-scrollbar pr-1">
|
||||
<AnimatePresence mode="popLayout" initial={false}>
|
||||
{liveEvents.slice(0, 8).map((ev) => (
|
||||
<LiveEventItem key={ev.id} event={ev} />
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// TAB 4 — War Room (Meta Graph Settings)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function WarRoom() {
|
||||
const { settings, setMetaSettings } = useMarketingStore();
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
const handleSave = () => {
|
||||
setMetaSettings({ isConnected: true });
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2500);
|
||||
};
|
||||
|
||||
const fields: Array<{ key: keyof typeof settings; label: string; desc: string; placeholder: string; type?: string }> = [
|
||||
{ key: 'metaAccessToken', label: 'Meta System User Access Token', desc: 'Long-lived system user token from Meta Business Manager', placeholder: 'EAAxxxxxxxx...' , type: 'password' },
|
||||
{ key: 'metaAdAccountId', label: 'Ad Account ID', desc: 'Format: act_XXXXXXXXXX', placeholder: 'act_123456789' },
|
||||
{ key: 'metaBusinessId', label: 'Business Portfolio ID', desc: 'Found in Meta Business Settings → Business Info', placeholder: '1234567890' },
|
||||
{ key: 'metaAppId', label: 'App ID', desc: 'From Meta Developers → Your App', placeholder: '9876543210' },
|
||||
{ key: 'whatsappPhoneNumberId',label: 'WhatsApp Phone Number ID', desc: 'Required for WhatsApp Business API integration', placeholder: '1098765432' },
|
||||
];
|
||||
|
||||
const assetLinks = [
|
||||
{ label: 'Instagram Business Page', href: '#', icon: Link2 },
|
||||
{ label: 'Facebook Business Page', href: '#', icon: Link2 },
|
||||
{ label: 'WhatsApp Business Account', href: '#', icon: Link2 },
|
||||
{ label: 'Ad Library (Creative Hub)', href: '#', icon: Link2 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* Connection status banner */}
|
||||
<Widget delay={0} colSpan={1}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<div className={`w-3 h-3 rounded-full ${settings.isConnected ? 'bg-green-500' : 'bg-zinc-600'}`} />
|
||||
{settings.isConnected && <div className="absolute inset-0 rounded-full bg-green-500 status-pulse" />}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white">Meta Graph API</p>
|
||||
<p className="text-xs" style={{ color: 'rgba(148,163,184,0.5)' }}>
|
||||
{settings.isConnected ? 'Connected — System User Token Active' : 'Not connected — enter credentials below'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="w-4 h-4" style={{ color: 'rgba(148,163,184,0.4)' }} />
|
||||
<span className="text-xs font-medium" style={{ color: settings.isConnected ? '#4ade80' : 'rgba(148,163,184,0.4)' }}>
|
||||
{settings.isConnected ? 'ONLINE' : 'OFFLINE'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Widget>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
{/* API Credentials */}
|
||||
<Widget delay={0.08}>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Settings2 className="w-4 h-4 text-blue-400" />
|
||||
<h3 className="text-sm font-semibold text-white">API Credentials</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{fields.map(({ key, label, desc, placeholder, type }) => (
|
||||
<div key={key}>
|
||||
<p className="text-xs font-medium text-white mb-0.5">{label}</p>
|
||||
<p className="text-[10px] mb-1.5" style={{ color: 'rgba(148,163,184,0.5)' }}>{desc}</p>
|
||||
<DarkInput
|
||||
type={type ?? 'text'}
|
||||
value={String(settings[key])}
|
||||
onChange={(v) => setMetaSettings({ [key]: v } as Partial<typeof settings>)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<motion.button
|
||||
type="button"
|
||||
className="w-full py-2.5 mt-2 rounded-xl text-sm font-semibold flex items-center justify-center gap-2 transition-all"
|
||||
style={{ background: saved ? 'rgba(74,222,128,0.15)' : 'hsl(var(--accent))', color: saved ? '#4ade80' : 'hsl(var(--accent-fg))' }}
|
||||
onClick={handleSave}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
>
|
||||
{saved ? <><Check className="w-4 h-4" /> Saved & Connected</> : 'Save & Connect to Meta'}
|
||||
</motion.button>
|
||||
</div>
|
||||
</Widget>
|
||||
|
||||
{/* Business Asset Links */}
|
||||
<Widget delay={0.12}>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Link2 className="w-4 h-4 text-blue-400" />
|
||||
<h3 className="text-sm font-semibold text-white">Business Asset Management</h3>
|
||||
</div>
|
||||
<p className="text-xs mb-4" style={{ color: 'rgba(148,163,184,0.55)' }}>
|
||||
Link WhatsApp, Instagram, and Facebook Pages to your Meta Business Portfolio for unified campaign management.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{assetLinks.map(({ label, icon: Icon }) => (
|
||||
<div key={label} className="flex items-center justify-between p-3 rounded-xl" style={INNER}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="w-4 h-4 text-blue-400" />
|
||||
<span className="text-sm text-white">{label}</span>
|
||||
</div>
|
||||
<GhostButton>
|
||||
<span className="text-xs">Configure</span>
|
||||
</GhostButton>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Permissions */}
|
||||
<div className="mt-4 p-3 rounded-xl" style={{ background: 'rgba(59,130,246,0.06)', border: '1px solid rgba(59,130,246,0.15)' }}>
|
||||
<p className="text-xs font-medium text-blue-300 mb-1">Required Scopes</p>
|
||||
{['ads_management', 'ads_read', 'business_management', 'pages_manage_ads', 'whatsapp_business_management'].map((scope) => (
|
||||
<div key={scope} className="flex items-center gap-2 py-0.5">
|
||||
<Check className="w-3 h-3 text-blue-400" />
|
||||
<span className="text-[11px] font-mono" style={{ color: 'rgba(148,163,184,0.7)' }}>{scope}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Tab Bar
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
type TabId = 'studio' | 'command' | 'intelligence' | 'war-room';
|
||||
|
||||
const TABS: Array<{ id: TabId; label: string; icon: LucideIcon }> = [
|
||||
{ id: 'studio', label: 'The Studio', icon: Clapperboard },
|
||||
{ id: 'command', label: 'Campaign Command', icon: Megaphone },
|
||||
{ id: 'intelligence', label: 'Intelligence & ROI', icon: BarChart3 },
|
||||
{ id: 'war-room', label: 'War Room', icon: Globe },
|
||||
];
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Root export
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function Catalyst() {
|
||||
const { activeTab, setActiveTab } = useMarketingStore();
|
||||
|
||||
const panels: Record<TabId, React.ReactNode> = {
|
||||
'studio': <TheStudio />,
|
||||
'command': <CampaignCommand />,
|
||||
'intelligence': <IntelligenceROI />,
|
||||
'war-room': <WarRoom />,
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="space-y-5 pb-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white tracking-tight flex items-center gap-2">
|
||||
<Megaphone className="w-5 h-5 text-blue-400" /> The Catalyst
|
||||
</h2>
|
||||
<p className="text-xs mt-0.5" style={{ color: 'rgba(148,163,184,0.5)' }}>
|
||||
Autonomous Deployment Engine · Meta Marketing API
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<motion.div animate={{ opacity: [1, 0.4, 1] }} transition={{ repeat: Infinity, duration: 2 }}
|
||||
className="w-1.5 h-1.5 rounded-full bg-blue-400" />
|
||||
<span className="text-xs font-medium" style={{ color: '#60a5fa' }}>Claw Agent Active</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab bar */}
|
||||
<div className="flex items-center gap-1 p-1 rounded-2xl w-fit" style={{ background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.07)' }}>
|
||||
{TABS.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === tab.id;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className="relative flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-colors outline-none"
|
||||
style={{ color: isActive ? 'white' : 'rgba(148,163,184,0.6)' }}
|
||||
>
|
||||
{isActive && (
|
||||
<motion.div layoutId="catalyst-tab-bg" className="absolute inset-0 rounded-xl"
|
||||
style={{ background: 'rgba(59,130,246,0.15)', border: '1px solid rgba(59,130,246,0.25)' }}
|
||||
transition={{ type: 'spring', stiffness: 380, damping: 30 }} />
|
||||
)}
|
||||
<Icon className="w-4 h-4 relative z-10" style={{ color: isActive ? '#60a5fa' : 'rgba(148,163,184,0.5)' }} />
|
||||
<span className="relative z-10">{tab.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Tab panel */}
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeTab}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -8 }}
|
||||
transition={{ duration: 0.25, ease: [0.4, 0, 0.2, 1] }}
|
||||
>
|
||||
{panels[activeTab]}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Live Optimization Feed — always visible */}
|
||||
<LiveOptimizationFeed />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
214
app/src/store/useMarketingStore.ts
Normal file
214
app/src/store/useMarketingStore.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import { create } from 'zustand';
|
||||
import type {
|
||||
Campaign,
|
||||
MarketingAsset,
|
||||
AdInsight,
|
||||
LiveOptimizationEvent,
|
||||
CatalystSettings,
|
||||
LiveEventType,
|
||||
} from '@/types';
|
||||
|
||||
// ── Mock Data ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const mockCampaigns: Campaign[] = [
|
||||
{
|
||||
id: 'c1',
|
||||
name: '3BHK Prestige Launch — Dubai Marina',
|
||||
objective: 'OUTCOME_LEADS',
|
||||
status: 'ACTIVE',
|
||||
dailyBudget: 50000, // AED 500
|
||||
lifetimeSpend: 2340000,
|
||||
impressions: 487200,
|
||||
clicks: 9744,
|
||||
ctr: 2.0,
|
||||
cpa: 240,
|
||||
roi: 18.5,
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: 'c2',
|
||||
name: 'Penthouse Whale Retarget — Instagram',
|
||||
objective: 'OUTCOME_SALES',
|
||||
status: 'ACTIVE',
|
||||
dailyBudget: 100000, // AED 1000
|
||||
lifetimeSpend: 5800000,
|
||||
impressions: 92400,
|
||||
clicks: 2772,
|
||||
ctr: 3.0,
|
||||
cpa: 2094,
|
||||
roi: 42.1,
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 14),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: 'c3',
|
||||
name: '1BHK Investment — Lookalike Audience',
|
||||
objective: 'OUTCOME_TRAFFIC',
|
||||
status: 'PAUSED',
|
||||
dailyBudget: 25000, // AED 250
|
||||
lifetimeSpend: 980000,
|
||||
impressions: 213000,
|
||||
clicks: 4260,
|
||||
ctr: 2.0,
|
||||
cpa: 230,
|
||||
roi: 8.2,
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
const mockAssets: MarketingAsset[] = [
|
||||
{
|
||||
id: 'a1',
|
||||
name: 'Penthouse Cinematic — Sea View',
|
||||
type: 'video',
|
||||
status: 'ready',
|
||||
localUrl: '/assets/renders/penthouse_wan22_001.mp4',
|
||||
metaAssetId: 'meta_vid_83920',
|
||||
language: 'en',
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2),
|
||||
},
|
||||
{
|
||||
id: 'a2',
|
||||
name: 'Arabic Poster — 3BHK (Qwen-2512)',
|
||||
type: 'image',
|
||||
status: 'uploaded',
|
||||
localUrl: '/assets/renders/3bhk_qwen_ar.png',
|
||||
metaAssetId: 'meta_img_74811',
|
||||
language: 'ar',
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 5),
|
||||
},
|
||||
{
|
||||
id: 'a3',
|
||||
name: 'Amenity Deck Reel — Wan 2.2 14B',
|
||||
type: 'video',
|
||||
status: 'rendering',
|
||||
renderMessage: 'Wan 2.2 is compositing the infinity pool reflection...',
|
||||
language: 'en',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: 'a4',
|
||||
name: 'English Poster — Penthouse Launch',
|
||||
type: 'image',
|
||||
status: 'queued',
|
||||
renderMessage: 'Qwen-Image 2512 queued for cinematic poster render...',
|
||||
language: 'en',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
const mockInsights: AdInsight[] = Array.from({ length: 14 }, (_, i) => {
|
||||
const d = new Date(Date.now() - 1000 * 60 * 60 * 24 * (13 - i));
|
||||
return {
|
||||
adSetId: `as_${i}`,
|
||||
adSetName: i % 2 === 0 ? '3BHK — Dubai Marina' : 'Penthouse Retarget',
|
||||
spend: 800 + Math.floor(Math.random() * 400),
|
||||
impressions: 18000 + Math.floor(Math.random() * 10000),
|
||||
clicks: 360 + Math.floor(Math.random() * 200),
|
||||
ctr: 1.8 + Math.random() * 1.5,
|
||||
cpa: 190 + Math.floor(Math.random() * 120),
|
||||
roi: 14 + Math.random() * 20,
|
||||
date: d.toISOString().split('T')[0],
|
||||
};
|
||||
});
|
||||
|
||||
function makeLiveEvent(
|
||||
type: LiveEventType,
|
||||
message: string,
|
||||
campaignName?: string,
|
||||
value?: string
|
||||
): LiveOptimizationEvent {
|
||||
return {
|
||||
id: `ev_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
||||
type,
|
||||
message,
|
||||
campaignName,
|
||||
timestamp: new Date(),
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
const mockLiveEvents: LiveOptimizationEvent[] = [
|
||||
makeLiveEvent('pause', 'Paused Ad Set B due to CPA exceeding AED 500 threshold.', '3BHK Prestige Launch', 'CPA: AED 512'),
|
||||
makeLiveEvent('shift', 'Shifted AED 200/day budget from Ad Set B to Ad Set A (lower CPA).', '3BHK Prestige Launch', '+AED 200'),
|
||||
makeLiveEvent('rotate', 'Rotated in Penthouse Cinematic (sea view) as new creative for A/B test.', 'Penthouse Whale Retarget'),
|
||||
makeLiveEvent('optimize', 'Expanded Lookalike Audience to 2% similarity — 48k new reach.', '1BHK Investment', '+48k reach'),
|
||||
makeLiveEvent('create', 'Created new Ad Set targeting High-Net-Worth Lookalike from 23 new CRM Closed/Won leads.', 'Penthouse Whale Retarget'),
|
||||
];
|
||||
|
||||
// ── Store Types ────────────────────────────────────────────────────────────────
|
||||
|
||||
interface MarketingState {
|
||||
campaigns: Campaign[];
|
||||
activeAssets: MarketingAsset[];
|
||||
adInsights: AdInsight[];
|
||||
liveEvents: LiveOptimizationEvent[];
|
||||
settings: CatalystSettings;
|
||||
activeTab: 'studio' | 'command' | 'intelligence' | 'war-room';
|
||||
|
||||
// Actions
|
||||
addCampaign: (campaign: Campaign) => void;
|
||||
updateCampaign: (id: string, updates: Partial<Campaign>) => void;
|
||||
addAsset: (asset: MarketingAsset) => void;
|
||||
updateAsset: (id: string, updates: Partial<MarketingAsset>) => void;
|
||||
updateInsights: (insights: AdInsight[]) => void;
|
||||
pushLiveEvent: (event: LiveOptimizationEvent) => void;
|
||||
setMetaSettings: (settings: Partial<CatalystSettings>) => void;
|
||||
setActiveTab: (tab: MarketingState['activeTab']) => void;
|
||||
}
|
||||
|
||||
// ── Store ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const useMarketingStore = create<MarketingState>()((set) => ({
|
||||
campaigns: mockCampaigns,
|
||||
activeAssets: mockAssets,
|
||||
adInsights: mockInsights,
|
||||
liveEvents: mockLiveEvents,
|
||||
activeTab: 'studio',
|
||||
|
||||
settings: {
|
||||
metaAccessToken: '',
|
||||
metaAdAccountId: '',
|
||||
metaBusinessId: '',
|
||||
metaAppId: '',
|
||||
whatsappPhoneNumberId: '',
|
||||
isConnected: false,
|
||||
},
|
||||
|
||||
addCampaign: (campaign) =>
|
||||
set((state) => ({ campaigns: [...state.campaigns, campaign] })),
|
||||
|
||||
updateCampaign: (id, updates) =>
|
||||
set((state) => ({
|
||||
campaigns: state.campaigns.map((c) =>
|
||||
c.id === id ? { ...c, ...updates, updatedAt: new Date() } : c
|
||||
),
|
||||
})),
|
||||
|
||||
addAsset: (asset) =>
|
||||
set((state) => ({ activeAssets: [...state.activeAssets, asset] })),
|
||||
|
||||
updateAsset: (id, updates) =>
|
||||
set((state) => ({
|
||||
activeAssets: state.activeAssets.map((a) =>
|
||||
a.id === id ? { ...a, ...updates } : a
|
||||
),
|
||||
})),
|
||||
|
||||
updateInsights: (insights) => set({ adInsights: insights }),
|
||||
|
||||
pushLiveEvent: (event) =>
|
||||
set((state) => ({
|
||||
// Keep a rolling window of the latest 30 events
|
||||
liveEvents: [event, ...state.liveEvents].slice(0, 30),
|
||||
})),
|
||||
|
||||
setMetaSettings: (updates) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, ...updates },
|
||||
})),
|
||||
|
||||
setActiveTab: (tab) => set({ activeTab: tab }),
|
||||
}));
|
||||
@@ -1,5 +1,5 @@
|
||||
// Navigation Module Types
|
||||
export type ModuleId = 'dashboard' | 'oracle' | 'sentinel' | 'inventory' | 'settings';
|
||||
export type ModuleId = 'dashboard' | 'oracle' | 'sentinel' | 'inventory' | 'settings' | 'catalyst';
|
||||
|
||||
export interface NavItem {
|
||||
id: ModuleId;
|
||||
@@ -121,3 +121,78 @@ export interface JourneyClient {
|
||||
overallScore: number;
|
||||
events: JourneyEvent[];
|
||||
}
|
||||
|
||||
// ── Catalyst Types (Meta Marketing Agency Module) ───────────────────────────
|
||||
|
||||
export type CampaignObjective =
|
||||
| 'OUTCOME_LEADS'
|
||||
| 'OUTCOME_SALES'
|
||||
| 'OUTCOME_AWARENESS'
|
||||
| 'OUTCOME_TRAFFIC'
|
||||
| 'OUTCOME_ENGAGEMENT';
|
||||
|
||||
export type CampaignStatus = 'ACTIVE' | 'PAUSED' | 'DELETED' | 'ARCHIVED';
|
||||
|
||||
export interface Campaign {
|
||||
id: string;
|
||||
name: string;
|
||||
objective: CampaignObjective;
|
||||
status: CampaignStatus;
|
||||
dailyBudget: number; // in cents (Meta format)
|
||||
lifetimeSpend: number;
|
||||
impressions: number;
|
||||
clicks: number;
|
||||
ctr: number; // percentage
|
||||
cpa: number; // cost per acquisition
|
||||
roi: number; // percentage
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export type AssetType = 'image' | 'video';
|
||||
export type AssetStatus = 'queued' | 'rendering' | 'ready' | 'uploaded' | 'failed';
|
||||
|
||||
export interface MarketingAsset {
|
||||
id: string;
|
||||
name: string;
|
||||
type: AssetType;
|
||||
status: AssetStatus;
|
||||
renderMessage?: string; // e.g. "Wan 2.2 is rendering cinematic lighting..."
|
||||
localUrl?: string; // ComfyUI output path
|
||||
metaAssetId?: string; // Meta Ad Library ID after upload
|
||||
thumbnailUrl?: string;
|
||||
language?: 'en' | 'ar';
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface AdInsight {
|
||||
adSetId: string;
|
||||
adSetName: string;
|
||||
spend: number;
|
||||
impressions: number;
|
||||
clicks: number;
|
||||
ctr: number;
|
||||
cpa: number;
|
||||
roi: number;
|
||||
date: string; // ISO date string
|
||||
}
|
||||
|
||||
export type LiveEventType = 'pause' | 'shift' | 'create' | 'optimize' | 'alert' | 'rotate';
|
||||
|
||||
export interface LiveOptimizationEvent {
|
||||
id: string;
|
||||
type: LiveEventType;
|
||||
message: string;
|
||||
campaignName?: string;
|
||||
timestamp: Date;
|
||||
value?: string; // e.g. CPA amount, budget shift amount
|
||||
}
|
||||
|
||||
export interface CatalystSettings {
|
||||
metaAccessToken: string;
|
||||
metaAdAccountId: string;
|
||||
metaBusinessId: string;
|
||||
metaAppId: string;
|
||||
whatsappPhoneNumberId: string;
|
||||
isConnected: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user