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.
Files changed (116) hide show
  1. package/.claude/commands/migrate-component.md +106 -0
  2. package/.claude/component-ledger/artifact-url.txt +1 -0
  3. package/.claude/component-ledger/audit.json +1 -0
  4. package/.claude/component-ledger/build.mjs +111 -0
  5. package/.claude/component-ledger/output.html +559 -0
  6. package/.claude/component-ledger/template.html +559 -0
  7. package/.claude/hooks/refresh-component-ledger.sh +32 -0
  8. package/.claude/settings.json +10 -0
  9. package/.claude/skills/components/animated-svg-text.md +78 -0
  10. package/.claude/skills/components/breadcrumb.md +1 -0
  11. package/.claude/skills/components/canvas-switcher.md +61 -0
  12. package/.claude/skills/components/clip-element.md +67 -0
  13. package/.claude/skills/components/clipped-panel.md +64 -0
  14. package/.claude/skills/components/content-docs.md +2 -0
  15. package/.claude/skills/components/deep-expanding-menu-classic.md +94 -0
  16. package/.claude/skills/components/deep-expanding-menu.md +92 -0
  17. package/.claude/skills/components/display-banner.md +60 -0
  18. package/.claude/skills/components/display-tooltip-defined.md +139 -0
  19. package/.claude/skills/components/display-tooltip.md +85 -0
  20. package/.claude/skills/components/marquee-scroller.md +130 -0
  21. package/.claude/skills/components/navigation-items.md +1 -0
  22. package/.claude/skills/components/responsive-header.md +3 -0
  23. package/.claude/skills/components/site-header.md +4 -0
  24. package/.claude/skills/components/site-navigation.md +1 -0
  25. package/.claude/skills/components/tab-navigation.md +1 -0
  26. package/.claude/skills/composable-tooltips-guide.md +10 -0
  27. package/.claude/skills/index.md +8 -1
  28. package/.claude/skills/storybook-add-story.md +30 -0
  29. package/.vscode/srcdev-component-animated-svg-text.code-snippets +15 -0
  30. package/.vscode/srcdev-component-breadcrumb.code-snippets +13 -0
  31. package/.vscode/srcdev-component-canvas-switcher.code-snippets +9 -0
  32. package/.vscode/srcdev-component-clip-element.code-snippets +20 -0
  33. package/.vscode/srcdev-component-clipped-panel.code-snippets +23 -0
  34. package/.vscode/srcdev-component-content-docs.code-snippets +18 -0
  35. package/.vscode/srcdev-component-deep-expanding-menu-classic.code-snippets +9 -0
  36. package/.vscode/srcdev-component-deep-expanding-menu.code-snippets +9 -0
  37. package/.vscode/srcdev-component-display-banner.code-snippets +30 -0
  38. package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +29 -0
  39. package/.vscode/srcdev-component-display-tooltip.code-snippets +38 -0
  40. package/.vscode/srcdev-component-marquee-scroller.code-snippets +93 -0
  41. package/.vscode/srcdev-component-responsive-header.code-snippets +12 -0
  42. package/.vscode/srcdev-component-site-header.code-snippets +17 -0
  43. package/.vscode/srcdev-component-site-navigation.code-snippets +30 -0
  44. package/.vscode/srcdev-component-slider-gallery.code-snippets +38 -0
  45. package/.vscode/srcdev-component-tab-navigation.code-snippets +30 -0
  46. package/app/components/01.atoms/animations/animated-svg-text/AnimatedSvgText.vue +58 -0
  47. package/app/components/01.atoms/animations/animated-svg-text/CONSUMER-STYLING.md +34 -0
  48. package/app/components/01.atoms/animations/animated-svg-text/stories/AnimatedSvgText.stories.ts +69 -0
  49. package/app/components/01.atoms/animations/animated-svg-text/tests/AnimatedSvgText.spec.ts +63 -0
  50. package/app/components/01.atoms/animations/animated-svg-text/tests/__snapshots__/AnimatedSvgText.spec.ts.snap +7 -0
  51. package/app/components/01.atoms/animations/clip-element/CONSUMER-STYLING.md +27 -0
  52. package/app/components/{clip-element → 01.atoms/animations/clip-element}/ClipElement.vue +16 -11
  53. package/app/components/01.atoms/animations/clip-element/stories/ClipElement.stories.ts +133 -0
  54. package/app/components/01.atoms/animations/clip-element/tests/ClipElement.spec.ts +70 -0
  55. package/app/components/01.atoms/animations/clip-element/tests/__snapshots__/ClipElement.spec.ts.snap +7 -0
  56. package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +94 -0
  57. package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +331 -0
  58. package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +151 -0
  59. package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +315 -0
  60. package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +28 -0
  61. package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +27 -0
  62. package/app/components/01.atoms/canvas-switcher/CanvasSwitcher.vue +111 -0
  63. package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +75 -0
  64. package/app/components/01.atoms/canvas-switcher/tests/CanvasSwitcher.spec.ts +67 -0
  65. package/app/components/01.atoms/clipped-panel/CONSUMER-STYLING.md +39 -0
  66. package/app/components/01.atoms/clipped-panel/ClippedPanel.vue +68 -0
  67. package/app/components/01.atoms/clipped-panel/stories/ClippedPanel.stories.ts +105 -0
  68. package/app/components/01.atoms/clipped-panel/tests/ClippedPanel.spec.ts +67 -0
  69. package/app/components/01.atoms/clipped-panel/tests/__snapshots__/ClippedPanel.spec.ts.snap +3 -0
  70. package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +8 -2
  71. package/app/components/01.atoms/display-banner/CONSUMER-STYLING.md +35 -0
  72. package/app/components/{display-banner → 01.atoms/display-banner}/DisplayBanner.vue +13 -35
  73. package/app/components/01.atoms/display-banner/stories/DisplayBanner.stories.ts +100 -0
  74. package/app/components/01.atoms/display-banner/tests/DisplayBanner.spec.ts +78 -0
  75. package/app/components/01.atoms/display-banner/tests/__snapshots__/DisplayBanner.spec.ts.snap +8 -0
  76. package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +62 -0
  77. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +188 -0
  78. package/app/components/01.atoms/display-tooltip/stories/DisplayTooltip.stories.ts +94 -0
  79. package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +75 -0
  80. package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +12 -0
  81. package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +4 -1
  82. package/app/components/02.molecules/display-tooltip-defined/CONSUMER-STYLING.md +42 -0
  83. package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +116 -0
  84. package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +126 -0
  85. package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +66 -0
  86. package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +17 -0
  87. package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +44 -0
  88. package/app/components/{deep-expanding-menu → 02.molecules/navigation/deep-expanding-menu}/DeepExpandingMenu.vue +53 -64
  89. package/app/components/02.molecules/navigation/deep-expanding-menu/stories/DeepExpandingMenu.stories.ts +79 -0
  90. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +92 -0
  91. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +17 -0
  92. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +40 -0
  93. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +200 -0
  94. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/stories/DeepExpandingMenuClassic.stories.ts +79 -0
  95. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/DeepExpandingMenuClassic.spec.ts +82 -0
  96. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/__snapshots__/DeepExpandingMenuClassic.spec.ts.snap +18 -0
  97. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +4 -1
  98. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +4 -1
  99. package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +16 -4
  100. package/app/components/03.organisms/responsive-header/NavigationItems.vue +4 -1
  101. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +16 -3
  102. package/app/components/03.organisms/site-header/SiteHeader.vue +16 -1
  103. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
  104. package/app/types/components/display-tooltip-defined.d.ts +10 -0
  105. package/app/types/components/index.ts +2 -0
  106. package/app/types/components/marquee-scroller.d.ts +10 -0
  107. package/package.json +1 -1
  108. package/app/components/animated-svg-text/AnimatedSvgText.vue +0 -89
  109. package/app/components/canvas-switcher/CanvasSwitcher.vue +0 -104
  110. package/app/components/clipped-panels/ClippedPanel.vue +0 -87
  111. package/app/components/deep-expanding-menu/DeepExpandingMenuOld.vue +0 -218
  112. package/app/components/display-details/DisplayDetailsCore.vue +0 -122
  113. package/app/components/display-tooltip/DisplayTooltip.vue +0 -170
  114. package/app/components/display-tooltip/DisplayTooltipDefined.vue +0 -103
  115. package/app/components/magnetic-navigation/MagneticNavigation.vue +0 -162
  116. package/app/components/marquee-scroller/MarqueeScroller.vue +0 -289
