srcdev-nuxt-components 9.4.7 → 9.4.8

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.
Files changed (49) hide show
  1. package/.claude/component-ledger/audit.json +1 -1
  2. package/.claude/component-ledger/output.html +1 -1
  3. package/.claude/skills/component-dynamic-slots.md +1 -1
  4. package/.claude/skills/components/header-block.md +92 -0
  5. package/.claude/skills/components/rotating-carousel-image.md +101 -0
  6. package/.claude/skills/components/skip-links.md +82 -0
  7. package/.claude/skills/components/tabs-core.md +187 -0
  8. package/.claude/skills/index.md +4 -0
  9. package/.vscode/srcdev-component-header-block.code-snippets +41 -0
  10. package/.vscode/srcdev-component-rotating-carousel-image.code-snippets +74 -0
  11. package/.vscode/srcdev-component-skip-links.code-snippets +55 -0
  12. package/.vscode/srcdev-component-tabs-core.code-snippets +78 -0
  13. package/app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-heading.css +2 -1
  14. package/app/components/01.atoms/animations/rotating-carousel-image/CONSUMER-STYLING.md +54 -0
  15. package/app/components/01.atoms/animations/rotating-carousel-image/RotatingCarouselImage.vue +315 -0
  16. package/app/components/01.atoms/animations/rotating-carousel-image/stories/RotatingCarouselImage.stories.ts +156 -0
  17. package/app/components/01.atoms/animations/rotating-carousel-image/tests/RotatingCarouselImage.spec.ts +230 -0
  18. package/app/components/01.atoms/animations/rotating-carousel-image/tests/__snapshots__/RotatingCarouselImage.spec.ts.snap +19 -0
  19. package/app/components/01.atoms/navigation/skip-links/CONSUMER-STYLING.md +31 -0
  20. package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +92 -0
  21. package/app/components/01.atoms/navigation/skip-links/stories/SkipLinks.stories.ts +113 -0
  22. package/app/components/01.atoms/navigation/skip-links/tests/SkipLinks.spec.ts +71 -0
  23. package/app/components/01.atoms/navigation/skip-links/tests/__snapshots__/SkipLinks.spec.ts.snap +15 -0
  24. package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +99 -0
  25. package/app/components/01.atoms/navigation/tabs/TabsCore.vue +272 -0
  26. package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +199 -0
  27. package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +274 -0
  28. package/app/components/01.atoms/text-blocks/header-block/CONSUMER-STYLING.md +43 -0
  29. package/app/components/01.atoms/text-blocks/header-block/HeaderBlock.vue +50 -0
  30. package/app/components/01.atoms/text-blocks/header-block/stories/HeaderBlock.stories.ts +111 -0
  31. package/app/components/01.atoms/text-blocks/header-block/tests/HeaderBlock.spec.ts +119 -0
  32. package/app/components/01.atoms/text-blocks/header-block/tests/__snapshots__/HeaderBlock.spec.ts.snap +5 -0
  33. package/app/components/03.organisms/site-header/SiteHeader.vue +1 -1
  34. package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +1 -1
  35. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +3 -3
  36. package/app/composables/useTabs.ts +225 -207
  37. package/app/types/components/index.ts +2 -0
  38. package/app/types/components/rotating-carousel-image.d.ts +4 -0
  39. package/app/types/components/skip-links.d.ts +4 -0
  40. package/package.json +1 -1
  41. package/app/components/01.atoms/grids/data-grid/tests/__snapshots__/DataGrid.spec.ts.snap +0 -11
  42. package/app/components/rotating-carousel/RotatingCarouselImage.vue +0 -216
  43. package/app/components/skip-links/SkipLinks.vue +0 -60
  44. package/app/components/tabs/TabsCore.vue +0 -306
  45. package/app/components/typography/HeaderBlock.vue +0 -35
  46. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/AutoGrid.vue +0 -0
  47. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/stories/AutoGrid.stories.ts +0 -0
  48. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/tests/AutoGrid.spec.ts +0 -0
  49. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/tests/__snapshots__/AutoGrid.spec.ts.snap +0 -0
