@riebeckite/plugin-garden-explorer 0.0.12 → 0.0.13

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/README.md CHANGED
@@ -1,89 +1,93 @@
1
- # @riebeckite/plugin-garden-explorer
2
-
3
- Interactive note garden explorer: a graph, search box, tag/folder filters, and
4
- note details in a single page.
5
-
6
- [日本語](./README_ja.md)
7
-
8
- ## Overview
9
-
10
- `gardenExplorer()` provides an interactive `GardenExplorer` component with three
11
- panels:
12
-
13
- - **Explorer** — search box plus tag and folder filter chips (with counts) and
14
- a filtered note list
15
- - **Graph** — an interactive radial SVG graph of notes and internal links with
16
- zoom (`+`/`−`/wheel), pan (drag), and reset
17
- - **Details** — the selected note's links, tags, excerpt, and related notes
18
-
19
- Selection state is mirrored to the URL query (`?note=`, `?tag=`, `?folder=`),
20
- so the view is shareable and the filter list adapts to the current selection.
21
-
22
- `getGardenExplorerData()` builds the note set from published entries, including
23
- headings, a plain-text body (truncated to 4,000 chars), tags, folders, outgoing
24
- links, and backlinks. Its client-side note search and radial graph layout are
25
- self-contained, so this plugin does not require the search plugin.
26
-
27
- ## Usage
28
-
29
- ```ts
30
- import { defineConfig } from "@riebeckite/core";
31
- import { gardenExplorerPlugin } from "@riebeckite/plugin-garden-explorer";
32
-
33
- export default defineConfig({
34
- // ...
35
- plugins: [gardenExplorerPlugin()],
36
- });
37
- ```
38
-
39
- `gardenExplorerPlugin()` registers the plugin in the plugin list and bundles
40
- `style.css` into the app stylesheet.
41
-
42
- ### Render the component
43
-
44
- ```tsx
45
- import GardenExplorer, {
46
- getGardenExplorerData,
47
- } from "@riebeckite/plugin-garden-explorer";
48
- import { config } from "../config";
49
- import { content } from "../content";
50
- import { getArticleTitle } from "../lib/article-title";
51
-
52
- const manifest = await content.getManifest();
53
- const data = getGardenExplorerData({
54
- manifest,
55
- config,
56
- resolveTitle: getArticleTitle,
57
- });
58
-
59
- // ...in your /explore route
60
- return <GardenExplorer data={data} />;
61
- ```
62
-
63
- The component is client-side interactive and expects `window` to be available
64
- in the browser.
65
-
66
- ## Data
67
-
68
- `getGardenExplorerData()` returns `GardenExplorerData`:
69
-
70
- - `notes` — published notes sorted by title, with searchable fields plus
71
- `folder`, `outgoing`, and `backlinks`
72
- - `edges` — note-to-note graph edges between published notes
73
- - `tags` — tag counts, most frequent first
74
- - `folders` — folder counts, alphabetical; root-level notes are `"Root"`
75
-
76
- ## Exports
77
-
78
- - `gardenExplorerPlugin()` — plugin factory
79
- - `GardenExplorer` — interactive explorer component (default export of
80
- `components/garden-explorer.tsx`)
81
- - `getGardenExplorerData({ manifest, config, resolveTitle })` — builds the
82
- explorer dataset
83
- - Types: `GardenExplorerData`, `GardenExplorerEdge`, `GardenExplorerFolder`,
84
- `GardenExplorerNote`, `GardenExplorerTag`
85
-
86
- ## See also
87
-
88
- - [Plugin guide](../../../docs/en/plugin-system.md)
89
- - [`@riebeckite/plugin-local-graph`](../local-graph/README.md)
1
+ # @riebeckite/plugin-garden-explorer
2
+
3
+ Interactive note garden explorer: a graph, search box, tag/folder filters, and
4
+ note details in a single page.
5
+
6
+ [日本語](./README_ja.md)
7
+
8
+ ## Overview
9
+
10
+ `gardenExplorer()` provides an interactive `GardenExplorer` component with three
11
+ panels:
12
+
13
+ - **Explorer** — search box plus tag and folder filter chips (with counts) and
14
+ a filtered note list
15
+ - **Graph** — an interactive radial SVG graph of notes and internal links with
16
+ zoom (`+`/`−`/wheel), pan (drag), and reset
17
+ - **Details** — the selected note's links, tags, excerpt, and related notes
18
+
19
+ Selection state is mirrored to the URL query (`?note=`, `?tag=`, `?folder=`),
20
+ so the view is shareable and the filter list adapts to the current selection.
21
+
22
+ `getGardenExplorerData()` builds the note set from published entries, including
23
+ headings, a plain-text body (truncated to 4,000 chars), tags, folders, outgoing
24
+ links, and backlinks. Its client-side note search and radial graph layout are
25
+ self-contained, so this plugin does not require the search plugin.
26
+
27
+ ## Usage
28
+
29
+ ```ts
30
+ import { defineConfig } from "@riebeckite/core";
31
+ import { gardenExplorerPlugin } from "@riebeckite/plugin-garden-explorer";
32
+
33
+ export default defineConfig({
34
+ // ...
35
+ plugins: [gardenExplorerPlugin()],
36
+ });
37
+ ```
38
+
39
+ `gardenExplorerPlugin()` registers the `/explore` page type and bundles
40
+ `style.css` and its client hydrator into the app. A catch-all route using
41
+ `resolveRiebeckiteRoute()` and `pluginPageSsgParams()` renders and emits it;
42
+ no plugin-specific application route is needed. The page is server-rendered
43
+ first, then the registered client entry hydrates its graph, filters, and URL
44
+ state after loading.
45
+
46
+ ### Embed the component elsewhere
47
+
48
+ ```tsx
49
+ import GardenExplorer, {
50
+ getGardenExplorerData,
51
+ } from "@riebeckite/plugin-garden-explorer";
52
+ import { config } from "../config";
53
+ import { content } from "../content";
54
+ import { getArticleTitle } from "../lib/article-title";
55
+
56
+ const manifest = await content.getManifest();
57
+ const data = getGardenExplorerData({
58
+ manifest,
59
+ config,
60
+ resolveTitle: getArticleTitle,
61
+ });
62
+
63
+ // ...in a site-owned component
64
+ return <GardenExplorer data={data} />;
65
+ ```
66
+
67
+ The component is client-side interactive and expects `window` to be available
68
+ in the browser.
69
+
70
+ ## Data
71
+
72
+ `getGardenExplorerData()` returns `GardenExplorerData`:
73
+
74
+ - `notes` — published notes sorted by title, with searchable fields plus
75
+ `folder`, `outgoing`, and `backlinks`
76
+ - `edges` — note-to-note graph edges between published notes
77
+ - `tags` — tag counts, most frequent first
78
+ - `folders` — folder counts, alphabetical; root-level notes are `"Root"`
79
+
80
+ ## Exports
81
+
82
+ - `gardenExplorerPlugin()` — plugin factory
83
+ - `GardenExplorer` — interactive explorer component (default export of
84
+ `components/garden-explorer.tsx`)
85
+ - `getGardenExplorerData({ manifest, config, resolveTitle })` — builds the
86
+ explorer dataset
87
+ - Types: `GardenExplorerData`, `GardenExplorerEdge`, `GardenExplorerFolder`,
88
+ `GardenExplorerNote`, `GardenExplorerTag`
89
+
90
+ ## See also
91
+
92
+ - [Plugin guide](../../../docs/en/reference/plugin-api.md)
93
+ - [`@riebeckite/plugin-local-graph`](../local-graph/README.md)
package/README_ja.md CHANGED
@@ -1,69 +1,77 @@
1
- # @riebeckite/plugin-garden-explorer
2
-
3
- ノートの一覧、検索、タグ・フォルダーによる絞り込み、リンクグラフを一画面にまとめる探索画面です。公開済みのノートだけを対象にするため、そのまま公開サイトの `/explore` などに置けます。
4
-
5
- [English](./README.md)
6
-
7
- ## 用意される画面
8
-
9
- `gardenExplorerPlugin()` は、次の三つの領域を持つ `GardenExplorer` コンポーネントとスタイルを提供します。
10
-
11
- - **一覧**: キーワード検索、タグとフォルダーのフィルター、該当ノートの一覧
12
- - **グラフ**: ノートと内部リンクを表示する放射状 SVG グラフ。ズーム、ドラッグ、リセットに対応
13
- - **詳細**: 選択したノートのリンク、タグ、抜粋、関連ノート
14
-
15
- 選択状態は `?note=`、`?tag=`、`?folder=` として URL に反映されます。表示中の状態をそのまま共有できます。
16
-
17
- ## 設定してページに配置する
18
-
19
- ```ts
20
- import { defineConfig } from "@riebeckite/core";
21
- import { gardenExplorerPlugin } from "@riebeckite/plugin-garden-explorer";
22
-
23
- export default defineConfig({
24
- // ...
25
- plugins: [gardenExplorerPlugin()],
26
- });
27
- ```
28
-
29
- ルート側では manifest から表示用データを作り、コンポーネントに渡します。
30
-
31
- ```tsx
32
- import GardenExplorer, {
33
- getGardenExplorerData,
34
- } from "@riebeckite/plugin-garden-explorer";
35
-
36
- const manifest = await content.getManifest();
37
- const data = getGardenExplorerData({
38
- manifest,
39
- config,
40
- resolveTitle: getArticleTitle,
41
- });
42
-
43
- return <GardenExplorer data={data} />;
44
- ```
45
-
46
- `GardenExplorer` はブラウザで操作するクライアントコンポーネントです。サーバーで描画するだけの場所には置かず、クライアント側で動作するルートに配置してください。
47
-
48
- ## データに含まれる範囲
49
-
50
- `getGardenExplorerData()` は公開済みエントリーから、見出し、本文テキストの先頭 4,000 文字、タグ、フォルダー、送信リンク、被リンクを集めます。本文全体をクライアントへ渡さないため、検索用のデータ量を抑えられます。
51
-
52
- - `notes`: タイトル順の公開ノート。フォルダー、送信リンク、被リンクを含む
53
- - `edges`: 公開ノート間のグラフの辺
54
- - `tags`: 件数の多い順のタグ
55
- - `folders`: 名前順のフォルダー。ルート直下のノートは `Root`
56
-
57
- 探索画面の検索はプラグイン内で完結しており、`@riebeckite/plugin-search` は不要です。
58
-
59
- ## 主なエクスポート
60
-
61
- - `gardenExplorerPlugin()`: プラグインを作成する
62
- - `GardenExplorer`: 探索画面のコンポーネント
63
- - `getGardenExplorerData({ manifest, config, resolveTitle })`: 表示データを構築する
64
- - 型: `GardenExplorerData`、`GardenExplorerEdge`、`GardenExplorerFolder`、`GardenExplorerNote`、`GardenExplorerTag`
65
-
66
- ## 関連資料
67
-
68
- - [プラグインシステム](../../../docs/ja/plugin-system.md)
69
- - [`@riebeckite/plugin-local-graph`](../local-graph/README_ja.md)
1
+ # @riebeckite/plugin-garden-explorer
2
+
3
+ ノートの一覧、検索、タグ・フォルダーによる絞り込み、リンクグラフを一画面にまとめる探索画面です。公開済みのノートだけを対象にするため、そのまま公開サイトの `/explore` などに置けます。
4
+
5
+ [English](./README.md)
6
+
7
+ ## 用意される画面
8
+
9
+ `gardenExplorerPlugin()` は、次の三つの領域を持つ `GardenExplorer` コンポーネントとスタイルを提供します。
10
+
11
+ - **一覧**: キーワード検索、タグとフォルダーのフィルター、該当ノートの一覧
12
+ - **グラフ**: ノートと内部リンクを表示する放射状 SVG グラフ。ズーム、ドラッグ、リセットに対応
13
+ - **詳細**: 選択したノートのリンク、タグ、抜粋、関連ノート
14
+
15
+ 選択状態は `?note=`、`?tag=`、`?folder=` として URL に反映されます。表示中の状態をそのまま共有できます。
16
+
17
+ ## 設定してページに配置する
18
+
19
+ ```ts
20
+ import { defineConfig } from "@riebeckite/core";
21
+ import { gardenExplorerPlugin } from "@riebeckite/plugin-garden-explorer";
22
+
23
+ export default defineConfig({
24
+ // ...
25
+ plugins: [gardenExplorerPlugin()],
26
+ });
27
+ ```
28
+
29
+ `gardenExplorerPlugin()` は `/explore` の Page Type、スタイル、クライアント側の
30
+ hydrator を登録します。
31
+ `resolveRiebeckiteRoute()` と `pluginPageSsgParams()` を使う共通 catch-all route
32
+ が表示と SSG を担当するため、Plugin 固有の route は不要です。
33
+ ページはまずサーバーで描画し、読み込み後に登録済みの client entry がグラフ、
34
+ フィルター、URL 状態を hydration します。
35
+
36
+ コンポーネントを別の site-owned component に埋め込む場合だけ、manifest から
37
+ 表示用データを作って渡します。
38
+
39
+ ```tsx
40
+ import GardenExplorer, {
41
+ getGardenExplorerData,
42
+ } from "@riebeckite/plugin-garden-explorer";
43
+
44
+ const manifest = await content.getManifest();
45
+ const data = getGardenExplorerData({
46
+ manifest,
47
+ config,
48
+ resolveTitle: getArticleTitle,
49
+ });
50
+
51
+ return <GardenExplorer data={data} />;
52
+ ```
53
+
54
+ `GardenExplorer` はブラウザで操作するクライアントコンポーネントです。サーバーで描画するだけの場所には置かず、クライアント側で動作するルートに配置してください。
55
+
56
+ ## データに含まれる範囲
57
+
58
+ `getGardenExplorerData()` は公開済みエントリーから、見出し、本文テキストの先頭 4,000 文字、タグ、フォルダー、送信リンク、被リンクを集めます。本文全体をクライアントへ渡さないため、検索用のデータ量を抑えられます。
59
+
60
+ - `notes`: タイトル順の公開ノート。フォルダー、送信リンク、被リンクを含む
61
+ - `edges`: 公開ノート間のグラフの辺
62
+ - `tags`: 件数の多い順のタグ
63
+ - `folders`: 名前順のフォルダー。ルート直下のノートは `Root`
64
+
65
+ 探索画面の検索はプラグイン内で完結しており、`@riebeckite/plugin-search` は不要です。
66
+
67
+ ## 主なエクスポート
68
+
69
+ - `gardenExplorerPlugin()`: プラグインを作成する
70
+ - `GardenExplorer`: 探索画面のコンポーネント
71
+ - `getGardenExplorerData({ manifest, config, resolveTitle })`: 表示データを構築する
72
+ - 型: `GardenExplorerData`、`GardenExplorerEdge`、`GardenExplorerFolder`、`GardenExplorerNote`、`GardenExplorerTag`
73
+
74
+ ## 関連資料
75
+
76
+ - [プラグインシステム](../../../docs/ja/reference/plugin-api.md)
77
+ - [`@riebeckite/plugin-local-graph`](../local-graph/README_ja.md)
@@ -0,0 +1,2 @@
1
+ /** Hydrates Garden Explorer only when its Page Type is present in the document. */
2
+ export default function initGardenExplorer(): void;
package/dist/client.js ADDED
@@ -0,0 +1,478 @@
1
+ // client.tsx
2
+ import { hydrateRoot } from "hono/jsx/dom/client";
3
+
4
+ // components/garden-explorer.tsx
5
+ import { layoutRadialGraph } from "@riebeckite/core/client";
6
+ import { useEffect, useMemo, useState } from "hono/jsx";
7
+
8
+ // src/search-notes.ts
9
+ var FIELD_WEIGHTS = {
10
+ slug: 64,
11
+ title: 56,
12
+ tags: 44,
13
+ headings: 32,
14
+ body: 10
15
+ };
16
+ function searchGardenExplorerNotes(notes, query) {
17
+ const normalizedQuery = normalizeSearchQuery(query);
18
+ if (normalizedQuery.length === 0) return [];
19
+ return notes.map((note) => scoreNote(note, normalizedQuery)).filter(
20
+ (result) => result !== null
21
+ ).sort(
22
+ (a, b) => b.score - a.score || a.note.title.localeCompare(b.note.title, "ja")
23
+ ).map(({ note }) => note);
24
+ }
25
+ function normalizeSearchQuery(value) {
26
+ return normalizeSearchText(value).replace(/^#+/, "");
27
+ }
28
+ function normalizeSearchText(value) {
29
+ return value.toLocaleLowerCase().normalize("NFKC").replace(
30
+ /[ァ-ン]/g,
31
+ (char) => String.fromCharCode(char.charCodeAt(0) - 96)
32
+ );
33
+ }
34
+ function scoreNote(note, normalizedQuery) {
35
+ const matches = [
36
+ scoreField("title", note.title, normalizedQuery),
37
+ scoreField("slug", note.slug, normalizedQuery),
38
+ ...note.tags.map((tag) => scoreField("tags", tag, normalizedQuery)),
39
+ ...note.headings.map(
40
+ (heading) => scoreField("headings", heading, normalizedQuery)
41
+ ),
42
+ scoreField("body", note.body, normalizedQuery)
43
+ ].filter((match) => match.score > 0);
44
+ if (matches.length === 0) return null;
45
+ return {
46
+ note,
47
+ score: matches.reduce((total, match) => total + match.score, 0)
48
+ };
49
+ }
50
+ function scoreField(field, value, normalizedQuery) {
51
+ const normalizedValue = normalizeSearchText(value);
52
+ const index = normalizedValue.indexOf(normalizedQuery);
53
+ const weight = FIELD_WEIGHTS[field];
54
+ if (normalizedValue === normalizedQuery) return { score: weight * 3 };
55
+ if (index !== -1) return { score: weight * (index === 0 ? 2 : 1) };
56
+ const fuzzyScore = scoreFuzzyMatch(normalizedValue, normalizedQuery);
57
+ return { score: fuzzyScore > 0 ? Math.round(weight * fuzzyScore) : 0 };
58
+ }
59
+ function scoreFuzzyMatch(value, query) {
60
+ if (query.length < 2) return 0;
61
+ let valueIndex = 0;
62
+ let matched = 0;
63
+ let gaps = 0;
64
+ for (const queryCharacter of query) {
65
+ const nextIndex = value.indexOf(queryCharacter, valueIndex);
66
+ if (nextIndex === -1) return 0;
67
+ gaps += nextIndex - valueIndex;
68
+ valueIndex = nextIndex + 1;
69
+ matched += 1;
70
+ }
71
+ const coverage = matched / Math.max(value.length, query.length);
72
+ const continuityPenalty = Math.min(gaps / Math.max(value.length, 1), 0.8);
73
+ return Math.max(0, coverage * (1 - continuityPenalty));
74
+ }
75
+
76
+ // components/explorer-panels.tsx
77
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
78
+ function FilterList(props) {
79
+ return /* @__PURE__ */ jsxs("div", { class: "garden-explorer__section", children: [
80
+ /* @__PURE__ */ jsx("p", { class: "garden-explorer__eyebrow", children: props.title }),
81
+ /* @__PURE__ */ jsx("div", { class: "garden-explorer__chips", children: props.items.slice(0, 28).map((item) => /* @__PURE__ */ jsxs(
82
+ "button",
83
+ {
84
+ type: "button",
85
+ class: "garden-explorer__chip",
86
+ "aria-pressed": props.selectedKey === item.key,
87
+ onClick: () => props.onSelect(item.key),
88
+ children: [
89
+ /* @__PURE__ */ jsx("span", { children: item.label }),
90
+ /* @__PURE__ */ jsx("span", { children: item.count })
91
+ ]
92
+ },
93
+ item.key
94
+ )) })
95
+ ] });
96
+ }
97
+ function NoteList(props) {
98
+ return /* @__PURE__ */ jsx("ul", { class: "garden-explorer__note-list", children: props.notes.map((note) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
99
+ "button",
100
+ {
101
+ type: "button",
102
+ "aria-pressed": props.selectedSlug === note.slug,
103
+ onClick: () => props.onSelect(note.slug),
104
+ children: [
105
+ /* @__PURE__ */ jsx("span", { children: note.title }),
106
+ /* @__PURE__ */ jsx("small", { children: note.folder })
107
+ ]
108
+ }
109
+ ) }, note.slug)) });
110
+ }
111
+ function NoteDetails(props) {
112
+ return /* @__PURE__ */ jsxs("div", { class: "garden-explorer__details", children: [
113
+ /* @__PURE__ */ jsx("p", { class: "garden-explorer__eyebrow", children: "Selected Note" }),
114
+ /* @__PURE__ */ jsx("h2", { children: /* @__PURE__ */ jsx("a", { href: props.note.permalink, children: props.note.title }) }),
115
+ /* @__PURE__ */ jsx("p", { class: "garden-explorer__path", children: props.note.slug }),
116
+ props.note.excerpt && /* @__PURE__ */ jsx("p", { children: props.note.excerpt }),
117
+ /* @__PURE__ */ jsx("div", { class: "garden-explorer__tag-row", children: props.note.tags.map((tag) => /* @__PURE__ */ jsxs("span", { children: [
118
+ "#",
119
+ tag
120
+ ] }, tag)) }),
121
+ /* @__PURE__ */ jsx(
122
+ LinkedNoteSection,
123
+ {
124
+ title: "Outgoing Links",
125
+ slugs: props.note.outgoing,
126
+ noteBySlug: props.noteBySlug,
127
+ onSelect: props.onSelect
128
+ }
129
+ ),
130
+ /* @__PURE__ */ jsx(
131
+ LinkedNoteSection,
132
+ {
133
+ title: "Backlinks",
134
+ slugs: props.note.backlinks,
135
+ noteBySlug: props.noteBySlug,
136
+ onSelect: props.onSelect
137
+ }
138
+ ),
139
+ /* @__PURE__ */ jsxs("div", { class: "garden-explorer__section garden-explorer__section--flush", children: [
140
+ /* @__PURE__ */ jsx("p", { class: "garden-explorer__eyebrow", children: "Related Notes" }),
141
+ /* @__PURE__ */ jsx(
142
+ NoteList,
143
+ {
144
+ notes: props.relatedNotes,
145
+ selectedSlug: props.note.slug,
146
+ onSelect: props.onSelect
147
+ }
148
+ )
149
+ ] })
150
+ ] });
151
+ }
152
+ function LinkedNoteSection(props) {
153
+ const notes = props.slugs.map((slug) => props.noteBySlug.get(slug)).filter((note) => note !== void 0);
154
+ return /* @__PURE__ */ jsxs("div", { class: "garden-explorer__section garden-explorer__section--flush", children: [
155
+ /* @__PURE__ */ jsx("p", { class: "garden-explorer__eyebrow", children: props.title }),
156
+ notes.length > 0 ? /* @__PURE__ */ jsx(NoteList, { notes, selectedSlug: "", onSelect: props.onSelect }) : /* @__PURE__ */ jsx("p", { class: "garden-explorer__empty", children: "No notes." })
157
+ ] });
158
+ }
159
+
160
+ // components/garden-explorer.tsx
161
+ import { jsx as jsx2, jsxs as jsxs2 } from "hono/jsx/jsx-runtime";
162
+ var GRAPH_WIDTH = 900;
163
+ var GRAPH_HEIGHT = 620;
164
+ var MIN_SCALE = 0.6;
165
+ var MAX_SCALE = 2.4;
166
+ function GardenExplorer(props) {
167
+ const noteBySlug = useMemo(
168
+ () => new Map(props.data.notes.map((note) => [note.slug, note])),
169
+ [props.data.notes]
170
+ );
171
+ const [query, setQuery] = useState("");
172
+ const [selectedTag, setSelectedTag] = useState(null);
173
+ const [selectedFolder, setSelectedFolder] = useState(null);
174
+ const [selectedSlug, setSelectedSlug] = useState(
175
+ () => props.data.notes[0]?.slug ?? ""
176
+ );
177
+ const [mobilePanel, setMobilePanel] = useState("graph");
178
+ const [view, setView] = useState({ x: 0, y: 0, scale: 1 });
179
+ useEffect(() => {
180
+ const params = new URLSearchParams(window.location.search);
181
+ const note = params.get("note");
182
+ const tag = params.get("tag");
183
+ const folder = params.get("folder");
184
+ if (note && noteBySlug.has(note)) setSelectedSlug(note);
185
+ if (tag) setSelectedTag(tag);
186
+ if (folder) setSelectedFolder(folder);
187
+ }, [noteBySlug]);
188
+ useEffect(() => {
189
+ const params = new URLSearchParams();
190
+ if (selectedSlug) params.set("note", selectedSlug);
191
+ if (selectedTag) params.set("tag", selectedTag);
192
+ if (selectedFolder) params.set("folder", selectedFolder);
193
+ const next = params.toString() ? `/explore?${params}` : "/explore";
194
+ window.history.replaceState(null, "", next);
195
+ }, [selectedSlug, selectedTag, selectedFolder]);
196
+ const filteredNotes = useMemo(() => {
197
+ const base = props.data.notes.filter((note) => {
198
+ if (selectedTag && !note.tags.includes(selectedTag)) return false;
199
+ if (selectedFolder && note.folder !== selectedFolder) return false;
200
+ return true;
201
+ });
202
+ const searched = query ? searchGardenExplorerNotes(base, query) : base;
203
+ return searched.slice(0, 80);
204
+ }, [props.data.notes, query, selectedTag, selectedFolder]);
205
+ const visibleSlugSet = useMemo(
206
+ () => new Set(filteredNotes.map((note) => note.slug)),
207
+ [filteredNotes]
208
+ );
209
+ const visibleEdges = useMemo(
210
+ () => props.data.edges.filter(
211
+ (edge) => visibleSlugSet.has(edge.source) && visibleSlugSet.has(edge.target)
212
+ ),
213
+ [props.data.edges, visibleSlugSet]
214
+ );
215
+ const graphNodes = useMemo(
216
+ () => layoutNodes(filteredNotes, selectedSlug),
217
+ [filteredNotes, selectedSlug]
218
+ );
219
+ const selectedNote = noteBySlug.get(selectedSlug) ?? filteredNotes[0] ?? null;
220
+ const relatedNotes = useMemo(
221
+ () => selectedNote ? getRelatedNotes(selectedNote, props.data.notes) : [],
222
+ [selectedNote, props.data.notes]
223
+ );
224
+ const selectNote = (slug) => {
225
+ setSelectedSlug(slug);
226
+ setMobilePanel("details");
227
+ };
228
+ const clearFilters = () => {
229
+ setQuery("");
230
+ setSelectedTag(null);
231
+ setSelectedFolder(null);
232
+ };
233
+ const zoom = (delta) => {
234
+ setView((current) => ({
235
+ ...current,
236
+ scale: clamp(current.scale + delta, MIN_SCALE, MAX_SCALE)
237
+ }));
238
+ };
239
+ return /* @__PURE__ */ jsxs2("div", { class: "garden-explorer rr-garden-explorer", "data-garden-explorer": true, children: [
240
+ /* @__PURE__ */ jsxs2("fieldset", { class: "garden-explorer__mobile-tabs", children: [
241
+ /* @__PURE__ */ jsx2("legend", { class: "sr-only", children: "Explorer panels" }),
242
+ ["graph", "explorer", "details"].map((panel) => /* @__PURE__ */ jsx2(
243
+ "button",
244
+ {
245
+ type: "button",
246
+ class: "garden-explorer__mobile-tab",
247
+ "aria-pressed": mobilePanel === panel,
248
+ onClick: () => setMobilePanel(panel),
249
+ children: panel
250
+ }
251
+ ))
252
+ ] }),
253
+ /* @__PURE__ */ jsxs2("aside", { class: panelClass("explorer", mobilePanel), children: [
254
+ /* @__PURE__ */ jsxs2("div", { class: "garden-explorer__section", children: [
255
+ /* @__PURE__ */ jsx2("p", { class: "garden-explorer__eyebrow", children: "Explorer" }),
256
+ /* @__PURE__ */ jsxs2("label", { class: "garden-explorer__search", children: [
257
+ /* @__PURE__ */ jsx2("span", { class: "sr-only", children: "Search notes" }),
258
+ /* @__PURE__ */ jsx2(
259
+ "input",
260
+ {
261
+ type: "search",
262
+ value: query,
263
+ placeholder: "Search notes...",
264
+ onInput: (event) => setQuery(event.currentTarget.value)
265
+ }
266
+ )
267
+ ] }),
268
+ /* @__PURE__ */ jsx2(
269
+ "button",
270
+ {
271
+ type: "button",
272
+ class: "garden-explorer__text-button",
273
+ onClick: clearFilters,
274
+ children: "Clear filters"
275
+ }
276
+ )
277
+ ] }),
278
+ /* @__PURE__ */ jsx2(
279
+ FilterList,
280
+ {
281
+ title: "Tags",
282
+ items: props.data.tags.map((tag) => ({
283
+ key: tag.name,
284
+ label: `#${tag.name}`,
285
+ count: tag.count
286
+ })),
287
+ selectedKey: selectedTag,
288
+ onSelect: (key) => setSelectedTag(key === selectedTag ? null : key)
289
+ }
290
+ ),
291
+ /* @__PURE__ */ jsx2(
292
+ FilterList,
293
+ {
294
+ title: "Folders",
295
+ items: props.data.folders.map((folder) => ({
296
+ key: folder.path,
297
+ label: folder.path,
298
+ count: folder.count
299
+ })),
300
+ selectedKey: selectedFolder,
301
+ onSelect: (key) => setSelectedFolder(key === selectedFolder ? null : key)
302
+ }
303
+ ),
304
+ /* @__PURE__ */ jsxs2("div", { class: "garden-explorer__section", children: [
305
+ /* @__PURE__ */ jsx2("p", { class: "garden-explorer__eyebrow", children: "Notes" }),
306
+ /* @__PURE__ */ jsx2(
307
+ NoteList,
308
+ {
309
+ notes: filteredNotes,
310
+ selectedSlug,
311
+ onSelect: selectNote
312
+ }
313
+ )
314
+ ] })
315
+ ] }),
316
+ /* @__PURE__ */ jsxs2("section", { class: panelClass("graph", mobilePanel), "aria-label": "Note graph", children: [
317
+ /* @__PURE__ */ jsxs2("div", { class: "garden-explorer__graph-toolbar", children: [
318
+ /* @__PURE__ */ jsxs2("span", { children: [
319
+ filteredNotes.length,
320
+ " notes"
321
+ ] }),
322
+ /* @__PURE__ */ jsxs2("div", { children: [
323
+ /* @__PURE__ */ jsx2(
324
+ "button",
325
+ {
326
+ type: "button",
327
+ onClick: () => zoom(-0.2),
328
+ "aria-label": "Zoom out",
329
+ children: "−"
330
+ }
331
+ ),
332
+ /* @__PURE__ */ jsx2(
333
+ "button",
334
+ {
335
+ type: "button",
336
+ onClick: () => zoom(0.2),
337
+ "aria-label": "Zoom in",
338
+ children: "+"
339
+ }
340
+ ),
341
+ /* @__PURE__ */ jsx2(
342
+ "button",
343
+ {
344
+ type: "button",
345
+ onClick: () => setView({ x: 0, y: 0, scale: 1 }),
346
+ children: "Reset"
347
+ }
348
+ )
349
+ ] })
350
+ ] }),
351
+ /* @__PURE__ */ jsx2(
352
+ "svg",
353
+ {
354
+ class: "garden-graph",
355
+ viewBox: `0 0 ${GRAPH_WIDTH} ${GRAPH_HEIGHT}`,
356
+ role: "img",
357
+ "aria-label": "Interactive graph of notes and internal links",
358
+ onWheel: (event) => {
359
+ event.preventDefault();
360
+ zoom(event.deltaY > 0 ? -0.1 : 0.1);
361
+ },
362
+ onPointerMove: (event) => {
363
+ if (event.buttons !== 1) return;
364
+ setView((current) => ({
365
+ ...current,
366
+ x: current.x + event.movementX / current.scale,
367
+ y: current.y + event.movementY / current.scale
368
+ }));
369
+ },
370
+ children: /* @__PURE__ */ jsxs2("g", { transform: `translate(${view.x} ${view.y}) scale(${view.scale})`, children: [
371
+ visibleEdges.map((edge) => {
372
+ const source = graphNodes.get(edge.source);
373
+ const target = graphNodes.get(edge.target);
374
+ if (!source || !target) return null;
375
+ return /* @__PURE__ */ jsx2(
376
+ "line",
377
+ {
378
+ class: "garden-graph__edge",
379
+ x1: source.x,
380
+ y1: source.y,
381
+ x2: target.x,
382
+ y2: target.y
383
+ },
384
+ `${edge.source}-${edge.target}`
385
+ );
386
+ }),
387
+ filteredNotes.map((note) => {
388
+ const node = graphNodes.get(note.slug);
389
+ if (!node) return null;
390
+ const selected = note.slug === selectedSlug;
391
+ return /* @__PURE__ */ jsx2("g", { class: "garden-graph__node", children: /* @__PURE__ */ jsxs2(
392
+ "a",
393
+ {
394
+ href: `/explore?note=${encodeURIComponent(note.slug)}`,
395
+ "aria-label": `Select ${note.title}`,
396
+ onClick: (event) => {
397
+ event.preventDefault();
398
+ selectNote(note.slug);
399
+ },
400
+ children: [
401
+ /* @__PURE__ */ jsx2(
402
+ "circle",
403
+ {
404
+ cx: node.x,
405
+ cy: node.y,
406
+ r: selected ? 15 : node.radius,
407
+ "data-selected": selected ? "true" : "false"
408
+ }
409
+ ),
410
+ /* @__PURE__ */ jsx2("text", { x: node.x + 18, y: node.y + 5, children: note.title })
411
+ ]
412
+ }
413
+ ) }, note.slug);
414
+ })
415
+ ] })
416
+ }
417
+ )
418
+ ] }),
419
+ /* @__PURE__ */ jsx2("aside", { class: panelClass("details", mobilePanel), children: selectedNote ? /* @__PURE__ */ jsx2(
420
+ NoteDetails,
421
+ {
422
+ note: selectedNote,
423
+ noteBySlug,
424
+ relatedNotes,
425
+ onSelect: selectNote
426
+ }
427
+ ) : /* @__PURE__ */ jsx2("p", { class: "garden-explorer__empty", children: "Select a note to inspect links." }) })
428
+ ] });
429
+ }
430
+ function layoutNodes(notes, selectedSlug) {
431
+ return layoutRadialGraph(notes, {
432
+ width: GRAPH_WIDTH,
433
+ height: GRAPH_HEIGHT,
434
+ centerSlug: selectedSlug
435
+ });
436
+ }
437
+ function getRelatedNotes(selectedNote, notes) {
438
+ const directSlugs = /* @__PURE__ */ new Set([
439
+ ...selectedNote.outgoing,
440
+ ...selectedNote.backlinks
441
+ ]);
442
+ const selectedTags = new Set(selectedNote.tags);
443
+ return notes.filter((note) => note.slug !== selectedNote.slug).map((note) => ({
444
+ note,
445
+ score: (directSlugs.has(note.slug) ? 8 : 0) + note.tags.filter((tag) => selectedTags.has(tag)).length * 3
446
+ })).filter((item) => item.score > 0).sort(
447
+ (a, b) => b.score - a.score || a.note.title.localeCompare(b.note.title, "ja")
448
+ ).slice(0, 8).map((item) => item.note);
449
+ }
450
+ function panelClass(panel, activePanel) {
451
+ return `garden-explorer__panel garden-explorer__panel--${panel}${panel === activePanel ? " garden-explorer__panel--active" : ""}`;
452
+ }
453
+ function clamp(value, min, max) {
454
+ return Math.min(Math.max(value, min), max);
455
+ }
456
+
457
+ // client.tsx
458
+ import { jsx as jsx3 } from "hono/jsx/jsx-runtime";
459
+ var ROOT_SELECTOR = "[data-riebeckite-garden-explorer]";
460
+ var DATA_ID = "riebeckite-garden-explorer-data";
461
+ function initGardenExplorer() {
462
+ const root = document.querySelector(ROOT_SELECTOR);
463
+ const dataElement = document.getElementById(DATA_ID);
464
+ if (!root || !dataElement?.textContent) return;
465
+ const data = parseData(dataElement.textContent);
466
+ if (!data) return;
467
+ hydrateRoot(root, /* @__PURE__ */ jsx3(GardenExplorer, { data }));
468
+ }
469
+ function parseData(value) {
470
+ try {
471
+ return JSON.parse(value);
472
+ } catch {
473
+ return null;
474
+ }
475
+ }
476
+ export {
477
+ initGardenExplorer as default
478
+ };
@@ -1,5 +1,5 @@
1
1
  // components/garden-explorer.tsx