@@ -0,0 +1,68 @@
1
+ <template>
2
+ <component :is="tag" class="clipped-panel" :class="[variant, elementClasses]">
3
+ <slot name="default"></slot>
4
+ </component>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ interface Props {
9
+ tag?: "div" | "p" | "span" | "section" | "article" | "aside" | "header" | "footer" | "main" | "nav" | "ul" | "ol";
10
+ variant?: "circle-cutout" | "rectangle" | "square";
11
+ styleClassPassthrough?: string | string[];
12
+ }
13
+
14
+ const props = withDefaults(defineProps<Props>(), {
15
+ tag: "div",
16
+ variant: "square",
17
+ styleClassPassthrough: () => [],
18
+ });
19
+
20
+ const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
21
+
22
+ watch(
23
+ () => props.styleClassPassthrough,
24
+ () => {
25
+ resetElementClasses(props.styleClassPassthrough);
26
+ }
27
+ );
28
+ </script>
29
+
30
+ <style lang="css">
31
+ @layer components {
32
+ .clipped-panel {
33
+ --_outline-colour: var(--clipped-panel-outline-colour, light-dark(hsl(0, 29%, 3%), hsl(0, 0%, 92%)));
34
+
35
+ background-color: var(--clipped-panel-background-colour, light-dark(hsl(0, 0%, 96%), hsl(0, 0%, 12%)));
36
+ color: var(--_outline-colour);
37
+ outline: var(--clipped-panel-outline-width, 1px) solid var(--_outline-colour);
38
+
39
+ aspect-ratio: 1;
40
+
41
+ /*
42
+ * clip-path: path(...) coordinates are fixed to each variant's exact
43
+ * pixel width below — CSS custom properties cannot be interpolated into
44
+ * a path() string, so these shapes cannot be resized via tokens. See
45
+ * CONSUMER-STYLING.md.
46
+ */
47
+ &.rectangle {
48
+ clip-path: path(
49
+ "M 10, 50 L 140, 50 A 10, 10, 0, 0, 0 150, 40 L 150, 10 A 10, 10, 0, 0, 1 160, 0 L 290, 0 A 10, 10, 0, 0, 1 300, 10 L 300, 190 A 10, 10, 0, 0, 1 290, 200 L 10, 200 A 10, 10, 0, 0, 1 0, 190 L 0, 60 A 10, 10, 0, 0, 1 10, 50 Z"
50
+ );
51
+ width: 300px;
52
+ }
53
+ &.square {
54
+ clip-path: path(
55
+ "M 10, 50 L 90, 50 A 10, 10, 0, 0, 0 100, 40 L 100, 10 A 10, 10, 0, 0, 1 110, 0 L 190, 0 A 10, 10, 0, 0, 1 200, 10 L 200, 190 A 10, 10, 0, 0, 1 190, 200 L 10, 200 A 10, 10, 0, 0, 1 0, 190 L 0, 60 A 10, 10, 0, 0, 1 10, 50 Z"
56
+ );
57
+ width: 200px;
58
+ }
59
+ &.circle-cutout {
60
+ /* 200x200 rounded square (10px corners) with a 50px-radius circular notch bitten out of the top-right corner. */
61
+ clip-path: path(
62
+ "M 10, 0 L 150, 0 A 50, 50, 0, 0, 0 200, 50 L 200, 190 A 10, 10, 0, 0, 1 190, 200 L 10, 200 A 10, 10, 0, 0, 1 0, 190 L 0, 10 A 10, 10, 0, 0, 1 10, 0 Z"
63
+ );
64
+ width: 200px;
65
+ }
66
+ }
67
+ }
68
+ </style>
@@ -0,0 +1,105 @@
1
+ import ClippedPanel from "../ClippedPanel.vue";
2
+ import type { Meta, StoryObj } from "@nuxtjs/storybook";
3
+
4
+ const meta: Meta<typeof ClippedPanel> = {
5
+ title: "Atoms/ClippedPanel",
6
+ component: ClippedPanel,
7
+ argTypes: {
8
+ tag: {
9
+ control: { type: "select" },
10
+ options: ["div", "p", "span", "section", "article", "aside", "header", "footer", "main", "nav", "ul", "ol"],
11
+ description: "Root element tag",
12
+ table: { category: "Basic" },
13
+ },
14
+ variant: {
15
+ control: { type: "select" },
16
+ options: ["square", "rectangle", "circle-cutout"],
17
+ description: "Which fixed-size notched shape to clip the panel to",
18
+ table: { category: "Basic" },
19
+ },
20
+ styleClassPassthrough: {
21
+ table: { disable: true },
22
+ },
23
+ },
24
+ parameters: {
25
+ docs: {
26
+ description: {
27
+ component:
28
+ "A panel clipped to a fixed-size notched shape via `clip-path: path(...)`. Each variant is a specific pixel-size shape (see CONSUMER-STYLING.md) — colour and outline are the only overridable surface.",
29
+ },
30
+ },
31
+ },
32
+ };
33
+
34
+ export default meta;
35
+ type Story = StoryObj<typeof ClippedPanel>;
36
+
37
+ export const Square: Story = {
38
+ args: {
39
+ tag: "div",
40
+ variant: "square",
41
+ },
42
+ render: (args) => ({
43
+ components: { ClippedPanel },
44
+ setup() {
45
+ return { args };
46
+ },
47
+ template: `<ClippedPanel v-bind="args">Square</ClippedPanel>`,
48
+ }),
49
+ };
50
+
51
+ export const Rectangle: Story = {
52
+ args: {
53
+ tag: "div",
54
+ variant: "rectangle",
55
+ },
56
+ render: (args) => ({
57
+ components: { ClippedPanel },
58
+ setup() {
59
+ return { args };
60
+ },
61
+ template: `<ClippedPanel v-bind="args">Rectangle</ClippedPanel>`,
62
+ }),
63
+ };
64
+
65
+ export const CircleCutout: Story = {
66
+ args: {
67
+ tag: "div",
68
+ variant: "circle-cutout",
69
+ },
70
+ render: (args) => ({
71
+ components: { ClippedPanel },
72
+ setup() {
73
+ return { args };
74
+ },
75
+ template: `<ClippedPanel v-bind="args">Circle cutout</ClippedPanel>`,
76
+ }),
77
+ };
78
+
79
+ export const CustomColours: Story = {
80
+ args: {
81
+ tag: "div",
82
+ variant: "square",
83
+ },
84
+ render: (args) => ({
85
+ components: { ClippedPanel },
86
+ setup() {
87
+ return { args };
88
+ },
89
+ template: `
90
+ <ClippedPanel
91
+ v-bind="args"
92
+ style="--clipped-panel-background-colour: #fef3e7; --clipped-panel-outline-colour: #b5651d; --clipped-panel-outline-width: 2px;"
93
+ >
94
+ Custom colours
95
+ </ClippedPanel>
96
+ `,
97
+ }),
98
+ parameters: {
99
+ docs: {
100
+ description: {
101
+ story: "Overriding the public tokens via inline `--clipped-panel-*` custom properties.",
102
+ },
103
+ },
104
+ },
105
+ };
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
3
+ import ClippedPanel from "../ClippedPanel.vue";
4
+
5
+ describe("ClippedPanel", () => {
6
+ it("mounts without error", async () => {
7
+ const wrapper = await mountSuspended(ClippedPanel);
8
+ expect(wrapper.vm).toBeTruthy();
9
+ });
10
+
11
+ it("renders correct HTML structure (default props)", async () => {
12
+ const wrapper = await mountSuspended(ClippedPanel);
13
+ expect(wrapper.html()).toMatchSnapshot();
14
+ });
15
+
16
+ it("renders a <div> by default", async () => {
17
+ const wrapper = await mountSuspended(ClippedPanel);
18
+ expect(wrapper.element.tagName).toBe("DIV");
19
+ });
20
+
21
+ it("renders the given tag", async () => {
22
+ const wrapper = await mountSuspended(ClippedPanel, { props: { tag: "section" } });
23
+ expect(wrapper.element.tagName).toBe("SECTION");
24
+ });
25
+
26
+ it("defaults to the square variant class", async () => {
27
+ const wrapper = await mountSuspended(ClippedPanel);
28
+ expect(wrapper.classes()).toContain("clipped-panel");
29
+ expect(wrapper.classes()).toContain("square");
30
+ });
31
+
32
+ it("applies the rectangle variant class", async () => {
33
+ const wrapper = await mountSuspended(ClippedPanel, { props: { variant: "rectangle" } });
34
+ expect(wrapper.classes()).toContain("rectangle");
35
+ });
36
+
37
+ it("applies the circle-cutout variant class", async () => {
38
+ const wrapper = await mountSuspended(ClippedPanel, { props: { variant: "circle-cutout" } });
39
+ expect(wrapper.classes()).toContain("circle-cutout");
40
+ });
41
+
42
+ it("renders slot content", async () => {
43
+ const wrapper = await mountSuspended(ClippedPanel, {
44
+ slots: { default: "<p class='slot-child'>Hello</p>" },
45
+ });
46
+ expect(wrapper.find(".slot-child").exists()).toBe(true);
47
+ expect(wrapper.find(".slot-child").text()).toBe("Hello");
48
+ });
49
+
50
+ it("applies styleClassPassthrough classes", async () => {
51
+ const wrapper = await mountSuspended(ClippedPanel, {
52
+ props: { styleClassPassthrough: ["custom-class"] },
53
+ });
54
+ expect(wrapper.classes()).toContain("custom-class");
55
+ });
56
+
57
+ it("resets classes when styleClassPassthrough prop changes", async () => {
58
+ const wrapper = await mountSuspended(ClippedPanel, {
59
+ props: { styleClassPassthrough: ["initial-class"] },
60
+ });
61
+ expect(wrapper.classes()).toContain("initial-class");
62
+
63
+ await wrapper.setProps({ styleClassPassthrough: ["updated-class"] });
64
+ expect(wrapper.classes()).not.toContain("initial-class");
65
+ expect(wrapper.classes()).toContain("updated-class");
66
+ });
67
+ });
@@ -0,0 +1,3 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`ClippedPanel > renders correct HTML structure (default props) 1`] = `"<div class="clipped-panel square"></div>"`;
@@ -17,7 +17,7 @@
17
17
  <h3 class="docs-nav-heading">{{ docsNavLabel }}</h3>
