srcdev-nuxt-components 9.4.4 → 9.4.6
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/commands/migrate-component.md +106 -0
- package/.claude/component-ledger/artifact-url.txt +1 -0
- package/.claude/component-ledger/audit.json +1 -0
- package/.claude/component-ledger/build.mjs +111 -0
- package/.claude/component-ledger/output.html +559 -0
- package/.claude/component-ledger/template.html +559 -0
- package/.claude/hooks/refresh-component-ledger.sh +32 -0
- package/.claude/settings.json +10 -0
- package/.claude/skills/components/animated-svg-text.md +78 -0
- package/.claude/skills/components/breadcrumb.md +1 -0
- package/.claude/skills/components/canvas-switcher.md +61 -0
- package/.claude/skills/components/clip-element.md +67 -0
- package/.claude/skills/components/clipped-panel.md +64 -0
- package/.claude/skills/components/content-docs.md +2 -0
- package/.claude/skills/components/deep-expanding-menu-classic.md +94 -0
- package/.claude/skills/components/deep-expanding-menu.md +92 -0
- package/.claude/skills/components/display-banner.md +60 -0
- package/.claude/skills/components/display-tooltip-defined.md +139 -0
- package/.claude/skills/components/display-tooltip.md +85 -0
- package/.claude/skills/components/marquee-scroller.md +130 -0
- package/.claude/skills/components/navigation-items.md +1 -0
- package/.claude/skills/components/responsive-header.md +3 -0
- package/.claude/skills/components/site-header.md +4 -0
- package/.claude/skills/components/site-navigation.md +1 -0
- package/.claude/skills/components/tab-navigation.md +1 -0
- package/.claude/skills/composable-tooltips-guide.md +10 -0
- package/.claude/skills/index.md +8 -1
- package/.claude/skills/storybook-add-story.md +30 -0
- package/.vscode/srcdev-component-animated-svg-text.code-snippets +15 -0
- package/.vscode/srcdev-component-breadcrumb.code-snippets +13 -0
- package/.vscode/srcdev-component-canvas-switcher.code-snippets +9 -0
- package/.vscode/srcdev-component-clip-element.code-snippets +20 -0
- package/.vscode/srcdev-component-clipped-panel.code-snippets +23 -0
- package/.vscode/srcdev-component-content-docs.code-snippets +18 -0
- package/.vscode/srcdev-component-deep-expanding-menu-classic.code-snippets +9 -0
- package/.vscode/srcdev-component-deep-expanding-menu.code-snippets +9 -0
- package/.vscode/srcdev-component-display-banner.code-snippets +30 -0
- package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +29 -0
- package/.vscode/srcdev-component-display-tooltip.code-snippets +38 -0
- package/.vscode/srcdev-component-marquee-scroller.code-snippets +93 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +12 -0
- package/.vscode/srcdev-component-site-header.code-snippets +17 -0
- package/.vscode/srcdev-component-site-navigation.code-snippets +30 -0
- package/.vscode/srcdev-component-slider-gallery.code-snippets +38 -0
- package/.vscode/srcdev-component-tab-navigation.code-snippets +30 -0
- package/app/components/01.atoms/animations/animated-svg-text/AnimatedSvgText.vue +58 -0
- package/app/components/01.atoms/animations/animated-svg-text/CONSUMER-STYLING.md +34 -0
- package/app/components/01.atoms/animations/animated-svg-text/stories/AnimatedSvgText.stories.ts +69 -0
- package/app/components/01.atoms/animations/animated-svg-text/tests/AnimatedSvgText.spec.ts +63 -0
- package/app/components/01.atoms/animations/animated-svg-text/tests/__snapshots__/AnimatedSvgText.spec.ts.snap +7 -0
- package/app/components/01.atoms/animations/clip-element/CONSUMER-STYLING.md +27 -0
- package/app/components/{clip-element → 01.atoms/animations/clip-element}/ClipElement.vue +16 -11
- package/app/components/01.atoms/animations/clip-element/stories/ClipElement.stories.ts +133 -0
- package/app/components/01.atoms/animations/clip-element/tests/ClipElement.spec.ts +70 -0
- package/app/components/01.atoms/animations/clip-element/tests/__snapshots__/ClipElement.spec.ts.snap +7 -0
- package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +94 -0
- package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +331 -0
- package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +151 -0
- package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +315 -0
- package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +28 -0
- package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +27 -0
- package/app/components/01.atoms/canvas-switcher/CanvasSwitcher.vue +111 -0
- package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +75 -0
- package/app/components/01.atoms/canvas-switcher/tests/CanvasSwitcher.spec.ts +67 -0
- package/app/components/01.atoms/clipped-panel/CONSUMER-STYLING.md +39 -0
- package/app/components/01.atoms/clipped-panel/ClippedPanel.vue +68 -0
- package/app/components/01.atoms/clipped-panel/stories/ClippedPanel.stories.ts +105 -0
- package/app/components/01.atoms/clipped-panel/tests/ClippedPanel.spec.ts +67 -0
- package/app/components/01.atoms/clipped-panel/tests/__snapshots__/ClippedPanel.spec.ts.snap +3 -0
- package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +8 -2
- package/app/components/01.atoms/display-banner/CONSUMER-STYLING.md +35 -0
- package/app/components/{display-banner → 01.atoms/display-banner}/DisplayBanner.vue +13 -35
- package/app/components/01.atoms/display-banner/stories/DisplayBanner.stories.ts +100 -0
- package/app/components/01.atoms/display-banner/tests/DisplayBanner.spec.ts +78 -0
- package/app/components/01.atoms/display-banner/tests/__snapshots__/DisplayBanner.spec.ts.snap +8 -0
- package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +62 -0
- package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +188 -0
- package/app/components/01.atoms/display-tooltip/stories/DisplayTooltip.stories.ts +94 -0
- package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +75 -0
- package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +12 -0
- package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +4 -1
- package/app/components/02.molecules/display-tooltip-defined/CONSUMER-STYLING.md +42 -0
- package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +116 -0
- package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +126 -0
- package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +66 -0
- package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +17 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +44 -0
- package/app/components/{deep-expanding-menu → 02.molecules/navigation/deep-expanding-menu}/DeepExpandingMenu.vue +53 -64
- package/app/components/02.molecules/navigation/deep-expanding-menu/stories/DeepExpandingMenu.stories.ts +79 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +92 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +17 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +40 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +200 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/stories/DeepExpandingMenuClassic.stories.ts +79 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/DeepExpandingMenuClassic.spec.ts +82 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/__snapshots__/DeepExpandingMenuClassic.spec.ts.snap +18 -0
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +4 -1
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +4 -1
- package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +16 -4
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +4 -1
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +16 -3
- package/app/components/03.organisms/site-header/SiteHeader.vue +16 -1
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
- package/app/types/components/display-tooltip-defined.d.ts +10 -0
- package/app/types/components/index.ts +2 -0
- package/app/types/components/marquee-scroller.d.ts +10 -0
- package/package.json +1 -1
- package/app/components/animated-svg-text/AnimatedSvgText.vue +0 -89
- package/app/components/canvas-switcher/CanvasSwitcher.vue +0 -104
- package/app/components/clipped-panels/ClippedPanel.vue +0 -87
- package/app/components/deep-expanding-menu/DeepExpandingMenuOld.vue +0 -218
- package/app/components/display-details/DisplayDetailsCore.vue +0 -122
- package/app/components/display-tooltip/DisplayTooltip.vue +0 -170
- package/app/components/display-tooltip/DisplayTooltipDefined.vue +0 -103
- package/app/components/magnetic-navigation/MagneticNavigation.vue +0 -162
- package/app/components/marquee-scroller/MarqueeScroller.vue +0 -289
package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import DisplayTooltipDefined from "../DisplayTooltipDefined.vue";
|
|
4
|
+
import type { TooltipContentText } from "~/types/components";
|
|
5
|
+
|
|
6
|
+
const contentText: TooltipContentText = {
|
|
7
|
+
tooltipTitle: { tag: "h4", text: "Title" },
|
|
8
|
+
tooltipContent: { tag: "p", text: "Body copy" },
|
|
9
|
+
tooltipAction: { tag: "span", text: "Learn more" },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe("DisplayTooltipDefined", () => {
|
|
13
|
+
it("mounts without error", async () => {
|
|
14
|
+
const wrapper = await mountSuspended(DisplayTooltipDefined);
|
|
15
|
+
expect(wrapper.vm).toBeTruthy();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("renders correct HTML structure", async () => {
|
|
19
|
+
const wrapper = await mountSuspended(DisplayTooltipDefined, { props: { contentText, tooltipId: "fixed" } });
|
|
20
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("renders the title, body, and action from contentText", async () => {
|
|
24
|
+
const wrapper = await mountSuspended(DisplayTooltipDefined, { props: { contentText } });
|
|
25
|
+
expect(wrapper.find(".tooltip-title").text()).toBe("Title");
|
|
26
|
+
expect(wrapper.find(".tooltip-body").text()).toBe("Body copy");
|
|
27
|
+
expect(wrapper.find(".tooltip-action").text()).toBe("Learn more");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("renders each contentText field with its given tag", async () => {
|
|
31
|
+
const wrapper = await mountSuspended(DisplayTooltipDefined, { props: { contentText } });
|
|
32
|
+
expect(wrapper.find(".tooltip-title").element.tagName.toLowerCase()).toBe("h4");
|
|
33
|
+
expect(wrapper.find(".tooltip-body").element.tagName.toLowerCase()).toBe("p");
|
|
34
|
+
expect(wrapper.find(".tooltip-action").element.tagName.toLowerCase()).toBe("span");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("omits a field when not present in contentText", async () => {
|
|
38
|
+
const wrapper = await mountSuspended(DisplayTooltipDefined, {
|
|
39
|
+
props: { contentText: { tooltipTitle: { tag: "h4", text: "Only a title" } } },
|
|
40
|
+
});
|
|
41
|
+
expect(wrapper.find(".tooltip-title").exists()).toBe(true);
|
|
42
|
+
expect(wrapper.find(".tooltip-body").exists()).toBe(false);
|
|
43
|
+
expect(wrapper.find(".tooltip-action").exists()).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("renders a close button wired to the tooltip id", async () => {
|
|
47
|
+
const wrapper = await mountSuspended(DisplayTooltipDefined, { props: { tooltipId: "my-tip" } });
|
|
48
|
+
const closeButton = wrapper.find(".display-tooltip-close-button");
|
|
49
|
+
expect(closeButton.attributes("popovertarget")).toBe("nuxt-tooltip-my-tip");
|
|
50
|
+
expect(closeButton.attributes("popovertargetaction")).toBe("hide");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("renders the triggerContent slot", async () => {
|
|
54
|
+
const wrapper = await mountSuspended(DisplayTooltipDefined, {
|
|
55
|
+
slots: { triggerContent: "<span class='slot-trigger'>?</span>" },
|
|
56
|
+
});
|
|
57
|
+
expect(wrapper.find(".slot-trigger").text()).toBe("?");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("applies styleClassPassthrough classes to the underlying DisplayTooltip", async () => {
|
|
61
|
+
const wrapper = await mountSuspended(DisplayTooltipDefined, {
|
|
62
|
+
props: { styleClassPassthrough: ["custom-class"] },
|
|
63
|
+
});
|
|
64
|
+
expect(wrapper.classes()).toContain("custom-class");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`DisplayTooltipDefined > renders correct HTML structure 1`] = `
|
|
4
|
+
"<div class="display-tooltip-core" style="--_anchor-name: --tooltip-anchor-v-0-0-0;">
|
|
5
|
+
<div class="display-tooltip-trigger-wrapper body-md">
|
|
6
|
+
<!--v-if--><button popovertarget="nuxt-tooltip-fixed" popovertargetaction="toggle" class="display-tooltip-trigger-button" aria-label="Toggle the popover"><span class="iconify i-fa7-solid:circle-question display-tooltip-trigger-icon" aria-hidden="true"></span></button>
|
|
7
|
+
</div>
|
|
8
|
+
<div id="nuxt-tooltip-fixed" popover="" class="display-tooltip-popover">
|
|
9
|
+
<div class="display-tooltip-popover-content">
|
|
10
|
+
<div class="popover-content-defined">
|
|
11
|
+
<h4 class="tooltip-title subtitle-sm">Title</h4>
|
|
12
|
+
<p class="tooltip-body body-sm">Body copy</p><span class="tooltip-action input-value">Learn more</span><button popovertarget="nuxt-tooltip-fixed" popovertargetaction="hide" class="display-tooltip-close-button" aria-label="Close tool tip">Close</button>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</div>"
|
|
17
|
+
`;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# DeepExpandingMenu — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## Public token API
|
|
4
|
+
|
|
5
|
+
| Token | Default | Controls |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `--deep-expanding-menu-gap` | `2.4rem` | Gap between top-level items |
|
|
8
|
+
| `--deep-expanding-menu-link-border-width` | `0.2rem` | Bottom border width on links and group toggles |
|
|
9
|
+
| `--deep-expanding-menu-link-padding-block` | `0.8rem` | Vertical padding on links and group toggles |
|
|
10
|
+
| `--deep-expanding-menu-link-border-colour-hover` | `light-dark(var(--blue-10), var(--slate-00))` | Bottom border colour on hover/focus |
|
|
11
|
+
| `--deep-expanding-menu-icon-size` | `1.2rem` | Caret icon size |
|
|
12
|
+
| `--deep-expanding-menu-panel-width` | `min(100%, 50vw)` | Popover panel width |
|
|
13
|
+
| `--deep-expanding-menu-panel-background-colour` | `white` | Popover panel background |
|
|
14
|
+
| `--deep-expanding-menu-panel-border-width` | `0.1rem` | Popover panel border width |
|
|
15
|
+
| `--deep-expanding-menu-panel-border-colour` | `black` | Popover panel border colour |
|
|
16
|
+
| `--deep-expanding-menu-panel-border-radius` | `1.2rem` | Popover panel corner radius |
|
|
17
|
+
| `--deep-expanding-menu-panel-shadow` | `0 0 1rem rgba(0, 0, 0, 0.1)` | Popover panel box-shadow |
|
|
18
|
+
| `--deep-expanding-menu-panel-padding` | `1.2rem` | Popover panel padding |
|
|
19
|
+
| `--deep-expanding-menu-panel-heading-colour` | `var(--slate-10)` | `childLinksTitle` heading colour inside the panel |
|
|
20
|
+
| `--deep-expanding-menu-panel-list-gap` | `1.2rem` | Gap between child link grid items |
|
|
21
|
+
| `--deep-expanding-menu-group-link-colour` | `var(--slate-10)` | Child link text colour |
|
|
22
|
+
| `--deep-expanding-menu-group-link-border-colour-hover` | `var(--slate-10)` | Child link bottom border colour on hover/focus |
|
|
23
|
+
|
|
24
|
+
```css
|
|
25
|
+
.my-page {
|
|
26
|
+
--deep-expanding-menu-panel-background-colour: #1a1a1a;
|
|
27
|
+
--deep-expanding-menu-panel-border-colour: transparent;
|
|
28
|
+
--deep-expanding-menu-group-link-colour: white;
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or scope to a single instance via `styleClassPassthrough`:
|
|
33
|
+
|
|
34
|
+
```vue
|
|
35
|
+
<DeepExpandingMenu style-class-passthrough="main-nav">...</DeepExpandingMenu>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Not tokenised
|
|
39
|
+
|
|
40
|
+
- The `@position-try` fallback offsets (`--anchor-left`/`--anchor-right`) are fixed at `1rem` —
|
|
41
|
+
these are CSS anchor-positioning fallback rules, not runtime custom properties, so they can't be
|
|
42
|
+
overridden per-instance without a new `@position-try` block.
|
|
43
|
+
- Anchor names and popover target ids are generated per-instance via `useId()` and are not
|
|
44
|
+
consumer-configurable — they only need to be unique, not meaningful.
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
<template v-for="(link, key) in navLinks" :key="key">
|
|
5
5
|
<NuxtLink v-if="link.path" :to="link.path" class="navigation-link">{{ link.name }}</NuxtLink>
|
|
6
6
|
|
|
7
|
-
<div v-else
|
|
8
|
-
<button :popovertarget="`popovertarget
|
|
7
|
+
<div v-else class="navigation-group" :style="`--_anchor-name: --anchor-${uid}-${key};`">
|
|
8
|
+
<button :popovertarget="`popovertarget-${uid}-${key}`" class="navigation-group-toggle">
|
|
9
9
|
<span>{{ link.name }}</span>
|
|
10
10
|
<Icon name="bi:caret-down-fill" class="icon" />
|
|
11
11
|
</button>
|
|
12
12
|
|
|
13
|
-
<div :id="`popovertarget
|
|
13
|
+
<div :id="`popovertarget-${uid}-${key}`" class="navigation-group-panel" popover role="menu">
|
|
14
14
|
<h4 class="page-heading-4 mb-6">{{ link.childLinksTitle }}</h4>
|
|
15
15
|
<ul class="navigation-group-list">
|
|
16
16
|
<li v-for="childLink in link.childLinks" :key="childLink.name" class="navigation-group-item">
|
|
@@ -25,27 +25,24 @@
|
|
|
25
25
|
</template>
|
|
26
26
|
|
|
27
27
|
<script setup lang="ts">
|
|
28
|
-
import type { ResponsiveHeaderNavItem } from "
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
},
|
|
41
|
-
styleClassPassthrough: {
|
|
42
|
-
type: [String, Array] as PropType<string | string[]>,
|
|
43
|
-
default: () => [],
|
|
44
|
-
},
|
|
28
|
+
import type { ResponsiveHeaderNavItem } from "~/types/components";
|
|
29
|
+
|
|
30
|
+
interface Props {
|
|
31
|
+
tag?: "div" | "section" | "nav" | "ul" | "ol";
|
|
32
|
+
navLinks?: ResponsiveHeaderNavItem[];
|
|
33
|
+
styleClassPassthrough?: string | string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
37
|
+
tag: "nav",
|
|
38
|
+
navLinks: () => [],
|
|
39
|
+
styleClassPassthrough: () => [],
|
|
45
40
|
});
|
|
46
41
|
|
|
47
42
|
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
48
|
-
|
|
43
|
+
|
|
44
|
+
// Scopes anchor-name/popovertarget ids so multiple instances on one page don't collide.
|
|
45
|
+
const uid = useId();
|
|
49
46
|
|
|
50
47
|
watch(
|
|
51
48
|
() => props.styleClassPassthrough,
|
|
@@ -60,19 +57,17 @@ watch(
|
|
|
60
57
|
@layer deep-expanding-menu-setup {
|
|
61
58
|
@position-try --anchor-left {
|
|
62
59
|
inset: auto;
|
|
63
|
-
top: calc(anchor(bottom) +
|
|
64
|
-
left: calc(anchor(left) +
|
|
60
|
+
top: calc(anchor(bottom) + 1rem);
|
|
61
|
+
left: calc(anchor(left) + 1rem);
|
|
65
62
|
}
|
|
66
63
|
|
|
67
64
|
@position-try-fallbacks --anchor-right {
|
|
68
65
|
inset: auto;
|
|
69
|
-
top: calc(anchor(bottom) +
|
|
70
|
-
right: calc(anchor(right) +
|
|
66
|
+
top: calc(anchor(bottom) + 1rem);
|
|
67
|
+
right: calc(anchor(right) + 1rem);
|
|
71
68
|
}
|
|
72
69
|
|
|
73
70
|
.deep-expanding-menu {
|
|
74
|
-
--_gap-between-top-level-items: 24px;
|
|
75
|
-
|
|
76
71
|
container-type: inline-size;
|
|
77
72
|
display: grid;
|
|
78
73
|
grid-template-areas: "element-stack";
|
|
@@ -80,28 +75,25 @@ watch(
|
|
|
80
75
|
.inner {
|
|
81
76
|
grid-area: element-stack;
|
|
82
77
|
display: flex;
|
|
83
|
-
gap: var(--
|
|
78
|
+
gap: var(--deep-expanding-menu-gap, 2.4rem);
|
|
84
79
|
align-items: center;
|
|
85
80
|
|
|
86
81
|
.navigation-link,
|
|
87
82
|
.navigation-group-toggle {
|
|
88
83
|
all: unset;
|
|
89
|
-
border-bottom:
|
|
90
|
-
padding-block:
|
|
84
|
+
border-bottom: var(--deep-expanding-menu-link-border-width, 0.2rem) solid transparent;
|
|
85
|
+
padding-block: var(--deep-expanding-menu-link-padding-block, 0.8rem);
|
|
91
86
|
|
|
92
87
|
transition: border-color 200ms;
|
|
93
88
|
|
|
94
|
-
&:hover
|
|
95
|
-
|
|
96
|
-
border-color: light-dark(var(--blue-10), var(--slate-00));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
&:focus {
|
|
100
|
-
border-color: light-dark(var(--blue-10), var(--slate-00));
|
|
101
|
-
}
|
|
102
|
-
|
|
89
|
+
&:hover,
|
|
90
|
+
&:focus,
|
|
103
91
|
&:focus-visible {
|
|
104
|
-
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
border-color: var(
|
|
94
|
+
--deep-expanding-menu-link-border-colour-hover,
|
|
95
|
+
light-dark(var(--blue-10), var(--slate-00))
|
|
96
|
+
);
|
|
105
97
|
}
|
|
106
98
|
}
|
|
107
99
|
|
|
@@ -114,11 +106,11 @@ watch(
|
|
|
114
106
|
|
|
115
107
|
display: flex;
|
|
116
108
|
align-items: center;
|
|
117
|
-
gap:
|
|
109
|
+
gap: 1.2rem;
|
|
118
110
|
|
|
119
111
|
.icon {
|
|
120
112
|
display: block;
|
|
121
|
-
font-size: 1.2rem;
|
|
113
|
+
font-size: var(--deep-expanding-menu-icon-size, 1.2rem);
|
|
122
114
|
|
|
123
115
|
transform: var(--_icon-transform);
|
|
124
116
|
transition: transform 200ms;
|
|
@@ -130,9 +122,8 @@ watch(
|
|
|
130
122
|
position: absolute;
|
|
131
123
|
position-anchor: var(--_anchor-name);
|
|
132
124
|
margin: 0;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
left: calc(anchor(left) + 0px);
|
|
125
|
+
top: calc(anchor(bottom) + 1rem);
|
|
126
|
+
left: calc(anchor(left) + 0rem);
|
|
136
127
|
|
|
137
128
|
opacity: 0;
|
|
138
129
|
transition:
|
|
@@ -141,13 +132,14 @@ watch(
|
|
|
141
132
|
overlay 200ms;
|
|
142
133
|
transition-behavior: allow-discrete;
|
|
143
134
|
|
|
144
|
-
width: min(100%, 50vw);
|
|
135
|
+
width: var(--deep-expanding-menu-panel-width, min(100%, 50vw));
|
|
145
136
|
|
|
146
|
-
background-color: white;
|
|
147
|
-
border:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
137
|
+
background-color: var(--deep-expanding-menu-panel-background-colour, white);
|
|
138
|
+
border: var(--deep-expanding-menu-panel-border-width, 0.1rem) solid
|
|
139
|
+
var(--deep-expanding-menu-panel-border-colour, black);
|
|
140
|
+
border-radius: var(--deep-expanding-menu-panel-border-radius, 1.2rem);
|
|
141
|
+
box-shadow: var(--deep-expanding-menu-panel-shadow, 0 0 1rem rgba(0, 0, 0, 0.1));
|
|
142
|
+
padding: var(--deep-expanding-menu-panel-padding, 1.2rem);
|
|
151
143
|
overflow: clip;
|
|
152
144
|
|
|
153
145
|
&:popover-open {
|
|
@@ -161,36 +153,33 @@ watch(
|
|
|
161
153
|
}
|
|
162
154
|
|
|
163
155
|
h4 {
|
|
164
|
-
color: var(--slate-10);
|
|
156
|
+
color: var(--deep-expanding-menu-panel-heading-colour, var(--slate-10));
|
|
165
157
|
}
|
|
166
158
|
|
|
167
159
|
.navigation-group-list {
|
|
168
160
|
display: grid;
|
|
169
|
-
grid-template-columns: repeat(auto-fill, minmax(
|
|
170
|
-
gap:
|
|
161
|
+
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
|
|
162
|
+
gap: var(--deep-expanding-menu-panel-list-gap, 1.2rem);
|
|
171
163
|
padding-inline-start: 0;
|
|
172
|
-
margin-block-end:
|
|
164
|
+
margin-block-end: 0.8rem;
|
|
173
165
|
|
|
174
166
|
.navigation-group-item {
|
|
175
167
|
display: block;
|
|
176
168
|
|
|
177
169
|
a.navigation-group-link {
|
|
178
170
|
display: inline-block;
|
|
179
|
-
color: var(--slate-10);
|
|
171
|
+
color: var(--deep-expanding-menu-group-link-colour, var(--slate-10));
|
|
180
172
|
text-decoration: none;
|
|
181
|
-
padding-block:
|
|
173
|
+
padding-block: 0.8rem;
|
|
182
174
|
|
|
183
|
-
border-bottom:
|
|
175
|
+
border-bottom: var(--deep-expanding-menu-link-border-width, 0.2rem) solid transparent;
|
|
184
176
|
|
|
185
177
|
transition: border-color 200ms;
|
|
186
178
|
|
|
187
|
-
&:hover
|
|
188
|
-
cursor: pointer;
|
|
189
|
-
border-color: var(--slate-10);
|
|
190
|
-
}
|
|
191
|
-
|
|
179
|
+
&:hover,
|
|
192
180
|
&:focus-visible {
|
|
193
|
-
|
|
181
|
+
cursor: pointer;
|
|
182
|
+
border-color: var(--deep-expanding-menu-group-link-border-colour-hover, var(--slate-10));
|
|
194
183
|
}
|
|
195
184
|
}
|
|
196
185
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import DeepExpandingMenu from "../DeepExpandingMenu.vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import type { ResponsiveHeaderNavItem } from "~/types/components";
|
|
4
|
+
|
|
5
|
+
const navLinks: ResponsiveHeaderNavItem[] = [
|
|
6
|
+
{ name: "Home", path: "/" },
|
|
7
|
+
{
|
|
8
|
+
name: "Services",
|
|
9
|
+
childLinksTitle: "Our services",
|
|
10
|
+
childLinks: [
|
|
11
|
+
{ name: "Haircuts", path: "/services/haircuts" },
|
|
12
|
+
{ name: "Colouring", path: "/services/colouring" },
|
|
13
|
+
{ name: "Styling", path: "/services/styling" },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
{ name: "Contact", path: "/contact" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const meta: Meta<typeof DeepExpandingMenu> = {
|
|
20
|
+
title: "Molecules/DeepExpandingMenu",
|
|
21
|
+
component: DeepExpandingMenu,
|
|
22
|
+
argTypes: {
|
|
23
|
+
tag: {
|
|
24
|
+
control: { type: "select" },
|
|
25
|
+
options: ["div", "section", "nav", "ul", "ol"],
|
|
26
|
+
description: "Root element tag",
|
|
27
|
+
table: { category: "Basic" },
|
|
28
|
+
},
|
|
29
|
+
navLinks: { table: { disable: true } },
|
|
30
|
+
styleClassPassthrough: { table: { disable: true } },
|
|
31
|
+
},
|
|
32
|
+
args: {
|
|
33
|
+
tag: "nav",
|
|
34
|
+
navLinks,
|
|
35
|
+
},
|
|
36
|
+
parameters: {
|
|
37
|
+
docs: {
|
|
38
|
+
description: {
|
|
39
|
+
component:
|
|
40
|
+
"Modern implementation of a top-level nav with expanding child-link panels, using CSS anchor-positioning and the Popover API. See `DeepExpandingMenuClassic` for a `<details>`-based fallback with wider browser support.",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default meta;
|
|
47
|
+
type Story = StoryObj<typeof DeepExpandingMenu>;
|
|
48
|
+
|
|
49
|
+
export const Default: Story = {
|
|
50
|
+
render: (args) => ({
|
|
51
|
+
components: { DeepExpandingMenu },
|
|
52
|
+
setup() {
|
|
53
|
+
return { args };
|
|
54
|
+
},
|
|
55
|
+
template: `<DeepExpandingMenu v-bind="args" style="padding: 4rem;" />`,
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const CustomColours: Story = {
|
|
60
|
+
render: (args) => ({
|
|
61
|
+
components: { DeepExpandingMenu },
|
|
62
|
+
setup() {
|
|
63
|
+
return { args };
|
|
64
|
+
},
|
|
65
|
+
template: `
|
|
66
|
+
<DeepExpandingMenu
|
|
67
|
+
v-bind="args"
|
|
68
|
+
style="padding: 4rem; --deep-expanding-menu-panel-background-colour: #1a1a1a; --deep-expanding-menu-panel-border-colour: transparent; --deep-expanding-menu-group-link-colour: white; --deep-expanding-menu-panel-heading-colour: #aaaaaa;"
|
|
69
|
+
/>
|
|
70
|
+
`,
|
|
71
|
+
}),
|
|
72
|
+
parameters: {
|
|
73
|
+
docs: {
|
|
74
|
+
description: {
|
|
75
|
+
story: "Overriding the public tokens via inline `--deep-expanding-menu-*` custom properties.",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import DeepExpandingMenu from "../DeepExpandingMenu.vue";
|
|
4
|
+
import type { ResponsiveHeaderNavItem } from "~/types/components";
|
|
5
|
+
|
|
6
|
+
const defaultNavLinks: ResponsiveHeaderNavItem[] = [
|
|
7
|
+
{ name: "Home", path: "/" },
|
|
8
|
+
{
|
|
9
|
+
name: "Services",
|
|
10
|
+
childLinksTitle: "Our services",
|
|
11
|
+
childLinks: [
|
|
12
|
+
{ name: "Haircuts", path: "/services/haircuts" },
|
|
13
|
+
{ name: "Colouring", path: "/services/colouring" },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
{ name: "Contact", path: "/contact" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
describe("DeepExpandingMenu", () => {
|
|
20
|
+
it("mounts without error", async () => {
|
|
21
|
+
const wrapper = await mountSuspended(DeepExpandingMenu, { props: { navLinks: defaultNavLinks } });
|
|
22
|
+
expect(wrapper.vm).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("renders correct HTML structure", async () => {
|
|
26
|
+
const wrapper = await mountSuspended(DeepExpandingMenu, { props: { navLinks: defaultNavLinks } });
|
|
27
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("renders a <nav> by default", async () => {
|
|
31
|
+
const wrapper = await mountSuspended(DeepExpandingMenu, { props: { navLinks: defaultNavLinks } });
|
|
32
|
+
expect(wrapper.element.tagName.toLowerCase()).toBe("nav");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("renders the given tag", async () => {
|
|
36
|
+
const wrapper = await mountSuspended(DeepExpandingMenu, {
|
|
37
|
+
props: { navLinks: defaultNavLinks, tag: "div" },
|
|
38
|
+
});
|
|
39
|
+
expect(wrapper.element.tagName.toLowerCase()).toBe("div");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("renders a direct link for items with a path", async () => {
|
|
43
|
+
const wrapper = await mountSuspended(DeepExpandingMenu, { props: { navLinks: defaultNavLinks } });
|
|
44
|
+
const links = wrapper.findAll(".navigation-link");
|
|
45
|
+
expect(links.map((l) => l.text())).toEqual(["Home", "Contact"]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("renders a toggle group for items with childLinks", async () => {
|
|
49
|
+
const wrapper = await mountSuspended(DeepExpandingMenu, { props: { navLinks: defaultNavLinks } });
|
|
50
|
+
expect(wrapper.find(".navigation-group-toggle").text()).toContain("Services");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("renders each child link inside the group panel", async () => {
|
|
54
|
+
const wrapper = await mountSuspended(DeepExpandingMenu, { props: { navLinks: defaultNavLinks } });
|
|
55
|
+
const childLinks = wrapper.findAll(".navigation-group-link");
|
|
56
|
+
expect(childLinks.map((l) => l.text())).toEqual(["Haircuts", "Colouring"]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("renders the childLinksTitle heading", async () => {
|
|
60
|
+
const wrapper = await mountSuspended(DeepExpandingMenu, { props: { navLinks: defaultNavLinks } });
|
|
61
|
+
expect(wrapper.find(".navigation-group-panel h4").text()).toBe("Our services");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("gives each group toggle a unique popovertarget", async () => {
|
|
65
|
+
const navLinks: ResponsiveHeaderNavItem[] = [
|
|
66
|
+
{ name: "A", childLinksTitle: "A", childLinks: [{ name: "A1", path: "/a1" }] },
|
|
67
|
+
{ name: "B", childLinksTitle: "B", childLinks: [{ name: "B1", path: "/b1" }] },
|
|
68
|
+
];
|
|
69
|
+
const wrapper = await mountSuspended(DeepExpandingMenu, { props: { navLinks } });
|
|
70
|
+
const toggles = wrapper.findAll(".navigation-group-toggle");
|
|
71
|
+
const targets = toggles.map((t) => t.attributes("popovertarget"));
|
|
72
|
+
expect(new Set(targets).size).toBe(2);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("applies styleClassPassthrough classes", async () => {
|
|
76
|
+
const wrapper = await mountSuspended(DeepExpandingMenu, {
|
|
77
|
+
props: { navLinks: defaultNavLinks, styleClassPassthrough: ["custom-class"] },
|
|
78
|
+
});
|
|
79
|
+
expect(wrapper.classes()).toContain("custom-class");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("resets classes when styleClassPassthrough prop changes", async () => {
|
|
83
|
+
const wrapper = await mountSuspended(DeepExpandingMenu, {
|
|
84
|
+
props: { navLinks: defaultNavLinks, styleClassPassthrough: ["initial-class"] },
|
|
85
|
+
});
|
|
86
|
+
expect(wrapper.classes()).toContain("initial-class");
|
|
87
|
+
|
|
88
|
+
await wrapper.setProps({ styleClassPassthrough: ["updated-class"] });
|
|
89
|
+
expect(wrapper.classes()).not.toContain("initial-class");
|
|
90
|
+
expect(wrapper.classes()).toContain("updated-class");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`DeepExpandingMenu > renders correct HTML structure 1`] = `
|
|
4
|
+
"<nav class="deep-expanding-menu">
|
|
5
|
+
<div class="inner"><a href="/" class="navigation-link">Home</a>
|
|
6
|
+
<div class="navigation-group" style="--_anchor-name: --anchor-v-0-0-1;"><button popovertarget="popovertarget-v-0-0-1" class="navigation-group-toggle"><span>Services</span><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></button>
|
|
7
|
+
<div id="popovertarget-v-0-0-1" class="navigation-group-panel" popover="" role="menu">
|
|
8
|
+
<h4 class="page-heading-4 mb-6">Our services</h4>
|
|
9
|
+
<ul class="navigation-group-list">
|
|
10
|
+
<li class="navigation-group-item"><a href="/services/haircuts" class="navigation-group-link">Haircuts</a></li>
|
|
11
|
+
<li class="navigation-group-item"><a href="/services/colouring" class="navigation-group-link">Colouring</a></li>
|
|
12
|
+
</ul>
|
|
13
|
+
</div>
|
|
14
|
+
</div><a href="/contact" class="navigation-link">Contact</a>
|
|
15
|
+
</div>
|
|
16
|
+
</nav>"
|
|
17
|
+
`;
|
package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# DeepExpandingMenuClassic — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## Public token API
|
|
4
|
+
|
|
5
|
+
| Token | Default | Controls |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `--deep-expanding-menu-classic-gap` | `1.2rem` | Gap on the root grid |
|
|
8
|
+
| `--deep-expanding-menu-classic-item-gap` | `2.4rem` | Gap between top-level items |
|
|
9
|
+
| `--deep-expanding-menu-classic-link-border-width` | `0.2rem` | Bottom border width on links and group toggles |
|
|
10
|
+
| `--deep-expanding-menu-classic-link-padding-block` | `0.8rem` | Vertical padding on links and group toggles |
|
|
11
|
+
| `--deep-expanding-menu-classic-link-border-colour-hover` | `light-dark(var(--blue-10), var(--slate-00))` | Bottom border colour on hover/focus |
|
|
12
|
+
| `--deep-expanding-menu-classic-icon-size` | `1.2rem` | Caret icon size |
|
|
13
|
+
| `--deep-expanding-menu-classic-panel-offset-top` | `4rem` | Panel offset from the toggle |
|
|
14
|
+
| `--deep-expanding-menu-classic-panel-width` | `20rem` | Panel width below the `768px` breakpoint |
|
|
15
|
+
| `--deep-expanding-menu-classic-panel-width-tablet` | `40rem` | Panel width from `768px` |
|
|
16
|
+
| `--deep-expanding-menu-classic-panel-width-desktop` | `60rem` | Panel width from `1024px` |
|
|
17
|
+
| `--deep-expanding-menu-classic-panel-background-colour` | `white` | Panel background |
|
|
18
|
+
| `--deep-expanding-menu-classic-panel-border-width` | `0.1rem` | Panel border width |
|
|
19
|
+
| `--deep-expanding-menu-classic-panel-border-colour` | `black` | Panel border colour |
|
|
20
|
+
| `--deep-expanding-menu-classic-panel-border-radius` | `1.2rem` | Panel corner radius |
|
|
21
|
+
| `--deep-expanding-menu-classic-panel-shadow` | `0 0 1rem rgba(0, 0, 0, 0.1)` | Panel box-shadow |
|
|
22
|
+
| `--deep-expanding-menu-classic-panel-padding` | `1.2rem` | Panel padding |
|
|
23
|
+
| `--deep-expanding-menu-classic-panel-heading-colour` | `var(--slate-10)` | `childLinksTitle` heading colour inside the panel |
|
|
24
|
+
| `--deep-expanding-menu-classic-panel-list-gap` | `1.2rem` | Gap between child link grid items |
|
|
25
|
+
| `--deep-expanding-menu-classic-group-link-colour` | `var(--slate-10)` | Child link text colour |
|
|
26
|
+
| `--deep-expanding-menu-classic-group-link-border-colour-hover` | `var(--slate-10)` | Child link bottom border colour on hover/focus |
|
|
27
|
+
|
|
28
|
+
```css
|
|
29
|
+
.my-page {
|
|
30
|
+
--deep-expanding-menu-classic-panel-background-colour: #1a1a1a;
|
|
31
|
+
--deep-expanding-menu-classic-panel-border-colour: transparent;
|
|
32
|
+
--deep-expanding-menu-classic-group-link-colour: white;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or scope to a single instance via `styleClassPassthrough`:
|
|
37
|
+
|
|
38
|
+
```vue
|
|
39
|
+
<DeepExpandingMenuClassic style-class-passthrough="main-nav">...</DeepExpandingMenuClassic>
|
|
40
|
+
```
|