srcdev-nuxt-components 9.6.0 → 9.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/component-compliance-checklist.md +8 -2
- package/.claude/skills/component-dynamic-slots.md +3 -3
- package/.claude/skills/component-export-types.md +10 -16
- package/.claude/skills/component-naming.md +1 -1
- package/.claude/skills/components/alert-content.md +9 -1
- package/.claude/skills/components/container-glow.md +21 -8
- package/.claude/skills/components/display-avatar.md +13 -7
- package/.claude/skills/components/display-chip.md +37 -24
- package/.claude/skills/components/display-pill.md +58 -10
- package/.claude/skills/components/display-prompt.md +8 -5
- package/.claude/skills/components/display-toast.md +21 -4
- package/.claude/skills/components/glowing-border.md +20 -2
- package/.claude/skills/components/responsive-header.md +11 -1
- package/.claude/skills/components/select-menu.md +54 -5
- package/.claude/skills/components/site-header.md +2 -2
- package/.claude/skills/components/tabbed-content.md +300 -0
- package/.claude/skills/composable-anchor-scroll.md +16 -15
- package/.claude/skills/index.md +4 -4
- package/.claude/skills/storybook-add-story.md +53 -0
- package/.claude/skills/theming-status-tokens.md +60 -0
- package/.vscode/srcdev-component-display-avatar.code-snippets +2 -1
- package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
- package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +6 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
- package/.vscode/{srcdev-component-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
- package/app/assets/styles/setup/03.theming/_status.css +24 -0
- package/app/assets/styles/setup/03.theming/index.css +1 -0
- package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
- package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
- package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
- package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
- package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
- package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
- package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
- package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
- package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +75 -9
- package/app/components/01.atoms/display-pill/DisplayPill.vue +57 -24
- package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
- package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
- package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
- package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
- package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
- package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
- package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
- package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
- package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
- package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
- package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
- package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
- package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +7 -5
- package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +6 -2
- package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +24 -3
- package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +19 -0
- package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +2 -1
- package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +52 -19
- package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
- package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
- package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
- package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
- package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
- package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
- package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
- package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
- package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
- package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
- package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
- package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
- package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
- package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
- package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
- package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
- package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
- package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +38 -11
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +27 -12
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +35 -20
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +1 -1
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +1 -1
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +1 -1
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +3 -3
- package/app/composables/useTabs.ts +148 -206
- package/app/types/components/display-chip.d.ts +8 -4
- package/app/types/components/display-toast.d.ts +2 -0
- package/app/types/components/index.ts +0 -1
- package/app/types/components/select-menu.d.ts +2 -0
- package/app/utils/pausableTimeout.ts +41 -0
- package/nuxt.config.ts +1 -1
- package/package.json +1 -1
- package/.claude/skills/components/tab-navigation.md +0 -288
- package/.claude/skills/components/tabs-core.md +0 -187
- package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
- package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
- package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
- package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
- package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
- package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -191
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -550
- package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -259
- package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -379
- package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
- package/app/composables/useNavCollapse.ts +0 -86
- package/app/types/components/nav-item.d.ts +0 -11
|
@@ -1,304 +1,246 @@
|
|
|
1
1
|
import { useResizeObserver } from "@vueuse/core";
|
|
2
|
-
|
|
3
|
-
interface UseTabsOptions {
|
|
4
|
-
trackHover: boolean;
|
|
5
|
-
trackActive: boolean;
|
|
6
|
-
trackIndicator: boolean;
|
|
7
|
-
}
|
|
2
|
+
import type { MaybeRefOrGetter } from "vue";
|
|
8
3
|
|
|
9
4
|
const useTabs = (
|
|
10
|
-
axis:
|
|
5
|
+
axis: MaybeRefOrGetter<"x" | "y">,
|
|
11
6
|
tabsNavRef: Ref<HTMLElement | null>,
|
|
12
7
|
tabsContentRefs: Ref<HTMLElement[] | null>,
|
|
13
|
-
duration: number
|
|
14
|
-
|
|
8
|
+
duration: MaybeRefOrGetter<number>,
|
|
9
|
+
trackHover: MaybeRefOrGetter<boolean> = true
|
|
15
10
|
) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
const previousActiveTab = ref<HTMLElement | null>(null);
|
|
11
|
+
// All triggers, and the ones not collapsed into an overflow menu (keyboard and roving tabindex use these).
|
|
12
|
+
const allItems = ref<HTMLElement[]>([]);
|
|
13
|
+
const navItems = ref<HTMLElement[]>([]);
|
|
14
|
+
// Plain refs, not useState: per-instance UI state that must not be shared between instances.
|
|
21
15
|
const currentActiveTab = ref<HTMLElement>();
|
|
22
|
-
|
|
23
|
-
const previousHoveredTab = ref<HTMLElement>();
|
|
24
16
|
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;
|
|
29
17
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
18
|
+
type IndicatorKind = "active" | "hovered";
|
|
19
|
+
// Leading edge (start along the axis) each indicator was last sent to, to tell the direction of a move.
|
|
20
|
+
const lastStart: Record<IndicatorKind, number | null> = { active: null, hovered: null };
|
|
21
|
+
let coverageFrameId: number | null = null;
|
|
22
|
+
let coverageUntil = 0;
|
|
34
23
|
|
|
35
|
-
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
24
|
+
const transitionMs = () => Math.max(0, Number(toValue(duration)) || 0);
|
|
38
25
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const activeIndex = ref(0);
|
|
45
|
-
|
|
46
|
-
// Temporarily set the first nav item as active
|
|
47
|
-
firstNavItem.setAttribute("aria-selected", "true");
|
|
26
|
+
const activeIndex = computed(() => {
|
|
27
|
+
const index = currentActiveTab.value?.dataset.tabIndex;
|
|
28
|
+
return index === undefined ? null : Number(index);
|
|
29
|
+
});
|
|
48
30
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
31
|
+
// A collapsed tab has no box of its own, so the indicators sit on the overflow menu's trigger instead.
|
|
32
|
+
const indicatorTarget = (element?: HTMLElement | null) =>
|
|
33
|
+
element?.hidden ? (tabsNavRef.value?.querySelector<HTMLElement>("[data-more-trigger]") ?? element) : element;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Re-reads the tabs from the DOM (after mount, or when the tab count/axis/indicators/overflow change),
|
|
37
|
+
* keeping the active tab when it still exists. `animate` retargets indicators that are mid-move
|
|
38
|
+
* instead of snapping them.
|
|
39
|
+
*/
|
|
40
|
+
const refreshTabs = (animate = false) => {
|
|
41
|
+
allItems.value = tabsNavRef.value
|
|
42
|
+
? Array.from(tabsNavRef.value.querySelectorAll<HTMLElement>("[data-nav-item]"))
|
|
43
|
+
: [];
|
|
44
|
+
navItems.value = allItems.value.filter((tab) => !tab.hidden);
|
|
54
45
|
|
|
55
|
-
const
|
|
56
|
-
|
|
46
|
+
const items = allItems.value;
|
|
47
|
+
const activeTab =
|
|
48
|
+
currentActiveTab.value && items.includes(currentActiveTab.value) ? currentActiveTab.value : items[0];
|
|
57
49
|
|
|
58
|
-
currentActiveTab.value =
|
|
59
|
-
currentHoveredTab.value =
|
|
50
|
+
currentActiveTab.value = activeTab;
|
|
51
|
+
currentHoveredTab.value = activeTab;
|
|
60
52
|
|
|
61
|
-
|
|
62
|
-
|
|
53
|
+
items.forEach((tab) => {
|
|
54
|
+
tab.setAttribute("aria-selected", tab === activeTab ? "true" : "false");
|
|
55
|
+
});
|
|
63
56
|
|
|
64
57
|
setRovingTabindex();
|
|
65
|
-
|
|
58
|
+
syncIndicators(animate);
|
|
66
59
|
setActiveTabContent();
|
|
67
60
|
};
|
|
68
61
|
|
|
69
|
-
|
|
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
|
-
);
|
|
76
|
-
}
|
|
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
|
-
|
|
62
|
+
// When the active tab is collapsed, the first visible tab keeps the tablist reachable with Tab.
|
|
85
63
|
const setRovingTabindex = () => {
|
|
86
|
-
navItems.value
|
|
87
|
-
|
|
64
|
+
const focusTarget = navItems.value.includes(currentActiveTab.value as HTMLElement)
|
|
65
|
+
? currentActiveTab.value
|
|
66
|
+
: navItems.value[0];
|
|
67
|
+
allItems.value.forEach((tab) => {
|
|
68
|
+
tab.setAttribute("tabindex", tab === focusTarget ? "0" : "-1");
|
|
88
69
|
});
|
|
89
70
|
};
|
|
90
71
|
|
|
91
72
|
const navItemHovered = (event: Event) => {
|
|
92
|
-
if (!
|
|
73
|
+
if (!toValue(trackHover)) return;
|
|
93
74
|
|
|
94
|
-
const target = event.
|
|
75
|
+
const target = event.currentTarget as HTMLElement;
|
|
95
76
|
const newTabPosition = currentHoveredTab.value ? currentHoveredTab.value.compareDocumentPosition(target) : 0;
|
|
96
77
|
|
|
97
78
|
if (newTabPosition !== 0) {
|
|
98
|
-
previousHoveredTab.value = currentHoveredTab.value;
|
|
99
79
|
currentHoveredTab.value = target;
|
|
100
|
-
|
|
80
|
+
placeIndicator("hovered", true);
|
|
101
81
|
}
|
|
102
82
|
};
|
|
103
83
|
|
|
104
84
|
const resetHoverToActivePosition = () => {
|
|
105
|
-
if (!
|
|
85
|
+
if (!toValue(trackHover)) return;
|
|
106
86
|
|
|
107
|
-
previousHoveredTab.value = currentHoveredTab.value;
|
|
108
87
|
currentHoveredTab.value = currentActiveTab.value;
|
|
109
|
-
|
|
88
|
+
placeIndicator("hovered", true);
|
|
110
89
|
};
|
|
111
90
|
|
|
112
91
|
const activateTab = (target: HTMLElement) => {
|
|
113
92
|
if (target === currentActiveTab.value) return;
|
|
114
93
|
|
|
115
|
-
previousActiveTab.value = currentActiveTab.value || null;
|
|
116
94
|
currentActiveTab.value = target;
|
|
117
95
|
|
|
118
|
-
|
|
96
|
+
allItems.value.forEach((tab) => {
|
|
119
97
|
tab.setAttribute("aria-selected", currentActiveTab.value === tab ? "true" : "false");
|
|
120
98
|
});
|
|
121
99
|
|
|
122
100
|
setRovingTabindex();
|
|
123
|
-
|
|
101
|
+
placeIndicator("active", true);
|
|
124
102
|
setActiveTabContent();
|
|
125
103
|
};
|
|
126
104
|
|
|
105
|
+
const activateTabByIndex = (index: number) => {
|
|
106
|
+
const target = allItems.value.find((tab) => Number(tab.dataset.tabIndex) === index);
|
|
107
|
+
if (target) activateTab(target);
|
|
108
|
+
};
|
|
109
|
+
|
|
127
110
|
const navItemClicked = (event: Event) => {
|
|
128
|
-
activateTab(event.
|
|
111
|
+
activateTab(event.currentTarget as HTMLElement);
|
|
129
112
|
};
|
|
130
113
|
|
|
131
114
|
// WAI-ARIA Tabs pattern: arrow keys move focus and activate (automatic activation);
|
|
132
115
|
// Home/End jump to the first/last tab.
|
|
133
116
|
const navItemKeydown = (event: KeyboardEvent) => {
|
|
134
|
-
|
|
117
|
+
const items = navItems.value;
|
|
118
|
+
if (items.length === 0) return;
|
|
135
119
|
|
|
136
|
-
const
|
|
137
|
-
const
|
|
120
|
+
const isVertical = toValue(axis) === "y";
|
|
121
|
+
const previousKey = isVertical ? "ArrowUp" : "ArrowLeft";
|
|
122
|
+
const nextKey = isVertical ? "ArrowDown" : "ArrowRight";
|
|
138
123
|
|
|
139
124
|
let targetIndex: number | null = null;
|
|
140
|
-
const currentIndex = currentActiveTab.value ?
|
|
125
|
+
const currentIndex = currentActiveTab.value ? items.indexOf(currentActiveTab.value) : 0;
|
|
141
126
|
|
|
142
127
|
if (event.key === nextKey) {
|
|
143
|
-
targetIndex = (currentIndex + 1) %
|
|
128
|
+
targetIndex = (currentIndex + 1) % items.length;
|
|
144
129
|
} else if (event.key === previousKey) {
|
|
145
|
-
targetIndex = (currentIndex - 1 +
|
|
130
|
+
targetIndex = (currentIndex - 1 + items.length) % items.length;
|
|
146
131
|
} else if (event.key === "Home") {
|
|
147
132
|
targetIndex = 0;
|
|
148
133
|
} else if (event.key === "End") {
|
|
149
|
-
targetIndex =
|
|
134
|
+
targetIndex = items.length - 1;
|
|
150
135
|
}
|
|
151
136
|
|
|
152
137
|
if (targetIndex === null) return;
|
|
153
138
|
|
|
154
139
|
event.preventDefault();
|
|
155
|
-
const targetTab =
|
|
140
|
+
const targetTab = items[targetIndex];
|
|
156
141
|
if (!targetTab) return;
|
|
157
142
|
|
|
158
143
|
targetTab.focus();
|
|
159
144
|
activateTab(targetTab);
|
|
160
145
|
};
|
|
161
146
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
};
|
|
180
|
-
|
|
181
|
-
const setFinalActivePositions = (resized: boolean = false) => {
|
|
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
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Sends an indicator's two edges to its target tab, as insets from the bar's start and end edges
|
|
149
|
+
* along the axis. On a move, the edge in the direction of travel goes first and the trailing edge
|
|
150
|
+
* follows one duration later, so the indicator stretches across and then catches up. CSS retargets
|
|
151
|
+
* each edge from wherever it is, so a quick second move never pulls an edge backwards.
|
|
152
|
+
* `animate: false` (mount, resize, tab-count change) snaps both edges.
|
|
153
|
+
*/
|
|
154
|
+
const placeIndicator = (kind: IndicatorKind, animate: boolean) => {
|
|
189
155
|
const nav = tabsNavRef.value;
|
|
190
|
-
|
|
191
|
-
nav
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
156
|
+
const target = indicatorTarget(kind === "active" ? currentActiveTab.value : currentHoveredTab.value);
|
|
157
|
+
if (!nav || !target) return;
|
|
158
|
+
|
|
159
|
+
const vertical = toValue(axis) === "y";
|
|
160
|
+
const start = vertical ? target.offsetTop : target.offsetLeft;
|
|
161
|
+
const size = vertical ? target.offsetHeight : target.offsetWidth;
|
|
162
|
+
const end = (vertical ? nav.clientHeight : nav.clientWidth) - start - size;
|
|
163
|
+
|
|
164
|
+
const previous = lastStart[kind];
|
|
165
|
+
const ms = animate && previous !== null ? transitionMs() : 0;
|
|
166
|
+
const forward = previous !== null && start > previous;
|
|
167
|
+
const backward = previous !== null && start < previous;
|
|
168
|
+
lastStart[kind] = start;
|
|
169
|
+
|
|
170
|
+
nav.style.setProperty(`--_${kind}-duration`, ms + "ms");
|
|
171
|
+
nav.style.setProperty(`--_${kind}-start-delay`, forward ? ms + "ms" : "0ms");
|
|
172
|
+
nav.style.setProperty(`--_${kind}-end-delay`, backward ? ms + "ms" : "0ms");
|
|
173
|
+
nav.style.setProperty(`--_${kind}-start`, start + "px");
|
|
174
|
+
nav.style.setProperty(`--_${kind}-end`, end + "px");
|
|
175
|
+
|
|
176
|
+
if (kind === "active") trackCoverage(ms * 2);
|
|
196
177
|
};
|
|
197
178
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
179
|
+
// Marks each visible tab (and the overflow trigger) that the active indicator covers by at least half,
|
|
180
|
+
// so its text can take the active text colour while the indicator slides under it.
|
|
181
|
+
const updateCoverage = () => {
|
|
202
182
|
const nav = tabsNavRef.value;
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const navWidth = nav.offsetWidth;
|
|
213
|
-
|
|
214
|
-
let transitionWidth: number;
|
|
215
|
-
let xActive: string | null = null;
|
|
216
|
-
|
|
217
|
-
if (newTabPosition === 4) {
|
|
218
|
-
transitionWidth = previous ? currentLeft + currentWidth - previousLeft : 0;
|
|
219
|
-
} else {
|
|
220
|
-
transitionWidth = previous ? previousLeft + previousWidth - currentLeft : 0;
|
|
221
|
-
xActive = currentLeft + "px";
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Then batch writes.
|
|
225
|
-
nav.style.setProperty("--_transition-duration", duration + "ms");
|
|
226
|
-
if (xActive !== null) {
|
|
227
|
-
nav.style.setProperty("--_x-active", xActive);
|
|
183
|
+
if (!nav) return;
|
|
184
|
+
const targets = [...navItems.value];
|
|
185
|
+
const more = nav.querySelector<HTMLElement>("[data-more-trigger]");
|
|
186
|
+
if (more) targets.push(more);
|
|
187
|
+
|
|
188
|
+
const indicator = nav.querySelector<HTMLElement>("[data-active-indicator]");
|
|
189
|
+
if (!indicator) {
|
|
190
|
+
targets.forEach((target) => target.removeAttribute("data-under-active"));
|
|
191
|
+
return;
|
|
228
192
|
}
|
|
229
|
-
nav.style.setProperty("--_width-active", String(transitionWidth / navWidth));
|
|
230
|
-
|
|
231
|
-
if (activeSettleTimeoutId) clearTimeout(activeSettleTimeoutId);
|
|
232
|
-
activeSettleTimeoutId = setTimeout(
|
|
233
|
-
() => {
|
|
234
|
-
setFinalActivePositions();
|
|
235
|
-
},
|
|
236
|
-
Math.floor(duration + 20)
|
|
237
|
-
);
|
|
238
|
-
};
|
|
239
193
|
|
|
240
|
-
|
|
241
|
-
|
|
194
|
+
const vertical = toValue(axis) === "y";
|
|
195
|
+
const box = indicator.getBoundingClientRect();
|
|
196
|
+
const [boxStart, boxEnd] = vertical ? [box.top, box.bottom] : [box.left, box.right];
|
|
242
197
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const currentWidth = current.offsetWidth;
|
|
251
|
-
const previousLeft = previous?.offsetLeft ?? 0;
|
|
252
|
-
const previousWidth = previous?.offsetWidth ?? 0;
|
|
253
|
-
const navWidth = nav.offsetWidth;
|
|
254
|
-
|
|
255
|
-
let transitionWidth: number;
|
|
256
|
-
let xHovered: string | null = null;
|
|
257
|
-
|
|
258
|
-
if (newTabPosition === 4) {
|
|
259
|
-
transitionWidth = previous ? currentLeft + currentWidth - previousLeft : 0;
|
|
260
|
-
} else {
|
|
261
|
-
transitionWidth = previous ? previousLeft + previousWidth - currentLeft : 0;
|
|
262
|
-
xHovered = currentLeft + "px";
|
|
263
|
-
}
|
|
198
|
+
targets.forEach((target) => {
|
|
199
|
+
const rect = target.getBoundingClientRect();
|
|
200
|
+
const [start, end] = vertical ? [rect.top, rect.bottom] : [rect.left, rect.right];
|
|
201
|
+
const overlap = Math.min(boxEnd, end) - Math.max(boxStart, start);
|
|
202
|
+
target.toggleAttribute("data-under-active", end > start && overlap >= (end - start) / 2);
|
|
203
|
+
});
|
|
204
|
+
};
|
|
264
205
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
},
|
|
277
|
-
Math.floor(duration + 20)
|
|
278
|
-
);
|
|
206
|
+
// Re-checks coverage every frame while the indicator's transition runs. A new request only ever
|
|
207
|
+
// extends the window: a snap (e.g. a refresh) mid-move mustn't stop the checks before it lands.
|
|
208
|
+
const trackCoverage = (animationMs: number) => {
|
|
209
|
+
coverageUntil = Math.max(coverageUntil, performance.now() + animationMs + 50);
|
|
210
|
+
updateCoverage();
|
|
211
|
+
if (coverageFrameId !== null) return;
|
|
212
|
+
const step = () => {
|
|
213
|
+
updateCoverage();
|
|
214
|
+
coverageFrameId = performance.now() < coverageUntil ? requestAnimationFrame(step) : null;
|
|
215
|
+
};
|
|
216
|
+
coverageFrameId = requestAnimationFrame(step);
|
|
279
217
|
};
|
|
280
218
|
|
|
219
|
+
// Matched by data-tab-index, since a v-for ref array isn't guaranteed to stay in source order.
|
|
281
220
|
const setActiveTabContent = () => {
|
|
282
|
-
const activeIndex =
|
|
283
|
-
tabsContentRefs.value?.forEach((tabContent: HTMLElement
|
|
284
|
-
|
|
285
|
-
tabContent.style.display = isActive ? "block" : "none";
|
|
286
|
-
tabContent.setAttribute("aria-hidden", isActive ? "false" : "true");
|
|
221
|
+
const activeIndex = currentActiveTab.value?.dataset.tabIndex;
|
|
222
|
+
tabsContentRefs.value?.forEach((tabContent: HTMLElement) => {
|
|
223
|
+
tabContent.hidden = tabContent.dataset.tabIndex !== activeIndex;
|
|
287
224
|
});
|
|
288
225
|
};
|
|
289
226
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
227
|
+
/** Re-places both indicators onto their current targets, e.g. after the overflow trigger changes size. */
|
|
228
|
+
const syncIndicators = (animate = false) => {
|
|
229
|
+
placeIndicator("active", animate);
|
|
230
|
+
placeIndicator("hovered", animate);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
useResizeObserver(tabsNavRef, () => syncIndicators(false));
|
|
294
234
|
|
|
295
235
|
onUnmounted(() => {
|
|
296
|
-
if (
|
|
297
|
-
if (hoveredSettleTimeoutId) clearTimeout(hoveredSettleTimeoutId);
|
|
236
|
+
if (coverageFrameId !== null) cancelAnimationFrame(coverageFrameId);
|
|
298
237
|
});
|
|
299
238
|
|
|
300
239
|
return {
|
|
301
|
-
|
|
240
|
+
refreshTabs,
|
|
241
|
+
syncIndicators,
|
|
242
|
+
activeIndex,
|
|
243
|
+
activateTabByIndex,
|
|
302
244
|
navItemClicked,
|
|
303
245
|
navItemHovered,
|
|
304
246
|
navItemKeydown,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
export type DisplayChipStatus = "offline" | "online" | "idle" | "dnd"
|
|
2
|
+
|
|
1
3
|
export interface DisplayChipConfig {
|
|
2
|
-
size
|
|
3
|
-
maskWidth
|
|
4
|
-
offset
|
|
5
|
-
angle
|
|
4
|
+
size?: string
|
|
5
|
+
maskWidth?: string
|
|
6
|
+
offset?: string
|
|
7
|
+
angle?: string
|
|
6
8
|
icon?: string
|
|
7
9
|
label?: string
|
|
8
10
|
}
|
|
@@ -10,6 +12,8 @@ export interface DisplayChipConfig {
|
|
|
10
12
|
export interface DisplayChipProps {
|
|
11
13
|
tag?: "div" | "span"
|
|
12
14
|
shape?: "circle" | "square"
|
|
15
|
+
status?: DisplayChipStatus
|
|
16
|
+
statusLabel?: string
|
|
13
17
|
config?: DisplayChipConfig
|
|
14
18
|
styleClassPassthrough?: string | string[]
|
|
15
19
|
}
|
|
@@ -26,6 +26,8 @@ export interface DisplayToastContentConfig {
|
|
|
26
26
|
title?: string
|
|
27
27
|
description?: string
|
|
28
28
|
customIcon?: string
|
|
29
|
+
/** Screen-reader text for the dismiss button (non-auto-dismiss toasts). Defaults to "Close". */
|
|
30
|
+
dismissLabel?: string
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export interface DisplayToastConfig {
|
|
@@ -11,7 +11,6 @@ export * from "./qr-code.d"
|
|
|
11
11
|
export * from "./alert-mask-core.d"
|
|
12
12
|
export * from "./container-glow.d"
|
|
13
13
|
export * from "./hero-text"
|
|
14
|
-
export * from "./nav-item.d"
|
|
15
14
|
export * from "./social-icons-list.d"
|
|
16
15
|
export * from "./content-docs.d"
|
|
17
16
|
export * from "./breadcrumb"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function createPausableTimeout(callback: () => void, ms: number) {
|
|
2
|
+
let remaining = ms;
|
|
3
|
+
let startedAt = 0;
|
|
4
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
5
|
+
|
|
6
|
+
const resume = () => {
|
|
7
|
+
if (timeoutId !== null || remaining <= 0) return;
|
|
8
|
+
startedAt = Date.now();
|
|
9
|
+
timeoutId = setTimeout(() => {
|
|
10
|
+
timeoutId = null;
|
|
11
|
+
remaining = 0;
|
|
12
|
+
callback();
|
|
13
|
+
}, remaining);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const pause = () => {
|
|
17
|
+
if (timeoutId === null) return;
|
|
18
|
+
clearTimeout(timeoutId);
|
|
19
|
+
timeoutId = null;
|
|
20
|
+
remaining -= Date.now() - startedAt;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const clear = () => {
|
|
24
|
+
if (timeoutId !== null) clearTimeout(timeoutId);
|
|
25
|
+
timeoutId = null;
|
|
26
|
+
remaining = 0;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
resume();
|
|
30
|
+
return { pause, resume, clear };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Programmatic focus on the toast itself only pauses for keyboard users; focus on anything inside always does.
|
|
34
|
+
export function focusPausesAutoDismiss(root: HTMLElement, target: EventTarget | null): boolean {
|
|
35
|
+
if (target !== root) return true;
|
|
36
|
+
try {
|
|
37
|
+
return root.matches(":focus-visible");
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
package/nuxt.config.ts
CHANGED
|
@@ -150,7 +150,7 @@ export default defineNuxtConfig({
|
|
|
150
150
|
// "esbuild". lightningcss has an open bug where it drops the unprefixed `backdrop-filter`
|
|
151
151
|
// once autoprefixer adds `-webkit-backdrop-filter` alongside it, breaking glass/blur effects
|
|
152
152
|
// (see https://github.com/vitejs/vite/issues/22649) — this repo relies on backdrop-filter in
|
|
153
|
-
// several components (GlassPanel,
|
|
153
|
+
// several components (GlassPanel, DisplayDialog,
|
|
154
154
|
// MagneticNavigation). Pin back to esbuild until upstream fixes it.
|
|
155
155
|
build: {
|
|
156
156
|
cssMinify: "esbuild",
|