18
18
  </template>
19
19
  <template #content>
20
- <nav class="docs-nav-list" aria-label="Docs navigation">
20
+ <nav class="docs-nav-list" :aria-label="docsNavAriaLabel">
21
21
  <ul>
22
22
  <li v-for="item in docsNavItems" :key="item.to">
23
23
  <component
@@ -53,7 +53,7 @@
53
53
  <h3 class="docs-page-nav-heading">{{ docsPageNavLabel }}</h3>
54
54
  </template>
55
55
  <template #content>
56
- <nav class="docs-page-nav-list" aria-label="On this page">
56
+ <nav class="docs-page-nav-list" :aria-label="docsPageNavAriaLabel">
57
57
  <ul>
58
58
  <li v-for="item in docsPageNavItems" :key="item.to">
59
59
  <component
@@ -91,6 +91,10 @@ interface Props {
91
91
  docsPageNavItems?: DocsNavItem[];
92
92
  docsNavLabel?: string;
93
93
  docsPageNavLabel?: string;
94
+ /** aria-label on the docs-nav <nav> landmark — override for localisation. */
95
+ docsNavAriaLabel?: string;
96
+ /** aria-label on the page-nav <nav> landmark — override for localisation. */
97
+ docsPageNavAriaLabel?: string;
94
98
  panelVariant?: "modern" | "classic";
95
99
  styleClassPassthrough?: string | string[];
96
100
  }
