From 97317087702910f326de30d70fbc57990bd66ad3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 12:39:15 +0000 Subject: [PATCH 1/7] feat(spec): calendar config gains an optional initialDate anchor CalendarConfigSchema declares initialDate, a closed set of relative anchors (today, first_record, next_record) for the date a calendar opens on. Absent means today, so every stored calendar keeps its behaviour; no spec-side default is materialized. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- packages/spec/liveness/view.json | 2 +- .../ui/calendar-config-initial-date.test.ts | 112 ++++++++++++++++++ packages/spec/src/ui/component.zod.ts | 2 +- packages/spec/src/ui/view.zod.ts | 31 +++++ 4 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 packages/spec/src/ui/calendar-config-initial-date.test.ts diff --git a/packages/spec/liveness/view.json b/packages/spec/liveness/view.json index f5f02e9073d..af717584c5e 100644 --- a/packages/spec/liveness/view.json +++ b/packages/spec/liveness/view.json @@ -105,7 +105,7 @@ }, "calendar": { "status": "live", - "note": "objectui: calendar-view-renderer.tsx:147 (audit L16). Read points of the `calendar` view-binding row in packages/spec/src/kernel/functional-completeness.ts (moved out of spec source, #23434), re-read at objectui @b9814b334 (2026-10-14): objectui @b9814b334 packages/plugin-list/src/ListView.tsx#ListView (its `case 'calendar'` arm restates only the bindings the view declared; the `startDateField || 'start_date'` / `endDateField || 'end_date'` floors were deleted by objectui#7029); objectui @b9814b334 packages/plugin-calendar/src/ObjectCalendar.tsx#getCalendarConfig returns `null` with neither a `calendar` block nor a flat `startDateField`, and the `if (!calendarConfig)` arm in objectui @b9814b334 packages/plugin-calendar/src/ObjectCalendar.tsx#ObjectCalendar renders the localised `calendar.configRequired` refusal naming `startDateField` (objectui#10101; `titleField` is not required, objectui#8170)." + "note": "objectui: calendar-view-renderer.tsx:147 (audit L16). One key rides this blanket verdict at PLANNED strength, as `viewMode` does on the gantt row: `initialDate` (the date the calendar opens on, a closed set today / first_record / next_record, absent meaning today) is declared ahead of its reader — at this repo's `ui/` tree on 549203d040, ui/packages/plugin-calendar/src/ObjectCalendar.tsx#ObjectCalendar still seeds `currentDate` with `useState(new Date())` and reads no anchor. Amend this note to plain live, citing that read, when the console half lands. Read points of the `calendar` view-binding row in packages/spec/src/kernel/functional-completeness.ts (moved out of spec source, #23434), re-read at objectui @b9814b334 (2026-10-14): objectui @b9814b334 packages/plugin-list/src/ListView.tsx#ListView (its `case 'calendar'` arm restates only the bindings the view declared; the `startDateField || 'start_date'` / `endDateField || 'end_date'` floors were deleted by objectui#7029); objectui @b9814b334 packages/plugin-calendar/src/ObjectCalendar.tsx#getCalendarConfig returns `null` with neither a `calendar` block nor a flat `startDateField`, and the `if (!calendarConfig)` arm in objectui @b9814b334 packages/plugin-calendar/src/ObjectCalendar.tsx#ObjectCalendar renders the localised `calendar.configRequired` refusal naming `startDateField` (objectui#10101; `titleField` is not required, objectui#8170)." }, "gantt": { "status": "live", diff --git a/packages/spec/src/ui/calendar-config-initial-date.test.ts b/packages/spec/src/ui/calendar-config-initial-date.test.ts new file mode 100644 index 00000000000..9578857912f --- /dev/null +++ b/packages/spec/src/ui/calendar-config-initial-date.test.ts @@ -0,0 +1,112 @@ +import { describe, it, expect } from 'vitest'; + +import { CalendarConfigSchema, ListViewSchema } from './view.zod'; +import { ComponentPropsMap } from './component.zod'; +import { zodIssuesToFields } from '../api/zod-issues-to-fields'; +import { ErrorHttpStatusMap } from '../api/errors.zod'; + +/** + * `calendar.initialDate` — the date a calendar opens on. + * + * Without it every calendar opened on the current month, so one whose records + * all fall months ahead opened on an empty grid. The key is OPTIONAL and has no + * spec-side default: absence means today, which is what every calendar stored + * before the key did, so the parsed output of an existing view is unchanged. + * + * Pinned here: the three accepted values on both doors that carry the block + * (the list view's `calendar` and the `object-calendar` node's `calendar`, + * which is the same member by reference), the absence behaviour, and the + * refusal of a value outside the closed set — a fixed calendar date included, + * which the set leaves out on purpose. + */ + +const ACCEPTED = ['today', 'first_record', 'next_record'] as const; + +const binding = { startDateField: 'renewal_date', titleField: 'name' } as const; + +/** A list view that parses clean apart from whatever the case under test adds. */ +const calendarView = (calendar: Record) => ({ + type: 'calendar' as const, + name: 'renewals', + columns: ['name'], + calendar, +}); + +const objectCalendar = ComponentPropsMap['object-calendar']; + +describe('calendar.initialDate — accepted values', () => { + it.each(ACCEPTED)('%s parses clean on CalendarConfigSchema and keeps its value', (value) => { + const r = CalendarConfigSchema.safeParse({ ...binding, initialDate: value }); + expect(r.success).toBe(true); + expect(r.data?.initialDate).toBe(value); + }); + + it.each(ACCEPTED)('%s parses clean on a whole stored list view', (value) => { + const r = ListViewSchema.safeParse(calendarView({ ...binding, initialDate: value })); + expect(r.success, JSON.stringify(r.error?.issues)).toBe(true); + expect(r.data?.calendar?.initialDate).toBe(value); + }); + + it.each(ACCEPTED)('%s parses clean on the `object-calendar` node\'s `calendar` block', (value) => { + const r = objectCalendar.safeParse({ objectName: 'contract', calendar: { ...binding, initialDate: value } }); + expect(r.success, JSON.stringify(r.error?.issues)).toBe(true); + }); +}); + +describe('calendar.initialDate — absent', () => { + it('an omitted key stays omitted: no default is materialized onto the parsed block', () => { + const r = CalendarConfigSchema.safeParse({ ...binding }); + expect(r.success).toBe(true); + expect(r.data).not.toHaveProperty('initialDate'); + // The parse of a calendar stored before the key existed is byte-identical. + expect(r.data).toEqual(binding); + }); + + it('a stored list view without the key parses to exactly the block it stored', () => { + const r = ListViewSchema.safeParse(calendarView({ ...binding })); + expect(r.success).toBe(true); + expect(r.data?.calendar).toEqual(binding); + }); +}); + +describe('calendar.initialDate — refused values', () => { + it.each([ + ['a fixed ISO calendar date', '2026-11-01'], + ['a fixed ISO timestamp', '2026-11-01T00:00:00Z'], + ['a near-miss spelling', 'first'], + ['the camelCase spelling of a member', 'firstRecord'], + ['an upper-cased member', 'TODAY'], + ['a date-macro token', '{today}'], + ])('%s (%s) is refused with invalid_value at calendar.initialDate', (_label, value) => { + const view = calendarView({ ...binding, initialDate: value }); + const r = ListViewSchema.safeParse(view); + expect(r.success).toBe(false); + const issues = r.error!.issues; + expect(issues).toHaveLength(1); + const [issue] = issues; + expect(issue.code).toBe('invalid_value'); + expect(issue.path).toEqual(['calendar', 'initialDate']); + // The message names every accepted value, so the author's next move is a member. + for (const member of ACCEPTED) expect(issue.message).toContain(`"${member}"`); + + // The wire envelope a save door answers with: the field and its catalog code, + // under the validation class's HTTP status. + const fields = zodIssuesToFields(issues, view); + expect(fields).toEqual([ + expect.objectContaining({ field: 'calendar.initialDate', code: 'invalid_option' }), + ]); + expect(ErrorHttpStatusMap.validation).toBe(400); + }); + + it('a non-string value is refused at the same path', () => { + const r = CalendarConfigSchema.safeParse({ ...binding, initialDate: new Date('2026-11-01') }); + expect(r.success).toBe(false); + expect(r.error!.issues.map((i) => i.path)).toEqual([['initialDate']]); + }); + + it('the widening did not open the block: an unknown key is still refused by name', () => { + const r = CalendarConfigSchema.safeParse({ ...binding, initialView: 'month' }); + expect(r.success).toBe(false); + expect(r.error!.issues.map((i) => i.code)).toContain('unrecognized_keys'); + }); +}); diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index e713b4f7e81..54377640d9f 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -6189,7 +6189,7 @@ export const ObjectCalendarPropsSchema = lazySchema(() => strictObject({ * `endField` aliases among them — was never read. */ calendar: ListViewSchema.shape.calendar - .describe('Calendar field config `{ startDateField, endDateField?, titleField?, colorField?, allDayField? }` — the same block a list view\'s `calendar` declares; `startDateField` is required'), + .describe('Calendar config `{ startDateField, endDateField?, titleField?, colorField?, allDayField?, initialDate? }` — the same block a list view\'s `calendar` declares; `startDateField` is required'), defaultView: z.enum(['month', 'week', 'day']).optional().describe('Initial view mode'), /** * Base query filter — the `ViewFilterRule` ARRAY form, the one filter diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 0703cc38c85..0ac0a497f18 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -2166,6 +2166,37 @@ export const CalendarConfigSchema = lazySchema(() => strictObject({ * card's own defect inverted. */ allDayField: z.string().optional().describe('Field carrying the all-day flag for each event (names a boolean field, not a value): a record whose flag is true is drawn as an all-day band rather than at a clock time, and one whose flag is absent or false is not all-day. Omit to leave the renderer inference in place — an event with no end date draws as all-day'), + /** + * The date the calendar opens on. Without it every calendar opens on the + * current month, so a calendar whose records all fall months ahead (a + * renewals calendar) opens on an empty grid and the author has no way to say + * otherwise. + * + * Why it lives on this block and not beside `defaultView` (the initial view + * MODE, an `object-calendar` component prop): a stored list view has no + * other authorable home, and two of the three values are defined in terms of + * this block's own `startDateField` binding — the anchor is a question about + * the view's records, not a widget preference. + * + * Spelling: no sibling view config (gantt, timeline, map) declares an + * initial date or range, so there is nothing to mirror; `initialDate` names + * where the calendar OPENS, and avoids the `default*` prefix, which on this + * platform also reads as "fallback value" (`defaultValue`). + * + * A CLOSED set of relative anchors. ⛔ No fixed calendar date: a date stored + * on a view goes stale as time passes — the same view opens on an ever older + * month — and no producer asking for one was found. A fixed date is refused + * with the enum's own `invalid_value` issue. + * + * ⛔ No spec-side `.default('today')`, for the reason the gantt `viewMode` + * gives: a materialized default would write the key onto every parsed + * calendar and read as an author choice. Absence means today, which is what + * every calendar stored before this key did. + * + * Lands ahead of its reader: the renderer seeds its current date from the + * clock until the console half reads this key where it seeds that date. + */ + initialDate: z.enum(['today', 'first_record', 'next_record']).optional().describe("Where the calendar opens. 'today' (also what an omitted key does) opens on the period containing today; 'first_record' on the period containing the earliest start date (the startDateField value) among the records the view selects; 'next_record' on the period containing the earliest start date on or after today among those records. When no record qualifies, the calendar opens on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: a date stored on a view goes stale as time passes"), })); /** From d41351fa44c8f79fe44edcbfb21ecd6de5e17697 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 12:45:47 +0000 Subject: [PATCH 2/7] chore(spec): regenerate the authorable surface and reference docs for calendar initialDate, add changeset Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- .changeset/28021-calendar-initial-date.md | 16 ++++++++++++++++ content/docs/references/ui/component.mdx | 3 ++- content/docs/references/ui/view.mdx | 3 +++ packages/spec/authorable-surface/ui.json | 1 + 4 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .changeset/28021-calendar-initial-date.md diff --git a/.changeset/28021-calendar-initial-date.md b/.changeset/28021-calendar-initial-date.md new file mode 100644 index 00000000000..3cfb2623601 --- /dev/null +++ b/.changeset/28021-calendar-initial-date.md @@ -0,0 +1,16 @@ +--- +'@objectstack/spec': minor +--- + +feat(spec): a calendar view can declare the date it opens on, `calendar.initialDate` + +Clause-②: yes (widening) +Metadata-protocol: added + +A calendar view used to open on the current month, always, so a calendar whose records all fall months ahead (a renewals calendar) opened on an empty grid and the author had no key to say otherwise. `CalendarConfigSchema` (the list view's `calendar` block, and the `object-calendar` node's `calendar` prop, which is the same block by reference) gains one optional key: + +- **`initialDate: 'today' | 'first_record' | 'next_record'`.** `'today'` opens on the period containing today. `'first_record'` opens on the period containing the earliest start date (the `startDateField` value) among the records the view selects. `'next_record'` opens on the period containing the earliest start date on or after today among those records. When no record qualifies, the calendar opens on today. The period is the month, week or day the active view mode shows. +- **Absent means today.** The key has no spec-side default, so a calendar stored before it existed parses to exactly the block it stored and opens where it always did. +- **A closed set.** A fixed calendar date (`'2026-11-01'`), a date-macro token (`'{today}'`) or any other spelling is refused at `calendar.initialDate` with Zod's `invalid_value`, whose message lists the three accepted values. A date stored on a view goes stale as time passes, which is why a fixed date is not among them. + +The Console renderer reads the key in a follow-up change; until then a calendar that declares it still opens on today. diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index 1a0516dda67..df91c9c8c47 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -484,7 +484,7 @@ const result = ActionButtonPropsSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **objectName** | `string` | optional | Object this calendar binds to. Optional because the component-level `dataSource` binding can supply the object instead | -| **calendar** | `{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }` | optional | Calendar field config `{ startDateField, endDateField?, titleField?, colorField?, allDayField? }` — the same block a list view's `calendar` declares; `startDateField` is required | +| **calendar** | `{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }` | optional | Calendar config `{ startDateField, endDateField?, titleField?, colorField?, allDayField?, initialDate? }` — the same block a list view's `calendar` declares; `startDateField` is required | | **defaultView** | `Enum<'month' \| 'week' \| 'day'>` | optional | Initial view mode | | **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Base query filter — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Row order for the fetched events — the SortItem array form `[{ field, order }, ...]`, the one sort orthography every declared `sort` door on this platform shares; lowered to the wire `$orderby`. The legacy string clause (`name desc`) is refused — see migration `object-block-sort-item-array` | @@ -503,6 +503,7 @@ const result = ActionButtonPropsSchema.parse(data); | **titleField** | `string` | optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) | | **colorField** | `string` | optional | Field to derive each event color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the calendar theme-aware palette color hashed from the value | | **allDayField** | `string` | optional | Field carrying the all-day flag for each event (names a boolean field, not a value): a record whose flag is true is drawn as an all-day band rather than at a clock time, and one whose flag is absent or false is not all-day. Omit to leave the renderer inference in place — an event with no end date draws as all-day | +| **initialDate** | `Enum<'today' \| 'first_record' \| 'next_record'>` | optional | Where the calendar opens. 'today' (also what an omitted key does) opens on the period containing today; 'first_record' on the period containing the earliest start date (the startDateField value) among the records the view selects; 'next_record' on the period containing the earliest start date on or after today among those records. When no record qualifies, the calendar opens on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: a date stored on a view goes stale as time passes | ### Nested Shape: `ObjectCalendarProps.filter[number]` diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 36a2f14f9f9..e2ce4a2b130 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -109,6 +109,7 @@ Appearance and visualization configuration | **titleField** | `string` | optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) | | **colorField** | `string` | optional | Field to derive each event color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the calendar theme-aware palette color hashed from the value | | **allDayField** | `string` | optional | Field carrying the all-day flag for each event (names a boolean field, not a value): a record whose flag is true is drawn as an all-day band rather than at a clock time, and one whose flag is absent or false is not all-day. Omit to leave the renderer inference in place — an event with no end date draws as all-day | +| **initialDate** | `Enum<'today' \| 'first_record' \| 'next_record'>` | optional | Where the calendar opens. 'today' (also what an omitted key does) opens on the period containing today; 'first_record' on the period containing the earliest start date (the startDateField value) among the records the view selects; 'next_record' on the period containing the earliest start date on or after today among those records. When no record qualifies, the calendar opens on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: a date stored on a view goes stale as time passes | --- @@ -915,6 +916,7 @@ View filter rule | **titleField** | `string` | optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) | | **colorField** | `string` | optional | Field to derive each event color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the calendar theme-aware palette color hashed from the value | | **allDayField** | `string` | optional | Field carrying the all-day flag for each event (names a boolean field, not a value): a record whose flag is true is drawn as an all-day band rather than at a clock time, and one whose flag is absent or false is not all-day. Omit to leave the renderer inference in place — an event with no end date draws as all-day | +| **initialDate** | `Enum<'today' \| 'first_record' \| 'next_record'>` | optional | Where the calendar opens. 'today' (also what an omitted key does) opens on the period containing today; 'first_record' on the period containing the earliest start date (the startDateField value) among the records the view selects; 'next_record' on the period containing the earliest start date on or after today among those records. When no record qualifies, the calendar opens on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: a date stored on a view goes stale as time passes | ### Nested Shape: `ListView.gantt` @@ -1284,6 +1286,7 @@ View filter rule | **titleField** | `string` | optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) | | **colorField** | `string` | optional | Field to derive each event color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the calendar theme-aware palette color hashed from the value | | **allDayField** | `string` | optional | Field carrying the all-day flag for each event (names a boolean field, not a value): a record whose flag is true is drawn as an all-day band rather than at a clock time, and one whose flag is absent or false is not all-day. Omit to leave the renderer inference in place — an event with no end date draws as all-day | +| **initialDate** | `Enum<'today' \| 'first_record' \| 'next_record'>` | optional | Where the calendar opens. 'today' (also what an omitted key does) opens on the period containing today; 'first_record' on the period containing the earliest start date (the startDateField value) among the records the view selects; 'next_record' on the period containing the earliest start date on or after today among those records. When no record qualifies, the calendar opens on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: a date stored on a view goes stale as time passes | ### Nested Shape: `ObjectListView.gantt` diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index 8653b7dcf6a..5cc35cdfa01 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -228,6 +228,7 @@ "ui/CalendarConfig:allDayField", "ui/CalendarConfig:colorField", "ui/CalendarConfig:endDateField", + "ui/CalendarConfig:initialDate", "ui/CalendarConfig:startDateField", "ui/CalendarConfig:titleField", "ui/ChartAggregate:field", From fc292749a0bb1f7f6f683e8c372676c32d4da2f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 15:12:42 +0000 Subject: [PATCH 3/7] feat(spec): calendar initialDate takes a record anchor or a date-macro token initialDate now accepts first_record / next_record, or a wrapped, known date-macro token ({today}, {next_month_start}, {3_months_from_now}), judged by the date-macro vocabulary's own predicate pair. The bare 'today' value is dropped ({today} is its one spelling; an absent key still means today). A fixed date stays refused, and the refusal names both forms and DATE_MACRO_TOKENS. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- .changeset/28021-calendar-initial-date.md | 6 +- packages/spec/liveness/view.json | 2 +- .../ui/calendar-config-initial-date.test.ts | 65 ++++++++++++++----- packages/spec/src/ui/view.zod.ts | 58 ++++++++++++++--- 4 files changed, 102 insertions(+), 29 deletions(-) diff --git a/.changeset/28021-calendar-initial-date.md b/.changeset/28021-calendar-initial-date.md index 3cfb2623601..4aaee1d2e88 100644 --- a/.changeset/28021-calendar-initial-date.md +++ b/.changeset/28021-calendar-initial-date.md @@ -9,8 +9,8 @@ Metadata-protocol: added A calendar view used to open on the current month, always, so a calendar whose records all fall months ahead (a renewals calendar) opened on an empty grid and the author had no key to say otherwise. `CalendarConfigSchema` (the list view's `calendar` block, and the `object-calendar` node's `calendar` prop, which is the same block by reference) gains one optional key: -- **`initialDate: 'today' | 'first_record' | 'next_record'`.** `'today'` opens on the period containing today. `'first_record'` opens on the period containing the earliest start date (the `startDateField` value) among the records the view selects. `'next_record'` opens on the period containing the earliest start date on or after today among those records. When no record qualifies, the calendar opens on today. The period is the month, week or day the active view mode shows. -- **Absent means today.** The key has no spec-side default, so a calendar stored before it existed parses to exactly the block it stored and opens where it always did. -- **A closed set.** A fixed calendar date (`'2026-11-01'`), a date-macro token (`'{today}'`) or any other spelling is refused at `calendar.initialDate` with Zod's `invalid_value`, whose message lists the three accepted values. A date stored on a view goes stale as time passes, which is why a fixed date is not among them. +- **`initialDate`, one of two forms.** A record anchor: `'first_record'` opens on the period containing the earliest start date (the `startDateField` value) among the records the view selects, and `'next_record'` on the period containing the earliest start date on or after today among those records; when no record qualifies, the calendar opens on today. Or a wrapped date-macro token, the same formula vocabulary filter values and a dashboard date filter's `defaultValue` already accept (`DATE_MACRO_TOKENS` and the parameterised `{N_unit_ago}` / `{N_unit_from_now}` grammar): `'{today}'`, `'{next_month_start}'`, `'{current_quarter_start}'`, `'{3_months_from_now}'`, resolved each time the calendar opens. The period is the month, week or day the active view mode shows. +- **Absent means today.** The key has no spec-side default, so a calendar stored before it existed parses to exactly the block it stored and opens where it always did. There is no bare `'today'`: `'{today}'` is its one spelling. +- **What is refused.** A fixed calendar date (`'2026-11-01'`) or timestamp, an unwrapped token (`'today'`), an unknown token (`'{next_moon}'`) and any other spelling are refused at `calendar.initialDate`. The message names both forms and points to `DATE_MACRO_TOKENS`, and a bare known token is told the wrapped spelling to write. A fixed date goes stale as time passes; a macro does not. The Console renderer reads the key in a follow-up change; until then a calendar that declares it still opens on today. diff --git a/packages/spec/liveness/view.json b/packages/spec/liveness/view.json index af717584c5e..f447ae33b16 100644 --- a/packages/spec/liveness/view.json +++ b/packages/spec/liveness/view.json @@ -105,7 +105,7 @@ }, "calendar": { "status": "live", - "note": "objectui: calendar-view-renderer.tsx:147 (audit L16). One key rides this blanket verdict at PLANNED strength, as `viewMode` does on the gantt row: `initialDate` (the date the calendar opens on, a closed set today / first_record / next_record, absent meaning today) is declared ahead of its reader — at this repo's `ui/` tree on 549203d040, ui/packages/plugin-calendar/src/ObjectCalendar.tsx#ObjectCalendar still seeds `currentDate` with `useState(new Date())` and reads no anchor. Amend this note to plain live, citing that read, when the console half lands. Read points of the `calendar` view-binding row in packages/spec/src/kernel/functional-completeness.ts (moved out of spec source, #23434), re-read at objectui @b9814b334 (2026-10-14): objectui @b9814b334 packages/plugin-list/src/ListView.tsx#ListView (its `case 'calendar'` arm restates only the bindings the view declared; the `startDateField || 'start_date'` / `endDateField || 'end_date'` floors were deleted by objectui#7029); objectui @b9814b334 packages/plugin-calendar/src/ObjectCalendar.tsx#getCalendarConfig returns `null` with neither a `calendar` block nor a flat `startDateField`, and the `if (!calendarConfig)` arm in objectui @b9814b334 packages/plugin-calendar/src/ObjectCalendar.tsx#ObjectCalendar renders the localised `calendar.configRequired` refusal naming `startDateField` (objectui#10101; `titleField` is not required, objectui#8170)." + "note": "objectui: calendar-view-renderer.tsx:147 (audit L16). One key rides this blanket verdict at PLANNED strength, as `viewMode` does on the gantt row: `initialDate` (the date the calendar opens on: a record anchor first_record / next_record, or a wrapped date-macro token such as {today} or {next_month_start} resolved when the calendar opens; absent meaning today) is declared ahead of its reader — at this repo's `ui/` tree on 549203d040, ui/packages/plugin-calendar/src/ObjectCalendar.tsx#ObjectCalendar still seeds `currentDate` with `useState(new Date())` and reads no anchor. Amend this note to plain live, citing that read, when the console half lands. Read points of the `calendar` view-binding row in packages/spec/src/kernel/functional-completeness.ts (moved out of spec source, #23434), re-read at objectui @b9814b334 (2026-10-14): objectui @b9814b334 packages/plugin-list/src/ListView.tsx#ListView (its `case 'calendar'` arm restates only the bindings the view declared; the `startDateField || 'start_date'` / `endDateField || 'end_date'` floors were deleted by objectui#7029); objectui @b9814b334 packages/plugin-calendar/src/ObjectCalendar.tsx#getCalendarConfig returns `null` with neither a `calendar` block nor a flat `startDateField`, and the `if (!calendarConfig)` arm in objectui @b9814b334 packages/plugin-calendar/src/ObjectCalendar.tsx#ObjectCalendar renders the localised `calendar.configRequired` refusal naming `startDateField` (objectui#10101; `titleField` is not required, objectui#8170)." }, "gantt": { "status": "live", diff --git a/packages/spec/src/ui/calendar-config-initial-date.test.ts b/packages/spec/src/ui/calendar-config-initial-date.test.ts index 9578857912f..6878d24200c 100644 --- a/packages/spec/src/ui/calendar-config-initial-date.test.ts +++ b/packages/spec/src/ui/calendar-config-initial-date.test.ts @@ -13,14 +13,27 @@ import { ErrorHttpStatusMap } from '../api/errors.zod'; * spec-side default: absence means today, which is what every calendar stored * before the key did, so the parsed output of an existing view is unchanged. * - * Pinned here: the three accepted values on both doors that carry the block - * (the list view's `calendar` and the `object-calendar` node's `calendar`, - * which is the same member by reference), the absence behaviour, and the - * refusal of a value outside the closed set — a fixed calendar date included, - * which the set leaves out on purpose. + * Two forms are accepted: a record anchor (`first_record`, `next_record`), or a + * wrapped, KNOWN date-macro token — the formula vocabulary of + * `date-macros.zod.ts`, fixed, period and parameterised tokens alike, so a + * stored value is resolved when the calendar opens and never goes stale. + * + * Pinned here: the accepted values on both doors that carry the block (the + * list view's `calendar` and the `object-calendar` node's `calendar`, which is + * the same member by reference), the absence behaviour, and the refusal of + * everything else — a fixed calendar date included, which is left out on + * purpose because it goes stale. */ -const ACCEPTED = ['today', 'first_record', 'next_record'] as const; +const ACCEPTED = [ + 'first_record', + 'next_record', + '{today}', + '{next_month_start}', + '{current_quarter_start}', + '{3_months_from_now}', + '{2_weeks_ago}', +] as const; const binding = { startDateField: 'renewal_date', titleField: 'name' } as const; @@ -73,31 +86,53 @@ describe('calendar.initialDate — refused values', () => { it.each([ ['a fixed ISO calendar date', '2026-11-01'], ['a fixed ISO timestamp', '2026-11-01T00:00:00Z'], - ['a near-miss spelling', 'first'], - ['the camelCase spelling of a member', 'firstRecord'], - ['an upper-cased member', 'TODAY'], - ['a date-macro token', '{today}'], - ])('%s (%s) is refused with invalid_value at calendar.initialDate', (_label, value) => { + ['the bare `today` (its one spelling is `{today}`)', 'today'], + ['a bare known period token', 'next_month_start'], + ['an unknown wrapped token', '{next_moon}'], + ['a malformed parameterised token', '{3_fortnights_from_now}'], + ['a near-miss anchor spelling', 'first'], + ['the camelCase spelling of an anchor', 'firstRecord'], + ['an upper-cased anchor', 'NEXT_RECORD'], + ])('%s (%s) is refused at calendar.initialDate, naming both forms', (_label, value) => { const view = calendarView({ ...binding, initialDate: value }); const r = ListViewSchema.safeParse(view); expect(r.success).toBe(false); const issues = r.error!.issues; expect(issues).toHaveLength(1); const [issue] = issues; - expect(issue.code).toBe('invalid_value'); + expect(issue.code).toBe('custom'); expect(issue.path).toEqual(['calendar', 'initialDate']); - // The message names every accepted value, so the author's next move is a member. - for (const member of ACCEPTED) expect(issue.message).toContain(`"${member}"`); + // The message names the value, both accepted forms and the vocabulary to read. + expect(issue.message.startsWith(`${JSON.stringify(value)} is not a calendar \`initialDate\`.`)).toBe(true); + expect(issue.message).toContain('`first_record` or `next_record`'); + expect(issue.message).toContain('`{today}`'); + expect(issue.message).toContain('`DATE_MACRO_TOKENS`'); // The wire envelope a save door answers with: the field and its catalog code, // under the validation class's HTTP status. const fields = zodIssuesToFields(issues, view); expect(fields).toEqual([ - expect.objectContaining({ field: 'calendar.initialDate', code: 'invalid_option' }), + expect.objectContaining({ field: 'calendar.initialDate', code: 'invalid_value' }), ]); expect(ErrorHttpStatusMap.validation).toBe(400); }); + it.each([ + ['today', '{today}'], + ['next_month_start', '{next_month_start}'], + ['3_months_from_now', '{3_months_from_now}'], + ])('a bare known token (%s) is told the wrapped spelling to write, %s', (bare, wrapped) => { + const r = CalendarConfigSchema.safeParse({ ...binding, initialDate: bare }); + expect(r.success).toBe(false); + expect(r.error!.issues[0].message).toContain(`write \`${wrapped}\``); + }); + + it('a value that is no token at all gets no wrapped-spelling hint', () => { + const r = CalendarConfigSchema.safeParse({ ...binding, initialDate: '2026-11-01' }); + expect(r.success).toBe(false); + expect(r.error!.issues[0].message).not.toContain('without its braces'); + }); + it('a non-string value is refused at the same path', () => { const r = CalendarConfigSchema.safeParse({ ...binding, initialDate: new Date('2026-11-01') }); expect(r.success).toBe(false); diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 0ac0a497f18..be317264504 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -71,6 +71,7 @@ import { enumWithRetiredValues, retiredKey } from '../shared/retired-key'; // docblock for the measurement and for the two routes that were not taken. import { MAX_RENDERABLE_SCALE, SCALE_UPPER_BOUND_MESSAGE } from '../shared/scale-ceiling'; import { FieldType, InlineGridColumnSchema, SelectOptionSchema } from '../data/field.zod'; +import { DATE_MACRO_WRAPPED_RE, isDateMacroToken } from '../data/date-macros.zod'; // [#19514] The text-comparand door the Filter Protocol publishes for the // case-insensitive contains operator — the discrimination `FILTER_TEXT_CASES`' // two REJECTION rows are about, and the reason text that answers them. Imported @@ -2116,6 +2117,36 @@ export const ListChartConfigSchema = lazySchema(() => strictObject({ values: z.array(z.string()).min(1).describe('Measure names — Y (at least one)'), }).describe('List chart view configuration')); +/** The record anchors `calendar.initialDate` accepts beside a date-macro token. */ +const CALENDAR_INITIAL_DATE_RECORD_ANCHORS = ['first_record', 'next_record'] as const; + +/** + * The `calendar.initialDate` value check: a record anchor, or a wrapped, KNOWN + * date-macro token (`{today}`, `{next_month_start}`, `{3_months_from_now}`). + * The token half is the date-macro vocabulary's own predicate pair, the one a + * dashboard date filter's `defaultValue` check uses — no second grammar. A bare + * known token (`today`) is refused with the wrapped spelling to write instead. + */ +function checkCalendarInitialDate(value: string, ctx: z.RefinementCtx): void { + if ((CALENDAR_INITIAL_DATE_RECORD_ANCHORS as readonly string[]).includes(value)) return; + const wrapped = value.match(DATE_MACRO_WRAPPED_RE); + if (wrapped && isDateMacroToken(wrapped[1])) return; + + const bareToken = !wrapped && isDateMacroToken(value) + ? ` \`${value}\` is a date-macro token written without its braces: write \`{${value}}\`.` + : ''; + ctx.addIssue({ + code: 'custom', + message: + `${JSON.stringify(value)} is not a calendar \`initialDate\`.${bareToken} ` + + 'Write one of two forms: a record anchor, `first_record` or `next_record`; or a wrapped ' + + 'date-macro token such as `{today}`, `{next_month_start}` or `{3_months_from_now}`, ' + + 'resolved each time the calendar opens (the full vocabulary is `DATE_MACRO_TOKENS` in ' + + '`@objectstack/spec/data`). Omit the key to open on today. A fixed date is not accepted: ' + + 'it goes stale as time passes, while a macro does not.', + }); +} + /** * Calendar Settings * @@ -2174,7 +2205,7 @@ export const CalendarConfigSchema = lazySchema(() => strictObject({ * * Why it lives on this block and not beside `defaultView` (the initial view * MODE, an `object-calendar` component prop): a stored list view has no - * other authorable home, and two of the three values are defined in terms of + * other authorable home, and the two record anchors are defined in terms of * this block's own `startDateField` binding — the anchor is a question about * the view's records, not a widget preference. * @@ -2183,20 +2214,27 @@ export const CalendarConfigSchema = lazySchema(() => strictObject({ * where the calendar OPENS, and avoids the `default*` prefix, which on this * platform also reads as "fallback value" (`defaultValue`). * - * A CLOSED set of relative anchors. ⛔ No fixed calendar date: a date stored - * on a view goes stale as time passes — the same view opens on an ever older - * month — and no producer asking for one was found. A fixed date is refused - * with the enum's own `invalid_value` issue. + * Two forms, and only two (see {@link checkCalendarInitialDate}): a record + * anchor, or a wrapped, KNOWN date-macro token — the formula vocabulary the + * platform already resolves in filter values and in a dashboard date + * filter's `defaultValue` (`DATE_MACRO_TOKENS` / `DATE_MACRO_PARAM_RE`, + * judged by the same `DATE_MACRO_WRAPPED_RE` + `isDateMacroToken` pair, so + * there is no second grammar). ⛔ No fixed calendar date: a date stored on a + * view goes stale as time passes — the same view opens on an ever older + * month — while a macro such as `{next_month_start}` is resolved each time + * the calendar opens, so it says the same thing tomorrow. ⛔ No bare + * `'today'`: `{today}` is its one spelling, and an omitted key already + * means today. * - * ⛔ No spec-side `.default('today')`, for the reason the gantt `viewMode` - * gives: a materialized default would write the key onto every parsed - * calendar and read as an author choice. Absence means today, which is what - * every calendar stored before this key did. + * ⛔ No spec-side `.default()`, for the reason the gantt `viewMode` gives: a + * materialized default would write the key onto every parsed calendar and + * read as an author choice. Absence means today, which is what every + * calendar stored before this key did. * * Lands ahead of its reader: the renderer seeds its current date from the * clock until the console half reads this key where it seeds that date. */ - initialDate: z.enum(['today', 'first_record', 'next_record']).optional().describe("Where the calendar opens. 'today' (also what an omitted key does) opens on the period containing today; 'first_record' on the period containing the earliest start date (the startDateField value) among the records the view selects; 'next_record' on the period containing the earliest start date on or after today among those records. When no record qualifies, the calendar opens on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: a date stored on a view goes stale as time passes"), + initialDate: z.string().superRefine(checkCalendarInitialDate).optional().describe("Where the calendar opens. Two forms. A record anchor: `first_record` opens on the period containing the earliest start date (the startDateField value) among the records the view selects, `next_record` on the period containing the earliest start date on or after today among those records; when no record qualifies, the calendar opens on today. Or a wrapped date-macro token, resolved each time the calendar opens: `{today}`, `{next_month_start}`, `{current_quarter_start}`, `{3_months_from_now}` (the full vocabulary is `DATE_MACRO_TOKENS` and the parameterised `DATE_MACRO_PARAM_RE` grammar in `@objectstack/spec/data`). Omit the key to open on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: it goes stale as time passes, while a macro does not"), })); /** From 562fb7159f04e2495cd2a929d32b8728e7ea5b3b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 15:22:19 +0000 Subject: [PATCH 4/7] refactor(spec): spell calendar initialDate as a pattern derived from the date-macro vocabulary A superRefine around isDateMacroToken left the published JSON Schema a bare string. The accepted set is now one regex derived at load from DATE_MACRO_TOKENS and DATE_MACRO_PARAM_RE, so json-schema carries the rule as a pattern; parity with the macro predicate pair is pinned. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- .../ui/calendar-config-initial-date.test.ts | 35 +++++++++- packages/spec/src/ui/view.zod.ts | 67 +++++++++++-------- 2 files changed, 73 insertions(+), 29 deletions(-) diff --git a/packages/spec/src/ui/calendar-config-initial-date.test.ts b/packages/spec/src/ui/calendar-config-initial-date.test.ts index 6878d24200c..802bf22f9c4 100644 --- a/packages/spec/src/ui/calendar-config-initial-date.test.ts +++ b/packages/spec/src/ui/calendar-config-initial-date.test.ts @@ -4,6 +4,7 @@ import { CalendarConfigSchema, ListViewSchema } from './view.zod'; import { ComponentPropsMap } from './component.zod'; import { zodIssuesToFields } from '../api/zod-issues-to-fields'; import { ErrorHttpStatusMap } from '../api/errors.zod'; +import { DATE_MACRO_TOKENS, DATE_MACRO_WRAPPED_RE, isDateMacroToken } from '../data/date-macros.zod'; /** * `calendar.initialDate` — the date a calendar opens on. @@ -100,7 +101,7 @@ describe('calendar.initialDate — refused values', () => { const issues = r.error!.issues; expect(issues).toHaveLength(1); const [issue] = issues; - expect(issue.code).toBe('custom'); + expect(issue.code).toBe('invalid_format'); expect(issue.path).toEqual(['calendar', 'initialDate']); // The message names the value, both accepted forms and the vocabulary to read. expect(issue.message.startsWith(`${JSON.stringify(value)} is not a calendar \`initialDate\`.`)).toBe(true); @@ -112,7 +113,7 @@ describe('calendar.initialDate — refused values', () => { // under the validation class's HTTP status. const fields = zodIssuesToFields(issues, view); expect(fields).toEqual([ - expect.objectContaining({ field: 'calendar.initialDate', code: 'invalid_value' }), + expect.objectContaining({ field: 'calendar.initialDate', code: 'invalid_format' }), ]); expect(ErrorHttpStatusMap.validation).toBe(400); }); @@ -145,3 +146,33 @@ describe('calendar.initialDate — refused values', () => { expect(r.error!.issues.map((i) => i.code)).toContain('unrecognized_keys'); }); }); + +describe('calendar.initialDate — one vocabulary with the date macros', () => { + /** The verdict the date-macro vocabulary's own predicate pair gives, plus the two anchors. */ + const vocabularyVerdict = (value: string): boolean => { + if (value === 'first_record' || value === 'next_record') return true; + const m = value.match(DATE_MACRO_WRAPPED_RE); + return !!m && isDateMacroToken(m[1]); + }; + const accepts = (value: string): boolean => + CalendarConfigSchema.safeParse({ ...binding, initialDate: value }).success; + + it('every fixed DATE_MACRO_TOKENS member is accepted wrapped, in both wrappings, and refused bare', () => { + // Control: the vocabulary is the real list, not an empty one. + expect(DATE_MACRO_TOKENS.length).toBeGreaterThan(30); + for (const token of DATE_MACRO_TOKENS) { + expect(accepts(`{${token}}`), `{${token}}`).toBe(true); + expect(accepts(`\${${token}}`), `\${${token}}`).toBe(true); + expect(accepts(token), token).toBe(false); + } + }); + + it.each([ + '{1_day_ago}', '{30_days_ago}', '{90_days_from_now}', '{1_year_ago}', '{15_minutes_ago}', '{2_hours_from_now}', + '{0_days_ago}', '{3_fortnights_from_now}', '{3_days}', '{days_ago}', '{-3_days_ago}', '{3_Days_ago}', + '{today', 'today}', '{{today}}', '{ today }', '{today}x', 'x{today}', '{}', '', '\${}', '{first_record}', + 'first_record ', ' next_record', 'first_record|{today}', + ])('%s gets the verdict the date-macro predicate pair gives', (value) => { + expect(accepts(value)).toBe(vocabularyVerdict(value)); + }); +}); diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index be317264504..537ce85ec2a 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -71,7 +71,7 @@ import { enumWithRetiredValues, retiredKey } from '../shared/retired-key'; // docblock for the measurement and for the two routes that were not taken. import { MAX_RENDERABLE_SCALE, SCALE_UPPER_BOUND_MESSAGE } from '../shared/scale-ceiling'; import { FieldType, InlineGridColumnSchema, SelectOptionSchema } from '../data/field.zod'; -import { DATE_MACRO_WRAPPED_RE, isDateMacroToken } from '../data/date-macros.zod'; +import { DATE_MACRO_PARAM_RE, DATE_MACRO_TOKENS, isDateMacroToken } from '../data/date-macros.zod'; // [#19514] The text-comparand door the Filter Protocol publishes for the // case-insensitive contains operator — the discrimination `FILTER_TEXT_CASES`' // two REJECTION rows are about, and the reason text that answers them. Imported @@ -2121,30 +2121,43 @@ export const ListChartConfigSchema = lazySchema(() => strictObject({ const CALENDAR_INITIAL_DATE_RECORD_ANCHORS = ['first_record', 'next_record'] as const; /** - * The `calendar.initialDate` value check: a record anchor, or a wrapped, KNOWN - * date-macro token (`{today}`, `{next_month_start}`, `{3_months_from_now}`). - * The token half is the date-macro vocabulary's own predicate pair, the one a - * dashboard date filter's `defaultValue` check uses — no second grammar. A bare - * known token (`today`) is refused with the wrapped spelling to write instead. - */ -function checkCalendarInitialDate(value: string, ctx: z.RefinementCtx): void { - if ((CALENDAR_INITIAL_DATE_RECORD_ANCHORS as readonly string[]).includes(value)) return; - const wrapped = value.match(DATE_MACRO_WRAPPED_RE); - if (wrapped && isDateMacroToken(wrapped[1])) return; + * The accepted set of `calendar.initialDate`, as ONE pattern: a record anchor, + * or a wrapped (`{…}`, optionally `${…}`), KNOWN date-macro token. + * + * ⛔ Not a second grammar. The token half is DERIVED at load from the + * date-macro vocabulary's own sources — every `DATE_MACRO_TOKENS` member plus + * the body of `DATE_MACRO_PARAM_RE` — and the wrapping is the one + * `DATE_MACRO_WRAPPED_RE` accepts, so a token added to the vocabulary is + * accepted here with no edit. It is spelled as a pattern rather than a + * `superRefine` around `isDateMacroToken` so the published JSON Schema carries + * the rule too (`pattern`), instead of a bare `string` that accepts what the + * parse then refuses. The parity with `DATE_MACRO_WRAPPED_RE` + + * `isDateMacroToken` is pinned in `calendar-config-initial-date.test.ts`. + */ +const CALENDAR_INITIAL_DATE_RE = new RegExp( + '^(?:' + + CALENDAR_INITIAL_DATE_RECORD_ANCHORS.join('|') + + '|\\$?\\{(?:' + + DATE_MACRO_TOKENS.join('|') + + '|' + DATE_MACRO_PARAM_RE.source.replace(/^\^/, '').replace(/\$$/, '') + + ')\\})$', +); - const bareToken = !wrapped && isDateMacroToken(value) - ? ` \`${value}\` is a date-macro token written without its braces: write \`{${value}}\`.` +/** + * The refusal text for a value outside {@link CALENDAR_INITIAL_DATE_RE}: it + * names the value, both accepted forms and the vocabulary to read, and tells a + * bare known token (`today`) the wrapped spelling to write. + */ +function calendarInitialDateRefusal(input: unknown): string { + const bareToken = typeof input === 'string' && isDateMacroToken(input) + ? ` \`${input}\` is a date-macro token written without its braces: write \`{${input}}\`.` : ''; - ctx.addIssue({ - code: 'custom', - message: - `${JSON.stringify(value)} is not a calendar \`initialDate\`.${bareToken} ` + - 'Write one of two forms: a record anchor, `first_record` or `next_record`; or a wrapped ' + - 'date-macro token such as `{today}`, `{next_month_start}` or `{3_months_from_now}`, ' + - 'resolved each time the calendar opens (the full vocabulary is `DATE_MACRO_TOKENS` in ' + - '`@objectstack/spec/data`). Omit the key to open on today. A fixed date is not accepted: ' + - 'it goes stale as time passes, while a macro does not.', - }); + return `${JSON.stringify(input)} is not a calendar \`initialDate\`.${bareToken} ` + + 'Write one of two forms: a record anchor, `first_record` or `next_record`; or a wrapped ' + + 'date-macro token such as `{today}`, `{next_month_start}` or `{3_months_from_now}`, ' + + 'resolved each time the calendar opens (the full vocabulary is `DATE_MACRO_TOKENS` in ' + + '`@objectstack/spec/data`). Omit the key to open on today. A fixed date is not accepted: ' + + 'it goes stale as time passes, while a macro does not.'; } /** @@ -2214,12 +2227,12 @@ export const CalendarConfigSchema = lazySchema(() => strictObject({ * where the calendar OPENS, and avoids the `default*` prefix, which on this * platform also reads as "fallback value" (`defaultValue`). * - * Two forms, and only two (see {@link checkCalendarInitialDate}): a record + * Two forms, and only two (see {@link CALENDAR_INITIAL_DATE_RE}): a record * anchor, or a wrapped, KNOWN date-macro token — the formula vocabulary the * platform already resolves in filter values and in a dashboard date * filter's `defaultValue` (`DATE_MACRO_TOKENS` / `DATE_MACRO_PARAM_RE`, - * judged by the same `DATE_MACRO_WRAPPED_RE` + `isDateMacroToken` pair, so - * there is no second grammar). ⛔ No fixed calendar date: a date stored on a + * from which the accepted pattern is derived, so there is no second + * grammar). ⛔ No fixed calendar date: a date stored on a * view goes stale as time passes — the same view opens on an ever older * month — while a macro such as `{next_month_start}` is resolved each time * the calendar opens, so it says the same thing tomorrow. ⛔ No bare @@ -2234,7 +2247,7 @@ export const CalendarConfigSchema = lazySchema(() => strictObject({ * Lands ahead of its reader: the renderer seeds its current date from the * clock until the console half reads this key where it seeds that date. */ - initialDate: z.string().superRefine(checkCalendarInitialDate).optional().describe("Where the calendar opens. Two forms. A record anchor: `first_record` opens on the period containing the earliest start date (the startDateField value) among the records the view selects, `next_record` on the period containing the earliest start date on or after today among those records; when no record qualifies, the calendar opens on today. Or a wrapped date-macro token, resolved each time the calendar opens: `{today}`, `{next_month_start}`, `{current_quarter_start}`, `{3_months_from_now}` (the full vocabulary is `DATE_MACRO_TOKENS` and the parameterised `DATE_MACRO_PARAM_RE` grammar in `@objectstack/spec/data`). Omit the key to open on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: it goes stale as time passes, while a macro does not"), + initialDate: z.string().regex(CALENDAR_INITIAL_DATE_RE, { error: (issue) => calendarInitialDateRefusal(issue.input) }).optional().describe("Where the calendar opens. Two forms. A record anchor: `first_record` opens on the period containing the earliest start date (the startDateField value) among the records the view selects, `next_record` on the period containing the earliest start date on or after today among those records; when no record qualifies, the calendar opens on today. Or a wrapped date-macro token, resolved each time the calendar opens: `{today}`, `{next_month_start}`, `{current_quarter_start}`, `{3_months_from_now}` (the full vocabulary is `DATE_MACRO_TOKENS` and the parameterised `DATE_MACRO_PARAM_RE` grammar in `@objectstack/spec/data`). Omit the key to open on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: it goes stale as time passes, while a macro does not"), })); /** From 558ec1b74f843248941478fcf681f4ab61e680c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 15:27:29 +0000 Subject: [PATCH 5/7] chore(spec): regenerate reference docs and skill reference indexes for calendar initialDate Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- content/docs/references/ui/component.mdx | 2 +- content/docs/references/ui/view.mdx | 6 +++--- skills/objectstack-ai/references/_index.md | 1 + skills/objectstack-data/references/_index.md | 1 + 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index df91c9c8c47..598f75e5390 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -503,7 +503,7 @@ const result = ActionButtonPropsSchema.parse(data); | **titleField** | `string` | optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) | | **colorField** | `string` | optional | Field to derive each event color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the calendar theme-aware palette color hashed from the value | | **allDayField** | `string` | optional | Field carrying the all-day flag for each event (names a boolean field, not a value): a record whose flag is true is drawn as an all-day band rather than at a clock time, and one whose flag is absent or false is not all-day. Omit to leave the renderer inference in place — an event with no end date draws as all-day | -| **initialDate** | `Enum<'today' \| 'first_record' \| 'next_record'>` | optional | Where the calendar opens. 'today' (also what an omitted key does) opens on the period containing today; 'first_record' on the period containing the earliest start date (the startDateField value) among the records the view selects; 'next_record' on the period containing the earliest start date on or after today among those records. When no record qualifies, the calendar opens on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: a date stored on a view goes stale as time passes | +| **initialDate** | `string` | optional | Where the calendar opens. Two forms. A record anchor: `first_record` opens on the period containing the earliest start date (the startDateField value) among the records the view selects, `next_record` on the period containing the earliest start date on or after today among those records; when no record qualifies, the calendar opens on today. Or a wrapped date-macro token, resolved each time the calendar opens: `{today}`, `{next_month_start}`, `{current_quarter_start}`, `{3_months_from_now}` (the full vocabulary is `DATE_MACRO_TOKENS` and the parameterised `DATE_MACRO_PARAM_RE` grammar in `@objectstack/spec/data`). Omit the key to open on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: it goes stale as time passes, while a macro does not | ### Nested Shape: `ObjectCalendarProps.filter[number]` diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index e2ce4a2b130..8a921ac235a 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -109,7 +109,7 @@ Appearance and visualization configuration | **titleField** | `string` | optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) | | **colorField** | `string` | optional | Field to derive each event color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the calendar theme-aware palette color hashed from the value | | **allDayField** | `string` | optional | Field carrying the all-day flag for each event (names a boolean field, not a value): a record whose flag is true is drawn as an all-day band rather than at a clock time, and one whose flag is absent or false is not all-day. Omit to leave the renderer inference in place — an event with no end date draws as all-day | -| **initialDate** | `Enum<'today' \| 'first_record' \| 'next_record'>` | optional | Where the calendar opens. 'today' (also what an omitted key does) opens on the period containing today; 'first_record' on the period containing the earliest start date (the startDateField value) among the records the view selects; 'next_record' on the period containing the earliest start date on or after today among those records. When no record qualifies, the calendar opens on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: a date stored on a view goes stale as time passes | +| **initialDate** | `string` | optional | Where the calendar opens. Two forms. A record anchor: `first_record` opens on the period containing the earliest start date (the startDateField value) among the records the view selects, `next_record` on the period containing the earliest start date on or after today among those records; when no record qualifies, the calendar opens on today. Or a wrapped date-macro token, resolved each time the calendar opens: `{today}`, `{next_month_start}`, `{current_quarter_start}`, `{3_months_from_now}` (the full vocabulary is `DATE_MACRO_TOKENS` and the parameterised `DATE_MACRO_PARAM_RE` grammar in `@objectstack/spec/data`). Omit the key to open on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: it goes stale as time passes, while a macro does not | --- @@ -916,7 +916,7 @@ View filter rule | **titleField** | `string` | optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) | | **colorField** | `string` | optional | Field to derive each event color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the calendar theme-aware palette color hashed from the value | | **allDayField** | `string` | optional | Field carrying the all-day flag for each event (names a boolean field, not a value): a record whose flag is true is drawn as an all-day band rather than at a clock time, and one whose flag is absent or false is not all-day. Omit to leave the renderer inference in place — an event with no end date draws as all-day | -| **initialDate** | `Enum<'today' \| 'first_record' \| 'next_record'>` | optional | Where the calendar opens. 'today' (also what an omitted key does) opens on the period containing today; 'first_record' on the period containing the earliest start date (the startDateField value) among the records the view selects; 'next_record' on the period containing the earliest start date on or after today among those records. When no record qualifies, the calendar opens on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: a date stored on a view goes stale as time passes | +| **initialDate** | `string` | optional | Where the calendar opens. Two forms. A record anchor: `first_record` opens on the period containing the earliest start date (the startDateField value) among the records the view selects, `next_record` on the period containing the earliest start date on or after today among those records; when no record qualifies, the calendar opens on today. Or a wrapped date-macro token, resolved each time the calendar opens: `{today}`, `{next_month_start}`, `{current_quarter_start}`, `{3_months_from_now}` (the full vocabulary is `DATE_MACRO_TOKENS` and the parameterised `DATE_MACRO_PARAM_RE` grammar in `@objectstack/spec/data`). Omit the key to open on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: it goes stale as time passes, while a macro does not | ### Nested Shape: `ListView.gantt` @@ -1286,7 +1286,7 @@ View filter rule | **titleField** | `string` | optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) | | **colorField** | `string` | optional | Field to derive each event color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the calendar theme-aware palette color hashed from the value | | **allDayField** | `string` | optional | Field carrying the all-day flag for each event (names a boolean field, not a value): a record whose flag is true is drawn as an all-day band rather than at a clock time, and one whose flag is absent or false is not all-day. Omit to leave the renderer inference in place — an event with no end date draws as all-day | -| **initialDate** | `Enum<'today' \| 'first_record' \| 'next_record'>` | optional | Where the calendar opens. 'today' (also what an omitted key does) opens on the period containing today; 'first_record' on the period containing the earliest start date (the startDateField value) among the records the view selects; 'next_record' on the period containing the earliest start date on or after today among those records. When no record qualifies, the calendar opens on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: a date stored on a view goes stale as time passes | +| **initialDate** | `string` | optional | Where the calendar opens. Two forms. A record anchor: `first_record` opens on the period containing the earliest start date (the startDateField value) among the records the view selects, `next_record` on the period containing the earliest start date on or after today among those records; when no record qualifies, the calendar opens on today. Or a wrapped date-macro token, resolved each time the calendar opens: `{today}`, `{next_month_start}`, `{current_quarter_start}`, `{3_months_from_now}` (the full vocabulary is `DATE_MACRO_TOKENS` and the parameterised `DATE_MACRO_PARAM_RE` grammar in `@objectstack/spec/data`). Omit the key to open on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: it goes stale as time passes, while a macro does not | ### Nested Shape: `ObjectListView.gantt` diff --git a/skills/objectstack-ai/references/_index.md b/skills/objectstack-ai/references/_index.md index 06b18a96b71..b62a2339f15 100644 --- a/skills/objectstack-ai/references/_index.md +++ b/skills/objectstack-ai/references/_index.md @@ -19,6 +19,7 @@ from `node_modules` — there is no local copy in the skill bundle. ## Transitive dependencies - `node_modules/@objectstack/spec/src/ai/embedding.zod.ts` — Embedding & Vector Store Primitives +- `node_modules/@objectstack/spec/src/data/date-macros.zod.ts` — Date Macro Tokens — the declarative placeholders the UI substitutes - `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1). - `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, UniqueScopeSchema - `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification diff --git a/skills/objectstack-data/references/_index.md b/skills/objectstack-data/references/_index.md index 32bda8f7c56..d036e57f460 100644 --- a/skills/objectstack-data/references/_index.md +++ b/skills/objectstack-data/references/_index.md @@ -23,6 +23,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/automation/flow-function.zod.ts` — The contract for a **named handler function a `script` node invokes** — - `node_modules/@objectstack/spec/src/data/column-apply-on.zod.ts` — Exports: ColumnApplyOnSchema - `node_modules/@objectstack/spec/src/data/data-engine.zod.ts` — Data Engine Protocol +- `node_modules/@objectstack/spec/src/data/date-macros.zod.ts` — Date Macro Tokens — the declarative placeholders the UI substitutes - `node_modules/@objectstack/spec/src/data/driver-sql.zod.ts` — Exports: SQLDialectSchema, DataTypeMappingSchema, SSLConfigSchema, SQLDriverConfigSchema, SQLiteDataTypeMappingDefaults - `node_modules/@objectstack/spec/src/data/driver.zod.ts` — Exports: DriverOptionsSchema, DriverCapabilitiesSchema, DriverInterfaceSchema, PoolConfigSchema, DriverConfigSchema - `node_modules/@objectstack/spec/src/data/driver/common.zod.ts` — Shared building blocks for the per-driver `datasource.config` shapes. From 0b105291050fe700b0a4cbbd320aec988de0075d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 17:43:49 +0000 Subject: [PATCH 6/7] fix(spec): calendar.initialDate is marked planned in its describe, says whose today, and the flat spelling is prescribed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The describe now opens with `[PLANNED — not yet rendered]` and says a calendar that declares the key still opens on today until the console reads it, the house form for a key landed ahead of its reader inside an undrilled container. It also says "today" is the viewer's local calendar day and that the record anchors read the view's whole filter scope, not the visible window. The TSDoc names the console's fixed `currentDate` and retired `defaultDate` on `calendar-view`, and when the marker leaves. A flat `initialDate` on `object-calendar` is now refused with the nested-shape prescription instead of a bare unrecognized key. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- content/docs/references/ui/component.mdx | 2 +- content/docs/references/ui/view.mdx | 6 +++--- .../src/ui/calendar-config-initial-date.test.ts | 11 +++++++++++ packages/spec/src/ui/component.zod.ts | 5 +++-- packages/spec/src/ui/view.zod.ts | 15 +++++++++++++-- 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index 598f75e5390..fbbff8ff1ff 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -503,7 +503,7 @@ const result = ActionButtonPropsSchema.parse(data); | **titleField** | `string` | optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) | | **colorField** | `string` | optional | Field to derive each event color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the calendar theme-aware palette color hashed from the value | | **allDayField** | `string` | optional | Field carrying the all-day flag for each event (names a boolean field, not a value): a record whose flag is true is drawn as an all-day band rather than at a clock time, and one whose flag is absent or false is not all-day. Omit to leave the renderer inference in place — an event with no end date draws as all-day | -| **initialDate** | `string` | optional | Where the calendar opens. Two forms. A record anchor: `first_record` opens on the period containing the earliest start date (the startDateField value) among the records the view selects, `next_record` on the period containing the earliest start date on or after today among those records; when no record qualifies, the calendar opens on today. Or a wrapped date-macro token, resolved each time the calendar opens: `{today}`, `{next_month_start}`, `{current_quarter_start}`, `{3_months_from_now}` (the full vocabulary is `DATE_MACRO_TOKENS` and the parameterised `DATE_MACRO_PARAM_RE` grammar in `@objectstack/spec/data`). Omit the key to open on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: it goes stale as time passes, while a macro does not | +| **initialDate** | `string` | optional | [PLANNED — not yet rendered] Where the calendar opens. No console reads it yet: until the calendar renderer reads it, a calendar that declares it still opens on today. Two forms. A record anchor: `first_record` opens on the period containing the earliest start date (the startDateField value) among the records the view's filter selects (its whole filter scope, not only the visible window), `next_record` on the period containing the earliest start date on or after today among those records; when no record qualifies, the calendar opens on today. "Today" is the viewer's local calendar day, and a `date` start field compares as a calendar day. Or a wrapped date-macro token, resolved each time the calendar opens: `{today}`, `{next_month_start}`, `{current_quarter_start}`, `{3_months_from_now}` (the full vocabulary is `DATE_MACRO_TOKENS` and the parameterised `DATE_MACRO_PARAM_RE` grammar in `@objectstack/spec/data`). Omit the key to open on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: it goes stale as time passes, while a macro does not | ### Nested Shape: `ObjectCalendarProps.filter[number]` diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 8a921ac235a..0d28438b017 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -109,7 +109,7 @@ Appearance and visualization configuration | **titleField** | `string` | optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) | | **colorField** | `string` | optional | Field to derive each event color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the calendar theme-aware palette color hashed from the value | | **allDayField** | `string` | optional | Field carrying the all-day flag for each event (names a boolean field, not a value): a record whose flag is true is drawn as an all-day band rather than at a clock time, and one whose flag is absent or false is not all-day. Omit to leave the renderer inference in place — an event with no end date draws as all-day | -| **initialDate** | `string` | optional | Where the calendar opens. Two forms. A record anchor: `first_record` opens on the period containing the earliest start date (the startDateField value) among the records the view selects, `next_record` on the period containing the earliest start date on or after today among those records; when no record qualifies, the calendar opens on today. Or a wrapped date-macro token, resolved each time the calendar opens: `{today}`, `{next_month_start}`, `{current_quarter_start}`, `{3_months_from_now}` (the full vocabulary is `DATE_MACRO_TOKENS` and the parameterised `DATE_MACRO_PARAM_RE` grammar in `@objectstack/spec/data`). Omit the key to open on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: it goes stale as time passes, while a macro does not | +| **initialDate** | `string` | optional | [PLANNED — not yet rendered] Where the calendar opens. No console reads it yet: until the calendar renderer reads it, a calendar that declares it still opens on today. Two forms. A record anchor: `first_record` opens on the period containing the earliest start date (the startDateField value) among the records the view's filter selects (its whole filter scope, not only the visible window), `next_record` on the period containing the earliest start date on or after today among those records; when no record qualifies, the calendar opens on today. "Today" is the viewer's local calendar day, and a `date` start field compares as a calendar day. Or a wrapped date-macro token, resolved each time the calendar opens: `{today}`, `{next_month_start}`, `{current_quarter_start}`, `{3_months_from_now}` (the full vocabulary is `DATE_MACRO_TOKENS` and the parameterised `DATE_MACRO_PARAM_RE` grammar in `@objectstack/spec/data`). Omit the key to open on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: it goes stale as time passes, while a macro does not | --- @@ -916,7 +916,7 @@ View filter rule | **titleField** | `string` | optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) | | **colorField** | `string` | optional | Field to derive each event color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the calendar theme-aware palette color hashed from the value | | **allDayField** | `string` | optional | Field carrying the all-day flag for each event (names a boolean field, not a value): a record whose flag is true is drawn as an all-day band rather than at a clock time, and one whose flag is absent or false is not all-day. Omit to leave the renderer inference in place — an event with no end date draws as all-day | -| **initialDate** | `string` | optional | Where the calendar opens. Two forms. A record anchor: `first_record` opens on the period containing the earliest start date (the startDateField value) among the records the view selects, `next_record` on the period containing the earliest start date on or after today among those records; when no record qualifies, the calendar opens on today. Or a wrapped date-macro token, resolved each time the calendar opens: `{today}`, `{next_month_start}`, `{current_quarter_start}`, `{3_months_from_now}` (the full vocabulary is `DATE_MACRO_TOKENS` and the parameterised `DATE_MACRO_PARAM_RE` grammar in `@objectstack/spec/data`). Omit the key to open on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: it goes stale as time passes, while a macro does not | +| **initialDate** | `string` | optional | [PLANNED — not yet rendered] Where the calendar opens. No console reads it yet: until the calendar renderer reads it, a calendar that declares it still opens on today. Two forms. A record anchor: `first_record` opens on the period containing the earliest start date (the startDateField value) among the records the view's filter selects (its whole filter scope, not only the visible window), `next_record` on the period containing the earliest start date on or after today among those records; when no record qualifies, the calendar opens on today. "Today" is the viewer's local calendar day, and a `date` start field compares as a calendar day. Or a wrapped date-macro token, resolved each time the calendar opens: `{today}`, `{next_month_start}`, `{current_quarter_start}`, `{3_months_from_now}` (the full vocabulary is `DATE_MACRO_TOKENS` and the parameterised `DATE_MACRO_PARAM_RE` grammar in `@objectstack/spec/data`). Omit the key to open on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: it goes stale as time passes, while a macro does not | ### Nested Shape: `ListView.gantt` @@ -1286,7 +1286,7 @@ View filter rule | **titleField** | `string` | optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) | | **colorField** | `string` | optional | Field to derive each event color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the calendar theme-aware palette color hashed from the value | | **allDayField** | `string` | optional | Field carrying the all-day flag for each event (names a boolean field, not a value): a record whose flag is true is drawn as an all-day band rather than at a clock time, and one whose flag is absent or false is not all-day. Omit to leave the renderer inference in place — an event with no end date draws as all-day | -| **initialDate** | `string` | optional | Where the calendar opens. Two forms. A record anchor: `first_record` opens on the period containing the earliest start date (the startDateField value) among the records the view selects, `next_record` on the period containing the earliest start date on or after today among those records; when no record qualifies, the calendar opens on today. Or a wrapped date-macro token, resolved each time the calendar opens: `{today}`, `{next_month_start}`, `{current_quarter_start}`, `{3_months_from_now}` (the full vocabulary is `DATE_MACRO_TOKENS` and the parameterised `DATE_MACRO_PARAM_RE` grammar in `@objectstack/spec/data`). Omit the key to open on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: it goes stale as time passes, while a macro does not | +| **initialDate** | `string` | optional | [PLANNED — not yet rendered] Where the calendar opens. No console reads it yet: until the calendar renderer reads it, a calendar that declares it still opens on today. Two forms. A record anchor: `first_record` opens on the period containing the earliest start date (the startDateField value) among the records the view's filter selects (its whole filter scope, not only the visible window), `next_record` on the period containing the earliest start date on or after today among those records; when no record qualifies, the calendar opens on today. "Today" is the viewer's local calendar day, and a `date` start field compares as a calendar day. Or a wrapped date-macro token, resolved each time the calendar opens: `{today}`, `{next_month_start}`, `{current_quarter_start}`, `{3_months_from_now}` (the full vocabulary is `DATE_MACRO_TOKENS` and the parameterised `DATE_MACRO_PARAM_RE` grammar in `@objectstack/spec/data`). Omit the key to open on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: it goes stale as time passes, while a macro does not | ### Nested Shape: `ObjectListView.gantt` diff --git a/packages/spec/src/ui/calendar-config-initial-date.test.ts b/packages/spec/src/ui/calendar-config-initial-date.test.ts index 802bf22f9c4..4dccbe01c98 100644 --- a/packages/spec/src/ui/calendar-config-initial-date.test.ts +++ b/packages/spec/src/ui/calendar-config-initial-date.test.ts @@ -145,6 +145,17 @@ describe('calendar.initialDate — refused values', () => { expect(r.success).toBe(false); expect(r.error!.issues.map((i) => i.code)).toContain('unrecognized_keys'); }); + + it('a flat `initialDate` on `object-calendar` is refused with the nested-shape prescription', () => { + const r = objectCalendar.safeParse({ objectName: 'renewals', calendar: binding, initialDate: '{today}' }); + expect(r.success).toBe(false); + const issue = r.success === false + ? r.error.issues.find((i) => i.code === 'unrecognized_keys') + : undefined; + expect((issue as { keys?: string[] } | undefined)?.keys).toEqual(['initialDate']); + expect(issue?.message).toContain('Write this as a key of the `calendar` config object instead'); + expect(issue?.message).toContain('`initialDate`, the date it opens on, in the same block'); + }); }); describe('calendar.initialDate — one vocabulary with the date macros', () => { diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index 54377640d9f..9a81f938634 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -6153,11 +6153,12 @@ const OBJECT_CALENDAR_FLAT_FIELD_GUIDANCE: readonly KeySetGuidance[] = [ ...COMPONENT_LEVEL_GUIDANCE, { name: 'OBJECT_CALENDAR_FLAT_FIELD_KEYS', - keys: ['startDateField', 'dateField', 'endDateField', 'endField', 'titleField', 'colorField', 'allDayField'], + keys: ['startDateField', 'dateField', 'endDateField', 'endField', 'titleField', 'colorField', 'allDayField', 'initialDate'], examples: ['startDateField', 'titleField'], prescription: 'Write this as a key of the `calendar` config object instead — `calendar: { startDateField, ' - + 'endDateField, titleField, colorField, allDayField }`. `ObjectView`/`ListView` hand the binding ' + + 'endDateField, titleField, colorField, allDayField }`, and `initialDate`, the date it opens on, ' + + 'in the same block. `ObjectView`/`ListView` hand the binding ' + 'off in that block too, and the renderer reads the binding there only: the flat spelling is not ' + 'read and not a second authorable spelling (one key per concept, Prime Directive #12).', }, diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 537ce85ec2a..cd5d7042b05 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -2225,7 +2225,18 @@ export const CalendarConfigSchema = lazySchema(() => strictObject({ * Spelling: no sibling view config (gantt, timeline, map) declares an * initial date or range, so there is nothing to mirror; `initialDate` names * where the calendar OPENS, and avoids the `default*` prefix, which on this - * platform also reads as "fallback value" (`defaultValue`). + * platform also reads as "fallback value" (`defaultValue`). Two console + * facts were in view: the `calendar-view` node reads an authored, FIXED + * `currentDate` (`ui/packages/types/src/zod/complex.zod.ts`, + * `calendar-view-renderer.tsx#resolveAuthoredCurrentDate`), the one producer + * of a fixed opening date, deliberately not admitted here because a date + * stored on a view goes stale; and the same node retired `defaultDate` as + * inert, which is a second reason not to spell this key `defaultDate`. + * + * Marked `[PLANNED — not yet rendered]` in its describe until the console + * half (#28102) reads it where `ObjectCalendar` seeds its current date; that + * change removes the marker, as `navigation.dataSource` (`app.zod.ts`) and + * the bound-record empty state (`component.zod.ts`) do. * * Two forms, and only two (see {@link CALENDAR_INITIAL_DATE_RE}): a record * anchor, or a wrapped, KNOWN date-macro token — the formula vocabulary the @@ -2247,7 +2258,7 @@ export const CalendarConfigSchema = lazySchema(() => strictObject({ * Lands ahead of its reader: the renderer seeds its current date from the * clock until the console half reads this key where it seeds that date. */ - initialDate: z.string().regex(CALENDAR_INITIAL_DATE_RE, { error: (issue) => calendarInitialDateRefusal(issue.input) }).optional().describe("Where the calendar opens. Two forms. A record anchor: `first_record` opens on the period containing the earliest start date (the startDateField value) among the records the view selects, `next_record` on the period containing the earliest start date on or after today among those records; when no record qualifies, the calendar opens on today. Or a wrapped date-macro token, resolved each time the calendar opens: `{today}`, `{next_month_start}`, `{current_quarter_start}`, `{3_months_from_now}` (the full vocabulary is `DATE_MACRO_TOKENS` and the parameterised `DATE_MACRO_PARAM_RE` grammar in `@objectstack/spec/data`). Omit the key to open on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: it goes stale as time passes, while a macro does not"), + initialDate: z.string().regex(CALENDAR_INITIAL_DATE_RE, { error: (issue) => calendarInitialDateRefusal(issue.input) }).optional().describe("[PLANNED — not yet rendered] Where the calendar opens. No console reads it yet: until the calendar renderer reads it, a calendar that declares it still opens on today. Two forms. A record anchor: `first_record` opens on the period containing the earliest start date (the startDateField value) among the records the view's filter selects (its whole filter scope, not only the visible window), `next_record` on the period containing the earliest start date on or after today among those records; when no record qualifies, the calendar opens on today. \"Today\" is the viewer's local calendar day, and a `date` start field compares as a calendar day. Or a wrapped date-macro token, resolved each time the calendar opens: `{today}`, `{next_month_start}`, `{current_quarter_start}`, `{3_months_from_now}` (the full vocabulary is `DATE_MACRO_TOKENS` and the parameterised `DATE_MACRO_PARAM_RE` grammar in `@objectstack/spec/data`). Omit the key to open on today. The period is the month, week or day the active view mode shows. A fixed calendar date is not accepted: it goes stale as time passes, while a macro does not"), })); /** From 254b2fb067ddc379fa28dcf20e0dacebe70056d7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 18:03:35 +0000 Subject: [PATCH 7/7] fix(spec): the calendar initialDate TSDoc cites the calendar-view renderer by its repo path The doc-block anchor gate resolves a `file#symbol` citation only from a path it can find; the bare file name did not resolve. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- packages/spec/src/ui/view.zod.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index cd5d7042b05..e64f99e2ba2 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -2228,8 +2228,8 @@ export const CalendarConfigSchema = lazySchema(() => strictObject({ * platform also reads as "fallback value" (`defaultValue`). Two console * facts were in view: the `calendar-view` node reads an authored, FIXED * `currentDate` (`ui/packages/types/src/zod/complex.zod.ts`, - * `calendar-view-renderer.tsx#resolveAuthoredCurrentDate`), the one producer - * of a fixed opening date, deliberately not admitted here because a date + * `ui/packages/plugin-calendar/src/calendar-view-renderer.tsx#resolveAuthoredCurrentDate`), + * the one producer of a fixed opening date, deliberately not admitted here because a date * stored on a view goes stale; and the same node retired `defaultDate` as * inert, which is a second reason not to spell this key `defaultDate`. *