Skip to content

Conversation

@Myranae
Copy link
Contributor

@Myranae Myranae commented Feb 6, 2026

Summary:

Shorten our CLAUDE.md by referencing sub-documents on specific topics. The goal is to use progressive disclosure to keep the main file short so context stays as relevant as possible. I will be testing these out on my next couple tickets to see if it works well, but would love feedback!

@Myranae Myranae self-assigned this Feb 6, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Feb 6, 2026

🗄️ Schema Change: No Changes ✅

@github-actions
Copy link
Contributor

github-actions bot commented Feb 6, 2026

Size Change: 0 B

Total Size: 484 kB

ℹ️ View Unchanged
Filename Size
packages/kas/dist/es/index.js 20.8 kB
packages/keypad-context/dist/es/index.js 1 kB
packages/kmath/dist/es/index.js 5.98 kB
packages/math-input/dist/es/index.js 98.5 kB
packages/math-input/dist/es/strings.js 1.61 kB
packages/perseus-core/dist/es/index.item-splitting.js 11.8 kB
packages/perseus-core/dist/es/index.js 24.8 kB
packages/perseus-editor/dist/es/index.js 99.2 kB
packages/perseus-linter/dist/es/index.js 8.83 kB
packages/perseus-score/dist/es/index.js 9.32 kB
packages/perseus-utils/dist/es/index.js 403 B
packages/perseus/dist/es/index.js 187 kB
packages/perseus/dist/es/strings.js 7.44 kB
packages/pure-markdown/dist/es/index.js 1.39 kB
packages/simple-markdown/dist/es/index.js 6.71 kB

compressed-size-action

@github-actions
Copy link
Contributor

github-actions bot commented Feb 6, 2026

🛠️ Item Splitting: No Changes ✅

@github-actions
Copy link
Contributor

github-actions bot commented Feb 6, 2026

npm Snapshot: Published

Good news!! We've packaged up the latest commit from this PR (ab8a533) and published it to npm. You
can install it using the tag PR3235.

Example:

pnpm add @khanacademy/perseus@PR3235

If you are working in Khan Academy's frontend, you can run the below command.

./dev/tools/bump_perseus_version.ts -t PR3235

If you are working in Khan Academy's webapp, you can run the below command.

./dev/tools/bump_perseus_version.js -t PR3235

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This document is what was used to find updated information for all these docs. It would not be part of the final merge. Will delete this after discussions. Kept for reference.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This document is what was used to find updated information for all these docs. It would not be part of the final merge. Will delete this after discussions. Kept for reference.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It may be possible to trim this file even more, but we do want some amount of info here.

return <div>{props.content}</div>;
};
ComponentName.displayName = "ComponentName"; // Extra step
```
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm having trouble understanding the difference between these two examples. I understand that one is a named function, and the other is an arrow function, but I don't see the "Extra step" that is called out. I also don't know why the named function is the preferred form.

optional?: boolean;
};

// For complex props, use intersection types
Copy link
Contributor

Choose a reason for hiding this comment

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

Did we decide that we prefer interfaces when we export types? I don't recall if that impacts how we define complex props.


function MyComponent() {
return (
<View>
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps this is more appropriate for a separate discussion: I would like to get away from using the <View> component as a standard "element". That component has some core styling associated with it (namely flexbox) that isn't always needed. Generally speaking, if we want a Perseus component to output elements that "need" a container with specific styling, then a <div> element (or other more semantically relevant element) with styling attached to it is my preference (simpler styling rather than trying to override <View>). However, if the component is contained within some other component that manages its allotted area, then my preference is to return the component content surrounded by React fragments (<> ... </>). Otherwise, if we promote using <View>, we end up with div-soup, which is cumbersome and inefficient. The <View> component has its place, but I think we should be judicious with it.


// User answer state goes through handleUserInput, not local state!
}
```
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you expand on why this is helpful for the AI? Doesn't it already know about state management?

const filteredItems = React.useMemo(
() => items.filter(item => item.matches(filter)),
[items, filter]
);
Copy link
Contributor

Choose a reason for hiding this comment

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

We may want to check in with Jeff Yates on the matter of useMemo. I believe he has some well-founded opinions on the matter of when (if ever) to use this feature.


// ✅ Good - Stable reference
const style = {margin: 10};
<Component style={style} />
Copy link
Contributor

Choose a reason for hiding this comment

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

This instruction is combining two concepts: inline object definition and how to style. I would like to keep them separate. Is there a different inline object definition that could be used as an example? I don't want to give the AI any idea that it can provide styling inline.

aria-describedby="hint-text"
aria-invalid={hasError}
/>
```
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this telling the AI to do? If it is trying to enforce the use of labels/names on element, then I think this needs to be expanded significantly. My experience has been that the AI doesn't know when it is appropriate to add aria and when it isn't, nor how to properly generate a label.


return <input ref={inputRef} />;
}
```
Copy link
Contributor

