@anthusai/papyrus 0.0.0 → 1.0.0-next.2
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/LICENSE +21 -0
- package/amplify/auth/resource.js +61 -0
- package/amplify/auth/resource.ts +65 -0
- package/amplify/data/resource.js +2037 -0
- package/amplify/data/resource.ts +2148 -0
- package/amplify/data/schema.js +1850 -0
- package/amplify/data/schema.ts +1953 -0
- package/amplify/functions/assignment-action/handler.ts +1416 -0
- package/amplify/functions/assignment-action/resource.js +8 -0
- package/amplify/functions/assignment-action/resource.ts +9 -0
- package/amplify/functions/category-action/handler.ts +2259 -0
- package/amplify/functions/category-action/resource.js +8 -0
- package/amplify/functions/category-action/resource.ts +9 -0
- package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
- package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
- package/amplify/functions/console-chat-responder/Dockerfile +19 -0
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
- package/amplify/functions/console-chat-responder/resource.js +153 -0
- package/amplify/functions/console-chat-responder/resource.ts +179 -0
- package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
- package/amplify/functions/email-submission-processor/handler.py +46 -0
- package/amplify/functions/email-submission-processor/requirements.txt +2 -0
- package/amplify/functions/email-submission-processor/resource.js +56 -0
- package/amplify/functions/email-submission-processor/resource.ts +69 -0
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
- package/amplify/functions/knowledge-query/handler.py +1 -0
- package/amplify/functions/knowledge-query/requirements.txt +3 -0
- package/amplify/functions/knowledge-query/resource.js +50 -0
- package/amplify/functions/knowledge-query/resource.ts +61 -0
- package/amplify/functions/manage-user-role/handler.ts +799 -0
- package/amplify/functions/manage-user-role/resource.js +8 -0
- package/amplify/functions/manage-user-role/resource.ts +9 -0
- package/amplify/functions/model-attachment-upload/handler.ts +387 -0
- package/amplify/functions/model-attachment-upload/resource.js +8 -0
- package/amplify/functions/model-attachment-upload/resource.ts +9 -0
- package/amplify/functions/newsroom-summary/handler.ts +321 -0
- package/amplify/functions/newsroom-summary/resource.js +8 -0
- package/amplify/functions/newsroom-summary/resource.ts +9 -0
- package/amplify/functions/procedure-action/handler.ts +621 -0
- package/amplify/functions/procedure-action/resource.js +8 -0
- package/amplify/functions/procedure-action/resource.ts +9 -0
- package/amplify/functions/reader-settings/handler.ts +322 -0
- package/amplify/functions/reader-settings/resource.js +8 -0
- package/amplify/functions/reader-settings/resource.ts +9 -0
- package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
- package/amplify/functions/ses-inbound-receive/resource.js +8 -0
- package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
- package/amplify/functions/shared/amplify-secrets.ts +65 -0
- package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
- package/amplify/functions/shared/email-mime-intake.ts +196 -0
- package/amplify/functions/shared/email-submission.ts +415 -0
- package/amplify/functions/shared/inbound-email-intake.ts +37 -0
- package/amplify/functions/shared/lambda-data-client.ts +21 -0
- package/amplify/functions/shared/model-payloads.ts +186 -0
- package/amplify/functions/shared/python-bundle.js +115 -0
- package/amplify/functions/shared/python-bundle.ts +141 -0
- package/amplify/functions/shared/slack-events.ts +111 -0
- package/amplify/functions/slack-delivery/handler.py +112 -0
- package/amplify/functions/slack-delivery/requirements.txt +2 -0
- package/amplify/functions/slack-delivery/resource.js +38 -0
- package/amplify/functions/slack-delivery/resource.ts +51 -0
- package/amplify/functions/slack-events/handler.ts +155 -0
- package/amplify/functions/slack-events/resource.js +11 -0
- package/amplify/functions/slack-events/resource.ts +12 -0
- package/amplify/inbound-email/stack.js +46 -0
- package/amplify/inbound-email/stack.ts +61 -0
- package/amplify/package.json +3 -0
- package/amplify/site-backend-config.js +1 -0
- package/amplify/site-backend-config.ts +30 -0
- package/amplify/site-backend.js +587 -0
- package/amplify/site-backend.ts +704 -0
- package/amplify/storage/resource.js +18 -0
- package/amplify/storage/resource.ts +19 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
- package/app/[year]/[month]/[day]/page.tsx +22 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
- package/app/api/archive/editions/route.ts +18 -0
- package/app/api/media/[...path]/route.ts +35 -0
- package/app/api/revalidate/route.ts +94 -0
- package/app/archive/page.tsx +24 -0
- package/app/articles/[slug]/page.tsx +35 -0
- package/app/globals.css +10329 -0
- package/app/layout.tsx +146 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
- package/app/newsroom/[section]/page.tsx +47 -0
- package/app/newsroom/forum/[threadId]/page.tsx +35 -0
- package/app/newsroom/insights/[threadId]/page.tsx +22 -0
- package/app/newsroom/insights/page.tsx +14 -0
- package/app/newsroom/layout.tsx +12 -0
- package/app/newsroom/page.tsx +31 -0
- package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
- package/app/page.tsx +272 -0
- package/app/settings/page.tsx +14 -0
- package/app/tailwind.css +65 -0
- package/bin/papyrus-app.mjs +10 -0
- package/bin/papyrus-infra.mjs +39 -0
- package/components/ai-elements/conversation.tsx +168 -0
- package/components/ai-elements/message.tsx +360 -0
- package/components/ai-elements/model-selector.tsx +30 -0
- package/components/ai-elements/prompt-input.tsx +1461 -0
- package/components/ai-elements/shimmer.tsx +76 -0
- package/components/ai-elements/suggestion.tsx +54 -0
- package/components/ai-elements/tool.tsx +173 -0
- package/components/amplify-client-provider.tsx +140 -0
- package/components/archive-grid.tsx +89 -0
- package/components/archive-shell.tsx +41 -0
- package/components/article-video.tsx +47 -0
- package/components/blog-page-background.tsx +20 -0
- package/components/dev-sandbox-editor-sign-in.tsx +60 -0
- package/components/news-desk-client-provider.tsx +211 -0
- package/components/news-desk-page.tsx +119 -0
- package/components/news-desk-taxonomy-client.ts +3684 -0
- package/components/newspaper.tsx +1929 -0
- package/components/newsroom-assignments-view.tsx +664 -0
- package/components/newsroom-ops-overview.tsx +226 -0
- package/components/newsroom-ops-shell.tsx +319 -0
- package/components/newsroom-references-view.tsx +598 -0
- package/components/newsroom-topics-view.tsx +425 -0
- package/components/papyrus-console-shell.tsx +1912 -0
- package/components/pictogram-figure.tsx +65 -0
- package/components/presentation-footer.tsx +146 -0
- package/components/reader-auth-control.tsx +162 -0
- package/components/reader-auth-state.ts +122 -0
- package/components/reader-settings.ts +332 -0
- package/components/reader-theme-control.tsx +85 -0
- package/components/reference-source-preview.tsx +163 -0
- package/components/settings-page.tsx +239 -0
- package/components/topic-steering-workspace.tsx +18063 -0
- package/components/ui/badge.tsx +30 -0
- package/components/ui/button-group.tsx +87 -0
- package/components/ui/button.tsx +58 -0
- package/components/ui/card.tsx +32 -0
- package/components/ui/command.tsx +194 -0
- package/components/ui/dialog.tsx +158 -0
- package/components/ui/dropdown-menu.tsx +266 -0
- package/components/ui/hover-card.tsx +51 -0
- package/components/ui/input-group.tsx +158 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/scroll-area.tsx +16 -0
- package/components/ui/select.tsx +199 -0
- package/components/ui/separator.tsx +23 -0
- package/components/ui/sheet.tsx +92 -0
- package/components/ui/spinner.tsx +10 -0
- package/components/ui/tabs.tsx +113 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components/use-resolved-papyrus-theme.ts +47 -0
- package/infra/amplify-app-shell.js +109 -0
- package/infra/amplify-app-shell.ts +155 -0
- package/infra/index.js +12 -0
- package/lib/amplify-outputs-guard.ts +24 -0
- package/lib/amplify-outputs-path.ts +16 -0
- package/lib/amplify-server-runtime.ts +63 -0
- package/lib/archive-data.ts +30 -0
- package/lib/archive-types.ts +15 -0
- package/lib/articles.ts +320 -0
- package/lib/cached-content-repository.ts +89 -0
- package/lib/category-dashboard.ts +37 -0
- package/lib/category-repository.ts +1907 -0
- package/lib/console-chat-client.ts +1316 -0
- package/lib/console-web-context.ts +118 -0
- package/lib/content-load-timing.ts +23 -0
- package/lib/content-repository.ts +40 -0
- package/lib/content-types.ts +104 -0
- package/lib/define-site.js +3 -0
- package/lib/define-site.ts +21 -0
- package/lib/demo-amplify-outputs.ts +15 -0
- package/lib/dev-sandbox-editor-auth.ts +18 -0
- package/lib/doctrine.ts +128 -0
- package/lib/edition-routes.ts +155 -0
- package/lib/edition-sections.ts +111 -0
- package/lib/empty-edition-layout-plan.ts +109 -0
- package/lib/excerpts.ts +8 -0
- package/lib/graphql-content-repository.ts +986 -0
- package/lib/image-url.ts +10 -0
- package/lib/layout-plan.ts +981 -0
- package/lib/layout-scenarios.ts +564 -0
- package/lib/markus-body.ts +168 -0
- package/lib/markus-ir.ts +235 -0
- package/lib/markus-projection.ts +243 -0
- package/lib/markus-to-article.ts +93 -0
- package/lib/news-desk-session.ts +160 -0
- package/lib/newspaper-layout.ts +3459 -0
- package/lib/newsroom-assignments.ts +173 -0
- package/lib/newsroom-base-path.ts +48 -0
- package/lib/newsroom-card-layout.ts +102 -0
- package/lib/newsroom-category-drilldown.ts +133 -0
- package/lib/newsroom-demo-dashboard.ts +267 -0
- package/lib/newsroom-demo-profile.ts +46 -0
- package/lib/newsroom-forum-routes.ts +153 -0
- package/lib/newsroom-index-filters.ts +202 -0
- package/lib/newsroom-knowledge-query-request.ts +51 -0
- package/lib/newsroom-list-selection.ts +42 -0
- package/lib/newsroom-nav.ts +42 -0
- package/lib/newsroom-references.ts +116 -0
- package/lib/newsroom-sections.ts +52 -0
- package/lib/newsroom-topics.ts +118 -0
- package/lib/papyrus-object-kinds.ts +24 -0
- package/lib/papyrus-web-locations.ts +480 -0
- package/lib/pilobol-sample.ts +167 -0
- package/lib/presentation-footer.ts +92 -0
- package/lib/pretext-layout.ts +179 -0
- package/lib/public-placeholder-config.ts +102 -0
- package/lib/publication-items.ts +181 -0
- package/lib/reader-route-config.ts +1 -0
- package/lib/reader-static-params.ts +56 -0
- package/lib/reader-storage-url.ts +63 -0
- package/lib/reference-display.ts +74 -0
- package/lib/reference-policy.ts +97 -0
- package/lib/reference-source-preview.ts +175 -0
- package/lib/renderer-config.ts +50 -0
- package/lib/renderer.ts +87 -0
- package/lib/reporting-story-budget.ts +638 -0
- package/lib/semantic-graph.ts +554 -0
- package/lib/site-brand-tenant.ts +41 -0
- package/lib/site-brand.ts +179 -0
- package/lib/site-renderer.ts +17 -0
- package/lib/site-stack.ts +195 -0
- package/lib/themed-image.ts +30 -0
- package/lib/utils.ts +6 -0
- package/middleware.ts +78 -0
- package/package.json +296 -1
- package/publications/anth_us/brand.ts +68 -0
- package/publications/anth_us/theme.css +66 -0
- package/publications/papyrus/theme.css +11 -0
- package/publications/pilobol_us/brand.ts +37 -0
- package/publications/pilobol_us/theme.css +140 -0
- package/publications/threat_intelligence/blog-defense/graph.ts +636 -0
- package/publications/threat_intelligence/blog-defense/layout.ts +934 -0
- package/publications/threat_intelligence/blog-defense/page-background.tsx +590 -0
- package/publications/threat_intelligence/brand.ts +40 -0
- package/publications/threat_intelligence/pictograms/art.tsx +608 -0
- package/publications/threat_intelligence/pictograms/figure.tsx +82 -0
- package/publications/threat_intelligence/pictograms/registry.ts +30 -0
- package/publications/threat_intelligence/pictograms/system.tsx +305 -0
- package/publications/threat_intelligence/seed/seed-edition-content.json +629 -0
- package/publications/threat_intelligence/theme.css +1440 -0
- package/renderers/markus/index.ts +25 -0
- package/renderers/markus/stubs.tsx +33 -0
- package/renderers/pretext/article-page.tsx +174 -0
- package/renderers/pretext/index.ts +30 -0
- package/renderers/pretext/layouts.ts +16 -0
- package/renderers/pretext/presentation-shell.tsx +477 -0
- package/routes.manifest.json +182 -0
- package/scripts/fixtures/pilobol-sample.ast.json +234 -0
- package/src/backend.js +1 -0
- package/src/backend.ts +3 -0
- package/src/sync.mjs +87 -0
- package/src/with-papyrus.d.mts +2 -0
- package/src/with-papyrus.mjs +47 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { generateClient } from "aws-amplify/data";
|
|
4
|
+
import type { Schema } from "../amplify/data/resource";
|
|
5
|
+
import type { EditionPresentationFormat } from "../lib/content-types";
|
|
6
|
+
import { enforcePresentation, getDefaultPretextLayout, getPresentationChoices } from "../lib/site-brand";
|
|
7
|
+
import { configureAmplifyClient } from "./amplify-client-provider";
|
|
8
|
+
import { isUnauthenticatedError, loadReaderSessionSnapshot } from "./reader-auth-state";
|
|
9
|
+
|
|
10
|
+
export type ReaderThemeSetting = "system" | "light" | "dark";
|
|
11
|
+
export type ReaderMotionSetting = "standard" | "low";
|
|
12
|
+
|
|
13
|
+
export type ReaderSettings = {
|
|
14
|
+
presentation: EditionPresentationFormat;
|
|
15
|
+
theme: ReaderThemeSetting;
|
|
16
|
+
motion: ReaderMotionSetting;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type ReaderSettingsSource = "local" | "cloud";
|
|
20
|
+
|
|
21
|
+
export type ReaderSettingsResolution = {
|
|
22
|
+
settings: ReaderSettings;
|
|
23
|
+
source: ReaderSettingsSource;
|
|
24
|
+
signedIn: boolean;
|
|
25
|
+
email?: string | null;
|
|
26
|
+
userProfileId?: string | null;
|
|
27
|
+
avatarHash?: string | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type DataClientErrors = Array<{ message?: string | null } | string | null> | null | undefined;
|
|
31
|
+
type ReaderSettingsResult = {
|
|
32
|
+
email?: string | null;
|
|
33
|
+
userProfileId?: string | null;
|
|
34
|
+
avatarHash?: string | null;
|
|
35
|
+
settings?: ReaderSettings | null;
|
|
36
|
+
};
|
|
37
|
+
type RawReaderSettingsResult = {
|
|
38
|
+
email?: string | null;
|
|
39
|
+
userProfileId?: string | null;
|
|
40
|
+
avatarHash?: string | null;
|
|
41
|
+
settings?: unknown;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const USER_POOL_AUTH_MODE = "userPool";
|
|
45
|
+
const SETTINGS_STORAGE_KEY = "papyrus:reader-settings";
|
|
46
|
+
const LEGACY_PRESENTATION_STORAGE_KEY = "papyrus:presentation";
|
|
47
|
+
const LEGACY_PRESENTATION_COOKIE = "papyrus-presentation";
|
|
48
|
+
const LEGACY_COOKIE_CLEANUP_STORAGE_KEY = "papyrus:legacy-presentation-cookie-cleaned";
|
|
49
|
+
const SETTINGS_EVENT = "papyrus:settings-changed";
|
|
50
|
+
|
|
51
|
+
export const DEFAULT_READER_SETTINGS: ReaderSettings = {
|
|
52
|
+
presentation: enforcePresentation(getDefaultPretextLayout()),
|
|
53
|
+
theme: "system",
|
|
54
|
+
motion: "standard",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const ALL_PRESENTATION_OPTIONS: Array<{
|
|
58
|
+
value: EditionPresentationFormat;
|
|
59
|
+
label: string;
|
|
60
|
+
description: string;
|
|
61
|
+
}> = [
|
|
62
|
+
{
|
|
63
|
+
value: "newsprint",
|
|
64
|
+
label: "Newspaper",
|
|
65
|
+
description: "Planned pages, feature headlines, and exact continuations.",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
value: "blog",
|
|
69
|
+
label: "Blog",
|
|
70
|
+
description: "Continuous sections with measured Pretext text and no page jumps.",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
value: "magazine",
|
|
74
|
+
label: "Magazine",
|
|
75
|
+
description: "Feature-led section spreads using the same edition items.",
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const ALLOWED_PRESENTATION_CHOICES = new Set<EditionPresentationFormat>(getPresentationChoices());
|
|
80
|
+
|
|
81
|
+
export const PRESENTATION_OPTIONS = ALL_PRESENTATION_OPTIONS.filter((option) =>
|
|
82
|
+
ALLOWED_PRESENTATION_CHOICES.has(option.value),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
export const THEME_OPTIONS: Array<{
|
|
86
|
+
value: ReaderThemeSetting;
|
|
87
|
+
label: string;
|
|
88
|
+
}> = [
|
|
89
|
+
{ value: "system", label: "System" },
|
|
90
|
+
{ value: "light", label: "Light" },
|
|
91
|
+
{ value: "dark", label: "Dark" },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
export const MOTION_OPTIONS: Array<{
|
|
95
|
+
value: ReaderMotionSetting;
|
|
96
|
+
label: string;
|
|
97
|
+
}> = [
|
|
98
|
+
{ value: "standard", label: "Standard" },
|
|
99
|
+
{ value: "low", label: "Low Motion" },
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
export function readLocalReaderSettings(): ReaderSettings {
|
|
103
|
+
if (typeof window === "undefined") return DEFAULT_READER_SETTINGS;
|
|
104
|
+
cleanupLegacyPresentationCookie();
|
|
105
|
+
const parsed = parseStoredSettings(window.localStorage.getItem(SETTINGS_STORAGE_KEY));
|
|
106
|
+
const legacyPresentation = readPresentationValue(window.localStorage.getItem(LEGACY_PRESENTATION_STORAGE_KEY));
|
|
107
|
+
return normalizeReaderSettings({
|
|
108
|
+
...parsed,
|
|
109
|
+
presentation: parsed?.presentation ?? legacyPresentation ?? DEFAULT_READER_SETTINGS.presentation,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function writeLocalReaderSettings(settings: ReaderSettings) {
|
|
114
|
+
if (typeof window === "undefined") return;
|
|
115
|
+
cleanupLegacyPresentationCookie();
|
|
116
|
+
const normalized = normalizeReaderSettings(settings);
|
|
117
|
+
window.localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(normalized));
|
|
118
|
+
window.localStorage.setItem(LEGACY_PRESENTATION_STORAGE_KEY, normalized.presentation);
|
|
119
|
+
applyReaderTheme(normalized.theme);
|
|
120
|
+
window.dispatchEvent(new CustomEvent(SETTINGS_EVENT, { detail: normalized }));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function applyReaderTheme(theme: ReaderThemeSetting) {
|
|
124
|
+
if (typeof document === "undefined") return;
|
|
125
|
+
document.documentElement.dataset.papyrusTheme = theme;
|
|
126
|
+
applyReaderThemeClass(theme);
|
|
127
|
+
updateThemeMeta(theme);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function subscribeReaderSettingsChanges(listener: (settings: ReaderSettings) => void): () => void {
|
|
131
|
+
if (typeof window === "undefined") return () => {};
|
|
132
|
+
const notify = () => listener(readLocalReaderSettings());
|
|
133
|
+
const handleSettingsEvent = (event: Event) => {
|
|
134
|
+
const detail = "detail" in event ? (event as CustomEvent<unknown>).detail : null;
|
|
135
|
+
listener(normalizeReaderSettings(detail));
|
|
136
|
+
};
|
|
137
|
+
const handleStorageEvent = (event: StorageEvent) => {
|
|
138
|
+
if (event.key === SETTINGS_STORAGE_KEY || event.key === LEGACY_PRESENTATION_STORAGE_KEY) notify();
|
|
139
|
+
};
|
|
140
|
+
window.addEventListener(SETTINGS_EVENT, handleSettingsEvent);
|
|
141
|
+
window.addEventListener("storage", handleStorageEvent);
|
|
142
|
+
return () => {
|
|
143
|
+
window.removeEventListener(SETTINGS_EVENT, handleSettingsEvent);
|
|
144
|
+
window.removeEventListener("storage", handleStorageEvent);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function resolveReaderSettings(): Promise<ReaderSettingsResolution> {
|
|
149
|
+
const localSettings = readLocalReaderSettings();
|
|
150
|
+
writeLocalReaderSettings(localSettings);
|
|
151
|
+
const session = await loadReaderSessionSnapshot();
|
|
152
|
+
if (!session.hasSession) {
|
|
153
|
+
return { settings: localSettings, source: "local", signedIn: false };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const remote = await getRemoteReaderSettings();
|
|
158
|
+
if (remote.settings) {
|
|
159
|
+
writeLocalReaderSettings(remote.settings);
|
|
160
|
+
return {
|
|
161
|
+
settings: remote.settings,
|
|
162
|
+
source: "cloud",
|
|
163
|
+
signedIn: true,
|
|
164
|
+
email: remote.email,
|
|
165
|
+
userProfileId: remote.userProfileId,
|
|
166
|
+
avatarHash: remote.avatarHash,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const saved = await saveRemoteReaderSettings(localSettings);
|
|
170
|
+
return {
|
|
171
|
+
settings: saved.settings ?? localSettings,
|
|
172
|
+
source: "cloud",
|
|
173
|
+
signedIn: true,
|
|
174
|
+
email: saved.email,
|
|
175
|
+
userProfileId: saved.userProfileId,
|
|
176
|
+
avatarHash: saved.avatarHash,
|
|
177
|
+
};
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (isUnauthenticatedError(error)) {
|
|
180
|
+
return { settings: localSettings, source: "local", signedIn: false };
|
|
181
|
+
}
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function saveReaderSettings(settings: ReaderSettings): Promise<ReaderSettingsResolution> {
|
|
187
|
+
const normalized = normalizeReaderSettings(settings);
|
|
188
|
+
writeLocalReaderSettings(normalized);
|
|
189
|
+
const session = await loadReaderSessionSnapshot();
|
|
190
|
+
if (!session.hasSession) {
|
|
191
|
+
return { settings: normalized, source: "local", signedIn: false };
|
|
192
|
+
}
|
|
193
|
+
const saved = await saveRemoteReaderSettings(normalized);
|
|
194
|
+
const savedSettings = saved.settings ?? normalized;
|
|
195
|
+
writeLocalReaderSettings(savedSettings);
|
|
196
|
+
return {
|
|
197
|
+
settings: savedSettings,
|
|
198
|
+
source: "cloud",
|
|
199
|
+
signedIn: true,
|
|
200
|
+
email: saved.email,
|
|
201
|
+
userProfileId: saved.userProfileId,
|
|
202
|
+
avatarHash: saved.avatarHash,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function getRemoteReaderSettings(): Promise<ReaderSettingsResult> {
|
|
207
|
+
configureAmplifyClient();
|
|
208
|
+
const client = generateClient<Schema>() as unknown as {
|
|
209
|
+
queries: {
|
|
210
|
+
getReaderSettings(options: { authMode: typeof USER_POOL_AUTH_MODE }): Promise<{
|
|
211
|
+
data?: RawReaderSettingsResult | null;
|
|
212
|
+
errors?: DataClientErrors;
|
|
213
|
+
}>;
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
const response = await client.queries.getReaderSettings({ authMode: USER_POOL_AUTH_MODE });
|
|
217
|
+
assertNoDataErrors(response.errors, "get reader settings");
|
|
218
|
+
return normalizeReaderSettingsResult(response.data);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function saveRemoteReaderSettings(settings: ReaderSettings): Promise<ReaderSettingsResult> {
|
|
222
|
+
configureAmplifyClient();
|
|
223
|
+
const client = generateClient<Schema>() as unknown as {
|
|
224
|
+
mutations: {
|
|
225
|
+
updateReaderSettings(
|
|
226
|
+
input: { settings: ReaderSettings },
|
|
227
|
+
options: { authMode: typeof USER_POOL_AUTH_MODE },
|
|
228
|
+
): Promise<{
|
|
229
|
+
data?: RawReaderSettingsResult | null;
|
|
230
|
+
errors?: DataClientErrors;
|
|
231
|
+
}>;
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
const response = await client.mutations.updateReaderSettings({ settings }, { authMode: USER_POOL_AUTH_MODE });
|
|
235
|
+
assertNoDataErrors(response.errors, "update reader settings");
|
|
236
|
+
return normalizeReaderSettingsResult(response.data);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function normalizeReaderSettingsResult(value: RawReaderSettingsResult | null | undefined): ReaderSettingsResult {
|
|
240
|
+
if (!value) return {};
|
|
241
|
+
return {
|
|
242
|
+
email: typeof value.email === "string" && value.email.trim() ? value.email.trim() : null,
|
|
243
|
+
userProfileId: value.userProfileId ?? null,
|
|
244
|
+
avatarHash: typeof value.avatarHash === "string" && value.avatarHash.trim() ? value.avatarHash.trim() : null,
|
|
245
|
+
settings: value.settings ? normalizeReaderSettings(value.settings) : null,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function normalizeReaderSettings(value: unknown): ReaderSettings {
|
|
250
|
+
const candidate = value && typeof value === "object" ? value as Record<string, unknown> : {};
|
|
251
|
+
return {
|
|
252
|
+
presentation: normalizePresentation(candidate.presentation),
|
|
253
|
+
theme: normalizeTheme(candidate.theme),
|
|
254
|
+
motion: normalizeMotion(candidate.motion),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function parseStoredSettings(value: string | null): Partial<ReaderSettings> | null {
|
|
259
|
+
if (!value) return null;
|
|
260
|
+
try {
|
|
261
|
+
const parsed = JSON.parse(value);
|
|
262
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
263
|
+
return parsed as Partial<ReaderSettings>;
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function normalizePresentation(value: unknown): EditionPresentationFormat {
|
|
270
|
+
const normalized =
|
|
271
|
+
value === "blog" || value === "magazine" || value === "newsprint"
|
|
272
|
+
? value
|
|
273
|
+
: DEFAULT_READER_SETTINGS.presentation;
|
|
274
|
+
return enforcePresentation(normalized);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function normalizeTheme(value: unknown): ReaderThemeSetting {
|
|
278
|
+
return value === "light" || value === "dark" || value === "system" ? value : DEFAULT_READER_SETTINGS.theme;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function normalizeMotion(value: unknown): ReaderMotionSetting {
|
|
282
|
+
return value === "low" || value === "standard" ? value : DEFAULT_READER_SETTINGS.motion;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function readPresentationValue(value: string | null | undefined): EditionPresentationFormat | null {
|
|
286
|
+
if (value !== "newsprint" && value !== "blog" && value !== "magazine") return null;
|
|
287
|
+
return enforcePresentation(value);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function cleanupLegacyPresentationCookie() {
|
|
291
|
+
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
292
|
+
if (window.localStorage.getItem(LEGACY_COOKIE_CLEANUP_STORAGE_KEY) === "true") return;
|
|
293
|
+
document.cookie = `${LEGACY_PRESENTATION_COOKIE}=; path=/; max-age=0; samesite=lax`;
|
|
294
|
+
window.localStorage.setItem(LEGACY_COOKIE_CLEANUP_STORAGE_KEY, "true");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function updateThemeMeta(theme: ReaderThemeSetting) {
|
|
298
|
+
const colorScheme = theme === "dark" ? "dark" : theme === "light" ? "light" : "light dark";
|
|
299
|
+
let meta = document.querySelector<HTMLMetaElement>('meta[name="color-scheme"][data-papyrus-theme-meta="true"]');
|
|
300
|
+
if (!meta) {
|
|
301
|
+
meta = document.createElement("meta");
|
|
302
|
+
meta.name = "color-scheme";
|
|
303
|
+
meta.setAttribute("data-papyrus-theme-meta", "true");
|
|
304
|
+
document.head.appendChild(meta);
|
|
305
|
+
}
|
|
306
|
+
meta.content = colorScheme;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function applyReaderThemeClass(theme: ReaderThemeSetting) {
|
|
310
|
+
if (typeof document === "undefined") return;
|
|
311
|
+
const isDark = resolveReaderThemeIsDark(theme);
|
|
312
|
+
document.documentElement.classList.toggle("dark", isDark);
|
|
313
|
+
document.documentElement.classList.toggle("dark-theme", isDark);
|
|
314
|
+
document.documentElement.classList.toggle("light", !isDark);
|
|
315
|
+
document.documentElement.classList.toggle("light-theme", !isDark);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function resolveReaderThemeIsDark(theme: ReaderThemeSetting) {
|
|
319
|
+
if (theme === "dark") return true;
|
|
320
|
+
if (theme === "light") return false;
|
|
321
|
+
return typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function assertNoDataErrors(errors: DataClientErrors, operation: string): void {
|
|
325
|
+
if (!errors?.length) return;
|
|
326
|
+
throw new Error(`${operation} failed: ${errors.map(formatDataError).join("; ")}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function formatDataError(error: { message?: string | null } | string | null): string {
|
|
330
|
+
if (typeof error === "string") return error;
|
|
331
|
+
return error?.message ?? "GraphQL data operation failed.";
|
|
332
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
|
|
4
|
+
import { useCallback, useEffect, useState } from "react";
|
|
5
|
+
import {
|
|
6
|
+
THEME_OPTIONS,
|
|
7
|
+
applyReaderTheme,
|
|
8
|
+
readLocalReaderSettings,
|
|
9
|
+
saveReaderSettings,
|
|
10
|
+
subscribeReaderSettingsChanges,
|
|
11
|
+
type ReaderThemeSetting,
|
|
12
|
+
} from "./reader-settings";
|
|
13
|
+
|
|
14
|
+
type ReaderThemeControlProps = {
|
|
15
|
+
className?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const THEME_ICON_BY_VALUE: Record<ReaderThemeSetting, typeof MonitorIcon> = {
|
|
19
|
+
system: MonitorIcon,
|
|
20
|
+
light: SunIcon,
|
|
21
|
+
dark: MoonIcon,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const THEME_LABEL_BY_VALUE: Record<ReaderThemeSetting, string> = {
|
|
25
|
+
system: "Use system theme",
|
|
26
|
+
light: "Use light theme",
|
|
27
|
+
dark: "Use dark theme",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function ReaderThemeControl({ className }: ReaderThemeControlProps) {
|
|
31
|
+
const [theme, setTheme] = useState<ReaderThemeSetting>("system");
|
|
32
|
+
const [pendingTheme, setPendingTheme] = useState<ReaderThemeSetting | null>(null);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const localSettings = readLocalReaderSettings();
|
|
36
|
+
setTheme(localSettings.theme);
|
|
37
|
+
applyReaderTheme(localSettings.theme);
|
|
38
|
+
return subscribeReaderSettingsChanges((settings) => {
|
|
39
|
+
setTheme(settings.theme);
|
|
40
|
+
});
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const updateTheme = useCallback(async (nextTheme: ReaderThemeSetting) => {
|
|
44
|
+
const nextSettings = { ...readLocalReaderSettings(), theme: nextTheme };
|
|
45
|
+
setTheme(nextTheme);
|
|
46
|
+
setPendingTheme(nextTheme);
|
|
47
|
+
applyReaderTheme(nextTheme);
|
|
48
|
+
try {
|
|
49
|
+
const resolution = await saveReaderSettings(nextSettings);
|
|
50
|
+
setTheme(resolution.settings.theme);
|
|
51
|
+
} catch {
|
|
52
|
+
setTheme(nextTheme);
|
|
53
|
+
} finally {
|
|
54
|
+
setPendingTheme(null);
|
|
55
|
+
}
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
const rootClassName = className ? `reader-theme-control ${className}` : "reader-theme-control";
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div aria-label="Theme mode" className={rootClassName} role="radiogroup">
|
|
62
|
+
{THEME_OPTIONS.map((option) => {
|
|
63
|
+
const selected = theme === option.value;
|
|
64
|
+
const ThemeIcon = THEME_ICON_BY_VALUE[option.value];
|
|
65
|
+
const label = THEME_LABEL_BY_VALUE[option.value];
|
|
66
|
+
return (
|
|
67
|
+
<button
|
|
68
|
+
aria-checked={selected}
|
|
69
|
+
aria-label={label}
|
|
70
|
+
className="reader-theme-control__button"
|
|
71
|
+
data-selected={selected ? "true" : "false"}
|
|
72
|
+
disabled={pendingTheme !== null}
|
|
73
|
+
key={option.value}
|
|
74
|
+
onClick={() => void updateTheme(option.value)}
|
|
75
|
+
role="radio"
|
|
76
|
+
title={label}
|
|
77
|
+
type="button"
|
|
78
|
+
>
|
|
79
|
+
<ThemeIcon aria-hidden="true" className="reader-theme-control__icon" focusable="false" />
|
|
80
|
+
</button>
|
|
81
|
+
);
|
|
82
|
+
})}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { FullscreenIcon, XIcon } from "lucide-react";
|
|
4
|
+
import { useEffect, useId, useState } from "react";
|
|
5
|
+
import type { ReferenceAttachmentRecord } from "../lib/category-repository";
|
|
6
|
+
import {
|
|
7
|
+
isPdfAttachment,
|
|
8
|
+
resolveReferenceSourcePreview,
|
|
9
|
+
type ReferenceSourcePreview,
|
|
10
|
+
} from "../lib/reference-source-preview";
|
|
11
|
+
|
|
12
|
+
function PdfSourcePreview({ preview }: { preview: Extract<ReferenceSourcePreview, { kind: "pdf" }> }) {
|
|
13
|
+
const viewportTitleId = useId();
|
|
14
|
+
const [expanded, setExpanded] = useState(false);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!expanded) return undefined;
|
|
18
|
+
const previousOverflow = document.body.style.overflow;
|
|
19
|
+
document.body.style.overflow = "hidden";
|
|
20
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
21
|
+
if (event.key === "Escape") setExpanded(false);
|
|
22
|
+
};
|
|
23
|
+
window.addEventListener("keydown", onKeyDown);
|
|
24
|
+
return () => {
|
|
25
|
+
document.body.style.overflow = previousOverflow;
|
|
26
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
27
|
+
};
|
|
28
|
+
}, [expanded]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
<div
|
|
33
|
+
className="news-desk-reference-source-preview news-desk-reference-source-preview--pdf"
|
|
34
|
+
data-news-desk-reference-source-preview="pdf"
|
|
35
|
+
>
|
|
36
|
+
<div className="news-desk-reference-source-preview__pdf-card">
|
|
37
|
+
<button
|
|
38
|
+
aria-label="Expand PDF preview"
|
|
39
|
+
className="news-desk-reference-source-preview__pdf-expand"
|
|
40
|
+
onClick={() => setExpanded(true)}
|
|
41
|
+
type="button"
|
|
42
|
+
>
|
|
43
|
+
<FullscreenIcon aria-hidden size={18} strokeWidth={2} />
|
|
44
|
+
</button>
|
|
45
|
+
<div className="news-desk-reference-source-preview__frame news-desk-reference-source-preview__frame--pdf">
|
|
46
|
+
<iframe
|
|
47
|
+
loading="lazy"
|
|
48
|
+
referrerPolicy="strict-origin-when-cross-origin"
|
|
49
|
+
src={preview.embedUrl}
|
|
50
|
+
tabIndex={-1}
|
|
51
|
+
title={preview.label}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
{expanded ? (
|
|
57
|
+
<div
|
|
58
|
+
aria-labelledby={viewportTitleId}
|
|
59
|
+
aria-modal="true"
|
|
60
|
+
className="news-desk-reference-source-preview__viewport"
|
|
61
|
+
data-news-desk-reference-source-preview-viewport="open"
|
|
62
|
+
role="dialog"
|
|
63
|
+
>
|
|
64
|
+
<header className="news-desk-reference-source-preview__viewport-strip">
|
|
65
|
+
<span className="sr-only" id={viewportTitleId}>
|
|
66
|
+
{preview.label}
|
|
67
|
+
</span>
|
|
68
|
+
<button
|
|
69
|
+
aria-label="Close expanded PDF preview"
|
|
70
|
+
className="news-desk-reference-source-preview__viewport-close"
|
|
71
|
+
onClick={() => setExpanded(false)}
|
|
72
|
+
type="button"
|
|
73
|
+
>
|
|
74
|
+
<XIcon aria-hidden size={18} strokeWidth={2} />
|
|
75
|
+
</button>
|
|
76
|
+
</header>
|
|
77
|
+
<div className="news-desk-reference-source-preview__viewport-frame">
|
|
78
|
+
<iframe
|
|
79
|
+
referrerPolicy="strict-origin-when-cross-origin"
|
|
80
|
+
src={preview.embedUrl}
|
|
81
|
+
title={`${preview.label} (expanded)`}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
) : null}
|
|
86
|
+
</>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function ReferenceSourcePreview({
|
|
91
|
+
attachments = [],
|
|
92
|
+
sourceUri,
|
|
93
|
+
}: {
|
|
94
|
+
attachments?: ReferenceAttachmentRecord[];
|
|
95
|
+
sourceUri?: string | null;
|
|
96
|
+
}) {
|
|
97
|
+
const preview = resolveReferenceSourcePreview(sourceUri, attachments);
|
|
98
|
+
const pendingPdfPreview = !preview && attachments.some(
|
|
99
|
+
(attachment) => isPdfAttachment(attachment) && Boolean(attachment.storagePath),
|
|
100
|
+
);
|
|
101
|
+
if (!preview) {
|
|
102
|
+
if (!pendingPdfPreview) return null;
|
|
103
|
+
return (
|
|
104
|
+
<p
|
|
105
|
+
className="news-desk-reference-source-preview__hint news-desk-reference-source-preview__hint--pdf-loading"
|
|
106
|
+
data-news-desk-reference-source-preview="pdf-loading"
|
|
107
|
+
>
|
|
108
|
+
Loading PDF preview…
|
|
109
|
+
</p>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (preview.kind === "youtube") {
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
className="news-desk-reference-source-preview news-desk-reference-source-preview--youtube"
|
|
117
|
+
data-news-desk-reference-source-preview="youtube"
|
|
118
|
+
>
|
|
119
|
+
<div className="news-desk-reference-source-preview__frame">
|
|
120
|
+
<iframe
|
|
121
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
122
|
+
allowFullScreen
|
|
123
|
+
loading="lazy"
|
|
124
|
+
referrerPolicy="strict-origin-when-cross-origin"
|
|
125
|
+
src={preview.embedUrl}
|
|
126
|
+
title={`YouTube preview ${preview.videoId}`}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
<p className="news-desk-reference-source-preview__actions">
|
|
130
|
+
<a href={preview.watchUrl} rel="noopener noreferrer" target="_blank">
|
|
131
|
+
Open on YouTube
|
|
132
|
+
</a>
|
|
133
|
+
</p>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (preview.kind === "pdf") {
|
|
139
|
+
return <PdfSourcePreview preview={preview} />;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div
|
|
144
|
+
className="news-desk-reference-source-preview news-desk-reference-source-preview--html"
|
|
145
|
+
data-news-desk-reference-source-preview="html"
|
|
146
|
+
>
|
|
147
|
+
<div className="news-desk-reference-source-preview__frame">
|
|
148
|
+
<iframe
|
|
149
|
+
loading="lazy"
|
|
150
|
+
referrerPolicy="strict-origin-when-cross-origin"
|
|
151
|
+
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
|
|
152
|
+
src={preview.href}
|
|
153
|
+
title={preview.label}
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
<p className="news-desk-reference-source-preview__actions">
|
|
157
|
+
<a href={preview.href} rel="noopener noreferrer" target="_blank">
|
|
158
|
+
{preview.label}
|
|
159
|
+
</a>
|
|
160
|
+
</p>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|