diff --git a/.mf/diagnostics/latest.json b/.mf/diagnostics/latest.json new file mode 100644 index 0000000..1f196dc --- /dev/null +++ b/.mf/diagnostics/latest.json @@ -0,0 +1,10 @@ +{ + "latestErrorEvent": { + "code": "TYPE-001", + "message": "Failed to generate type declaration. Execute the below cmd to reproduce and fix the error. #TYPE-001\nargs: {\"cmd\":\"npx tsc --project /Users/ninojunghans/Projects/k3-sample-plugin/node_modules/.federation/tsconfig.e13503699e2d9fbd8a96160998c952e9.json\"}\nView the docs to see how to solve: https://module-federation.io/guide/troubleshooting/type#type-001", + "args": { + "cmd": "npx tsc --project /Users/ninojunghans/Projects/k3-sample-plugin/node_modules/.federation/tsconfig.e13503699e2d9fbd8a96160998c952e9.json" + }, + "timestamp": 1783330058246 + } +} \ No newline at end of file diff --git a/package.json b/package.json index 8115931..d67f8bf 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "deploy": "gh-pages -d dist --dotfiles" }, "devDependencies": { - "k3-plugin-api": "^1.5.0", "@emotion/cache": "^11.14.0", "@emotion/react": "^11.10.0", "@emotion/styled": "^11.10.5", @@ -30,6 +29,7 @@ "eslint-plugin-react-refresh": "^0.4.16", "gh-pages": "^6.3.0", "globals": "^15.14.0", + "k3-plugin-api": "^2.2.0", "react": "19.1.1", "react-dom": "19.1.1", "react-ga4": "^2.0.0", @@ -51,4 +51,4 @@ "react-ga4": "^2.0.0", "three": "^0.177.0" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc5f6fd..7c75dad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,8 +63,8 @@ importers: specifier: ^15.14.0 version: 15.14.0 k3-plugin-api: - specifier: ^1.5.0 - version: 1.5.0(react@19.1.1) + specifier: ^2.2.0 + version: 2.2.0(react@19.1.1) react: specifier: 19.1.1 version: 19.1.1 @@ -1502,8 +1502,9 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - k3-plugin-api@1.5.0: - resolution: {integrity: sha512-8MjtV8y8StONfILL+NDUvvtkRiQb+zU12ZZMsatodTUIWTJ8Ke0MkJFGrZWpCevZs5Z4JhwOo1b4It/RrGaXzg==} + k3-plugin-api@2.2.0: + resolution: {integrity: sha512-ulgtUW6M0+Xq9+mrCZSIJXqW0KPq5WbsNrgL9mrSEe7JSXMATLJQYzDf9/ymza+Y9rFK1tF3e/EzdZv5Rp9aWA==} + hasBin: true peerDependencies: react: '>=19' @@ -3569,7 +3570,7 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - k3-plugin-api@1.5.0(react@19.1.1): + k3-plugin-api@2.2.0(react@19.1.1): dependencies: react: 19.1.1 diff --git a/src/HoverInfoModel.tsx b/src/HoverInfoModel.tsx new file mode 100644 index 0000000..f54bd87 --- /dev/null +++ b/src/HoverInfoModel.tsx @@ -0,0 +1,94 @@ +/** + * 3D side of the HoverInfo demo. + * + * A dynamic model that highlights on hover and opens a context popup on + * click. The popup (see InfoPopup.tsx) renders the K3 `StandaloneVariable` + * component for every variable picked in this model's props dialog — + * the admin selects the variables via the `type: "variable"` props below. + */ +import type { ThreeEvent } from "@react-three/fiber"; +import { DynamicModel } from "k3-plugin-api"; +import { useState } from "react"; +import { openInfoPopup } from "./InfoPopup"; + +/** K3 resolves variable props to their runtime VALUES before passing them to + * the component; the raw refs arrive separately in props.variableRefs + * (keyed by prop name) — that is where the variable identity for the popup + * comes from. */ +const variableIdsFromProps = (props: any): (number | string)[] => + Object.values( + (props.variableRefs ?? {}) as Record< + string, + { variableId: number | string } + >, + ).map((ref) => ref.variableId); + +// eslint-disable-next-line react-refresh/only-export-components +const HoverInfoMesh = (props: any) => { + const [hovered, setHovered] = useState(false); + const variableIds = variableIdsFromProps(props); + + const onClick = (e: ThreeEvent) => { + e.stopPropagation(); + openInfoPopup( + e.nativeEvent.clientX, + e.nativeEvent.clientY, + variableIds, + ); + }; + + return ( + + { + e.stopPropagation(); + setHovered(true); + document.body.style.cursor = "pointer"; + }} + onPointerOut={() => { + setHovered(false); + document.body.style.cursor = "auto"; + }} + onClick={onClick} + scale={hovered ? 1.05 : 1} + > + + + + + ); +}; + +export const hoverInfoModel: DynamicModel = { + type: "hoverInfoDemo", + label: "Hover-Info Demo", + description: `

Hover-Info Demo

+

Ein 3D-Modell, das beim Hovern hervorgehoben wird und beim Klick ein +Kontext-Popup öffnet. Das Popup rendert die im Props-Dialog ausgewählten +Merkmale als vollwertige K3-Eingabekomponenten (StandaloneVariable) +sowie den Live-Gesamtpreis — direkt aus dem Plugin heraus über die +k3-plugin-api Komponenten-Proxies und K3Providers.

`, + disabledForAR: false, + materials: [], + tag: "demo", + component: HoverInfoMesh, + propsDialog: { + basic: { type: "basic" }, + popupVar1: { type: "variable", label: "Popup Merkmal 1" }, + popupVar2: { type: "variable", label: "Popup Merkmal 2" }, + popupVar3: { type: "variable", label: "Popup Merkmal 3" }, + }, + defaultProps: { + width: { expression: "1" }, + height: { expression: "1" }, + depth: { expression: "1" }, + }, +}; diff --git a/src/InfoPopup.tsx b/src/InfoPopup.tsx new file mode 100644 index 0000000..84dab98 --- /dev/null +++ b/src/InfoPopup.tsx @@ -0,0 +1,117 @@ +/** + * DOM side of the HoverInfo demo. + * + * Renders a context popup into its OWN React root (createRoot on a div + * appended to document.body) — deliberately outside the K3 tree, to + * demonstrate `K3Providers`: it bridges the host's Redux store, theme and + * query client, so K3 components like `StandaloneVariable` and `Price` + * work inside a plugin-owned root. + */ +import { K3Providers, Price, StandaloneVariable } from "k3-plugin-api"; +import { useEffect, useSyncExternalStore } from "react"; +import { createRoot } from "react-dom/client"; + +type PopupState = { + /** Screen position of the click that opened the popup. */ + anchor: { x: number; y: number } | null; + /** Database IDs of the variables to show (from the model's VariableRef props). */ + variableIds: (number | string)[]; +}; + +// ponytail: module-level store instead of zustand — the plugin has no state +// lib as dependency and this is one popup. Upgrade path: zustand vanilla store. +let state: PopupState = { anchor: null, variableIds: [] }; +const listeners = new Set<() => void>(); +const setState = (next: PopupState) => { + state = next; + listeners.forEach((l) => l()); +}; +const subscribe = (l: () => void) => { + listeners.add(l); + return () => listeners.delete(l); +}; + +export const closeInfoPopup = () => setState({ anchor: null, variableIds: [] }); + +/** Opens the popup at the given screen position. Mounts the root on first use. */ +export const openInfoPopup = ( + x: number, + y: number, + variableIds: (number | string)[], +) => { + mountOnce(); + setState({ anchor: { x, y }, variableIds }); +}; + +const InfoPopupHost = () => { + const { anchor, variableIds } = useSyncExternalStore(subscribe, () => state); + + // Close on Escape + useEffect(() => { + if (!anchor) return; + const onKey = (e: KeyboardEvent) => e.key === "Escape" && closeInfoPopup(); + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [anchor]); + + if (!anchor) return null; + + return ( + <> + {/* click-outside backdrop. z-index stays BELOW MUI's modal layer (1300) + so Select dropdowns from StandaloneVariable open ABOVE the popup. */} +
+
+ {/* Plain header: host components like TranslatableText grow an + editor-only edit affordance that needs the router — which this + standalone root intentionally does not have. */} +
+ Komponente anpassen +
+ {variableIds.length === 0 && ( +
+ No variables assigned — pick some in the model's props dialog. +
+ )} + {variableIds.map((id) => ( + + ))} +
+ +
+
+ + ); +}; + +let mounted = false; +const mountOnce = () => { + if (mounted) return; + mounted = true; + const el = document.createElement("div"); + el.id = "k3-sample-plugin-info-popup"; + document.body.appendChild(el); + createRoot(el).render( + + + , + ); +}; diff --git a/src/Plugin.tsx b/src/Plugin.tsx index 3224c00..3c64de4 100644 --- a/src/Plugin.tsx +++ b/src/Plugin.tsx @@ -1,5 +1,6 @@ import { ColorChooser } from "./ColorChooser"; import { dynamicRing } from "./DynamicRing"; +import { hoverInfoModel } from "./HoverInfoModel"; import { PriceDisplay } from "./PriceDisplay"; import { K3PluginDescriptor } from "k3-plugin-api"; @@ -19,7 +20,7 @@ export default { }, }, viewer: { - models: [dynamicRing], + models: [dynamicRing, hoverInfoModel], customLayoutComponents: { PriceDisplay }, }, } satisfies K3PluginDescriptor; diff --git a/vite.config.ts b/vite.config.ts index 1a69064..b8e4740 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -15,6 +15,7 @@ export default defineConfig({ shared: { react: { singleton: true, requiredVersion: "19.1.1" }, "react-dom": { singleton: true, requiredVersion: "19.1.1" }, + "k3-plugin-api": { singleton: true, requiredVersion: "*" }, "@mui/material": { singleton: true, requiredVersion: "^7.1.1" }, "@mui/styled-engine": { singleton: true, requiredVersion: "^7.1.1" },