srcdev-nuxt-components 9.3.0 → 9.3.2
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/hooks/check-component-update.sh +66 -0
- package/.claude/settings.json +1 -1
- package/.claude/skills/component-local-style-override.md +45 -0
- package/.claude/skills/components/accordian-core.md +26 -5
- package/.claude/skills/components/expanding-panel-classic.md +221 -0
- package/.claude/skills/components/expanding-panel.md +8 -8
- package/.claude/skills/components/navigation-items.md +51 -0
- package/.claude/skills/components/responsive-header.md +161 -0
- package/.claude/skills/components/site-header.md +98 -0
- package/.claude/skills/index.md +6 -1
- package/.claude/skills/page-transitions.md +116 -0
- package/.claude/skills/theming-form-geometry-tokens.md +12 -0
- package/.claude/skills/theming-override-default.md +14 -0
- package/.claude/skills/theming-partial-override.md +17 -0
- package/.vscode/srcdev-component-accordian-core.code-snippets +74 -0
- package/.vscode/srcdev-component-expanding-panel-classic.code-snippets +121 -0
- package/.vscode/srcdev-component-expanding-panel.code-snippets +1 -3
- package/.vscode/srcdev-component-responsive-header.code-snippets +46 -0
- package/.vscode/srcdev-component-site-header.code-snippets +74 -0
- package/app/components/02.molecules/expandable/accordian/AccordianCore.vue +19 -11
- package/app/components/02.molecules/expandable/accordian/CONSUMER-STYLING.md +93 -0
- package/app/components/02.molecules/expandable/accordian/stories/AccordianCore.stories.ts +31 -0
- package/app/components/02.molecules/expandable/accordian/tests/AccordianCore.spec.ts +31 -0
- package/app/components/02.molecules/expandable/accordian/tests/__snapshots__/AccordianCore.spec.ts.snap +7 -21
- package/app/components/02.molecules/expandable/expanding-panel/CONSUMER-STYLING.md +10 -10
- package/app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue +35 -37
- package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +5 -6
- package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +5 -13
- package/app/components/02.molecules/expandable/expanding-panel-classic/CONSUMER-STYLING.md +103 -0
- package/app/components/02.molecules/expandable/expanding-panel-classic/ExpandingPanelClassic.vue +191 -0
- package/app/components/02.molecules/expandable/expanding-panel-classic/stories/ExpandingPanelClassic.stories.ts +293 -0
- package/app/components/02.molecules/expandable/expanding-panel-classic/tests/ExpandingPanelClassic.spec.ts +514 -0
- package/app/components/02.molecules/expandable/expanding-panel-classic/tests/__snapshots__/ExpandingPanelClassic.spec.ts.snap +59 -0
- package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +66 -0
- package/app/components/{responsive-header → 03.organisms/responsive-header}/NavigationItems.vue +21 -28
- package/app/components/{responsive-header → 03.organisms/responsive-header}/ResponsiveHeader.vue +141 -43
- package/app/components/03.organisms/responsive-header/stories/NavigationItems.stories.ts +80 -0
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +122 -0
- package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +155 -0
- package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +319 -0
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/NavigationItems.spec.ts.snap +34 -0
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +72 -0
- package/app/components/03.organisms/site-header/CONSUMER-STYLING.md +60 -0
- package/app/components/03.organisms/site-header/SiteHeader.vue +96 -0
- package/app/components/03.organisms/site-header/stories/SiteHeader.stories.ts +135 -0
- package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +103 -0
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +15 -0
- package/app/components/05.forms/input-select/CONSUMER-STYLING.md +42 -0
- package/app/components/05.forms/input-select/InputSelectCore.vue +21 -9
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +60 -0
- package/app/components/05.forms/input-text/InputTextCore.vue +17 -8
- package/app/layouts/default.vue +64 -67
- package/package.json +4 -1
package/app/components/{responsive-header → 03.organisms/responsive-header}/NavigationItems.vue
RENAMED
|
@@ -60,7 +60,6 @@
|
|
|
60
60
|
v-model="panelOpenStates[`${String(groupKey)}-${localIndex}`]"
|
|
61
61
|
name="overflow-navigation-group"
|
|
62
62
|
:animation-duration="DETAILS_ANIMATION_DURATION"
|
|
63
|
-
icon-size="medium"
|
|
64
63
|
:style-class-passthrough="['overflow-navigation-details']"
|
|
65
64
|
>
|
|
66
65
|
<template #summary>
|
|
@@ -107,7 +106,7 @@
|
|
|
107
106
|
</template>
|
|
108
107
|
|
|
109
108
|
<script setup lang="ts">
|
|
110
|
-
import type { ResponsiveHeaderState, ResponsiveHeaderNavItem } from "
|
|
109
|
+
import type { ResponsiveHeaderState, ResponsiveHeaderNavItem } from "../../../types/components";
|
|
111
110
|
|
|
112
111
|
interface Props {
|
|
113
112
|
mainNavigationState?: ResponsiveHeaderState;
|
|
@@ -259,10 +258,8 @@ watch(
|
|
|
259
258
|
}
|
|
260
259
|
+ .expanding-panel-content {
|
|
261
260
|
border-bottom: 0.1rem solid var(--overflow-nav-link-border-color, #efefef75);
|
|
262
|
-
.inner {
|
|
263
|
-
|
|
264
|
-
margin-top: var(--overflow-nav-items-gap, 0px);
|
|
265
|
-
}
|
|
261
|
+
.overflow-navigation-sub-nav-inner {
|
|
262
|
+
margin-top: var(--overflow-nav-items-gap, 0px);
|
|
266
263
|
}
|
|
267
264
|
}
|
|
268
265
|
}
|
|
@@ -271,29 +268,25 @@ watch(
|
|
|
271
268
|
.expanding-panel-content {
|
|
272
269
|
border-bottom: 0.1rem solid transparent;
|
|
273
270
|
|
|
274
|
-
.inner {
|
|
271
|
+
.overflow-navigation-sub-nav-inner {
|
|
275
272
|
margin-top: 0;
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
display: block;
|
|
294
|
-
text-decoration: none;
|
|
295
|
-
color: inherit;
|
|
296
|
-
}
|
|
273
|
+
position: relative;
|
|
274
|
+
|
|
275
|
+
.overflow-navigation-sub-nav-list {
|
|
276
|
+
display: flex;
|
|
277
|
+
flex-direction: column;
|
|
278
|
+
gap: 2px;
|
|
279
|
+
|
|
280
|
+
.overflow-navigation-sub-nav-item {
|
|
281
|
+
padding-block: var(--overflow-nav-items-padding-block, 0.8rem);
|
|
282
|
+
padding-inline: var(--overflow-nav-padding-inline, 0.8rem);
|
|
283
|
+
font-size: var(--overflow-nav-sub-item-font-size, inherit);
|
|
284
|
+
color: var(--overflow-nav-sub-item-color, inherit);
|
|
285
|
+
|
|
286
|
+
.overflow-navigation-sub-nav-link {
|
|
287
|
+
display: block;
|
|
288
|
+
text-decoration: none;
|
|
289
|
+
color: inherit;
|
|
297
290
|
}
|
|
298
291
|
}
|
|
299
292
|
}
|
package/app/components/{responsive-header → 03.organisms/responsive-header}/ResponsiveHeader.vue
RENAMED
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
|
|
64
64
|
{{ link.childLinksTitle }}
|
|
65
65
|
</summary>
|
|
66
|
-
<div class="main-navigation-sub-nav" role="menu">
|
|
66
|
+
<div class="main-navigation-sub-nav" role="menu" @mouseenter="handleSubNavHover">
|
|
67
67
|
<ul class="main-navigation-sub-nav-list">
|
|
68
68
|
<li v-for="childLink in link.childLinks" :key="childLink.name" class="main-navigation-sub-nav-item">
|
|
69
69
|
<NuxtLink :to="childLink.path" class="main-navigation-sub-nav-link" role="menuitem">
|
|
@@ -114,7 +114,7 @@ import type {
|
|
|
114
114
|
ResponsiveHeaderState,
|
|
115
115
|
IFlooredRect,
|
|
116
116
|
ResponsiveHeaderNavItem,
|
|
117
|
-
} from "
|
|
117
|
+
} from "../../../types/components";
|
|
118
118
|
import { useResizeObserver, onClickOutside } from "@vueuse/core";
|
|
119
119
|
|
|
120
120
|
interface Props {
|
|
@@ -164,6 +164,51 @@ const closeAllNavigationDetails = () => {
|
|
|
164
164
|
overflowDetailsRef.value?.removeAttribute("open");
|
|
165
165
|
};
|
|
166
166
|
|
|
167
|
+
// ─── Hover-intent delay for the "safe triangle" problem ───────────────────
|
|
168
|
+
// handleNavigationItemHover() used to call closeAllNavigationDetails() the
|
|
169
|
+
// instant the mouse entered ANY .main-navigation-item — including a sibling
|
|
170
|
+
// crossed only in transit while moving diagonally from an open dropdown's
|
|
171
|
+
// summary down into its own .main-navigation-sub-nav panel, closing it
|
|
172
|
+
// before the user got there.
|
|
173
|
+
//
|
|
174
|
+
// A pure-CSS "safe triangle" bridge was tried first and reverted: for the
|
|
175
|
+
// bridge to escape .main-navigation-item's overflow:hidden the same way
|
|
176
|
+
// .main-navigation-sub-nav does, its containing block must resolve to
|
|
177
|
+
// .navigation (the only positioned ancestor above the clipping box) — but
|
|
178
|
+
// any element it's attached to that's already positioned (the summary)
|
|
179
|
+
// terminates that search at itself, landing the bridge back inside the
|
|
180
|
+
// clipped box; anything not already positioned falls back to the CSS
|
|
181
|
+
// static-position algorithm, which doesn't reliably land a generated
|
|
182
|
+
// pseudo-element where the visible content is. Neither is fixable without
|
|
183
|
+
// restructuring the panel's positioning, which the collapse-measurement
|
|
184
|
+
// pipeline depends on (see the measurement-pipeline note above).
|
|
185
|
+
//
|
|
186
|
+
// This delay sidesteps that entirely: closing is scheduled, not immediate,
|
|
187
|
+
// and cancelled if the cursor reaches the summary or the sub-nav panel
|
|
188
|
+
// before it fires — so a brief diagonal dip through a sibling's hit area
|
|
189
|
+
// survives, while genuinely moving elsewhere still closes promptly.
|
|
190
|
+
const HOVER_CLOSE_DELAY = 200;
|
|
191
|
+
let closeAllTimer: ReturnType<typeof setTimeout> | null = null;
|
|
192
|
+
|
|
193
|
+
const cancelScheduledClose = () => {
|
|
194
|
+
if (closeAllTimer) {
|
|
195
|
+
clearTimeout(closeAllTimer);
|
|
196
|
+
closeAllTimer = null;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const scheduleCloseAllNavigationDetails = () => {
|
|
201
|
+
cancelScheduledClose();
|
|
202
|
+
closeAllTimer = setTimeout(() => {
|
|
203
|
+
closeAllNavigationDetails();
|
|
204
|
+
closeAllTimer = null;
|
|
205
|
+
}, HOVER_CLOSE_DELAY);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
onUnmounted(() => {
|
|
209
|
+
cancelScheduledClose();
|
|
210
|
+
});
|
|
211
|
+
|
|
167
212
|
const toggleDetailsElement = (event: Event) => {
|
|
168
213
|
const summaryElement = event.currentTarget as HTMLElement;
|
|
169
214
|
const parentDetailsElement = summaryElement.closest("details");
|
|
@@ -182,6 +227,10 @@ const handleSummaryHover = (event: MouseEvent | FocusEvent) => {
|
|
|
182
227
|
return;
|
|
183
228
|
}
|
|
184
229
|
|
|
230
|
+
// The cursor reached a summary — any close scheduled while it was in
|
|
231
|
+
// transit (see handleNavigationItemHover) no longer applies.
|
|
232
|
+
cancelScheduledClose();
|
|
233
|
+
|
|
185
234
|
// Close all other open navigation details first
|
|
186
235
|
const summaryElement = event.currentTarget as HTMLElement;
|
|
187
236
|
const parentDetailsElement = summaryElement.closest("details");
|
|
@@ -193,8 +242,16 @@ const handleSummaryHover = (event: MouseEvent | FocusEvent) => {
|
|
|
193
242
|
});
|
|
194
243
|
overflowDetailsRef.value?.removeAttribute("open");
|
|
195
244
|
|
|
196
|
-
//
|
|
197
|
-
|
|
245
|
+
// Ensure THIS one is open — never toggle it closed here. A real mouse click
|
|
246
|
+
// moves focus to the summary before the click event fires, so a single
|
|
247
|
+
// click on an already-(hover-)open item fires focusin (this handler) AND
|
|
248
|
+
// click (handleSummaryAction, which does its own toggle) in quick
|
|
249
|
+
// succession. If this handler also toggled, an already-open item would
|
|
250
|
+
// flip closed here and then flip back open in handleSummaryAction — a
|
|
251
|
+
// visible open→closed→open (or the reverse) flicker on every click.
|
|
252
|
+
// Hover/focus only ever guarantees "this one is open"; only an explicit
|
|
253
|
+
// click is allowed to close it.
|
|
254
|
+
parentDetailsElement?.setAttribute("open", "");
|
|
198
255
|
};
|
|
199
256
|
|
|
200
257
|
const handleNavigationItemHover = (key: string) => {
|
|
@@ -202,7 +259,13 @@ const handleNavigationItemHover = (key: string) => {
|
|
|
202
259
|
if (!props.allowExpandOnGesture) {
|
|
203
260
|
return;
|
|
204
261
|
}
|
|
205
|
-
|
|
262
|
+
scheduleCloseAllNavigationDetails();
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// Reaching the actual destination panel cancels the scheduled close from
|
|
266
|
+
// handleNavigationItemHover — same reasoning as handleSummaryHover above.
|
|
267
|
+
const handleSubNavHover = () => {
|
|
268
|
+
cancelScheduledClose();
|
|
206
269
|
};
|
|
207
270
|
|
|
208
271
|
const handleNavFocusout = (event: FocusEvent) => {
|
|
@@ -455,42 +518,21 @@ const setupClickOutsideListeners = () => {
|
|
|
455
518
|
}
|
|
456
519
|
};
|
|
457
520
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
// Always (re-)populate the local nav refs — firstNavRef / secondNavRef are local
|
|
465
|
-
// refs that reset on unmount, so they must be re-assigned on every mount even
|
|
466
|
-
// when the useState flags say we're already initialized.
|
|
467
|
-
await initTemplateRefs();
|
|
468
|
-
|
|
469
|
-
if (!navigationInitialized.value || !navLoaded.value) {
|
|
470
|
-
navLoaded.value = true;
|
|
471
|
-
navigationInitialized.value = true;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Geometry recalculation is handled by useResizeObserver which fires when the
|
|
475
|
-
// element first becomes observed. isGeometryReady is set there after the pass.
|
|
476
|
-
setupClickOutsideListeners();
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
// ─── Re-entrancy guard for the ResizeObserver callback ────────────────────
|
|
480
|
-
// The observer fires an async callback that awaits multiple ticks and toggles
|
|
481
|
-
// layout-affecting state. If the observer fires again while the first pass is
|
|
482
|
-
// still in flight, two concurrent passes would race on isOverflowVisibleForMeasurement
|
|
483
|
-
// and mainNavigationState.
|
|
521
|
+
// ─── Re-entrancy guard for the geometry pass ──────────────────────────────
|
|
522
|
+
// The pass is async and awaits multiple ticks while toggling layout-affecting
|
|
523
|
+
// state. If it's triggered again (resize, or the post-fonts-ready recheck
|
|
524
|
+
// below) while a pass is still in flight, two concurrent passes would race on
|
|
525
|
+
// isOverflowVisibleForMeasurement and mainNavigationState.
|
|
484
526
|
//
|
|
485
527
|
// Pattern: "run-latest" queue.
|
|
486
|
-
// • isMeasuring gates entry — any new
|
|
528
|
+
// • isMeasuring gates entry — any new trigger while a pass is running
|
|
487
529
|
// sets pendingMeasure = true and returns immediately.
|
|
488
|
-
// • The do/while re-runs once after the pass completes if a new
|
|
489
|
-
// so we never silently drop the final geometry update.
|
|
530
|
+
// • The do/while re-runs once after the pass completes if a new trigger
|
|
531
|
+
// arrived, so we never silently drop the final geometry update.
|
|
490
532
|
let isMeasuring = false;
|
|
491
533
|
let pendingMeasure = false;
|
|
492
534
|
|
|
493
|
-
|
|
535
|
+
const runGeometryPass = async () => {
|
|
494
536
|
if (isMeasuring) {
|
|
495
537
|
pendingMeasure = true;
|
|
496
538
|
return;
|
|
@@ -538,15 +580,51 @@ useResizeObserver(navigationWrapperRef, async () => {
|
|
|
538
580
|
// ─── Normal resize after geometry is settled ───────────────────────────
|
|
539
581
|
// The button's state is already correct (driven by showOverflowDetails),
|
|
540
582
|
// so a single-pass measurement gives accurate secondaryNavRects.
|
|
541
|
-
await updateNavigationConfig("
|
|
583
|
+
await updateNavigationConfig("resize-or-fonts-ready");
|
|
542
584
|
initMainNavigationState();
|
|
543
585
|
}
|
|
544
586
|
} while (pendingMeasure);
|
|
545
587
|
} finally {
|
|
546
588
|
isMeasuring = false;
|
|
547
589
|
}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
onMounted(async () => {
|
|
593
|
+
// Always reset on every mount — this is a local ref so it naturally resets,
|
|
594
|
+
// but being explicit here makes the intent clear: geometry must be re-verified
|
|
595
|
+
// on every route change before the overflow button appears or transitions play.
|
|
596
|
+
isGeometryReady.value = false;
|
|
597
|
+
|
|
598
|
+
// Always (re-)populate the local nav refs — firstNavRef / secondNavRef are local
|
|
599
|
+
// refs that reset on unmount, so they must be re-assigned on every mount even
|
|
600
|
+
// when the useState flags say we're already initialized.
|
|
601
|
+
await initTemplateRefs();
|
|
602
|
+
|
|
603
|
+
if (!navigationInitialized.value || !navLoaded.value) {
|
|
604
|
+
navLoaded.value = true;
|
|
605
|
+
navigationInitialized.value = true;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Geometry recalculation is handled by useResizeObserver which fires when the
|
|
609
|
+
// element first becomes observed. isGeometryReady is set there after the pass.
|
|
610
|
+
setupClickOutsideListeners();
|
|
611
|
+
|
|
612
|
+
// ─── Re-verify geometry once webfonts have settled ────────────────────────
|
|
613
|
+
// The initial ResizeObserver-driven pass can run before self-hosted fonts
|
|
614
|
+
// (see nuxt.config.ts `fonts.families`) have swapped in, measuring nav-item
|
|
615
|
+
// widths against fallback-font metrics. That undercounts item width, letting
|
|
616
|
+
// one extra item fit that shouldn't — the wrapper's own size hasn't changed,
|
|
617
|
+
// so the ResizeObserver never refires on its own to correct it. document.fonts
|
|
618
|
+
// isn't available in SSR/non-browser test environments, so this is a no-op there.
|
|
619
|
+
if (typeof document !== "undefined" && document.fonts) {
|
|
620
|
+
document.fonts.ready.then(() => {
|
|
621
|
+
if (isGeometryReady.value) runGeometryPass();
|
|
622
|
+
});
|
|
623
|
+
}
|
|
548
624
|
});
|
|
549
625
|
|
|
626
|
+
useResizeObserver(navigationWrapperRef, runGeometryPass);
|
|
627
|
+
|
|
550
628
|
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
551
629
|
|
|
552
630
|
watch(
|
|
@@ -611,8 +689,17 @@ watch(
|
|
|
611
689
|
--responsive-nav-decorator-indicator-color (default: currentColor)
|
|
612
690
|
--responsive-nav-decorator-hovered-indicator-color (default: inherits --responsive-nav-decorator-indicator-color)
|
|
613
691
|
--responsive-nav-decorator-hovered-bg (default: oklch(100% 0 0 / 8%))
|
|
692
|
+
|
|
693
|
+
--responsive-header-link-font-size (default: inherit)
|
|
614
694
|
──────────────────────────────────────────────────────────────────────── */
|
|
615
695
|
|
|
696
|
+
/* `inherit` means a viewport-relative (vw/clamp) ancestor font-size silently
|
|
697
|
+
flows into the overflow-collapse width measurement — the ResizeObserver
|
|
698
|
+
only watches this element's own box size, so a scrollbar-driven vw shift
|
|
699
|
+
can change nav-item text width without ever re-triggering a re-measurement.
|
|
700
|
+
Set this token to a fixed value (or a small set of breakpoint-based values)
|
|
701
|
+
to avoid it. */
|
|
702
|
+
|
|
616
703
|
--_link-visibility-transition: none;
|
|
617
704
|
position: relative;
|
|
618
705
|
color: var(--responsive-header-color, inherit);
|
|
@@ -682,6 +769,7 @@ watch(
|
|
|
682
769
|
display: flex;
|
|
683
770
|
gap: 6px;
|
|
684
771
|
text-wrap-mode: nowrap;
|
|
772
|
+
font-size: var(--responsive-header-link-font-size, inherit);
|
|
685
773
|
color: var(--responsive-header-link-color, inherit);
|
|
686
774
|
text-decoration: none;
|
|
687
775
|
cursor: pointer;
|
|
@@ -696,6 +784,17 @@ watch(
|
|
|
696
784
|
border-bottom: var(--_link-border-default);
|
|
697
785
|
}
|
|
698
786
|
|
|
787
|
+
/* Reserved so the icon's async-loaded SVG (Icon component) can't widen
|
|
788
|
+
the item after the initial geometry-measurement pass has already run —
|
|
789
|
+
an unsized icon measures at ~0 width until its content arrives, which
|
|
790
|
+
the overflow-collapse ResizeObserver has no way to detect afterwards
|
|
791
|
+
(the wrapper's own box doesn't necessarily change size when it happens). */
|
|
792
|
+
.decorator-icon {
|
|
793
|
+
flex-shrink: 0;
|
|
794
|
+
width: 1.35em;
|
|
795
|
+
height: 1.35em;
|
|
796
|
+
}
|
|
797
|
+
|
|
699
798
|
.main-navigation-details {
|
|
700
799
|
--_icon-transform: scaleY(1);
|
|
701
800
|
|
|
@@ -711,8 +810,13 @@ watch(
|
|
|
711
810
|
gap: 6px;
|
|
712
811
|
text-wrap-mode: nowrap;
|
|
713
812
|
|
|
813
|
+
/* Reserved for the same reason as .decorator-icon above — an
|
|
814
|
+
unsized chevron would widen the summary once its SVG loads. */
|
|
714
815
|
.icon {
|
|
715
816
|
display: block;
|
|
817
|
+
flex-shrink: 0;
|
|
818
|
+
width: 1.35em;
|
|
819
|
+
height: 1.35em;
|
|
716
820
|
transform: var(--_icon-transform);
|
|
717
821
|
transition: transform 0.2s ease-in-out;
|
|
718
822
|
}
|
|
@@ -728,6 +832,7 @@ watch(
|
|
|
728
832
|
cursor: pointer;
|
|
729
833
|
position: relative;
|
|
730
834
|
z-index: 4;
|
|
835
|
+
font-size: var(--responsive-header-link-font-size, inherit);
|
|
731
836
|
color: var(--responsive-header-link-color, inherit);
|
|
732
837
|
|
|
733
838
|
&::-webkit-details-marker,
|
|
@@ -836,13 +941,6 @@ watch(
|
|
|
836
941
|
}
|
|
837
942
|
}
|
|
838
943
|
|
|
839
|
-
.main-navigation-link {
|
|
840
|
-
.icon {
|
|
841
|
-
height: 1.35em;
|
|
842
|
-
width: 1.35em;
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
|
|
846
944
|
.overflow-details {
|
|
847
945
|
list-style: none;
|
|
848
946
|
padding: 0;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import NavigationItems from "../NavigationItems.vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import type { ResponsiveHeaderState } from "../../../../types/components";
|
|
4
|
+
|
|
5
|
+
const mainNavigationState: ResponsiveHeaderState = {
|
|
6
|
+
hasSecondNav: true,
|
|
7
|
+
navListVisibility: { firstNav: false, secondNav: false },
|
|
8
|
+
clonedNavLinks: {
|
|
9
|
+
firstNav: [
|
|
10
|
+
{ name: "Home", path: "/", config: { left: 0, right: 0, width: 0, visible: false } },
|
|
11
|
+
{ name: "About", path: "/about", config: { left: 0, right: 0, width: 0, visible: false } },
|
|
12
|
+
{
|
|
13
|
+
name: "Components",
|
|
14
|
+
childLinksTitle: "UI Components",
|
|
15
|
+
childLinks: [
|
|
16
|
+
{ name: "Buttons", path: "/forms/examples/buttons" },
|
|
17
|
+
{ name: "Tabs", path: "/ui/tabs" },
|
|
18
|
+
],
|
|
19
|
+
config: { left: 0, right: 0, width: 0, visible: false },
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
secondNav: [{ name: "Contact", path: "/contact", config: { left: 0, right: 0, width: 0, visible: false } }],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const meta: Meta<typeof NavigationItems> = {
|
|
27
|
+
title: "Organisms/Responsive Header/Navigation Items",
|
|
28
|
+
component: NavigationItems,
|
|
29
|
+
parameters: {
|
|
30
|
+
docs: {
|
|
31
|
+
description: {
|
|
32
|
+
component:
|
|
33
|
+
"Internal panel rendered inside ResponsiveHeader's overflow burger menu — not typically used standalone. " +
|
|
34
|
+
"Shows the *complement* of ResponsiveHeader's main nav: an item appears here only when its `config.visible` " +
|
|
35
|
+
"is `false` in the main bar.",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
argTypes: {
|
|
40
|
+
mainNavigationState: {
|
|
41
|
+
control: "object",
|
|
42
|
+
description: "Shared geometry/visibility state, normally supplied by the parent ResponsiveHeader",
|
|
43
|
+
},
|
|
44
|
+
styleClassPassthrough: {
|
|
45
|
+
control: "object",
|
|
46
|
+
description: "Extra CSS classes applied to the root element",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
args: {
|
|
50
|
+
mainNavigationState,
|
|
51
|
+
styleClassPassthrough: [],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default meta;
|
|
56
|
+
type Story = StoryObj<typeof NavigationItems>;
|
|
57
|
+
|
|
58
|
+
export const Default: Story = {};
|
|
59
|
+
|
|
60
|
+
export const MixedVisibility: Story = {
|
|
61
|
+
name: "Mixed Visibility (only hidden items shown)",
|
|
62
|
+
args: {
|
|
63
|
+
mainNavigationState: {
|
|
64
|
+
hasSecondNav: false,
|
|
65
|
+
navListVisibility: { firstNav: false },
|
|
66
|
+
clonedNavLinks: {
|
|
67
|
+
firstNav: [
|
|
68
|
+
{ name: "Home", path: "/", config: { left: 0, right: 0, width: 0, visible: true } },
|
|
69
|
+
{ name: "About", path: "/about", config: { left: 0, right: 0, width: 0, visible: false } },
|
|
70
|
+
{ name: "Contact", path: "/contact", config: { left: 0, right: 0, width: 0, visible: false } },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const Empty: Story = {
|
|
78
|
+
name: "Empty (nothing collapsed)",
|
|
79
|
+
args: { mainNavigationState: { hasSecondNav: false, navListVisibility: {}, clonedNavLinks: {} } },
|
|
80
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import ResponsiveHeader from "../ResponsiveHeader.vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof ResponsiveHeader> = {
|
|
5
|
+
title: "Organisms/Responsive Header",
|
|
6
|
+
component: ResponsiveHeader,
|
|
7
|
+
argTypes: {
|
|
8
|
+
responsiveNavLinks: {
|
|
9
|
+
control: "object",
|
|
10
|
+
description: "Nav groups keyed by name — each item is either a link (`path`) or a dropdown (`childLinks`)",
|
|
11
|
+
},
|
|
12
|
+
gapBetweenFirstAndSecondNav: {
|
|
13
|
+
control: { type: "number", min: 0, step: 4 },
|
|
14
|
+
description: "Gap in pixels reserved between the first and second nav groups",
|
|
15
|
+
},
|
|
16
|
+
overflowDetailsSummaryIcons: {
|
|
17
|
+
control: "object",
|
|
18
|
+
description: "Icon names for the overflow button's two states: `more` (some items collapsed) and `burger`",
|
|
19
|
+
},
|
|
20
|
+
collapseBreakpoint: {
|
|
21
|
+
control: { type: "number" },
|
|
22
|
+
description: "Fixed pixel width below which the whole main nav collapses into the overflow burger menu",
|
|
23
|
+
},
|
|
24
|
+
collapseAtMainNavIntersection: {
|
|
25
|
+
control: "boolean",
|
|
26
|
+
description: "Collapse the whole main nav into the overflow burger menu once it no longer fits its container",
|
|
27
|
+
},
|
|
28
|
+
allowExpandOnGesture: {
|
|
29
|
+
control: "boolean",
|
|
30
|
+
description: "Allow a dropdown to open on hover/focus, not just click",
|
|
31
|
+
},
|
|
32
|
+
styleClassPassthrough: {
|
|
33
|
+
control: "object",
|
|
34
|
+
description: "Extra CSS classes applied to the root element",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
args: {
|
|
38
|
+
gapBetweenFirstAndSecondNav: 12,
|
|
39
|
+
collapseBreakpoint: null,
|
|
40
|
+
collapseAtMainNavIntersection: false,
|
|
41
|
+
allowExpandOnGesture: true,
|
|
42
|
+
styleClassPassthrough: [],
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default meta;
|
|
47
|
+
type Story = StoryObj<typeof ResponsiveHeader>;
|
|
48
|
+
|
|
49
|
+
const responsiveNavLinks = {
|
|
50
|
+
firstNav: [
|
|
51
|
+
{ name: "Home", path: "/" },
|
|
52
|
+
{
|
|
53
|
+
name: "Components",
|
|
54
|
+
iconName: "material-symbols:widgets",
|
|
55
|
+
childLinksTitle: "UI Components",
|
|
56
|
+
childLinks: [
|
|
57
|
+
{ name: "Buttons", path: "/forms/examples/buttons" },
|
|
58
|
+
{ name: "Tabs", path: "/ui/tabs" },
|
|
59
|
+
{ name: "Carousels", path: "/ui/carousel-basic" },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{ name: "Typography", path: "/typography/page-heading" },
|
|
63
|
+
],
|
|
64
|
+
secondNav: [{ name: "Contact", path: "#" }],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const Default: Story = {
|
|
68
|
+
args: { responsiveNavLinks },
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Constrains the canvas so some items must collapse into the overflow burger menu —
|
|
72
|
+
* the same behaviour a narrow viewport triggers in a real page, not a bug. */
|
|
73
|
+
export const ConstrainedWidth: Story = {
|
|
74
|
+
name: "Constrained Width (items overflow)",
|
|
75
|
+
args: { responsiveNavLinks },
|
|
76
|
+
render: (args) => ({
|
|
77
|
+
components: { ResponsiveHeader },
|
|
78
|
+
setup() {
|
|
79
|
+
return { args };
|
|
80
|
+
},
|
|
81
|
+
template: `<div style="max-width: 420px"><ResponsiveHeader v-bind="args" /></div>`,
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const CustomOverflowIcons: Story = {
|
|
86
|
+
args: {
|
|
87
|
+
responsiveNavLinks,
|
|
88
|
+
overflowDetailsSummaryIcons: { more: "mdi:dots-horizontal", burger: "mdi:menu" },
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const WithSecondaryNavigationSlot: Story = {
|
|
93
|
+
name: "With secondaryNavigation slot",
|
|
94
|
+
args: { responsiveNavLinks },
|
|
95
|
+
render: (args) => ({
|
|
96
|
+
components: { ResponsiveHeader },
|
|
97
|
+
setup() {
|
|
98
|
+
return { args };
|
|
99
|
+
},
|
|
100
|
+
template: `
|
|
101
|
+
<ResponsiveHeader v-bind="args">
|
|
102
|
+
<template #secondaryNavigation>
|
|
103
|
+
<a href="/settings" aria-label="Settings">⚙</a>
|
|
104
|
+
</template>
|
|
105
|
+
</ResponsiveHeader>
|
|
106
|
+
`,
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const SingleFlatGroup: Story = {
|
|
111
|
+
name: "Single Flat Group (no dropdowns)",
|
|
112
|
+
args: {
|
|
113
|
+
responsiveNavLinks: {
|
|
114
|
+
main: [
|
|
115
|
+
{ name: "Home", path: "/" },
|
|
116
|
+
{ name: "About", path: "/about" },
|
|
117
|
+
{ name: "Services", path: "/services" },
|
|
118
|
+
{ name: "Contact", path: "/contact" },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|