srcdev-nuxt-components 9.4.6 → 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 (109) hide show
  1. package/.claude/commands/migrate-component.md +34 -3
  2. package/.claude/component-ledger/audit.json +1 -1
  3. package/.claude/component-ledger/build.mjs +27 -0
  4. package/.claude/component-ledger/output.html +8 -3
  5. package/.claude/component-ledger/template.html +7 -2
  6. package/.claude/skills/component-dynamic-slots.md +1 -1
  7. package/.claude/skills/components/column-flow-grid.md +93 -0
  8. package/.claude/skills/components/entry-animation.md +88 -0
  9. package/.claude/skills/components/header-block.md +92 -0
  10. package/.claude/skills/components/input-copy.md +2 -0
  11. package/.claude/skills/components/input-text-core.md +186 -0
  12. package/.claude/skills/components/masonry-grid.md +153 -0
  13. package/.claude/skills/components/pop-over.md +100 -0
  14. package/.claude/skills/components/rotating-carousel-image.md +101 -0
  15. package/.claude/skills/components/skip-links.md +82 -0
  16. package/.claude/skills/components/tabs-core.md +187 -0
  17. package/.claude/skills/index.md +10 -1
  18. package/.vscode/srcdev-component-column-flow-grid.code-snippets +39 -0
  19. package/.vscode/srcdev-component-entry-animation.code-snippets +29 -0
  20. package/.vscode/srcdev-component-header-block.code-snippets +41 -0
  21. package/.vscode/srcdev-component-input-text.code-snippets +107 -0
  22. package/.vscode/srcdev-component-masonry-grid.code-snippets +51 -0
  23. package/.vscode/srcdev-component-pop-over.code-snippets +44 -0
  24. package/.vscode/srcdev-component-rotating-carousel-image.code-snippets +74 -0
  25. package/.vscode/srcdev-component-skip-links.code-snippets +55 -0
  26. package/.vscode/srcdev-component-tabs-core.code-snippets +78 -0
  27. package/app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-heading.css +2 -1
  28. package/app/components/01.atoms/animations/entry/EntryAnimation.vue +7 -1
  29. package/app/components/01.atoms/animations/entry/stories/EntryAnimation.stories.ts +47 -0
  30. package/app/components/01.atoms/animations/entry/tests/EntryAnimation.spec.ts +57 -0
  31. package/app/components/01.atoms/animations/rotating-carousel-image/CONSUMER-STYLING.md +54 -0
  32. package/app/components/01.atoms/animations/rotating-carousel-image/RotatingCarouselImage.vue +315 -0
  33. package/app/components/01.atoms/animations/rotating-carousel-image/stories/RotatingCarouselImage.stories.ts +156 -0
  34. package/app/components/01.atoms/animations/rotating-carousel-image/tests/RotatingCarouselImage.spec.ts +230 -0
  35. package/app/components/01.atoms/animations/rotating-carousel-image/tests/__snapshots__/RotatingCarouselImage.spec.ts.snap +19 -0
  36. package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +9 -10
  37. package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +12 -13
  38. package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +33 -0
  39. package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +55 -0
  40. package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +178 -0
  41. package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +75 -0
  42. package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +35 -0
  43. package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +178 -0
  44. package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +199 -0
  45. package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +158 -0
  46. package/app/components/01.atoms/navigation/skip-links/CONSUMER-STYLING.md +31 -0
  47. package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +92 -0
  48. package/app/components/01.atoms/navigation/skip-links/stories/SkipLinks.stories.ts +113 -0
  49. package/app/components/01.atoms/navigation/skip-links/tests/SkipLinks.spec.ts +71 -0
  50. package/app/components/01.atoms/navigation/skip-links/tests/__snapshots__/SkipLinks.spec.ts.snap +15 -0
  51. package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +99 -0
  52. package/app/components/01.atoms/navigation/tabs/TabsCore.vue +272 -0
  53. package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +199 -0
  54. package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +274 -0
  55. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +54 -0
  56. package/app/components/01.atoms/pop-over/PopOver.vue +201 -0
  57. package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +141 -0
  58. package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +195 -0
  59. package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +11 -0
  60. package/app/components/01.atoms/text-blocks/header-block/CONSUMER-STYLING.md +43 -0
  61. package/app/components/01.atoms/text-blocks/header-block/HeaderBlock.vue +50 -0
  62. package/app/components/01.atoms/text-blocks/header-block/stories/HeaderBlock.stories.ts +111 -0
  63. package/app/components/01.atoms/text-blocks/header-block/tests/HeaderBlock.spec.ts +119 -0
  64. package/app/components/01.atoms/text-blocks/header-block/tests/__snapshots__/HeaderBlock.spec.ts.snap +5 -0
  65. package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +8 -2
  66. package/app/components/02.molecules/input-copy/InputCopy.vue +14 -0
  67. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +15 -0
  68. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +41 -0
  69. package/app/components/03.organisms/site-header/SiteHeader.vue +1 -1
  70. package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +1 -1
  71. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +3 -3
  72. package/app/components/05.forms/input-select/InputSelectCore.vue +1 -1
  73. package/app/components/05.forms/input-text/InputTextCore.vue +6 -0
  74. package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +49 -20
  75. package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +41 -22
  76. package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +48 -22
  77. package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +43 -17
  78. package/app/components/05.forms/input-text/tests/InputPasswordWithLabel.spec.ts +56 -0
  79. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +61 -0
  80. package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +60 -0
  81. package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +46 -0
  82. package/app/components/05.forms/input-text/variants/InputPasswordWithLabel.vue +7 -1
  83. package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +8 -2
  84. package/app/components/05.forms/input-text/variants/InputTextWithLabel.vue +6 -0
  85. package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +20 -20
  86. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +32 -26
  87. package/app/composables/useTabs.ts +225 -207
  88. package/app/types/components/index.ts +2 -0
  89. package/app/types/components/rotating-carousel-image.d.ts +4 -0
  90. package/app/types/components/skip-links.d.ts +4 -0
  91. package/app/types/forms/types.forms.d.ts +1 -1
  92. package/package.json +1 -1
  93. package/app/components/01.atoms/grids/data-grid/tests/__snapshots__/DataGrid.spec.ts.snap +0 -11
  94. package/app/components/masonry-grid/MasonryGrid.vue +0 -68
  95. package/app/components/masonry-grid-ordered/MasonryGridOrdered.vue +0 -163
  96. package/app/components/masonry-grid-ordered/MasonryGridOrderedGridExperiment.vue +0 -259
  97. package/app/components/masonry-grid-ordered/stories/MasonryGridOrdered.stories.ts +0 -354
  98. package/app/components/masonry-grid-sorted/MasonryGridSorted.vue +0 -120
  99. package/app/components/pop-over/PopOver.vue +0 -90
  100. package/app/components/rotating-carousel/RotatingCarouselImage.vue +0 -216
  101. package/app/components/skip-links/SkipLinks.vue +0 -60
  102. package/app/components/tabs/TabsCore.vue +0 -306
  103. package/app/components/typography/HeaderBlock.vue +0 -35
  104. package/app/layouts/default.vue +0 -308
  105. package/app/layouts/site-navigation-demo.vue +0 -188
  106. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/AutoGrid.vue +0 -0
  107. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/stories/AutoGrid.stories.ts +0 -0
  108. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/tests/AutoGrid.spec.ts +0 -0
  109. /package/app/components/01.atoms/grids/{data-grid → auto-grid}/tests/__snapshots__/AutoGrid.spec.ts.snap +0 -0
