Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
35 changes: 35 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# pie-lib

## What this is

A monorepo of shared React components/utilities for the **PIE (Portable Interactions & Elements)** assessment framework — math rendering/input, drag-and-drop, charting, rich-text editing, config UI, rubric/scoring UI, icons, style utils. Published under the `@pie-lib/*` npm scope.

Consumed by the sibling repo **`pie-elements`** (`../pie-elements`), which installs these as normal npm dependencies (not a local link) — `pie-elements` pins exact versions in its root `package.json` `resolutions` block.

## Repo structure

- Lerna (independent versioning, conventional-commits driven) + Yarn workspaces (`packages/*`).
- `packages/` — 28 library packages + `demo` (a Next.js app for local preview of all packages, deployed to now.sh on `develop`/`master` merges).
- Every package: `src/` (with `__tests__/`) → `lib/` (compiled output, checked in).
- No TypeScript anywhere — plain JS/JSX with PropTypes.

Notable packages: `render-ui` (most widely consumed — preview layout, feedback, collapsible, response indicators), `drag` (dnd-kit based), `math-input`/`math-rendering`/`math-toolbar` (MathQuill, mid-migration to MathLive — see `docs/mathquill-to-mathlive-migration.md`), `charting`/`plot` (visx), `config-ui`, `controller-utils`, `test-utils` (shared test helpers/mocks).

## Commands

- `npm run build` — build all packages
- `npm test` — run all tests; to test a single package: `./node_modules/.bin/jest packages/pkg-name/src/`
- `npm run lint` — ESLint
- `scripts/dev --scope $package-name` — run the demo site on localhost:3000 (`--scope` optional, defaults to all)
- `npm run release` — release + deploy (merging to `develop` → `next` dist-tag / pie-lib-next.now.sh; merging to `master` → `latest` / pie-lib.now.sh)

## Conventions

- **Conventional commits syntax** on commit messages — Lerna uses this to detect the appropriate independent version bump per package.
- Styling has migrated to MUI v7 + Emotion (older packages may still show JSS-era patterns).
- If test setup gets out of sync: `npm run build`, `rm -fr packages/test-utils/node_modules`, then retry.
- Node >=18 required; there's a known Jest/Node quirk documented in `.cursor/skills/nvm-jest-v22/SKILL.md` (use `nvm use v22` before running jest directly if you hit a syntax error).

## Working preferences

- **Do not create git commits unless explicitly asked.** The user commits their own changes — leave the working tree staged/unstaged as appropriate and let them review and commit themselves.
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ export const EditableHtml = (props) => {
let cb;

if (scheduled && result) {
// finish editing only on success
// finish editing only on success
cb = props.onChange;
}

Expand Down Expand Up @@ -424,10 +424,12 @@ const StyledEditorContent = styled(EditorContent, {
},

// Out of flow so the caret stays at the start of the block; in-flow ::before pushes the caret after the hint text.
'& p.is-editor-empty, & div.is-editor-empty': {
// :only-child ensures the placeholder is hidden whenever the editor has other content (images, upload nodes, etc.)
// and covers the type+backspace edge case where Tiptap only adds is-empty (not is-editor-empty).
'& p[data-placeholder].is-empty:only-child, & div[data-placeholder].is-empty:only-child': {
position: 'relative',
},
'& p.is-editor-empty::before, & div.is-editor-empty::before': {
'& p[data-placeholder].is-empty:only-child::before, & div[data-placeholder].is-empty:only-child::before': {
content: 'attr(data-placeholder)',
position: 'absolute',
left: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe('ImageComponent', () => {

const createMockEditor = (selection = { from: 0, to: 1 }) => ({
_tiptapContainerEl: document.body,
isEditable: true,
commands: {
updateAttributes: jest.fn(),
focus: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ function ImageComponent(props) {
);

const applySizeData = useCallback(() => {
if (!node.attrs.width || !imgRef.current) return;
if (!node.attrs.width || !imgRef.current || !imgRef.current.naturalWidth) return;
const resizePercent = getPercentFromWidth(node.attrs.width);
if (node.attrs.resizePercent === resizePercent) return;
updateThisNode({ resizePercent });
Expand All @@ -138,7 +138,7 @@ function ImageComponent(props) {

useEffect(() => {
if (selected) {
if (onlyThisNodeSelected) {
if (onlyThisNodeSelected && editor.isEditable) {
// Only open the upload UI for a fresh placeholder. Remounting after tab switch
// would otherwise call insertImageRequested again and reopen the file modal.
const hasImageSrc = String(node.attrs?.src ?? '').trim();
Expand All @@ -156,15 +156,17 @@ function ImageComponent(props) {
} else {
setShowToolbar(selected);
}
}, [onlyThisNodeSelected, selected]);
}, [onlyThisNodeSelected, selected, editor.isEditable]);

useEffect(() => {
applySizeData();

const resizeHandle = resizeRef.current;
if (resizeHandle) {
const resizeHandle = resizeRef?.current;

if (resizeHandle && editor.isEditable) {
resizeHandle.addEventListener('mousedown', initResize, false);
}

return () => {
if (resizeHandle) {
resizeHandle.removeEventListener('mousedown', initResize, false);
Expand Down Expand Up @@ -272,7 +274,7 @@ function ImageComponent(props) {
onLoad={loadImage}
alt={node.attrs.alt}
/>
<StyledResize ref={resizeRef} className="resize" />
<StyledResize ref={resizeRef} className="resize" style={{ display: editor.isEditable ? undefined : 'none' }} />
</StyledImageContainer>
</StyledRoot>

Expand Down
Loading