srcdev-nuxt-components 9.6.9 → 9.6.11
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/rules/consumer/ids-from-useid.md +4 -2
- package/.claude/skills/component-compliance-checklist.md +24 -2
- package/.claude/skills/component-inline-action-button.md +5 -4
- package/.claude/skills/component-naming.md +8 -2
- package/.claude/skills/components/{alert-mask-core.md → alert-mask.md} +38 -23
- package/.claude/skills/components/alert-masked-content.md +3 -3
- package/.claude/skills/components/container-glow.md +1 -1
- package/.claude/skills/components/display-theme-switch.md +8 -8
- package/.claude/skills/components/input-button.md +26 -7
- package/.claude/skills/components/input-checkbox-radio.md +2 -0
- package/.claude/skills/components/input-description.md +5 -0
- package/.claude/skills/components/input-error.md +9 -2
- package/.claude/skills/components/input-label.md +8 -2
- package/.claude/skills/components/input-number.md +2 -0
- package/.claude/skills/components/input-otp.md +2 -0
- package/.claude/skills/components/input-range.md +2 -0
- package/.claude/skills/components/input-select.md +2 -0
- package/.claude/skills/components/input-text.md +2 -0
- package/.claude/skills/components/input-textarea.md +2 -0
- package/.claude/skills/components/pending-effect.md +5 -4
- package/.claude/skills/components/{triple-toggle-switch.md → segmented-control.md} +34 -19
- package/.claude/skills/components/services-card.md +1 -1
- package/.claude/skills/components/tabbed-content.md +1 -1
- package/.claude/skills/components/toggle-switch.md +2 -0
- package/.claude/skills/css-nesting-conventions.md +22 -1
- package/.claude/skills/index.md +5 -5
- package/.claude/skills/storybook-add-story.md +3 -1
- package/.claude/skills/theming-component-token-pattern.md +24 -0
- package/.claude/skills/theming-form-geometry-tokens.md +1 -1
- package/.claude/skills/theming-partial-override.md +1 -1
- package/.vscode/srcdev-component-alert-mask.code-snippets +41 -0
- package/.vscode/{srcdev-component-triple-toggle-switch.code-snippets → srcdev-component-segmented-control.code-snippets} +6 -7
- package/app/assets/styles/setup/03.theming/_theme-slots.css +1 -1
- package/app/assets/styles/setup/04.elements/forms/03.generic-input-geometry.css +4 -0
- package/app/components/01.atoms/alert-mask/AlertMask.vue +194 -0
- package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +57 -42
- package/app/components/01.atoms/alert-mask/stories/AlertMask.stories.ts +198 -0
- package/app/components/01.atoms/alert-mask/tests/AlertMask.spec.ts +233 -0
- package/app/components/02.molecules/alert-masked-content/AlertMaskedContent.vue +2 -2
- package/app/components/02.molecules/alert-masked-content/CONSUMER-STYLING.md +4 -4
- package/app/components/02.molecules/alert-masked-content/stories/AlertMaskedContent.stories.ts +60 -0
- package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +12 -12
- package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +3 -3
- package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +1 -1
- package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +1 -1
- package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +9 -9
- package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +16 -16
- package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +53 -1
- package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +8 -8
- package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +4 -4
- package/app/components/02.molecules/input-copy/InputCopy.vue +2 -2
- package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +2 -2
- package/app/components/03.organisms/google-reviews/tests/GoogleReviews.spec.ts +4 -4
- package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +2 -2
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +2 -2
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +6 -6
- package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +8 -0
- package/app/components/05.forms/form-errors/InputError.vue +43 -37
- package/app/components/05.forms/form-errors/stories/InputError.stories.ts +79 -1
- package/app/components/05.forms/form-errors/tests/InputError.spec.ts +29 -1
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +80 -47
- package/app/components/05.forms/input-button/InputButton.vue +52 -100
- package/app/components/05.forms/input-button/playwright/input-button.playwright.ts +1 -1
- package/app/components/05.forms/input-button/stories/InputButton.stories.ts +145 -8
- package/app/components/05.forms/input-button/tests/InputButton.spec.ts +76 -46
- package/app/components/05.forms/input-button/tests/__snapshots__/InputButton.spec.ts.snap +17 -17
- package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +9 -1
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +8 -0
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +2 -2
- package/app/components/05.forms/input-description/CONSUMER-STYLING.md +14 -2
- package/app/components/05.forms/input-description/InputDescription.vue +2 -1
- package/app/components/05.forms/input-description/stories/InputDescription.stories.ts +70 -4
- package/app/components/05.forms/input-description/tests/InputDescription.spec.ts +2 -1
- package/app/components/05.forms/input-label/CONSUMER-STYLING.md +10 -2
- package/app/components/05.forms/input-label/InputLabel.vue +1 -0
- package/app/components/05.forms/input-label/stories/InputLabel.stories.ts +68 -2
- package/app/components/05.forms/input-label/tests/InputLabel.spec.ts +13 -3
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-number/InputNumber.vue +4 -5
- package/app/components/05.forms/input-otp/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-otp/InputOtp.vue +2 -2
- package/app/components/05.forms/input-range/InputRange.vue +1 -1
- package/app/components/05.forms/input-select/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-select/InputSelect.vue +14 -5
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-text/InputText.vue +4 -4
- package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +7 -1
- package/app/components/05.forms/input-textarea/InputTextarea.vue +2 -3
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +4 -4
- package/app/components/05.forms/pending-effect/CONSUMER-STYLING.md +7 -3
- package/app/components/05.forms/pending-effect/PendingEffect.vue +3 -3
- package/app/components/05.forms/pending-effect/tests/PendingEffect.spec.ts +1 -1
- package/app/components/05.forms/segmented-control/CONSUMER-STYLING.md +125 -0
- package/app/components/05.forms/segmented-control/SegmentedControl.vue +231 -0
- package/app/components/05.forms/segmented-control/stories/SegmentedControl.stories.ts +215 -0
- package/app/components/05.forms/segmented-control/tests/SegmentedControl.spec.ts +202 -0
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +7 -0
- package/app/components/05.forms/toggle-switch/ToggleSwitch.vue +2 -0
- package/app/types/components/index.ts +1 -1
- package/package.json +1 -1
- package/.vscode/srcdev-component-alert-mask-core.code-snippets +0 -31
- package/app/components/01.atoms/alert-mask/AlertMaskCore.vue +0 -166
- package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +0 -105
- package/app/components/01.atoms/alert-mask/tests/AlertMaskCore.spec.ts +0 -342
- package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +0 -99
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitch.vue +0 -251
- package/app/components/05.forms/triple-toggle-switch/stories/TripleToggleSwitch.stories.ts +0 -104
- package/app/components/05.forms/triple-toggle-switch/tests/TripleToggleSwitch.spec.ts +0 -138
- /package/app/types/components/{alert-mask-core.d.ts → alert-mask.d.ts} +0 -0
|
@@ -8,7 +8,7 @@ export * from "./responsive-header.d"
|
|
|
8
8
|
export * from "./display-toast.d"
|
|
9
9
|
export * from "./display-prompt.d"
|
|
10
10
|
export * from "./qr-code.d"
|
|
11
|
-
export * from "./alert-mask
|
|
11
|
+
export * from "./alert-mask.d"
|
|
12
12
|
export * from "./container-glow.d"
|
|
13
13
|
export * from "./hero-text"
|
|
14
14
|
export * from "./social-icons-list.d"
|
package/package.json
CHANGED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"SRCDEV AlertMaskCore Basic": {
|
|
3
|
-
"description": "AlertMaskCore — SVG border/background mask sized to slotted content",
|
|
4
|
-
"scope": "vue,html",
|
|
5
|
-
"body": [
|
|
6
|
-
"<AlertMaskCore>",
|
|
7
|
-
" $0",
|
|
8
|
-
"</AlertMaskCore>"
|
|
9
|
-
]
|
|
10
|
-
},
|
|
11
|
-
"SRCDEV AlertMaskCore Custom Config": {
|
|
12
|
-
"description": "AlertMaskCore with custom colour/geometry config",
|
|
13
|
-
"scope": "vue,html",
|
|
14
|
-
"body": [
|
|
15
|
-
"<AlertMaskCore",
|
|
16
|
-
" :config=\"{",
|
|
17
|
-
" borderColour: '$1var(--theme-accent)',",
|
|
18
|
-
" backgroundColour: '$2rgba(0, 0, 0, 0.3)',",
|
|
19
|
-
" radiusLeft: $38,",
|
|
20
|
-
" radiusRight: $44,",
|
|
21
|
-
" borderLeft: $56,",
|
|
22
|
-
" borderTop: $61,",
|
|
23
|
-
" borderRight: $71,",
|
|
24
|
-
" borderBottom: $81,",
|
|
25
|
-
" }\"",
|
|
26
|
-
">",
|
|
27
|
-
" $0",
|
|
28
|
-
"</AlertMaskCore>"
|
|
29
|
-
]
|
|
30
|
-
}
|
|
31
|
-
}
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="alert-mask-core" :class="[elementClasses]">
|
|
3
|
-
<svg
|
|
4
|
-
class="alert-mask-decorator"
|
|
5
|
-
:style="{ '--_height': svgHeight + 'px' }"
|
|
6
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
7
|
-
aria-hidden="true"
|
|
8
|
-
focusable="false"
|
|
9
|
-
>
|
|
10
|
-
<defs>
|
|
11
|
-
<mask :id="maskId" maskUnits="userSpaceOnUse">
|
|
12
|
-
<path :d="outerPath" fill="white" />
|
|
13
|
-
<path :d="innerPath" fill="black" />
|
|
14
|
-
</mask>
|
|
15
|
-
</defs>
|
|
16
|
-
|
|
17
|
-
<path
|
|
18
|
-
:d="outerPath"
|
|
19
|
-
:style="{ fill: cfg.borderColour }"
|
|
20
|
-
:mask="`url(#${maskId})`"
|
|
21
|
-
vector-effect="non-scaling-stroke"
|
|
22
|
-
/>
|
|
23
|
-
<path :d="innerPath" :style="{ fill: cfg.backgroundColour }" />
|
|
24
|
-
</svg>
|
|
25
|
-
|
|
26
|
-
<div
|
|
27
|
-
class="alert-mask-content"
|
|
28
|
-
:style="{
|
|
29
|
-
'--_inset-inline-start': (props.config?.borderLeft ?? 0) + 'px',
|
|
30
|
-
'--_inset-inline-end': (props.config?.borderRight ?? 0) + 'px',
|
|
31
|
-
'--_inset-block-start': (props.config?.borderTop ?? 0) + 'px',
|
|
32
|
-
'--_inset-block-end': (props.config?.borderBottom ?? 0) + 'px',
|
|
33
|
-
}"
|
|
34
|
-
>
|
|
35
|
-
<div ref="alertContentRef" class="alert-mask-content-slot">
|
|
36
|
-
<slot name="default"></slot>
|
|
37
|
-
</div>
|
|
38
|
-
</div>
|
|
39
|
-
</div>
|
|
40
|
-
</template>
|
|
41
|
-
|
|
42
|
-
<script setup lang="ts">
|
|
43
|
-
import type { AlertMaskConfig } from "~/types/components";
|
|
44
|
-
|
|
45
|
-
interface Props {
|
|
46
|
-
config?: AlertMaskConfig;
|
|
47
|
-
styleClassPassthrough?: string | string[];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
51
|
-
config: () => ({}),
|
|
52
|
-
styleClassPassthrough: () => [],
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
56
|
-
|
|
57
|
-
const maskId = `alert-mask-${useId()}`;
|
|
58
|
-
const alertContentRef = useTemplateRef<HTMLElement | null>("alertContentRef");
|
|
59
|
-
const svgWidth = ref(0);
|
|
60
|
-
const svgHeight = ref(0);
|
|
61
|
-
let resizeObserver: ResizeObserver | null = null;
|
|
62
|
-
|
|
63
|
-
// Update dimensions based on content
|
|
64
|
-
onMounted(() => {
|
|
65
|
-
const updateDimensions = () => {
|
|
66
|
-
const contentEl = alertContentRef.value;
|
|
67
|
-
if (!contentEl) return;
|
|
68
|
-
|
|
69
|
-
const rect = contentEl.getBoundingClientRect();
|
|
70
|
-
const { borderLeft, borderTop, borderRight, borderBottom } = cfg.value;
|
|
71
|
-
|
|
72
|
-
// Calculate total dimensions including borders
|
|
73
|
-
svgWidth.value = rect.width + borderLeft + borderRight;
|
|
74
|
-
svgHeight.value = rect.height + borderTop + borderBottom;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// Initial measurement
|
|
78
|
-
nextTick(updateDimensions);
|
|
79
|
-
|
|
80
|
-
// Observe content changes
|
|
81
|
-
const contentEl = alertContentRef.value;
|
|
82
|
-
if (contentEl) {
|
|
83
|
-
resizeObserver = new ResizeObserver(updateDimensions);
|
|
84
|
-
resizeObserver.observe(contentEl);
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
onBeforeUnmount(() => {
|
|
89
|
-
resizeObserver?.disconnect();
|
|
90
|
-
resizeObserver = null;
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const cfg = computed(() => ({
|
|
94
|
-
backgroundColour: props.config?.backgroundColour ?? "rgba(0,0,0,0.25)",
|
|
95
|
-
borderColour: props.config?.borderColour ?? "var(--orange-08)",
|
|
96
|
-
radiusLeft: props.config?.radiusLeft ?? 12,
|
|
97
|
-
radiusRight: props.config?.radiusRight ?? 12,
|
|
98
|
-
borderLeft: props.config?.borderLeft ?? 8,
|
|
99
|
-
borderTop: props.config?.borderTop ?? 8,
|
|
100
|
-
borderRight: props.config?.borderRight ?? 8,
|
|
101
|
-
borderBottom: props.config?.borderBottom ?? 8,
|
|
102
|
-
}));
|
|
103
|
-
|
|
104
|
-
// Outer border path (fixed radii)
|
|
105
|
-
const outerPath = computed(() => {
|
|
106
|
-
const width = svgWidth.value;
|
|
107
|
-
const height = svgHeight.value;
|
|
108
|
-
const { radiusLeft, radiusRight } = cfg.value;
|
|
109
|
-
|
|
110
|
-
if (!width || !height) return "";
|
|
111
|
-
|
|
112
|
-
return `
|
|
113
|
-
M ${radiusLeft} 0
|
|
114
|
-
L ${width - radiusRight} 0
|
|
115
|
-
Q ${width} 0 ${width} ${radiusRight}
|
|
116
|
-
L ${width} ${height - radiusRight}
|
|
117
|
-
Q ${width} ${height} ${width - radiusRight} ${height}
|
|
118
|
-
L ${radiusLeft} ${height}
|
|
119
|
-
Q 0 ${height} 0 ${height - radiusLeft}
|
|
120
|
-
L 0 ${radiusLeft}
|
|
121
|
-
Q 0 0 ${radiusLeft} 0 Z
|
|
122
|
-
`;
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// Inner cutout (based on fixed pixel border thickness)
|
|
126
|
-
const innerPath = computed(() => {
|
|
127
|
-
const width = svgWidth.value;
|
|
128
|
-
const height = svgHeight.value;
|
|
129
|
-
const { radiusLeft, radiusRight, borderLeft, borderTop, borderRight, borderBottom: borderBottom } = cfg.value;
|
|
130
|
-
|
|
131
|
-
if (!width || !height) return "";
|
|
132
|
-
|
|
133
|
-
return `
|
|
134
|
-
M ${radiusLeft + borderLeft} ${borderTop}
|
|
135
|
-
L ${width - radiusRight - borderRight} ${borderTop}
|
|
136
|
-
Q ${width - borderRight} ${borderTop} ${width - borderRight} ${radiusRight + borderTop}
|
|
137
|
-
L ${width - borderRight} ${height - radiusRight - borderBottom}
|
|
138
|
-
Q ${width - borderRight} ${height - borderBottom} ${width - radiusRight - borderRight} ${height - borderBottom}
|
|
139
|
-
L ${radiusLeft + borderLeft} ${height - borderBottom}
|
|
140
|
-
Q ${borderLeft} ${height - borderBottom} ${borderLeft} ${height - radiusLeft - borderBottom}
|
|
141
|
-
L ${borderLeft} ${radiusLeft + borderTop}
|
|
142
|
-
Q ${borderLeft} ${borderTop} ${radiusLeft + borderLeft} ${borderTop} Z
|
|
143
|
-
`;
|
|
144
|
-
});
|
|
145
|
-
</script>
|
|
146
|
-
|
|
147
|
-
<style lang="css">
|
|
148
|
-
@layer components {
|
|
149
|
-
.alert-mask-core {
|
|
150
|
-
display: grid;
|
|
151
|
-
grid-template-areas: "mask";
|
|
152
|
-
|
|
153
|
-
.alert-mask-decorator {
|
|
154
|
-
grid-area: mask;
|
|
155
|
-
width: 100%;
|
|
156
|
-
height: var(--_height);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
.alert-mask-content {
|
|
160
|
-
grid-area: mask;
|
|
161
|
-
margin-block: var(--_inset-block-start) var(--_inset-block-end);
|
|
162
|
-
margin-inline: var(--_inset-inline-start) var(--_inset-inline-end);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
</style>
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import AlertMaskCore from "../AlertMaskCore.vue";
|
|
2
|
-
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
-
|
|
4
|
-
const meta: Meta<typeof AlertMaskCore> = {
|
|
5
|
-
title: "Atoms/AlertMaskCore",
|
|
6
|
-
component: AlertMaskCore,
|
|
7
|
-
argTypes: {
|
|
8
|
-
config: {
|
|
9
|
-
control: "object",
|
|
10
|
-
description:
|
|
11
|
-
"Border/background colour and geometry: backgroundColour, borderColour, radiusLeft, radiusRight, borderLeft, borderTop, borderRight, borderBottom",
|
|
12
|
-
table: { category: "Basic" },
|
|
13
|
-
},
|
|
14
|
-
styleClassPassthrough: {
|
|
15
|
-
table: { disable: true },
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
parameters: {
|
|
19
|
-
docs: {
|
|
20
|
-
description: {
|
|
21
|
-
component:
|
|
22
|
-
"Draws a border/background mask around slotted content via an SVG path, sized to the measured content. Colour and geometry are set via the `config` prop, not CSS tokens — see CONSUMER-STYLING.md.",
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export default meta;
|
|
29
|
-
type Story = StoryObj<typeof AlertMaskCore>;
|
|
30
|
-
|
|
31
|
-
// The mask is a translucent fill plus a cut-out border, so it needs something busy behind it to read at all.
|
|
32
|
-
const backdrop =
|
|
33
|
-
"background: url('/images/banners/video/lake-banner.jpg') center / cover; padding: 4rem 2.4rem; border-radius: 1.2rem; max-width: 60rem;";
|
|
34
|
-
const gradientBackdrop =
|
|
35
|
-
"background: repeating-linear-gradient(45deg, oklch(100% 0 0 / 0.18) 0 1.2rem, transparent 1.2rem 2.4rem), linear-gradient(120deg, oklch(70% 0.2 30), oklch(65% 0.2 200) 60%, oklch(55% 0.22 300)); padding: 4rem 2.4rem; border-radius: 1.2rem; max-width: 60rem;";
|
|
36
|
-
|
|
37
|
-
export const CustomConfig: Story = {
|
|
38
|
-
args: {
|
|
39
|
-
config: {
|
|
40
|
-
backgroundColour: "rgba(0, 0, 0, 0.3)",
|
|
41
|
-
borderColour: "var(--theme-accent)",
|
|
42
|
-
radiusLeft: 8,
|
|
43
|
-
radiusRight: 4,
|
|
44
|
-
borderLeft: 6,
|
|
45
|
-
borderTop: 1,
|
|
46
|
-
borderRight: 1,
|
|
47
|
-
borderBottom: 1,
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
render: (args) => ({
|
|
51
|
-
components: { AlertMaskCore },
|
|
52
|
-
setup() {
|
|
53
|
-
return { args, backdrop };
|
|
54
|
-
},
|
|
55
|
-
template: `
|
|
56
|
-
<div data-theme="info" :style="backdrop">
|
|
57
|
-
<AlertMaskCore v-bind="args">
|
|
58
|
-
<p style="margin: 0; padding: 1.6rem; color: white;">Custom accent-bar config, matching AlertMaskedContent's default shape</p>
|
|
59
|
-
</AlertMaskCore>
|
|
60
|
-
</div>
|
|
61
|
-
`,
|
|
62
|
-
}),
|
|
63
|
-
parameters: {
|
|
64
|
-
docs: {
|
|
65
|
-
description: {
|
|
66
|
-
story: "The accent-bar shape used by AlertMaskedContent: thick left radius/border, thin elsewhere.",
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
export const OverGradient: Story = {
|
|
73
|
-
args: {
|
|
74
|
-
config: {
|
|
75
|
-
backgroundColour: "rgba(0, 0, 0, 0.3)",
|
|
76
|
-
borderColour: "white",
|
|
77
|
-
radiusLeft: 16,
|
|
78
|
-
radiusRight: 16,
|
|
79
|
-
borderLeft: 4,
|
|
80
|
-
borderTop: 4,
|
|
81
|
-
borderRight: 4,
|
|
82
|
-
borderBottom: 4,
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
render: (args) => ({
|
|
86
|
-
components: { AlertMaskCore },
|
|
87
|
-
setup() {
|
|
88
|
-
return { args, gradientBackdrop };
|
|
89
|
-
},
|
|
90
|
-
template: `
|
|
91
|
-
<div :style="gradientBackdrop">
|
|
92
|
-
<AlertMaskCore v-bind="args">
|
|
93
|
-
<p style="margin: 0; padding: 1.6rem; color: white;">Even border all round: the stripes show through the fill and stop at the border's inner edge.</p>
|
|
94
|
-
</AlertMaskCore>
|
|
95
|
-
</div>
|
|
96
|
-
`,
|
|
97
|
-
}),
|
|
98
|
-
parameters: {
|
|
99
|
-
docs: {
|
|
100
|
-
description: {
|
|
101
|
-
story: "A striped gradient behind an even 4px white border, so the translucent fill and the border's cut-out are both visible.",
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
};
|
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
-
import AlertMaskCore from "../AlertMaskCore.vue";
|
|
4
|
-
import type { AlertMaskConfig } from "../../../../types/components/alert-mask-core.d";
|
|
5
|
-
|
|
6
|
-
// Type interface for AlertMaskCore component instance
|
|
7
|
-
interface AlertMaskCoreInstance {
|
|
8
|
-
cfg: {
|
|
9
|
-
backgroundColour: string;
|
|
10
|
-
borderColour: string;
|
|
11
|
-
radiusLeft: number;
|
|
12
|
-
radiusRight: number;
|
|
13
|
-
borderLeft: number;
|
|
14
|
-
borderTop: number;
|
|
15
|
-
borderRight: number;
|
|
16
|
-
borderBottom: number;
|
|
17
|
-
};
|
|
18
|
-
alertContentRef: HTMLElement | null;
|
|
19
|
-
svgWidth: number;
|
|
20
|
-
svgHeight: number;
|
|
21
|
-
outerPath: string;
|
|
22
|
-
innerPath: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Mock ResizeObserver
|
|
26
|
-
// Vitest 4: a mock's implementation must be a regular function (not an
|
|
27
|
-
// arrow function) to remain constructible via `new`.
|
|
28
|
-
const mockResizeObserver = vi.fn(function () {
|
|
29
|
-
return {
|
|
30
|
-
observe: vi.fn(),
|
|
31
|
-
unobserve: vi.fn(),
|
|
32
|
-
disconnect: vi.fn(),
|
|
33
|
-
};
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
vi.stubGlobal("ResizeObserver", mockResizeObserver);
|
|
37
|
-
|
|
38
|
-
describe("AlertMaskCore", () => {
|
|
39
|
-
beforeEach(() => {
|
|
40
|
-
vi.clearAllMocks();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
afterEach(() => {
|
|
44
|
-
vi.restoreAllMocks();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("mounts without error", async () => {
|
|
48
|
-
const wrapper = await mountSuspended(AlertMaskCore);
|
|
49
|
-
expect(wrapper.vm).toBeTruthy();
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("renders base structure correctly", async () => {
|
|
53
|
-
const wrapper = await mountSuspended(AlertMaskCore);
|
|
54
|
-
|
|
55
|
-
expect(wrapper.find(".alert-mask-core").exists()).toBe(true);
|
|
56
|
-
expect(wrapper.find(".alert-mask-decorator").exists()).toBe(true);
|
|
57
|
-
expect(wrapper.find(".alert-mask-content").exists()).toBe(true);
|
|
58
|
-
expect(wrapper.find(".alert-mask-content-slot").exists()).toBe(true);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("renders default slot content", async () => {
|
|
62
|
-
const wrapper = await mountSuspended(AlertMaskCore, {
|
|
63
|
-
slots: {
|
|
64
|
-
default: "<p>Test alert content</p>",
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
expect(wrapper.find(".alert-mask-content-slot p").text()).toBe("Test alert content");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("applies style class passthrough correctly", async () => {
|
|
72
|
-
const wrapper = await mountSuspended(AlertMaskCore, {
|
|
73
|
-
props: {
|
|
74
|
-
styleClassPassthrough: ["custom-class", "another-class"],
|
|
75
|
-
},
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const alertMask = wrapper.find(".alert-mask-core");
|
|
79
|
-
expect(alertMask.classes()).toContain("custom-class");
|
|
80
|
-
expect(alertMask.classes()).toContain("another-class");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("applies style class passthrough as string", async () => {
|
|
84
|
-
const wrapper = await mountSuspended(AlertMaskCore, {
|
|
85
|
-
props: {
|
|
86
|
-
styleClassPassthrough: "single-class",
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const alertMask = wrapper.find(".alert-mask-core");
|
|
91
|
-
expect(alertMask.classes()).toContain("single-class");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("uses default configuration when no config provided", async () => {
|
|
95
|
-
const wrapper = await mountSuspended(AlertMaskCore);
|
|
96
|
-
|
|
97
|
-
// Check that computed config uses defaults
|
|
98
|
-
const vm = wrapper.vm as unknown as AlertMaskCoreInstance;
|
|
99
|
-
expect(vm.cfg.backgroundColour).toBe("rgba(0,0,0,0.25)");
|
|
100
|
-
expect(vm.cfg.borderColour).toBe("var(--orange-08)");
|
|
101
|
-
expect(vm.cfg.radiusLeft).toBe(12);
|
|
102
|
-
expect(vm.cfg.radiusRight).toBe(12);
|
|
103
|
-
expect(vm.cfg.borderLeft).toBe(8);
|
|
104
|
-
expect(vm.cfg.borderTop).toBe(8);
|
|
105
|
-
expect(vm.cfg.borderRight).toBe(8);
|
|
106
|
-
expect(vm.cfg.borderBottom).toBe(8);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("applies custom configuration correctly", async () => {
|
|
110
|
-
const config: AlertMaskConfig = {
|
|
111
|
-
backgroundColour: "rgba(255,0,0,0.5)",
|
|
112
|
-
borderColour: "#ff0000",
|
|
113
|
-
radiusLeft: 20,
|
|
114
|
-
radiusRight: 15,
|
|
115
|
-
borderLeft: 12,
|
|
116
|
-
borderTop: 10,
|
|
117
|
-
borderRight: 8,
|
|
118
|
-
borderBottom: 6,
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const wrapper = await mountSuspended(AlertMaskCore, {
|
|
122
|
-
props: { config },
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
const vm = wrapper.vm as unknown as AlertMaskCoreInstance;
|
|
126
|
-
expect(vm.cfg.backgroundColour).toBe("rgba(255,0,0,0.5)");
|
|
127
|
-
expect(vm.cfg.borderColour).toBe("#ff0000");
|
|
128
|
-
expect(vm.cfg.radiusLeft).toBe(20);
|
|
129
|
-
expect(vm.cfg.radiusRight).toBe(15);
|
|
130
|
-
expect(vm.cfg.borderLeft).toBe(12);
|
|
131
|
-
expect(vm.cfg.borderTop).toBe(10);
|
|
132
|
-
expect(vm.cfg.borderRight).toBe(8);
|
|
133
|
-
expect(vm.cfg.borderBottom).toBe(6);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("applies partial configuration with defaults", async () => {
|
|
137
|
-
const config: AlertMaskConfig = {
|
|
138
|
-
backgroundColour: "blue",
|
|
139
|
-
radiusLeft: 25,
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const wrapper = await mountSuspended(AlertMaskCore, {
|
|
143
|
-
props: { config },
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
const vm = wrapper.vm as unknown as AlertMaskCoreInstance;
|
|
147
|
-
expect(vm.cfg.backgroundColour).toBe("blue");
|
|
148
|
-
expect(vm.cfg.radiusLeft).toBe(25);
|
|
149
|
-
// Check defaults are maintained for other values
|
|
150
|
-
expect(vm.cfg.borderColour).toBe("var(--orange-08)");
|
|
151
|
-
expect(vm.cfg.radiusRight).toBe(12);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("sets up template ref for content element", async () => {
|
|
155
|
-
const wrapper = await mountSuspended(AlertMaskCore, {
|
|
156
|
-
slots: {
|
|
157
|
-
default: "<div>Content</div>",
|
|
158
|
-
},
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
await nextTick();
|
|
162
|
-
const vm = wrapper.vm as unknown as AlertMaskCoreInstance;
|
|
163
|
-
expect(vm.alertContentRef).toBeTruthy();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("creates SVG with proper structure", async () => {
|
|
167
|
-
const wrapper = await mountSuspended(AlertMaskCore);
|
|
168
|
-
|
|
169
|
-
const svg = wrapper.find(".alert-mask-decorator");
|
|
170
|
-
expect(svg.element.tagName).toBe("svg");
|
|
171
|
-
|
|
172
|
-
// Check for defs and mask
|
|
173
|
-
expect(wrapper.find("defs").exists()).toBe(true);
|
|
174
|
-
const mask = wrapper.find("mask");
|
|
175
|
-
expect(mask.exists()).toBe(true);
|
|
176
|
-
expect(wrapper.find("path[mask]").attributes("mask")).toBe(`url(#${mask.attributes("id")})`);
|
|
177
|
-
|
|
178
|
-
// Check for paths
|
|
179
|
-
const paths = wrapper.findAll("path");
|
|
180
|
-
expect(paths.length).toBe(4); // 2 in mask, 2 visible paths
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("gives each instance its own mask id so multiple masks on a page don't share one shape", async () => {
|
|
184
|
-
const wrapper = await mountSuspended({
|
|
185
|
-
components: { AlertMaskCore },
|
|
186
|
-
template: "<div><AlertMaskCore></AlertMaskCore><AlertMaskCore></AlertMaskCore></div>",
|
|
187
|
-
});
|
|
188
|
-
const ids = wrapper.findAll("mask").map((mask) => mask.attributes("id"));
|
|
189
|
-
expect(ids).toHaveLength(2);
|
|
190
|
-
expect(ids[0]).toBeTruthy();
|
|
191
|
-
expect(ids[0]).not.toBe(ids[1]);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("hides the decorative SVG from assistive technology", async () => {
|
|
195
|
-
const wrapper = await mountSuspended(AlertMaskCore);
|
|
196
|
-
expect(wrapper.find(".alert-mask-decorator").attributes("aria-hidden")).toBe("true");
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("applies config colours as fill styles so CSS var() values resolve", async () => {
|
|
200
|
-
const wrapper = await mountSuspended(AlertMaskCore, {
|
|
201
|
-
props: { config: { borderColour: "var(--my-border)", backgroundColour: "var(--my-bg)" } },
|
|
202
|
-
});
|
|
203
|
-
const paths = wrapper.findAll(".alert-mask-decorator > path");
|
|
204
|
-
expect((paths[0]!.element as unknown as SVGElement).style.fill).toBe("var(--my-border)");
|
|
205
|
-
expect((paths[1]!.element as unknown as SVGElement).style.fill).toBe("var(--my-bg)");
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("disconnects its ResizeObserver on unmount", async () => {
|
|
209
|
-
const wrapper = await mountSuspended(AlertMaskCore);
|
|
210
|
-
const observer = mockResizeObserver.mock.results.at(-1)?.value as { disconnect: ReturnType<typeof vi.fn> };
|
|
211
|
-
wrapper.unmount();
|
|
212
|
-
expect(observer.disconnect).toHaveBeenCalled();
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("applies CSS custom properties for content positioning", async () => {
|
|
216
|
-
const config: AlertMaskConfig = {
|
|
217
|
-
borderLeft: 15,
|
|
218
|
-
borderTop: 20,
|
|
219
|
-
borderRight: 25,
|
|
220
|
-
borderBottom: 30,
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const wrapper = await mountSuspended(AlertMaskCore, {
|
|
224
|
-
props: { config },
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
const contentEl = wrapper.find(".alert-mask-content");
|
|
228
|
-
const style = (contentEl.element as HTMLElement).style;
|
|
229
|
-
|
|
230
|
-
expect(style.getPropertyValue("--_inset-inline-start")).toBe("15px");
|
|
231
|
-
expect(style.getPropertyValue("--_inset-block-start")).toBe("20px");
|
|
232
|
-
expect(style.getPropertyValue("--_inset-inline-end")).toBe("25px");
|
|
233
|
-
expect(style.getPropertyValue("--_inset-block-end")).toBe("30px");
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it("handles zero border values correctly", async () => {
|
|
237
|
-
const config: AlertMaskConfig = {
|
|
238
|
-
borderLeft: 0,
|
|
239
|
-
borderTop: 0,
|
|
240
|
-
borderRight: 0,
|
|
241
|
-
borderBottom: 0,
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
const wrapper = await mountSuspended(AlertMaskCore, {
|
|
245
|
-
props: { config },
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
const contentEl = wrapper.find(".alert-mask-content");
|
|
249
|
-
const style = (contentEl.element as HTMLElement).style;
|
|
250
|
-
|
|
251
|
-
expect(style.getPropertyValue("--_inset-inline-start")).toBe("0px");
|
|
252
|
-
expect(style.getPropertyValue("--_inset-block-start")).toBe("0px");
|
|
253
|
-
expect(style.getPropertyValue("--_inset-inline-end")).toBe("0px");
|
|
254
|
-
expect(style.getPropertyValue("--_inset-block-end")).toBe("0px");
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it("sets up ResizeObserver on mount", async () => {
|
|
258
|
-
await mountSuspended(AlertMaskCore, {
|
|
259
|
-
slots: {
|
|
260
|
-
default: "<div>Content to observe</div>",
|
|
261
|
-
},
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
// ResizeObserver should be instantiated
|
|
265
|
-
expect(mockResizeObserver).toHaveBeenCalled();
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it("generates SVG paths when dimensions are available", async () => {
|
|
269
|
-
const wrapper = await mountSuspended(AlertMaskCore);
|
|
270
|
-
const vm = wrapper.vm as unknown as AlertMaskCoreInstance;
|
|
271
|
-
|
|
272
|
-
// Set dimensions
|
|
273
|
-
vm.svgWidth = 200;
|
|
274
|
-
vm.svgHeight = 100;
|
|
275
|
-
|
|
276
|
-
await nextTick();
|
|
277
|
-
|
|
278
|
-
// Check that paths are generated (not empty)
|
|
279
|
-
expect(vm.outerPath).toBeTruthy();
|
|
280
|
-
expect(vm.innerPath).toBeTruthy();
|
|
281
|
-
expect(vm.outerPath.length).toBeGreaterThan(0);
|
|
282
|
-
expect(vm.innerPath.length).toBeGreaterThan(0);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it("returns empty paths when dimensions are zero", async () => {
|
|
286
|
-
const wrapper = await mountSuspended(AlertMaskCore);
|
|
287
|
-
const vm = wrapper.vm as unknown as AlertMaskCoreInstance;
|
|
288
|
-
|
|
289
|
-
// Explicitly set dimensions to zero to test the condition
|
|
290
|
-
vm.svgWidth = 0;
|
|
291
|
-
vm.svgHeight = 0;
|
|
292
|
-
|
|
293
|
-
await nextTick();
|
|
294
|
-
|
|
295
|
-
expect(vm.outerPath).toBe("");
|
|
296
|
-
expect(vm.innerPath).toBe("");
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it("updates SVG height CSS custom property", async () => {
|
|
300
|
-
const wrapper = await mountSuspended(AlertMaskCore);
|
|
301
|
-
const vm = wrapper.vm as unknown as AlertMaskCoreInstance;
|
|
302
|
-
|
|
303
|
-
vm.svgHeight = 150;
|
|
304
|
-
await nextTick();
|
|
305
|
-
|
|
306
|
-
const svg = wrapper.find(".alert-mask-decorator");
|
|
307
|
-
const style = (svg.element as HTMLElement).style;
|
|
308
|
-
expect(style.getPropertyValue("--_height")).toBe("150px");
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
it("handles empty configuration object", async () => {
|
|
312
|
-
const wrapper = await mountSuspended(AlertMaskCore, {
|
|
313
|
-
props: {
|
|
314
|
-
config: {},
|
|
315
|
-
},
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// Should use all defaults when empty config provided
|
|
319
|
-
const vm = wrapper.vm as unknown as AlertMaskCoreInstance;
|
|
320
|
-
expect(vm.cfg.backgroundColour).toBe("rgba(0,0,0,0.25)");
|
|
321
|
-
expect(vm.cfg.borderColour).toBe("var(--orange-08)");
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it("maintains reactivity with config changes", async () => {
|
|
325
|
-
const wrapper = await mountSuspended(AlertMaskCore, {
|
|
326
|
-
props: {
|
|
327
|
-
config: { backgroundColour: "red" },
|
|
328
|
-
},
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
let vm = wrapper.vm as unknown as AlertMaskCoreInstance;
|
|
332
|
-
expect(vm.cfg.backgroundColour).toBe("red");
|
|
333
|
-
|
|
334
|
-
// Update props
|
|
335
|
-
await wrapper.setProps({
|
|
336
|
-
config: { backgroundColour: "blue" },
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
vm = wrapper.vm as unknown as AlertMaskCoreInstance;
|
|
340
|
-
expect(vm.cfg.backgroundColour).toBe("blue");
|
|
341
|
-
});
|
|
342
|
-
});
|