@@ -1,291 +1,309 @@
1
- import { useResizeObserver } from "@vueuse/core"
1
+ import { useResizeObserver } from "@vueuse/core";
2
+
3
+ interface UseTabsOptions {
4
+ trackHover: boolean;
5
+ trackActive: boolean;
6
+ trackIndicator: boolean;
7
+ }
2
8
 
3
9
  const useTabs = (
4
10
  axis: string,
5
11
  tabsNavRef: Ref<HTMLElement | null>,
6
12
  tabsContentRefs: Ref<HTMLElement[] | null>,
7
- duration: number
13
+ duration: number,
14
+ options: UseTabsOptions = { trackHover: true, trackActive: true, trackIndicator: true }
8
15
  ) => {
9
- const navItems = ref<HTMLElement[] | null>(null)
10
- const previousActiveTab = useState<HTMLElement | null>("previousActiveTab", () => null)
11
- const currentActiveTab = ref<HTMLElement>()
16
+ const navItems = ref<HTMLElement[] | null>(null);
17
+ // Plain ref, not useState: this is ephemeral per-instance UI state, not something that
18
+ // needs to survive SSR hydration or be shared by key — useState's fixed key here previously
19
+ // meant every TabsCore instance on the page silently shared the same "previous active tab".
20
+ const previousActiveTab = ref<HTMLElement | null>(null);
21
+ const currentActiveTab = ref<HTMLElement>();
12
22
 
13
- const previousHoveredTab = ref<HTMLElement>()
14
- const currentHoveredTab = ref<HTMLElement>()
15
- const tagName = ref<string>()
23
+ const previousHoveredTab = ref<HTMLElement>();
24
+ const currentHoveredTab = ref<HTMLElement>();
25
+ const tagName = ref<string>();
26
+
27
+ let activeSettleTimeoutId: ReturnType<typeof setTimeout> | null = null;
28
+ let hoveredSettleTimeoutId: ReturnType<typeof setTimeout> | null = null;
16
29
 
17
30
  const initNavDecorators = () => {
18
31
  navItems.value = tabsNavRef.value
19
- ? (Array.from(tabsNavRef.value.querySelectorAll("[data-nav-item")) as HTMLElement[])
20
- : []
32
+ ? (Array.from(tabsNavRef.value.querySelectorAll("[data-nav-item]")) as HTMLElement[])
33
+ : [];
21
34
 
22
35
  if (!navItems.value || navItems.value.length === 0) {
23
- return
36
+ return;
24
37
  }
25
38
 
26
- const firstNavItem = navItems.value[0]
27
- if (!firstNavItem) return
39
+ const firstNavItem = navItems.value[0];
40
+ if (!firstNavItem) return;
28
41
 
29
- tagName.value = firstNavItem.tagName.toLowerCase()
42
+ tagName.value = firstNavItem.tagName.toLowerCase();
30
43
 
31
- const activeIndex = ref(0)
44
+ const activeIndex = ref(0);
32
45
 
33
46
  // Temporarily set the first nav item as active
34
- firstNavItem.setAttribute("aria-selected", "true")
47
+ firstNavItem.setAttribute("aria-selected", "true");
35
48
 
36
49
  // Test if navItems are hyperlinks
37
50
  if (firstNavItem.tagName.toLowerCase() === "a") {
38
51
  // Find index of element with class "router-link-active"
39
- activeIndex.value = navItems.value.findIndex((el) => el.classList.contains("router-link-active"))
52
+ activeIndex.value = navItems.value.findIndex((el) => el.classList.contains("router-link-active"));
40
53
  }
41
- // else {
42
- // Set actve tab
43
- // }
44
54
 
45
- const activeElement = navItems.value[activeIndex.value]
46
- if (!activeElement) return
55
+ const activeElement = navItems.value[activeIndex.value];
56
+ if (!activeElement) return;
47
57
 
48
- currentActiveTab.value = activeElement
49
- currentHoveredTab.value = activeElement
58
+ currentActiveTab.value = activeElement;
59
+ currentHoveredTab.value = activeElement;
50
60
 
51
- previousActiveTab.value = activeElement
52
- previousHoveredTab.value = activeElement
61
+ previousActiveTab.value = activeElement;
62
+ previousHoveredTab.value = activeElement;
53
63
 
54
- addNavDecorators()
55
- setActiveTabContent()
56
- }
64
+ setRovingTabindex();
65
+ addNavDecorators();
66
+ setActiveTabContent();
67
+ };
57
68
 
