@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/app",
3
- "version": "1.0.0-next.137",
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}
@@ -1 +1,2 @@
1
1
  export { default as ThemeSwitcherToggle } from './ThemeSwitcherToggle.svelte'
2
+ export { default as TableOfContents } from './TableOfContents.svelte'
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // Components
2
- export { ThemeSwitcherToggle } from './components/index.js'
2
+ export { ThemeSwitcherToggle, TableOfContents } from './components/index.js'
3
3
 
4
4
  // Types
5
5
  export * from './types/index.js'