Skip to content

Commit 8f36f6b

Browse files
feat(spec): a calendar view can declare the date it opens on, calendar.initialDate (#28138)
🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V Fixes #28021 Clause-②: yes (widening) Metadata-protocol: added Skills-docs: none — no file under `skills/**` lists the calendar block's members. The key's describe ships to `content/docs/references/ui/view.mdx` and `component.mdx`, regenerated here. Until the Console reads the key (#28102), teaching it in a skill would advertise behaviour the renderer does not have yet. ## 维护者速读 - 改了什么:日历视图配置 `calendar` 新增可选键 `initialDate`,取值三选一:`'today'` / `'first_record'` / `'next_record'`。作者可以声明日历打开时落在哪一天。不写这个键就是今天,存量日历行为不变。 - 为什么改:日历总是打开在当月。记录都排在几个月以后的日历(比如续约日历),一打开就是空的一个月,作者无法改变。 - 风险与回滚:纯放宽,只加了一个可选键,spec 侧没有默认值,存量视图解析结果逐字节不变。回滚就是 revert 本 PR。控制台读取这个键在 #28102 做;在那之前写了这个键也照样打开在今天。 - 先看:`packages/spec/src/ui/view.zod.ts`(`CalendarConfigSchema.initialDate` 的取值与 describe);`packages/spec/src/ui/calendar-config-initial-date.test.ts`;`packages/spec/liveness/view.json`(calendar 行注明此键暂为 planned)。 - 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run src/ui/calendar-config-initial-date.test.ts`,预期 19 个用例全部通过。 - 需要你做的:批准键名 `initialDate` 和三个取值(不批准的话,请给出要换的名字或取值)。 ## What changes - `CalendarConfigSchema` gains `initialDate: z.enum(['today', 'first_record', 'next_record']).optional()`. The list view's `calendar` block and the `object-calendar` node's `calendar` prop both get it, because the prop takes the block by reference. - `'today'`: the calendar opens on the period that holds today. An omitted key does the same. - `'first_record'`: the period that holds the earliest `startDateField` value among the records the view selects. - `'next_record'`: the period that holds the earliest `startDateField` value on or after today. - When no record qualifies, the calendar opens on today. "Period" means the month, week or day of the active view mode. - **Why this name.** No sibling view config (gantt, timeline, map) declares an initial date or range, so there is no spelling to mirror. `initialDate` says where the calendar opens. It avoids the `default*` prefix, which on this platform also reads as "fallback value". The nearest precedent is `object-calendar`'s `defaultView` ("Initial view mode"). If the maintainer wants the two to match, the alternative is `defaultDate`. - **Why these values.** `next_record` is included because it is the value the card's own reach needs. A renewals calendar that also holds past renewals would open years back under `first_record`. Under `next_record` it opens on the next renewal that is still ahead. - **Why no fixed ISO date.** A date stored on a view goes stale as time passes: the same view opens on an older month every day. No producer that asks for a fixed date was found. A fixed date (`'2026-11-01'`), a date-macro token (`'{today}'`) and every near-miss spelling are refused with Zod's `invalid_value` at `calendar.initialDate`. The refusal message lists the three values. Admitting a date later would be a widening. - **Why it lives on this block.** The block's own TSDoc keeps `defaultView` out, as a UI preference with its home on the component. `initialDate` differs in two ways: a stored list view has no other place to author it, and two of its three values are defined by this block's `startDateField` binding. - **No spec-side default.** The reason is the same as for gantt `viewMode`: a materialized `'today'` would be written onto every parsed calendar and would read as an author's choice. - **Copies kept in step.** - The `object-calendar` `calendar` prop's describe now lists `initialDate?`. - The liveness ledger's `calendar` row says `initialDate` rides at planned strength until its reader lands. - The authorable surface (`authorable-surface/ui.json`) and the reference docs (`view.mdx`, `component.mdx`) are regenerated. - Nothing under `skills/**` lists the block's members. - **Console half: #28102** (`domain:ui`, filed by this PR, lands after it). `ObjectCalendar` still seeds `currentDate` with `useState(new Date())`. ## Tests `packages/spec/src/ui/calendar-config-initial-date.test.ts`, 19 cases: - **Accepted:** each of the 3 values, on `CalendarConfigSchema`, on a whole stored `ListViewSchema`, and on the `object-calendar` node. - **Absent:** no default is materialized, so a calendar stored without the key parses to exactly the block it stored. - **Refused:** a fixed date, a fixed timestamp, `first`, `firstRecord`, `TODAY` and `{today}`. Each case asserts: - the issue `code` `invalid_value`; - the path `['calendar', 'initialDate']`; - that the message names all three members; - that the wire envelope from `zodIssuesToFields` is `{ field: 'calendar.initialDate', code: 'invalid_option' }`; - the validation class status, 400. A non-string value is refused at the same path. An unknown key is still refused as `unrecognized_keys`. <details><summary>Local runs at d7c3dce</summary> - `pnpm --filter @objectstack/spec test` (under the verify lock): `Test Files 757 passed (757)`, `Tests 22529 passed | 1 todo`, `VERDICT command-exit 0`. - `pnpm --filter @objectstack/spec typecheck`: exit 0, in the same locked run. - `pnpm --filter @objectstack/spec check:generated`, before regeneration: 13 of 14 green and `check:docs` stale. Then `gen:docs` was run. `check:authorable-surface` rewrote `authorable-surface/ui.json` (+1 line, `ui/CalendarConfig:initialDate`). `authorable-surface.base.json` was not touched. </details> ## Acceptance notes - **`ui/`-owned copies that list the calendar block's keys.** These are not edited here, because none of them reads the key and editing them would advertise behaviour the renderer does not have yet. They are named on #28102: - the `list-view` registry input description (`ui/packages/plugin-list/src/index.tsx`), which feeds `sdui.manifest.json` and `content/docs/ui/components/plugins.mdx`; - the `object-calendar` registry `calendar` input description; - the hand-typed `calendar` options in `ui/packages/types/src/designer.ts`. - **Mirror and Studio need no edit.** - The `@object-ui/types` mirror takes the block by reference, so it accepts the key without an edit. - The Studio view inspector's `SchemaForm` is driven by the JSON Schema and renders an enum prop as a select. It picks a field picker only for names ending in `Field`, so `initialDate` should show as a select of the three values. This is from reading the code; it was not run in a browser. - **Independent review: none run (quota).** The PR waits for the seat's contract-review hand-off. ## Verification <details><summary>Derived gate families at d7c3dce</summary> - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 104 commands for 8 paths (+167 / -3). - All 104 were run, and each exit code was captured before any pipe. 97 exited 0. - 7 exited 3 (PREREQUISITE NOT MET) and are NOT MEASURED locally. Each one reads built output of packages outside this diff, and the container builds only `@objectstack/spec`. CI's `Lint & Repo Gates` and `TypeScript Type Check` run them: - `pnpm --filter @objectstack/lint run check:doc-formula-expressions` and `check:doc-security-posture` (need lint and formula built); - `pnpm --filter @objectstack/spec run check:skill-examples` (needs client-react built); - `pnpm check:docs-transcript-drift` (needs lint built); - `pnpm check:dual-build-cjs-loads` (needs every package built); - `pnpm check:lean-entry-closure` (needs objectql built); - `pnpm check:type-check-debt` (its closure build timed out in the verify lock queue). - `--ran` reconciliation: `✓ 104 derived famil(ies) accounted for — 97 run, 7 NOT-MEASURED (7 DERIVED from a recorded exit 3)`. - Local-only families: - `check:skill-frame-freshness`: exit 0. - `check:dev-prereqs`: exit 1, `The workspace is not built — 1 unmet precondition`. Only spec is built in this container, so this is a fact about the container, not about the diff. - Full dogfood: n/a. The title has no `!` and there is no `(narrowing)` arm. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 73b7270 commit 8f36f6b

10 files changed

Lines changed: 311 additions & 5 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): a calendar view can declare the date it opens on, `calendar.initialDate`
6+
7+
Clause-②: yes (widening)
8+
Metadata-protocol: added
9+
10+
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:
11+
12+
- **`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.
13+
- **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.
14+
- **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.
15+
16+
The Console renderer reads the key in a follow-up change; until then a calendar that declares it still opens on today.

‎content/docs/references/ui/component.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ const result = ActionButtonPropsSchema.parse(data);
484484
| Property | Type | Required | Description |
485485
| :--- | :--- | :--- | :--- |
486486
| **objectName** | `string` | optional | Object this calendar binds to. Optional because the component-level `dataSource` binding can supply the object instead |
487-
| **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 |
487+
| **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 |
488488
| **defaultView** | `Enum<'month' \| 'week' \| 'day'>` | optional | Initial view mode |
489489
| **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` |
490490
| **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);
503503
| **titleField** | `string` | optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) |
504504
| **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 |
505505
| **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 |
506+
| **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 |
506507

507508
### Nested Shape: `ObjectCalendarProps.filter[number]`
508509

‎content/docs/references/ui/view.mdx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ Appearance and visualization configuration
109109
| **titleField** | `string` | optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) |
110110
| **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 |
111111
| **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 |
112+
| **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 |
112113

113114

114115
---
@@ -917,6 +918,7 @@ View filter rule
917918
| **titleField** | `string` | optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) |
918919
| **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 |
919920
| **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 |
921+
| **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 |
920922

921923
### Nested Shape: `ListView.gantt`
922924

@@ -1287,6 +1289,7 @@ View filter rule
12871289
| **titleField** | `string` | optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) |
12881290
| **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 |
12891291
| **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 |
1292+
| **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 |
12901293

12911294
### Nested Shape: `ObjectListView.gantt`
12921295

‎packages/spec/authorable-surface/ui.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@
229229
"ui/CalendarConfig:allDayField",
230230
"ui/CalendarConfig:colorField",
231231
"ui/CalendarConfig:endDateField",
232+
"ui/CalendarConfig:initialDate",
232233
"ui/CalendarConfig:startDateField",
233234
"ui/CalendarConfig:titleField",
234235
"ui/ChartAggregate:field",

‎packages/spec/liveness/view.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
},
106106
"calendar": {
107107
"status": "live",
108-
"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)."
108+
"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)."
109109
},
110110
"gantt": {
111111
"status": "live",

0 commit comments

Comments
 (0)