Add TipTap page editor with REST persistence (#25)
All checks were successful
CD / Build and push images (push) Successful in 2m0s
CI / Lint, typecheck, test (push) Successful in 1m42s
CI / Auth e2e pack (push) Successful in 1m50s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m10s
CD / Promote to Int (push) Successful in 10s
All checks were successful
CD / Build and push images (push) Successful in 2m0s
CI / Lint, typecheck, test (push) Successful in 1m42s
CI / Auth e2e pack (push) Successful in 1m50s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m10s
CD / Promote to Int (push) Successful in 10s
TipTap is bound to the canonical ProseMirror schema (packages/shared, #24) via a generic bridge (spec-utils.ts) that re-derives every node/mark's attrs/parseDOM/toDOM from editorSchema instead of duplicating them, so the editor's schema stays byte-for-byte identical to what the api decodes Yjs states against — guarded by a schema- fidelity + real Yjs round-trip test (@tiptap/y-tiptap client encoding against y-prosemirror server decoding). Route /p/:pondSlug/:pageSlug (RequireAuth) resolves the page via a new GET /ponds/:pondId/pages/:slug endpoint, binds a local Y.Doc via @tiptap/extension-collaboration (fragment "default"), and offers a view/edit mode toggle (sidebar auto-hides in edit mode via a small AppLayout context). Page state saves debounced to PUT /pages/:id/state with a truthful saving/saved/error(retrying) indicator; title saves separately via PATCH /pages/:id. Toolbar covers headings, marks, lists, blockquote, code block, hr, table (insert/row/column/header ops via prosemirror-tables), a minimal link mark, and an image placeholder (real upload is #27/#28). Closes #25 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
98e159ab50
commit
076883a9a6
@ -44,6 +44,18 @@ export class PagesController {
|
|||||||
return this.pages.getState(request.user!, id);
|
return this.pages.getState(request.user!, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resolves the pond-slug + page-slug pair the `/p/:pondSlug/:pageSlug` route
|
||||||
|
* navigates to (issue #25); the pond id must already be known to the caller
|
||||||
|
* (e.g. from `GET /ponds/:slug`). Listing pages by pond arrives with #26. */
|
||||||
|
@Get('ponds/:pondId/pages/:slug')
|
||||||
|
async getStateBySlug(
|
||||||
|
@Param('pondId') pondId: string,
|
||||||
|
@Param('slug') slug: string,
|
||||||
|
@Req() request: AuthedRequest,
|
||||||
|
): Promise<PageStateView> {
|
||||||
|
return this.pages.getStateBySlug(request.user!, pondId, slug);
|
||||||
|
}
|
||||||
|
|
||||||
@Put('pages/:id/state')
|
@Put('pages/:id/state')
|
||||||
async saveState(
|
async saveState(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
|
|||||||
@ -226,6 +226,30 @@ describe.skipIf(!hasTestDb)('pages (e2e, issue #23)', () => {
|
|||||||
await api().get(`/api/v1/pages/${created.body.id}`).set('Cookie', ownerCookie).expect(404);
|
await api().get(`/api/v1/pages/${created.body.id}`).set('Cookie', ownerCookie).expect(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('resolves a page by pond id + slug (issue #25 editor route)', async () => {
|
||||||
|
const created = await api()
|
||||||
|
.post(`/api/v1/ponds/${pondId}/pages`)
|
||||||
|
.set('Cookie', ownerCookie)
|
||||||
|
.send({ title: `Slug Lookup ${suffix}` })
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const bySlug = await api()
|
||||||
|
.get(`/api/v1/ponds/${pondId}/pages/${created.body.slug}`)
|
||||||
|
.set('Cookie', ownerCookie)
|
||||||
|
.expect(200);
|
||||||
|
expect(bySlug.body.id).toBe(created.body.id);
|
||||||
|
expect(typeof bySlug.body.state).toBe('string');
|
||||||
|
|
||||||
|
await api()
|
||||||
|
.get(`/api/v1/ponds/${pondId}/pages/${created.body.slug}`)
|
||||||
|
.set('Cookie', outsiderCookie)
|
||||||
|
.expect(404);
|
||||||
|
await api()
|
||||||
|
.get(`/api/v1/ponds/${pondId}/pages/does-not-exist-${suffix}`)
|
||||||
|
.set('Cookie', ownerCookie)
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
|
|
||||||
it('hides pages in foreign ponds (404, not 403)', async () => {
|
it('hides pages in foreign ponds (404, not 403)', async () => {
|
||||||
const created = await api()
|
const created = await api()
|
||||||
.post(`/api/v1/ponds/${pondId}/pages`)
|
.post(`/api/v1/ponds/${pondId}/pages`)
|
||||||
|
|||||||
@ -138,6 +138,16 @@ export class PagesService {
|
|||||||
return this.stateViewOf(page);
|
return this.stateViewOf(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getStateBySlug(user: User, pondId: string, slug: string): Promise<PageStateView> {
|
||||||
|
const page = await this.prisma.page.findFirst({
|
||||||
|
where: { pondId, slug, deletedAt: null },
|
||||||
|
include: { pond: true },
|
||||||
|
});
|
||||||
|
if (!page) throw new NotFoundException();
|
||||||
|
this.access.assertCanSee(user, page.pond);
|
||||||
|
return this.stateViewOf(page);
|
||||||
|
}
|
||||||
|
|
||||||
async saveState(user: User, id: string, input: SavePageStateInput): Promise<PageStateView> {
|
async saveState(user: User, id: string, input: SavePageStateInput): Promise<PageStateView> {
|
||||||
const page = await this.findModifiablePage(user, id);
|
const page = await this.findModifiablePage(user, id);
|
||||||
const state = new Uint8Array(Buffer.from(input.state, 'base64'));
|
const state = new Uint8Array(Buffer.from(input.state, 'base64'));
|
||||||
|
|||||||
97
apps/web/e2e/editor.spec.ts
Normal file
97
apps/web/e2e/editor.spec.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
import { contextForUser } from './helpers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TipTap editor pack (issue #25). Runs against the local dev stack (api +
|
||||||
|
* web); no Mailpit needed. Creates its own page per test via the api (the
|
||||||
|
* sidebar/"new page" flow is issue #26) and navigates straight to
|
||||||
|
* `/p/:pondSlug/:pageSlug`.
|
||||||
|
*/
|
||||||
|
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173';
|
||||||
|
|
||||||
|
async function createPage(
|
||||||
|
context: Awaited<ReturnType<typeof contextForUser>>,
|
||||||
|
title: string,
|
||||||
|
): Promise<{ pondSlug: string; pageSlug: string }> {
|
||||||
|
const ponds = await context.request.get('/api/v1/ponds');
|
||||||
|
const pond = (await ponds.json()).find((p: { type: string }) => p.type === 'personal');
|
||||||
|
const created = await context.request.post(`/api/v1/ponds/${pond.id}/pages`, {
|
||||||
|
data: { title },
|
||||||
|
});
|
||||||
|
const page = await created.json();
|
||||||
|
return { pondSlug: pond.slug, pageSlug: page.slug };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('typing persists across reload and undo/redo work', async ({ browser }) => {
|
||||||
|
const context = await contextForUser(browser, BASE_URL, 'fixture-user');
|
||||||
|
const { pondSlug, pageSlug } = await createPage(context, `E2E Editor ${Date.now()}`);
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
await page.goto(`/p/${pondSlug}/${pageSlug}`);
|
||||||
|
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
|
||||||
|
|
||||||
|
const content = page.locator('.ProseMirror');
|
||||||
|
await expect(content).toHaveAttribute('contenteditable', 'true');
|
||||||
|
await content.click();
|
||||||
|
await page.keyboard.type('Hello editor');
|
||||||
|
await expect(page.getByRole('status')).toHaveText(/saved|gespeichert/i, { timeout: 10000 });
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
|
||||||
|
await expect(content).toHaveAttribute('contenteditable', 'true');
|
||||||
|
await expect(content).toContainText('Hello editor');
|
||||||
|
|
||||||
|
await content.click();
|
||||||
|
await page.keyboard.type(' more');
|
||||||
|
await expect(content).toContainText('Hello editor more');
|
||||||
|
await page.keyboard.press('ControlOrMeta+z');
|
||||||
|
await expect(content).toContainText('Hello editor');
|
||||||
|
await expect(content).not.toContainText('Hello editor more');
|
||||||
|
await page.keyboard.press('ControlOrMeta+y');
|
||||||
|
await expect(content).toContainText('Hello editor more');
|
||||||
|
|
||||||
|
await context.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('edit mode hides the sidebar; leaving edit mode restores it', async ({ browser }) => {
|
||||||
|
const context = await contextForUser(browser, BASE_URL, 'fixture-user');
|
||||||
|
const { pondSlug, pageSlug } = await createPage(context, `E2E Sidebar ${Date.now()}`);
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
await page.goto(`/p/${pondSlug}/${pageSlug}`);
|
||||||
|
// CSS selector, not getByRole: aria-hidden removes the element from the
|
||||||
|
// accessibility tree, which would make a role-based locator "disappear"
|
||||||
|
// exactly when we need to assert that attribute.
|
||||||
|
const sidebar = page.locator('nav.sidebar');
|
||||||
|
await expect(sidebar).toHaveAttribute('aria-hidden', 'false');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
|
||||||
|
await expect(sidebar).toHaveAttribute('aria-hidden', 'true');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: /read|lesen/i }).click();
|
||||||
|
await expect(sidebar).toHaveAttribute('aria-hidden', 'false');
|
||||||
|
|
||||||
|
await context.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a save failure shows a truthful error and retries once online again', async ({ browser }) => {
|
||||||
|
const context = await contextForUser(browser, BASE_URL, 'fixture-user');
|
||||||
|
const { pondSlug, pageSlug } = await createPage(context, `E2E Offline ${Date.now()}`);
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
await page.goto(`/p/${pondSlug}/${pageSlug}`);
|
||||||
|
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
|
||||||
|
await page.locator('.ProseMirror').click();
|
||||||
|
|
||||||
|
await context.setOffline(true);
|
||||||
|
await page.keyboard.type('offline text');
|
||||||
|
await expect(page.getByRole('status')).toHaveText(/failed|retrying|fehlgeschlagen/i, {
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await context.setOffline(false);
|
||||||
|
await expect(page.getByRole('status')).toHaveText(/saved|gespeichert/i, { timeout: 10000 });
|
||||||
|
|
||||||
|
await context.close();
|
||||||
|
});
|
||||||
@ -17,13 +17,21 @@
|
|||||||
"@dorfteich/shared": "workspace:*",
|
"@dorfteich/shared": "workspace:*",
|
||||||
"@hookform/resolvers": "^5.4.0",
|
"@hookform/resolvers": "^5.4.0",
|
||||||
"@tanstack/react-query": "^5.66.0",
|
"@tanstack/react-query": "^5.66.0",
|
||||||
|
"@tiptap/core": "^3.27.1",
|
||||||
|
"@tiptap/extension-collaboration": "^3.27.1",
|
||||||
|
"@tiptap/pm": "^3.27.1",
|
||||||
|
"@tiptap/react": "^3.27.1",
|
||||||
"i18next": "^26.3.4",
|
"i18next": "^26.3.4",
|
||||||
"i18next-browser-languagedetector": "^8.2.1",
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
|
"prosemirror-model": "^1.25.9",
|
||||||
|
"prosemirror-schema-list": "^1.5.0",
|
||||||
|
"prosemirror-tables": "^1.8.5",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.80.0",
|
"react-hook-form": "^7.80.0",
|
||||||
"react-i18next": "^17.0.8",
|
"react-i18next": "^17.0.8",
|
||||||
"react-router-dom": "^7.1.0",
|
"react-router-dom": "^7.1.0",
|
||||||
|
"yjs": "^13.6.31",
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -31,8 +39,10 @@
|
|||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
"@vitejs/plugin-react": "^4.3.0",
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
|
"jsdom": "^26.0.0",
|
||||||
"typescript": "^5.7.0",
|
"typescript": "^5.7.0",
|
||||||
"vite": "^6.1.0",
|
"vite": "^6.1.0",
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0",
|
||||||
|
"y-prosemirror": "^1.3.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { AppLayout } from './layout/AppLayout';
|
|||||||
import { AdminSettingsPage } from './pages/AdminSettingsPage';
|
import { AdminSettingsPage } from './pages/AdminSettingsPage';
|
||||||
import { HomePage } from './pages/HomePage';
|
import { HomePage } from './pages/HomePage';
|
||||||
import { NotFoundPage } from './pages/NotFoundPage';
|
import { NotFoundPage } from './pages/NotFoundPage';
|
||||||
|
import { PageEditorPage } from './pages/PageEditorPage';
|
||||||
import { SettingsPage } from './pages/SettingsPage';
|
import { SettingsPage } from './pages/SettingsPage';
|
||||||
import { ForgotPasswordPage } from './pages/auth/ForgotPasswordPage';
|
import { ForgotPasswordPage } from './pages/auth/ForgotPasswordPage';
|
||||||
import { LoginPage } from './pages/auth/LoginPage';
|
import { LoginPage } from './pages/auth/LoginPage';
|
||||||
@ -29,6 +30,7 @@ export function App(): React.JSX.Element {
|
|||||||
|
|
||||||
<Route element={<RequireAuth />}>
|
<Route element={<RequireAuth />}>
|
||||||
<Route path="settings" element={<SettingsPage />} />
|
<Route path="settings" element={<SettingsPage />} />
|
||||||
|
<Route path="p/:pondSlug/:pageSlug" element={<PageEditorPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route element={<RequireSiteAdmin />}>
|
<Route element={<RequireSiteAdmin />}>
|
||||||
<Route path="admin" element={<AdminSettingsPage />} />
|
<Route path="admin" element={<AdminSettingsPage />} />
|
||||||
|
|||||||
338
apps/web/src/editor/Toolbar.tsx
Normal file
338
apps/web/src/editor/Toolbar.tsx
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
import { isAllowedLinkHref } from '@dorfteich/shared';
|
||||||
|
import type { Editor } from '@tiptap/core';
|
||||||
|
import { useEditorState } from '@tiptap/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface ToolbarProps {
|
||||||
|
editor: Editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolbarButton({
|
||||||
|
label,
|
||||||
|
active,
|
||||||
|
disabled,
|
||||||
|
onClick,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
active?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={active ? 'toolbar-button toolbar-button--active' : 'toolbar-button'}
|
||||||
|
title={label}
|
||||||
|
aria-label={label}
|
||||||
|
aria-pressed={active ?? false}
|
||||||
|
disabled={disabled}
|
||||||
|
// Toolbar clicks must not steal focus/selection from the editor.
|
||||||
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LinkLabels {
|
||||||
|
add: string;
|
||||||
|
remove: string;
|
||||||
|
urlLabel: string;
|
||||||
|
apply: string;
|
||||||
|
cancel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LinkControl({ editor, label }: { editor: Editor; label: LinkLabels }): React.JSX.Element {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [url, setUrl] = useState('');
|
||||||
|
const active = editor.isActive('link');
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
return (
|
||||||
|
<ToolbarButton
|
||||||
|
label={label.remove}
|
||||||
|
active
|
||||||
|
onClick={() => editor.chain().focus().unsetLink().run()}
|
||||||
|
>
|
||||||
|
🔗
|
||||||
|
</ToolbarButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
className="editor-link-form"
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (isAllowedLinkHref(url)) editor.chain().focus().setLink(url).run();
|
||||||
|
setOpen(false);
|
||||||
|
setUrl('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
aria-label={label.urlLabel}
|
||||||
|
placeholder="https://…"
|
||||||
|
value={url}
|
||||||
|
autoFocus
|
||||||
|
onChange={(event) => setUrl(event.target.value)}
|
||||||
|
/>
|
||||||
|
<button type="submit" className="toolbar-button">
|
||||||
|
{label.apply}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="toolbar-button" onClick={() => setOpen(false)}>
|
||||||
|
{label.cancel}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolbarButton
|
||||||
|
label={label.add}
|
||||||
|
disabled={editor.state.selection.empty}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
🔗
|
||||||
|
</ToolbarButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Keyboard-accessible toolbar for the page editor (issue #25). Table row/
|
||||||
|
* column controls stay visible but disabled outside a table, so the
|
||||||
|
* toolbar's layout and tab order never shift while typing. */
|
||||||
|
export function Toolbar({ editor }: ToolbarProps): React.JSX.Element {
|
||||||
|
const { t } = useTranslation('editor');
|
||||||
|
const state = useEditorState({
|
||||||
|
editor,
|
||||||
|
selector: ({ editor: e }) => ({
|
||||||
|
paragraph: e.isActive('paragraph'),
|
||||||
|
heading1: e.isActive('heading', { level: 1 }),
|
||||||
|
heading2: e.isActive('heading', { level: 2 }),
|
||||||
|
heading3: e.isActive('heading', { level: 3 }),
|
||||||
|
heading4: e.isActive('heading', { level: 4 }),
|
||||||
|
bold: e.isActive('bold'),
|
||||||
|
italic: e.isActive('italic'),
|
||||||
|
code: e.isActive('code'),
|
||||||
|
strikethrough: e.isActive('strikethrough'),
|
||||||
|
bulletList: e.isActive('bullet_list'),
|
||||||
|
orderedList: e.isActive('ordered_list'),
|
||||||
|
taskList: e.isActive('task_list'),
|
||||||
|
blockquote: e.isActive('blockquote'),
|
||||||
|
codeBlock: e.isActive('code_block'),
|
||||||
|
canAddRow: e.can().addRowAfter(),
|
||||||
|
canDeleteRow: e.can().deleteRow(),
|
||||||
|
canAddColumn: e.can().addColumnAfter(),
|
||||||
|
canDeleteColumn: e.can().deleteColumn(),
|
||||||
|
canDeleteTable: e.can().deleteTable(),
|
||||||
|
canToggleHeaderRow: e.can().toggleHeaderRow(),
|
||||||
|
canUndo: e.can().undo(),
|
||||||
|
canRedo: e.can().redo(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="editor-toolbar" role="toolbar" aria-label={t('toolbar.paragraph')}>
|
||||||
|
<div className="editor-toolbar__group">
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.paragraph')}
|
||||||
|
active={state.paragraph}
|
||||||
|
onClick={() => editor.chain().focus().setParagraph().run()}
|
||||||
|
>
|
||||||
|
P
|
||||||
|
</ToolbarButton>
|
||||||
|
{([1, 2, 3, 4] as const).map((level) => (
|
||||||
|
<ToolbarButton
|
||||||
|
key={level}
|
||||||
|
label={t(`toolbar.heading${level}` as const)}
|
||||||
|
active={state[`heading${level}` as const]}
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading(level).run()}
|
||||||
|
>
|
||||||
|
H{level}
|
||||||
|
</ToolbarButton>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="editor-toolbar__group">
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.bold')}
|
||||||
|
active={state.bold}
|
||||||
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||||
|
>
|
||||||
|
<strong>B</strong>
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.italic')}
|
||||||
|
active={state.italic}
|
||||||
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||||
|
>
|
||||||
|
<em>I</em>
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.code')}
|
||||||
|
active={state.code}
|
||||||
|
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||||
|
>
|
||||||
|
{'</>'}
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.strikethrough')}
|
||||||
|
active={state.strikethrough}
|
||||||
|
onClick={() => editor.chain().focus().toggleStrikethrough().run()}
|
||||||
|
>
|
||||||
|
<s>S</s>
|
||||||
|
</ToolbarButton>
|
||||||
|
<LinkControl
|
||||||
|
editor={editor}
|
||||||
|
label={{
|
||||||
|
add: t('toolbar.link.add'),
|
||||||
|
remove: t('toolbar.link.remove'),
|
||||||
|
urlLabel: t('toolbar.link.urlLabel'),
|
||||||
|
apply: t('toolbar.link.apply'),
|
||||||
|
cancel: t('toolbar.link.cancel'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="editor-toolbar__group">
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.bulletList')}
|
||||||
|
active={state.bulletList}
|
||||||
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||||
|
>
|
||||||
|
•
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.orderedList')}
|
||||||
|
active={state.orderedList}
|
||||||
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||||
|
>
|
||||||
|
1.
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.taskList')}
|
||||||
|
active={state.taskList}
|
||||||
|
onClick={() => editor.chain().focus().toggleTaskList().run()}
|
||||||
|
>
|
||||||
|
☑
|
||||||
|
</ToolbarButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="editor-toolbar__group">
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.blockquote')}
|
||||||
|
active={state.blockquote}
|
||||||
|
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||||
|
>
|
||||||
|
❝
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.codeBlock')}
|
||||||
|
active={state.codeBlock}
|
||||||
|
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||||
|
>
|
||||||
|
{'{ }'}
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.horizontalRule')}
|
||||||
|
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
||||||
|
>
|
||||||
|
―
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.image')}
|
||||||
|
onClick={() => editor.chain().focus().insertImagePlaceholder().run()}
|
||||||
|
>
|
||||||
|
🖼
|
||||||
|
</ToolbarButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="editor-toolbar__group">
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.table.insert')}
|
||||||
|
onClick={() => editor.chain().focus().insertTable().run()}
|
||||||
|
>
|
||||||
|
⊞
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.table.addColumnBefore')}
|
||||||
|
disabled={!state.canAddColumn}
|
||||||
|
onClick={() => editor.chain().focus().addColumnBefore().run()}
|
||||||
|
>
|
||||||
|
⊞←
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.table.addColumnAfter')}
|
||||||
|
disabled={!state.canAddColumn}
|
||||||
|
onClick={() => editor.chain().focus().addColumnAfter().run()}
|
||||||
|
>
|
||||||
|
⊞→
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.table.deleteColumn')}
|
||||||
|
disabled={!state.canDeleteColumn}
|
||||||
|
onClick={() => editor.chain().focus().deleteColumn().run()}
|
||||||
|
>
|
||||||
|
⊟↕
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.table.addRowBefore')}
|
||||||
|
disabled={!state.canAddRow}
|
||||||
|
onClick={() => editor.chain().focus().addRowBefore().run()}
|
||||||
|
>
|
||||||
|
⊞↑
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.table.addRowAfter')}
|
||||||
|
disabled={!state.canAddRow}
|
||||||
|
onClick={() => editor.chain().focus().addRowAfter().run()}
|
||||||
|
>
|
||||||
|
⊞↓
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.table.deleteRow')}
|
||||||
|
disabled={!state.canDeleteRow}
|
||||||
|
onClick={() => editor.chain().focus().deleteRow().run()}
|
||||||
|
>
|
||||||
|
⊟↔
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.table.toggleHeaderRow')}
|
||||||
|
disabled={!state.canToggleHeaderRow}
|
||||||
|
onClick={() => editor.chain().focus().toggleHeaderRow().run()}
|
||||||
|
>
|
||||||
|
⊤
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.table.deleteTable')}
|
||||||
|
disabled={!state.canDeleteTable}
|
||||||
|
onClick={() => editor.chain().focus().deleteTable().run()}
|
||||||
|
>
|
||||||
|
⊠
|
||||||
|
</ToolbarButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="editor-toolbar__group">
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.undo')}
|
||||||
|
disabled={!state.canUndo}
|
||||||
|
onClick={() => editor.chain().focus().undo().run()}
|
||||||
|
>
|
||||||
|
↺
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
label={t('toolbar.redo')}
|
||||||
|
disabled={!state.canRedo}
|
||||||
|
onClick={() => editor.chain().focus().redo().run()}
|
||||||
|
>
|
||||||
|
↻
|
||||||
|
</ToolbarButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
140
apps/web/src/editor/document-extensions.test.ts
Normal file
140
apps/web/src/editor/document-extensions.test.ts
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
import { docToMarkdown, docToPlainText, editorSchema } from '@dorfteich/shared';
|
||||||
|
import { Editor, getSchema } from '@tiptap/core';
|
||||||
|
import { Collaboration } from '@tiptap/extension-collaboration';
|
||||||
|
import { yXmlFragmentToProseMirrorRootNode } from 'y-prosemirror';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
|
import { documentExtensions } from './document-extensions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `editorSchema` (packages/shared, #24) is the one document schema the api
|
||||||
|
* decodes Yjs states against (`apps/api/src/pages/yjs-content.ts`, #23). The
|
||||||
|
* TipTap editor (#25) cannot reuse that `Schema` instance directly — TipTap
|
||||||
|
* always builds its own from extensions — so these tests guard the bridge in
|
||||||
|
* `spec-utils.ts`/`nodes/*`/`marks.ts` against silent drift.
|
||||||
|
*/
|
||||||
|
describe('web editor schema matches editorSchema (issue #25)', () => {
|
||||||
|
const builtSchema = getSchema(documentExtensions);
|
||||||
|
const nodeNames = Object.keys(editorSchema.spec.nodes.toObject());
|
||||||
|
const markNames = Object.keys(editorSchema.spec.marks.toObject());
|
||||||
|
|
||||||
|
it('defines exactly the same node types', () => {
|
||||||
|
expect(Object.keys(builtSchema.nodes).sort()).toEqual([...nodeNames].sort());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defines exactly the same mark types', () => {
|
||||||
|
expect(Object.keys(builtSchema.marks).sort()).toEqual([...markNames].sort());
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(nodeNames)('node "%s" has the same content expression, group, and attrs', (name) => {
|
||||||
|
const canonical = editorSchema.spec.nodes.get(name)!;
|
||||||
|
const built = builtSchema.nodes[name]!.spec;
|
||||||
|
expect(built.content ?? undefined).toBe(canonical.content ?? undefined);
|
||||||
|
expect(built.group ?? undefined).toBe(canonical.group ?? undefined);
|
||||||
|
expect(Object.keys(built.attrs ?? {}).sort()).toEqual(
|
||||||
|
Object.keys(canonical.attrs ?? {}).sort(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(markNames)('mark "%s" has the same attrs', (name) => {
|
||||||
|
const canonical = editorSchema.spec.marks.get(name)!;
|
||||||
|
const built = builtSchema.marks[name]!.spec;
|
||||||
|
expect(Object.keys(built.attrs ?? {}).sort()).toEqual(
|
||||||
|
Object.keys(canonical.attrs ?? {}).sort(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Yjs round-trip between the web editor and the api decoder (issues #23/#25)', () => {
|
||||||
|
/** Drives a real headless TipTap `Editor` (the exact client code path,
|
||||||
|
* `@tiptap/extension-collaboration` + `@tiptap/y-tiptap`) and decodes the
|
||||||
|
* resulting Yjs state the same way the api does (`y-prosemirror` against
|
||||||
|
* `editorSchema`) — proving the two independently-built bindings agree on
|
||||||
|
* the wire format for the node/mark set this schema actually uses. */
|
||||||
|
it('preserves headings, lists, task items, marks, and tables', () => {
|
||||||
|
const ydoc = new Y.Doc();
|
||||||
|
const editor = new Editor({
|
||||||
|
element: document.createElement('div'),
|
||||||
|
extensions: [
|
||||||
|
...documentExtensions,
|
||||||
|
Collaboration.configure({ document: ydoc, field: 'default' }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.commands.setContent({
|
||||||
|
type: 'doc',
|
||||||
|
content: [
|
||||||
|
{ type: 'heading', attrs: { level: 2 }, content: [{ type: 'text', text: 'Title' }] },
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [
|
||||||
|
{ type: 'text', text: 'bold', marks: [{ type: 'bold' }] },
|
||||||
|
{ type: 'text', text: ' and a ' },
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'link',
|
||||||
|
marks: [{ type: 'link', attrs: { href: 'https://example.org' } }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'bullet_list',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'list_item',
|
||||||
|
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'item one' }] }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'task_list',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'task_item',
|
||||||
|
attrs: { checked: true },
|
||||||
|
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'done' }] }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'table',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'table_row',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'table_cell',
|
||||||
|
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'cell' }] }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = Y.encodeStateAsUpdate(ydoc);
|
||||||
|
editor.destroy();
|
||||||
|
|
||||||
|
const decodeDoc = new Y.Doc();
|
||||||
|
Y.applyUpdate(decodeDoc, state);
|
||||||
|
const decoded = yXmlFragmentToProseMirrorRootNode(
|
||||||
|
decodeDoc.getXmlFragment('default'),
|
||||||
|
editorSchema,
|
||||||
|
);
|
||||||
|
decodeDoc.destroy();
|
||||||
|
|
||||||
|
expect(decoded.textContent).toContain('Title');
|
||||||
|
expect(decoded.textContent).toContain('item one');
|
||||||
|
expect(decoded.textContent).toContain('done');
|
||||||
|
expect(decoded.textContent).toContain('cell');
|
||||||
|
|
||||||
|
const markdown = docToMarkdown(decoded);
|
||||||
|
expect(markdown).toContain('## Title');
|
||||||
|
expect(markdown).toContain('**bold**');
|
||||||
|
|
||||||
|
expect(docToPlainText(decoded)).toContain('bold and a link');
|
||||||
|
});
|
||||||
|
});
|
||||||
48
apps/web/src/editor/document-extensions.ts
Normal file
48
apps/web/src/editor/document-extensions.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import type { AnyExtension } from '@tiptap/core';
|
||||||
|
|
||||||
|
import { Bold, CodeMark, Italic, LinkMark, Strikethrough } from './marks';
|
||||||
|
import { Image } from './nodes/image';
|
||||||
|
import { BulletList, ListItem, OrderedList, TaskList } from './nodes/lists';
|
||||||
|
import { Table, TableCell, TableHeader, TableRow } from './nodes/table';
|
||||||
|
import { TaskItem } from './nodes/task-item';
|
||||||
|
import {
|
||||||
|
Blockquote,
|
||||||
|
CodeBlock,
|
||||||
|
Doc,
|
||||||
|
HardBreak,
|
||||||
|
Heading,
|
||||||
|
HorizontalRule,
|
||||||
|
Paragraph,
|
||||||
|
Text,
|
||||||
|
} from './nodes/text-basics';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The full node/mark set for `editorSchema` (packages/shared, issue #24),
|
||||||
|
* minus `Collaboration` — that extension needs a per-page `Y.Doc` and is
|
||||||
|
* added by the caller (`PageEditorPage`, issue #25).
|
||||||
|
*/
|
||||||
|
export const documentExtensions: AnyExtension[] = [
|
||||||
|
Doc,
|
||||||
|
Text,
|
||||||
|
Paragraph,
|
||||||
|
Heading,
|
||||||
|
Blockquote,
|
||||||
|
CodeBlock,
|
||||||
|
HorizontalRule,
|
||||||
|
BulletList,
|
||||||
|
OrderedList,
|
||||||
|
ListItem,
|
||||||
|
TaskList,
|
||||||
|
TaskItem,
|
||||||
|
HardBreak,
|
||||||
|
Image,
|
||||||
|
Table,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableHeader,
|
||||||
|
Bold,
|
||||||
|
Italic,
|
||||||
|
CodeMark,
|
||||||
|
Strikethrough,
|
||||||
|
LinkMark,
|
||||||
|
];
|
||||||
131
apps/web/src/editor/marks.ts
Normal file
131
apps/web/src/editor/marks.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { isAllowedLinkHref } from '@dorfteich/shared';
|
||||||
|
import { Mark, markInputRule, markPasteRule } from '@tiptap/core';
|
||||||
|
|
||||||
|
import { attributesFromSpec, markSpec } from './spec-utils';
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
documentMarks: {
|
||||||
|
toggleBold: () => ReturnType;
|
||||||
|
toggleItalic: () => ReturnType;
|
||||||
|
toggleCode: () => ReturnType;
|
||||||
|
toggleStrikethrough: () => ReturnType;
|
||||||
|
setLink: (href: string) => ReturnType;
|
||||||
|
unsetLink: () => ReturnType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const boldSpec = markSpec('bold');
|
||||||
|
export const Bold = Mark.create({
|
||||||
|
name: 'bold',
|
||||||
|
parseHTML: () => boldSpec.parseDOM,
|
||||||
|
renderHTML: ({ mark }) => boldSpec.toDOM!(mark, true),
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
toggleBold:
|
||||||
|
() =>
|
||||||
|
({ commands }) =>
|
||||||
|
commands.toggleMark(this.name),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return { 'Mod-b': () => this.editor.commands.toggleBold() };
|
||||||
|
},
|
||||||
|
addInputRules() {
|
||||||
|
return [markInputRule({ find: /(?:^|\s)(\*\*([^*]+)\*\*)$/, type: this.type })];
|
||||||
|
},
|
||||||
|
addPasteRules() {
|
||||||
|
return [markPasteRule({ find: /(?:^|\s)(\*\*([^*]+)\*\*)/g, type: this.type })];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const italicSpec = markSpec('italic');
|
||||||
|
export const Italic = Mark.create({
|
||||||
|
name: 'italic',
|
||||||
|
parseHTML: () => italicSpec.parseDOM,
|
||||||
|
renderHTML: ({ mark }) => italicSpec.toDOM!(mark, true),
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
toggleItalic:
|
||||||
|
() =>
|
||||||
|
({ commands }) =>
|
||||||
|
commands.toggleMark(this.name),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return { 'Mod-i': () => this.editor.commands.toggleItalic() };
|
||||||
|
},
|
||||||
|
addInputRules() {
|
||||||
|
return [markInputRule({ find: /(?:^|\s)(_([^_]+)_)$/, type: this.type })];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const codeSpec = markSpec('code');
|
||||||
|
export const CodeMark = Mark.create({
|
||||||
|
name: 'code',
|
||||||
|
excludes: '_',
|
||||||
|
code: true,
|
||||||
|
parseHTML: () => codeSpec.parseDOM,
|
||||||
|
renderHTML: ({ mark }) => codeSpec.toDOM!(mark, true),
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
toggleCode:
|
||||||
|
() =>
|
||||||
|
({ commands }) =>
|
||||||
|
commands.toggleMark(this.name),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return { 'Mod-e': () => this.editor.commands.toggleCode() };
|
||||||
|
},
|
||||||
|
addInputRules() {
|
||||||
|
return [markInputRule({ find: /(?:^|\s)(`([^`]+)`)$/, type: this.type })];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const strikethroughSpec = markSpec('strikethrough');
|
||||||
|
export const Strikethrough = Mark.create({
|
||||||
|
name: 'strikethrough',
|
||||||
|
parseHTML: () => strikethroughSpec.parseDOM,
|
||||||
|
renderHTML: ({ mark }) => strikethroughSpec.toDOM!(mark, true),
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
toggleStrikethrough:
|
||||||
|
() =>
|
||||||
|
({ commands }) =>
|
||||||
|
commands.toggleMark(this.name),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return { 'Mod-Shift-s': () => this.editor.commands.toggleStrikethrough() };
|
||||||
|
},
|
||||||
|
addInputRules() {
|
||||||
|
return [markInputRule({ find: /(?:^|\s)(~~([^~]+)~~)$/, type: this.type })];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Full link-editing UX (bubble menu, "open in new tab") is issue #29; here
|
||||||
|
* a link is just a mark applicable to a selection with a validated href. */
|
||||||
|
const linkSpec = markSpec('link');
|
||||||
|
export const LinkMark = Mark.create({
|
||||||
|
name: 'link',
|
||||||
|
inclusive: false,
|
||||||
|
addAttributes() {
|
||||||
|
return attributesFromSpec(linkSpec);
|
||||||
|
},
|
||||||
|
parseHTML: () => linkSpec.parseDOM,
|
||||||
|
renderHTML: ({ mark }) => linkSpec.toDOM!(mark, true),
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setLink:
|
||||||
|
(href: string) =>
|
||||||
|
({ commands }) =>
|
||||||
|
isAllowedLinkHref(href) && commands.setMark(this.name, { href }),
|
||||||
|
unsetLink:
|
||||||
|
() =>
|
||||||
|
({ commands }) =>
|
||||||
|
commands.unsetMark(this.name),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
58
apps/web/src/editor/nodes/image.tsx
Normal file
58
apps/web/src/editor/nodes/image.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { Node } from '@tiptap/core';
|
||||||
|
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
|
import type { NodeViewProps } from '@tiptap/react';
|
||||||
|
|
||||||
|
import { attributesFromSpec, nodeSpec } from '../spec-utils';
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
documentImage: {
|
||||||
|
insertImagePlaceholder: (alt?: string) => ReturnType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Real uploads and clipboard paste arrive with #27/#28; for now inserting
|
||||||
|
* an image always yields a placeholder box, since there is no file to
|
||||||
|
* resolve `fileId` against yet. */
|
||||||
|
function ImageView({ node }: NodeViewProps): React.JSX.Element {
|
||||||
|
const alt = typeof node.attrs.alt === 'string' ? node.attrs.alt : '';
|
||||||
|
return (
|
||||||
|
<NodeViewWrapper
|
||||||
|
as="span"
|
||||||
|
className="editor-image-placeholder"
|
||||||
|
data-file-id={node.attrs.fileId}
|
||||||
|
>
|
||||||
|
{alt || '\u{1F5BC}'}
|
||||||
|
</NodeViewWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageSpec = nodeSpec('image');
|
||||||
|
export const Image = Node.create({
|
||||||
|
name: 'image',
|
||||||
|
group: imageSpec.group,
|
||||||
|
inline: imageSpec.inline,
|
||||||
|
atom: imageSpec.atom,
|
||||||
|
addAttributes() {
|
||||||
|
return attributesFromSpec(imageSpec);
|
||||||
|
},
|
||||||
|
parseHTML: () => imageSpec.parseDOM,
|
||||||
|
renderHTML: ({ node }) => imageSpec.toDOM!(node),
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(ImageView);
|
||||||
|
},
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
insertImagePlaceholder:
|
||||||
|
(alt = '') =>
|
||||||
|
({ chain }) =>
|
||||||
|
chain()
|
||||||
|
.insertContent({
|
||||||
|
type: this.name,
|
||||||
|
attrs: { fileId: crypto.randomUUID(), alt },
|
||||||
|
})
|
||||||
|
.run(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
108
apps/web/src/editor/nodes/lists.ts
Normal file
108
apps/web/src/editor/nodes/lists.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { Node, wrappingInputRule } from '@tiptap/core';
|
||||||
|
|
||||||
|
import { attributesFromSpec, nodeSpec, passthroughNodeIO } from '../spec-utils';
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
documentLists: {
|
||||||
|
toggleBulletList: () => ReturnType;
|
||||||
|
toggleOrderedList: () => ReturnType;
|
||||||
|
toggleTaskList: () => ReturnType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bulletListSpec = nodeSpec('bullet_list');
|
||||||
|
export const BulletList = Node.create({
|
||||||
|
name: 'bullet_list',
|
||||||
|
group: bulletListSpec.group,
|
||||||
|
content: bulletListSpec.content,
|
||||||
|
...passthroughNodeIO(bulletListSpec),
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
toggleBulletList:
|
||||||
|
() =>
|
||||||
|
({ commands }) =>
|
||||||
|
commands.toggleList(this.name, 'list_item'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return { 'Mod-Shift-8': () => this.editor.commands.toggleBulletList() };
|
||||||
|
},
|
||||||
|
addInputRules() {
|
||||||
|
return [wrappingInputRule({ find: /^\s*[-*]\s$/, type: this.type })];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const orderedListSpec = nodeSpec('ordered_list');
|
||||||
|
export const OrderedList = Node.create({
|
||||||
|
name: 'ordered_list',
|
||||||
|
group: orderedListSpec.group,
|
||||||
|
content: orderedListSpec.content,
|
||||||
|
addAttributes() {
|
||||||
|
return attributesFromSpec(orderedListSpec);
|
||||||
|
},
|
||||||
|
...passthroughNodeIO(orderedListSpec),
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
toggleOrderedList:
|
||||||
|
() =>
|
||||||
|
({ commands }) =>
|
||||||
|
commands.toggleList(this.name, 'list_item'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return { 'Mod-Shift-9': () => this.editor.commands.toggleOrderedList() };
|
||||||
|
},
|
||||||
|
addInputRules() {
|
||||||
|
return [
|
||||||
|
wrappingInputRule({
|
||||||
|
find: /^(\d+)\.\s$/,
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: (match) => ({ order: Number(match[1]) }),
|
||||||
|
joinPredicate: (match, node) => node.attrs.order + node.childCount === Number(match[1]),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const listItemSpec = nodeSpec('list_item');
|
||||||
|
export const ListItem = Node.create({
|
||||||
|
name: 'list_item',
|
||||||
|
content: listItemSpec.content,
|
||||||
|
...passthroughNodeIO(listItemSpec),
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return {
|
||||||
|
Enter: () => this.editor.commands.splitListItem(this.name),
|
||||||
|
Tab: () => this.editor.commands.sinkListItem(this.name),
|
||||||
|
'Shift-Tab': () => this.editor.commands.liftListItem(this.name),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const taskListSpec = nodeSpec('task_list');
|
||||||
|
export const TaskList = Node.create({
|
||||||
|
name: 'task_list',
|
||||||
|
group: taskListSpec.group,
|
||||||
|
content: taskListSpec.content,
|
||||||
|
...passthroughNodeIO(taskListSpec),
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
toggleTaskList:
|
||||||
|
() =>
|
||||||
|
({ commands }) =>
|
||||||
|
commands.toggleList(this.name, 'task_item'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return { 'Mod-Shift-7': () => this.editor.commands.toggleTaskList() };
|
||||||
|
},
|
||||||
|
addInputRules() {
|
||||||
|
return [
|
||||||
|
wrappingInputRule({
|
||||||
|
find: /^\s*\[([ x])]\s$/,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
142
apps/web/src/editor/nodes/table.ts
Normal file
142
apps/web/src/editor/nodes/table.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { Node } from '@tiptap/core';
|
||||||
|
import type { Node as PMNode, Schema } from 'prosemirror-model';
|
||||||
|
import {
|
||||||
|
addColumnAfter,
|
||||||
|
addColumnBefore,
|
||||||
|
addRowAfter,
|
||||||
|
addRowBefore,
|
||||||
|
deleteColumn,
|
||||||
|
deleteRow,
|
||||||
|
deleteTable,
|
||||||
|
tableEditing,
|
||||||
|
toggleHeaderRow,
|
||||||
|
} from 'prosemirror-tables';
|
||||||
|
|
||||||
|
import {
|
||||||
|
attributesFromSpec,
|
||||||
|
extendWithTableRole,
|
||||||
|
nodeSpec,
|
||||||
|
passthroughNodeIO,
|
||||||
|
} from '../spec-utils';
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
documentTable: {
|
||||||
|
insertTable: (options?: {
|
||||||
|
rows?: number;
|
||||||
|
cols?: number;
|
||||||
|
withHeaderRow?: boolean;
|
||||||
|
}) => ReturnType;
|
||||||
|
addColumnBefore: () => ReturnType;
|
||||||
|
addColumnAfter: () => ReturnType;
|
||||||
|
deleteColumn: () => ReturnType;
|
||||||
|
addRowBefore: () => ReturnType;
|
||||||
|
addRowAfter: () => ReturnType;
|
||||||
|
deleteRow: () => ReturnType;
|
||||||
|
deleteTable: () => ReturnType;
|
||||||
|
toggleHeaderRow: () => ReturnType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `type.createAndFill()` picks a default child (an empty paragraph) that
|
||||||
|
* satisfies the cell's `block+` content expression. */
|
||||||
|
function buildTableRow(schema: Schema, cols: number, header: boolean): PMNode {
|
||||||
|
const cellType = schema.nodes[header ? 'table_header' : 'table_cell']!;
|
||||||
|
const cells = Array.from({ length: cols }, () => cellType.createAndFill()!);
|
||||||
|
return schema.nodes.table_row!.create(null, cells);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTable(schema: Schema, rows: number, cols: number, withHeaderRow: boolean): PMNode {
|
||||||
|
const rowNodes = Array.from({ length: rows }, (_, index) =>
|
||||||
|
buildTableRow(schema, cols, withHeaderRow && index === 0),
|
||||||
|
);
|
||||||
|
return schema.nodes.table!.create(null, rowNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableSpec = nodeSpec('table');
|
||||||
|
export const Table = Node.create({
|
||||||
|
name: 'table',
|
||||||
|
group: tableSpec.group,
|
||||||
|
content: tableSpec.content,
|
||||||
|
isolating: tableSpec.isolating,
|
||||||
|
...passthroughNodeIO(tableSpec),
|
||||||
|
...extendWithTableRole('table', 'table'),
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [tableEditing()];
|
||||||
|
},
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
insertTable:
|
||||||
|
({ rows = 3, cols = 3, withHeaderRow = true } = {}) =>
|
||||||
|
({ chain, editor }) =>
|
||||||
|
chain()
|
||||||
|
.insertContent(buildTable(editor.schema, rows, cols, withHeaderRow).toJSON())
|
||||||
|
.run(),
|
||||||
|
addColumnBefore:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) =>
|
||||||
|
addColumnBefore(state, dispatch),
|
||||||
|
addColumnAfter:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) =>
|
||||||
|
addColumnAfter(state, dispatch),
|
||||||
|
deleteColumn:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) =>
|
||||||
|
deleteColumn(state, dispatch),
|
||||||
|
addRowBefore:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) =>
|
||||||
|
addRowBefore(state, dispatch),
|
||||||
|
addRowAfter:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) =>
|
||||||
|
addRowAfter(state, dispatch),
|
||||||
|
deleteRow:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) =>
|
||||||
|
deleteRow(state, dispatch),
|
||||||
|
deleteTable:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) =>
|
||||||
|
deleteTable(state, dispatch),
|
||||||
|
toggleHeaderRow:
|
||||||
|
() =>
|
||||||
|
({ state, dispatch }) =>
|
||||||
|
toggleHeaderRow(state, dispatch),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableRowSpec = nodeSpec('table_row');
|
||||||
|
export const TableRow = Node.create({
|
||||||
|
name: 'table_row',
|
||||||
|
content: tableRowSpec.content,
|
||||||
|
...passthroughNodeIO(tableRowSpec),
|
||||||
|
...extendWithTableRole('table_row', 'row'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableCellSpec = nodeSpec('table_cell');
|
||||||
|
export const TableCell = Node.create({
|
||||||
|
name: 'table_cell',
|
||||||
|
content: tableCellSpec.content,
|
||||||
|
isolating: tableCellSpec.isolating,
|
||||||
|
addAttributes() {
|
||||||
|
return attributesFromSpec(tableCellSpec);
|
||||||
|
},
|
||||||
|
...passthroughNodeIO(tableCellSpec),
|
||||||
|
...extendWithTableRole('table_cell', 'cell'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableHeaderSpec = nodeSpec('table_header');
|
||||||
|
export const TableHeader = Node.create({
|
||||||
|
name: 'table_header',
|
||||||
|
content: tableHeaderSpec.content,
|
||||||
|
isolating: tableHeaderSpec.isolating,
|
||||||
|
addAttributes() {
|
||||||
|
return attributesFromSpec(tableHeaderSpec);
|
||||||
|
},
|
||||||
|
...passthroughNodeIO(tableHeaderSpec),
|
||||||
|
...extendWithTableRole('table_header', 'header_cell'),
|
||||||
|
});
|
||||||
46
apps/web/src/editor/nodes/task-item.tsx
Normal file
46
apps/web/src/editor/nodes/task-item.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { Node } from '@tiptap/core';
|
||||||
|
import { NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
|
import type { NodeViewProps } from '@tiptap/react';
|
||||||
|
|
||||||
|
import { attributesFromSpec, nodeSpec } from '../spec-utils';
|
||||||
|
|
||||||
|
/** `packages/shared`'s task_item.parseDOM does not read `data-checked` back
|
||||||
|
* (issue #24) — checked state only ever comes from the node's own attrs, set
|
||||||
|
* here via the checkbox, never re-parsed from HTML. */
|
||||||
|
function TaskItemView({ node, updateAttributes, editor }: NodeViewProps): React.JSX.Element {
|
||||||
|
const checked = Boolean(node.attrs.checked);
|
||||||
|
return (
|
||||||
|
<NodeViewWrapper as="li" data-type="task_item" data-checked={String(checked)}>
|
||||||
|
<label contentEditable={false}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
disabled={!editor.isEditable}
|
||||||
|
onChange={(event) => updateAttributes({ checked: event.target.checked })}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<NodeViewContent as="div" />
|
||||||
|
</NodeViewWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskItemSpec = nodeSpec('task_item');
|
||||||
|
export const TaskItem = Node.create({
|
||||||
|
name: 'task_item',
|
||||||
|
content: taskItemSpec.content,
|
||||||
|
addAttributes() {
|
||||||
|
return attributesFromSpec(taskItemSpec);
|
||||||
|
},
|
||||||
|
parseHTML: () => taskItemSpec.parseDOM,
|
||||||
|
renderHTML: ({ node }) => taskItemSpec.toDOM!(node),
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(TaskItemView);
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return {
|
||||||
|
Enter: () => this.editor.commands.splitListItem(this.name),
|
||||||
|
Tab: () => this.editor.commands.sinkListItem(this.name),
|
||||||
|
'Shift-Tab': () => this.editor.commands.liftListItem(this.name),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
180
apps/web/src/editor/nodes/text-basics.ts
Normal file
180
apps/web/src/editor/nodes/text-basics.ts
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import { Node, nodeInputRule, textblockTypeInputRule, wrappingInputRule } from '@tiptap/core';
|
||||||
|
|
||||||
|
import { attributesFromSpec, nodeSpec, passthroughNodeIO } from '../spec-utils';
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
documentTextBasics: {
|
||||||
|
setParagraph: () => ReturnType;
|
||||||
|
setHeading: (level: 1 | 2 | 3 | 4) => ReturnType;
|
||||||
|
toggleHeading: (level: 1 | 2 | 3 | 4) => ReturnType;
|
||||||
|
toggleBlockquote: () => ReturnType;
|
||||||
|
toggleCodeBlock: () => ReturnType;
|
||||||
|
setHorizontalRule: () => ReturnType;
|
||||||
|
setHardBreak: () => ReturnType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ProseMirror's document root; `topNode` is a schema-construction option,
|
||||||
|
* not part of the NodeSpec, so it is not derived from `nodeSpec('doc')`. */
|
||||||
|
export const Doc = Node.create({
|
||||||
|
name: 'doc',
|
||||||
|
topNode: true,
|
||||||
|
content: nodeSpec('doc').content,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Text = Node.create({
|
||||||
|
name: 'text',
|
||||||
|
group: nodeSpec('text').group,
|
||||||
|
});
|
||||||
|
|
||||||
|
const paragraphSpec = nodeSpec('paragraph');
|
||||||
|
export const Paragraph = Node.create({
|
||||||
|
name: 'paragraph',
|
||||||
|
group: paragraphSpec.group,
|
||||||
|
content: paragraphSpec.content,
|
||||||
|
...passthroughNodeIO(paragraphSpec),
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setParagraph:
|
||||||
|
() =>
|
||||||
|
({ commands }) =>
|
||||||
|
commands.setNode(this.name),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return { 'Mod-Alt-0': () => this.editor.commands.setParagraph() };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const headingSpec = nodeSpec('heading');
|
||||||
|
const HEADING_LEVELS = [1, 2, 3, 4] as const;
|
||||||
|
export const Heading = Node.create({
|
||||||
|
name: 'heading',
|
||||||
|
group: headingSpec.group,
|
||||||
|
content: headingSpec.content,
|
||||||
|
defining: headingSpec.defining,
|
||||||
|
addAttributes() {
|
||||||
|
return attributesFromSpec(headingSpec);
|
||||||
|
},
|
||||||
|
...passthroughNodeIO(headingSpec),
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setHeading:
|
||||||
|
(level: 1 | 2 | 3 | 4) =>
|
||||||
|
({ commands }) =>
|
||||||
|
commands.setNode(this.name, { level }),
|
||||||
|
toggleHeading:
|
||||||
|
(level: 1 | 2 | 3 | 4) =>
|
||||||
|
({ commands }) =>
|
||||||
|
commands.toggleNode(this.name, 'paragraph', { level }),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return Object.fromEntries(
|
||||||
|
HEADING_LEVELS.map((level) => [
|
||||||
|
`Mod-Alt-${level}`,
|
||||||
|
() => this.editor.commands.toggleHeading(level),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
addInputRules() {
|
||||||
|
return HEADING_LEVELS.map((level) =>
|
||||||
|
textblockTypeInputRule({
|
||||||
|
find: new RegExp(`^(#{${level}})\\s$`),
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: { level },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const blockquoteSpec = nodeSpec('blockquote');
|
||||||
|
export const Blockquote = Node.create({
|
||||||
|
name: 'blockquote',
|
||||||
|
group: blockquoteSpec.group,
|
||||||
|
content: blockquoteSpec.content,
|
||||||
|
...passthroughNodeIO(blockquoteSpec),
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
toggleBlockquote:
|
||||||
|
() =>
|
||||||
|
({ commands }) =>
|
||||||
|
commands.toggleWrap(this.name),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return { 'Mod-Shift-b': () => this.editor.commands.toggleBlockquote() };
|
||||||
|
},
|
||||||
|
addInputRules() {
|
||||||
|
return [wrappingInputRule({ find: /^\s*>\s$/, type: this.type })];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const codeBlockSpec = nodeSpec('code_block');
|
||||||
|
export const CodeBlock = Node.create({
|
||||||
|
name: 'code_block',
|
||||||
|
group: codeBlockSpec.group,
|
||||||
|
content: codeBlockSpec.content,
|
||||||
|
marks: codeBlockSpec.marks,
|
||||||
|
code: codeBlockSpec.code,
|
||||||
|
defining: codeBlockSpec.defining,
|
||||||
|
whitespace: codeBlockSpec.whitespace,
|
||||||
|
...passthroughNodeIO(codeBlockSpec),
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
toggleCodeBlock:
|
||||||
|
() =>
|
||||||
|
({ commands }) =>
|
||||||
|
commands.toggleNode(this.name, 'paragraph'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return { 'Mod-Alt-c': () => this.editor.commands.toggleCodeBlock() };
|
||||||
|
},
|
||||||
|
addInputRules() {
|
||||||
|
return [textblockTypeInputRule({ find: /^```$/, type: this.type })];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const horizontalRuleSpec = nodeSpec('horizontal_rule');
|
||||||
|
export const HorizontalRule = Node.create({
|
||||||
|
name: 'horizontal_rule',
|
||||||
|
group: horizontalRuleSpec.group,
|
||||||
|
...passthroughNodeIO(horizontalRuleSpec),
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setHorizontalRule:
|
||||||
|
() =>
|
||||||
|
({ chain }) =>
|
||||||
|
chain().insertContent({ type: this.name }).run(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addInputRules() {
|
||||||
|
return [nodeInputRule({ find: /^---$/, type: this.type })];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const hardBreakSpec = nodeSpec('hard_break');
|
||||||
|
export const HardBreak = Node.create({
|
||||||
|
name: 'hard_break',
|
||||||
|
group: hardBreakSpec.group,
|
||||||
|
inline: hardBreakSpec.inline,
|
||||||
|
selectable: hardBreakSpec.selectable,
|
||||||
|
...passthroughNodeIO(hardBreakSpec),
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setHardBreak:
|
||||||
|
() =>
|
||||||
|
({ chain }) =>
|
||||||
|
chain().insertContent({ type: this.name }).run(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return {
|
||||||
|
'Shift-Enter': () => this.editor.commands.setHardBreak(),
|
||||||
|
'Mod-Enter': () => this.editor.commands.setHardBreak(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
61
apps/web/src/editor/spec-utils.ts
Normal file
61
apps/web/src/editor/spec-utils.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { editorSchema } from '@dorfteich/shared';
|
||||||
|
import type { Attributes, NodeConfig } from '@tiptap/core';
|
||||||
|
import type { MarkSpec, NodeSpec } from 'prosemirror-model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bridges the canonical ProseMirror schema (`packages/shared/editor-schema`,
|
||||||
|
* issue #24) into TipTap node/mark extensions (issue #25). TipTap always
|
||||||
|
* builds its own `Schema` instance from the extensions handed to `useEditor`
|
||||||
|
* — it cannot take a premade `Schema` — so this module re-derives every
|
||||||
|
* node/mark spec from `editorSchema` instead of duplicating attrs/parseDOM/
|
||||||
|
* toDOM by hand. That keeps the editor's schema byte-for-byte identical to
|
||||||
|
* what the api decodes Yjs states against (`apps/api/src/pages/yjs-content.ts`);
|
||||||
|
* see the schema-fidelity test in `schema-extensions.test.ts`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function nodeSpec(name: string): NodeSpec {
|
||||||
|
const spec = editorSchema.spec.nodes.get(name);
|
||||||
|
if (!spec) throw new Error(`editorSchema has no node "${name}"`);
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markSpec(name: string): MarkSpec {
|
||||||
|
const spec = editorSchema.spec.marks.get(name);
|
||||||
|
if (!spec) throw new Error(`editorSchema has no mark "${name}"`);
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Converts a ProseMirror `attrs` map into TipTap's `addAttributes()` shape,
|
||||||
|
* preserving "no default" (required attribute) instead of defaulting to
|
||||||
|
* `undefined`, so e.g. `image.fileId` stays a required attribute. */
|
||||||
|
export function attributesFromSpec(spec: NodeSpec | MarkSpec): Attributes {
|
||||||
|
const attrs = spec.attrs ?? {};
|
||||||
|
const result: Attributes = {};
|
||||||
|
for (const name of Object.keys(attrs)) {
|
||||||
|
const attr = attrs[name]!;
|
||||||
|
result[name] =
|
||||||
|
'default' in attr
|
||||||
|
? { default: attr.default, validate: attr.validate }
|
||||||
|
: { isRequired: true, validate: attr.validate };
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `parseHTML()`/`renderHTML()` for nodes that need no extra TipTap-side
|
||||||
|
* behavior (no custom commands, NodeView, or attrs beyond the spec). */
|
||||||
|
export function passthroughNodeIO(spec: NodeSpec): Pick<NodeConfig, 'parseHTML' | 'renderHTML'> {
|
||||||
|
return {
|
||||||
|
parseHTML: spec.parseDOM ? () => spec.parseDOM : undefined,
|
||||||
|
renderHTML: spec.toDOM ? ({ node }) => spec.toDOM!(node) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Marks a table node's built schema with the `tableRole` prosemirror-tables
|
||||||
|
* needs (selection/keymap/commands key off this, not the node name), without
|
||||||
|
* having to teach TipTap's schema builder a new field for every node. */
|
||||||
|
export function extendWithTableRole(name: string, tableRole: string) {
|
||||||
|
return {
|
||||||
|
extendNodeSchema: (extension: { name: string }) =>
|
||||||
|
extension.name === name ? { tableRole } : {},
|
||||||
|
};
|
||||||
|
}
|
||||||
64
apps/web/src/editor/use-page-autosave.ts
Normal file
64
apps/web/src/editor/use-page-autosave.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
|
import { apiPut } from '../lib/api';
|
||||||
|
import { encodeBase64 } from './yjs-base64';
|
||||||
|
|
||||||
|
export type SaveStatus = 'saved' | 'saving' | 'error';
|
||||||
|
|
||||||
|
const DEBOUNCE_MS = 800;
|
||||||
|
const RETRY_MS = 3000;
|
||||||
|
|
||||||
|
/** Debounced `PUT /pages/:id/state` on every local Yjs update, with a
|
||||||
|
* truthful save-state indicator: failures (e.g. no network) surface as
|
||||||
|
* `error` and keep retrying every `RETRY_MS` until a save succeeds
|
||||||
|
* (issue #25 acceptance criterion: "saving failed / retrying"). */
|
||||||
|
export function usePageStateAutosave(ydoc: Y.Doc | null, pageId: string): SaveStatus {
|
||||||
|
const [status, setStatus] = useState<SaveStatus>('saved');
|
||||||
|
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
const inFlightRef = useRef(false);
|
||||||
|
const dirtyRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ydoc) return;
|
||||||
|
const doc = ydoc;
|
||||||
|
|
||||||
|
function save(): void {
|
||||||
|
if (inFlightRef.current) {
|
||||||
|
dirtyRef.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
inFlightRef.current = true;
|
||||||
|
dirtyRef.current = false;
|
||||||
|
setStatus('saving');
|
||||||
|
apiPut(`/pages/${pageId}/state`, { state: encodeBase64(Y.encodeStateAsUpdate(doc)) })
|
||||||
|
.then(() => {
|
||||||
|
inFlightRef.current = false;
|
||||||
|
setStatus('saved');
|
||||||
|
if (dirtyRef.current) {
|
||||||
|
dirtyRef.current = false;
|
||||||
|
timerRef.current = setTimeout(save, DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
inFlightRef.current = false;
|
||||||
|
setStatus('error');
|
||||||
|
timerRef.current = setTimeout(save, RETRY_MS);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUpdate(): void {
|
||||||
|
setStatus((current) => (current === 'error' ? current : 'saving'));
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
timerRef.current = setTimeout(save, DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.on('update', onUpdate);
|
||||||
|
return () => {
|
||||||
|
doc.off('update', onUpdate);
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
};
|
||||||
|
}, [ydoc, pageId]);
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
15
apps/web/src/editor/yjs-base64.ts
Normal file
15
apps/web/src/editor/yjs-base64.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/** Browser-side base64 <-> Yjs update bytes; the api exchanges Yjs state as
|
||||||
|
* base64 over JSON (`apps/api/src/pages/pages.service.ts`, issue #23). */
|
||||||
|
|
||||||
|
export function decodeBase64(base64: string): Uint8Array {
|
||||||
|
const binary = atob(base64);
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeBase64(bytes: Uint8Array): string {
|
||||||
|
let binary = '';
|
||||||
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
@ -1,9 +1,11 @@
|
|||||||
import deAuth from '@dorfteich/shared/i18n/de/auth.json';
|
import deAuth from '@dorfteich/shared/i18n/de/auth.json';
|
||||||
import deCommon from '@dorfteich/shared/i18n/de/common.json';
|
import deCommon from '@dorfteich/shared/i18n/de/common.json';
|
||||||
|
import deEditor from '@dorfteich/shared/i18n/de/editor.json';
|
||||||
import deErrors from '@dorfteich/shared/i18n/de/errors.json';
|
import deErrors from '@dorfteich/shared/i18n/de/errors.json';
|
||||||
import deSettings from '@dorfteich/shared/i18n/de/settings.json';
|
import deSettings from '@dorfteich/shared/i18n/de/settings.json';
|
||||||
import enAuth from '@dorfteich/shared/i18n/en/auth.json';
|
import enAuth from '@dorfteich/shared/i18n/en/auth.json';
|
||||||
import enCommon from '@dorfteich/shared/i18n/en/common.json';
|
import enCommon from '@dorfteich/shared/i18n/en/common.json';
|
||||||
|
import enEditor from '@dorfteich/shared/i18n/en/editor.json';
|
||||||
import enErrors from '@dorfteich/shared/i18n/en/errors.json';
|
import enErrors from '@dorfteich/shared/i18n/en/errors.json';
|
||||||
import enSettings from '@dorfteich/shared/i18n/en/settings.json';
|
import enSettings from '@dorfteich/shared/i18n/en/settings.json';
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
@ -21,8 +23,20 @@ void i18n
|
|||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
.init({
|
.init({
|
||||||
resources: {
|
resources: {
|
||||||
en: { common: enCommon, errors: enErrors, auth: enAuth, settings: enSettings },
|
en: {
|
||||||
de: { common: deCommon, errors: deErrors, auth: deAuth, settings: deSettings },
|
common: enCommon,
|
||||||
|
errors: enErrors,
|
||||||
|
auth: enAuth,
|
||||||
|
settings: enSettings,
|
||||||
|
editor: enEditor,
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
common: deCommon,
|
||||||
|
errors: deErrors,
|
||||||
|
auth: deAuth,
|
||||||
|
settings: deSettings,
|
||||||
|
editor: deEditor,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
defaultNS: 'common',
|
defaultNS: 'common',
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en',
|
||||||
|
|||||||
@ -1,24 +1,30 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
import { usePersistentState } from '../lib/use-persistent-state';
|
import { usePersistentState } from '../lib/use-persistent-state';
|
||||||
|
import { SidebarChromeContext } from './sidebar-chrome';
|
||||||
import { Sidebar } from './Sidebar';
|
import { Sidebar } from './Sidebar';
|
||||||
import { TopBar } from './TopBar';
|
import { TopBar } from './TopBar';
|
||||||
|
|
||||||
export function AppLayout(): React.JSX.Element {
|
export function AppLayout(): React.JSX.Element {
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = usePersistentState('ui.sidebar.collapsed', false);
|
const [sidebarCollapsed, setSidebarCollapsed] = usePersistentState('ui.sidebar.collapsed', false);
|
||||||
|
const [forcedHidden, setForcedHidden] = useState(false);
|
||||||
|
const collapsed = sidebarCollapsed || forcedHidden;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<SidebarChromeContext.Provider value={setForcedHidden}>
|
||||||
<TopBar
|
<div className="app">
|
||||||
sidebarCollapsed={sidebarCollapsed}
|
<TopBar
|
||||||
onToggleSidebar={() => setSidebarCollapsed(!sidebarCollapsed)}
|
sidebarCollapsed={collapsed}
|
||||||
/>
|
onToggleSidebar={() => setSidebarCollapsed(!sidebarCollapsed)}
|
||||||
<div className="app-body">
|
/>
|
||||||
<Sidebar collapsed={sidebarCollapsed} />
|
<div className="app-body">
|
||||||
<main className="main">
|
<Sidebar collapsed={collapsed} />
|
||||||
<Outlet />
|
<main className="main">
|
||||||
</main>
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SidebarChromeContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
16
apps/web/src/layout/sidebar-chrome.tsx
Normal file
16
apps/web/src/layout/sidebar-chrome.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { createContext, useContext, useEffect } from 'react';
|
||||||
|
|
||||||
|
type SetSidebarForcedHidden = (hidden: boolean) => void;
|
||||||
|
|
||||||
|
export const SidebarChromeContext = createContext<SetSidebarForcedHidden>(() => {});
|
||||||
|
|
||||||
|
/** Lets a page (the editor's edit mode, issue #25) temporarily hide the
|
||||||
|
* sidebar regardless of the user's collapse preference, restoring it on
|
||||||
|
* unmount or once the condition turns false again. */
|
||||||
|
export function useForceSidebarHidden(hidden: boolean): void {
|
||||||
|
const setForcedHidden = useContext(SidebarChromeContext);
|
||||||
|
useEffect(() => {
|
||||||
|
setForcedHidden(hidden);
|
||||||
|
return () => setForcedHidden(false);
|
||||||
|
}, [hidden, setForcedHidden]);
|
||||||
|
}
|
||||||
@ -43,6 +43,8 @@ async function requestJson<T>(method: string, path: string, body?: unknown): Pro
|
|||||||
export const apiGet = <T>(path: string): Promise<T> => requestJson<T>('GET', path);
|
export const apiGet = <T>(path: string): Promise<T> => requestJson<T>('GET', path);
|
||||||
export const apiPost = <T>(path: string, body?: unknown): Promise<T> =>
|
export const apiPost = <T>(path: string, body?: unknown): Promise<T> =>
|
||||||
requestJson<T>('POST', path, body);
|
requestJson<T>('POST', path, body);
|
||||||
|
export const apiPut = <T>(path: string, body?: unknown): Promise<T> =>
|
||||||
|
requestJson<T>('PUT', path, body);
|
||||||
export const apiPatch = <T>(path: string, body?: unknown): Promise<T> =>
|
export const apiPatch = <T>(path: string, body?: unknown): Promise<T> =>
|
||||||
requestJson<T>('PATCH', path, body);
|
requestJson<T>('PATCH', path, body);
|
||||||
export const apiDelete = <T>(path: string): Promise<T> => requestJson<T>('DELETE', path);
|
export const apiDelete = <T>(path: string): Promise<T> => requestJson<T>('DELETE', path);
|
||||||
|
|||||||
130
apps/web/src/pages/PageEditorPage.tsx
Normal file
130
apps/web/src/pages/PageEditorPage.tsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import type { PageStateView, PondView } from '@dorfteich/shared';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Collaboration } from '@tiptap/extension-collaboration';
|
||||||
|
import { EditorContent, useEditor } from '@tiptap/react';
|
||||||
|
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
|
import { FormError } from '../components/forms';
|
||||||
|
import { documentExtensions } from '../editor/document-extensions';
|
||||||
|
import { Toolbar } from '../editor/Toolbar';
|
||||||
|
import { usePageStateAutosave } from '../editor/use-page-autosave';
|
||||||
|
import { decodeBase64 } from '../editor/yjs-base64';
|
||||||
|
import { useForceSidebarHidden } from '../layout/sidebar-chrome';
|
||||||
|
import { apiGet, apiPatch } from '../lib/api';
|
||||||
|
|
||||||
|
type Mode = 'view' | 'edit';
|
||||||
|
|
||||||
|
function PageEditor({ page, mode }: { page: PageStateView; mode: Mode }): React.JSX.Element {
|
||||||
|
const { t } = useTranslation('editor');
|
||||||
|
|
||||||
|
// Created and destroyed within the same effect (not `useMemo` + a separate
|
||||||
|
// cleanup effect): React StrictMode's dev-only mount→cleanup→remount would
|
||||||
|
// otherwise destroy the memoized `Y.Doc` on the simulated unmount without
|
||||||
|
// ever creating a fresh one, silently breaking Yjs-internal machinery
|
||||||
|
// (e.g. the undo manager) while `Y.encodeStateAsUpdate`/`applyUpdate`
|
||||||
|
// still happen to keep working — a bug that only shows up in dev.
|
||||||
|
const [ydoc, setYdoc] = useState<Y.Doc | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
const doc = new Y.Doc();
|
||||||
|
Y.applyUpdate(doc, decodeBase64(page.state));
|
||||||
|
setYdoc(doc);
|
||||||
|
return () => doc.destroy();
|
||||||
|
// Deliberately not depending on `page.state`: a background refetch of
|
||||||
|
// the same page must not blow away in-progress local edits by rebuilding
|
||||||
|
// the Y.Doc from the (stale) server snapshot.
|
||||||
|
}, [page.id]);
|
||||||
|
|
||||||
|
const editor = useEditor(
|
||||||
|
{
|
||||||
|
// `documentExtensions` alone is a valid (uncollaborated) schema, so the
|
||||||
|
// editor never gets built without its 'doc'/'paragraph'/'text' nodes
|
||||||
|
// while `ydoc` is still being created (see the effect above).
|
||||||
|
extensions: ydoc
|
||||||
|
? [...documentExtensions, Collaboration.configure({ document: ydoc, field: 'default' })]
|
||||||
|
: documentExtensions,
|
||||||
|
editable: mode === 'edit',
|
||||||
|
immediatelyRender: false,
|
||||||
|
},
|
||||||
|
[ydoc],
|
||||||
|
);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
editor?.setEditable(mode === 'edit');
|
||||||
|
}, [editor, mode]);
|
||||||
|
|
||||||
|
const saveStatus = usePageStateAutosave(ydoc, page.id);
|
||||||
|
|
||||||
|
if (!editor || !ydoc) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="editor-shell">
|
||||||
|
{mode === 'edit' && <Toolbar editor={editor} />}
|
||||||
|
<div className="editor-save-indicator" role="status">
|
||||||
|
{t(`save.${saveStatus}`)}
|
||||||
|
</div>
|
||||||
|
<EditorContent editor={editor} className="editor-content" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PageEditorPage(): React.JSX.Element {
|
||||||
|
const { t } = useTranslation('editor');
|
||||||
|
const { pondSlug = '', pageSlug = '' } = useParams<{ pondSlug: string; pageSlug: string }>();
|
||||||
|
const [mode, setMode] = useState<Mode>('view');
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
|
||||||
|
useForceSidebarHidden(mode === 'edit');
|
||||||
|
|
||||||
|
const pond = useQuery({
|
||||||
|
queryKey: ['pond', pondSlug],
|
||||||
|
queryFn: () => apiGet<PondView>(`/ponds/${pondSlug}`),
|
||||||
|
});
|
||||||
|
const page = useQuery({
|
||||||
|
queryKey: ['page', pond.data?.id, pageSlug],
|
||||||
|
queryFn: () => apiGet<PageStateView>(`/ponds/${pond.data!.id}/pages/${pageSlug}`),
|
||||||
|
enabled: Boolean(pond.data),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (page.data) setTitle(page.data.title);
|
||||||
|
}, [page.data?.id, page.data?.title]);
|
||||||
|
|
||||||
|
async function saveTitle(): Promise<void> {
|
||||||
|
if (!page.data || title === page.data.title) return;
|
||||||
|
await apiPatch(`/pages/${page.data.id}`, { title });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pond.error || page.error) {
|
||||||
|
return <FormError error={pond.error ?? page.error} />;
|
||||||
|
}
|
||||||
|
if (!page.data) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="editor-page">
|
||||||
|
<div className="editor-page__header">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="editor-page__title"
|
||||||
|
value={title}
|
||||||
|
placeholder={t('title.placeholder')}
|
||||||
|
disabled={mode !== 'edit'}
|
||||||
|
onChange={(event) => setTitle(event.target.value)}
|
||||||
|
onBlur={() => void saveTitle()}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button"
|
||||||
|
onClick={() => setMode(mode === 'edit' ? 'view' : 'edit')}
|
||||||
|
>
|
||||||
|
{mode === 'edit' ? t('mode.view') : t('mode.edit')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<PageEditor page={page.data} mode={mode} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -330,3 +330,207 @@ button {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Page editor (issue #25) */
|
||||||
|
.editor-page {
|
||||||
|
max-width: 48rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-page__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-page__title {
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
font-weight: var(--font-weight-heading);
|
||||||
|
font-size: 1.6rem;
|
||||||
|
padding: var(--space-1) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-page__title:disabled {
|
||||||
|
color: var(--color-text);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-page__title:focus-visible {
|
||||||
|
outline: 2px solid var(--color-accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-shell {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
background: var(--color-bg-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar__group {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-1);
|
||||||
|
padding-right: var(--space-3);
|
||||||
|
border-right: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar__group:last-child {
|
||||||
|
border-right: none;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
padding: 0 var(--space-1);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-button:hover:not(:disabled) {
|
||||||
|
background: var(--color-bg);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-button:focus-visible {
|
||||||
|
outline: 2px solid var(--color-accent);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-button--active {
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: var(--color-accent-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-button:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-link-form {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-1);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-link-form input {
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-save-indicator {
|
||||||
|
padding: var(--space-1) var(--space-3);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content {
|
||||||
|
padding: var(--space-4) var(--space-6);
|
||||||
|
min-height: 12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content .ProseMirror {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content h1,
|
||||||
|
.editor-content h2,
|
||||||
|
.editor-content h3,
|
||||||
|
.editor-content h4 {
|
||||||
|
margin-top: var(--space-6);
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content blockquote {
|
||||||
|
margin: var(--space-4) 0;
|
||||||
|
padding-left: var(--space-4);
|
||||||
|
border-left: 3px solid var(--color-border);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content pre {
|
||||||
|
padding: var(--space-3);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--color-bg-subtle);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content code {
|
||||||
|
background: var(--color-bg-subtle);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content pre code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content ul[data-type='task_list'] {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content ul[data-type='task_list'] li {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: var(--space-4) 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content table td,
|
||||||
|
.editor-content table th {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content table th {
|
||||||
|
background: var(--color-bg-subtle);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
margin: var(--space-6) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-image-placeholder {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 3rem;
|
||||||
|
min-height: 2rem;
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
border: 1px dashed var(--color-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--color-bg-subtle);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|||||||
55
packages/shared/i18n/de/editor.json
Normal file
55
packages/shared/i18n/de/editor.json
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"title": {
|
||||||
|
"placeholder": "Unbenannte Seite"
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"edit": "Bearbeiten",
|
||||||
|
"view": "Lesen",
|
||||||
|
"toggleToEdit": "In den Bearbeitungsmodus wechseln",
|
||||||
|
"toggleToView": "In den Lesemodus wechseln"
|
||||||
|
},
|
||||||
|
"save": {
|
||||||
|
"saved": "Gespeichert",
|
||||||
|
"saving": "Speichert …",
|
||||||
|
"error": "Speichern fehlgeschlagen, erneuter Versuch …",
|
||||||
|
"unsavedTitle": "Titel wird gespeichert …"
|
||||||
|
},
|
||||||
|
"toolbar": {
|
||||||
|
"paragraph": "Absatz",
|
||||||
|
"heading1": "Überschrift 1",
|
||||||
|
"heading2": "Überschrift 2",
|
||||||
|
"heading3": "Überschrift 3",
|
||||||
|
"heading4": "Überschrift 4",
|
||||||
|
"bold": "Fett",
|
||||||
|
"italic": "Kursiv",
|
||||||
|
"code": "Inline-Code",
|
||||||
|
"strikethrough": "Durchgestrichen",
|
||||||
|
"bulletList": "Aufzählungsliste",
|
||||||
|
"orderedList": "Nummerierte Liste",
|
||||||
|
"taskList": "Aufgabenliste",
|
||||||
|
"blockquote": "Zitat",
|
||||||
|
"codeBlock": "Codeblock",
|
||||||
|
"horizontalRule": "Trennlinie",
|
||||||
|
"image": "Bild-Platzhalter einfügen",
|
||||||
|
"undo": "Rückgängig",
|
||||||
|
"redo": "Wiederholen",
|
||||||
|
"link": {
|
||||||
|
"add": "Link hinzufügen",
|
||||||
|
"remove": "Link entfernen",
|
||||||
|
"urlLabel": "Link-URL",
|
||||||
|
"apply": "Übernehmen",
|
||||||
|
"cancel": "Abbrechen"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"insert": "Tabelle einfügen",
|
||||||
|
"addColumnBefore": "Spalte davor einfügen",
|
||||||
|
"addColumnAfter": "Spalte danach einfügen",
|
||||||
|
"deleteColumn": "Spalte löschen",
|
||||||
|
"addRowBefore": "Zeile davor einfügen",
|
||||||
|
"addRowAfter": "Zeile danach einfügen",
|
||||||
|
"deleteRow": "Zeile löschen",
|
||||||
|
"toggleHeaderRow": "Kopfzeile umschalten",
|
||||||
|
"deleteTable": "Tabelle löschen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
packages/shared/i18n/en/editor.json
Normal file
55
packages/shared/i18n/en/editor.json
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"title": {
|
||||||
|
"placeholder": "Untitled page"
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"edit": "Edit",
|
||||||
|
"view": "Read",
|
||||||
|
"toggleToEdit": "Switch to edit mode",
|
||||||
|
"toggleToView": "Switch to read mode"
|
||||||
|
},
|
||||||
|
"save": {
|
||||||
|
"saved": "Saved",
|
||||||
|
"saving": "Saving …",
|
||||||
|
"error": "Saving failed, retrying …",
|
||||||
|
"unsavedTitle": "Saving title …"
|
||||||
|
},
|
||||||
|
"toolbar": {
|
||||||
|
"paragraph": "Paragraph",
|
||||||
|
"heading1": "Heading 1",
|
||||||
|
"heading2": "Heading 2",
|
||||||
|
"heading3": "Heading 3",
|
||||||
|
"heading4": "Heading 4",
|
||||||
|
"bold": "Bold",
|
||||||
|
"italic": "Italic",
|
||||||
|
"code": "Inline code",
|
||||||
|
"strikethrough": "Strikethrough",
|
||||||
|
"bulletList": "Bullet list",
|
||||||
|
"orderedList": "Numbered list",
|
||||||
|
"taskList": "Task list",
|
||||||
|
"blockquote": "Quote",
|
||||||
|
"codeBlock": "Code block",
|
||||||
|
"horizontalRule": "Horizontal rule",
|
||||||
|
"image": "Insert image placeholder",
|
||||||
|
"undo": "Undo",
|
||||||
|
"redo": "Redo",
|
||||||
|
"link": {
|
||||||
|
"add": "Add link",
|
||||||
|
"remove": "Remove link",
|
||||||
|
"urlLabel": "Link URL",
|
||||||
|
"apply": "Apply",
|
||||||
|
"cancel": "Cancel"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"insert": "Insert table",
|
||||||
|
"addColumnBefore": "Add column before",
|
||||||
|
"addColumnAfter": "Add column after",
|
||||||
|
"deleteColumn": "Delete column",
|
||||||
|
"addRowBefore": "Add row before",
|
||||||
|
"addRowAfter": "Add row after",
|
||||||
|
"deleteRow": "Delete row",
|
||||||
|
"toggleHeaderRow": "Toggle header row",
|
||||||
|
"deleteTable": "Delete table"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
558
pnpm-lock.yaml
generated
558
pnpm-lock.yaml
generated
@ -134,13 +134,13 @@ importers:
|
|||||||
version: 1.5.9(@swc/core@1.15.43)(rollup@4.62.2)
|
version: 1.5.9(@swc/core@1.15.43)(rollup@4.62.2)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.2.6(@types/node@26.1.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.0)
|
version: 3.2.6(@types/node@26.1.0)(jiti@2.7.0)(jsdom@26.1.0)(terser@5.48.0)(tsx@4.23.0)
|
||||||
|
|
||||||
apps/collab:
|
apps/collab:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.2.6(@types/node@26.1.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.0)
|
version: 3.2.6(@types/node@26.1.0)(jiti@2.7.0)(jsdom@26.1.0)(terser@5.48.0)(tsx@4.23.0)
|
||||||
|
|
||||||
apps/web:
|
apps/web:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -153,12 +153,33 @@ importers:
|
|||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: ^5.66.0
|
specifier: ^5.66.0
|
||||||
version: 5.101.2(react@19.2.7)
|
version: 5.101.2(react@19.2.7)
|
||||||
|
'@tiptap/core':
|
||||||
|
specifier: ^3.27.1
|
||||||
|
version: 3.27.1(@tiptap/pm@3.27.1)
|
||||||
|
'@tiptap/extension-collaboration':
|
||||||
|
specifier: ^3.27.1
|
||||||
|
version: 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(@tiptap/y-tiptap@3.0.6(prosemirror-model@1.25.9)(prosemirror-state@1.4.4)(prosemirror-view@1.42.0)(y-protocols@1.0.7(yjs@13.6.31))(yjs@13.6.31))(yjs@13.6.31)
|
||||||
|
'@tiptap/pm':
|
||||||
|
specifier: ^3.27.1
|
||||||
|
version: 3.27.1
|
||||||
|
'@tiptap/react':
|
||||||
|
specifier: ^3.27.1
|
||||||
|
version: 3.27.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
i18next:
|
i18next:
|
||||||
specifier: ^26.3.4
|
specifier: ^26.3.4
|
||||||
version: 26.3.4(typescript@5.9.3)
|
version: 26.3.4(typescript@5.9.3)
|
||||||
i18next-browser-languagedetector:
|
i18next-browser-languagedetector:
|
||||||
specifier: ^8.2.1
|
specifier: ^8.2.1
|
||||||
version: 8.2.1
|
version: 8.2.1
|
||||||
|
prosemirror-model:
|
||||||
|
specifier: ^1.25.9
|
||||||
|
version: 1.25.9
|
||||||
|
prosemirror-schema-list:
|
||||||
|
specifier: ^1.5.0
|
||||||
|
version: 1.5.1
|
||||||
|
prosemirror-tables:
|
||||||
|
specifier: ^1.8.5
|
||||||
|
version: 1.8.5
|
||||||
react:
|
react:
|
||||||
specifier: ^19.0.0
|
specifier: ^19.0.0
|
||||||
version: 19.2.7
|
version: 19.2.7
|
||||||
@ -174,6 +195,9 @@ importers:
|
|||||||
react-router-dom:
|
react-router-dom:
|
||||||
specifier: ^7.1.0
|
specifier: ^7.1.0
|
||||||
version: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
|
yjs:
|
||||||
|
specifier: ^13.6.31
|
||||||
|
version: 13.6.31
|
||||||
zod:
|
zod:
|
||||||
specifier: ^4.4.3
|
specifier: ^4.4.3
|
||||||
version: 4.4.3
|
version: 4.4.3
|
||||||
@ -190,6 +214,9 @@ importers:
|
|||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^4.3.0
|
specifier: ^4.3.0
|
||||||
version: 4.7.0(vite@6.4.3(@types/node@26.1.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.0))
|
version: 4.7.0(vite@6.4.3(@types/node@26.1.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.0))
|
||||||
|
jsdom:
|
||||||
|
specifier: ^26.0.0
|
||||||
|
version: 26.1.0
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.7.0
|
specifier: ^5.7.0
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@ -198,7 +225,10 @@ importers:
|
|||||||
version: 6.4.3(@types/node@26.1.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.0)
|
version: 6.4.3(@types/node@26.1.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.0)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.2.6(@types/node@26.1.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.0)
|
version: 3.2.6(@types/node@26.1.0)(jiti@2.7.0)(jsdom@26.1.0)(terser@5.48.0)(tsx@4.23.0)
|
||||||
|
y-prosemirror:
|
||||||
|
specifier: ^1.3.7
|
||||||
|
version: 1.3.7(prosemirror-model@1.25.9)(prosemirror-state@1.4.4)(prosemirror-view@1.42.0)(y-protocols@1.0.7(yjs@13.6.31))(yjs@13.6.31)
|
||||||
|
|
||||||
packages/shared:
|
packages/shared:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -229,7 +259,7 @@ importers:
|
|||||||
version: 8.5.1(@swc/core@1.15.43)(jiti@2.7.0)(postcss@8.5.16)(tsx@4.23.0)(typescript@5.9.3)
|
version: 8.5.1(@swc/core@1.15.43)(jiti@2.7.0)(postcss@8.5.16)(tsx@4.23.0)(typescript@5.9.3)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.2.6(@types/node@26.1.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.0)
|
version: 3.2.6(@types/node@26.1.0)(jiti@2.7.0)(jsdom@26.1.0)(terser@5.48.0)(tsx@4.23.0)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@ -264,6 +294,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-/PZmyAlb2NGWPikRRuiWLdfHQd8Wrx6lX4HqvTcaDhlU43M3T0ud4PH2T3QDp7BzHYY92xtD8iPxX2asg67G1A==}
|
resolution: {integrity: sha512-/PZmyAlb2NGWPikRRuiWLdfHQd8Wrx6lX4HqvTcaDhlU43M3T0ud4PH2T3QDp7BzHYY92xtD8iPxX2asg67G1A==}
|
||||||
engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
|
engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
|
||||||
|
|
||||||
|
'@asamuzakjp/css-color@3.2.0':
|
||||||
|
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
|
||||||
|
|
||||||
'@babel/code-frame@7.29.7':
|
'@babel/code-frame@7.29.7':
|
||||||
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
|
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@ -358,6 +391,34 @@ packages:
|
|||||||
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
|
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
|
||||||
engines: {node: '>=0.1.90'}
|
engines: {node: '>=0.1.90'}
|
||||||
|
|
||||||
|
'@csstools/color-helpers@5.1.0':
|
||||||
|
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@csstools/css-calc@2.1.4':
|
||||||
|
resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
'@csstools/css-parser-algorithms': ^3.0.5
|
||||||
|
'@csstools/css-tokenizer': ^3.0.4
|
||||||
|
|
||||||
|
'@csstools/css-color-parser@3.1.0':
|
||||||
|
resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
'@csstools/css-parser-algorithms': ^3.0.5
|
||||||
|
'@csstools/css-tokenizer': ^3.0.4
|
||||||
|
|
||||||
|
'@csstools/css-parser-algorithms@3.0.5':
|
||||||
|
resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
'@csstools/css-tokenizer': ^3.0.4
|
||||||
|
|
||||||
|
'@csstools/css-tokenizer@3.0.4':
|
||||||
|
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@epic-web/invariant@1.0.0':
|
'@epic-web/invariant@1.0.0':
|
||||||
resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
|
resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
|
||||||
|
|
||||||
@ -867,6 +928,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
'@floating-ui/core@1.7.5':
|
||||||
|
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
|
||||||
|
|
||||||
|
'@floating-ui/dom@1.7.6':
|
||||||
|
resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
|
||||||
|
|
||||||
|
'@floating-ui/utils@0.2.11':
|
||||||
|
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
|
||||||
|
|
||||||
'@hookform/resolvers@5.4.0':
|
'@hookform/resolvers@5.4.0':
|
||||||
resolution: {integrity: sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw==}
|
resolution: {integrity: sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1436,6 +1506,55 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18 || ^19
|
react: ^18 || ^19
|
||||||
|
|
||||||
|
'@tiptap/core@3.27.1':
|
||||||
|
resolution: {integrity: sha512-rV6Qn4wmC6BxfF+4mu6bqGWj9vA4oXXhsrpXaJL2uhjxeHAGofjwcHof2X84VYzeyXgdlsGmqKie4TAppVXZUQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@tiptap/pm': 3.27.1
|
||||||
|
|
||||||
|
'@tiptap/extension-bubble-menu@3.27.1':
|
||||||
|
resolution: {integrity: sha512-j/j8Qp9Z5nViade2m7zjrO/CYH/Ca80Qj7aqo0eUaei6FZQ5izlF9o4XQU5EFMAutV6mwynsPUp8FVo5sCuYfw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@tiptap/core': 3.27.1
|
||||||
|
'@tiptap/pm': 3.27.1
|
||||||
|
|
||||||
|
'@tiptap/extension-collaboration@3.27.1':
|
||||||
|
resolution: {integrity: sha512-Da7WeKNIaLsbcHBWlgexMgm5ygoA1mhRroFND1vweLNsWIPxvyjci7jrq/uDN1tSnpqMlJsdyW0tXzbVGYTpMw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@tiptap/core': 3.27.1
|
||||||
|
'@tiptap/pm': 3.27.1
|
||||||
|
'@tiptap/y-tiptap': ^3.0.5
|
||||||
|
yjs: ^13
|
||||||
|
|
||||||
|
'@tiptap/extension-floating-menu@3.27.1':
|
||||||
|
resolution: {integrity: sha512-BmJF1VqB7dSJkgAalrpVFj88WLhxKjcWPuWHOqf2ITrUU2832BhKLXKmxjWUy1gqV8PfNNVWtGfIERy7I0y0+Q==}
|
||||||
|
peerDependencies:
|
||||||
|
'@floating-ui/dom': ^1.0.0
|
||||||
|
'@tiptap/core': 3.27.1
|
||||||
|
'@tiptap/pm': 3.27.1
|
||||||
|
|
||||||
|
'@tiptap/pm@3.27.1':
|
||||||
|
resolution: {integrity: sha512-Ffjx+vimmBU7zH/KrpXzJid3+pziCe/VL2aexSTP63cyQwKQ65LkFkCKaIsSpFdQQuakVZBGWjCA5RoBV852pw==}
|
||||||
|
|
||||||
|
'@tiptap/react@3.27.1':
|
||||||
|
resolution: {integrity: sha512-/Wn2fc9zMtX08MXYScDFsm4wJ8lzfhfPEdbtls7WCDlbtrop48PWlkHDBBJrywARfAQTB2mFs9KiFy9yrQm5Lg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@tiptap/core': 3.27.1
|
||||||
|
'@tiptap/pm': 3.27.1
|
||||||
|
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
'@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
|
'@tiptap/y-tiptap@3.0.6':
|
||||||
|
resolution: {integrity: sha512-kcGeVGKtq/cPGVseNKjtmtcY2WXUAEm1SqS5x0Smubj4nOCRyPiHg6kY4QuuZhmXjTK7hdo8chokkPUKWXPE9Q==}
|
||||||
|
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
prosemirror-model: ^1.7.1
|
||||||
|
prosemirror-state: ^1.2.3
|
||||||
|
prosemirror-view: ^1.9.10
|
||||||
|
y-protocols: ^1.0.1
|
||||||
|
yjs: ^13.5.38
|
||||||
|
|
||||||
'@tokenizer/inflate@0.4.1':
|
'@tokenizer/inflate@0.4.1':
|
||||||
resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
|
resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -1540,6 +1659,9 @@ packages:
|
|||||||
'@types/supertest@6.0.3':
|
'@types/supertest@6.0.3':
|
||||||
resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==}
|
resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==}
|
||||||
|
|
||||||
|
'@types/use-sync-external-store@0.0.6':
|
||||||
|
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.62.1':
|
'@typescript-eslint/eslint-plugin@8.62.1':
|
||||||
resolution: {integrity: sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==}
|
resolution: {integrity: sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@ -1705,6 +1827,10 @@ packages:
|
|||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
agent-base@7.1.4:
|
||||||
|
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||||
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
ajv-formats@2.1.1:
|
ajv-formats@2.1.1:
|
||||||
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -2020,9 +2146,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
cssstyle@4.6.0:
|
||||||
|
resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
csstype@3.2.3:
|
csstype@3.2.3:
|
||||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
|
|
||||||
|
data-urls@5.0.0:
|
||||||
|
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
dateformat@4.6.3:
|
dateformat@4.6.3:
|
||||||
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
|
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
|
||||||
|
|
||||||
@ -2035,6 +2169,9 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
decimal.js@10.6.0:
|
||||||
|
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
||||||
|
|
||||||
deep-eql@5.0.2:
|
deep-eql@5.0.2:
|
||||||
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -2109,6 +2246,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
|
entities@6.0.1:
|
||||||
|
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
|
||||||
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
error-ex@1.3.4:
|
error-ex@1.3.4:
|
||||||
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
|
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
|
||||||
|
|
||||||
@ -2260,6 +2401,10 @@ packages:
|
|||||||
fast-deep-equal@3.1.3:
|
fast-deep-equal@3.1.3:
|
||||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||||
|
|
||||||
|
fast-equals@5.4.0:
|
||||||
|
resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
fast-json-stable-stringify@2.1.0:
|
fast-json-stable-stringify@2.1.0:
|
||||||
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
||||||
|
|
||||||
@ -2415,6 +2560,10 @@ packages:
|
|||||||
help-me@5.0.0:
|
help-me@5.0.0:
|
||||||
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
|
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
|
||||||
|
|
||||||
|
html-encoding-sniffer@4.0.0:
|
||||||
|
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
html-parse-stringify@3.0.1:
|
html-parse-stringify@3.0.1:
|
||||||
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||||
|
|
||||||
@ -2422,6 +2571,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
http-proxy-agent@7.0.2:
|
||||||
|
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
|
||||||
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
|
https-proxy-agent@7.0.6:
|
||||||
|
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||||
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
i18next-browser-languagedetector@8.2.1:
|
i18next-browser-languagedetector@8.2.1:
|
||||||
resolution: {integrity: sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==}
|
resolution: {integrity: sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==}
|
||||||
|
|
||||||
@ -2433,6 +2590,10 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
iconv-lite@0.6.3:
|
||||||
|
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
iconv-lite@0.7.2:
|
iconv-lite@0.7.2:
|
||||||
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
|
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -2482,6 +2643,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
|
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
is-potential-custom-element-name@1.0.1:
|
||||||
|
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||||
|
|
||||||
is-promise@4.0.0:
|
is-promise@4.0.0:
|
||||||
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
||||||
|
|
||||||
@ -2521,6 +2685,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}
|
resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jsdom@26.1.0:
|
||||||
|
resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
canvas: ^3.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
canvas:
|
||||||
|
optional: true
|
||||||
|
|
||||||
jsesc@3.1.0:
|
jsesc@3.1.0:
|
||||||
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -2603,6 +2776,9 @@ packages:
|
|||||||
loupe@3.2.1:
|
loupe@3.2.1:
|
||||||
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
|
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
|
||||||
|
|
||||||
|
lru-cache@10.4.3:
|
||||||
|
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||||
|
|
||||||
lru-cache@11.5.1:
|
lru-cache@11.5.1:
|
||||||
resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==}
|
resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@ -2755,6 +2931,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==}
|
resolution: {integrity: sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
|
nwsapi@2.2.24:
|
||||||
|
resolution: {integrity: sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==}
|
||||||
|
|
||||||
nypm@0.6.8:
|
nypm@0.6.8:
|
||||||
resolution: {integrity: sha512-Q9K4Diu6l5u6xJQogeFSs/zKtyMSgFKFtRQV+tHP4kL7KPm2grpBU0dFIwFaXwNxN0MtfKWc43VpCugAa+LPsw==}
|
resolution: {integrity: sha512-Q9K4Diu6l5u6xJQogeFSs/zKtyMSgFKFtRQV+tHP4kL7KPm2grpBU0dFIwFaXwNxN0MtfKWc43VpCugAa+LPsw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -2813,6 +2992,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
parse5@7.3.0:
|
||||||
|
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||||
|
|
||||||
parseurl@1.3.3:
|
parseurl@1.3.3:
|
||||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@ -2945,6 +3127,24 @@ packages:
|
|||||||
process-warning@5.0.0:
|
process-warning@5.0.0:
|
||||||
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
|
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
|
||||||
|
|
||||||
|
prosemirror-changeset@2.4.1:
|
||||||
|
resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==}
|
||||||
|
|
||||||
|
prosemirror-commands@1.7.1:
|
||||||
|
resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==}
|
||||||
|
|
||||||
|
prosemirror-dropcursor@1.8.2:
|
||||||
|
resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==}
|
||||||
|
|
||||||
|
prosemirror-gapcursor@1.4.1:
|
||||||
|
resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==}
|
||||||
|
|
||||||
|
prosemirror-history@1.5.0:
|
||||||
|
resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==}
|
||||||
|
|
||||||
|
prosemirror-inputrules@1.5.1:
|
||||||
|
resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==}
|
||||||
|
|
||||||
prosemirror-keymap@1.2.3:
|
prosemirror-keymap@1.2.3:
|
||||||
resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==}
|
resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==}
|
||||||
|
|
||||||
@ -2954,6 +3154,9 @@ packages:
|
|||||||
prosemirror-model@1.25.9:
|
prosemirror-model@1.25.9:
|
||||||
resolution: {integrity: sha512-pRTklkDDMMRopyoAcrr9wV/8g/RYgrLHBuJAb5hlEuYZRdm5yqmPjWId83fpBwPpSFqEdja0H7Dfd7z1X/npcA==}
|
resolution: {integrity: sha512-pRTklkDDMMRopyoAcrr9wV/8g/RYgrLHBuJAb5hlEuYZRdm5yqmPjWId83fpBwPpSFqEdja0H7Dfd7z1X/npcA==}
|
||||||
|
|
||||||
|
prosemirror-schema-list@1.5.1:
|
||||||
|
resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==}
|
||||||
|
|
||||||
prosemirror-state@1.4.4:
|
prosemirror-state@1.4.4:
|
||||||
resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==}
|
resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==}
|
||||||
|
|
||||||
@ -3090,10 +3293,16 @@ packages:
|
|||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
rope-sequence@1.3.4:
|
||||||
|
resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
|
||||||
|
|
||||||
router@2.2.0:
|
router@2.2.0:
|
||||||
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
|
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
|
rrweb-cssom@0.8.0:
|
||||||
|
resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
|
||||||
|
|
||||||
rxjs@7.8.1:
|
rxjs@7.8.1:
|
||||||
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
|
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
|
||||||
|
|
||||||
@ -3110,6 +3319,10 @@ packages:
|
|||||||
safer-buffer@2.1.2:
|
safer-buffer@2.1.2:
|
||||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
|
|
||||||
|
saxes@6.0.0:
|
||||||
|
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||||
|
engines: {node: '>=v12.22.7'}
|
||||||
|
|
||||||
scheduler@0.27.0:
|
scheduler@0.27.0:
|
||||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||||
|
|
||||||
@ -3276,6 +3489,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==}
|
resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==}
|
||||||
engines: {node: '>=0.10'}
|
engines: {node: '>=0.10'}
|
||||||
|
|
||||||
|
symbol-tree@3.2.4:
|
||||||
|
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||||
|
|
||||||
tapable@2.3.3:
|
tapable@2.3.3:
|
||||||
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
|
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -3364,6 +3580,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
|
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
|
tldts-core@6.1.86:
|
||||||
|
resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==}
|
||||||
|
|
||||||
|
tldts@6.1.86:
|
||||||
|
resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
toidentifier@1.0.1:
|
toidentifier@1.0.1:
|
||||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
@ -3372,6 +3595,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==}
|
resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
|
|
||||||
|
tough-cookie@5.1.2:
|
||||||
|
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
|
tr46@5.1.1:
|
||||||
|
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
tree-kill@1.2.2:
|
tree-kill@1.2.2:
|
||||||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -3622,6 +3853,10 @@ packages:
|
|||||||
w3c-keyname@2.2.8:
|
w3c-keyname@2.2.8:
|
||||||
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||||
|
|
||||||
|
w3c-xmlserializer@5.0.0:
|
||||||
|
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
watchpack@2.5.2:
|
watchpack@2.5.2:
|
||||||
resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==}
|
resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@ -3629,6 +3864,10 @@ packages:
|
|||||||
wcwidth@1.0.1:
|
wcwidth@1.0.1:
|
||||||
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
|
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
|
||||||
|
|
||||||
|
webidl-conversions@7.0.0:
|
||||||
|
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
webpack-node-externals@3.0.0:
|
webpack-node-externals@3.0.0:
|
||||||
resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==}
|
resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -3650,6 +3889,19 @@ packages:
|
|||||||
webpack-cli:
|
webpack-cli:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
whatwg-encoding@3.1.1:
|
||||||
|
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
|
||||||
|
|
||||||
|
whatwg-mimetype@4.0.0:
|
||||||
|
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
whatwg-url@14.2.0:
|
||||||
|
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
which@2.0.2:
|
which@2.0.2:
|
||||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@ -3671,6 +3923,25 @@ packages:
|
|||||||
wrappy@1.0.2:
|
wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
|
ws@8.21.0:
|
||||||
|
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: '>=5.0.2'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
xml-name-validator@5.0.0:
|
||||||
|
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
xmlchars@2.2.0:
|
||||||
|
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||||
|
|
||||||
y-prosemirror@1.3.7:
|
y-prosemirror@1.3.7:
|
||||||
resolution: {integrity: sha512-NpM99WSdD4Fx4if5xOMDpPtU3oAmTSjlzh5U4353ABbRHl1HtAFUx6HlebLZfyFxXN9jzKMDkVbcRjqOZVkYQg==}
|
resolution: {integrity: sha512-NpM99WSdD4Fx4if5xOMDpPtU3oAmTSjlzh5U4353ABbRHl1HtAFUx6HlebLZfyFxXN9jzKMDkVbcRjqOZVkYQg==}
|
||||||
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
|
||||||
@ -3768,6 +4039,14 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- chokidar
|
- chokidar
|
||||||
|
|
||||||
|
'@asamuzakjp/css-color@3.2.0':
|
||||||
|
dependencies:
|
||||||
|
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
|
||||||
|
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
|
||||||
|
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
|
||||||
|
'@csstools/css-tokenizer': 3.0.4
|
||||||
|
lru-cache: 10.4.3
|
||||||
|
|
||||||
'@babel/code-frame@7.29.7':
|
'@babel/code-frame@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-validator-identifier': 7.29.7
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
@ -3887,6 +4166,26 @@ snapshots:
|
|||||||
'@colors/colors@1.5.0':
|
'@colors/colors@1.5.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@csstools/color-helpers@5.1.0': {}
|
||||||
|
|
||||||
|
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
|
||||||
|
dependencies:
|
||||||
|
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
|
||||||
|
'@csstools/css-tokenizer': 3.0.4
|
||||||
|
|
||||||
|
'@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
|
||||||
|
dependencies:
|
||||||
|
'@csstools/color-helpers': 5.1.0
|
||||||
|
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
|
||||||
|
'@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
|
||||||
|
'@csstools/css-tokenizer': 3.0.4
|
||||||
|
|
||||||
|
'@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
|
||||||
|
dependencies:
|
||||||
|
'@csstools/css-tokenizer': 3.0.4
|
||||||
|
|
||||||
|
'@csstools/css-tokenizer@3.0.4': {}
|
||||||
|
|
||||||
'@epic-web/invariant@1.0.0': {}
|
'@epic-web/invariant@1.0.0': {}
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.25.12':
|
'@esbuild/aix-ppc64@0.25.12':
|
||||||
@ -4169,6 +4468,20 @@ snapshots:
|
|||||||
'@eslint/core': 0.17.0
|
'@eslint/core': 0.17.0
|
||||||
levn: 0.4.1
|
levn: 0.4.1
|
||||||
|
|
||||||
|
'@floating-ui/core@1.7.5':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/utils': 0.2.11
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@floating-ui/dom@1.7.6':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/core': 1.7.5
|
||||||
|
'@floating-ui/utils': 0.2.11
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@floating-ui/utils@0.2.11':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@hookform/resolvers@5.4.0(react-hook-form@7.80.0(react@19.2.7))':
|
'@hookform/resolvers@5.4.0(react-hook-form@7.80.0(react@19.2.7))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@standard-schema/utils': 0.3.0
|
'@standard-schema/utils': 0.3.0
|
||||||
@ -4657,6 +4970,73 @@ snapshots:
|
|||||||
'@tanstack/query-core': 5.101.2
|
'@tanstack/query-core': 5.101.2
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
|
|
||||||
|
'@tiptap/core@3.27.1(@tiptap/pm@3.27.1)':
|
||||||
|
dependencies:
|
||||||
|
'@tiptap/pm': 3.27.1
|
||||||
|
|
||||||
|
'@tiptap/extension-bubble-menu@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/dom': 1.7.6
|
||||||
|
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
|
||||||
|
'@tiptap/pm': 3.27.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tiptap/extension-collaboration@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(@tiptap/y-tiptap@3.0.6(prosemirror-model@1.25.9)(prosemirror-state@1.4.4)(prosemirror-view@1.42.0)(y-protocols@1.0.7(yjs@13.6.31))(yjs@13.6.31))(yjs@13.6.31)':
|
||||||
|
dependencies:
|
||||||
|
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
|
||||||
|
'@tiptap/pm': 3.27.1
|
||||||
|
'@tiptap/y-tiptap': 3.0.6(prosemirror-model@1.25.9)(prosemirror-state@1.4.4)(prosemirror-view@1.42.0)(y-protocols@1.0.7(yjs@13.6.31))(yjs@13.6.31)
|
||||||
|
yjs: 13.6.31
|
||||||
|
|
||||||
|
'@tiptap/extension-floating-menu@3.27.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/dom': 1.7.6
|
||||||
|
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
|
||||||
|
'@tiptap/pm': 3.27.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tiptap/pm@3.27.1':
|
||||||
|
dependencies:
|
||||||
|
prosemirror-changeset: 2.4.1
|
||||||
|
prosemirror-commands: 1.7.1
|
||||||
|
prosemirror-dropcursor: 1.8.2
|
||||||
|
prosemirror-gapcursor: 1.4.1
|
||||||
|
prosemirror-history: 1.5.0
|
||||||
|
prosemirror-inputrules: 1.5.1
|
||||||
|
prosemirror-keymap: 1.2.3
|
||||||
|
prosemirror-model: 1.25.9
|
||||||
|
prosemirror-schema-list: 1.5.1
|
||||||
|
prosemirror-state: 1.4.4
|
||||||
|
prosemirror-tables: 1.8.5
|
||||||
|
prosemirror-transform: 1.12.0
|
||||||
|
prosemirror-view: 1.42.0
|
||||||
|
|
||||||
|
'@tiptap/react@3.27.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
|
dependencies:
|
||||||
|
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
|
||||||
|
'@tiptap/pm': 3.27.1
|
||||||
|
'@types/react': 19.2.17
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.17)
|
||||||
|
'@types/use-sync-external-store': 0.0.6
|
||||||
|
fast-equals: 5.4.0
|
||||||
|
react: 19.2.7
|
||||||
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
|
use-sync-external-store: 1.6.0(react@19.2.7)
|
||||||
|
optionalDependencies:
|
||||||
|
'@tiptap/extension-bubble-menu': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)
|
||||||
|
'@tiptap/extension-floating-menu': 3.27.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@floating-ui/dom'
|
||||||
|
|
||||||
|
'@tiptap/y-tiptap@3.0.6(prosemirror-model@1.25.9)(prosemirror-state@1.4.4)(prosemirror-view@1.42.0)(y-protocols@1.0.7(yjs@13.6.31))(yjs@13.6.31)':
|
||||||
|
dependencies:
|
||||||
|
lib0: 0.2.117
|
||||||
|
prosemirror-model: 1.25.9
|
||||||
|
prosemirror-state: 1.4.4
|
||||||
|
prosemirror-view: 1.42.0
|
||||||
|
y-protocols: 1.0.7(yjs@13.6.31)
|
||||||
|
yjs: 13.6.31
|
||||||
|
|
||||||
'@tokenizer/inflate@0.4.1':
|
'@tokenizer/inflate@0.4.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
@ -4790,6 +5170,8 @@ snapshots:
|
|||||||
'@types/methods': 1.1.4
|
'@types/methods': 1.1.4
|
||||||
'@types/superagent': 8.1.10
|
'@types/superagent': 8.1.10
|
||||||
|
|
||||||
|
'@types/use-sync-external-store@0.0.6': {}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)':
|
'@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
@ -5030,6 +5412,8 @@ snapshots:
|
|||||||
|
|
||||||
acorn@8.17.0: {}
|
acorn@8.17.0: {}
|
||||||
|
|
||||||
|
agent-base@7.1.4: {}
|
||||||
|
|
||||||
ajv-formats@2.1.1(ajv@8.20.0):
|
ajv-formats@2.1.1(ajv@8.20.0):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
ajv: 8.20.0
|
ajv: 8.20.0
|
||||||
@ -5326,14 +5710,26 @@ snapshots:
|
|||||||
shebang-command: 2.0.0
|
shebang-command: 2.0.0
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
|
cssstyle@4.6.0:
|
||||||
|
dependencies:
|
||||||
|
'@asamuzakjp/css-color': 3.2.0
|
||||||
|
rrweb-cssom: 0.8.0
|
||||||
|
|
||||||
csstype@3.2.3: {}
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
|
data-urls@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
whatwg-mimetype: 4.0.0
|
||||||
|
whatwg-url: 14.2.0
|
||||||
|
|
||||||
dateformat@4.6.3: {}
|
dateformat@4.6.3: {}
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
decimal.js@10.6.0: {}
|
||||||
|
|
||||||
deep-eql@5.0.2: {}
|
deep-eql@5.0.2: {}
|
||||||
|
|
||||||
deep-is@0.1.4: {}
|
deep-is@0.1.4: {}
|
||||||
@ -5393,6 +5789,8 @@ snapshots:
|
|||||||
|
|
||||||
entities@4.5.0: {}
|
entities@4.5.0: {}
|
||||||
|
|
||||||
|
entities@6.0.1: {}
|
||||||
|
|
||||||
error-ex@1.3.4:
|
error-ex@1.3.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-arrayish: 0.2.1
|
is-arrayish: 0.2.1
|
||||||
@ -5647,6 +6045,8 @@ snapshots:
|
|||||||
|
|
||||||
fast-deep-equal@3.1.3: {}
|
fast-deep-equal@3.1.3: {}
|
||||||
|
|
||||||
|
fast-equals@5.4.0: {}
|
||||||
|
|
||||||
fast-json-stable-stringify@2.1.0: {}
|
fast-json-stable-stringify@2.1.0: {}
|
||||||
|
|
||||||
fast-levenshtein@2.0.6: {}
|
fast-levenshtein@2.0.6: {}
|
||||||
@ -5817,6 +6217,10 @@ snapshots:
|
|||||||
|
|
||||||
help-me@5.0.0: {}
|
help-me@5.0.0: {}
|
||||||
|
|
||||||
|
html-encoding-sniffer@4.0.0:
|
||||||
|
dependencies:
|
||||||
|
whatwg-encoding: 3.1.1
|
||||||
|
|
||||||
html-parse-stringify@3.0.1:
|
html-parse-stringify@3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
void-elements: 3.1.0
|
void-elements: 3.1.0
|
||||||
@ -5829,6 +6233,20 @@ snapshots:
|
|||||||
statuses: 2.0.2
|
statuses: 2.0.2
|
||||||
toidentifier: 1.0.1
|
toidentifier: 1.0.1
|
||||||
|
|
||||||
|
http-proxy-agent@7.0.2:
|
||||||
|
dependencies:
|
||||||
|
agent-base: 7.1.4
|
||||||
|
debug: 4.4.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
https-proxy-agent@7.0.6:
|
||||||
|
dependencies:
|
||||||
|
agent-base: 7.1.4
|
||||||
|
debug: 4.4.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
i18next-browser-languagedetector@8.2.1:
|
i18next-browser-languagedetector@8.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.29.7
|
'@babel/runtime': 7.29.7
|
||||||
@ -5837,6 +6255,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
|
iconv-lite@0.6.3:
|
||||||
|
dependencies:
|
||||||
|
safer-buffer: 2.1.2
|
||||||
|
|
||||||
iconv-lite@0.7.2:
|
iconv-lite@0.7.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
@ -5870,6 +6292,8 @@ snapshots:
|
|||||||
|
|
||||||
is-interactive@1.0.0: {}
|
is-interactive@1.0.0: {}
|
||||||
|
|
||||||
|
is-potential-custom-element-name@1.0.1: {}
|
||||||
|
|
||||||
is-promise@4.0.0: {}
|
is-promise@4.0.0: {}
|
||||||
|
|
||||||
is-unicode-supported@0.1.0: {}
|
is-unicode-supported@0.1.0: {}
|
||||||
@ -5898,6 +6322,33 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
argparse: 2.0.1
|
argparse: 2.0.1
|
||||||
|
|
||||||
|
jsdom@26.1.0:
|
||||||
|
dependencies:
|
||||||
|
cssstyle: 4.6.0
|
||||||
|
data-urls: 5.0.0
|
||||||
|
decimal.js: 10.6.0
|
||||||
|
html-encoding-sniffer: 4.0.0
|
||||||
|
http-proxy-agent: 7.0.2
|
||||||
|
https-proxy-agent: 7.0.6
|
||||||
|
is-potential-custom-element-name: 1.0.1
|
||||||
|
nwsapi: 2.2.24
|
||||||
|
parse5: 7.3.0
|
||||||
|
rrweb-cssom: 0.8.0
|
||||||
|
saxes: 6.0.0
|
||||||
|
symbol-tree: 3.2.4
|
||||||
|
tough-cookie: 5.1.2
|
||||||
|
w3c-xmlserializer: 5.0.0
|
||||||
|
webidl-conversions: 7.0.0
|
||||||
|
whatwg-encoding: 3.1.1
|
||||||
|
whatwg-mimetype: 4.0.0
|
||||||
|
whatwg-url: 14.2.0
|
||||||
|
ws: 8.21.0
|
||||||
|
xml-name-validator: 5.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
jsesc@3.1.0: {}
|
jsesc@3.1.0: {}
|
||||||
|
|
||||||
json-buffer@3.0.1: {}
|
json-buffer@3.0.1: {}
|
||||||
@ -5962,6 +6413,8 @@ snapshots:
|
|||||||
|
|
||||||
loupe@3.2.1: {}
|
loupe@3.2.1: {}
|
||||||
|
|
||||||
|
lru-cache@10.4.3: {}
|
||||||
|
|
||||||
lru-cache@11.5.1: {}
|
lru-cache@11.5.1: {}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
@ -6086,6 +6539,8 @@ snapshots:
|
|||||||
|
|
||||||
nodemailer@9.0.3: {}
|
nodemailer@9.0.3: {}
|
||||||
|
|
||||||
|
nwsapi@2.2.24: {}
|
||||||
|
|
||||||
nypm@0.6.8:
|
nypm@0.6.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
citty: 0.2.2
|
citty: 0.2.2
|
||||||
@ -6154,6 +6609,10 @@ snapshots:
|
|||||||
json-parse-even-better-errors: 2.3.1
|
json-parse-even-better-errors: 2.3.1
|
||||||
lines-and-columns: 1.2.4
|
lines-and-columns: 1.2.4
|
||||||
|
|
||||||
|
parse5@7.3.0:
|
||||||
|
dependencies:
|
||||||
|
entities: 6.0.1
|
||||||
|
|
||||||
parseurl@1.3.3: {}
|
parseurl@1.3.3: {}
|
||||||
|
|
||||||
path-exists@4.0.0: {}
|
path-exists@4.0.0: {}
|
||||||
@ -6281,6 +6740,41 @@ snapshots:
|
|||||||
|
|
||||||
process-warning@5.0.0: {}
|
process-warning@5.0.0: {}
|
||||||
|
|
||||||
|
prosemirror-changeset@2.4.1:
|
||||||
|
dependencies:
|
||||||
|
prosemirror-transform: 1.12.0
|
||||||
|
|
||||||
|
prosemirror-commands@1.7.1:
|
||||||
|
dependencies:
|
||||||
|
prosemirror-model: 1.25.9
|
||||||
|
prosemirror-state: 1.4.4
|
||||||
|
prosemirror-transform: 1.12.0
|
||||||
|
|
||||||
|
prosemirror-dropcursor@1.8.2:
|
||||||
|
dependencies:
|
||||||
|
prosemirror-state: 1.4.4
|
||||||
|
prosemirror-transform: 1.12.0
|
||||||
|
prosemirror-view: 1.42.0
|
||||||
|
|
||||||
|
prosemirror-gapcursor@1.4.1:
|
||||||
|
dependencies:
|
||||||
|
prosemirror-keymap: 1.2.3
|
||||||
|
prosemirror-model: 1.25.9
|
||||||
|
prosemirror-state: 1.4.4
|
||||||
|
prosemirror-view: 1.42.0
|
||||||
|
|
||||||
|
prosemirror-history@1.5.0:
|
||||||
|
dependencies:
|
||||||
|
prosemirror-state: 1.4.4
|
||||||
|
prosemirror-transform: 1.12.0
|
||||||
|
prosemirror-view: 1.42.0
|
||||||
|
rope-sequence: 1.3.4
|
||||||
|
|
||||||
|
prosemirror-inputrules@1.5.1:
|
||||||
|
dependencies:
|
||||||
|
prosemirror-state: 1.4.4
|
||||||
|
prosemirror-transform: 1.12.0
|
||||||
|
|
||||||
prosemirror-keymap@1.2.3:
|
prosemirror-keymap@1.2.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
prosemirror-state: 1.4.4
|
prosemirror-state: 1.4.4
|
||||||
@ -6296,6 +6790,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
orderedmap: 2.1.1
|
orderedmap: 2.1.1
|
||||||
|
|
||||||
|
prosemirror-schema-list@1.5.1:
|
||||||
|
dependencies:
|
||||||
|
prosemirror-model: 1.25.9
|
||||||
|
prosemirror-state: 1.4.4
|
||||||
|
prosemirror-transform: 1.12.0
|
||||||
|
|
||||||
prosemirror-state@1.4.4:
|
prosemirror-state@1.4.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
prosemirror-model: 1.25.9
|
prosemirror-model: 1.25.9
|
||||||
@ -6449,6 +6949,8 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc': 4.62.2
|
'@rollup/rollup-win32-x64-msvc': 4.62.2
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
rope-sequence@1.3.4: {}
|
||||||
|
|
||||||
router@2.2.0:
|
router@2.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
@ -6459,6 +6961,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
rrweb-cssom@0.8.0: {}
|
||||||
|
|
||||||
rxjs@7.8.1:
|
rxjs@7.8.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@ -6473,6 +6977,10 @@ snapshots:
|
|||||||
|
|
||||||
safer-buffer@2.1.2: {}
|
safer-buffer@2.1.2: {}
|
||||||
|
|
||||||
|
saxes@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
xmlchars: 2.2.0
|
||||||
|
|
||||||
scheduler@0.27.0: {}
|
scheduler@0.27.0: {}
|
||||||
|
|
||||||
schema-utils@3.3.0:
|
schema-utils@3.3.0:
|
||||||
@ -6660,6 +7168,8 @@ snapshots:
|
|||||||
|
|
||||||
symbol-observable@4.0.0: {}
|
symbol-observable@4.0.0: {}
|
||||||
|
|
||||||
|
symbol-tree@3.2.4: {}
|
||||||
|
|
||||||
tapable@2.3.3: {}
|
tapable@2.3.3: {}
|
||||||
|
|
||||||
terser-webpack-plugin@5.6.1(@swc/core@1.15.43)(webpack@5.106.2(@swc/core@1.15.43)):
|
terser-webpack-plugin@5.6.1(@swc/core@1.15.43)(webpack@5.106.2(@swc/core@1.15.43)):
|
||||||
@ -6708,6 +7218,12 @@ snapshots:
|
|||||||
|
|
||||||
tinyspy@4.0.4: {}
|
tinyspy@4.0.4: {}
|
||||||
|
|
||||||
|
tldts-core@6.1.86: {}
|
||||||
|
|
||||||
|
tldts@6.1.86:
|
||||||
|
dependencies:
|
||||||
|
tldts-core: 6.1.86
|
||||||
|
|
||||||
toidentifier@1.0.1: {}
|
toidentifier@1.0.1: {}
|
||||||
|
|
||||||
token-types@6.1.2:
|
token-types@6.1.2:
|
||||||
@ -6716,6 +7232,14 @@ snapshots:
|
|||||||
'@tokenizer/token': 0.3.0
|
'@tokenizer/token': 0.3.0
|
||||||
ieee754: 1.2.1
|
ieee754: 1.2.1
|
||||||
|
|
||||||
|
tough-cookie@5.1.2:
|
||||||
|
dependencies:
|
||||||
|
tldts: 6.1.86
|
||||||
|
|
||||||
|
tr46@5.1.1:
|
||||||
|
dependencies:
|
||||||
|
punycode: 2.3.1
|
||||||
|
|
||||||
tree-kill@1.2.2: {}
|
tree-kill@1.2.2: {}
|
||||||
|
|
||||||
ts-api-utils@2.5.0(typescript@5.9.3):
|
ts-api-utils@2.5.0(typescript@5.9.3):
|
||||||
@ -6905,7 +7429,7 @@ snapshots:
|
|||||||
terser: 5.48.0
|
terser: 5.48.0
|
||||||
tsx: 4.23.0
|
tsx: 4.23.0
|
||||||
|
|
||||||
vitest@3.2.6(@types/node@26.1.0)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.0):
|
vitest@3.2.6(@types/node@26.1.0)(jiti@2.7.0)(jsdom@26.1.0)(terser@5.48.0)(tsx@4.23.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/chai': 5.2.3
|
'@types/chai': 5.2.3
|
||||||
'@vitest/expect': 3.2.6
|
'@vitest/expect': 3.2.6
|
||||||
@ -6932,6 +7456,7 @@ snapshots:
|
|||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 26.1.0
|
'@types/node': 26.1.0
|
||||||
|
jsdom: 26.1.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- jiti
|
- jiti
|
||||||
- less
|
- less
|
||||||
@ -6950,6 +7475,10 @@ snapshots:
|
|||||||
|
|
||||||
w3c-keyname@2.2.8: {}
|
w3c-keyname@2.2.8: {}
|
||||||
|
|
||||||
|
w3c-xmlserializer@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
xml-name-validator: 5.0.0
|
||||||
|
|
||||||
watchpack@2.5.2:
|
watchpack@2.5.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@ -6958,6 +7487,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
defaults: 1.0.4
|
defaults: 1.0.4
|
||||||
|
|
||||||
|
webidl-conversions@7.0.0: {}
|
||||||
|
|
||||||
webpack-node-externals@3.0.0: {}
|
webpack-node-externals@3.0.0: {}
|
||||||
|
|
||||||
webpack-sources@3.5.0: {}
|
webpack-sources@3.5.0: {}
|
||||||
@ -7004,6 +7535,17 @@ snapshots:
|
|||||||
- postcss
|
- postcss
|
||||||
- uglify-js
|
- uglify-js
|
||||||
|
|
||||||
|
whatwg-encoding@3.1.1:
|
||||||
|
dependencies:
|
||||||
|
iconv-lite: 0.6.3
|
||||||
|
|
||||||
|
whatwg-mimetype@4.0.0: {}
|
||||||
|
|
||||||
|
whatwg-url@14.2.0:
|
||||||
|
dependencies:
|
||||||
|
tr46: 5.1.1
|
||||||
|
webidl-conversions: 7.0.0
|
||||||
|
|
||||||
which@2.0.2:
|
which@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
isexe: 2.0.0
|
isexe: 2.0.0
|
||||||
@ -7023,6 +7565,12 @@ snapshots:
|
|||||||
|
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|
||||||
|
ws@8.21.0: {}
|
||||||
|
|
||||||
|
xml-name-validator@5.0.0: {}
|
||||||
|
|
||||||
|
xmlchars@2.2.0: {}
|
||||||
|
|
||||||
y-prosemirror@1.3.7(prosemirror-model@1.25.9)(prosemirror-state@1.4.4)(prosemirror-view@1.42.0)(y-protocols@1.0.7(yjs@13.6.31))(yjs@13.6.31):
|
y-prosemirror@1.3.7(prosemirror-model@1.25.9)(prosemirror-state@1.4.4)(prosemirror-view@1.42.0)(y-protocols@1.0.7(yjs@13.6.31))(yjs@13.6.31):
|
||||||
dependencies:
|
dependencies:
|
||||||
lib0: 0.2.117
|
lib0: 0.2.117
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user