From 88eb06fec5e4a6ef0feeaad89db568d2254c656f Mon Sep 17 00:00:00 2001 From: stan nesi Date: Fri, 5 Jun 2026 18:33:42 -0500 Subject: [PATCH 1/2] feat: separate hosted landing and viewer routes --- BUILD_STATUS.md | 4 +- NEXT_STEPS.md | 4 +- README.md | 8 +- apps/prd-viewer-web/src/App.tsx | 962 ++++++++++++------ apps/prd-viewer-web/src/styles.css | 732 +++++++++++++ .../src/viewerDocumentOutline.test.ts | 100 ++ .../src/viewerDocumentOutline.ts | 119 +++ apps/prd-viewer-web/src/viewerRoutes.test.ts | 47 + apps/prd-viewer-web/src/viewerRoutes.ts | 48 + codex/SESSION_HANDOFF.md | 30 +- docs/README.md | 2 +- docs/foundation/04_PRD/PRD_ROADMAP.md | 2 +- docs/product/PRD_AUTHORING_WORKFLOW.md | 2 +- docs/product/PRD_IMPORT_EXPORT_MATRIX.md | 2 +- docs/product/PRD_PRODUCT_BOUNDARIES.md | 4 +- 15 files changed, 1712 insertions(+), 354 deletions(-) create mode 100644 apps/prd-viewer-web/src/viewerDocumentOutline.test.ts create mode 100644 apps/prd-viewer-web/src/viewerDocumentOutline.ts create mode 100644 apps/prd-viewer-web/src/viewerRoutes.test.ts create mode 100644 apps/prd-viewer-web/src/viewerRoutes.ts diff --git a/BUILD_STATUS.md b/BUILD_STATUS.md index d7ee028..5803e86 100644 --- a/BUILD_STATUS.md +++ b/BUILD_STATUS.md @@ -4,10 +4,12 @@ - Synced local `main` after PR `#43` (`[stannesi] add public viewer demo ux`) merged, then created `thehive/prd-landing-viewer-dashboard` for the hosted landing/viewer refresh. - Refreshed `apps/prd-viewer-web` into one deployable PRD landing page plus reference web viewer workspace with premium dark mode by default, first-class light mode, persistent theme selection, and a dashboard-style viewer surface inspired by the provided references. +- Revised the hosted app route structure so `/` is the product landing page and `/viewer/` is the actual PRD Web Viewer workspace, including base-path support for GitHub Pages at `/prd/`. +- Added real package-derived outline panels for structured `general-document`, `comic`, and `storyboard` content rather than static/fake dashboard navigation. - Added hosted sample archive support through generated demo assets under the ignored `apps/prd-viewer-web/public/examples/` path. Hosted samples load through the same eager whole-package in-memory open path as user-selected archives and do not change PRD format loading semantics. - Added `scripts/prepare-viewer-demo-assets.mjs`, root `pnpm viewer:demo:assets`, `pnpm viewer:demo:build`, and `pnpm viewer:demo:dev` so canonical examples can be packed and copied into the web viewer demo surface without committing `.prd` binaries. - Added `.github/workflows/viewer-demo-pages.yml` to build the static viewer with `PRD_VIEWER_BASE_PATH=/prd/` and deploy `apps/prd-viewer-web/dist` through GitHub Pages. -- Added test coverage for landing/demo content, archive file filtering/sample URL behavior, and demo asset preparation. +- Added test coverage for landing/demo content, archive file filtering/sample URL behavior, viewer route mapping, document-outline derivation, and demo asset preparation. - Updated root README, docs index, product-boundary docs, authoring workflow, import/export matrix, roadmap, `NEXT_STEPS.md`, and `codex/SESSION_HANDOFF.md`. - This slice did not add a changeset because it changes only the private web viewer app plus docs/operator/deployment surfaces. - Local validation included targeted web viewer/script tests, web viewer TypeScript build, `pnpm viewer:demo:build`, docs consistency with root docs, `git diff --check`, root typecheck, `pnpm foundation:gate`, and browser desktop/mobile visual checks. Browser verification confirmed dark/light mode, hosted sample loading for `document-basic`, `comic-basic`, and `storyboard-basic`, and no console warnings/errors. diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md index 0d5e4bb..2c9c2b2 100644 --- a/NEXT_STEPS.md +++ b/NEXT_STEPS.md @@ -48,5 +48,5 @@ 39. [x] Implement the first real import lane: `prd import markdown ./source.md --out ./my-document`, targeting structured `general-document` package directories with deterministic validation-friendly output. 40. [x] Implemented the next small visual-profile import lane: `prd import images ./pages --profile --out ./package`, producing validator-valid ordered image packages without broad conversion, Studio, Cloud, PRDc, payment, crypto, rights, or visual-editor scope. 41. [x] Implemented a public demo/viewer/landing UX slice inside `apps/prd-viewer-web` that demonstrates the real `prd init/import -> validate -> inspect -> pack -> open` flow now that text and visual import lanes exist. -42. [x] Chose and implemented a public hosted demo/deployment path for the reference viewer using a single `apps/prd-viewer-web` landing/viewer app, generated hosted sample archives, light/dark theme support, and a GitHub Pages workflow. -43. [ ] After the hosted demo PR lands, run launch QA against the live GitHub Pages URL, verify hosted samples open correctly, and choose the next focused viewer/product polish step. +42. [x] Chose and implemented a public hosted demo/deployment path for the reference viewer using a single `apps/prd-viewer-web` app with separate `/` landing and `/viewer/` workspace routes, generated hosted sample archives, light/dark theme support, and a GitHub Pages workflow. +43. [ ] After the hosted demo PR lands, run launch QA against the live GitHub Pages URL, verify `/prd/`, `/prd/viewer/`, hosted samples, theme persistence, and manual `.prd` upload, then choose the next focused viewer/product polish step. diff --git a/README.md b/README.md index 0e9fe3e..cb7ba8d 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,8 @@ pnpm viewer:demo:dev `viewer:demo:assets` packs canonical examples and copies selected `.prd` archives into the web viewer's ignored `public/examples/` directory for the hosted demo. Those hosted samples are demo assets only; they do not change PRD's packaged-first loading contract. +The hosted app has two public routes in one deployable Vite build: `/` is the PRD landing page and `/viewer/` is the reference Web Viewer workspace. Under GitHub Pages those routes are served beneath the configured `/prd/` base path. + ## Contributor MVP gate (no npm credentials required) @@ -482,7 +484,7 @@ The repo also now includes [`.github/workflows/codex-ci.yml`](/Users/nappy.cat/L For release automation, the repo also includes [`.github/workflows/release.yml`](/Users/nappy.cat/Labs/eonHive.lab/prd.lab/prd/.github/workflows/release.yml), which gates npm publication through Changesets and CI on `main`. -The hosted PRD Web Viewer demo is built and deployed by [`.github/workflows/viewer-demo-pages.yml`](/Users/nappy.cat/Labs/eonHive.lab/prd.lab/prd/.github/workflows/viewer-demo-pages.yml) using GitHub Pages. The workflow builds with `PRD_VIEWER_BASE_PATH=/prd/` and deploys `apps/prd-viewer-web/dist`. +The hosted PRD Web Viewer demo is built and deployed by [`.github/workflows/viewer-demo-pages.yml`](/Users/nappy.cat/Labs/eonHive.lab/prd.lab/prd/.github/workflows/viewer-demo-pages.yml) using GitHub Pages. The workflow builds with `PRD_VIEWER_BASE_PATH=/prd/` and deploys `apps/prd-viewer-web/dist`, with `/prd/` as the landing route and `/prd/viewer/` as the viewer workspace route. --- @@ -490,8 +492,8 @@ The hosted PRD Web Viewer demo is built and deployed by [`.github/workflows/view 1. Run `pnpm codex:check` 2. Run `pnpm viewer:demo:dev` -3. Open the PRD landing page and use either light or premium dark mode -4. Load a hosted sample archive or choose/drag a `.prd` archive into the viewer drop zone +3. Open the PRD landing page at `/` and use either light or premium dark mode +4. Open the viewer workspace at `/viewer/`, then load a hosted sample archive or choose/drag a `.prd` archive into the viewer drop zone 5. Confirm package status, package facts, manifest metadata, localization/attachments, and rendered content in the reference viewer Current example behavior: diff --git a/apps/prd-viewer-web/src/App.tsx b/apps/prd-viewer-web/src/App.tsx index 6012e88..4ef4893 100644 --- a/apps/prd-viewer-web/src/App.tsx +++ b/apps/prd-viewer-web/src/App.tsx @@ -4,6 +4,7 @@ import { type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, type ReactNode, + type RefObject, useEffect, useMemo, useRef, @@ -57,6 +58,15 @@ import { getPrdArchiveSelectionError, getViewerDemoSampleArchiveUrl } from "./viewerArchiveFiles.js"; +import { + createViewerDocumentOutline, + type ViewerDocumentOutlineItem +} from "./viewerDocumentOutline.js"; +import { + getViewerAppRouteFromPath, + getViewerAppRoutePath, + type ViewerAppRoute +} from "./viewerRoutes.js"; type AssetUrlMap = Record; type AttachmentUrlMap = Record; @@ -108,6 +118,9 @@ const referenceViewerRuntimeDescriptor = PRD_REFERENCE_VIEWER_RUNTIME_DESCRIPTOR const referenceViewerLoadMode = referenceViewerRuntimeDescriptor.referenceLoadMode ?? "eager-whole-package"; const viewerThemeStorageKey = "prd-viewer-theme"; +const viewerAppBasePath = + ((import.meta as ImportMeta & { env?: { BASE_URL?: string } }).env?.BASE_URL ?? + "/"); function formatReferenceLoadMode(loadMode: PrdReferenceLoadMode): string { if (loadMode === "eager-whole-package") { @@ -2127,19 +2140,79 @@ function ThemeToggle({ ); } +function AppNavigation({ + route, + theme, + onNavigate, + onToggleTheme, + onOpenArchive +}: { + route: ViewerAppRoute; + theme: ViewerTheme; + onNavigate: (route: ViewerAppRoute, hash?: string) => void; + onToggleTheme: () => void; + onOpenArchive: () => void; +}) { + return ( +
+ + +
+ + +
+
+ ); +} + function LandingSurface({ onOpenViewer, onLoadSample, - sampleLoadingId + sampleLoadingId, + onLoadArchive }: { onOpenViewer: () => void; onLoadSample: () => void; sampleLoadingId: string | null; + onLoadArchive: (example: ViewerDemoExampleArchive) => void; }) { return ( -
-
+
+
+ 0.1.1 public preview

{viewerLandingHero.eyebrow}

{viewerLandingHero.title}

{viewerLandingHero.description}

@@ -2159,6 +2232,11 @@ function LandingSurface({ {viewerLandingHero.tertiaryAction}
+
+ No account required + Packaged-first + Offline-first core +
@@ -2170,6 +2248,7 @@ function LandingSurface({
+ Table of contents Manifest Package facts Assets @@ -2189,9 +2268,9 @@ function LandingSurface({
- +
-
+
{viewerLandingCapabilities.map((capability) => (

{capability.proof}

@@ -2199,9 +2278,11 @@ function LandingSurface({

{capability.description}

))} -
+ -
+ + +
{viewerLandingProfiles.map((profile) => (
))} -
+ + +
Future lanes stay future:{" "} {viewerFutureLanes.join(", ")} are not shipped by this demo and do not change what counts as a valid PRD package.
- + ); } @@ -2254,13 +2341,19 @@ function ViewerDemoFlowView() { function ViewerExampleGuideView({ onLoadSample, - sampleLoadingId + sampleLoadingId, + compact = false }: { onLoadSample: (example: ViewerDemoExampleArchive) => void; sampleLoadingId: string | null; + compact?: boolean; }) { return ( -
+

Try hosted examples

Load a sample archive or pack one locally.

@@ -2302,6 +2395,432 @@ function ViewerExampleGuideView({ ); } +function ViewerDocumentOutlineView({ + items +}: { + items: ViewerDocumentOutlineItem[]; +}) { + return ( +
+
+

Outline

+ {items.length > 0 ? `${items.length} items` : "empty"} +
+ {items.length > 0 ? ( +
    + {items.map((item) => ( +
  1. + + {item.kind} + {item.label} + +
  2. + ))} +
+ ) : ( +

+ Load a structured package to show document sections, comic panels, or + storyboard frames. +

+ )} +
+ ); +} + +function UploadDropzoneView({ + fileInputRef, + dragActive, + onArchiveFiles, + onDrop, + onDragOver, + onDragLeave, + onKeyDown +}: { + fileInputRef: RefObject; + dragActive: boolean; + onArchiveFiles: (files: FileList | File[] | null) => void; + onDrop: (event: ReactDragEvent) => void; + onDragOver: (event: ReactDragEvent) => void; + onDragLeave: (event: ReactDragEvent) => void; + onKeyDown: (event: ReactKeyboardEvent) => void; +}) { + return ( +
fileInputRef.current?.click()} + onKeyDown={onKeyDown} + onDrop={onDrop} + onDragOver={onDragOver} + onDragLeave={onDragLeave} + aria-label="Choose or drop a PRD archive" + > +

Open package

+ + Choose or drag a .prd archive + + + Source folders must be packed first. If you drop multiple files, the + first .prd archive is opened. + + { + onArchiveFiles(event.target.files); + event.currentTarget.value = ""; + }} + /> +
+ ); +} + +function PackageStatusPanel({ + viewerState, + profileLabel, + activeLocale, + activeEntryPath +}: { + viewerState: ViewerState; + profileLabel: string | null; + activeLocale: string | undefined; + activeEntryPath: string | undefined; +}) { + return ( +
+
+

Package Status

+ + {viewerState.validation.valid ? "Valid" : "Invalid"} + +
+
+
+
Profile
+
{profileLabel ?? "n/a"}
+
+
+
Profile ID
+
{viewerState.opened?.manifest.profile ?? "n/a"}
+
+
+
Support state
+
{viewerState.opened?.supportState ?? "n/a"}
+
+
+
Entry
+
{activeEntryPath ?? "n/a"}
+
+
+
Default locale
+
{viewerState.opened?.localization?.defaultLocale ?? "none"}
+
+
+
Active locale
+
{activeLocale ?? "none"}
+
+
+ + {viewerState.validation.errors.length > 0 && ( + <> +

Errors

+
    + {viewerState.validation.errors.map((issue) => ( +
  • + {issue.code} {issue.message} +
  • + ))} +
+ + )} + + {viewerState.validation.warnings.length > 0 && ( + <> +

Warnings

+
    + {viewerState.validation.warnings.map((issue) => ( +
  • + {issue.code} {issue.message} +
  • + ))} +
+ + )} +
+ ); +} + +function ViewerWorkspaceView({ + fileInputRef, + dragActive, + loading, + error, + sampleLoadingId, + viewerState, + profileLabel, + viewerRenderMode, + activeLocale, + activeEntryPath, + activeEntryDocument, + activeComicDocument, + activeStoryboardDocument, + activeRenderedHtml, + activePublicMetadata, + documentOutline, + localeOptions, + comicPanelNavigationEnabled, + storyboardReviewGridEnabled, + resumePresentationEnabled, + onArchiveFiles, + onDrop, + onDragOver, + onDragLeave, + onUploadKeyDown, + onLoadSample, + onLocaleChange +}: { + fileInputRef: RefObject; + dragActive: boolean; + loading: boolean; + error: string | null; + sampleLoadingId: string | null; + viewerState: ViewerState | null; + profileLabel: string | null; + viewerRenderMode: ViewerRenderMode; + activeLocale: string | undefined; + activeEntryPath: string | undefined; + activeEntryDocument: PrdGeneralDocumentRoot | undefined; + activeComicDocument: PrdComicRoot | undefined; + activeStoryboardDocument: PrdStoryboardRoot | undefined; + activeRenderedHtml: string | undefined; + activePublicMetadata: PrdPublicMetadata | undefined; + documentOutline: ViewerDocumentOutlineItem[]; + localeOptions: string[]; + comicPanelNavigationEnabled: boolean; + storyboardReviewGridEnabled: boolean; + resumePresentationEnabled: boolean; + onArchiveFiles: (files: FileList | File[] | null) => void; + onDrop: (event: ReactDragEvent) => void; + onDragOver: (event: ReactDragEvent) => void; + onDragLeave: (event: ReactDragEvent) => void; + onUploadKeyDown: (event: ReactKeyboardEvent) => void; + onLoadSample: (example: ViewerDemoExampleArchive) => void; + onLocaleChange: (locale: string) => void; +}) { + return ( +
+
+
+
+

PRD Web Viewer

+

Open, validate, and inspect portable documents.

+
+
+ + {viewerState?.validation.valid ? "Valid package" : "No package loaded"} + + + {formatReferenceLoadMode(referenceViewerLoadMode)} + +
+
+ +
+ + +
+ {loading &&

Loading package...

} + {error &&

{error}

} + + {!viewerState && !loading && ( +
+

Ready for a package

+

Load a hosted sample or open your own `.prd` archive.

+

+ The viewer validates the package first, then reports package + status, manifest metadata, runtime support state, and the + current reference render mode. +

+
+ )} + + {viewerState && ( +
+
+

Render Stage

+ {getViewerRenderModeMessage(viewerRenderMode)} +
+ + {viewerState.opened?.supportState === "reserved-profile" && ( +
+ {profileLabel} +

{viewerState.opened.message}

+
+ )} + + {viewerState.opened?.message && + viewerState.opened.supportState !== "reserved-profile" && ( +
+

{viewerState.opened.message}

+
+ )} + + {activeComicDocument && + (comicPanelNavigationEnabled ? ( + + ) : ( + + ))} + + {activeStoryboardDocument && + (storyboardReviewGridEnabled ? ( + + ) : ( + + ))} + + {viewerState.opened?.supportState !== "reserved-profile" && + activeRenderedHtml && ( +