Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
1eea810
Update checklist according to latest proposal
ghengeveld Oct 30, 2025
8e471bf
Update checklistData according to latest concept
ghengeveld Nov 1, 2025
c0eb34b
Handle narrow width for ChecklistModule by introducing an Optional co…
ghengeveld Nov 1, 2025
94c5c62
Update checklist criteria and checks
ghengeveld Nov 3, 2025
5f1bd57
Persist autocompleted items in node_modules/.cache and add telemetry
ghengeveld Nov 4, 2025
5ba33eb
Support conditional guide items
ghengeveld Nov 4, 2025
9d28bb0
Use simpler navigation API
ghengeveld Nov 4, 2025
cd97967
Add guided tour and intent survey to checklist
ghengeveld Nov 4, 2025
10917ad
Show completion message instead of mute toggle when all tasks are com…
ghengeveld Nov 4, 2025
589f1f0
Prevent onboarding from returning to splash screen
ghengeveld Nov 4, 2025
624bfc6
Fix build command
ghengeveld Nov 4, 2025
5845f57
Prevent guided tour from showing intent survey if it has already been…
ghengeveld Nov 4, 2025
1411869
Ensure subscription callback is a function
ghengeveld Nov 4, 2025
70914fe
Revert back to using ReactDOM.render
ghengeveld Nov 5, 2025
af5996a
Completion detection for install-vitest and run-tests tasks
ghengeveld Nov 6, 2025
571eb37
Don't skip checklist tasks when closing tour tooltip
ghengeveld Nov 6, 2025
da9c7b8
Better looking code blocks
ghengeveld Nov 6, 2025
8a27b33
Fix mocks
ghengeveld Nov 6, 2025
2adfb83
Add items for installing dependencies
ghengeveld Nov 6, 2025
870ab94
Use createRoot rather than deprecated ReactDOM.render
ghengeveld Nov 7, 2025
d6e6f09
Fix testing widget overflow to allow highlighting for onboarding
ghengeveld Nov 7, 2025
85b9fa1
Reduce update frequency by throttling index updates
ghengeveld Nov 7, 2025
8c5c660
Pass index to available function to receive updates, and don't pass i…
ghengeveld Nov 7, 2025
02917c4
Make content a function
ghengeveld Nov 7, 2025
073778c
Ignore example stories where appropriate
ghengeveld Nov 7, 2025
bc7ac39
Prevent clicking on underlying collapse toggle when using checklist r…
ghengeveld Nov 7, 2025
9aef937
Fix addon identifier
ghengeveld Nov 7, 2025
e311a22
Rename Tour to TourGuide and make it a generic component
ghengeveld Nov 7, 2025
1f79889
Make renderTourGuide a static method on the TourGuide component
ghengeveld Nov 7, 2025
1c8bb66
Fix GuidePage sidebar toggle
ghengeveld Nov 7, 2025
056a80f
Refactor onboarding components: consolidate HighlightElement and Tour…
ghengeveld Nov 10, 2025
f0dfab7
Refactor useChecklist to provide all data needed in Checklist component
ghengeveld Nov 12, 2025
d9df904
Animate the completion of a checklist item in the widget
ghengeveld Nov 12, 2025
2fd404c
Animate strikethrough
ghengeveld Nov 13, 2025
bb87ae0
Hide certain checklist items once completed, and update API for items…
ghengeveld Nov 13, 2025
7591d75
Reset itemIndex to only include ready items, in order to fix round-ro…
ghengeveld Nov 13, 2025
88afdc1
Hide action buttons in checklist module unless hovering the item
ghengeveld Nov 13, 2025
64c5d96
Update FocusProxy to use a data attribute rather than id attribute
ghengeveld Nov 13, 2025
760764f
Merge branch 'onboarding-guide' into checklist-tasks
ghengeveld Nov 13, 2025
bc804e3
Merge branch 'onboarding-checklist' into checklist-tasks
ghengeveld Nov 14, 2025
aad4c79
Remove storybook/internal/manager/manager-stores
ghengeveld Nov 14, 2025
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
Prev Previous commit
Next Next commit
Animate the completion of a checklist item in the widget
  • Loading branch information
ghengeveld committed Nov 12, 2025
commit d9df904f11e71d36cb9c962c7cff7b47e4de450d
18 changes: 18 additions & 0 deletions code/core/src/components/components/Particles.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import preview from '../../../../.storybook/preview';
import { Particles } from './Particles';

const SomeComponent = (props: React.ComponentProps<'div'>) => {
return <div {...props}>Cool</div>;
};

const meta = preview.meta({
component: Particles,
parameters: {
layout: 'centered',
},
args: {
anchor: SomeComponent,
},
});