@@ -100,6 +104,8 @@ const props = withDefaults(defineProps<Props>(), {
100
104
  docsPageNavItems: () => [],
101
105
  docsNavLabel: "Navigation",
102
106
  docsPageNavLabel: "On this page",
107
+ docsNavAriaLabel: "Docs navigation",
108
+ docsPageNavAriaLabel: "On this page",
103
109
  // Defaults to "classic" — the docs/admin nav toggle relies on contentIsOnTop on mobile, and
104
110
  // ExpandingPanel's ::details-content-based positioning silently fails on WebKit (Safari/iOS),
105
111
  // making the nav unreachable there. See AccordianCore's own variant prop and CLAUDE.md
@@ -0,0 +1,35 @@
1
+ # DisplayBanner — Consumer Styling Guide
2
+
3
+ ## Public token API
4
+
5
+ | Token | Default | Controls |
6
+ |---|---|---|
7
+ | `--display-banner-min-height` | `auto` | Minimum height of the banner |
8
+
9
+ ```css
10
+ .my-page {
11
+ --display-banner-min-height: 40rem;
12
+ }
13
+ ```
14
+
15
+ Or scope to a single instance via `styleClassPassthrough`:
16
+
17
+ ```vue
18
+ <DisplayBanner style-class-passthrough="hero-banner">...</DisplayBanner>
19
+ ```
20
+
21
+ ```css
22
+ .hero-banner {
23
+ --display-banner-min-height: 40rem;
24
+ }
25
+ ```
26
+
27
+ ## Layout
28
+
29
+ `DisplayBanner` stacks its `canvas` and `content` slots on top of each other in a single grid
30
+ area (`grid-template-areas: "banner"`). Whichever slot is rendered later in the DOM sits visually
31
+ on top — put background media (an image, video, `CanvasSwitcher`-style visual) in `canvas` and
32
+ overlaid text/CTAs in `content`, since `content` follows `canvas` in the template.
33
+
34
+ Both slots are conditionally rendered (`v-if="$slots.canvas"` / `v-if="$slots.content"`), so an
35
+ unused slot contributes no empty wrapper `<div>` to the DOM.
@@ -10,41 +10,24 @@
10
10
  </template>
11
11
 
12
12
  <script setup lang="ts">
13
- const props = defineProps({
14
- tag: {
15
- type: String,
16
- default: "div",
17
- validator(value: string) {
18
- return [
19
- "div",
20
- "p",
21
- "span",
22
- "section",
23
- "article",
24
- "aside",
25
- "header",
26
- "footer",
27
- "main",
28
- "nav",
29
- "ul",
30
- "ol",
31
- ].includes(value)
32
- },
33
- },
34
- styleClassPassthrough: {
35
- type: [String, Array] as PropType<string | string[]>,
36
- default: () => [],
37
- },
38
- })
13
+ interface Props {
14
+ tag?: "div" | "p" | "span" | "section" | "article" | "aside" | "header" | "footer" | "main" | "nav" | "ul" | "ol";
15
+ styleClassPassthrough?: string | string[];
16
+ }
17
+
18
+ const props = withDefaults(defineProps<Props>(), {
19
+ tag: "div",
20
+ styleClassPassthrough: () => [],
21
+ });
39
22
 
40
- const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough)
23
+ const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
41
24
 
42
25
  watch(
43
26
  () => props.styleClassPassthrough,
44
27
  () => {
45
- resetElementClasses(props.styleClassPassthrough)
28
+ resetElementClasses(props.styleClassPassthrough);
46
29
  }
47
- )
30
+ );
48
31
  </script>
