@cosmicdrift/kumiko-headless 0.328.0 → 0.329.0
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/dist/apex/css.d.ts +3 -0
- package/{src/apex/css.ts → dist/apex/css.js} +11 -24
- package/dist/apex/index.d.ts +229 -0
- package/dist/apex/index.js +360 -0
- package/dist/apex/lightbox.d.ts +4 -0
- package/{src/apex/lightbox.ts → dist/apex/lightbox.js} +0 -4
- package/dist/contracts/asset.d.ts +15 -0
- package/dist/contracts/asset.js +17 -0
- package/dist/contracts/index.d.ts +3 -0
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/locale.d.ts +7 -0
- package/dist/contracts/locale.js +11 -0
- package/dist/contracts/primitives.d.ts +93 -0
- package/dist/contracts/primitives.js +21 -0
- package/dist/dispatcher/index.d.ts +2 -0
- package/dist/dispatcher/index.js +1 -0
- package/dist/dispatcher/types.d.ts +86 -0
- package/dist/dispatcher/types.js +34 -0
- package/dist/form/form-controller.d.ts +2 -0
- package/dist/form/form-controller.js +404 -0
- package/dist/form/index.d.ts +3 -0
- package/dist/form/index.js +2 -0
- package/dist/form/types.d.ts +71 -0
- package/dist/form/types.js +1 -0
- package/dist/form/zod-bridge.d.ts +5 -0
- package/{src/form/zod-bridge.ts → dist/form/zod-bridge.js} +52 -58
- package/dist/format/escape.d.ts +5 -0
- package/{src/format/escape.ts → dist/format/escape.js} +43 -47
- package/dist/format/html-template.d.ts +9 -0
- package/dist/format/html-template.js +39 -0
- package/dist/format/index.d.ts +10 -0
- package/dist/format/index.js +192 -0
- package/dist/format/money.d.ts +1 -0
- package/{src/format/money.ts → dist/format/money.js} +6 -5
- package/dist/index.d.ts +14 -0
- package/dist/index.js +9 -0
- package/dist/locale-routing/index.d.ts +39 -0
- package/dist/locale-routing/index.js +143 -0
- package/dist/nav/index.d.ts +2 -0
- package/dist/nav/index.js +1 -0
- package/dist/nav/resolve.d.ts +2 -0
- package/dist/nav/resolve.js +58 -0
- package/dist/nav/types.d.ts +26 -0
- package/dist/nav/types.js +1 -0
- package/dist/store/create-store.d.ts +2 -0
- package/{src/store/create-store.ts → dist/store/create-store.js} +21 -23
- package/dist/store/equality.d.ts +1 -0
- package/{src/store/equality.ts → dist/store/equality.js} +19 -17
- package/dist/store/index.d.ts +3 -0
- package/dist/store/index.js +2 -0
- package/dist/store/types.d.ts +7 -0
- package/dist/store/types.js +10 -0
- package/dist/view-model/edit.d.ts +10 -0
- package/dist/view-model/edit.js +392 -0
- package/dist/view-model/embedded-list.d.ts +27 -0
- package/dist/view-model/embedded-list.js +63 -0
- package/dist/view-model/index.d.ts +7 -0
- package/dist/view-model/index.js +3 -0
- package/dist/view-model/list.d.ts +16 -0
- package/dist/view-model/list.js +201 -0
- package/dist/view-model/types.d.ts +300 -0
- package/dist/view-model/types.js +1 -0
- package/package.json +31 -12
- package/src/apex/__tests__/lightbox.test.ts +0 -20
- package/src/apex/__tests__/page-head.test.ts +0 -63
- package/src/apex/__tests__/render.test.ts +0 -401
- package/src/apex/index.ts +0 -667
- package/src/contracts/__tests__/contracts.test.ts +0 -123
- package/src/contracts/__tests__/field-issue-compat.test.ts +0 -19
- package/src/contracts/asset.ts +0 -48
- package/src/contracts/index.ts +0 -23
- package/src/contracts/locale.ts +0 -33
- package/src/contracts/primitives.ts +0 -158
- package/src/dispatcher/__tests__/contract.test.ts +0 -163
- package/src/dispatcher/index.ts +0 -16
- package/src/dispatcher/types.ts +0 -217
- package/src/form/__tests__/conditional-fields.test.ts +0 -177
- package/src/form/__tests__/form-controller.test.ts +0 -231
- package/src/form/__tests__/submit.test.ts +0 -575
- package/src/form/__tests__/validation.test.ts +0 -274
- package/src/form/form-controller.ts +0 -458
- package/src/form/index.ts +0 -15
- package/src/form/types.ts +0 -297
- package/src/format/__tests__/escape.test.ts +0 -79
- package/src/format/__tests__/format.test.ts +0 -285
- package/src/format/__tests__/html-template.test.ts +0 -47
- package/src/format/html-template.ts +0 -49
- package/src/format/index.ts +0 -223
- package/src/index.ts +0 -118
- package/src/locale-routing/__tests__/locale-routing.test.ts +0 -276
- package/src/locale-routing/index.ts +0 -218
- package/src/nav/__tests__/resolve.test.ts +0 -279
- package/src/nav/index.ts +0 -8
- package/src/nav/resolve.ts +0 -60
- package/src/nav/types.ts +0 -70
- package/src/store/__tests__/create-store.test.ts +0 -139
- package/src/store/__tests__/equality.test.ts +0 -66
- package/src/store/index.ts +0 -3
- package/src/store/types.ts +0 -27
- package/src/view-model/__tests__/edit.test.ts +0 -1072
- package/src/view-model/__tests__/embedded-list.test.ts +0 -96
- package/src/view-model/__tests__/field-labels.test.ts +0 -62
- package/src/view-model/__tests__/list.test.ts +0 -439
- package/src/view-model/edit.ts +0 -645
- package/src/view-model/embedded-list.ts +0 -89
- package/src/view-model/index.ts +0 -43
- package/src/view-model/list.ts +0 -280
- package/src/view-model/types.ts +0 -400
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import type { ZodError, ZodIssue } from "zod";
|
|
2
|
-
import type { FieldIssue } from "../dispatcher";
|
|
3
|
-
|
|
4
1
|
// Translates a ZodError into the same FieldIssue shape the server emits via
|
|
5
2
|
// its own zod-bridge (packages/framework/src/errors/zod-bridge.ts). The two
|
|
6
3
|
// bridges stay in sync on purpose: a field failing zod on the client reads
|
|
@@ -17,75 +14,72 @@ import type { FieldIssue } from "../dispatcher";
|
|
|
17
14
|
// tend to introduce new param keys, and the server's classes.test.ts is
|
|
18
15
|
// what catches them. A value added there should be added here too.
|
|
19
16
|
const ISSUE_PARAM_KEYS = [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
]
|
|
34
|
-
|
|
17
|
+
"minimum",
|
|
18
|
+
"maximum",
|
|
19
|
+
"expected",
|
|
20
|
+
"received",
|
|
21
|
+
"type",
|
|
22
|
+
"inclusive",
|
|
23
|
+
"exact",
|
|
24
|
+
"keys",
|
|
25
|
+
// Zod 4 additions
|
|
26
|
+
"format",
|
|
27
|
+
"divisor",
|
|
28
|
+
"values",
|
|
29
|
+
"pattern",
|
|
30
|
+
];
|
|
35
31
|
// Flat FieldIssue list — same shape as server-side ValidationError.fields.
|
|
36
32
|
// The form-controller groups this by `path` for its snapshot's errors map.
|
|
37
|
-
export function zodErrorToFieldIssues(error
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
33
|
+
export function zodErrorToFieldIssues(error) {
|
|
34
|
+
return error.issues.map((issue) => {
|
|
35
|
+
const params = extractIssueParams(issue);
|
|
36
|
+
return {
|
|
37
|
+
// Empty-path issues (top-level object/array failures) use "(root)" so
|
|
38
|
+
// the form-controller has a stable key to show "form itself is
|
|
39
|
+
// malformed" errors under — matches what the server does.
|
|
40
|
+
path: issue.path.map(String).join(".") || "(root)",
|
|
41
|
+
code: issue.code,
|
|
42
|
+
i18nKey: resolveI18nKey(issue),
|
|
43
|
+
...(params && { params }),
|
|
44
|
+
};
|
|
45
|
+
});
|
|
50
46
|
}
|
|
51
|
-
|
|
52
47
|
// Same magic key both sides of a superRefine can set to opt out of the
|
|
53
48
|
// generic `errors.validation.custom` message — see form-schema.ts.
|
|
54
49
|
export const I18N_KEY_PARAM = "i18nKey";
|
|
55
|
-
|
|
56
50
|
// `code: "custom"` is zod's catch-all for every superRefine/refine check;
|
|
57
51
|
// left mechanical it'd collapse onto one generic key, so a superRefine can
|
|
58
52
|
// set `params[I18N_KEY_PARAM]` to override it. Keep in sync with the server
|
|
59
53
|
// mirror (packages/framework/src/errors/zod-bridge.ts).
|
|
60
|
-
function resolveI18nKey(issue
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
54
|
+
function resolveI18nKey(issue) {
|
|
55
|
+
if (issue.code === "custom") {
|
|
56
|
+
const override = issue.params?.[I18N_KEY_PARAM];
|
|
57
|
+
if (typeof override === "string")
|
|
58
|
+
return override;
|
|
59
|
+
}
|
|
60
|
+
return `errors.validation.${issue.code}`;
|
|
66
61
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
62
|
+
function extractIssueParams(issue) {
|
|
63
|
+
const out = {};
|
|
64
|
+
const bag = issue; // @cast-boundary zod-issue
|
|
65
|
+
for (const key of ISSUE_PARAM_KEYS) {
|
|
66
|
+
if (bag[key] !== undefined)
|
|
67
|
+
out[key] = bag[key];
|
|
68
|
+
}
|
|
69
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
75
70
|
}
|
|
76
|
-
|
|
77
71
|
// Groups a flat FieldIssue list into the `errors` map carried by a
|
|
78
72
|
// FormSnapshot. A field with multiple issues (e.g. both "required" and
|
|
79
73
|
// "min") keeps both — UIs that only render the first can pick the first
|
|
80
74
|
// element, those that want a full list have it.
|
|
81
|
-
export function groupIssuesByPath(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
75
|
+
export function groupIssuesByPath(issues) {
|
|
76
|
+
const out = {};
|
|
77
|
+
for (const issue of issues) {
|
|
78
|
+
const bucket = out[issue.path];
|
|
79
|
+
if (bucket)
|
|
80
|
+
bucket.push(issue);
|
|
81
|
+
else
|
|
82
|
+
out[issue.path] = [issue];
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
91
85
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function escapeHtml(s: string): string;
|
|
2
|
+
export declare function escapeHtmlAttr(s: string): string;
|
|
3
|
+
export declare function escapeXml(s: string): string;
|
|
4
|
+
export declare function stripControlChars(value: string): string;
|
|
5
|
+
export declare function isSafeHref(href: string): boolean;
|
|
@@ -1,28 +1,25 @@
|
|
|
1
|
-
export function escapeHtml(s
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
export function escapeHtml(s) {
|
|
2
|
+
return s
|
|
3
|
+
.replace(/&/g, "&")
|
|
4
|
+
.replace(/</g, "<")
|
|
5
|
+
.replace(/>/g, ">")
|
|
6
|
+
.replace(/"/g, """);
|
|
7
7
|
}
|
|
8
|
-
|
|
9
8
|
// Superset of escapeHtml for attribute contexts: additionally escapes ' so
|
|
10
9
|
// single-quoted attributes cannot be broken out of.
|
|
11
|
-
export function escapeHtmlAttr(s
|
|
12
|
-
|
|
10
|
+
export function escapeHtmlAttr(s) {
|
|
11
|
+
return escapeHtml(s).replace(/'/g, "'");
|
|
13
12
|
}
|
|
14
|
-
|
|
15
13
|
// Shared API for app repos (publicstatus badge-SVG renders XML by hand) —
|
|
16
14
|
// no in-repo consumer yet, deliberately exported for cross-repo dedup.
|
|
17
|
-
export function escapeXml(s
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
export function escapeXml(s) {
|
|
16
|
+
return s
|
|
17
|
+
.replace(/&/g, "&")
|
|
18
|
+
.replace(/</g, "<")
|
|
19
|
+
.replace(/>/g, ">")
|
|
20
|
+
.replace(/"/g, """)
|
|
21
|
+
.replace(/'/g, "'");
|
|
24
22
|
}
|
|
25
|
-
|
|
26
23
|
// Strips ASCII control chars + space (codepoints 0x00-0x20 and 0x7f) — the
|
|
27
24
|
// same characters browsers strip before scheme detection. `.trim()` alone
|
|
28
25
|
// only removes leading/trailing whitespace, so a scheme like
|
|
@@ -31,15 +28,15 @@ export function escapeXml(s: string): string {
|
|
|
31
28
|
// `return true`, and the browser then normalizes it back to "javascript:"
|
|
32
29
|
// and executes it. Written as a char-code filter rather than a regex range
|
|
33
30
|
// to avoid embedding literal control characters in source.
|
|
34
|
-
export function stripControlChars(value
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
export function stripControlChars(value) {
|
|
32
|
+
let result = "";
|
|
33
|
+
for (const ch of value) {
|
|
34
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
35
|
+
if (code > 0x20 && code !== 0x7f)
|
|
36
|
+
result += ch;
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
41
39
|
}
|
|
42
|
-
|
|
43
40
|
// http(s)/mailto or scheme-less (relative/anchor) allowed; javascript:,
|
|
44
41
|
// data:, vbscript:, etc. rejected. Shared between renderer-web's Link
|
|
45
42
|
// primitive and page-render's server-side markdown renderer (both take
|
|
@@ -53,27 +50,26 @@ export function stripControlChars(value: string): string {
|
|
|
53
50
|
// (not delete) so the scheme check sees what the browser will execute —
|
|
54
51
|
// deleting ":" would leave no colon at all, which reads as a safe
|
|
55
52
|
// scheme-less href, exactly backwards.
|
|
56
|
-
const NAMED_HTML_ENTITY_CHARS
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
53
|
+
const NAMED_HTML_ENTITY_CHARS = {
|
|
54
|
+
colon: ":",
|
|
55
|
+
tab: "\t",
|
|
56
|
+
newline: "\n",
|
|
57
|
+
semi: ";",
|
|
58
|
+
amp: "&",
|
|
62
59
|
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
});
|
|
60
|
+
function decodeHtmlEntities(value) {
|
|
61
|
+
return value.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z][a-z0-9]*);/gi, (match, entity) => {
|
|
62
|
+
if (entity[0] === "#") {
|
|
63
|
+
const isHex = entity[1]?.toLowerCase() === "x";
|
|
64
|
+
const code = Number.parseInt(isHex ? entity.slice(2) : entity.slice(1), isHex ? 16 : 10);
|
|
65
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : match;
|
|
66
|
+
}
|
|
67
|
+
return NAMED_HTML_ENTITY_CHARS[entity.toLowerCase()] ?? match;
|
|
68
|
+
});
|
|
73
69
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
70
|
+
export function isSafeHref(href) {
|
|
71
|
+
const trimmed = stripControlChars(decodeHtmlEntities(href)).toLowerCase();
|
|
72
|
+
if (!/^[a-z][a-z0-9+.-]*:/.test(trimmed))
|
|
73
|
+
return true;
|
|
74
|
+
return /^(?:https?|mailto):/.test(trimmed);
|
|
79
75
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare class RawHtml {
|
|
2
|
+
readonly html: string;
|
|
3
|
+
constructor(html: string);
|
|
4
|
+
toString(): string;
|
|
5
|
+
}
|
|
6
|
+
/** Markiert bereits escaptes/vertrauenswürdiges Markup für html`...`. */
|
|
7
|
+
export declare function raw(html: string): RawHtml;
|
|
8
|
+
export type HtmlValue = string | number | boolean | null | undefined | RawHtml | ReadonlyArray<HtmlValue>;
|
|
9
|
+
export declare function html(strings: TemplateStringsArray, ...values: ReadonlyArray<HtmlValue>): RawHtml;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// html`...` — Tagged-Template, das jede Interpolation automatisch HTML-escaped.
|
|
2
|
+
// Macht Escaping strukturell statt per-Callsite-Konvention: vergessen ist
|
|
3
|
+
// unmöglich, bewusst rohes HTML braucht ein explizites raw(). Der
|
|
4
|
+
// HTML-Escape-Guard (kumiko-guards) akzeptiert html`...` als safe.
|
|
5
|
+
import { escapeHtml } from "./escape.js";
|
|
6
|
+
export class RawHtml {
|
|
7
|
+
html;
|
|
8
|
+
constructor(html) {
|
|
9
|
+
this.html = html;
|
|
10
|
+
}
|
|
11
|
+
toString() {
|
|
12
|
+
return this.html;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Markiert bereits escaptes/vertrauenswürdiges Markup für html`...`. */
|
|
16
|
+
export function raw(html) {
|
|
17
|
+
return new RawHtml(html);
|
|
18
|
+
}
|
|
19
|
+
function renderValue(value) {
|
|
20
|
+
if (value === null || value === undefined)
|
|
21
|
+
return "";
|
|
22
|
+
if (value instanceof RawHtml)
|
|
23
|
+
return value.html;
|
|
24
|
+
if (Array.isArray(value))
|
|
25
|
+
return value.map(renderValue).join("");
|
|
26
|
+
if (typeof value === "string")
|
|
27
|
+
return escapeHtml(value);
|
|
28
|
+
return String(value);
|
|
29
|
+
}
|
|
30
|
+
// Rückgabe ist RawHtml, damit Fragmente verschachtelbar sind ohne doppelt zu
|
|
31
|
+
// escapen: `html`<div>${item}</div>`` innerhalb eines äußeren html`...`
|
|
32
|
+
// passiert unverändert durch. toString() liefert das fertige Markup.
|
|
33
|
+
export function html(strings, ...values) {
|
|
34
|
+
let out = strings[0] ?? "";
|
|
35
|
+
values.forEach((value, i) => {
|
|
36
|
+
out += renderValue(value) + (strings[i + 1] ?? "");
|
|
37
|
+
});
|
|
38
|
+
return new RawHtml(out);
|
|
39
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Temporal } from "temporal-polyfill";
|
|
2
|
+
export declare function toInstant(raw: string): Temporal.Instant;
|
|
3
|
+
/** Exported for UnitKey parity tests against @cosmicdrift/kumiko-types/screen. */
|
|
4
|
+
export declare const UNIT_FORMAT_KEYS: readonly string[];
|
|
5
|
+
export { escapeHtml, escapeHtmlAttr, escapeXml, isSafeHref, stripControlChars } from "./escape.js";
|
|
6
|
+
export { type HtmlValue, html, RawHtml, raw } from "./html-template.js";
|
|
7
|
+
export { currencyDecimals } from "./money.js";
|
|
8
|
+
export declare function applyFormatSpec(spec: {
|
|
9
|
+
format: string;
|
|
10
|
+
} & Record<string, unknown>, value: unknown, translate?: (key: string) => string): string;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Pure format utilities — no web or platform dependencies.
|
|
2
|
+
// Shared between renderer-web, renderer-native, and server-side tests.
|
|
3
|
+
import { Temporal } from "temporal-polyfill";
|
|
4
|
+
function toPlainDate(raw) {
|
|
5
|
+
try {
|
|
6
|
+
return Temporal.PlainDate.from(raw);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
// "date"-typed field stored as a full instant (day-boundary timestamp) —
|
|
10
|
+
// take the calendar date in the local zone rather than fail.
|
|
11
|
+
return toInstant(raw).toZonedDateTimeISO(Temporal.Now.timeZoneId()).toPlainDate();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
// Temporal.Instant.from is stricter than the `new Date(raw)` this replaced:
|
|
15
|
+
// it requires a UTC designator/offset (Z/+hh:mm). Timestamps without one
|
|
16
|
+
// (e.g. "2026-07-18T12:00:00", still valid input to `new Date`) throw here
|
|
17
|
+
// instead of parsing — fall back to reading them as a local wall-clock time,
|
|
18
|
+
// same posture as toPlainDate's own fallback above.
|
|
19
|
+
export function toInstant(raw) {
|
|
20
|
+
try {
|
|
21
|
+
return Temporal.Instant.from(raw);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return Temporal.PlainDateTime.from(raw).toZonedDateTime(Temporal.Now.timeZoneId()).toInstant();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function formatDateCell(value, type, opts) {
|
|
28
|
+
try {
|
|
29
|
+
const raw = typeof value === "string" ? value : String(value);
|
|
30
|
+
const locale = opts?.locale;
|
|
31
|
+
if (opts?.dateStyle || opts?.timeStyle) {
|
|
32
|
+
if (type === "date") {
|
|
33
|
+
// ponytail: timeStyle is ignored here on purpose — a PlainDate has no
|
|
34
|
+
// time component to format, so a timeStyle-only "date" field falls
|
|
35
|
+
// back to PlainDate's default dateStyle instead of rendering a time.
|
|
36
|
+
return toPlainDate(raw).toLocaleString(locale, {
|
|
37
|
+
dateStyle: opts.dateStyle,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return toInstant(raw).toLocaleString(locale, {
|
|
41
|
+
dateStyle: opts.dateStyle,
|
|
42
|
+
timeStyle: opts.timeStyle,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (type === "date") {
|
|
46
|
+
return toPlainDate(raw).toLocaleString(locale, {
|
|
47
|
+
year: "numeric",
|
|
48
|
+
month: "2-digit",
|
|
49
|
+
day: "2-digit",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return toInstant(raw).toLocaleString(locale, {
|
|
53
|
+
year: "numeric",
|
|
54
|
+
month: "short",
|
|
55
|
+
day: "numeric",
|
|
56
|
+
hour: "2-digit",
|
|
57
|
+
minute: "2-digit",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return String(value);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// No `locale` fallback to a browser API here (headless has none) — passing
|
|
65
|
+
// `undefined` to Intl.NumberFormat resolves the runtime's default locale,
|
|
66
|
+
// the same value a browser's navigator.language-based guess would produce.
|
|
67
|
+
function formatNumberCell(value, locale, grouping = true) {
|
|
68
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
69
|
+
return String(value);
|
|
70
|
+
try {
|
|
71
|
+
return new Intl.NumberFormat(locale, { useGrouping: grouping }).format(value);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Malformed BCP-47 locale tag — mirrors formatDateCell's fallback above.
|
|
75
|
+
return String(value);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Two-tier unit table for `{ format: "unit" }`. Keys mirror
|
|
79
|
+
// @cosmicdrift/kumiko-types' UnitKey (kept as a local literal union instead
|
|
80
|
+
// of an import — headless has no build-time dependency on kumiko-types
|
|
81
|
+
// today, and applyFormatSpec's spec param is already an untyped
|
|
82
|
+
// Record<string, unknown>, cast per-case like every other format below).
|
|
83
|
+
const UNIT_INTL_IDS = {
|
|
84
|
+
km: "kilometer",
|
|
85
|
+
m: "meter",
|
|
86
|
+
kg: "kilogram",
|
|
87
|
+
percent: "percent",
|
|
88
|
+
mi: "mile",
|
|
89
|
+
};
|
|
90
|
+
// "m2" (square meter) has no ECMA-402 sanctioned unit — Intl.NumberFormat
|
|
91
|
+
// throws RangeError for "square-meter". Locale-format the number and append
|
|
92
|
+
// the literal suffix instead of going through `style: "unit"`.
|
|
93
|
+
const UNIT_SUFFIXES = { m2: "m²" };
|
|
94
|
+
function isIntlUnit(unit) {
|
|
95
|
+
return Object.hasOwn(UNIT_INTL_IDS, unit);
|
|
96
|
+
}
|
|
97
|
+
function isSuffixUnit(unit) {
|
|
98
|
+
return Object.hasOwn(UNIT_SUFFIXES, unit);
|
|
99
|
+
}
|
|
100
|
+
function formatUnitCell(value, unit, locale, unitDisplay) {
|
|
101
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
102
|
+
if (unit === undefined || !Number.isFinite(n))
|
|
103
|
+
return String(value);
|
|
104
|
+
if (isIntlUnit(unit)) {
|
|
105
|
+
try {
|
|
106
|
+
return new Intl.NumberFormat(locale, {
|
|
107
|
+
style: "unit",
|
|
108
|
+
unit: UNIT_INTL_IDS[unit],
|
|
109
|
+
unitDisplay,
|
|
110
|
+
}).format(n);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Malformed BCP-47 locale tag — mirrors formatNumberCell's fallback below.
|
|
114
|
+
return formatNumberCell(n, locale);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (isSuffixUnit(unit)) {
|
|
118
|
+
return `${formatNumberCell(n, locale)} ${UNIT_SUFFIXES[unit]}`;
|
|
119
|
+
}
|
|
120
|
+
return String(value);
|
|
121
|
+
}
|
|
122
|
+
/** Exported for UnitKey parity tests against @cosmicdrift/kumiko-types/screen. */
|
|
123
|
+
export const UNIT_FORMAT_KEYS = [
|
|
124
|
+
...Object.keys(UNIT_INTL_IDS),
|
|
125
|
+
...Object.keys(UNIT_SUFFIXES),
|
|
126
|
+
];
|
|
127
|
+
export { escapeHtml, escapeHtmlAttr, escapeXml, isSafeHref, stripControlChars } from "./escape.js";
|
|
128
|
+
export { html, RawHtml, raw } from "./html-template.js";
|
|
129
|
+
export { currencyDecimals } from "./money.js";
|
|
130
|
+
export function applyFormatSpec(spec, value, translate) {
|
|
131
|
+
const isEmpty = value === null || value === undefined || value === "";
|
|
132
|
+
// priority renders its emptyLabel for empty values — every other format
|
|
133
|
+
// collapses empty to "".
|
|
134
|
+
if (isEmpty && spec.format !== "priority")
|
|
135
|
+
return "";
|
|
136
|
+
switch (spec.format) {
|
|
137
|
+
case "timestamp":
|
|
138
|
+
case "date":
|
|
139
|
+
return formatDateCell(value, spec.format, {
|
|
140
|
+
locale: spec["locale"],
|
|
141
|
+
dateStyle: spec["dateStyle"],
|
|
142
|
+
timeStyle: spec.format === "timestamp"
|
|
143
|
+
? spec["timeStyle"]
|
|
144
|
+
: undefined,
|
|
145
|
+
});
|
|
146
|
+
case "boolean": {
|
|
147
|
+
if (value === true) {
|
|
148
|
+
const trueLabel = spec["trueLabel"];
|
|
149
|
+
return trueLabel === undefined ? "✓" : (translate?.(trueLabel) ?? trueLabel);
|
|
150
|
+
}
|
|
151
|
+
if (value === false) {
|
|
152
|
+
const falseLabel = spec["falseLabel"];
|
|
153
|
+
return falseLabel === undefined ? "" : (translate?.(falseLabel) ?? falseLabel);
|
|
154
|
+
}
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
case "currency": {
|
|
158
|
+
const sym = spec["symbol"] ?? "";
|
|
159
|
+
return sym.length > 0 ? `${value} ${sym}` : String(value);
|
|
160
|
+
}
|
|
161
|
+
case "number":
|
|
162
|
+
case "decimal":
|
|
163
|
+
case "bigInt":
|
|
164
|
+
return formatNumberCell(value, spec["locale"], spec["grouping"] ?? true);
|
|
165
|
+
case "unit":
|
|
166
|
+
return formatUnitCell(value, spec["unit"], spec["locale"], spec["unitDisplay"] ?? "short");
|
|
167
|
+
case "priority": {
|
|
168
|
+
const emptyLabel = spec["emptyLabel"] ?? "—";
|
|
169
|
+
const prefix = spec["prefix"] ?? "";
|
|
170
|
+
if (isEmpty || value === 0)
|
|
171
|
+
return emptyLabel;
|
|
172
|
+
return `${prefix}${value}`;
|
|
173
|
+
}
|
|
174
|
+
case "enumOption": {
|
|
175
|
+
const raw = typeof value === "string" ? value : String(value);
|
|
176
|
+
const keyPrefix = spec["keyPrefix"];
|
|
177
|
+
if (keyPrefix === undefined || translate === undefined)
|
|
178
|
+
return raw;
|
|
179
|
+
const key = `${keyPrefix}${raw}`;
|
|
180
|
+
const translated = translate(key);
|
|
181
|
+
return translated === key ? raw : translated;
|
|
182
|
+
}
|
|
183
|
+
case "json":
|
|
184
|
+
return JSON.stringify(value, null, spec["indent"] ?? 2);
|
|
185
|
+
default:
|
|
186
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
|
|
187
|
+
// biome-ignore lint/suspicious/noConsole: dev-only warning
|
|
188
|
+
console.warn(`[kumiko] applyFormatSpec: unknown format key "${spec.format}" — registered via FieldFormatRegistry module augmentation?`);
|
|
189
|
+
}
|
|
190
|
+
return typeof value === "string" ? value : String(value);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function currencyDecimals(code: string): number;
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
// MoneyInput (minor-unit display factor) and RenderField's minor/major-unit
|
|
4
4
|
// conversion (kumiko-framework#1923) — a single source keeps both sides
|
|
5
5
|
// scaling the same currency the same way.
|
|
6
|
-
export function currencyDecimals(code
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
export function currencyDecimals(code) {
|
|
7
|
+
if (code === "JPY" || code === "KRW" || code === "VND" || code === "ISK")
|
|
8
|
+
return 0;
|
|
9
|
+
if (code === "BHD" || code === "JOD" || code === "KWD" || code === "OMR" || code === "TND")
|
|
10
|
+
return 3;
|
|
11
|
+
return 2;
|
|
11
12
|
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type { ParsedRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
export { parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
|
+
export type { AssetResolution, AssetResolveContext, AssetResolver, BadgeProps, ButtonProps, CardProps, DatePickerProps, IconProps, LocaleResolver, ModalProps, NumberInputProps, PrimitiveCommonProps, PrimitivesContract, SelectOption, SelectProps, TextInputProps, ToastIntent, ToastProps, ToggleProps, } from "./contracts/index.js";
|
|
4
|
+
export type { BatchResult, Command, Dispatcher, DispatcherError, DispatcherStatus, FieldIssue, PendingFile, PendingWrite, QueryOpts, QueryResult, StreamOpts, WriteOpts, WriteResult, } from "./dispatcher/index.js";
|
|
5
|
+
export { StreamFrame, type StreamFrameEvent } from "./dispatcher/index.js";
|
|
6
|
+
export type { FieldConditionPredicate, FieldConditions, FieldConditionValue, FieldState, FormController, FormControllerOptions, FormSnapshot, FormValues, SubmitConfig, SubmitPayloadMode, SubmitResult, } from "./form/index.js";
|
|
7
|
+
export { createFormController, I18N_KEY_PARAM } from "./form/index.js";
|
|
8
|
+
export { applyFormatSpec, currencyDecimals, escapeHtml, escapeHtmlAttr, escapeXml, type HtmlValue, html, isSafeHref, RawHtml, raw, stripControlChars, toInstant, } from "./format/index.js";
|
|
9
|
+
export type { NavDefinition, NavNode, NavRegistrySlice, NavTree, ResolveNavigationOptions, } from "./nav/index.js";
|
|
10
|
+
export { resolveNavigation } from "./nav/index.js";
|
|
11
|
+
export type { Store, WritableStore } from "./store/index.js";
|
|
12
|
+
export { createStore, shallowEqual } from "./store/index.js";
|
|
13
|
+
export type { ComputeEditViewModelInput, ComputeListViewModelInput, DerivedCellRoundingTarget, EditExtensionSectionViewModel, EditFieldSpec, EditFieldsSectionViewModel, EditFieldViewModel, EditRelatedListSectionViewModel, EditSectionSpec, EditSectionViewModel, EditViewModel, EditWriteFormSectionViewModel, EmbeddedDerivedOp, EmbeddedListCellViewModel, EmbeddedListIssueGroups, FieldConditionCtx, FieldRenderer, ListColumnSpec, ListColumnViewModel, ListRowViewModel, ListViewModel, RuntimeRenderer, ScreenSlots, Translate, } from "./view-model/index.js";
|
|
14
|
+
export { computeDerivedCellValue, computeEditViewModel, computeListViewModel, embeddedCellLabelKey, embeddedCellOptionLabelKey, fieldLabelKey, fieldOptionLabelKey, fieldOptionLabelKeyPrefix, groupEmbeddedListIssues, roundDerivedCellValue, sumEmbeddedListColumn, } from "./view-model/index.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Re-Export aus framework/ui-types damit Renderer-Code denselben
|
|
2
|
+
// Parser nutzt wie der Server-Boot-Validator (Cross-Feature-Refs).
|
|
3
|
+
export { parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
4
|
+
export { StreamFrame } from "./dispatcher/index.js";
|
|
5
|
+
export { createFormController, I18N_KEY_PARAM } from "./form/index.js";
|
|
6
|
+
export { applyFormatSpec, currencyDecimals, escapeHtml, escapeHtmlAttr, escapeXml, html, isSafeHref, RawHtml, raw, stripControlChars, toInstant, } from "./format/index.js";
|
|
7
|
+
export { resolveNavigation } from "./nav/index.js";
|
|
8
|
+
export { createStore, shallowEqual } from "./store/index.js";
|
|
9
|
+
export { computeDerivedCellValue, computeEditViewModel, computeListViewModel, embeddedCellLabelKey, embeddedCellOptionLabelKey, fieldLabelKey, fieldOptionLabelKey, fieldOptionLabelKeyPrefix, groupEmbeddedListIssues, roundDerivedCellValue, sumEmbeddedListColumn, } from "./view-model/index.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { LocaleResolver } from "../contracts/index.js";
|
|
2
|
+
export type LocaleRouterConfig<TPage extends string> = {
|
|
3
|
+
/** Canonical default, usually "de". No URL prefix. */
|
|
4
|
+
readonly defaultLocale: string;
|
|
5
|
+
/** Locales that get a URL prefix, e.g. ["en"] → /en/... */
|
|
6
|
+
readonly prefixedLocales: readonly string[];
|
|
7
|
+
/** Prefix segment per locale, default: locale code ("en" → "/en"). */
|
|
8
|
+
readonly prefixFor?: (locale: string) => string;
|
|
9
|
+
/** Logical page → path per locale. Every page must define defaultLocale path. */
|
|
10
|
+
readonly routes: Record<TPage, Record<string, string>>;
|
|
11
|
+
/** Legacy slug-only paths for detectLang/resolvePage, e.g. { en: ["/features"] }. */
|
|
12
|
+
readonly localeHints?: Readonly<Record<string, readonly string[]>>;
|
|
13
|
+
/** Fallback page when altLocalePath cannot resolve pathname. Default: "home". */
|
|
14
|
+
readonly homePage?: TPage;
|
|
15
|
+
};
|
|
16
|
+
export type LocaleRouter<TPage extends string> = {
|
|
17
|
+
detectLang(pathname: string): string;
|
|
18
|
+
publicPath(page: TPage, locale: string): string;
|
|
19
|
+
resolvePage(pathname: string): TPage | undefined;
|
|
20
|
+
altLocalePath(pathname: string, targetLocale?: string): string;
|
|
21
|
+
sectionAnchor(page: TPage, locale: string, fragment: string): string;
|
|
22
|
+
};
|
|
23
|
+
export declare function createLocaleRouter<TPage extends string>(config: LocaleRouterConfig<TPage>): LocaleRouter<TPage>;
|
|
24
|
+
export type WrapUrlLocaleResolverOptions = {
|
|
25
|
+
resolvePage: (pathname: string) => string | undefined;
|
|
26
|
+
detectLang: (pathname: string) => string;
|
|
27
|
+
pathname?: () => string | undefined;
|
|
28
|
+
/** Optional pathname-change subscription (browser default: popstate). */
|
|
29
|
+
subscribePathname?: (listener: () => void) => () => void;
|
|
30
|
+
/** When set, translate against the URL-resolved locale instead of base.translate
|
|
31
|
+
* (needed for fixed-T / cloneInstance resolvers that are not locale-live). */
|
|
32
|
+
translateFor?: (locale: string, key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Prefer URL-derived locale when `resolvePage` matches the current path;
|
|
36
|
+
* otherwise defer to `base.locale()`. Passes through setLocale and the rest of LocaleResolver.
|
|
37
|
+
* Without `translateFor`, `base.translate` must already be locale-live.
|
|
38
|
+
*/
|
|
39
|
+
export declare function wrapUrlLocaleResolver(base: LocaleResolver, options: WrapUrlLocaleResolverOptions): LocaleResolver;
|