58
69
  const addNavDecorators = () => {
59
- const elementClasses = ["nav__active-indicator", "nav__active", "nav__hovered"]
60
- if (tabsNavRef.value) {
61
- for (let i = 0; i < 3; i++) {
62
- const div = document.createElement("div")
63
- const className = elementClasses[i]
64
- if (className) {
65
- div.classList.add(className)
66
- }
67
- tabsNavRef.value.appendChild(div)
68
- }
70
+ if (!tabsNavRef.value) return;
71
+
72
+ if (options.trackIndicator) {
73
+ tabsNavRef.value.appendChild(
74
+ Object.assign(document.createElement("div"), { className: "nav__active-indicator" })
75
+ );
69
76
  }
70
- }
77
+ if (options.trackActive) {
78
+ tabsNavRef.value.appendChild(Object.assign(document.createElement("div"), { className: "nav__active" }));
79
+ }
80
+ if (options.trackHover) {
81
+ tabsNavRef.value.appendChild(Object.assign(document.createElement("div"), { className: "nav__hovered" }));
82
+ }
83
+ };
84
+
85
+ const setRovingTabindex = () => {
86
+ navItems.value?.forEach((tab) => {
87
+ tab.setAttribute("tabindex", tab === currentActiveTab.value ? "0" : "-1");
88
+ });
89
+ };
71
90
 
