diff --git a/.changeset/calendar-readme-schema-keys-5045.md b/.changeset/calendar-readme-schema-keys-5045.md
new file mode 100644
index 000000000..eaf0ed083
--- /dev/null
+++ b/.changeset/calendar-readme-schema-keys-5045.md
@@ -0,0 +1,26 @@
+---
+'@object-ui/plugin-calendar': patch
+---
+
+`README.md`'s "Schema API / CalendarView" block described a `CalendarViewSchema`
+that does not exist. Measured against the interface itself
+(`packages/types/src/complex.ts`) and its zod mirror: `events` — the schema's
+only required key besides `type` — was published as `events?`, so a reader
+following the README omits it and TypeScript rejects the node; `defaultDate` was
+`string` where the schema says `string | Date`; and `onDateClick` was listed as a
+schema key when it is a `CalendarViewProps` **component** prop, sending readers
+to a different package's surface for a key `calendar-view` does not have (the
+schema's key is `onDateChange`). The block also listed 6 of the schema's 13 keys
+with nothing saying it was a summary (objectui#5045).
+
+The block now carries the requiredness the schema declares, names itself a
+partial summary of `CalendarViewSchema`, and adds the author-facing
+`defaultView` / `view` / `views` / `editable` / `date`. It also states plainly
+what the registered `calendar-view` renderer actually reads — it builds events
+from the node's `data` array and drops an authored `events` key (objectui#4433) —
+so the corrected requiredness does not itself become a new wrong instruction.
+
+This is a documentation fix to a file `plugin-calendar` publishes to npm, which
+is why it carries a version: the npm landing page only picks up the correction
+on a release. No behaviour, export, type, or `dist` byte changes. The pin test
+added alongside it publishes nothing.
diff --git a/packages/plugin-calendar/README.md b/packages/plugin-calendar/README.md
index f41fe647a..16b2dce16 100644
--- a/packages/plugin-calendar/README.md
+++ b/packages/plugin-calendar/README.md
@@ -148,19 +148,44 @@ ComponentRegistry.register('my-calendar', ObjectCalendarRenderer);
### CalendarView
-Display a monthly calendar with events:
+Display a monthly calendar with events. This is a **partial summary**: the full
+contract is `CalendarViewSchema` in `@object-ui/types`, which declares 13 keys of
+its own on top of the common `BaseSchema` keys (`className`, `id`, `data`,
+`visible`, ...).
```typescript
{
type: 'calendar-view',
- events?: CalendarEvent[],
- defaultDate?: string, // ISO date string
- onEventClick?: (event) => void,
- onDateClick?: (date) => void,
- className?: string
+ events: CalendarEvent[], // REQUIRED — the only required key besides `type`
+ defaultView?: CalendarViewMode, // 'month' | 'week' | 'day' | 'agenda' (default 'month')
+ view?: CalendarViewMode, // controlled
+ views?: CalendarViewMode[], // default ['month', 'week', 'day']
+ defaultDate?: string | Date,
+ date?: string | Date, // controlled
+ editable?: boolean, // default false
+ onEventClick?: (event: CalendarEvent) => void,
+ onDateChange?: (date: Date) => void,
+ onViewChange?: (view: CalendarViewMode) => void,
+ className?: string // from `BaseSchema`, not `CalendarViewSchema`
}
```
+`onEventCreate` and `onEventUpdate` complete the 13; see `CalendarViewSchema` for
+their signatures. `onDateClick` is **not** on this schema — it is a
+`CalendarViewProps` component prop (see [Drag-and-Drop](#drag-and-drop) and
+[Click-to-Create](#click-to-create)).
+
+> **The type is not the renderer.** `CalendarViewSchema` is the shape
+> `@object-ui/types` publishes for this node, not a description of what the
+> registered `calendar-view` renderer reads. That renderer builds its events from
+> the node's `data` array plus the `titleField` / `startDateField` /
+> `endDateField` / `colorField` / `allDayField` inputs, and **drops an authored
+> `events` key** (objectui#4433) — so the type's one required key does nothing
+> when written as JSON. Of the keys above it also reads `view` and `className`;
+> the handlers only ever arrive from a React host
+> (``), because JSON cannot carry a
+> function.
+
### Calendar Event Structure
The authored event shape, declared by `@object-ui/types`:
@@ -226,9 +251,9 @@ const schema = {
console.log('Event clicked:', event);
// Open event details modal
},
- onDateClick: (date) => {
- console.log('Date clicked:', date);
- // Create new event
+ onDateChange: (date) => {
+ console.log('Visible date changed:', date);
+ // React to the calendar moving to another month/week/day
}
};
```
diff --git a/packages/plugin-calendar/src/readme-calendar-view-schema.test.ts b/packages/plugin-calendar/src/readme-calendar-view-schema.test.ts
new file mode 100644
index 000000000..eb669209a
--- /dev/null
+++ b/packages/plugin-calendar/src/readme-calendar-view-schema.test.ts
@@ -0,0 +1,179 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * `packages/plugin-calendar/README.md`'s "Schema API -> CalendarView" fence
+ * describes real `CalendarViewSchema` keys, with the requiredness the schema
+ * actually declares (objectui#5045).
+ *
+ * ## Why this file exists
+ *
+ * The fence had drifted from the type in four separate ways at once, and every
+ * one of them was reader-facing on the npm landing page:
+ *
+ * - `events?` — the schema's ONLY required key besides `type`, published as
+ * optional. A reader following the README omits it and TypeScript rejects
+ * the node;
+ * - `defaultDate?: string` — the schema says `string | Date`;
+ * - `onDateClick` — not on `CalendarViewSchema` at all. It is a
+ * `CalendarViewProps` prop, i.e. a different package's *component* surface,
+ * so the README sent readers to a key the schema does not have;
+ * - six keys of thirteen listed, with nothing saying the list was partial.
+ *
+ * Nothing was holding the fence to the type, so all four drifted silently and
+ * were found by reading. That is the failure mode `readme-registration-keys`
+ * in `@object-ui/layout` names for the same class of defect: "someone read it"
+ * is not a mechanism. This card's fix was another hand-correction; this pin is
+ * what stops the next one being needed.
+ *
+ * ## Exhaustiveness is deliberately NOT asserted
+ *
+ * The fence is a declared PARTIAL summary — it names the author-facing keys and
+ * points at `CalendarViewSchema` for the rest. A pin demanding all thirteen
+ * would convert an editorial choice into a gate and force every future schema
+ * key into the README. What is asserted is the direction that misleads a
+ * reader: a key the README names must EXIST, and must carry the requiredness
+ * the schema gives it.
+ *
+ * ## Both sides are parsed from source, never restated here
+ *
+ * Hardcoding the key list in this file would reproduce the defect one layer up
+ * — the README was itself a confident hand-written restatement of the schema.
+ * So `CalendarViewSchema` and `BaseSchema` are read out of
+ * `packages/types/src/*.ts` on every run, and a red test here always means "fix
+ * the README (or the schema)", never "update the test". A moved or renamed
+ * interface fails LOUDLY rather than vacuously passing an empty key set.
+ */
+
+import { describe, it, expect } from 'vitest';
+import { readFileSync, existsSync } from 'node:fs';
+import { dirname, join, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+/** Walk up to the workspace root, so the type source is found by repo layout. */
+function repoRoot(): string {
+ let dir = dirname(fileURLToPath(import.meta.url));
+ for (let i = 0; i < 10; i += 1) {
+ if (existsSync(join(dir, 'pnpm-workspace.yaml'))) return dir;
+ dir = resolve(dir, '..');
+ }
+ throw new Error('repo root (pnpm-workspace.yaml) not found from this test file');
+}
+
+const ROOT = repoRoot();
+const README = join(ROOT, 'packages/plugin-calendar/README.md');
+
+interface Key {
+ optional: boolean;
+}
+
+/**
+ * The own (not inherited) property declarations of a TS interface, by brace
+ * matching rather than a line regex — the bodies here carry JSDoc blocks and
+ * function types with their own braces and semicolons.
+ */
+function interfaceKeys(file: string, name: string): Map {
+ const src = readFileSync(file, 'utf8');
+ const opener = new RegExp(`export interface ${name}\\b[^{]*\\{`).exec(src);
+ if (!opener) throw new Error(`interface ${name} not found in ${file}`);
+
+ let i = opener.index + opener[0].length;
+ const start = i;
+ for (let depth = 1; depth > 0; i += 1) {
+ if (i >= src.length) throw new Error(`unterminated interface ${name} in ${file}`);
+ if (src[i] === '{') depth += 1;
+ else if (src[i] === '}') depth -= 1;
+ }
+
+ const body = src
+ .slice(start, i - 1)
+ .replace(/\/\*[\s\S]*?\*\//g, '')
+ .replace(/\/\/.*$/gm, '');
+
+ const keys = new Map();
+ let depth = 0;
+ let buf = '';
+ const flush = () => {
+ const m = /^(?:readonly\s+)?([A-Za-z_$][\w$]*)(\?)?\s*:/.exec(buf.trim());
+ if (m) keys.set(m[1], { optional: Boolean(m[2]) });
+ buf = '';
+ };
+ for (const ch of body) {
+ if ('{(['.includes(ch)) depth += 1;
+ if ('})]'.includes(ch)) depth -= 1;
+ if ((ch === ';' || ch === '\n') && depth === 0) flush();
+ else buf += ch;
+ }
+ flush();
+
+ if (keys.size === 0) throw new Error(`parsed zero keys out of ${name} in ${file}`);
+ return keys;
+}
+
+/** The "Schema API -> CalendarView" fence, and the keys it publishes. */
+function readmeFence(): { keys: Map; ownCountClaim: number } {
+ const src = readFileSync(README, 'utf8');
+ const heading = src.indexOf('\n### CalendarView\n');
+ if (heading < 0) throw new Error('"### CalendarView" heading not found in the README');
+
+ const section = src.slice(heading, src.indexOf('\n### ', heading + 1));
+ const fence = /```typescript\n([\s\S]*?)\n```/.exec(section);
+ if (!fence) throw new Error('no typescript fence under "### CalendarView"');
+
+ const keys = new Map();
+ for (const line of fence[1].split('\n')) {
+ const m = /^ {2}([A-Za-z_$][\w$]*)(\?)?:/.exec(line);
+ if (m) keys.set(m[1], { optional: Boolean(m[2]) });
+ }
+ if (keys.size === 0) throw new Error('parsed zero keys out of the README fence');
+
+ const claim = /declares (\d+) keys of\s+its own/.exec(section);
+ if (!claim) throw new Error('the fence no longer states how many keys the schema declares');
+
+ return { keys, ownCountClaim: Number(claim[1]) };
+}
+
+const TYPES = join(ROOT, 'packages/types/src');
+const own = interfaceKeys(join(TYPES, 'complex.ts'), 'CalendarViewSchema');
+const base = interfaceKeys(join(TYPES, 'base.ts'), 'BaseSchema');
+const fence = readmeFence();
+
+describe('plugin-calendar README: "Schema API -> CalendarView"', () => {
+ it('names only keys `CalendarViewSchema` or `BaseSchema` actually declares', () => {
+ const phantom = [...fence.keys.keys()].filter((k) => !own.has(k) && !base.has(k));
+ expect(
+ phantom,
+ `README publishes ${JSON.stringify(phantom)} as \`calendar-view\` schema keys, but ` +
+ 'neither `CalendarViewSchema` nor `BaseSchema` declares them. `onDateClick` is the ' +
+ 'original offender: a `CalendarViewProps` component prop, not a schema key.',
+ ).toEqual([]);
+ });
+
+ it('gives every key the requiredness the schema declares', () => {
+ const wrong: string[] = [];
+ for (const [key, { optional }] of fence.keys) {
+ const declared = own.get(key) ?? base.get(key);
+ if (declared && declared.optional !== optional) {
+ wrong.push(`${key}: README says ${optional ? 'optional' : 'required'}, schema says ${declared.optional ? 'optional' : 'required'}`);
+ }
+ }
+ expect(
+ wrong,
+ 'The README must not restate the schema\'s requiredness incorrectly — `events` is the ' +
+ 'schema\'s only required key besides `type`, and was published as `events?`.',
+ ).toEqual([]);
+ });
+
+ it('states the number of keys `CalendarViewSchema` declares, and states it correctly', () => {
+ expect(
+ fence.ownCountClaim,
+ 'The README tells readers how many keys the full schema has. That figure is prose and ' +
+ 'drifts like any other; it is held to the interface here.',
+ ).toBe(own.size);
+ });
+});
diff --git a/packages/plugin-calendar/tsconfig.test.json b/packages/plugin-calendar/tsconfig.test.json
index a98ee894b..99d7e7aac 100644
--- a/packages/plugin-calendar/tsconfig.test.json
+++ b/packages/plugin-calendar/tsconfig.test.json
@@ -13,8 +13,13 @@
// a global augmentation, not an import, and `@testing-library/jest-dom` does
// not live under `@types/` — so it is never picked up automatically and has
// to be named here. Naming `types` at all switches off automatic `@types/*`
- // inclusion, which is fine: nothing in these tests touches Node globals.
- "types": ["@testing-library/jest-dom"],
+ // inclusion, which is why `node` is named alongside it:
+ // `readme-calendar-view-schema.test.ts` reads this package's README and the
+ // `@object-ui/types` sources off disk to hold the published schema docs to
+ // the schema (objectui#5045). Same reason `packages/layout/tsconfig.test.json`
+ // names it, and same reason it stays OUT of `tsconfig.json`: package SOURCE
+ // ships to browsers and must not compile against Node APIs.
+ "types": ["@testing-library/jest-dom", "node"],
// Drop the root tsconfig's source-tree `paths` so `@object-ui/*` and
// `@objectstack/spec` resolve through the workspace dependency's built
// `.d.ts` instead of pulling sibling sources in as program inputs (TS6059).