@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.
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
@@ -1,401 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { APEX_NAV_MENU_CSS, APEX_NAV_TOGGLE_RESPONSIVE_CSS, APEX_STRUCTURAL_CSS } from "../css";
3
- import { type ApexPage, renderApexPage } from "../index";
4
-
5
- const brand = { tokensCss: ":root{--primary:#123;--primary-fg:#fff;--bg:#fff;--fg:#000;}" };
6
-
7
- function page(overrides: Partial<ApexPage> = {}): ApexPage {
8
- return {
9
- brand,
10
- head: { lang: "de", title: "T", description: "D" },
11
- header: { brand: { href: "/", label: "Acme" } },
12
- sections: [],
13
- footer: { brand: { label: "Acme" } },
14
- ...overrides,
15
- };
16
- }
17
-
18
- describe("renderApexPage", () => {
19
- test("theme dark sets body class, light (default) does not", () => {
20
- expect(renderApexPage(page({ theme: "dark" }))).toContain('<body class="apex-dark">');
21
- // light is default — both CSS sets ship, only the body class toggles
22
- expect(renderApexPage(page())).toContain("<body>");
23
- expect(renderApexPage(page())).not.toContain('<body class="apex-dark">');
24
- });
25
-
26
- test("escapes user-provided text in title and content", () => {
27
- const html = renderApexPage(
28
- page({
29
- head: { lang: "de", title: "<script>x</script>", description: "a & b" },
30
- sections: [{ kind: "info-grid", items: [{ title: "<b>t</b>", desc: "d" }] }],
31
- }),
32
- );
33
- expect(html).not.toContain("<script>x</script>");
34
- expect(html).toContain("&lt;script&gt;");
35
- expect(html).toContain("a &amp; b");
36
- expect(html).toContain("&lt;b&gt;t&lt;/b&gt;");
37
- });
38
-
39
- test("metaHtml and feature icon are passed through unescaped (app-authored)", () => {
40
- const html = renderApexPage(
41
- page({
42
- sections: [
43
- { kind: "hero", title: "h", tagline: "t", metaHtml: "<strong>ok</strong>" },
44
- {
45
- kind: "feature-grid",
46
- heading: "F",
47
- items: [{ icon: "<rect/>", title: "x", desc: "d" }],
48
- },
49
- ],
50
- }),
51
- );
52
- expect(html).toContain("<strong>ok</strong>");
53
- expect(html).toContain("<svg"); // icon wrapped in standard svg
54
- expect(html).toContain("<rect/>");
55
- });
56
-
57
- test("nav menu entry renders a dropdown; plain links stay anchors", () => {
58
- const html = renderApexPage(
59
- page({
60
- header: {
61
- brand: { href: "/", label: "Acme" },
62
- navLinks: [
63
- {
64
- kind: "menu",
65
- label: "Product",
66
- items: [
67
- { icon: "<rect/>", title: "Planner", desc: "Plan it", href: "/p" },
68
- { title: "<b>Reports</b>", href: "/r" },
69
- ],
70
- footer: { label: "See all", href: "/all" },
71
- },
72
- { label: "Pricing", href: "#pricing" },
73
- ],
74
- },
75
- }),
76
- );
77
- // Menu chrome + trigger label.
78
- expect(html).toContain('class="nav-menu"');
79
- expect(html).toContain('class="nav-menu__trigger"');
80
- expect(html).toContain("Product");
81
- // Items: title, description, wrapped icon; missing desc/icon simply omitted.
82
- expect(html).toContain('class="nav-menu__item" href="/p"');
83
- expect(html).toContain("Planner");
84
- expect(html).toContain("Plan it");
85
- expect(html).toContain("<rect/>");
86
- // Footer link under the divider.
87
- expect(html).toContain('class="nav-menu__more" href="/all"');
88
- expect(html).toContain("See all");
89
- // Item titles are escaped (app-data, not trusted HTML).
90
- expect(html).toContain("&lt;b&gt;Reports&lt;/b&gt;");
91
- expect(html).not.toContain("<b>Reports</b>");
92
- // The sibling plain link still renders as a normal anchor.
93
- expect(html).toContain('<a href="#pricing">Pricing</a>');
94
- });
95
-
96
- test("nav-links wrap in a mobile hamburger toggle; no navLinks omits it", () => {
97
- const withNav = renderApexPage(
98
- page({
99
- header: {
100
- brand: { href: "/", label: "Acme" },
101
- navLinks: [{ label: "Pricing", href: "#pricing" }],
102
- },
103
- }),
104
- );
105
- expect(withNav).toContain('<details class="nav-toggle">');
106
- expect(withNav).toContain('<summary class="nav-toggle__trigger" aria-label="Menu">');
107
- expect(withNav).toContain('<nav class="nav-links">');
108
-
109
- const customLabel = renderApexPage(
110
- page({
111
- header: {
112
- brand: { href: "/", label: "Acme" },
113
- navLinks: [{ label: "Pricing", href: "#pricing" }],
114
- menuLabel: "Menü",
115
- },
116
- }),
117
- );
118
- expect(customLabel).toContain('aria-label="Menü"');
119
-
120
- const withoutNav = renderApexPage(page());
121
- expect(withoutNav).not.toContain('<details class="nav-toggle">');
122
- });
123
-
124
- test("mobile-open .nav-toggle rule wins the cascade over the desktop-hidden default", () => {
125
- // APEX_NAV_MENU_CSS sets `.nav-toggle { display: none; }` unconditionally;
126
- // APEX_NAV_TOGGLE_RESPONSIVE_CSS re-declares it `.nav .nav-toggle { display:
127
- // inline-flex; }` inside the 640px query. A markup-only assertion can't catch
128
- // a reordering that silently restores fw#2047 (nav-links hidden on mobile
129
- // with no way to reopen them).
130
- const hiddenDefault = APEX_STRUCTURAL_CSS.indexOf(".nav-toggle { display: none; }");
131
- const mobileOpen = APEX_STRUCTURAL_CSS.indexOf(".nav .nav-toggle { display: inline-flex; }");
132
- expect(hiddenDefault).toBeGreaterThan(-1);
133
- expect(mobileOpen).toBeGreaterThan(-1);
134
- expect(mobileOpen).toBeGreaterThan(hiddenDefault);
135
- });
136
-
137
- test("APEX_NAV_TOGGLE_RESPONSIVE_CSS's mobile-open rules outrank APEX_NAV_MENU_CSS's hidden default regardless of concatenation order (fw#2125)", () => {
138
- // money-horse/show-pony/the docs sample call renderApexHeader() standalone and
139
- // concatenate APEX_NAV_MENU_CSS with their own page CSS in whatever order they
140
- // choose — they don't control ordering relative to APEX_STRUCTURAL_CSS's
141
- // internal layout. The "show" rules are therefore specificity-hardened
142
- // (`.nav .nav-toggle`, 2 classes) rather than relying on source order to beat
143
- // APEX_NAV_MENU_CSS's bare `.nav-toggle` default (1 class) — verify both carry
144
- // that extra `.nav ` prefix so the fix holds even when a consumer emits
145
- // APEX_NAV_TOGGLE_RESPONSIVE_CSS BEFORE APEX_NAV_MENU_CSS.
146
- expect(APEX_NAV_MENU_CSS).toContain(".nav-toggle { display: none; }");
147
- expect(APEX_NAV_MENU_CSS).toContain(".nav-toggle__trigger { display: none;");
148
- expect(APEX_NAV_MENU_CSS).not.toContain(".nav-toggle { display: inline-flex");
149
- expect(APEX_NAV_MENU_CSS).not.toContain(".nav-toggle__trigger { display: inline-flex");
150
-
151
- expect(APEX_NAV_TOGGLE_RESPONSIVE_CSS).toContain(".nav .nav-toggle { display: inline-flex; }");
152
- expect(APEX_NAV_TOGGLE_RESPONSIVE_CSS).toContain(
153
- ".nav .nav-toggle__trigger { display: inline-flex; }",
154
- );
155
- });
156
-
157
- test("APEX_NAV_TOGGLE_RESPONSIVE_CSS declares .nav as the positioned ancestor for the open dropdown", () => {
158
- // `.nav-toggle[open] > .nav-links` is `position: absolute; top: calc(100% +
159
- // 0.5rem); left: 0; right: 0` — it needs `.nav { position: relative; }` as
160
- // its containing block. renderApexPage gets that from CHROME_LIGHT, but a
161
- // standalone renderApexHeader() consumer's own header CSS typically doesn't
162
- // declare it, and without it the dropdown opens off-screen against the
163
- // viewport instead of under the header (fw#2125 follow-up: verified none of
164
- // money-horse/show-pony/the docs sample's HEADER_CSS set it).
165
- expect(APEX_NAV_TOGGLE_RESPONSIVE_CSS).toContain(".nav { position: relative; }");
166
- });
167
-
168
- test("pricing: featured card gets badge + featured class, cap line precedes benefits", () => {
169
- const html = renderApexPage(
170
- page({
171
- sections: [
172
- {
173
- kind: "pricing-grid",
174
- heading: "P",
175
- tiers: [
176
- {
177
- name: "Pro",
178
- amount: "9 €",
179
- priceSuffix: "/Monat",
180
- featured: true,
181
- badge: "Beliebt",
182
- capLine: "50 X",
183
- benefits: ["b1"],
184
- cta: { label: "Go", href: "/s" },
185
- },
186
- ],
187
- },
188
- ],
189
- }),
190
- );
191
- expect(html).toContain("price-card--featured");
192
- expect(html).toContain('class="price-badge">Beliebt');
193
- expect(html).toContain("/Monat");
194
- // cap line must render before the first benefit
195
- expect(html.indexOf("price-cap")).toBeLessThan(html.indexOf("b1"));
196
- // featured tier without explicit cta variant → primary button
197
- expect(html).toContain('class="btn btn-primary" href="/s"');
198
-
199
- expect(html).not.toContain("btn-link");
200
- });
201
-
202
- test("cta variant link renders a plain anchor, default renders a button", () => {
203
- const html = renderApexPage(
204
- page({
205
- header: {
206
- brand: { href: "/", label: "Acme" },
207
- actions: [
208
- { label: "Login", href: "/login", variant: "link" },
209
- { label: "Start", href: "/signup" },
210
- ],
211
- },
212
- }),
213
- );
214
- expect(html).toContain('<a href="/login">Login</a>'); // link → no class
215
- expect(html).toContain('<a class="btn btn-primary" href="/signup">Start</a>');
216
- });
217
-
218
- test("pricing-tier cta variant link renders a plain anchor via renderCta, not a btn-link class", () => {
219
- const html = renderApexPage(
220
- page({
221
- sections: [
222
- {
223
- kind: "pricing-grid",
224
- heading: "Preise",
225
- tiers: [
226
- {
227
- name: "Free",
228
- amount: "0 €",
229
- benefits: ["b1"],
230
- cta: { label: "Los", href: "/free", variant: "link" },
231
- },
232
- ],
233
- },
234
- ],
235
- }),
236
- );
237
- expect(html).toContain('<a href="/free">Los</a>');
238
- expect(html).not.toContain("btn-link");
239
- });
240
-
241
- test("footer --footer-cols reflects column count and survives without columns", () => {
242
- const twoCols = renderApexPage(
243
- page({
244
- footer: {
245
- brand: { label: "Acme" },
246
- columns: [
247
- { heading: "A", links: [{ href: "/a", label: "a" }] },
248
- { heading: "B", links: [{ href: "/b", label: "b" }] },
249
- ],
250
- },
251
- }),
252
- );
253
- expect(twoCols).toContain("--footer-cols:2");
254
- // 502/2: --footer-cols:0 makes grid-template-columns's repeat(0, 1fr)
255
- // invalid CSS — the browser drops the whole declaration. Clamped to a
256
- // minimum of 1 so a footer with no columns still gets a valid grid.
257
- expect(renderApexPage(page())).toContain("--footer-cols:1");
258
- expect(renderApexPage(page())).not.toContain("--footer-cols:0");
259
- });
260
-
261
- test("renders every section kind without throwing or leaking undefined", () => {
262
- const html = renderApexPage(
263
- page({
264
- sections: [
265
- { kind: "hero", title: "h", tagline: "t" },
266
- { kind: "feature-grid", heading: "F", items: [{ title: "x", desc: "d" }] },
267
- {
268
- kind: "pricing-grid",
269
- heading: "P",
270
- tiers: [
271
- { name: "Free", amount: "0 €", benefits: ["b"], cta: { label: "g", href: "/s" } },
272
- ],
273
- },
274
- { kind: "info-grid", items: [{ title: "t", desc: "d" }] },
275
- { kind: "final-cta", heading: "c", cta: { label: "g", href: "/s" } },
276
- { kind: "html", html: "<div id='raw'></div>" },
277
- ],
278
- }),
279
- );
280
- expect(html).not.toContain("undefined");
281
- expect(html).not.toContain("[object Object]");
282
- expect(html).toContain("id='raw'");
283
- expect(html).toContain("trust-item");
284
- });
285
-
286
- test("renders robots meta when set, absent when omitted", () => {
287
- const without = renderApexPage(page());
288
- expect(without).not.toContain("robots");
289
- const withRobots = renderApexPage(
290
- page({ head: { lang: "de", title: "T", description: "D", robots: "noindex, nofollow" } }),
291
- );
292
- expect(withRobots).toContain('<meta name="robots" content="noindex, nofollow" />');
293
- });
294
- test("omits both description meta tags when description is empty", () => {
295
- const html = renderApexPage(page({ head: { lang: "de", title: "T", description: "" } }));
296
- expect(html).not.toContain('<meta name="description"');
297
- expect(html).not.toContain('<meta property="og:description"');
298
- });
299
-
300
- test("renders og:site_name and og:locale", () => {
301
- const html = renderApexPage(
302
- page({
303
- head: {
304
- lang: "de",
305
- title: "T",
306
- description: "D",
307
- siteName: "Acme",
308
- locale: "de_DE",
309
- },
310
- }),
311
- );
312
- expect(html).toContain('<meta property="og:site_name" content="Acme" />');
313
- expect(html).toContain('<meta property="og:locale" content="de_DE" />');
314
- });
315
-
316
- test("renders twitter:card when ogImage is set, plus twitter:site", () => {
317
- const html = renderApexPage(
318
- page({
319
- head: {
320
- lang: "de",
321
- title: "T",
322
- description: "D",
323
- ogImage: "https://example.com/image.png",
324
- twitterSite: "@acme",
325
- },
326
- }),
327
- );
328
- expect(html).toContain('<meta name="twitter:card" content="summary_large_image" />');
329
- expect(html).toContain('<meta name="twitter:site" content="@acme" />');
330
- });
331
-
332
- test("does not render twitter:card without ogImage", () => {
333
- const html = renderApexPage(
334
- page({
335
- head: { lang: "de", title: "T", description: "D", twitterSite: "@acme" },
336
- }),
337
- );
338
- expect(html).not.toContain("twitter:card");
339
- expect(html).toContain("twitter:site");
340
- });
341
-
342
- test("renders preconnect links", () => {
343
- const html = renderApexPage(
344
- page({
345
- head: {
346
- lang: "de",
347
- title: "T",
348
- description: "D",
349
- preconnects: ["https://fonts.example.com", "https://api.example.com"],
350
- },
351
- }),
352
- );
353
- expect(html).toContain('<link rel="preconnect" href="https://fonts.example.com" />');
354
- expect(html).toContain('<link rel="preconnect" href="https://api.example.com" />');
355
- });
356
-
357
- test("hero screenshot includes apex lightbox chrome", () => {
358
- const html = renderApexPage(
359
- page({
360
- sections: [
361
- {
362
- kind: "hero",
363
- title: "h",
364
- tagline: "t",
365
- screenshot: { src: "/shots/demo.png", alt: "Dashboard" },
366
- },
367
- ],
368
- }),
369
- );
370
- expect(html).toContain('class="shot-frame"');
371
- expect(html).toContain('<dialog id="apex-lightbox"');
372
- expect(html).toContain("apex-lightbox");
373
- expect(html).toContain(".shot-frame img");
374
- });
375
-
376
- test("renders schemaJson as json-ld script tag", () => {
377
- const html = renderApexPage(
378
- page({
379
- head: {
380
- lang: "de",
381
- title: "T",
382
- description: "D",
383
- schemaJson: { "@context": "https://schema.org", "@type": "WebSite", name: "Acme" },
384
- },
385
- }),
386
- );
387
- expect(html).toContain('<script type="application/ld+json">');
388
- expect(html).toContain('{"@context":"https://schema.org","@type":"WebSite","name":"Acme"}');
389
- expect(html).toContain("</script>");
390
- });
391
-
392
- test("header .nav rule doesn't reset the .container's horizontal padding (#2038)", () => {
393
- const html = renderApexPage(page());
394
- const navRule = html.match(/\.nav\s*\{[^}]*\}/)?.[0];
395
- expect(navRule).toBeDefined();
396
- // A `padding`/`padding-inline`/`padding-left`/`padding-right` shorthand or
397
- // longhand here would win the cascade over .container's horizontal padding
398
- // (same specificity, .nav declared later) and zero it out on <div class="container nav">.
399
- expect(navRule).not.toMatch(/padding(-inline|-left|-right)?\s*:/);
400
- });
401
- });