@gradial/aci 0.1.41 → 0.1.43
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/CHANGELOG.md +15 -11
- package/README.md +4 -4
- package/dist/astro/index.d.ts +1 -1
- package/dist/astro/index.js +1 -1
- package/dist/astro/page.d.ts +6 -0
- package/dist/astro/page.js +3 -2
- package/dist/audience-data-models.d.ts +286 -0
- package/dist/audience-data-models.js +46 -0
- package/dist/cli/compile-registry.d.mts +6 -0
- package/dist/cli/compile-registry.mjs +774 -0
- package/dist/cli/materialize-template.d.mts +19 -0
- package/dist/cli/materialize-template.mjs +128 -0
- package/dist/cli/validate-content.d.mts +32 -0
- package/dist/cli/validate-content.mjs +464 -0
- package/dist/cli/verify-next-cdn-routes.d.mts +2 -0
- package/dist/cli/verify-next-cdn-routes.mjs +47 -0
- package/dist/collect/aci.js +1 -0
- package/dist/collect/aci.js.sha256 +1 -0
- package/dist/collect/assignments.d.ts +65 -0
- package/dist/collect/assignments.js +138 -0
- package/dist/collect/batcher.d.ts +23 -0
- package/dist/collect/batcher.js +28 -6
- package/dist/collect/consent/envelope.d.ts +9 -3
- package/dist/collect/consent/envelope.js +58 -40
- package/dist/collect/declarative.d.ts +1 -1
- package/dist/collect/declarative.js +7 -1
- package/dist/collect/index.js +39 -6
- package/dist/collect/linkclicks.js +4 -2
- package/dist/collect/pageviews.d.ts +15 -3
- package/dist/collect/pageviews.js +28 -13
- package/dist/collect/transport.d.ts +4 -0
- package/dist/collect/transport.js +37 -2
- package/dist/consent/portable-envelope.d.ts +9 -0
- package/dist/consent/portable-envelope.js +110 -0
- package/dist/consent-banner/index.d.ts +33 -0
- package/dist/consent-banner/index.js +100 -0
- package/dist/content/assets.d.ts +1 -0
- package/dist/content/assets.js +1 -0
- package/dist/content/personalization.d.ts +35 -0
- package/dist/content/personalization.js +57 -0
- package/dist/content/resolve-slots.js +1 -0
- package/dist/content/types.d.ts +9 -1
- package/dist/edge/personalization-core.d.ts +11 -0
- package/dist/edge/personalization-core.js +265 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/next/assignments-script.d.ts +14 -0
- package/dist/next/assignments-script.js +17 -0
- package/dist/next/collect-route.d.ts +19 -0
- package/dist/next/collect-route.js +422 -0
- package/dist/next/envelope-digest.d.ts +21 -0
- package/dist/next/envelope-digest.js +58 -0
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.js +1 -0
- package/dist/next/middleware.d.ts +31 -3
- package/dist/next/middleware.js +254 -30
- package/dist/next/page.js +2 -1
- package/dist/next/personalization-host.d.ts +127 -0
- package/dist/next/personalization-host.js +257 -0
- package/dist/next/server.d.ts +1 -0
- package/dist/next/server.js +9 -6
- package/dist/next/vercel-collect-sdk-route.d.ts +27 -0
- package/dist/next/vercel-collect-sdk-route.js +205 -0
- package/dist/next/vercel-personalization.d.ts +4 -0
- package/dist/next/vercel-personalization.js +4 -0
- package/dist/next/vercel-s3-personalization.d.ts +79 -0
- package/dist/next/vercel-s3-personalization.js +574 -0
- package/dist/providers/s3.d.ts +20 -5
- package/dist/providers/s3.js +93 -23
- package/dist/react/VideoPlayer.d.ts +6 -6
- package/dist/react/index.d.ts +6 -6
- package/dist/testing/conformance/checks.js +75 -4
- package/dist/types/block-ref.d.ts +2 -0
- package/dist/types/block-ref.js +1 -0
- package/dist/types/page.d.ts +1 -1
- package/package.json +44 -15
- package/src/astro/ConsentBanner.astro +62 -0
- package/src/cli/compile-registry.mjs +24 -10
- package/src/cli/validate-content.mjs +52 -13
- package/src/cli/verify-next-cdn-routes.mjs +5 -4
- package/src/consent-banner/styles.css +110 -0
- package/src/types/block-ref.test.ts +45 -0
- package/src/types/block-ref.ts +2 -0
- package/src/types/page.ts +1 -1
- package/templates/astro/starter-content/src/cms/contracts/components/index.ts +2 -0
- package/templates/astro/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
- package/templates/astro/starter-content/src/cms/registry.ts +3 -0
- package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +41 -0
- package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +7 -6
- package/templates/astro/starter-content/src/fixtures/composition.ts +3 -2
- package/templates/astro/starter-content/src/fixtures/sections.ts +2 -1
- package/templates/astro/starter-content/src/render/RenderPage.astro +3 -2
- package/templates/astro/template/src/layouts/SiteLayout.astro +3 -1
- package/templates/astro/template/src/pages/[...slug].astro +1 -0
- package/templates/common/.agents/skills/authoring/SKILL.md +11 -3
- package/templates/common/.agents/skills/authoring/references/block-slots.md +3 -0
- package/templates/common/.content/audiences/preference-blue/_index.json +11 -0
- package/templates/common/.content/audiences/preference-red/_index.json +11 -0
- package/templates/common/.content/overlays/audience/preference-blue/pages/home/_index.json +13 -0
- package/templates/common/.content/overlays/audience/preference-red/pages/home/_index.json +13 -0
- package/templates/common/.content/pages/home/_index.json +124 -4
- package/templates/common/.content/segments/preference-blue/_index.json +20 -0
- package/templates/common/.content/segments/preference-red/_index.json +20 -0
- package/templates/common/contracts/components/index.ts +21 -1
- package/templates/common/contracts/components/personalizationChoice.contract.ts +17 -0
- package/templates/common/starter-content/.content/pages/about/_index.json +3 -0
- package/templates/common/starter-content/.content/pages/home/_index.json +9 -0
- package/templates/nextjs/starter-content/src/cms/contracts/components/index.ts +3 -1
- package/templates/nextjs/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
- package/templates/nextjs/starter-content/src/cms/registry.ts +3 -0
- package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +53 -0
- package/templates/nextjs/starter-content/src/middleware.ts +41 -0
- package/templates/nextjs/template/src/app/collect/route.ts +18 -0
- package/templates/nextjs/template/src/middleware.ts +36 -2
- package/templates/nextjs/template/src/middleware.ts.tmpl +31 -1
- package/templates/astro/template/.aci.yaml +0 -28
- package/templates/common/.content/.gradial-dam/blobs/sha256/7f/bf/7fbfc7fe230ff12688c496f660119e41d9725c4504d82e310c15665c73c09292 +0 -0
- package/templates/common/.content/.gradial-dam/blobs/sha256/a0/f5/a0f506689de9b15a7055f70103a3f02cd3ba105771ae2de6754df2ede1210d3a +0 -0
- package/templates/common/.content/.gradial-dam/blobs/sha256/dd/b7/ddb7e9642be08317bcd4b2bd26b58ba8d0d34dd7239e03e684a4f3634fa3845b +0 -0
- package/templates/common/.content/.gradial-dam/blobs/sha256/fb/32/fb3260e8286ea8231ea1c77b9c0be92d6edd9026565731eb14cfdcfd6b1b75b3 +0 -0
- package/templates/common/.content/config/robots.txt +0 -2
- package/templates/common/.content/config/site.json +0 -197
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/asset.json +0 -9
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/editorial.json +0 -14
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/governance.json +0 -23
- package/templates/common/.content/dam/folders/seed/ast_site_about_hero/versions/v0001.json +0 -37
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/asset.json +0 -9
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/editorial.json +0 -14
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/governance.json +0 -23
- package/templates/common/.content/dam/folders/seed/ast_site_home_hero/versions/v0001.json +0 -37
- package/templates/common/.content/fragments/navbar/_index.json +0 -25
- package/templates/common/.content/fragments/site-footer/_index.json +0 -62
- package/templates/common/.content/pages/about/_index.json +0 -51
- package/templates/nextjs/template/.aci.yaml +0 -26
|
@@ -19,21 +19,24 @@
|
|
|
19
19
|
* Uses the JSON schemas generated by compile-registry — portable, no React/CSS
|
|
20
20
|
* imports needed, same schemas usable by Go backend or other tools.
|
|
21
21
|
*/
|
|
22
|
+
import { realpathSync } from 'node:fs';
|
|
22
23
|
import { readdir, readFile } from 'node:fs/promises';
|
|
23
24
|
import path from 'node:path';
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
if (isMainModule()) {
|
|
28
|
+
const args = parseArgs(process.argv.slice(2));
|
|
29
|
+
try {
|
|
30
|
+
const result = await validateContent(args);
|
|
31
|
+
console.log(JSON.stringify(result, null, 2));
|
|
32
|
+
if (!result.success) process.exitCode = 1;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(JSON.stringify({
|
|
35
|
+
success: false,
|
|
36
|
+
errors: [{ message: String(error?.message ?? error) }],
|
|
37
|
+
}, null, 2));
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
}
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
// ---------------------------------------------------------------------------
|
|
@@ -95,6 +98,24 @@ async function validateContent(options) {
|
|
|
95
98
|
for (const block of blocks) {
|
|
96
99
|
blocksValidated++;
|
|
97
100
|
|
|
101
|
+
if (!block || typeof block !== 'object') {
|
|
102
|
+
collector.push({
|
|
103
|
+
page: relativePath,
|
|
104
|
+
blockId: '<unknown>',
|
|
105
|
+
message: 'page region entry must be a component block',
|
|
106
|
+
});
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (block.$type !== 'component') {
|
|
111
|
+
collector.push({
|
|
112
|
+
page: relativePath,
|
|
113
|
+
blockId: block.id ?? '<unknown>',
|
|
114
|
+
path: '$type',
|
|
115
|
+
message: 'block "$type" must be "component"',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
98
119
|
if (!block.component) {
|
|
99
120
|
collector.push({
|
|
100
121
|
page: relativePath,
|
|
@@ -380,6 +401,13 @@ function validateNestedSlots(parentBlock, componentMap, schemaMap, path = 'props
|
|
|
380
401
|
issues.push({ path: nestedPath, message: `${parentBlock.component}: block ref child must be a block` });
|
|
381
402
|
continue;
|
|
382
403
|
}
|
|
404
|
+
if (nestedBlock.$type !== 'component') {
|
|
405
|
+
issues.push({
|
|
406
|
+
path: `${nestedPath}.$type`,
|
|
407
|
+
message: `${parentBlock.component}: block ref child "$type" must be "component"`,
|
|
408
|
+
});
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
383
411
|
|
|
384
412
|
if (!componentMap.has(nestedBlock.component)) {
|
|
385
413
|
issues.push({ path: `${nestedPath}.component`, message: `${parentBlock.component}: unknown nested component "${nestedBlock.component}"` });
|
|
@@ -450,3 +478,14 @@ function parseArgs(argv) {
|
|
|
450
478
|
}
|
|
451
479
|
return out;
|
|
452
480
|
}
|
|
481
|
+
|
|
482
|
+
function isMainModule() {
|
|
483
|
+
if (!process.argv[1]) return false;
|
|
484
|
+
try {
|
|
485
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(path.resolve(process.argv[1]));
|
|
486
|
+
} catch {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export { validateContent };
|
|
@@ -52,16 +52,17 @@ checkMissing(
|
|
|
52
52
|
'Use src/app/[[...slug]]/page.tsx (optional catch-all). The required [...slug] form breaks file-provider local dev where "/" arrives with an empty slug.',
|
|
53
53
|
);
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
'
|
|
55
|
+
checkFile(
|
|
56
|
+
'canonical asset route exists',
|
|
57
57
|
'src/app/gradial/assets/[...path]/route.ts',
|
|
58
|
-
|
|
58
|
+
(content) => content.includes('@gradial/aci/next/asset-route'),
|
|
59
|
+
'Expected the canonical asset route to use @gradial/aci/next/asset-route.',
|
|
59
60
|
);
|
|
60
61
|
|
|
61
62
|
checkMissing(
|
|
62
63
|
'legacy release-scoped asset route is removed',
|
|
63
64
|
'src/app/gradial/assets/[releaseId]/[...path]/route.ts',
|
|
64
|
-
'
|
|
65
|
+
'Release-scoped asset URLs are produced by withAci() rewrites into the canonical src/app/gradial/assets/[...path]/route.ts; do not add a [releaseId] route.',
|
|
65
66
|
);
|
|
66
67
|
|
|
67
68
|
checkMissing(
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/* Base consent banner styles — sites override with their own CSS. */
|
|
2
|
+
/* The banner is injected into document.body by mountConsentBanner(). */
|
|
3
|
+
|
|
4
|
+
#aci-consent-banner {
|
|
5
|
+
position: fixed;
|
|
6
|
+
bottom: 0;
|
|
7
|
+
left: 0;
|
|
8
|
+
right: 0;
|
|
9
|
+
z-index: 9999;
|
|
10
|
+
background: #1a1a2e;
|
|
11
|
+
color: #fff;
|
|
12
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
13
|
+
font-size: 14px;
|
|
14
|
+
line-height: 1.5;
|
|
15
|
+
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
16
|
+
opacity: 1;
|
|
17
|
+
transform: translateY(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#aci-consent-banner.aci-consent-banner--hidden {
|
|
21
|
+
opacity: 0;
|
|
22
|
+
transform: translateY(100%);
|
|
23
|
+
pointer-events: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#aci-consent-banner .aci-consent-banner__content {
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: space-between;
|
|
30
|
+
gap: 16px;
|
|
31
|
+
max-width: 1200px;
|
|
32
|
+
margin: 0 auto;
|
|
33
|
+
padding: 16px 24px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#aci-consent-banner .aci-consent-banner__text {
|
|
37
|
+
flex: 1;
|
|
38
|
+
min-width: 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#aci-consent-banner .aci-consent-banner__heading {
|
|
42
|
+
margin: 0 0 4px 0;
|
|
43
|
+
font-size: 16px;
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#aci-consent-banner .aci-consent-banner__message {
|
|
48
|
+
margin: 0;
|
|
49
|
+
font-size: 13px;
|
|
50
|
+
opacity: 0.85;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#aci-consent-banner .aci-consent-banner__privacy-link {
|
|
54
|
+
margin-left: 8px;
|
|
55
|
+
color: #8ecae6;
|
|
56
|
+
text-decoration: underline;
|
|
57
|
+
font-size: 13px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#aci-consent-banner .aci-consent-banner__actions {
|
|
61
|
+
display: flex;
|
|
62
|
+
gap: 8px;
|
|
63
|
+
flex-shrink: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#aci-consent-banner .aci-consent-banner__btn {
|
|
67
|
+
padding: 8px 20px;
|
|
68
|
+
border: none;
|
|
69
|
+
border-radius: 6px;
|
|
70
|
+
font-size: 14px;
|
|
71
|
+
font-weight: 500;
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
transition: background-color 0.15s ease;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#aci-consent-banner .aci-consent-banner__btn--accept {
|
|
77
|
+
background: #2196f3;
|
|
78
|
+
color: #fff;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#aci-consent-banner .aci-consent-banner__btn--accept:hover {
|
|
82
|
+
background: #1976d2;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#aci-consent-banner .aci-consent-banner__btn--reject {
|
|
86
|
+
background: transparent;
|
|
87
|
+
color: #fff;
|
|
88
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#aci-consent-banner .aci-consent-banner__btn--reject:hover {
|
|
92
|
+
background: rgba(255, 255, 255, 0.1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@media (max-width: 640px) {
|
|
96
|
+
#aci-consent-banner .aci-consent-banner__content {
|
|
97
|
+
flex-direction: column;
|
|
98
|
+
align-items: stretch;
|
|
99
|
+
gap: 12px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#aci-consent-banner .aci-consent-banner__actions {
|
|
103
|
+
flex-direction: column-reverse;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#aci-consent-banner .aci-consent-banner__btn {
|
|
107
|
+
width: 100%;
|
|
108
|
+
text-align: center;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
import { compileSchema } from '../compiler/zod-to-jsonschema.js';
|
|
6
|
+
import { defineComponentContract } from '../define-component.js';
|
|
7
|
+
import { blockRefArray } from './block-ref.js';
|
|
8
|
+
|
|
9
|
+
const childContract = defineComponentContract({
|
|
10
|
+
id: 'child',
|
|
11
|
+
props: z.object({
|
|
12
|
+
label: z.string(),
|
|
13
|
+
}),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('block references require and accept the component discriminator', () => {
|
|
17
|
+
const schema = blockRefArray([childContract]);
|
|
18
|
+
const typed = {
|
|
19
|
+
$type: 'component',
|
|
20
|
+
id: 'child-1',
|
|
21
|
+
component: 'child',
|
|
22
|
+
props: { label: 'Hello' },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
assert.equal(schema.safeParse([typed]).success, true);
|
|
26
|
+
assert.equal(schema.safeParse([{
|
|
27
|
+
id: typed.id,
|
|
28
|
+
component: typed.component,
|
|
29
|
+
props: typed.props,
|
|
30
|
+
}]).success, false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('compiled block-reference schema exposes the component discriminator', () => {
|
|
34
|
+
const compiled = compileSchema(blockRefArray([childContract])) as {
|
|
35
|
+
items?: {
|
|
36
|
+
properties?: Record<string, { const?: string }>;
|
|
37
|
+
required?: string[];
|
|
38
|
+
additionalProperties?: boolean;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
assert.equal(compiled.items?.properties?.$type?.const, 'component');
|
|
43
|
+
assert.equal(compiled.items?.required?.includes('$type'), true);
|
|
44
|
+
assert.equal(compiled.items?.additionalProperties, false);
|
|
45
|
+
});
|
package/src/types/block-ref.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface BlockRefMeta {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
const blockRefBase = z.object({
|
|
12
|
+
$type: z.literal('component'),
|
|
12
13
|
id: z.string().min(1),
|
|
13
14
|
component: z.string().min(1),
|
|
14
15
|
props: z.record(z.string(), z.unknown()),
|
|
@@ -21,6 +22,7 @@ type InferContractProps<T> = T extends ComponentContract<string, infer Schema>
|
|
|
21
22
|
type InferContractId<T> = T extends ComponentContract<infer Id, unknown> ? Id : never;
|
|
22
23
|
|
|
23
24
|
export type BlockRefEntry<T extends ComponentContract> = {
|
|
25
|
+
$type: 'component';
|
|
24
26
|
id: string;
|
|
25
27
|
component: InferContractId<T>;
|
|
26
28
|
props: InferContractProps<T>;
|
package/src/types/page.ts
CHANGED
|
@@ -3,12 +3,14 @@ import { containerContract } from './container.contract';
|
|
|
3
3
|
import { featureCardContract } from './featureCard.contract';
|
|
4
4
|
import { featureGridContract } from './featureGrid.contract';
|
|
5
5
|
import { homeHeroContract } from './homeHero.contract';
|
|
6
|
+
import { personalizationChoiceContract } from './personalizationChoice.contract';
|
|
6
7
|
import { siteNavigationContract } from './siteNavigation.contract';
|
|
7
8
|
import { siteFooterContract } from './siteFooter.contract';
|
|
8
9
|
import { statsBarContract } from './statsBar.contract';
|
|
9
10
|
|
|
10
11
|
const componentContracts = [
|
|
11
12
|
homeHeroContract,
|
|
13
|
+
personalizationChoiceContract,
|
|
12
14
|
containerContract,
|
|
13
15
|
buttonContract,
|
|
14
16
|
siteNavigationContract,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineComponentContract } from '@gradial/aci';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
export const schema = z.object({
|
|
5
|
+
title: z.string().min(1),
|
|
6
|
+
description: z.string().optional(),
|
|
7
|
+
redLabel: z.string().min(1),
|
|
8
|
+
blueLabel: z.string().min(1),
|
|
9
|
+
refreshLabel: z.string().min(1),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export type ContentProps = z.infer<typeof schema>;
|
|
13
|
+
|
|
14
|
+
export const personalizationChoiceContract = defineComponentContract({
|
|
15
|
+
id: 'personalization_choice',
|
|
16
|
+
props: schema,
|
|
17
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRegistry } from '@gradial/aci';
|
|
2
2
|
import { homeHeroContract } from './contracts/components/homeHero.contract';
|
|
3
|
+
import { personalizationChoiceContract } from './contracts/components/personalizationChoice.contract';
|
|
3
4
|
import { containerContract } from './contracts/components/container.contract';
|
|
4
5
|
import { buttonContract } from './contracts/components/button.contract';
|
|
5
6
|
import { siteNavigationContract } from './contracts/components/siteNavigation.contract';
|
|
@@ -8,6 +9,7 @@ import { featureGridContract } from './contracts/components/featureGrid.contract
|
|
|
8
9
|
import { featureCardContract } from './contracts/components/featureCard.contract';
|
|
9
10
|
import { statsBarContract } from './contracts/components/statsBar.contract';
|
|
10
11
|
import HomeHero from '../components/sections/HomeHero.astro';
|
|
12
|
+
import PersonalizationChoice from '../components/sections/PersonalizationChoice.astro';
|
|
11
13
|
import ContainerSection from '../components/sections/ContainerSection.astro';
|
|
12
14
|
import ButtonBlock from '../components/elements/ButtonBlock.astro';
|
|
13
15
|
import Header from '../components/chrome/Header.astro';
|
|
@@ -18,6 +20,7 @@ import StatsBar from '../components/sections/StatsBar.astro';
|
|
|
18
20
|
|
|
19
21
|
export const registry = createRegistry([
|
|
20
22
|
[homeHeroContract, HomeHero],
|
|
23
|
+
[personalizationChoiceContract, PersonalizationChoice],
|
|
21
24
|
[containerContract, ContainerSection],
|
|
22
25
|
[buttonContract, ButtonBlock],
|
|
23
26
|
[siteNavigationContract, Header],
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ContentProps } from '../../cms/contracts/components/personalizationChoice.contract';
|
|
3
|
+
|
|
4
|
+
type Props = ContentProps;
|
|
5
|
+
|
|
6
|
+
const { title, description, redLabel, blueLabel, refreshLabel } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<section class="border-y border-[var(--color-border)] bg-[var(--color-surface)] py-[var(--spacing-section-y)]">
|
|
10
|
+
<div class="mx-auto max-w-[var(--layout-content-max)] px-[var(--layout-gutter)] text-center">
|
|
11
|
+
<h2 class="text-[length:var(--text-h2)] font-[var(--font-weight-semibold)] text-[var(--color-fg)]">{title}</h2>
|
|
12
|
+
{description ? <p class="mx-auto mt-3 max-w-2xl text-[var(--color-fg-muted)]">{description}</p> : null}
|
|
13
|
+
<div class="mt-6 flex flex-wrap justify-center gap-3">
|
|
14
|
+
<button type="button" class="inline-flex min-h-10 items-center justify-center rounded-button border border-emerald-700 bg-emerald-600 px-[var(--spacing-button-x)] py-[var(--spacing-button-y)] font-[var(--font-weight-semibold)] text-white transition hover:bg-emerald-700" data-aci-consent-button>Enable personalization</button>
|
|
15
|
+
<button type="button" class="inline-flex min-h-10 items-center justify-center rounded-button border border-red-700 bg-red-600 px-[var(--spacing-button-x)] py-[var(--spacing-button-y)] font-[var(--font-weight-semibold)] text-white transition hover:bg-red-700" data-aci-event="preference" data-aci-prop-color="red">{redLabel}</button>
|
|
16
|
+
<button type="button" class="inline-flex min-h-10 items-center justify-center rounded-button border border-blue-700 bg-blue-600 px-[var(--spacing-button-x)] py-[var(--spacing-button-y)] font-[var(--font-weight-semibold)] text-white transition hover:bg-blue-700" data-aci-event="preference" data-aci-prop-color="blue">{blueLabel}</button>
|
|
17
|
+
<button type="button" class="inline-flex min-h-10 items-center justify-center rounded-button border border-[var(--button-outline-border)] bg-[var(--button-outline-bg)] px-[var(--spacing-button-x)] py-[var(--spacing-button-y)] font-[var(--font-weight-semibold)] text-[var(--button-outline-text)] transition hover:opacity-85" data-aci-refresh>{refreshLabel}</button>
|
|
18
|
+
</div>
|
|
19
|
+
<p class="mx-auto mt-4 max-w-2xl text-sm text-[var(--color-fg-muted)]" data-aci-cookie-status aria-live="polite">Consent: checking... | Browser marker: checking... | Identity and audience cookies: HttpOnly server-managed</p>
|
|
20
|
+
</div>
|
|
21
|
+
</section>
|
|
22
|
+
|
|
23
|
+
<script is:inline>
|
|
24
|
+
(() => {
|
|
25
|
+
const hasCookie = (name) => document.cookie.split('; ').some((value) => value.startsWith(`${name}=`));
|
|
26
|
+
const status = document.querySelector('[data-aci-cookie-status]');
|
|
27
|
+
const render = () => {
|
|
28
|
+
if (!status) return;
|
|
29
|
+
status.textContent = `Consent: ${hasCookie('aci_consent') ? 'granted' : 'not granted'} | Browser marker: ${hasCookie('aci_m') ? 'minted' : 'not minted'} | Identity and audience cookies: HttpOnly server-managed`;
|
|
30
|
+
};
|
|
31
|
+
document.querySelector('[data-aci-consent-button]')?.addEventListener('click', () => {
|
|
32
|
+
const envelope = encodeURIComponent(JSON.stringify({ v: 1, p: ['personalization'], t: new Date().toISOString(), src: 'demo-consent' }));
|
|
33
|
+
document.cookie = `aci_consent=${envelope}; Path=/; Max-Age=31536000; SameSite=Lax; Secure`;
|
|
34
|
+
window.aci?.('consent', { p: ['personalization'], src: 'demo-consent' });
|
|
35
|
+
render();
|
|
36
|
+
window.setTimeout(() => document.querySelector('[data-aci-refresh]')?.click(), 50);
|
|
37
|
+
}, true);
|
|
38
|
+
render();
|
|
39
|
+
window.setInterval(render, 1000);
|
|
40
|
+
})();
|
|
41
|
+
</script>
|
|
@@ -13,17 +13,18 @@ export const atomicBlockStoryFixtures: AtomicBlockStoryFixture[] = [
|
|
|
13
13
|
storybookId: 'core-atomic-blocks-button--variants',
|
|
14
14
|
title: 'Button / All Variants',
|
|
15
15
|
block: {
|
|
16
|
+
$type: 'component',
|
|
16
17
|
id: 'fixture-button-variants',
|
|
17
18
|
component: 'container',
|
|
18
19
|
props: {
|
|
19
20
|
theme: 'auto',
|
|
20
21
|
blocks: [
|
|
21
|
-
{ id: 'btn-primary', component: 'button', props: { label: 'Primary button', href: '#', variant: 'primary' } },
|
|
22
|
-
{ id: 'btn-secondary', component: 'button', props: { label: 'Secondary button', href: '#', variant: 'secondary' } },
|
|
23
|
-
{ id: 'btn-tertiary', component: 'button', props: { label: 'Tertiary button', href: '#', variant: 'tertiary' } },
|
|
24
|
-
{ id: 'btn-icon-before', component: 'button', props: { label: 'Icon before', href: '#', variant: 'primary', icon: 'arrow-right', iconPosition: 'before' } },
|
|
25
|
-
{ id: 'btn-icon-after', component: 'button', props: { label: 'Icon after', href: '#', variant: 'secondary', icon: 'arrow-right', iconPosition: 'after' } },
|
|
26
|
-
{ id: 'btn-new-window', component: 'button', props: { label: 'Opens in new tab', href: 'https://example.com', variant: 'primary', newWindow: true, icon: 'arrow-right', iconPosition: 'after' } }
|
|
22
|
+
{ $type: 'component', id: 'btn-primary', component: 'button', props: { label: 'Primary button', href: '#', variant: 'primary' } },
|
|
23
|
+
{ $type: 'component', id: 'btn-secondary', component: 'button', props: { label: 'Secondary button', href: '#', variant: 'secondary' } },
|
|
24
|
+
{ $type: 'component', id: 'btn-tertiary', component: 'button', props: { label: 'Tertiary button', href: '#', variant: 'tertiary' } },
|
|
25
|
+
{ $type: 'component', id: 'btn-icon-before', component: 'button', props: { label: 'Icon before', href: '#', variant: 'primary', icon: 'arrow-right', iconPosition: 'before' } },
|
|
26
|
+
{ $type: 'component', id: 'btn-icon-after', component: 'button', props: { label: 'Icon after', href: '#', variant: 'secondary', icon: 'arrow-right', iconPosition: 'after' } },
|
|
27
|
+
{ $type: 'component', id: 'btn-new-window', component: 'button', props: { label: 'Opens in new tab', href: 'https://example.com', variant: 'primary', newWindow: true, icon: 'arrow-right', iconPosition: 'after' } }
|
|
27
28
|
]
|
|
28
29
|
}
|
|
29
30
|
}
|
|
@@ -14,14 +14,15 @@ export const compositionStoryFixtures: CompositionStoryFixture[] = [
|
|
|
14
14
|
storybookId: 'composition-container--buttons',
|
|
15
15
|
title: 'Container / Buttons',
|
|
16
16
|
block: {
|
|
17
|
+
$type: 'component',
|
|
17
18
|
id: 'fixture-container-buttons',
|
|
18
19
|
component: 'container',
|
|
19
20
|
props: {
|
|
20
21
|
theme: 'auto',
|
|
21
22
|
className: 'space-y-4',
|
|
22
23
|
blocks: [
|
|
23
|
-
{ id: 'btn-1', component: 'button', props: { label: 'Primary action', href: '#', variant: 'primary', icon: 'arrow_forward', iconPosition: 'after' } },
|
|
24
|
-
{ id: 'btn-2', component: 'button', props: { label: 'Secondary action', href: '#', variant: 'secondary' } }
|
|
24
|
+
{ $type: 'component', id: 'btn-1', component: 'button', props: { label: 'Primary action', href: '#', variant: 'primary', icon: 'arrow_forward', iconPosition: 'after' } },
|
|
25
|
+
{ $type: 'component', id: 'btn-2', component: 'button', props: { label: 'Secondary action', href: '#', variant: 'secondary' } }
|
|
25
26
|
]
|
|
26
27
|
}
|
|
27
28
|
}
|
|
@@ -14,6 +14,7 @@ export const sectionStoryFixtures: SectionStoryFixture[] = [
|
|
|
14
14
|
storybookId: 'sections-home-hero--default',
|
|
15
15
|
title: 'Home Hero',
|
|
16
16
|
block: {
|
|
17
|
+
$type: 'component',
|
|
17
18
|
id: 'fixture-home-hero',
|
|
18
19
|
component: 'home_hero',
|
|
19
20
|
props: {
|
|
@@ -22,7 +23,7 @@ export const sectionStoryFixtures: SectionStoryFixture[] = [
|
|
|
22
23
|
description: 'One content model, three renderers. Build once in Next.js, Astro, and SvelteKit—then prove parity with shared fixtures.',
|
|
23
24
|
ctaLabel: 'Get started',
|
|
24
25
|
ctaHref: '/docs',
|
|
25
|
-
|
|
26
|
+
image: {
|
|
26
27
|
$type: 'gradial.image',
|
|
27
28
|
assetId: 'fixture_seattle',
|
|
28
29
|
versionId: 'v0001',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import SiteLayout from '../layouts/SiteLayout.astro';
|
|
3
3
|
import type { KernelPage, KernelSiteConfig } from '@gradial/aci/content';
|
|
4
|
-
import { registryLookup } from '@gradial/aci/astro';
|
|
4
|
+
import { registryLookup, collectScriptTagHTML } from '@gradial/aci/astro';
|
|
5
5
|
import { registry } from '../cms/registry';
|
|
6
6
|
import Header from '../components/chrome/Header.astro';
|
|
7
7
|
import Footer from '../components/chrome/Footer.astro';
|
|
@@ -24,12 +24,13 @@ const title = page.metadata?.title || siteConfig.seo?.title || siteConfig.title;
|
|
|
24
24
|
const description = page.metadata?.description || siteConfig.seo?.description || '';
|
|
25
25
|
const canonical = page.metadata?.canonical;
|
|
26
26
|
const siteName = siteConfig.seo?.siteName || siteConfig.title;
|
|
27
|
+
const collectTag = collectScriptTagHTML(siteConfig);
|
|
27
28
|
|
|
28
29
|
const footer = siteConfig.footer || {};
|
|
29
30
|
const blocks = page?.regions?.main || [];
|
|
30
31
|
---
|
|
31
32
|
|
|
32
|
-
<SiteLayout {title} {description} {canonical} {siteName}>
|
|
33
|
+
<SiteLayout {title} {description} {canonical} {siteName} {collectTag}>
|
|
33
34
|
<Header
|
|
34
35
|
brandHref={siteConfig.brandHref || '/'}
|
|
35
36
|
brandLabel={siteConfig.title || 'Gradial'}
|
|
@@ -14,9 +14,10 @@ interface Props {
|
|
|
14
14
|
* content).
|
|
15
15
|
*/
|
|
16
16
|
collectTag?: string;
|
|
17
|
+
assignmentsTag?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
const { title, description, canonical, siteName = 'Gradial', collectTag } = Astro.props;
|
|
20
|
+
const { title, description, canonical, siteName = 'Gradial', collectTag, assignmentsTag } = Astro.props;
|
|
20
21
|
---
|
|
21
22
|
|
|
22
23
|
<!doctype html>
|
|
@@ -32,6 +33,7 @@ const { title, description, canonical, siteName = 'Gradial', collectTag } = Astr
|
|
|
32
33
|
<meta property="og:description" content={description} />
|
|
33
34
|
<meta property="og:site_name" content={siteName} />
|
|
34
35
|
{collectTag ? <Fragment set:html={collectTag} /> : null}
|
|
36
|
+
{assignmentsTag ? <Fragment set:html={assignmentsTag} /> : null}
|
|
35
37
|
</head>
|
|
36
38
|
<body>
|
|
37
39
|
<slot />
|
|
@@ -17,6 +17,7 @@ const renderChildren = createRenderChildren(registry);
|
|
|
17
17
|
canonical={data.meta.canonical}
|
|
18
18
|
siteName={data.meta.siteName}
|
|
19
19
|
collectTag={data.collectTag}
|
|
20
|
+
assignmentsTag={data.assignmentsTag}
|
|
20
21
|
>
|
|
21
22
|
{data.layout.slots.map((slot) => {
|
|
22
23
|
const blocks = data.slots[slot.name];
|
|
@@ -98,6 +98,7 @@ The `home` slug is special-cased to route `/`. All other slugs map directly.
|
|
|
98
98
|
|
|
99
99
|
```json
|
|
100
100
|
{
|
|
101
|
+
"$type": "component",
|
|
101
102
|
"id": "hero-section",
|
|
102
103
|
"component": "home_hero",
|
|
103
104
|
"props": {
|
|
@@ -109,6 +110,7 @@ The `home` slug is special-cased to route `/`. All other slugs map directly.
|
|
|
109
110
|
```
|
|
110
111
|
|
|
111
112
|
- `component` must match a contract `id` exactly.
|
|
113
|
+
- Every embedded block must declare `"$type": "component"`.
|
|
112
114
|
- `props` must validate against the contract's Zod schema.
|
|
113
115
|
- `id` is unique within the page.
|
|
114
116
|
|
|
@@ -141,12 +143,14 @@ export const containerContract = defineComponentContract({
|
|
|
141
143
|
**Content JSON:**
|
|
142
144
|
```json
|
|
143
145
|
{
|
|
146
|
+
"$type": "component",
|
|
144
147
|
"id": "cta-section",
|
|
145
148
|
"component": "container",
|
|
146
149
|
"props": {
|
|
147
150
|
"className": "space-y-4",
|
|
148
151
|
"content": [
|
|
149
152
|
{
|
|
153
|
+
"$type": "component",
|
|
150
154
|
"id": "primary-cta",
|
|
151
155
|
"component": "button",
|
|
152
156
|
"props": {
|
|
@@ -156,6 +160,7 @@ export const containerContract = defineComponentContract({
|
|
|
156
160
|
}
|
|
157
161
|
},
|
|
158
162
|
{
|
|
163
|
+
"$type": "component",
|
|
159
164
|
"id": "secondary-cta",
|
|
160
165
|
"component": "button",
|
|
161
166
|
"props": {
|
|
@@ -182,6 +187,7 @@ The `button` component is a common atomic block:
|
|
|
182
187
|
|
|
183
188
|
```json
|
|
184
189
|
{
|
|
190
|
+
"$type": "component",
|
|
185
191
|
"id": "cta-button",
|
|
186
192
|
"component": "button",
|
|
187
193
|
"props": {
|
|
@@ -411,6 +417,7 @@ framework.
|
|
|
411
417
|
```json
|
|
412
418
|
// .content/pages/home/_index.json (inside regions.main)
|
|
413
419
|
{
|
|
420
|
+
"$type": "component",
|
|
414
421
|
"id": "features",
|
|
415
422
|
"component": "feature_showcase",
|
|
416
423
|
"props": {
|
|
@@ -499,9 +506,10 @@ export default [
|
|
|
499
506
|
];
|
|
500
507
|
```
|
|
501
508
|
|
|
502
|
-
Fragment content lives in `.content/fragments/{id}/_index.json` and
|
|
503
|
-
|
|
504
|
-
Footer) are registered in the registry
|
|
509
|
+
Fragment content lives in `.content/fragments/{id}/_index.json` and retains
|
|
510
|
+
the resource envelope `"$type": "fragment"` while declaring `component` +
|
|
511
|
+
`props`. Chrome components (Header, Footer) are registered in the registry
|
|
512
|
+
alongside page components.
|
|
505
513
|
|
|
506
514
|
The `layout` field in page JSON is **required** — there is no implicit
|
|
507
515
|
default. Every page must declare which layout it uses.
|
|
@@ -113,12 +113,14 @@ type RenderChildren<T extends readonly BlockRef[]> = (blocks: T) => Promise<Reac
|
|
|
113
113
|
|
|
114
114
|
```json
|
|
115
115
|
{
|
|
116
|
+
"$type": "component",
|
|
116
117
|
"id": "cta-section",
|
|
117
118
|
"component": "container",
|
|
118
119
|
"props": {
|
|
119
120
|
"theme": "dark",
|
|
120
121
|
"blocks": [
|
|
121
122
|
{
|
|
123
|
+
"$type": "component",
|
|
122
124
|
"id": "primary-cta",
|
|
123
125
|
"component": "callout",
|
|
124
126
|
"props": {
|
|
@@ -127,6 +129,7 @@ type RenderChildren<T extends readonly BlockRef[]> = (blocks: T) => Promise<Reac
|
|
|
127
129
|
}
|
|
128
130
|
},
|
|
129
131
|
{
|
|
132
|
+
"$type": "component",
|
|
130
133
|
"id": "heading",
|
|
131
134
|
"component": "heading",
|
|
132
135
|
"props": {
|