72
91
  const navItemHovered = (event: Event) => {
73
- const target = event.target as HTMLElement
74
- const newTabPosition = currentHoveredTab.value ? currentHoveredTab.value.compareDocumentPosition(target) : 0
92
+ if (!options.trackHover) return;
93
+
94
+ const target = event.target as HTMLElement;
95
+ const newTabPosition = currentHoveredTab.value ? currentHoveredTab.value.compareDocumentPosition(target) : 0;
75
96
 
76
97
  if (newTabPosition !== 0) {
77
- previousHoveredTab.value = currentHoveredTab.value
78
- currentHoveredTab.value = target
79
- moveHoveredIndicator()
98
+ previousHoveredTab.value = currentHoveredTab.value;
99
+ currentHoveredTab.value = target;
100
+ moveHoveredIndicator();
80
101
  }
81
- }
102
+ };
82
103
 
83
104
  const resetHoverToActivePosition = () => {
84
- previousHoveredTab.value = currentHoveredTab.value
85
- currentHoveredTab.value = currentActiveTab.value
86
- moveHoveredIndicator()
87
- }
105
+ if (!options.trackHover) return;
88
106
 
89
- const navItemClicked = (event: Event) => {
90
- const target = event.target as HTMLElement
107
+ previousHoveredTab.value = currentHoveredTab.value;
108
+ currentHoveredTab.value = currentActiveTab.value;
109
+ moveHoveredIndicator();
110
+ };
91
111
 
92
- previousActiveTab.value = currentActiveTab.value || null
93
- currentActiveTab.value = target
112
+ const activateTab = (target: HTMLElement) => {
113
+ if (target === currentActiveTab.value) return;
114
+
115
+ previousActiveTab.value = currentActiveTab.value || null;
116
+ currentActiveTab.value = target;
94
117
 
95
118
  navItems.value?.forEach((tab) => {
96
- tab.setAttribute("aria-selected", currentActiveTab.value === tab ? "true" : "false")
97
- })
98
-
99
- moveActiveIndicator()
100
- setActiveTabContent()
101
- }
102
-
103
- const handleTransitioningClass = (transitionDuration: number = 200) => {
104
- if (previousHoveredTab.value && currentHoveredTab.value && navItems.value) {
105
- const newTabPosition = previousHoveredTab.value.compareDocumentPosition(currentHoveredTab.value)
106
- const navItemsArray = navItems.value
107
-
108
- const timeout = Math.floor(
109
- transitionDuration /
110
- Math.abs(navItemsArray.indexOf(currentHoveredTab.value) - navItemsArray.indexOf(previousHoveredTab.value))
111
- )
112
-
113
- if (newTabPosition === 4) {
114
- for (
115
- let i = navItemsArray.indexOf(previousHoveredTab.value);
116
- i < navItemsArray.indexOf(currentHoveredTab.value);
117
- i++
118
- ) {
119
- const element = navItemsArray[i]
120
- if (element) {
121
- element.classList.add("transitioning")
122
- if (
123
- i >= navItemsArray.indexOf(previousHoveredTab.value) &&
124
- i < navItemsArray.indexOf(currentHoveredTab.value)
125
- ) {
126
- setTimeout(() => {
127
- if (element) {
128
- element.classList.remove("transitioning")
129
- }
130
- // }, timeout * (i - navItemsArray.indexOf(previousHoveredTab.value) - 1));
131
- }, duration * 1.5)
132
- }
133
- }
134
- }
135
- } else {
136
- for (
137
- let i = navItemsArray.indexOf(previousHoveredTab.value);
138
- i > navItemsArray.indexOf(currentHoveredTab.value);
139
- i--
140
- ) {
141
- const element = navItemsArray[i]
142
- if (element) {
143
- element.classList.add("transitioning")
144
- if (
145
- i <= navItemsArray.indexOf(previousHoveredTab.value) &&
146
- i > navItemsArray.indexOf(currentHoveredTab.value)
147
- ) {
148
- setTimeout(() => {
149
- if (element) {
150
- element.classList.remove("transitioning")
151
- }
152
- // }, timeout * (i - navItemsArray.indexOf(previousHoveredTab.value) - 1));
153
- }, duration * 1.5)
154
- }
155
- }
156
- }
157
- }
119
+ tab.setAttribute("aria-selected", currentActiveTab.value === tab ? "true" : "false");
120
+ });
121
+
122
+ setRovingTabindex();
123
+ moveActiveIndicator();
124
+ setActiveTabContent();
125
+ };
126
+
127
+ const navItemClicked = (event: Event) => {
128
+ activateTab(event.target as HTMLElement);
129
+ };
130
+
131
+ // WAI-ARIA Tabs pattern: arrow keys move focus and activate (automatic activation);
132
+ // Home/End jump to the first/last tab.
133
+ const navItemKeydown = (event: KeyboardEvent) => {
134
+ if (!navItems.value || navItems.value.length === 0) return;
135
+
136
+ const previousKey = axis === "y" ? "ArrowUp" : "ArrowLeft";
137
+ const nextKey = axis === "y" ? "ArrowDown" : "ArrowRight";
138
+
139
+ let targetIndex: number | null = null;
140
+ const currentIndex = currentActiveTab.value ? navItems.value.indexOf(currentActiveTab.value) : 0;
141
+
142
+ if (event.key === nextKey) {
143
+ targetIndex = (currentIndex + 1) % navItems.value.length;
144
+ } else if (event.key === previousKey) {
145
+ targetIndex = (currentIndex - 1 + navItems.value.length) % navItems.value.length;
146
+ } else if (event.key === "Home") {
147
+ targetIndex = 0;
148
+ } else if (event.key === "End") {
149
+ targetIndex = navItems.value.length - 1;
158
150
  }
159
- }
151
+
152
+ if (targetIndex === null) return;
153
+
154
+ event.preventDefault();
155
+ const targetTab = navItems.value[targetIndex];
156
+ if (!targetTab) return;
157
+
158
+ targetTab.focus();
159
+ activateTab(targetTab);
160
+ };
160
161
 
