All checks were successful
CI / Lint, typecheck, test (push) Successful in 3m35s
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Successful in 3m44s
CD / Deploy to Test (push) Successful in 15s
CD / Smoke tests against Test (push) Successful in 1m17s
CD / Promote to Int (push) Successful in 11s
CI / Auth e2e pack (push) Successful in 5m36s
CI / Import/export fidelity gate (push) Successful in 46s
- content footer: the collab status is an icon (wifi/off/refresh, localized tooltip + visually-hidden text, class/data-status hooks kept for e2e) on the left, the legal links right-aligned; read mode drops the editor frame and its inner padding, edit mode keeps it - menus (page overflow, user, notifications bell, pond switcher) close on outside click and Escape via a shared useDismissable hook; the bell got its missing tooltip - side panels (labels, history) stack vertically in one column - edit mode gains a Save-version icon (prompt for the name, POST /pages/:id/versions); the history panel lists contributors by display name — more than three collapse to two plus an expandable ellipsis (PageVersionView.contributors resolved server-side, deleted users drop out) - search finds partial words via a LIKE fallback next to the tsquery (FTS matches still rank first; regression-pinned in the db pack), and the recent-searches list has a clear button - pond owners create labels directly in the label picker (plus a permanent link to the full manager); add/remove/delete buttons across the pond settings (members, access rules, labels, files) and the watch/unwatch toggles in pond/user settings are icon buttons now — class hooks and accessible names unchanged for the e2e packs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
113 lines
3.6 KiB
TypeScript
113 lines
3.6 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
/**
|
|
* Page schemas and views shared between api and web (issue #23). A page
|
|
* carries a Yjs document from day one (ADR 0003); in M2 its state is
|
|
* saved wholesale over REST as a base64 string.
|
|
*/
|
|
|
|
export const pageTitleSchema = z
|
|
.string()
|
|
.trim()
|
|
.min(1, 'validation.required')
|
|
.max(200, 'validation.tooLong');
|
|
|
|
export const pageSlugSchema = z
|
|
.string()
|
|
.trim()
|
|
.min(1, 'validation.required')
|
|
.max(60, 'validation.tooLong');
|
|
|
|
export const createPageInputSchema = z.object({
|
|
title: pageTitleSchema,
|
|
});
|
|
export type CreatePageInput = z.infer<typeof createPageInputSchema>;
|
|
|
|
export const updatePageInputSchema = z
|
|
.object({
|
|
title: pageTitleSchema,
|
|
slug: pageSlugSchema,
|
|
})
|
|
.partial();
|
|
export type UpdatePageInput = z.infer<typeof updatePageInputSchema>;
|
|
|
|
/**
|
|
* Move a page in the manual sidebar order (issue #45): place it between the
|
|
* `afterId` page (its new predecessor) and the `beforeId` page (its new
|
|
* successor); either is `null` at an end of the list. The server recomputes
|
|
* only the moved page's `sort_key`.
|
|
*/
|
|
export const repositionPageInputSchema = z.object({
|
|
afterId: z.string().min(1).nullable(),
|
|
beforeId: z.string().min(1).nullable(),
|
|
});
|
|
export type RepositionPageInput = z.infer<typeof repositionPageInputSchema>;
|
|
|
|
export const savePageStateInputSchema = z.object({
|
|
/** Base64-encoded Yjs state (`Y.encodeStateAsUpdate`). */
|
|
state: z.string().min(1, 'validation.required'),
|
|
});
|
|
export type SavePageStateInput = z.infer<typeof savePageStateInputSchema>;
|
|
|
|
/** Max Yjs document size (operations.md §Limits) — a fixed operational
|
|
* ceiling, not a per-pond/user quota. */
|
|
export const MAX_PAGE_DOCUMENT_BYTES = 5 * 1024 * 1024;
|
|
|
|
export interface PageView {
|
|
id: string;
|
|
pondId: string;
|
|
title: string;
|
|
slug: string;
|
|
sortKey: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
deletedAt: string | null;
|
|
}
|
|
|
|
/** What `GET /pages/:id` returns: page meta plus its base64 Yjs state. */
|
|
export interface PageStateView extends PageView {
|
|
state: string;
|
|
}
|
|
|
|
/**
|
|
* A page in the sidebar list (`GET /ponds/:id/pages`) with the ids of the
|
|
* labels assigned to it (issue #44). The sidebar maps these to colours/names
|
|
* via the pond's label tree for chips and filters by them (descendant-inclusive
|
|
* via the shared tree helpers) — so no per-label detail is repeated here.
|
|
*/
|
|
export interface PageListItemView extends PageView {
|
|
labelIds: string[];
|
|
}
|
|
|
|
/** Why a version snapshot exists (ADR 0013): automatic (session end / active
|
|
* interval), a named manual snapshot, or the automatic pre-restore snapshot. */
|
|
export type PageVersionTrigger = 'auto' | 'manual' | 'pre_restore';
|
|
|
|
export const createVersionInputSchema = z.object({
|
|
label: z.string().trim().min(1, 'validation.required').max(100, 'validation.tooLong'),
|
|
});
|
|
export type CreateVersionInput = z.infer<typeof createVersionInputSchema>;
|
|
|
|
/** A version in the history list (issue #41; no snapshot bytes). */
|
|
export interface PageVersionView {
|
|
id: string;
|
|
pageId: string;
|
|
trigger: PageVersionTrigger;
|
|
label: string | null;
|
|
/** Editor for manual/pre-restore versions; null for automatic snapshots. */
|
|
createdBy: string | null;
|
|
/** Users who edited since the previous version. */
|
|
contributorIds: string[];
|
|
/** Display names for contributorIds; deleted accounts are omitted. */
|
|
contributors: { id: string; name: string }[];
|
|
createdAt: string;
|
|
}
|
|
|
|
/** A single version with its rendered content, for viewing/diffing (issue #42). */
|
|
export interface PageVersionContentView extends PageVersionView {
|
|
/** Read-only HTML render of the snapshot. */
|
|
html: string;
|
|
/** Markdown of the snapshot, for the diff against the current page. */
|
|
markdown: string;
|
|
}
|