Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .config/oxlint/react-compiler.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "../../node_modules/oxlint/configuration_schema.json",
"jsPlugins": [
{
"name": "react-hooks-js",
"specifier": "eslint-plugin-react-hooks"
}
],
"rules": {
"react/rules-of-hooks": "off",
"react/exhaustive-deps": "off",
"react-hooks-js/rules-of-hooks": "error",
"react-hooks-js/exhaustive-deps": "error",
"react-hooks-js/static-components": "warn",
"react-hooks-js/use-memo": "warn",
"react-hooks-js/void-use-memo": "warn",
"react-hooks-js/component-hook-factories": "warn",
"react-hooks-js/preserve-manual-memoization": "warn",
"react-hooks-js/incompatible-library": "warn",
"react-hooks-js/immutability": "warn",
"react-hooks-js/globals": "warn",
"react-hooks-js/refs": "warn",
"react-hooks-js/set-state-in-effect": "warn",
"react-hooks-js/error-boundaries": "warn",
"react-hooks-js/purity": "warn",
"react-hooks-js/set-state-in-render": "warn",
"react-hooks-js/unsupported-syntax": "warn",
"react-hooks-js/config": "warn",
"react-hooks-js/gating": "warn"
}
}
4 changes: 1 addition & 3 deletions apps/petrinaut-website/.oxlintrc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"extends": ["../../.config/oxlint/react-compiler.json"],
"plugins": ["import", "react", "jsx-a11y", "unicorn", "typescript"],
"categories": {
"correctness": "error"
Expand Down Expand Up @@ -82,9 +83,6 @@
{ "button": true, "submit": true, "reset": false }
],

"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "off",

"jsx-a11y/prefer-tag-over-role": "off",
"jsx-a11y/aria-role": ["error", { "ignoreNonDOM": false }],
"jsx-a11y/no-noninteractive-tabindex": [
Expand Down
6 changes: 3 additions & 3 deletions apps/petrinaut-website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
"@rolldown/plugin-babel": "0.2.1",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@typescript/native-preview": "7.0.0-dev.20260315.1",
"@typescript/native-preview": "7.0.0-dev.20260507.1",
"@vitejs/plugin-react": "6.0.1",
"babel-plugin-react-compiler": "1.0.0",
"oxlint": "1.55.0",
"oxlint-tsgolint": "0.17.0",
"oxlint": "1.63.0",
"oxlint-tsgolint": "0.22.1",
"vite": "8.0.10"
}
}
4 changes: 1 addition & 3 deletions libs/@hashintel/petrinaut/.oxlintrc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"extends": ["../../../.config/oxlint/react-compiler.json"],
"plugins": ["import", "react", "jsx-a11y", "unicorn", "typescript"],
"categories": {
"correctness": "error"
Expand Down Expand Up @@ -94,9 +95,6 @@
{ "button": true, "submit": true, "reset": false }
],

"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "off",