2
- import { layoutRadialGraph } from "@riebeckite/core";
2
+ import { layoutRadialGraph } from "@riebeckite/core/client";
3
3
  import { useEffect, useMemo, useState } from "hono/jsx";
4
4
 
5
5
  // src/search-notes.ts
package/dist/index.js CHANGED
@@ -1,8 +1,99 @@
1
1
  // index.ts
2
- import { definePlugin } from "@riebeckite/core";
2
+ import { createClientEntry, definePlugin } from "@riebeckite/core";
3
+
4
+ // src/garden-explorer.server.ts
5
+ import {
6
+ buildGraphEdges,
7
+ isPublished
8
+ } from "@riebeckite/core";
9
+ var MAX_BODY_LENGTH = 4e3;
10
+ function getGardenExplorerData(args) {
11
+ const graph = args.manifest.graph;
12
+ const publishedEntries = graph.nodes().filter((entry) => isPublished(args.config, entry.frontmatter));
13
+ const publishedSlugs = new Set(publishedEntries.map((entry) => entry.slug));
14
+ const notes = publishedEntries.map(
15
+ (entry) => toGardenNote(entry, graph, publishedSlugs, args.resolveTitle)
16
+ ).sort((a, b) => a.title.localeCompare(b.title, "ja"));
17
+ return {
18
+ notes,
19
+ edges: buildGraphEdges(notes, publishedSlugs),
20
+ tags: buildTags(notes),
21
+ folders: buildFolders(notes)
22
+ };
23
+ }
24
+ function toGardenNote(entry, graph, publishedSlugs, resolveTitle) {
25
+ const { outgoingSlugs, incomingSlugs } = graph.filterNeighbors(
26
+ entry.slug,
27
+ (slug) => publishedSlugs.has(slug)
28
+ );
29
+ return {
30
+ slug: entry.slug,
31
+ permalink: entry.permalink,
32
+ title: resolveTitle(entry.slug, entry.frontmatter.title),
33
+ headings: extractHeadings(entry.html),
34
+ body: toPlainText(entry.html).slice(0, MAX_BODY_LENGTH),
35
+ excerpt: createExcerpt(entry),
36
+ tags: entry.tags,
37
+ date: getEntryDate(entry.frontmatter),
38
+ folder: getFolder(entry.slug),
39
+ outgoing: outgoingSlugs,
40
+ backlinks: incomingSlugs
41
+ };
42
+ }
43
+ function buildTags(notes) {
44
+ const counts = /* @__PURE__ */ new Map();
45
+ for (const note of notes) {
46
+ for (const tag of note.tags) counts.set(tag, (counts.get(tag) ?? 0) + 1);
47
+ }
48
+ return Array.from(counts, ([name, count]) => ({ name, count })).sort(
49
+ (a, b) => b.count - a.count || a.name.localeCompare(b.name, "ja")
50
+ );
51
+ }
52
+ function buildFolders(notes) {
53
+ const counts = /* @__PURE__ */ new Map();
54
+ for (const note of notes)
55
+ counts.set(note.folder, (counts.get(note.folder) ?? 0) + 1);
56
+ return Array.from(counts, ([path, count]) => ({ path, count })).sort(
57
+ (a, b) => a.path.localeCompare(b.path, "ja")
58
+ );
59
+ }
60
+ function getFolder(slug) {
61
+ const segments = slug.split("/");
62
+ if (segments.length <= 1) return "Root";
63
+ return segments.slice(0, -1).join("/");
64
+ }
65
+ function createExcerpt(entry) {
66
+ const description = entry.frontmatter.description;
67
+ const text = typeof description === "string" && description.trim() ? description.trim() : toPlainText(entry.html);
68
+ return text.slice(0, 180);
69
+ }
70
+ function extractHeadings(html) {
71
+ const headings = [];
72
+ const headingPattern = /<h([1-4])\b[^>]*>([\s\S]*?)<\/h\1>/g;
73
+ for (const match of html.matchAll(headingPattern)) {
74
+ const heading = toPlainText(match[2] ?? "");
75
+ if (heading) headings.push(heading);
76
+ }
77
+ return headings;
78
+ }
79
+ function getEntryDate(frontmatter) {
80
+ const value = frontmatter.published ?? frontmatter.date ?? frontmatter.created;
81
+ if (value instanceof Date && !Number.isNaN(value.getTime()))
82
+ return value.toISOString();
83
+ if (typeof value !== "string" || !value.trim()) return null;
84
+ const date = new Date(value);
85
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
86
+ }
87
+ function toPlainText(html) {
88
+ return html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/\s+/g, " ").trim();
89
+ }
90
+
91
+ // src/garden-explorer-page.tsx
92
+ import { escapeScriptJson } from "@riebeckite/core";
93
+ import { renderToString } from "hono/jsx/dom/server";
3
94
 
