@ainsleydev/sveltekit-helper 0.5.1 → 0.6.1
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/dist/components/TableOfContents.svelte +188 -0
- package/dist/components/TableOfContents.svelte.d.ts +53 -0
- package/dist/components/TableOfContents.svelte.d.ts.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type TOCItem = {
|
|
3
|
+
label: string;
|
|
4
|
+
href: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type TableOfContentsProps = {
|
|
8
|
+
/**
|
|
9
|
+
* Optional heading displayed above the TOC list.
|
|
10
|
+
*/
|
|
11
|
+
heading?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Optional pre-generated items. If omitted, items are auto-discovered
|
|
14
|
+
* from the DOM using contentSelector + headingSelector on mount.
|
|
15
|
+
*/
|
|
16
|
+
items?: TOCItem[];
|
|
17
|
+
/**
|
|
18
|
+
* Adds a left border to the TOC on tablet and above.
|
|
19
|
+
*/
|
|
20
|
+
displayBorder?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* CSS selector used to find the content element containing headings.
|
|
23
|
+
* Defaults to `[data-sidebar-content="true"]`.
|
|
24
|
+
*/
|
|
25
|
+
contentSelector?: string;
|
|
26
|
+
/**
|
|
27
|
+
* CSS selector for headings within the content element.
|
|
28
|
+
* Falls back to the `data-sidebar-selector` attribute on the content
|
|
29
|
+
* element, then `h2`.
|
|
30
|
+
*
|
|
31
|
+
* Priority: prop > data-sidebar-selector attribute > 'h2'
|
|
32
|
+
*/
|
|
33
|
+
headingSelector?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Scroll offset in pixels applied to scrollspy detection.
|
|
36
|
+
* @default 80
|
|
37
|
+
*/
|
|
38
|
+
scrollOffset?: number;
|
|
39
|
+
};
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<script lang="ts">
|
|
43
|
+
import { onMount } from 'svelte'
|
|
44
|
+
import Sidebar from './Sidebar.svelte'
|
|
45
|
+
|
|
46
|
+
let {
|
|
47
|
+
heading = '',
|
|
48
|
+
items: itemsProp,
|
|
49
|
+
displayBorder = false,
|
|
50
|
+
contentSelector = '[data-sidebar-content="true"]',
|
|
51
|
+
headingSelector,
|
|
52
|
+
scrollOffset = 80,
|
|
53
|
+
}: TableOfContentsProps = $props()
|
|
54
|
+
|
|
55
|
+
let activeId = $state<string | null>(null)
|
|
56
|
+
let items = $state<TOCItem[]>(itemsProp ?? [])
|
|
57
|
+
|
|
58
|
+
onMount(() => {
|
|
59
|
+
const content = document.querySelector(contentSelector)
|
|
60
|
+
if (!content) return
|
|
61
|
+
|
|
62
|
+
// Priority: prop > data-sidebar-selector attribute > 'h2'
|
|
63
|
+
const resolvedHeadingSelector =
|
|
64
|
+
headingSelector ?? content.getAttribute('data-sidebar-selector') ?? 'h2'
|
|
65
|
+
|
|
66
|
+
// Auto-generate items from DOM if not provided as props.
|
|
67
|
+
if (!itemsProp) {
|
|
68
|
+
items = Array.from(content.querySelectorAll<HTMLElement>(resolvedHeadingSelector))
|
|
69
|
+
.filter((el) => el.id)
|
|
70
|
+
.map((el) => ({ label: el.textContent?.trim() ?? '', href: el.id }))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const sections = Array.from(
|
|
74
|
+
content.querySelectorAll<HTMLElement>(resolvedHeadingSelector),
|
|
75
|
+
).filter((el) => el.id)
|
|
76
|
+
|
|
77
|
+
if (sections.length === 0) return
|
|
78
|
+
|
|
79
|
+
const onScroll = () => {
|
|
80
|
+
const scrollPosition = window.scrollY + scrollOffset
|
|
81
|
+
let activeSection = sections[0]
|
|
82
|
+
|
|
83
|
+
for (const section of sections) {
|
|
84
|
+
if (section.offsetTop > scrollPosition) break
|
|
85
|
+
activeSection = section
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const bottomThreshold = 10
|
|
89
|
+
const scrolledToBottom =
|
|
90
|
+
window.innerHeight + window.scrollY >= document.body.scrollHeight - bottomThreshold
|
|
91
|
+
if (scrolledToBottom) {
|
|
92
|
+
activeSection = sections[sections.length - 1]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
activeId = activeSection?.id ?? null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
window.addEventListener('scroll', onScroll, { passive: true })
|
|
99
|
+
onScroll()
|
|
100
|
+
|
|
101
|
+
return () => window.removeEventListener('scroll', onScroll)
|
|
102
|
+
})
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<!--
|
|
106
|
+
@component
|
|
107
|
+
|
|
108
|
+
Table of Contents with scrollspy, designed to be used alongside a richtext
|
|
109
|
+
or content area that has headings with `id` attributes.
|
|
110
|
+
|
|
111
|
+
By default the component discovers the content element via a
|
|
112
|
+
`data-sidebar-content="true"` attribute and uses the `data-sidebar-selector`
|
|
113
|
+
attribute (defaulting to `h2`) to determine which headings to track.
|
|
114
|
+
|
|
115
|
+
@example
|
|
116
|
+
```svelte
|
|
117
|
+
<!-- Attach data attributes to the content element -->
|
|
118
|
+
<RichText content={data.body} data-sidebar-content="true" data-sidebar-selector="h3" />
|
|
119
|
+
|
|
120
|
+
<!-- TOC auto-discovers headings -->
|
|
121
|
+
<TableOfContents heading="On this page" />
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
@example
|
|
125
|
+
```svelte
|
|
126
|
+
<!-- Explicit prop overrides -->
|
|
127
|
+
<TableOfContents
|
|
128
|
+
contentSelector=".article-body"
|
|
129
|
+
headingSelector="h2, h3"
|
|
130
|
+
heading="Contents"
|
|
131
|
+
displayBorder
|
|
132
|
+
/>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
@example
|
|
136
|
+
```svelte
|
|
137
|
+
<!-- Manual items (scrollspy still driven by DOM) -->
|
|
138
|
+
<TableOfContents items={[{ label: 'Intro', href: 'intro' }]} />
|
|
139
|
+
```
|
|
140
|
+
-->
|
|
141
|
+
<Sidebar>
|
|
142
|
+
<div class="toc" class:toc--border={displayBorder}>
|
|
143
|
+
{#if heading !== ''}
|
|
144
|
+
<p class="toc__heading">
|
|
145
|
+
{heading}
|
|
146
|
+
</p>
|
|
147
|
+
{/if}
|
|
148
|
+
<menu class="toc__items">
|
|
149
|
+
{#each items as item, index (index)}
|
|
150
|
+
<li class="toc__item">
|
|
151
|
+
<a
|
|
152
|
+
class="toc__link"
|
|
153
|
+
class:toc__link--active={activeId ===
|
|
154
|
+
(item.href.startsWith('#') ? item.href.slice(1) : item.href)}
|
|
155
|
+
href="#{item.href}"
|
|
156
|
+
>
|
|
157
|
+
<small>{item.label}</small>
|
|
158
|
+
</a>
|
|
159
|
+
</li>
|
|
160
|
+
{/each}
|
|
161
|
+
</menu>
|
|
162
|
+
</div>
|
|
163
|
+
</Sidebar>
|
|
164
|
+
|
|
165
|
+
<style>.toc__item {
|
|
166
|
+
margin-bottom: 0.5rem;
|
|
167
|
+
}
|
|
168
|
+
.toc__link {
|
|
169
|
+
text-decoration: none;
|
|
170
|
+
font-weight: var(--font-weight-normal);
|
|
171
|
+
color: var(--token-colour-text);
|
|
172
|
+
transition: color 100ms ease;
|
|
173
|
+
will-change: color;
|
|
174
|
+
}
|
|
175
|
+
.toc__link--active {
|
|
176
|
+
color: var(--token-text-action);
|
|
177
|
+
font-weight: var(--font-weight-medium);
|
|
178
|
+
}
|
|
179
|
+
.toc__link:hover {
|
|
180
|
+
color: var(--token-text-action);
|
|
181
|
+
}
|
|
182
|
+
@media screen and (min-width: 768px) {
|
|
183
|
+
.toc--border {
|
|
184
|
+
margin-left: 3rem;
|
|
185
|
+
padding-left: 3rem;
|
|
186
|
+
border-left: 1px solid var(--colour-light-600);
|
|
187
|
+
}
|
|
188
|
+
}</style>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type TOCItem = {
|
|
2
|
+
label: string;
|
|
3
|
+
href: string;
|
|
4
|
+
};
|
|
5
|
+
export type TableOfContentsProps = {
|
|
6
|
+
/**
|
|
7
|
+
* Optional heading displayed above the TOC list.
|
|
8
|
+
*/
|
|
9
|
+
heading?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Optional pre-generated items. If omitted, items are auto-discovered
|
|
12
|
+
* from the DOM using contentSelector + headingSelector on mount.
|
|
13
|
+
*/
|
|
14
|
+
items?: TOCItem[];
|
|
15
|
+
/**
|
|
16
|
+
* Adds a left border to the TOC on tablet and above.
|
|
17
|
+
*/
|
|
18
|
+
displayBorder?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* CSS selector used to find the content element containing headings.
|
|
21
|
+
* Defaults to `[data-sidebar-content="true"]`.
|
|
22
|
+
*/
|
|
23
|
+
contentSelector?: string;
|
|
24
|
+
/**
|
|
25
|
+
* CSS selector for headings within the content element.
|
|
26
|
+
* Falls back to the `data-sidebar-selector` attribute on the content
|
|
27
|
+
* element, then `h2`.
|
|
28
|
+
*
|
|
29
|
+
* Priority: prop > data-sidebar-selector attribute > 'h2'
|
|
30
|
+
*/
|
|
31
|
+
headingSelector?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Scroll offset in pixels applied to scrollspy detection.
|
|
34
|
+
* @default 80
|
|
35
|
+
*/
|
|
36
|
+
scrollOffset?: number;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Table of Contents with scrollspy, designed to be used alongside a richtext
|
|
40
|
+
* or content area that has headings with `id` attributes.
|
|
41
|
+
*
|
|
42
|
+
* By default the component discovers the content element via a
|
|
43
|
+
* `data-sidebar-content="true"` attribute and uses the `data-sidebar-selector`
|
|
44
|
+
* attribute (defaulting to `h2`) to determine which headings to track.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```svelte
|
|
48
|
+
* <!-- Attach data attributes to the content element
|
|
49
|
+
*/
|
|
50
|
+
declare const TableOfContents: import("svelte").Component<TableOfContentsProps, {}, "">;
|
|
51
|
+
type TableOfContents = ReturnType<typeof TableOfContents>;
|
|
52
|
+
export default TableOfContents;
|
|
53
|
+
//# sourceMappingURL=TableOfContents.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TableOfContents.svelte.d.ts","sourceRoot":"","sources":["../../src/components/TableOfContents.svelte.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,OAAO,GAAG;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IAClC;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB;;OAEG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAkHF;;;;;;;;;;;GAWG;AACH,QAAA,MAAM,eAAe,0DAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { default as Modal } from './Modal.svelte';
|
|
2
2
|
export { default as Sidebar } from './Sidebar.svelte';
|
|
3
3
|
export { default as Hamburger } from './Hamburger.svelte';
|
|
4
|
+
export { default as TableOfContents } from './TableOfContents.svelte';
|
|
4
5
|
export { Alert, Notice } from './notifications';
|
|
5
6
|
export type { ModalProps, TransitionFn } from './Modal.svelte';
|
|
6
7
|
export type { AlertProps, AlertType, NoticeProps, NoticeType } from './notifications';
|
|
8
|
+
export type { TableOfContentsProps, TOCItem } from './TableOfContents.svelte';
|
|
7
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC/D,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC/D,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACtF,YAAY,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC"}
|
package/dist/components/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as Modal } from './Modal.svelte';
|
|
2
2
|
export { default as Sidebar } from './Sidebar.svelte';
|
|
3
3
|
export { default as Hamburger } from './Hamburger.svelte';
|
|
4
|
+
export { default as TableOfContents } from './TableOfContents.svelte';
|
|
4
5
|
export { Alert, Notice } from './notifications';
|