diff --git a/.github/workflows/viewer-demo-pages.yml b/.github/workflows/viewer-demo-pages.yml new file mode 100644 index 0000000..3192c1e --- /dev/null +++ b/.github/workflows/viewer-demo-pages.yml @@ -0,0 +1,72 @@ +name: Viewer Demo Pages + +on: + push: + branches: + - main + paths: + - ".github/workflows/viewer-demo-pages.yml" + - "apps/prd-viewer-web/**" + - "examples/**" + - "packages/**" + - "scripts/prepare-viewer-demo-assets.mjs" + - "package.json" + - "pnpm-lock.yaml" + - "pnpm-workspace.yaml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: viewer-demo-pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.32.1 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Configure GitHub Pages + uses: actions/configure-pages@v5 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build hosted viewer demo + run: pnpm viewer:demo:build + env: + PRD_VIEWER_BASE_PATH: /prd/ + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: apps/prd-viewer-web/dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 6b3c8ed..6db8b29 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ dist .vite coverage examples/dist +apps/prd-viewer-web/public/examples *.tsbuildinfo .DS_Store diff --git a/BUILD_STATUS.md b/BUILD_STATUS.md index f6b3be5..5803e86 100644 --- a/BUILD_STATUS.md +++ b/BUILD_STATUS.md @@ -1,5 +1,19 @@ # BUILD STATUS +## 2026-06-05 + +- 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, 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. + ## 2026-06-02 - Merged PR `#42` (`[stannesi] add ordered image import command`) into `main` at `22d76df4e601a15508ab9475aff7924669b0e913` and synced local `main` before starting the public viewer demo UX slice. diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md index 3d53248..2c9c2b2 100644 --- a/NEXT_STEPS.md +++ b/NEXT_STEPS.md @@ -48,4 +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. [ ] Choose and implement a public hosted demo/deployment path for the reference viewer so the demo can be shared without a local repo checkout. +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 2f5b1e7..cb7ba8d 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,9 @@ The current top-level scripts are: * `pnpm codex:pack` * `pnpm codex:run:web` * `pnpm dev:web` +* `pnpm viewer:demo:assets` +* `pnpm viewer:demo:build` +* `pnpm viewer:demo:dev` * `pnpm examples:pack` * `pnpm examples:validate` * `pnpm examples:smoke` @@ -304,6 +307,18 @@ Current authoring workflow: `docs/product/PRD_AUTHORING_WORKFLOW.md`. Import/export sequencing matrix: `docs/product/PRD_IMPORT_EXPORT_MATRIX.md`. Versioning policy across format, manifest, runtime, and npm tooling surfaces: `docs/core/PRD_VERSIONING_POLICY.md`. +Hosted viewer demo workflow: + +```bash +pnpm viewer:demo:assets +pnpm viewer:demo:build +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) @@ -448,6 +463,10 @@ For Codex-driven work, use the repo-local run actions instead of manually piecin Builds the workspace and produces the example `.prd` archives under `examples/dist/`. * `pnpm codex:run:web` Builds the workspace, packs the example archives, and starts the web viewer. +* `pnpm viewer:demo:build` + Packs canonical examples, prepares hosted sample assets, and builds the deployable PRD Web Viewer demo. +* `pnpm viewer:demo:dev` + Prepares hosted sample assets and starts the PRD Web Viewer demo locally. Codex desktop also reads [`.codex/environments/environment.toml`](/Users/nappy.cat/Labs/eonHive.lab/prd.lab/prd/.codex/environments/environment.toml) for this workspace. That file now defines: @@ -465,15 +484,17 @@ 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`, with `/prd/` as the landing route and `/prd/viewer/` as the viewer workspace route. + --- ## Example flow 1. Run `pnpm codex:check` -2. Run `pnpm codex:run:web` -3. Open the web viewer demo landing page -4. Choose or drag a `.prd` archive from `examples/dist/` into the drop zone -5. Confirm package status, package facts, and rendered content in the reference viewer +2. Run `pnpm viewer:demo:dev` +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 2cbc14c..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, @@ -43,10 +44,29 @@ import { inferViewerRenderMode } from "./viewerRenderMode.js"; import { + type ViewerDemoExampleArchive, viewerDemoExampleArchives, viewerDemoFlowSteps, - viewerDemoPreparationCommands + viewerDemoPreparationCommands, + viewerFutureLanes, + viewerLandingCapabilities, + viewerLandingHero, + viewerLandingProfiles } from "./viewerDemoContent.js"; +import { + findFirstPrdArchive, + 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; @@ -77,6 +97,7 @@ type ViewerRenderMode = | "structured-json-rendered" | "html-fallback-rendered" | "unsupported-entry-mode"; +type ViewerTheme = "dark" | "light"; interface PackageFacts { fileCount: number; @@ -96,6 +117,10 @@ interface PackageFacts { 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") { @@ -528,12 +553,13 @@ function formatBytes(byteCount: number): string { return new Intl.NumberFormat("en-US").format(byteCount); } -function isPrdArchiveFile(file: File): boolean { - return file.name.toLowerCase().endsWith(".prd"); -} +function getInitialViewerTheme(): ViewerTheme { + if (typeof window === "undefined") { + return "dark"; + } -function findFirstPrdArchive(files: FileList | File[]): File | undefined { - return Array.from(files).find((file) => isPrdArchiveFile(file)); + const storedTheme = window.localStorage.getItem(viewerThemeStorageKey); + return storedTheme === "light" || storedTheme === "dark" ? storedTheme : "dark"; } function parseLocalizedDocumentOverridesRoot( @@ -2099,9 +2125,193 @@ function PackageFactsView({ ); } +function ThemeToggle({ + theme, + onToggle +}: { + theme: ViewerTheme; + onToggle: () => void; +}) { + return ( + + ); +} + +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, + onLoadArchive +}: { + onOpenViewer: () => void; + onLoadSample: () => void; + sampleLoadingId: string | null; + onLoadArchive: (example: ViewerDemoExampleArchive) => void; +}) { + return ( +
+
+
+ 0.1.1 public preview +

{viewerLandingHero.eyebrow}

+

{viewerLandingHero.title}

+

{viewerLandingHero.description}

+
+ + + + {viewerLandingHero.tertiaryAction} + +
+
+ No account required + Packaged-first + Offline-first core +
+
+ +
+
+ + + + PRD Web Viewer +
+
+
+ Table of contents + Manifest + Package facts + Assets + Localization +
+
+

Reference package

+

Responsive document, packed and portable.

+

+ Validates first. Opens locally. Reports the actual loading profile. +

+
+ Valid + Structured JSON + Offline package +
+
+
+
+
+ +
+ {viewerLandingCapabilities.map((capability) => ( +
+

{capability.proof}

+

{capability.title}

+

{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. +
+
+ ); +} + function ViewerDemoFlowView() { return ( -
+

Public product path

Create or import, verify, package, then open.

@@ -2129,16 +2339,28 @@ function ViewerDemoFlowView() { ); } -function ViewerExampleGuideView() { +function ViewerExampleGuideView({ + onLoadSample, + sampleLoadingId, + compact = false +}: { + onLoadSample: (example: ViewerDemoExampleArchive) => void; + sampleLoadingId: string | null; + compact?: boolean; +}) { return ( -
+
-

Try canonical examples

-

Pack examples, then drop an archive here.

+

Try hosted examples

+

Load a sample archive or pack one locally.

- The viewer does not fetch or bundle examples. Generate the archives - locally, then use the drop zone to open any `.prd` file from - `examples/dist/`. + Hosted samples are generated from canonical examples for this demo + build. They use the same eager whole-package in-memory open path as + user-selected archives; this is not a new PRD network-loading contract.

@@ -2157,6 +2379,14 @@ function ViewerExampleGuideView() {
{example.path}

{example.description}

+ ))} @@ -2165,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 && ( +