feat(orgchart): integrate chart-only viewport and scoped guard fixes

Add initial fit and direct wheel zoom with preserved default presentation. Integrate five scoped GuardFix findings and AI generation preselection policy. Exclude Cell experiments and retain five historical guard failures.
This commit is contained in:
shkim4 committed 2026-10-13 18:41:14 +09:00
1 parent 0a8daf7ee7
commit d56d587fd2
28 files changed
+1170 -77

No files matched your search

+21 -8
View File
@@ -21,12 +21,17 @@ function annotateEmployeeCards(shadow: ShadowRoot, popup: NonNullable<OrgChartPr
}
}
// Core captures option objects by value. Compare them the same way so a parent
// re-render with equal inline literals neither remounts nor re-fits the chart.
const optionKey = (value: unknown) => JSON.stringify(value ?? null);
interface Instance {
readonly handle: OrgChartHandle;
readonly generation: number;
source: OrgChartDataSource;
autoLayout: OrgChartAutoLayoutOptions | undefined;
interactions: OrgChartInteractionOptions | undefined;
selectedCompanyIds: readonly string[] | undefined;
dragDropEnabled: boolean;
readonly employeeClick: NonNullable<OrgChartOptions['onEmployeeClick']>;
employeeCallbackEnabled: boolean;
@@ -49,6 +54,9 @@ export function OrgChart({
companyTopNodes,
employeeConnections,
zoom,
presentationMode,
initialView,
wheelZoom,
organizationLinks,
onEmployeeClick,
onMove,
@@ -126,6 +134,9 @@ export function OrgChart({
companyTopNodes,
employeeConnections,
zoom,
presentationMode,
initialView,
wheelZoom,
organizationLinks,
onEmployeeClick: hasAdapterCallback ? employeeClick : undefined,
onMove: result => {
@@ -134,7 +145,7 @@ export function OrgChart({
committedRef.current.onMove?.(result);
},
});
const instance: Instance = { handle, source, autoLayout, interactions, dragDropEnabled, employeeClick, employeeCallbackEnabled: hasAdapterCallback, popupMode, generation, operation: 1 };
const instance: Instance = { handle, source, autoLayout, interactions, selectedCompanyIds, dragDropEnabled, employeeClick, employeeCallbackEnabled: hasAdapterCallback, popupMode, generation, operation: 1 };
instanceRef.current = instance;
unsubscribe = handle.subscribe(state => {
if (instanceRef.current === instance && generationRef.current === generation) {
@@ -181,7 +192,7 @@ export function OrgChart({
}
}
};
}, [aggregationPolicy, engineStylesheetUrl, coreStylesheetUrl, displayMode, layoutMode, companyId, companyTopNodes, employeeConnections, zoom, organizationLinks, fixedHasEmployeeCallback]);
}, [optionKey(aggregationPolicy), engineStylesheetUrl, coreStylesheetUrl, displayMode, layoutMode, companyId, optionKey(companyTopNodes), optionKey(employeeConnections), optionKey(zoom), presentationMode, initialView, wheelZoom, optionKey(organizationLinks), fixedHasEmployeeCallback]);
useLayoutEffect(() => {
const instance = instanceRef.current;
@@ -208,14 +219,14 @@ export function OrgChart({
useLayoutEffect(() => {
const instance = instanceRef.current;
if (!instance || layoutMode !== 'auto' || instance.autoLayout === autoLayout) return;
if (!instance || layoutMode !== 'auto' || optionKey(instance.autoLayout) === optionKey(autoLayout)) return;
instance.autoLayout = autoLayout;
void instance.handle.setAutoLayout(autoLayout ?? { type: 'tb', hybridStartDepth: 3, initialVisibleDepth: 'all', hybridOrganizationIds: [] }).catch(error => {
if (instanceRef.current !== instance) return;
try { committedRef.current.onLifecycleError?.(error); }
catch { /* Keep layout setter failures in the lifecycle error contract. */ }
});
}, [autoLayout, layoutMode]);
}, [optionKey(autoLayout), layoutMode]);
useLayoutEffect(() => {
const instance = instanceRef.current;
@@ -231,24 +242,26 @@ export function OrgChart({
useLayoutEffect(() => {
const instance = instanceRef.current;
if (!instance || layoutMode !== 'auto' || instance.interactions === interactions) return;
if (!instance || layoutMode !== 'auto' || optionKey(instance.interactions) === optionKey(interactions)) return;
instance.interactions = interactions;
void instance.handle.setInteractions({ collapse: true, zoom: true, pan: true, ...interactions }).catch(error => {
if (instanceRef.current !== instance) return;
try { committedRef.current.onLifecycleError?.(error); }
catch { /* Interaction controls use the existing lifecycle error contract. */ }
});
}, [interactions, layoutMode]);
}, [optionKey(interactions), layoutMode]);
useLayoutEffect(() => {
const instance = instanceRef.current;
if (!instance || layoutMode !== 'auto') return;
// Mount already passed this selection to Core; only a changed value re-applies.
if (!instance || layoutMode !== 'auto' || optionKey(instance.selectedCompanyIds) === optionKey(selectedCompanyIds)) return;
instance.selectedCompanyIds = selectedCompanyIds;
void instance.handle.setSelectedCompanies(selectedCompanyIds).catch(error => {
if (instanceRef.current !== instance) return;
try { committedRef.current.onLifecycleError?.(error); }
catch { /* Match other lifecycle callbacks: never leak a rejected Promise. */ }
});
}, [selectedCompanyIds, layoutMode]);
}, [optionKey(selectedCompanyIds), layoutMode]);
useLayoutEffect(() => {
const instance = instanceRef.current;