@gradial/aci 0.1.28 → 0.1.30

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 (73) hide show
  1. package/README.md +47 -0
  2. package/bin/aci-darwin-arm64 +0 -0
  3. package/dist/astro/index.d.ts +1 -0
  4. package/dist/astro/index.js +7 -0
  5. package/dist/collect/api.d.ts +24 -0
  6. package/dist/collect/api.js +63 -0
  7. package/dist/collect/batcher.d.ts +65 -0
  8. package/dist/collect/batcher.js +127 -0
  9. package/dist/collect/config.d.ts +30 -0
  10. package/dist/collect/config.js +53 -0
  11. package/dist/collect/consent/adapters/c15t.d.ts +2 -0
  12. package/dist/collect/consent/adapters/c15t.js +43 -0
  13. package/dist/collect/consent/adapters/cookiebot.d.ts +2 -0
  14. package/dist/collect/consent/adapters/cookiebot.js +48 -0
  15. package/dist/collect/consent/adapters/onetrust.d.ts +2 -0
  16. package/dist/collect/consent/adapters/onetrust.js +54 -0
  17. package/dist/collect/consent/adapters/trustarc.d.ts +2 -0
  18. package/dist/collect/consent/adapters/trustarc.js +65 -0
  19. package/dist/collect/consent/envelope.d.ts +49 -0
  20. package/dist/collect/consent/envelope.js +100 -0
  21. package/dist/collect/consent/mapping.d.ts +62 -0
  22. package/dist/collect/consent/mapping.js +129 -0
  23. package/dist/collect/declarative.d.ts +35 -0
  24. package/dist/collect/declarative.js +100 -0
  25. package/dist/collect/gapbuffer.d.ts +55 -0
  26. package/dist/collect/gapbuffer.js +134 -0
  27. package/dist/collect/index.d.ts +6 -0
  28. package/dist/collect/index.js +154 -0
  29. package/dist/collect/linkclicks.d.ts +12 -0
  30. package/dist/collect/linkclicks.js +35 -0
  31. package/dist/collect/pageviews.d.ts +22 -0
  32. package/dist/collect/pageviews.js +42 -0
  33. package/dist/collect/transport.d.ts +22 -0
  34. package/dist/collect/transport.js +52 -0
  35. package/dist/collect/ulid.d.ts +6 -0
  36. package/dist/collect/ulid.js +36 -0
  37. package/dist/collect/variationviews.d.ts +31 -0
  38. package/dist/collect/variationviews.js +80 -0
  39. package/dist/collect/webvitals.d.ts +38 -0
  40. package/dist/collect/webvitals.js +99 -0
  41. package/dist/content/index.d.ts +1 -0
  42. package/dist/content/index.js +1 -0
  43. package/dist/content/personalization.d.ts +57 -0
  44. package/dist/content/personalization.js +54 -0
  45. package/dist/content/types.d.ts +3 -0
  46. package/dist/next/collect-script.d.ts +12 -0
  47. package/dist/next/collect-script.js +15 -0
  48. package/dist/next/config.js +1 -0
  49. package/dist/next/index.d.ts +1 -0
  50. package/dist/next/index.js +1 -0
  51. package/dist/next/page.d.ts +2 -0
  52. package/dist/next/page.js +5 -2
  53. package/dist/providers/file.js +1 -8
  54. package/dist/react/VideoPlayer.d.ts +3 -3
  55. package/dist/react/index.d.ts +4 -4
  56. package/dist/react/index.js +1 -1
  57. package/dist/types/image.d.ts +27 -13
  58. package/dist/types/image.js +17 -17
  59. package/dist/types/media.d.ts +10 -4
  60. package/dist/types/video.d.ts +5 -2
  61. package/package.json +8 -9
  62. package/src/cli/validate-content.mjs +0 -0
  63. package/src/types/image.ts +32 -29
  64. package/templates/astro/template/src/fixtures/sections.ts +1 -1
  65. package/templates/astro/template/src/layouts/SiteLayout.astro +77 -1
  66. package/templates/astro/template/src/pages/[...slug].astro +3 -2
  67. package/templates/common/.content/config/site.json +5 -0
  68. package/templates/common/.content/schemas/site.schema.json +15 -0
  69. package/templates/common/README.md.tmpl +15 -0
  70. package/templates/nextjs/template/package.json.tmpl +2 -0
  71. package/templates/nextjs/template/src/app/_components/ConsentManager.tsx +132 -0
  72. package/templates/nextjs/template/src/app/layout.tsx +3 -1
  73. package/templates/nextjs/template/src/components/sections/HomeHero.tsx +1 -1
