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
17 changes: 17 additions & 0 deletions .changeset/button-block-fixes-and-polish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@xproeditor/core': minor
'@xproeditor/vue': minor
'@xproeditor/react': minor
---

Add a new `button` block type (label, link URL, open-in-new-tab, primary/outline/ghost style, alignment) with a settings popover in both adapters and read-only rendering in `DocRenderer`.

Add a standalone, categorized emoji picker (search, 7 categories, recents) usable both from the callout icon button and via a Slack/Discord-style `:` inline trigger that searches and inserts an emoji directly into text.

Fix several real bugs surfaced while building the Bento showcase theme:
- Slash-menu arrow-key/Enter navigation was dead on arrival — the root keydown handler bailed out for any key typed inside the contenteditable block the menu lives in, which is the only place slash state ever exists.
- Floating UI (slash menu, bubble toolbar, popovers, dropdowns) is portaled to `document.body`, which silently escaped any theme class scoped to an ancestor of the editor — these now resync `--xpe-*` variables onto the portaled node so scoped custom themes apply everywhere, not just inline.
- The to-do checkbox's checkmark and the toggle chevron were sized via Tailwind utility classes that weren't reliably taking effect on raw SVGs — both now use explicit width/height so the checkmark is always visible.
- Vue's button-block label lost all styling because it reused `ui/Button.vue`'s scoped `.xpe-btn` class names on a plain `<div>` — Vue scoped CSS only applies to elements a component itself renders, so the classes were dead. Replaced with locally-scoped styles.

Slash menu now groups items into Basic/Lists/Media/Advanced sections, locks page scroll while open (Notion-style), and flips above the caret when there's no room below.
13 changes: 13 additions & 0 deletions .changeset/context-menu-scrollbars-radius-color-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@xproeditor/core': minor
'@xproeditor/vue': minor
'@xproeditor/react': minor
---

Add a right-click context menu on blocks (Notion-style): Duplicate, Delete, and — for callout blocks — a Color flyout with the existing background presets, all in a clean two-level menu with no extra clutter.

Add slim, theme-token-driven custom scrollbars across every scrollable surface (popovers, dropdowns, the slash/emoji menus, tables, code blocks, the editor root, and the doc renderer) instead of the browser default — add the `.xpe-scroll` class to any other scrollable container you build to match.

Soften corners across the whole popover/menu/button/input/callout/table/code family to scale off `--xpe-radius` instead of hardcoded pixel values, so a single token change now reshapes the entire editor consistently.

Fix: the text/highlight color popover in the floating bubble toolbar could never actually be used — clicking a swatch immediately closed the panel. The panel's "stay open" effect compared the toolbar's position by object reference, and a same-range `selectionchange` event (fired by clicking inside the toolbar itself) always produces a fresh position object, so the panel closed itself before a color could register. Also fixed a malformed Tailwind class that left the "active color" checkmark badge invisible, and replaced a few remaining hardcoded indigo accents with theme tokens.
7 changes: 7 additions & 0 deletions .changeset/media-blocks-and-theming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@xproeditor/core': minor
'@xproeditor/vue': minor
'@xproeditor/react': minor
---

Add `audio` and `file` block types with upload / library / URL insertion, per-file metadata (name, size, MIME), download card UI, and read-only rendering in `DocRenderer`. Media import now works out of the box without an `upload` prop (object-URL fallback), supports pasting files from the clipboard, and dropping OS files directly onto the editor at a precise position. Image blocks gain a paste-URL tab. Core exports new media helpers: `blockTypeForFile`, `formatFileSize`, `mediaPropsFromFile`, `fileToObjectUrl`, `acceptForBlockType`.
6 changes: 6 additions & 0 deletions .changeset/slash-menu-theming-rtl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@xproeditor/vue': minor
'@xproeditor/react': minor
---

