@anthusai/papyrus 1.0.0-next.4 → 1.0.0-next.6
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/amplify/data/resource.js +6 -0
- package/amplify/data/resource.ts +7 -0
- package/amplify/data/schema.js +6 -0
- package/amplify/data/schema.ts +7 -0
- package/amplify/functions/content-actions/handler.py +44 -2
- package/amplify/rebuild-trigger-settings.js +22 -0
- package/amplify/rebuild-trigger-settings.ts +42 -0
- package/amplify/site-backend-config.ts +6 -0
- package/amplify/site-backend.js +18 -0
- package/amplify/site-backend.ts +20 -0
- package/amplify/storage/resource.js +3 -0
- package/amplify/storage/resource.ts +3 -0
- package/app/%5F_preview/[[...path]]/route.ts +8 -0
- package/app/globals.css +19 -0
- package/app/newsroom/articles/[id]/page.tsx +15 -0
- package/app/newsroom/articles/page.tsx +13 -0
- package/bin/papyrus-app.mjs +12 -2
- package/bin/papyrus-infra.mjs +26 -0
- package/components/article-video.tsx +112 -3
- package/components/markus-ir-preview.tsx +279 -0
- package/components/newsroom-access-gate.tsx +66 -0
- package/components/newsroom-article-editor.tsx +637 -0
- package/components/newsroom-articles-view.tsx +129 -0
- package/components/newsroom-ops-shell.tsx +3 -1
- package/components/topic-steering-workspace.tsx +1 -59
- package/infra/amplify-app-shell.js +56 -1
- package/infra/amplify-app-shell.ts +79 -1
- package/infra/github-oidc-provider.js +24 -0
- package/infra/github-oidc-provider.ts +30 -0
- package/infra/index.js +1 -0
- package/infra/site-config.js +31 -1
- package/infra/site-config.ts +38 -1
- package/lib/content-actions-demo.ts +172 -0
- package/lib/newsroom-articles.ts +325 -0
- package/lib/newsroom-demo-articles.ts +90 -0
- package/lib/newsroom-nav.ts +2 -0
- package/lib/site-brand.ts +13 -0
- package/lib/staging-preview-object.ts +98 -0
- package/lib/video-mode.ts +42 -0
- package/lib/video-script.ts +63 -0
- package/middleware.ts +14 -1
- package/package.json +16 -1
- package/routes.manifest.json +27 -1
- package/scripts/videoml/build-browser-bundle.mjs +22 -0
- package/scripts/videoml/build-preview-bundle.mjs +50 -0
- package/scripts/videoml/bundle-common.mjs +129 -0
- package/scripts/videoml/preview/preview-entry.tsx +381 -0
- package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
- package/scripts/videoml/preview/preview-timing.ts +42 -0
- package/scripts/videoml/preview.html +77 -0
- package/scripts/videoml/standard-entry.tsx +46 -0
package/amplify/data/resource.js
CHANGED
|
@@ -493,6 +493,12 @@ const schema = a.schema({
|
|
|
493
493
|
.returns(a.json())
|
|
494
494
|
.authorization((allow) => [allow.groups(contentWriteGroups)])
|
|
495
495
|
.handler(a.handler.function(contentActions)),
|
|
496
|
+
requestStagingBuild: a
|
|
497
|
+
.mutation()
|
|
498
|
+
.arguments({ input: a.json().required() })
|
|
499
|
+
.returns(a.json())
|
|
500
|
+
.authorization((allow) => [allow.groups(contentWriteGroups)])
|
|
501
|
+
.handler(a.handler.function(contentActions)),
|
|
496
502
|
knowledgeQuery: a
|
|
497
503
|
.query()
|
|
498
504
|
.arguments({
|
package/amplify/data/resource.ts
CHANGED
|
@@ -537,6 +537,13 @@ const schema = a.schema({
|
|
|
537
537
|
.authorization((allow) => [allow.groups(contentWriteGroups)])
|
|
538
538
|
.handler(a.handler.function(contentActions)),
|
|
539
539
|
|
|
540
|
+
requestStagingBuild: a
|
|
541
|
+
.mutation()
|
|
542
|
+
.arguments({ input: a.json().required() })
|
|
543
|
+
.returns(a.json())
|
|
544
|
+
.authorization((allow) => [allow.groups(contentWriteGroups)])
|
|
545
|
+
.handler(a.handler.function(contentActions)),
|
|
546
|
+
|
|
540
547
|
knowledgeQuery: a
|
|
541
548
|
.query()
|
|
542
549
|
.arguments({
|
package/amplify/data/schema.js
CHANGED
|
@@ -393,6 +393,12 @@ export const schema = a.schema({
|
|
|
393
393
|
.returns(a.json())
|
|
394
394
|
.authorization((allow) => [allow.groups(contentWriteGroups)])
|
|
395
395
|
.handler(a.handler.function(contentActions)),
|
|
396
|
+
requestStagingBuild: a
|
|
397
|
+
.mutation()
|
|
398
|
+
.arguments({ input: a.json().required() })
|
|
399
|
+
.returns(a.json())
|
|
400
|
+
.authorization((allow) => [allow.groups(contentWriteGroups)])
|
|
401
|
+
.handler(a.handler.function(contentActions)),
|
|
396
402
|
knowledgeQuery: a
|
|
397
403
|
.query()
|
|
398
404
|
.arguments({
|
package/amplify/data/schema.ts
CHANGED
|
@@ -431,6 +431,13 @@ export const schema = a.schema({
|
|
|
431
431
|
.authorization((allow) => [allow.groups(contentWriteGroups)])
|
|
432
432
|
.handler(a.handler.function(contentActions)),
|
|
433
433
|
|
|
434
|
+
requestStagingBuild: a
|
|
435
|
+
.mutation()
|
|
436
|
+
.arguments({ input: a.json().required() })
|
|
437
|
+
.returns(a.json())
|
|
438
|
+
.authorization((allow) => [allow.groups(contentWriteGroups)])
|
|
439
|
+
.handler(a.handler.function(contentActions)),
|
|
440
|
+
|
|
434
441
|
knowledgeQuery: a
|
|
435
442
|
.query()
|
|
436
443
|
.arguments({
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
7
|
from papyrus_content.graphql_authoring import create_authoring_client
|
|
7
8
|
from papyrus_content.markus_renderer.derive import derive_body
|
|
9
|
+
from papyrus_content.rebuild_trigger import (
|
|
10
|
+
DEFAULT_STAGING_BRANCH,
|
|
11
|
+
reader_target_from_environment,
|
|
12
|
+
trigger_pretext_revalidation,
|
|
13
|
+
trigger_rebuild,
|
|
14
|
+
trigger_staging_build,
|
|
15
|
+
)
|
|
8
16
|
from papyrus_content.publishing import (
|
|
9
17
|
ItemFields,
|
|
10
18
|
PublishError,
|
|
@@ -72,21 +80,53 @@ def save_item_draft_action(arguments: dict, actor: str) -> dict:
|
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
|
|
83
|
+
def site_triggers_for(client, item_id: str) -> dict:
|
|
84
|
+
triggers: dict = {}
|
|
85
|
+
reader_app_id, reader_branch = reader_target_from_environment()
|
|
86
|
+
revalidate_base_url = (os.environ.get("PAPYRUS_REVALIDATE_BASE_URL") or "").strip()
|
|
87
|
+
if revalidate_base_url:
|
|
88
|
+
try:
|
|
89
|
+
item = client.get_record("Item", item_id) or {}
|
|
90
|
+
slugs = [item["slug"]] if item.get("slug") else []
|
|
91
|
+
revalidated = trigger_pretext_revalidation(slugs, None, base_url=revalidate_base_url)
|
|
92
|
+
triggers["revalidated"] = revalidated if revalidated is not None else {"ok": False, "reason": "not-configured"}
|
|
93
|
+
except Exception as error: # noqa: BLE001 - a trigger failure never fails the publish
|
|
94
|
+
triggers["revalidated"] = {"ok": False, "error": f"{type(error).__name__}: {error}"}
|
|
95
|
+
if reader_app_id:
|
|
96
|
+
triggers["rebuild"] = trigger_rebuild(reader_app_id=reader_app_id, reader_branch=reader_branch)
|
|
97
|
+
return triggers
|
|
98
|
+
|
|
99
|
+
|
|
75
100
|
def publish_item_action(arguments: dict, actor: str) -> dict:
|
|
76
101
|
client, _claims = create_authoring_client()
|
|
77
102
|
result = publish_item(client, arguments["id"], actor=actor)
|
|
78
|
-
|
|
103
|
+
response = {
|
|
79
104
|
"ok": True,
|
|
80
105
|
"changed": result.changed,
|
|
81
106
|
"publishedId": result.published_item_id,
|
|
82
107
|
"versionNumber": result.version_number,
|
|
83
108
|
}
|
|
109
|
+
if result.changed:
|
|
110
|
+
response.update(site_triggers_for(client, arguments["id"]))
|
|
111
|
+
return response
|
|
84
112
|
|
|
85
113
|
|
|
86
114
|
def unpublish_item_action(arguments: dict, actor: str) -> dict:
|
|
87
115
|
client, _claims = create_authoring_client()
|
|
88
116
|
result = unpublish_item(client, arguments["id"], actor=actor)
|
|
89
|
-
|
|
117
|
+
response = {"ok": True, "changed": result.changed}
|
|
118
|
+
if result.changed:
|
|
119
|
+
response.update(site_triggers_for(client, arguments["id"]))
|
|
120
|
+
return response
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def request_staging_build_action() -> dict:
|
|
124
|
+
cms_app_id = (os.environ.get("PAPYRUS_CMS_AMPLIFY_APP_ID") or "").strip()
|
|
125
|
+
if not cms_app_id:
|
|
126
|
+
return {"ok": False, "errors": [{"code": "staging-build-not-configured", "message": "This site has no staging build configured.", "line": None}]}
|
|
127
|
+
staging_branch = (os.environ.get("PAPYRUS_STAGING_BRANCH") or "").strip() or DEFAULT_STAGING_BRANCH
|
|
128
|
+
build = trigger_staging_build(cms_app_id=cms_app_id, staging_branch=staging_branch)
|
|
129
|
+
return {"ok": True, "build": build}
|
|
90
130
|
|
|
91
131
|
|
|
92
132
|
def handler(event, context):
|
|
@@ -102,6 +142,8 @@ def handler(event, context):
|
|
|
102
142
|
return publish_item_action(arguments, actor)
|
|
103
143
|
if field == "unpublishItem":
|
|
104
144
|
return unpublish_item_action(arguments, actor)
|
|
145
|
+
if field == "requestStagingBuild":
|
|
146
|
+
return request_staging_build_action()
|
|
105
147
|
except PublishError as error:
|
|
106
148
|
return failure_response(error)
|
|
107
149
|
raise ValueError(f"Unsupported field {field}")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const REBUILD_TRIGGER_ACTIONS = ["amplify:StartJob", "amplify:ListJobs"];
|
|
2
|
+
function branchJobsArn(input, appId, branch) {
|
|
3
|
+
return `arn:aws:amplify:${input.region}:${input.account}:apps/${appId}/branches/${branch}/jobs/*`;
|
|
4
|
+
}
|
|
5
|
+
export function rebuildTriggerSettings(input) {
|
|
6
|
+
const environment = {};
|
|
7
|
+
const resources = [];
|
|
8
|
+
const readerAppId = (input.reader?.amplifyAppId ?? "").trim();
|
|
9
|
+
if (readerAppId !== "") {
|
|
10
|
+
const readerBranch = (input.reader?.branchName ?? "").trim() || "main";
|
|
11
|
+
environment.PAPYRUS_READER_AMPLIFY_APP_ID = readerAppId;
|
|
12
|
+
environment.PAPYRUS_READER_BRANCH = readerBranch;
|
|
13
|
+
resources.push(branchJobsArn(input, readerAppId, readerBranch));
|
|
14
|
+
}
|
|
15
|
+
const cmsAppId = input.cmsAppId.trim();
|
|
16
|
+
if (input.stagingBuildEnabled && cmsAppId !== "") {
|
|
17
|
+
environment.PAPYRUS_CMS_AMPLIFY_APP_ID = cmsAppId;
|
|
18
|
+
environment.PAPYRUS_STAGING_BRANCH = "staging";
|
|
19
|
+
resources.push(branchJobsArn(input, cmsAppId, "staging"));
|
|
20
|
+
}
|
|
21
|
+
return { environment, actions: REBUILD_TRIGGER_ACTIONS, resources };
|
|
22
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment and IAM policy for the content-actions Lambda's site triggers.
|
|
3
|
+
* Pure (no CDK imports) so the policy can be tested without synthesizing.
|
|
4
|
+
*/
|
|
5
|
+
export type RebuildTriggerInput = {
|
|
6
|
+
reader?: { amplifyAppId: string; branchName?: string };
|
|
7
|
+
stagingBuildEnabled: boolean;
|
|
8
|
+
cmsAppId: string;
|
|
9
|
+
region: string;
|
|
10
|
+
account: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type RebuildTriggerSettings = {
|
|
14
|
+
environment: Record<string, string>;
|
|
15
|
+
actions: string[];
|
|
16
|
+
resources: string[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const REBUILD_TRIGGER_ACTIONS = ["amplify:StartJob", "amplify:ListJobs"];
|
|
20
|
+
|
|
21
|
+
function branchJobsArn(input: RebuildTriggerInput, appId: string, branch: string): string {
|
|
22
|
+
return `arn:aws:amplify:${input.region}:${input.account}:apps/${appId}/branches/${branch}/jobs/*`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function rebuildTriggerSettings(input: RebuildTriggerInput): RebuildTriggerSettings {
|
|
26
|
+
const environment: Record<string, string> = {};
|
|
27
|
+
const resources: string[] = [];
|
|
28
|
+
const readerAppId = (input.reader?.amplifyAppId ?? "").trim();
|
|
29
|
+
if (readerAppId !== "") {
|
|
30
|
+
const readerBranch = (input.reader?.branchName ?? "").trim() || "main";
|
|
31
|
+
environment.PAPYRUS_READER_AMPLIFY_APP_ID = readerAppId;
|
|
32
|
+
environment.PAPYRUS_READER_BRANCH = readerBranch;
|
|
33
|
+
resources.push(branchJobsArn(input, readerAppId, readerBranch));
|
|
34
|
+
}
|
|
35
|
+
const cmsAppId = input.cmsAppId.trim();
|
|
36
|
+
if (input.stagingBuildEnabled && cmsAppId !== "") {
|
|
37
|
+
environment.PAPYRUS_CMS_AMPLIFY_APP_ID = cmsAppId;
|
|
38
|
+
environment.PAPYRUS_STAGING_BRANCH = "staging";
|
|
39
|
+
resources.push(branchJobsArn(input, cmsAppId, "staging"));
|
|
40
|
+
}
|
|
41
|
+
return { environment, actions: REBUILD_TRIGGER_ACTIONS, resources };
|
|
42
|
+
}
|
|
@@ -21,6 +21,12 @@ export type PapyrusSiteBackendConfig = {
|
|
|
21
21
|
*/
|
|
22
22
|
productionAppId?: string;
|
|
23
23
|
auth?: PapyrusAuthConfig;
|
|
24
|
+
/** Static reader app (Markus-static sites) that a publish rebuilds through IAM `amplify:StartJob`. */
|
|
25
|
+
reader?: { amplifyAppId: string; branchName?: string };
|
|
26
|
+
/** Lets the newsroom start the `staging` branch build of this CMS app. */
|
|
27
|
+
stagingBuild?: { enabled: boolean };
|
|
28
|
+
/** Base URL of a Pretext reader whose `/api/revalidate` a publish calls (secret `PAPYRUS_REVALIDATE_SECRET`). */
|
|
29
|
+
revalidateBaseUrl?: string;
|
|
24
30
|
features?: {
|
|
25
31
|
consoleResponder?: boolean;
|
|
26
32
|
inboundEmail?: boolean;
|
package/amplify/site-backend.js
CHANGED
|
@@ -12,6 +12,7 @@ import { CfnEventSourceMapping, FunctionUrlAuthType } from "aws-cdk-lib/aws-lamb
|
|
|
12
12
|
import { CfnIndex, CfnVectorBucket, CfnVectorBucketPolicy } from "aws-cdk-lib/aws-s3vectors";
|
|
13
13
|
import { dirname, resolve } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { rebuildTriggerSettings } from "./rebuild-trigger-settings.js";
|
|
15
16
|
import { authConfigFromEnv, defineSiteAuth } from "./auth/resource.js";
|
|
16
17
|
import { data } from "./data/resource.js";
|
|
17
18
|
import { assignmentAction } from "./functions/assignment-action/resource.js";
|
|
@@ -333,6 +334,23 @@ export function defineSiteBackend(papyrusSite) {
|
|
|
333
334
|
actions: ["appsync:GraphQL"],
|
|
334
335
|
resources: ["*"],
|
|
335
336
|
}));
|
|
337
|
+
const rebuildSettings = rebuildTriggerSettings({
|
|
338
|
+
reader: site.reader,
|
|
339
|
+
stagingBuildEnabled: site.stagingBuild?.enabled === true,
|
|
340
|
+
cmsAppId: amplifyAppId,
|
|
341
|
+
region: Stack.of(contentActionsLambda).region,
|
|
342
|
+
account: Stack.of(contentActionsLambda).account,
|
|
343
|
+
});
|
|
344
|
+
for (const [name, value] of Object.entries(rebuildSettings.environment)) {
|
|
345
|
+
contentActionsLambda.addEnvironment(name, value);
|
|
346
|
+
}
|
|
347
|
+
if (rebuildSettings.resources.length > 0) {
|
|
348
|
+
contentActionsLambda.addToRolePolicy(new PolicyStatement({ actions: rebuildSettings.actions, resources: rebuildSettings.resources }));
|
|
349
|
+
}
|
|
350
|
+
if ((site.revalidateBaseUrl ?? "").trim() !== "") {
|
|
351
|
+
backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_BASE_URL", (site.revalidateBaseUrl ?? "").trim());
|
|
352
|
+
backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_SECRET", secret("PAPYRUS_REVALIDATE_SECRET"));
|
|
353
|
+
}
|
|
336
354
|
grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
|
|
337
355
|
grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda);
|
|
338
356
|
if (backend.sesInboundReceive) {
|
package/amplify/site-backend.ts
CHANGED
|
@@ -13,6 +13,7 @@ import * as s3 from "aws-cdk-lib/aws-s3";
|
|
|
13
13
|
import { CfnIndex, CfnVectorBucket, CfnVectorBucketPolicy } from "aws-cdk-lib/aws-s3vectors";
|
|
14
14
|
import { dirname, resolve } from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { rebuildTriggerSettings } from "./rebuild-trigger-settings";
|
|
16
17
|
import { authConfigFromEnv, defineSiteAuth } from "./auth/resource";
|
|
17
18
|
import { data } from "./data/resource";
|
|
18
19
|
import { assignmentAction } from "./functions/assignment-action/resource";
|
|
@@ -414,6 +415,25 @@ export function defineSiteBackend(papyrusSite: PapyrusSite) {
|
|
|
414
415
|
resources: ["*"],
|
|
415
416
|
}),
|
|
416
417
|
);
|
|
418
|
+
const rebuildSettings = rebuildTriggerSettings({
|
|
419
|
+
reader: site.reader,
|
|
420
|
+
stagingBuildEnabled: site.stagingBuild?.enabled === true,
|
|
421
|
+
cmsAppId: amplifyAppId,
|
|
422
|
+
region: Stack.of(contentActionsLambda).region,
|
|
423
|
+
account: Stack.of(contentActionsLambda).account,
|
|
424
|
+
});
|
|
425
|
+
for (const [name, value] of Object.entries(rebuildSettings.environment)) {
|
|
426
|
+
contentActionsLambda.addEnvironment(name, value);
|
|
427
|
+
}
|
|
428
|
+
if (rebuildSettings.resources.length > 0) {
|
|
429
|
+
contentActionsLambda.addToRolePolicy(
|
|
430
|
+
new PolicyStatement({ actions: rebuildSettings.actions, resources: rebuildSettings.resources }),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
if ((site.revalidateBaseUrl ?? "").trim() !== "") {
|
|
434
|
+
backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_BASE_URL", (site.revalidateBaseUrl ?? "").trim());
|
|
435
|
+
backend.contentActions.addEnvironment("PAPYRUS_REVALIDATE_SECRET", secret("PAPYRUS_REVALIDATE_SECRET"));
|
|
436
|
+
}
|
|
417
437
|
grantNewsroomReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
|
|
418
438
|
grantMediaReadWriteDelete(backend.modelAttachmentUpload.resources.lambda as LambdaFunction);
|
|
419
439
|
if (backend.sesInboundReceive) {
|
|
@@ -11,6 +11,9 @@ export const storage = defineStorage({
|
|
|
11
11
|
"corpora/*": [
|
|
12
12
|
allow.groups(["editor", "admin"]).to(["read"]),
|
|
13
13
|
],
|
|
14
|
+
"preview/*": [
|
|
15
|
+
allow.groups(["editor", "admin"]).to(["read"]),
|
|
16
|
+
],
|
|
14
17
|
"newsroom/*": [
|
|
15
18
|
allow.groups(["editor", "admin"]).to(["read", "write", "delete"]),
|
|
16
19
|
],
|
|
@@ -12,6 +12,9 @@ export const storage = defineStorage({
|
|
|
12
12
|
"corpora/*": [
|
|
13
13
|
allow.groups(["editor", "admin"]).to(["read"]),
|
|
14
14
|
],
|
|
15
|
+
"preview/*": [
|
|
16
|
+
allow.groups(["editor", "admin"]).to(["read"]),
|
|
17
|
+
],
|
|
15
18
|
"newsroom/*": [
|
|
16
19
|
allow.groups(["editor", "admin"]).to(["read", "write", "delete"]),
|
|
17
20
|
],
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { streamPreviewObject } from "../../../lib/staging-preview-object";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
export async function GET(request: NextRequest): Promise<Response> {
|
|
7
|
+
return streamPreviewObject(request.nextUrl.pathname);
|
|
8
|
+
}
|
package/app/globals.css
CHANGED
|
@@ -2282,6 +2282,25 @@ img {
|
|
|
2282
2282
|
width: 100%;
|
|
2283
2283
|
}
|
|
2284
2284
|
|
|
2285
|
+
.article-video__preview-frame {
|
|
2286
|
+
aspect-ratio: 16 / 9;
|
|
2287
|
+
background: #000;
|
|
2288
|
+
border: 0;
|
|
2289
|
+
display: block;
|
|
2290
|
+
width: 100%;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
.article-video__preview-controls {
|
|
2294
|
+
align-items: center;
|
|
2295
|
+
display: flex;
|
|
2296
|
+
gap: 0.75rem;
|
|
2297
|
+
margin-top: 0.5rem;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
.article-video__preview-controls input[type="range"] {
|
|
2301
|
+
flex: 1;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2285
2304
|
.article-video__credit {
|
|
2286
2305
|
color: rgb(var(--ink-solid-rgb) / 0.62);
|
|
2287
2306
|
font-size: 0.82rem;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NewsroomArticleEditor } from "../../../../components/newsroom-article-editor";
|
|
2
|
+
|
|
3
|
+
export const dynamic = "force-dynamic";
|
|
4
|
+
|
|
5
|
+
type NewsroomArticleEditorPageProps = {
|
|
6
|
+
params: Promise<{ id: string }>;
|
|
7
|
+
searchParams?: Promise<{ demo?: string | string[] }>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default async function NewsroomArticleEditorPage({ params, searchParams }: NewsroomArticleEditorPageProps) {
|
|
11
|
+
const { id } = await params;
|
|
12
|
+
const resolved = await searchParams;
|
|
13
|
+
const demoValue = Array.isArray(resolved?.demo) ? resolved?.demo[0] : resolved?.demo;
|
|
14
|
+
return <NewsroomArticleEditor articleId={decodeURIComponent(id)} demo={demoValue === "1"} />;
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NewsroomArticlesView } from "../../../components/newsroom-articles-view";
|
|
2
|
+
|
|
3
|
+
export const dynamic = "force-dynamic";
|
|
4
|
+
|
|
5
|
+
type NewsroomArticlesPageProps = {
|
|
6
|
+
searchParams?: Promise<{ demo?: string | string[] }>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default async function NewsroomArticlesPage({ searchParams }: NewsroomArticlesPageProps) {
|
|
10
|
+
const resolved = await searchParams;
|
|
11
|
+
const demoValue = Array.isArray(resolved?.demo) ? resolved?.demo[0] : resolved?.demo;
|
|
12
|
+
return <NewsroomArticlesView demo={demoValue === "1"} />;
|
|
13
|
+
}
|
package/bin/papyrus-app.mjs
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { sync } from "../src/sync.mjs";
|
|
3
3
|
|
|
4
|
-
const [command] = process.argv.slice(2);
|
|
4
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
5
5
|
if (command === "sync") {
|
|
6
6
|
sync();
|
|
7
|
+
} else if (command === "videoml-bundle") {
|
|
8
|
+
const preview = rest.includes("--preview");
|
|
9
|
+
const { buildBrowserBundle } = await import("../scripts/videoml/build-browser-bundle.mjs");
|
|
10
|
+
const { buildPreviewBundle } = await import("../scripts/videoml/build-preview-bundle.mjs");
|
|
11
|
+
try {
|
|
12
|
+
await (preview ? buildPreviewBundle() : buildBrowserBundle());
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error(error.message);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
7
17
|
} else {
|
|
8
|
-
console.error("Usage: papyrus-app sync");
|
|
18
|
+
console.error("Usage: papyrus-app sync | papyrus-app videoml-bundle [--preview]");
|
|
9
19
|
process.exit(command ? 1 : 0);
|
|
10
20
|
}
|
package/bin/papyrus-infra.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* papyrus-infra synth --site infra/site.json [--out cdk.out] [--account A] [--region R]
|
|
4
|
+
* papyrus-infra synth --account-stack github-oidc [--out cdk.out] [--account A] [--region R]
|
|
4
5
|
* papyrus-infra print-buildspec --site infra/site.json --app cms-production|cms-staging|reader
|
|
5
6
|
* Synthesizes the app-shell stack for a publication from its own site.json, or
|
|
6
7
|
* prints one of the generated Amplify build specs. Synth only: deploying is a
|
|
@@ -14,6 +15,7 @@ import { parseSiteConfig } from "../infra/site-config.js";
|
|
|
14
15
|
|
|
15
16
|
const USAGE = [
|
|
16
17
|
"Usage: papyrus-infra synth --site infra/site.json [--out cdk.out] [--account A] [--region R]",
|
|
18
|
+
" papyrus-infra synth --account-stack github-oidc [--out cdk.out] [--account A] [--region R]",
|
|
17
19
|
" papyrus-infra print-buildspec --site infra/site.json --app cms-production|cms-staging|reader",
|
|
18
20
|
].join("\n");
|
|
19
21
|
|
|
@@ -28,6 +30,30 @@ if (command !== "synth" && command !== "print-buildspec") {
|
|
|
28
30
|
process.exit(command ? 1 : 0);
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
const accountStack = opt("account-stack");
|
|
34
|
+
if (accountStack !== undefined) {
|
|
35
|
+
if (command !== "synth" || accountStack !== "github-oidc") {
|
|
36
|
+
console.error(USAGE);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
let GithubOidcProviderStack;
|
|
40
|
+
try {
|
|
41
|
+
({ GithubOidcProviderStack } = await import("../infra/index.js"));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(`papyrus-infra: ${error instanceof Error ? error.message : error}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
const { App } = createRequire(path.join(process.cwd(), "noop.js"))("aws-cdk-lib");
|
|
47
|
+
const app = new App({ outdir: path.resolve(opt("out", "cdk.out")) });
|
|
48
|
+
new GithubOidcProviderStack(app, "GithubOidcProviderStack", {
|
|
49
|
+
env: { account: opt("account", "335163751677"), region: opt("region", "us-east-1") },
|
|
50
|
+
stackName: "github-oidc-provider",
|
|
51
|
+
});
|
|
52
|
+
app.synth();
|
|
53
|
+
console.log(`synthesized github-oidc-provider -> ${path.resolve(opt("out", "cdk.out"))}`);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
31
57
|
let config;
|
|
32
58
|
try {
|
|
33
59
|
config = parseSiteConfig(JSON.parse(fs.readFileSync(path.resolve(opt("site", "infra/site.json")), "utf8")));
|
|
@@ -1,31 +1,140 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import type { ArticleVideoAsset } from "../lib/articles";
|
|
5
5
|
import { resolveThemedVideoSrc } from "../lib/themed-image";
|
|
6
6
|
import { useResolvedPapyrusTheme } from "./use-resolved-papyrus-theme";
|
|
7
|
+
import { getSiteBrand } from "../lib/site-brand";
|
|
8
|
+
import { normalizeDevPreviewDsl, type VideoScriptRef } from "../lib/video-script";
|
|
9
|
+
import { resolveVideoMode, resolveVideoModeFromEnv, type VideoMode } from "../lib/video-mode";
|
|
7
10
|
|
|
8
11
|
type ArticleVideoFigureProps = {
|
|
9
12
|
video: ArticleVideoAsset;
|
|
10
13
|
slug: string;
|
|
11
14
|
figureClassName?: string;
|
|
12
15
|
priority?: boolean;
|
|
16
|
+
videoScript?: VideoScriptRef | null;
|
|
13
17
|
};
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
const PREVIEW_STORAGE_PREFIX = "papyrus-video-preview:";
|
|
20
|
+
|
|
21
|
+
export function ArticleVideoFigure({
|
|
22
|
+
video,
|
|
23
|
+
slug,
|
|
24
|
+
figureClassName = "article-photo article-video",
|
|
25
|
+
videoScript = null,
|
|
26
|
+
}: ArticleVideoFigureProps) {
|
|
16
27
|
const theme = useResolvedPapyrusTheme();
|
|
17
28
|
const [hasHydrated, setHasHydrated] = useState(false);
|
|
29
|
+
const [videoMode, setVideoMode] = useState<VideoMode>(() => resolveVideoModeFromEnv());
|
|
30
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
31
|
+
const [currentTime, setCurrentTime] = useState(0);
|
|
32
|
+
const [duration, setDuration] = useState(0);
|
|
33
|
+
const [previewReady, setPreviewReady] = useState(false);
|
|
34
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
18
35
|
|
|
19
36
|
useEffect(() => {
|
|
20
37
|
setHasHydrated(true);
|
|
38
|
+
setVideoMode(resolveVideoMode());
|
|
21
39
|
}, []);
|
|
22
40
|
|
|
23
41
|
// Match SSR to the default dark MP4 in `video.src` until after hydration.
|
|
24
42
|
const resolvedTheme = hasHydrated ? theme : "dark";
|
|
25
43
|
const src = resolveThemedVideoSrc(video.src, video.themeVariants, resolvedTheme);
|
|
44
|
+
const usePreview = videoMode === "preview" && Boolean(videoScript?.dsl);
|
|
45
|
+
const previewDsl = videoScript?.dsl
|
|
46
|
+
? normalizeDevPreviewDsl(videoScript.dsl, getSiteBrand().video?.sceneComponents)
|
|
47
|
+
: null;
|
|
48
|
+
const previewSrc = `/videoml/preview.html?target=${encodeURIComponent(slug)}&theme=${resolvedTheme}${
|
|
49
|
+
src ? `&audio=${encodeURIComponent(src)}` : ""
|
|
50
|
+
}`;
|
|
51
|
+
|
|
52
|
+
if (usePreview && previewDsl && typeof window !== "undefined") {
|
|
53
|
+
sessionStorage.setItem(`${PREVIEW_STORAGE_PREFIX}${slug}`, previewDsl);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
setPreviewReady(false);
|
|
58
|
+
setIsPlaying(false);
|
|
59
|
+
setCurrentTime(0);
|
|
60
|
+
setDuration(0);
|
|
61
|
+
}, [previewSrc, usePreview]);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!usePreview || !previewReady) return;
|
|
65
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
66
|
+
{ kind: "papyrus-video-refresh", target: slug, theme: resolvedTheme, audio: src || undefined },
|
|
67
|
+
"*",
|
|
68
|
+
);
|
|
69
|
+
}, [previewReady, resolvedTheme, slug, src, usePreview]);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!usePreview) return;
|
|
73
|
+
const onMessage = (event: MessageEvent) => {
|
|
74
|
+
const payload = event.data;
|
|
75
|
+
if (!payload || typeof payload !== "object" || payload.kind !== "papyrus-video-time") return;
|
|
76
|
+
if (typeof payload.currentTime === "number") setCurrentTime(payload.currentTime);
|
|
77
|
+
if (typeof payload.duration === "number") setDuration(payload.duration);
|
|
78
|
+
};
|
|
79
|
+
window.addEventListener("message", onMessage);
|
|
80
|
+
return () => window.removeEventListener("message", onMessage);
|
|
81
|
+
}, [usePreview]);
|
|
82
|
+
|
|
83
|
+
const togglePreviewPlayback = useCallback(() => {
|
|
84
|
+
iframeRef.current?.contentWindow?.postMessage({ kind: isPlaying ? "papyrus-video-pause" : "papyrus-video-play" }, "*");
|
|
85
|
+
setIsPlaying(!isPlaying);
|
|
86
|
+
}, [isPlaying]);
|
|
87
|
+
|
|
88
|
+
const seekPreview = useCallback((nextTime: number) => {
|
|
89
|
+
const clamped = Math.max(0, Number.isFinite(nextTime) ? nextTime : 0);
|
|
90
|
+
iframeRef.current?.contentWindow?.postMessage({ kind: "papyrus-video-seek", time: clamped }, "*");
|
|
91
|
+
setCurrentTime(clamped);
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
if (usePreview) {
|
|
95
|
+
return (
|
|
96
|
+
<figure
|
|
97
|
+
className={`${figureClassName} article-video--preview`}
|
|
98
|
+
data-media-type="videoml-preview"
|
|
99
|
+
data-video-theme={resolvedTheme}
|
|
100
|
+
data-video-mode={videoMode}
|
|
101
|
+
>
|
|
102
|
+
{hasHydrated ? (
|
|
103
|
+
<iframe
|
|
104
|
+
key={`${slug}-${resolvedTheme}-${src}`}
|
|
105
|
+
ref={iframeRef}
|
|
106
|
+
className="article-video__preview-frame"
|
|
107
|
+
src={previewSrc}
|
|
108
|
+
title={video.alt}
|
|
109
|
+
onLoad={() => setPreviewReady(true)}
|
|
110
|
+
/>
|
|
111
|
+
) : null}
|
|
112
|
+
<div className="article-video__preview-controls">
|
|
113
|
+
<button type="button" onClick={togglePreviewPlayback} aria-pressed={isPlaying}>
|
|
114
|
+
{isPlaying ? "Pause video" : "Play video"}
|
|
115
|
+
</button>
|
|
116
|
+
<input
|
|
117
|
+
type="range"
|
|
118
|
+
min={0}
|
|
119
|
+
max={duration || 0}
|
|
120
|
+
step={0.1}
|
|
121
|
+
value={Math.min(currentTime, duration || 0)}
|
|
122
|
+
onChange={(event) => seekPreview(Number(event.target.value))}
|
|
123
|
+
aria-label="Seek video"
|
|
124
|
+
disabled={!duration}
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
{video.caption ? <figcaption>{video.caption}</figcaption> : null}
|
|
128
|
+
{video.credit ? <p className="article-video__credit">{video.credit}</p> : null}
|
|
129
|
+
<span className="sr-only" data-video-slug={slug}>
|
|
130
|
+
{video.alt}
|
|
131
|
+
</span>
|
|
132
|
+
</figure>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
26
135
|
|
|
27
136
|
return (
|
|
28
|
-
<figure className={figureClassName} data-media-type="video" data-video-theme={resolvedTheme}>
|
|
137
|
+
<figure className={figureClassName} data-media-type="video" data-video-theme={resolvedTheme} data-video-mode={videoMode}>
|
|
29
138
|
<video
|
|
30
139
|
controls
|
|
31
140
|
playsInline
|