@@ -0,0 +1,99 @@
1
+ // Web vitals (design/04): eventName "rum.web_vital" with { vitalName,
2
+ // vitalValue }. Captures LCP, FID, CLS, INP, TTFB and FCP via
3
+ // PerformanceObserver. LCP/FID/FCP fire as their entries arrive; TTFB is read
4
+ // once from the navigation entry on init; CLS and INP accumulate and are
5
+ // reported on visibilitychange to hidden (session-window semantics). Each
6
+ // vital is emitted at most once per page load — there is no SPA reset in this
7
+ // first version. If PerformanceObserver is missing the collector is a no-op.
8
+ // Errors are swallowed silently.
9
+ export const WEB_VITAL_EVENT = "rum.web_vital";
10
+ /**
11
+ * startWebVitals observes Core Web Vitals and emits one rum.web_vital per
12
+ * vital. Returns silently when PerformanceObserver is unavailable.
13
+ */
14
+ export function startWebVitals(emit, env) {
15
+ const Observer = env.PerformanceObserver;
16
+ if (typeof Observer !== "function")
17
+ return;
18
+ const { performance: perf, debug } = env;
19
+ const url = env.window?.location?.href;
20
+ const doc = env.document;
21
+ const emitted = new Set();
22
+ const emitVital = (name, value) => {
23
+ if (emitted.has(name))
24
+ return;
25
+ emitted.add(name);
26
+ emit({
27
+ eventName: WEB_VITAL_EVENT,
28
+ url,
29
+ properties: { vitalName: name, vitalValue: value },
30
+ });
31
+ };
32
+ try {
33
+ const nav = perf?.getEntriesByType("navigation")[0];
34
+ if (nav && typeof nav.responseStart === "number" && typeof nav.requestStart === "number") {
35
+ emitVital("TTFB", nav.responseStart - nav.requestStart);
36
+ }
37
+ }
38
+ catch (err) {
39
+ debug?.("capture", { reason: "ttfb_failed", error: String(err) });
40
+ }
41
+ let clsValue = 0;
42
+ let inpWorst = 0;
43
+ const observe = (type, handler) => {
44
+ try {
45
+ const obs = new Observer((list) => {
46
+ try {
47
+ for (const entry of list.getEntries())
48
+ handler(entry);
49
+ }
50
+ catch (err) {
51
+ debug?.("capture", { reason: `${type}_handler_failed`, error: String(err) });
52
+ }
53
+ });
54
+ obs.observe({ type, buffered: true });
55
+ }
56
+ catch (err) {
57
+ debug?.("capture", { reason: `${type}_observe_failed`, error: String(err) });
58
+ }
59
+ };
60
+ observe("largest-contentful-paint", (entry) => {
61
+ emitVital("LCP", entry.startTime);
62
+ });
63
+ observe("first-input", (entry) => {
64
+ emitVital("FID", entry.startTime);
65
+ });
66
+ observe("layout-shift", (entry) => {
67
+ if (entry.hadRecentInput)
68
+ return;
69
+ clsValue += entry.value ?? 0;
70
+ });
71
+ observe("event", (entry) => {
72
+ const value = entry.value ?? 0;
73
+ if (value > inpWorst)
74
+ inpWorst = value;
75
+ });
76
+ observe("paint", (entry) => {
77
+ if (entry.name === "first-contentful-paint") {
78
+ emitVital("FCP", entry.startTime);
79
+ }
80
+ });
81
+ const onHidden = () => {
82
+ try {
83
+ if (!emitted.has("CLS"))
84
+ emitVital("CLS", clsValue);
85
+ if (!emitted.has("INP"))
86
+ emitVital("INP", inpWorst);
87
+ }
88
+ catch (err) {
89
+ debug?.("capture", { reason: "visibility_handler_failed", error: String(err) });
90
+ }
91
+ };
92
+ if (doc) {
93
+ doc.addEventListener("visibilitychange", () => {
94
+ if (doc.visibilityState === "hidden")
95
+ onHidden();
96
+ });
97
+ }
98
+ env.window?.addEventListener("pagehide", onHidden);
99
+ }
@@ -1,6 +1,7 @@
1
1
  export * from './provider.js';
2
2
  export * from './routes.js';
