diff --git a/.changeset/6170-timeline-schema-declared-keys.md b/.changeset/6170-timeline-schema-declared-keys.md
new file mode 100644
index 0000000000..da89bd6eca
--- /dev/null
+++ b/.changeset/6170-timeline-schema-declared-keys.md
@@ -0,0 +1,48 @@
+---
+'@object-ui/plugin-timeline': minor
+'@object-ui/types': minor
+---
+
+`TimelineSchema` now declares the presentational keys the timeline renderer actually reads
+(objectui#6170, maintainer ruling 2026-08-25 — the same family rule adopted on
+objectui#6172: the exported type aligns to the measured authored + read set).
+
+Before this, `TimelineSchema` declared `events` (required), `orientation` and `position`,
+and nothing else. `TimelineRenderer` is annotated `schema: TimelineSchema` and reads nine
+keys off that node — `variant`, `items`, `dateFormat`, `onItemClick`, `minDate`, `maxDate`,
+`rowLabel`, `scale`, `timeScale` — and **none** of the three that were declared. The docs
+property table and the registration's own designer `inputs` had agreed with the renderer
+all along; only the exported type disagreed. It was invisible to `tsc` because `BaseSchema`
+carries `[key: string]: any`, so every undeclared key resolved as `any` and the annotation
+constrained nothing.
+
+The most visible casualty was the docs page's own TypeScript example, which did not
+compile: `Property 'events' is missing in type '{ type: "timeline"; variant: string; items:
+… }' but required in type 'TimelineSchema'`. The page taught an authoring form its own
+published type refused.
+
+**Declared now** (TS interface and the `@object-ui/types/zod` mirror together): `variant`,
+`items`, `dateFormat`, `scale`, `timeScale`, `rowLabel`, `minDate`, `maxDate`. `onItemClick`
+is deliberately left undeclared — it is a runtime slot `ObjectTimeline` installs, and this
+package keeps callback-shaped keys off the authored surface.
+
+**`scale` is the canonical axis key.** It is `@objectstack/spec`'s `ui/TimelineConfig.json`
+spelling and the one `resolveTimelineScale` reads first (`scale ?? timeScale`). The designer
+now offers it, with all six buckets: `hour` / `quarter` / `year` have rendered correctly
+since objectui#2942 but were offered by neither the designer (which listed three) nor the
+exported type (which listed none), so they were authorable and undiscoverable. `timeScale`
+stays as a deprecated alias so stored JSON keeps working; retiring it is routed separately.
+
+**`events` is now optional.** It was required, which is why the documented authoring form
+did not type-check. That widening is the only non-additive change here — strictly more
+programs compile and strictly more input parses than before. `events`, `orientation` and
+`position` remain declared and remain read by nothing; a timeline authored with `events`
+still renders an empty rail. Their removal is a breaking narrowing of a published type and
+is routed through ADR-0049 enforce-or-remove as its own change, not smuggled into this one.
+
+Accept-set note for consumers: keys that previously resolved as `any` are now typed, so a
+value the renderer never implemented — `variant: 'diagonal'`, `dateFormat: 'medieval'`,
+`scale: 'fortnight'` — is a type error and a Zod rejection where it used to pass silently.
+Nothing that renders today stops rendering. `BaseSchema`'s index signature is untouched, so
+an undeclared key is still accepted by both halves (objectui#5155 / objectui#6269 own that
+ceiling).
diff --git a/content/docs/plugins/plugin-timeline.mdx b/content/docs/plugins/plugin-timeline.mdx
index dd750a94f9..ed89574354 100644
--- a/content/docs/plugins/plugin-timeline.mdx
+++ b/content/docs/plugins/plugin-timeline.mdx
@@ -58,7 +58,7 @@ const schema = {
- **Customizable Markers**: Color-coded markers with icon support
- **Date Formatting**: Multiple date format options
- **Gantt Charts**: Project timeline visualization with task bars
-- **Time Scales**: Day, week, or month scales for Gantt view
+- **Time Scales**: Hour, day, week, month, quarter, or year scales for Gantt view
- **Lightweight**: Pure CSS and React components
## Schema API
@@ -72,7 +72,8 @@ const schema = {
items?: TimelineItem[],
dateFormat?: 'short' | 'long' | 'iso',
// Gantt-specific
- timeScale?: 'day' | 'week' | 'month',
+ scale?: 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year',
+ timeScale?: 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year', // deprecated alias of `scale`
rowLabel?: string,
minDate?: string,
maxDate?: string,
@@ -115,12 +116,24 @@ const schema = {
| `variant` | string | `'vertical'` | Timeline layout: vertical, horizontal, or gantt |
| `items` | array | `[]` | Array of timeline items |
| `dateFormat` | string | `'short'` | Date formatting: short, long, or iso |
-| `timeScale` | string | `'month'` | Gantt time scale: day, week, or month |
+| `scale` | string | `'month'` | Gantt axis bucket: hour, day, week, month, quarter, or year |
+| `timeScale` | string | — | **Deprecated** — the pre-spec spelling of `scale`, still read as a fallback |
| `rowLabel` | string | `'Items'` | Label for Gantt rows |
| `minDate` | string | auto | Override min date for Gantt (YYYY-MM-DD) |
| `maxDate` | string | auto | Override max date for Gantt (YYYY-MM-DD) |
| `className` | string | `''` | Additional Tailwind CSS classes |
+Every key above is declared on the exported `TimelineSchema`, so an editor
+completes them and a wrong value is a type error. `scale` is the canonical axis
+key — it is `@objectstack/spec`'s `ui/TimelineConfig.json` spelling and the one
+the renderer reads first (`scale ?? timeScale`).
+
+
+`events`, `orientation` and `position` are also declared on `TimelineSchema`
+and are **read by nothing**. A timeline authored with `events` renders an empty
+rail — use `items`. They are deprecated and scheduled for removal.
+
+
## Variants
### Marker Variants
diff --git a/packages/plugin-timeline/src/renderer.tsx b/packages/plugin-timeline/src/renderer.tsx
index 29e4865a45..7e1a426478 100644
--- a/packages/plugin-timeline/src/renderer.tsx
+++ b/packages/plugin-timeline/src/renderer.tsx
@@ -524,13 +524,33 @@ ComponentRegistry.register(
label: 'Date Format',
defaultValue: 'short',
},
+ // The designer's axis key is `scale` — the spec's spelling
+ // (`ui/TimelineConfig.json`) and the one `resolveTimelineScale` prefers.
+ // It offers all six buckets: `hour` / `quarter` / `year` have rendered
+ // correctly since #2942 but were offered by neither the designer nor the
+ // exported type, so they were authorable and undiscoverable (objectui#6170).
{
- name: 'timeScale',
+ name: 'scale',
type: 'enum',
- enum: ['day', 'week', 'month'],
+ enum: [...TIMELINE_SCALES],
label: 'Time Scale (Gantt only)',
defaultValue: 'month',
},
+ // Kept so a stored `timeScale` still round-trips through the designer.
+ // Deprecated in favour of `scale`; retiring the alias is routed separately
+ // (objectui#6170 maintainer ruling 2026-08-25). `ComponentInput` has no
+ // `deprecated` slot and no index signature, so the notice lives in
+ // `description` — this package's stated ceiling for anything the coarse
+ // `type` cannot express.
+ {
+ name: 'timeScale',
+ type: 'enum',
+ enum: [...TIMELINE_SCALES],
+ label: 'Time Scale (Gantt only)',
+ description:
+ 'DEPRECATED — use `scale`, which @objectstack/spec owns and this renderer reads first. Kept so stored JSON keeps working.',
+ advanced: true,
+ },
{
name: 'rowLabel',
type: 'string',
diff --git a/packages/types/src/__tests__/timeline-declared-keys.test.ts b/packages/types/src/__tests__/timeline-declared-keys.test.ts
new file mode 100644
index 0000000000..114be7dca2
--- /dev/null
+++ b/packages/types/src/__tests__/timeline-declared-keys.test.ts
@@ -0,0 +1,279 @@
+/**
+ * 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.
+ */
+
+/**
+ * Declaration pin — the eight presentational keys `TimelineRenderer` reads that
+ * `TimelineSchema` did not declare (objectui#6170).
+ *
+ * ## What was wrong
+ *
+ * `TimelineSchema` declared `events` (REQUIRED), `orientation` and `position`,
+ * and nothing else. `TimelineRenderer`
+ * (`plugin-timeline/src/renderer.tsx:250`) is annotated `schema:
+ * TimelineSchema` and reads nine keys off it — `variant`, `items`,
+ * `dateFormat`, `onItemClick`, `minDate`, `maxDate`, `rowLabel`, `scale`,
+ * `timeScale` — and NONE of the three that were declared. So the exported type
+ * matched neither what authors write, nor what the designer offers (the
+ * registration's own `inputs`), nor what the renderer reads; those three agreed
+ * with each other all along.
+ *
+ * The divergence was invisible to `tsc` because `BaseSchema` carries
+ * `[key: string]: any`, so every undeclared key resolved as `any` and the
+ * annotation constrained nothing. Its most visible casualty was the docs page's
+ * own TypeScript example, which did not compile — `events` was required and
+ * nothing on the page ever writes it. That example is pinned below.
+ *
+ * Eight of the nine are declared. `onItemClick` is deliberately NOT: it is a
+ * runtime slot `ObjectTimeline` installs when it composes the schema it hands
+ * to `TimelineRenderer`, and this package keeps callback-shaped keys off the
+ * authored surface (`RuntimeOnlyDeclared` in `zod-mirror-parity.test.ts`).
+ *
+ * ## What the pin has teeth against, and what it does not
+ *
+ * Same ceiling as objectui#5903's gantt pin, and stated here rather than left
+ * to be assumed. `BaseSchema` is `.passthrough()` on the zod side and carries
+ * an index signature on the TS side (objectui#5155 / objectui#6269 own that
+ * ceiling; this card does not touch it), so:
+ *
+ * - an UNDECLARED key is still accepted by both halves. Declaring these eight
+ * did NOT buy rejection of a misspelling;
+ * - a DECLARED key IS validated. `variant: 'diagonal'` type-checked and
+ * parsed green before this card and is refused now — that is the accept-set
+ * narrowing landed here;
+ * - on the TS side a read site can never be the detector, because the index
+ * signature types `schema.variant` as `any` either way. So the compile-time
+ * pin is the `@ts-expect-error` block at the bottom: remove a declaration
+ * and its member resolves to `any`, the wrong-typed assignment starts
+ * succeeding, and the now-unused directive fails the build (TS2578) NAMING
+ * the key. `tsconfig.test.json` compiles this file, so that is real
+ * enforcement and not decoration (objectui#3009).
+ *
+ * ## The three keys that are still declared and still dead
+ *
+ * `events` / `orientation` / `position` have zero read points. Their RETIREMENT
+ * is routed, not done — objectui#6170's maintainer ruling (2026-08-25) sends
+ * them down the ADR-0049 enforce-or-remove route, which is a breaking removal
+ * from a published type. `events` went required → OPTIONAL here, which is a
+ * strict widening and the smallest change that lets the documented authoring
+ * form compile. They are pinned below as STILL DECLARED so the removal, when it
+ * comes, is a deliberate edit against a red test rather than a silent drift.
+ */
+
+import { describe, it, expect } from 'vitest';
+import { TimelineConfigSchema } from '@objectstack/spec/ui';
+import { TimelineSchema } from '../zod/data-display.zod.js';
+import type { TimelineSchema as TimelineSchemaTS, TimelineScale } from '../data-display.js';
+
+const MINIMAL = { type: 'timeline' } as const;
+
+/** Unwrap ZodOptional/ZodDefault/description wrappers down to the inner enum schema. */
+function unwrap(schema: any): any {
+ let cur = schema;
+ while (cur?._def?.innerType) cur = cur._def.innerType;
+ return cur;
+}
+
+/** The eight keys this card declared, each with a value its declared type refuses. */
+const DECLARED: ReadonlyArray = [
+ ['variant', 'diagonal'],
+ ['items', 'not-an-array'],
+ ['dateFormat', 'medieval'],
+ ['scale', 'fortnight'],
+ ['timeScale', 'fortnight'],
+ ['rowLabel', 5],
+ ['minDate', 20240101],
+ ['maxDate', 20241231],
+];
+
+describe('TimelineSchema — the eight presentational keys are declared (objectui#6170)', () => {
+ it('the mirror declares every one of them', () => {
+ const shape = Object.keys(TimelineSchema.shape);
+ for (const [key] of DECLARED) expect(shape, `mirror is missing ${key}`).toContain(key);
+ });
+
+ it('declares them all OPTIONAL — a bare `{ type: "timeline" }` still parses', () => {
+ // Requiredness is the half the zod-mirror-parity ratchet compares against
+ // `../data-display.ts`, where all eight are `?:`. A mirror that required one
+ // would reject every timeline already published — including the three
+ // fixtures in `examples/schema-catalog/src/schemas/plugin-timeline/`.
+ const result = TimelineSchema.safeParse(MINIMAL);
+ expect(result.success ? null : result.error.issues).toBe(null);
+ });
+
+ it('materialises NO defaults — an omitted key stays absent after parse', () => {
+ // `variant` and `dateFormat` default IN THE RENDERER, by destructuring
+ // (`variant = 'vertical'`). A `.default()` here would arrive downstream as
+ // an explicit author choice; the two spellings are not interchangeable.
+ const result = TimelineSchema.safeParse(MINIMAL);
+ expect(result.success).toBe(true);
+ if (!result.success) return;
+ for (const [key] of DECLARED) expect(key in result.data, `${key} must stay absent`).toBe(false);
+ });
+
+ it('refuses a wrong-typed value on each declared key (declared-key validation under passthrough)', () => {
+ for (const [key, bad] of DECLARED) {
+ const result = TimelineSchema.safeParse({ ...MINIMAL, [key]: bad });
+ expect(result.success, `${key} accepted ${JSON.stringify(bad)}`).toBe(false);
+ if (result.success) continue;
+ const issue = result.error.issues.find((i) => i.path[0] === key);
+ expect(issue, `${key} failed, but not on the ${key} path`).toBeTruthy();
+ }
+ });
+
+ it('accepts a well-typed value on each declared key', () => {
+ // Counter-probe for the assertion above: it must be the VALUE being refused,
+ // not the key. A pin that only ever sees red proves nothing.
+ const good = {
+ ...MINIMAL,
+ variant: 'gantt',
+ items: [{ label: 'Backend', items: [{ title: 'API', startDate: '2024-01-01', endDate: '2024-01-31' }] }],
+ dateFormat: 'iso',
+ scale: 'quarter',
+ timeScale: 'month',
+ rowLabel: 'Projects',
+ minDate: '2024-01-01',
+ maxDate: '2024-12-31',
+ };
+ const result = TimelineSchema.safeParse(good);
+ expect(result.success ? null : result.error.issues).toBe(null);
+ });
+
+ it('does NOT reject an undeclared key — objectui#5155’s ceiling, measured not assumed', () => {
+ // Declaring the eight bought validation of DECLARED keys, not rejection of
+ // undeclared ones: `BaseSchema` is `.passthrough()`. Anyone reading this
+ // card as "misspellings now fail" is reading it wrong, and this pin says so
+ // in the one place that cannot rot.
+ const misspelled = TimelineSchema.safeParse({ ...MINIMAL, varient: 'gantt', timescale: 'month' });
+ expect(misspelled.success).toBe(true);
+ });
+});
+
+describe('TimelineSchema — `scale` is canonical and shares the spec vocabulary', () => {
+ // The renderer resolves `scale ?? timeScale` and accepts six values on either
+ // (`resolveTimelineScale`, pinned against the spec by
+ // `plugin-timeline/src/__tests__/timeline-scale-spec-parity.test.ts`). This is
+ // the third leg of that agreement: the exported TYPE offers the same six.
+ const specScales: string[] = unwrap(TimelineConfigSchema.shape.scale).options;
+
+ it('reads a non-empty scale enum from the spec', () => {
+ expect(specScales, 'could not read TimelineConfigSchema.shape.scale options').not.toEqual([]);
+ });
+
+ it('`scale` and the deprecated `timeScale` alias accept exactly the spec vocabulary', () => {
+ for (const key of ['scale', 'timeScale'] as const) {
+ for (const value of specScales) {
+ const result = TimelineSchema.safeParse({ ...MINIMAL, [key]: value });
+ expect(result.success, `${key} refused spec scale '${value}'`).toBe(true);
+ }
+ }
+ });
+
+ it('the registry `inputs` three-value timeScale enum is NOT the contract', () => {
+ // Before this card the designer offered `timeScale: day | week | month` and
+ // the type offered neither key. `hour` / `quarter` / `year` were authorable,
+ // rendered correctly, and were undiscoverable from both surfaces.
+ for (const value of ['hour', 'quarter', 'year']) {
+ expect(TimelineSchema.safeParse({ ...MINIMAL, scale: value }).success, value).toBe(true);
+ }
+ });
+});
+
+describe('TimelineSchema — the three unread keys are still declared (ADR-0049 route pending)', () => {
+ it('`events` / `orientation` / `position` remain in the mirror', () => {
+ const shape = Object.keys(TimelineSchema.shape);
+ for (const key of ['events', 'orientation', 'position']) {
+ expect(shape, `${key} left the mirror — see the ADR-0049 note in data-display.ts`).toContain(key);
+ }
+ });
+
+ it('`events` is OPTIONAL — this is the widening objectui#6170 landed', () => {
+ // It was required. That is why the docs page's own TypeScript example did
+ // not compile, and it is the single non-additive change in this card.
+ expect(TimelineSchema.safeParse({ type: 'timeline', items: [] }).success).toBe(true);
+ });
+
+ it('still validates them when authored, so retirement is a visible edit', () => {
+ expect(TimelineSchema.safeParse({ ...MINIMAL, orientation: 'diagonal' }).success).toBe(false);
+ expect(TimelineSchema.safeParse({ ...MINIMAL, position: 'centre' }).success).toBe(false);
+ expect(TimelineSchema.safeParse({ ...MINIMAL, events: 'nope' }).success).toBe(false);
+ });
+});
+
+describe('TimelineSchema (TS) — compile-time pin on the same keys', () => {
+ it('accepts the docs page’s own TypeScript example', () => {
+ // `content/docs/plugins/plugin-timeline.mdx` — the "TypeScript Support"
+ // block, verbatim. Before objectui#6170 this exact object was
+ // `TS2741: Property 'events' is missing … but required in type
+ // 'TimelineSchema'`. The page taught an authoring form its own published
+ // type refused.
+ const timelineSchema: TimelineSchemaTS = {
+ type: 'timeline',
+ variant: 'vertical',
+ items: [
+ { time: '2024-01-15', title: 'Event', description: 'Description', variant: 'success' },
+ ],
+ };
+ expect(timelineSchema.items).toHaveLength(1);
+ });
+
+ it('refuses a wrong-typed value on every declared key', () => {
+ // Each directive below fails the build (TS2578, "unused '@ts-expect-error'")
+ // the moment its key stops being declared, because the member then resolves
+ // to `any` through `BaseSchema`'s index signature and the assignment starts
+ // succeeding. That failure is the signal this card exists to create.
+
+ // @ts-expect-error — `variant` is declared `'vertical' | 'horizontal' | 'gantt' | undefined`.
+ const variant: TimelineSchemaTS['variant'] = 'diagonal';
+ // @ts-expect-error — `dateFormat` is declared `'short' | 'long' | 'iso' | undefined`.
+ const dateFormat: TimelineSchemaTS['dateFormat'] = 'medieval';
+ // @ts-expect-error — `scale` is declared `TimelineScale | undefined`.
+ const scale: TimelineSchemaTS['scale'] = 'fortnight';
+ // @ts-expect-error — `timeScale` is the deprecated alias, same six values.
+ const timeScale: TimelineSchemaTS['timeScale'] = 'fortnight';
+ // @ts-expect-error — `rowLabel` is declared `string | undefined`.
+ const rowLabel: TimelineSchemaTS['rowLabel'] = 5;
+ // @ts-expect-error — `minDate` is declared `string | undefined` (schemas are JSON).
+ const minDate: TimelineSchemaTS['minDate'] = 20240101;
+ // @ts-expect-error — `maxDate` is declared `string | undefined`.
+ const maxDate: TimelineSchemaTS['maxDate'] = 20241231;
+ // @ts-expect-error — `orientation` is declared `'vertical' | 'horizontal' | undefined`.
+ const orientation: TimelineSchemaTS['orientation'] = 'diagonal';
+ // @ts-expect-error — `position` is declared `'left' | 'right' | 'alternate' | undefined`.
+ const position: TimelineSchemaTS['position'] = 'centre';
+
+ expect([
+ variant, dateFormat, scale, timeScale, rowLabel, minDate, maxDate, orientation, position,
+ ]).toHaveLength(9);
+ });
+
+ it('accepts the well-typed value on every declared key', () => {
+ // Counter-probe for the directives above: without this, a declaration
+ // narrowed to `never` would satisfy all nine of them.
+ const ok: TimelineSchemaTS = {
+ type: 'timeline',
+ variant: 'gantt',
+ items: [{ label: 'Backend', items: [{ title: 'API', startDate: '2024-01-01', endDate: '2024-01-31' }] }],
+ dateFormat: 'iso',
+ scale: 'quarter',
+ timeScale: 'month',
+ rowLabel: 'Projects',
+ minDate: '2024-01-01',
+ maxDate: '2024-12-31',
+ orientation: 'vertical',
+ position: 'left',
+ };
+ expect(ok.rowLabel).toBe('Projects');
+ });
+
+ it('`TimelineScale` is the one axis vocabulary, not a second spelling', () => {
+ const every: TimelineScale[] = ['hour', 'day', 'week', 'month', 'quarter', 'year'];
+ // @ts-expect-error — the type is closed; a seventh bucket is not authorable.
+ const extra: TimelineScale = 'fortnight';
+ expect([...every, extra]).toHaveLength(7);
+ });
+});
diff --git a/packages/types/src/data-display.ts b/packages/types/src/data-display.ts
index 939176ba14..44a3416da7 100644
--- a/packages/types/src/data-display.ts
+++ b/packages/types/src/data-display.ts
@@ -1199,21 +1199,151 @@ export interface TimelineEvent {
}
/**
- * Timeline component
+ * The axis-bucket vocabulary for the `gantt` variant.
+ *
+ * One spelling, one source: these are exactly the six values of
+ * `@objectstack/spec` `ui/TimelineConfig.json#scale`, and exactly the six
+ * `TIMELINE_SCALES` that `packages/plugin-timeline/src/renderer.tsx`
+ * (`resolveTimelineScale`) accepts. Declared here so the two agree by
+ * construction rather than by coincidence.
+ */
+export type TimelineScale = 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
+
+/**
+ * Timeline component (`type: 'timeline'`).
+ *
+ * ## The members below are the set `TimelineRenderer` actually reads
+ *
+ * objectui#6170, maintainer ruling 2026-08-25 (「同意」), the same family rule
+ * adopted on objectui#6172: **the exported type aligns to the measured
+ * authored + read set.** Before that ruling this interface declared `events` /
+ * `orientation` / `position` and nothing else, and the divergence was
+ * invisible to `tsc` because {@link BaseSchema} carries `[key: string]: any` —
+ * every key the renderer reads resolved as `any`, so `schema: TimelineSchema`
+ * constrained nothing. (The index signature itself is objectui#5155 /
+ * objectui#6269, deliberately not touched here.)
+ *
+ * Measured on `origin/main` @ `79ebf30d1`: `TimelineRenderer`
+ * (`plugin-timeline/src/renderer.tsx:250`) reads NINE keys off this node —
+ * `variant`, `items`, `dateFormat`, `onItemClick`, `minDate`, `maxDate`,
+ * `rowLabel`, `scale`, `timeScale` — and NONE of `events` / `orientation` /
+ * `position`. EIGHT of those nine are declared below; `onItemClick` is not, on
+ * purpose — it is a runtime slot `ObjectTimeline` installs when it composes
+ * this schema, not authorable metadata, and this package's convention keeps
+ * callback-shaped keys off the authored surface (see `RuntimeOnlyDeclared` in
+ * `__tests__/zod-mirror-parity.test.ts`). The registration's own `inputs` metadata and
+ * `content/docs/plugins/plugin-timeline.mdx`'s property table agreed with the
+ * renderer all along; only this type disagreed — including with the docs
+ * page's own TypeScript example, which did not compile, because `events` was
+ * required and nothing writes it.
+ *
+ * ## Two timelines, and this is the presentational one
+ *
+ * `TimelineSchema` describes HOW TO DRAW a rail from items already in hand.
+ * The OBJECT-BOUND config — WHICH RECORD FIELDS to project — is
+ * `@objectstack/spec`'s `TimelineConfig`, surfaced here as
+ * {@link ListViewTimelineConfig} (`startDateField` / `titleField` / …) and
+ * consumed by `ObjectTimeline`, which resolves those field names against
+ * fetched records and composes the presentational shape below before handing
+ * it to `TimelineRenderer`. The two vocabularies are disjoint by design; only
+ * `scale` is common to both, deliberately spelled the same in each.
*/
export interface TimelineSchema extends BaseSchema {
type: 'timeline';
/**
- * Timeline events
+ * Layout variant. The renderer implements exactly these three and returns
+ * `null` for anything else.
+ * @default 'vertical'
+ */
+ variant?: 'vertical' | 'horizontal' | 'gantt';
+ /**
+ * The rows to draw.
+ *
+ * TWO element shapes, discriminated by `variant`, both read dynamically by
+ * the renderer (`items.map((item: any) => …)`), so the element type is left
+ * open rather than narrowed to either one:
+ *
+ * - `vertical` / `horizontal` — a feed item:
+ * `{ time, title, description?, variant?, icon?, color?, content?, className?, meta?, group? }`
+ * - `gantt` — a row:
+ * `{ label, items: [{ title, startDate, endDate, variant? }] }`
+ *
+ * `content/docs/plugins/plugin-timeline.mdx` carries both in full.
+ */
+ items?: any[];
+ /**
+ * How item dates are rendered.
+ * @default 'short'
+ */
+ dateFormat?: 'short' | 'long' | 'iso';
+ /**
+ * Gantt axis bucket size. **Canonical spelling** — it is `@objectstack/spec`
+ * `ui/TimelineConfig.json`'s axis key AND the renderer's preferred read
+ * (`resolveTimelineScale` resolves `scale ?? timeScale`).
+ * @default 'month'
*/
- events: TimelineEvent[];
+ scale?: TimelineScale;
/**
- * Timeline orientation
+ * Gantt axis bucket size — this renderer's pre-spec dialect, still read as a
+ * fallback so stored JSON keeps working.
+ *
+ * @deprecated Use {@link TimelineSchema.scale}, which `@objectstack/spec`
+ * owns and this renderer prefers. Retiring the alias is routed separately
+ * (objectui#6170 maintainer ruling 2026-08-25: "`timeScale` goes the
+ * alias-retirement route, not a silent second spelling").
+ */
+ timeScale?: TimelineScale;
+ /**
+ * Header label above the Gantt row-label gutter.
+ * @default 'Items'
+ */
+ rowLabel?: string;
+ /**
+ * Override the auto-calculated Gantt axis start (`YYYY-MM-DD`).
+ */
+ minDate?: string;
+ /**
+ * Override the auto-calculated Gantt axis end (`YYYY-MM-DD`).
+ */
+ maxDate?: string;
+ /**
+ * Timeline events.
+ *
+ * ⚠️ ZERO read points — `packages/plugin-timeline` never reads this key, so a
+ * timeline authored with `events` renders an EMPTY rail. It was `required`
+ * until objectui#6170, which is why the docs page's own TypeScript example
+ * did not compile; it is OPTIONAL now so that documented authoring form
+ * type-checks, and that widening is the whole of the change made here.
+ *
+ * Its RETIREMENT is routed, not done: objectui#6170's maintainer ruling
+ * (2026-08-25) sends this key, {@link TimelineSchema.orientation} and
+ * {@link TimelineSchema.position} down the ADR-0049 enforce-or-remove route.
+ * That is a breaking removal from a published type and therefore its own
+ * change; the house form for it is the `?: never` tombstone convention on
+ * {@link StaticTableColumn} above (objectui#5474).
+ *
+ * @deprecated Never read by any renderer. Use `items` — see
+ * `content/docs/plugins/plugin-timeline.mdx`.
+ */
+ events?: TimelineEvent[];
+ /**
+ * Timeline orientation.
+ *
+ * ⚠️ ZERO read points — the renderer discriminates on
+ * {@link TimelineSchema.variant}, not on this key. Retirement routed via
+ * ADR-0049; see {@link TimelineSchema.events}.
+ *
+ * @deprecated Never read by any renderer. Use `variant`.
* @default 'vertical'
*/
orientation?: 'vertical' | 'horizontal';
/**
- * Timeline position (for vertical)
+ * Timeline position (for vertical).
+ *
+ * ⚠️ ZERO read points. Retirement routed via ADR-0049; see
+ * {@link TimelineSchema.events}.
+ *
+ * @deprecated Never read by any renderer.
* @default 'left'
*/
position?: 'left' | 'right' | 'alternate';
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index 819c46cbc6..fb7687d4a9 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -200,6 +200,7 @@ export type {
PivotTableSchema,
DrillDownConfig,
TimelineEvent,
+ TimelineScale,
TimelineSchema,
KbdSchema,
HtmlSchema,
diff --git a/packages/types/src/zod/data-display.zod.ts b/packages/types/src/zod/data-display.zod.ts
index 96b4847182..d8c289287d 100644
--- a/packages/types/src/zod/data-display.zod.ts
+++ b/packages/types/src/zod/data-display.zod.ts
@@ -298,14 +298,45 @@ export const TimelineEventSchema = z.object({
content: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Custom content'),
});
+/**
+ * Timeline scale — the six values of `@objectstack/spec`
+ * `ui/TimelineConfig.json#scale`, which are also the six `TIMELINE_SCALES` the
+ * gantt renderer accepts. Mirrors `TimelineScale` in `../data-display.ts`.
+ *
+ * Deliberately NOT exported: every exported const in this directory has to be
+ * registered in `zod-mirror-parity.test.ts`'s `MIRRORS` or `EXCLUSIONS`, and a
+ * shared inline enum is not a mirror of any declaration — it is spelling reuse
+ * between the two keys below.
+ */
+const TimelineScaleSchema = z.enum(['hour', 'day', 'week', 'month', 'quarter', 'year']);
+
/**
* Timeline Schema - Timeline component
+ *
+ * Mirrors `TimelineSchema` in `../data-display.ts`, which objectui#6170 aligned
+ * to the key set `TimelineRenderer` actually reads (maintainer ruling
+ * 2026-08-25). `zod-mirror-parity.test.ts` pins the two together: a key
+ * declared there and absent here is `UnmirroredDeclaredKeys` and reddens the
+ * pair, so the nine presentational keys below are not optional to carry.
+ *
+ * `events` / `orientation` / `position` stay declared and stay mirrored: they
+ * have zero read points, but removing them is a breaking narrowing routed
+ * through ADR-0049 rather than done here. `events` follows the declaration from
+ * required to OPTIONAL — strictly more input parses than before.
*/
export const TimelineSchema = BaseSchema.extend({
type: z.literal('timeline'),
- events: z.array(TimelineEventSchema).describe('Timeline events'),
- orientation: z.enum(['vertical', 'horizontal']).optional().describe('Timeline orientation'),
- position: z.enum(['left', 'right', 'alternate']).optional().describe('Event position'),
+ variant: z.enum(['vertical', 'horizontal', 'gantt']).optional().describe('Layout variant'),
+ items: z.array(z.any()).optional().describe('Rows to draw — feed items, or gantt rows when variant is gantt'),
+ dateFormat: z.enum(['short', 'long', 'iso']).optional().describe('How item dates are rendered'),
+ scale: TimelineScaleSchema.optional().describe('Gantt axis bucket size (canonical spelling — the spec key)'),
+ timeScale: TimelineScaleSchema.optional().describe('DEPRECATED pre-spec alias for scale; still read as a fallback'),
+ rowLabel: z.string().optional().describe('Header label above the gantt row-label gutter'),
+ minDate: z.string().optional().describe('Override the auto-calculated gantt axis start (YYYY-MM-DD)'),
+ maxDate: z.string().optional().describe('Override the auto-calculated gantt axis end (YYYY-MM-DD)'),
+ events: z.array(TimelineEventSchema).optional().describe('DEPRECATED — zero read points; renders an empty rail. Use items'),
+ orientation: z.enum(['vertical', 'horizontal']).optional().describe('DEPRECATED — zero read points. Use variant'),
+ position: z.enum(['left', 'right', 'alternate']).optional().describe('DEPRECATED — zero read points'),
});
/**