narrarium-astro-reader 0.1.19 → 0.1.21

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.
@@ -26,6 +26,7 @@ export declare function loadHomePageData(): Promise<{
26
26
  path: string;
27
27
  metadata: import("narrarium").ChapterFrontmatter;
28
28
  }[];
29
+ draftChapterCount: number;
29
30
  characters: {
30
31
  slug: string;
31
32
  path: string;
@@ -1 +1 @@
1
- {"version":3,"file":"book.d.ts","sourceRoot":"","sources":["../../src/lib/book.ts"],"names":[],"mappings":"AAaA,KAAK,gBAAgB,GACjB,WAAW,GACX,UAAU,GACV,SAAS,GACT,MAAM,GACN,QAAQ,GACR,gBAAgB,CAAC;AAErB,wBAAgB,WAAW,IAAI,MAAM,CAKpC;AAED,wBAAsB,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CrC;AAED,wBAAsB,mBAAmB,CAAC,WAAW,EAAE,MAAM;;;;;;;;GAG5D;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,gBAAgB;;;;;;;;;GAa/D;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM;;;;;GAG5E;AAED,wBAAsB,oBAAoB;;;;;;;;;;;;;GAoBzC"}
1
+ {"version":3,"file":"book.d.ts","sourceRoot":"","sources":["../../src/lib/book.ts"],"names":[],"mappings":"AAcA,KAAK,gBAAgB,GACjB,WAAW,GACX,UAAU,GACV,SAAS,GACT,MAAM,GACN,QAAQ,GACR,gBAAgB,CAAC;AAErB,wBAAgB,WAAW,IAAI,MAAM,CAKpC;AAED,wBAAsB,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CrC;AAQD,wBAAsB,mBAAmB,CAAC,WAAW,EAAE,MAAM;;;;;;;;GAG5D;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,gBAAgB;;;;;;;;;GAa/D;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM;;;;;GAG5E;AAED,wBAAsB,oBAAoB;;;;;;;;;;;;;GAoBzC"}
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { readdir } from "node:fs/promises";
2
3
  import { listChapters, listEntities, pathExists, readBook, readChapter, readEntity, readTimelineMain, } from "narrarium";
3
4
  import { defaultBookRoot } from "./book-config.js";
4
5
  import { readReaderBookRootEnv, resolveReaderBookRootCandidate } from "./env.js";
@@ -18,6 +19,7 @@ export async function loadHomePageData() {
18
19
  root,
19
20
  book: null,
20
21
  chapters: [],
22
+ draftChapterCount: 0,
21
23
  characters: [],
22
24
  locations: [],
23
25
  factions: [],
@@ -26,9 +28,10 @@ export async function loadHomePageData() {
26
28
  timelineEvents: [],
27
29
  };
28
30
  }
29
- const [book, chapters, characters, locations, factions, items, secrets, timelineEvents] = await Promise.all([
31
+ const [book, chapters, draftChapterCount, characters, locations, factions, items, secrets, timelineEvents] = await Promise.all([
30
32
  readBook(root),
31
33
  listChapters(root),
34
+ countDraftChapters(root),
32
35
  listEntities(root, "character"),
33
36
  listEntities(root, "location"),
34
37
  listEntities(root, "faction"),
@@ -41,6 +44,7 @@ export async function loadHomePageData() {
41
44
  root,
42
45
  book,
43
46
  chapters,
47
+ draftChapterCount,
44
48
  characters,
45
49
  locations,
46
50
  factions,
@@ -49,6 +53,11 @@ export async function loadHomePageData() {
49
53
  timelineEvents,
50
54
  };
51
55
  }
56
+ async function countDraftChapters(root) {
57
+ const draftsRoot = path.join(root, "drafts");
58
+ const entries = await readdir(draftsRoot, { withFileTypes: true }).catch(() => []);
59
+ return entries.filter((entry) => entry.isDirectory()).length;
60
+ }
52
61
  export async function loadChapterPageData(chapterSlug) {
53
62
  const root = getBookRoot();
54
63
  return readChapter(root, chapterSlug);
@@ -1 +1 @@
1
- {"version":3,"file":"book.js","sourceRoot":"","sources":["../../src/lib/book.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"book.js","sourceRoot":"","sources":["../../src/lib/book.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ export type WorkshopEntry = {
2
+ id: string;
3
+ title: string;
4
+ body: string;
5
+ status: string;
6
+ tags: string[];
7
+ sourceKind?: string;
8
+ promotedTo?: string;
9
+ };
10
+ export type WorkshopDocument = {
11
+ path: string;
12
+ title: string;
13
+ bucket: string;
14
+ bodyHtml: string;
15
+ entries: WorkshopEntry[];
16
+ };
17
+ export type WorkshopDraftChapter = {
18
+ slug: string;
19
+ title: string;
20
+ summary: string;
21
+ bodyHtml: string;
22
+ paragraphs: Array<{
23
+ slug: string;
24
+ title: string;
25
+ summary: string;
26
+ }>;
27
+ ideas: WorkshopDocument | null;
28
+ notes: WorkshopDocument | null;
29
+ promoted: WorkshopDocument | null;
30
+ };
31
+ export declare function loadWorkshopPageData(): Promise<{
32
+ ready: boolean;
33
+ root: string;
34
+ global: null;
35
+ draftChapters: never[];
36
+ } | {
37
+ ready: boolean;
38
+ root: string;
39
+ global: {
40
+ context: WorkshopDocument | null;
41
+ ideas: WorkshopDocument | null;
42
+ notes: WorkshopDocument | null;
43
+ storyDesign: WorkshopDocument | null;
44
+ promoted: WorkshopDocument | null;
45
+ };
46
+ draftChapters: WorkshopDraftChapter[];
47
+ }>;
48
+ export declare function countDraftChapters(root: string): Promise<number>;
49
+ //# sourceMappingURL=workshop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workshop.d.ts","sourceRoot":"","sources":["../../src/lib/workshop.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpE,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC/B,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC/B,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAC;CACnC,CAAC;AAEF,wBAAsB,oBAAoB;;;;;;;;;;;;;;;;GAkCzC;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEtE"}
@@ -0,0 +1,113 @@
1
+ import path from "node:path";
2
+ import { readFile, readdir } from "node:fs/promises";
3
+ import { marked } from "marked";
4
+ import { parseNarrariumMarkdownDocument, pathExists, readChapterDraft } from "narrarium";
5
+ import { getBookRoot } from "./book.js";
6
+ export async function loadWorkshopPageData() {
7
+ const root = getBookRoot();
8
+ const ready = await pathExists(path.join(root, "book.md"));
9
+ if (!ready) {
10
+ return {
11
+ ready: false,
12
+ root,
13
+ global: null,
14
+ draftChapters: [],
15
+ };
16
+ }
17
+ const [context, ideas, notes, storyDesign, promoted, draftChapters] = await Promise.all([
18
+ readWorkshopDocument(root, "context.md"),
19
+ readWorkshopDocument(root, "ideas.md"),
20
+ readWorkshopDocument(root, "notes.md"),
21
+ readWorkshopDocument(root, "story-design.md"),
22
+ readWorkshopDocument(root, "promoted.md"),
23
+ listDraftChapters(root),
24
+ ]);
25
+ return {
26
+ ready: true,
27
+ root,
28
+ global: {
29
+ context,
30
+ ideas,
31
+ notes,
32
+ storyDesign,
33
+ promoted,
34
+ },
35
+ draftChapters,
36
+ };
37
+ }
38
+ export async function countDraftChapters(root) {
39
+ return (await listDraftChapters(root)).length;
40
+ }
41
+ async function listDraftChapters(root) {
42
+ const draftsRoot = path.join(root, "drafts");
43
+ const entries = await readdir(draftsRoot, { withFileTypes: true }).catch(() => []);
44
+ const slugs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
45
+ const chapters = await Promise.all(slugs.map(async (slug) => {
46
+ const chapterFile = path.join(draftsRoot, slug, "chapter.md");
47
+ if (!(await pathExists(chapterFile))) {
48
+ return null;
49
+ }
50
+ const chapter = await readChapterDraft(root, slug);
51
+ const [ideas, notes, promoted] = await Promise.all([
52
+ readWorkshopDocument(root, path.posix.join("drafts", slug, "ideas.md")),
53
+ readWorkshopDocument(root, path.posix.join("drafts", slug, "notes.md")),
54
+ readWorkshopDocument(root, path.posix.join("drafts", slug, "promoted.md")),
55
+ ]);
56
+ return {
57
+ slug,
58
+ title: chapter.metadata.title,
59
+ summary: chapter.metadata.summary ?? "Draft chapter in progress.",
60
+ bodyHtml: await toHtml(chapter.body || "No chapter draft body yet."),
61
+ paragraphs: chapter.paragraphs.map((paragraph) => ({
62
+ slug: path.basename(paragraph.path, ".md"),
63
+ title: paragraph.metadata.title,
64
+ summary: paragraph.metadata.summary ?? "Draft scene.",
65
+ })),
66
+ ideas,
67
+ notes,
68
+ promoted,
69
+ };
70
+ }));
71
+ return chapters.filter((chapter) => Boolean(chapter));
72
+ }
73
+ async function readWorkshopDocument(root, relativePath) {
74
+ const absolutePath = path.join(root, relativePath);
75
+ const raw = await readFile(absolutePath, "utf8").catch(() => null);
76
+ if (!raw) {
77
+ return null;
78
+ }
79
+ const document = parseNarrariumMarkdownDocument(relativePath, raw);
80
+ const frontmatter = document.frontmatter;
81
+ const title = typeof frontmatter.title === "string" && frontmatter.title.trim() ? frontmatter.title : relativePath;
82
+ const bucket = typeof frontmatter.bucket === "string" ? frontmatter.bucket : document.kind;
83
+ return {
84
+ path: relativePath,
85
+ title,
86
+ bucket,
87
+ bodyHtml: await toHtml(document.body || "No content yet."),
88
+ entries: readWorkshopEntries(frontmatter),
89
+ };
90
+ }
91
+ function readWorkshopEntries(frontmatter) {
92
+ if (!Array.isArray(frontmatter.entries)) {
93
+ return [];
94
+ }
95
+ return frontmatter.entries
96
+ .filter((entry) => Boolean(entry && typeof entry === "object"))
97
+ .map((entry) => ({
98
+ id: typeof entry.id === "string" ? entry.id : "",
99
+ title: typeof entry.title === "string" ? entry.title : "Untitled",
100
+ body: typeof entry.body === "string" ? entry.body : "",
101
+ status: typeof entry.status === "string" ? entry.status : "active",
102
+ tags: Array.isArray(entry.tags) ? entry.tags.filter((tag) => typeof tag === "string") : [],
103
+ sourceKind: typeof entry.source_kind === "string" ? entry.source_kind : undefined,
104
+ promotedTo: typeof entry.promoted_to === "string" ? entry.promoted_to : undefined,
105
+ }))
106
+ .filter((entry) => entry.id.length > 0)
107
+ .sort((left, right) => left.title.localeCompare(right.title));
108
+ }
109
+ async function toHtml(markdown) {
110
+ const rendered = await marked.parse(markdown);
111
+ return typeof rendered === "string" ? rendered : String(rendered);
112
+ }
113
+ //# sourceMappingURL=workshop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workshop.js","sourceRoot":"","sources":["../../src/lib/workshop.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "narrarium-astro-reader",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "type": "module",
5
5
  "description": "Astro reader and scaffolding CLI for Narrarium book repositories.",
6
6
  "license": "MIT",
@@ -50,7 +50,7 @@
50
50
  "test": "npm run build:cli && node --test test/**/*.test.mjs"
51
51
  },
52
52
  "dependencies": {
53
- "narrarium": "^0.1.19",
53
+ "narrarium": "^0.1.21",
54
54
  "astro": "^5.14.1",
55
55
  "chokidar": "^4.0.3",
56
56
  "marked": "^16.3.0"
@@ -14,6 +14,7 @@ interface Props {
14
14
  activeSection?:
15
15
  | "home"
16
16
  | "chapters"
17
+ | "workshop"
17
18
  | "characters"
18
19
  | "locations"
19
20
  | "factions"
@@ -75,6 +76,7 @@ const isChapterPage = Number.isFinite(currentChapterNumber);
75
76
  <nav class="site-nav">
76
77
  <a class:list={["nav-link", activeSection === "home" && "is-active"]} href="./">Book</a>
77
78
  <a class:list={["nav-link", activeSection === "chapters" && "is-active"]} href="./#chapters">Chapters</a>
79
+ {fullCanonMode && <a class:list={["nav-link", activeSection === "workshop" && "is-active"]} href="workshop/">Workshop</a>}
78
80
  <a class:list={["nav-link", activeSection === "characters" && "is-active"]} href="characters/">Characters</a>
79
81
  <a class:list={["nav-link", activeSection === "locations" && "is-active"]} href="locations/">Locations</a>
80
82
  <a class:list={["nav-link", activeSection === "factions" && "is-active"]} href="factions/">Factions</a>
package/src/lib/book.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { readdir } from "node:fs/promises";
2
3
  import {
3
4
  listChapters,
4
5
  listEntities,
@@ -36,6 +37,7 @@ export async function loadHomePageData() {
36
37
  root,
37
38
  book: null,
38
39
  chapters: [],
40
+ draftChapterCount: 0,
39
41
  characters: [],
40
42
  locations: [],
41
43
  factions: [],
@@ -45,9 +47,10 @@ export async function loadHomePageData() {
45
47
  };
46
48
  }
47
49
 
48
- const [book, chapters, characters, locations, factions, items, secrets, timelineEvents] = await Promise.all([
50
+ const [book, chapters, draftChapterCount, characters, locations, factions, items, secrets, timelineEvents] = await Promise.all([
49
51
  readBook(root),
50
52
  listChapters(root),
53
+ countDraftChapters(root),
51
54
  listEntities(root, "character"),
52
55
  listEntities(root, "location"),
53
56
  listEntities(root, "faction"),
@@ -61,6 +64,7 @@ export async function loadHomePageData() {
61
64
  root,
62
65
  book,
63
66
  chapters,
67
+ draftChapterCount,
64
68
  characters,
65
69
  locations,
66
70
  factions,
@@ -70,6 +74,12 @@ export async function loadHomePageData() {
70
74
  };
71
75
  }
72
76
 
77
+ async function countDraftChapters(root: string): Promise<number> {
78
+ const draftsRoot = path.join(root, "drafts");
79
+ const entries = await readdir(draftsRoot, { withFileTypes: true }).catch(() => []);
80
+ return entries.filter((entry) => entry.isDirectory()).length;
81
+ }
82
+
73
83
  export async function loadChapterPageData(chapterSlug: string) {
74
84
  const root = getBookRoot();
75
85
  return readChapter(root, chapterSlug);
@@ -0,0 +1,159 @@
1
+ import path from "node:path";
2
+ import { readFile, readdir } from "node:fs/promises";
3
+ import { marked } from "marked";
4
+ import { parseNarrariumMarkdownDocument, pathExists, readChapterDraft } from "narrarium";
5
+ import { getBookRoot } from "./book.js";
6
+
7
+ export type WorkshopEntry = {
8
+ id: string;
9
+ title: string;
10
+ body: string;
11
+ status: string;
12
+ tags: string[];
13
+ sourceKind?: string;
14
+ promotedTo?: string;
15
+ };
16
+
17
+ export type WorkshopDocument = {
18
+ path: string;
19
+ title: string;
20
+ bucket: string;
21
+ bodyHtml: string;
22
+ entries: WorkshopEntry[];
23
+ };
24
+
25
+ export type WorkshopDraftChapter = {
26
+ slug: string;
27
+ title: string;
28
+ summary: string;
29
+ bodyHtml: string;
30
+ paragraphs: Array<{ slug: string; title: string; summary: string }>;
31
+ ideas: WorkshopDocument | null;
32
+ notes: WorkshopDocument | null;
33
+ promoted: WorkshopDocument | null;
34
+ };
35
+
36
+ export async function loadWorkshopPageData() {
37
+ const root = getBookRoot();
38
+ const ready = await pathExists(path.join(root, "book.md"));
39
+
40
+ if (!ready) {
41
+ return {
42
+ ready: false,
43
+ root,
44
+ global: null,
45
+ draftChapters: [],
46
+ };
47
+ }
48
+
49
+ const [context, ideas, notes, storyDesign, promoted, draftChapters] = await Promise.all([
50
+ readWorkshopDocument(root, "context.md"),
51
+ readWorkshopDocument(root, "ideas.md"),
52
+ readWorkshopDocument(root, "notes.md"),
53
+ readWorkshopDocument(root, "story-design.md"),
54
+ readWorkshopDocument(root, "promoted.md"),
55
+ listDraftChapters(root),
56
+ ]);
57
+
58
+ return {
59
+ ready: true,
60
+ root,
61
+ global: {
62
+ context,
63
+ ideas,
64
+ notes,
65
+ storyDesign,
66
+ promoted,
67
+ },
68
+ draftChapters,
69
+ };
70
+ }
71
+
72
+ export async function countDraftChapters(root: string): Promise<number> {
73
+ return (await listDraftChapters(root)).length;
74
+ }
75
+
76
+ async function listDraftChapters(root: string): Promise<WorkshopDraftChapter[]> {
77
+ const draftsRoot = path.join(root, "drafts");
78
+ const entries = await readdir(draftsRoot, { withFileTypes: true }).catch(() => []);
79
+ const slugs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
80
+
81
+ const chapters = await Promise.all(
82
+ slugs.map(async (slug) => {
83
+ const chapterFile = path.join(draftsRoot, slug, "chapter.md");
84
+ if (!(await pathExists(chapterFile))) {
85
+ return null;
86
+ }
87
+
88
+ const chapter = await readChapterDraft(root, slug);
89
+ const [ideas, notes, promoted] = await Promise.all([
90
+ readWorkshopDocument(root, path.posix.join("drafts", slug, "ideas.md")),
91
+ readWorkshopDocument(root, path.posix.join("drafts", slug, "notes.md")),
92
+ readWorkshopDocument(root, path.posix.join("drafts", slug, "promoted.md")),
93
+ ]);
94
+
95
+ return {
96
+ slug,
97
+ title: chapter.metadata.title,
98
+ summary: chapter.metadata.summary ?? "Draft chapter in progress.",
99
+ bodyHtml: await toHtml(chapter.body || "No chapter draft body yet."),
100
+ paragraphs: chapter.paragraphs.map((paragraph) => ({
101
+ slug: path.basename(paragraph.path, ".md"),
102
+ title: paragraph.metadata.title,
103
+ summary: paragraph.metadata.summary ?? "Draft scene.",
104
+ })),
105
+ ideas,
106
+ notes,
107
+ promoted,
108
+ } satisfies WorkshopDraftChapter;
109
+ }),
110
+ );
111
+
112
+ return chapters.filter((chapter): chapter is WorkshopDraftChapter => Boolean(chapter));
113
+ }
114
+
115
+ async function readWorkshopDocument(root: string, relativePath: string): Promise<WorkshopDocument | null> {
116
+ const absolutePath = path.join(root, relativePath);
117
+ const raw = await readFile(absolutePath, "utf8").catch(() => null);
118
+ if (!raw) {
119
+ return null;
120
+ }
121
+
122
+ const document = parseNarrariumMarkdownDocument(relativePath, raw);
123
+ const frontmatter = document.frontmatter as Record<string, unknown>;
124
+ const title = typeof frontmatter.title === "string" && frontmatter.title.trim() ? frontmatter.title : relativePath;
125
+ const bucket = typeof frontmatter.bucket === "string" ? frontmatter.bucket : document.kind;
126
+
127
+ return {
128
+ path: relativePath,
129
+ title,
130
+ bucket,
131
+ bodyHtml: await toHtml(document.body || "No content yet."),
132
+ entries: readWorkshopEntries(frontmatter),
133
+ };
134
+ }
135
+
136
+ function readWorkshopEntries(frontmatter: Record<string, unknown>): WorkshopEntry[] {
137
+ if (!Array.isArray(frontmatter.entries)) {
138
+ return [];
139
+ }
140
+
141
+ return frontmatter.entries
142
+ .filter((entry): entry is Record<string, unknown> => Boolean(entry && typeof entry === "object"))
143
+ .map((entry) => ({
144
+ id: typeof entry.id === "string" ? entry.id : "",
145
+ title: typeof entry.title === "string" ? entry.title : "Untitled",
146
+ body: typeof entry.body === "string" ? entry.body : "",
147
+ status: typeof entry.status === "string" ? entry.status : "active",
148
+ tags: Array.isArray(entry.tags) ? entry.tags.filter((tag): tag is string => typeof tag === "string") : [],
149
+ sourceKind: typeof entry.source_kind === "string" ? entry.source_kind : undefined,
150
+ promotedTo: typeof entry.promoted_to === "string" ? entry.promoted_to : undefined,
151
+ }))
152
+ .filter((entry) => entry.id.length > 0)
153
+ .sort((left, right) => left.title.localeCompare(right.title));
154
+ }
155
+
156
+ async function toHtml(markdown: string): Promise<string> {
157
+ const rendered = await marked.parse(markdown);
158
+ return typeof rendered === "string" ? rendered : String(rendered);
159
+ }
@@ -6,7 +6,7 @@ import { loadAssetFigure } from "../lib/assets";
6
6
  import { countPublicCanonEntries } from "../lib/public-canon";
7
7
  import { isFullCanonMode } from "../lib/reader-mode";
8
8
 
9
- const { ready, root, book, chapters, characters, locations, factions, items, secrets, timelineEvents } = await loadHomePageData();
9
+ const { ready, root, book, chapters, draftChapterCount, characters, locations, factions, items, secrets, timelineEvents } = await loadHomePageData();
10
10
  const title = book?.frontmatter.title ?? "Narrarium Reader";
11
11
  const coverFigure = await loadAssetFigure("book", `${title} cover`);
12
12
  const fullCanonMode = isFullCanonMode();
@@ -129,6 +129,16 @@ const chapterCards = await Promise.all(
129
129
  <div class="chip-row"><span class="chip">{visibleTimelineCount} visible</span></div>
130
130
  </a>
131
131
  </article>
132
+ {fullCanonMode && (
133
+ <article class="catalog-card">
134
+ <a href="workshop/">
135
+ <span class="browse-card-icon">🧭</span>
136
+ <h3 class="chapter-title">Workshop</h3>
137
+ <div class="chapter-meta">Inspect drafts, ideas, notes, story design, and promoted items.</div>
138
+ <div class="chip-row"><span class="chip">{draftChapterCount} draft chapters</span></div>
139
+ </a>
140
+ </article>
141
+ )}
132
142
  </div>
133
143
  </section>
134
144
 
@@ -0,0 +1,138 @@
1
+ ---
2
+ import BaseLayout from "../../layouts/BaseLayout.astro";
3
+ import { isFullCanonMode } from "../../lib/reader-mode";
4
+ import { loadWorkshopPageData } from "../../lib/workshop";
5
+
6
+ const fullCanonMode = isFullCanonMode();
7
+ const { ready, global, draftChapters } = await loadWorkshopPageData();
8
+
9
+ const globalDocuments = global
10
+ ? [
11
+ { label: "Context", document: global.context },
12
+ { label: "Story Design", document: global.storyDesign },
13
+ { label: "Ideas", document: global.ideas },
14
+ { label: "Notes", document: global.notes },
15
+ { label: "Promoted", document: global.promoted },
16
+ ]
17
+ : [];
18
+ ---
19
+
20
+ <BaseLayout title="Workshop" description="Inspect drafts, ideas, notes, and story design." activeSection="workshop">
21
+ <section class="hero">
22
+ <p class="eyebrow">Workshop</p>
23
+ <h1>Drafts, Ideas, and Notes</h1>
24
+ <p class="lede">
25
+ {fullCanonMode
26
+ ? "Review global planning material, draft chapters, and promoted work items from one author-facing workspace."
27
+ : "Workshop content is available only in full canon mode."}
28
+ </p>
29
+ </section>
30
+
31
+ {!ready ? (
32
+ <div class="empty">No Narrarium book repository is connected yet.</div>
33
+ ) : !fullCanonMode ? (
34
+ <div class="empty">Switch the reader to full canon mode to inspect workshop material such as drafts, ideas, notes, and story design.</div>
35
+ ) : (
36
+ <>
37
+ <section class="section">
38
+ <h2>Global Workspace</h2>
39
+ <div class="chapter-list">
40
+ {globalDocuments.map(({ label, document }) => (
41
+ <article class="chapter-card workspace-card">
42
+ <div class="chapter-number">{label}</div>
43
+ <h3 class="chapter-title">{document?.title ?? `${label} unavailable`}</h3>
44
+ <div class="chapter-meta">{document ? document.path : "File not present in this book yet."}</div>
45
+ {document ? (
46
+ <>
47
+ <div class="chip-row"><span class="chip">{document.entries.length} entries</span></div>
48
+ <div class="prose" set:html={document.bodyHtml}></div>
49
+ {document.entries.length > 0 && (
50
+ <div class="workspace-entry-list">
51
+ {document.entries.map((entry) => (
52
+ <article class="workspace-entry">
53
+ <div class="chip-row">
54
+ <span class="chip">{entry.status}</span>
55
+ {entry.tags.map((tag) => (
56
+ <span class="chip">{tag}</span>
57
+ ))}
58
+ {entry.promotedTo && <span class="chip">to {entry.promotedTo}</span>}
59
+ </div>
60
+ <h4>{entry.title}</h4>
61
+ <p>{entry.body || "No details yet."}</p>
62
+ </article>
63
+ ))}
64
+ </div>
65
+ )}
66
+ </>
67
+ ) : null}
68
+ </article>
69
+ ))}
70
+ </div>
71
+ </section>
72
+
73
+ <section class="section">
74
+ <h2>Draft Chapters</h2>
75
+ {draftChapters.length > 0 ? (
76
+ <div class="chapter-list">
77
+ {draftChapters.map((chapter) => (
78
+ <article class="chapter-card workspace-card">
79
+ <div class="chapter-number">Draft</div>
80
+ <h3 class="chapter-title">{chapter.title}</h3>
81
+ <div class="chapter-meta">{chapter.summary}</div>
82
+ <div class="chip-row">
83
+ <span class="chip">{chapter.paragraphs.length} scenes</span>
84
+ <span class="chip">{chapter.ideas?.entries.length ?? 0} ideas</span>
85
+ <span class="chip">{chapter.notes?.entries.length ?? 0} notes</span>
86
+ <span class="chip">{chapter.promoted?.entries.length ?? 0} promoted</span>
87
+ </div>
88
+ <div class="prose" set:html={chapter.bodyHtml}></div>
89
+ {chapter.paragraphs.length > 0 && (
90
+ <div class="workspace-subsection">
91
+ <h4>Draft Scenes</h4>
92
+ <div class="workspace-entry-list">
93
+ {chapter.paragraphs.map((paragraph) => (
94
+ <article class="workspace-entry">
95
+ <h5>{paragraph.title}</h5>
96
+ <p>{paragraph.summary}</p>
97
+ </article>
98
+ ))}
99
+ </div>
100
+ </div>
101
+ )}
102
+
103
+ {[chapter.ideas, chapter.notes, chapter.promoted].map(
104
+ (document, index) =>
105
+ document && (
106
+ <div class="workspace-subsection">
107
+ <h4>{index === 0 ? "Ideas" : index === 1 ? "Notes" : "Promoted"}</h4>
108
+ <div class="prose" set:html={document.bodyHtml}></div>
109
+ {document.entries.length > 0 && (
110
+ <div class="workspace-entry-list">
111
+ {document.entries.map((entry) => (
112
+ <article class="workspace-entry">
113
+ <div class="chip-row">
114
+ <span class="chip">{entry.status}</span>
115
+ {entry.tags.map((tag) => (
116
+ <span class="chip">{tag}</span>
117
+ ))}
118
+ {entry.promotedTo && <span class="chip">to {entry.promotedTo}</span>}
119
+ </div>
120
+ <h5>{entry.title}</h5>
121
+ <p>{entry.body || "No details yet."}</p>
122
+ </article>
123
+ ))}
124
+ </div>
125
+ )}
126
+ </div>
127
+ ),
128
+ )}
129
+ </article>
130
+ ))}
131
+ </div>
132
+ ) : (
133
+ <div class="empty">No chapter drafts exist yet.</div>
134
+ )}
135
+ </section>
136
+ </>
137
+ )}
138
+ </BaseLayout>
@@ -248,6 +248,42 @@ body.tts-enabled .shell {
248
248
  flex-wrap: wrap;
249
249
  }
250
250
 
251
+ .workspace-card {
252
+ width: 100%;
253
+ }
254
+
255
+ .workspace-entry-list {
256
+ display: grid;
257
+ gap: 0.85rem;
258
+ margin-top: 1rem;
259
+ }
260
+
261
+ .workspace-entry {
262
+ padding: 1rem 1.1rem;
263
+ border: 1px solid var(--line);
264
+ border-radius: 14px;
265
+ background: var(--surface-muted);
266
+ }
267
+
268
+ .workspace-entry h4,
269
+ .workspace-entry h5 {
270
+ margin: 0.35rem 0 0.45rem;
271
+ font-size: 1rem;
272
+ }
273
+
274
+ .workspace-entry p {
275
+ margin: 0;
276
+ color: var(--text-muted);
277
+ }
278
+
279
+ .workspace-subsection {
280
+ margin-top: 1.4rem;
281
+ }
282
+
283
+ .workspace-subsection h4 {
284
+ margin: 0 0 0.75rem;
285
+ }
286
+
251
287
  /* ─── Icon buttons (masthead controls) ──────────────────────── */
252
288
  .masthead-btn {
253
289
  display: inline-flex;