@cosmicdrift/kumiko-headless 0.328.0 → 0.328.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.
Files changed (108) hide show
  1. package/dist/apex/css.d.ts +3 -0
  2. package/{src/apex/css.ts → dist/apex/css.js} +11 -24
  3. package/dist/apex/index.d.ts +229 -0
  4. package/dist/apex/index.js +360 -0
  5. package/dist/apex/lightbox.d.ts +4 -0
  6. package/{src/apex/lightbox.ts → dist/apex/lightbox.js} +0 -4
  7. package/dist/contracts/asset.d.ts +15 -0
  8. package/dist/contracts/asset.js +17 -0
  9. package/dist/contracts/index.d.ts +3 -0
  10. package/dist/contracts/index.js +1 -0
  11. package/dist/contracts/locale.d.ts +7 -0
  12. package/dist/contracts/locale.js +11 -0
  13. package/dist/contracts/primitives.d.ts +93 -0
  14. package/dist/contracts/primitives.js +21 -0
  15. package/dist/dispatcher/index.d.ts +2 -0
  16. package/dist/dispatcher/index.js +1 -0
  17. package/dist/dispatcher/types.d.ts +86 -0
  18. package/dist/dispatcher/types.js +34 -0
  19. package/dist/form/form-controller.d.ts +2 -0
  20. package/dist/form/form-controller.js +404 -0
  21. package/dist/form/index.d.ts +3 -0
  22. package/dist/form/index.js +2 -0
  23. package/dist/form/types.d.ts +71 -0
  24. package/dist/form/types.js +1 -0
  25. package/dist/form/zod-bridge.d.ts +5 -0
  26. package/{src/form/zod-bridge.ts → dist/form/zod-bridge.js} +52 -58
  27. package/dist/format/escape.d.ts +5 -0
  28. package/{src/format/escape.ts → dist/format/escape.js} +43 -47
  29. package/dist/format/html-template.d.ts +9 -0
  30. package/dist/format/html-template.js +39 -0
  31. package/dist/format/index.d.ts +10 -0
  32. package/dist/format/index.js +192 -0
  33. package/dist/format/money.d.ts +1 -0
  34. package/{src/format/money.ts → dist/format/money.js} +6 -5
  35. package/dist/index.d.ts +14 -0
  36. package/dist/index.js +9 -0
  37. package/dist/locale-routing/index.d.ts +39 -0
  38. package/dist/locale-routing/index.js +143 -0
  39. package/dist/nav/index.d.ts +2 -0
  40. package/dist/nav/index.js +1 -0
  41. package/dist/nav/resolve.d.ts +2 -0
  42. package/dist/nav/resolve.js +58 -0
  43. package/dist/nav/types.d.ts +26 -0
  44. package/dist/nav/types.js +1 -0
  45. package/dist/store/create-store.d.ts +2 -0
  46. package/{src/store/create-store.ts → dist/store/create-store.js} +21 -23
  47. package/dist/store/equality.d.ts +1 -0
  48. package/{src/store/equality.ts → dist/store/equality.js} +19 -17
  49. package/dist/store/index.d.ts +3 -0
  50. package/dist/store/index.js +2 -0
  51. package/dist/store/types.d.ts +7 -0
  52. package/dist/store/types.js +10 -0
  53. package/dist/view-model/edit.d.ts +10 -0
  54. package/dist/view-model/edit.js +392 -0
  55. package/dist/view-model/embedded-list.d.ts +27 -0
  56. package/dist/view-model/embedded-list.js +63 -0
  57. package/dist/view-model/index.d.ts +7 -0
  58. package/dist/view-model/index.js +3 -0
  59. package/dist/view-model/list.d.ts +16 -0
  60. package/dist/view-model/list.js +201 -0
  61. package/dist/view-model/types.d.ts +300 -0
  62. package/dist/view-model/types.js +1 -0
  63. package/package.json +31 -12
  64. package/src/apex/__tests__/lightbox.test.ts +0 -20
  65. package/src/apex/__tests__/page-head.test.ts +0 -63
  66. package/src/apex/__tests__/render.test.ts +0 -401
  67. package/src/apex/index.ts +0 -667
  68. package/src/contracts/__tests__/contracts.test.ts +0 -123
  69. package/src/contracts/__tests__/field-issue-compat.test.ts +0 -19
  70. package/src/contracts/asset.ts +0 -48
  71. package/src/contracts/index.ts +0 -23
  72. package/src/contracts/locale.ts +0 -33
  73. package/src/contracts/primitives.ts +0 -158
  74. package/src/dispatcher/__tests__/contract.test.ts +0 -163
  75. package/src/dispatcher/index.ts +0 -16
  76. package/src/dispatcher/types.ts +0 -217
  77. package/src/form/__tests__/conditional-fields.test.ts +0 -177
  78. package/src/form/__tests__/form-controller.test.ts +0 -231
  79. package/src/form/__tests__/submit.test.ts +0 -575
  80. package/src/form/__tests__/validation.test.ts +0 -274
  81. package/src/form/form-controller.ts +0 -458
  82. package/src/form/index.ts +0 -15
  83. package/src/form/types.ts +0 -297
  84. package/src/format/__tests__/escape.test.ts +0 -79
  85. package/src/format/__tests__/format.test.ts +0 -285
  86. package/src/format/__tests__/html-template.test.ts +0 -47
  87. package/src/format/html-template.ts +0 -49
  88. package/src/format/index.ts +0 -223
  89. package/src/index.ts +0 -118
  90. package/src/locale-routing/__tests__/locale-routing.test.ts +0 -276
  91. package/src/locale-routing/index.ts +0 -218
  92. package/src/nav/__tests__/resolve.test.ts +0 -279
  93. package/src/nav/index.ts +0 -8
  94. package/src/nav/resolve.ts +0 -60
  95. package/src/nav/types.ts +0 -70
  96. package/src/store/__tests__/create-store.test.ts +0 -139
  97. package/src/store/__tests__/equality.test.ts +0 -66
  98. package/src/store/index.ts +0 -3
  99. package/src/store/types.ts +0 -27
  100. package/src/view-model/__tests__/edit.test.ts +0 -1072
  101. package/src/view-model/__tests__/embedded-list.test.ts +0 -96
  102. package/src/view-model/__tests__/field-labels.test.ts +0 -62
  103. package/src/view-model/__tests__/list.test.ts +0 -439
  104. package/src/view-model/edit.ts +0 -645
  105. package/src/view-model/embedded-list.ts +0 -89
  106. package/src/view-model/index.ts +0 -43
  107. package/src/view-model/list.ts +0 -280
  108. package/src/view-model/types.ts +0 -400
