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.
Files changed (125) hide show
  1. package/.claude/rules/consumer/ids-from-useid.md +4 -2
  2. package/.claude/skills/component-compliance-checklist.md +14 -3
  3. package/.claude/skills/component-inline-action-button.md +5 -4
  4. package/.claude/skills/component-naming.md +2 -1
  5. package/.claude/skills/components/{alert-mask-core.md → alert-mask.md} +38 -23
  6. package/.claude/skills/components/alert-masked-content.md +3 -3
  7. package/.claude/skills/components/container-glow.md +1 -1
  8. package/.claude/skills/components/display-theme-switch.md +8 -8
  9. package/.claude/skills/components/form-field.md +15 -0
  10. package/.claude/skills/components/form-fieldset.md +18 -2
  11. package/.claude/skills/components/input-button.md +26 -7
  12. package/.claude/skills/components/input-checkbox-radio.md +2 -0
  13. package/.claude/skills/components/input-description.md +5 -0
  14. package/.claude/skills/components/input-error.md +9 -2
  15. package/.claude/skills/components/input-label.md +8 -2
  16. package/.claude/skills/components/input-number.md +2 -0
  17. package/.claude/skills/components/input-otp.md +2 -0
  18. package/.claude/skills/components/input-range.md +2 -0
  19. package/.claude/skills/components/input-select.md +2 -0
  20. package/.claude/skills/components/input-text.md +2 -0
  21. package/.claude/skills/components/input-textarea.md +2 -0
  22. package/.claude/skills/components/marquee-scroller.md +51 -14
  23. package/.claude/skills/components/pending-effect.md +5 -4
  24. package/.claude/skills/components/{triple-toggle-switch.md → segmented-control.md} +34 -19
  25. package/.claude/skills/components/services-card.md +1 -1
  26. package/.claude/skills/components/tabbed-content.md +1 -1
  27. package/.claude/skills/components/toggle-switch.md +2 -0
  28. package/.claude/skills/index.md +5 -5
  29. package/.claude/skills/storybook-add-story.md +3 -1
  30. package/.claude/skills/theming-component-token-pattern.md +24 -0
  31. package/.claude/skills/theming-form-geometry-tokens.md +1 -1
  32. package/.claude/skills/theming-partial-override.md +1 -1
  33. package/.vscode/srcdev-component-alert-mask.code-snippets +41 -0
  34. package/.vscode/srcdev-component-form-field.code-snippets +7 -6
  35. package/.vscode/srcdev-component-form-fieldset.code-snippets +12 -12
  36. package/.vscode/srcdev-component-marquee-scroller.code-snippets +31 -28
  37. package/.vscode/{srcdev-component-triple-toggle-switch.code-snippets → srcdev-component-segmented-control.code-snippets} +6 -7
  38. package/app/assets/styles/setup/03.theming/_theme-slots.css +1 -1
  39. package/app/assets/styles/setup/04.elements/forms/03.generic-input-geometry.css +4 -0
  40. package/app/components/01.atoms/alert-mask/AlertMask.vue +194 -0
  41. package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +57 -42
  42. package/app/components/01.atoms/alert-mask/stories/AlertMask.stories.ts +198 -0
  43. package/app/components/01.atoms/alert-mask/tests/AlertMask.spec.ts +233 -0
  44. package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +77 -54
  45. package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +103 -91
  46. package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +132 -3
  47. package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +190 -102
  48. package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +17 -13
  49. package/app/components/02.molecules/alert-masked-content/AlertMaskedContent.vue +2 -2
  50. package/app/components/02.molecules/alert-masked-content/CONSUMER-STYLING.md +4 -4
  51. package/app/components/02.molecules/alert-masked-content/stories/AlertMaskedContent.stories.ts +60 -0
  52. package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +12 -12
  53. package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +3 -3
  54. package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +1 -1
  55. package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +1 -1
  56. package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +9 -9
  57. package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +16 -16
  58. package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +53 -1
  59. package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +8 -8
  60. package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +4 -4
  61. package/app/components/02.molecules/input-copy/InputCopy.vue +2 -2
  62. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +2 -2
  63. package/app/components/03.organisms/google-reviews/tests/GoogleReviews.spec.ts +4 -4
  64. package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +2 -2
  65. package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +2 -2
  66. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +6 -6
  67. package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +8 -0
  68. package/app/components/05.forms/form-errors/InputError.vue +43 -37
  69. package/app/components/05.forms/form-errors/stories/InputError.stories.ts +79 -1
  70. package/app/components/05.forms/form-errors/tests/InputError.spec.ts +29 -1
  71. package/app/components/05.forms/form-field/CONSUMER-STYLING.md +7 -0
  72. package/app/components/05.forms/form-field/stories/FormField.stories.ts +116 -2
  73. package/app/components/05.forms/form-fieldset/CONSUMER-STYLING.md +11 -1
  74. package/app/components/05.forms/form-fieldset/FormFieldset.vue +4 -2
  75. package/app/components/05.forms/form-fieldset/stories/FormFieldset.stories.ts +68 -4
  76. package/app/components/05.forms/form-fieldset/tests/FormFieldset.spec.ts +10 -0
  77. package/app/components/05.forms/input-button/CONSUMER-STYLING.md +80 -47
  78. package/app/components/05.forms/input-button/InputButton.vue +52 -100
  79. package/app/components/05.forms/input-button/playwright/input-button.playwright.ts +1 -1
  80. package/app/components/05.forms/input-button/stories/InputButton.stories.ts +145 -8
  81. package/app/components/05.forms/input-button/tests/InputButton.spec.ts +76 -46
  82. package/app/components/05.forms/input-button/tests/__snapshots__/InputButton.spec.ts.snap +17 -17
  83. package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +9 -1
  84. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +8 -0
  85. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +2 -2
  86. package/app/components/05.forms/input-description/CONSUMER-STYLING.md +14 -2
  87. package/app/components/05.forms/input-description/InputDescription.vue +2 -1
  88. package/app/components/05.forms/input-description/stories/InputDescription.stories.ts +70 -4
  89. package/app/components/05.forms/input-description/tests/InputDescription.spec.ts +2 -1
  90. package/app/components/05.forms/input-label/CONSUMER-STYLING.md +8 -0
  91. package/app/components/05.forms/input-label/InputLabel.vue +1 -0
  92. package/app/components/05.forms/input-label/stories/InputLabel.stories.ts +68 -2
  93. package/app/components/05.forms/input-label/tests/InputLabel.spec.ts +13 -3
  94. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +7 -1
  95. package/app/components/05.forms/input-number/InputNumber.vue +4 -5
  96. package/app/components/05.forms/input-otp/CONSUMER-STYLING.md +7 -1
  97. package/app/components/05.forms/input-otp/InputOtp.vue +2 -2
  98. package/app/components/05.forms/input-range/InputRange.vue +1 -1
  99. package/app/components/05.forms/input-select/CONSUMER-STYLING.md +7 -1
  100. package/app/components/05.forms/input-select/InputSelect.vue +14 -5
  101. package/app/components/05.forms/input-text/CONSUMER-STYLING.md +7 -1
  102. package/app/components/05.forms/input-text/InputText.vue +4 -4
  103. package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +7 -1
  104. package/app/components/05.forms/input-textarea/InputTextarea.vue +2 -3
  105. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +4 -4
  106. package/app/components/05.forms/pending-effect/CONSUMER-STYLING.md +7 -3
  107. package/app/components/05.forms/pending-effect/PendingEffect.vue +3 -3
  108. package/app/components/05.forms/pending-effect/tests/PendingEffect.spec.ts +1 -1
  109. package/app/components/05.forms/segmented-control/CONSUMER-STYLING.md +125 -0
  110. package/app/components/05.forms/segmented-control/SegmentedControl.vue +231 -0
  111. package/app/components/05.forms/segmented-control/stories/SegmentedControl.stories.ts +215 -0
  112. package/app/components/05.forms/segmented-control/tests/SegmentedControl.spec.ts +202 -0
  113. package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +7 -0
  114. package/app/components/05.forms/toggle-switch/ToggleSwitch.vue +2 -0
  115. package/app/types/components/index.ts +1 -1
  116. package/package.json +1 -1
  117. package/.vscode/srcdev-component-alert-mask-core.code-snippets +0 -31
  118. package/app/components/01.atoms/alert-mask/AlertMaskCore.vue +0 -166
  119. package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +0 -105
  120. package/app/components/01.atoms/alert-mask/tests/AlertMaskCore.spec.ts +0 -342
  121. package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +0 -99
  122. package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitch.vue +0 -251
  123. package/app/components/05.forms/triple-toggle-switch/stories/TripleToggleSwitch.stories.ts +0 -104
  124. package/app/components/05.forms/triple-toggle-switch/tests/TripleToggleSwitch.spec.ts +0 -138
  125. /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: '$1logo-a' },",
