diff --git a/.ai/AGENTS.md b/.ai/AGENTS.md new file mode 100644 index 00000000..e14d339a --- /dev/null +++ b/.ai/AGENTS.md @@ -0,0 +1,64 @@ +# AI Agent Instructions + +## Project Context + +TanStack Time is a headless time/calendar utility library for TS/JS, React, Solid, Vue, Svelte, Angular. Uses Temporal API polyfill. Monorepo under pnpm + nx. + +## Core Constraints + +- All public API methods return Date or string. Never Temporal objects. +- Input dates: RFC 3339 string, epoch numeric, or Date object. +- Formatting via Intl.DateTimeFormat parts, not string tokens. +- Timezone and calendar are always tracked. Defaults from Intl.DateTimeFormat. +- Date objects are mutable. Never mutate inputs. + +## Package Hierarchy + +- `@tanstack/time` — core logic (calendar, date ops, formatting) +- `@tanstack/react-time` — React hooks (useCalendar), re-exports from core +- `@tanstack/time-devtools` — devtools UI, depends on core +- `@tanstack/solid-time` — Solid adapter (mirrors react-time) +- `@tanstack/solid-time-devtools` — Solid devtools + +## File Organization + +- `packages/time/src/date/` — date manipulation (add, subtract, startOf, endOf, format, parse, etc.) +- `packages/time/src/calendar/` — calendar engine (CalendarCore, DateCore, event rendering, recurrence, resizing) +- `packages/time/src/client/` — client-side utilities +- `packages/time/src/formatter/` — Intl.DateTimeFormat wrappers +- `packages/time/src/utils/` — shared helpers + +## Testing + +- Vitest for unit tests. `*.test.ts` co-located with source. +- React tests use `@testing-library/react`. +- No comment policy: zero comments in source. Prefer naming, types, structure. + +## Build + +- `tsdown` for bundling. Outputs ESM + CJS. +- `nx` for task orchestration. +- `pnpm` for package management. + +## Key Files + +- `packages/time/src/calendar/calendar.ts` — CalendarCore class +- `packages/time/src/calendar/date-core.ts` — DateCore abstract class +- `packages/time/src/date/index.ts` — date utility exports +- `packages/time/src/index.ts` — top-level exports +- `Methodology.md` — API design philosophy +- `TODO.md` — roadmap + +## Navigation + +- Read `.ai/graph.json` before exploring. It contains the full package map, file lists, exports, dependencies, and test locations. +- Use the `edges` array to trace internal dependencies and identify affected packages before modifying core logic. +- If `graph.json` is stale or missing, run `pnpm run graphify` to regenerate it. + +## Agent Rules + +- Do not add comments to source code. +- Follow existing code style. Use explicit types. +- When changing core, check react-time and devtools for breakage. +- Run `pnpm test:lib` and `pnpm test:types` before declaring done. +- All new date logic must handle timezone and calendar correctly. diff --git a/.ai/README.md b/.ai/README.md new file mode 100644 index 00000000..4c9d973f --- /dev/null +++ b/.ai/README.md @@ -0,0 +1,20 @@ +# AI Infrastructure + +Generated and maintained for AI agents working on TanStack Time. + +## Graphify + +`pnpm run graphify` generates `.ai/graph.json` — a machine-readable graph of the monorepo (packages, exports, dependencies, files, tests). + +## Spec Kit + +`.ai/spec-kit/` contains domain-specific specifications: + +- `architecture.md` — monorepo structure, package relationships, build system +- `testing.md` — test framework, commands, coverage expectations +- `api-design.md` — input/output contracts, method signatures, design philosophy +- `calendar-domain.md` — Temporal API, event model, recurrence, availability, views + +## Agent Context + +`.ai/AGENTS.md` — top-level instructions for AI agents. Read this first. diff --git a/.ai/graph.json b/.ai/graph.json new file mode 100644 index 00000000..15f112d4 --- /dev/null +++ b/.ai/graph.json @@ -0,0 +1,399 @@ +{ + "generated": "2026-08-07T18:06:55.381Z", + "packages": [ + { + "name": "@tanstack/react-time", + "path": "packages/react-time", + "exports": [ + "useCalendar from ./useCalendar", + "calculateGhostPreviewStyle from @tanstack/time", + "calculateSegmentResizePreview from @tanstack/time", + "calculateTimelineResizePreview from @tanstack/time", + "formatEventTimeRange from @tanstack/time", + "getEventDisplayTimeRange from @tanstack/time", + "getSegmentInfo from @tanstack/time", + "isMultiDayEvent from @tanstack/time", + "useCalendar", + "calculateGhostPreviewStyle", + "calculateSegmentResizePreview", + "calculateTimelineResizePreview", + "formatEventTimeRange", + "getEventDisplayTimeRange", + "getSegmentInfo", + "isMultiDayEvent" + ], + "dependencies": ["@tanstack/react-store", "@tanstack/time"], + "devDependencies": [ + "@testing-library/react", + "@types/react", + "@vitejs/plugin-react", + "react" + ], + "peerDependencies": ["react", "react-dom"], + "internalDeps": ["@tanstack/react-store", "@tanstack/time"], + "files": [ + "packages/react-time/src/index.ts", + "packages/react-time/src/useCalendar/index.ts", + "packages/react-time/src/useCalendar/inertResizeController.ts", + "packages/react-time/src/useCalendar/useCalendar.ts" + ], + "tests": [] + }, + { + "name": "@tanstack/react-time-devtools", + "path": "packages/react-time-devtools", + "exports": ["TimeDevtoolsPanel", "timeDevtoolsPlugin"], + "dependencies": ["@tanstack/devtools-utils", "@tanstack/time-devtools"], + "devDependencies": ["@types/react", "@vitejs/plugin-react", "react", "solid-js"], + "peerDependencies": ["react", "react-dom"], + "internalDeps": ["@tanstack/devtools-utils", "@tanstack/time-devtools"], + "files": [ + "packages/react-time-devtools/src/index.ts", + "packages/react-time-devtools/src/production.ts" + ], + "tests": [] + }, + { + "name": "@tanstack/solid-time", + "path": "packages/solid-time", + "exports": ["* from ./createCalendar"], + "dependencies": ["@tanstack/solid-store", "@tanstack/time"], + "devDependencies": ["@types/node", "solid-js", "vite-plugin-solid"], + "peerDependencies": ["solid-js"], + "internalDeps": ["@tanstack/solid-store", "@tanstack/time"], + "files": [ + "packages/solid-time/src/index.ts", + "packages/solid-time/src/createCalendar/createCalendar.ts", + "packages/solid-time/src/createCalendar/index.ts" + ], + "tests": [] + }, + { + "name": "@tanstack/solid-time-devtools", + "path": "packages/solid-time-devtools", + "exports": ["TimeDevtools", "timeDevtoolsPlugin"], + "dependencies": ["@tanstack/devtools-utils", "@tanstack/time-devtools"], + "devDependencies": ["solid-js", "vite-plugin-solid"], + "peerDependencies": ["solid-js"], + "internalDeps": ["@tanstack/devtools-utils", "@tanstack/time-devtools"], + "files": [ + "packages/solid-time-devtools/src/index.ts", + "packages/solid-time-devtools/src/production.ts" + ], + "tests": [] + }, + { + "name": "@tanstack/time", + "path": "packages/time", + "exports": ["* from ./date", "* from ./calendar", "* from ./projection", "* from ./client"], + "dependencies": ["@tanstack/devtools-event-client", "@tanstack/store"], + "devDependencies": ["vite-tsconfig-paths"], + "peerDependencies": [], + "internalDeps": ["@tanstack/devtools-event-client", "@tanstack/store"], + "files": [ + "packages/time/src/index.ts", + "packages/time/src/calendar/calendar.ts", + "packages/time/src/calendar/date-core.ts", + "packages/time/src/calendar/generateDateRange.ts", + "packages/time/src/calendar/getEventProps.ts", + "packages/time/src/calendar/getResizeProps.ts", + "packages/time/src/calendar/getTimeSlots.ts", + "packages/time/src/calendar/groupDaysBy.ts", + "packages/time/src/calendar/index.ts", + "packages/time/src/calendar/resizeController.ts", + "packages/time/src/calendar/splitMultiDayEvents.ts", + "packages/time/src/calendar/types.ts", + "packages/time/src/client/TimeClient.ts", + "packages/time/src/client/index.ts", + "packages/time/src/date/index.ts", + "packages/time/src/date/types.ts", + "packages/time/src/date/validateDate.ts", + "packages/time/src/formatter/extractLocaleOptions.ts", + "packages/time/src/formatter/shared.ts", + "packages/time/src/polyfills/getWeekInfo.ts", + "packages/time/src/polyfills/types.ts", + "packages/time/src/polyfills/weekInfoData.ts", + "packages/time/src/kernel/history.ts", + "packages/time/src/kernel/index.ts", + "packages/time/src/kernel/kernel.ts", + "packages/time/src/kernel/pipeline.ts", + "packages/time/src/kernel/types.ts", + "packages/time/src/projection/bucketByDay.ts", + "packages/time/src/projection/index.ts", + "packages/time/src/projection/layout.ts", + "packages/time/src/projection/splitMultiDay.ts", + "packages/time/src/projection/timelineLayout.ts", + "packages/time/src/projection/unavailability.ts", + "packages/time/src/recurrence/expandRecurringEvent.ts", + "packages/time/src/recurrence/index.ts", + "packages/time/src/recurrence/materialize.ts", + "packages/time/src/recurrence/occurrence.ts", + "packages/time/src/utils/dateDefaults.ts", + "packages/time/src/utils/dateRange.ts", + "packages/time/src/utils/dateTimeDefaults.ts", + "packages/time/src/utils/index.ts", + "packages/time/src/utils/weekUtils.ts", + "packages/time/src/calendar/tests/composedApi.test-d.ts", + "packages/time/src/calendar/features/availability.ts", + "packages/time/src/calendar/features/calendarFeatures.ts", + "packages/time/src/calendar/features/dayLayout.ts", + "packages/time/src/calendar/features/dependency.ts", + "packages/time/src/calendar/features/history.ts", + "packages/time/src/calendar/features/index.ts", + "packages/time/src/calendar/features/recurrence.ts", + "packages/time/src/calendar/features/registry.ts", + "packages/time/src/calendar/features/resize.ts", + "packages/time/src/calendar/features/timeline.ts", + "packages/time/src/calendar/features/types.ts", + "packages/time/src/date/ceil/ceil.ts", + "packages/time/src/date/ceil/index.ts", + "packages/time/src/date/add/add.ts", + "packages/time/src/date/add/index.ts", + "packages/time/src/date/clamp/clamp.ts", + "packages/time/src/date/clamp/index.ts", + "packages/time/src/date/count/count.ts", + "packages/time/src/date/count/index.ts", + "packages/time/src/date/equals/equals.ts", + "packages/time/src/date/equals/index.ts", + "packages/time/src/date/endOf/endOf.ts", + "packages/time/src/date/endOf/index.ts", + "packages/time/src/date/fromUnixTime/fromUnixTime.ts", + "packages/time/src/date/fromUnixTime/index.ts", + "packages/time/src/date/format/format.ts", + "packages/time/src/date/format/index.ts", + "packages/time/src/date/getUnixTime/getUnixTime.ts", + "packages/time/src/date/getUnixTime/index.ts", + "packages/time/src/date/getWeek/getWeek.ts", + "packages/time/src/date/getWeek/index.ts", + "packages/time/src/date/helpers/index.ts", + "packages/time/src/date/helpers/normalizeLocale.ts", + "packages/time/src/date/helpers/normalizeWeek.ts", + "packages/time/src/date/helpers/toZonedDateTime.ts", + "packages/time/src/date/intersects/index.ts", + "packages/time/src/date/intersects/intersects.ts", + "packages/time/src/date/isAfter/index.ts", + "packages/time/src/date/isAfter/isAfter.ts", + "packages/time/src/date/isBetween/index.ts", + "packages/time/src/date/isBetween/isBetween.ts", + "packages/time/src/date/isBefore/index.ts", + "packages/time/src/date/isBefore/isBefore.ts", + "packages/time/src/date/isFuture/index.ts", + "packages/time/src/date/isFuture/isFuture.ts", + "packages/time/src/date/getDayOfYear/getDayOfYear.ts", + "packages/time/src/date/getDayOfYear/index.ts", + "packages/time/src/date/isLeapYear/index.ts", + "packages/time/src/date/isLeapYear/isLeapYear.ts", + "packages/time/src/date/isPast/index.ts", + "packages/time/src/date/isPast/isPast.ts", + "packages/time/src/date/isSameOrAfter/index.ts", + "packages/time/src/date/isSameOrAfter/isSameOrAfter.ts", + "packages/time/src/date/isValidDate/index.ts", + "packages/time/src/date/isValidDate/isValidDate.ts", + "packages/time/src/date/max/index.ts", + "packages/time/src/date/max/max.ts", + "packages/time/src/date/isSameOrBefore/index.ts", + "packages/time/src/date/isSameOrBefore/isSameOrBefore.ts", + "packages/time/src/date/min/index.ts", + "packages/time/src/date/min/min.ts", + "packages/time/src/date/range/index.ts", + "packages/time/src/date/range/range.ts", + "packages/time/src/date/parse/index.ts", + "packages/time/src/date/parse/parse.ts", + "packages/time/src/date/set/index.ts", + "packages/time/src/date/set/set.ts", + "packages/time/src/date/round/index.ts", + "packages/time/src/date/round/round.ts", + "packages/time/src/date/since/index.ts", + "packages/time/src/date/since/since.ts", + "packages/time/src/date/isWeekend/index.ts", + "packages/time/src/date/isWeekend/isWeekend.ts", + "packages/time/src/date/startOf/index.ts", + "packages/time/src/date/startOf/startOf.ts", + "packages/time/src/date/subtract/index.ts", + "packages/time/src/date/subtract/subtract.ts", + "packages/time/src/date/until/index.ts", + "packages/time/src/date/until/until.ts", + "packages/time/src/date/withDateOperation/index.ts", + "packages/time/src/date/withDateOperation/withDateOperation.ts", + "packages/time/src/formatter/buildDateFormatter/buildDateFormatter.ts", + "packages/time/src/formatter/buildDateFormatter/index.ts", + "packages/time/src/formatter/buildDateTimeFormatter/buildDateTimeFormatter.ts", + "packages/time/src/formatter/buildDateTimeFormatter/index.ts", + "packages/time/src/formatter/buildFinalFormatter/buildFinalFormatter.ts", + "packages/time/src/formatter/buildFinalFormatter/index.ts", + "packages/time/src/formatter/buildTimeFormatter/buildTimeFormatter.ts", + "packages/time/src/formatter/buildTimeFormatter/index.ts", + "packages/time/src/kernel/modules/availability.ts", + "packages/time/src/kernel/modules/dependency.ts", + "packages/time/src/kernel/modules/index.ts", + "packages/time/src/kernel/modules/layout.ts", + "packages/time/src/kernel/modules/recurrence.ts", + "packages/time/src/kernel/modules/resize.ts", + "packages/time/src/kernel/modules/undo.ts", + "packages/time/src/validation/availability/checkAvailability.ts", + "packages/time/src/validation/availability/checkDaySpan.ts", + "packages/time/src/validation/availability/conflicts.ts", + "packages/time/src/validation/availability/index.ts", + "packages/time/src/validation/availability/time.ts", + "packages/time/src/validation/availability/unavailableRanges.ts", + "packages/time/src/validation/dependency/computeCascade.ts", + "packages/time/src/validation/dependency/graph.ts", + "packages/time/src/validation/dependency/index.ts", + "packages/time/src/validation/dependency/propagate.ts", + "packages/time/src/validation/dependency/shift.ts", + "packages/time/src/validation/dependency/validateDependencies.ts" + ], + "tests": [ + "packages/time/src/calendar/tests/calendar.test.ts", + "packages/time/src/calendar/tests/features.test.ts", + "packages/time/src/calendar/tests/resizeController.test.ts", + "packages/time/src/date/withDateOperation/withDateOperation.test.ts", + "packages/time/src/kernel/tests/history.test.ts", + "packages/time/src/kernel/tests/kernel.test.ts", + "packages/time/src/projection/tests/bucketByDay.test.ts", + "packages/time/src/projection/tests/kernelDaysParity.test.ts", + "packages/time/src/projection/tests/layout.test.ts", + "packages/time/src/projection/tests/timelineLayout.test.ts", + "packages/time/src/projection/tests/unavailability.test.ts", + "packages/time/src/recurrence/tests/expandRecurringEvent.test.ts", + "packages/time/src/recurrence/tests/materialize.test.ts", + "packages/time/src/recurrence/tests/occurrence.test.ts", + "packages/time/src/date/ceil/tests/ceil.test.ts", + "packages/time/src/validation/tests/purity.test.ts", + "packages/time/src/date/add/tests/add.test.ts", + "packages/time/src/date/clamp/tests/clamp.test.ts", + "packages/time/src/date/count/tests/count.test.ts", + "packages/time/src/date/equals/tests/equals.test.ts", + "packages/time/src/date/endOf/tests/endOf.test.ts", + "packages/time/src/date/fromUnixTime/tests/fromUnixTime.test.ts", + "packages/time/src/date/format/tests/format.test.ts", + "packages/time/src/date/getUnixTime/tests/getUnixTime.test.ts", + "packages/time/src/date/getWeek/tests/getWeek.test.ts", + "packages/time/src/date/intersects/tests/intersects.test.ts", + "packages/time/src/date/isAfter/tests/isAfter.test.ts", + "packages/time/src/date/isBetween/tests/isBetween.test.ts", + "packages/time/src/date/isBefore/tests/isBefore.test.ts", + "packages/time/src/date/getDayOfYear/tests/getDayOfYear.test.ts", + "packages/time/src/date/isFuture/tests/isFuture.test.ts", + "packages/time/src/date/isPast/tests/isPast.test.ts", + "packages/time/src/date/isLeapYear/tests/isLeapYear.test.ts", + "packages/time/src/date/isValidDate/tests/isValidDate.test.ts", + "packages/time/src/date/isSameOrAfter/tests/isSameOrAfter.test.ts", + "packages/time/src/date/max/tests/max.test.ts", + "packages/time/src/date/isSameOrBefore/tests/isSameOrBefore.test.ts", + "packages/time/src/date/range/tests/range.test.ts", + "packages/time/src/date/min/tests/min.test.ts", + "packages/time/src/date/since/tests/since.test.ts", + "packages/time/src/date/set/tests/set.test.ts", + "packages/time/src/date/startOf/tests/startOf.test.ts", + "packages/time/src/date/round/tests/round.test.ts", + "packages/time/src/date/isWeekend/tests/isWeekend.test.ts", + "packages/time/src/date/subtract/tests/subtract.test.ts", + "packages/time/src/date/parse/tests/parse.test.ts", + "packages/time/src/formatter/buildFinalFormatter/tests/buildFinalFormatter.test.ts", + "packages/time/src/date/until/tests/until.test.ts", + "packages/time/src/validation/availability/tests/checkAvailability.test.ts", + "packages/time/src/validation/availability/tests/checkDaySpan.test.ts", + "packages/time/src/validation/availability/tests/time.test.ts", + "packages/time/src/validation/availability/tests/unavailableRanges.test.ts", + "packages/time/src/kernel/modules/tests/availability.test.ts", + "packages/time/src/kernel/modules/tests/dependency.test.ts", + "packages/time/src/kernel/modules/tests/layout.test.ts", + "packages/time/src/kernel/modules/tests/recurrence.test.ts", + "packages/time/src/kernel/modules/tests/resize.test.ts", + "packages/time/src/kernel/modules/tests/undo.test.ts", + "packages/time/src/validation/dependency/tests/dependency.test.ts", + "packages/time/src/validation/dependency/tests/graph.test.ts", + "packages/time/src/validation/dependency/tests/propagate.test.ts" + ] + }, + { + "name": "@tanstack/time-devtools", + "path": "packages/time-devtools", + "exports": ["TimeDevtoolsCore"], + "dependencies": [ + "@tanstack/devtools-ui", + "@tanstack/devtools-utils", + "@tanstack/time", + "goober", + "solid-js" + ], + "devDependencies": ["rolldown-plugin-solid"], + "peerDependencies": [], + "internalDeps": ["@tanstack/devtools-ui", "@tanstack/devtools-utils", "@tanstack/time"], + "files": [ + "packages/time-devtools/src/index.ts", + "packages/time-devtools/src/production.ts", + "packages/time-devtools/src/styles/use-styles.ts" + ], + "tests": [] + } + ], + "edges": [ + { + "from": "@tanstack/react-time", + "to": "@tanstack/react-store", + "type": "dependency" + }, + { + "from": "@tanstack/react-time", + "to": "@tanstack/time", + "type": "dependency" + }, + { + "from": "@tanstack/react-time-devtools", + "to": "@tanstack/devtools-utils", + "type": "dependency" + }, + { + "from": "@tanstack/react-time-devtools", + "to": "@tanstack/time-devtools", + "type": "dependency" + }, + { + "from": "@tanstack/solid-time", + "to": "@tanstack/solid-store", + "type": "dependency" + }, + { + "from": "@tanstack/solid-time", + "to": "@tanstack/time", + "type": "dependency" + }, + { + "from": "@tanstack/solid-time-devtools", + "to": "@tanstack/devtools-utils", + "type": "dependency" + }, + { + "from": "@tanstack/solid-time-devtools", + "to": "@tanstack/time-devtools", + "type": "dependency" + }, + { + "from": "@tanstack/time", + "to": "@tanstack/devtools-event-client", + "type": "dependency" + }, + { + "from": "@tanstack/time", + "to": "@tanstack/store", + "type": "dependency" + }, + { + "from": "@tanstack/time-devtools", + "to": "@tanstack/devtools-ui", + "type": "dependency" + }, + { + "from": "@tanstack/time-devtools", + "to": "@tanstack/devtools-utils", + "type": "dependency" + }, + { + "from": "@tanstack/time-devtools", + "to": "@tanstack/time", + "type": "dependency" + } + ] +} diff --git a/.ai/spec-kit/api-design.md b/.ai/spec-kit/api-design.md new file mode 100644 index 00000000..a98c6574 --- /dev/null +++ b/.ai/spec-kit/api-design.md @@ -0,0 +1,53 @@ +# API Design Spec + +## Design Philosophy + +TanStack Time is designed after TanStack Query and TanStack Table: headless, framework-agnostic core with thin framework adapters. See `Methodology.md` for detailed rationale. + +## Input Contracts + +- Dates accepted as RFC 3339 string, epoch milliseconds (number), or Date object. +- Timezone and calendar are explicit parameters. If omitted, defaults from `Intl.DateTimeFormat`. +- Never mutate input Date objects. Always clone or create new Temporal instances. + +## Output Contracts + +- Public methods return either Date objects or strings. +- Never return Temporal objects directly to consumers. +- Methods return `{ value, options }` tuple where `options` carries timezone/calendar used. +- Formatting methods return string parts via Intl.DateTimeFormat. + +## Method Signature Pattern + +```ts +function operationName({ + date, + unit, + returnFormat, + options, +}: { + date: Date | string | number + unit: DateUnit + returnFormat?: 'standard' | 'long' | 'epoch' | 'Date' | 'ZonedDateTime' + options?: { timeZone?: string; calendar?: string } +}): { value: ReturnType; options: { timeZone: string; calendar: string } } +``` + +## Chaining vs Standalone + +- Prefer standalone functions over fluent/chaining APIs. +- Chaining creates object overhead and hinders treeshaking. +- Each function is idempotent and composable. + +## TypeScript Requirements + +- All public APIs must be fully typed. +- Generic parameters for calendar resources and events. +- No `any` in public surface. + +## Framework Adapter Pattern + +- Core is pure TS/JS. No framework dependencies. +- React adapter: `useCalendar` hook that creates `CalendarCore` instance and exposes reactive selectors. +- Solid adapter: equivalent `useCalendar` using Solid's reactive primitives. +- Devtools are separate packages that inject into core store. diff --git a/.ai/spec-kit/architecture.md b/.ai/spec-kit/architecture.md new file mode 100644 index 00000000..0f6e7e28 --- /dev/null +++ b/.ai/spec-kit/architecture.md @@ -0,0 +1,62 @@ +# Architecture Spec + +## Monorepo Layout + +``` +packages/ + time/ — core package + src/date/ — date manipulation primitives + src/calendar/ — calendar engine + src/client/ — client utilities + src/formatter/ — Intl.DateTimeFormat wrappers + src/utils/ — shared helpers + src/index.ts — public API surface + react-time/ — React adapter + src/useCalendar/ — useCalendar hook + src/index.ts — re-exports core + React-specific + time-devtools/ — devtools UI + src/components/ — UI components (Solid-based via goober) + src/store/ — devtools state + src/core.tsx — devtools core + solid-time/ — Solid adapter (mirrors react-time) + solid-time-devtools/ — Solid devtools +``` + +## Dependency Graph + +- `react-time` → `time` +- `time-devtools` → `time` +- `solid-time` → `time` +- `solid-time-devtools` → `time` + +## Build System + +- `tsdown` bundles each package. Produces `dist/index.js` (ESM) and `dist/index.cjs` (CJS). +- `nx` caches builds and runs tests selectively. +- Root scripts run via `nx run-many` or `nx affected`. + +## Public API Surface + +Core exports from `@tanstack/time`: + +- Date primitives: `add`, `subtract`, `startOf`, `endOf`, `format`, `parse`, `equals`, `isBefore`, `isAfter`, `isBetween`, `range`, `since`, `until` +- Calendar: `CalendarCore`, `DateCore`, event helpers, recurrence logic +- Types: `CalendarEvent`, `CalendarResource`, `ResizeState`, etc. + +React exports from `@tanstack/react-time`: + +- `useCalendar` hook +- `ResizeState`, `ResizeOptions`, `UseCalendarOptions` +- Re-exports of core event utilities and types + +## State Management + +- Core uses `@tanstack/store` for reactive state. +- React adapter uses `@tanstack/react-store`. +- CalendarCore maintains event collections, selected dates, current view. + +## Key Abstractions + +- `DateCore` — abstract base for date operations and navigation +- `CalendarCore` — concrete calendar with events, resources, availability +- `useCalendar` — React hook that instantiates CalendarCore and provides reactive selectors diff --git a/.ai/spec-kit/calendar-domain.md b/.ai/spec-kit/calendar-domain.md new file mode 100644 index 00000000..d9ce8e30 --- /dev/null +++ b/.ai/spec-kit/calendar-domain.md @@ -0,0 +1,51 @@ +# Calendar Domain Spec + +## Temporal API + +- Until native browser support, use `@js-temporal/polyfill`. +- Internal computations use `Temporal.ZonedDateTime` and `Temporal.PlainDate`. +- Public API never exposes Temporal objects. + +## Timezone and Calendar + +- A date/time instance is not a static point; it is relative to timezone and calendar. +- Always track timezone and calendar alongside the instant value. +- RFC 3339 extended format may include timezone and calendar: `2024-03-05T12:34:56.789Z[America/New_York][u-ca=gregory]`. + +## Event Model + +- `CalendarEvent` has: id, start, end, title, allDay flag, recurrence rule, resources, dependencies, metadata. +- `CalendarResource` has: id, title, availability windows, unavailability rules. +- Events can span multiple days. Split multi-day events into per-day segments for rendering. +- All-day events have no time component. Rendered at top of day view. + +## Recurrence + +- Supported: daily, weekly, monthly, yearly. +- Recurrence exceptions: EXDATE (skip occurrences), modified single occurrences. +- End conditions: UNTIL date, COUNT number. +- Expand recurring events into individual occurrences before rendering. + +## Resizing and Dragging + +- `ResizeController` handles pointer events for event resizing. +- `calculateSegmentResizePreview` computes preview state during resize. +- `calculateGhostPreviewStyle` computes CSS for drag ghost. +- `calculateTimelineResizePreview` computes timeline-specific resize preview. +- `getResizeProps` returns styling props for resize handles. + +## Availability + +- Resources define availability windows (start/end times per day). +- Unavailability rules define blocked ranges. +- `validateEventDependencies` checks if placing an event satisfies dependency constraints. +- `getUnavailableRanges` returns blocked ranges for a date. + +## Views + +- CalendarCore supports: day, week, month, year, agenda, timeline. +- `groupDaysBy` groups days by unit (week, month) for grid layouts. +- `goToSpecificPeriod` navigates to a specific date with view context. +- `getEventsByDate` retrieves all events for a date (including multi-day segments). +- `getAllDayEventsByDate` retrieves all-day events for a date. +- `getTimeSlots` generates time slots for day/week views. diff --git a/.ai/spec-kit/testing.md b/.ai/spec-kit/testing.md new file mode 100644 index 00000000..eb720280 --- /dev/null +++ b/.ai/spec-kit/testing.md @@ -0,0 +1,36 @@ +# Testing Spec + +## Test Framework + +- Vitest for all unit and integration tests. +- Co-located tests: `*.test.ts` in same directory as source. +- React tests use `@testing-library/react` + `@testing-library/jest-dom` matchers. +- Faker via `@faker-js/faker` for test data generation. + +## Test Commands + +- `pnpm test:lib` — run Vitest tests +- `pnpm test:lib:dev` — watch mode +- `pnpm test:types` — TypeScript type checking via `tsc --noEmit` +- `pnpm test:lint` — linting +- `pnpm test:ci` — full CI suite (oxlint, sherif, knip, docs, lib, types, build) + +## Coverage Expectations + +- Date primitives: edge cases around leap years, timezone transitions, calendar variants +- Calendar engine: event overlap, multi-day splitting, recurrence, resize, availability +- React hooks: render cycles, state updates, unmount cleanup + +## Test Patterns + +- Use explicit timezones in calendar tests. Avoid relying on local system time. +- Create helper functions for common calendar setup to reduce boilerplate. +- Mock Temporal API polyfill behavior when testing edge cases. +- For resize/drag tests, simulate pointer events and verify state transitions. + +## CI Requirements + +- `pnpm test:pr` must pass before merge. +- `pnpm test:sherif` ensures dependency consistency across packages. +- `pnpm test:knip` detects unused exports and dependencies. +- `pnpm test:docs` verifies all markdown links are valid. diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 00000000..6db028ae --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json", + "changelog": ["@changesets/changelog-github", { "repo": "TanStack/time", "disableThanks": true }], + "commit": false, + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "fixed": [], + "linked": [], + "ignore": [], + "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { + "onlyUpdatePeerDependentsWhenOutOfRange": true + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..9d08a1a8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..e2ea8ad3 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: tannerlinsley diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..cb3fd88b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,100 @@ +name: '🐛 Bug report' +description: Report a reproducible bug or regression +body: + - type: markdown + attributes: + value: | + Thank you for reporting an issue :pray:. + + This issue tracker is for reporting reproducible bugs or regression's found in [time](https://github.com/tanstack/time) + If you have a question about how to achieve or implement something and are struggling, please post a question + inside of time's [Discussions tab](https://github.com/tanstack/time/discussions) instead of filing an issue. + + Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already: + - TanStack time's [Discussions tab](https://github.com/tanstack/time/discussions) + - TanStack time's [Open Issues](https://github.com/tanstack/time/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) + - TanStack time's [Closed Issues](https://github.com/tanstack/time/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed) + + The more information you fill in, the better the community can help you. + + - type: input + id: tanstack-time-version + attributes: + label: TanStack Time version + description: | + - Please let us know the exact version of the TanStack Time framework adapter that you were using when the issue occurred. If you are using an older version, check to see if your bug has already been solved in the latest version. Please don't just put in "latest", as this is subject to change. + - The latest "time" version is + placeholder: | + e.g. v0.0.1 + validations: + required: true + + - type: input + id: framework-library-version + attributes: + label: Framework/Library version + description: Which framework and what version of that framework are you using? + placeholder: | + e.g. React v19.2.3 + validations: + required: true + + - type: textarea + id: description + attributes: + label: Describe the bug and the steps to reproduce it + description: Provide a clear and concise description of the challenge you are running into, and the steps we should take to try to reproduce your bug. + validations: + required: true + + - type: input + id: link + attributes: + label: Your Minimal, Reproducible Example - (Sandbox Highly Recommended) + description: | + Please add a link to a minimal reproduction. + Note: + - Your bug may get fixed much faster if we can run your code and it doesn't have dependencies other than React. + - To create a shareable code example for web, you can use CodeSandbox (https://codesandbox.io/s/new) or Stackblitz (https://stackblitz.com/). + - Please make sure the example is complete and runnable without prior dependencies and free of unnecessary abstractions + - Feel free to fork any of the official CodeSandbox examples to reproduce your issue: https://github.com/tanstack/time/tree/main/examples/ + - For React Native, you can use: https://snack.expo.dev/ + - For TypeScript related issues only, a TypeScript Playground link might be sufficient: https://www.typescriptlang.org/play + - Please read these tips for providing a minimal example: https://stackoverflow.com/help/mcve. + placeholder: | + e.g. Code Sandbox, Stackblitz, TypeScript Playground, etc. + validations: + required: true + + - type: textarea + id: screenshots_or_videos + attributes: + label: Screenshots or Videos (Optional) + description: | + If applicable, add screenshots or a video to help explain your problem. + For more information on the supported file image/file types and the file size limits, please refer + to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files + placeholder: | + You can drag your video or image files inside of this editor ↓ + + - type: dropdown + attributes: + options: + - No, because I do not know how + - No, because I do not have time to dig into it + - Maybe, I'll investigate and start debugging + - Yes, I think I know how to fix it and will discuss it in the comments of this issue + - Yes, I am also opening a PR that solves the problem along side this issue + label: Do you intend to try to help solve this bug with your own PR? + description: | + If you think you know the cause of the problem, the fastest way to get it fixed is to suggest a fix, or fix it yourself! However, it is ok if you cannot solve this yourself and are just wanting help. + - type: checkboxes + id: agrees-to-terms + attributes: + label: Terms & Code of Conduct + description: By submitting this issue, you agree to follow our Code of Conduct and can verify that you have followed the requirements outlined above to the best of your ability. + options: + - label: I agree to follow this project's Code of Conduct + required: true + - label: I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..d47ed610 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: 🤔 Feature Requests & Questions + url: https://github.com/TanStack/time/discussions + about: Please ask and answer questions here. + - name: 💬 Community Chat + url: https://discord.gg/mQd7egN + about: A dedicated discord server hosted by TanStack + - name: 🦋 TanStack Bluesky + url: https://bsky.app/profile/tanstack.com + about: Stay up to date with new releases of our libraries diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..0262cea3 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## 🎯 Changes + + + +## ✅ Checklist + +- [ ] I have followed the steps in the [Contributing guide](https://github.com/TanStack/time/blob/main/CONTRIBUTING.md). +- [ ] I have tested this code locally with `pnpm run test:pr`. + +## 🚀 Release Impact + +- [ ] This change affects published code, and I have generated a [changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). +- [ ] This change is docs/CI/dev-only (no release). diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 00000000..fb2083cf --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "configMigration": true, + "extends": [ + "config:recommended", + "group:allNonMajor", + "schedule:weekly", + ":approveMajorUpdates", + ":automergeMinor", + ":disablePeerDependencies", + ":maintainLockFilesMonthly", + ":semanticCommits", + ":semanticCommitTypeAll(chore)" + ], + "ignorePresets": [":ignoreModulesAndTests"], + "labels": ["dependencies"], + "rangeStrategy": "bump", + "postUpdateOptions": ["pnpmDedupe"], + "ignoreDeps": ["@types/node", "node"] +} diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml new file mode 100644 index 00000000..12dceb88 --- /dev/null +++ b/.github/workflows/autofix.yml @@ -0,0 +1,33 @@ +name: autofix.ci # needed to securely identify the workflow + +on: + pull_request: + push: + branches: [main, alpha, beta, rc] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + autofix: + name: autofix + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Setup Tools + uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main + - name: Fix formatting + run: pnpm run format + - name: Regenerate docs + run: pnpm build:all && pnpm generate-docs + - name: Apply fixes + uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 + with: + commit-message: 'ci: apply automated fixes' diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 00000000..128fe388 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,52 @@ +name: PR + +on: + pull_request: + paths-ignore: + - 'docs/**' + - 'media/**' + - '**/*.md' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + cancel-in-progress: true + +env: + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} + +permissions: + contents: read + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + - name: Setup Tools + uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main + - name: Get base and head commits for `nx affected` + uses: nrwl/nx-set-shas@3e9ad7370203c1e93d109be57f3b72eb0eb511b1 # v4.4.0 + with: + main-branch-name: main + - name: Run Checks + run: pnpm run test:pr + preview: + name: Preview + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + - name: Setup Tools + uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main + - name: Build Packages + run: pnpm run build:all + - name: Publish Previews + run: pnpx pkg-pr-new publish --pnpm './packages/*' --template './examples/*/*' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..6f9391b4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,42 @@ +name: Release + +on: + push: + # branches: [main, alpha, beta, rc] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + cancel-in-progress: true + +env: + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} + +permissions: + contents: write + id-token: write + pull-requests: write + +jobs: + release: + name: Release + if: github.repository_owner == 'TanStack' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + - name: Setup Tools + uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main + - name: Run Tests + run: pnpm run test:ci + - name: Run Changesets (version or publish) + uses: changesets/action@e0145edc7d9d8679003495b11f87bd8ef63c0cba # v1.5.3 + with: + version: pnpm run changeset:version + publish: pnpm run changeset:publish + commit: 'ci: Version Packages' + title: 'ci: Version Packages' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 00000000..ba8e8cfa --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,25 @@ +name: GitHub Actions Security Analysis + +on: + push: + branches: [alpha] + pull_request: + branches: ['**'] + +permissions: {} + +jobs: + zizmor: + name: Run zizmor + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Run zizmor + uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 + with: + advanced-security: false + annotations: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..42c51276 --- /dev/null +++ b/.gitignore @@ -0,0 +1,71 @@ + +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules/ +node_modules +**/node_modules/ +package-lock.json +yarn.lock + +# builds +build +dist +es +artifacts +.rpt2_cache +coverage +*.tgz + +# deploy/tooling output +.vercel +.netlify +.turbo +.output +.vite + +# misc +.DS_Store +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +.next +.svelte-kit + +# developer-specific VS Code config files +.vscode/tasks.json +.vscode/launch.json + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.history +size-plugin.json +stats-hydration.json +stats-react.json +stats.html +.vscode/settings.json +.idea +.cursor +.claude + +*.log +.DS_Store +.cache +.idea +.pnpm-store + +package-lock.json +yarn.lock +*.tsbuildinfo +*.tsbuildinfo + +.svelte-kit +.nx/cache +.nx/workspace-data +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +.angular diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..268c392d --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +provenance=true diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..b4040276 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.8.0 diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 00000000..843720ff --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,12 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "overrides": [ + { + "files": ["*.svelte"], + "options": {} + } + ] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 00000000..1375ffb7 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "rules": { + "no-case-declarations": "off", + "no-shadow": "off", + "react-hooks/exhaustive-deps": "error", + "react-hooks/rules-of-hooks": "error" + } +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..fe4bf24f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +**/.nx/ +**/.nx/cache +**/.svelte-kit +**/build +**/coverage +**/dist +**/docs +**/old-examples +pnpm-lock.yaml + +.angular diff --git a/.specify/README.md b/.specify/README.md new file mode 100644 index 00000000..b16931ae --- /dev/null +++ b/.specify/README.md @@ -0,0 +1,70 @@ +# Spec Kit — TanStack Time + +This is the Spec-Driven Development (SDD) infrastructure for TanStack Time, following the [github/spec-kit](https://github.com/github/spec-kit) methodology. + +## Workflow + +``` +/speckit.constitution → /speckit.specify → /speckit.clarify (optional) + ↓ + /speckit.plan → /speckit.analyze (optional) → /speckit.tasks + ↓ + /speckit.implement +``` + +## Directory Structure + +``` +.specify/ + memory/ + constitution.md # Project principles (read first, always) + scripts/bash/ + common.sh # Shared utilities + create-new-feature.sh # Scaffolds specs/-/ + setup-plan.sh # Scaffolds plan.md + setup-tasks.sh # Scaffolds tasks.md + templates/ + constitution-template.md + spec-template.md + plan-template.md + tasks-template.md + +specs/ + 001-/ # Feature specs live here + spec.md # Functional requirements + plan.md # Technical implementation + tasks.md # Actionable task breakdown + contracts/ # API contracts, data models + +.claude/commands/ + speckit-constitution.md # /speckit.constitution + speckit-specify.md # /speckit.specify + speckit-clarify.md # /speckit.clarify + speckit-plan.md # /speckit.plan + speckit-analyze.md # /speckit.analyze + speckit-tasks.md # /speckit.tasks + speckit-implement.md # /speckit.implement +``` + +## Commands + +| Command | Purpose | When to run | +| ----------------------- | -------------------------------- | ----------------------------- | +| `/speckit.constitution` | Create/update project principles | First, before any feature | +| `/speckit.specify` | Write functional requirements | For each new feature | +| `/speckit.clarify` | Ask clarifying questions | After specify, before plan | +| `/speckit.plan` | Write technical plan | After clarify/specify | +| `/speckit.analyze` | Check spec-plan-task coverage | After tasks, before implement | +| `/speckit.tasks` | Break plan into tasks | After plan | +| `/speckit.implement` | Execute task breakdown | After tasks | + +## Constitution + +Read `.specify/memory/constitution.md` before any development. It defines: + +- Zero comments in source code +- Headless core, thin adapters +- No Temporal leakage in public API +- Immutable inputs +- Treeshakable standalone functions +- Test-first for all public functions diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md new file mode 100644 index 00000000..2cdb26b5 --- /dev/null +++ b/.specify/memory/constitution.md @@ -0,0 +1,41 @@ +# TanStack Time Constitution + +## Purpose + +TanStack Time is a headless, framework-agnostic date/time and calendar utility library. It provides the core logic for building time and calendar UI components across React, Solid, Vue, Svelte, and Angular. It is designed after TanStack Query and TanStack Table: a powerful, treeshakable core with thin framework adapters. + +## Core Principles + +1. **Headless First**: All UI logic lives in the core. Framework adapters are thin reactive wrappers. +2. **Temporal Accuracy**: Date/time instances are relative to timezone and calendar. Never treat them as static points. +3. **Zero Comments**: Source code contains no comments. Prefer names, types, structure, and tests. +4. **Treeshakable**: Prefer standalone functions over fluent APIs. Each function is idempotent and composable. +5. **No Temporal Leakage**: Public API returns Date or string. Never expose Temporal objects to consumers. +6. **Immutable Inputs**: Never mutate Date objects passed into the API. Always clone or create new Temporal instances. +7. **Standards-Based**: Use RFC 3339 for serialization, Intl.DateTimeFormat for display, and the Temporal polyfill for computation. + +## Quality Standards + +- **Testing**: Every public function must have co-located unit tests using Vitest. React hooks tested with `@testing-library/react`. +- **Type Safety**: All public APIs fully typed. No `any` in the public surface. Generics for calendar resources and events. +- **Performance**: Date primitives should be fast enough for frequent re-renders. Avoid creating unnecessary Temporal objects. +- **Edge Cases**: Handle leap years, timezone transitions, and calendar variants correctly. +- **CI Gate**: `pnpm test:pr` (oxlint, sherif, knip, docs, lib, types, build) must pass before merge. + +## Technical Decisions + +- **Monorepo**: pnpm workspaces + nx for task orchestration and caching. +- **Build**: `tsdown` produces ESM + CJS for each package. +- **State**: `@tanstack/store` in core. `@tanstack/react-store` in React adapter. +- **Temporal**: `@js-temporal/polyfill` until native browser support. +- **Formatting**: Intl.DateTimeFormat parts, not string tokens. +- **Date Input**: Accept RFC 3339 string, epoch milliseconds, or Date object. + +## Process Rules + +1. **No comments in source code**. If a constraint is non-obvious, encode it in the type system or test. +2. **Small focused functions over classes**. Only use classes where state management is necessary (CalendarCore, DateCore). +3. **When changing core, check all adapters**. react-time, solid-time, and devtools must remain compatible. +4. **Run tests before declaring done**. `pnpm test:lib` and `pnpm test:types`. +5. **All new date logic must handle timezone and calendar correctly**. +6. **Prefer improving names, types, APIs, structure, and tests over adding comments**. diff --git a/.specify/scripts/bash/common.sh b/.specify/scripts/bash/common.sh new file mode 100644 index 00000000..7a7cb6b4 --- /dev/null +++ b/.specify/scripts/bash/common.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +SPECS_DIR="${ROOT_DIR}/specs" +SPECIFY_DIR="${ROOT_DIR}/.specify" +MEMORY_DIR="${SPECIFY_DIR}/memory" +TEMPLATES_DIR="${SPECIFY_DIR}/templates" + +next_feature_id() { + local max_id=0 + for dir in "${SPECS_DIR}"/*/; do + if [ -d "$dir" ]; then + local id=$(basename "$dir" | cut -d'-' -f1) + if [[ "$id" =~ ^[0-9]+$ ]]; then + if [ "$id" -gt "$max_id" ]; then + max_id=$id + fi + fi + fi + done + printf "%03d" $((max_id + 1)) +} + +slugify() { + echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd '[:alnum:]-' | sed 's/-*$//' +} + +check_prerequisites() { + if [ ! -f "${MEMORY_DIR}/constitution.md" ]; then + echo "ERROR: No constitution found. Run /speckit.constitution first." + exit 1 + fi +} diff --git a/.specify/scripts/bash/create-new-feature.sh b/.specify/scripts/bash/create-new-feature.sh new file mode 100644 index 00000000..bec03212 --- /dev/null +++ b/.specify/scripts/bash/create-new-feature.sh @@ -0,0 +1,50 @@ +#!/bin/bash +set -euo pipefail + +source "$(dirname "$0")/common.sh" + +FEATURE_NAME="${1:-}" +if [ -z "$FEATURE_NAME" ]; then + echo "Usage: $0 " + exit 1 +fi + +ID=$(next_feature_id) +SLUG=$(slugify "$FEATURE_NAME") +FEATURE_DIR="${SPECS_DIR}/${ID}-${SLUG}" + +mkdir -p "$FEATURE_DIR" + +cat > "${FEATURE_DIR}/spec.md" <" + exit 1 +fi + +cat > "${FEATURE_DIR}/plan.md" <" + exit 1 +fi + +cat > "${FEATURE_DIR}/tasks.md" <