diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..227a2f3de --- /dev/null +++ b/CLAUDE.md @@ -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. diff --git a/packages/editable-html-tip-tap/src/components/EditableHtml.jsx b/packages/editable-html-tip-tap/src/components/EditableHtml.jsx index b6d93144e..13f764c9f 100644 --- a/packages/editable-html-tip-tap/src/components/EditableHtml.jsx +++ b/packages/editable-html-tip-tap/src/components/EditableHtml.jsx @@ -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; } @@ -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, diff --git a/packages/editable-html-tip-tap/src/extensions/__tests__/image-component.test.jsx b/packages/editable-html-tip-tap/src/extensions/__tests__/image-component.test.jsx index 84216ceee..6ad818db1 100644 --- a/packages/editable-html-tip-tap/src/extensions/__tests__/image-component.test.jsx +++ b/packages/editable-html-tip-tap/src/extensions/__tests__/image-component.test.jsx @@ -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(), diff --git a/packages/editable-html-tip-tap/src/extensions/image-component.jsx b/packages/editable-html-tip-tap/src/extensions/image-component.jsx index 4e5db4e61..7c9d31c21 100644 --- a/packages/editable-html-tip-tap/src/extensions/image-component.jsx +++ b/packages/editable-html-tip-tap/src/extensions/image-component.jsx @@ -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 }); @@ -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(); @@ -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); @@ -272,7 +274,7 @@ function ImageComponent(props) { onLoad={loadImage} alt={node.attrs.alt} /> - +