@riebeckite/plugin-sidenotes 0.0.31 → 0.0.33

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
@@ -2,33 +2,38 @@
2
2
 
3
3
  <!-- Generated from docs/docs/plugins/sidenotes.md. Edit the canonical documentation in docs/docs/plugins and run `pnpm docs:sync`. -->
4
4
 
5
- Tufte-style side notes for Riebeckite. Authors keep writing ordinary GFM
6
- footnotes (`[^1]` and `[^1]: text`); the plugin rewrites the generated
7
- footnote markup into an inline reference plus a note that renders as a margin
8
- note on desktop and as a tap-open popover on mobile.
5
+ Footnote styling for Riebeckite. Authors keep writing ordinary GFM footnotes
6
+ (`[^1]` and `[^1]: text`). By default the definitions stay in the trailing
7
+ footnote list at the end of the article. Set `placement: "margin"` to render
8
+ them as Tufte-style margin notes instead.
9
9
 
10
10
  [日本語](./README_ja.md)
11
11
 
12
12
  ## Overview
13
13
 
14
- The plugin registers a rehype plugin that rewrites the footnote markup the
15
- core pipeline produces from `remark-gfm`:
16
-
17
- - Each footnote reference becomes a `sup` with a small reference link
18
- (`[data-rr-sidenotes-ref]`) that keeps its `href="#fn-…"` target, so the
19
- footnote stays reachable with plain browser navigation.
20
- - Each footnote definition becomes a `.rr-sidenotes__note` aside next to the
21
- reference: an inline note from tablet width (`@media (min-width: 48rem)`), a
22
- sticky **margin note** when there is room for an article rail
23
- (`@media (min-width: 88rem)`), and a **popover** on mobile
24
- (`@media (max-width: 48rem)`).
25
- - The trailing footnote definitions section is kept (it is the link target on
26
- mobile and the no-JavaScript fallback) and hidden from tablet width up, where
27
- the inline or margin notes are always visible.
28
-
29
- A small client entry (`initSidenotes`) toggles the mobile popover: tap to
30
- open/close, `Escape` to close, and click outside to close. It ignores the page
31
- when `(min-width: 48rem)` matches — desktop margin notes need no JavaScript.
14
+ The plugin registers a rehype plugin that reads the footnote markup the core
15
+ pipeline produces from `remark-gfm`.
16
+
17
+ With the default `placement: "end"`:
18
+
19
+ - The trailing `<section data-footnotes>` gets `.rr-sidenotes__footnotes` and
20
+ `data-rr-sidenotes-placement="end"`.
21
+ - Each reference gets `rr-sidenotes__ref` on its `sup` and
22
+ `rr-sidenotes__toggle` on the link. The native `href`, `id`, and back-links
23
+ are left untouched, so navigation keeps working without JavaScript.
24
+ - The section stays at the end of the article at every width.
25
+
26
+ With `placement: "margin"`:
27
+
28
+ - Each definition becomes a `.rr-sidenotes__note` aside after the nearest block
29
+ that contains its first reference. The note stays in the article flow until
30
+ there is room for an article rail, then becomes sticky in the right margin
31
+ (`@media (min-width: 88rem)`).
32
+ - The reference points at the note (`href="#rr-sidenotes-…"`).
33
+ - The trailing definitions section is hidden with
34
+ `data-rr-sidenotes-placement="margin"`.
35
+
36
+ The plugin ships no client entry; both placements work without JavaScript.
32
37
 
33
38
  ## Usage
34
39
 