4
95
  // components/garden-explorer.tsx
5
- import { layoutRadialGraph } from "@riebeckite/core";
96
+ import { layoutRadialGraph } from "@riebeckite/core/client";
6
97
  import { useEffect, useMemo, useState } from "hono/jsx";
7
98
 
8
99
  // src/search-notes.ts
@@ -454,92 +545,30 @@ function clamp(value, min, max) {
454
545
  return Math.min(Math.max(value, min), max);
455
546
  }
456
547
 
457
- // src/garden-explorer.server.ts
458
- import {
459
- buildGraphEdges,
460
- isPublished
461
- } from "@riebeckite/core";
462
- var MAX_BODY_LENGTH = 4e3;
463
- function getGardenExplorerData(args) {
464
- const graph = args.manifest.graph;
465
- const publishedEntries = graph.nodes().filter((entry) => isPublished(args.config, entry.frontmatter));
466
- const publishedSlugs = new Set(publishedEntries.map((entry) => entry.slug));
467
- const notes = publishedEntries.map(
468
- (entry) => toGardenNote(entry, graph, publishedSlugs, args.resolveTitle)
469
- ).sort((a, b) => a.title.localeCompare(b.title, "ja"));
470
- return {
471
- notes,
472
- edges: buildGraphEdges(notes, publishedSlugs),
473
- tags: buildTags(notes),
474
- folders: buildFolders(notes)
475
- };
476
- }
477
- function toGardenNote(entry, graph, publishedSlugs, resolveTitle) {
478
- const { outgoingSlugs, incomingSlugs } = graph.filterNeighbors(
479
- entry.slug,
480
- (slug) => publishedSlugs.has(slug)
481
- );
482
- return {
483
- slug: entry.slug,
484
- permalink: entry.permalink,
485
- title: resolveTitle(entry.slug, entry.frontmatter.title),
486
- headings: extractHeadings(entry.html),
487
- body: toPlainText(entry.html).slice(0, MAX_BODY_LENGTH),
488
- excerpt: createExcerpt(entry),
489
- tags: entry.tags,
490
- date: getEntryDate(entry.frontmatter),
491
- folder: getFolder(entry.slug),
492
- outgoing: outgoingSlugs,
493
- backlinks: incomingSlugs
494
- };
495
- }
496
- function buildTags(notes) {
497
- const counts = /* @__PURE__ */ new Map();
498
- for (const note of notes) {
499
- for (const tag of note.tags) counts.set(tag, (counts.get(tag) ?? 0) + 1);
500
- }
501
- return Array.from(counts, ([name, count]) => ({ name, count })).sort(
502
- (a, b) => b.count - a.count || a.name.localeCompare(b.name, "ja")
503
- );
504
- }
505
- function buildFolders(notes) {
506
- const counts = /* @__PURE__ */ new Map();
507
- for (const note of notes)
508
- counts.set(note.folder, (counts.get(note.folder) ?? 0) + 1);
509
- return Array.from(counts, ([path, count]) => ({ path, count })).sort(
510
- (a, b) => a.path.localeCompare(b.path, "ja")
548
+ // src/garden-explorer-page.tsx
549
+ import { jsx as jsx3, jsxs as jsxs3 } from "hono/jsx/jsx-runtime";
550
+ function renderGardenExplorerPage(data, siteTitle) {
551
+ return renderToString(
552
+ /* @__PURE__ */ jsxs3("main", { class: "garden-explorer-page", children: [
553
+ /* @__PURE__ */ jsxs3("header", { class: "garden-explorer-page__header", children: [
554
+ /* @__PURE__ */ jsx3("p", { class: "garden-explorer-page__eyebrow", children: siteTitle }),
555
+ /* @__PURE__ */ jsx3("h1", { class: "garden-explorer-page__title", children: "Garden Explorer" }),
556
+ /* @__PURE__ */ jsx3("p", { class: "garden-explorer-page__description", children: "Graph, Search, Tags, Folders, Backlinks, and Related Notes を横断して Digital Garden を探索できます。" })
557
+ ] }),
558
+ /* @__PURE__ */ jsx3("div", { "data-riebeckite-garden-explorer": true, children: /* @__PURE__ */ jsx3(GardenExplorer, { data }) }),
559
+ /* @__PURE__ */ jsx3(
560
+ "script",
561
+ {
562
+ id: "riebeckite-garden-explorer-data",
563
+ type: "application/json",
564
+ dangerouslySetInnerHTML: {
565
+ __html: escapeScriptJson(JSON.stringify(data))
566
+ }
567
+ }
568
+ )
569
+ ] })
511
570
  );
512
571
  }
513
- function getFolder(slug) {
514
- const segments = slug.split("/");
515
- if (segments.length <= 1) return "Root";
516
- return segments.slice(0, -1).join("/");
517
- }
518
- function createExcerpt(entry) {
519
- const description = entry.frontmatter.description;
520
- const text = typeof description === "string" && description.trim() ? description.trim() : toPlainText(entry.html);
521
- return text.slice(0, 180);
522
- }
523
- function extractHeadings(html) {
524
- const headings = [];
525
- const headingPattern = /<h([1-4])\b[^>]*>([\s\S]*?)<\/h\1>/g;
526
- for (const match of html.matchAll(headingPattern)) {
527
- const heading = toPlainText(match[2] ?? "");
528
- if (heading) headings.push(heading);
529
- }
530
- return headings;
531
- }
532
- function getEntryDate(frontmatter) {
533
- const value = frontmatter.published ?? frontmatter.date ?? frontmatter.created;
534
- if (value instanceof Date && !Number.isNaN(value.getTime()))
535
- return value.toISOString();
536
- if (typeof value !== "string" || !value.trim()) return null;
537
- const date = new Date(value);
538
- return Number.isNaN(date.getTime()) ? null : date.toISOString();
539
- }
540
- function toPlainText(html) {
541
- return html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/\s+/g, " ").trim();
542
- }
543
572
 
544
573
  // index.ts
545
574
  function gardenExplorerPlugin() {
@@ -551,6 +580,31 @@ function gardenExplorerPlugin() {
551
580
  kind: "style",
552
581
  moduleSpecifier: "@riebeckite/plugin-garden-explorer/style.css"
553
582
  }
583
+ ],
584
+ clientEntries: [createClientEntry("garden-explorer")],
585
+ pageTypes: [
586
+ {
587
+ id: "garden-explorer",
588
+ paths: ["/explore"],
589
+ resolve: ({ pathname, manifest, config }) => {
590
+ if (pathname !== "/explore" || !config) return null;
591
+ const siteTitle = config.site.title;
592
+ return {
593
+ type: "garden-explorer",
594
+ pathname,
595
+ title: "Garden Explorer",
596
+ description: "Explore notes through links, backlinks, tags, and folders.",
597
+ body: renderGardenExplorerPage(
598
+ getGardenExplorerData({
599
+ manifest,
600
+ config,
601
+ resolveTitle: (slug, title) => typeof title === "string" && title.trim() ? title : slug
602
+ }),
603
+ siteTitle
604
+ )
605
+ };
606
+ }
607
+ }
554
608
  ]
555
609
  });
556
610
  }
@@ -0,0 +1,2 @@
1
+ import type { GardenExplorerData } from "./garden-explorer.js";
2
+ export declare function renderGardenExplorerPage(data: GardenExplorerData, siteTitle: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/plugin-garden-explorer",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,11 +18,17 @@
18
18
  "source": "./components/garden-explorer.tsx",
19
19
  "default": "./dist/components/garden-explorer.js"
20
20
  },
21
+ "./client": {
22
+ "types": "./dist/client.d.ts",
23
+ "import": "./dist/client.js",
24
+ "source": "./client.tsx",
25
+ "default": "./dist/client.js"
26
+ },
21
27
  "./style.css": "./style.css"
22
28
  },
23
29
  "dependencies": {
24
30
  "hono": "^4.12.25",
25
- "@riebeckite/core": "0.0.12"
31
+ "@riebeckite/core": "0.0.13"
26
32
  },
27
33
  "publishConfig": {
28
34
  "access": "public"