161
162
  const setFinalHoveredPositions = (resized: boolean = false) => {
162
- const setDuration = resized ? 0 : duration
163
- // const tabsNavRefYPosition = tabsNavRef.value?.getBoundingClientRect().top || 0;
164
- const newTabWidth =
165
- currentHoveredTab.value && tabsNavRef.value
166
- ? currentHoveredTab.value.offsetWidth / tabsNavRef.value.offsetWidth
167
- : 0
168
- tabsNavRef.value?.style.setProperty("--_transition-duration", setDuration + "ms")
169
- tabsNavRef.value?.style.setProperty("--_x-hovered", currentHoveredTab.value?.offsetLeft + "px")
170
-
171
- tabsNavRef.value?.style.setProperty("--_width-hovered", newTabWidth?.toString())
172
- tabsNavRef.value?.style.setProperty("--_y-hovered", currentHoveredTab.value?.offsetTop + "px")
173
- tabsNavRef.value?.style.setProperty("--_y-height", currentHoveredTab.value?.offsetHeight + "px")
174
- tabsNavRef.value?.style.setProperty("--_y-width", currentHoveredTab.value?.offsetWidth + "px")
175
- }
163
+ if (!tabsNavRef.value || !currentHoveredTab.value) return;
164
+
165
+ // Batch every layout read before any style write below, so the browser doesn't need to
166
+ // perform a forced synchronous reflow between each read/write pair.
167
+ const { offsetLeft, offsetTop, offsetWidth, offsetHeight } = currentHoveredTab.value;
168
+ const { offsetWidth: navWidth } = tabsNavRef.value;
169
+ const setDuration = resized ? 0 : duration;
170
+ const newTabWidth = navWidth ? offsetWidth / navWidth : 0;
171
+
172
+ const nav = tabsNavRef.value;
173
+ nav.style.setProperty("--_transition-duration", setDuration + "ms");
174
+ nav.style.setProperty("--_x-hovered", offsetLeft + "px");
175
+ nav.style.setProperty("--_width-hovered", newTabWidth.toString());
176
+ nav.style.setProperty("--_y-hovered", offsetTop + "px");
177
+ nav.style.setProperty("--_y-height", offsetHeight + "px");
178
+ nav.style.setProperty("--_y-width", offsetWidth + "px");
179
+ };
176
180
 
