@rokkit/app 1.0.0-next.137 → 1.0.0-next.139
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/package.json +2 -1
- package/src/components/TableOfContents.svelte +138 -0
- package/src/components/index.ts +1 -0
- package/src/index.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rokkit/app",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.139",
|
|
4
4
|
"description": "App-level controls for Rokkit applications - theme management and UI chrome",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"build": "echo 'No build step needed for source-only package'"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
+
"@rokkit/actions": "workspace:*",
|
|
40
41
|
"@rokkit/core": "workspace:*",
|
|
41
42
|
"@rokkit/states": "workspace:*",
|
|
42
43
|
"@rokkit/ui": "workspace:*"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import { navigable } from '@rokkit/actions'
|
|
4
|
+
import { onMount } from 'svelte'
|
|
5
|
+
|
|
6
|
+
let { container = 'main-content' } = $props()
|
|
7
|
+
|
|
8
|
+
let headings = $state([])
|
|
9
|
+
let activeId = $state('')
|
|
10
|
+
let focusedIndex = $state(0)
|
|
11
|
+
let navEl = $state(null)
|
|
12
|
+
let observer = null
|
|
13
|
+
|
|
14
|
+
function slugify(text) {
|
|
15
|
+
return (text ?? '')
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.trim()
|
|
18
|
+
.replace(/\s+/g, '-')
|
|
19
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
20
|
+
.replace(/-+/g, '-')
|
|
21
|
+
.replace(/^-|-$/g, '')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getContainer() {
|
|
25
|
+
return document.getElementById(container)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function scan() {
|
|
29
|
+
const main = getContainer()
|
|
30
|
+
if (!main) return
|
|
31
|
+
const els = [...main.querySelectorAll('h2, h3')]
|
|
32
|
+
headings = els.map((el, i) => {
|
|
33
|
+
if (!el.id) el.id = slugify(el.textContent) || `section-${i}`
|
|
34
|
+
return { id: el.id, text: el.textContent?.trim() ?? '', level: el.tagName.toLowerCase() }
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function observe() {
|
|
39
|
+
observer?.disconnect()
|
|
40
|
+
const main = getContainer()
|
|
41
|
+
if (!main || headings.length === 0) return
|
|
42
|
+
observer = new IntersectionObserver(
|
|
43
|
+
(entries) => {
|
|
44
|
+
const visible = entries
|
|
45
|
+
.filter((e) => e.isIntersecting)
|
|
46
|
+
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)
|
|
47
|
+
if (visible.length > 0) activeId = visible[0].target.id
|
|
48
|
+
},
|
|
49
|
+
{ root: main, rootMargin: '-5% 0px -70% 0px' }
|
|
50
|
+
)
|
|
51
|
+
headings.forEach(({ id }) => {
|
|
52
|
+
const el = document.getElementById(id)
|
|
53
|
+
if (el) observer.observe(el)
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function scrollToHeading(id) {
|
|
58
|
+
const el = document.getElementById(id)
|
|
59
|
+
const main = getContainer()
|
|
60
|
+
if (!el || !main) return
|
|
61
|
+
main.scrollTo({ top: el.offsetTop - 16, behavior: 'smooth' })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function rescan() {
|
|
65
|
+
activeId = ''
|
|
66
|
+
focusedIndex = 0
|
|
67
|
+
scan()
|
|
68
|
+
observe()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getItems() {
|
|
72
|
+
return navEl?.querySelectorAll('[data-toc-item]') ?? []
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function handlePrevious() {
|
|
76
|
+
if (focusedIndex > 0) {
|
|
77
|
+
focusedIndex -= 1
|
|
78
|
+
getItems()[focusedIndex]?.focus()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function handleNext() {
|
|
83
|
+
if (focusedIndex < headings.length - 1) {
|
|
84
|
+
focusedIndex += 1
|
|
85
|
+
getItems()[focusedIndex]?.focus()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function handleSelect() {
|
|
90
|
+
scrollToHeading(headings[focusedIndex]?.id)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function handleClick(event) {
|
|
94
|
+
const btn = event.target.closest('[data-toc-item]')
|
|
95
|
+
if (!btn) return
|
|
96
|
+
const index = parseInt(btn.dataset.tocIndex ?? '0', 10)
|
|
97
|
+
focusedIndex = index
|
|
98
|
+
scrollToHeading(headings[index]?.id)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
onMount(() => {
|
|
102
|
+
scan()
|
|
103
|
+
observe()
|
|
104
|
+
return () => observer?.disconnect()
|
|
105
|
+
})
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
{#if headings.length > 1}
|
|
109
|
+
<nav
|
|
110
|
+
bind:this={navEl}
|
|
111
|
+
data-toc
|
|
112
|
+
aria-label="On this page"
|
|
113
|
+
use:navigable
|
|
114
|
+
onclick={handleClick}
|
|
115
|
+
onprevious={handlePrevious}
|
|
116
|
+
onnext={handleNext}
|
|
117
|
+
onselect={handleSelect}
|
|
118
|
+
>
|
|
119
|
+
<p data-toc-label>On this page</p>
|
|
120
|
+
<ul data-toc-list>
|
|
121
|
+
{#each headings as h, i (h.id)}
|
|
122
|
+
<li>
|
|
123
|
+
<button
|
|
124
|
+
data-toc-item
|
|
125
|
+
data-toc-level={h.level}
|
|
126
|
+
data-toc-index={i}
|
|
127
|
+
data-toc-active={activeId === h.id ? '' : undefined}
|
|
128
|
+
data-toc-focused={focusedIndex === i ? '' : undefined}
|
|
129
|
+
tabindex={focusedIndex === i ? 0 : -1}
|
|
130
|
+
onfocusin={() => (focusedIndex = i)}
|
|
131
|
+
>
|
|
132
|
+
{h.text}
|
|
133
|
+
</button>
|
|
134
|
+
</li>
|
|
135
|
+
{/each}
|
|
136
|
+
</ul>
|
|
137
|
+
</nav>
|
|
138
|
+
{/if}
|
package/src/components/index.ts
CHANGED
package/src/index.ts
CHANGED