Slash-command menu now measures itself and positions precisely at the caret — flipping above when near the viewport bottom, clamping to the viewport edges, staying aligned in RTL, and showing a "No results" state instead of abruptly closing. Theming: a richer minimal token set (`--xpe-surface`, `--xpe-surface-hover`, `--xpe-primary-muted`, `--xpe-primary-foreground`, `--xpe-ring`, `--xpe-danger`, `--xpe-radius`, `--xpe-shadow`) plus a built-in dark theme via the `xpe-dark` class or `data-xpe-theme="dark"`; the slash menu, media blocks, and all editor chrome (toolbars, popovers, dropdown menus, inputs, tabs, table controls, gutter, text blocks) are fully token-driven, so the built-in dark theme and custom themes restyle the entire editor. Remaining physical CSS (left/right) converted to logical properties for correct RTL layout.
56 changes: 56 additions & 0 deletions .claude/skills/adapter-parity/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
name: adapter-parity
description: Use whenever you change editor *behavior* (keyboard shortcuts, markdown shortcuts, selection handling, table operations, drag/drop, paste handling) in either the Vue or React adapter, or when auditing whether the two adapters have drifted apart. Ports the change to the sibling adapter and verifies the two state machines still mirror each other function-for-function.
---

# Keeping the Vue and React adapters in sync

xproeditor intentionally keeps two state machines — `BlockEditor.vue`'s
`<script setup>` (Vue) and `useBlockEditor` in
`packages/react/src/hooks/useBlockEditor.ts` (React) — structurally
near-identical: same function names, same call sites into
`@xproeditor/core`. See [docs/architecture.md](../../docs/architecture.md).
This lets a behavior change be ported by shape-matching instead of
re-deriving the logic from scratch.

## When you change one adapter

1. Identify the exact function(s) touched in
`packages/vue/src/components/BlockEditor.vue` or
`packages/react/src/hooks/useBlockEditor.ts`.
2. Find the same-named function/handler in the sibling file. If none exists
yet, that's the drift to fix, not a sign the other adapter doesn't need it.
3. Port the change preserving the same control flow and the same
`@xproeditor/core` calls (same function name, same argument shape) — do
not "improve" the logic differently per framework; behavioral differences
between adapters are bugs users will file.
4. If the change touches the toolbar surfaces, update both pairs:
- `EditorFormatToolbar.vue` ↔ `FormatToolbar.tsx`
- `EditorBubbleToolbar.vue` ↔ `BubbleToolbar.tsx`
- `EditorSlashMenu.vue` ↔ `SlashMenu.tsx`
5. If the change touches read-only rendering, update
`DocRenderer.vue` ↔ `DocRenderer.tsx`.

## Auditing for existing drift

When asked to check whether the adapters have drifted:

1. List the exported/internal function names in `BlockEditor.vue`'s
`<script setup>` and in `useBlockEditor.ts`.
2. Diff the two name sets — anything present in one but not the other is a
parity gap. Flag it; don't assume it's intentional.
3. For functions present in both, skim for behavioral divergence (different
keyboard shortcut, different core function called, different edge-case
handling) rather than just name matching.
4. Report gaps with file:line references rather than silently fixing large
amounts of behavior — parity fixes can be subtle and are worth a
confirmation before landing.

## Verifying

- `npm run dev:vue` and `npm run dev:react` (ports 5173/5174 per
`.claude/launch.json`) — exercise the same interaction in both demos.
- `npm run typecheck && npm test` from the repo root.
- Public API shape should stay symmetric between packages "where the
framework allows it" (CONTRIBUTING.md) — same prop/option names, same
`toolbar` prop semantics on `<ProEditor>`.
59 changes: 59 additions & 0 deletions .claude/skills/changeset/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
name: changeset
description: Use before finishing any PR that changes the behavior of a published package (@xproeditor/core, @xproeditor/vue, or @xproeditor/react) — decide whether a changeset is needed, which packages/semver bump it should declare, and write it correctly. Also use when asked to prepare a release.
---