9
- " { id: 2, content: '$2logo-b' },",
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
- " $6",
15
+ " $7",
15
16
  " </template>",
16
17
  " <template #2>",
17
- " $7",
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=\"$1items\"",
28
+ " :marquee-data=\"${1:items}\"",
28
29
  " show-controls",
29
- " aria-label=\"$2Scrolling client logos\"",
30
+ " aria-label=\"${2:Scrolling client logos}\"",
30
31
  ">",
31
- " <template v-for=\"item in $1items\" #[item.id] :key=\"item.id\">",
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=\"$1items\"",
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 $1items\" #[item.id] :key=\"item.id\">",
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=\"$1items\" show-controls>",
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
- ".$1my-page {",
71
+ ".${1:my-page} {",
71
72
  " .marquee-scroller {",
72
- " --marquee-scroller-fade-width: ;",
73
- " --marquee-scroller-focus-outline-width: ;",
74
- " --marquee-scroller-focus-outline-offset: ;",
75
- " --marquee-scroller-item-border-colour: ;",
76
- " --marquee-scroller-item-border-width: ;",
77
- " --marquee-scroller-item-border-radius: ;",
78
- " --marquee-scroller-item-transition-duration: ;",
79
- " --marquee-scroller-group-hover-filter: ;",
80
- " --marquee-scroller-item-hover-filter: ;",
81
- " --marquee-scroller-control-offset: ;",
82
- " --marquee-scroller-control-background-colour: ;",
83
- " --marquee-scroller-control-background-colour-hover: ;",
84
- " --marquee-scroller-control-text-colour: ;",
85
- " --marquee-scroller-control-border-radius: ;",
86
- " --marquee-scroller-control-border-width: ;",
87
- " --marquee-scroller-control-padding: ;",
88
- " --marquee-scroller-control-font-size: ;",
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 TripleToggleSwitch": {
3
- "description": "TripleToggleSwitch: three-option radio pill with a sliding marker",
2
+ "SRCDEV SegmentedControl": {
3
+ "description": "SegmentedControl: three-option radio pill with a sliding marker",
4
4
  "scope": "vue,html",
5
5
  "body": [
6
- "<TripleToggleSwitch",
6
+ "<SegmentedControl",
7
7
  " v-model=\"${1:selected}\"",
8
8
  " v-model:field-data=\"${2:options}\"",
9
- " name=\"${3:triple-toggle}\"",
10
- " aria-label=\"${4:Choose an option}\"",
9
+ " aria-label=\"${3:Choose an option}\"",
11
10
  "/>"
12
11
  ]
13
12
  },
