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>
143 lines
4.0 KiB
TypeScript
143 lines
4.0 KiB
TypeScript
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'),
|
|
});
|