# Writing a changeset for xproeditor

This repo publishes `@xproeditor/core`, `@xproeditor/vue`, and
`@xproeditor/react` independently via [Changesets](https://github.com/changesets/changesets),
fully automated through `.github/workflows/release.yml`. See
[docs/releasing.md](../../docs/releasing.md) for the full pipeline.

## Decide if a changeset is needed

Needed when the change affects published package *behavior* or public API:
new block type, new prop/option, bug fix in editor behavior, new exported
core function, styling change visible in `dist/style.css`, etc.

Not needed for: internal refactors with no observable behavior change,
`examples/*` or `site/` changes, docs, CI/tooling, test-only changes.

## Which packages to bump

Because `@xproeditor/vue` and `@xproeditor/react` both depend on
`@xproeditor/core`'s compiled `dist/`:

- A change inside `packages/core/src` → bump `@xproeditor/core`, and bump
whichever adapter(s) actually consume the changed core surface (if the
change is purely internal to core with no adapter-visible effect, core
alone is enough).
- A change inside `packages/vue/src` only → bump `@xproeditor/vue` only.
- A change inside `packages/react/src` only → bump `@xproeditor/react` only.
- A behavior change ported to both adapters (see the `adapter-parity`
skill) → bump `@xproeditor/vue` and `@xproeditor/react` together (and
`@xproeditor/core` too if the core layer changed).

## Semver bump

- `patch` — bug fix, no API change.
- `minor` — new backward-compatible feature (new block type, new prop, new
exported function).
- `major` — breaking change to a public export, prop, or persisted
`BlocksContent`/`Block` shape. Flag this explicitly to the user before
writing it — a major bump on any of these packages is a deliberate,
user-facing decision, not a default.

## Writing it

Run `npx changeset`, select the affected package(s) and bump, and write a
one-line, user-facing summary (it becomes the changelog entry verbatim —
write it as "what changed for the consumer," not as an implementation note).
Commit the generated `.changeset/*.md` file as part of the PR.

## What happens next (informational — do not act on this yourself)

On merge to `main`, CI either opens/updates a "Version Packages" PR (bumping
versions + writing `CHANGELOG.md`) or, once that PR is merged, publishes to
npm automatically. Never hand-bump `package.json` versions or hand-edit
`CHANGELOG.md` — that's the bot's job.
66 changes: 66 additions & 0 deletions .claude/skills/new-block-type/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
name: new-block-type
description: Use when adding a brand-new block type (like a new BlockType such as "callout" or "table") to xproeditor. Walks through every layer that must change — core model/serialization, Vue adapter, React adapter, toolbar and slash menu — so no adapter is left half-implemented.
---

# Adding a new block type

xproeditor stores documents as a flat `Block[]` (see [docs/block-model.md](../../docs/block-model.md)).
A new `BlockType` touches four layers, in this order. Do not skip a layer —
a block type that only exists in `core` renders nowhere, and a block type
that only exists in one adapter breaks parity (see the `adapter-parity` skill).

## 1. `@xproeditor/core` (framework-agnostic)

- `packages/core/src/types.ts` — add the new literal to the `BlockType` union
and any new fields to `BlockProps`.
- `packages/core/src/normalize.ts` — default/normalize the new props when a
block is created or loaded (e.g. defaults for `checked`, `collapsed`, `width`).
- `packages/core/src/serialize.ts` and `packages/core/src/html.ts` — how the
block round-trips to/from `BlocksContent` and HTML (`blocksToHtmlContent`,
`htmlToBlocks`).
- `packages/core/src/clipboard.ts` — copy/paste (de)serialization if the
block carries non-text data.
- Add/extend a test in `packages/core/src/*.test.ts` covering
normalize → serialize → round-trip for the new type.
- Rebuild core before touching adapters: `npm run build -w @xproeditor/core`
(adapters import core's compiled `dist/`, not its source).

## 2. Vue adapter (`packages/vue/src`)

- Rendering: add a new block component under `components/` (pattern:
`EditorImageBlock.vue`, `EditorTableBlock.vue`) or extend
`EditorTextBlock.vue` if it's a text-like block, and wire it into
`EditorBlockItem.vue`'s type switch.
- Behavior: extend `BlockEditor.vue`'s `<script setup>` state machine —
keydown/markdown-shortcut handling, `turnIntoBlock`, table-style ops as
precedent.
- Discoverability: add a slash-menu entry in `EditorSlashMenu.vue` and, if the
block has a distinct toolbar action, a control in
`components/toolbar/` / `EditorFormatToolbar.vue` / `EditorBubbleToolbar.vue`.

## 3. React adapter (`packages/react/src`)

Mirror step 2 exactly, function-for-function:

- Rendering: a new component under `components/` (pattern: `ImageBlock.tsx`,
`TableBlock.tsx`), wired into `BlockItem.tsx`'s type switch.
- Behavior: the equivalent addition in `hooks/useBlockEditor.ts` — same
function names and call sites into `@xproeditor/core` as the Vue version.
- Discoverability: `SlashMenu.tsx` and, if applicable,
`components/toolbar/` / `FormatToolbar.tsx` / `BubbleToolbar.tsx`.

## 4. Cross-cutting

- `DocRenderer.vue` / `DocRenderer.tsx` — the read-only renderer needs to know
how to display the new block type too.
- Styling: if the block needs new visual chrome, follow
[docs/theming.md](../../docs/theming.md) — Tailwind utility classes authored
in each package's own `src`, no new CSS variables unless the value is meant
to be end-user themeable (background/foreground/border/primary/mono font).
- Update [docs/block-model.md](../../docs/block-model.md) with the new
`BlockType`/`BlockProps` fields.
- Run `npm run lint && npm run typecheck && npm test` from the repo root
before considering the change done.
- Add a changeset (see the `changeset` skill) — a new block type is a
user-visible feature in all three published packages.
62 changes: 62 additions & 0 deletions .claude/skills/release-check/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
name: release-check
description: Use before merging a PR into main, before merging the bot-generated "Version Packages" PR, or when explicitly asked to prepare/verify a release of xproeditor's packages. Runs the same gates CI runs plus the repo-specific checks CI can't (adapter parity, changeset correctness) so nothing broken reaches npm.
---

# Pre-release / pre-merge checklist

xproeditor auto-publishes on merge to `main` once a "Version Packages" PR is
merged (see [docs/releasing.md](../../docs/releasing.md)). Because publishing
is automatic, catch problems *before* merge — there's no manual publish gate
after.

## Build order matters

`@xproeditor/vue` and `@xproeditor/react` import `@xproeditor/core`'s
compiled `dist/`, not its source. Always:

```bash
npm run build # builds core, then vue, then react, in that order
npm run typecheck
npm test
npm run lint
```

Running `typecheck`/`test` without building core first can pass locally on
stale `dist/` output and still fail in CI — don't skip the build step even
for a "small" change.

## Repo-specific checks CI doesn't fully cover

1. **Changeset correctness** — every PR that changes published behavior has
a `.changeset/*.md` file naming the right package(s) and a semver bump
that matches the change (see the `changeset` skill). A missing changeset
means the fix ships silently with no changelog entry on the next release.
2. **Adapter parity** — if the PR touched `BlockEditor.vue` or
`useBlockEditor.ts`, confirm the sibling adapter got the equivalent change
(see the `adapter-parity` skill). CI's typecheck/test/lint will not catch
a Vue-only fix that should also apply to React.
3. **Demo apps still work** — `npm run dev:vue` / `npm run dev:react` /
`npm run dev:site` and manually exercise the changed behavior; these
import the workspace packages directly and are the fastest way to catch a
broken build before it's live.

## Reviewing the bot's "Version Packages" PR

Before merging it:

- Confirm the version bumps match what you'd expect from the changesets
that went in (no unexpected `major`).
- Skim the generated `CHANGELOG.md` entries for clarity — they're copied
verbatim from changeset summaries, so a vague summary written earlier
becomes a vague changelog entry now; fix it in the changeset source if
still possible, otherwise flag it.
- Merging this PR triggers the actual `npm publish` on the next workflow
run — treat the merge itself as the release action and confirm with the
user first if there's any doubt about timing.

## First-time / infra checks (rarely needed)

If publishing fails, check the `NPM_TOKEN` repo secret (Settings → Secrets
and variables → Actions) has publish rights to the `@xproeditor` npm org
before assuming a code problem.
63 changes: 63 additions & 0 deletions .claude/skills/theming/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
name: theming
description: Use when styling anything inside packages/vue or packages/react — new components, visual tweaks, dark-mode support, or CSS variable additions. Keeps the zero-Tailwind-dependency build intact and decides whether a value belongs in a CSS variable (user-themeable) or plain scoped CSS (internal chrome).
---

# Styling inside the adapters

Full background: [docs/theming.md](../../docs/theming.md).

## The constraint

`@xproeditor/vue` and `@xproeditor/react` are authored with Tailwind utility
classes, but consumers never install Tailwind. Each package runs the
Tailwind CLI over its own `src` at build time (`corePlugins.preflight`
disabled) and ships one `dist/style.css`. This means:

- Only use Tailwind utility classes that exist in each package's own
`tailwind.config.js` content globs — classes assembled dynamically at
runtime via string concatenation (`` `text-${color}-500` ``) won't be
picked up by the Tailwind CLI's static scan and will silently not ship.
Use the full class name literally, or a lookup table of full class names.
- Don't add a runtime dependency on Tailwind, PostCSS plugins, or any CSS
framework that isn't already bundled into `dist/style.css` — it would
leak a peer dependency onto every consumer.
- Global element selectors are off-limits (`corePlugins.preflight: false`
is deliberate) — never reset margins/box-sizing/etc. globally; scope
everything to `.ebi-*` / `.etb-*` / `.xpe-*`-prefixed classes or the
component's own root class.

## CSS variable vs. plain scoped CSS

Ask: would a consuming app plausibly want to override this without
forking the stylesheet?

- **Yes → CSS variable** on `:root`, following the existing
`--xpe-*` naming (`--xpe-background`, `--xpe-foreground`,
`--xpe-muted-foreground`, `--xpe-muted`, `--xpe-border`, `--xpe-primary`,
`--xpe-font-mono`). Define both packages' `styles/theme.css` in lockstep —
a variable added to one adapter should exist in the other too, since apps
often use both.
- **No (internal chrome: toolbar/popover/table-gutter/syntax palette/etc.)
→ plain scoped CSS** with an `.ebi-*`/`.etb-*`/`.xpe-*` class, no
`!important` (consumers rely on being able to override these by loading
their own stylesheet after `style.css`).

## Dark mode

Dark mode is just a second set of `--xpe-*` values scoped under whatever
selector the consuming app uses (commonly `.dark`) — don't build a
dark-mode switch into the package itself; document the override in
`docs/theming.md` if you add new variables.

## Verifying

- Rebuild the affected package(s) so `dist/style.css` regenerates:
`npm run build -w @xproeditor/vue` / `-w @xproeditor/react`.
- Check both demos (`npm run dev:vue`, `npm run dev:react`) since Tailwind's
static class scanning can behave differently per package's `tailwind.config.js`
content globs — a class that resolves in one adapter's demo can be missing
from the other's `dist/style.css` if its config wasn't updated too.
- If you added/changed a `--xpe-*` variable, update
[docs/theming.md](../../docs/theming.md)'s reference table for both light
and dark examples.
Loading
Loading