177
181
  const setFinalActivePositions = (resized: boolean = false) => {
178
- const setDuration = resized ? 0 : duration
179
- const newTabWidth =
180
- currentActiveTab.value && tabsNavRef.value ? currentActiveTab.value.offsetWidth / tabsNavRef.value.offsetWidth : 0
181
- tabsNavRef.value?.style.setProperty("--_transition-duration", setDuration + "ms")
182
- tabsNavRef.value?.style.setProperty("--_x-active", currentActiveTab.value?.offsetLeft + "px")
183
- tabsNavRef.value?.style.setProperty("--_width-active", newTabWidth?.toString())
184
- tabsNavRef.value?.style.setProperty("--_y-active", currentActiveTab.value?.offsetTop + "px")
185
- tabsNavRef.value?.style.setProperty("--_y-height", currentActiveTab.value?.offsetHeight + "px")
186
- tabsNavRef.value?.style.setProperty("--_y-width", currentActiveTab.value?.offsetWidth + "px")
187
- }
182
+ if (!tabsNavRef.value || !currentActiveTab.value) return;
183
+
184
+ const { offsetLeft, offsetTop, offsetWidth, offsetHeight } = currentActiveTab.value;
185
+ const { offsetWidth: navWidth } = tabsNavRef.value;
186
+ const setDuration = resized ? 0 : duration;
187
+ const newTabWidth = navWidth ? offsetWidth / navWidth : 0;
188
+
189
+ const nav = tabsNavRef.value;
190
+ nav.style.setProperty("--_transition-duration", setDuration + "ms");
191
+ nav.style.setProperty("--_x-active", offsetLeft + "px");
192
+ nav.style.setProperty("--_width-active", newTabWidth.toString());
193
+ nav.style.setProperty("--_y-active", offsetTop + "px");
194
+ nav.style.setProperty("--_y-height", offsetHeight + "px");
195
+ nav.style.setProperty("--_y-width", offsetWidth + "px");
196
+ };
188
197
 
189
198
  const moveActiveIndicator = () => {
190
- if (!tabsNavRef.value || !currentActiveTab.value) return
199
+ if (!tabsNavRef.value || !currentActiveTab.value) return;
200
+ if (!options.trackActive && !options.trackIndicator) return;
201
+
202
+ const nav = tabsNavRef.value;
203
+ const current = currentActiveTab.value;
204
+ const previous = previousActiveTab.value;
191
205
 
192
- tabsNavRef.value.style.setProperty("--_transition-duration", duration + "ms")
206
+ // Batch reads first.
207
+ const newTabPosition = previous ? previous.compareDocumentPosition(current) : 0;
208
+ const currentLeft = current.offsetLeft;
209
+ const currentWidth = current.offsetWidth;
210
+ const previousLeft = previous?.offsetLeft ?? 0;
211
+ const previousWidth = previous?.offsetWidth ?? 0;
212
+ const navWidth = nav.offsetWidth;
193
213
 
194
- const newTabPosition =
195
- previousActiveTab.value && currentActiveTab.value
196
- ? previousActiveTab.value.compareDocumentPosition(currentActiveTab.value)
197
- : 0
198
- let transitionWidth
214
+ let transitionWidth: number;
215
+ let xActive: string | null = null;
199
216
 
200
217
  if (newTabPosition === 4) {
201
- transitionWidth =
202
- currentActiveTab.value && previousActiveTab.value
203
- ? currentActiveTab.value.offsetLeft + currentActiveTab.value.offsetWidth - previousActiveTab.value.offsetLeft
204
- : 0
218
+ transitionWidth = previous ? currentLeft + currentWidth - previousLeft : 0;
205
219
  } else {
206
- transitionWidth =
207
- previousActiveTab.value && currentActiveTab.value
208
- ? previousActiveTab.value.offsetLeft + previousActiveTab.value.offsetWidth - currentActiveTab.value.offsetLeft
209
- : 0
210
- tabsNavRef.value.style.setProperty(
211
- "--_x-active",
212
- currentActiveTab.value ? currentActiveTab.value.offsetLeft + "px" : "0"
213
- )
220
+ transitionWidth = previous ? previousLeft + previousWidth - currentLeft : 0;
221
+ xActive = currentLeft + "px";
214
222
  }
215
223
 
216
- tabsNavRef.value.style.setProperty("--_width-active", String(transitionWidth / tabsNavRef.value.offsetWidth))
217
-
218
- handleTransitioningClass(duration)
224
+ // Then batch writes.
225
+ nav.style.setProperty("--_transition-duration", duration + "ms");
226
+ if (xActive !== null) {
227
+ nav.style.setProperty("--_x-active", xActive);
228
+ }
229
+ nav.style.setProperty("--_width-active", String(transitionWidth / navWidth));
219
230
 
220
- setTimeout(
231
+ if (activeSettleTimeoutId) clearTimeout(activeSettleTimeoutId);
232
+ activeSettleTimeoutId = setTimeout(
221
233
  () => {
222
- setFinalActivePositions()
234
+ setFinalActivePositions();
223
235
  },
224
236
  Math.floor(duration + 20)
225
- )
226
- }
237
+ );
238
+ };
227
239
 