"jsx-a11y/prefer-tag-over-role": "off",
"jsx-a11y/aria-role": ["error", { "ignoreNonDOM": false }],
"jsx-a11y/no-noninteractive-tabindex": [
Expand Down
14 changes: 7 additions & 7 deletions libs/@hashintel/petrinaut/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,18 +91,18 @@
"@types/lodash-es": "4.17.12",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@typescript/native-preview": "7.0.0-dev.20260315.1",
"@typescript/native-preview": "7.0.0-dev.20260507.1",
"@vitejs/plugin-react": "6.0.1",
"babel-plugin-react-compiler": "1.0.0",
"jsdom": "24.1.3",
"oxlint": "1.55.0",
"oxlint-tsgolint": "0.17.0",
"oxlint": "1.63.0",
"oxlint-tsgolint": "0.22.1",
"react": "19.2.3",
"react-dom": "19.2.3",
"rolldown": "1.0.0-rc.9",
"rolldown-plugin-dts": "0.22.5",
"storybook": "10.2.19",
"vite": "8.0.10",
"rolldown": "1.0.0",
"rolldown-plugin-dts": "0.25.0",
"storybook": "10.3.6",
"vite": "8.0.11",
"vitest": "4.1.5"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,11 +284,11 @@ const ScrollableContent: React.FC<{ children: React.ReactNode }> = ({
const [canScrollUp, setCanScrollUp] = useState(false);
const [canScrollDown, setCanScrollDown] = useState(false);

const updateShadows = () => {
const el = scrollRef.current;
const setShadowState = (el: HTMLDivElement | null) => {
if (!el) {
return;
}

setCanScrollUp(el.scrollTop > 0);
setCanScrollDown(el.scrollTop + el.clientHeight < el.scrollHeight - 1);
};
Expand All @@ -299,16 +299,20 @@ const ScrollableContent: React.FC<{ children: React.ReactNode }> = ({
return;
}

updateShadows();
const updateObservedShadows = () => {
setShadowState(el);
};

updateObservedShadows();

const observer = new ResizeObserver(updateShadows);
const observer = new ResizeObserver(updateObservedShadows);
observer.observe(el);
for (const child of el.children) {
observer.observe(child);
}

return () => observer.disconnect();
}, [updateShadows]);
});

return (
<div className={scrollContainerStyle}>
Expand All @@ -321,7 +325,9 @@ const ScrollableContent: React.FC<{ children: React.ReactNode }> = ({
<div
ref={scrollRef}
className={panelContentStyle}
onScroll={updateShadows}
onScroll={() => {
setShadowState(scrollRef.current);
}}
>
{children}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,5 +198,5 @@ export function useKeyboardShortcuts(
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [handleKeyDown]);
}, []);
}
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,6 @@ const UNTYPED_COLOR = "#94a3b8"; // slate-400
*/
function useStreamingData(): {
store: StreamingStore;
revision: number;
metricError: string | null;
} {
"use no memo"; // imperative streaming with refs
Expand Down Expand Up @@ -602,7 +601,7 @@ function useStreamingData(): {
createEmptyStore(seriesConfig.series),
);
const processedRef = useRef(0);
const [revision, setRevision] = useState(0);
const [, setRevision] = useState(0);

// Reset store when the series structure changes (view switch or net edits).
useEffect(() => {
Expand Down Expand Up @@ -673,7 +672,6 @@ function useStreamingData(): {

return {
store: storeRef.current,
revision,
metricError: compiledMetric.error,
};
}
Expand All @@ -683,12 +681,13 @@ function useStreamingData(): {
function buildRunData(
store: StreamingStore,
hiddenPlaces: Set<string>,
length = store.length,
): uPlot.AlignedData {
const result: (number | null | undefined)[][] = [store.columns[0]!];
for (let i = 0; i < store.places.length; i++) {
if (hiddenPlaces.has(store.places[i]!.placeId)) {
// Hidden series: array of nulls (uPlot skips nulls)
result.push(new Array(store.length).fill(null));
result.push(new Array(length).fill(null));
} else {
// Visible series: direct reference to the column array (no copy!)
result.push(store.columns[i + 1]!);
Expand All @@ -700,18 +699,19 @@ function buildRunData(
function buildStackedData(
store: StreamingStore,
hiddenPlaces: Set<string>,
length = store.length,
): uPlot.AlignedData {
const visible = store.places
.map((p, i) => ({ ...p, colIdx: i + 1 }))
.filter((p) => !hiddenPlaces.has(p.placeId));

const cumulative = new Float64Array(store.length);
const cumulative = new Float64Array(length);
const series: number[][] = [];

for (const p of visible) {
const col = store.columns[p.colIdx]!;
const stacked = new Array<number>(store.length);
for (let i = 0; i < store.length; i++) {
const stacked = new Array<number>(length);
for (let i = 0; i < length; i++) {
cumulative[i]! += col[i] ?? 0;
stacked[i] = cumulative[i]!;
}
Expand Down Expand Up @@ -1180,15 +1180,13 @@ const UPlotChart: React.FC<{
store: StreamingStore;
chartType: TimelineChartType;
hiddenPlaces: Set<string>;
revision: number;
totalFrames: number;
currentFrameIndex: number;
className?: string;
}> = ({
store,
chartType,
hiddenPlaces,
revision,
totalFrames,
currentFrameIndex,
className,
Expand All @@ -1207,6 +1205,7 @@ const UPlotChart: React.FC<{
// Boolean flag for the creation effect β€” triggers when size first becomes
// available (null β†’ non-null) without re-firing on every resize.
const hasSize = size != null;
const dataLength = store.length;

// Stable identity: always calls the latest closure but never changes reference,
// so it doesn't trigger chart recreation when totalFrames changes.
Expand All @@ -1218,13 +1217,13 @@ const UPlotChart: React.FC<{
// React Compiler ("use no memo"), and buildStackedData allocates O(places Γ—
// frames) per call. Without memoization it would recompute on every render
// (e.g. every playback frame), and the result would be silently discarded
// since Effect 3 only consumes it when `revision` changes.
// since Effect 3 only consumes it when the store length or series changes.
const data = useMemo(
() =>
chartType === "stacked"
? buildStackedData(store, hiddenPlaces)
: buildRunData(store, hiddenPlaces),
[revision, chartType, hiddenPlaces],
? buildStackedData(store, hiddenPlaces, dataLength)
: buildRunData(store, hiddenPlaces, dataLength),
[store, dataLength, chartType, hiddenPlaces],
);

// -- Effect 1: create/destroy uPlot on structural changes -------------------
Expand All @@ -1233,18 +1232,28 @@ const UPlotChart: React.FC<{
// Note: parent (SimulationTimelineContent) gates on store.length === 0,
// so this component only mounts once data is available.
const wrapper = wrapperRef.current;
if (!wrapper || !size) {
if (!wrapper || !hasSize) {
return;
}

const initialSize = {
width: wrapper.clientWidth,
height: wrapper.clientHeight,
};

const initialData =
chartType === "stacked"
? buildStackedData(store, hiddenPlaces)
: buildRunData(store, hiddenPlaces);

const tooltip = createTooltip();

const opts = buildUPlotOptions({
store,
storeRef,
chartType,
hiddenPlaces,
size,
size: initialSize,
onScrub,
getPlayheadFrame: () => playheadFrameRef.current,
tooltip,
Expand All @@ -1253,7 +1262,7 @@ const UPlotChart: React.FC<{
chartRef.current?.destroy();

// eslint-disable-next-line new-cap -- uPlot's constructor is lowercase by convention
const u = new uPlot(opts, data, wrapper);
const u = new uPlot(opts, initialData, wrapper);
chartRef.current = u;

// Mount tooltip inside u.over (the cursor overlay div). It positions
Expand All @@ -1268,10 +1277,18 @@ const UPlotChart: React.FC<{
u.destroy();
chartRef.current = null;
};
// Recreate only when chart type, visible series, or size availability changes.
// Recreate only when chart type, store, visible series, or size availability changes.
// onScrub is stable (useStableCallback). Subsequent size changes trigger
// setSize (Effect 2), not recreation.
}, [chartType, hiddenPlaces, store.places.length, hasSize]);
}, [
chartType,
hiddenPlaces,
store,
store.places.length,
storeRef,
hasSize,
onScrub,
]);

// -- Effect 2: sync container size to existing chart ------------------------

Expand All @@ -1285,7 +1302,7 @@ const UPlotChart: React.FC<{

useEffect(() => {
chartRef.current?.setData(data);
}, [revision]);
}, [data]);

// -- Effect 4: playhead redraw ---------------------------------------------

Expand Down Expand Up @@ -1346,7 +1363,7 @@ const SimulationTimelineContent: React.FC = () => {
const { timelineChartType: chartType } = use(EditorContext);
const { totalFrames } = use(SimulationContext);
const { currentFrameIndex } = use(PlaybackContext);
const { store, revision, metricError } = useStreamingData();
const { store, metricError } = useStreamingData();

const [hiddenPlaces, setHiddenPlaces] = useState<Set<string>>(new Set());

Expand Down Expand Up @@ -1387,7 +1404,6 @@ const SimulationTimelineContent: React.FC = () => {
store={store}
chartType={chartType}
hiddenPlaces={hiddenPlaces}
revision={revision}
totalFrames={totalFrames}
currentFrameIndex={currentFrameIndex}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,7 @@ const FilterableListContent = <T extends FilterableListItem>({
}
}}
role="option"
tabIndex={-1}
aria-selected={selected}
className={listItemRowStyle({
selectable: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const InitialStateEditor: React.FC<InitialStateEditorProps> = ({

setInitialMarking(placeId, { values, count });
};
}, [hasSimulation, columns.length, setInitialMarking, placeId]);
}, [hasSimulation, readOnly, columns.length, setInitialMarking, placeId]);

return <Spreadsheet columns={columns} data={data} onChange={handleChange} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -154,16 +154,16 @@ export function useMetricLspSession(code: string): string {
const [sessionId] = useState(() => crypto.randomUUID());
const initializedRef = useRef(false);

const sessionData = { sessionId, code };

useEffect(() => {
const sessionData = { sessionId, code };

if (!initializedRef.current) {
initializeMetricSession(sessionData);
initializedRef.current = true;
} else {
updateMetricSession(sessionData);
}
}, [sessionData, initializeMetricSession, updateMetricSession]);
}, [code, initializeMetricSession, sessionId, updateMetricSession]);

useEffect(() => {
return () => {
Expand Down
Loading
Loading