@cosmicdrift/kumiko-headless 0.155.0 → 0.155.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-headless",
3
- "version": "0.155.0",
3
+ "version": "0.155.1",
4
4
  "description": "Headless UI logic for Kumiko — Dispatcher contract, Form-Controller, View-Model, Nav-Resolver. Plattform- und React-frei; jeder Renderer (renderer, renderer-web, renderer-native, …) komponiert darauf.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -36,7 +36,8 @@
36
36
  }
37
37
  },
38
38
  "dependencies": {
39
- "@cosmicdrift/kumiko-framework": "0.155.0",
39
+ "@cosmicdrift/kumiko-framework": "0.155.1",
40
+ "temporal-polyfill": "^0.3.2",
40
41
  "zod": "^4.4.3"
41
42
  },
42
43
  "publishConfig": {
@@ -197,7 +197,7 @@ export function createFormController<TValues extends FormValues, TCtx = unknown>
197
197
  getSnapshot: snapshotStore.getSnapshot,
198
198
  subscribe: snapshotStore.subscribe,
199
199
  setField(key, value) {
200
- // Skip work when the new value is identical to the current one —
200
+ // skip: value unchanged, avoid notify/re-render on identical set
201
201
  // avoids a notify + re-render for "setField with same value" which
202
202
  // happens a lot in controlled inputs on every keystroke of an
203
203
  // untouched field.
@@ -206,7 +206,7 @@ export function createFormController<TValues extends FormValues, TCtx = unknown>
206
206
  invalidate();
207
207
  },
208
208
  setValues(partial) {
209
- // Detect any effective change before rebuilding setValues with a
209
+ // skip: partial matches current values, avoid no-op notify
210
210
  // partial that matches current values shouldn't fire listeners.
211
211
  let changed = false;
212
212
  const v = values as Record<string, unknown>; // @cast-boundary form-values
@@ -217,15 +217,18 @@ export function createFormController<TValues extends FormValues, TCtx = unknown>
217
217
  break;
218
218
  }
219
219
  }
220
+ // skip: no key in partial actually changed, avoid no-op notify
220
221
  if (!changed) return;
221
222
  values = { ...values, ...partial };
222
223
  invalidate();
223
224
  },
224
225
  clearErrors(path) {
225
226
  if (path === undefined) {
227
+ // skip: no errors present, avoid no-op notify
226
228
  if (Object.keys(errors).length === 0) return;
227
229
  errors = Object.freeze({});
228
230
  } else {
231
+ // skip: path has no error entry, avoid no-op notify
229
232
  if (!(path in errors)) return;
230
233
  const next: Record<string, readonly FieldIssue[]> = { ...errors };
231
234
  delete next[path];
@@ -239,8 +242,8 @@ export function createFormController<TValues extends FormValues, TCtx = unknown>
239
242
  },
240
243
  validate: runValidate,
241
244
  reset() {
242
- // Cheap no-op when already at baseline and no errors to clear.
243
245
  const alreadyClean = !snapshotStore.getSnapshot().isDirty && Object.keys(errors).length === 0;
246
+ // skip: already at baseline with no errors, no-op reset
244
247
  if (alreadyClean) return;
245
248
  values = { ...initial };
246
249
  errors = Object.freeze({});
@@ -1,6 +1,18 @@
1
1
  // Pure format utilities — no web or platform dependencies.
2
2
  // Shared between renderer-web, renderer-native, and server-side tests.
3
3
 
4
+ import { Temporal } from "temporal-polyfill";
5
+
6
+ function toPlainDate(raw: string): Temporal.PlainDate {
7
+ try {
8
+ return Temporal.PlainDate.from(raw);
9
+ } catch {
10
+ // "date"-typed field stored as a full instant (day-boundary timestamp) —
11
+ // take the calendar date in the local zone rather than fail.
12
+ return Temporal.Instant.from(raw).toZonedDateTimeISO(Temporal.Now.timeZoneId()).toPlainDate();
13
+ }
14
+ }
15
+
4
16
  function formatDateCell(
5
17
  value: unknown,
6
18
  type: string,
@@ -12,17 +24,22 @@ function formatDateCell(
12
24
  ): string {
13
25
  try {
14
26
  const raw = typeof value === "string" ? value : String(value);
15
- const date = new Date(raw);
16
- if (Number.isNaN(date.getTime())) return raw;
17
27
  const locale = opts?.locale;
18
28
  if (opts?.dateStyle || opts?.timeStyle) {
19
- return date.toLocaleString(locale, {
29
+ if (type === "date") {
30
+ return toPlainDate(raw).toLocaleString(locale, {
31
+ dateStyle: opts.dateStyle,
32
+ });
33
+ }
34
+ return Temporal.Instant.from(raw).toLocaleString(locale, {
20
35
  dateStyle: opts.dateStyle,
21
36
  timeStyle: opts.timeStyle,
22
37
  });
23
38
  }
24
- if (type === "date") return date.toLocaleDateString(locale);
25
- return date.toLocaleString(locale, {
39
+ if (type === "date") {
40
+ return toPlainDate(raw).toLocaleString(locale);
41
+ }
42
+ return Temporal.Instant.from(raw).toLocaleString(locale, {
26
43
  year: "numeric",
27
44
  month: "short",
28
45
  day: "numeric",
@@ -36,6 +36,7 @@ export function createStore<T>(initial: T): WritableStore<T> {
36
36
  },
37
37
  setState: (next) => {
38
38
  const nextValue = typeof next === "function" ? (next as (prev: T) => T)(snapshot) : next;
39
+ // skip: next value identical to snapshot, avoid notifying listeners
39
40
  if (Object.is(nextValue, snapshot)) return;
40
41
  snapshot = nextValue;
41
42
  for (const listener of listeners) listener();