@@ -0,0 +1,143 @@
1
+ function normalizePath(pathname) {
2
+ if (pathname === "" || pathname === "/")
3
+ return "/";
4
+ return pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
5
+ }
6
+ function defaultPrefixFor(locale) {
7
+ return `/${locale}`;
8
+ }
9
+ function stripLocalePrefix(path, locale, prefixFor) {
10
+ const prefix = prefixFor(locale);
11
+ if (path === prefix)
12
+ return "/";
13
+ if (path.startsWith(`${prefix}/`))
14
+ return path.slice(prefix.length);
15
+ return path;
16
+ }
17
+ export function createLocaleRouter(config) {
18
+ const { defaultLocale, prefixedLocales, routes, localeHints = {}, prefixFor = defaultPrefixFor, homePage = "home", } = config;
19
+ if (routes[homePage] === undefined) {
20
+ throw new Error(`locale-routing: homePage "${String(homePage)}" has no entry in routes — altLocalePath's fallback would throw at request time`);
21
+ }
22
+ const pathIndex = new Map();
23
+ for (const [page, localePaths] of Object.entries(routes)) {
24
+ for (const [locale, routePath] of Object.entries(localePaths)) {
25
+ pathIndex.set(normalizePath(routePath), { page, locale });
26
+ }
27
+ }
28
+ for (const [hintLocale, hints] of Object.entries(localeHints)) {
29
+ for (const hint of hints) {
30
+ const normalizedHint = normalizePath(hint);
31
+ if (pathIndex.has(normalizedHint))
32
+ continue;
33
+ for (const [page, localePaths] of Object.entries(routes)) {
34
+ const canonical = localePaths[hintLocale];
35
+ if (canonical === undefined)
36
+ continue;
37
+ const canonicalNorm = normalizePath(canonical);
38
+ const hintSlug = stripLocalePrefix(normalizedHint, hintLocale, prefixFor);
39
+ const canonicalSlug = stripLocalePrefix(canonicalNorm, hintLocale, prefixFor);
40
+ if (hintSlug === canonicalSlug) {
41
+ pathIndex.set(normalizedHint, { page, locale: hintLocale });
42
+ break;
43
+ }
44
+ }
45
+ }
46
+ }
47
+ function detectLang(pathname) {
48
+ const path = normalizePath(pathname);
49
+ for (const locale of prefixedLocales) {
50
+ const prefix = prefixFor(locale);
51
+ if (path === prefix || path.startsWith(`${prefix}/`))
52
+ return locale;
53
+ }
54
+ const resolved = pathIndex.get(path);
55
+ if (resolved !== undefined)
56
+ return resolved.locale;
57
+ for (const [locale, hints] of Object.entries(localeHints)) {
58
+ if (hints.some((hint) => normalizePath(hint) === path))
59
+ return locale;
60
+ }
61
+ return defaultLocale;
62
+ }
63
+ function resolvePage(pathname) {
64
+ return pathIndex.get(normalizePath(pathname))?.page;
65
+ }
66
+ function publicPath(page, locale) {
67
+ const localePaths = routes[page];
68
+ const routePath = localePaths?.[locale];
69
+ if (localePaths === undefined ||
70
+ !Object.hasOwn(localePaths, locale) ||
71
+ routePath === undefined) {
72
+ throw new Error(`locale-routing: no path for page "${String(page)}" locale "${locale}"`);
73
+ }
74
+ return routePath;
75
+ }
76
+ function otherLocale(currentLocale) {
77
+ if (currentLocale === defaultLocale) {
78
+ return prefixedLocales[0] ?? defaultLocale;
79
+ }
80
+ return defaultLocale;
81
+ }
82
+ function altLocalePath(pathname, targetLocale) {
83
+ const path = normalizePath(pathname);
84
+ const resolved = pathIndex.get(path);
85
+ const locale = targetLocale ?? otherLocale(detectLang(pathname));
86
+ if (resolved !== undefined) {
87
+ return publicPath(resolved.page, locale);
88
+ }
89
+ return publicPath(homePage, locale);
90
+ }
91
+ function sectionAnchor(page, locale, fragment) {
92
+ return `${publicPath(page, locale)}#${fragment}`;
93
+ }
94
+ return { detectLang, publicPath, resolvePage, altLocalePath, sectionAnchor };
95
+ }
96
+ function defaultBrowserPathname() {
97
+ const loc = globalThis.location;
98
+ return typeof loc?.pathname === "string" ? loc.pathname : undefined;
99
+ }
100
+ function defaultSubscribePathname(listener) {
101
+ const g = globalThis;
102
+ if (typeof g.addEventListener !== "function" || typeof g.removeEventListener !== "function") {
103
+ return () => { };
104
+ }
105
+ g.addEventListener("popstate", listener);
106
+ return () => {
107
+ g.removeEventListener?.("popstate", listener);
108
+ };
109
+ }
110
+ /**
111
+ * Prefer URL-derived locale when `resolvePage` matches the current path;
112
+ * otherwise defer to `base.locale()`. Passes through setLocale and the rest of LocaleResolver.
113
+ * Without `translateFor`, `base.translate` must already be locale-live.
114
+ */
115
+ export function wrapUrlLocaleResolver(base, options) {
116
+ const getPathname = options.pathname ?? defaultBrowserPathname;
117
+ const subscribePathname = options.subscribePathname ?? defaultSubscribePathname;
118
+ const resolveLocale = () => {
119
+ const path = getPathname();
120
+ if (path === undefined)
121
+ return base.locale();
122
+ if (options.resolvePage(path) != null) {
123
+ return options.detectLang(path);
124
+ }
125
+ return base.locale();
126
+ };
127
+ return {
128
+ translate: (key, params) => options.translateFor
129
+ ? options.translateFor(resolveLocale(), key, params)
130
+ : base.translate(key, params),
131
+ timeZone: () => base.timeZone(),
132
+ subscribe: (listener) => {
133
+ const unsubBase = base.subscribe(listener);
134
+ const unsubPath = subscribePathname(listener);
135
+ return () => {
136
+ unsubBase();
137
+ unsubPath();
138
+ };
139
+ },
140
+ setLocale: base.setLocale !== undefined ? (locale) => base.setLocale?.(locale) : undefined,
141
+ locale: resolveLocale,
142
+ };
143
+ }
@@ -0,0 +1,2 @@
1
+ export { resolveNavigation } from "./resolve.js";
2
+ export type { NavDefinition, NavNode, NavRegistrySlice, NavTree, ResolveNavigationOptions, } from "./types.js";
@@ -0,0 +1 @@
1
+ export { resolveNavigation } from "./resolve.js";
@@ -0,0 +1,2 @@
1
+ import type { NavTree, ResolveNavigationOptions } from "./types.js";
2
+ export declare function resolveNavigation(options: ResolveNavigationOptions): NavTree;
@@ -0,0 +1,58 @@
1
+ import { isUiAccessGranted } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ // Assembles the renderable nav tree from the registry's pre-grouped
3
+ // indexes (topLevel + byParent). Walks top-down: each node is
4
+ // access-checked; a hidden parent drops its entire subtree implicitly
5
+ // because we never recurse into it. Siblings sort by `order` (ascending,
6
+ // default 0), tie-broken by qualified name so renders stay deterministic
7
+ // across registry iteration orders.
8
+ //
9
+ // Pure — same inputs produce the same tree. The renderer memoizes the
10
+ // result and only recomputes when the registry changes (rare — boot
11
+ // only) or the user's roles change (logout/login, tenant-switch).
12
+ export function resolveNavigation(options) {
13
+ const { source, user } = options;
14
+ function build(entry) {
15
+ // Shared default-visible UI predicate, not hasAccess from the engine:
16
+ // that module pulls server-side deps and would break bundle purity.
17
+ if (!isUiAccessGranted(entry.access, user?.roles))
18
+ return null;
19
+ // `entry.id` is already the qualified name — the registry stores
20
+ // it that way. No reverse-index lookup needed.
21
+ const children = [];
22
+ for (const child of source.byParent(entry.id)) {
23
+ const node = build(child);
24
+ if (node !== null)
25
+ children.push(node);
26
+ }
27
+ children.sort(bySortKey);
28
+ return {
29
+ qualifiedName: entry.id,
30
+ label: entry.label,
31
+ order: entry.order ?? 0,
32
+ children,
33
+ ...(entry.icon !== undefined && { icon: entry.icon }),
34
+ ...(entry.screen !== undefined && { screen: entry.screen }),
35
+ ...(entry.target !== undefined && { target: entry.target }),
36
+ ...(entry.actions !== undefined && { actions: entry.actions }),
37
+ ...(entry.createAction !== undefined && { createAction: entry.createAction }),
38
+ ...(entry.provider !== undefined && { provider: entry.provider }),
39
+ };
40
+ }
41
+ const roots = [];
42
+ for (const entry of source.topLevel) {
43
+ const node = build(entry);
44
+ if (node !== null)
45
+ roots.push(node);
46
+ }
47
+ roots.sort(bySortKey);
48
+ return roots;
49
+ }
50
+ function bySortKey(a, b) {
51
+ // Primary key: `order` ascending. Secondary: qualifiedName alphabetic,
52
+ // which is a stable fallback — the registry-iteration order isn't
53
+ // guaranteed across boots, so tied-order entries would otherwise
54
+ // shuffle between renders.
55
+ if (a.order !== b.order)
56
+ return a.order - b.order;
57
+ return a.qualifiedName.localeCompare(b.qualifiedName);
58
+ }
@@ -0,0 +1,26 @@
1
+ import type { NavDefinition, TargetRef, TreeAction } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ export type NavNode = {
3
+ readonly qualifiedName: string;
4
+ readonly label: string;
5
+ readonly icon?: string;
6
+ readonly screen?: string;
7
+ readonly target?: TargetRef;
8
+ readonly actions?: readonly TreeAction[];
9
+ readonly createAction?: TreeAction;
10
+ readonly provider?: boolean;
11
+ readonly order: number;
12
+ readonly children: readonly NavNode[];
13
+ };
14
+ export type NavTree = readonly NavNode[];
15
+ export type NavRegistrySlice = {
16
+ readonly topLevel: readonly NavDefinition[];
17
+ readonly byParent: (parentQualifiedName: string) => readonly NavDefinition[];
18
+ };
19
+ export type ResolveNavigationOptions = {
20
+ readonly source: NavRegistrySlice;
21
+ readonly user?: {
22
+ readonly id: string;
23
+ readonly roles: readonly string[];
24
+ };
25
+ };
26
+ export type { NavDefinition };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { WritableStore } from "./types.js";
2
+ export declare function createStore<T>(initial: T): WritableStore<T>;
@@ -1,5 +1,3 @@
1
- import type { WritableStore } from "./types";
2
-
3
1
  // Subscribe/Emit primitive matching React's useSyncExternalStore.
