srcdev-nuxt-components 9.1.48 → 9.1.49

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.
@@ -20,7 +20,8 @@ Auto-imported by Nuxt — **do not create a local copy**.
20
20
 
21
21
  | Option | Type | Default | Description |
22
22
  |---|---|---|---|
23
- | `offset` | `number \| (() => number)` | `0` | Pixels subtracted from the final scroll position. Pass a getter function to read a sticky element's height at scroll time rather than at composable init. |
23
+ | `offset` | `number | (() => number)` | `0` | Pixels subtracted from the final scroll position. Pass a getter function to read a sticky element's height at scroll time rather than at composable init. |
24
+ | `offsetElement` | `Ref<HTMLElement &#124; null>` | — | Convenience alternative to `offset`. Reads `element.offsetHeight` at scroll time. Takes priority over `offset` when both are supplied. |
24
25
 
25
26
  ### Returns
26
27
 
@@ -28,17 +29,69 @@ Auto-imported by Nuxt — **do not create a local copy**.
28
29
  |---|---|---|
29
30
  | `handleNavClick` | `(event: MouseEvent, href: string) => void` | Attach to click handlers. No-ops silently for non-`#` hrefs — safe on all links. |
30
31
  | `scrollToAnchor` | `(hash: string) => void` | Scroll programmatically. Accepts `"#section"` or `"section"`. Respects motion preference and offset. |
32
+ | `activeHash` | `Ref<string>` | Reactive ref tracking the currently active hash. Starts `""` on server and after hydration is set from `window.location.hash`. Updated on every `handleNavClick` call. Use this to drive `is-active` classes — **do not use `route.hash`**, which is not updated by `history.pushState`. |
33
+
34
+ ---
35
+
36
+ ## Active state
37
+
38
+ `activeHash` is the correct way to drive active-state styling on anchor nav links.
39
+
40
+ **Why not `route.hash`?** `handleNavClick` calls `history.pushState` directly (to avoid triggering Vue Router's scroll behaviour). `pushState` does not update Vue Router's reactive `route.hash`, so `route.hash` stays stale after clicks.
41
+
42
+ **Hydration safety:** `activeHash` is initialised to `""` on both server and client so the SSR-rendered HTML always matches the pre-mount client vdom. The real hash is applied in `onMounted`, after hydration, to avoid mismatches.
43
+
44
+ **Default active item:** `activeHash` is empty until `onMounted` fires. If no hash is in the URL, set a default in the component's own `onMounted` (which runs after the composable's `onMounted`):
45
+
46
+ ```ts
47
+ const { handleNavClick, activeHash } = useAnchorScroll({ offset: 64 });
48
+
49
+ onMounted(() => {
50
+ // Default to first section when no hash is in the URL
51
+ if (!activeHash.value && sections[0]) activeHash.value = `#${sections[0].id}`;
52
+ });
53
+ ```
54
+
55
+ ### Binding active state in a template
56
+
57
+ ```vue
58
+ <a
59
+ v-for="section in sections"
60
+ :key="section.id"
61
+ :href="`#${section.id}`"
62
+ :class="{ 'is-active': `#${section.id}` === activeHash }"
63
+ @click="(e) => handleNavClick(e, `#${section.id}`)"
64
+ >{{ section.label }}</a>
65
+ ```
66
+
67
+ ### Active state in TabNavigation
68
+
69
+ `TabNavigation` handles `activeHash` internally for hash nav items. No extra work needed —
70
+ just pass anchor hrefs in `navItemData` and the active indicator moves automatically on click
71
+ and on initial load (defaulting to the first hash item when the URL has no hash).
31
72
 
32
73
  ---
33
74
 
34
75
  ## Usage patterns
35
76
 
36
- ### 1. TabNavigation with anchor links (no extra code needed)
77
+ ### 1. TabNavigation with a sticky site header
37
78
 
38
- `TabNavigation` has `useAnchorScroll` wired up internally. Pass anchor hrefs in
39
- `navItemData.main` and smooth scrolling works automatically.
79
+ `TabNavigation` accepts an `anchorScrollOffset` prop that is passed directly to `useAnchorScroll`.
80
+ Pass a getter so the live header height is read at scroll time — this stays correct if the
81
+ header resizes on different viewports.
82
+
83
+ ```vue
84
+ <header ref="headerRef">
85
+ <TabNavigation
86
+ :nav-item-data="navItemData"
87
+ :anchor-scroll-offset="() => headerRef?.offsetHeight ?? 0"
88
+ />
89
+ </header>
90
+ ```
40
91
 
41
92
  ```ts
