@statewalker/wiki.view.react 0.1.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) {{year}} Mikhail Kotelnikov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ import initWikiReact from "./public/init.js";
2
+ export { initWikiReact as default };
@@ -0,0 +1,2 @@
1
+ import initWikiReact from "./public/init.js";
2
+ export { initWikiReact as default };
@@ -0,0 +1,3 @@
1
+ import { WIKI_SITE_CATALOG_ID, makeWikiSiteSpec, wikiSiteCatalog, wikiSitePanelId, wikiSiteSpecId } from "./public/catalog.js";
2
+ import { OpenWikiSiteCommand, OpenWikiSitePayload } from "./public/index.js";
3
+ export { OpenWikiSiteCommand, type OpenWikiSitePayload, WIKI_SITE_CATALOG_ID, makeWikiSiteSpec, wikiSiteCatalog, wikiSitePanelId, wikiSiteSpecId };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { WIKI_SITE_CATALOG_ID, makeWikiSiteSpec, wikiSiteCatalog, wikiSitePanelId, wikiSiteSpecId } from "./public/catalog.js";
2
+ import { OpenWikiSiteCommand } from "./public/index.js";
3
+ export { OpenWikiSiteCommand, WIKI_SITE_CATALOG_ID, makeWikiSiteSpec, wikiSiteCatalog, wikiSitePanelId, wikiSiteSpecId };
@@ -0,0 +1,77 @@
1
+ import { Commands } from "@statewalker/shared-commands";
2
+ import { LoadFileCommand } from "@statewalker/workspace.core";
3
+ import { useEffect, useState } from "react";
4
+ import { useAppWorkspace } from "@statewalker/ui.view.react";
5
+ //#region src/internal/site-data.ts
6
+ /** Workspace-absolute path of a site's `site.json` manifest. */
7
+ function siteJsonPath(project, slug) {
8
+ return `/${project}/sites/${slug}/site.json`;
9
+ }
10
+ /** Workspace-absolute path of a generated page (manifest `page.path` is a file name). */
11
+ function pageUri(project, slug, pagePath) {
12
+ return `/${project}/sites/${slug}/${pagePath}`;
13
+ }
14
+ /** Load a site's manifest via `files:load-file`. A missing file resolves to `empty`. */
15
+ function useSiteManifest(project, slug) {
16
+ const commands = useAppWorkspace().requireAdapter(Commands);
17
+ const [state, setState] = useState({ status: "loading" });
18
+ useEffect(() => {
19
+ let cancelled = false;
20
+ setState({ status: "loading" });
21
+ commands.call(LoadFileCommand, { path: siteJsonPath(project, slug) }).promise.then((loaded) => {
22
+ if (cancelled) return;
23
+ try {
24
+ const manifest = JSON.parse(new TextDecoder().decode(loaded.bytes));
25
+ setState({
26
+ status: "ready",
27
+ manifest
28
+ });
29
+ } catch (error) {
30
+ setState({
31
+ status: "error",
32
+ message: error instanceof Error ? error.message : String(error)
33
+ });
34
+ }
35
+ }).catch(() => {
36
+ if (!cancelled) setState({ status: "empty" });
37
+ });
38
+ return () => {
39
+ cancelled = true;
40
+ };
41
+ }, [
42
+ project,
43
+ slug,
44
+ commands
45
+ ]);
46
+ return state;
47
+ }
48
+ /** Load a markdown page's text via `files:load-file`. */
49
+ function useFileText(uri) {
50
+ const commands = useAppWorkspace().requireAdapter(Commands);
51
+ const [state, setState] = useState({ status: "loading" });
52
+ useEffect(() => {
53
+ let cancelled = false;
54
+ setState({ status: "loading" });
55
+ commands.call(LoadFileCommand, { path: uri.replace(/^file:\/\//, "") }).promise.then((loaded) => {
56
+ if (cancelled) return;
57
+ setState({
58
+ status: "ready",
59
+ text: new TextDecoder().decode(loaded.bytes)
60
+ });
61
+ }).catch((error) => {
62
+ if (cancelled) return;
63
+ setState({
64
+ status: "error",
65
+ message: error instanceof Error ? error.message : String(error)
66
+ });
67
+ });
68
+ return () => {
69
+ cancelled = true;
70
+ };
71
+ }, [uri, commands]);
72
+ return state;
73
+ }
74
+ //#endregion
75
+ export { pageUri, siteJsonPath, useFileText, useSiteManifest };
76
+
77
+ //# sourceMappingURL=site-data.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"site-data.js","names":[],"sources":["../../src/internal/site-data.ts"],"sourcesContent":["import { Commands } from \"@statewalker/shared-commands\";\nimport { useAppWorkspace } from \"@statewalker/ui.view.react\";\nimport type { SiteManifest } from \"@statewalker/wiki.core\";\nimport { LoadFileCommand } from \"@statewalker/workspace.core\";\nimport { useEffect, useState } from \"react\";\n\n/** Workspace-absolute path of a site's `site.json` manifest. */\nexport function siteJsonPath(project: string, slug: string): string {\n return `/${project}/sites/${slug}/site.json`;\n}\n\n/** Workspace-absolute path of a generated page (manifest `page.path` is a file name). */\nexport function pageUri(project: string, slug: string, pagePath: string): string {\n return `/${project}/sites/${slug}/${pagePath}`;\n}\n\nexport type ManifestState =\n | { status: \"loading\" }\n | { status: \"ready\"; manifest: SiteManifest }\n | { status: \"empty\" }\n | { status: \"error\"; message: string };\n\n/** Load a site's manifest via `files:load-file`. A missing file resolves to `empty`. */\nexport function useSiteManifest(project: string, slug: string): ManifestState {\n const workspace = useAppWorkspace();\n const commands = workspace.requireAdapter(Commands);\n const [state, setState] = useState<ManifestState>({ status: \"loading\" });\n\n useEffect(() => {\n let cancelled = false;\n setState({ status: \"loading\" });\n commands\n .call(LoadFileCommand, { path: siteJsonPath(project, slug) })\n .promise.then((loaded) => {\n if (cancelled) return;\n try {\n const manifest = JSON.parse(new TextDecoder().decode(loaded.bytes)) as SiteManifest;\n setState({ status: \"ready\", manifest });\n } catch (error) {\n setState({\n status: \"error\",\n message: error instanceof Error ? error.message : String(error),\n });\n }\n })\n .catch(() => {\n if (!cancelled) setState({ status: \"empty\" });\n });\n return () => {\n cancelled = true;\n };\n }, [project, slug, commands]);\n\n return state;\n}\n\nexport type FileTextState =\n | { status: \"loading\" }\n | { status: \"ready\"; text: string }\n | { status: \"error\"; message: string };\n\n/** Load a markdown page's text via `files:load-file`. */\nexport function useFileText(uri: string): FileTextState {\n const workspace = useAppWorkspace();\n const commands = workspace.requireAdapter(Commands);\n const [state, setState] = useState<FileTextState>({ status: \"loading\" });\n\n useEffect(() => {\n let cancelled = false;\n setState({ status: \"loading\" });\n commands\n .call(LoadFileCommand, { path: uri.replace(/^file:\\/\\//, \"\") })\n .promise.then((loaded) => {\n if (cancelled) return;\n setState({ status: \"ready\", text: new TextDecoder().decode(loaded.bytes) });\n })\n .catch((error: unknown) => {\n if (cancelled) return;\n setState({\n status: \"error\",\n message: error instanceof Error ? error.message : String(error),\n });\n });\n return () => {\n cancelled = true;\n };\n }, [uri, commands]);\n\n return state;\n}\n"],"mappings":";;;;;;AAOA,SAAgB,aAAa,SAAiB,MAAsB;CAClE,OAAO,IAAI,QAAQ,SAAS,KAAK;AACnC;;AAGA,SAAgB,QAAQ,SAAiB,MAAc,UAA0B;CAC/E,OAAO,IAAI,QAAQ,SAAS,KAAK,GAAG;AACtC;;AASA,SAAgB,gBAAgB,SAAiB,MAA6B;CAE5E,MAAM,WADY,gBACO,CAAC,CAAC,eAAe,QAAQ;CAClD,MAAM,CAAC,OAAO,YAAY,SAAwB,EAAE,QAAQ,UAAU,CAAC;CAEvE,gBAAgB;EACd,IAAI,YAAY;EAChB,SAAS,EAAE,QAAQ,UAAU,CAAC;EAC9B,SACG,KAAK,iBAAiB,EAAE,MAAM,aAAa,SAAS,IAAI,EAAE,CAAC,CAAC,CAC5D,QAAQ,MAAM,WAAW;GACxB,IAAI,WAAW;GACf,IAAI;IACF,MAAM,WAAW,KAAK,MAAM,IAAI,YAAY,CAAC,CAAC,OAAO,OAAO,KAAK,CAAC;IAClE,SAAS;KAAE,QAAQ;KAAS;IAAS,CAAC;GACxC,SAAS,OAAO;IACd,SAAS;KACP,QAAQ;KACR,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;IAChE,CAAC;GACH;EACF,CAAC,CAAC,CACD,YAAY;GACX,IAAI,CAAC,WAAW,SAAS,EAAE,QAAQ,QAAQ,CAAC;EAC9C,CAAC;EACH,aAAa;GACX,YAAY;EACd;CACF,GAAG;EAAC;EAAS;EAAM;CAAQ,CAAC;CAE5B,OAAO;AACT;;AAQA,SAAgB,YAAY,KAA4B;CAEtD,MAAM,WADY,gBACO,CAAC,CAAC,eAAe,QAAQ;CAClD,MAAM,CAAC,OAAO,YAAY,SAAwB,EAAE,QAAQ,UAAU,CAAC;CAEvE,gBAAgB;EACd,IAAI,YAAY;EAChB,SAAS,EAAE,QAAQ,UAAU,CAAC;EAC9B,SACG,KAAK,iBAAiB,EAAE,MAAM,IAAI,QAAQ,cAAc,EAAE,EAAE,CAAC,CAAC,CAC9D,QAAQ,MAAM,WAAW;GACxB,IAAI,WAAW;GACf,SAAS;IAAE,QAAQ;IAAS,MAAM,IAAI,YAAY,CAAC,CAAC,OAAO,OAAO,KAAK;GAAE,CAAC;EAC5E,CAAC,CAAC,CACD,OAAO,UAAmB;GACzB,IAAI,WAAW;GACf,SAAS;IACP,QAAQ;IACR,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAChE,CAAC;EACH,CAAC;EACH,aAAa;GACX,YAAY;EACd;CACF,GAAG,CAAC,KAAK,QAAQ,CAAC;CAElB,OAAO;AACT"}
@@ -0,0 +1,23 @@
1
+ import { ScrollArea } from "@statewalker/ui.view.shadcn";
2
+ import { jsx } from "react/jsx-runtime";
3
+ //#region src/internal/site-nav.tsx
4
+ /** Left-rail table of contents: the site's pages in manifest order, single-select. */
5
+ function SiteNav({ pages, selected, onSelect }) {
6
+ return /* @__PURE__ */ jsx(ScrollArea, {
7
+ className: "h-full w-56 shrink-0 border-r",
8
+ children: /* @__PURE__ */ jsx("nav", {
9
+ className: "flex flex-col gap-0.5 p-2",
10
+ children: pages.map((page, i) => /* @__PURE__ */ jsx("button", {
11
+ type: "button",
12
+ onClick: () => onSelect(i),
13
+ "aria-current": i === selected ? "page" : void 0,
14
+ className: `rounded px-2 py-1 text-left text-sm ${i === selected ? "bg-accent font-medium" : "hover:bg-accent/50"}`,
15
+ children: page.title
16
+ }, page.id))
17
+ })
18
+ });
19
+ }
20
+ //#endregion
21
+ export { SiteNav };
22
+
23
+ //# sourceMappingURL=site-nav.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"site-nav.js","names":[],"sources":["../../src/internal/site-nav.tsx"],"sourcesContent":["import { ScrollArea } from \"@statewalker/ui.view.shadcn\";\nimport type { SitePage } from \"@statewalker/wiki.core\";\nimport type { ReactElement } from \"react\";\n\ninterface SiteNavProps {\n pages: SitePage[];\n selected: number;\n onSelect: (index: number) => void;\n}\n\n/** Left-rail table of contents: the site's pages in manifest order, single-select. */\nexport function SiteNav({ pages, selected, onSelect }: SiteNavProps): ReactElement {\n return (\n <ScrollArea className=\"h-full w-56 shrink-0 border-r\">\n <nav className=\"flex flex-col gap-0.5 p-2\">\n {pages.map((page, i) => (\n <button\n key={page.id}\n type=\"button\"\n onClick={() => onSelect(i)}\n aria-current={i === selected ? \"page\" : undefined}\n className={`rounded px-2 py-1 text-left text-sm ${\n i === selected ? \"bg-accent font-medium\" : \"hover:bg-accent/50\"\n }`}\n >\n {page.title}\n </button>\n ))}\n </nav>\n </ScrollArea>\n );\n}\n"],"mappings":";;;;AAWA,SAAgB,QAAQ,EAAE,OAAO,UAAU,YAAwC;CACjF,OACE,oBAAC,YAAD;EAAY,WAAU;EACpB,UAAA,oBAAC,OAAD;GAAK,WAAU;GACZ,UAAA,MAAM,KAAK,MAAM,MAChB,oBAAC,UAAD;IAEE,MAAK;IACL,eAAe,SAAS,CAAC;IACzB,gBAAc,MAAM,WAAW,SAAS,KAAA;IACxC,WAAW,uCACT,MAAM,WAAW,0BAA0B;IAG5C,UAAA,KAAK;GACA,GATD,KAAK,EASJ,CACT;EACE,CAAA;CACK,CAAA;AAEhB"}
@@ -0,0 +1,86 @@
1
+ import { pageUri, useFileText, useSiteManifest } from "./site-data.js";
2
+ import { SiteNav } from "./site-nav.js";
3
+ import { Markdown } from "@statewalker/mime.view.markdown";
4
+ import { Button } from "@statewalker/ui.view.shadcn";
5
+ import { useState } from "react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ //#region src/internal/wiki-site-view.tsx
8
+ function Centered({ children }) {
9
+ return /* @__PURE__ */ jsx("div", {
10
+ className: "flex h-full items-center justify-center p-6 text-sm text-muted-foreground",
11
+ children
12
+ });
13
+ }
14
+ function PageBody({ uri }) {
15
+ const state = useFileText(uri);
16
+ if (state.status === "loading") return /* @__PURE__ */ jsx(Centered, { children: "Loading…" });
17
+ if (state.status === "error") return /* @__PURE__ */ jsxs(Centered, { children: ["Failed to load page: ", state.message] });
18
+ return /* @__PURE__ */ jsx("div", {
19
+ className: "prose prose-sm dark:prose-invert max-w-none overflow-auto p-6",
20
+ children: /* @__PURE__ */ jsx(Markdown, { children: state.text })
21
+ });
22
+ }
23
+ /**
24
+ * Site-browser panel: reads a site's `site.json` manifest and renders a TOC
25
+ * sidebar + breadcrumbs + prev/next over its pages, with the selected page's
26
+ * markdown body rendered via the shared `Markdown` component. ADR-0002: all wiki
27
+ * React lives here, never in `@statewalker/wiki`.
28
+ */
29
+ function WikiSiteView({ project, slug }) {
30
+ const manifest = useSiteManifest(project, slug);
31
+ const [selected, setSelected] = useState(0);
32
+ if (manifest.status === "loading") return /* @__PURE__ */ jsx(Centered, { children: "Loading site…" });
33
+ if (manifest.status === "error") return /* @__PURE__ */ jsxs(Centered, { children: ["Failed to load site: ", manifest.message] });
34
+ if (manifest.status === "empty" || manifest.manifest.pages.length === 0) return /* @__PURE__ */ jsx(Centered, { children: "No site generated yet." });
35
+ const pages = manifest.manifest.pages;
36
+ const index = Math.min(Math.max(selected, 0), pages.length - 1);
37
+ const page = pages[index];
38
+ if (!page) return /* @__PURE__ */ jsx(Centered, { children: "No site generated yet." });
39
+ return /* @__PURE__ */ jsxs("div", {
40
+ className: "flex h-full flex-col",
41
+ children: [/* @__PURE__ */ jsxs("div", {
42
+ className: "flex items-center gap-2 border-b px-3 py-2 text-sm",
43
+ children: [
44
+ /* @__PURE__ */ jsx("span", {
45
+ className: "font-medium",
46
+ children: manifest.manifest.title
47
+ }),
48
+ /* @__PURE__ */ jsx("span", {
49
+ className: "text-muted-foreground",
50
+ children: "›"
51
+ }),
52
+ /* @__PURE__ */ jsx("span", { children: page.title }),
53
+ /* @__PURE__ */ jsxs("div", {
54
+ className: "ml-auto flex gap-1",
55
+ children: [/* @__PURE__ */ jsx(Button, {
56
+ variant: "ghost",
57
+ size: "sm",
58
+ disabled: index <= 0,
59
+ onClick: () => setSelected(index - 1),
60
+ children: "Prev"
61
+ }), /* @__PURE__ */ jsx(Button, {
62
+ variant: "ghost",
63
+ size: "sm",
64
+ disabled: index >= pages.length - 1,
65
+ onClick: () => setSelected(index + 1),
66
+ children: "Next"
67
+ })]
68
+ })
69
+ ]
70
+ }), /* @__PURE__ */ jsxs("div", {
71
+ className: "flex min-h-0 flex-1",
72
+ children: [/* @__PURE__ */ jsx(SiteNav, {
73
+ pages,
74
+ selected: index,
75
+ onSelect: setSelected
76
+ }), /* @__PURE__ */ jsx("div", {
77
+ className: "min-w-0 flex-1",
78
+ children: /* @__PURE__ */ jsx(PageBody, { uri: pageUri(project, slug, page.path) })
79
+ })]
80
+ })]
81
+ });
82
+ }
83
+ //#endregion
84
+ export { WikiSiteView };
85
+
86
+ //# sourceMappingURL=wiki-site-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wiki-site-view.js","names":[],"sources":["../../src/internal/wiki-site-view.tsx"],"sourcesContent":["import { Markdown } from \"@statewalker/mime.view.markdown\";\nimport { Button } from \"@statewalker/ui.view.shadcn\";\nimport { type ReactElement, useState } from \"react\";\nimport { pageUri, useFileText, useSiteManifest } from \"./site-data.js\";\nimport { SiteNav } from \"./site-nav.js\";\n\ninterface WikiSiteViewProps {\n project: string;\n slug: string;\n}\n\nfunction Centered({ children }: { children: React.ReactNode }): ReactElement {\n return (\n <div className=\"flex h-full items-center justify-center p-6 text-sm text-muted-foreground\">\n {children}\n </div>\n );\n}\n\nfunction PageBody({ uri }: { uri: string }): ReactElement {\n const state = useFileText(uri);\n if (state.status === \"loading\") return <Centered>Loading…</Centered>;\n if (state.status === \"error\") return <Centered>Failed to load page: {state.message}</Centered>;\n return (\n <div className=\"prose prose-sm dark:prose-invert max-w-none overflow-auto p-6\">\n <Markdown>{state.text}</Markdown>\n </div>\n );\n}\n\n/**\n * Site-browser panel: reads a site's `site.json` manifest and renders a TOC\n * sidebar + breadcrumbs + prev/next over its pages, with the selected page's\n * markdown body rendered via the shared `Markdown` component. ADR-0002: all wiki\n * React lives here, never in `@statewalker/wiki`.\n */\nexport function WikiSiteView({ project, slug }: WikiSiteViewProps): ReactElement {\n const manifest = useSiteManifest(project, slug);\n const [selected, setSelected] = useState(0);\n\n if (manifest.status === \"loading\") return <Centered>Loading site…</Centered>;\n if (manifest.status === \"error\")\n return <Centered>Failed to load site: {manifest.message}</Centered>;\n if (manifest.status === \"empty\" || manifest.manifest.pages.length === 0) {\n return <Centered>No site generated yet.</Centered>;\n }\n\n const pages = manifest.manifest.pages;\n const index = Math.min(Math.max(selected, 0), pages.length - 1);\n const page = pages[index];\n if (!page) return <Centered>No site generated yet.</Centered>;\n\n return (\n <div className=\"flex h-full flex-col\">\n <div className=\"flex items-center gap-2 border-b px-3 py-2 text-sm\">\n <span className=\"font-medium\">{manifest.manifest.title}</span>\n <span className=\"text-muted-foreground\">›</span>\n <span>{page.title}</span>\n <div className=\"ml-auto flex gap-1\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n disabled={index <= 0}\n onClick={() => setSelected(index - 1)}\n >\n Prev\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n disabled={index >= pages.length - 1}\n onClick={() => setSelected(index + 1)}\n >\n Next\n </Button>\n </div>\n </div>\n <div className=\"flex min-h-0 flex-1\">\n <SiteNav pages={pages} selected={index} onSelect={setSelected} />\n <div className=\"min-w-0 flex-1\">\n <PageBody uri={pageUri(project, slug, page.path)} />\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;AAWA,SAAS,SAAS,EAAE,YAAyD;CAC3E,OACE,oBAAC,OAAD;EAAK,WAAU;EACZ;CACE,CAAA;AAET;AAEA,SAAS,SAAS,EAAE,OAAsC;CACxD,MAAM,QAAQ,YAAY,GAAG;CAC7B,IAAI,MAAM,WAAW,WAAW,OAAO,oBAAC,UAAD,EAAA,UAAU,WAAkB,CAAA;CACnE,IAAI,MAAM,WAAW,SAAS,OAAO,qBAAC,UAAD,EAAA,UAAA,CAAU,yBAAsB,MAAM,OAAkB,EAAA,CAAA;CAC7F,OACE,oBAAC,OAAD;EAAK,WAAU;EACb,UAAA,oBAAC,UAAD,EAAA,UAAW,MAAM,KAAe,CAAA;CAC7B,CAAA;AAET;;;;;;;AAQA,SAAgB,aAAa,EAAE,SAAS,QAAyC;CAC/E,MAAM,WAAW,gBAAgB,SAAS,IAAI;CAC9C,MAAM,CAAC,UAAU,eAAe,SAAS,CAAC;CAE1C,IAAI,SAAS,WAAW,WAAW,OAAO,oBAAC,UAAD,EAAA,UAAU,gBAAuB,CAAA;CAC3E,IAAI,SAAS,WAAW,SACtB,OAAO,qBAAC,UAAD,EAAA,UAAA,CAAU,yBAAsB,SAAS,OAAkB,EAAA,CAAA;CACpE,IAAI,SAAS,WAAW,WAAW,SAAS,SAAS,MAAM,WAAW,GACpE,OAAO,oBAAC,UAAD,EAAA,UAAU,yBAAgC,CAAA;CAGnD,MAAM,QAAQ,SAAS,SAAS;CAChC,MAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,UAAU,CAAC,GAAG,MAAM,SAAS,CAAC;CAC9D,MAAM,OAAO,MAAM;CACnB,IAAI,CAAC,MAAM,OAAO,oBAAC,UAAD,EAAA,UAAU,yBAAgC,CAAA;CAE5D,OACE,qBAAC,OAAD;EAAK,WAAU;EAAf,UAAA,CACE,qBAAC,OAAD;GAAK,WAAU;GAAf,UAAA;IACE,oBAAC,QAAD;KAAM,WAAU;KAAe,UAAA,SAAS,SAAS;IAAY,CAAA;IAC7D,oBAAC,QAAD;KAAM,WAAU;KAAwB,UAAA;IAAO,CAAA;IAC/C,oBAAC,QAAD,EAAA,UAAO,KAAK,MAAY,CAAA;IACxB,qBAAC,OAAD;KAAK,WAAU;KAAf,UAAA,CACE,oBAAC,QAAD;MACE,SAAQ;MACR,MAAK;MACL,UAAU,SAAS;MACnB,eAAe,YAAY,QAAQ,CAAC;MACrC,UAAA;KAEO,CAAA,GACR,oBAAC,QAAD;MACE,SAAQ;MACR,MAAK;MACL,UAAU,SAAS,MAAM,SAAS;MAClC,eAAe,YAAY,QAAQ,CAAC;MACrC,UAAA;KAEO,CAAA,CACL;;GACF;EACL,CAAA,GAAA,qBAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACE,oBAAC,SAAD;IAAgB;IAAO,UAAU;IAAO,UAAU;GAAc,CAAA,GAChE,oBAAC,OAAD;IAAK,WAAU;IACb,UAAA,oBAAC,UAAD,EAAU,KAAK,QAAQ,SAAS,MAAM,KAAK,IAAI,EAAI,CAAA;GAChD,CAAA,CACF;EACF,CAAA,CAAA;;AAET"}
@@ -0,0 +1,64 @@
1
+ import { Spec } from "@json-render/core";
2
+ import { z } from "zod";
3
+ //#region src/public/catalog.d.ts
4
+ /**
5
+ * Catalog declaration for the wiki site-browser panel — typed schema only. The
6
+ * `WikiSiteView` React binding lives in `init.tsx` (ADR-0002 logic/renderer split).
7
+ * The single element carries `{ project, slug }`; the component reads `site.json`.
8
+ */
9
+ export declare const wikiSiteCatalog: import("@json-render/core").Catalog<{
10
+ spec: import("@json-render/core").SchemaType<"object", {
11
+ root: import("@json-render/core").SchemaType<"string", unknown>;
12
+ elements: import("@json-render/core").SchemaType<"record", import("@json-render/core").SchemaType<"object", {
13
+ type: import("@json-render/core").SchemaType<"ref", string>;
14
+ props: import("@json-render/core").SchemaType<"propsOf", string>;
15
+ children: import("@json-render/core").SchemaType<"array", import("@json-render/core").SchemaType<"string", unknown>>;
16
+ slots: {
17
+ optional: true;
18
+ kind: "record";
19
+ inner?: import("@json-render/core").SchemaType<"array", import("@json-render/core").SchemaType<"string", unknown>> | undefined;
20
+ };
21
+ visible: {
22
+ optional: true;
23
+ kind: "any";
24
+ inner?: unknown;
25
+ };
26
+ repeat: {
27
+ optional: true;
28
+ kind: "any";
29
+ inner?: unknown;
30
+ };
31
+ }>>;
32
+ }>;
33
+ catalog: import("@json-render/core").SchemaType<"object", {
34
+ components: import("@json-render/core").SchemaType<"map", {
35
+ props: import("@json-render/core").SchemaType<"zod", unknown>;
36
+ slots: import("@json-render/core").SchemaType<"array", import("@json-render/core").SchemaType<"string", unknown>>;
37
+ description: import("@json-render/core").SchemaType<"string", unknown>;
38
+ example: import("@json-render/core").SchemaType<"any", unknown>;
39
+ }>;
40
+ actions: import("@json-render/core").SchemaType<"map", {
41
+ params: import("@json-render/core").SchemaType<"zod", unknown>;
42
+ description: import("@json-render/core").SchemaType<"string", unknown>;
43
+ }>;
44
+ }>;
45
+ }, {
46
+ components: {
47
+ WikiSiteView: {
48
+ props: z.ZodObject<{
49
+ project: z.ZodString;
50
+ slug: z.ZodString;
51
+ }, z.core.$strip>;
52
+ };
53
+ };
54
+ actions: {};
55
+ }>;
56
+ export declare const WIKI_SITE_CATALOG_ID = "wiki-site";
57
+ /** Build the one-element spec for a (project, slug) site. */
58
+ export declare function makeWikiSiteSpec(project: string, slug: string): Spec;
59
+ /** Deterministic panel id so reopening the same site focuses the tab. */
60
+ export declare function wikiSitePanelId(project: string, slug: string): string;
61
+ /** Deterministic spec id; pairs with `wikiSitePanelId`. */
62
+ export declare function wikiSiteSpecId(project: string, slug: string): string;
63
+ //#endregion
64
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","names":[],"sources":["../../src/public/catalog.ts"],"mappings":";;;;;;;;qBASa,6CAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAExB;MAAgB,OAAK,EAAA;;;;;;;;qBAKZ;;wBAGG,iBAAiB,iBAAiB,eAAe;;wBAcjD,gBAAgB,iBAAiB;;wBAKjC,eAAe,iBAAiB"}
@@ -0,0 +1,43 @@
1
+ import { schema } from "@statewalker/render.view.react";
2
+ import { defineCatalog } from "@json-render/core";
3
+ import { z } from "zod";
4
+ //#region src/public/catalog.ts
5
+ /**
6
+ * Catalog declaration for the wiki site-browser panel — typed schema only. The
7
+ * `WikiSiteView` React binding lives in `init.tsx` (ADR-0002 logic/renderer split).
8
+ * The single element carries `{ project, slug }`; the component reads `site.json`.
9
+ */
10
+ const wikiSiteCatalog = defineCatalog(schema, {
11
+ components: { WikiSiteView: { props: z.object({
12
+ project: z.string(),
13
+ slug: z.string()
14
+ }) } },
15
+ actions: {}
16
+ });
17
+ const WIKI_SITE_CATALOG_ID = "wiki-site";
18
+ /** Build the one-element spec for a (project, slug) site. */
19
+ function makeWikiSiteSpec(project, slug) {
20
+ return {
21
+ root: "panel",
22
+ elements: { panel: {
23
+ type: "WikiSiteView",
24
+ props: {
25
+ project,
26
+ slug
27
+ },
28
+ children: []
29
+ } }
30
+ };
31
+ }
32
+ /** Deterministic panel id so reopening the same site focuses the tab. */
33
+ function wikiSitePanelId(project, slug) {
34
+ return `wiki-site:${project}/${slug}`;
35
+ }
36
+ /** Deterministic spec id; pairs with `wikiSitePanelId`. */
37
+ function wikiSiteSpecId(project, slug) {
38
+ return `spec:wiki-site:${project}/${slug}`;
39
+ }
40
+ //#endregion
41
+ export { WIKI_SITE_CATALOG_ID, makeWikiSiteSpec, wikiSiteCatalog, wikiSitePanelId, wikiSiteSpecId };
42
+
43
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.js","names":[],"sources":["../../src/public/catalog.ts"],"sourcesContent":["import { defineCatalog, type Spec } from \"@json-render/core\";\nimport { schema } from \"@statewalker/render.view.react\";\nimport { z } from \"zod\";\n\n/**\n * Catalog declaration for the wiki site-browser panel — typed schema only. The\n * `WikiSiteView` React binding lives in `init.tsx` (ADR-0002 logic/renderer split).\n * The single element carries `{ project, slug }`; the component reads `site.json`.\n */\nexport const wikiSiteCatalog = defineCatalog(schema, {\n components: {\n WikiSiteView: { props: z.object({ project: z.string(), slug: z.string() }) },\n },\n actions: {},\n});\n\nexport const WIKI_SITE_CATALOG_ID = \"wiki-site\";\n\n/** Build the one-element spec for a (project, slug) site. */\nexport function makeWikiSiteSpec(project: string, slug: string): Spec {\n return {\n root: \"panel\",\n elements: {\n panel: {\n type: \"WikiSiteView\",\n props: { project, slug },\n children: [],\n },\n },\n } as Spec;\n}\n\n/** Deterministic panel id so reopening the same site focuses the tab. */\nexport function wikiSitePanelId(project: string, slug: string): string {\n return `wiki-site:${project}/${slug}`;\n}\n\n/** Deterministic spec id; pairs with `wikiSitePanelId`. */\nexport function wikiSiteSpecId(project: string, slug: string): string {\n return `spec:wiki-site:${project}/${slug}`;\n}\n"],"mappings":";;;;;;;;;AASA,MAAa,kBAAkB,cAAc,QAAQ;CACnD,YAAY,EACV,cAAc,EAAE,OAAO,EAAE,OAAO;EAAE,SAAS,EAAE,OAAO;EAAG,MAAM,EAAE,OAAO;CAAE,CAAC,EAAE,EAC7E;CACA,SAAS,CAAC;AACZ,CAAC;AAED,MAAa,uBAAuB;;AAGpC,SAAgB,iBAAiB,SAAiB,MAAoB;CACpE,OAAO;EACL,MAAM;EACN,UAAU,EACR,OAAO;GACL,MAAM;GACN,OAAO;IAAE;IAAS;GAAK;GACvB,UAAU,CAAC;EACb,EACF;CACF;AACF;;AAGA,SAAgB,gBAAgB,SAAiB,MAAsB;CACrE,OAAO,aAAa,QAAQ,GAAG;AACjC;;AAGA,SAAgB,eAAe,SAAiB,MAAsB;CACpE,OAAO,kBAAkB,QAAQ,GAAG;AACtC"}
@@ -0,0 +1,3 @@
1
+ import { WIKI_SITE_CATALOG_ID, makeWikiSiteSpec, wikiSiteCatalog, wikiSitePanelId, wikiSiteSpecId } from "./catalog.js";
2
+ import { OpenWikiSiteCommand as OpenWikiSiteCommand$1, OpenWikiSitePayload } from "@statewalker/wiki.core";
3
+ export { OpenWikiSiteCommand$1 as OpenWikiSiteCommand, type OpenWikiSitePayload, WIKI_SITE_CATALOG_ID, makeWikiSiteSpec, wikiSiteCatalog, wikiSitePanelId, wikiSiteSpecId };
@@ -0,0 +1,3 @@
1
+ import { WIKI_SITE_CATALOG_ID, makeWikiSiteSpec, wikiSiteCatalog, wikiSitePanelId, wikiSiteSpecId } from "./catalog.js";
2
+ import { OpenWikiSiteCommand as OpenWikiSiteCommand$1 } from "@statewalker/wiki.core";
3
+ export { OpenWikiSiteCommand$1 as OpenWikiSiteCommand, WIKI_SITE_CATALOG_ID, makeWikiSiteSpec, wikiSiteCatalog, wikiSitePanelId, wikiSiteSpecId };
@@ -0,0 +1,10 @@
1
+ //#region src/public/init.d.ts
2
+ /**
3
+ * Renderer-fragment init for `@statewalker/wiki.view.react`. Registers the
4
+ * `wiki-site` json-render catalog (binding `WikiSiteView`) and the `wiki:open-site`
5
+ * command that opens a generated thematic site as a dock panel. ADR-0002: all wiki
6
+ * React lives here, never in `@statewalker/wiki`.
7
+ */
8
+ export default function initWikiReact(ctx: Record<string, unknown>): () => Promise<void>;
9
+ //#endregion
10
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","names":[],"sources":["../../src/public/init.tsx"],"mappings":";;;;;;;wBAsCwB,cAAc,KAAK,gCAAgC"}
@@ -0,0 +1,88 @@
1
+ import { WikiSiteView } from "../internal/wiki-site-view.js";
2
+ import { WIKI_SITE_CATALOG_ID, makeWikiSiteSpec, wikiSiteCatalog, wikiSitePanelId, wikiSiteSpecId } from "./catalog.js";
3
+ import { LayoutStore, SpecStore, catalogsSlot, restorePanelSpecsFromLayout } from "@statewalker/render.core";
4
+ import { defineRegistry } from "@statewalker/render.view.react";
5
+ import { Commands } from "@statewalker/shared-commands";
6
+ import { newRegistry } from "@statewalker/shared-registry";
7
+ import { Slots } from "@statewalker/shared-slots";
8
+ import { ShowDockPanelCommand } from "@statewalker/shell.core";
9
+ import { dockTabIconSlot } from "@statewalker/shell.view.react";
10
+ import { OpenWikiSiteCommand } from "@statewalker/wiki.core";
11
+ import { getWorkspace } from "@statewalker/workspace.core";
12
+ import { BookText } from "lucide-react";
13
+ import { jsx } from "react/jsx-runtime";
14
+ //#region src/public/init.tsx
15
+ /** Split a restored panel id suffix (`<project>/<slug>`) back into its parts. */
16
+ function splitSiteId(id) {
17
+ const slash = id.indexOf("/");
18
+ return slash < 0 ? {
19
+ project: id,
20
+ slug: ""
21
+ } : {
22
+ project: id.slice(0, slash),
23
+ slug: id.slice(slash + 1)
24
+ };
25
+ }
26
+ /**
27
+ * Renderer-fragment init for `@statewalker/wiki.view.react`. Registers the
28
+ * `wiki-site` json-render catalog (binding `WikiSiteView`) and the `wiki:open-site`
29
+ * command that opens a generated thematic site as a dock panel. ADR-0002: all wiki
30
+ * React lives here, never in `@statewalker/wiki`.
31
+ */
32
+ function initWikiReact(ctx) {
33
+ const [register, cleanup] = newRegistry();
34
+ const workspace = getWorkspace(ctx);
35
+ const slots = workspace.requireAdapter(Slots);
36
+ const store = workspace.requireAdapter(SpecStore);
37
+ const commands = workspace.requireAdapter(Commands);
38
+ const layoutStore = workspace.requireAdapter(LayoutStore);
39
+ register(workspace.onLoad(() => {
40
+ restorePanelSpecsFromLayout({
41
+ store,
42
+ layout: layoutStore.get(),
43
+ panelIdPrefix: "wiki-site:",
44
+ catalogId: WIKI_SITE_CATALOG_ID,
45
+ buildSpec: (id) => {
46
+ const { project, slug } = splitSiteId(id);
47
+ return makeWikiSiteSpec(project, slug);
48
+ },
49
+ buildSpecId: (id) => {
50
+ const { project, slug } = splitSiteId(id);
51
+ return wikiSiteSpecId(project, slug);
52
+ }
53
+ });
54
+ }));
55
+ const { registry } = defineRegistry(wikiSiteCatalog, {
56
+ components: { WikiSiteView: ({ props }) => /* @__PURE__ */ jsx(WikiSiteView, {
57
+ project: props.project,
58
+ slug: props.slug
59
+ }) },
60
+ actions: {}
61
+ });
62
+ register(slots.register(catalogsSlot, WIKI_SITE_CATALOG_ID, registry));
63
+ register(commands.listen(OpenWikiSiteCommand, (command) => {
64
+ const { project, slug } = command.payload;
65
+ const specId = wikiSiteSpecId(project, slug);
66
+ const panelId = wikiSitePanelId(project, slug);
67
+ if (!store.get(specId)) store.create({
68
+ id: specId,
69
+ catalogId: WIKI_SITE_CATALOG_ID,
70
+ spec: makeWikiSiteSpec(project, slug)
71
+ });
72
+ commands.call(ShowDockPanelCommand, {
73
+ panelId,
74
+ specId,
75
+ title: slug
76
+ }).promise.then(() => command.resolve()).catch((error) => command.reject(error));
77
+ return true;
78
+ }));
79
+ register(slots.provide(dockTabIconSlot, {
80
+ panelIdPrefix: "wiki-site:",
81
+ Icon: BookText
82
+ }));
83
+ return cleanup;
84
+ }
85
+ //#endregion
86
+ export { initWikiReact as default };
87
+
88
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","names":[],"sources":["../../src/public/init.tsx"],"sourcesContent":["import {\n catalogsSlot,\n LayoutStore,\n restorePanelSpecsFromLayout,\n SpecStore,\n} from \"@statewalker/render.core\";\nimport { defineRegistry } from \"@statewalker/render.view.react\";\nimport { Commands } from \"@statewalker/shared-commands\";\nimport { newRegistry } from \"@statewalker/shared-registry\";\nimport { Slots } from \"@statewalker/shared-slots\";\nimport { ShowDockPanelCommand } from \"@statewalker/shell.core\";\nimport { dockTabIconSlot } from \"@statewalker/shell.view.react\";\nimport { OpenWikiSiteCommand } from \"@statewalker/wiki.core\";\nimport { getWorkspace } from \"@statewalker/workspace.core\";\nimport { BookText } from \"lucide-react\";\nimport { WikiSiteView } from \"../internal/wiki-site-view.js\";\nimport {\n makeWikiSiteSpec,\n WIKI_SITE_CATALOG_ID,\n wikiSiteCatalog,\n wikiSitePanelId,\n wikiSiteSpecId,\n} from \"./catalog.js\";\n\n/** Split a restored panel id suffix (`<project>/<slug>`) back into its parts. */\nfunction splitSiteId(id: string): { project: string; slug: string } {\n const slash = id.indexOf(\"/\");\n return slash < 0\n ? { project: id, slug: \"\" }\n : { project: id.slice(0, slash), slug: id.slice(slash + 1) };\n}\n\n/**\n * Renderer-fragment init for `@statewalker/wiki.view.react`. Registers the\n * `wiki-site` json-render catalog (binding `WikiSiteView`) and the `wiki:open-site`\n * command that opens a generated thematic site as a dock panel. ADR-0002: all wiki\n * React lives here, never in `@statewalker/wiki`.\n */\nexport default function initWikiReact(ctx: Record<string, unknown>): () => Promise<void> {\n const [register, cleanup] = newRegistry();\n const workspace = getWorkspace(ctx);\n const slots = workspace.requireAdapter(Slots);\n const store = workspace.requireAdapter(SpecStore);\n const commands = workspace.requireAdapter(Commands);\n const layoutStore = workspace.requireAdapter(LayoutStore);\n\n // Pre-allocate specs for restored wiki-site tabs on workspace-connect,\n // reading the layout held by the LayoutStore adapter (see mime.view.pdf).\n register(\n workspace.onLoad(() => {\n restorePanelSpecsFromLayout({\n store,\n layout: layoutStore.get(),\n panelIdPrefix: \"wiki-site:\",\n catalogId: WIKI_SITE_CATALOG_ID,\n buildSpec: (id) => {\n const { project, slug } = splitSiteId(id);\n return makeWikiSiteSpec(project, slug);\n },\n buildSpecId: (id) => {\n const { project, slug } = splitSiteId(id);\n return wikiSiteSpecId(project, slug);\n },\n });\n }),\n );\n\n const { registry } = defineRegistry(wikiSiteCatalog, {\n components: {\n WikiSiteView: ({ props }) => <WikiSiteView project={props.project} slug={props.slug} />,\n },\n actions: {},\n });\n register(slots.register(catalogsSlot, WIKI_SITE_CATALOG_ID, registry));\n\n register(\n commands.listen(OpenWikiSiteCommand, (command) => {\n const { project, slug } = command.payload;\n const specId = wikiSiteSpecId(project, slug);\n const panelId = wikiSitePanelId(project, slug);\n if (!store.get(specId)) {\n store.create({\n id: specId,\n catalogId: WIKI_SITE_CATALOG_ID,\n spec: makeWikiSiteSpec(project, slug),\n });\n }\n void commands\n .call(ShowDockPanelCommand, { panelId, specId, title: slug })\n .promise.then(() => command.resolve())\n .catch((error) => command.reject(error));\n return true;\n }),\n );\n\n register(slots.provide(dockTabIconSlot, { panelIdPrefix: \"wiki-site:\", Icon: BookText }));\n\n return cleanup;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAyBA,SAAS,YAAY,IAA+C;CAClE,MAAM,QAAQ,GAAG,QAAQ,GAAG;CAC5B,OAAO,QAAQ,IACX;EAAE,SAAS;EAAI,MAAM;CAAG,IACxB;EAAE,SAAS,GAAG,MAAM,GAAG,KAAK;EAAG,MAAM,GAAG,MAAM,QAAQ,CAAC;CAAE;AAC/D;;;;;;;AAQA,SAAwB,cAAc,KAAmD;CACvF,MAAM,CAAC,UAAU,WAAW,YAAY;CACxC,MAAM,YAAY,aAAa,GAAG;CAClC,MAAM,QAAQ,UAAU,eAAe,KAAK;CAC5C,MAAM,QAAQ,UAAU,eAAe,SAAS;CAChD,MAAM,WAAW,UAAU,eAAe,QAAQ;CAClD,MAAM,cAAc,UAAU,eAAe,WAAW;CAIxD,SACE,UAAU,aAAa;EACrB,4BAA4B;GAC1B;GACA,QAAQ,YAAY,IAAI;GACxB,eAAe;GACf,WAAW;GACX,YAAY,OAAO;IACjB,MAAM,EAAE,SAAS,SAAS,YAAY,EAAE;IACxC,OAAO,iBAAiB,SAAS,IAAI;GACvC;GACA,cAAc,OAAO;IACnB,MAAM,EAAE,SAAS,SAAS,YAAY,EAAE;IACxC,OAAO,eAAe,SAAS,IAAI;GACrC;EACF,CAAC;CACH,CAAC,CACH;CAEA,MAAM,EAAE,aAAa,eAAe,iBAAiB;EACnD,YAAY,EACV,eAAe,EAAE,YAAY,oBAAC,cAAD;GAAc,SAAS,MAAM;GAAS,MAAM,MAAM;EAAO,CAAA,EACxF;EACA,SAAS,CAAC;CACZ,CAAC;CACD,SAAS,MAAM,SAAS,cAAc,sBAAsB,QAAQ,CAAC;CAErE,SACE,SAAS,OAAO,sBAAsB,YAAY;EAChD,MAAM,EAAE,SAAS,SAAS,QAAQ;EAClC,MAAM,SAAS,eAAe,SAAS,IAAI;EAC3C,MAAM,UAAU,gBAAgB,SAAS,IAAI;EAC7C,IAAI,CAAC,MAAM,IAAI,MAAM,GACnB,MAAM,OAAO;GACX,IAAI;GACJ,WAAW;GACX,MAAM,iBAAiB,SAAS,IAAI;EACtC,CAAC;EAEH,SACG,KAAK,sBAAsB;GAAE;GAAS;GAAQ,OAAO;EAAK,CAAC,CAAC,CAC5D,QAAQ,WAAW,QAAQ,QAAQ,CAAC,CAAC,CACrC,OAAO,UAAU,QAAQ,OAAO,KAAK,CAAC;EACzC,OAAO;CACT,CAAC,CACH;CAEA,SAAS,MAAM,QAAQ,iBAAiB;EAAE,eAAe;EAAc,MAAM;CAAS,CAAC,CAAC;CAExF,OAAO;AACT"}
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@statewalker/wiki.view.react",
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Wiki renderer fragment: search panel, indexer panel, and browsing/citation views (DockView specs / slot contributions) consuming the React-free @statewalker/wiki adapters.",
7
+ "homepage": "https://github.com/statewalker/statewalker-workbench",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+ssh://git@github.com/statewalker/statewalker-workbench.git"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "source": "./src/index.ts",
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js",
18
+ "default": "./dist/index.js"
19
+ },
20
+ "./fragment": {
21
+ "source": "./src/fragment.ts",
22
+ "types": "./dist/fragment.d.ts",
23
+ "import": "./dist/fragment.js",
24
+ "default": "./dist/fragment.js"
25
+ },
26
+ "./styles": "./src/styles.css"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "src"
31
+ ],
32
+ "dependencies": {
33
+ "@json-render/core": "^0.21.0",
34
+ "@statewalker/mime.view.markdown": "^0.2.0",
35
+ "@statewalker/mime.core": "^0.5.0",
36
+ "@statewalker/render.core": "^0.5.0",
37
+ "@statewalker/render.view.react": "^0.2.0",
38
+ "@statewalker/shared-commands": "^0.2.0",
39
+ "@statewalker/shared-registry": "^0.1.0",
40
+ "@statewalker/shared-slots": "^0.2.0",
41
+ "@statewalker/shell.core": "^0.5.0",
42
+ "@statewalker/shell.view.react": "^0.2.0",
43
+ "@statewalker/ui.view.react": "^0.2.0",
44
+ "@statewalker/ui.view.shadcn": "^0.1.1",
45
+ "@statewalker/workspace.core": "^0.5.0",
46
+ "lucide-react": "^1.51.0",
47
+ "zod": "^4.6.5",
48
+ "@statewalker/wiki.core": "^0.1.1"
49
+ },
50
+ "peerDependencies": {
51
+ "react": "^19.3.0",
52
+ "react-dom": "^19.3.0"
53
+ },
54
+ "devDependencies": {
55
+ "@testing-library/react": "^16.3.3",
56
+ "@types/react": "^19.3.0",
57
+ "@types/react-dom": "^19.3.0",
58
+ "react": "^19.3.0",
59
+ "react-dom": "^19.3.0",
60
+ "tsdown": "^0.23.0",
61
+ "typescript": "^7.0.2",
62
+ "vitest": "^5.0.3"
63
+ },
64
+ "sideEffects": [
65
+ "**/*.css"
66
+ ],
67
+ "publishConfig": {
68
+ "access": "public"
69
+ },
70
+ "scripts": {
71
+ "test": "vitest run --passWithNoTests",
72
+ "test:watch": "vitest --passWithNoTests",
73
+ "typecheck": "tsc --noEmit",
74
+ "lint": "biome check --write .",
75
+ "format": "biome format --write .",
76
+ "build": "tsdown"
77
+ }
78
+ }
@@ -0,0 +1 @@
1
+ export { default } from "./public/init.js";
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./public/index.js";
@@ -0,0 +1,90 @@
1
+ import { Commands } from "@statewalker/shared-commands";
2
+ import { useAppWorkspace } from "@statewalker/ui.view.react";
3
+ import type { SiteManifest } from "@statewalker/wiki.core";
4
+ import { LoadFileCommand } from "@statewalker/workspace.core";
5
+ import { useEffect, useState } from "react";
6
+
7
+ /** Workspace-absolute path of a site's `site.json` manifest. */
8
+ export function siteJsonPath(project: string, slug: string): string {
9
+ return `/${project}/sites/${slug}/site.json`;
10
+ }
11
+
12
+ /** Workspace-absolute path of a generated page (manifest `page.path` is a file name). */
13
+ export function pageUri(project: string, slug: string, pagePath: string): string {
14
+ return `/${project}/sites/${slug}/${pagePath}`;
15
+ }
16
+
17
+ export type ManifestState =
18
+ | { status: "loading" }
19
+ | { status: "ready"; manifest: SiteManifest }
20
+ | { status: "empty" }
21
+ | { status: "error"; message: string };
22
+
23
+ /** Load a site's manifest via `files:load-file`. A missing file resolves to `empty`. */
24
+ export function useSiteManifest(project: string, slug: string): ManifestState {
25
+ const workspace = useAppWorkspace();
26
+ const commands = workspace.requireAdapter(Commands);
27
+ const [state, setState] = useState<ManifestState>({ status: "loading" });
28
+
29
+ useEffect(() => {
30
+ let cancelled = false;
31
+ setState({ status: "loading" });
32
+ commands
33
+ .call(LoadFileCommand, { path: siteJsonPath(project, slug) })
34
+ .promise.then((loaded) => {
35
+ if (cancelled) return;
36
+ try {
37
+ const manifest = JSON.parse(new TextDecoder().decode(loaded.bytes)) as SiteManifest;
38
+ setState({ status: "ready", manifest });
39
+ } catch (error) {
40
+ setState({
41
+ status: "error",
42
+ message: error instanceof Error ? error.message : String(error),
43
+ });
44
+ }
45
+ })
46
+ .catch(() => {
47
+ if (!cancelled) setState({ status: "empty" });
48
+ });
49
+ return () => {
50
+ cancelled = true;
51
+ };
52
+ }, [project, slug, commands]);
53
+
54
+ return state;
55
+ }
56
+
57
+ export type FileTextState =
58
+ | { status: "loading" }
59
+ | { status: "ready"; text: string }
60
+ | { status: "error"; message: string };
61
+
62
+ /** Load a markdown page's text via `files:load-file`. */
63
+ export function useFileText(uri: string): FileTextState {
64
+ const workspace = useAppWorkspace();
65
+ const commands = workspace.requireAdapter(Commands);
66
+ const [state, setState] = useState<FileTextState>({ status: "loading" });
67
+
68
+ useEffect(() => {
69
+ let cancelled = false;
70
+ setState({ status: "loading" });
71
+ commands
72
+ .call(LoadFileCommand, { path: uri.replace(/^file:\/\//, "") })
73
+ .promise.then((loaded) => {
74
+ if (cancelled) return;
75
+ setState({ status: "ready", text: new TextDecoder().decode(loaded.bytes) });
76
+ })
77
+ .catch((error: unknown) => {
78
+ if (cancelled) return;
79
+ setState({
80
+ status: "error",
81
+ message: error instanceof Error ? error.message : String(error),
82
+ });
83
+ });
84
+ return () => {
85
+ cancelled = true;
86
+ };
87
+ }, [uri, commands]);
88
+
89
+ return state;
90
+ }
@@ -0,0 +1,32 @@
1
+ import { ScrollArea } from "@statewalker/ui.view.shadcn";
2
+ import type { SitePage } from "@statewalker/wiki.core";
3
+ import type { ReactElement } from "react";
4
+
5
+ interface SiteNavProps {
6
+ pages: SitePage[];
7
+ selected: number;
8
+ onSelect: (index: number) => void;
9
+ }
10
+
11
+ /** Left-rail table of contents: the site's pages in manifest order, single-select. */
12
+ export function SiteNav({ pages, selected, onSelect }: SiteNavProps): ReactElement {
13
+ return (
14
+ <ScrollArea className="h-full w-56 shrink-0 border-r">
15
+ <nav className="flex flex-col gap-0.5 p-2">
16
+ {pages.map((page, i) => (
17
+ <button
18
+ key={page.id}
19
+ type="button"
20
+ onClick={() => onSelect(i)}
21
+ aria-current={i === selected ? "page" : undefined}
22
+ className={`rounded px-2 py-1 text-left text-sm ${
23
+ i === selected ? "bg-accent font-medium" : "hover:bg-accent/50"
24
+ }`}
25
+ >
26
+ {page.title}
27
+ </button>
28
+ ))}
29
+ </nav>
30
+ </ScrollArea>
31
+ );
32
+ }
@@ -0,0 +1,82 @@
1
+ import { Commands } from "@statewalker/shared-commands";
2
+ import { AppWorkspaceProvider } from "@statewalker/ui.view.react";
3
+ import { LoadFileCommand, Workspace } from "@statewalker/workspace.core";
4
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
5
+ import { afterEach, describe, expect, it } from "vitest";
6
+ import { WikiSiteView } from "./wiki-site-view.js";
7
+
8
+ afterEach(cleanup);
9
+
10
+ const MANIFEST = {
11
+ slug: "themes",
12
+ title: "Acme themes",
13
+ generated: "",
14
+ pages: [
15
+ { id: "founders", title: "Founders", path: "founders.md", order: 0, promptHash: "a" },
16
+ { id: "products", title: "Products", path: "products.md", order: 1, promptHash: "b" },
17
+ ],
18
+ };
19
+
20
+ const FILES: Record<string, string> = {
21
+ "/proj/sites/themes/site.json": JSON.stringify(MANIFEST),
22
+ "/proj/sites/themes/founders.md": "# Founders\n\nJane founded Acme.",
23
+ "/proj/sites/themes/products.md": "# Products\n\nWe make widgets.",
24
+ };
25
+
26
+ function makeWorkspace(files: Record<string, string>): Workspace {
27
+ const ws = new Workspace();
28
+ const commands = ws.requireAdapter(Commands);
29
+ commands.listen(LoadFileCommand, (command) => {
30
+ const content = files[command.payload.path];
31
+ if (content === undefined) {
32
+ command.reject(new Error(`not found: ${command.payload.path}`));
33
+ return true;
34
+ }
35
+ command.resolve({
36
+ path: command.payload.path,
37
+ bytes: new TextEncoder().encode(content),
38
+ mimeType: "text/markdown",
39
+ });
40
+ return true;
41
+ });
42
+ return ws;
43
+ }
44
+
45
+ function renderSite(ws: Workspace) {
46
+ return render(
47
+ <AppWorkspaceProvider workspace={ws}>
48
+ <WikiSiteView project="proj" slug="themes" />
49
+ </AppWorkspaceProvider>,
50
+ );
51
+ }
52
+
53
+ describe("WikiSiteView", () => {
54
+ it("renders the TOC in order and shows the first page", async () => {
55
+ renderSite(makeWorkspace(FILES));
56
+ // TOC sidebar buttons, in manifest order.
57
+ expect(await screen.findByRole("button", { name: "Founders" })).toBeTruthy();
58
+ expect(screen.getByRole("button", { name: "Products" })).toBeTruthy();
59
+ // First page body is shown.
60
+ expect(await screen.findByText("Jane founded Acme.")).toBeTruthy();
61
+ });
62
+
63
+ it("selecting a sidebar entry swaps the page body", async () => {
64
+ renderSite(makeWorkspace(FILES));
65
+ fireEvent.click(await screen.findByRole("button", { name: "Products" }));
66
+ expect(await screen.findByText("We make widgets.")).toBeTruthy();
67
+ });
68
+
69
+ it("prev/next navigates between pages", async () => {
70
+ renderSite(makeWorkspace(FILES));
71
+ await screen.findByText("Jane founded Acme.");
72
+ fireEvent.click(screen.getByRole("button", { name: "Next" }));
73
+ expect(await screen.findByText("We make widgets.")).toBeTruthy();
74
+ fireEvent.click(screen.getByRole("button", { name: "Prev" }));
75
+ expect(await screen.findByText("Jane founded Acme.")).toBeTruthy();
76
+ });
77
+
78
+ it("shows a placeholder when no site exists", async () => {
79
+ renderSite(makeWorkspace({}));
80
+ expect(await screen.findByText("No site generated yet.")).toBeTruthy();
81
+ });
82
+ });
@@ -0,0 +1,86 @@
1
+ import { Markdown } from "@statewalker/mime.view.markdown";
2
+ import { Button } from "@statewalker/ui.view.shadcn";
3
+ import { type ReactElement, useState } from "react";
4
+ import { pageUri, useFileText, useSiteManifest } from "./site-data.js";
5
+ import { SiteNav } from "./site-nav.js";
6
+
7
+ interface WikiSiteViewProps {
8
+ project: string;
9
+ slug: string;
10
+ }
11
+
12
+ function Centered({ children }: { children: React.ReactNode }): ReactElement {
13
+ return (
14
+ <div className="flex h-full items-center justify-center p-6 text-sm text-muted-foreground">
15
+ {children}
16
+ </div>
17
+ );
18
+ }
19
+
20
+ function PageBody({ uri }: { uri: string }): ReactElement {
21
+ const state = useFileText(uri);
22
+ if (state.status === "loading") return <Centered>Loading…</Centered>;
23
+ if (state.status === "error") return <Centered>Failed to load page: {state.message}</Centered>;
24
+ return (
25
+ <div className="prose prose-sm dark:prose-invert max-w-none overflow-auto p-6">
26
+ <Markdown>{state.text}</Markdown>
27
+ </div>
28
+ );
29
+ }
30
+
31
+ /**
32
+ * Site-browser panel: reads a site's `site.json` manifest and renders a TOC
33
+ * sidebar + breadcrumbs + prev/next over its pages, with the selected page's
34
+ * markdown body rendered via the shared `Markdown` component. ADR-0002: all wiki
35
+ * React lives here, never in `@statewalker/wiki`.
36
+ */
37
+ export function WikiSiteView({ project, slug }: WikiSiteViewProps): ReactElement {
38
+ const manifest = useSiteManifest(project, slug);
39
+ const [selected, setSelected] = useState(0);
40
+
41
+ if (manifest.status === "loading") return <Centered>Loading site…</Centered>;
42
+ if (manifest.status === "error")
43
+ return <Centered>Failed to load site: {manifest.message}</Centered>;
44
+ if (manifest.status === "empty" || manifest.manifest.pages.length === 0) {
45
+ return <Centered>No site generated yet.</Centered>;
46
+ }
47
+
48
+ const pages = manifest.manifest.pages;
49
+ const index = Math.min(Math.max(selected, 0), pages.length - 1);
50
+ const page = pages[index];
51
+ if (!page) return <Centered>No site generated yet.</Centered>;
52
+
53
+ return (
54
+ <div className="flex h-full flex-col">
55
+ <div className="flex items-center gap-2 border-b px-3 py-2 text-sm">
56
+ <span className="font-medium">{manifest.manifest.title}</span>
57
+ <span className="text-muted-foreground">›</span>
58
+ <span>{page.title}</span>
59
+ <div className="ml-auto flex gap-1">
60
+ <Button
61
+ variant="ghost"
62
+ size="sm"
63
+ disabled={index <= 0}
64
+ onClick={() => setSelected(index - 1)}
65
+ >
66
+ Prev
67
+ </Button>
68
+ <Button
69
+ variant="ghost"
70
+ size="sm"
71
+ disabled={index >= pages.length - 1}
72
+ onClick={() => setSelected(index + 1)}
73
+ >
74
+ Next
75
+ </Button>
76
+ </div>
77
+ </div>
78
+ <div className="flex min-h-0 flex-1">
79
+ <SiteNav pages={pages} selected={index} onSelect={setSelected} />
80
+ <div className="min-w-0 flex-1">
81
+ <PageBody uri={pageUri(project, slug, page.path)} />
82
+ </div>
83
+ </div>
84
+ </div>
85
+ );
86
+ }
@@ -0,0 +1,41 @@
1
+ import { defineCatalog, type Spec } from "@json-render/core";
2
+ import { schema } from "@statewalker/render.view.react";
3
+ import { z } from "zod";
4
+
5
+ /**
6
+ * Catalog declaration for the wiki site-browser panel — typed schema only. The
7
+ * `WikiSiteView` React binding lives in `init.tsx` (ADR-0002 logic/renderer split).
8
+ * The single element carries `{ project, slug }`; the component reads `site.json`.
9
+ */
10
+ export const wikiSiteCatalog = defineCatalog(schema, {
11
+ components: {
12
+ WikiSiteView: { props: z.object({ project: z.string(), slug: z.string() }) },
13
+ },
14
+ actions: {},
15
+ });
16
+
17
+ export const WIKI_SITE_CATALOG_ID = "wiki-site";
18
+
19
+ /** Build the one-element spec for a (project, slug) site. */
20
+ export function makeWikiSiteSpec(project: string, slug: string): Spec {
21
+ return {
22
+ root: "panel",
23
+ elements: {
24
+ panel: {
25
+ type: "WikiSiteView",
26
+ props: { project, slug },
27
+ children: [],
28
+ },
29
+ },
30
+ } as Spec;
31
+ }
32
+
33
+ /** Deterministic panel id so reopening the same site focuses the tab. */
34
+ export function wikiSitePanelId(project: string, slug: string): string {
35
+ return `wiki-site:${project}/${slug}`;
36
+ }
37
+
38
+ /** Deterministic spec id; pairs with `wikiSitePanelId`. */
39
+ export function wikiSiteSpecId(project: string, slug: string): string {
40
+ return `spec:wiki-site:${project}/${slug}`;
41
+ }
@@ -0,0 +1,12 @@
1
+ // Public surface for `@statewalker/wiki.view.react`.
2
+
3
+ // The open-site command contract lives in `@statewalker/wiki` (React-free) so logic
4
+ // can dispatch it; re-exported here for renderer consumers.
5
+ export { OpenWikiSiteCommand, type OpenWikiSitePayload } from "@statewalker/wiki.core";
6
+ export {
7
+ makeWikiSiteSpec,
8
+ WIKI_SITE_CATALOG_ID,
9
+ wikiSiteCatalog,
10
+ wikiSitePanelId,
11
+ wikiSiteSpecId,
12
+ } from "./catalog.js";
@@ -0,0 +1,39 @@
1
+ import { SpecStore } from "@statewalker/render.core";
2
+ import { Commands } from "@statewalker/shared-commands";
3
+ import { Slots } from "@statewalker/shared-slots";
4
+ import { ShowDockPanelCommand } from "@statewalker/shell.core";
5
+ import { OpenWikiSiteCommand } from "@statewalker/wiki.core";
6
+ import { Workspace } from "@statewalker/workspace.core";
7
+ import { describe, expect, it } from "vitest";
8
+ import { wikiSiteSpecId } from "./catalog.js";
9
+ import initWikiReact from "./init.js";
10
+
11
+ describe("initWikiReact — wiki:open-site", () => {
12
+ it("creates the site spec and shows a panel; re-opening reuses the same panel", async () => {
13
+ const ws = new Workspace();
14
+ ws.setAdapter(SpecStore, () => new SpecStore());
15
+ ws.setAdapter(Slots, () => new Slots());
16
+ const commands = ws.requireAdapter(Commands);
17
+ const store = ws.requireAdapter(SpecStore);
18
+
19
+ const shown: string[] = [];
20
+ commands.listen(ShowDockPanelCommand, (command) => {
21
+ shown.push(command.payload.panelId);
22
+ command.resolve();
23
+ return true;
24
+ });
25
+
26
+ const cleanup = initWikiReact({ "workspace:workspace": ws });
27
+
28
+ await commands.call(OpenWikiSiteCommand, { project: "proj", slug: "themes" }).promise;
29
+ expect(store.get(wikiSiteSpecId("proj", "themes"))).toBeTruthy();
30
+ expect(shown).toEqual(["wiki-site:proj/themes"]);
31
+
32
+ // Re-opening the same site does not duplicate the spec (the guard prevents a
33
+ // throwing re-create) and targets the same deterministic panel id (focus).
34
+ await commands.call(OpenWikiSiteCommand, { project: "proj", slug: "themes" }).promise;
35
+ expect(shown).toEqual(["wiki-site:proj/themes", "wiki-site:proj/themes"]);
36
+
37
+ await cleanup();
38
+ });
39
+ });
@@ -0,0 +1,99 @@
1
+ import {
2
+ catalogsSlot,
3
+ LayoutStore,
4
+ restorePanelSpecsFromLayout,
5
+ SpecStore,
6
+ } from "@statewalker/render.core";
7
+ import { defineRegistry } from "@statewalker/render.view.react";
8
+ import { Commands } from "@statewalker/shared-commands";
9
+ import { newRegistry } from "@statewalker/shared-registry";
10
+ import { Slots } from "@statewalker/shared-slots";
11
+ import { ShowDockPanelCommand } from "@statewalker/shell.core";
12
+ import { dockTabIconSlot } from "@statewalker/shell.view.react";
13
+ import { OpenWikiSiteCommand } from "@statewalker/wiki.core";
14
+ import { getWorkspace } from "@statewalker/workspace.core";
15
+ import { BookText } from "lucide-react";
16
+ import { WikiSiteView } from "../internal/wiki-site-view.js";
17
+ import {
18
+ makeWikiSiteSpec,
19
+ WIKI_SITE_CATALOG_ID,
20
+ wikiSiteCatalog,
21
+ wikiSitePanelId,
22
+ wikiSiteSpecId,
23
+ } from "./catalog.js";
24
+
25
+ /** Split a restored panel id suffix (`<project>/<slug>`) back into its parts. */
26
+ function splitSiteId(id: string): { project: string; slug: string } {
27
+ const slash = id.indexOf("/");
28
+ return slash < 0
29
+ ? { project: id, slug: "" }
30
+ : { project: id.slice(0, slash), slug: id.slice(slash + 1) };
31
+ }
32
+
33
+ /**
34
+ * Renderer-fragment init for `@statewalker/wiki.view.react`. Registers the
35
+ * `wiki-site` json-render catalog (binding `WikiSiteView`) and the `wiki:open-site`
36
+ * command that opens a generated thematic site as a dock panel. ADR-0002: all wiki
37
+ * React lives here, never in `@statewalker/wiki`.
38
+ */
39
+ export default function initWikiReact(ctx: Record<string, unknown>): () => Promise<void> {
40
+ const [register, cleanup] = newRegistry();
41
+ const workspace = getWorkspace(ctx);
42
+ const slots = workspace.requireAdapter(Slots);
43
+ const store = workspace.requireAdapter(SpecStore);
44
+ const commands = workspace.requireAdapter(Commands);
45
+ const layoutStore = workspace.requireAdapter(LayoutStore);
46
+
47
+ // Pre-allocate specs for restored wiki-site tabs on workspace-connect,
48
+ // reading the layout held by the LayoutStore adapter (see mime.view.pdf).
49
+ register(
50
+ workspace.onLoad(() => {
51
+ restorePanelSpecsFromLayout({
52
+ store,
53
+ layout: layoutStore.get(),
54
+ panelIdPrefix: "wiki-site:",
55
+ catalogId: WIKI_SITE_CATALOG_ID,
56
+ buildSpec: (id) => {
57
+ const { project, slug } = splitSiteId(id);
58
+ return makeWikiSiteSpec(project, slug);
59
+ },
60
+ buildSpecId: (id) => {
61
+ const { project, slug } = splitSiteId(id);
62
+ return wikiSiteSpecId(project, slug);
63
+ },
64
+ });
65
+ }),
66
+ );
67
+
68
+ const { registry } = defineRegistry(wikiSiteCatalog, {
69
+ components: {
70
+ WikiSiteView: ({ props }) => <WikiSiteView project={props.project} slug={props.slug} />,
71
+ },
72
+ actions: {},
73
+ });
74
+ register(slots.register(catalogsSlot, WIKI_SITE_CATALOG_ID, registry));
75
+
76
+ register(
77
+ commands.listen(OpenWikiSiteCommand, (command) => {
78
+ const { project, slug } = command.payload;
79
+ const specId = wikiSiteSpecId(project, slug);
80
+ const panelId = wikiSitePanelId(project, slug);
81
+ if (!store.get(specId)) {
82
+ store.create({
83
+ id: specId,
84
+ catalogId: WIKI_SITE_CATALOG_ID,
85
+ spec: makeWikiSiteSpec(project, slug),
86
+ });
87
+ }
88
+ void commands
89
+ .call(ShowDockPanelCommand, { panelId, specId, title: slug })
90
+ .promise.then(() => command.resolve())
91
+ .catch((error) => command.reject(error));
92
+ return true;
93
+ }),
94
+ );
95
+
96
+ register(slots.provide(dockTabIconSlot, { panelIdPrefix: "wiki-site:", Icon: BookText }));
97
+
98
+ return cleanup;
99
+ }
package/src/styles.css ADDED
@@ -0,0 +1,9 @@
1
+ /*
2
+ * Tailwind v4 content discovery for this package.
3
+ *
4
+ * The host app's `main.tsx` imports `<package>/styles` once at boot. Tailwind
5
+ * v4 then walks the `@source` glob and picks up class usage inside this
6
+ * package's source so utility classes are emitted into the host's CSS bundle.
7
+ */
8
+ @source "./**/*.tsx";
9
+ @source "./**/*.ts";