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'), }); /**