228
240
  const moveHoveredIndicator = () => {
229
- if (!tabsNavRef.value || !currentHoveredTab.value) return
241
+ if (!tabsNavRef.value || !currentHoveredTab.value) return;
242
+
243
+ const nav = tabsNavRef.value;
244
+ const current = currentHoveredTab.value;
245
+ const previous = previousHoveredTab.value;
230
246
 
231
- tabsNavRef.value.style.setProperty("--_transition-duration", duration + "ms")
247
+ // Batch reads first.
248
+ const newTabPosition = previous ? previous.compareDocumentPosition(current) : 0;
249
+ const currentLeft = current.offsetLeft;
250
+ const currentWidth = current.offsetWidth;
251
+ const previousLeft = previous?.offsetLeft ?? 0;
252
+ const previousWidth = previous?.offsetWidth ?? 0;
253
+ const navWidth = nav.offsetWidth;
232
254
 
233
- const newTabPosition =
234
- previousHoveredTab.value && currentHoveredTab.value
235
- ? previousHoveredTab.value.compareDocumentPosition(currentHoveredTab.value)
236
- : 0
237
- let transitionWidth
255
+ let transitionWidth: number;
256
+ let xHovered: string | null = null;
238
257
 
239
258
  if (newTabPosition === 4) {
240
- transitionWidth =
241
- currentHoveredTab.value && previousHoveredTab.value
242
- ? currentHoveredTab.value.offsetLeft +
243
- currentHoveredTab.value.offsetWidth -
244
- previousHoveredTab.value.offsetLeft
245
- : 0
259
+ transitionWidth = previous ? currentLeft + currentWidth - previousLeft : 0;
246
260
  } else {
247
- transitionWidth =
248
- previousHoveredTab.value && currentHoveredTab.value
249
- ? previousHoveredTab.value.offsetLeft +
250
- previousHoveredTab.value.offsetWidth -
251
- currentHoveredTab.value.offsetLeft
252
- : 0
253
- tabsNavRef.value.style.setProperty(
254
- "--_x-hovered",
255
- currentHoveredTab.value ? currentHoveredTab.value.offsetLeft + "px" : "0"
256
- )
261
+ transitionWidth = previous ? previousLeft + previousWidth - currentLeft : 0;
262
+ xHovered = currentLeft + "px";
257
263
  }
258
264
 
259
- tabsNavRef.value.style.setProperty("--_width-hovered", String(transitionWidth / tabsNavRef.value.offsetWidth))
260
-
261
- handleTransitioningClass(duration)
265
+ // Then batch writes.
266
+ nav.style.setProperty("--_transition-duration", duration + "ms");
267
+ if (xHovered !== null) {
268
+ nav.style.setProperty("--_x-hovered", xHovered);
269
+ }
270
+ nav.style.setProperty("--_width-hovered", String(transitionWidth / navWidth));
262
271
 
263
- setTimeout(
272
+ if (hoveredSettleTimeoutId) clearTimeout(hoveredSettleTimeoutId);
273
+ hoveredSettleTimeoutId = setTimeout(
264
274
  () => {
265
- setFinalHoveredPositions()
275
+ setFinalHoveredPositions();
266
276
  },
267
277
  Math.floor(duration + 20)
268
- )
269
- }
278
+ );
279
+ };
270
280
 
271
281
  const setActiveTabContent = () => {
272
- const activeIndex = navItems.value?.findIndex((el) => el === currentActiveTab.value)
282
+ const activeIndex = navItems.value?.findIndex((el) => el === currentActiveTab.value);
273
283
  tabsContentRefs.value?.forEach((tabContent: HTMLElement, index: number) => {
274
- tabContent.style.display = activeIndex === index ? "block" : "none"
275
- })
276
- }
284
+ const isActive = activeIndex === index;
285
+ tabContent.style.display = isActive ? "block" : "none";
286
+ tabContent.setAttribute("aria-hidden", isActive ? "false" : "true");
287
+ });
288
+ };
277
289
 
278
290
  useResizeObserver(tabsNavRef, () => {
279
- setFinalActivePositions(true)
280
- setFinalHoveredPositions(true)
281
- })
291
+ setFinalActivePositions(true);
292
+ setFinalHoveredPositions(true);
293
+ });
294
+
295
+ onUnmounted(() => {
296
+ if (activeSettleTimeoutId) clearTimeout(activeSettleTimeoutId);
297
+ if (hoveredSettleTimeoutId) clearTimeout(hoveredSettleTimeoutId);
298
+ });
282
299
 