Choose a reason for hiding this comment

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

Is focus management something that we do in Perseus?

break;
}
}
```
Copy link
Contributor

Choose a reason for hiding this comment

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

Does having this in the document encourage the AI to implement keyboard handling over native functionality?

</View>
);
}
```
Copy link
Contributor

Choose a reason for hiding this comment

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

I like this concept. However, I would prefer that it is handled via CSS, since that would work better in a wider variety of situations (i.e. zoomed viewport and increased font sizes).

const {container} = render(<ComplexComponent {...props} />);
expect(container).toMatchSnapshot();
});
```
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be cool if we could add guidance on when and how to generate visual regression tests, as well.

if (error) return <ErrorMessage />;
return <Content />;
})()}
```
Copy link
Contributor

Choose a reason for hiding this comment

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

I would like to expand on this concept. I believe that we should be transitioning our hidden components instead of conditionally rendering them. In order to transition them (fade-in/fade-out, expand/collapse), we need the component rendered at all times, and CSS would determine how to show/hide them. This is also helpful for elements that control the visibility of other components (like an accordion). They need an aria-controls attribute that references an ID of an element that exists on the page. If that element is conditionally rendered, then it's a validation error that fails WCAG.

├── types.ts # Widget-specific types (if complex)
├── utils.ts # Widget-specific utilities (if needed)
└── __docs__/
├── widget-name.stories.tsx # Storybook stories
Copy link
Contributor

@mark-fitzgerald mark-fitzgerald Feb 9, 2026

Choose a reason for hiding this comment

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

Can we add in Visual Regression stories here, as well? (see Radio widget for examples)


### TypeScript/JavaScript Files
```typescript
// Components - PascalCase file, named export
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be "kebab-case file"?

```
widgets/widget-name/
├── index.ts # Public exports only
├── widget-name.tsx # Main component
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we also indicate CSS files in this structure?

export const WithHint = {};
export const Mobile = {};
export const ErrorState = {};
```
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this another place where we want to call out visual regression stories?

packages/perseus/src/
├── styles/
│ ├── global.less # Global styles
│ └── variables.less # Shared variables
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't use LESS anymore.

│ └── variables.less # Shared variables
├── widgets/
│ └── widget-name/
│ └── widget-name.less # Widget-specific styles
Copy link
Contributor

Choose a reason for hiding this comment

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

Widgets should be using CSS Modules: widget-name.module.css

return (
<input
value={userInput.value}
onChange={(e) => handleUserInput({value: e.target.value})}
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this reflect the useCallback optimization that was presented in the best-practices file?

);

// After: O(n)
const hasDuplicate = items.length !== new Set(items).size;
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a good example!

2. **Verify accessibility** - Use the a11y addon to catch issues
3. **Check mobile layouts** - Use device frame addon
4. **Debug interactions** - Use actions addon to log callbacks
5. **Visual debugging** - Compare component states side-by-side
Copy link
Contributor

Choose a reason for hiding this comment

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

How does comparing "side-by-side" work here?


# 6. Test in Storybook
pnpm storybook
```
Copy link
Contributor

Choose a reason for hiding this comment

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

Wasn't this called out in an earlier document?

1. Monitor for any reverted changes
2. Check for related bug reports
3. Watch performance metrics
4. Respond to code review feedback
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we expecting the AI to be doing these?

Copy link
Contributor

@ivyolamit ivyolamit left a comment

Choose a reason for hiding this comment

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

Hi @Myranae here's' my first round of review. I'll add in my final review comments 🔜

I'm also interested to see if this will be lesser use of tokens or not 🤔


```
packages/
├── perseus/ (v74.0.2) # Core rendering engine
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion add a section on our versioning instead of adding it here, since our version changes almost all the time

│ │ ├── components/ # Reusable UI components
│ │ ├── renderers/ # Content renderers
│ │ ├── util/ # Utility functions
│ │ └── __docs__/ # Storybook documentation
Copy link
Contributor

Choose a reason for hiding this comment

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

currently widget documentation is under the specific widget, not sure how we want to represent that here.

│ │ ├── renderers/ # Content renderers
│ │ ├── util/ # Utility functions
│ │ └── __docs__/ # Storybook documentation
│ └── __testdata__/ # Test fixtures
Copy link
Contributor

Choose a reason for hiding this comment

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

we also have the __tests__ folder that contains the test


## Migration Notes

- Radio widget is transitioning (TODO(LEMS-2994))
Copy link
Contributor

Choose a reason for hiding this comment

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

This information will be outdated soon


## Three-Layer Type Architecture

Perseus widgets use a three-layer type system:
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

I think some of my comments were not posted :(
Just a thought, if we can reference existing documentation links here for context or sample it will be better. It will be less maintenance and having information outdated.

import type {PerseusWidgetNameUserInput} from "@khanacademy/perseus-core";

// Props are just the widget options spread directly, plus system props
type Props = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Based on previous discussion we would use an interface instead of the Props type and we would give it proper name for readability instead of just Props.

see sample here: https://github.com/Khan/perseus/blob/main/packages/perseus/src/widgets/radio/multiple-choice-component.tsx#L20


// Export with WidgetExports interface
export default {
name: "widget-name" as const,
Copy link
Contributor

Choose a reason for hiding this comment

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

not sure if this would matter but widget names are in kebab case and the widget itself will be in upper camel case

### Type Definitions
```typescript
// Place types at the top of the file
type Props = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Base on discussion we would prefer interface than types see slack discussion here