14
- "SRCDEV TripleToggleSwitch Options": {
15
- "description": "IFormMultipleOptions data for TripleToggleSwitch (system/light/dark values get marker gradients)",
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 (InputButton .secondary). */
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
- # AlertMaskCore — Consumer Styling Guide
1
+ # AlertMask — Consumer Styling Guide
2
2
 
3
- ## No public CSS token API
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
- `AlertMaskCore` draws its border/background via an SVG `<mask>` whose path geometry is computed
6
- from measured content dimensions, so it can't expose colour/radius/thickness as CSS custom
7
- properties the way most components do — a `var()` can't be interpolated into an SVG path or
8
- resized reactively the way a `fill` attribute can. Instead, the override surface is the `config`
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
- ```vue
12
- <AlertMaskCore
13
- :config="{
14
- backgroundColour: '#1a1a1a',
15
- borderColour: 'var(--theme-accent)',
16
- radiusLeft: 8,
17
- radiusRight: 4,
18
- borderLeft: 6,
19
- borderTop: 1,
20
- borderRight: 1,
21
- borderBottom: 1,
22
- }"
23
- >
24
- <p>Alert content</p>
25
- </AlertMaskCore>
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
- Colours are applied as `fill` styles, so `borderColour`/`backgroundColour` can be `var()`
29
- references to your own tokens (a `var(--theme-*)` value needs a `data-theme` ancestor to resolve).
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
- All fields are optional and fall back to `backgroundColour: "rgba(0,0,0,0.25)"`,
32
- `borderColour: "var(--orange-08)"`, `radiusLeft`/`radiusRight: 12`, `borderLeft`/`borderTop`/
33
- `borderRight`/`borderBottom: 8`.
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
- ## Internal (non-overridable) custom properties
41
+ ## State hooks
38
42
 
39
- `--_height`, `--_inset-inline-start`, `--_inset-inline-end`, `--_inset-block-start`, and
40
- `--_inset-block-end` are set inline from measured content size and `config` border widths. They're
41
- private implementation plumbing, not a consumer override surface — use `config` instead.
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
- There are no public tokens, so override the classes above with direct properties, scoped to an
48
- element you own (a page or section class, or a class added with `:style-class-passthrough`). Keep
49
- the block **unlayered** (no `@layer` wrapper) so it beats the library's `@layer components`. If your
50
- own file uses `<style scoped>`, selectors that reach inside the component need `:deep()`. Patterns
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
- Use `styleClassPassthrough` to add classes to the root `.alert-mask-core` element for layout
58
- purposes (positioning, sizing) — it doesn't affect the mask's own colours or geometry, which stay
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
- <AlertMaskCore style-class-passthrough="my-alert-mask">...</AlertMaskCore>
78
+ <AlertMask style-class-passthrough="my-alert-mask">...</AlertMask>
63
79
  ```
64
-