diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..6ad3794 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,20 @@ +name: Documentation checks +on: + pull_request: + push: + branches: [main] +permissions: + contents: read +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm audit --audit-level=high + - run: npm run build + - run: npm run check:search diff --git a/README.md b/README.md index 753de30..16d3ccb 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,12 @@ Documentation portal for [forceCalendar](https://forcecalendar.org), built with All content is MDX under `content/docs/`: -- **Getting started** — installation, quick start +- **Getting started** — installation, quick start, migration, release status - **`core/`** — engine guides: calendar, events, recurrence, timezones, ICS, search, state, performance - **`api/`** — per-class API reference (Calendar, EventStore, RecurrenceEngine, RRuleParser, ICSParser/Handler, StateManager, …) - **`interface/`** — Web Components, views, theming, event bus -- **`salesforce/`** — LWC integration, Apex controller, Locker Service, deployment +- **`salesforce/`** — LWC integration, Apex controller, LWS, permissions, deployment +- **`agent/`** — current headless calendar APIs and integrator-owned data/access boundaries - **`security/`** — security model and remediation history Navigation order is controlled by `meta.json` files alongside the content. @@ -20,11 +21,18 @@ Navigation order is controlled by `meta.json` files alongside the content. ## Development ```bash -npm install +npm ci +npm test # API coverage, internal links/navigation, and runnable Core examples npm run dev # http://localhost:3000 npm run build ``` +## Dependency maintenance + +The deployment remains on Next.js 15.5.27. Compatible transitive patches are locked, with a PostCSS 8.5.28 override because Next 15 pins an older PostCSS release. CI audits high/critical advisories as well as building the docs. Audit results are time-scoped, not a security certification. + +The public search endpoint at `app/api/search/route.ts` indexes the same MDX source as the sidebar. Verify searches after deployment when changing source configuration. + ## Contributing Documentation fixes are the easiest way to contribute to forceCalendar — edit the MDX under `content/docs/` and open a PR. See the [contributing guide](https://github.com/forceCalendar/.github/blob/main/CONTRIBUTING.md). diff --git a/app/api/search/route.ts b/app/api/search/route.ts new file mode 100644 index 0000000..7ea903e --- /dev/null +++ b/app/api/search/route.ts @@ -0,0 +1,5 @@ +import { source } from '@/lib/source'; +import { createFromSource } from 'fumadocs-core/search/server'; + +// The built-in search dialog requests /api/search. Index only public docs. +export const { GET } = createFromSource(source); diff --git a/app/docs/[[...slug]]/page.tsx b/app/docs/[[...slug]]/page.tsx index 4d1dc8b..eb6f793 100644 --- a/app/docs/[[...slug]]/page.tsx +++ b/app/docs/[[...slug]]/page.tsx @@ -27,6 +27,9 @@ export default async function Page(props: PageProps) { + ); } diff --git a/app/layout.tsx b/app/layout.tsx index be47517..41c25bf 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -13,7 +13,7 @@ export const metadata: Metadata = { template: '%s | forceCalendar', }, description: - 'Enterprise-grade calendar infrastructure for Salesforce and strict-CSP environments. Zero dependencies.', + 'Calendar engine, Web Components, framework adapters, and Salesforce LWS integration.', }; export default function Layout({ children }: { children: ReactNode }) { diff --git a/content/docs/adapters/react.mdx b/content/docs/adapters/react.mdx index 69eac58..df034c1 100644 --- a/content/docs/adapters/react.mdx +++ b/content/docs/adapters/react.mdx @@ -1,50 +1,146 @@ --- title: React -description: Use forceCalendar in React and Next.js with @forcecalendar/react +description: React 0.3.1 props, typed events, snapshots, refs, SSR, and read-only integration. --- -`@forcecalendar/react` is a thin adapter around the `` Web Component: props map to attributes, DOM events map to callbacks. It has no dependencies beyond its peers. - -## Install +`@forcecalendar/react` **0.3.1** maps props, callbacks and a ref to the Interface Web Component. Install React 18 or later alongside the documented package set: ```bash -npm install @forcecalendar/react @forcecalendar/core @forcecalendar/interface +npm install @forcecalendar/react@0.3.1 @forcecalendar/core@2.5.7 @forcecalendar/interface@1.9.0 ``` -## Use +## Use in React or Next.js ```tsx -import { ForceCalendar } from '@forcecalendar/react'; +'use client'; +import { useState } from 'react'; +import { ForceCalendar, type CalendarEvent } from '@forcecalendar/react'; + +export default function Schedule() { + const [events] = useState([{ + id: 'planning', title: 'Planning', + start: '2026-10-05T09:00:00Z', end: '2026-10-05T10:00:00Z', + }]); + return console.log('Persist local edit:', event.id)} />; +} +``` + +In Next.js App Router, put interactive usage behind a client boundary as shown. Server rendering can emit the tag; registration runs in an effect. Imperative methods still need `whenReady()`. No `ssr: false` wrapper is required just to avoid registering custom elements on the server. + +The examples below use application-provided `fetchEvents` when loading a backend; implement it with authentication, error handling and stale-response protection. UI callbacks alone do not persist edits. + +## Props + +| Prop | Type | Description | +| --- | --- | --- | +| `view` | `'month' \| 'week' \| 'day'` | Initial/controlled view. | +| `date` | `Date \| string` | Date the view is centred on (`Date` is serialised to ISO). | +| `locale` | `string` | BCP 47 locale, e.g. `en-AU`. | +| `timezone` | `string` | IANA time zone. | +| `weekStartsOn` | `0..6` | First day of the week (0 = Sunday). | +| `height` | `string` | CSS height of the calendar. | +| `theme` | `'slds' \| string` | Named theme preset applied as `--fc-*` custom properties. | +| `events` | `CalendarEvent[]` | Complete snapshot of events, applied through `setEvents()` (see below). | +| `removeMissingEvents` | `boolean` (default `true`) | Whether events absent from `events` are removed from the calendar. | +| `className`, `style` | | Applied to the element (`style` as an object; a string would wipe the theme tokens). | +| any other HTML attribute | `id`, `role`, `aria-*`, `data-*`, `tabIndex`, `onClick`, ... | Passed straight through to ``. | + +### Callbacks + +Each callback receives the `detail` of the corresponding `calendar-*` DOM event. + +| Callback | DOM event | `detail` | +| --- | --- | --- | +| `onEventAdded` | `calendar-event-added` | `{ event }` | +| `onEventUpdated` | `calendar-event-updated` | `{ event }` | +| `onEventDeleted` | `calendar-event-deleted` | `{ eventId }` | +| `onEventsSet` | `calendar-events-set` | `{ events, added, updated, removed, unchanged }` | +| `onDateSelect` | `calendar-date-select` | `{ date }` | +| `onViewChange` | `calendar-view-change` | `{ view }` | +| `onNavigate` | `calendar-navigate` | `{ action, date }` | +| `onRangeChange` | `calendar-range-change` | `{ start, end, view, date }` | +| `onRangeSelect` | `calendar-range-select` | `{ start, end }` | + +`onRangeChange` also reports the initial visible window right after the element is ready, so you can fetch data for it without waiting for the user to navigate. The legacy `calendar-event-add`/`-update`/`-remove` aliases are not mapped; listen to them on the element directly if you need them. + +### The `events` prop + +`events` is the declarative form of the element's `setEvents()`: pass the complete list and the calendar reconciles it — unchanged events are kept, changed ones replaced, new ones added and (unless `removeMissingEvents={false}`) missing ones removed. The view re-renders at most once and a single `onEventsSet` call describes the change set. + +- A snapshot load does **not** emit per-event `onEventAdded` / `onEventDeleted` callbacks, so handlers that persist user edits are not triggered by your own data. +- The snapshot is re-applied whenever the array identity changes. Inline arrays (`events={[...]}`) therefore re-run the reconciliation on every render; it is cheap when nothing changed (everything lands in `unchanged`) but `onEventsSet` still fires, so wrap the array in `useMemo` or keep it in state. +- `events` is never rendered as an attribute: it is applied imperatively after mount, and is buffered by the element if it is still loading. + +## Imperative handle + +The ref exposes the element and all of its methods. Methods are no-ops that log a warning until the element is mounted and `@forcecalendar/interface` has loaded, so await `whenReady()` first when calling them outside an event handler. + +```tsx +import { useEffect, useRef } from 'react'; +import { ForceCalendar, type ForceCalendarHandle } from '@forcecalendar/react'; + +export function Agenda() { + const calendar = useRef(null); + + useEffect(() => { + let cancelled = false; + calendar.current?.whenReady().then(async () => { + if (cancelled) return; + const range = calendar.current?.getVisibleRange(); + if (!range) return; + const rows = await fetchEvents(range.start, range.end); + calendar.current?.setEvents(rows, { removeMissing: true }); + }); + return () => { + cancelled = true; + }; + }, []); -export default function Scheduling() { return ( - console.log('selected', date)} - onEventAdded={detail => console.log('added', detail)} - /> + <> + + + + + + ); } ``` -## Props +| Member | Signature | +| --- | --- | +| `element` | `ForceCalendarElement \| null` — the underlying `` | +| `whenReady()` | `Promise` — resolves once the element is defined | +| `addEvent(event)` | `CalendarEvent \| null` | +| `updateEvent(id, updates)` | `CalendarEvent \| null` | +| `deleteEvent(id)` | `boolean` | +| `getEvents()` | `CalendarEvent[]` | +| `setEvents(events, { removeMissing? })` | `EventsSetResult \| null` | +| `getVisibleRange()` | `{ start, end } \| null` (`end` inclusive) | +| `setView(view)`, `setDate(date)`, `next()`, `previous()`, `today()` | `void` | + +## Types + +The package exports `ForceCalendarProps`, `ForceCalendarHandle`, `CalendarEvent`, `CalendarView`, `VisibleRange`, `EventsSetOptions`, `EventsSetResult`, `ForceCalendarElement`, `ForceCalendarEventMap` and one `*Detail` type per callback. It also declares `forcecal-main` in `JSX.IntrinsicElements`, so the raw element is type-checked in JSX. + +The global `HTMLElementTagNameMap` mapping belongs to `@forcecalendar/interface` 1.7 and newer. Import its element type when using the raw DOM API. The adapter keeps its exported structural types, including plain `CalendarEvent` inputs, without redeclaring the global mapping: + +```ts +import type { ForceCalendarElement } from '@forcecalendar/interface'; + +const element: ForceCalendarElement = document.createElement('forcecal-main'); +``` -| Prop | Type | Maps to | -|---|---|---| -| `view` | `'month' \| 'week' \| 'day'` | `view` attribute | -| `date` | `Date \| string` | `date` attribute (Dates are serialized to ISO) | -| `locale` | `string` | `locale` attribute | -| `timezone` | `string` | `timezone` attribute (IANA name) | -| `weekStartsOn` | `0–6` | `week-starts-on` attribute | -| `height` | `string` | `height` attribute | -| `className`, `style` | — | passed through | +With interface 1.6, use the adapter's exported type explicitly for direct DOM access: `document.querySelector('forcecal-main')`. The React component and its ref keep the same types across supported interface versions. -## Callbacks -`onEventAdded`, `onEventUpdated`, `onEventDeleted`, `onDateSelect`, `onViewChange`, `onNavigate` — each receives the `detail` object of the corresponding `calendar-*` DOM event. -## Next.js / SSR +### Read-only calendars -The adapter is SSR-safe out of the box: the custom elements are registered client-side only (inside `useEffect`), so server rendering emits the tag without touching browser APIs. No `dynamic(() => ..., { ssr: false })` workaround is needed. +Pass the boolean `readOnly` prop to disable interactive editing (requires +`@forcecalendar/interface >= 1.8.0`). `false` or omission keeps editing enabled. +The adapter maps this to the `readonly` boolean attribute consistently during +server rendering, lazy element registration, and later prop changes. +Programmatic event methods remain available in read-only mode. diff --git a/content/docs/adapters/vue.mdx b/content/docs/adapters/vue.mdx index 71b356e..6d99f29 100644 --- a/content/docs/adapters/vue.mdx +++ b/content/docs/adapters/vue.mdx @@ -1,51 +1,148 @@ --- title: Vue -description: Use forceCalendar in Vue 3 and Nuxt with @forcecalendar/vue +description: Vue 0.3.1 props, typed events, snapshots, refs, SSR, and read-only integration. --- -`@forcecalendar/vue` is a thin adapter around the `` Web Component: props map to attributes, DOM events map to Vue emits. It has no dependencies beyond its peers. - -## Install +`@forcecalendar/vue` **0.3.1** maps props, typed emits and a template ref to the Interface Web Component. Install Vue 3.3 or later alongside: ```bash -npm install @forcecalendar/vue @forcecalendar/core @forcecalendar/interface +npm install @forcecalendar/vue@0.3.1 @forcecalendar/core@2.5.7 @forcecalendar/interface@1.9.0 ``` -## Use +## Use in Vue or Nuxt ```vue - ``` +SSR emits the tag and attributes; registration happens in `onMounted`. Browser methods still require readiness. A `` wrapper is not required solely for registration. Examples below use an application-provided `fetchEvents` function; implement authorization, errors and stale-response protection in your host. + ## Props -`view`, `date` (Date or string), `locale`, `timezone`, `weekStartsOn`, `height` — each maps to the corresponding element attribute. +| Prop | Type | Notes | +| --- | --- | --- | +| `view` | `'month' \| 'week' \| 'day'` | Rendered as the `view` attribute. | +| `date` | `Date \| string` | A `Date` is serialised with `toISOString()`. | +| `locale` | `string` | BCP 47 tag, e.g. `en-AU`. | +| `timezone` | `string` | IANA zone, e.g. `Australia/Sydney`. | +| `weekStartsOn` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6` | Rendered as `week-starts-on`. | +| `height` | `string` | Any CSS length. | +| `theme` | `'slds' \| string` | Built-in preset or a custom theme name. | +| `events` | `CalendarEvent[]` | Complete snapshot of the calendar's events; see below. | +| `removeMissingEvents` | `boolean` (default `true`) | Whether events absent from the next `events` snapshot are removed. | + +Anything else — `class`, `id`, `aria-*`, `data-*`, an object `:style` — falls through to ``. + +### The `events` prop + +The snapshot is applied imperatively with `element.setEvents(events, { removeMissing })` and is never rendered as an attribute. Details worth knowing: + +- Replace the array to update the calendar (`events.value = [...]`). The prop is watched by identity, not deeply; the element then diffs the snapshot, so unchanged events keep their instances and the view re-renders at most once. +- A snapshot load emits one `events-set` describing the change set (`added`, `updated`, `removed`, `unchanged`). It does **not** emit per-event `event-added` / `event-updated` / `event-deleted`; those fire for changes made through the element's own UI or `addEvent()` / `updateEvent()` / `deleteEvent()`. +- Keep the array in a `ref` or `computed`. An inline literal in the template is a new array every render, which re-applies the snapshot (cheap, but `events-set` fires with everything `unchanged`). +- `undefined` (or `null`) leaves the calendar's events alone — use it when you manage events through the exposed methods instead. Pass `[]` to clear. +- `removeMissingEvents` is read when the next snapshot is applied; toggling it alone does nothing. +- When the component mounts before `@forcecalendar/interface` has registered the element, the snapshot is stored on the element and applied as soon as it upgrades (interface 1.6.0 and later). + +### Styling caveat + +A string `style` attribute replaces the element's inline `style` wholesale and wipes the `--fc-*` theme tokens the calendar sets there. Use the object form (`:style="{ borderRadius: '8px' }"`), which Vue merges property by property. ## Emits -| Emit | Fired on | -|---|---| -| `event-added` | `calendar-event-added` | -| `event-updated` | `calendar-event-updated` | -| `event-deleted` | `calendar-event-deleted` | -| `date-select` | `calendar-date-select` | -| `view-change` | `calendar-view-change` | -| `navigate` | `calendar-navigate` | +All emits deliver the DOM event's `detail` as their single argument and are fully typed. Use the kebab-case spelling in templates (`@events-set`, `@range-change`, `@range-select`, …) or the `onXxx` form with `h()` / TSX. + +| Emit | Payload | Fires when | +| --- | --- | --- | +| `@navigate` | `{ action: 'next' \| 'previous' \| 'today' \| 'goto'; date: Date }` | The visible date changes through navigation. | +| `@view-change` | `{ view: CalendarView }` | The view switches. | +| `@date-select` | `{ date: Date }` | A day is selected. | +| `@range-select` | `{ start: Date; end: Date }` | A range is selected by dragging. | +| `@range-change` | `{ start: Date; end: Date; view: CalendarView; date: Date }` | The visible window changes — also once after mount with the initial window, so you can load data for it. | +| `@event-added` | `{ event: CalendarEvent }` | An event was added. | +| `@event-updated` | `{ event: CalendarEvent }` | An event was changed. | +| `@event-deleted` | `{ eventId: string }` | An event was removed. | +| `@events-set` | `EventsSetResult` | A snapshot was applied through `events` / `setEvents()`. | + +> **Listen through the emits, not the raw DOM events.** `` dispatches `calendar-range-change`, `calendar-events-set` and friends; because they are undeclared, a `@calendar-range-change` listener still falls through to the element and receives the raw `CustomEvent`, untyped, with the payload on `.detail`. That path is not supported and may stop working — use `@range-change`. + +The element's legacy `calendar-event-add` / `-update` / `-remove` aliases are intentionally not mapped: each operation already fires its `-added` / `-updated` / `-deleted` event, and mapping both would run every handler twice. + +## Methods (template ref) + +A template ref resolves to a `ForceCalendarHandle`: the element's public methods, the element itself, and `whenReady()`, which resolves once the component is mounted and `forcecal-main` is defined. Until then (or with an `@forcecalendar/interface` older than 1.6.0) a method call logs a warning and returns `undefined` rather than throwing. + +```vue + + + +``` + +| Method | Returns | +| --- | --- | +| `element` | The `` element (`null` before mount). | +| `whenReady()` | `Promise` | +| `addEvent(event)` | `CalendarEvent \| null` | +| `updateEvent(id, updates)` | `CalendarEvent \| null` | +| `deleteEvent(id)` | `boolean` | +| `getEvents()` | `CalendarEvent[]` | +| `setEvents(events, { removeMissing? })` | `EventsSetResult \| null` (null when applied before initialisation) | +| `getVisibleRange()` | `{ start: Date; end: Date } \| null` | +| `setView(view)`, `setDate(date)`, `next()`, `previous()`, `today()` | `void` | + +`$el` still resolves to the element for anyone relying on the Vue instance API. + +## Types + +Registering the component globally (`app.component('ForceCalendar', ForceCalendar)`) is typed in templates through Vue's `GlobalComponents` augmentation. The package also exports `CalendarEvent`, `CalendarView`, `CalendarTheme`, `VisibleRange`, `EventsSetOptions`, `EventsSetResult`, `EventsSetUpdate`, `ForceCalendarElement`, `ForceCalendarEventMap`, `ForceCalendarHandle` and one `*Detail` type per emit. Event inputs remain structural, so plain objects are accepted. + +`@forcecalendar/interface` 1.7 and later owns the global `HTMLElementTagNameMap` declaration for `forcecal-main`. Load its declarations with `import type {} from '@forcecalendar/interface'` to get automatic typing for direct DOM queries. The adapter does not redeclare it, avoiding conflicts when both packages' types are loaded. When using interface 1.6, import `ForceCalendarElement` from this adapter and annotate direct DOM queries explicitly, for example `document.querySelector('forcecal-main')`. Component props, emits and template refs keep the same types on both versions. + +## SSR + +`renderToString` produces `` with its attributes; the custom elements are registered on the client by importing `@forcecalendar/interface` in `onMounted`. If that import fails the error is logged with a `[@forcecalendar/vue]` prefix instead of being swallowed. + +## Upgrading from 0.2 + +- `@forcecalendar/interface` 1.6.0 or later is required. +- Emits are declared as an object with typed validators. Templates that relied on the previous string-array typing to accept arbitrary `onXxx` listeners or payload types now get type errors — the intended fix is to use one of the nine emits above. +- `@range-change`, `@range-select` and `@events-set` are new (in 0.2 they silently never fired). + -Each emit receives the DOM event's `detail` object. -## Nuxt / SSR +### Read-only calendars -The adapter is SSR-safe: custom elements register client-side only (inside `onMounted`), so server rendering emits the tag without touching browser APIs. No `` wrapper is needed. +Pass the boolean `readOnly` prop to disable interactive editing (requires +`@forcecalendar/interface >= 1.8.0`). `false` or omission keeps editing enabled. +The adapter maps this to the `readonly` boolean attribute consistently during +server rendering, lazy element registration, and later prop changes. +Programmatic event methods remain available in read-only mode. diff --git a/content/docs/agent/index.mdx b/content/docs/agent/index.mdx new file mode 100644 index 0000000..4598129 --- /dev/null +++ b/content/docs/agent/index.mdx @@ -0,0 +1,49 @@ +--- +title: Headless Calendar APIs +description: Embed calendar logic behind application and agent-facing tools with your own data and access controls. +--- + +## Calendar logic for your application + +forceCalendar is a calendar library. An application can expose its headless APIs through its existing backend, plugin or tool interface. An agent can then be an authorized client of those calendar operations, just as a human-facing application is. + +The integrator decides storage, users, authentication, permissions and the actions an agent may request. Core manages calendar data and calculations; the agent decides what to do with that information. Event descriptions are data, not instructions or authorization to execute work. + +## Published building blocks + +Core **2.5.7** is ESM, has no runtime package dependencies, and supplies these public APIs without requiring a calendar UI: + +| Operation | Library API | +| --- | --- | +| Read stored event data | `Calendar.getEvent()`, `getEvents()`, `getEventsInRange()` | +| Load an application snapshot | `Calendar.reconcileEvents()` | +| Create or change a calendar entry | `Calendar.addEvent()`, `updateEvent()`, `removeEvent()` | +| Search events | `EventSearch` or `EnhancedCalendar.search()` | +| Check conflicts | `ConflictDetector` and EventStore conflict methods | +| Query recurring occurrences | `RecurrenceEngine` / `RecurrenceEngineV2` lazy and bounded expansion APIs | +| Import/export iCalendar data | `ICSParser` and `ICSHandler` | + +See the [API reference](/docs/api), [server-side examples](/docs/core/server-side), and [snapshot contract](/docs/core/calendar#snapshot-reconciliation). A Core Calendar/EventStore is in-memory; choose and implement persistence in the host application. + +## Put tools behind the host's authorization + +A typical integration flow is: + +1. Resolve the caller using the host application's authentication. +2. Check which calendar data and operation that caller may access. +3. Load a bounded, authorized event snapshot into Core. +4. Run the requested read or validated calendar mutation. +5. Persist an authorized change through the application's data layer, with its concurrency and audit controls. +6. Return calendar data or an explicit error; destroy owned Core instances when finished. + +This is an integration pattern, not a bundled server implementation. Core does not make external persistence happen when a local mutation method returns. Keep IDs stable, reject stale writes in the data layer, and never treat a UI callback or agent request as proof of authorization. + +## Dates, recurrence and exports + +Use explicit input contracts for timed instants and all-day civil dates. V2's named-zone recurrence behavior, legacy conversion metadata, moved-instance bounds and ancient-date limits are described in the [timezone contract](/docs/core/recurrence#v2-timezone-contract). + +Bound queries and expansions to the workload. An ICS import/export is a data interchange operation; any live account integration belongs in an authorized application adapter. URL imports require the [host network controls](/docs/security/model#network-limits-matter) described in the security guide. + +## Optional human interface + +Use [Interface](/docs/interface), [React](/docs/adapters/react), or [Vue](/docs/adapters/vue) when the application needs a visual calendar. The [Salesforce integration](/docs/salesforce) supplies its own LWC/Apex adapter. Human and agent clients can operate on the application's calendar data according to the same server-side permission rules. diff --git a/content/docs/agent/meta.json b/content/docs/agent/meta.json new file mode 100644 index 0000000..953526a --- /dev/null +++ b/content/docs/agent/meta.json @@ -0,0 +1 @@ +{"title":"Headless Integrations","pages":["index"]} diff --git a/content/docs/api/calendar.mdx b/content/docs/api/calendar.mdx index 58551ee..03f2132 100644 --- a/content/docs/api/calendar.mdx +++ b/content/docs/api/calendar.mdx @@ -52,7 +52,9 @@ new Calendar(config?: CalendarConfig) | `removeEvent` | `(eventId: string) => void` | — | | `getEvent` | `(eventId: string) => Event \| undefined` | Event or undefined | | `getEvents` | `() => Event[]` | All events | -| `setEvents` | `(events: EventData[]) => void` | — | +| `setEvents` | `(events: EventData[], options?: SetEventsOptions) => EventsSetPayload` | Snapshot change set | +| `reconcileEvents` | `(events: EventData[], options?: ReconcileOptions) => EventsSetPayload` | Reconciled change set | +| `getEventsVersion` | `() => number` | Store change counter | | `queryEvents` | `(filters: QueryFilters) => Event[]` | Filtered events | | `getEventsForDate` | `(date: Date, timezone?: string) => Event[]` | Events on date | | `getEventsInRange` | `(start: Date, end: Date, timezone?: string) => Event[]` | Events in range | @@ -123,3 +125,5 @@ new Calendar(config?: CalendarConfig) | `state` | `StateManager` | Internal state manager | | `timezoneManager` | `TimezoneManager` | Timezone singleton | | `config` | `object` | Resolved configuration | + +Core `setEvents()` clears/reloads by default; opt into `{ reconcile: true }` or call `reconcileEvents()`. Interface snapshots reconcile by default. See [snapshot reconciliation](/docs/core/calendar#snapshot-reconciliation). diff --git a/content/docs/api/event-store.mdx b/content/docs/api/event-store.mdx index 04ada46..cf64cf4 100644 --- a/content/docs/api/event-store.mdx +++ b/content/docs/api/event-store.mdx @@ -94,3 +94,16 @@ new EventStore(options?: { timezone?: string }) | Method | Signature | Returns | |--------|-----------|---------| | `destroy` | `() => void` | — | + +## Snapshot and occurrence APIs + +| Method | Purpose | +| --- | --- | +| `reconcile(events, options?)` | Diff a complete snapshot; return added/updated/removed/unchanged | +| `resolveEventId(id)` | Resolve an occurrence identity to its master | +| `getOccurrence(id, timezone?)` | Read the specific occurrence | +| `iterateOccurrences(eventId, options?)` | Lazy single-use occurrence generator | +| `getNextOccurrence(eventId, after?, options?)` | Next occurrence or null | +| `takeOccurrences(eventId, count, options?)` | Bounded next occurrences | + +Reconcile options include `removeMissing` and `isEquivalent`. Lazy options include exclusive `after`/`before` bounds, `inclusive` and `timezone`. See the [store guide](/docs/core/event-store#reconciliation-and-stable-occurrence-identity). diff --git a/content/docs/api/event.mdx b/content/docs/api/event.mdx index 1a7fad1..29d7fd4 100644 --- a/content/docs/api/event.mdx +++ b/content/docs/api/event.mdx @@ -20,7 +20,7 @@ See [Event Model](/docs/core/event) for the full list of constructor properties. | Method | Signature | Returns | |--------|-----------|---------| | `normalize` | `(data: object) => EventData` | Normalized event data | -| `validate` | `(data: object) => { valid: boolean, errors: string[] }` | Validation result | +| `validate` | `(data: EventData) => void` | Throws on invalid normalized data | | `fromObject` | `(obj: object) => Event` | Event instance | ## Getters @@ -92,3 +92,13 @@ See [Event Model](/docs/core/event) for the full list of constructor properties. | `hasCategory` | `(category: string) => boolean` | — | | `hasAnyCategory` | `(categories: string[]) => boolean` | — | | `hasAllCategories` | `(categories: string[]) => boolean` | — | + +## Snapshot and occurrence helpers + +| Static method | Purpose | +| --- | --- | +| `isEquivalent(a, b)` | Compare supported event fields for reconciliation | +| `occurrenceId(masterId, start)` | Generate stable occurrence identity | +| `parseOccurrenceId(id)` | Parse occurrence identity | + +Core 2.5.6 preserves `endTimeZone` through `clone()`/`toObject()`. See [full-field snapshots](/docs/core/event#full-field-snapshots-and-serialization). diff --git a/content/docs/api/ics-handler.mdx b/content/docs/api/ics-handler.mdx index c97ba71..299de25 100644 --- a/content/docs/api/ics-handler.mdx +++ b/content/docs/api/ics-handler.mdx @@ -40,7 +40,7 @@ new ICSHandler(calendar: Calendar) |--------|------|---------| | `dateRange` | `{ start: Date, end: Date }` | `undefined` | | `categories` | `string[]` | `[]` | -| `calendarName` | `string` | `'Calendar'` | +| `calendarName` | `string` | `'Lightning Calendar Export'` | | `includeRecurring` | `boolean` | `true` | | `expandRecurring` | `boolean` | `false` | @@ -57,3 +57,9 @@ new ICSHandler(calendar: Calendar) ```ts { valid: boolean, errors: string[], eventCount: number } ``` + +## URL transport options + +`importFromURL(url, options)` additionally accepts `requestTimeout` (default 30,000 ms), `maxRedirects` (5), and `maxFileSize` (parser default). This helper performs network requests and does not replace host egress controls. See [network boundaries](/docs/security/model#network-limits-matter). + +Keep the object returned by `subscribe()` and call `stop()` on teardown. Its initial import is asynchronous, and the subscription is in-process rather than durable provider synchronization. diff --git a/content/docs/api/index.mdx b/content/docs/api/index.mdx index 3102813..a4ac33b 100644 --- a/content/docs/api/index.mdx +++ b/content/docs/api/index.mdx @@ -26,6 +26,7 @@ Unless marked internal, these classes are exported from `@forcecalendar/core`: | Class | Import | Description | |-------|--------|-------------| | [RecurrenceEngine](/docs/api/recurrence-engine) | `@forcecalendar/core` | Static recurrence expansion | +| [RecurrenceEngineV2](/docs/api/recurrence-engine-v2) | `@forcecalendar/core` | Instance expansion and modified occurrences | | [RRuleParser](/docs/api/rrule-parser) | `@forcecalendar/core` | RRULE string parsing | ### ICS @@ -46,12 +47,15 @@ Unless marked internal, these classes are exported from `@forcecalendar/core`: | Class | Import | Description | |-------|--------|-------------| | [EventSearch](/docs/api/event-search) | `@forcecalendar/core` | Full-text search | +| [InvertedIndex](/docs/api/inverted-index) | `@forcecalendar/core` | Synchronous search index | | [SearchWorkerManager](/docs/api/search-worker-manager) | `@forcecalendar/core` | Web Worker search | ### Utilities | Class | Import | Description | |-------|--------|-------------| +| [TimezoneManager](/docs/api/timezone-manager) | `@forcecalendar/core` | Timezone operations | +| [ConflictDetector](/docs/api/conflict-detector) | `@forcecalendar/core` | Event conflict analysis | | [DateUtils](/docs/api/date-utils) | `@forcecalendar/core` | Static date utilities | ### Performance (internal) @@ -67,7 +71,7 @@ These classes are documented for reference but are **not** exported from the `@f ```js // Everything from main entry -import { Calendar, Event, EventStore, ... } from '@forcecalendar/core'; +import { Calendar, Event, EventStore, TimezoneManager, ConflictDetector } from '@forcecalendar/core'; // Specific subpath exports import { Calendar } from '@forcecalendar/core/calendar'; diff --git a/content/docs/api/recurrence-engine-v2.mdx b/content/docs/api/recurrence-engine-v2.mdx index 7b68ada..4615402 100644 --- a/content/docs/api/recurrence-engine-v2.mdx +++ b/content/docs/api/recurrence-engine-v2.mdx @@ -22,3 +22,9 @@ const occurrences = engine.expandEvent(event, rangeStart, rangeEnd, { | `addModifiedInstance(eventId, occurrenceDate, modifications)` | Override a single occurrence (reschedule, retitle) without breaking the series | | `findDSTTransitions(start, end, timezone)` | DST transitions within a range | | `adjustForDST(start, end, timezone, transitions)` | Wall-clock correction across transitions | + +## Lazy series queries and release boundary + +`iterateOccurrences(event, options?)`, `nextOccurrence(event, after?, options?)` and `takeOccurrences(event, count, options?)` are instance methods. See [bounds and semantics](/docs/core/recurrence#lazy-occurrence-queries). + +Core 2.5.7 corrects [V2 named-zone stepping](/docs/core/recurrence#v2-timezone-contract). Legacy UTC metadata, moved-instance query bounds and ancient-date limits remain. Modified instances and timezone support do not imply universal recurrence conformance. diff --git a/content/docs/api/recurrence-engine.mdx b/content/docs/api/recurrence-engine.mdx index 4c248ef..8750ad2 100644 --- a/content/docs/api/recurrence-engine.mdx +++ b/content/docs/api/recurrence-engine.mdx @@ -44,3 +44,13 @@ Each expanded occurrence has: | `isOccurrence` | `boolean` | Always `true` | | `originalEventId` | `string` | ID of the recurring event | | `occurrenceDate` | `Date` | Date of this occurrence | + +## Lazy series queries + +| Static method | Result | +| --- | --- | +| `iterateOccurrences(event, options?)` | Single-use generator | +| `nextOccurrence(event, after?, options?)` | Next occurrence or `null` | +| `takeOccurrences(event, count, options?)` | Bounded occurrence array | + +Options include `after`, `before`, `inclusive` and `timezone`. Bounds are exclusive unless `inclusive: true`. Use `nextOccurrence` for COUNT/UNTIL/exception-aware queries; the older `getNextOccurrence` only steps a rule. See [lazy occurrence semantics](/docs/core/recurrence#lazy-occurrence-queries). diff --git a/content/docs/core/calendar.mdx b/content/docs/core/calendar.mdx index a4f69ed..40ec574 100644 --- a/content/docs/core/calendar.mdx +++ b/content/docs/core/calendar.mdx @@ -453,3 +453,17 @@ calendar.destroy(); ``` Emits: `destroy`. + +## Snapshot reconciliation + +Core `setEvents(rows)` keeps its clear-and-reload default. Use `setEvents(rows, { reconcile: true })` or `reconcileEvents(rows, options)` to retain unchanged Event instances and apply only differences. Interface `setEvents()` already uses reconciliation by default. + +```js +const result = calendar.reconcileEvents(rows, { removeMissing: true }); +// { events, added, updated: [{ event, oldEvent }], removed, unchanged } +const version = calendar.getEventsVersion(); +``` + +A snapshot emits one `eventsSet` with the change set and no `eventAdd`/`eventUpdate`/`eventRemove` callbacks. `removeMissing: false` preserves absent records. `isEquivalent` optionally replaces the comparison function. Duplicate IDs and invalid reconciliation input fail before mutation; occurrence references retain their master. + +`getEventsVersion()` reads the store's change counter. `destroy()` releases the owned EventStore and its maintenance resources; call it in server `finally` blocks or when permanently disposing an instance. diff --git a/content/docs/core/event-store.mdx b/content/docs/core/event-store.mdx index 843129c..3e73e20 100644 --- a/content/docs/core/event-store.mdx +++ b/content/docs/core/event-store.mdx @@ -300,3 +300,11 @@ const occurrences = store.expandRecurringEvent( ### destroy() Clean up subscriptions and internal state. + +## Reconciliation and stable occurrence identity + +`store.reconcile(rows, { removeMissing: true })` retains unchanged objects and returns `{ added, updated, removed, unchanged }`. Updated entries have `{ event, oldEvent }`. It emits one batch notification when changes exist. Invalid data/duplicate IDs are validated before mutation; if the caller already opened a batch, the caller owns rollback on an application/comparator failure. + +An occurrence reference resolves to its recurring master. `resolveEventId(id)` resolves the identity; `getOccurrence(id, timezone?)` retrieves a specific occurrence. Do not write a clicked occurrence back as a replacement master. + +The store uses `RecurrenceEngineV2` by default and exposes lazy `iterateOccurrences(eventId, options)`, `getNextOccurrence(eventId, after, options)`, and `takeOccurrences(eventId, count, options)`. See [recurrence bounds and limitations](/docs/core/recurrence#lazy-occurrence-queries). diff --git a/content/docs/core/event.mdx b/content/docs/core/event.mdx index 8f4641b..df6344b 100644 --- a/content/docs/core/event.mdx +++ b/content/docs/core/event.mdx @@ -11,6 +11,7 @@ The `Event` class represents a calendar event with full timezone support, attend import { Event } from '@forcecalendar/core'; const event = new Event({ + id: 'team-meeting', title: 'Team Meeting', start: new Date('2026-03-01T09:00:00'), end: new Date('2026-03-01T10:00:00'), @@ -22,9 +23,9 @@ const event = new Event({ | Property | Type | Default | Description | |----------|------|---------|-------------| -| `id` | `string` | `''` | Unique identifier (not auto-generated; assign your own) | -| `title` | `string` | `''` | Event title | -| `start` | `Date` | `new Date()` | Start date/time | +| `id` | `string` | Required | Unique identifier (not auto-generated by Event; assign your own) | +| `title` | `string` | Required | Non-empty event title | +| `start` | `Date \| string` | Required | Valid start date/time | | `end` | `Date` | Same as `start` | End date/time | | `allDay` | `boolean` | `false` | All-day event flag | | `description` | `string` | `''` | Event description | @@ -65,20 +66,17 @@ const normalized = Event.normalize({ ### Event.validate(data) -Validate event data. Returns `{ valid: boolean, errors: string[] }`. +Validate normalized event data. Returns `void` and **throws an Error** on failure. ```js -const result = Event.validate({ title: '', start: 'invalid' }); -// { valid: false, errors: ['Title is required', 'Invalid start date'] } +const normalized = Event.normalize({ + id: 'meeting', title: 'Meeting', + start: new Date(2026, 9, 5, 9), end: new Date(2026, 9, 5, 10), +}); +Event.validate(normalized); ``` -Validation checks: -- Title must be a non-empty string -- Start must be a valid Date -- End must be a valid Date -- End must be after start (unless all-day) -- Status must be one of the valid values -- Visibility must be one of the valid values +IDs, titles and start dates are required. Dates must be valid, and an end cannot precede the start (equal instants are allowed). Recurring events require a rule. Attendees and metadata also have validation/size constraints. Normalization defaults invalid status/visibility to supported values; it does not generate an ID. The constructor normalizes and validates its input. ### Event.fromObject(obj) @@ -275,3 +273,11 @@ event.hasAnyCategory(['meeting', 'review']); // true if has either ### hasAllCategories(categories) Returns `true` if the event has all of the given categories. + +## Full-field snapshots and serialization + +Core 2.5.6 retains `endTimeZone` in `clone()` and `toObject()`, alongside supported event fields. `Event.isEquivalent(a, b)` compares the fields used by reconciliation, including nested host data; it is distinct from relying on object identity. + +All-day normalization uses local start-of-day through the inclusive last day's 23:59:59.999. Use civil-date parsing at an external adapter boundary; see [date migration](/docs/migration#dates-and-timezones). + +Occurrence Events produced by EventStore carry `isOccurrence`, `recurringEventId` and `occurrenceStart`, with stable IDs derived from the master and occurrence instant. `Event.occurrenceId(masterId, start)` and `Event.parseOccurrenceId(id)` support that identity contract. Reconciliation resolves occurrence references back to their master rather than replacing a recurring series with one visible instance. diff --git a/content/docs/core/ics.mdx b/content/docs/core/ics.mdx index 290f861..2280578 100644 --- a/content/docs/core/ics.mdx +++ b/content/docs/core/ics.mdx @@ -26,7 +26,7 @@ The parser enforces hard limits to prevent denial-of-service from oversized inpu | Maximum line count | 100,000 lines | | Maximum event count | 10,000 events | -Inputs exceeding these limits throw an error before parsing begins. +Oversized input is rejected before parsing; line/event limits are checked as the input is processed. ### parse(icsString) @@ -106,7 +106,7 @@ The parser handles three RFC 5545 date formats: ## ICSHandler -High-level ICS operations built on top of `ICSParser`. Requires a `Calendar` or `EventStore` instance. +High-level ICS operations built on top of `ICSParser`. Requires a `Calendar`-compatible instance implementing its event methods; use a `Calendar` for the documented integration. ```js import { ICSHandler } from '@forcecalendar/core'; @@ -238,3 +238,9 @@ handler.downloadAsFile('monthly-calendar.ics', { calendarName: 'Monthly Export', }); ``` + +## URL imports and external clients + +`importFromURL` performs network requests, unlike pure parsing. Options include `requestTimeout` (30,000 ms), `maxRedirects` (5) and `maxFileSize` (parser default). Node validates DNS and redirect destinations; non-Node runtimes can follow redirects before final-response validation. Host egress/allowlisting remains necessary. See [network limits](/docs/security/model#network-limits-matter). + +`subscribe()` initially imports and can poll; retain its handle and call `stop()` during teardown. This is an in-process import helper, not a durable two-way provider sync service. An ICS file export/import is a snapshot, and all-day exclusive end dates need explicit conversion at an integration boundary. diff --git a/content/docs/core/index.mdx b/content/docs/core/index.mdx index a19eb7f..d571d43 100644 --- a/content/docs/core/index.mdx +++ b/content/docs/core/index.mdx @@ -5,7 +5,7 @@ description: Architecture and module overview for @forcecalendar/core. ## Architecture -`@forcecalendar/core` is the headless calendar engine. It has zero runtime dependencies and ships as pure ESM JavaScript. The primary public classes are exported from the main entry point at `core/index.js`; some supporting classes -- `LRUCache`, `PerformanceOptimizer`, `AdaptiveMemoryManager`, `TimezoneManager`, `TimezoneDatabase`, and `ConflictDetector` -- are internal and not exported from the package. +`@forcecalendar/core` is the headless calendar engine. It has zero runtime dependencies and ships as pure ESM JavaScript. The primary public classes are exported from the main entry point at `core/index.js`; some supporting classes -- `LRUCache`, `PerformanceOptimizer`, `AdaptiveMemoryManager`, and `TimezoneDatabase` -- are internal and not exported from the package. ``` core/ @@ -55,14 +55,14 @@ The `Calendar` class is the primary entry point. It composes: The `EventStore` uses: -- **RecurrenceEngine** for expanding recurring events in date ranges +- **RecurrenceEngineV2** by default for expanding recurring events in date ranges - **ConflictDetector** for overlap detection and busy/free period calculation ## Key Design Decisions -**No DOM dependency.** The entire core package runs in any JavaScript environment -- browser, Node.js, Web Worker, or Salesforce Locker Service. There are no references to `document`, `window`, or any browser API except `Intl.DateTimeFormat` for timezone support and `performance.memory` (with graceful fallback) for memory monitoring. +**Headless computation.** Core calendar computation can run without a DOM. Browser download helpers, optional Workers and URL-based ICS operations require their host APIs. Test runtime/bundler capabilities for the features you use; the Salesforce Web Component integration requires LWS. -**O(1) event lookups.** EventStore maintains five indices (`byDate`, `byMonth`, `recurring`, `byCategory`, `byStatus`) backed by `Map>`. Adding or removing an event updates all indices in constant time. +**O(1) event lookups.** EventStore maintains five indices (`byDate`, `byMonth`, `recurring`, `byCategory`, `byStatus`) backed by `Map>`. Direct ID lookup is constant-time; date indexing, recurrence and batch work depend on the event span and workload. **Immutable state transitions.** StateManager creates a new state object on every `setState()` call. Nested objects (`filters`, `businessHours`, `metadata`) are shallow-merged. All state snapshots are deep-cloned before being added to undo/redo history. diff --git a/content/docs/core/recurrence.mdx b/content/docs/core/recurrence.mdx index 020b11a..169cb6e 100644 --- a/content/docs/core/recurrence.mdx +++ b/content/docs/core/recurrence.mdx @@ -13,7 +13,7 @@ forceCalendar provides three classes for recurrence handling: | `RecurrenceEngine` | Static | Expand recurring events into occurrences | | `RecurrenceEngineV2` | Instance | Enhanced expansion with modified instances, DST, caching | -All three follow RFC 5545 (iCalendar) for recurrence rule syntax and semantics. +These implement RFC 5545 recurrence features with bounded expansion. Supported syntax is not a claim that every RFC combination or host timezone is fully conformant. ## RRuleParser @@ -113,6 +113,7 @@ Expand a recurring event into occurrences within a date range. import { RecurrenceEngine } from '@forcecalendar/core'; const event = { + id: 'weekly', title: 'Weekly review', recurring: true, start: new Date('2026-01-05T09:00:00'), end: new Date('2026-01-05T10:00:00'), recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO;COUNT=12', @@ -128,11 +129,7 @@ const occurrences = RecurrenceEngine.expandEvent( // Returns array of Event-like objects, each with adjusted start/end dates ``` -Each occurrence is a clone of the original event with: -- `start` and `end` adjusted to the occurrence date -- `isOccurrence: true` flag -- `originalEventId` referencing the recurring event -- `occurrenceDate` for this specific occurrence +The engine returns occurrence data with adjusted `start`/`end` and recurrence metadata such as `recurringEventId`. EventStore wraps rendered occurrences as Event objects carrying `isOccurrence`, `recurringEventId` and `occurrenceStart`. Do not conflate the raw engine result with a stored master or its rendered occurrence ID. Regardless of the `maxOccurrences` parameter, expansion is capped by a `MAX_OCCURRENCES_HARD_LIMIT` of **10,000** to prevent runaway expansion from malformed or unbounded rules. @@ -254,3 +251,62 @@ FREQ=YEARLY;BYMONTH=11;BYDAY=4TH 4th Thursday in November FREQ=WEEKLY;BYDAY=MO;COUNT=10 10 Mondays FREQ=DAILY;UNTIL=20261231T235959Z Daily until end of 2026 ``` + +## Lazy occurrence queries + +Both engines expose `iterateOccurrences(event, options)`, `nextOccurrence(event, after, options)` and `takeOccurrences(event, count, options)`. Use static methods on `RecurrenceEngine` or instance methods on `RecurrenceEngineV2`. + +```js +import { RecurrenceEngine } from '@forcecalendar/core'; +const event = { + id: 'daily', title: 'Daily check', recurring: true, + start: new Date('2026-10-05T09:00:00Z'), end: new Date('2026-10-05T09:15:00Z'), + timeZone: 'UTC', recurrenceRule: 'FREQ=DAILY;COUNT=3', +}; +const nextTwo = RecurrenceEngine.takeOccurrences(event, 2, { + after: event.start, inclusive: true, timezone: 'UTC', +}); +``` + +`after` and `before` are exclusive by default; `inclusive: true` includes both bounds. Bounds are valid Dates or timestamps. A generator is single-use. `takeOccurrences` caps the requested count at the 10,000 hard limit; bound consumers of an open-ended iterator explicitly. + +`nextOccurrence(event, after, options)` honors the complete event's COUNT/UNTIL/exceptions. The older `getNextOccurrence(currentDate, rule, timezone)` helper only steps a parsed rule and does not provide those series-query guarantees. + +EventStore exposes `iterateOccurrences(eventId, options)`, `getNextOccurrence(eventId, after, options)` and `takeOccurrences(eventId, count, options)` against stored masters, using V2 by default. + +## V2 timezone contract + +Core **2.5.7** corrects V2's cross-host DST stepping. `options.timezone`, then `event.timeZone`, then UTC determines the calendar-step zone; `event.start` anchors the series. Generated `start` and `end` are occurrence instants. This covers V2, the default engine used by EventStore, and V2-based EnhancedCalendar expansion. + +```js +import { RecurrenceEngineV2 } from '@forcecalendar/core'; +const event = { + id: 'ny-daily', title: 'Daily review', recurring: true, + start: new Date('2026-03-06T09:00:00-05:00'), + end: new Date('2026-03-06T09:30:00-05:00'), + timeZone: 'America/New_York', recurrenceRule: 'FREQ=DAILY;COUNT=5', +}; +const occurrences = new RecurrenceEngineV2().takeOccurrences(event, 5, { + after: event.start, inclusive: true, timezone: 'America/New_York', +}); +console.log(occurrences.map(({ start }) => start.toISOString())); +// March 6–7 at 14:00Z, March 8–10 at 13:00Z: 09:00 in New York. +``` + +- Daily, weekly, monthly, yearly and subdaily calendar steps resolve in the recurrence zone. A nonexistent local time advances through the DST gap; that adjusted clock time carries forward. An overlap chooses the earlier instant. Subdaily calendar increments are not constant elapsed-duration increments. +- Before instance overrides, duration remains the original `end - start` milliseconds. An override can change duration. +- Floating RRULE `UNTIL`/`EXDATE` strings use the recurrence zone; `Z` values remain UTC. Date-based exception and modification matching uses that zone's calendar date. +- `handleDST` is retained for compatibility; `true` and `false` now yield the same resolved instants. The option never selected elapsed-duration stepping. +- Explicit invalid zones are rejected by helpers such as `getNextDate`; omitted zones retain local Date behavior in those helpers. + +The release was checked with 696 fixture/host combinations, plus the published package's runtime verification. That is scoped regression evidence, not complete RFC conformance. See the [released contract](https://github.com/forceCalendar/core/blob/v2.5.7/docs/recurrence-timezones.md). + +### Remaining limits + +`startUTC`/`endUTC` metadata still uses legacy conversions that may depend on the host timezone. Use `start`/`end` for the occurrence instants covered above. The legacy static `RecurrenceEngine` and every timezone API have not been redesigned. + +Year 0000/BCE offset handling and UTC-suffixed RRULE years 0000–0099 retain known limitations. Month-end selection and supported RRULE fields are unchanged. Interface still disables built-in recurring edit/delete/drag/resize until an explicit series/occurrence workflow is supported. + +## Modified-instance query boundary + +V2 range filtering applies to the generated recurrence start before a stored start/end override is applied. A moved instance can therefore appear outside the requested window or change the returned ordering/duration. This is existing behavior: hosts requiring final-time filtering or ordering must apply that policy explicitly. Do not infer that every modified result remains bounded and sorted from the base iterator contract. diff --git a/content/docs/core/search.mdx b/content/docs/core/search.mdx index 21f7d08..3e0b2aa 100644 --- a/content/docs/core/search.mdx +++ b/content/docs/core/search.mdx @@ -133,7 +133,7 @@ Force a rebuild of the search index. Called automatically when events change. ## SearchWorkerManager -Offloads search to a Web Worker for non-blocking performance on large datasets. Falls back to `InvertedIndex` on the main thread if Web Workers are unavailable (e.g., Salesforce Locker Service). +Offloads search to a Web Worker for non-blocking performance on large datasets. Falls back to `InvertedIndex` on the main thread if Web Workers are unavailable (for example, a host that blocks Workers). ```js import { SearchWorkerManager } from '@forcecalendar/core'; diff --git a/content/docs/core/server-side.mdx b/content/docs/core/server-side.mdx index d934c0c..3d1ff9e 100644 --- a/content/docs/core/server-side.mdx +++ b/content/docs/core/server-side.mdx @@ -1,68 +1,59 @@ --- title: Server-Side Scheduling -description: Use the headless engine in Node, serverless functions, and edge runtimes +description: Bounded recurrence, ICS, cleanup, and runtime checks for headless applications. --- -`@forcecalendar/core` is DOM-free, dependency-free, pure ES modules — it runs anywhere JavaScript does, including runtimes that ban `eval` and dynamic code such as Cloudflare Workers. That makes it a server-side scheduling engine, not just a UI companion. +Core computation runs without a DOM and ships as ESM with no runtime package dependencies. That is useful in Node.js, serverless functions and compatible edge runtimes. Optional network/Worker/browser helpers still depend on their host APIs; verify the actual runtime and bundler. -## What you can do server-side - -- **Expand recurrence rules** — turn an RFC 5545 RRULE into concrete occurrences for a date range -- **Detect conflicts** — time, attendee, and resource overlap across event sets -- **Import/export ICS** — parse calendar feeds and generate iCalendar files -- **Search** — full-text queries across large event sets - -## Expanding recurrences in Node +## Bounded recurrence queries ```js import { RecurrenceEngine } from '@forcecalendar/core'; -const occurrences = RecurrenceEngine.expandEvent( - { - id: 'standup', - title: 'Team Standup', - start: new Date('2026-01-05T09:00:00Z'), - end: new Date('2026-01-05T09:15:00Z'), - recurring: true, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR', - timeZone: 'America/New_York' - }, - new Date('2026-01-01'), - new Date('2026-03-31'), - 500 // max occurrences -); -// -> [{ start, end, recurringEventId, timezone, originalStart }, ...] +const event = { + id: 'standup', title: 'Standup', + start: new Date('2026-10-05T09:00:00Z'), + end: new Date('2026-10-05T09:15:00Z'), + recurring: true, recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=12', + timeZone: 'UTC', +}; +const upcoming = RecurrenceEngine.takeOccurrences(event, 3, { + after: new Date('2026-10-01T00:00:00Z'), + before: new Date('2026-11-01T00:00:00Z'), + timezone: 'UTC', +}); ``` -Daily and weekly rules use a numeric fast path (pure timestamp arithmetic between DST transitions), so expanding multi-year series takes fractions of a millisecond — safe inside request handlers. +This example deliberately uses UTC. Lazy queries avoid materializing an entire open-ended series, but still require explicit workload bounds. Do not assume every recurrence completes within a fixed request budget. See [iterator semantics](/docs/core/recurrence#lazy-occurrence-queries) and the [V2 timezone contract and limits](/docs/core/recurrence#v2-timezone-contract). -## Generating ICS +## Generate an ICS snapshot ```js import { Calendar, ICSHandler } from '@forcecalendar/core'; const calendar = new Calendar({ timeZone: 'UTC' }); -calendar.addEvent({ - id: 'evt-1', - title: 'Quarterly Review', - start: new Date('2026-04-01T14:00:00Z'), - end: new Date('2026-04-01T15:00:00Z'), - recurring: true, - recurrenceRule: 'FREQ=MONTHLY;INTERVAL=3;COUNT=4' -}); - -const ics = new ICSHandler(calendar).export({ calendarName: 'Reviews' }); -// -> "BEGIN:VCALENDAR..." — serve with Content-Type: text/calendar +try { + calendar.addEvent({ + id: 'review', title: 'Review', + start: new Date('2026-10-05T14:00:00Z'), + end: new Date('2026-10-05T15:00:00Z'), + }); + const ics = new ICSHandler(calendar).export({ calendarName: 'Reviews' }); + console.log(ics); // Serve as text/calendar using your host's response API. +} finally { + calendar.destroy(); +} ``` -## Edge runtimes +Always dispose owned Calendars/EventStores when finished, especially in jobs and tests. Stop any independent ICS subscription handles as well. File download helpers need a browser; return the ICS string from server routes instead. -The engine deploys to Cloudflare Workers unmodified — see the runnable -[Worker example](https://github.com/forcecalendar/examples/tree/main/cloudflare-worker-scheduling), -which exposes `/occurrences` and `/ics` endpoints and includes a smoke test. +## Deployment responsibilities -## Timezones +- Authorize access to calendar data before queries, exports or mutations. +- Bound input size, event count, expansion windows, concurrency and response size. +- Set egress policies for [ICS URL imports](/docs/security/model#network-limits-matter). +- Test host/event timezone combinations and DST transitions; use occurrence `start`/`end` according to the engine's documented contract. +- Keep IDs stable and use [reconciliation](/docs/core/calendar#snapshot-reconciliation) for data reloads. +- Treat text as data; calendar storage does not authorize an agent to execute work. -Expansion is timezone-aware per occurrence: pass an IANA `timeZone` on the event -(or as the fifth argument to `expandEvent`) and DST transitions are handled for -you, including half-hour and 45-minute offset zones. +The [examples repository](https://github.com/forceCalendar/examples) contains platform starters, including server-side scheduling. A starter is not proof of production certification on every platform. diff --git a/content/docs/core/timezone.mdx b/content/docs/core/timezone.mdx index 1115190..938aede 100644 --- a/content/docs/core/timezone.mdx +++ b/content/docs/core/timezone.mdx @@ -1,178 +1,54 @@ --- title: Timezone Support -description: Full IANA timezone support via Intl API with DST handling and cross-timezone conversion. +description: IANA formatting, legacy conversion conventions, UI limits, and recurrence verification. --- -## Overview +Core uses the runtime's `Intl.DateTimeFormat` timezone data. A JavaScript Date represents an instant; formatting that instant in another zone is different from reinterpreting a Date's wall-clock fields. -forceCalendar uses the browser's `Intl.DateTimeFormat` API for timezone operations. There are no bundled timezone databases or polyfills -- this keeps the package size minimal and ensures timezone data stays up-to-date with the browser. - -Timezone support is spread across three classes: - -| Class | Role | -|-------|------| -| `TimezoneManager` | Singleton for timezone operations and IANA data | -| `TimezoneDatabase` | IANA timezone data: names, regions, UTC offsets | -| `DateUtils` | Static timezone conversion and DST methods | - -## TimezoneManager - -A singleton class that caches timezone operations for performance. - -`TimezoneManager` is an internal class -- it is not exported from `@forcecalendar/core`. Access it through a calendar instance via `calendar.timezoneManager`, or use the `Calendar` wrapper methods (`convertTimezone`, `toCalendarTimezone`, `fromCalendarTimezone`, `formatInTimezone`, `getTimezones`) shown below. +## Format an instant safely ```js -// Accessed via a Calendar instance -const tz = calendar.timezoneManager; +const instant = new Date('2026-10-05T13:00:00Z'); +const text = new Intl.DateTimeFormat('en-US', { + timeZone: 'America/New_York', dateStyle: 'medium', timeStyle: 'short', +}).format(instant); ``` -### Key Methods - -| Method | Description | -|--------|-------------| -| `getSystemTimezone()` | Detect the system's IANA timezone | -| `convertTimezone(date, from, to)` | Convert a date between timezones | -| `getTimezoneOffset(date, timezone)` | Get UTC offset in minutes | -| `formatInTimezone(date, timezone, options)` | Format in a timezone | -| `getCommonTimezones()` | List common timezones as `Array<{ value, label, offset }>` | -| `isValidTimezone(tz)` | Check if a timezone string is valid | - -## Calendar Timezone API - -The `Calendar` class exposes these timezone methods: - -### setTimezone(timezone) - -Change the calendar's timezone. All view data, event queries, and date comparisons will use the new timezone. - -```js -calendar.setTimezone('Asia/Tokyo'); -``` - -When the timezone changes: -- All date-based indices are recalculated -- View data is regenerated -- The `timezoneChange` event fires with `{ timezone, previousTimezone }` - -### getTimezone() - -```js -const tz = calendar.getTimezone(); // 'America/New_York' -``` - -### convertTimezone(date, fromTz, toTz) - -Convert a date between any two IANA timezones. - -```js -// What time is 9am New York in Tokyo? -const tokyoTime = calendar.convertTimezone( - new Date('2026-03-01T09:00:00-05:00'), - 'America/New_York', - 'Asia/Tokyo' -); -// 2026-03-01T23:00:00+09:00 -``` - -### toCalendarTimezone(date, fromTz?) - -Convert a date to the calendar's configured timezone. +Core exposes `TimezoneManager` publicly: ```js -// Convert UTC time to calendar timezone -const local = calendar.toCalendarTimezone(utcDate, 'UTC'); +import { TimezoneManager } from '@forcecalendar/core'; +const tz = TimezoneManager.getInstance(); +const offset = tz.getTimezoneOffset(new Date(), 'America/New_York'); ``` -### fromCalendarTimezone(date, toTz?) +## Calendar timezone API -Convert a date from the calendar's timezone to another timezone. +| Method | Purpose | +| --- | --- | +| `setTimezone(timezone)` / `getTimezone()` | Configure/read Core's calendar zone | +| `convertTimezone(date, from, to)` | Legacy wall-clock conversion between zones | +| `toCalendarTimezone(date, from?)` | Convert to the configured calendar zone | +| `fromCalendarTimezone(date, to?)` | Convert from the configured calendar zone | +| `formatInTimezone(date, timezone?, options?)` | Format through the timezone manager | +| `getTimezones()` | Common `{ value, label, offset }` descriptors | -### formatInTimezone(date, timezone?, options?) +`TimezoneManager` also exposes validation, offsets, DST/transition helpers and caches. See its [API reference](/docs/api/timezone-manager). `TimezoneDatabase` is an internal supporting class, not a package root export. -Format a date in a specific timezone. - -```js -calendar.formatInTimezone(new Date(), 'Asia/Tokyo', { - weekday: 'long', - hour: 'numeric', - minute: 'numeric', -}); -``` - -### getTimezones() - -Returns an array of common timezone descriptors -- `Array<{ value, label, offset }>` -- where `value` is the IANA identifier. (Wraps `TimezoneManager.getCommonTimezones()`.) - -## Event Timezone Support - -Events can specify their own timezones independent of the calendar timezone: - -```js -calendar.addEvent({ - title: 'Cross-timezone Meeting', - start: new Date('2026-03-01T09:00:00'), - end: new Date('2026-03-01T10:00:00'), - timeZone: 'America/Los_Angeles', // Start time is in LA - endTimeZone: 'America/New_York', // End time is in NY (rare) -}); -``` - -### Event Timezone Methods - -```js -// Get start time in any timezone -const tokyoStart = event.getStartInTimezone('Asia/Tokyo'); -const nyEnd = event.getEndInTimezone('America/New_York'); - -// Update times in a specific timezone -event.updateTimes( - new Date('2026-03-01T09:00:00'), - new Date('2026-03-01T10:00:00'), - 'Europe/London' -); -``` - -## DST Handling - -### Detection - -```js -DateUtils.isDST(new Date('2026-03-08'), 'America/New_York'); // false (before spring forward) -DateUtils.isDST(new Date('2026-03-09'), 'America/New_York'); // true (after spring forward) -``` - -### Safe Arithmetic - -`DateUtils.addHoursWithDST()` accounts for DST transitions: - -```js -// During spring forward (2am -> 3am), adding 1 hour to 1:30am gives 3:30am -DateUtils.addHoursWithDST( - new Date('2026-03-08T01:30:00'), - 1, - 'America/New_York' -); -``` +## Event fields and conversions -### Recurrence and DST +`timeZone` and `endTimeZone` retain event zone metadata; the latter can describe a different end zone. Core 2.5.6 preserves `endTimeZone` in `clone()` and `toObject()`. Invalid Event timezone identifiers throw. -`RecurrenceEngineV2` pre-calculates DST transitions for expansion ranges using `findDSTTransitions()`. This ensures that a recurring 9:00 AM meeting stays at 9:00 AM wall-clock time across DST boundaries, rather than shifting by the offset change. +Legacy Event and `TimezoneManager.toUTC`/`fromUTC` APIs use wall-clock carrier conventions. In particular, do not assume `startUTC`/`endUTC` are a second universally host-independent encoding of the supplied timestamps, or apply those conversions twice to an already-normalized instant. Validate the relevant API against your input contract. -## TimezoneDatabase +## Interface grid and visible range -Provides static data about IANA timezones. Like `TimezoneManager`, this class is internal and is not exported from `@forcecalendar/core` -- its data is surfaced through `calendar.getTimezones()` and `calendar.timezoneManager`. +The current Interface grid, editor display and `getVisibleRange()` use the browser's local timezone. The `timezone` attribute configures Core but does **not** change the grid into an arbitrary-zone view. `getVisibleRange().end` is inclusive. -The database includes: -- All IANA timezone identifiers -- Timezone display names -- UTC offsets -- Region groupings (Americas, Europe, Asia, etc.) +All-day values are civil dates, not instants: local start-of-day through the inclusive last day's 23:59:59.999. Use calendar-day arithmetic across DST transitions. Providers/ICS with exclusive ends and Salesforce's inclusive UTC-date representation need explicit adapters. See [date migration](/docs/migration#dates-and-timezones). -This class is used internally by `TimezoneManager` and the UI timezone selector. +## Recurrence and DST -## Implementation Notes +Core 2.5.7 corrects `RecurrenceEngineV2` calendar stepping in the named recurrence zone across host timezones. Use generated `start`/`end` for that contract. Legacy `startUTC`/`endUTC` metadata, conversion helpers, moved-instance bounds and ancient-date limits remain. See the [V2 timezone contract](/docs/core/recurrence#v2-timezone-contract). This is a scoped recurrence correction, not a redesign of every conversion API. -- Timezone operations rely entirely on `Intl.DateTimeFormat`. If the browser doesn't support a timezone, operations gracefully fall back to UTC. -- `TimezoneManager` uses a singleton pattern. Multiple `Calendar` instances share the same manager and its cache. -- Timezone conversions are cached within `TimezoneManager` for performance. -- The `Event.occursOn(date)` method respects the event's `timeZone` property when checking date membership. +Test the host timezone, event timezone, clock gaps/overlaps, exception dates and query bounds relevant to the deployment. Host `Intl` data may differ between runtime versions; keep runtimes maintained. diff --git a/content/docs/index.mdx b/content/docs/index.mdx index 0ee7cea..104ef5a 100644 --- a/content/docs/index.mdx +++ b/content/docs/index.mdx @@ -1,88 +1,40 @@ --- title: Introduction -description: Enterprise-grade calendar infrastructure for Salesforce and strict-CSP environments. +description: Calendar infrastructure for the web, frameworks, and Salesforce Lightning Web Security. --- -## What is forceCalendar? +forceCalendar is a calendar library with a headless engine, optional visual interfaces and platform integrations. Applications and agents can use the same calendar APIs; agents decide what to do with them. Choose the layer you need: -forceCalendar is a modular, zero-dependency calendar engine built from the ground up for environments where most calendar libraries fail: Salesforce Lightning Locker Service, strict Content Security Policy (CSP) restrictions, and enterprise deployments that cannot tolerate third-party supply chain risk. +| Layer | Current documented version | What it provides | +| --- | --- | --- | +| [Core](/docs/core) | 2.5.7 | Headless events, recurrence, search, timezone utilities and ICS | +| [Interface](/docs/interface) | 1.9.0 | Month/week/day Web Components, snapshots, selection and event editing | +| [React](/docs/adapters/react) | 0.3.1 | Typed props, callbacks, refs and client-side registration | +| [Vue](/docs/adapters/vue) | 0.3.1 | Typed props, emits, template refs and client-side registration | +| [Salesforce](/docs/salesforce) | 0.3.0.1 released | LWC, Apex persistence and optional Reader/Editor permission sets | -The architecture follows a strict layered separation: +Versions and verification boundaries are summarized in [What's new](/docs/whats-new). The npm packages and the Salesforce installer have different release processes. -``` -@forcecalendar/core Headless engine (pure JS, zero deps) - ↓ -@forcecalendar/interface Web Components UI layer - ↓ -Applications www (Next.js), Salesforce LWC -``` +## Start with a working integration -Each layer can be used independently. The core package works in any JavaScript environment. The interface package adds a visual layer using Web Components with Shadow DOM encapsulation. Application layers adapt the interface to specific platforms. +- [Quick start](/docs/quick-start): render a calendar and load a snapshot +- [Installation](/docs/installation): pinned packages, runtime requirements and TypeScript +- [Migration](/docs/migration): snapshots, read-only behavior, dates and lifecycle changes +- [Salesforce installation](/docs/salesforce/deployment): supported environment and deployment choices +- [Permissions](/docs/salesforce/permissions): least-privilege setup and its platform-wide effects +- [Headless integrations](/docs/agent): expose calendar APIs through your application +- [API reference](/docs/api): public Core APIs and labeled internal utilities -## Design Principles +## Design and boundaries -**Zero dependencies.** The entire core package ships as pure ESM JavaScript with no `node_modules` requirements. This eliminates supply chain risk and guarantees compatibility with locked-down environments. +**Headless Core.** Core ships as ESM with no runtime package dependencies. Calendar computation works without a DOM; browser download helpers and optional Workers still need the corresponding host APIs. No dependencies reduces dependency exposure, but does not eliminate security risk. -**Locker Service first.** Every API is designed to work within Salesforce Lightning Locker Service constraints: no `eval()`, no `innerHTML` in security-critical paths, no dynamic `import()`, no `Function()` constructors, no direct `window` or `document` global access. +**Web Components.** Interface supplies `` and `` with Shadow DOM. Applications own persistence and server authorization. A browser event notification is not proof that a remote save succeeded. -**Headless core.** The calendar engine manages state, events, recurrence, search, and ICS parsing without any DOM dependency. This enables server-side usage, testing without a browser, and framework-agnostic integration. +**Salesforce support.** The LWC requires Lightning Web Security (LWS). Legacy Lightning Locker does not support the third-party custom elements used by this integration. Manual DOM insertion does not bypass that requirement. -**RFC 5545 compliance.** Recurrence rules, ICS import/export, and timezone handling follow the iCalendar specification. The RRULE parser supports all RFC 5545 frequencies (SECONDLY through YEARLY), BYDAY with nth positioning, BYMONTHDAY, BYYEARDAY, BYWEEKNO, BYSETPOS, EXDATE exceptions, and WKST configuration. +**Dates are an integration contract.** Timed events, inclusive all-day civil dates, visible-range ends and provider-exclusive end dates are different concepts. Read the [date guidance](/docs/migration#dates-and-timezones) before adapting an external calendar. The Interface grid and visible range currently use the browser's local timezone; setting `timezone` does not turn them into an arbitrary-zone grid. -**Immutable state management.** All state transitions produce new objects. The StateManager provides undo/redo history, key-specific watchers, and prototype pollution protection. +**Recurrence is bounded.** Core implements RFC 5545 recurrence features, with explicit resource limits and known edge cases. Built-in recurring-event mutation is disabled until a series/occurrence scope workflow exists. See [recurrence](/docs/core/recurrence) for the V2 named-zone contract and remaining limits. -## Package Overview - -### @forcecalendar/core (v2.1.68) - -The headless engine. Published on npm as `@forcecalendar/core`. - -| Module | Purpose | -|--------|---------| -| `Calendar` | Main calendar controller with view management, navigation, event CRUD | -| `EnhancedCalendar` | Extended Calendar with Web Worker search and RecurrenceEngineV2 | -| `Event` | Event model with timezone support, attendees, reminders, categories | -| `EventStore` | Indexed event storage with O(1) lookups, batch operations, conflict detection | -| `RecurrenceEngine` | RFC 5545 recurrence expansion (static API) | -| `RecurrenceEngineV2` | Enhanced recurrence with modified instances, DST handling, caching | -| `RRuleParser` | RRULE string parsing and generation | -| `ICSParser` | Low-level RFC 5545 ICS parser | -| `ICSHandler` | High-level ICS import/export with subscriptions | -| `EventSearch` | Full-text search with fuzzy matching and Levenshtein distance | -| `SearchWorkerManager` | Web Worker-based search with InvertedIndex fallback | -| `StateManager` | Immutable state with undo/redo and change subscriptions | -| `DateUtils` | Static date utility methods with timezone support | -| `PerformanceOptimizer` | LRU caching, lazy indexing, metrics collection | -| `AdaptiveMemoryManager` | Dynamic cache sizing based on memory pressure | -| `LRUCache` | O(1) LRU cache using Map insertion order | - -### @forcecalendar/interface - -Web Components that consume the core engine. - -| Component | Element | Purpose | -|-----------|---------|---------| -| `ForceCalendar` | `` | Main calendar Web Component | -| `BaseComponent` | - | Foundation class for all components | -| `MonthViewRenderer` | - | Month grid renderer | -| `WeekViewRenderer` | - | Week timeline renderer | -| `DayViewRenderer` | - | Day timeline renderer | -| `EventBus` | - | Cross-component event communication | -| `StyleUtils` | - | CSS custom property theming system | - -### Salesforce LWC - -A Lightning Web Component wrapper that bridges `@forcecalendar/interface` with Salesforce: - -- Apex controller with `WITH SECURITY_ENFORCED` and CRUD/FLS checks -- `@wire` integration for reactive event loading -- Manual DOM insertion to bypass Locker Service static analysis -- SLDS-styled chrome (spinner, error display, refresh) - -## Quick Links - -- [Quick Start](/docs/quick-start) -- Get running in 5 minutes -- [Installation](/docs/installation) -- Package installation and configuration -- [Calendar API](/docs/core/calendar) -- Main Calendar class documentation -- [Event Model](/docs/core/event) -- Event creation and manipulation -- [API Reference](/docs/api) -- Complete API reference for all public classes +**Verify your environment.** Shipped tests cover specific paths, not every browser, Salesforce org, assistive technology or external provider. See [security](/docs/security) and the component guides for the remaining boundaries. diff --git a/content/docs/installation.mdx b/content/docs/installation.mdx index 0a42d34..b7a13c2 100644 --- a/content/docs/installation.mdx +++ b/content/docs/installation.mdx @@ -1,146 +1,82 @@ --- title: Installation -description: Install and configure forceCalendar packages. +description: Install the documented release set and choose a supported runtime. --- -## Core Package +## Pinned npm release set + +For a reproducible baseline: ```bash -npm install @forcecalendar/core +npm install @forcecalendar/core@2.5.7 @forcecalendar/interface@1.9.0 ``` -The core package is published as pure ESM with no dependencies. It requires a JavaScript environment that supports ES modules. - -### Package Exports - -The package exposes the following entry points via `exports` in `package.json`: - -| Import Path | Module | -|------------|--------| -| `@forcecalendar/core` | All exports (Calendar, Event, EventStore, etc.) | -| `@forcecalendar/core/calendar` | Calendar class | -| `@forcecalendar/core/events` | Event class | -| `@forcecalendar/core/state` | StateManager class | -| `@forcecalendar/core/search` | EventSearch class | -| `@forcecalendar/core/ics` | ICSHandler class | -| `@forcecalendar/core/types` | TypeScript type definitions | +Core alone is useful for server-side computation: -### Import Examples - -```js -// Import everything -import { - Calendar, - Event, - EventStore, - StateManager, - DateUtils, - ICSParser, - ICSHandler, - EventSearch, - RecurrenceEngine, - RecurrenceEngineV2, - RRuleParser, - EnhancedCalendar, - SearchWorkerManager, - InvertedIndex, -} from '@forcecalendar/core'; - -// Import specific modules (tree-shakeable) -import { Calendar } from '@forcecalendar/core/calendar'; -import { Event } from '@forcecalendar/core/events'; -import { StateManager } from '@forcecalendar/core/state'; -import { ICSHandler } from '@forcecalendar/core/ics'; +```bash +npm install @forcecalendar/core@2.5.7 ``` -## Interface Package +Add the adapter for your framework: ```bash -npm install @forcecalendar/interface +npm install @forcecalendar/react@0.3.1 +# OR +npm install @forcecalendar/vue@0.3.1 ``` -The interface package provides Web Components that consume the core engine. It registers the `` custom element automatically on import. +Keep the Core and Interface peers installed. React requires React 18 or later; Vue requires Vue 3.3 or later. Adapter 0.3 requires Interface 1.6 or later; its `readOnly` support requires Interface 1.8 or later. The documented release set above includes both features. -```js -import '@forcecalendar/interface'; -// is now available in the DOM -``` +## Core imports + +Core is ESM and has no runtime package dependencies. Use a maintained Node.js release for server workloads. `fetch`-based ICS helpers need a runtime with `fetch`, `AbortController` and the relevant network permissions; `downloadAsFile()` is browser-only. Optional search Workers have a synchronous fallback. -### Individual Imports +| Import | Exports | +| --- | --- | +| `@forcecalendar/core` | Public classes, `VERSION`, and default `Calendar` | +| `@forcecalendar/core/calendar` | `Calendar` | +| `@forcecalendar/core/events` | `Event` | +| `@forcecalendar/core/state` | `StateManager` | +| `@forcecalendar/core/search` | `EventSearch` | +| `@forcecalendar/core/ics` | `ICSHandler` | +| `@forcecalendar/core/types` | Shared TypeScript types | ```js -import { - ForceCalendar, - BaseComponent, - MonthViewRenderer, - WeekViewRenderer, - DayViewRenderer, - EventBus, - eventBus, // Singleton instance - StyleUtils, - DateUtils, - StateManager, -} from '@forcecalendar/interface'; +import { Calendar, Event, TimezoneManager, ConflictDetector } from '@forcecalendar/core'; +import { ICSHandler } from '@forcecalendar/core/ics'; ``` -## Salesforce Deployment +`LRUCache`, `PerformanceOptimizer`, `AdaptiveMemoryManager` and `TimezoneDatabase` are internal utilities, not root exports. Do not deep-import unexported package paths. -The Salesforce package is deployed as a Lightning Web Component with an Apex controller. It is not published on npm. +## Browser Interface -### Prerequisites - -- Salesforce org with Lightning Experience enabled -- API version 58.0 or higher -- Read access to the `Event` standard object (or your custom object) - -### Deployment Steps - -1. Deploy the Apex controller: `ForceCalendarController.cls` -2. Deploy the LWC component: `forceCalendar/` -3. Deploy `@forcecalendar/core` and `@forcecalendar/interface` as static resources -4. Add the component to a Lightning page via App Builder - -See the [Salesforce guide](/docs/salesforce) for detailed deployment instructions. - -## Browser Compatibility +```js +import '@forcecalendar/interface'; +``` -forceCalendar uses standard Web APIs with no polyfills required in modern browsers: +This registers `` and ``. Named imports from the same entry point also run its registration side effects. Interface needs a browser DOM, Custom Elements and Shadow DOM; use the framework adapters for client-side registration during SSR rather than importing Interface in server-only code. -| Feature | Used By | Minimum Support | -|---------|---------|-----------------| -| ES Modules | Core | All modern browsers | -| `Intl.DateTimeFormat` | Timezone handling | All modern browsers | -| `Map` / `Set` | EventStore, StateManager | All modern browsers | -| `CustomEvent` | Interface events | All modern browsers | -| `Shadow DOM` | Web Components | Chrome 53+, Firefox 63+, Safari 10+ | -| `Web Workers` | SearchWorkerManager | All modern browsers (optional) | -| `performance.memory` | AdaptiveMemoryManager | Chrome only (graceful fallback) | +Core declarations are generated from source. Interface also owns the global `HTMLElementTagNameMap` entry for `forcecal-main`: -The core package works in Node.js 16+ for server-side usage and testing. +```ts +import type { ForceCalendarElement } from '@forcecalendar/interface'; +const calendar: ForceCalendarElement = document.createElement('forcecal-main'); +``` -## CDN Usage +See [Quick start](/docs/quick-start) for snapshot loading and [migration](/docs/migration) for readiness and cleanup. -For quick prototyping without a build step: +## Salesforce -```html - -``` +Follow [deployment](/docs/salesforce/deployment), then select the minimum appropriate [permissions](/docs/salesforce/permissions). No permission set is assigned automatically. -## TypeScript Support +## Compatibility checks -Type definitions are included in the core package at `@forcecalendar/core/types`: +Test your actual browser and host policies. In particular: -```ts -import type { - CalendarConfig, - CalendarState, - EventData, - RecurrenceRule, - ICSExportOptions, - SearchOptions, -} from '@forcecalendar/core/types'; -``` +- The Interface uses Shadow DOM styles and inline style properties. A strict `style-src` policy needs host-specific validation; absence of `eval()` alone does not establish full CSP compatibility. +- The grid and visible range use the browser's local timezone even when `timezone` is configured. +- An SSR-safe adapter does not make browser-only imperative methods callable on the server. +- Read-only UI is separate from server authorization. +- [Headless tool integrations](/docs/agent) use the host application's persistence and authorization; the Core library does not supply a remote calendar service. diff --git a/content/docs/interface/index.mdx b/content/docs/interface/index.mdx index 7bc2bbf..1aceb2e 100644 --- a/content/docs/interface/index.mdx +++ b/content/docs/interface/index.mdx @@ -43,7 +43,7 @@ import '@forcecalendar/interface'; // is now available ``` -Or import specific classes without auto-registration: +Named imports from the same entry point also run custom-element registration: ```js import { ForceCalendar, BaseComponent, EventBus } from '@forcecalendar/interface'; @@ -64,3 +64,7 @@ import { ForceCalendar, BaseComponent, EventBus } from '@forcecalendar/interface | `DateUtils` | Date formatting utilities | | `DOMUtils` | DOM manipulation utilities | | `StateManager` | Interface package's own state management class (`src/core/StateManager.js`) that wraps the core `Calendar` | + +## Current interaction contract + +Interface 1.9 includes snapshots, visible-range callbacks, details/edit/delete dialogs, read-only interaction, recurring-event guards and explicit lifecycle cleanup. See [Web Components](/docs/interface/web-components) for the supported methods and limitations. The grid uses browser-local dates; the `timezone` attribute is not arbitrary-zone rendering. Salesforce requires LWS. diff --git a/content/docs/interface/views.mdx b/content/docs/interface/views.mdx index 1548852..cbcb6ab 100644 --- a/content/docs/interface/views.mdx +++ b/content/docs/interface/views.mdx @@ -5,7 +5,7 @@ description: Month, week, and day view rendering -- layout, event positioning, a ## Overview -Each calendar view has a dedicated renderer class that produces DOM content for the `` component. Renderers use pure JavaScript DOM creation (no `innerHTML`) for Locker Service compatibility. +Each calendar view has a dedicated renderer class that produces DOM content for the `` component. Renderers use pure JavaScript DOM creation (no `innerHTML`) to keep event text separate from HTML. Salesforce deployment requires LWS. ## MonthViewRenderer diff --git a/content/docs/interface/web-components.mdx b/content/docs/interface/web-components.mdx index 0e01d51..b541cea 100644 --- a/content/docs/interface/web-components.mdx +++ b/content/docs/interface/web-components.mdx @@ -1,233 +1,176 @@ --- title: Web Components -description: The forcecal-main custom element -- attributes, API, events, and lifecycle. +description: Snapshots, editing, read-only mode, dates, typed events, and lifecycle in Interface 1.9. --- -## forcecal-main - -The primary Web Component. Renders a full calendar with header navigation, view switching, and event display. - -```html - -``` - -## Observed Attributes - -These HTML attributes are reactive -- changes trigger re-renders. - -| Attribute | Type | Default | Description | -|-----------|------|---------|-------------| -| `view` | `string` | `'month'` | `'month'`, `'week'`, `'day'` | -| `date` | `string` | Today | ISO date string | -| `locale` | `string` | `'en-US'` | BCP 47 locale | -| `timezone` | `string` | System | IANA timezone | -| `week-starts-on` | `string` | `'0'` | 0=Sunday, 1=Monday | -| `height` | `string` | `'800px'` | CSS height value | - -```js -const el = document.querySelector('forcecal-main'); -el.setAttribute('view', 'week'); -el.setAttribute('timezone', 'Asia/Tokyo'); -``` - -## JavaScript API - -### addEvent(eventData) +Interface **1.9.0** registers `` and `` when imported in a browser. Named imports from the same entry point also perform registration. ```js -el.addEvent({ - title: 'Meeting', - start: new Date('2026-03-01T10:00:00'), - end: new Date('2026-03-01T11:00:00'), -}); -``` - -### updateEvent(eventId, updates) - -```js -el.updateEvent('evt_123', { title: 'Updated Meeting' }); +import '@forcecalendar/interface'; ``` -### deleteEvent(eventId) +| Attribute | JavaScript property | Meaning | +| --- | --- | --- | +| `view` | Use `setView(view)` | `month`, `week`, or `day` | +| `date` | Use `setDate(date)` | Visible date | +| `locale` | Attribute | BCP 47 locale | +| `timezone` | Attribute | Core timezone configuration; does not change the browser-local grid | +| `week-starts-on` | Attribute | `0`–`6`, Sunday through Saturday | +| `height` | Attribute | CSS height | +| `theme` | Attribute | Theme preset, including `slds` | +| `readonly` | `readOnly` | Boolean interaction mode | +| `show-color-picker` | `showColorPicker` | Explicit `false` hides the editor color picker | + +## Public methods + +| Method | Result | +| --- | --- | +| `addEvent(data)` / `updateEvent(id, updates)` | Event or `null` | +| `deleteEvent(id)` | Boolean | +| `getEvents()` | Event array | +| `setEvents(events, { removeMissing? })` | Change set or `null` while queued | +| `getVisibleRange()` | `{ start, end }` or `null` before initialization | +| `setView(view)` / `setDate(date)` | Change view/date | +| `next()` / `previous()` / `today()` | Navigate | +| `destroy()` | Tear down owned state and resources | + +## Loading events + +`setEvents()` and the `events` property take a _complete snapshot_ and reconcile it with what the calendar already holds: unchanged events keep their instance, changed ones are replaced, new ones are added and events missing from the snapshot are removed (pass `{ removeMissing: false }` to keep them). The view re-renders at most once per snapshot. ```js -el.deleteEvent('evt_123'); -``` +const calendar = document.querySelector('forcecal-main'); -### getEvents() +// Declarative: reconcile with removeMissing: true +calendar.events = rows; -Returns all events from the internal calendar instance. +// Imperative: the change set is returned and dispatched as calendar-events-set +const { added, updated, removed, unchanged } = calendar.setEvents(rows, { removeMissing: false }); -### setView(viewType) - -```js -el.setView('week'); -``` - -### setDate(date) - -```js -el.setDate(new Date('2026-06-15')); +calendar.addEventListener('calendar-events-set', e => { + console.log(e.detail.added.length, 'added', e.detail.removed.length, 'removed'); +}); ``` -### next() - -Navigate to the next period. - -### previous() +Rules of the road: -Navigate to the previous period. +- Every entry must be an object with an `id`; `null`/`undefined` clears the calendar and anything else that is not iterable throws a `TypeError` (nothing is applied). +- No per-event `calendar-event-add`/`-update`/`-remove` events are dispatched for a snapshot, so listeners that persist user edits are not triggered by a data load. +- Occurrences of a recurring series (the expanded instances found in view data, `getEventsInRange()` results and click events) are mapped back to their master, so displayed events can be echoed through a snapshot without collapsing the series. +- Calls made before the element is connected (or after `destroy()`) are queued and replayed in order once it initialises; `setEvents()` returns `null` in that case and `events` reads back the queued snapshot. +- `calendar-events-set` carries `{ events, added, updated, removed, unchanged }`; `updated` entries are `{ event, oldEvent }` pairs. -### today() +### Recurring events -Navigate to today. +Rendered chips of a recurring series carry occurrence ids (`_`). Clicking or selecting an occurrence resolves to its master (`stateManager.findEvent(id)`), while details retain the clicked occurrence's time. Built-in drag, resize, Edit and Delete are unavailable for recurring events: changing one occurrence or a whole series requires an explicit scope workflow, which is not supported yet. Host APIs remain available for applications that implement their own scoped editing. -## Custom Events +For instances already expanded by an external service (for example Salesforce), +keep `recurring`/`recurrenceRule` unset and add +`metadata: { forceCalendarRecurring: true }`. This explicit host marker disables +built-in Edit/Delete/drag/resize while rendering the ordinary supplied event once; +it does not generate occurrences or invent an RRULE. Hosts remain responsible for +server-side recurrence mutation permissions. The standalone editor also declines +recurring events; imperative host CRUD APIs retain their documented behavior. -The component dispatches these `CustomEvent`s with `bubbles: true` and `composed: true` (crosses Shadow DOM boundaries). - -| Event | `detail` | When | -|-------|----------|------| -| `calendar-navigate` | `{ action, date }` | Navigation occurs | -| `calendar-view-change` | `{ view }` | View type changes | -| `calendar-event-add` | `{ event }` | Event creation requested | -| `calendar-event-update` | `{ event }` | Event update requested | -| `calendar-event-remove` | `{ eventId }` | Event deletion requested | -| `calendar-event-added` | `{ event }` | Event was added | -| `calendar-event-updated` | `{ event }` | Event was updated | -| `calendar-event-deleted` | `{ eventId }` | Event was deleted | -| `calendar-date-select` | `{ date }` | Date cell clicked | - -```js -el.addEventListener('calendar-event-added', (e) => { - console.log('Event created:', e.detail.event); -}); +## Event details and editing -el.addEventListener('calendar-date-select', (e) => { - console.log('Date selected:', e.detail.date); -}); -``` +Click an event, or focus it and press Enter/Space, to open its details: title, the displayed instance's date/time, location and available actions. Times use the browser's local display, as the calendar grid does. The calendar-owned dialog uses ordinary DOM nodes inside the calendar's shadow root, without a portal or the native Popover API. -## Template Structure +- **Edit** prefills the existing event. Save updates its stable id and emits the normal `calendar-event-update` / `calendar-event-updated` pair; it does not add a duplicate. Description, attendees, metadata and other host-owned fields are retained. Unchanged date inputs preserve the original full-precision instants, including DST-overlap times. +- **Delete** first opens an explicit confirmation. Cancel or Escape returns to details; confirming emits the normal remove/deleted pair. Failed saves/deletes leave the draft or confirmation available to retry. +- Close, backdrop click and Escape dismiss details; Tab stays inside the dialog and focus returns to the event (or Today after deletion). Cancelling the editor discards its draft. Navigation, teardown and enabling read-only mode dismiss active interactions. +- If the event changes or disappears while being edited, the draft stays visible with an error and cannot overwrite the newer data. Close and reopen the event to edit its current state. Changes to unrelated events or host-owned metadata do not block a save. +- Recurring details display the clicked occurrence's time, with a clear recurring-event notice. Built-in Edit/Delete are intentionally unavailable for recurring events until an explicit series/occurrence editing workflow is supported; host APIs remain available. -The component renders this internal structure inside its Shadow DOM: +The standalone `EventForm` also supports `edit(event)` and `open(start, end)` for prefilled editing and creation ranges. Its cancellable `save` event carries editable fields; a host can call `preventDefault()` and `showError(message)` to retain a failed draft. Its `close` event reports `{ restoreFocus }`. The main calendar wires these to its state manager automatically. -``` -
-
-
- -
-
- -

March 2026

- -
-
- -
- - - -
-
-
-
- -
-
-``` +## All-day date boundaries -## BaseComponent +All-day editing uses date-only **Start date** and **Last day (inclusive)** fields. +A same-day event uses the same date in both fields. This preserves Core's existing +contract: local start-of-day through local 23:59:59.999 on the last included day. +Dates are parsed as local civil dates, not UTC strings; DST days may span 23 or 25 +hours. Timed events retain exact instants, including unchanged DST-overlap times. +Adapters for systems with an exclusive end date must convert that boundary using +calendar-day arithmetic rather than a fixed 24-hour duration. -All Web Components extend `BaseComponent`, which provides: +## Optional editor color control -### Lifecycle +The color picker remains visible by default. Hosts that cannot persist colors can use +`` or `calendar.showColorPicker = false`. +The explicit attribute value `false` hides it; `true`, an empty attribute, or removing +the attribute shows it. Changing this option while editing keeps the unsaved draft. +Hidden controls do not reset existing event colors. Standalone `EventForm` supports +the same attribute and property. -| Method | Called When | -|--------|-----------| -| `initialize()` | Component first connected to DOM | -| `mount()` | After initialization | -| `render()` | When state changes require a re-render | -| `afterRender()` | After render completes | -| `unmount()` | Component disconnected from DOM | -| `cleanup()` | Final cleanup (remove listeners) | +## Read-only interaction mode -### State Management +Set the boolean HTML attribute `readonly`, or the reflected JavaScript property `readOnly`, to disable built-in user editing. The default is `false`. -```js -// Inside a component -this.setState({ loading: true }); -const state = this.getState(); +```html + ``` -### Properties - ```js -this.setProp('locale', 'en-US'); -const locale = this.getProp('locale'); +const calendar = document.createElement('forcecal-main'); +calendar.readOnly = true; // Can be set before registration/connection +calendar.events = rows; // Snapshot hydration still works +host.appendChild(calendar); +calendar.readOnly = false; // Re-enable editing at any time ``` -### DOM Helpers +Attribute spelling is `readonly` (no hyphen); property spelling is `readOnly`. Like native boolean attributes, `readonly="false"` still enables it: remove the attribute or assign `calendar.readOnly = false` to turn it off. Framework adapters, including LWC, should assign the boolean property before inserting the element and whenever their option changes. -```js -// Query within shadow root -const btn = this.$('.fc-btn-today'); -const allBtns = this.$$('.fc-btn'); -``` +- Disables New Event, form creation/saves, event dragging, resizing and drag-to-create in month/week/day views. Resize handles and details Edit/Delete actions are omitted and grids expose `aria-readonly`. +- Keeps event details, mouse/keyboard event and date selection, grid focus navigation, view switching and date navigation available. +- Enabling it closes the current details, deletion confirmation or event form, discards its unsaved edits, cancels an active drag/resize/creation gesture and releases the gesture's document listeners. Disabling it restores editing without duplicating listeners. Instances remain independent. +- Host code can still call `setEvents()`, assign `events`, and call `addEvent()`, `updateEvent()` or `deleteEvent()`. Imperative CRUD retains its usual mutation notifications; snapshots still emit only `calendar-events-set`. -### Event Emission +This is a UI interaction option, **not a security or authorization boundary**. Host-provided editors/context menus must also honor the option, and applications must enforce permissions and validate all writes on the server. -```js -// Emit a custom event that crosses shadow DOM boundaries -this.emit('calendar-navigate', { date: new Date(), view: 'month' }); -``` +## Visible range -### Listener Management +`getVisibleRange()` returns the `{ start, end }` window the current view covers, including the leading and trailing other-month days of the month grid. `end` is inclusive (the last millisecond of the window), so the pair can be passed straight to a range query. The window is expressed in the browser's local time zone regardless of the `timezone` attribute, and it is computed from the date, view and week start alone, so it is cheap to call. -Listeners registered via `addListener()` are automatically cleaned up on disconnect: +`calendar-range-change` fires whenever the window changes (navigation, view switch, `week-starts-on`), after the `calendar-navigate`/`calendar-view-change` event that caused it, with `{ start, end, view, date }`. It is also announced once per attach, on the next macrotask after the element is connected, so listeners added right after `appendChild` (framework refs and effects, a lazy `customElements.define()`) receive it. `getVisibleRange()` is the source of truth if you need the window synchronously. ```js -this.addListener(this.$('.fc-nav-arrow'), 'click', () => this.previous()); +calendar.addEventListener('calendar-range-change', async e => { + calendar.events = await api.fetchEvents(e.detail.start, e.detail.end); +}); ``` -### Re-render Preservation - -`BaseComponent` preserves scroll positions and focus across re-renders. Before rendering, it saves the current scroll position and active element reference. After rendering, it restores both. - -## View Renderers +## Lifecycle: detach and destroy -Each view has a dedicated renderer that produces DOM content: +Removing the element from the document releases its rendered tree, DOM listeners and view timers but keeps its state (view, date, events) and keeps dispatching `calendar-*` events for API calls, so a re-attach (framework reconciliation, portals, StrictMode double-mount) picks up where it left off. Attribute changes made while detached are applied to state and rendered on the next attach. -### MonthViewRenderer +`destroy()` tears the state manager and its owned Core Calendar down, including background maintenance timers (use Core 2.5.4 or later for full timer cleanup). Call it when you are finished with an element permanently; detaching alone intentionally preserves the calendar. Repeated calls are safe. Afterwards the public API no-ops or queues instead of throwing (`events` and `getEvents()` read the queued snapshot, `getVisibleRange()` is `null`, `setEvents()` queues, CRUD mutations return `null`/`false`, and navigation no-ops) and the next attach initialises a fresh calendar from the attributes and any queued snapshot. -Renders a grid of weeks. Each day cell shows up to 3 events with a "+N more" overflow indicator. +## Events -- Pure JS rendering (no innerHTML) for Locker Service compatibility -- Respects `weekStartsOn` configuration -- Highlights today and selected date -- Shows week numbers when enabled +| Event | `detail` | +| -------------------------------------------------- | ------------------------------------------------------------- | +| `calendar-navigate` | `{ action: 'next' \| 'previous' \| 'today' \| 'goto', date }` | +| `calendar-view-change` | `{ view }` | +| `calendar-range-change` | `{ start, end, view, date }` | +| `calendar-events-set` | `{ events, added, updated, removed, unchanged }` | +| `calendar-event-add` / `calendar-event-added` | `{ event }` | +| `calendar-event-update` / `calendar-event-updated` | `{ event }` | +| `calendar-event-remove` / `calendar-event-deleted` | `{ eventId }` | +| `calendar-date-select` | `{ date }` | +| `calendar-range-select` | `{ start, end }` | -### WeekViewRenderer +All events bubble and are composed. -Renders a 7-day timeline with hourly slots. +## TypeScript -- All-day events displayed in a header row -- Timed events positioned based on start/end times -- Business hours highlighted -- Overlap groups handled via `calculateEventPositions()` +The package ships declarations generated from JSDoc plus DOM typings for the element, so `document.createElement('forcecal-main')` is a `ForceCalendarElement` and `addEventListener` is typed for every `calendar-*` event: -### DayViewRenderer +```ts +import type { ForceCalendarElement, CalendarEvent } from '@forcecalendar/interface'; -Renders a single-day timeline with hourly slots. - -- Same layout principles as WeekViewRenderer -- More detail per event (full title, time, location) -- All-day events in header +const calendar = document.createElement('forcecal-main'); +calendar.addEventListener('calendar-events-set', e => { + const added: CalendarEvent[] = e.detail.added; +}); +``` diff --git a/content/docs/meta.json b/content/docs/meta.json index 203ca15..3b5da39 100644 --- a/content/docs/meta.json +++ b/content/docs/meta.json @@ -5,6 +5,7 @@ "whats-new", "quick-start", "installation", + "migration", "---Core Package---", "core", "---Interface Package---", @@ -13,9 +14,11 @@ "adapters", "---Salesforce---", "salesforce", + "---Headless Integrations---", + "agent", "---Security---", "security", "---API Reference---", "api" ] -} \ No newline at end of file +} diff --git a/content/docs/migration.mdx b/content/docs/migration.mdx new file mode 100644 index 0000000..5dd054e --- /dev/null +++ b/content/docs/migration.mdx @@ -0,0 +1,61 @@ +--- +title: Migration and compatibility +description: Upgrade guidance for package versions, snapshots, events, dates, SSR, and Salesforce. +--- + +## Recommended release set + +Use Core **2.5.7**, Interface **1.9.0**, and React/Vue **0.3.1** together. Pin versions for rollout, commit the lockfile, and test your actual persistence callbacks before deployment. Salesforce 0.3.0.1 is released with a frozen Core 2.5.6 / Interface 1.9.0 bundle; the separate Core 2.5.7 npm release does not change that installer. + +## Snapshot loading replaces CRUD loops + +Use `element.setEvents(rows)` or `element.events = rows` for a server snapshot. Every item needs a stable ID. Missing events are removed unless `{ removeMissing: false }` is passed. Snapshots emit only `calendar-events-set`; do not synthesize mutation callbacks while hydrating. + +Adapter `events` props update by array identity. Replace the array (React state/memo or Vue ref/computed), rather than mutating it in place. Adapter `undefined` leaves state alone; use `[]` to clear. Raw element `events = null` clears the snapshot. + +## Subscribe once per mutation + +The Interface emits legacy and current spellings together: `calendar-event-add`/`calendar-event-added`, `-update`/`-updated`, `-remove`/`-deleted`. These are aliases, not separate request and server-success stages. Listen to one per action. Adapters expose only the current spellings. + +Persistence belongs to the host. Reject unauthorized writes server-side, keep the stable ID across saves, and reload confirmed data after an error. Protect async range fetches from overwriting a newer visible range. + +## Read-only mode + +Interface uses `readonly` as a boolean HTML attribute and `readOnly` as a boolean property. `readonly="false"` is still enabled. Remove the attribute or set `element.readOnly = false`. + +React/Vue 0.3.1 accept a boolean `readOnly` prop. Use Interface 1.8 or later. This blocks built-in interaction, not programmatic APIs or server writes. The Salesforce wrapper additionally guards its persistence callbacks and public `addEvent` method; Apex independently enforces permissions. + +## Event editor and recurrence + +Interface 1.9 adds details, prefilled editing and confirmed deletion. Existing IDs and non-editable event fields are retained. A concurrent change to an edited field blocks a stale save; reopen details to use the current event. + +Built-in recurring-event edit/delete/drag/resize is unavailable. Hosts supplying already-expanded instances should use `metadata: { forceCalendarRecurring: true }`, leaving `recurring` and `recurrenceRule` unset. This protects UI mutation without causing a second expansion. It is not server-side authorization. + +If your backend has no color field, set `showColorPicker = false` or `show-color-picker="false"`. Hiding it retains the existing color. + +## Dates and timezones + +- Timed events represent instants; agree on timezone interpretation at the boundary. An offset-free datetime or `new Date('YYYY-MM-DD')` is not a safe universal civil-date parser. +- Core/Interface all-day values cover local start-of-day through **23:59:59.999 on the inclusive last day**. Use `new Date(year, monthIndex, day)` for local civil dates. A same-day event has the same start/last-day date. +- External APIs and ICS often use an exclusive end date. Convert explicitly with calendar-day arithmetic; never assume every day lasts 24 hours. +- The Salesforce adapter has separate `startDate`/`lastDay` civil-date transport. Its native UTC-date representation must not be shifted through the browser timezone or corrected by blindly subtracting a day. +- `getVisibleRange()` has an inclusive end and uses the browser's local timezone. The Interface `timezone` attribute does not make the grid render in an arbitrary IANA zone. +- Core 2.5.6 preserves `endTimeZone` during clone/serialization. Core 2.5.7 corrects [V2 named-zone stepping](/docs/core/recurrence#v2-timezone-contract) across host timezones. Legacy UTC metadata, moved-instance bounds and ancient-date limits remain separate; validate the API contract your integration uses. + +## Lifecycle and SSR + +Detach preserves state for reattachment. `destroy()` permanently releases owned Core state/timers; Core 2.5.4 or later is needed for complete timer cleanup. Reattaching a destroyed element initializes a fresh calendar and replays queued snapshots. + +Adapters register on the client. Await the ref's `whenReady()` before imperative methods, and use a client component for interactive Next.js usage. Interface 1.7+ owns the DOM tag-name type mapping; adapter structural event types no longer redeclare it. + +## Salesforce upgrade + +Deploy LWC, Apex and static resources together. The 0.3 LWC calls date-aware `createCalendarEvent`/`updateCalendarEvent`; installing only its JavaScript against an older controller is unsupported. Enable LWS, select an optional permission set deliberately, and test as intended users. Native Salesforce recurrence remains managed in Salesforce Calendar. See [deployment](/docs/salesforce/deployment). + +## Core 2.5.7 recurrence behavior + +V2 steps the calendar in the explicit recurrence zone, falling back to the event zone and then UTC. Use returned `start`/`end` as the generated occurrence instants. The legacy `startUTC`/`endUTC` fields and conversion helpers were not redesigned. + +`handleDST` remains accepted, but `true` and `false` now return the same correctly resolved instants: it previously toggled an additional post-step adjustment, not elapsed-duration arithmetic. Calendar increments advance through nonexistent local times and choose the earlier instant in an overlap. An adjusted gap time carries into subsequent steps. + +Explicit invalid zones now throw from stepping helpers such as `getNextDate`; previously those helpers ignored the zone. Stored overrides still apply after generated-start range filtering, and ancient-date/parser limits remain. Read the full [V2 contract](/docs/core/recurrence#v2-timezone-contract) before migrating integrations that relied on those details. diff --git a/content/docs/quick-start.mdx b/content/docs/quick-start.mdx index fd0c370..46c1bcb 100644 --- a/content/docs/quick-start.mdx +++ b/content/docs/quick-start.mdx @@ -1,178 +1,89 @@ --- title: Quick Start -description: Get a working forceCalendar instance in under 5 minutes. +description: Render a calendar, apply a snapshot, and listen without duplicating persistence. --- -## Browser / Vanilla JS +## Render a browser calendar -Import the core package and create a calendar: +Install Core 2.5.7 and Interface 1.9.0, then include the element: -```js -import { Calendar } from '@forcecalendar/core'; - -const calendar = new Calendar({ - view: 'month', - date: new Date(), - weekStartsOn: 0, // 0 = Sunday, 1 = Monday - locale: 'en-US', - timeZone: 'America/New_York', - showWeekNumbers: false, - showWeekends: true, - fixedWeekCount: true, // Always show 6 weeks in month view - businessHours: { - start: '09:00', - end: '17:00', - }, -}); +```html + ``` -## Adding Events +In your browser entry module: ```js -// Add a single event -calendar.addEvent({ - title: 'Team Standup', - start: new Date('2026-03-01T09:00:00'), - end: new Date('2026-03-01T09:30:00'), - location: 'Conference Room A', - color: '#4285f4', -}); +import '@forcecalendar/interface'; -// Add a recurring event -calendar.addEvent({ - title: 'Weekly Review', - start: new Date('2026-03-02T14:00:00'), - end: new Date('2026-03-02T15:00:00'), - recurring: true, - recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO;COUNT=12', -}); +const calendar = document.querySelector('#calendar'); +calendar.events = [ + { + id: 'planning', + title: 'Planning', + start: new Date(2026, 9, 5, 9), + end: new Date(2026, 9, 5, 10), + location: 'Room A', + }, + { + id: 'offsite', + title: 'Team offsite', + allDay: true, + start: new Date(2026, 9, 6), + end: new Date(2026, 9, 7, 23, 59, 59, 999), + }, +]; -// Add an all-day event -calendar.addEvent({ - title: 'Company Holiday', - start: new Date('2026-03-15'), - allDay: true, - color: '#34a853', +calendar.addEventListener('calendar-events-set', ({ detail }) => { + console.log('Snapshot applied:', detail.added.length, 'new events'); }); -``` - -## Querying Events - -```js -// Get events for a specific date -const todayEvents = calendar.getEventsForDate(new Date()); - -// Get events in a date range -const marchEvents = calendar.getEventsInRange( - new Date('2026-03-01'), - new Date('2026-03-31') -); - -// Query with filters -const meetings = calendar.queryEvents({ - categories: ['meeting'], - status: 'confirmed', - start: new Date('2026-03-01'), - end: new Date('2026-03-31'), +calendar.addEventListener('calendar-event-updated', ({ detail }) => { + console.log('Local change to persist:', detail.event.id); }); ``` -## Navigation - -```js -// Navigate between periods -calendar.next(); // Next month/week/day (based on current view) -calendar.previous(); // Previous month/week/day -calendar.today(); // Jump to today - -// Switch views -calendar.setView('week'); -calendar.setView('day'); -calendar.setView('month'); -calendar.setView('list'); - -// Jump to a specific date -calendar.goToDate(new Date('2026-06-15')); -``` - -## Listening to Events - -```js -calendar.on('eventAdd', ({ event }) => { - console.log('Event added:', event.title); -}); - -calendar.on('navigate', ({ date, view }) => { - console.log(`Navigated to ${date} in ${view} view`); -}); +These examples deliberately construct local civil dates: JavaScript's month index is zero-based. The offsite covers October 6–7 inclusive. For API data, agree on the source timezone and all-day boundary before converting it. -calendar.on('viewChange', ({ view }) => { - console.log(`View changed to: ${view}`); -}); +Click an event to view its details, edit it, or confirm deletion. Those actions change local calendar state. Connect your own backend, handle rejected writes and refresh authoritative state; the npm Interface does not save to a server for you. -calendar.on('eventUpdate', ({ event, oldEvent }) => { - console.log('Event updated:', event.id, oldEvent); -}); +## Snapshots and range loading -calendar.on('eventRemove', ({ event }) => { - console.log('Event removed:', event.id); -}); -``` - -## Web Component Usage - -If using `@forcecalendar/interface`, drop the custom element into your HTML: +`events`/`setEvents()` reconcile a complete snapshot. A snapshot emits one `calendar-events-set`, not per-event mutation events, so loading server data does not trigger duplicate saves. -```html - -``` +Use `calendar-range-change` to request the visible window. It fires initially and after navigation/view changes; `{ start, end }` has an inclusive end and uses the browser's local timezone. Protect asynchronous requests from out-of-order responses: ```js -import '@forcecalendar/interface'; - -const el = document.querySelector('forcecal-main'); - -// Add events via the element API -el.addEvent({ - title: 'Meeting', - start: new Date('2026-03-01T10:00:00'), - end: new Date('2026-03-01T11:00:00'), -}); - -// Listen for calendar events -el.addEventListener('calendar-event-added', (e) => { - console.log('Added:', e.detail); +let request = 0; +async function loadRange(start, end) { + const current = ++request; + // Implement fetchEvents in your application with authorization and error handling. + const rows = await fetchEvents(start, end); + if (current === request && calendar.isConnected) calendar.events = rows; +} +calendar.addEventListener('calendar-range-change', ({ detail }) => { + loadRange(detail.start, detail.end).catch(console.error); }); +const initialRange = calendar.getVisibleRange(); +if (initialRange) loadRange(initialRange.start, initialRange.end).catch(console.error); ``` -## Salesforce LWC Usage +For a single-event mutation use `addEvent`, `updateEvent` or `deleteEvent`. Listen to one spelling of each event: the legacy `-add` and current `-added` forms are aliases for the same mutation, not separate request/success stages. -In a Lightning Web Component: - -```html - - -``` +## Headless Core ```js -// myComponent.js -import { LightningElement, api } from 'lwc'; +import { Calendar } from '@forcecalendar/core'; -export default class MyComponent extends LightningElement { - @api recordId; +const calendar = new Calendar({ view: 'month', date: new Date(2026, 9, 5) }); +try { + calendar.addEvent({ + id: 'planning', title: 'Planning', + start: new Date(2026, 9, 5, 9), end: new Date(2026, 9, 5, 10), + }); + console.log(calendar.getEventsForDate(new Date(2026, 9, 5))); +} finally { + calendar.destroy(); } ``` -The LWC wrapper handles Apex CRUD operations automatically via `@wire`. +Core returns data and does not render a UI. Use the [React](/docs/adapters/react), [Vue](/docs/adapters/vue), or [Salesforce](/docs/salesforce) guide for those hosts. Continue with [Web Component methods](/docs/interface/web-components) and [migration](/docs/migration). diff --git a/content/docs/salesforce/apex-controller.mdx b/content/docs/salesforce/apex-controller.mdx index 7e0c4ee..5c59e88 100644 --- a/content/docs/salesforce/apex-controller.mdx +++ b/content/docs/salesforce/apex-controller.mdx @@ -1,200 +1,39 @@ --- title: Apex Controller -description: ForceCalendarController -- SOQL queries, CRUD operations, and security enforcement. +description: Date-aware APIs, overlap queries, record association, and user-mode authorization. --- -## Overview +`ForceCalendarController` is declared `public with sharing`. Queries and writes use Salesforce user-mode enforcement plus explicit validation. Deploy the controller together with the 0.3 LWC and resource. -`ForceCalendarController` is the server-side Apex class that handles data access for the forceCalendar LWC. It uses `with sharing` to respect the running user's sharing rules and `WITH SECURITY_ENFORCED` in all SOQL queries. +## Current LWC entry points -```apex -public with sharing class ForceCalendarController { - // All methods use security enforcement -} -``` +| Method | Parameters | Result | +| --- | --- | --- | +| `getEvents` | `Datetime startDateTime, Datetime endDateTime, Id recordId` | `List>`; cacheable | +| `createCalendarEvent` | `String title, Datetime startDateTime, Datetime endDateTime, Boolean isAllDay, Date startDate, Date lastDay, String description, String location, Id recordId` | Created `Id` | +| `updateCalendarEvent` | `Id eventId, String title, Datetime startDateTime, Datetime endDateTime, Boolean isAllDay, Date startDate, Date lastDay, String description, String location` | `void` | +| `deleteEvent` | `Id eventId` | `void` | -## Methods +For timed events provide datetime instants. For all-day events provide `startDate` and inclusive `lastDay`; the date-aware methods derive Salesforce-native values. `lastDay` may equal `startDate` but may not precede it. -### getEvents +Legacy `createEvent`, `createEventForRecord` and `updateEvent` remain available to existing callers following native Salesforce date semantics. Prefer the date-aware APIs for new LWC integrations. -Query events within a date range, optionally filtered by a related record. +## Query contract -```apex -@AuraEnabled(cacheable=true) -public static List> getEvents( - DateTime startDateTime, - DateTime endDateTime, - Id recordId -) -``` +Queries include overlapping events, not only events starting in the window. For timed events, an event ending exactly at the range start is excluded; an event starting exactly at the inclusive range end is included. All-day overlap uses native civil-date semantics. -**SOQL:** +The query is ordered by `StartDateTime` and limited to **1,000 records**. This is a bounded query, not a paginated export of every matching Event. Keep visible windows bounded and evaluate dense-calendar behavior before rollout. -```sql -SELECT Id, Subject, StartDateTime, EndDateTime, IsAllDayEvent, - Description, Location, WhoId, WhatId -FROM Event -WHERE StartDateTime >= :startDateTime - AND EndDateTime <= :endDateTime - [AND (WhoId = :recordId OR WhatId = :recordId)] -WITH SECURITY_ENFORCED -ORDER BY StartDateTime ASC -``` +Returned fields include `id`, `title`, `start`, `end`, `allDay`, `startDate`, `lastDay`, `description`, `location`, `whoId`, `whatId`, `backgroundColor` and `recurring`. `startDate`/`lastDay` are supplied for all-day entries. Optional relationship fields can be absent from the query and returned as null when inaccessible. -When `recordId` is provided, the query filters to events related to that record (via `WhoId` or `WhatId`). When `null`, all visible events in the range are returned. +## Authorization and associations -The `cacheable=true` annotation enables Lightning Data Service caching. Cached results are automatically invalidated when the LWC calls refreshApex. +User-mode SOQL/DML enforce object/field access, and sharing remains in force. An inaccessible or unsupported record-page context fails closed. Contact/Lead associations use `WhoId`; supported other record types use `WhatId`. The controller checks the related record as the current user before accepting it. -### createEvent +Native recurring events and exceptions are recognized server-side and cannot be edited/deleted through these unsupported mutation paths. This guard is independent of the Interface marker and UI controls. -Create a new Event record. +Permission-set assignment is explicit and additive; see [permission scope](/docs/salesforce/permissions). Test private Events, restricted readers/editors and related-record access in the target org. -```apex -@AuraEnabled -public static Id createEvent( - String title, - DateTime startDateTime, - DateTime endDateTime, - Boolean isAllDay, - String description, - String location, - Id whoId, - Id whatId -) -``` +## Verification -**Security checks:** - -```apex -if (!Schema.sObjectType.Event.isCreateable()) { - throw new AuraHandledException('Insufficient permissions to create events'); -} -``` - -Fields are checked at the object level. The method creates an `Event` SObject, inserts it, and returns the new record's `Id`. - -### updateEvent - -Update an existing Event record. - -```apex -@AuraEnabled -public static void updateEvent( - Id eventId, - String title, - DateTime startDateTime, - DateTime endDateTime, - Boolean isAllDay, - String description, - String location -) -``` - -**Security checks:** - -```apex -if (!Schema.sObjectType.Event.isUpdateable()) { - throw new AuraHandledException('Insufficient permissions to update events'); -} -``` - -Queries the existing event first, applies updates, and performs DML update. - -### deleteEvent - -Delete an Event record by ID. - -```apex -@AuraEnabled -public static void deleteEvent(String eventId) -``` - -**Security checks:** - -```apex -if (!Schema.sObjectType.Event.isDeletable()) { - throw new AuraHandledException('Insufficient permissions to delete events'); -} -``` - -## Security Model - -### Object-Level Security - -Every mutation method checks CRUD permissions before executing DML: - -| Operation | Check | -|-----------|-------| -| Create | `Schema.sObjectType.Event.isCreateable()` | -| Update | `Schema.sObjectType.Event.isUpdateable()` | -| Delete | `Schema.sObjectType.Event.isDeletable()` | - -### Field-Level Security - -All SOQL queries use `WITH SECURITY_ENFORCED`, which: - -- Enforces field-level security (FLS) for all fields in the SELECT clause -- Throws a `System.QueryException` if the user lacks read access to any queried field -- Cannot be bypassed by the running user - -### Sharing Rules - -The `with sharing` class keyword ensures: - -- Record-level sharing rules are respected -- Only events visible to the running user are returned -- OWD (Organization-Wide Default) settings are enforced - -### Error Handling - -All methods wrap exceptions in `AuraHandledException` for proper LWC error display: - -```apex -try { - insert newEvent; -} catch (DmlException e) { - throw new AuraHandledException('Failed to create event: ' + e.getMessage()); -} -``` - -## Field Mapping - -| Salesforce Field | forceCalendar Property | Direction | -|-----------------|----------------------|-----------| -| `Subject` | `title` | Read/Write | -| `StartDateTime` | `start` | Read/Write | -| `EndDateTime` | `end` | Read/Write | -| `IsAllDayEvent` | `allDay` | Read/Write | -| `Description` | `description` | Read/Write | -| `Location` | `location` | Read/Write | -| `WhoId` | — | Write (create only) | -| `WhatId` | — | Write (create only) | -| `Id` | `id` | Read only | - -## Customization - -To use a custom object instead of the standard `Event` object: - -1. Replace `Event` SObject references with your custom object -2. Update field API names in SOQL and DML -3. Update security checks to reference the custom object -4. Adjust the field mapping in the LWC component - -For example, to use a custom `Calendar_Event__c` object: - -```apex -@AuraEnabled(cacheable=true) -public static List getEvents( - DateTime startDateTime, - DateTime endDateTime, - String recordId -) { - return [ - SELECT Id, Name, Start_Date__c, End_Date__c, ... - FROM Calendar_Event__c - WHERE Start_Date__c >= :startDateTime - AND End_Date__c <= :endDateTime - WITH SECURITY_ENFORCED - ]; -} -``` +The source includes `ForceCalendarControllerTest` and `ForceCalendarPermissionsTest`. LWC Jest tests mock Apex transport and cannot substitute for those org tests. Live synthetic CRUD verification also does not establish behavior for every intended user's profile. See [deployment checks](/docs/salesforce/deployment). diff --git a/content/docs/salesforce/deployment.mdx b/content/docs/salesforce/deployment.mdx index 5ffa065..5fa3dcd 100644 --- a/content/docs/salesforce/deployment.mdx +++ b/content/docs/salesforce/deployment.mdx @@ -1,174 +1,88 @@ --- -title: Deployment -description: Deploying forceCalendar to a Salesforce org -- static resources, metadata, and app builder. +title: Installation and Deployment +description: Released Salesforce installer, source deployment, compatibility, and rollout checks. --- -## Prerequisites - -- Salesforce org with Lightning Experience enabled -- System Administrator or developer permissions -- Salesforce CLI (`sf`) installed locally -- API version 62.0 or higher - -## Project Structure +## Release status -``` -salesforce/ -└── src/ - └── force-app/ - └── main/ - └── default/ - ├── classes/ - │ ├── ForceCalendarController.cls - │ └── ForceCalendarController.cls-meta.xml - ├── lwc/ - │ └── forceCalendar/ - │ ├── forceCalendar.js - │ ├── forceCalendar.html - │ └── forceCalendar.js-meta.xml - └── staticresources/ - ├── forcecalendar.js - └── forcecalendar.resource-meta.xml -``` +Unlocked package **0.3.0.1** (`04tg5000000ExTFAA0`) is **released**: promotion was completed and independently read back as `IsReleased=true`. It bundles Core 2.5.6 and Interface 1.9.0 and includes Reader/Editor permission sets. Package creation reported **98% Apex coverage**. A clean disposable-org install succeeded. All **32 Apex test methods plus test setup** passed with no failures or skips; the test run reported **99% coverage**. Retrieved source/resource contents matched exactly, and all three permission sets matched semantically, including the five Event fields and three shared Task fields in Reader/Editor. Package coverage and test-run coverage are different measurements. -## Step 1: Build the Static Resource +This exact installer still contains **Core 2.5.6**. The separately published **Core 2.5.7** V2 timezone correction is not retroactively part of this package. Native Salesforce recurrence remains read-only in the integration; keep the documented package and recurrence boundaries when evaluating an upgrade. -The LWC loads a single static resource named `forcecalendar` -- a combined bundle of `@forcecalendar/core` and `@forcecalendar/interface`: +Older 0.2 betas do not include the current editor, date transport or Reader/Editor sets. Do not infer their contents from a newer source README. -```js -// forceCalendar.js (LWC) -import { loadScript } from 'lightning/platformResourceLoader'; -import FORCECALENDAR_LIB from '@salesforce/resourceUrl/forcecalendar'; +## Prerequisites -// ... -await loadScript(this, FORCECALENDAR_LIB); -``` +- Lightning Experience and **Lightning Web Security** enabled +- An org compatible with the package and API 62.0 metadata +- Salesforce CLI and an authorized org connection for CLI deployment +- A plan for activity permissions, sharing and related-record access -Build the combined bundle from the interface package (which includes core), then copy it to `staticresources/` as `forcecalendar.js`: +Legacy Lightning Locker is unsupported. Do not work around a missing custom-element API by weakening browser or org security. -```bash -# In the interface package directory -cd /path/to/interface -npm run build # Produces a single-file UMD bundle including core +## Option A: install the released unlocked package -cp dist/force-calendar-interface.umd.js /path/to/salesforce/src/force-app/main/default/staticresources/forcecalendar.js -``` +Evaluate the release in a sandbox or disposable org before production rollout. Use the official installer for the target environment: -## Step 2: Deploy to Org +- [Production or Developer Edition](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tg5000000ExTFAA0) +- [Sandbox](https://test.salesforce.com/packaging/installPackage.apexp?p0=04tg5000000ExTFAA0) -Using Salesforce CLI: +Or use Salesforce CLI: ```bash -cd salesforce -sf project deploy start --source-dir src/force-app +sf package install --package 04tg5000000ExTFAA0 --target-org your-org-alias --wait 20 ``` -Or deploy individual components: +Review the CLI prompts and package details before installation. Installing does not automatically assign the optional permission sets or activate a Lightning page. The version is promoted, but installing it into any org is a separate administration action. No production-org installation is implied by the release or its clean-org verification. -```bash -# Deploy Apex controller -sf project deploy start --source-dir src/force-app/main/default/classes - -# Deploy LWC -sf project deploy start --source-dir src/force-app/main/default/lwc +## Option B: deploy the source distribution -# Deploy static resources -sf project deploy start --source-dir src/force-app/main/default/staticresources -``` +Download the matching `forcecalendar-salesforce-dist.zip` from the [Salesforce releases](https://github.com/forceCalendar/salesforce/releases). Unzip it and deploy the generated Salesforce project: -## Step 3: Configure in App Builder - -1. Open Lightning App Builder for the target page -2. Drag `forceCalendar` from the custom components panel -3. Configure properties in the right sidebar: - - **Current View**: `month`, `week`, or `day` - - **Height**: CSS height value (e.g., `600px`) - - **Record Id**: `{!recordId}` for context-aware filtering -4. Save and activate the page - -## LWC Metadata Configuration - -The `forceCalendar.js-meta.xml` controls where the component can be placed: - -```xml - - - 62.0 - true - Force Calendar - Enterprise Calendar Component for Salesforce - - lightning__AppPage - lightning__RecordPage - lightning__HomePage - - - - - - - - - - - - - - +```bash +cd dist +sf project deploy start --target-org your-org-alias ``` -## Permission Setup +Deploy Apex, LWC and static resources as one matched set. Do not copy an arbitrary Interface UMD file into the org: the Salesforce build produces the combined Core/Interface resource and matching source layout. -Ensure users have the following permissions: +## Build from source -| Permission | Required For | -|-----------|-------------| -| Read on Event | Viewing calendar events | -| Create on Event | Creating events from the calendar | -| Edit on Event | Updating events (drag/resize/edit) | -| Delete on Event | Removing events | -| Apex Class Access | `ForceCalendarController` must be in the user's profile or permission set | +From the [Salesforce repository](https://github.com/forceCalendar/salesforce): -To grant Apex class access: - -1. Go to Setup > Profiles (or Permission Sets) -2. Find the target profile/permission set -3. Under "Apex Class Access", add `ForceCalendarController` +```bash +npm ci +cd src && npm ci && cd .. +npm run build +npm test +cd dist +sf project deploy start --target-org your-org-alias +``` -## Troubleshooting Deployment +`npm test` builds the bundle and exercises the real component with mocked Apex transport. It does not execute Apex or prove LWS behavior in your org. -### "Cannot find module" Error +## App Builder and permissions -If the LWC cannot find the static resource, ensure: -- The static resource name matches the `import` statement -- The static resource has `Content-Type: application/javascript` -- The resource is deployed and active +Drag **Force Calendar** onto an App, Home or Record page. Configure: -### Apex Test Coverage +| Setting | Default | Options | +| --- | --- | --- | +| `currentView` | `month` | `month`, `week`, `day` | +| `height` | `800px` | CSS height | +| `readOnly` | `false` | Disable UI editing and LWC persistence callbacks | -You may need to create test classes for `ForceCalendarController` before deploying to production: +Record pages supply the context record ID. Save/activate the page through your normal deployment process. Choose the minimum suitable [permission set](/docs/salesforce/permissions) and assign it only to intended users. -```apex -@IsTest -private class ForceCalendarControllerTest { - @IsTest - static void testGetEvents() { - Event testEvent = new Event( - Subject = 'Test', - StartDateTime = DateTime.now(), - EndDateTime = DateTime.now().addHours(1) - ); - insert testEvent; +## Rollout checklist - Test.startTest(); - List> events = ForceCalendarController.getEvents( - DateTime.now().addDays(-1), - DateTime.now().addDays(1), - null - ); - Test.stopTest(); +- Confirm LWS and the installed package/source versions. +- Run `ForceCalendarControllerTest` and `ForceCalendarPermissionsTest` in the target test org. +- Test as intended readers/editors, including users with no related-object access. +- Confirm private/unshared records remain protected. +- Create a disposable timed event; edit it, cancel an edit, refresh, navigate and delete it. +- Test same-day/multi-day all-day dates and a DST boundary in your users' timezones. +- Check Contact/Lead and other supported record-page associations. +- Verify recurring events cannot be mutated here and `readOnly` blocks persistence. +- Inspect rejected writes and refresh failures without duplicating records. - System.assert(events.size() > 0, 'Should return events'); - } -} -``` +No migration rewrites existing historical all-day records. If old data is wrong, investigate its source before deciding on a separate repair. diff --git a/content/docs/salesforce/index.mdx b/content/docs/salesforce/index.mdx index fc8e55d..a9b25ba 100644 --- a/content/docs/salesforce/index.mdx +++ b/content/docs/salesforce/index.mdx @@ -1,83 +1,41 @@ --- title: Salesforce Overview -description: Using forceCalendar in Salesforce Lightning with LWC, Apex, and Locker Service. +description: LWC and Apex integration over Salesforce Event, requiring Lightning Web Security. --- -## Architecture +The Salesforce integration connects the Interface Web Component to the standard `Event` object through an LWC and Apex controller. Unlocked package **0.3.0.1** is released, with a separate version/bundle lifecycle from the npm packages. -The Salesforce integration wraps `@forcecalendar/interface` in a Lightning Web Component (LWC) with an Apex controller for CRUD operations against the Salesforce `Event` standard object. +**Lightning Web Security is required.** Legacy Lightning Locker does not support the third-party custom elements used here. See [LWS compatibility](/docs/salesforce/locker-service). -``` -┌─────────────────────────────────────┐ -│ Lightning App Builder Page │ -│ ┌───────────────────────────────┐ │ -│ │ c-force-calendar (LWC) │ │ -│ │ ┌─────────────────────────┐ │ │ -│ │ │ forcecal-main │ │ │ -│ │ │ (Web Component) │ │ │ -│ │ └─────────────────────────┘ │ │ -│ │ ↕ DOM events │ │ -│ │ ┌─────────────────────────┐ │ │ -│ │ │ @wire(getEvents) │ │ │ -│ │ │ Apex imperative calls │ │ │ -│ │ └─────────────────────────┘ │ │ -│ └───────────────────────────────┘ │ -│ ↕ Apex │ -│ ┌───────────────────────────────┐ │ -│ │ ForceCalendarController.cls │ │ -│ │ (WITH SECURITY_ENFORCED) │ │ -│ └───────────────────────────────┘ │ -│ ↕ SOQL │ -│ ┌───────────────────────────────┐ │ -│ │ Event (Standard Object) │ │ -│ └───────────────────────────────┘ │ -└─────────────────────────────────────┘ -``` +## Data flow -## Components +`Event` records → user-mode Apex query → LWC snapshot → ``. -### c-force-calendar (LWC) +User create/edit/delete actions → guarded LWC handlers → user-mode Apex writes → refreshed authoritative records. -The Lightning Web Component wrapper. Handles: +The 0.3 integration bundles Core 2.5.6 and Interface 1.9.0. Deploy the LWC, controller and static resource together: the LWC now calls date-aware `createCalendarEvent` and `updateCalendarEvent` methods. -- `@wire` integration for reactive event loading -- Dynamic `forcecal-main` element creation (bypasses static module analysis) -- Event CRUD via imperative Apex calls -- SLDS-styled error display with retry -- Loading spinner -- Refresh button +## Supported workflows -### ForceCalendarController.cls +- Month, week and day views with date navigation and refresh +- Event creation, existing-event details/editing, cancellation and confirmed deletion +- Location and description transport +- Record-page filtering and creation: Contact/Lead use `WhoId`; supported other records use `WhatId` +- Inclusive all-day start/last-day dates, including same-day events and timed conversion +- Read-only UI/persistence guards and independent Apex permissions +- Optional controller-only Access, Reader and Editor permission sets -Apex controller with `with sharing` (respects sharing rules). Methods: +Fetched snapshots do not trigger Apex writes. Writes retain record identity, queue changes for the same record, and reconcile rejected mutations. Native Salesforce recurring events are displayed but remain read-only here; use Salesforce Calendar for series/occurrence changes. Production color selection is hidden because no custom color field is mapped. -| Method | Access | Description | -|--------|--------|-------------| -| `getEvents(start, end, recordId)` | `@AuraEnabled(cacheable=true)` | Query events in date range | -| `createEvent(title, start, end, ...)` | `@AuraEnabled` | Create a new event | -| `updateEvent(eventId, title, start, ...)` | `@AuraEnabled` | Update an existing event | -| `deleteEvent(eventId)` | `@AuraEnabled` | Delete an event | +## Start here -All methods use `WITH SECURITY_ENFORCED` in SOQL and check `isCreateable()`, `isUpdateable()`, `isDeletable()` on the `Event` SObject. +1. [Choose an installation/deployment route](/docs/salesforce/deployment). +2. [Review permissions and platform scope](/docs/salesforce/permissions). +3. Add **Force Calendar** in Lightning App Builder on an App, Home or Record page. +4. Verify as the intended users before wider rollout. -### forcecal-main +## Verification boundaries -The `@forcecalendar/interface` Web Component, deployed as a static resource and instantiated dynamically inside the LWC's `lwc:dom="manual"` container. +The October 0.3 candidate passed 38 LWC tests in UTC, Los Angeles, Kolkata and Auckland using the real bundled Interface with mocked Apex transport. Isolated Apex tests exercise actual Reader/Editor sets, denial paths and sharing. Live synthetic Event tests verified timed CRUD, cancellation, refresh/navigation, inclusive all-day dates and all-day-to-timed conversion; fixtures were removed. -## Quick Start - -```html - - -``` - -The component automatically: -1. Loads events from Salesforce via `@wire` -2. Creates the `forcecal-main` element in `renderedCallback` -3. Handles navigation and CRUD via Apex -4. Shows SLDS spinner during loading -5. Shows error messages with retry button on failure +A restricted-user Lightning browser session and live forced network-failure scenario have not been verified. There is no comprehensive security audit or assistive-technology certification claim. See the [source verification record](https://github.com/forceCalendar/salesforce/blob/93d9f8a/docs/VERIFICATION.md) and [released installer and verification](/docs/salesforce/deployment). diff --git a/content/docs/salesforce/locker-service.mdx b/content/docs/salesforce/locker-service.mdx index 7f1a0ab..d2e4c4b 100644 --- a/content/docs/salesforce/locker-service.mdx +++ b/content/docs/salesforce/locker-service.mdx @@ -1,122 +1,26 @@ --- -title: Locker Service Compatibility -description: How forceCalendar maintains compatibility with Salesforce Lightning Locker Service. +title: Lightning Web Security +description: Required LWS configuration and why legacy Lightning Locker is unsupported. --- -## What is Locker Service? +## LWS is required -Lightning Locker Service is Salesforce's security architecture that isolates Lightning components in sandboxed environments. It restricts access to global objects, DOM APIs, and certain JavaScript features to prevent cross-component interference and XSS attacks. +The Salesforce LWC uses third-party custom elements, including ``, backed by the browser Custom Elements API and Shadow DOM. Salesforce supports this with **Lightning Web Security**, not legacy **Lightning Locker**. See [Salesforce custom-element support](https://developer.salesforce.com/docs/platform/lwc/guide/create-use-custom-elements.html). -## Restrictions and Solutions +This page retains its historical URL so existing links still work. Earlier documentation claiming that the full Web Component integration was Locker-compatible was incorrect. -### No eval() or Function() +## Deployment requirements -**Restriction:** `eval()`, `new Function()`, and `setTimeout(string)` are blocked. +Enable LWS through your organization's approved Salesforce administration process, then test the matched LWC/Apex/static-resource build. A manual DOM container (`lwc:dom="manual"`) allows the component to insert the custom element; it does not bypass Locker restrictions or authorize data access. -**Solution:** forceCalendar uses no dynamic code evaluation. All logic is in static modules. Template rendering uses programmatic DOM creation, not template strings. +The production LWC loads a static resource with `lightning/platformResourceLoader`. It attaches listeners and sets properties before rendering the calendar. Custom events bubble and are composed so the host can receive them across Shadow DOM boundaries. -### No innerHTML (security-critical paths) +## Security remains layered -**Restriction:** `innerHTML` assignments can be blocked or sanitized by Locker Service. +- Use Apex for Salesforce record access; the controller enforces user-mode CRUD/FLS and sharing. +- `readOnly` is an interaction setting, not a substitute for permissions. +- Treat event titles/descriptions as plain text; do not inject them into HTML. +- Validate your CSP and host styling behavior. The absence of `eval()` does not mean arbitrary `style-src` rules will permit all Interface styles. +- Core calculation is separate from browser helpers: optional Workers and network/ICS helpers depend on host capabilities. -**Solution:** View renderers use `document.createElement()`, `element.textContent`, and `element.appendChild()` for all DOM construction. No HTML strings are parsed. - -```js -// What we do (safe) -const div = document.createElement('div'); -div.className = 'fc-event'; -div.textContent = event.title; -container.appendChild(div); - -// What we avoid (blocked by Locker) -container.innerHTML = `
${event.title}
`; -``` - -### Restricted Global Access - -**Restriction:** Direct access to `window`, `document`, and `navigator` may be proxied or restricted. - -**Solution:** The core package has zero DOM dependencies. The interface package accesses DOM APIs only through component-scoped references (`this.shadowRoot`, `this.template`). The `AdaptiveMemoryManager` accesses `process.memoryUsage` through an indirect reference to avoid triggering Locker Service restrictions: - -```js -// Indirect access pattern for Locker Service -const getMemory = () => { - try { - const p = globalThis.process || {}; - return typeof p.memoryUsage === 'function' ? p.memoryUsage() : null; - } catch { - return null; - } -}; -``` - -### No Dynamic import() - -**Restriction:** Dynamic `import()` is not available in Locker Service contexts. - -**Solution:** All imports are static ESM `import` statements resolved at bundle time. The LWC component uses dynamic `document.createElement()` for the custom element instead of dynamic imports. - -### Web Worker Limitations - -**Restriction:** Web Workers may not be available or may have restricted APIs. - -**Solution:** `SearchWorkerManager` detects Worker availability and falls back to `InvertedIndex` (synchronous, main-thread) when Workers are unavailable: - -```js -// SearchWorkerManager automatically falls back -if (typeof Worker === 'undefined') { - // Use InvertedIndex on the main thread - this.fallbackIndex = new InvertedIndex(); -} -``` - -### Shadow DOM - -**Restriction:** Locker Service enforces Shadow DOM boundaries more strictly than standard browsers. - -**Solution:** All events use `composed: true` to cross shadow boundaries: - -```js -this.dispatchEvent(new CustomEvent('calendar-navigate', { - detail: { action, date }, - bubbles: true, - composed: true, // Crosses Shadow DOM boundaries -})); -``` - -### No External Network Requests - -**Restriction:** CSP headers in Salesforce block requests to external origins unless explicitly allowlisted. - -**Solution:** The core package makes no network requests. ICS URL subscription (`ICSHandler.subscribe()`) and `importFromURL()` should only be used in non-Salesforce contexts. In Salesforce, use the Apex controller for all data access. - -## Compatibility Checklist - -| Feature | Locker Service Safe | Notes | -|---------|-------------------|-------| -| Core Calendar API | Yes | Zero DOM dependency | -| Event CRUD | Yes | Pure JS objects | -| Recurrence expansion | Yes | Pure computation | -| State management | Yes | Pure JS | -| Date utilities | Yes | Uses `Intl.DateTimeFormat` | -| ICS parsing | Yes | String processing only | -| ICS URL import | No | Blocked by CSP; use Apex instead | -| Full-text search | Partial | InvertedIndex works; Web Workers may not | -| Web Components | Yes | Shadow DOM with `composed` events | -| CSS theming | Yes | CSS custom properties inherit through Shadow DOM | -| Adaptive memory | Yes | Indirect `process` access with fallback | -| Drag and drop | Partial | Standard DOM events work; pointer events may be proxied | - -## Best Practices for Salesforce - -1. **Use the Apex controller for all data access.** Do not use `fetch()`, `XMLHttpRequest`, or ICS URL imports. - -2. **Deploy as static resources.** Bundle `@forcecalendar/core` and `@forcecalendar/interface` into single files and deploy as static resources. - -3. **Use `lwc:dom="manual"` for the custom element.** This is required because LWC templates cannot directly reference custom elements from static resources. - -4. **Test in Lightning Experience.** Locker Service behavior differs between Lightning Experience, the Salesforce mobile app, and development environments. Always test in the target environment. - -5. **Handle Worker fallback.** Expect `SearchWorkerManager` to use the `InvertedIndex` fallback. Design your search UX to work synchronously. - -6. **Avoid large event sets.** Without Web Workers, search and recurrence expansion run on the main thread. Keep event counts manageable (under 5000) for responsive UI. +If the custom element fails to register, check the org's LWS setting, resource version and browser errors. Do not weaken security policies or claim support for Locker on the basis of a Jest test. diff --git a/content/docs/salesforce/lwc-integration.mdx b/content/docs/salesforce/lwc-integration.mdx index c448455..8868371 100644 --- a/content/docs/salesforce/lwc-integration.mdx +++ b/content/docs/salesforce/lwc-integration.mdx @@ -1,190 +1,61 @@ --- title: LWC Integration -description: Lightning Web Component wrapper -- @api properties, @wire, lifecycle, and event handling. +description: Public component properties and methods, snapshots, persistence, and all-day transport. --- -## Component Definition +## Public properties -The `forceCalendar` LWC wraps `forcecal-main` and bridges it with Salesforce. - -### @api Properties - -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `currentView` | `string` | `'month'` | Initial view type | -| `height` | `string` | `'800px'` | Calendar height | -| `recordId` | `string` | `null` | Related record ID for context-filtered events | -| `readOnly` | `boolean` | `false` | Disable event creation/editing | +| Property | Default | Purpose | +| --- | --- | --- | +| `currentView` | `month` | `month`, `week`, `day` | +| `height` | `800px` | Calendar CSS height | +| `recordId` | `null` | Related-record filtering and creation context | +| `readOnly` | `false` | Disable UI mutations and LWC save callbacks | ```html - -``` - -### @api Methods - -| Method | Description | -|--------|-------------| -| `refreshEvents()` | Force reload events from Salesforce | -| `addEvent(event)` | Programmatically add an event | -| `setView(view)` | Change the view | -| `goToDate(date)` | Navigate to a date | - -```js -// From a parent component -this.template.querySelector('c-force-calendar').refreshEvents(); -this.template.querySelector('c-force-calendar').setView('week'); + ``` -## Reactive Data Loading +## Public methods -Events are loaded via `@wire`: +| Method | Purpose | +| --- | --- | +| `refreshEvents()` | Refresh the wired Apex result | +| `addEvent(event)` | Add through the component; blocked when read-only | +| `setView(view)` | Change the visible view | +| `goToDate(date)` | Change the visible date and fetch range | ```js -import getEvents from '@salesforce/apex/ForceCalendarController.getEvents'; - -@wire(getEvents, { - startDateTime: '$startDate', - endDateTime: '$endDate', - recordId: '$recordId' -}) -wiredEvents({ error, data }) { - if (data) { - this.events = data; - this.updateCalendarEvents(); - } else if (error) { - this.error = error; - } -} +const calendar = this.template.querySelector('c-force-calendar'); +calendar.setView('week'); +calendar.refreshEvents(); ``` -The `$startDate` and `$endDate` reactive properties are updated when the user navigates, triggering automatic data refresh. +## Loading and persistence -## Dynamic Element Creation +`@wire(ForceCalendarController.getEvents)` receives the requested range and record context. Its result is applied with the Interface snapshot API, not a loop of event-create commands. Refresh and navigation therefore do not write the loaded records back to Salesforce. -Due to Locker Service restrictions on static ES module imports of custom elements, the LWC creates `forcecal-main` dynamically: +The LWC loads the combined static resource once, creates `forcecal-main` inside a manual DOM container, and applies `readOnly`/the production color-picker setting before use. LWS is required. -```js -renderedCallback() { - if (this.calendarInitialized) return; - - const container = this.template.querySelector('.calendar-container'); - if (!container) return; - - // Create the element dynamically to bypass static analysis - const calendarEl = document.createElement('forcecal-main'); - calendarEl.setAttribute('view', this.currentView); - calendarEl.setAttribute('height', this.height); - - // Attach event listeners - calendarEl.addEventListener('calendar-navigate', (e) => { - this.handleNavigate(e.detail); - }); - calendarEl.addEventListener('calendar-event-add', (e) => { - this.handleEventAdd(e.detail); - }); - // ... more listeners - - container.appendChild(calendarEl); - this.calendarElement = calendarEl; - this.calendarInitialized = true; -} -``` +Mutation handlers call `createCalendarEvent`, `updateCalendarEvent` and `deleteEvent`. Writes for the same record are serialized. Pending edits are preserved, temporary create IDs are mapped to Salesforce IDs, and failed writes restore confirmed state. Async results are scoped to their connection/range context so a late response does not replace a newer view. -The `lwc:dom="manual"` directive on the container allows direct DOM manipulation: +These handlers are integration details, not a second public API for applications to call directly. Listen only once per Interface mutation: legacy/current DOM event names are aliases. -```html -
-``` +## All-day data boundary -## Event Handling +Timed events send `startDateTime` and `endDateTime` as instants. All-day events instead send `startDate` and **inclusive** `lastDay` as civil dates, with `isAllDay: true`. A same-day event is valid. -### Navigation +The Apex controller maps those civil dates to Salesforce's native inclusive UTC-midnight representation. The LWC reconstructs local start-of-day/end-of-day for Interface. Do not shift all-day native values through the browser timezone, subtract a day, or use a fixed 24-hour conversion. -When the user navigates in the calendar: +## Record contexts and recurrence -```js -handleNavigate(detail) { - const { date, view } = detail; - // Update wire parameters to trigger new data fetch - this.startDate = this.getViewStart(date, view); - this.endDate = this.getViewEnd(date, view); -} -``` +Contact and Lead context creation uses `WhoId`; supported other records use `WhatId`. Apex verifies object and record access and rejects unsupported/inaccessible contexts. A standalone own-calendar listing can omit inaccessible optional relationship fields. -### CRUD Operations +Native recurring events are marked with `metadata.forceCalendarRecurring` in the Interface, without adding a synthetic RRULE. Their Edit/Delete/drag/resize are disabled, and Apex independently refuses unsupported recurring mutations. Manage series and occurrences in Salesforce Calendar. -Event creation, update, and deletion use imperative Apex calls: +## Errors and read-only behavior -```js -import createEvent from '@salesforce/apex/ForceCalendarController.createEvent'; - -async handleEventAdd(detail) { - try { - const result = await createEvent({ - title: detail.event.title, - startDateTime: detail.event.start, - endDateTime: detail.event.end, - isAllDay: detail.event.allDay || false, - description: detail.event.description || '', - location: detail.event.location || '', - }); - // Refresh to pick up the new event - this.refreshEvents(); - } catch (error) { - this.showError(error); - } -} -``` - -## Error Handling - -The component displays errors using SLDS styling: - -```html - -``` - -## Template Structure - -```html - -``` +The component displays errors and supports refresh/retry. Read-only mode blocks built-in editing and the LWC's persistence callbacks/public `addEvent`; it does not revoke Salesforce permissions or constrain a separate host editor. See [permissions](/docs/salesforce/permissions) for authorization. diff --git a/content/docs/salesforce/meta.json b/content/docs/salesforce/meta.json index 04a64ec..0eb52b9 100644 --- a/content/docs/salesforce/meta.json +++ b/content/docs/salesforce/meta.json @@ -5,6 +5,7 @@ "lwc-integration", "apex-controller", "deployment", + "permissions", "locker-service" ] } diff --git a/content/docs/salesforce/permissions.mdx b/content/docs/salesforce/permissions.mdx new file mode 100644 index 0000000..33d8f0b --- /dev/null +++ b/content/docs/salesforce/permissions.mdx @@ -0,0 +1,37 @@ +--- +title: Permissions +description: Optional Access, Reader, and Editor sets, their activity scope, and least-privilege setup. +--- + +Choose one permission set according to the intended user's role. Installing does not assign any permission set. + +| Optional set | Grants | +| --- | --- | +| ForceCalendar Access | ForceCalendarController Apex access only | +| ForceCalendar Reader | Controller access, Access Activities, and read FLS for Event Description, IsAllDayEvent, Location, WhoId and WhatId | +| ForceCalendar Editor | Reader capabilities, Edit Events, and edit FLS for the five fields above | + +Edit Events permits creating, updating and deleting Events that Salesforce sharing permits. No set grants View All, Modify All, Edit Tasks, or related-object access. Permission sets are additive: assigning Reader cannot revoke permissions already granted by a profile or another set. The component's `readOnly` setting disables editing UI but is not an authorization boundary. + +## Platform scope and prerequisites + +Salesforce does not retain Event `objectPermissions` declarations in permission-set metadata. Activity user permissions are the supported controls. Access Activities also applies to Task/calendar/email activity access. Salesforce shares Description, WhoId and WhatId FLS with Task. The metadata explicitly includes those three Task counterparts so Salesforce retains the shared Event grants during redeployment or upgrades. Subject, StartDateTime and EndDateTime have no independent configurable FLS. + +- Verify the user's Salesforce license supports activities and these permissions. +- Assign the selected set through your ordinary administration process. +- Grant access to related records/objects only where needed. A Contact/Lead record page needs that person's object and record access; supported other record pages need the corresponding access. The calendar does not grant those permissions for you. +- Configure sharing through your existing access model. Private or unshared Events must remain protected. +- Enable Lightning Web Security; the third-party custom element is unsupported under Lightning Locker. +- For intentionally view-only pages, enable the component’s `readOnly` setting in Lightning App Builder. Permission sets themselves do not configure page UI. +- Verify as an intended user before wider deployment. These sets do not override other grants or organization policies. + +Standalone own-calendar listing conditionally omits inaccessible optional relationship fields. A record-context query fails closed when its object or record is inaccessible. + +See Salesforce's [activity access permissions](https://help.salesforce.com/s/articleView?id=sf.activity_access_user_perm.htm&language=en_US&type=5). Isolated Apex fixture tests verify both allowed and denied paths; no persistent test users or assignments are created by those tests. + + +## API names + +Use `ForceCalendarAccess`, `ForceCalendarReader` or `ForceCalendarEditor` in metadata/CLI workflows. Labels shown in Setup contain spaces. Select one according to your access model; do not assign all three merely because they are installed. + +Apex uses user-mode queries and DML in a `with sharing` controller. An actual restricted-user browser session remains a rollout check even though isolated Apex permission tests cover the allowed and denied operations. diff --git a/content/docs/security/index.mdx b/content/docs/security/index.mdx index 22362e4..3bc3751 100644 --- a/content/docs/security/index.mdx +++ b/content/docs/security/index.mdx @@ -1,80 +1,29 @@ --- title: Security Overview -description: Security architecture, threat model, and hardening measures. +description: Security controls, host responsibilities, and limits of verification. --- -## Security Architecture +forceCalendar provides defensive controls, but deployment security depends on the host, data and integrations. Test results and an npm audit snapshot are not a comprehensive security certification. -forceCalendar is designed for deployment in security-sensitive environments. The architecture addresses threats at multiple layers: +## Package and host boundaries -### Zero Dependencies +Core has no runtime package dependencies. That reduces transitive dependency exposure; it does not remove vulnerabilities in Core, build tooling, frameworks or applications. Keep lockfiles and runtime dependencies reviewed across the entire deployed stack. -The core package has zero runtime dependencies. This eliminates: +Calendar computation is headless. Some optional helpers access the browser, Workers or network. `ICSHandler.importFromURL()` and subscriptions perform outbound requests; it is incorrect to describe every Core API as network-free. -- Supply chain attacks via compromised transitive dependencies -- Known vulnerability accumulation in `node_modules` -- License compliance risk from third-party code -- Version pinning and audit overhead +The Interface renders event text through DOM/text APIs and sanitizes color values. It also uses Shadow DOM styles and inline style properties. Validate `script-src`, `style-src`, `connect-src` and `worker-src` against the actual host policy instead of assuming that avoiding `eval()` guarantees strict-CSP support. -### CSP Compliance +## Important controls -The codebase avoids all patterns that violate strict Content Security Policy: +| Boundary | Control | Host responsibility | +| --- | --- | --- | +| Event data | Required-field/date checks and field limits | Schema, payload size, permissions and business rules | +| ICS input | Size, line and event limits | Smaller workload limits and trust decisions | +| ICS URL fetch | URL/IP checks; Node DNS and redirect validation | Egress restrictions, allowlisting and DNS-rebinding protection | +| Recurrence | Occurrence/iteration bounds | Bounded windows, workload testing and correctness checks | +| DOM output | Text nodes and color sanitization | Never turn event content into executable HTML/CSS | +| UI editing | `readOnly` interaction option | Server-side authorization for every write | +| Salesforce | User-mode queries/DML and sharing | Intended-user permissions and org verification | +| Application tools | Public headless calendar APIs | Host authentication, authorization, persistence, concurrency and audit controls | -| CSP Directive | forceCalendar Compliance | -|---------------|-------------------------| -| `script-src 'self'` | No `eval()`, `Function()`, or inline scripts | -| `style-src 'self'` | CSS custom properties, no inline `