93
+ const headerRef = ref<HTMLElement | null>(null);
94
+
42
95
  const navItemData = {
43
96
  main: [
44
97
  { text: "About", href: "#about" },
@@ -49,39 +102,55 @@ const navItemData = {
49
102
  };
50
103
  ```
51
104
 
52
- ```vue
53
- <TabNavigation :nav-item-data="navItemData" />
105
+ **CSS alternative** — if you want all anchor links site-wide to respect the sticky header
106
+ (not just the ones inside `TabNavigation`), add `scroll-padding-top` to `html` instead and
107
+ skip the prop:
108
+
109
+ ```css
110
+ /* ─ app/assets/styles/setup/01.config/_head.css ─ */
111
+ html {
112
+ scroll-padding-top: var(--sticky-header-height, 64px);
113
+ }
54
114
  ```
55
115
 
56
- No offset is applied here. If your site header is fixed/sticky and obscures section headings,
57
- use pattern 2 below to build a custom nav with an explicit offset.
116
+ ```css
117
+ /* app/assets/styles/main.css */
118
+ :root {
119
+ --sticky-header-height: 64px; /* adjust to match actual header height */
120
+ }
121
+ ```
122
+
123
+ `scrollIntoView` respects `scroll-padding-top`, so this works with the no-offset code path.
58
124
 
59
125
  ---
60
126
 
61
- ### 2. Sticky section nav with dynamic offset
127
+ ### 2. Sticky section nav with dynamic offset (using offsetElement)
62
128
 
63
- The most common single-page pattern: a sticky bar at the top of the content area, where
64
- each link scrolls to a section below it. Pass a getter for `offset` so the bar's live height
65
- is read at scroll time — this stays correct if the bar resizes (e.g. on viewport changes).
129
+ The `offsetElement` option is the clearest way to derive an offset from an element ref.
130
+ It reads `offsetHeight` at scroll time, so resize changes are always captured.
66
131
 
67
132
  ```ts
68
133
  const stickyNavRef = ref<HTMLElement | null>(null);
69
134
 
70
- const { handleNavClick } = useAnchorScroll({
71
- offset: () => stickyNavRef.value?.offsetHeight ?? 0,
135
+ const { handleNavClick, activeHash } = useAnchorScroll({ offsetElement: stickyNavRef });
136
+
137
+ onMounted(() => {
138
+ if (!activeHash.value && sections[0]) activeHash.value = `#${sections[0].id}`;
72
139
  });
73
140
  ```
74
141
 
75
142
  ```vue
76
143
  <nav ref="stickyNavRef" class="sticky-section-nav">
77
- <a href="#overview" @click="(e) => handleNavClick(e, '#overview')">Overview</a>
78
- <a href="#pricing" @click="(e) => handleNavClick(e, '#pricing')">Pricing</a>
79
- <a href="#contact" @click="(e) => handleNavClick(e, '#contact')">Contact</a>
144
+ <a
145
+ v-for="section in sections"
146
+ :key="section.id"
147
+ :href="`#${section.id}`"
148
+ :class="{ 'is-active': `#${section.id}` === activeHash }"
149
+ @click="(e) => handleNavClick(e, `#${section.id}`)"
150
+ >{{ section.label }}</a>
80
151
  </nav>
81
152
 
82
- <section id="overview">…</section>
83
- <section id="pricing">…</section>
84
- <section id="contact">…</section>
153
+ <section v-for="section in sections" :key="section.id" :id="section.id">…</section>
85
154
  ```
86
155
 
87
156
  CSS to make the nav sticky:
@@ -102,7 +171,7 @@ Same pattern as above — useful for terms, privacy policy, or documentation pag
102
171
  sidebar links to in-document sections.
103
172
 
104
173
  ```ts