@@ -0,0 +1,19 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`RotatingCarouselImage > renders correct HTML structure (all props set) 1`] = `
4
+ "<section class="rotating-carousel hover-pauses" style="--_rotate-x: 10deg; --_perspective: 1200px; --_translateZ: 1200px;" role="region" aria-label="Custom carousel" tabindex="0">
5
+ <div class="sr-only">Custom instructions</div><button class="control-btn" type="button" aria-label="Pause rotation"><span class="iconify i-mdi:pause" aria-hidden="true"></span></button>
6
+ <div class="slider" style="--_quantity: 2;">
7
+ <div class="item" style="--_position: 0;"><img data-nuxt-img="" srcset="/_ipx/_/a.jpg 1x, /_ipx/_/a.jpg 2x" alt="A" src="/_ipx/_/a.jpg"></div>
8
+ <div class="item" style="--_position: 1;"><img data-nuxt-img="" srcset="/_ipx/_/b.jpg 1x, /_ipx/_/b.jpg 2x" alt="B" src="/_ipx/_/b.jpg"></div>
9
+ </div>
10
+ </section>"
11
+ `;
12
+
13
+ exports[`RotatingCarouselImage > renders correct HTML structure (default props) 1`] = `
14
+ "<div class="rotating-carousel" style="--_rotate-x: 0deg; --_perspective: 1000px; --_translateZ: 1000px;" role="region" aria-label="Rotating image carousel" tabindex="0">
15
+ <div class="sr-only">Use spacebar to pause or play the rotation.</div>
16
+ <!--v-if-->
17
+ <div class="slider" style="--_quantity: 0;"></div>
18
+ </div>"
19
+ `;
@@ -0,0 +1,31 @@
1
+ # SkipLinks — Consumer Styling Guide
2
+
3
+ ## Public token API
4
+
5
+ | Token | Default | Controls |
6
+ |---|---|---|
7
+ | `--skip-links-z-index` | `1000` | Stacking context of the revealed skip-nav panel |
8
+ | `--skip-links-gap` | `0.2rem` | Gap between stacked skip links |
9
+ | `--skip-links-background-colour` | `black` | Skip-nav panel background |
10
+ | `--skip-links-border-colour` | `white` | Skip-nav panel border colour |
11
+ | `--skip-links-border-width` | `1px` | Skip-nav panel border width |
12
+ | `--skip-links-padding` | `0.2rem` | Skip-nav panel padding |
13
+ | `--skip-links-text-colour` | `white` | Skip link text colour |
14
+ | `--skip-links-link-padding-block` | `0.8rem` | Skip link vertical padding |
15
+ | `--skip-links-link-padding-inline` | `1.2rem` | Skip link horizontal padding |
16
+ | `--skip-links-transition-duration` | `0.3s` | Duration of the reveal transition when a link is focused |
17
+ | `--skip-links-focus-outline-width` | `2px` | Focus outline width on a skip link |
18
+ | `--skip-links-focus-outline-colour` | `white` | Focus outline colour on a skip link |
19
+
20
+ ```css
21
+ .my-page {
22
+ --skip-links-background-colour: #111;
23
+ --skip-links-border-colour: #ffd700;
24
+ --skip-links-focus-outline-colour: #ffd700;
25
+ }
26
+ ```
27
+
28
+ ## Links are prop-driven
29
+
30
+ The set of skip links is provided via the `links` prop (`{ href, label }[]`), not slots — see the
31
+ skill doc for the default value and localisation notes.
@@ -0,0 +1,92 @@
1
+ <template>
2
+ <div class="skip-links" :class="[elementClasses]">
3
+ <div v-if="$slots.homeLink" class="skip-links__home">
4
+ <slot name="homeLink"></slot>
5
+ </div>
6
+ <nav class="skip-links__nav" :aria-label="ariaLabel">
7
+ <a v-for="link in links" :key="link.href" :href="link.href" class="skip-links__link">{{ link.label }}</a>
8
+ </nav>
9
+ </div>
10
+ </template>
11
+
12
+ <script lang="ts" setup>
13
+ import type { SkipLink } from "~/types/components"
14
+
15
+ interface Props {
16
+ links?: SkipLink[]
17
+ /** aria-label on the nav landmark — override for localisation. */
18
+ ariaLabel?: string
19
+ styleClassPassthrough?: string | string[]
20
+ }
21
+
22
+ const props = withDefaults(defineProps<Props>(), {
23
+ links: () => [
24
+ { href: "#main-content", label: "Skip to main content" },
25
+ { href: "#footer-content", label: "Skip to footer" },
26
+ ],
27
+ ariaLabel: "Skip navigation",
28
+ styleClassPassthrough: () => [],
29
+ })
30
+
31
+ const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough)
32
+
33
+ watch(
34
+ () => props.styleClassPassthrough,
35
+ () => {
36
+ resetElementClasses(props.styleClassPassthrough)
37
+ }
38
+ )
39
+ </script>
40
+
41
+ <style lang="css">
42
+ @layer components {
43
+ .skip-links {
44
+ position: relative;
45
+
46
+ .skip-links__nav {
47
+ position: absolute;
48
+ top: 100%;
49
+ left: 0;
50
+ z-index: var(--skip-links-z-index, 1000);
51
+
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: var(--skip-links-gap, 0.2rem);
55
+ background-color: var(--skip-links-background-colour, black);
56
+ border: var(--skip-links-border-width, 1px) solid var(--skip-links-border-colour, white);
57
+ padding: var(--skip-links-padding, 0.2rem);
58
+ margin: 0;
59
+
60
+ /* Hidden off-screen via transform — does NOT break keyboard focusability */
61
+ opacity: 0;
62
+ transform: translateY(-100%);
63
+ transition:
64
+ transform var(--skip-links-transition-duration, 0.3s) ease-in-out,
65
+ opacity var(--skip-links-transition-duration, 0.3s) ease-in-out;
66
+
67
+ &:focus-within {
68
+ opacity: 1;
69
+ transform: translateY(0);
70
+ }
71
+ }
72
+
73
+ .skip-links__link {
74
+ display: block;
75
+ color: var(--skip-links-text-colour, white);
76
+ text-decoration: none;
77
+ padding: var(--skip-links-link-padding-block, 0.8rem) var(--skip-links-link-padding-inline, 1.2rem);
78
+ text-wrap: nowrap;
79
+
80
+ &:focus-visible {
81
+ outline: var(--skip-links-focus-outline-width, 2px) solid var(--skip-links-focus-outline-colour, white);
82
+ }
83
+ }
84
+ }
85
+
86
+ @media (prefers-reduced-motion: reduce) {
87
+ .skip-links .skip-links__nav {
88
+ transition: none;
89
+ }
90
+ }
91
+ }
92
+ </style>
@@ -0,0 +1,113 @@
1
+ import SkipLinks from "../SkipLinks.vue";
2
+ import type { Meta, StoryObj } from "@nuxtjs/storybook";
3
+ import type { SkipLink } from "~/types/components";
4
+
5
+ const meta: Meta<typeof SkipLinks> = {
6
+ title: "Atoms/Navigation/SkipLinks",
7
+ component: SkipLinks,
8
+ argTypes: {
9
+ links: {
10
+ control: "object",
11
+ description: "Ordered list of { href, label } skip targets",
12
+ },
13
+ ariaLabel: {
14
+ control: "text",
15
+ description: "aria-label on the nav landmark — override for localisation",
16
+ },
17
+ styleClassPassthrough: {
18
+ control: "object",
19
+ description: "Additional CSS classes applied to the root element",
20
+ },
21
+ },
22
+ args: {
23
+ ariaLabel: "Skip navigation",
24
+ styleClassPassthrough: [],
25
+ },
26
+ parameters: {
27
+ docs: {
28
+ description: {
29
+ component:
30
+ "An accessibility utility that renders a set of visually-hidden links revealed only on keyboard focus, letting keyboard/screen-reader users jump past repeated navigation straight to main content, the footer, or any other in-page target. The links are placed first in the DOM — consumers should place `<SkipLinks>` at the very top of their page layout.",
31
+ },
32
+ },
33
+ },
34
+ };
35
+
36
+ export default meta;
37
+ type Story = StoryObj<typeof SkipLinks>;
38
+
39
+ // ─── Stories ──────────────────────────────────────────────────────────────────
40
+
41
+ export const Default: Story = {
42
+ render: (args) => ({
43
+ components: { SkipLinks },
44
+ setup() {
45
+ return { args };
46
+ },
47
+ template: `
48
+ <div>
49
+ <p style="padding: 2rem; text-align: center;">
50
+ The skip links below are visually hidden until focused. Click into this preview, then
51
+ press <kbd>Tab</kbd> to reveal them.
52
+ </p>
53
+ <SkipLinks v-bind="args" />
54
+ </div>
55
+ `,
56
+ }),
57
+ };
58
+
59
+ const customLinks: SkipLink[] = [
60
+ { href: "#site-navigation", label: "Skip to navigation" },
61
+ { href: "#main-content", label: "Skip to main content" },
62
+ { href: "#footer-content", label: "Skip to footer" },
63
+ ];
64
+
65
+ export const CustomLinks: Story = {
66
+ name: "Custom Links",
67
+ args: { links: customLinks },
68
+ render: (args) => ({
69
+ components: { SkipLinks },
70
+ setup() {
71
+ return { args };
72
+ },
73
+ template: `
74
+ <div>
75
+ <p style="padding: 2rem; text-align: center;">
76
+ Press <kbd>Tab</kbd> after clicking into this preview to reveal three custom skip links.
77
+ </p>
78
+ <SkipLinks v-bind="args" />
79
+ </div>
80
+ `,
81
+ }),
82
+ };
83
+
84
+ export const AlwaysVisible: Story = {
85
+ name: "Always Visible (for review)",
86
+ parameters: {
87
+ docs: {
88
+ description: {
89
+ story: "Forces the skip-nav panel open via `:focus-within` emulation so it can be inspected without tabbing — for visual review only, not representative of real usage.",
90
+ },
91
+ },
92
+ },
93
+ render: (args) => ({
94
+ components: { SkipLinks },
95
+ setup() {
96
+ return { args };
97
+ },
98
+ template: `
99
+ <div>
100
+ <style>
101
+ .always-visible-demo .skip-links__nav {
102
+ opacity: 1 !important;
103
+ transform: translateY(0) !important;
104
+ position: static !important;
105
+ }
106
+ </style>
107
+ <div class="always-visible-demo">
108
+ <SkipLinks v-bind="args" />
109
+ </div>
110
+ </div>
111
+ `,
112
+ }),
113
+ };
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
3
+ import SkipLinks from "../SkipLinks.vue";
4
+ import type { SkipLink } from "~/types/components";
5
+
6
+ describe("SkipLinks", () => {
7
+ it("mounts without error", async () => {
8
+ const wrapper = await mountSuspended(SkipLinks);
9
+ expect(wrapper.vm).toBeTruthy();
10
+ });
11
+
12
+ it("renders correct HTML structure (default props)", async () => {
13
+ const wrapper = await mountSuspended(SkipLinks);
14
+ expect(wrapper.html()).toMatchSnapshot();
15
+ });
16
+
17
+ it("renders correct HTML structure (all props set)", async () => {
18
+ const wrapper = await mountSuspended(SkipLinks, {
19
+ props: {
20
+ links: [{ href: "#content", label: "Skip to content" }],
21
+ ariaLabel: "Custom skip nav",
22
+ },
23
+ });
24
+ expect(wrapper.html()).toMatchSnapshot();
25
+ });
26
+
27
+ it("renders the default main-content and footer links", async () => {
28
+ const wrapper = await mountSuspended(SkipLinks);
29
+ const links = wrapper.findAll(".skip-links__link");
30
+ expect(links).toHaveLength(2);
31
+ expect(links[0]!.attributes("href")).toBe("#main-content");
32
+ expect(links[0]!.text()).toBe("Skip to main content");
33
+ expect(links[1]!.attributes("href")).toBe("#footer-content");
34
+ expect(links[1]!.text()).toBe("Skip to footer");
35
+ });
36
+
37
+ it("renders custom links when the links prop is provided", async () => {
38
+ const customLinks: SkipLink[] = [
39
+ { href: "#nav", label: "Skip to navigation" },
40
+ { href: "#search", label: "Skip to search" },
41
+ { href: "#content", label: "Skip to content" },
42
+ ];
43
+ const wrapper = await mountSuspended(SkipLinks, { props: { links: customLinks } });
44
+ const links = wrapper.findAll(".skip-links__link");
45
+ expect(links).toHaveLength(3);
46
+ expect(links.map((link) => link.text())).toEqual(["Skip to navigation", "Skip to search", "Skip to content"]);
47
+ });
48
+
49
+ it("defaults the nav aria-label to 'Skip navigation'", async () => {
50
+ const wrapper = await mountSuspended(SkipLinks);
51
+ expect(wrapper.find(".skip-links__nav").attributes("aria-label")).toBe("Skip navigation");
52
+ });
53
+
54
+ it("uses a custom ariaLabel when provided", async () => {
55
+ const wrapper = await mountSuspended(SkipLinks, { props: { ariaLabel: "Custom skip nav" } });
56
+ expect(wrapper.find(".skip-links__nav").attributes("aria-label")).toBe("Custom skip nav");
57
+ });
58
+
59
+ it("does not render the home-link slot wrapper when the slot is unused", async () => {
60
+ const wrapper = await mountSuspended(SkipLinks);
61
+ expect(wrapper.find(".skip-links__home").exists()).toBe(false);
62
+ });
63
+
64
+ it("renders the homeLink slot content when provided", async () => {
65
+ const wrapper = await mountSuspended(SkipLinks, {
66
+ slots: { homeLink: "<a href='/' class='logo-link'>Home</a>" },
67
+ });
68
+ expect(wrapper.find(".skip-links__home").exists()).toBe(true);
69
+ expect(wrapper.find(".logo-link").exists()).toBe(true);
70
+ });
71
+ });
@@ -0,0 +1,15 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`SkipLinks > renders correct HTML structure (all props set) 1`] = `
4
+ "<div class="skip-links">
5
+ <!--v-if-->
6
+ <nav class="skip-links__nav" aria-label="Custom skip nav"><a href="#content" class="skip-links__link">Skip to content</a></nav>
7
+ </div>"
8
+ `;
9
+
10
+ exports[`SkipLinks > renders correct HTML structure (default props) 1`] = `
11
+ "<div class="skip-links">
12
+ <!--v-if-->
13
+ <nav class="skip-links__nav" aria-label="Skip navigation"><a href="#main-content" class="skip-links__link">Skip to main content</a><a href="#footer-content" class="skip-links__link">Skip to footer</a></nav>
14
+ </div>"
15
+ `;
@@ -0,0 +1,99 @@
1
+ # TabsCore — Consumer Styling Guide
2
+
3
+ ## Public token API
4
+
5
+ | Token | Default | Controls |
6
+ |---|---|---|
7
+ | `--tabs-nav-border` | `0.1rem solid var(--slate-06)` | Border between the tab list and its content (bottom border on `axis="x"`, left border on `axis="y"`) |
8
+ | `--tabs-hover-indicator-colour` | `var(--slate-07)` | Background of the moving hover highlight (`trackHover`) |
9
+ | `--tabs-hover-indicator-text-colour` | `var(--slate-00)` | Text colour of a hovered tab trigger's label — keep this in contrast with `--tabs-hover-indicator-colour` |
10
+ | `--tabs-active-indicator-colour` | `var(--slate-09)` | Background of the moving active-tab highlight (`trackActive`) |
11
+ | `--tabs-active-indicator-text-colour` | `var(--slate-00)` | Fallback text colour of the active tab trigger's label — keep this in contrast with `--tabs-active-indicator-colour` |
12
+ | `--tabs-underline-indicator-colour` | `var(--slate-10)` | Colour of the underline/sideline indicator (`trackIndicator`) |
13
+ | `--tabs-underline-indicator-height` | `0.4rem` | Thickness of the underline indicator (`axis="x"`) — also used as its width on `axis="y"` |
14
+ | `--tabs-list-item-opacity` | `0.7` | Opacity of an inactive tab trigger |
15
+ | `--tabs-list-item-colour` | `var(--slate-10)` | Text colour of an inactive tab trigger |
16
+ | `--tabs-list-item-colour-selected` | falls back to `--tabs-active-indicator-text-colour` | Text colour of the active tab trigger — set this directly to override without touching `--tabs-active-indicator-text-colour` |
17
+ | `--tabs-list-item-text-transform` | `uppercase` | `text-transform` of tab trigger labels |
18
+ | `--tabs-list-item-font-weight` | `500` | Font weight of tab trigger labels |
19
+ | `--tabs-list-item-padding-block` | `1em` | Vertical padding of a tab trigger |
20
+ | `--tabs-list-item-padding-inline` | `2em` | Horizontal padding of a tab trigger |
21
+ | `--tabs-list-item-colour-transition-duration` | `100ms` | Transition duration of the trigger text-colour change |
22
+ | `--tabs-list-item-focus-outline-width` | `2px` | Focus outline width on a tab trigger/panel (colour is `--theme-ring`) |
23
+ | `--tabs-list-item-focus-outline-offset` | `-2px` | Focus outline offset on a tab trigger |
24
+ | `--tabs-content-background-colour` | `var(--slate-09)` | Background of the content panel wrapper |
25
+ | `--tabs-content-border-width` | `0.1rem` | Border/outline width of the content panel wrapper |
26
+ | `--tabs-content-border-colour` | `var(--slate-06)` | Border/outline colour of the content panel wrapper |
27
+ | `--tabs-content-border-radius` | `0` | Corner radius of the content panel wrapper |
28
+ | `--tabs-axis-y-gap` | `2em` | Gap between the tab list and content when `axis="y"` |
29
+
30
+ ```css
31
+ .my-page {
32
+ --tabs-active-indicator-colour: #1a1a1a;
33
+ --tabs-underline-indicator-colour: #b5651d;
34
+ --tabs-content-border-radius: 0.8rem;
35
+ }
36
+ ```
37
+
38
+ ## Colour tokens are flat values, not light-dark() pairs
39
+
40
+ Every colour token above defaults to a single flat `--slate-*` value rather than a
41
+ `light-dark(lightValue, darkValue)` pair — this component doesn't bake in automatic light/dark
42
+ scheme-switching for its own defaults. If you want a token to adapt to `color-scheme`, wrap your
43
+ own override in `light-dark()`:
44
+
45
+ ```css
46
+ .my-page {
47
+ --tabs-active-indicator-colour: light-dark(#1a1a1a, #f5f5f5);
48
+ }
49
+ ```
50
+
51
+ ## Tab-label text colour tracks the indicator behind it
52
+
53
+ Hover and active can have different background colours (`--tabs-hover-indicator-colour` vs.
54
+ `--tabs-active-indicator-colour`), so the trigger label uses two separate text-colour tokens
55
+ rather than one shared "selected" colour — otherwise overriding just one indicator's background
56
+ risks making the label unreadable against it while leaving the other state fine. If you override
57
+ either indicator background, override its matching text-colour token to keep contrast:
58
+
59
+ ```css
60
+ .my-page {
61
+ --tabs-hover-indicator-colour: gold;
62
+ --tabs-hover-indicator-text-colour: black;
63
+ --tabs-active-indicator-colour: navy;
64
+ --tabs-active-indicator-text-colour: white;
65
+ }
66
+ ```
67
+
68
+ ## Tabs spanned by a jump keep their normal styling
69
+
70
+ When the active tab (or hover) jumps across several tabs at once (e.g. clicking tab 5 while tab 1
71
+ is active), the sliding indicator animates across the tabs in between, but their own labels are
72
+ **not** recoloured to the active/hover text colour during that transition — they keep their normal
73
+ inactive styling throughout. An earlier version force-applied the active text colour to every
74
+ spanned tab for the whole transition, on the assumption the sliding indicator visually covered them
75
+ the entire time — it doesn't (a forward jump grows the indicator gradually from the old tab's
76
+ position, so it only reaches later tabs partway through), so intermediate labels could end up a
77
+ colour that matched neither their own background nor the indicator yet, making them briefly
78
+ invisible. Fixed 2026-09-15.
79
+
80
+ Since a non-active tab keeps its normal inactive text colour throughout, and the active indicator
81
+ still visually passes under it while sliding, `--tabs-active-indicator-colour`'s default
82
+ (`var(--slate-09)`) is deliberately *not* the same value as `--tabs-list-item-colour`'s default
83
+ (`var(--slate-10)`) — they used to be identical, which meant a passed-over tab's text became
84
+ literally the same colour as the indicator sliding beneath it (invisible). If you override
85
+ `--tabs-active-indicator-colour`, check it still differs enough from `--tabs-list-item-colour` (or
86
+ override that too) to avoid the same collision recurring.
87
+
88
+ ## Indicator visibility is prop-driven, not tokens
89
+
90
+ Whether the hover highlight, active highlight, and underline indicator render at all is controlled
91
+ by the `trackHover`/`trackActive`/`trackIndicator` boolean props (all default `true`), since they
92
+ also gate whether the underlying DOM nodes are created — not just their styling.
93
+
94
+ ## Movement/position values are private
95
+
96
+ `--_x-active`, `--_x-hovered`, `--_width-active`, `--_width-hovered`, `--_y-active`, `--_y-hovered`,
97
+ `--_y-height`, `--_y-width`, and `--_transition-duration` are computed at runtime from the tab
98
+ elements' measured layout (`offsetLeft`/`offsetWidth`/etc. in `useTabs`) and aren't meaningful to
99
+ override directly — use `transitionDuration` (a prop, in ms) to control indicator movement speed.