@riebeckite/plugin-navigation 0.0.19 → 0.0.20

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
@@ -3,8 +3,8 @@
3
3
  Site navigation as a Riebeckite plugin. `navigation()` derives the primary
4
4
  navigation from the notes already in the vault, so an existing Obsidian vault
5
5
  becomes navigable with no Riebeckite-specific file and no required frontmatter.
6
- The plugin owns the navigation *model*; the Site shell owns rendering and
7
- placement.
6
+ The plugin owns the navigation *model* and the *rendering mechanics*; the Site
7
+ owns placement — where each rendered list appears.
8
8
 
9
9
  [日本語](./README_ja.md)
10
10
 
@@ -52,6 +52,34 @@ const secondary = model?.secondary ?? [];
52
52
  `resolveSiteNavigation` returns `null` when the plugin is not registered, so a
53
53
  site without navigation keeps working.
54
54
 
55
+ ### Rendering
56
+
57
+ `SiteNav` renders a resolved tree as a `<nav>` landmark with nested lists. It
58
+ owns the rendering mechanics — recursive children, active-path detection,
59
+ locale-aware normalization, external-link handling, and `aria-current` — and
60
+ ships the structural CSS for that `rb-nav` tree in its own `style.css`, loaded
61
+ through the generated plugin styles. The Site decides where the tree is placed
62
+ and whether to wrap it in a `<details>` element on small screens:
63
+
64
+ ```tsx
65
+ import { SiteNav } from "@riebeckite/plugin-navigation";
66
+
67
+ <header class="site-header rb-site-header">
68
+ <a href="/" class="site-header__home rb-site-header__home">My Site</a>
69
+ <SiteNav
70
+ items={primary}
71
+ path={c.req.path}
72
+ language={c.get("htmlLanguage")}
73
+ />
74
+ </header>;
75
+ ```
76
+
77
+ `path` is the current request path. `language` is the current content language;
78
+ when set, `SiteNav` strips a matching `/language` prefix before resolving the
79
+ active item, so unlocalized authored hrefs still match localized pages. Pass
80
+ `localizeHref` to rewrite hrefs for the current language, `label` to change the
81
+ landmark label, and `class`/`className` to extend the `<nav>` classes.
82
+
55
83
  ## Authored navigation
56
84
 
57
85
  Automated derivation cannot express everything, and some sites curate their
@@ -109,8 +137,9 @@ tab.
109
137
  - `resolveSiteNavigation(config, manifest)` — find the enabled plugin and build
110
138
  the model, or `null`
111
139
  - `validateNavigationOptions(options)` — option validation
140
+ - `SiteNav` — render a resolved tree with the standard `rb-nav` structure
112
141
  - Types: `NavigationItem`, `NavigationOptions`, `NavigationSource`,
113
- `SiteNavigation`
142
+ `SiteNavigation`, `SiteNavProps`
114
143
 
115
144
  ## Limitations
116
145
 
@@ -123,5 +152,5 @@ tab.
123
152
 
124
153
  ## See also
125
154
 
126
- - [Configuration reference](../../../docs/docs/reference/configuration.en.md)
127
- - [Plugin guide](../../../docs/docs/reference/plugin-api.en.md)
155
+ - [Configuration reference](../../../docs/docs/reference/configuration.md)
156
+ - [Plugin guide](../../../docs/docs/reference/plugin-api.md)
package/README_ja.md CHANGED
@@ -3,8 +3,8 @@
3
3
  サイトナビゲーションを提供する Riebeckite プラグインです。`navigation()` は
4
4
  Vault にすでにあるノートから主要ナビゲーションを導出します。Riebeckite 専用の
5
5
  ファイルも必須 frontmatter も不要なので、既存の Obsidian Vault をそのまま
6
- 公開できます。ナビゲーションの「モデル」はプラグインが持ち、描画と配置は
7
- Site シェルが担当します。
6
+ 公開できます。ナビゲーションの「モデル」と描画の仕組みはプラグインが持ち、
7
+ 配置は Site が担当します。
8
8
 