49
32
 
50
33
  <style lang="css">
@@ -53,16 +36,11 @@ watch(
53
36
  display: grid;
54
37
  grid-template-areas: "banner";
55
38
  container-type: inline-size;
39
+ min-height: var(--display-banner-min-height, auto);
56
40
  overflow: hidden;
57
41
 
58
42
  .canvas {
59
43
  grid-area: banner;
60
-
61
- .image {
62
- object-fit: cover;
63
- width: 100%;
64
- height: 100%;
65
- }
66
44
  }
67
45
 
68
46
  .content {
@@ -0,0 +1,100 @@
1
+ import DisplayBanner from "../DisplayBanner.vue";
2
+ import type { Meta, StoryObj } from "@nuxtjs/storybook";
3
+
4
+ const meta: Meta<typeof DisplayBanner> = {
5
+ title: "Atoms/DisplayBanner",
6
+ component: DisplayBanner,
7
+ argTypes: {
8
+ tag: {
9
+ control: { type: "select" },
10
+ options: ["div", "p", "span", "section", "article", "aside", "header", "footer", "main", "nav", "ul", "ol"],
11
+ description: "Root element tag",
12
+ table: { category: "Basic" },
13
+ },
14
+ styleClassPassthrough: {
15
+ table: { disable: true },
16
+ },
17
+ },
18
+ parameters: {
19
+ docs: {
20
+ description: {
21
+ component:
22
+ "Stacks a `canvas` slot (background media) and a `content` slot (overlaid text/CTAs) in a single grid area. Either slot may be omitted.",
23
+ },
24
+ },
25
+ },
26
+ };
27
+
28
+ export default meta;
29
+ type Story = StoryObj<typeof DisplayBanner>;
30
+
31
+ export const Default: Story = {
32
+ args: {
33
+ tag: "div",
34
+ },
35
+ render: (args) => ({
36
+ components: { DisplayBanner },
37
+ setup() {
38
+ return { args };
39
+ },
40
+ template: `
41
+ <DisplayBanner v-bind="args" style="--display-banner-min-height: 24rem;">
42
+ <template #canvas>
43
+ <div style="width: 100%; height: 100%; background: linear-gradient(135deg, #5b8def, #8f5be9);"></div>
44
+ </template>
45
+ <template #content>
46
+ <div style="display: grid; place-items: center; height: 100%; color: white;">
47
+ <h2 style="margin: 0;">Banner content</h2>
48
+ </div>
49
+ </template>
50
+ </DisplayBanner>
51
+ `,
52
+ }),
53
+ };
54
+
55
+ export const CanvasOnly: Story = {
56
+ args: {
57
+ tag: "div",
58
+ },
59
+ render: (args) => ({
60
+ components: { DisplayBanner },
61
+ setup() {
62
+ return { args };
63
+ },
64
+ template: `
65
+ <DisplayBanner v-bind="args" style="--display-banner-min-height: 16rem;">
66
+ <template #canvas>
67
+ <div style="width: 100%; height: 100%; background: linear-gradient(135deg, #5b8def, #8f5be9);"></div>
68
+ </template>
69
+ </DisplayBanner>
70
+ `,
71
+ }),
72
+ parameters: {
73
+ docs: {
74
+ description: {
75
+ story: "With no `content` slot, no empty `.content` wrapper is rendered.",
76
+ },
77
+ },
78
+ },
79
+ };
80
+
81
+ export const ContentOnly: Story = {
82
+ args: {
83
+ tag: "div",
84
+ },
85
+ render: (args) => ({
86
+ components: { DisplayBanner },
87
+ setup() {
88
+ return { args };
89
+ },
90
+ template: `
91
+ <DisplayBanner v-bind="args" style="--display-banner-min-height: 16rem; background: #1a1a1a;">
92
+ <template #content>
93
+ <div style="display: grid; place-items: center; height: 100%; color: white;">
94
+ <h2 style="margin: 0;">Content, no canvas</h2>
95
+ </div>
96
+ </template>
97
+ </DisplayBanner>
98
+ `,
99
+ }),
100
+ };
@@ -0,0 +1,78 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
3
+ import DisplayBanner from "../DisplayBanner.vue";
4
+
5
+ describe("DisplayBanner", () => {
6
+ it("mounts without error", async () => {
7
+ const wrapper = await mountSuspended(DisplayBanner);
8
+ expect(wrapper.vm).toBeTruthy();
9
+ });
10
+
11
+ it("renders correct HTML structure (default props)", async () => {
12
+ const wrapper = await mountSuspended(DisplayBanner);
13
+ expect(wrapper.html()).toMatchSnapshot();
14
+ });
15
+
16
+ it("renders a <div> by default", async () => {
17
+ const wrapper = await mountSuspended(DisplayBanner);
18
+ expect(wrapper.element.tagName).toBe("DIV");
19
+ });
20
+
21
+ it("renders the given tag", async () => {
22
+ const wrapper = await mountSuspended(DisplayBanner, { props: { tag: "section" } });
23
+ expect(wrapper.element.tagName).toBe("SECTION");
24
+ });
25
+
26
+ it("does not render a .canvas wrapper when the canvas slot is empty", async () => {
27
+ const wrapper = await mountSuspended(DisplayBanner);
28
+ expect(wrapper.find(".canvas").exists()).toBe(false);
29
+ });
30
+
31
+ it("does not render a .content wrapper when the content slot is empty", async () => {
32
+ const wrapper = await mountSuspended(DisplayBanner);
33
+ expect(wrapper.find(".content").exists()).toBe(false);
34
+ });
35
+
36
+ it("renders the canvas slot", async () => {
37
+ const wrapper = await mountSuspended(DisplayBanner, {
38
+ slots: { canvas: "<img class='slot-canvas' />" },
39
+ });
40
+ expect(wrapper.find(".canvas .slot-canvas").exists()).toBe(true);
41
+ });
42
+
43
+ it("renders the content slot", async () => {
44
+ const wrapper = await mountSuspended(DisplayBanner, {
45
+ slots: { content: "<p class='slot-content'>Hello</p>" },
46
+ });
47
+ expect(wrapper.find(".content .slot-content").text()).toBe("Hello");
48
+ });
49
+
50
+ it("renders both slots together", async () => {
51
+ const wrapper = await mountSuspended(DisplayBanner, {
52
+ slots: {
53
+ canvas: "<img class='slot-canvas' />",
54
+ content: "<p class='slot-content'>Hello</p>",
55
+ },
56
+ });
57
+ expect(wrapper.find(".canvas .slot-canvas").exists()).toBe(true);
58
+ expect(wrapper.find(".content .slot-content").exists()).toBe(true);
59
+ });
60
+
61
+ it("applies styleClassPassthrough classes", async () => {
62
+ const wrapper = await mountSuspended(DisplayBanner, {
63
+ props: { styleClassPassthrough: ["custom-class"] },
64
+ });
65
+ expect(wrapper.classes()).toContain("custom-class");
66
+ });
67
+
68
+ it("resets classes when styleClassPassthrough prop changes", async () => {
69
+ const wrapper = await mountSuspended(DisplayBanner, {
70
+ props: { styleClassPassthrough: ["initial-class"] },
71
+ });
72
+ expect(wrapper.classes()).toContain("initial-class");
73
+
74
+ await wrapper.setProps({ styleClassPassthrough: ["updated-class"] });
75
+ expect(wrapper.classes()).not.toContain("initial-class");
76
+ expect(wrapper.classes()).toContain("updated-class");
77
+ });
78
+ });
@@ -0,0 +1,8 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`DisplayBanner > renders correct HTML structure (default props) 1`] = `
4
+ "<div class="display-banner">
5
+ <!--v-if-->
6
+ <!--v-if-->
7
+ </div>"
8
+ `;
@@ -0,0 +1,62 @@
1
+ # DisplayTooltip — Consumer Styling Guide
2
+
3
+ ## Public token API
4
+
5
+ | Token | Default | Controls |
6
+ |---|---|---|
7
+ | `--display-tooltip-padding-block` | `0.8rem` | Vertical padding around the trigger row |
8
+ | `--display-tooltip-trigger-gap` | `0.8rem` | Space between `triggerContent` slot and the trigger button |
9
+ | `--display-tooltip-trigger-icon-box-size` | `2rem` | Trigger icon's box width/height |
10
+ | `--display-tooltip-trigger-icon-font-size` | `1.8rem` | Trigger icon glyph size |
11
+ | `--display-tooltip-trigger-icon-colour` | `var(--theme-text)` | Trigger icon colour |
12
+ | `--display-tooltip-trigger-outline-width` | `0.1rem` | Trigger button focus/hover outline width |
13
+ | `--display-tooltip-trigger-outline-colour-hover` | `var(--theme-ring)` | Trigger button outline colour on hover/focus-visible |
14
+ | `--display-tooltip-popover-width` | `30rem` | Popover panel width |
15
+ | `--display-tooltip-popover-padding` | `1.2rem` | Popover panel content padding |
16
+ | `--display-tooltip-popover-content-gap` | `1.2rem` | Gap between stacked children in the `tooltipContent` slot |
17
+ | `--display-tooltip-popover-outline-width` | `0.1rem` | Popover panel outline (border) width |
18
+ | `--display-tooltip-popover-outline-colour` | `light-dark(var(--slate-02), var(--slate-06))` | Popover panel outline (border) colour |
19
+ | `--display-tooltip-popover-text-colour` | `light-dark(var(--slate-09), var(--slate-01))` | Popover panel text colour |
20
+ | `--display-tooltip-popover-background-colour` | `light-dark(var(--slate-00), var(--slate-07))` | Popover panel background |
21
+ | `--display-tooltip-popover-border-radius` | `0.8rem` | Popover panel corner radius |
22
+ | `--display-tooltip-popover-shadow` | `light-dark(0 0.4rem 1.6rem rgba(0,0,0,.12), 0 0.4rem 1.6rem rgba(0,0,0,.5))` | Popover panel elevation shadow |
23
+ | `--display-tooltip-popover-offset` | `0.1rem` | Gap between the trigger and the popover panel |
24
+ | `--display-tooltip-close-button-colour` | `light-dark(var(--slate-09), var(--slate-01))` | Close button text colour (used by `DisplayTooltipDefined`) |
25
+ | `--display-tooltip-close-button-border-width` | `0.1rem` | Close button border width |
26
+ | `--display-tooltip-close-button-border-colour` | `light-dark(var(--slate-03), var(--slate-06))` | Close button border colour |
27
+ | `--display-tooltip-close-button-outline-width` | `0.1rem` | Close button outline width |
28
+ | `--display-tooltip-close-button-outline-colour` | `transparent` | Close button outline colour at rest (no permanent ring) |
29
+ | `--display-tooltip-close-button-border-colour-hover` | `light-dark(var(--slate-06), var(--slate-03))` | Close button border colour on hover/focus |
30
+ | `--display-tooltip-close-button-outline-colour-hover` | `light-dark(var(--slate-06), var(--slate-03))` | Close button outline colour on hover/focus |
31
+ | `--display-tooltip-close-button-padding` | `0.4rem 1rem` | Close button padding |
32
+
33
+ ```css
34
+ .my-page {
35
+ --display-tooltip-popover-background-colour: #1a1a1a;
36
+ --display-tooltip-popover-text-colour: white;
37
+ --display-tooltip-popover-outline-colour: transparent;
38
+ }
39
+ ```
40
+
41
+ Or scope to a single instance via `styleClassPassthrough`:
42
+
43
+ ```vue
44
+ <DisplayTooltip style-class-passthrough="promo-tooltip">...</DisplayTooltip>
45
+ ```
46
+
47
+ ## Notes
48
+
49
+ - Built on the native Popover API (`popover`/`popovertarget`) and CSS anchor-positioning
50
+ (`anchor-name`/`position-anchor`/`anchor()`) — check current browser support before relying on
51
+ it where broad support is a hard requirement.
52
+ - The `.display-tooltip-close-button` styled here is rendered by `DisplayTooltipDefined`, not this
53
+ component directly — `DisplayTooltip` only provides the `tooltipContent` slot, so a plain
54
+ `DisplayTooltip` usage supplies its own close affordance if it wants one (give it
55
+ `style="align-self: flex-end;"` or a class to right-align it within the content's flex column,
56
+ same as `DisplayTooltipDefined` does internally).
57
+ - 2026-09-07: default colours switched from raw `light-dark(black, white)` to the library's
58
+ neutral `--slate-*` scale, and `.display-tooltip-popover-content` gained default `padding`,
59
+ `display: flex; flex-direction: column;`, and `gap` (previously unstyled, meaning content sat
60
+ flush against the panel edges) plus the popover gained a default elevation `box-shadow`
61
+ (previously none) — the out-of-the-box look was flat and under-styled before this. All of these
62
+ remain overridable via the tokens above.