@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.
@@ -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"}
@@ -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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainsleydev/sveltekit-helper",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "SvelteKit utilities, components and helpers for ainsley.dev builds",
5
5
  "license": "MIT",
6
6
  "type": "module",