export const Default = meta.story({});
165 changes: 165 additions & 0 deletions code/core/src/components/components/Particles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import type { ComponentProps } from 'react';
import React, { memo, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';

import { keyframes, styled } from 'storybook/theming';

const Shape = styled.svg(({ color }) => ({
fill: color,
position: 'absolute',
inset: '0',
margin: 'auto',
width: '12px',
height: '12px',
pointerEvents: 'none',
}));

const Donut = (props: ComponentProps<typeof Shape>) => (
<Shape viewBox="0 0 90 90" xmlns="http://www.w3.org/2000/svg" color="red" {...props}>
<path d="M45 0c24.853 0 45 20.147 45 45S69.853 90 45 90 0 69.853 0 45 20.147 0 45 0zm.5 27C35.283 27 27 35.283 27 45.5S35.283 64 45.5 64 64 55.717 64 45.5 55.717 27 45.5 27z" />
</Shape>
);

const L = (props: ComponentProps<typeof Shape>) => (
<Shape viewBox="0 0 55 89" xmlns="http://www.w3.org/2000/svg" color="#66BF3C" {...props}>
<path d="M55 3v83a3 3 0 01-3 3H3a3 3 0 01-3-3V64a3 3 0 013-3h21a3 3 0 003-3V3a3 3 0 013-3h22a3 3 0 013 3z" />
</Shape>
);

const Slice = (props: ComponentProps<typeof Shape>) => (
<Shape viewBox="0 0 92 92" xmlns="http://www.w3.org/2000/svg" color="#FF4785" {...props}>
<path d="M92 89V3c0-3-2.056-3-3-3C39.294 0 0 39.294 0 89c0 0 0 3 3 3h86a3 3 0 003-3z" />
</Shape>
);

const Square = ({ style, ...props }: ComponentProps<typeof Shape>) => (
<Shape
viewBox="0 0 90 90"
xmlns="http://www.w3.org/2000/svg"
color="#1EA7FD"
{...props}
style={{ borderRadius: 5, ...style }}
>
<path d="M0 0h90v90H0z" />
</Shape>
);

const Triangle = (props: ComponentProps<typeof Shape>) => (
<Shape viewBox="0 0 96 88" xmlns="http://www.w3.org/2000/svg" color="#FFAE00" {...props}>
<path d="M50.63 1.785l44.928 81.77A3 3 0 0192.928 88H3.072a3 3 0 01-2.629-4.445l44.929-81.77a3 3 0 015.258 0z" />
</Shape>
);

const T = (props: ComponentProps<typeof Shape>) => (
<Shape viewBox="0 0 92 62" xmlns="http://www.w3.org/2000/svg" color="#FC521F" {...props}>
<path d="M63 3v25a3 3 0 003 3h23a3 3 0 013 3v25a3 3 0 01-3 3H3a3 3 0 01-3-3V34a3 3 0 013-3h24a3 3 0 003-3V3a3 3 0 013-3h27a3 3 0 013 3z" />
</Shape>
);

const Z = (props: ComponentProps<typeof Shape>) => (
<Shape viewBox="0 0 56 90" xmlns="http://www.w3.org/2000/svg" color="#6F2CAC" {...props}>
<path d="M28 3v25a3 3 0 003 3h22a3 3 0 013 3v53a3 3 0 01-3 3H31a3 3 0 01-3-3V62a3 3 0 00-3-3H3a3 3 0 01-3-3V3a3 3 0 013-3h22a3 3 0 013 3z" />
</Shape>
);
Comment on lines +17 to +63
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix the hardcoded colors and shape count mismatch.

Two issues here:

  1. Hardcoded colors are overridden: Each shape component has a hardcoded color prop (e.g., color="red" on line 18, color="#66BF3C" on line 24), but these are always overridden by the colors array passed from the parent Particles component (line 158: color={colors[index]}). This creates confusion about which colors are actually used.

  2. Shape count mismatch: Only 7 shapes are defined here, but NUM_OF_PARTICLES is set to 8 (line 105). This will cause shapes[7] to be undefined, leading to a runtime error when rendering the 8th particle.

Apply this diff to fix both issues:

 const Donut = (props: ComponentProps<typeof Shape>) => (
-  <Shape viewBox="0 0 90 90" xmlns="http://www.w3.org/2000/svg" color="red" {...props}>
+  <Shape viewBox="0 0 90 90" xmlns="http://www.w3.org/2000/svg" {...props}>
     <path d="M45 0c24.853 0 45 20.147 45 45S69.853 90 45 90 0 69.853 0 45 20.147 0 45 0zm.5 27C35.283 27 27 35.283 27 45.5S35.283 64 45.5 64 64 55.717 64 45.5 55.717 27 45.5 27z" />
   </Shape>
 );
 
 const L = (props: ComponentProps<typeof Shape>) => (
-  <Shape viewBox="0 0 55 89" xmlns="http://www.w3.org/2000/svg" color="#66BF3C" {...props}>
+  <Shape viewBox="0 0 55 89" xmlns="http://www.w3.org/2000/svg" {...props}>
     <path d="M55 3v83a3 3 0 01-3 3H3a3 3 0 01-3-3V64a3 3 0 013-3h21a3 3 0 003-3V3a3 3 0 013-3h22a3 3 0 013 3z" />
   </Shape>
 );
 
 const Slice = (props: ComponentProps<typeof Shape>) => (
-  <Shape viewBox="0 0 92 92" xmlns="http://www.w3.org/2000/svg" color="#FF4785" {...props}>
+  <Shape viewBox="0 0 92 92" xmlns="http://www.w3.org/2000/svg" {...props}>
     <path d="M92 89V3c0-3-2.056-3-3-3C39.294 0 0 39.294 0 89c0 0 0 3 3 3h86a3 3 0 003-3z" />
   </Shape>
 );
 
 const Square = ({ style, ...props }: ComponentProps<typeof Shape>) => (
   <Shape
     viewBox="0 0 90 90"
     xmlns="http://www.w3.org/2000/svg"
-    color="#1EA7FD"
     {...props}
     style={{ borderRadius: 5, ...style }}
   >
     <path d="M0 0h90v90H0z" />
   </Shape>
 );
 
 const Triangle = (props: ComponentProps<typeof Shape>) => (
-  <Shape viewBox="0 0 96 88" xmlns="http://www.w3.org/2000/svg" color="#FFAE00" {...props}>
+  <Shape viewBox="0 0 96 88" xmlns="http://www.w3.org/2000/svg" {...props}>
     <path d="M50.63 1.785l44.928 81.77A3 3 0 0192.928 88H3.072a3 3 0 01-2.629-4.445l44.929-81.77a3 3 0 015.258 0z" />
   </Shape>
 );
 
 const T = (props: ComponentProps<typeof Shape>) => (
-  <Shape viewBox="0 0 92 62" xmlns="http://www.w3.org/2000/svg" color="#FC521F" {...props}>
+  <Shape viewBox="0 0 92 62" xmlns="http://www.w3.org/2000/svg" {...props}>
     <path d="M63 3v25a3 3 0 003 3h23a3 3 0 013 3v25a3 3 0 01-3 3H3a3 3 0 01-3-3V34a3 3 0 013-3h24a3 3 0 003-3V3a3 3 0 013-3h27a3 3 0 013 3z" />
   </Shape>
 );
 
 const Z = (props: ComponentProps<typeof Shape>) => (
-  <Shape viewBox="0 0 56 90" xmlns="http://www.w3.org/2000/svg" color="#6F2CAC" {...props}>
+  <Shape viewBox="0 0 56 90" xmlns="http://www.w3.org/2000/svg" {...props}>
     <path d="M28 3v25a3 3 0 003 3h22a3 3 0 013 3v53a3 3 0 01-3 3H31a3 3 0 01-3-3V62a3 3 0 00-3-3H3a3 3 0 01-3-3V3a3 3 0 013-3h22a3 3 0 013 3z" />
   </Shape>
 );
+
+const Circle = (props: ComponentProps<typeof Shape>) => (
+  <Shape viewBox="0 0 90 90" xmlns="http://www.w3.org/2000/svg" {...props}>
+    <circle cx="45" cy="45" r="45" />
+  </Shape>
+);

Then update line 122 to include the new shape:

-  const shapes = sortRandomly([Donut, L, Slice, Square, Triangle, T, Z]);
+  const shapes = sortRandomly([Donut, L, Slice, Square, Triangle, T, Z, Circle]);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In code/core/src/components/components/Particles.tsx around lines 17–63, several
Shape components include hardcoded color props which are always overridden by
the parent colors array, and only 7 shape components are defined while
NUM_OF_PARTICLES is 8 causing an out-of-bounds access. Remove the hardcoded
color=... attributes from each Shape declaration so the parent-provided color is
authoritative, add a new eighth Shape component (e.g., Diamond or Hexagon)
matching the same pattern, and then update the shapes array at line 122 to
include the new component so its length equals NUM_OF_PARTICLES (8).


const fadeToTransparent = keyframes`
to {
opacity: 0;
}
`;

const disperse = keyframes`
to {
transform: translate(
calc(cos(var(--angle)) * var(--distance)),
calc(sin(var(--angle)) * var(--distance))
) rotate(var(--rotation));
}
`;

const slideDown = keyframes`
to {
transform: translateY(50px);
}
`;

const Container = styled.div({
position: 'absolute',
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
gap: 10,
'--particle-curve': 'cubic-bezier(0.2, 0.56, 0, 1)',
animation: `${slideDown} 1000ms forwards cubic-bezier(0.587, -0.381, 0.583, 0.599)`,
animationDelay: '150ms',
zIndex: 999,

svg: {
width: 15,
height: 15,
animation: `${fadeToTransparent} var(--fade-duration) forwards, ${disperse} 1000ms forwards var(--particle-curve)`,
},
});

const FADE_DURATION = 1200;
const NUM_OF_PARTICLES = 8;
// `JITTER` is the amount of variance allowed for each angle.
// Tweak this value to control how orderly/chaotic the animation appears.
const JITTER = 15;

const random = (min: number, max: number) => Math.random() * (max - min) + min;
const sortRandomly = (array: any[]) => array.sort(() => Math.random() - 0.5);

export const Particles = memo(function Particles({
anchor: Anchor,
}: {
anchor: React.ElementType;
}) {
const anchorRef = useRef<HTMLDivElement>(null);
const [left, setLeft] = useState(0);
const [top, setTop] = useState(0);

const shapes = sortRandomly([Donut, L, Slice, Square, Triangle, T, Z]);
const colors = sortRandomly([
'#FF0000',
'#FF4787',
'#66BF3C',
'#1EA7FD',
'#FC521F',
'#6F2CAC',
'#FFAE00',
]);
Comment on lines +122 to +131
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Move array initialization outside render or use useMemo.

The shapes and colors arrays are created and randomized on every render, which defeats the purpose of using memo on this component. This causes unnecessary work and can lead to animation inconsistencies if the component re-renders.

Apply this diff to fix the performance issue:

+const shapes = sortRandomly([Donut, L, Slice, Square, Triangle, T, Z, Circle]);
+const colors = sortRandomly([
+  '#FF0000',
+  '#FF4785',
+  '#66BF3C',
+  '#1EA7FD',
+  '#FC521F',
+  '#6F2CAC',
+  '#FFAE00',
+  '#37D5D3',
+]);
+
 export const Particles = memo(function Particles({
   anchor: Anchor,
 }: {
   anchor: React.ElementType;
 }) {
   const anchorRef = useRef<HTMLDivElement>(null);
   const [left, setLeft] = useState(0);
   const [top, setTop] = useState(0);
 
-  const shapes = sortRandomly([Donut, L, Slice, Square, Triangle, T, Z]);
-  const colors = sortRandomly([
-    '#FF0000',
-    '#FF4787',
-    '#66BF3C',
-    '#1EA7FD',
-    '#FC521F',
-    '#6F2CAC',
-    '#FFAE00',
-  ]);
-
   useLayoutEffect(() => {

Note: I've also added an 8th color ('#37D5D3' - seafoam from the theme palette) to match the 8 particles after adding the Circle shape.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In code/core/src/components/components/Particles.tsx around lines 122 to 131,
the shapes and colors arrays are being created and randomized on every render;
move their initialization out of the render path by wrapping the arrays in
useMemo with an empty dependency array so they are computed once (or hoist them
to module scope if hooks are unsuitable), include the new Circle shape in the
shapes list to match particle count, and add the 8th color '#37D5D3' to the
colors array; ensure you call sortRandomly inside useMemo and keep the
dependency array empty to avoid re-randomizing on re-renders.


useLayoutEffect(() => {
const rect = anchorRef.current?.getBoundingClientRect();
if (rect) {
setLeft(rect.left + rect.width / 2);
setTop(rect.top + rect.height / 2);
}
}, []);
Comment on lines +133 to +139
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add handling for anchor repositioning.

The useLayoutEffect only runs once on mount (empty dependency array). If the anchor element moves due to window resize, layout shifts, or other dynamic changes, the particles will remain positioned at the original coordinates and appear disconnected from the anchor.

Consider adding a resize observer or window resize handler:

   useLayoutEffect(() => {
-    const rect = anchorRef.current?.getBoundingClientRect();
-    if (rect) {
-      setLeft(rect.left + rect.width / 2);
-      setTop(rect.top + rect.height / 2);
-    }
+    const updatePosition = () => {
+      const rect = anchorRef.current?.getBoundingClientRect();
+      if (rect) {
+        setLeft(rect.left + rect.width / 2);
+        setTop(rect.top + rect.height / 2);
+      }
+    };
+
+    updatePosition();
+
+    const resizeObserver = new ResizeObserver(updatePosition);
+    if (anchorRef.current) {
+      resizeObserver.observe(anchorRef.current);
+    }
+
+    window.addEventListener('resize', updatePosition);
+    window.addEventListener('scroll', updatePosition, true);
+
+    return () => {
+      resizeObserver.disconnect();
+      window.removeEventListener('resize', updatePosition);
+      window.removeEventListener('scroll', updatePosition, true);
+    };
   }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useLayoutEffect(() => {
const rect = anchorRef.current?.getBoundingClientRect();
if (rect) {
setLeft(rect.left + rect.width / 2);
setTop(rect.top + rect.height / 2);
}
}, []);
useLayoutEffect(() => {
const updatePosition = () => {
const rect = anchorRef.current?.getBoundingClientRect();
if (rect) {
setLeft(rect.left + rect.width / 2);
setTop(rect.top + rect.height / 2);
}
};
updatePosition();
const resizeObserver = new ResizeObserver(updatePosition);
if (anchorRef.current) {
resizeObserver.observe(anchorRef.current);
}
window.addEventListener('resize', updatePosition);
window.addEventListener('scroll', updatePosition, true);
return () => {
resizeObserver.disconnect();
window.removeEventListener('resize', updatePosition);
window.removeEventListener('scroll', updatePosition, true);
};
}, []);
🤖 Prompt for AI Agents
In code/core/src/components/components/Particles.tsx around lines 133 to 139,
the current useLayoutEffect only runs on mount so particle coordinates never
update when the anchor moves; add logic to recompute left/top whenever the
anchor's layout changes by creating a ResizeObserver on anchorRef.current (and
also listen for window 'resize' for broader layout changes), call the same
function that reads getBoundingClientRect to update setLeft/setTop, debounce or
coalesce updates using requestAnimationFrame (or a short timeout) to avoid
thrashing, and ensure you clean up by disconnecting the ResizeObserver and
removing the window listener in the effect's cleanup.


return (
<div ref={anchorRef}>
<Anchor />
{createPortal(
<Container style={{ top: top + 'px', left: left + 'px' }}>
{shapes.map((Particle, index) => {
const angle = (360 / NUM_OF_PARTICLES) * index + random(-JITTER, JITTER);
const distance = random(50, 80);
const rotation = random(-360, 360);

const style = {
'--angle': angle + 'deg',
'--distance': distance + 'px',
'--rotation': rotation + 'deg',
'--fade-duration': FADE_DURATION + 'ms',
} as React.CSSProperties;

return <Particle key={Particle.name} style={style} color={colors[index]} />;
})}
Comment on lines +146 to +159
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Randomized particle properties should be stable across re-renders.

The angle, distance, and rotation values are recalculated with new random values on every render. This means if the component re-renders for any reason (e.g., parent state change), the animation will restart with completely different trajectories, creating a jarring visual experience.

Move the particle configuration outside the render cycle:

+const particleConfigs = shapes.map((_, index) => ({
+  angle: (360 / NUM_OF_PARTICLES) * index + random(-JITTER, JITTER),
+  distance: random(50, 80),
+  rotation: random(-360, 360),
+}));
+
 export const Particles = memo(function Particles({
   anchor: Anchor,
 }: {
   anchor: React.ElementType;
 }) {
   // ... existing code ...
 
   return (
     <div ref={anchorRef}>
       <Anchor />
       {createPortal(
         <Container style={{ top: top + 'px', left: left + 'px' }}>
           {shapes.map((Particle, index) => {
-            const angle = (360 / NUM_OF_PARTICLES) * index + random(-JITTER, JITTER);
-            const distance = random(50, 80);
-            const rotation = random(-360, 360);
+            const { angle, distance, rotation } = particleConfigs[index];
 
             const style = {
               '--angle': angle + 'deg',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{shapes.map((Particle, index) => {
const angle = (360 / NUM_OF_PARTICLES) * index + random(-JITTER, JITTER);
const distance = random(50, 80);
const rotation = random(-360, 360);
const style = {
'--angle': angle + 'deg',
'--distance': distance + 'px',
'--rotation': rotation + 'deg',
'--fade-duration': FADE_DURATION + 'ms',
} as React.CSSProperties;
return <Particle key={Particle.name} style={style} color={colors[index]} />;
})}
export const Particles = memo(function Particles({
anchor: Anchor,
}: {
anchor: React.ElementType;
}) {
// ... existing code ...
const particleConfigs = useMemo(
() => shapes.map((_, index) => ({
angle: (360 / NUM_OF_PARTICLES) * index + random(-JITTER, JITTER),
distance: random(50, 80),
rotation: random(-360, 360),
})),
[shapes]
);
return (
<div ref={anchorRef}>
<Anchor />
{createPortal(
<Container style={{ top: top + 'px', left: left + 'px' }}>
{shapes.map((Particle, index) => {
const { angle, distance, rotation } = particleConfigs[index];
const style = {
'--angle': angle + 'deg',
'--distance': distance + 'px',
'--rotation': rotation + 'deg',
'--fade-duration': FADE_DURATION + 'ms',
} as React.CSSProperties;
return <Particle key={Particle.name} style={style} color={colors[index]} />;
})}
</Container>,
anchorRef.current
)}
</div>
);
});
🤖 Prompt for AI Agents
In code/core/src/components/components/Particles.tsx around lines 146 to 159,
the per-particle random values (angle, distance, rotation) are being
recalculated on every render which restarts animations; fix this by generating
and storing a stable particle-config array once (on mount) and referencing it
during renders — e.g., create an array of objects with
angle/distance/rotation/color keyed by particle index using useRef or useMemo so
values are initialized once, then map shapes to that stored config to build the
style prop and keep keys stable.

</Container>,
document.getElementById('root') ?? document.body
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Verify portal target assumption.

The portal assumes a DOM element with id 'root' exists. While falling back to document.body is reasonable, this might not work optimally in all contexts (e.g., Shadow DOM, iframes, or custom Storybook configurations).

Run this script to check if the 'root' element assumption holds across the Storybook setup:


🏁 Script executed:

#!/bin/bash
# Description: Search for root element usage and portal patterns in the codebase

# Check for other portal usages and their targets
echo "=== Portal usage patterns in codebase ==="
rg -n "createPortal" --type tsx --type ts -A 2 -B 1

# Check for root element references
echo -e "\n=== References to 'root' element ==="
rg -n "getElementById\(['\"]root['\"]" --type tsx --type ts --type html -C 2

Length of output: 135


I'll fix the script syntax to work with ripgrep's actual supported type flags.


🏁 Script executed:

#!/bin/bash
# Description: Search for root element usage and portal patterns in the codebase

# Check for other portal usages and their targets
echo "=== Portal usage patterns in codebase ==="
rg -n "createPortal" -g "*.ts" -g "*.tsx" -A 2 -B 1

# Check for root element references
echo -e "\n=== References to 'root' element ==="
rg -n "getElementById\(['\"]root['\"]" -g "*.ts" -g "*.tsx" -g "*.html" -C 2

# Check the specific file and surrounding context
echo -e "\n=== Particles.tsx context around line 161 ==="
rg -n "createPortal\|getElementById.*root\|document.body" code/core/src/components/components/Particles.tsx -C 3

# Check for Storybook configurations that might define root element
echo -e "\n=== Storybook configuration files ==="
fd -e "preview" -e "storybook" -e "config"

Length of output: 4968


🏁 Script executed:

#!/bin/bash
# Check WithTooltip.tsx to see how portalTarget is set
echo "=== WithTooltip.tsx portalTarget definition ==="
rg -n "portalTarget" code/core/src/components/components/tooltip/WithTooltip.tsx -B 5 -A 2

# Get fuller context around Particles.tsx line 161
echo -e "\n=== Particles.tsx createPortal call context ==="
sed -n '140,165p' code/core/src/components/components/Particles.tsx

# Check if Particles component is used in Storybook
echo -e "\n=== Particles component usage ==="
rg -n "Particles" -g "*.stories.tsx" -C 2

Length of output: 2221


Update portal target to check storybook-root before root for Storybook 7.0+ compatibility.

The review comment is valid. The codebase contains a defensive pattern in code/addons/pseudo-states/src/preview/withPseudoState.ts (lines 142-143) that checks for 'storybook-root' (Storybook 7.0+) before falling back to 'root'. The Particles component, which is used in Storybook (see Particles.stories.tsx), only checks for 'root', which means it will miss the correct container in Storybook 7.0+ environments and fall back to document.body, causing the portal to render in a suboptimal location.

Align Particles.tsx line 161 with the established pattern:

document.getElementById('storybook-root') || document.getElementById('root') ?? document.body
🤖 Prompt for AI Agents
In code/core/src/components/components/Particles.tsx around line 161, the portal
target only checks for 'root' and falls back to document.body, which misses
Storybook 7+ container; update the target resolution to first try
document.getElementById('storybook-root') then document.getElementById('root')
and finally document.body so the portal mounts into the correct Storybook
container.

)}
</div>
);
});
1 change: 0 additions & 1 deletion code/core/src/manager-api/stores/checklist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
export const universalChecklistStore = experimental_UniversalStore.create<StoreState, StoreEvent>({
...UNIVERSAL_CHECKLIST_STORE_OPTIONS,
leader: globalThis.CONFIG_TYPE === 'PRODUCTION',
debug: true,
});

export const checklistStore = createChecklistStore(universalChecklistStore);
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,28 @@ const meta = preview.meta({
},
});

export const Default = meta.story({});
export const Default = meta.story({
play: () => {
setTimeout(() => {
mockStore.setState({
loaded: true,
muted: false,
accepted: ['controls'],
done: ['install-storybook', 'render-component', 'whats-new-storybook-10', 'viewports'],
skipped: ['more-components', 'more-stories'],
});
}, 4000);
setTimeout(() => {
mockStore.setState({
loaded: true,
muted: false,
accepted: ['controls'],
done: ['install-storybook', 'render-component', 'whats-new-storybook-10', 'viewports'],
skipped: ['more-components', 'more-stories', 'install-vitest'],
});
}, 8000);
Comment on lines +41 to +58
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These show be managed using promises somehow, so storybook has any idea that the play function is actually still running; and the steps show up in the interactions panel.

},
});

export const Narrow = meta.story({
decorators: [(Story) => <div style={{ maxWidth: 200 }}>{Story()}</div>],
Expand Down
80 changes: 70 additions & 10 deletions code/core/src/manager/components/sidebar/ChecklistModule.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { createRef, useMemo } from 'react';
import React, { createRef, useEffect, useMemo, useState } from 'react';

import {
Card,
Expand All @@ -20,15 +20,32 @@ import {
EyeCloseIcon,
ListUnorderedIcon,
StatusFailIcon,
StatusPassIcon,
} from '@storybook/icons';

import { internal_checklistStore as checklistStore } from '#manager-stores';
import { useStorybookApi } from 'storybook/manager-api';
import { styled } from 'storybook/theming';
import { keyframes, styled } from 'storybook/theming';

import { Particles } from '../../../components/components/Particles';
import { TextFlip } from '../TextFlip';
import { Transition, TransitionGroup } from '../Transition';
import { useChecklist } from './useChecklist';
import { type ChecklistItem, useChecklist } from './useChecklist';

const fadeScaleIn = keyframes`
from {
opacity: 0;
transform: scale(0.7);
}
to {
opacity: 1;
transform: scale(1);
}
`;

type ChecklistItemWithRef = ChecklistItem & {
nodeRef: React.RefObject<HTMLLIElement>;
};

const CollapsibleWithMargin = styled(Collapsible)(({ collapsed }) => ({
marginTop: collapsed ? 0 : 16,
Expand All @@ -52,6 +69,27 @@ const ProgressCircle = styled(ProgressSpinner)(({ theme }) => ({
color: theme.color.secondary,
}));

const Checked = styled(StatusPassIcon)(({ theme }) => ({
padding: 1,
borderRadius: '50%',
background: theme.color.positive,
color: theme.background.content,
animation: `${fadeScaleIn} 500ms forwards`,
}));

const ItemLabel = styled.span<{ isCompleted: boolean; isSkipped: boolean }>(
({ theme, isCompleted, isSkipped }) => ({
textDecoration: isSkipped ? 'line-through' : 'none',
color: isSkipped
? theme.textMutedColor
: isCompleted
? theme.base === 'dark'
? theme.color.positive
: theme.color.positiveText
: theme.color.defaultText,
})
);

const title = (progress: number) => {
switch (true) {
case progress < 25:
Expand Down Expand Up @@ -84,13 +122,28 @@ const OpenGuideAction = ({ children }: { children?: React.ReactNode }) => {

export const ChecklistModule = () => {
const api = useStorybookApi();
const { loaded, allItems, nextItems, progress, mute } = useChecklist();
const { loaded, accepted, done, skipped, allItems, nextItems, progress, mute } = useChecklist();

const [items, setItems] = useState<ChecklistItemWithRef[]>([]);

const next = useMemo(
() => nextItems.map((item) => ({ ...item, nodeRef: createRef<HTMLDivElement>() })),
const itemsWithRef = useMemo(
() => nextItems.map((item) => ({ ...item, nodeRef: createRef<HTMLLIElement>() })),
[nextItems]
);
const hasTasks = next.length > 0;

useEffect(() => {
setItems((current) =>
current.map((item) => ({
...item,
isCompleted: accepted.includes(item.id) || done.includes(item.id),
isSkipped: skipped.includes(item.id),
}))
);
const timeout = setTimeout(setItems, 2000, itemsWithRef);
return () => clearTimeout(timeout);
}, [accepted, done, skipped, itemsWithRef]);

const hasTasks = items.length > 0;

return (
<CollapsibleWithMargin collapsed={!hasTasks}>
Expand Down Expand Up @@ -182,15 +235,22 @@ export const ChecklistModule = () => {
)}
>
<TransitionGroup as="ul" component={Listbox}>
{next.map((item) => (
{items.map((item) => (
<Transition key={item.id} nodeRef={item.nodeRef} timeout={300}>
{/* @ts-expect-error Ref doesn't understand "as" prop */}
<ListboxItem as="li" ref={item.nodeRef}>
<ListboxAction onClick={() => api.navigate(`/settings/guide#${item.id}`)}>
<ListboxIcon>
<StatusFailIcon />
{item.isCompleted ? (
<Particles anchor={Checked} key={item.id} />
) : (
<StatusFailIcon />
)}
</ListboxIcon>
<ListboxText>
<span>{item.label}</span>
<ItemLabel isCompleted={item.isCompleted} isSkipped={item.isSkipped}>
{item.label}
</ItemLabel>
</ListboxText>
</ListboxAction>
{item.action && (
Expand Down
3 changes: 3 additions & 0 deletions code/core/src/manager/components/sidebar/useChecklist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export type ChecklistItem = RawItemWithSection & {
isOpen: boolean;
isLockedBy: string[];
isReady: boolean;
isCompleted: boolean;
isAccepted: boolean;
isDone: boolean;
isSkipped: boolean;
Expand Down Expand Up @@ -85,6 +86,7 @@ export const useChecklist = () => {
return Object.values(itemsById).map<ChecklistItem>((item) => {
const isAccepted = accepted.includes(item.id);
const isDone = done.includes(item.id);
const isCompleted = isAccepted || isDone;
const isSkipped = skipped.includes(item.id);
const isMuted = Array.isArray(muted) ? muted.includes(item.id) : !!muted;

Expand All @@ -101,6 +103,7 @@ export const useChecklist = () => {
isOpen,
isLockedBy,
isReady,
isCompleted,
isAccepted,
isDone,
isSkipped,
Expand Down
1 change: 0 additions & 1 deletion code/core/src/manager/manager-stores.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ export const internal_universalChecklistStore = new experimental_MockUniversalSt
{
...UNIVERSAL_CHECKLIST_STORE_OPTIONS,
leader: globalThis.CONFIG_TYPE === 'PRODUCTION',
debug: true,
},
testUtils
) as unknown as UniversalStore<StoreState, StoreEvent>;
Expand Down
2 changes: 1 addition & 1 deletion code/core/src/manager/settings/Checklist/Checklist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type ChecklistSection = {
};

type ChecklistItemWithRef = ChecklistItem & {
nodeRef?: React.RefObject<HTMLLIElement>;
nodeRef: React.RefObject<HTMLLIElement>;
};
Comment on lines +25 to 36
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

import type { useChecklist } conflicts with ReturnType<typeof useChecklist>

useChecklist is imported via import type, but later referenced as a value in ReturnType<typeof useChecklist>. In TypeScript this is invalid: a symbol imported with import type cannot be used in a typeof value context.

Adjust the import so useChecklist is a value import while keeping ChecklistItem type‑only:

-import type { ChecklistItem, useChecklist } from '../../components/sidebar/useChecklist';
+import { type ChecklistItem, useChecklist } from '../../components/sidebar/useChecklist';

This preserves tree‑shaking while allowing ReturnType<typeof useChecklist> to type the props correctly.

Also applies to: 235-238



---

🏁 Script executed:

```shell
#!/bin/bash
# Locate the Checklist.tsx file and verify import pattern and ReturnType usage
fd -t f "Checklist.tsx" | head -5

Length of output: 54


🏁 Script executed:

#!/bin/bash
# Show the full content of the Checklist.tsx file to examine imports and usage
cat code/core/src/manager/settings/Checklist/Checklist.tsx

Length of output: 15839


🏁 Script executed:

#!/bin/bash
# Search for ReturnType usage with useChecklist to verify the claim
rg -n "ReturnType.*useChecklist" code/core/src/manager/settings/Checklist/

Length of output: 93


Fix TypeScript import: useChecklist must be a value import, not type-only

Line 21 imports useChecklist with import type, but line 236 uses it in ReturnType<typeof useChecklist>. TypeScript rejects typeof on type-only imports. Move useChecklist to a value import:

-import type { ChecklistItem, useChecklist } from '../../components/sidebar/useChecklist';
+import { type ChecklistItem, useChecklist } from '../../components/sidebar/useChecklist';

This preserves tree-shaking for ChecklistItem while allowing the typeof operator to resolve correctly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type { ChecklistItem, useChecklist } from '../../components/sidebar/useChecklist';
type ChecklistSection = Omit<ChecklistData['sections'][number], 'items'> & {
type ChecklistSection = {
id: string;
title: string;
itemIds: string[];
progress: number;
};
type ChecklistItem = ChecklistData['sections'][number]['items'][number] & {
isAccepted: boolean; // was it manually accepted by the user?
isDone: boolean; // was it automatically completed by the system?
isLockedBy: string[]; // items that must be completed before this item can be completed
isSkipped: boolean; // was it skipped by the user?
nodeRef?: React.RefObject<HTMLLIElement>;
type ChecklistItemWithRef = ChecklistItem & {
nodeRef: React.RefObject<HTMLLIElement>;
};
import { type ChecklistItem, useChecklist } from '../../components/sidebar/useChecklist';
type ChecklistSection = {
id: string;
title: string;
itemIds: string[];
progress: number;
};
type ChecklistItemWithRef = ChecklistItem & {
nodeRef: React.RefObject<HTMLLIElement>;
};
🤖 Prompt for AI Agents
In code/core/src/manager/settings/Checklist/Checklist.tsx around lines 25 to 36,
the import currently uses `import type { ChecklistItem, useChecklist }` which
makes `useChecklist` a type-only import but later `ReturnType<typeof
useChecklist>` requires a value import; change the imports so `ChecklistItem`
stays a type-only import and `useChecklist` is imported as a runtime value (e.g.
`import type { ChecklistItem } from '...'; import { useChecklist } from
'...';`), preserving tree-shaking for the type while allowing `typeof
useChecklist` to resolve.


const Sections = styled.ol(({ theme }) => ({
Expand Down
Loading