9
9
  [English](./README.md)
10
10
 
@@ -52,6 +52,35 @@ const secondary = model?.secondary ?? [];
52
52
  プラグインが登録されていない場合、`resolveSiteNavigation` は `null` を返すため、
53
53
  ナビゲーションのないサイトもそのまま動作します。
54
54
 
55
+ ### 描画
56
+
57
+ `SiteNav` は解決済みのツリーを `<nav>` ランドマークとネストしたリストとして
58
+ 描画します。子の再帰描画、現在パスの判定、言語を考慮した正規化、外部リンクの
59
+ 扱い、`aria-current` といった描画の仕組みを担当し、その `rb-nav` ツリーを
60
+ 成立させる構造 CSS を自身の `style.css` に同梱します(自動生成される plugin
61
+ styles 経由で読み込まれます)。ツリーをどこに置くか、モバイルで `<details>` を
62
+ 使うかは Site が決めます。
63
+
64
+ ```tsx
65
+ import { SiteNav } from "@riebeckite/plugin-navigation";
66
+
67
+ <header class="site-header rb-site-header">
68
+ <a href="/" class="site-header__home rb-site-header__home">My Site</a>
69
+ <SiteNav
70
+ items={primary}
71
+ path={c.req.path}
72
+ language={c.get("htmlLanguage")}
73
+ />
74
+ </header>;
75
+ ```
76
+
77
+ `path` は現在のリクエストパスです。`language` は現在のコンテンツ言語で、指定すると
78
+ `SiteNav` は `/language` プレフィックスを取り除いてから現在のアイテムを判定します。
79
+ そのため、言語プレフィックスのない手書き href でもローカライズされたページに
80
+ 一致します。`localizeHref` を渡すと現在の言語に合わせて href を書き換えられます。
81
+ `label` でランドマークのラベルを、`class` / `className` で `<nav>` のクラスを
82
+ 拡張できます。
83
+
55
84
  ## 手書きナビゲーション
56
85
 
57
86
  自動導出ですべてを表現できるわけではなく、リンクを意図的に整えたいサイトも