## Wonder Blocks Integration

Perseus uses Khan Academy's Wonder Blocks design system:
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think of adding a context where wonder blocks is found https://khan.github.io/wonder-blocks by explicitly saying this here it will only search what available components based on what are installed in our packages, but there might be more to it available in the documentation.

### Deprecated Patterns
- `onChange` prop - Use `handleUserInput` instead (LEMS-3245, LEMS-3542)
- Local widget state for answers - Use UserInputManager
- Class components for new widgets - Use functional components
Copy link
Contributor

Choose a reason for hiding this comment

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

These here is a bit confusing to me. We can probably simplify these statements here, like:

More descriptive: "New widgets must use functional components, not classes"
Action-oriented: "Use functional components for new widgets"


### MathJax Configuration

Perseus uses MathJax for rendering. Key points:
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be something more like "Perseus uses MathJax for rendering math"? The implied context might be missed and I wouldn't want to see MathJax used in ways that it shouldn't.

{"content": "The answer is $4$."}
]
}
```
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be labelled as an example and not a template? Or, should there be a template here, instead?

```typescript
// Ensure widgets have proper labels
<TextField
aria-label="Enter the x-coordinate"
Copy link
Contributor

Choose a reason for hiding this comment

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

The preference would be to have a visible label for the text field that is associated with the input so that no aria-label is needed. I think this example should be removed in favor of a more expansive accessibility-focused document.

- Consider pagination for content with 50+ expressions

### Content Loading
- Large exercises should lazy-load hints
Copy link
Contributor

Choose a reason for hiding this comment

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

What constitutes as "large"?

- [ ] Images load and align correctly
- [ ] Content is responsive on mobile
- [ ] Screen reader announces math properly
- [ ] Print view works correctly
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this referencing? Are we supposed to consider printing when we style widgets?

expect(result).toBe(expected);
});

it("handles user interaction", async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe a different test title? I would expect that a "user interaction" would have an "Act" section.

2. **Accessibility** - Keyboard navigation, screen readers
3. **Edge cases** - Empty states, invalid input, boundaries
4. **Scoring logic** - Correct/incorrect answers
5. **Mobile behavior** - Touch interactions
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe this is a side discussion, but I am confused as to what "Touch interactions" mean. I would expect that the standard onClick event would handle touches. What are we testing in a mobile environment?

// In test files
expect(result).toHaveBeenAnsweredCorrectly();
expect(result).toHaveInvalidInput("message");
```
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps it's because I'm not familiar with the custom matchers, but is it possible to have the result both "answered correctly" and "have invalid input"?

render(<Widget />);
expect(screen.getByLabelText("Answer")).toBeInTheDocument();
});
```
Copy link
Contributor

Choose a reason for hiding this comment

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

I recommend removing this example in favor of a more detailed accessibility-focused document.

render(<Widget />);

await user.pointer({keys: "[TouchA>]", target: element});
// Assert touch-specific behavior
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this a model that we are following - touch-specific behavior? I thought we were wanting to get away from mobile-specific anything.

└── __docs__/
├── [widget-name].stories.tsx # Storybook stories
└── a11y.mdx # Accessibility docs
```
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need this here, again? It seems like it would be a pain to have to update this information in multiple locations whenever we make a change to directory structure.

handleUserInput: (input: PerseusWidgetNameUserInput) => void;
trackInteraction: () => void;
onFocus: () => void;
onBlur: () => void;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to encourage outside containers telling the widget how to handle these kinds of events?

focus: () => inputRef.current?.focus(),
blur: () => inputRef.current?.blur(),
}));
```
Copy link
Contributor

Choose a reason for hiding this comment

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

Same concern here about focus management.

Copy link
Contributor

@ivyolamit ivyolamit Feb 9, 2026

Choose a reason for hiding this comment

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

I'm just watching the session from last week, I wonder if we want to do this structure of claude.md per package, so it will only load the needed context when needed and will not overload claude context on the outset.

Image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants