You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 8f36f6b
Browse filesBrowse the repository at this point in the historyBrowse 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_017ihL2fi9zfJhUTubyJYb6VFixes#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>
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.
Copy file name to clipboardExpand all lines: content/docs/references/ui/component.mdx
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -484,7 +484,7 @@ const result = ActionButtonPropsSchema.parse(data);
484
484
| Property | Type | Required | Description |
485
485
| :--- | :--- | :--- | :--- |
486
486
|**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 |
|**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`|
490
490
|**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);
503
503
|**titleField**|`string`| optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) |
504
504
|**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 |
505
505
|**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 |
Copy file name to clipboardExpand all lines: content/docs/references/ui/view.mdx
+3Lines changed: 3 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -109,6 +109,7 @@ Appearance and visualization configuration
109
109
|**titleField**|`string`| optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) |
110
110
|**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 |
111
111
|**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 |
112
113
113
114
114
115
---
@@ -917,6 +918,7 @@ View filter rule
917
918
|**titleField**|`string`| optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) |
918
919
|**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 |
919
920
|**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 |
920
922
921
923
### Nested Shape: `ListView.gantt`
922
924
@@ -1287,6 +1289,7 @@ View filter rule
1287
1289
|**titleField**|`string`| optional | Field displayed as the event title. Omit to fall back to the record display name (ADR-0079 resolver chain) |
1288
1290
|**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 |
1289
1291
|**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 |
Copy file name to clipboardExpand all lines: packages/spec/liveness/view.json
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -105,7 +105,7 @@
105
105
},
106
106
"calendar": {
107
107
"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)."
0 commit comments