srcdev-nuxt-components 9.6.10 → 9.6.12
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 +14 -3
- package/.claude/skills/component-inline-action-button.md +5 -4
- package/.claude/skills/component-naming.md +2 -1
- 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/form-field.md +15 -0
- package/.claude/skills/components/form-fieldset.md +18 -2
- 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/marquee-scroller.md +51 -14
- 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/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-form-field.code-snippets +7 -6
- package/.vscode/srcdev-component-form-fieldset.code-snippets +12 -12
- package/.vscode/srcdev-component-marquee-scroller.code-snippets +31 -28
- 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/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +77 -54
- package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +103 -91
- package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +132 -3
- package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +190 -102
- package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +17 -13
- 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/form-field/CONSUMER-STYLING.md +7 -0
- package/app/components/05.forms/form-field/stories/FormField.stories.ts +116 -2
- package/app/components/05.forms/form-fieldset/CONSUMER-STYLING.md +11 -1
- package/app/components/05.forms/form-fieldset/FormFieldset.vue +4 -2
- package/app/components/05.forms/form-fieldset/stories/FormFieldset.stories.ts +68 -4
- package/app/components/05.forms/form-fieldset/tests/FormFieldset.spec.ts +10 -0
- 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 +8 -0
- 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
|
@@ -5,16 +5,17 @@
|
|
|
5
5
|
"body": [
|
|
6
6
|
"<MarqueeScroller",
|
|
7
7
|
" :marquee-data=\"[",
|
|
8
|
-
" { id: 1, content: '$
|
|
9
|
-
" { id: 2, content: '$
|
|
8
|
+
" { id: 1, content: '${1:logo-a}' },",
|
|
9
|
+
" { id: 2, content: '${2:logo-b}' },",
|
|
10
10
|
" ]\"",
|
|
11
11
|
" :item-config=\"{ width: '${3:120px}', height: '${4:60px}', gap: '${5:24px}' }\"",
|
|
12
|
+
" :style-class-passthrough=\"[${6:}]\"",
|
|
12
13
|
">",
|
|
13
14
|
" <template #1>",
|
|
14
|
-
" $
|
|
15
|
+
" $7",
|
|
15
16
|
" </template>",
|
|
16
17
|
" <template #2>",
|
|
17
|
-
" $
|
|
18
|
+
" $8",
|
|
18
19
|
" </template>",
|
|
19
20
|
"</MarqueeScroller>"
|
|
20
21
|
]
|
|
@@ -24,11 +25,11 @@
|
|
|
24
25
|
"scope": "vue,html",
|
|
25
26
|
"body": [
|
|
26
27
|
"<MarqueeScroller",
|
|
27
|
-
" :marquee-data=\"$
|
|
28
|
+
" :marquee-data=\"${1:items}\"",
|
|
28
29
|
" show-controls",
|
|
29
|
-
" aria-label=\"$
|
|
30
|
+
" aria-label=\"${2:Scrolling client logos}\"",
|
|
30
31
|
">",
|
|
31
|
-
" <template v-for=\"item in $
|
|
32
|
+
" <template v-for=\"item in ${1:items}\" #[item.id] :key=\"item.id\">",
|
|
32
33
|
" $3",
|
|
33
34
|
" </template>",
|
|
34
35
|
"</MarqueeScroller>"
|
|
@@ -39,14 +40,14 @@
|
|
|
39
40
|
"scope": "vue,html",
|
|
40
41
|
"body": [
|
|
41
42
|
"<MarqueeScroller",
|
|
42
|
-
" :marquee-data=\"$
|
|
43
|
+
" :marquee-data=\"${1:items}\"",
|
|
43
44
|
" show-controls",
|
|
44
45
|
" play-icon=\"${2:mdi:play-circle}\"",
|
|
45
46
|
" pause-icon=\"${3:mdi:pause-circle}\"",
|
|
46
47
|
" play-label=\"${4:Play}\"",
|
|
47
48
|
" pause-label=\"${5:Pause}\"",
|
|
48
49
|
">",
|
|
49
|
-
" <template v-for=\"item in $
|
|
50
|
+
" <template v-for=\"item in ${1:items}\" #[item.id] :key=\"item.id\">",
|
|
50
51
|
" $6",
|
|
51
52
|
" </template>",
|
|
52
53
|
"</MarqueeScroller>"
|
|
@@ -56,7 +57,7 @@
|
|
|
56
57
|
"description": "MarqueeScroller with the toggle-icon slot fully replacing the control button's icon",
|
|
57
58
|
"scope": "vue,html",
|
|
58
59
|
"body": [
|
|
59
|
-
"<MarqueeScroller :marquee-data=\"$
|
|
60
|
+
"<MarqueeScroller :marquee-data=\"${1:items}\" show-controls>",
|
|
60
61
|
" <template #toggle-icon=\"{ isPaused }\">",
|
|
61
62
|
" $2",
|
|
62
63
|
" </template>",
|
|
@@ -67,25 +68,27 @@
|
|
|
67
68
|
"description": "CSS override scaffold for MarqueeScroller — scope to a page or section class",
|
|
68
69
|
"scope": "css",
|
|
69
70
|
"body": [
|
|
70
|
-
".$
|
|
71
|
+
".${1:my-page} {",
|
|
71
72
|
" .marquee-scroller {",
|
|
72
|
-
" --marquee-scroller-fade-width: ;",
|
|
73
|
-
" --marquee-scroller-focus-outline-width: ;",
|
|
74
|
-
" --marquee-scroller-focus-outline-
|
|
75
|
-
" --marquee-scroller-
|
|
76
|
-
" --marquee-scroller-item-border-
|
|
77
|
-
" --marquee-scroller-item-border-
|
|
78
|
-
" --marquee-scroller-item-
|
|
79
|
-
" --marquee-scroller-
|
|
80
|
-
" --marquee-scroller-
|
|
81
|
-
" --marquee-scroller-
|
|
82
|
-
" --marquee-scroller-control-
|
|
83
|
-
" --marquee-scroller-control-background-colour
|
|
84
|
-
" --marquee-scroller-control-
|
|
85
|
-
" --marquee-scroller-control-
|
|
86
|
-
" --marquee-scroller-control-border-
|
|
87
|
-
" --marquee-scroller-control-
|
|
88
|
-
" --marquee-scroller-control-
|
|
73
|
+
" --marquee-scroller-fade-width: ${2:10%};",
|
|
74
|
+
" --marquee-scroller-focus-outline-width: ${3:2px};",
|
|
75
|
+
" --marquee-scroller-focus-outline-colour: ${4:var(--theme-ring)};",
|
|
76
|
+
" --marquee-scroller-focus-outline-offset: ${5:2px};",
|
|
77
|
+
" --marquee-scroller-item-border-colour: ${6:var(--slate-10)};",
|
|
78
|
+
" --marquee-scroller-item-border-width: ${7:1px};",
|
|
79
|
+
" --marquee-scroller-item-border-radius: ${8:4px};",
|
|
80
|
+
" --marquee-scroller-item-transition-duration: ${9:0.5s};",
|
|
81
|
+
" --marquee-scroller-group-hover-filter: ${10:grayscale(1)};",
|
|
82
|
+
" --marquee-scroller-item-hover-filter: ${11:grayscale(0)};",
|
|
83
|
+
" --marquee-scroller-control-offset: ${12:8px};",
|
|
84
|
+
" --marquee-scroller-control-background-colour: ${13:rgba(0, 0, 0, 0.7)};",
|
|
85
|
+
" --marquee-scroller-control-background-colour-hover: ${14:rgba(0, 0, 0, 0.9)};",
|
|
86
|
+
" --marquee-scroller-control-text-colour: ${15:white};",
|
|
87
|
+
" --marquee-scroller-control-border-radius: ${16:4px};",
|
|
88
|
+
" --marquee-scroller-control-border-width: ${17:1px};",
|
|
89
|
+
" --marquee-scroller-control-padding: ${18:8px};",
|
|
90
|
+
" --marquee-scroller-control-font-size: ${19:14px};",
|
|
91
|
+
" --marquee-scroller-control-transition-duration: ${20:0.2s};",
|
|
89
92
|
" }",
|
|
90
93
|
"}"
|
|
91
94
|
]
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
-
"SRCDEV
|
|
3
|
-
"description": "
|
|
2
|
+
"SRCDEV SegmentedControl": {
|
|
3
|
+
"description": "SegmentedControl: three-option radio pill with a sliding marker",
|
|
4
4
|
"scope": "vue,html",
|
|
5
5
|
"body": [
|
|
6
|
-
"<
|
|
6
|
+
"<SegmentedControl",
|
|
7
7
|
" v-model=\"${1:selected}\"",
|
|
8
8
|
" v-model:field-data=\"${2:options}\"",
|
|
9
|
-
"
|
|
10
|
-
" aria-label=\"${4:Choose an option}\"",
|
|
9
|
+
" aria-label=\"${3:Choose an option}\"",
|
|
11
10
|
"/>"
|
|
12
11
|
]
|
|
13
12
|
},
|
|
14
|
-
"SRCDEV
|
|
15
|
-
"description": "IFormMultipleOptions data for
|
|
13
|
+
"SRCDEV SegmentedControl Options": {
|
|
14
|
+
"description": "IFormMultipleOptions data for SegmentedControl (system/light/dark values get marker gradients)",
|
|
16
15
|
"scope": "typescript,vue",
|
|
17
16
|
"body": [
|
|
18
17
|
"const ${1:options} = ref<IFormMultipleOptions>({",
|
|
@@ -13,6 +13,6 @@
|
|
|
13
13
|
--theme-ring: var(--colour-theme-1);
|
|
14
14
|
--theme-on-surface: var(--colour-theme-0);
|
|
15
15
|
--theme-text: var(--colour-theme-9);
|
|
16
|
-
/* Light step: pairs with the dark --theme-surface-inverted
|
|
16
|
+
/* Light step: pairs with the dark --theme-surface-inverted. */
|
|
17
17
|
--theme-text-inverted: var(--colour-theme-0);
|
|
18
18
|
}
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
--form-element-outline-width-focus: 0.2rem;
|
|
21
21
|
--form-element-outline-offset-focus: 0.2rem;
|
|
22
22
|
|
|
23
|
+
/* Resting shadow ring outside bordered controls. Its colour, --form-element-shadow-colour, is unset
|
|
24
|
+
so each control falls back to its own theme step 2 (red when invalid). */
|
|
25
|
+
--form-element-shadow-width: 0.2rem;
|
|
26
|
+
|
|
23
27
|
/* Focus */
|
|
24
28
|
--input-focus-ring-width: 0.2rem; /* 2px */
|
|
25
29
|
--input-focus-ring-offset: 0.2rem;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="alert-mask" :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
|
+
class="alert-mask-border"
|
|
19
|
+
:d="outerPath"
|
|
20
|
+
:style="cfg.borderColour ? { fill: cfg.borderColour } : undefined"
|
|
21
|
+
:mask="`url(#${maskId})`"
|
|
22
|
+
/>
|
|
23
|
+
<path
|
|
24
|
+
class="alert-mask-background"
|
|
25
|
+
:d="innerPath"
|
|
26
|
+
:style="cfg.backgroundColour ? { fill: cfg.backgroundColour } : undefined"
|
|
27
|
+
/>
|
|
28
|
+
</svg>
|
|
29
|
+
|
|
30
|
+
<div
|
|
31
|
+
class="alert-mask-content"
|
|
32
|
+
:style="{
|
|
33
|
+
'--_inset-left': cfg.borderLeft + 'px',
|
|
34
|
+
'--_inset-right': cfg.borderRight + 'px',
|
|
35
|
+
'--_inset-top': cfg.borderTop + 'px',
|
|
36
|
+
'--_inset-bottom': cfg.borderBottom + 'px',
|
|
37
|
+
}"
|
|
38
|
+
>
|
|
39
|
+
<div ref="alertContentRef" class="alert-mask-content-slot">
|
|
40
|
+
<slot name="default"></slot>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<script setup lang="ts">
|
|
47
|
+
import type { AlertMaskConfig } from "~/types/components";
|
|
48
|
+
|
|
49
|
+
interface Props {
|
|
50
|
+
config?: AlertMaskConfig;
|
|
51
|
+
styleClassPassthrough?: string | string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
55
|
+
config: () => ({}),
|
|
56
|
+
styleClassPassthrough: () => [],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
60
|
+
|
|
61
|
+
watch(
|
|
62
|
+
() => props.styleClassPassthrough,
|
|
63
|
+
() => {
|
|
64
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const maskId = `alert-mask-${useId()}`;
|
|
69
|
+
const alertContentRef = useTemplateRef<HTMLElement | null>("alertContentRef");
|
|
70
|
+
const contentWidth = ref(0);
|
|
71
|
+
const contentHeight = ref(0);
|
|
72
|
+
const measured = ref(false);
|
|
73
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
74
|
+
|
|
75
|
+
const toSize = (value: number | undefined, fallback: number) =>
|
|
76
|
+
typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : fallback;
|
|
77
|
+
|
|
78
|
+
const cfg = computed(() => ({
|
|
79
|
+
backgroundColour: props.config?.backgroundColour,
|
|
80
|
+
borderColour: props.config?.borderColour,
|
|
81
|
+
radiusLeft: toSize(props.config?.radiusLeft, 12),
|
|
82
|
+
radiusRight: toSize(props.config?.radiusRight, 12),
|
|
83
|
+
borderLeft: toSize(props.config?.borderLeft, 8),
|
|
84
|
+
borderTop: toSize(props.config?.borderTop, 8),
|
|
85
|
+
borderRight: toSize(props.config?.borderRight, 8),
|
|
86
|
+
borderBottom: toSize(props.config?.borderBottom, 8),
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
const svgWidth = computed(() =>
|
|
90
|
+
measured.value ? contentWidth.value + cfg.value.borderLeft + cfg.value.borderRight : 0
|
|
91
|
+
);
|
|
92
|
+
const svgHeight = computed(() =>
|
|
93
|
+
measured.value ? contentHeight.value + cfg.value.borderTop + cfg.value.borderBottom : 0
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const updateDimensions = () => {
|
|
97
|
+
const contentEl = alertContentRef.value;
|
|
98
|
+
if (!contentEl) return;
|
|
99
|
+
const rect = contentEl.getBoundingClientRect();
|
|
100
|
+
contentWidth.value = rect.width;
|
|
101
|
+
contentHeight.value = rect.height;
|
|
102
|
+
measured.value = true;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
onMounted(() => {
|
|
106
|
+
nextTick(updateDimensions);
|
|
107
|
+
if (alertContentRef.value) {
|
|
108
|
+
resizeObserver = new ResizeObserver(updateDimensions);
|
|
109
|
+
resizeObserver.observe(alertContentRef.value);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
onBeforeUnmount(() => {
|
|
114
|
+
resizeObserver?.disconnect();
|
|
115
|
+
resizeObserver = null;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const outerPath = computed(() => {
|
|
119
|
+
const width = svgWidth.value;
|
|
120
|
+
const height = svgHeight.value;
|
|
121
|
+
if (!width || !height) return "";
|
|
122
|
+
|
|
123
|
+
const rl = Math.min(cfg.value.radiusLeft, width / 2, height / 2);
|
|
124
|
+
const rr = Math.min(cfg.value.radiusRight, width / 2, height / 2);
|
|
125
|
+
|
|
126
|
+
return `
|
|
127
|
+
M ${rl} 0
|
|
128
|
+
L ${width - rr} 0
|
|
129
|
+
Q ${width} 0 ${width} ${rr}
|
|
130
|
+
L ${width} ${height - rr}
|
|
131
|
+
Q ${width} ${height} ${width - rr} ${height}
|
|
132
|
+
L ${rl} ${height}
|
|
133
|
+
Q 0 ${height} 0 ${height - rl}
|
|
134
|
+
L 0 ${rl}
|
|
135
|
+
Q 0 0 ${rl} 0 Z
|
|
136
|
+
`;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const innerPath = computed(() => {
|
|
140
|
+
const width = svgWidth.value;
|
|
141
|
+
const height = svgHeight.value;
|
|
142
|
+
const { borderLeft: bl, borderTop: bt, borderRight: br, borderBottom: bb } = cfg.value;
|
|
143
|
+
const innerWidth = width - bl - br;
|
|
144
|
+
const innerHeight = height - bt - bb;
|
|
145
|
+
if (!width || !height || innerWidth <= 0 || innerHeight <= 0) return "";
|
|
146
|
+
|
|
147
|
+
const rl = Math.min(cfg.value.radiusLeft, innerWidth / 2, innerHeight / 2);
|
|
148
|
+
const rr = Math.min(cfg.value.radiusRight, innerWidth / 2, innerHeight / 2);
|
|
149
|
+
const right = width - br;
|
|
150
|
+
const bottom = height - bb;
|
|
151
|
+
|
|
152
|
+
return `
|
|
153
|
+
M ${bl + rl} ${bt}
|
|
154
|
+
L ${right - rr} ${bt}
|
|
155
|
+
Q ${right} ${bt} ${right} ${bt + rr}
|
|
156
|
+
L ${right} ${bottom - rr}
|
|
157
|
+
Q ${right} ${bottom} ${right - rr} ${bottom}
|
|
158
|
+
L ${bl + rl} ${bottom}
|
|
159
|
+
Q ${bl} ${bottom} ${bl} ${bottom - rl}
|
|
160
|
+
L ${bl} ${bt + rl}
|
|
161
|
+
Q ${bl} ${bt} ${bl + rl} ${bt} Z
|
|
162
|
+
`;
|
|
163
|
+
});
|
|
164
|
+
</script>
|
|
165
|
+
|
|
166
|
+
<style lang="css">
|
|
167
|
+
@layer components {
|
|
168
|
+
.alert-mask {
|
|
169
|
+
display: grid;
|
|
170
|
+
grid-template-areas: "mask";
|
|
171
|
+
min-inline-size: 0;
|
|
172
|
+
|
|
173
|
+
.alert-mask-decorator {
|
|
174
|
+
grid-area: mask;
|
|
175
|
+
width: 100%;
|
|
176
|
+
height: var(--_height);
|
|
177
|
+
|
|
178
|
+
.alert-mask-border {
|
|
179
|
+
fill: var(--alert-mask-border-colour, var(--theme-accent));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.alert-mask-background {
|
|
183
|
+
fill: var(--alert-mask-background-colour, color-mix(in oklab, var(--theme-surface-subtle) 80%, transparent));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.alert-mask-content {
|
|
188
|
+
grid-area: mask;
|
|
189
|
+
min-inline-size: 0;
|
|
190
|
+
margin: var(--_inset-top) var(--_inset-right) var(--_inset-bottom) var(--_inset-left);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
</style>
|
|
@@ -1,64 +1,79 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AlertMask — Consumer Styling Guide
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Renamed 2026-10-17**: `AlertMaskCore` → `AlertMask`, root class `.alert-mask-core` →
|
|
4
|
+
> `.alert-mask`. The colours also became CSS tokens (below); `config` colours still override them.
|
|
4
5
|
|
|
5
|
-
`
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
prop (`AlertMaskConfig`):
|
|
6
|
+
`AlertMask` draws a border and a translucent fill around its slot as an SVG path, sized to the
|
|
7
|
+
measured content. Geometry (radii, per-side border widths) can't be a CSS token, because the path
|
|
8
|
+
is computed in script, so it comes from the `config` prop. Colours are plain CSS `fill` values, so
|
|
9
|
+
they're tokens.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
11
|
+
## Public token API
|
|
12
|
+
|
|
13
|
+
| Token | Default | Controls |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `--alert-mask-border-colour` | `var(--theme-accent)` | Border fill |
|
|
16
|
+
| `--alert-mask-background-colour` | `color-mix(in oklab, var(--theme-surface-subtle) 80%, transparent)` | Inner fill |
|
|
17
|
+
|
|
18
|
+
A `config.borderColour` or `config.backgroundColour` is applied as an inline `fill` style on its
|
|
19
|
+
path and beats the token. Both read the theme of the nearest `data-theme` ancestor.
|
|
20
|
+
|
|
21
|
+
> **Changed 2026-10-17**: the colours were `config`-only, with hardcoded defaults
|
|
22
|
+
> (`var(--orange-08)` border, `rgba(0,0,0,0.25)` fill). They now default from the tokens above.
|
|
23
|
+
|
|
24
|
+
### Geometry (`config`, `AlertMaskConfig`)
|
|
25
|
+
|
|
26
|
+
| Field | Default | Controls |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `radiusLeft` / `radiusRight` | `12` | Corner radius (px) of the left and right corners |
|
|
29
|
+
| `borderLeft` / `borderTop` / `borderRight` / `borderBottom` | `8` | Border width (px) per side; the content is inset by the same amounts |
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
Sides are physical (left stays left in a right-to-left layout), matching the drawn path. Negative
|
|
32
|
+
or non-finite values fall back to `0` and the default respectively, and a radius larger than the
|
|
33
|
+
box is clamped to half its smaller side.
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
Private tokens (not public API): `--_height` (the measured SVG height) and `--_inset-left`,
|
|
36
|
+
`--_inset-right`, `--_inset-top`, `--_inset-bottom` (content margins from the border widths), all set
|
|
37
|
+
inline from script.
|
|
34
38
|
|
|
35
39
|
---
|
|
36
40
|
|
|
37
|
-
##
|
|
41
|
+
## State hooks
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
None. Inner elements: `.alert-mask` > `.alert-mask-decorator` (the SVG, with `.alert-mask-border`
|
|
44
|
+
and `.alert-mask-background` paths) + `.alert-mask-content` > `.alert-mask-content-slot`.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Global theming
|
|
49
|
+
|
|
50
|
+
```css
|
|
51
|
+
:where(html) {
|
|
52
|
+
--alert-mask-border-colour: var(--theme-border);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
42
55
|
|
|
43
56
|
---
|
|
44
57
|
|
|
45
58
|
## Local overrides
|
|
46
59
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
and examples: `.claude/skills/component-local-style-override.md`.
|
|
60
|
+
Set the tokens above on an element you own (a page or section class, or a class added with
|
|
61
|
+
`:style-class-passthrough`): they inherit down into the component. Keep the block **unlayered** (no
|
|
62
|
+
`@layer` wrapper) so it beats the library's `@layer components`. If your own file uses
|
|
63
|
+
`<style scoped>`, tokens set on your element still work, but selectors that reach inside the
|
|
64
|
+
component need `:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
|
|
65
|
+
|
|
66
|
+
**Caveat:** a colour passed in `config` is an inline style on the path, so it wins over the token
|
|
67
|
+
wherever the token is set. `AlertMaskedContent` passes both colours this way (through its own
|
|
68
|
+
`--alert-masked-content-*` tokens), so inside it set those instead.
|
|
52
69
|
|
|
53
70
|
---
|
|
54
71
|
|
|
55
72
|
## Class passthrough
|
|
56
73
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
governed by `config`.
|
|
74
|
+
`style-class-passthrough` adds classes to the root `.alert-mask` element and updates after mount.
|
|
75
|
+
Use it for layout (sizing, positioning) or to scope the colour tokens.
|
|
60
76
|
|
|
61
77
|
```vue
|
|
62
|
-
<
|
|
78
|
+
<AlertMask style-class-passthrough="my-alert-mask">...</AlertMask>
|
|
63
79
|
```
|
|
64
|
-
|