@riebeckite/plugin-navigation 0.0.18 → 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 +34 -5
- package/README_ja.md +35 -5
- package/dist/components/site-nav.d.ts +28 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +83 -1
- package/package.json +11 -6
- package/style.css +29 -0
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
|
|
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.
|
|
127
|
-
- [Plugin guide](../../../docs/docs/reference/plugin-api.
|
|
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.
|
|
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
|
-
"
|
|
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.
|
|
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":
|
|
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
|
+
}
|