283
300
  return {
284
301
  initNavDecorators,
285
302
  navItemClicked,
286
303
  navItemHovered,
304
+ navItemKeydown,
287
305
  resetHoverToActivePosition,
288
- }
289
- }
306
+ };
307
+ };
290
308
 
291
- export default useTabs
309
+ export default useTabs;
@@ -18,3 +18,5 @@ export * from "./breadcrumb"
18
18
  export * from "./cookie-consent-banner.d"
19
19
  export * from "./display-tooltip-defined.d"
20
20
  export * from "./marquee-scroller.d"
21
+ export * from "./rotating-carousel-image.d"
22
+ export * from "./skip-links.d"
@@ -0,0 +1,4 @@
1
+ export interface CarouselImageData {
2
+ src: string
3
+ alt: string
4
+ }
@@ -0,0 +1,4 @@
1
+ export interface SkipLink {
2
+ href: string
3
+ label: string
4
+ }
@@ -16,7 +16,7 @@ export type CheckboxStyle = "check" | "cross";
16
16
  export type RadioAppearance = null | "with-decorator";
17
17
  export type OptionsLayout = "block" | "inline" | "equal-widths";
18
18
  export type LabelWeight = "normal" | "semi-bold" | "bold";
19
- export type InputTypesText = "text" | "email" | "password" | "number" | "tel" | "url";
19
+ export type InputTypesText = "text" | "email" | "password" | "number" | "tel" | "url" | "date";
20
20
  export type InputMode = "text" | "email" | "tel" | "url" | "search" | "numeric" | "none" | "decimal";
21
21
 
22
22
  // Weight types for form components
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.4.6",
4
+ "version": "9.4.8",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",
@@ -1,11 +0,0 @@
1
- // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
-
3
- exports[`AutoGrid > renders correct HTML structure (all props and slots set) 1`] = `
4
- "<section class="auto-grid custom-class" aria-labelledby="v-0-0">
5
- <div>Item 1</div>
6
- <div>Item 2</div>
7
- <div>Item 3</div>
8
- </section>"
9
- `;
10
-
11
- exports[`AutoGrid > renders correct HTML structure (default props) 1`] = `"<div class="auto-grid"></div>"`;
@@ -1,68 +0,0 @@
1
- <template>
2
- <div
3
- class="masonry-grid-wrapper"
4
- :class="[elementClasses]"
5
- :style="`--_masonry-grid-gap: ${gap}${unit}; --_item-min-width: ${itemMinWidth}px`"
6
- >
7
- <template v-for="item in gridData" :key="item.id">
8
- <div class="masonry-grid-item">
9
- <slot :name="item.id"></slot>
10
- </div>
11
- </template>
12
- </div>
13
- </template>
14
-
15
- <script setup lang="ts">
16
- const props = defineProps({
17
- gridData: {
18
- type: Object,
19
- default: () => ({}),
20
- },
21
- itemMinWidth: {
22
- type: Number,
23
- default: 300,
24
- },
25
- gap: {
26
- type: Number,
27
- default: 1.2,
28
- },
29
- unit: {
30
- type: String,
31
- default: "rem",
32
- },
33
- styleClassPassthrough: {
34
- type: [String, Array] as PropType<string | string[]>,
35
- default: () => [],
36
- },
37
- });
38
-
39
- const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
40
-
41
- const gridData = toRef(() => props.gridData);
42
-
43
- watch(
44
- () => props.styleClassPassthrough,
45
- () => {
46
- resetElementClasses(props.styleClassPassthrough);
47
- }
48
- );
49
- </script>
50
-
51
- <style lang="css">
52
- @layer components {
53
- .masonry-grid-wrapper {
54
- --_border-color: light-dark(hsl(0, 29%, 3%), hsl(0, 0%, 92%));
55
- --_color: light-dark(hsl(0, 29%, 3%), hsl(0, 0%, 92%));
56
-
57
- columns: auto var(--_item-min-width);
58
- column-gap: var(--_masonry-grid-gap);
59
-
60
- .masonry-grid-item {
61
- break-inside: avoid;
62
- outline: 0.1rem solid var(--_border-color);
63
- padding: 1.2rem;
64
- margin-block-end: var(--_masonry-grid-gap);
65
- }
66
- }
67
- }
68
- </style>