@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.
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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({});
|
package/src/format/index.ts
CHANGED
|
@@ -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
|
-
|
|
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")
|
|
25
|
-
|
|
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();
|