@@ -45,53 +50,79 @@ export default defineConfig({
45
50
  Write normal GFM footnotes:
46
51
 
47
52
  ```md
48
- Riebeckite renders margin notes at the side of the text.[^1]
53
+ Riebeckite keeps notes in the footnote list at the end.[^1]
49
54
 
50
- [^1]: The note text appears beside the reference on desktop and in a popover on mobile.
55
+ [^1]: The note text appears at the end of the article.
51
56
  ```
52
57
 
58
+ To render margin notes instead:
59
+
60
+ ```ts
61
+ plugins: [sidenotes({ placement: "margin" })];
62
+ ```
63
+
64
+ ## Live example
65
+
66
+ A footnote reference appears here.[^live-example]
67
+
68
+ [^live-example]: The note is rendered by the sidenotes plugin.
69
+
53
70
  ## Options
54
71
 
55
72
  | Option | Type | Default | Description |
56
73
  | ------ | ---- | ------- | ----------- |
57
- | `className` | `string` | `""` | Extra CSS class on each sidenote root |
58
- | `ariaLabel` | `string` | `"Sidenote"` | Accessible name prefix for each note (`"Sidenote 1"`) |
59
- | `openLabel` | `string` | `"Footnote"` | Accessible label prefix for a closed reference (`"Footnote 1"`) |
60
- | `closeLabel` | `string` | `"Close sidenote"` | Accessible label prefix for an open reference (`"Close sidenote 1"`) |
61
- | `popoverAlignment` | `"bottom" \| "end"` | `"bottom"` | Where the mobile popover is anchored |
74
+ | `placement` | `"end" \| "margin"` | `"end"` | `"end"` keeps the trailing footnote list; `"margin"` moves definitions next to their references |
75
+ | `className` | `string` | `""` | Extra CSS class on each margin note (ignored when `placement` is `"end"`) |
76
+ | `ariaLabel` | `string` | `"Sidenote"` | Accessible name prefix for each margin note (`"Sidenote 1"`) |
77
+ | `openLabel` | `string` | `"Footnote"` | Accessible label prefix for each reference (`"Footnote 1"`) |
62
78
 
63
79
  ```ts
64
80
  sidenotes({
81
+ placement: "margin",
65
82
  ariaLabel: "Note",
66
- popoverAlignment: "end",
67
83
  });
68
84
  ```
69
85
 
70
86
  ## Output
71
87
 
88
+ Default (`placement: "end"`):
89
+
90
+ ```html
91
+ <p>
92
+ Riebeckite keeps notes here.<sup class="rr-sidenotes__ref">
93
+ <a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref
94
+ class="rr-sidenotes__toggle" aria-label="Footnote 1">1</a></sup>
95
+ </p>
96
+ <section data-footnotes class="footnotes rr-sidenotes__footnotes"
97
+ data-rr-sidenotes-placement="end">
98
+ <h2 class="sr-only" id="footnote-label">Footnotes</h2>
99
+ <ol><li id="user-content-fn-1"><p>The note text.</p></li></ol>
100
+ </section>
101
+ ```
102
+
103
+ `placement: "margin"`:
104
+
72
105
  ```html
73
106
  <p>
74
- Riebeckite renders margin notes here.<sup class="rr-sidenotes__ref">
75
- <a href="#user-content-fn-1" id="user-content-fnref-1" class="rr-sidenotes__toggle"
76
- data-rr-sidenotes-ref aria-expanded="false" aria-controls="rr-sidenotes-1"
77
- aria-label="Footnote 1">1</a></sup>
107
+ Riebeckite keeps notes here.<sup class="rr-sidenotes__ref">
108
+ <a href="#rr-sidenotes-1" id="user-content-fnref-1" class="rr-sidenotes__toggle"
109
+ data-rr-sidenotes-ref aria-label="Footnote 1">1</a></sup>
78
110
  </p>
79
- <aside class="rr-sidenotes__note rr-sidenotes__note--popover-bottom" id="rr-sidenotes-1"
111
+ <aside class="rr-sidenotes__note" id="rr-sidenotes-1"
80
112
  data-rr-sidenotes-note aria-label="Sidenote 1" tabindex="-1">
81
113
  <span class="rr-sidenotes__index" aria-hidden="true">1</span>
82
- <div class="rr-sidenotes__body"><p>The note text appears beside the reference.</p></div>
114
+ <div class="rr-sidenotes__body"><p>The note text.</p></div>
83
115
  </aside>
84
116
  ```
85
117
 
86
118
  ## Accessibility
87
119
 
88
- - The reference is a real link (`href="#user-content-fn-…"`) with `aria-expanded`
89
- and `aria-controls`; the client toggles `aria-expanded` and swaps the label
90
- between `openLabel` and `closeLabel`.
91
- - The popover is labelled with `aria-label` and receives focus (`tabindex="-1"`)
92
- when opened via a script.
93
- - Without JavaScript, tapping the reference jumps to the footnote definitions
94
- as in ordinary GFM output.
120
+ - With `placement: "end"`, references keep the native GFM href, id, and
121
+ back-links; only the `aria-label` from `openLabel` is added.
122
+ - With `placement: "margin"`, the reference links to the note, and the note is
123
+ labelled with `aria-label` and can receive focus (`tabindex="-1"`).
124
+ - The footnote list and references use `scroll-margin-block-start` so the
125
+ target is not hidden behind a sticky header.
95
126
 
96
127
  ## Style
97
128
 
@@ -102,25 +133,24 @@ import "@riebeckite/plugin-sidenotes/style.css";
102
133
  ```
103
134
 
104
135
  Stable hooks follow the `rr-sidenotes` convention: `rr-sidenotes__toggle`,
105
- `rr-sidenotes__note`, `rr-sidenotes__index`, `rr-sidenotes__body`,
106
- `rr-sidenotes__footnotes`, plus the `rr-sidenotes__note--open` modifier.
136
+ `rr-sidenotes__ref`, `rr-sidenotes__note`, `rr-sidenotes__index`,
137
+ `rr-sidenotes__body`, and `rr-sidenotes__footnotes`.
107
138
 
108
139
  ## Exports
109
140
 
110
141
  - `sidenotes(options?)` — plugin factory
111
- - `sidenotesPlugin` — alias of `sidenotes`
142
+ - `sidenotes` — alias of `sidenotes`
112
143
  - `rehypeSidenotes(options?)` — the rehype transformer
113
144
  - `resolveSidenotesOptions(options?)` — apply option defaults
114
145
  - `renderSidenotesReference(input)` / `renderSidenotesNote(input)` — HTML builders
115
- - `initSidenotes(options?)` — client popover initializer
116
- - Types: `SidenotesOptions`, `ResolvedSidenotesOptions`, `SidenotesClientOptions`
146
+ - Types: `SidenotesOptions`, `ResolvedSidenotesOptions`
117
147
 
118
148
  ## Limitations
119
149
 
120
- - Footnotes that reuse the same definition share one margin note and popover.
121
- - The footnote definitions section is hidden from tablet width up. On widths
122
- that cannot fit an article rail, notes remain in the article flow; themes can
123
- restyle `.rr-sidenotes__note` freely.
150
+ - With `placement: "margin"`, footnotes that reuse the same definition share
151
+ one note.
152
+ - In margin placement the trailing definitions section stays in the DOM but is
153
+ hidden; themes can restyle `.rr-sidenotes__note` freely.
124
154
  - Notes inside tables or deeply nested inline markup are placed after the
125
155
  nearest block ancestor, so their vertical position is approximate.
126
156
 
package/README_ja.md CHANGED
@@ -2,35 +2,34 @@
2
2
 
3
3
  <!-- Generated from docs/docs/plugins/sidenotes.ja.md. Edit the canonical documentation in docs/docs/plugins and run `pnpm docs:sync`. -->
4
4
 
5
- Riebeckite 用の Tufte スタイルのサイドノート・プラグインです。執筆者は通常の
6
- GFM 脚注(`[^1]` と `[^1]: 本文`)をそのまま書き続けます。プラグインが生成
7
- された脚注マークアップを、インラインの参照リンクと、デスクトップではマージン
8
- ノート、モバイルではタップで開くポップオーバーとして表示されるノートに書き
9
- 換えます。
5
+ Riebeckite 用の脚注スタイルプラグインです。執筆者は通常の GFM 脚注(`[^1]` と `[^1]: 本文`)をそのまま書き続けます。既定では脚注定義は記事末尾の脚注リストに残ります。`placement: "margin"` を指定すると、Tufte スタイルのマージンノートとして参照の横に表示します。
10
6
 
11
7
  [English](./README.md)
12
8
 
13
9
  ## 仕組み
14
10
 
15
- このプラグインは、`remark-gfm` の出力をコアのパイプラインが脚注マークアップ
16
- に変換したものを、rehype プラグインで書き換えます。
17
-
18
- - 脚注参照は `[data-rr-sidenotes-ref]` を持つ小さな参照リンク付きの `sup`
19
- になり、`href="#fn-…"` のリンク先は保持されます。通常のブラウザのリンク
20
- 遷移でも脚注に到達できます。
21
- - 脚注定義は参照のそばの `.rr-sidenotes__note` の aside になります。タブレット
22
- 幅以上(`@media (min-width: 48rem)`)では本文中のノートとして表示し、記事の
23
- 横に置ける幅がある場合(`@media (min-width: 88rem)`)は sticky な**マージン
24
- ノート**にします。モバイル(`@media (max-width: 48rem)`)では**ポップ
25
- オーバー**として表示されます。
26
- - 文末の脚注定義セクションは残します(モバイルではリンク先、JavaScript が
27
- 無効な場合のフォールバックになります)。タブレット幅以上では本文中または
28
- マージンのノートが常に見えるため、非表示にします。
29
-
30
- クライアントエントリ(`initSidenotes`)がモバイルのポップオーバーを切り替え
31
- ます。タップで開閉、`Escape` で閉じる、外側のクリックで閉じる、という動作です。
32
- `(min-width: 48rem)` に一致するときは動作しません。デスクトップのマージン
33
- ノートに JavaScript は不要です。
11
+ このプラグインは、コアのパイプラインが `remark-gfm` の出力を脚注マークアップに変換した後、rehype プラグインとしてそれを読み取ります。
12
+
13
+ 既定の `placement: "end"` では:
14
+
15
+ - 文末の `<section data-footnotes>` に `.rr-sidenotes__footnotes` と
16
+ `data-rr-sidenotes-placement="end"` が付きます。
17
+ - 各参照は `sup` に `rr-sidenotes__ref`、リンクに
18
+ `rr-sidenotes__toggle` が付きます。元の `href`、`id`、バックリンクはその
19
+ まま残るため、JavaScript なしで遷移できます。
20
+ - セクションは画面幅にかかわらず記事末尾に表示されます。
21
+
22
+ `placement: "margin"` では:
23
+
24
+ - 各定義は、最初の参照を含む最も近いブロックの後ろに
25
+ `.rr-sidenotes__note` の aside として置かれます。記事の横に置ける幅
26
+ (`@media (min-width: 88rem)`)までは本文フロー内に表示し、それ以上の幅では
27
+ 右マージンに sticky で表示します。
28
+ - 参照のリンク先はノート(`href="#rr-sidenotes-…"`)になります。
29
+ - 文末の脚注定義セクションは
30
+ `data-rr-sidenotes-placement="margin"` で非表示になります。
31
+
32
+ クライアントエントリはありません。どちらの配置も JavaScript なしで動作します。
34
33
 
35
34
  ## 使い方
36
35
 
@@ -47,53 +46,79 @@ export default defineConfig({
47
46
  通常の GFM 脚注を書くだけです。
48
47
 
49
48
  ```md
50
- Riebeckite は本文の横にマージンノートを表示します。[^1]
49
+ Riebeckite は脚注を記事末尾のリストに表示します。[^1]
51
50
 
52
- [^1]: デスクトップでは参照の横に、モバイルではポップオーバーでノートが表示されます。
51
+ [^1]: 記事の最後に注記が表示されます。
53
52
  ```
54
53
 
54
+ マージンノートにする場合:
55
+
56
+ ```ts
57
+ plugins: [sidenotes({ placement: "margin" })];
58
+ ```
59
+
60
+ ## 実行例
61
+
62
+ ここに脚注への参照を置きます。[^live-example]
63
+
64
+ [^live-example]: この注釈は sidenotes プラグインが描画します。
65
+
55
66
  ## オプション
56
67
 
57
68
  | オプション | 型 | 既定値 | 説明 |
58
69
  | ---------- | -- | ------ | ---- |
59
- | `className` | `string` | `""` | 各サイドノートのルートに追加する CSS クラス |
60
- | `ariaLabel` | `string` | `"Sidenote"` | 各ノートのアクセシブル名の接頭辞(`"Sidenote 1"` の形) |
61
- | `openLabel` | `string` | `"Footnote"` | 閉じた参照リンクのアクセシブル名の接頭辞(`"Footnote 1"`) |
62
- | `closeLabel` | `string` | `"Close sidenote"` | 開いた参照リンクのアクセシブル名の接頭辞(`"Close sidenote 1"`) |
63
- | `popoverAlignment` | `"bottom" \| "end"` | `"bottom"` | モバイルのポップオーバーの表示位置 |
70
+ | `placement` | `"end" \| "margin"` | `"end"` | `"end"` は文末の脚注リスト、`"margin"` は参照の横のマージンノート |
71
+ | `className` | `string` | `""` | 各マージンノートに追加する CSS クラス(`"end"` では無視) |
72
+ | `ariaLabel` | `string` | `"Sidenote"` | 各マージンノートのアクセシブル名の接頭辞(`"Sidenote 1"` の形) |
73
+ | `openLabel` | `string` | `"Footnote"` | 各参照リンクのアクセシブル名の接頭辞(`"Footnote 1"`) |
64
74
 
65
75
  ```ts
66
76
  sidenotes({
77
+ placement: "margin",
67
78
  ariaLabel: "注記",
68
- popoverAlignment: "end",
69
79
  });
70
80
  ```
71
81
 
72
82
  ## 出力
73
83
 
84
+ 既定(`placement: "end"`):
85
+
86
+ ```html
87
+ <p>
88
+ Riebeckite は脚注を記事末尾に表示します。<sup class="rr-sidenotes__ref">
89
+ <a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref
90
+ class="rr-sidenotes__toggle" aria-label="Footnote 1">1</a></sup>
91
+ </p>
92
+ <section data-footnotes class="footnotes rr-sidenotes__footnotes"
93
+ data-rr-sidenotes-placement="end">
94
+ <h2 class="sr-only" id="footnote-label">Footnotes</h2>
95
+ <ol><li id="user-content-fn-1"><p>記事の最後に注記が表示されます。</p></li></ol>
96
+ </section>
97
+ ```
98
+
99
+ `placement: "margin"`:
100
+
74
101
  ```html
75
102
  <p>
76
- Riebeckite は本文の横にマージンノートを表示します。<sup class="rr-sidenotes__ref">
77
- <a href="#user-content-fn-1" id="user-content-fnref-1" class="rr-sidenotes__toggle"
78
- data-rr-sidenotes-ref aria-expanded="false" aria-controls="rr-sidenotes-1"
79
- aria-label="Footnote 1">1</a></sup>
103
+ Riebeckite は参照の横に注記を表示します。<sup class="rr-sidenotes__ref">
104
+ <a href="#rr-sidenotes-1" id="user-content-fnref-1" class="rr-sidenotes__toggle"
105
+ data-rr-sidenotes-ref aria-label="Footnote 1">1</a></sup>
80
106
  </p>
81
- <aside class="rr-sidenotes__note rr-sidenotes__note--popover-bottom" id="rr-sidenotes-1"
107
+ <aside class="rr-sidenotes__note" id="rr-sidenotes-1"
82
108
  data-rr-sidenotes-note aria-label="Sidenote 1" tabindex="-1">
83
109
  <span class="rr-sidenotes__index" aria-hidden="true">1</span>
84
- <div class="rr-sidenotes__body"><p>デスクトップでは参照の横に表示されます。</p></div>
110
+ <div class="rr-sidenotes__body"><p>参照の横に表示される注記です。</p></div>
85
111
  </aside>
86
112
  ```
87
113
 
88
114
  ## アクセシビリティ
89
115
 
90
- - 参照リンクは実際のリンク(`href="#user-content-fn-…"`)で、`aria-expanded`
91
- と `aria-controls` を持ちます。クライアントは `aria-expanded` を切り替え、
92
- `openLabel` と `closeLabel` の間でラベルを差し替えます。
93
- - ポップオーバーは `aria-label` でラベル付けされ、開いたときは
94
- `tabindex="-1"` によりフォーカスを受け取ります。
95
- - JavaScript が無効でも、タップで通常の GFM と同じように脚注定義へジャンプ
96
- します。
116
+ - `placement: "end"` では、参照は GFM 標準の `href`、`id`、バックリンクを
117
+ そのまま保ち、`openLabel` から作った `aria-label` だけを追加します。
118
+ - `placement: "margin"` では、参照はノートへリンクします。ノートは
119
+ `aria-label` でラベル付けされ、`tabindex="-1"` でフォーカスを受け取れます。
120
+ - 脚注リストと参照には `scroll-margin-block-start` を指定し、sticky
121
+ ヘッダーに隠れないようにしています。
97
122
 
98
123
  ## スタイル
99
124
 
@@ -104,28 +129,25 @@ import "@riebeckite/plugin-sidenotes/style.css";
104
129
  ```
105
130
 
106
131
  安定フックは `rr-sidenotes` 規約に従います。`rr-sidenotes__toggle`、
107
- `rr-sidenotes__note`、`rr-sidenotes__index`、`rr-sidenotes__body`、
108
- `rr-sidenotes__footnotes`、そして `rr-sidenotes__note--open` 修飾子です。
132
+ `rr-sidenotes__ref`、`rr-sidenotes__note`、`rr-sidenotes__index`、
133
+ `rr-sidenotes__body`、`rr-sidenotes__footnotes` です。
109
134
 
110
135
  ## エクスポート
111
136
 
112
137
  - `sidenotes(options?)`:プラグインファクトリ
113
- - `sidenotesPlugin`:`sidenotes` のエイリアス
138
+ - `sidenotes`:`sidenotes` のエイリアス
114
139
  - `rehypeSidenotes(options?)`:rehype トランスフォーマー
115
140
  - `resolveSidenotesOptions(options?)`:オプションの既定値を適用する
116
141
  - `renderSidenotesReference(input)` / `renderSidenotesNote(input)`:HTML 生成関数
117
- - `initSidenotes(options?)`:クライアントのポップオーバー初期化関数
118
- - 型: `SidenotesOptions`, `ResolvedSidenotesOptions`, `SidenotesClientOptions`
142
+ - 型: `SidenotesOptions`, `ResolvedSidenotesOptions`
119
143
 
120
144
  ## 制約
121
145
 
122
- - 同じ定義を再利用した脚注は、1 つのマージンノートとポップオーバーを共有
146
+ - `placement: "margin"` では、同じ定義を再利用した脚注は 1 つのノートを共有
123
147
  します。
124
- - タブレット幅以上では脚注定義セクションを非表示にします。記事の横に置けない
125
- 幅ではノートを本文フローに残します。テーマは `.rr-sidenotes__note` を自由に
126
- 再スタイルできます。
127
- - テーブル内や深く入れ子になったインライン要素の中の注は、最も近いブロック
128
- 要素の後ろに置かれるため、縦位置はおおよその位置になります。
148
+ - マージン配置では、文末の脚注定義セクションは DOM に残りますが非表示に
149
+ なります。テーマは `.rr-sidenotes__note` を自由に再スタイルできます。
150
+ - テーブル内や深く入れ子になったインライン要素の中の注記は、最も近いブロック要素の後ろに置かれるため、縦位置はおおよその位置になります。
129
151
 
130
152
  ## 関連リンク
131
153
 
package/dist/index.d.ts CHANGED
@@ -1,17 +1,13 @@
1
1
  import type { SidenotesOptions } from "./src/types.js";
2
- export type { SidenotesClientOptions } from "./src/client.js";
3
- export { initSidenotes } from "./src/client.js";
4
2
  export { resolveSidenotesOptions } from "./src/options.js";
5
3
  export { rehypeSidenotes } from "./src/rehype.js";
6
4
  export { renderHastChildren, renderSidenotesNote, renderSidenotesReference, sidenotesKey, sidenotesNoteId, } from "./src/render.js";
7
5
  export type { ResolvedSidenotesOptions, SidenotesOptions, } from "./src/types.js";
8
6
  export declare const SIDENOTES_PLUGIN_NAME = "sidenotes";
9
7
  /**
10
- * Tufte-style side notes. Authors keep writing GFM footnotes (`[^1]` /
11
- * `[^1]: text`); a rehype plugin rewrites the generated footnote markup into
12
- * an inline reference plus a note that renders as a margin note on desktop
13
- * and as a tap-open popover on mobile.
8
+ * Styles GFM footnotes for Riebeckite. By default the definitions stay in the
9
+ * trailing footnote list at the end of the article. With
10
+ * `placement: "margin"`, a rehype plugin rewrites the generated footnote
11
+ * markup into an inline reference plus a margin note.
14
12
  */
15
13
  export declare function sidenotes(options?: SidenotesOptions): import("@riebeckite/core").RiebeckitePlugin<SidenotesOptions>;
16
- /** Alias matching the `*Plugin` suffix used by other plugin factories. */
17
- export declare const sidenotesPlugin: typeof sidenotes;
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  // index.ts
2
2
  import {
3
- createClientEntry,
4
3
  createStyleAsset,
5
4
  definePlugin
6
5
  } from "@riebeckite/core";
@@ -8,8 +7,7 @@ import {
8
7
  // src/options.ts
9
8
  var DEFAULT_SIDENOTES_ARIA_LABEL = "Sidenote";
10
9
  var DEFAULT_SIDENOTES_OPEN_LABEL = "Footnote";
11
- var DEFAULT_SIDENOTES_CLOSE_LABEL = "Close sidenote";
12
- var DEFAULT_SIDENOTES_POPOVER_ALIGNMENT = "bottom";
10
+ var DEFAULT_SIDENOTES_PLACEMENT = "end";
13
11
  function resolveSidenotesOptions(options = {}) {
14
12
  return {
15
13
  className: normalizeNonEmpty(options.className, ""),
@@ -21,13 +19,9 @@ function resolveSidenotesOptions(options = {}) {
21
19
  options.openLabel,
22
20
  DEFAULT_SIDENOTES_OPEN_LABEL
23
21
  ),
24
- closeLabel: normalizeNonEmpty(
25
- options.closeLabel,
26
- DEFAULT_SIDENOTES_CLOSE_LABEL
27
- ),
28
- popoverAlignment: normalizePopoverAlignment(
29
- options.popoverAlignment,
30
- DEFAULT_SIDENOTES_POPOVER_ALIGNMENT
22
+ placement: normalizePlacement(
23
+ options.placement,
24
+ DEFAULT_SIDENOTES_PLACEMENT
31
25
  )
32
26
  };
33
27
  }
@@ -36,15 +30,15 @@ function normalizeNonEmpty(value, fallback) {
36
30
  const trimmed = value.trim();
37
31
  return trimmed === "" ? fallback : trimmed;
38
32
  }
39
- function normalizePopoverAlignment(value, fallback) {
40
- return value === "end" ? "end" : value === "bottom" ? "bottom" : fallback;
33
+ function normalizePlacement(value, fallback) {
34
+ return value === "margin" ? "margin" : value === "end" ? "end" : fallback;
41
35
  }
42
36
 
43
37
  // src/render.ts
44
38
  import { escapeHtml, escapeHtmlAttribute } from "@riebeckite/core";
39
+ var SIDENOTES_REF_CLASS = "rr-sidenotes__ref";
45
40
  var SIDENOTES_TOGGLE_CLASS = "rr-sidenotes__toggle";
46
41
  var SIDENOTES_NOTE_CLASS = "rr-sidenotes__note";
47
- var SIDENOTES_POPOVER_PREFIX = "rr-sidenotes__note--popover-";
48
42
  var SIDENOTES_INDEX_CLASS = "rr-sidenotes__index";
49
43
  var SIDENOTES_BODY_CLASS = "rr-sidenotes__body";
50
44
  var SIDENOTES_FOOTNOTES_CLASS = "rr-sidenotes__footnotes";
@@ -62,15 +56,15 @@ function sidenotesNoteId(key) {
62
56
  }
63
57
  function renderSidenotesReference(input) {
64
58
  const { href, id, index, options } = input;
65
- const key = sidenotesKey(href) ?? "";
59
+ const key = sidenotesKey(href);
60
+ const target = key === null ? href : `#${sidenotesNoteId(key)}`;
66
61
  const label = `${options.openLabel} ${index}`.trim();
67
- return `<a href="${escapeHtmlAttribute(href)}" id="${escapeHtmlAttribute(id)}" class="${SIDENOTES_TOGGLE_CLASS}" data-rr-sidenotes-ref aria-expanded="false" aria-controls="${escapeHtmlAttribute(sidenotesNoteId(key))}" aria-label="${escapeHtmlAttribute(label)}">${escapeHtml(index)}</a>`;
62
+ return `<a href="${escapeHtmlAttribute(target)}" id="${escapeHtmlAttribute(id)}" class="${SIDENOTES_TOGGLE_CLASS}" data-rr-sidenotes-ref aria-label="${escapeHtmlAttribute(label)}">${escapeHtml(index)}</a>`;
68
63
  }
69
64
  function renderSidenotesNote(input) {
70
65
  const { key, index, bodyHtml, options } = input;
71
66
  const className = [
72
67
  SIDENOTES_NOTE_CLASS,
73
- `${SIDENOTES_POPOVER_PREFIX}${options.popoverAlignment}`,
74
68
  ...options.className === "" ? [] : [options.className]
75
69
  ].join(" ");
76
70
  const label = `${options.ariaLabel} ${index}`.trim();
@@ -137,6 +131,7 @@ function hyphenateAttribute(name) {
137
131
  var FOOTNOTE_REFERENCE_ATTRIBUTE = "dataFootnoteRef";
138
132
  var FOOTNOTES_SECTION_ATTRIBUTE = "dataFootnotes";
139
133
  var FOOTNOTE_ID_PREFIX_PATTERN = /^(?:user-content-)?fn-(.+)$/;
134
+ var PLACEMENT_ATTRIBUTE = "data-rr-sidenotes-placement";
140
135
  var BLOCK_TAG_NAMES = /* @__PURE__ */ new Set([
141
136
  "article",
142
137
  "aside",
@@ -170,7 +165,6 @@ var BLOCK_TAG_NAMES = /* @__PURE__ */ new Set([
170
165
  "tr",
171
166
  "ul"
172
167
  ]);
173
- var SIDENOTES_REF_CLASS = "rr-sidenotes__ref";
174
168
  function rehypeSidenotes(options = {}) {
175
169
  const resolved = resolveSidenotesOptions(options);
176
170
  return (tree) => {
@@ -183,6 +177,14 @@ function transformSidenotes(tree, options) {
183
177
  const notes = collectFootnoteNotes(section);
184
178
  if (notes.size === 0) return;
185
179
  addClassName(section, SIDENOTES_FOOTNOTES_CLASS);
180
+ section.properties = {
181
+ ...section.properties,
182
+ [PLACEMENT_ATTRIBUTE]: options.placement
183
+ };
184
+ if (options.placement === "end") {
185
+ markReferences(tree, options);
186
+ return;
187
+ }
186
188
  const insertions = [];
187
189
  walkElements(tree, [], (ref, parent, index, ancestors) => {
188
190
  if (!isFootnoteReference(ref)) return;
@@ -232,6 +234,14 @@ function transformSidenotes(tree, options) {
232
234
  });
233
235
  applyInsertions(insertions);
234
236
  }
237
+ function markReferences(tree, options) {
238
+ walkElements(tree, [], (ref, parent) => {
239
+ if (parent.tagName === "sup") addClassName(parent, SIDENOTES_REF_CLASS);
240
+ addClassName(ref, SIDENOTES_TOGGLE_CLASS);
241
+ const label = `${options.openLabel} ${getTextContent(ref)}`.trim();
242
+ ref.properties = { ...ref.properties, "aria-label": label };
243
+ });
244
+ }
235
245
  function findFootnoteSection(node) {
236
246
  if (node.type === "element" && node.tagName === "section" && hasProperty(node, FOOTNOTES_SECTION_ATTRIBUTE)) {
237
247
  return node;
@@ -369,85 +379,6 @@ function getChildren(node) {
369
379
  return Array.isArray(children) ? children : [];
370
380
  }
371
381
 
372
- // src/client.ts
373
- var DEFAULT_SELECTOR = "[data-rr-sidenotes-ref]";
374
- var NOTE_OPEN_CLASS = "rr-sidenotes__note--open";
375
- var DESKTOP_QUERY = "(min-width: 48rem)";
376
- var DEFAULT_OPEN_LABEL = "Footnote";
377
- var DEFAULT_CLOSE_LABEL = "Close sidenote";
378
- function initSidenotes(options = {}) {
379
- const desktopQuery = window.matchMedia(DESKTOP_QUERY);
380
- if (desktopQuery.matches) return;
381
- const toggles = Array.from(
382
- document.querySelectorAll(
383
- options.selector ?? DEFAULT_SELECTOR
384
- )
385
- );
386
- if (toggles.length === 0) return;
387
- const openLabel = options.openLabel ?? DEFAULT_OPEN_LABEL;
388
- const closeLabel = options.closeLabel ?? DEFAULT_CLOSE_LABEL;
389
- const noteFor = (toggle) => {
390
- const controls = toggle.getAttribute("aria-controls");
391
- if (!controls) return null;
392
- const note = document.getElementById(controls);
393
- return note instanceof HTMLElement ? note : null;
394
- };
395
- const isInsideSidenotes = (target) => {
396
- if (!target) return false;
397
- for (const toggle of toggles) {
398
- const note = noteFor(toggle);
399
- if (toggle.contains(target) || note?.contains(target)) return true;
400
- }
401
- return false;
402
- };
403
- const setOpen = (toggle, note, open) => {
404
- const index = toggle.textContent?.trim() ?? "";
405
- const label = open ? `${closeLabel} ${index}`.trim() : `${openLabel} ${index}`.trim();
406
- toggle.setAttribute("aria-expanded", String(open));
407
- toggle.setAttribute("aria-label", label);
408
- note.classList.toggle(NOTE_OPEN_CLASS, open);
409
- if (open) note.focus({ preventScroll: true });
410
- };
411
- const closeAll = () => {
412
- for (const toggle of toggles) {
413
- const note = noteFor(toggle);
414
- if (note?.classList.contains(NOTE_OPEN_CLASS)) {
415
- setOpen(toggle, note, false);
416
- }
417
- }
418
- };
419
- for (const toggle of toggles) {
420
- toggle.addEventListener("click", (event) => {
421
- event.preventDefault();
422
- const note = noteFor(toggle);
423
- if (!note) return;
424
- const isOpen = note.classList.contains(NOTE_OPEN_CLASS);
425
- if (!isOpen) closeAll();
426
- setOpen(toggle, note, !isOpen);
427
- });
428
- }
429
- const onKeydown = (event) => {
430
- if (event.key !== "Escape") return;
431
- closeAll();
432
- };
433
- const onClickOutside = (event) => {
434
- if (isInsideSidenotes(event.target instanceof Node ? event.target : null)) {
435
- return;
436
- }
437
- closeAll();
438
- };
439
- document.addEventListener("keydown", onKeydown);
440
- document.addEventListener("click", onClickOutside);
441
- const onViewportChange = (event) => {
442
- if (!event.matches) return;
443
- closeAll();
444
- document.removeEventListener("keydown", onKeydown);
445
- document.removeEventListener("click", onClickOutside);
446
- desktopQuery.removeEventListener("change", onViewportChange);
447
- };
448
- desktopQuery.addEventListener("change", onViewportChange);
449
- }
450
-
451
382
  // index.ts
452
383
  var SIDENOTES_PLUGIN_NAME = "sidenotes";
453
384
  function sidenotes(options = {}) {
@@ -455,7 +386,7 @@ function sidenotes(options = {}) {
455
386
  return definePlugin({
456
387
  name: SIDENOTES_PLUGIN_NAME,
457
388
  processedContentCache: {
458
- version: "sidenotes-v1",
389
+ version: "sidenotes-v2",
459
390
  dependencyMode: "none"
460
391
  },
461
392
  options,
@@ -466,46 +397,28 @@ function sidenotes(options = {}) {
466
397
  SIDENOTES_PLUGIN_NAME,
467
398
  "@riebeckite/plugin-sidenotes/style.css"
468
399
  )
469
- ],
470
- clientEntries: [
471
- createClientEntry(
472
- SIDENOTES_PLUGIN_NAME,
473
- "@riebeckite/plugin-sidenotes/client",
474
- "initSidenotes",
475
- {
476
- openLabel: resolved.openLabel,
477
- closeLabel: resolved.closeLabel
478
- }
479
- )
480
400
  ]
481
401
  });
482
402
  }
483
- var sidenotesPlugin = sidenotes;
484
403
  function validateSidenotesOptions(options) {
485
404
  if (!options) return [];
486
405
  const issues = [];
487
- for (const key of [
488
- "className",
489
- "ariaLabel",
490
- "openLabel",
491
- "closeLabel"
492
- ]) {
406
+ for (const key of ["className", "ariaLabel", "openLabel"]) {
493
407
  const value = options[key];
494
408
  if (value !== void 0 && (typeof value !== "string" || value.trim() === "")) {
495
409
  issues.push({ path: key, message: "Expected a non-empty string." });
496
410
  }
497
411
  }
498
- if (options.popoverAlignment !== void 0 && options.popoverAlignment !== "bottom" && options.popoverAlignment !== "end") {
412
+ if (options.placement !== void 0 && options.placement !== "end" && options.placement !== "margin") {
499
413
  issues.push({
500
- path: "popoverAlignment",
501
- message: 'Expected "bottom" or "end".'
414
+ path: "placement",
415
+ message: 'Expected "end" or "margin".'
502
416
  });
503
417
  }
504
418
  return issues;
505
419
  }
506
420
  export {
507
421
  SIDENOTES_PLUGIN_NAME,
508
- initSidenotes,
509
422
  rehypeSidenotes,
510
423
  renderHastChildren,
511
424
  renderSidenotesNote,
@@ -513,6 +426,5 @@ export {
513
426
  resolveSidenotesOptions,
514
427
  sidenotes,
515
428
  sidenotesKey,
516
- sidenotesNoteId,
517
- sidenotesPlugin
429
+ sidenotesNoteId
518
430
  };
@@ -1,8 +1,4 @@
1
1
  import type { ResolvedSidenotesOptions, SidenotesOptions } from "./types.js";
2
- export declare const DEFAULT_SIDENOTES_ARIA_LABEL = "Sidenote";
3
- export declare const DEFAULT_SIDENOTES_OPEN_LABEL = "Footnote";
4
- export declare const DEFAULT_SIDENOTES_CLOSE_LABEL = "Close sidenote";
5
- export declare const DEFAULT_SIDENOTES_POPOVER_ALIGNMENT = "bottom";
6
2
  /**
7
3
  * Applies defaults to `SidenotesOptions`. Pure and deterministic so the
8
4
  * plugin can resolve options once and reuse them for every processed tree.
@@ -2,19 +2,14 @@ import type { HastNode, ResolvedSidenotesOptions } from "./types.js";
2
2
  /** Feature hooks follow the `rr-sidenotes` convention from the CSS hooks doc. */
3
3
  export declare const SIDENOTES_REF_CLASS = "rr-sidenotes__ref";
4
4
  export declare const SIDENOTES_TOGGLE_CLASS = "rr-sidenotes__toggle";
5
- export declare const SIDENOTES_NOTE_CLASS = "rr-sidenotes__note";
6
- export declare const SIDENOTES_POPOVER_PREFIX = "rr-sidenotes__note--popover-";
7
- export declare const SIDENOTES_INDEX_CLASS = "rr-sidenotes__index";
8
- export declare const SIDENOTES_BODY_CLASS = "rr-sidenotes__body";
9
5
  export declare const SIDENOTES_FOOTNOTES_CLASS = "rr-sidenotes__footnotes";
10
- export declare const SIDENOTES_NOTE_ID_PREFIX = "rr-sidenotes-";
11
6
  /** Extracts the footnote key from a reference href. Returns `null` when the
12
7
  * href does not look like a footnote target. */
13
8
  export declare function sidenotesKey(href: string): string | null;
14
- /** Stable id shared by a sidenote and its toggles' `aria-controls`. */
9
+ /** Stable id shared by a sidenote and its reference links. */
15
10
  export declare function sidenotesNoteId(key: string): string;
16
- export type SidenotesReferenceInput = {
17
- /** Existing `href` of the GFM footnote reference, kept for reachability. */
11
+ type SidenotesReferenceInput = {
12
+ /** Existing `href` of the GFM footnote reference, used when no key exists. */
18
13
  href: string;
19
14
  /** Existing `id` of the GFM footnote reference, kept for back-links. */
20
15
  id: string;
@@ -23,22 +18,19 @@ export type SidenotesReferenceInput = {
23
18
  options: ResolvedSidenotesOptions;
24
19
  };
25
20
  /**
26
- * Renders the inline reference link that replaces a GFM footnote reference.
27
- * The surrounding `sup` is managed by the rehype transformer so it can reuse
28
- * the existing wrapper when one is already present.
21
+ * Renders the inline reference link that replaces a GFM footnote reference in
22
+ * margin placement. The surrounding `sup` is managed by the rehype transformer
23
+ * so it can reuse the existing wrapper when one is already present.
29
24
  */
30
25
  export declare function renderSidenotesReference(input: SidenotesReferenceInput): string;
31
- export type SidenotesNoteInput = {
26
+ type SidenotesNoteInput = {
32
27
  key: string;
33
28
  index: string;
34
29
  /** Serialized definition content (back-links stripped). */
35
30
  bodyHtml: string;
36
31
  options: ResolvedSidenotesOptions;
37
32
  };
38
- /**
39
- * Renders the sidenote: a static margin note on desktop and the popover
40
- * source on mobile.
41
- */
33
+ /** Renders the margin note definition as an `aside` next to its reference. */
42
34
  export declare function renderSidenotesNote(input: SidenotesNoteInput): string;
43
35
  /**
44
36
  * Serializes existing hast children into the HTML embedded in a sidenote
@@ -46,3 +38,4 @@ export declare function renderSidenotesNote(input: SidenotesNoteInput): string;
46
38
  * every text value and attribute is escaped here.
47
39
  */
48
40
  export declare function renderHastChildren(children: readonly HastNode[]): string;
41
+ export {};
@@ -1,40 +1,34 @@
1
1
  /** Options accepted by `sidenotes()`. */
2
2
  export type SidenotesOptions = {
3
3
  /**
4
- * Extra CSS class appended to each sidenote root, on top of
5
- * `rr-sidenotes__note` and the popover modifier. Defaults to none.
4
+ * Extra CSS class appended to each margin note root, on top of
5
+ * `rr-sidenotes__note`. Ignored when `placement` is `"end"`.
6
6
  */
7
7
  className?: string;
8
8
  /**
9
- * Accessible name prefix for each sidenote. The rendered label is
10
- * `"${ariaLabel} ${index}"`. Defaults to `"Sidenote"`.
9
+ * Accessible name prefix for each margin note. The rendered label is
10
+ * `"${ariaLabel} ${index}"`. Ignored when `placement` is `"end"`.
11
11
  */
12
12
  ariaLabel?: string;
13
13
  /**
14
- * Accessible label prefix for a reference link while its mobile popover is
15
- * closed. The rendered label is `"${openLabel} ${index}"`. Defaults to
16
- * `"Footnote"`.
14
+ * Accessible label prefix for a reference link. The rendered label is
15
+ * `"${openLabel} ${index}"`. Defaults to `"Footnote"`.
17
16
  */
18
17
  openLabel?: string;
19
18
  /**
20
- * Accessible label prefix for a reference link while its mobile popover is
21
- * open. The rendered label is `"${closeLabel} ${index}"`. Defaults to
22
- * `"Close sidenote"`.
19
+ * Where footnote definitions render. `"end"` keeps the GFM footnote list at
20
+ * the end of the article and only styles the references. `"margin"` moves
21
+ * each definition next to its reference as a margin note. Defaults to
22
+ * `"end"`.
23
23
  */
24
- closeLabel?: string;
25
- /**
26
- * Where the mobile popover is anchored. Defaults to `"bottom"` (a bottom
27
- * sheet); `"end"` is a right-edge panel.
28
- */
29
- popoverAlignment?: "bottom" | "end";
24
+ placement?: "end" | "margin";
30
25
  };
31
26
  /** `SidenotesOptions` with every default applied. */
32
27
  export type ResolvedSidenotesOptions = {
33
28
  className: string;
34
29
  ariaLabel: string;
35
30
  openLabel: string;
36
- closeLabel: string;
37
- popoverAlignment: "bottom" | "end";
31
+ placement: "end" | "margin";
38
32
  };
39
33
  /** Minimal hast element shape the sidenotes transformer reads and emits. */
40
34
  export type HastElementNode = {
@@ -43,7 +37,7 @@ export type HastElementNode = {
43
37
  properties?: Record<string, unknown>;
44
38
  children?: HastNode[];
45
39
  };
46
- export type HastTextNode = {
40
+ type HastTextNode = {
47
41
  type: "text";
48
42
  value: string;
49
43
  };
@@ -52,17 +46,18 @@ export type HastRawNode = {
52
46
  type: "raw";
53
47
  value: string;
54
48
  };
55
- export type HastCommentNode = {
49
+ type HastCommentNode = {
56
50
  type: "comment";
57
51
  value: string;
58
52
  };
59
- export type HastDoctypeNode = {
53
+ type HastDoctypeNode = {
60
54
  type: "doctype";
61
55
  name?: string;
62
56
  };
63
- export type HastRootNode = {
57
+ type HastRootNode = {
64
58
  type: "root";
65
59
  children?: HastNode[];
66
60
  };
67
61
  /** Any node the sidenotes transformer may encounter in a hast tree. */
68
62
  export type HastNode = HastElementNode | HastTextNode | HastRawNode | HastCommentNode | HastDoctypeNode | HastRootNode;
63
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/plugin-sidenotes",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,16 +12,10 @@
12
12
  "source": "./index.ts",
13
13
  "default": "./dist/index.js"
14
14
  },
15
- "./client": {
16
- "types": "./dist/client.d.ts",
17
- "import": "./dist/client.js",
18
- "source": "./client.ts",
19
- "default": "./dist/client.js"
20
- },
21
15
  "./style.css": "./style.css"
22
16
  },
23
17
  "dependencies": {
24
- "@riebeckite/core": "0.0.31"
18
+ "@riebeckite/core": "0.0.33"
25
19
  },
26
20
  "publishConfig": {
27
21
  "access": "public"
@@ -56,6 +50,7 @@
56
50
  "./style.css"
57
51
  ],
58
52
  "scripts": {
59
- "build": "node ../../../scripts/build_package.mjs"
53
+ "build": "node ../../../scripts/build_package.mjs",
54
+ "test": "node --import tsx --test \"test/*.test.ts\""
60
55
  }
61
56
  }
package/style.css CHANGED
@@ -1,10 +1,11 @@
1
1
  /*
2
2
  * @riebeckite/plugin-sidenotes
3
3
  *
4
- * Tufte-style side notes. The same `.rr-sidenotes__note` element is a static
5
- * margin note on desktop (>= 48rem) and the popover source on mobile
6
- * (< 48rem). Plugin CSS intentionally loads before theme CSS, so a theme can
7
- * restyle these hooks without editing the plugin.
4
+ * `placement: "end"` (default) keeps the GFM footnote list at the end of the
5
+ * article and only styles the references. `placement: "margin"` collects the
6
+ * definitions into `aside` notes: inline until an article rail fits, sticky in
7
+ * the right margin from 88rem. Plugin CSS intentionally loads before theme
8
+ * CSS, so a theme can restyle these hooks without editing the plugin.
8
9
  */
9
10
 
10
11
  .rr-sidenotes__toggle {
@@ -21,6 +22,51 @@
21
22
  outline-offset: 2px;
22
23
  }
23
24
 
25
+ .rr-sidenotes__ref {
26
+ scroll-margin-block-start: var(--rr-sidenotes-scroll-margin, 6rem);
27
+ }
28
+
29
+ .rr-sidenotes__footnotes {
30
+ --rr-sidenotes-ink: var(--rb-color-ink, #202020);
31
+ --rr-sidenotes-muted: var(--rb-color-muted, #6b6b6b);
32
+ --rr-sidenotes-rule: color-mix(
33
+ in srgb,
34
+ var(--rr-sidenotes-ink) 18%,
35
+ transparent
36
+ );
37
+
38
+ margin-block-start: var(--rb-space-6, 3rem);
39
+ padding-block-start: var(--rb-space-3, 1.5rem);
40
+ border-block-start: var(--rb-rule-width, 1px) solid var(--rr-sidenotes-rule);
41
+ color: var(--rr-sidenotes-ink);
42
+ font-size: 0.875rem;
43
+ line-height: 1.6;
44
+ }
45
+
46
+ .rr-sidenotes__footnotes li {
47
+ scroll-margin-block-start: var(--rr-sidenotes-scroll-margin, 6rem);
48
+ }
49
+
50
+ .rr-sidenotes__footnotes li::marker {
51
+ color: var(--rr-sidenotes-muted);
52
+ }
53
+
54
+ .rr-sidenotes__footnotes [data-footnote-backref] {
55
+ margin-inline-start: 0.35em;
56
+ color: var(--rr-sidenotes-muted);
57
+ text-decoration: none;
58
+ }
59
+
60
+ .rr-sidenotes__footnotes [data-footnote-backref]:hover {
61
+ color: var(--rr-sidenotes-ink);
62
+ text-decoration: underline;
63
+ }
64
+
65
+ /* The list is the link target only when notes stay in the article flow. */
66
+ .rr-sidenotes__footnotes[data-rr-sidenotes-placement="margin"] {
67
+ display: none;
68
+ }
69
+
24
70
  .rr-sidenotes__note {
25
71
  --rr-sidenotes-ink: var(--rb-color-ink, #202020);
26
72
  --rr-sidenotes-muted: var(--rb-color-muted, #6b6b6b);
@@ -30,6 +76,11 @@
30
76
  transparent
31
77
  );
32
78
 
79
+ max-inline-size: 100%;
80
+ margin-block: var(--rb-space-3, 1.5rem);
81
+ padding-inline-start: var(--rb-space-2, 1rem);
82
+ border-inline-start: calc(var(--rb-rule-width, 1px) * 2) solid
83
+ var(--rr-sidenotes-rule);
33
84
  color: var(--rr-sidenotes-ink);
34
85
  font-size: 0.8125rem;
35
86
  line-height: 1.55;
@@ -51,56 +102,6 @@
51
102
  margin-bottom: 0;
52
103
  }
53
104
 
54
- .rr-sidenotes__footnotes {
55
- font-size: 0.875rem;
56
- color: var(--rr-sidenotes-ink);
57
- }
58
-
59
- /* Mobile: notes are hidden and become tap-open popovers. */
60
- @media (max-width: 48rem) {
61
- .rr-sidenotes__note {
62
- position: fixed;
63
- z-index: 60;
64
- bottom: var(--rb-space-2, 1rem);
65
- left: var(--rb-space-2, 1rem);
66
- right: var(--rb-space-2, 1rem);
67
- display: none;
68
- max-height: min(60vh, 24rem);
69
- overflow: auto;
70
- padding: var(--rb-space-3, 1.5rem);
71
- background: var(--rb-color-surface, #ffffff);
72
- border: var(--rb-rule-width, 1px) solid var(--rr-sidenotes-rule);
73
- box-shadow: 0 0.5rem 1.5rem rgb(0 0 0 / 0.18);
74
- }
75
-
76
- /* Right-edge panel variant driven by `popoverAlignment: "end"`. */
77
- .rr-sidenotes__note--popover-end {
78
- top: 50%;
79
- bottom: auto;
80
- left: auto;
81
- width: min(20rem, calc(100vw - 2 * var(--rb-space-2, 1rem)));
82
- transform: translateY(-50%);
83
- }
84
-
85
- .rr-sidenotes__note--open {
86
- display: block;
87
- }
88
- }
89
-
90
- @media (min-width: 48rem) {
91
- .rr-sidenotes__note {
92
- max-inline-size: 100%;
93
- margin-block: var(--rb-space-3, 1.5rem);
94
- padding-inline-start: var(--rb-space-2, 1rem);
95
- border-inline-start: calc(var(--rb-rule-width, 1px) * 2) solid
96
- var(--rr-sidenotes-rule);
97
- }
98
-
99
- .rr-sidenotes__footnotes {
100
- display: none;
101
- }
102
- }
103
-
104
105
  @media (min-width: 88rem) {
105
106
  .rr-sidenotes__note {
106
107
  position: sticky;
package/dist/client.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export type { SidenotesClientOptions } from "./src/client.js";
2
- export { initSidenotes } from "./src/client.js";
package/dist/client.js DELETED
@@ -1,81 +0,0 @@
1
- // src/client.ts
2
- var DEFAULT_SELECTOR = "[data-rr-sidenotes-ref]";
3
- var NOTE_OPEN_CLASS = "rr-sidenotes__note--open";
4
- var DESKTOP_QUERY = "(min-width: 48rem)";
5
- var DEFAULT_OPEN_LABEL = "Footnote";
6
- var DEFAULT_CLOSE_LABEL = "Close sidenote";
7
- function initSidenotes(options = {}) {
8
- const desktopQuery = window.matchMedia(DESKTOP_QUERY);
9
- if (desktopQuery.matches) return;
10
- const toggles = Array.from(
11
- document.querySelectorAll(
12
- options.selector ?? DEFAULT_SELECTOR
13
- )
14
- );
15
- if (toggles.length === 0) return;
16
- const openLabel = options.openLabel ?? DEFAULT_OPEN_LABEL;
17
- const closeLabel = options.closeLabel ?? DEFAULT_CLOSE_LABEL;
18
- const noteFor = (toggle) => {
19
- const controls = toggle.getAttribute("aria-controls");
20
- if (!controls) return null;
21
- const note = document.getElementById(controls);
22
- return note instanceof HTMLElement ? note : null;
23
- };
24
- const isInsideSidenotes = (target) => {
25
- if (!target) return false;
26
- for (const toggle of toggles) {
27
- const note = noteFor(toggle);
28
- if (toggle.contains(target) || note?.contains(target)) return true;
29
- }
30
- return false;
31
- };
32
- const setOpen = (toggle, note, open) => {
33
- const index = toggle.textContent?.trim() ?? "";
34
- const label = open ? `${closeLabel} ${index}`.trim() : `${openLabel} ${index}`.trim();
35
- toggle.setAttribute("aria-expanded", String(open));
36
- toggle.setAttribute("aria-label", label);
37
- note.classList.toggle(NOTE_OPEN_CLASS, open);
38
- if (open) note.focus({ preventScroll: true });
39
- };
40
- const closeAll = () => {
41
- for (const toggle of toggles) {
42
- const note = noteFor(toggle);
43
- if (note?.classList.contains(NOTE_OPEN_CLASS)) {
44
- setOpen(toggle, note, false);
45
- }
46
- }
47
- };
48
- for (const toggle of toggles) {
49
- toggle.addEventListener("click", (event) => {
50
- event.preventDefault();
51
- const note = noteFor(toggle);
52
- if (!note) return;
53
- const isOpen = note.classList.contains(NOTE_OPEN_CLASS);
54
- if (!isOpen) closeAll();
55
- setOpen(toggle, note, !isOpen);
56
- });
57
- }
58
- const onKeydown = (event) => {
59
- if (event.key !== "Escape") return;
60
- closeAll();
61
- };
62
- const onClickOutside = (event) => {
63
- if (isInsideSidenotes(event.target instanceof Node ? event.target : null)) {
64
- return;
65
- }
66
- closeAll();
67
- };
68
- document.addEventListener("keydown", onKeydown);
69
- document.addEventListener("click", onClickOutside);
70
- const onViewportChange = (event) => {
71
- if (!event.matches) return;
72
- closeAll();
73
- document.removeEventListener("keydown", onKeydown);
74
- document.removeEventListener("click", onClickOutside);
75
- desktopQuery.removeEventListener("change", onViewportChange);
76
- };
77
- desktopQuery.addEventListener("change", onViewportChange);
78
- }
79
- export {
80
- initSidenotes
81
- };
@@ -1,17 +0,0 @@
1
- /**
2
- * Mobile popover behavior for `@riebeckite/plugin-sidenotes`.
3
- *
4
- * Desktop margin notes are static and need no JavaScript: this entry ignores
5
- * the page whenever `(min-width: 48rem)` matches and disconnects itself when
6
- * the viewport crosses into the desktop layout.
7
- */
8
- /** Public options exposed to the browser through the plugin's client entry. */
9
- export type SidenotesClientOptions = {
10
- /** Selector for reference links. Defaults to `[data-rr-sidenotes-ref]`. */
11
- selector?: string;
12
- /** Accessible label prefix for a reference link while its popover is closed. */
13
- openLabel?: string;
14
- /** Accessible label prefix for a reference link while its popover is open. */
15
- closeLabel?: string;
16
- };
17
- export declare function initSidenotes(options?: SidenotesClientOptions): void;