105
- const { handleNavClick } = useAnchorScroll({ offset: 24 }); // fixed header height
174
+ const { handleNavClick, activeHash } = useAnchorScroll({ offset: 24 }); // fixed header height
106
175
  ```
107
176
 
108
177
  ```vue
@@ -110,6 +179,7 @@ const { handleNavClick } = useAnchorScroll({ offset: 24 }); // fixed header heig
110
179
  <nav>
111
180
  <a v-for="section in termsSections" :key="section.id"
112
181
  :href="`#${section.id}`"
182
+ :class="{ 'is-active': `#${section.id}` === activeHash }"
113
183
  @click="(e) => handleNavClick(e, `#${section.id}`)">
114
184
  {{ section.title }}
115
185
  </a>
@@ -156,10 +226,10 @@ onMounted(() => {
156
226
 
157
227
  ## How offset works
158
228
 
159
- Without `offset`, the composable calls `el.scrollIntoView({ behavior, block: "start" })` —
229
+ Without `offset` or `offsetElement`, the composable calls `el.scrollIntoView({ behavior, block: "start" })` —
160
230
  the element's top edge aligns with the viewport top.
161
231
 
162
- With `offset`, it uses `window.scrollTo` with a calculated position:
232
+ With an offset, it uses `window.scrollTo` with a calculated position:
163
233
 
164
234
  ```
165
235
  top = el.getBoundingClientRect().top + window.scrollY - offset
@@ -168,16 +238,19 @@ top = el.getBoundingClientRect().top + window.scrollY - offset
168
238
  This shifts the final resting position downward by `offset` pixels, so a sticky bar of that
169
239
  height does not overlap the section heading.
170
240
 
171
- **Use a getter when the bar can resize:**
241
+ **`offsetElement` vs `offset` getter:**
172
242
 
173
243
  ```ts
174
- // ✅ Read height at click time
175
- const { handleNavClick } = useAnchorScroll({
244
+ // ✅ offsetElement reads offsetHeight at click time, no boilerplate
245
+ const { handleNavClick, activeHash } = useAnchorScroll({ offsetElement: navRef });
246
+
247
+ // ✅ offset getter — equivalent, useful when the offset is derived from more than one element
248
+ const { handleNavClick, activeHash } = useAnchorScroll({
176
249
  offset: () => navRef.value?.offsetHeight ?? 0,
177
250
  });
178
251
 
179
- // ✗ Captured at init — stale if the bar changes height later
180
- const { handleNavClick } = useAnchorScroll({
252
+ // ✗ Static offset — stale if the bar changes height later
253
+ const { handleNavClick, activeHash } = useAnchorScroll({
181
254
  offset: navRef.value?.offsetHeight ?? 0,
182
255
  });
183
256
  ```
@@ -195,10 +268,19 @@ When `window.matchMedia("(prefers-reduced-motion: reduce)").matches` is `true`:
195
268
  No configuration needed — the check happens at call time, so toggling the OS preference mid-session
196
269
  takes effect immediately on the next click.
197
270
 
271
+ > **Debugging tip:** If clicks produce an instant jump instead of smooth scroll, check whether
272
+ > "Reduce Motion" is enabled in your OS accessibility settings (macOS: System Preferences →
273
+ > Accessibility → Display → Reduce Motion). This is intentional behaviour, not a bug.
274
+
198
275
  ---
199
276
 
200
277
  ## Notes
201
278
 
279
+ - **`activeHash` vs `route.hash`** — always use `activeHash` for active-state classes. `route.hash`
280
+ is not updated by `history.pushState` and will be stale after clicks.
281
+ - **Hydration mismatches** — `activeHash` is always `""` at render time; the real value is set in
282
+ `onMounted`. Never initialise it from `window.location.hash` in setup — that causes a server/client
283
+ class mismatch.
202
284
  - **`history.pushState`** — `handleNavClick` pushes the hash into the URL so the back button and
203
285
  deep links work correctly. This runs after `preventDefault` stops Vue Router from navigating,
204
286
  so the URL stays in sync without triggering a router scroll.
@@ -19,10 +19,22 @@
19
19
  v-for="item in navItemData.main"
20
20
  :key="item.href"
21
21
  :data-href="item.href"
22
- :class="[item.cssName, { 'is-active': isActiveItem(item.href), 'is-hovered': hoveredItemHref === item.href }]"
22
+ :class="[item.cssName, { 'is-active': item.href?.startsWith('#') ? item.href === activeHash : isActiveItem(item.href), 'is-hovered': hoveredItemHref === item.href }]"
23
23
  @mouseenter="hoveredItemHref = item.href ?? null"
24
24
  >
25
+ <!-- Plain <a> for hash links — keeps Vue Router out of the smooth-scroll path -->
26
+ <a
27
+ v-if="item.href?.startsWith('#')"
28
+ :href="item.href"
29
+ class="tab-nav-link"
30
+ data-nav-item
31
+ @click="(e) => item.href && handleNavClick(e, item.href)"
32
+ >
33
+ <Icon v-if="item.iconName" :name="item.iconName" aria-hidden="true" />
34
+ {{ item.text }}
35
+ </a>
25
36
  <NuxtLink
37
+ v-else
26
38
  :href="item.href"
27
39
  :external="item.isExternal || undefined"
28
40
  class="tab-nav-link"
@@ -74,7 +86,17 @@
74
86
  <div class="tab-nav-panel-inner">
75
87
  <ul class="tab-nav-panel-list">
76
88
  <li v-for="item in navItemData.main" :key="item.href" :class="item.cssName">
89
+ <a
90
+ v-if="item.href?.startsWith('#')"
91
+ :href="item.href"
92
+ class="tab-nav-panel-link"
93
+ @click="(e) => { item.href && handleNavClick(e, item.href); closeMenu(); }"
94
+ >
95
+ <Icon v-if="item.iconName" :name="item.iconName" aria-hidden="true" />
96
+ {{ item.text }}
97
+ </a>
77
98
  <NuxtLink
99
+ v-else
78
100
  :href="item.href"
79
101
  :external="item.isExternal || undefined"
80
102
  class="tab-nav-panel-link"
@@ -97,17 +119,26 @@ interface Props {
97
119
  navItemData: NavItemData;
98
120
  navAlign?: "left" | "center" | "right";
99
121
  styleClassPassthrough?: string | string[];
122
+ anchorScrollOffset?: number | (() => number);
100
123
  }
101
124
 
102
125
  const props = withDefaults(defineProps<Props>(), {
103
126
  navAlign: "left",
104
127
  styleClassPassthrough: () => [],
128
+ anchorScrollOffset: undefined,
105
129
  });
106
130
 
107
131
  const { navRef, navListRef, isCollapsed, isLoaded, isMenuOpen, isActiveItem, toggleMenu, closeMenu } =
108
132
  useNavCollapse("tab-nav-loaded");
109
133
 
110
- const { handleNavClick } = useAnchorScroll();
134
+ const { handleNavClick, activeHash } = useAnchorScroll({ offset: props.anchorScrollOffset });
135
+
136
+ onMounted(() => {
137
+ if (!activeHash.value) {
138
+ const firstHashItem = props.navItemData.main?.find((item) => item.href?.startsWith("#"));
139
+ if (firstHashItem?.href) activeHash.value = firstHashItem.href;
140
+ }
141
+ });
111
142
 
112
143
  // ─── Animation gate — disables indicator transitions during route changes ────
113
144
  // Starts true: CSS anchor positioning resolves before first paint so there is
@@ -1,16 +1,20 @@
1
1
  interface UseAnchorScrollOptions {
2
2
  offset?: number | (() => number);
3
+ offsetElement?: Ref<HTMLElement | null>;
3
4
  }
4
5
 
5
6
  export const useAnchorScroll = (options: UseAnchorScrollOptions = {}) => {
6
- const { offset = 0 } = options;
7
+ const { offset = 0, offsetElement } = options;
7
8
 
8
9
  const prefersReducedMotion = (): boolean => {
9
10
  if (import.meta.server) return false;
10
11
  return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
11
12
  };
12
13
 
13
- const resolveOffset = (): number => (typeof offset === "function" ? offset() : offset);
14
+ const resolveOffset = (): number => {
15
+ if (offsetElement) return offsetElement.value?.offsetHeight ?? 0;
16
+ return typeof offset === "function" ? offset() : offset;
17
+ };
14
18
 
15
19
  const scrollToAnchor = (hash: string): void => {
16
20
  if (import.meta.server) return;
@@ -29,6 +33,14 @@ export const useAnchorScroll = (options: UseAnchorScrollOptions = {}) => {
29
33
  }
30
34
  };
31
35
 
36
+ // Starts empty so server and client render identically (no hydration mismatch).
37
+ // onMounted sets the real value after hydration, client-only.
38
+ const activeHash = ref("");
39
+
40
+ onMounted(() => {
41
+ activeHash.value = window.location.hash;
42
+ });
43
+
32
44
  // Intercepts anchor (#hash) clicks only. Routes and external links are left
33
45
  // to NuxtLink/router unchanged. When reduced motion is preferred, the default
34
46
  // browser/router anchor jump is preserved; otherwise we prevent that default
@@ -38,9 +50,10 @@ export const useAnchorScroll = (options: UseAnchorScrollOptions = {}) => {
38
50
  if (prefersReducedMotion()) return;
39
51
 
40
52
  event.preventDefault();
53
+ activeHash.value = href;
41
54
  history.pushState(null, "", href);
42
55
  scrollToAnchor(href);
43
56
  };
44
57
 
45
- return { handleNavClick, scrollToAnchor };
58
+ return { handleNavClick, scrollToAnchor, activeHash };
46
59
  };
@@ -38,7 +38,11 @@ export const useNavCollapse = (stateKey: string, options: NavCollapseOptions = {
38
38
  const route = useRoute();
39
39
 
40
40
  const activeHref = computed(() => route.path);
41
- const isActiveItem = (href?: string) => href === route.path;
41
+ const isActiveItem = (href?: string) => {
42
+ if (!href) return false;
43
+ if (href.startsWith("#")) return href === route.hash;
44
+ return href === route.path;
45
+ };
42
46
 
43
47
  watch(
44
48
  () => route.path,
@@ -92,6 +92,7 @@ const responsiveNavLinks = {
92
92
  { name: "Display Pill", path: "/ui/display-pill" },
93
93
  { name: "Qr Codes", path: "/ui/qr-code/display" },
94
94
  { name: "Anchor Scroll", path: "/ui/anchor-scroll" },
95
+ { name: "Anchor Scroll (TabNavigation)", path: "/ui/anchor-scroll-tab-navigation" },
95
96
  ],
96
97
  },
97
98
  {
@@ -0,0 +1,180 @@
1
+ <template>
2
+ <div>
3
+ <NuxtLayout name="default">
4
+ <template #layout-content>
5
+ <LayoutRow tag="div" variant="content" :style-class-passthrough="['mbe-4']">
6
+ <h1 class="page-heading-1">Anchor Scroll — TabNavigation</h1>
7
+ <p class="page-body-medium">
8
+ Tests <code class="inline-code">TabNavigation</code> with anchor hrefs. The nav uses
9
+ plain <code class="inline-code">&lt;a&gt;</code> tags for hash links (not
10
+ <code class="inline-code">NuxtLink</code>) to keep Vue Router out of the
11
+ smooth-scroll path. The <code class="inline-code">:anchor-scroll-offset</code> prop
12
+ passes a getter so the sticky bar height is read at scroll time.
13
+ </p>
14
+ </LayoutRow>
15
+
16
+ <div ref="stickyNavRef" class="anchor-tab-nav-sticky">
17
+ <LayoutRow tag="div" variant="content">
18
+ <TabNavigation
19
+ :nav-item-data="anchorNavData"
20
+ :anchor-scroll-offset="() => stickyNavRef?.offsetHeight ?? 0"
21
+ />
22
+ </LayoutRow>
23
+ </div>
24
+
25
+ <section id="overview" class="anchor-tab-section">
26
+ <LayoutRow tag="div" variant="content">
27
+ <h2 class="page-heading-2">Overview</h2>
28
+ <p class="page-body-medium">
29
+ This section verifies that clicking the <strong>Overview</strong> link above scrolls
30
+ here smoothly, with the heading landing below the sticky nav bar rather than behind it.
31
+ </p>
32
+ <p class="page-body-medium">
33
+ The sticky nav's height is resolved at click time via the getter passed to
34
+ <code class="inline-code">:anchor-scroll-offset</code>. Resize the viewport — the
35
+ offset adjusts automatically.
36
+ </p>
37
+ <p class="page-body-medium">
38
+ Hash links in <code class="inline-code">TabNavigation</code> render as plain
39
+ <code class="inline-code">&lt;a&gt;</code> elements (not
40
+ <code class="inline-code">NuxtLink</code>), which prevents Vue Router from
41
+ intercepting the click and triggering its own scroll behaviour.
42
+ </p>
43
+ </LayoutRow>
44
+ </section>
45
+
46
+ <section id="api" class="anchor-tab-section">
47
+ <LayoutRow tag="div" variant="content">
48
+ <h2 class="page-heading-2">API</h2>
49
+ <p class="page-body-medium">
50
+ This section verifies the <strong>API</strong> link. Confirm the heading lands
51
+ below the sticky nav after scrolling.
52
+ </p>
53
+ <p class="page-body-medium">
54
+ The <code class="inline-code">anchorScrollOffset</code> prop on
55
+ <code class="inline-code">TabNavigation</code> accepts
56
+ <code class="inline-code">number | (() => number)</code> — the same type as the
57
+ <code class="inline-code">offset</code> option on <code class="inline-code">useAnchorScroll</code>
58
+ directly. It is forwarded verbatim to the composable.
59
+ </p>
60
+ <div class="demo-code">const navItemData = {
61
+ main: [
62
+ { text: "Overview", href: "#overview" },
63
+ { text: "API", href: "#api" },
64
+ { text: "Usage", href: "#usage" },
65
+ { text: "Motion", href: "#motion" },
66
+ ],
67
+ };</div>
68
+ <div class="demo-code">&lt;div ref="stickyNavRef"&gt;
69
+ &lt;TabNavigation
70
+ :nav-item-data="navItemData"
71
+ :anchor-scroll-offset="() => stickyNavRef?.offsetHeight ?? 0"
72
+ /&gt;
73
+ &lt;/div&gt;</div>
74
+ </LayoutRow>
75
+ </section>
76
+
77
+ <section id="usage" class="anchor-tab-section">
78
+ <LayoutRow tag="div" variant="content">
79
+ <h2 class="page-heading-2">Usage</h2>
80
+ <p class="page-body-medium">
81
+ This section verifies the <strong>Usage</strong> link. Confirm the heading lands
82
+ below the sticky nav after scrolling.
83
+ </p>
84
+ <p class="page-body-medium">
85
+ Mixed navigation — routes and anchor hrefs in the same
86
+ <code class="inline-code">navItemData</code> — is supported. Route items render
87
+ as <code class="inline-code">NuxtLink</code>; anchor items render as plain
88
+ <code class="inline-code">&lt;a&gt;</code>. The handler is a no-op for routes
89
+ so both types can share the same click binding.
90
+ </p>
91
+ <div class="demo-code">const navItemData = {
92
+ main: [
93
+ { text: "About", href: "#about" }, // anchor — plain &lt;a&gt;
94
+ { text: "Services", href: "#services" }, // anchor — plain &lt;a&gt;
95
+ { text: "Blog", href: "/blog" }, // route — NuxtLink
96
+ { text: "Contact", href: "#contact" }, // anchor — plain &lt;a&gt;
97
+ ],
98
+ };</div>
99
+ </LayoutRow>
100
+ </section>
101
+
102
+ <section id="motion" class="anchor-tab-section">
103
+ <LayoutRow tag="div" variant="content">
104
+ <h2 class="page-heading-2">Reduced Motion</h2>
105
+ <p class="page-body-medium">
106
+ This section verifies the <strong>Reduced Motion</strong> link. With
107
+ <em>Reduce Motion</em> enabled in OS accessibility settings, clicks should
108
+ produce an instant jump rather than a smooth scroll — and the offset should still
109
+ be respected.
110
+ </p>
111
+ <p class="page-body-medium">
112
+ To test on macOS: <strong>System Settings → Accessibility → Display → Reduce
113
+ Motion</strong>. Refresh the page after toggling, then click any section link
114
+ above.
115
+ </p>
116
+ </LayoutRow>
117
+ </section>
118
+ </template>
119
+ </NuxtLayout>
120
+ </div>
121
+ </template>
122
+
123
+ <script setup lang="ts">
124
+ import type { NavItemData } from "~/types/components";
125
+
126
+ definePageMeta({ layout: false });
127
+
128
+ const stickyNavRef = ref<HTMLElement | null>(null);
129
+
130
+ const anchorNavData: NavItemData = {
131
+ main: [
132
+ { text: "Overview", href: "#overview" },
133
+ { text: "API", href: "#api" },
134
+ { text: "Usage", href: "#usage" },
135
+ { text: "Reduced Motion", href: "#motion" },
136
+ ],
137
+ };
138
+ </script>
139
+
140
+ <style lang="css">
141
+ .anchor-tab-nav-sticky {
142
+ position: sticky;
143
+ top: 0;
144
+ z-index: 5;
145
+ background-color: var(--page-bg, #000);
146
+ border-block-end: 1px solid oklch(100% 0 0 / 10%);
147
+ }
148
+
149
+ .anchor-tab-section {
150
+ min-block-size: 60vh;
151
+ padding-block: 6rem 8rem;
152
+ border-block-end: 1px solid oklch(100% 0 0 / 8%);
153
+
154
+ &:last-child {
155
+ border-block-end: none;
156
+ }
157
+
158
+ .page-heading-2 {
159
+ margin-block-end: 2rem;
160
+ }
161
+
162
+ .page-body-medium {
163
+ margin-block-end: 1.6rem;
164
+ }
165
+ }
166
+
167
+ .demo-code {
168
+ background-color: oklch(100% 0 0 / 4%);
169
+ border: 1px solid oklch(100% 0 0 / 10%);
170
+ border-radius: 0.6rem;
171
+ padding: 1.8rem 2rem;
172
+ overflow-x: auto;
173
+ font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;
174
+ font-size: 1.3rem;
175
+ line-height: 1.65;
176
+ margin-block: 1.2rem 2rem;
177
+ white-space: pre;
178
+ color: var(--slate-01, currentColor);
179
+ }
180
+ </style>
@@ -26,6 +26,7 @@
26
26
  <a
27
27
  :href="`#${section.id}`"
28
28
  class="anchor-nav-link"
29
+ :class="{ 'is-active': `#${section.id}` === activeHash }"
29
30
  @click="(e) => handleNavClick(e, `#${section.id}`)"
30
31
  >{{ section.label }}</a>
31
32
  </li>
@@ -216,9 +217,13 @@ const sections = [
216
217
  { id: "motion", label: "Reduced Motion" },
217
218
  ];
218
219
 
219
- const { handleNavClick } = useAnchorScroll({
220
+ const { handleNavClick, activeHash } = useAnchorScroll({
220
221
  offset: () => stickyNavRef.value?.offsetHeight ?? 0,
221
222
  });
223
+
224
+ onMounted(() => {
225
+ if (!activeHash.value && sections[0]) activeHash.value = `#${sections[0].id}`;
226
+ });
222
227
  </script>
223
228
 
224
229
  <style lang="css">
@@ -255,6 +260,11 @@ const { handleNavClick } = useAnchorScroll({
255
260
  border-block-end-color: currentColor;
256
261
  outline: none;
257
262
  }
263
+
264
+ &.is-active {
265
+ color: var(--slate-00, currentColor);
266
+ border-block-end-color: currentColor;
267
+ }
258
268
  }
259
269
 
260
270
  .anchor-demo-section {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.1.48",
4
+ "version": "9.1.49",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",