@@ -109,8 +138,9 @@ navigation({
109
138
  - `resolveSiteNavigation(config, manifest)` — 有効なプラグインを見つけてモデルを
110
139
  構築する。なければ `null`
111
140
  - `validateNavigationOptions(options)` — オプションの検証
141
+ - `SiteNav` — 解決済みツリーを標準の `rb-nav` 構造で描画する
112
142
  - 型: `NavigationItem`, `NavigationOptions`, `NavigationSource`,
113
- `SiteNavigation`
143
+ `SiteNavigation`, `SiteNavProps`
114
144
 
115
145
  ## 制約
116
146
 
@@ -123,5 +153,5 @@ navigation({
123
153
 
124
154
  ## 関連リンク
125
155
 
126
- - [設定リファレンス](../../../docs/docs/reference/configuration.md)
127
- - [プラグイン API](../../../docs/docs/reference/plugin-api.md)
156
+ - [設定リファレンス](../../../docs/docs/reference/configuration.ja.md)
157
+ - [プラグイン API](../../../docs/docs/reference/plugin-api.ja.md)
@@ -0,0 +1,28 @@
1
+ import type { NavigationItem } from "../src/types.js";
2
+ export type SiteNavProps = {
3
+ /** Navigation tree to render, in order. */
4
+ readonly items: readonly NavigationItem[];
5
+ /** Current request path, used to mark the active item. */
6
+ readonly path: string;
7
+ /**
8
+ * Current content language. When set, a matching `/language` prefix is
9
+ * stripped from `path` before the active item is resolved.
10
+ */
11
+ readonly language?: string;
12
+ /** Accessible label for the `<nav>` landmark. Defaults to "Site navigation". */
13
+ readonly label?: string;
14
+ /** Rewrites each internal href before it is rendered. */
15
+ readonly localizeHref?: (href: string) => string;
16
+ /** Extra classes appended to the `<nav>` landmark. */
17
+ readonly class?: string;
18
+ readonly className?: string;
19
+ };
20
+ /**
21
+ * Renders a resolved navigation tree as a `<nav>` landmark with nested lists.
22
+ *
23
+ * Owns the navigation rendering mechanics: recursive list markup, active-path
24
+ * semantics with locale-aware normalization, external-link handling, and the
25
+ * `aria-current` accessibility contract. The Site owns placement and decides
26
+ * where each tree is rendered.
27
+ */
28
+ export declare function SiteNav(props: SiteNavProps): import("hono/jsx/jsx-dev-runtime").JSX.Element;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { NavigationOptions } from "./src/types.js";
2
+ export type { SiteNavProps } from "./components/site-nav.js";
3
+ export { SiteNav } from "./components/site-nav.js";
2
4
  export { buildNavigation, NAVIGATION_PLUGIN_NAME, resolveSiteNavigation, } from "./src/navigation.js";
3
5
  export type { NavigationItem, NavigationOptions, NavigationSource, SiteNavigation, } from "./src/types.js";
4
6
  export declare function navigation(options?: NavigationOptions): import("@riebeckite/core").RiebeckitePlugin<NavigationOptions>;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // index.ts
2
- import { definePlugin } from "@riebeckite/core";
2
+ import { createStyleAsset, definePlugin } from "@riebeckite/core";
3
3
 
4
4
  // src/navigation.ts
5
5
  import {
@@ -158,11 +158,92 @@ function isRecord(value) {
158
158
  return typeof value === "object" && value !== null && !Array.isArray(value);
159
159
  }
160
160
 
161
+ // components/site-nav.tsx
162
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
163
+ function SiteNav(props) {
164
+ return /* @__PURE__ */ jsx(
165
+ "nav",
166
+ {
167
+ class: joinClassNames("rb-nav", props.class, props.className),
168
+ "aria-label": props.label ?? "Site navigation",
169
+ children: /* @__PURE__ */ jsx(
170
+ NavigationItems,
171
+ {
172
+ items: props.items,
173
+ path: props.path,
174
+ language: props.language,
175
+ localizeHref: props.localizeHref
176
+ }
177
+ )
178
+ }
179
+ );
180
+ }
181
+ function NavigationItems(props) {
182
+ return /* @__PURE__ */ jsx("ul", { class: props.nested ? "rb-nav__list rb-nav__children" : "rb-nav__list", children: props.items.map((item) => /* @__PURE__ */ jsx(
183
+ NavigationItemView,
184
+ {
185
+ item,
186
+ path: props.path,
187
+ language: props.language,
188
+ localizeHref: props.localizeHref
189
+ }
190
+ )) });
191
+ }
192
+ function NavigationItemView(props) {
193
+ const { item, path, language, localizeHref } = props;
194
+ const active = item.href ? isActive(item.href, path, language) : false;
195
+ const href = item.href ? localizeHref?.(item.href) ?? item.href : void 0;
196
+ return /* @__PURE__ */ jsxs("li", { class: "rb-nav__item", children: [
197
+ href ? /* @__PURE__ */ jsx(
198
+ "a",
199
+ {
200
+ href,
201
+ class: active ? "rb-nav__link rb-nav__link--active" : "rb-nav__link",
202
+ "aria-current": active ? "page" : void 0,
203
+ target: item.external ? "_blank" : void 0,
204
+ rel: item.external ? "noreferrer" : void 0,
205
+ children: item.label
206
+ }
207
+ ) : /* @__PURE__ */ jsx("span", { class: "rb-nav__label", children: item.label }),
208
+ item.children && item.children.length > 0 ? /* @__PURE__ */ jsx(
209
+ NavigationItems,
210
+ {
211
+ items: item.children,
212
+ path,
213
+ language,
214
+ localizeHref,
215
+ nested: true
216
+ }
217
+ ) : null
218
+ ] });
219
+ }
220
+ function isActive(href, path, language) {
221
+ if (!href.startsWith("/")) return false;
222
+ const target = normalizePath(href);
223
+ const current = normalizePath(stripLanguagePrefix(path, language));
224
+ if (target === "/") return current === target;
225
+ return current === target || current.startsWith(`${target}/`);
226
+ }
227
+ function normalizePath(path) {
228
+ return path.replace(/\/+$/, "") || "/";
229
+ }
230
+ function stripLanguagePrefix(path, language) {
231
+ if (!language) return path;
232
+ if (path === `/${language}`) return "/";
233
+ const prefix = `/${language}/`;
234
+ if (path.startsWith(prefix)) return path.slice(language.length + 1);
235
+ return path;
236
+ }
237
+ function joinClassNames(...classNames) {
238
+ return classNames.filter(Boolean).join(" ");
239
+ }
240
+
161
241
  // index.ts
162
242
  function navigation(options = {}) {
163
243
  return definePlugin({
164
244
  name: NAVIGATION_PLUGIN_NAME,
165
245
  options,
246
+ assets: [createStyleAsset("navigation")],
166
247
  validateOptions: validateNavigationOptions,
167
248
  outputDependencies: [{ type: "global" }]
168
249
  });
@@ -170,6 +251,7 @@ function navigation(options = {}) {
170
251
  var navigationPlugin = navigation;
171
252
  export {
172
253
  NAVIGATION_PLUGIN_NAME,
254
+ SiteNav,
173
255
  buildNavigation,
174
256
  navigation,
175
257
  navigationPlugin,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/plugin-navigation",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -11,14 +11,16 @@
11
11
  "import": "./dist/index.js",
12
12
  "source": "./index.ts",
13
13
  "default": "./dist/index.js"
14
- }
14
+ },
15
+ "./style.css": "./style.css"
15
16
  },
16
17
  "dependencies": {
17
- "@riebeckite/core": "0.0.19"
18
+ "hono": "^4.12.25",
19
+ "@riebeckite/core": "0.0.20"
18
20
  },
19
21
  "devDependencies": {
20
22
  "tsx": "^4.22.4",
21
- "@riebeckite/test": "0.0.19"
23
+ "@riebeckite/test": "0.0.20"
22
24
  },
23
25
  "publishConfig": {
24
26
  "access": "public"
@@ -27,7 +29,8 @@
27
29
  "LICENSE",
28
30
  "README.md",
29
31
  "README_ja.md",
30
- "dist"
32
+ "dist",
33
+ "style.css"
31
34
  ],
32
35
  "repository": {
33
36
  "type": "git",
@@ -47,7 +50,9 @@
47
50
  "engines": {
48
51
  "node": "^20.19.0 || >=22.12.0"
49
52
  },
50
- "sideEffects": false,
53
+ "sideEffects": [
54
+ "./style.css"
55
+ ],
51
56
  "scripts": {
52
57
  "build": "node ../../../scripts/build_package.mjs",
53
58
  "test": "node --import tsx --test \"test/*.test.ts\""
package/style.css ADDED
@@ -0,0 +1,29 @@
1
+ @layer components {
2
+ .rb-nav > .rb-nav__list,
3
+ .rb-nav__children {
4
+ margin: 0;
5
+ list-style: none;
6
+ }
7
+
8
+ .rb-nav > .rb-nav__list {
9
+ padding: 0;
10
+ }
11
+
12
+ .rb-nav__item {
13
+ position: relative;
14
+ }
15
+
16
+ .rb-nav__children {
17
+ display: none;
18
+ position: absolute;
19
+ top: 100%;
20
+ left: 0;
21
+ min-width: max-content;
22
+ }
23
+
24
+ .rb-nav__item:hover > .rb-nav__children,
25
+ .rb-nav__item:focus-within > .rb-nav__children {
26
+ display: flex;
27
+ flex-direction: column;
28
+ }
29
+ }