3
3
  export * from './types.js';
4
+ export * from './personalization.js';
4
5
  export * from './cache.js';
5
6
  export * from './resolve-slots.js';
6
7
  export * from './assets.js';
@@ -1,6 +1,7 @@
1
1
  export * from './provider.js';
2
2
  export * from './routes.js';
3
3
  export * from './types.js';
4
+ export * from './personalization.js';
4
5
  export * from './cache.js';
5
6
  export * from './resolve-slots.js';
6
7
  export * from './assets.js';
@@ -0,0 +1,57 @@
1
+ /** The single stable serving URL for the collect SDK (design/04). */
2
+ export declare const COLLECT_SCRIPT_SRC = "/_aci/aci.js";
3
+ /**
4
+ * The optional `personalization` block on the site config doc
5
+ * (`/config/site.json`, design/04 site config contract). Absent block or
6
+ * `collect` unset/false = personalization disabled: no tag is emitted.
7
+ */
8
+ export interface SitePersonalizationConfig {
9
+ /** Master switch for the collect tag. Only `true` enables emission. */
10
+ collect?: boolean;
11
+ /**
12
+ * CMP preset for `data-aci-consent` (`c15t`, `onetrust`, `cookiebot`,
13
+ * `trustarc`, `manual`). Defaults to `manual` — only the JS consent API
14
+ * writes envelopes. Unknown values are passed through; the SDK falls back
15
+ * to `manual` at runtime (unknown-attribute tolerance, design/04).
16
+ */
17
+ consentPreset?: string;
18
+ /**
19
+ * Flat comma list of CMP categories that imply the personalization
20
+ * purpose. Omitted from the tag when unset — each preset ships a sane
21
+ * default inside the SDK.
22
+ */
23
+ consentCategories?: string;
24
+ /** Emit `data-aci-debug="1"` (structured console.debug diagnostics). */
25
+ debug?: boolean;
26
+ }
27
+ /**
28
+ * The minimal site-config surface tag emission reads: the site doc id (for
29
+ * the optional debug `data-aci-site` attribute) plus the personalization
30
+ * block. `KernelSiteConfig` satisfies this structurally.
31
+ */
32
+ export interface PersonalizationSiteConfig {
33
+ id?: string;
34
+ personalization?: SitePersonalizationConfig;
35
+ }
36
+ /**
37
+ * Attributes of the emitted tag, in contract order. `defer` is a boolean
38
+ * attribute and is owned by the renderers (React prop / HTML literal).
39
+ */
40
+ export interface CollectTagAttributes {
41
+ src: string;
42
+ "data-aci-consent": string;
43
+ "data-aci-consent-categories"?: string;
44
+ "data-aci-site"?: string;
45
+ "data-aci-debug"?: string;
46
+ }
47
+ /**
48
+ * collectTagAttributes derives the tag attributes from the site config, or
49
+ * returns null when personalization is disabled or absent (no tag).
50
+ */
51
+ export declare function collectTagAttributes(siteConfig: PersonalizationSiteConfig | null | undefined): CollectTagAttributes | null;
52
+ /**
53
+ * collectScriptTagHTML renders the contract tag as an HTML snippet for
54
+ * emitters without a React element seam (the Astro kit layout). Returns ""
55
+ * when personalization is disabled.
56
+ */
57
+ export declare function collectScriptTagHTML(siteConfig: PersonalizationSiteConfig | null | undefined): string;
@@ -0,0 +1,54 @@
1
+ // Collect-tag emission (personalization-onboarding design/04): the script
2
+ // tag is the customer-visible contract and the ONLY per-site configuration
3
+ // surface (C-13). One stable URL — no hash plumbing, no versioned URL
4
+ // (D-ONB-5). The framework emitters (Next createPage, the Astro kit layout)
5
+ // both derive the tag from the site config's `personalization` block through
6
+ // this module, so there is exactly one attribute-derivation code path.
7
+ /** The single stable serving URL for the collect SDK (design/04). */
8
+ export const COLLECT_SCRIPT_SRC = "/_aci/aci.js";
9
+ const DEFAULT_CONSENT_PRESET = "manual";
10
+ /**
11
+ * collectTagAttributes derives the tag attributes from the site config, or
12
+ * returns null when personalization is disabled or absent (no tag).
13
+ */
14
+ export function collectTagAttributes(siteConfig) {
15
+ const personalization = siteConfig?.personalization;
16
+ if (personalization?.collect !== true)
17
+ return null;
18
+ const preset = personalization.consentPreset?.trim().toLowerCase() ||
19
+ DEFAULT_CONSENT_PRESET;
20
+ const attributes = {
21
+ src: COLLECT_SCRIPT_SRC,
22
+ "data-aci-consent": preset,
23
+ };
24
+ const categories = personalization.consentCategories?.trim();
25
+ if (categories)
26
+ attributes["data-aci-consent-categories"] = categories;
27
+ const site = siteConfig?.id?.trim();
28
+ if (site)
29
+ attributes["data-aci-site"] = site;
30
+ if (personalization.debug === true)
31
+ attributes["data-aci-debug"] = "1";
32
+ return attributes;
33
+ }
34
+ /**
35
+ * collectScriptTagHTML renders the contract tag as an HTML snippet for
36
+ * emitters without a React element seam (the Astro kit layout). Returns ""
37
+ * when personalization is disabled.
38
+ */
39
+ export function collectScriptTagHTML(siteConfig) {
40
+ const attributes = collectTagAttributes(siteConfig);
41
+ if (!attributes)
42
+ return "";
43
+ const rendered = Object.entries(attributes)
44
+ .map(([name, value]) => ` ${name}="${escapeAttribute(value)}"`)
45
+ .join("");
46
+ return `<script defer${rendered}></script>`;
47
+ }
48
+ function escapeAttribute(value) {
49
+ return value
50
+ .replace(/&/g, "&amp;")
51
+ .replace(/"/g, "&quot;")
52
+ .replace(/</g, "&lt;")
53
+ .replace(/>/g, "&gt;");
54
+ }
@@ -1,3 +1,4 @@
1
+ import type { SitePersonalizationConfig } from './personalization.js';
1
2
  export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'outline' | 'ghost';
2
3
  export interface KernelImage {
3
4
  src: string;
@@ -78,6 +79,8 @@ export interface KernelSiteConfig {
78
79
  description?: string;
79
80
  siteName?: string;
80
81
  };
82
+ /** Personalization collect-tag config (design/04); absent = disabled. */
83
+ personalization?: SitePersonalizationConfig;
81
84
  }
82
85
  export interface KernelRouteMetadata {
83
86
  title: string;
@@ -0,0 +1,12 @@
1
+ import { type PersonalizationSiteConfig } from '../content/personalization.js';
2
+ export interface CollectScriptProps {
3
+ siteConfig: PersonalizationSiteConfig | null | undefined;
4
+ }
5
+ /**
6
+ * CollectScript renders the personalization collect tag
7
+ * (`<script defer src="/_aci/aci.js" data-aci-consent="…">`, design/04) when
8
+ * the site config's `personalization` block enables collection, and nothing
9
+ * otherwise. createPage appends it to the page tree exactly once per page;
10
+ * sites composing their own pages can render it directly.
11
+ */
12
+ export declare function CollectScript({ siteConfig }: CollectScriptProps): import("react").JSX.Element | null;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { collectTagAttributes, } from '../content/personalization.js';
3
+ /**
4
+ * CollectScript renders the personalization collect tag
5
+ * (`<script defer src="/_aci/aci.js" data-aci-consent="…">`, design/04) when
6
+ * the site config's `personalization` block enables collection, and nothing
7
+ * otherwise. createPage appends it to the page tree exactly once per page;
8
+ * sites composing their own pages can render it directly.
9
+ */
10
+ export function CollectScript({ siteConfig }) {
11
+ const attributes = collectTagAttributes(siteConfig);
12
+ if (!attributes)
13
+ return null;
14
+ return _jsx("script", { defer: true, ...attributes });
15
+ }
@@ -13,6 +13,7 @@ export function withAci(nextConfig = {}, options = {}) {
13
13
  return {
14
14
  ...nextConfig,
15
15
  output: 'standalone',
16
+ outputFileTracingRoot: nextConfig.outputFileTracingRoot ?? process.cwd(),
16
17
  headers: async () => {
17
18
  const gradialHeaders = [
18
19
  {
@@ -4,3 +4,4 @@ export { isPreviewMode } from './preview-mode.js';
4
4
  export { createPage, type CreatePageOptions, type SlotPropsFn } from './page.js';
5
5
  export { getSiteConfig, getFragment, getLayout, getPage, getRenderInput, routeFromNextParams } from './server.js';
6
6
  export { PageNotFoundError, FragmentNotFoundError, LayoutNotFoundError } from './server.js';
7
+ export { CollectScript, type CollectScriptProps } from './collect-script.js';
@@ -4,3 +4,4 @@ export { isPreviewMode } from './preview-mode.js';
4
4
  export { createPage } from './page.js';
5
5
  export { getSiteConfig, getFragment, getLayout, getPage, getRenderInput, routeFromNextParams } from './server.js';
6
6
  export { PageNotFoundError, FragmentNotFoundError, LayoutNotFoundError } from './server.js';
7
+ export { CollectScript } from './collect-script.js';
@@ -27,6 +27,8 @@ export interface CreatePageOptions {
27
27
  *
28
28
  * The SDK owns param normalization, content loading, metadata generation,
29
29
  * notFound handling, layout resolution, and block rendering via the registry.
30
+ * When the site config enables personalization, the page tree also carries
31
+ * the collect script tag exactly once per page (design/04 contract tag).
30
32
  *
31
33
  * Release-scoped routes: when the middleware rewrites a public URL to
32
34
  * `/__r/{releaseId}/routes/{path}`, the catch-all slug becomes
package/dist/next/page.js CHANGED
@@ -1,9 +1,10 @@
1
- import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { notFound } from 'next/navigation.js';
3
3
  import { Fragment } from 'react';
4
4
  import { PageNotFoundError, routeFromNextParams, getRenderInput, getLayout, getFragment, } from './server.js';
5
5
  import { routeMetadataForContent, resolveSlots, } from '../content/index.js';
6
6
  import { renderBlocks } from './render.js';
7
+ import { CollectScript } from './collect-script.js';
7
8
  /**
8
9
  * Creates the catch-all route handler for a Next.js ACI site.
9
10
  *
@@ -21,6 +22,8 @@ import { renderBlocks } from './render.js';
21
22
  *
22
23
  * The SDK owns param normalization, content loading, metadata generation,
23
24
  * notFound handling, layout resolution, and block rendering via the registry.
25
+ * When the site config enables personalization, the page tree also carries
26
+ * the collect script tag exactly once per page (design/04 contract tag).
24
27
  *
25
28
  * Release-scoped routes: when the middleware rewrites a public URL to
26
29
  * `/__r/{releaseId}/routes/{path}`, the catch-all slug becomes
@@ -69,7 +72,7 @@ export function createPage(registry, options) {
69
72
  }
70
73
  return (_jsx(Fragment, { children: renderedBlocks }, slot.name));
71
74
  }));
72
- return _jsx(_Fragment, { children: renderedSlots.filter(Boolean) });
75
+ return (_jsxs(_Fragment, { children: [renderedSlots.filter(Boolean), _jsx(CollectScript, { siteConfig: input.siteConfig })] }));
73
76
  }
74
77
  catch (error) {
75
78
  if (error instanceof PageNotFoundError)
@@ -60,14 +60,7 @@ export class FileContentProvider {
60
60
  }
61
61
  }
62
62
  async manifest() {
63
- this.#manifest ??= this.readJSON('manifest.json').catch(() => ({
64
- manifestVersion: '0',
65
- buildDigest: '',
66
- siteConfigRef: '',
67
- routes: {},
68
- fragments: {},
69
- layouts: {},
70
- }));
63
+ this.#manifest ??= this.readJSON('manifest.json');
71
64
  return await this.#manifest;
72
65
  }
73
66
  async fetchRaw(key) {
@@ -17,6 +17,9 @@ export declare function VideoPlayer({ video, ...attrs }: VideoPlayerProps): impo
17
17
  rel?: string | undefined | undefined;
18
18
  content?: string | undefined | undefined;
19
19
  dir?: string | undefined | undefined;
20
+ hidden?: boolean | undefined | undefined;
21
+ slot?: string | undefined | undefined;
22
+ style?: import("react").CSSProperties | undefined;
20
23
  children?: import("react").ReactNode;
21
24
  className?: string | undefined | undefined;
22
25
  crossOrigin?: "" | "anonymous" | "use-credentials" | undefined;
@@ -31,12 +34,9 @@ export declare function VideoPlayer({ video, ...attrs }: VideoPlayerProps): impo
31
34
  contextMenu?: string | undefined | undefined;
32
35
  draggable?: (boolean | "true" | "false") | undefined;
33
36
  enterKeyHint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
34
- hidden?: boolean | undefined | undefined;
35
37
  lang?: string | undefined | undefined;
36
38
  nonce?: string | undefined | undefined;
37
- slot?: string | undefined | undefined;
38
39
  spellCheck?: (boolean | "true" | "false") | undefined;
39
- style?: import("react").CSSProperties | undefined;
40
40
  tabIndex?: number | undefined | undefined;
41
41
  translate?: "yes" | "no" | undefined | undefined;
42
42
  radioGroup?: string | undefined | undefined;
@@ -17,8 +17,8 @@ export declare function Image({ image, value, ...attrs }: ImageProps): import("r
17
17
  prefix?: string | undefined | undefined;
18
18
  width?: number | string | undefined | undefined;
19
19
  height?: number | string | undefined | undefined;
20
- srcSet?: string | undefined | undefined;
21
20
  sizes?: string | undefined | undefined;
21
+ srcSet?: string | undefined | undefined;
22
22
  loading?: "eager" | "lazy" | undefined | undefined;
23
23
  decoding?: "async" | "auto" | "sync" | undefined | undefined;
24
24
  title?: string | undefined | undefined;
@@ -26,6 +26,9 @@ export declare function Image({ image, value, ...attrs }: ImageProps): import("r
26
26
  content?: string | undefined | undefined;
27
27
  referrerPolicy?: import("react").HTMLAttributeReferrerPolicy | undefined;
28
28
  dir?: string | undefined | undefined;
29
+ hidden?: boolean | undefined | undefined;
30
+ slot?: string | undefined | undefined;
31
+ style?: import("react").CSSProperties | undefined;
29
32
  children?: ReactNode;
30
33
  className?: string | undefined | undefined;
31
34
  crossOrigin?: "" | "anonymous" | "use-credentials" | undefined;
@@ -42,12 +45,9 @@ export declare function Image({ image, value, ...attrs }: ImageProps): import("r
42
45
  contextMenu?: string | undefined | undefined;
43
46
  draggable?: (boolean | "true" | "false") | undefined;
44
47
  enterKeyHint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
45
- hidden?: boolean | undefined | undefined;
46
48
  lang?: string | undefined | undefined;
47
49
  nonce?: string | undefined | undefined;
48
- slot?: string | undefined | undefined;
49
50
  spellCheck?: (boolean | "true" | "false") | undefined;
50
- style?: import("react").CSSProperties | undefined;
51
51
  tabIndex?: number | undefined | undefined;
52
52
  translate?: "yes" | "no" | undefined | undefined;
53
53
  radioGroup?: string | undefined | undefined;
@@ -14,7 +14,7 @@ export function Image({ image, value, ...attrs }) {
14
14
  alt: '',
15
15
  sources: [],
16
16
  };
17
- if (!resolved.sources.length) {
17
+ if (!resolved.sources[0]?.src) {
18
18
  return createElement('img', {
19
19
  src: '',
20
20
  alt: resolved.alt,
@@ -1,11 +1,17 @@
1
1
  import { z } from 'zod';
2
2
  export interface ImageSource {
3
- /** Derivative URL. */
3
+ /** Primary derivative URL for this rendition candidate. */
4
4
  src: string;
5
- /** Intrinsic width in pixels. */
6
- width: number;
7
- /** Intrinsic height in pixels. */
8
- height: number;
5
+ /** Intrinsic width in pixels (0 when the format carries none, e.g. SVG). */
6
+ width?: number;
7
+ /** Intrinsic height in pixels (0 when the format carries none). */
8
+ height?: number;
9
+ /** Optional width-descriptor rendition candidates. */
10
+ srcset?: string;
11
+ /** sizes attribute from the slot contract. */
12
+ sizes?: string;
13
+ /** Media query scoping this source (art direction). */
14
+ media?: string;
9
15
  /** MIME type hint, e.g. image/webp. */
10
16
  type: string;
11
17
  }
@@ -18,7 +24,7 @@ export interface Image {
18
24
  versionId: string;
19
25
  /** Accessible description of the image content. */
20
26
  alt: string;
21
- /** Responsive derivative variants produced for the asset slot. */
27
+ /** <source> candidates produced for the asset slot. */
22
28
  sources: ImageSource[];
23
29
  }
24
30
  export interface ImageSlotContract {
@@ -33,8 +39,11 @@ export interface SlotOutput {
33
39
  }
34
40
  export declare const ImageSourceSchema: z.ZodObject<{
35
41
  src: z.ZodString;
36
- width: z.ZodNumber;
37
- height: z.ZodNumber;
42
+ srcset: z.ZodOptional<z.ZodString>;
43
+ width: z.ZodOptional<z.ZodNumber>;
44
+ height: z.ZodOptional<z.ZodNumber>;
45
+ sizes: z.ZodOptional<z.ZodString>;
46
+ media: z.ZodOptional<z.ZodString>;
38
47
  type: z.ZodString;
39
48
  }, z.core.$strip>;
40
49
  export declare const ImageSchema: z.ZodObject<{
@@ -44,21 +53,26 @@ export declare const ImageSchema: z.ZodObject<{
44
53
  alt: z.ZodString;
45
54
  sources: z.ZodArray<z.ZodObject<{
46
55
  src: z.ZodString;
47
- width: z.ZodNumber;
48
- height: z.ZodNumber;
56
+ srcset: z.ZodOptional<z.ZodString>;
57
+ width: z.ZodOptional<z.ZodNumber>;
58
+ height: z.ZodOptional<z.ZodNumber>;
59
+ sizes: z.ZodOptional<z.ZodString>;
60
+ media: z.ZodOptional<z.ZodString>;
49
61
  type: z.ZodString;
50
62
  }, z.core.$strip>>;
51
63
  }, z.core.$strip>;
52
64
  export type ImageAttrs = Record<string, unknown>;
53
65
  /**
54
- * Picks the primary variant from an image's sources.
55
- * Uses the largest width; ties fall back to the first entry.
66
+ * The primary rendition of an image — its first source entry.
56
67
  */
57
68
  export declare function primaryImageSource(image: Image): ImageSource;
58
69
  /**
59
70
  * Computes the full set of <img> attributes for an Image.
60
71
  *
61
72
  * Framework components (React <Picture>, Astro <img>, Svelte <img>) call this
62
- * and spread the result into their native element. No HTML string serialization.
73
+ * and spread the result into their native element. No HTML string
74
+ * serialization. Media-scoped sources need a <picture> wrapper and are not
75
+ * representable as plain <img> attributes, so srcset/sizes are taken from the
76
+ * first unscoped source only.
63
77
  */
64
78
  export declare function imageAttrs(image: Image, overrides?: ImageAttrs): ImageAttrs;
@@ -4,8 +4,11 @@ import { z } from 'zod';
4
4
  // ---------------------------------------------------------------------------
5
5
  export const ImageSourceSchema = z.object({
6
6
  src: z.string().min(1),
7
- width: z.number().int().nonnegative(),
8
- height: z.number().int().nonnegative(),
7
+ srcset: z.string().min(1).optional(),
8
+ width: z.number().int().nonnegative().optional(),
9
+ height: z.number().int().nonnegative().optional(),
10
+ sizes: z.string().min(1).optional(),
11
+ media: z.string().min(1).optional(),
9
12
  type: z.string().min(1),
10
13
  });
11
14
  export const ImageSchema = z.object({
@@ -16,33 +19,30 @@ export const ImageSchema = z.object({
16
19
  sources: z.array(ImageSourceSchema).min(1),
17
20
  });
18
21
  /**
19
- * Picks the primary variant from an image's sources.
20
- * Uses the largest width; ties fall back to the first entry.
22
+ * The primary rendition of an image — its first source entry.
21
23
  */
22
24
  export function primaryImageSource(image) {
23
- return image.sources.reduce((largest, source) => source.width > largest.width ? source : largest, image.sources[0]);
25
+ return image.sources[0];
24
26
  }
25
27
  /**
26
28
  * Computes the full set of <img> attributes for an Image.
27
29
  *
28
30
  * Framework components (React <Picture>, Astro <img>, Svelte <img>) call this
29
- * and spread the result into their native element. No HTML string serialization.
31
+ * and spread the result into their native element. No HTML string
32
+ * serialization. Media-scoped sources need a <picture> wrapper and are not
33
+ * representable as plain <img> attributes, so srcset/sizes are taken from the
34
+ * first unscoped source only.
30
35
  */
31
36
  export function imageAttrs(image, overrides = {}) {
32
- const primary = primaryImageSource(image);
33
- const srcset = image.sources
34
- .map((s) => `${s.src} ${s.width}w`)
35
- .join(', ');
36
- const sizes = image.sources
37
- .map((s) => `(max-width: ${s.width}px) ${s.width}px`)
38
- .join(', ');
37
+ const primary = image.sources[0];
38
+ const unscoped = image.sources.find((s) => !s.media) ?? primary;
39
39
  return {
40
40
  src: primary.src,
41
41
  alt: image.alt,
42
- width: primary.width > 0 ? primary.width : undefined,
43
- height: primary.height > 0 ? primary.height : undefined,
44
- srcSet: srcset || undefined,
45
- sizes: sizes || undefined,
42
+ width: primary.width && primary.width > 0 ? primary.width : undefined,
43
+ height: primary.height && primary.height > 0 ? primary.height : undefined,
44
+ srcSet: unscoped?.srcset || undefined,
45
+ sizes: unscoped?.sizes || undefined,
46
46
  loading: 'lazy',
47
47
  decoding: 'async',
48
48
  ...overrides,
@@ -11,8 +11,11 @@ export declare const AssetSchema: z.ZodUnion<readonly [z.ZodObject<{
11
11
  alt: z.ZodString;
12
12
  sources: z.ZodArray<z.ZodObject<{
13
13
  src: z.ZodString;
14
- width: z.ZodNumber;
15
- height: z.ZodNumber;
14
+ srcset: z.ZodOptional<z.ZodString>;
15
+ width: z.ZodOptional<z.ZodNumber>;
16
+ height: z.ZodOptional<z.ZodNumber>;
17
+ sizes: z.ZodOptional<z.ZodString>;
18
+ media: z.ZodOptional<z.ZodString>;
16
19
  type: z.ZodString;
17
20
  }, z.core.$strip>>;
18
21
  }, z.core.$strip>, z.ZodObject<{
@@ -31,8 +34,11 @@ export declare const AssetSchema: z.ZodUnion<readonly [z.ZodObject<{
31
34
  alt: z.ZodString;
32
35
  sources: z.ZodArray<z.ZodObject<{
33
36
  src: z.ZodString;
34
- width: z.ZodNumber;
35
- height: z.ZodNumber;
37
+ srcset: z.ZodOptional<z.ZodString>;
38
+ width: z.ZodOptional<z.ZodNumber>;
39
+ height: z.ZodOptional<z.ZodNumber>;
40
+ sizes: z.ZodOptional<z.ZodString>;
41
+ media: z.ZodOptional<z.ZodString>;
36
42
  type: z.ZodString;
37
43
  }, z.core.$strip>>;
38
44
  }, z.core.$strip>>;
@@ -54,8 +54,11 @@ export declare const VideoSchema: z.ZodObject<{
54
54
  alt: z.ZodString;
55
55
  sources: z.ZodArray<z.ZodObject<{
56
56
  src: z.ZodString;
57
- width: z.ZodNumber;
58
- height: z.ZodNumber;
57
+ srcset: z.ZodOptional<z.ZodString>;
58
+ width: z.ZodOptional<z.ZodNumber>;
59
+ height: z.ZodOptional<z.ZodNumber>;
60
+ sizes: z.ZodOptional<z.ZodString>;
61
+ media: z.ZodOptional<z.ZodString>;
59
62
  type: z.ZodString;
60
63
  }, z.core.$strip>>;
61
64
  }, z.core.$strip>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradial/aci",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -85,13 +85,6 @@
85
85
  "default": "./dist/astro/index.js"
86
86
  }
87
87
  },
88
- "scripts": {
89
- "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
90
- "build:cli": "cd ../.. && ./scripts/build-cli.sh",
91
- "generate-starters": "cd ../.. && tsx scripts/generate-starters.ts",
92
- "compile-registry": "tsx src/cli/compile-registry.mjs",
93
- "prepack": "npm run generate-starters && npm run build && npm run build:cli"
94
- },
95
88
  "dependencies": {
96
89
  "markdown-it": "^14.1.0",
97
90
  "sanitize-html": "^2.13.1",
@@ -127,5 +120,11 @@
127
120
  "tsx": "^4.20.0",
128
121
  "typescript": "^5.9.3",
129
122
  "zod": "^4.0.0"
123
+ },
124
+ "scripts": {
125
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
126
+ "build:cli": "cd ../.. && ./scripts/build-cli.sh",
127
+ "generate-starters": "cd ../.. && tsx scripts/generate-starters.ts",
128
+ "compile-registry": "tsx src/cli/compile-registry.mjs"
130
129
  }
131
- }
130
+ }
File without changes