4
2
  // Single canonical implementation — every stateful controller in
5
3
  // ui-core/renderer land composes on top of this instead of rolling
@@ -21,25 +19,25 @@ import type { WritableStore } from "./types";
21
19
  // store.setState(() => myNewFn);
22
20
  // In practice, function-valued stores are rare — feature controllers
23
21
  // hold values, not callbacks.
24
-
25
- export function createStore<T>(initial: T): WritableStore<T> {
26
- let snapshot = initial;
27
- const listeners = new Set<() => void>();
28
-
29
- return {
30
- getSnapshot: () => snapshot,
31
- subscribe: (listener) => {
32
- listeners.add(listener);
33
- return () => {
34
- listeners.delete(listener);
35
- };
36
- },
37
- setState: (next) => {
38
- const nextValue = typeof next === "function" ? (next as (prev: T) => T)(snapshot) : next;
39
- // skip: next value identical to snapshot, avoid notifying listeners
40
- if (Object.is(nextValue, snapshot)) return;
41
- snapshot = nextValue;
42
- for (const listener of listeners) listener();
43
- },
44
- };
22
+ export function createStore(initial) {
23
+ let snapshot = initial;
24
+ const listeners = new Set();
25
+ return {
26
+ getSnapshot: () => snapshot,
27
+ subscribe: (listener) => {
28
+ listeners.add(listener);
29
+ return () => {
30
+ listeners.delete(listener);
31
+ };
32
+ },
33
+ setState: (next) => {
34
+ const nextValue = typeof next === "function" ? next(snapshot) : next;
35
+ // skip: next value identical to snapshot, avoid notifying listeners
36
+ if (Object.is(nextValue, snapshot))
37
+ return;
38
+ snapshot = nextValue;
39
+ for (const listener of listeners)
40
+ listener();
41
+ },
42
+ };
45
43
  }
@@ -0,0 +1 @@
1
+ export declare function shallowEqual(a: unknown, b: unknown): boolean;
@@ -7,28 +7,30 @@
7
7
  // Mirrors Zustand's `shallow` and react-redux's `shallowEqual` — same
8
8
  //8-line shape, no recursion. Apps that need deep equality reach for
9
9
  // their own helper.
10
-
11
10
  // Signature is `(a: unknown, b: unknown)` not `<T>(a: T, b: T)` because the
12
11
  // function legitimately compares cross-type values (primitive vs. object,
13
12
  // null vs. {}, etc.) and returns false for mismatches. A generic `<T>`
14
13
  // would force callers into type-gymnastics for those cases. Contra-variance
15
14
  // ensures this still satisfies `(a: S, b: S) => boolean` slots like
16
15
  // useStoreSelector's `equals` arg.
17
- export function shallowEqual(a: unknown, b: unknown): boolean {
18
- if (Object.is(a, b)) return true;
19
- if (typeof a !== "object" || a === null) return false;
20
- if (typeof b !== "object" || b === null) return false;
21
-
22
- const keysA = Object.keys(a);
23
- const keysB = Object.keys(b);
24
- if (keysA.length !== keysB.length) return false;
25
-
26
- for (const key of keysA) {
27
- if (!Object.hasOwn(b, key)) return false;
28
- if (!Object.is((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])) {
29
- // @cast-boundary generic-record
30
- return false;
16
+ export function shallowEqual(a, b) {
17
+ if (Object.is(a, b))
18
+ return true;
19
+ if (typeof a !== "object" || a === null)
20
+ return false;
21
+ if (typeof b !== "object" || b === null)
22
+ return false;
23
+ const keysA = Object.keys(a);
24
+ const keysB = Object.keys(b);
25
+ if (keysA.length !== keysB.length)
26
+ return false;
27
+ for (const key of keysA) {
28
+ if (!Object.hasOwn(b, key))
29
+ return false;
30
+ if (!Object.is(a[key], b[key])) {
31
+ // @cast-boundary generic-record
32
+ return false;
33
+ }
31
34
  }
32
- }
33
- return true;
35
+ return true;
34
36
  }
@@ -0,0 +1,3 @@
1
+ export { createStore } from "./create-store.js";
2
+ export { shallowEqual } from "./equality.js";
3
+ export type { Store, WritableStore } from "./types.js";
@@ -0,0 +1,2 @@
1
+ export { createStore } from "./create-store.js";
2
+ export { shallowEqual } from "./equality.js";
@@ -0,0 +1,7 @@
1
+ export type Store<T> = {
2
+ getSnapshot(): T;
3
+ subscribe(listener: () => void): () => void;
4
+ };
5
+ export type WritableStore<T> = Store<T> & {
6
+ setState(next: T | ((prev: T) => T)): void;
7
+ };
@@ -0,0 +1,10 @@
1
+ // Store contract — Pull-Style Subscribe/Emit matching React's
2
+ // useSyncExternalStore signature. The single canonical shape for
3
+ // every stateful controller in ui-core/renderer land (form,
4
+ // dispatcher-status, locale, token, nav, lifecycle).
5
+ //
6
+ // Rationale in docs/plans/ui-store.md. Not a new state-management
7
+ // decision — ui-decisions.md already picked Subscribe/Emit; this
8
+ // module just materializes the pattern instead of rolling a new
9
+ // Set<() => void> per controller.
10
+ export {};
@@ -0,0 +1,10 @@
1
+ import type { EntityDefinition, EntityEditScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import type { EditViewModel, Translate } from "./types.js";
3
+ export type ComputeEditViewModelInput<TValues extends Readonly<Record<string, unknown>> = Readonly<Record<string, unknown>>> = {
4
+ readonly screen: EntityEditScreenDefinition;
5
+ readonly entity: EntityDefinition;
6
+ readonly values: TValues;
7
+ readonly translate: Translate;
8
+ readonly featureName: string;
9
+ };
10
+ export declare function computeEditViewModel<TValues extends Readonly<Record<string, unknown>> = Readonly<Record<string, unknown>>>(input: ComputeEditViewModelInput<TValues>): EditViewModel;