srcdev-nuxt-components 9.5.4 → 9.5.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 (55) hide show
  1. package/.claude/skills/components/action-menu.md +21 -15
  2. package/.claude/skills/components/deep-expanding-menu.md +18 -6
  3. package/.claude/skills/components/display-tooltip-defined.md +2 -1
  4. package/.claude/skills/components/display-tooltip.md +10 -4
  5. package/.claude/skills/components/pop-over.md +11 -8
  6. package/.claude/skills/components/select-menu.md +28 -8
  7. package/.claude/skills/components/triple-toggle-switch.md +7 -1
  8. package/.claude/skills/components/ui-block-decorated.md +7 -0
  9. package/.claude/skills/composable-anchored-popover.md +108 -0
  10. package/.claude/skills/composable-tooltips-guide.md +3 -1
  11. package/.claude/skills/index.md +1 -0
  12. package/app/assets/styles/setup/06.utility-classes/animations/_animation-scroller-x.css +5 -3
  13. package/app/assets/styles/setup/06.utility-classes/animations/_auto-rotate.css +5 -3
  14. package/app/assets/styles/setup/06.utility-classes/animations/_entry-exit-blur.css +5 -3
  15. package/app/assets/styles/setup/06.utility-classes/animations/_entry-slide-in.css +5 -3
  16. package/app/assets/styles/setup/06.utility-classes/animations/_entry-zoom-reveal.css +5 -3
  17. package/app/components/01.atoms/animations/entry/CONSUMER-STYLING.md +6 -2
  18. package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +15 -3
  19. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +62 -3
  20. package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +36 -1
  21. package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +1 -1
  22. package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +1 -1
  23. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +9 -3
  24. package/app/components/01.atoms/pop-over/PopOver.vue +70 -9
  25. package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +53 -0
  26. package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +1 -1
  27. package/app/components/01.atoms/ui-block-decorated/CONSUMER-STYLING.md +12 -4
  28. package/app/components/01.atoms/ui-block-decorated/UiBlockDecorated.vue +20 -16
  29. package/app/components/01.atoms/ui-block-decorated/stories/UiBlockDecorated.stories.ts +124 -96
  30. package/app/components/02.molecules/action-menu/ActionMenu.vue +41 -16
  31. package/app/components/02.molecules/action-menu/CONSUMER-STYLING.md +16 -3
  32. package/app/components/02.molecules/action-menu/tests/ActionMenu.spec.ts +28 -0
  33. package/app/components/02.molecules/action-menu/tests/__snapshots__/ActionMenu.spec.ts.snap +3 -3
  34. package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +2 -1
  35. package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +7 -4
  36. package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +20 -1
  37. package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +1 -1
  38. package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +18 -4
  39. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +87 -16
  40. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +83 -1
  41. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +2 -2
  42. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +1 -1
  43. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +1 -1
  44. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +26 -6
  45. package/app/components/02.molecules/select-menu/SelectMenu.vue +90 -18
  46. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +36 -0
  47. package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +9 -9
  48. package/app/components/03.organisms/responsive-header/NavigationItems.vue +2 -2
  49. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +4 -4
  50. package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitch.vue +11 -3
  51. package/app/composables/tests/useAnchoredPopover.spec.ts +231 -0
  52. package/app/composables/tests/useTooltips.spec.ts +69 -0
  53. package/app/composables/useAnchoredPopover.ts +183 -0
  54. package/app/composables/useTooltips.ts +37 -20
  55. package/package.json +1 -1
@@ -1,20 +1,38 @@
1
1
  <template>
2
- <div class="display-tooltip-core" :class="[elementClasses]" :style="{ '--_anchor-name': tooltipAnchorName }">
2
+ <div
3
+ ref="rootRef"
4
+ class="display-tooltip-core"
5
+ :class="[elementClasses]"
6
+ :style="{ '--_anchor-name': tooltipAnchorName }"
7
+ >
3
8
  <div class="display-tooltip-trigger-wrapper body-md">
4
9
  <slot v-if="$slots.triggerContent" name="triggerContent"></slot>
5
10
  <button
11
+ ref="triggerRef"
6
12
  :popovertarget="tooltipId"
7
13
  popovertargetaction="toggle"
8
14
  class="display-tooltip-trigger-button"
9
15
  :class="{ hide: hideTrigger }"
10
16
  :aria-label="triggerAriaLabel"
17
+ :aria-expanded="isOpen"
18
+ @click="handleTriggerClick"
11
19
  >
12
20
  <Icon name="fa7-solid:circle-question" class="display-tooltip-trigger-icon" aria-hidden="true" />
13
21
  </button>
14
22
  </div>
15
- <div :id="tooltipId" popover class="display-tooltip-popover">
23
+ <div
24
+ :id="tooltipId"
25
+ ref="popoverRef"
26
+ popover
27
+ class="display-tooltip-popover"
28
+ :class="{ 'display-tooltip-popover-open': usesFallbackPopover && isOpen }"
29
+ :style="positionStyle"
30
+ :data-placement="popoverPlacement"
31
+ @beforetoggle="handleBeforeToggle"
32
+ @toggle="handleToggle"
33
+ >
16
34
  <div class="display-tooltip-popover-content">
17
- <slot name="tooltipContent"></slot>
35
+ <slot name="tooltipContent" :close="hide"></slot>
18
36
  </div>
19
37
  </div>
20
38
  </div>
@@ -39,6 +57,21 @@ const props = withDefaults(defineProps<Props>(), {
39
57
  const tooltipId = computed(() => (props.tooltipId.length ? props.tooltipId : `nuxt-tooltip-${useId()}`));
40
58
  const tooltipAnchorName = `--tooltip-anchor-${useId()}`;
41
59
 
60
+ const rootRef = ref<HTMLElement | null>(null);
61
+ const triggerRef = ref<HTMLElement | null>(null);
62
+ const popoverRef = ref<HTMLElement | null>(null);
63
+
64
+ const {
65
+ isOpen,
66
+ usesFallbackPopover,
67
+ positionStyle,
68
+ popoverPlacement,
69
+ hide,
70
+ handleTriggerClick,
71
+ handleBeforeToggle,
72
+ handleToggle,
73
+ } = useAnchoredPopover({ rootRef, triggerRef, popoverRef, side: "right" });
74
+
42
75
  const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
43
76
 
44
77
  watch(
@@ -141,6 +174,13 @@ watch(
141
174
  display: flex;
142
175
  flex-direction: column;
143
176
  gap: var(--display-tooltip-popover-content-gap, 1.2rem);
177
+ font-size: var(--display-tooltip-popover-font-size, 1.4rem);
178
+ line-height: var(--display-tooltip-popover-line-height, 1.4);
179
+
180
+ /* Gap is the only spacing between slotted elements; :where keeps consumer margins winning. */
181
+ > :where(*) {
182
+ margin-block: 0;
183
+ }
144
184
 
145
185
  .display-tooltip-close-button {
146
186
  all: unset;
@@ -174,6 +214,25 @@ watch(
174
214
  opacity: 0;
175
215
  }
176
216
  }
217
+
218
+ /* Kept apart from :popover-open, which would invalidate a shared selector list where unsupported. */
219
+ &.display-tooltip-popover-open {
220
+ display: flex;
221
+ opacity: 1;
222
+ flex-direction: column;
223
+ }
224
+
225
+ @supports not (anchor-name: --a) {
226
+ position: fixed;
227
+ top: var(--_anchor-top, 0px);
228
+ left: calc(var(--_anchor-right, 0px) + var(--display-tooltip-popover-offset, 0.1rem));
229
+ z-index: var(--display-tooltip-popover-z-index, 999999);
230
+
231
+ &[data-placement="left"] {
232
+ left: auto;
233
+ right: calc(var(--_anchor-left-inverse, 0px) + var(--display-tooltip-popover-offset, 0.1rem));
234
+ }
235
+ }
177
236
  }
178
237
  }
179
238
  }
@@ -1,4 +1,4 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
2
  import { mountSuspended } from "@nuxt/test-utils/runtime";
3
3
  import DisplayTooltip from "../DisplayTooltip.vue";
4
4
 
@@ -72,4 +72,39 @@ describe("DisplayTooltip", () => {
72
72
  expect(wrapper.classes()).not.toContain("initial-class");
73
73
  expect(wrapper.classes()).toContain("updated-class");
74
74
  });
75
+
76
+ describe("without the Popover API", () => {
77
+ beforeEach(() => {
78
+ Object.defineProperty(HTMLElement.prototype, "showPopover", { value: undefined, writable: true, configurable: true });
79
+ });
80
+
81
+ afterEach(() => {
82
+ delete (HTMLElement.prototype as unknown as Record<string, unknown>)["showPopover"];
83
+ });
84
+
85
+ it("opens and closes from the trigger", async () => {
86
+ const wrapper = await mountSuspended(DisplayTooltip);
87
+ const trigger = wrapper.find(".display-tooltip-trigger-button");
88
+ const popover = wrapper.find(".display-tooltip-popover");
89
+
90
+ await trigger.trigger("click");
91
+ expect(popover.classes()).toContain("display-tooltip-popover-open");
92
+ expect(trigger.attributes("aria-expanded")).toBe("true");
93
+
94
+ await trigger.trigger("click");
95
+ expect(popover.classes()).not.toContain("display-tooltip-popover-open");
96
+ expect(trigger.attributes("aria-expanded")).toBe("false");
97
+ });
98
+
99
+ it("passes a close function to the tooltipContent slot", async () => {
100
+ const wrapper = await mountSuspended(DisplayTooltip, {
101
+ slots: {
102
+ tooltipContent: `<template #tooltipContent="{ close }"><button class="custom-close" @click="close">Done</button></template>`,
103
+ },
104
+ });
105
+ await wrapper.find(".display-tooltip-trigger-button").trigger("click");
106
+ await wrapper.find(".custom-close").trigger("click");
107
+ expect(wrapper.find(".display-tooltip-popover").classes()).not.toContain("display-tooltip-popover-open");
108
+ });
109
+ });
75
110
  });
@@ -3,7 +3,7 @@
3
3
  exports[`DisplayTooltip > renders correct HTML structure 1`] = `
4
4
  "<div class="display-tooltip-core" style="--_anchor-name: --tooltip-anchor-v-0-0;">
5
5
  <div class="display-tooltip-trigger-wrapper body-md">
6
- <!--v-if--><button popovertarget="fixed-id" popovertargetaction="toggle" class="display-tooltip-trigger-button" aria-label="Toggle the popover"><span class="iconify i-fa7-solid:circle-question display-tooltip-trigger-icon" aria-hidden="true"></span></button>
6
+ <!--v-if--><button popovertarget="fixed-id" popovertargetaction="toggle" class="display-tooltip-trigger-button" aria-label="Toggle the popover" aria-expanded="false"><span class="iconify i-fa7-solid:circle-question display-tooltip-trigger-icon" aria-hidden="true"></span></button>
7
7
  </div>
8
8
  <div id="fixed-id" popover="" class="display-tooltip-popover">
9
9
  <div class="display-tooltip-popover-content"></div>
@@ -81,7 +81,7 @@ watch(
81
81
  color: var(--skip-links-text-colour, white);
82
82
  text-decoration: none;
83
83
  padding: var(--skip-links-link-padding-block, 0.8rem) var(--skip-links-link-padding-inline, 1.2rem);
84
- text-wrap: nowrap;
84
+ white-space: nowrap;
85
85
 
86
86
  &:focus-visible {
87
87
  outline: var(--skip-links-focus-outline-width, 2px) solid var(--skip-links-focus-outline-colour, white);
@@ -15,6 +15,7 @@
15
15
  | `--pop-over-text-colour` | `var(--slate-09)` | Popover panel text colour |
16
16
  | `--pop-over-shadow` | `0 0.4rem 1.6rem rgba(0,0,0,.12)` | Popover panel elevation shadow |
17
17
  | `--pop-over-transition-duration` | `200ms` | Open/close fade duration |
18
+ | `--pop-over-z-index` | `999999` | Panel stacking order in browsers without CSS anchor positioning. Ignored where the panel renders in the top layer |
18
19
  | `--pop-over-content-padding` | `1.6rem` | Padding around the `content` slot |
19
20
  | `--pop-over-content-padding-block-start` | `3.2rem` | Extra top padding so content clears the close button |
20
21
  | `--pop-over-close-button-offset` | `0.8rem` | Distance of the close button from the panel's top/right edges |
@@ -54,9 +55,14 @@ Set the tokens above on an element you own (a page or section class, or a class
54
55
 
55
56
  ## Notes
56
57
 
57
- - Built on the native Popover API (`popover`/`popovertarget`) and CSS anchor-positioning
58
- (`anchor-name`/`position-anchor`/`anchor()`) — check current browser support before relying on
59
- it where broad support is a hard requirement.
58
+ - Built on the native Popover API and CSS anchor positioning, with fallbacks: without anchor
59
+ positioning (e.g. Safari 17–18) the panel is placed with `position: fixed` from the trigger's
60
+ measured position, and `.pop-over-popover[data-placement]` holds the resolved side (the
61
+ `placement` prop's side, or its opposite when that has no room). Without the Popover API
62
+ (Safari 16) it opens and closes in JS and shows via `.pop-over-popover-open` (style it in a
63
+ separate rule from `:popover-open`: a selector list containing `:popover-open` is dropped whole
64
+ where unsupported). It isn't in the top layer there, so the z-index token applies, and an
65
+ ancestor with `transform`, `filter` or `contain` can misplace it.
60
66
  - `placement` (`"top" | "right" | "bottom" | "left"`, default `"right"`) picks which side of the
61
67
  trigger the panel opens on; each side gets a `position-try-fallbacks` so the browser flips it
62
68
  automatically if it would overflow the viewport.
@@ -1,6 +1,12 @@
1
1
  <template>
2
2
  <ClientOnly>
3
- <div class="pop-over" :class="[elementClasses]" :style="{ '--_anchor-name': anchorName }" :data-placement="placement">
3
+ <div
4
+ ref="rootRef"
5
+ class="pop-over"
6
+ :class="[elementClasses]"
7
+ :style="{ '--_anchor-name': anchorName }"
8
+ :data-placement="placement"
9
+ >
4
10
  <button
5
11
  ref="triggerRef"
6
12
  :popovertarget="popoverId"
@@ -8,11 +14,24 @@
8
14
  type="button"
9
15
  class="pop-over-trigger"
10
16
  :aria-label="triggerAriaLabel || undefined"
17
+ :aria-expanded="isOpen"
18
+ @click="handleTriggerClick"
11
19
  >
12
20
  <slot name="trigger"></slot>
13
21
  </button>
14
22
 
15
- <div :id="popoverId" ref="popoverRef" popover class="pop-over-popover" :aria-label="popoverAriaLabel || undefined" @toggle="handleToggle">
23
+ <div
24
+ :id="popoverId"
25
+ ref="popoverRef"
26
+ popover
27
+ class="pop-over-popover"
28
+ :class="{ 'pop-over-popover-open': usesFallbackPopover && isOpen }"
29
+ :style="positionStyle"
30
+ :data-placement="popoverPlacement"
31
+ :aria-label="popoverAriaLabel || undefined"
32
+ @beforetoggle="handleBeforeToggle"
33
+ @toggle="handleToggle"
34
+ >
16
35
  <button
17
36
  ref="closeButtonRef"
18
37
  :popovertarget="popoverId"
@@ -20,6 +39,7 @@
20
39
  type="button"
21
40
  class="pop-over-close-button"
22
41
  :aria-label="closeButtonAriaLabel"
42
+ @click="hide"
23
43
  >
24
44
  <Icon name="lucide:x" class="pop-over-close-button-icon" aria-hidden="true" />
25
45
  </button>
@@ -55,17 +75,28 @@ const id = useId();
55
75
  const popoverId = `pop-over-${id}`;
56
76
  const anchorName = `--pop-over-anchor-${id}`;
57
77
 
78
+ const rootRef = ref<HTMLDivElement | null>(null);
58
79
  const triggerRef = ref<HTMLButtonElement | null>(null);
59
80
  const popoverRef = ref<HTMLDivElement | null>(null);
60
81
  const closeButtonRef = ref<HTMLButtonElement | null>(null);
61
82
 
62
- /** Move focus into the popover on open; the Popover API restores focus to the trigger on close. */
63
- const handleToggle = (event: Event) => {
64
- const toggleEvent = event as ToggleEvent;
65
- if (toggleEvent.newState === "open") {
66
- closeButtonRef.value?.focus();
67
- }
68
- };
83
+ // Focus moves to the close button on open; closing returns it to the trigger in every mode.
84
+ const {
85
+ isOpen,
86
+ usesFallbackPopover,
87
+ positionStyle,
88
+ popoverPlacement,
89
+ hide,
90
+ handleTriggerClick,
91
+ handleBeforeToggle,
92
+ handleToggle,
93
+ } = useAnchoredPopover({
94
+ rootRef,
95
+ triggerRef,
96
+ popoverRef,
97
+ side: () => props.placement,
98
+ onOpen: () => closeButtonRef.value?.focus(),
99
+ });
69
100
 
70
101
  const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
71
102
 
@@ -128,6 +159,12 @@ watch(
128
159
  }
129
160
  }
130
161
 
162
+ /* Kept apart from :popover-open, which would invalidate a shared selector list where unsupported. */
163
+ &.pop-over-popover-open {
164
+ display: block;
165
+ opacity: 1;
166
+ }
167
+
131
168
  .pop-over-close-button {
132
169
  all: unset;
133
170
  cursor: pointer;
@@ -196,6 +233,30 @@ watch(
196
233
  left: anchor(left);
197
234
  position-try-fallbacks: flip-block;
198
235
  }
236
+
237
+ /* After the placement rules above, so the resolved (possibly flipped) side wins over their insets. */
238
+ @supports not (anchor-name: --a) {
239
+ .pop-over-popover {
240
+ position: fixed;
241
+ z-index: var(--pop-over-z-index, 999999);
242
+
243
+ &[data-placement="right"] {
244
+ inset: var(--_anchor-top, 0px) auto auto calc(var(--_anchor-right, 0px) + var(--pop-over-gap, 1rem));
245
+ }
246
+
247
+ &[data-placement="left"] {
248
+ inset: var(--_anchor-top, 0px) calc(var(--_anchor-left-inverse, 0px) + var(--pop-over-gap, 1rem)) auto auto;
249
+ }
250
+
251
+ &[data-placement="bottom"] {
252
+ inset: calc(var(--_anchor-bottom, 0px) + var(--pop-over-gap, 1rem)) auto auto var(--_anchor-left, 0px);
253
+ }
254
+
255
+ &[data-placement="top"] {
256
+ inset: auto auto calc(var(--_anchor-top-inverse, 0px) + var(--pop-over-gap, 1rem)) var(--_anchor-left, 0px);
257
+ }
258
+ }
259
+ }
199
260
  }
200
261
  }
201
262
  </style>
@@ -1,6 +1,7 @@
1
1
  import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
2
  import { mountSuspended } from "@nuxt/test-utils/runtime";
3
3
  import PopOver from "../PopOver.vue";
4
+ import { nextTick } from "vue";
4
5
 
5
6
  interface PopOverInstance {
6
7
  popoverId: string;
@@ -192,4 +193,56 @@ describe("PopOver", () => {
192
193
  expect(wrapper.find(".pop-over").classes()).toContain("updated-class");
193
194
  });
194
195
  });
196
+
197
+ describe("Without the Popover API", () => {
198
+ beforeEach(() => {
199
+ Object.defineProperty(HTMLElement.prototype, "showPopover", { value: undefined, writable: true, configurable: true });
200
+ });
201
+
202
+ it("opens from the trigger and closes from the close button, returning focus to the trigger", async () => {
203
+ wrapper = await mountSuspended(PopOver, {
204
+ slots: { trigger: "<span>Open</span>", content: "<p>Body</p>" },
205
+ attachTo: document.body,
206
+ });
207
+ const trigger = wrapper.find(".pop-over-trigger");
208
+ const popover = wrapper.find(".pop-over-popover");
209
+
210
+ await trigger.trigger("click");
211
+ await nextTick();
212
+ expect(popover.classes()).toContain("pop-over-popover-open");
213
+ expect(trigger.attributes("aria-expanded")).toBe("true");
214
+ expect(document.activeElement).toBe(wrapper.find(".pop-over-close-button").element);
215
+
216
+ await wrapper.find(".pop-over-close-button").trigger("click");
217
+ expect(popover.classes()).not.toContain("pop-over-popover-open");
218
+ expect(document.activeElement).toBe(trigger.element);
219
+ });
220
+ });
221
+
222
+ describe("Without anchor positioning", () => {
223
+ const originalCSS = globalThis.CSS;
224
+
225
+ beforeEach(() => {
226
+ Object.defineProperty(globalThis, "CSS", { value: { supports: () => false }, writable: true, configurable: true });
227
+ });
228
+
229
+ afterEach(() => {
230
+ Object.defineProperty(globalThis, "CSS", { value: originalCSS, writable: true, configurable: true });
231
+ });
232
+
233
+ it("places the popover on the current placement prop's side", async () => {
234
+ wrapper = await createWrapper({ placement: "left" });
235
+ const vm = wrapper.vm as unknown as PopOverInstance;
236
+ const popover = wrapper.find(".pop-over-popover");
237
+
238
+ vm.handleToggle(Object.assign(new Event("toggle"), { newState: "open" }));
239
+ await nextTick();
240
+ expect(popover.attributes("data-placement")).toBe("left");
241
+
242
+ await wrapper.setProps({ placement: "top" });
243
+ vm.handleToggle(Object.assign(new Event("toggle"), { newState: "open" }));
244
+ await nextTick();
245
+ expect(popover.attributes("data-placement")).toBe("top");
246
+ });
247
+ });
195
248
  });
@@ -1,7 +1,7 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`PopOver > renders correct HTML structure 1`] = `
4
- "<div class="pop-over" style="--_anchor-name: --pop-over-anchor-v-0-0;" data-placement="right"><button popovertarget="pop-over-v-0-0" popovertargetaction="toggle" type="button" class="pop-over-trigger"><span>Open</span></button>
4
+ "<div class="pop-over" style="--_anchor-name: --pop-over-anchor-v-0-0;" data-placement="right"><button popovertarget="pop-over-v-0-0" popovertargetaction="toggle" type="button" class="pop-over-trigger" aria-expanded="false"><span>Open</span></button>
5
5
  <div id="pop-over-v-0-0" popover="" class="pop-over-popover"><button popovertarget="pop-over-v-0-0" popovertargetaction="hide" type="button" class="pop-over-close-button" aria-label="Close"><span class="iconify i-lucide:x pop-over-close-button-icon" aria-hidden="true"></span></button>
6
6
  <div class="pop-over-content">
7
7
  <p>Details</p>
@@ -4,17 +4,25 @@
4
4
 
5
5
  Each strength level (border 1-6, shadow 1-6, inner shadow 1-4) has its own token, defaulting to a
6
6
  built-in elevation/border scale. Only the tokens for the strength levels actually in use need
7
- overriding.
7
+ overriding. To recolour every level at once, set the colour tokens instead.
8
+
9
+ The colour tokens are private-wrapped (`--_border-colour`, `--_shadow-colour`, `--_inner-shadow-colour`)
10
+ because each is reused across every strength level; set the public token, not the `--_` one.
8
11
 
9
12
  | Token | Default | Controls |
10
13
  |---|---|---|
11
- | `--ui-block-decorated-border-1` .. `-6` | `1px` .. `6px solid var(--theme-border)` | Border at each strength level |
12
- | `--ui-block-decorated-shadow-1` .. `-6` | `0 1px 2px rgba(0,0,0,0.08)` .. `0 24px 32px rgba(0,0,0,0.18)` | Drop shadow at each strength level |
13
- | `--ui-block-decorated-inner-shadow-1` .. `-4` | `inset 0 1px 2px rgba(0,0,0,0.08)` .. `inset 0 6px 8px rgba(0,0,0,0.14)` | Inset shadow at each strength level |
14
+ | `--ui-block-decorated-border-colour` | `var(--theme-border)` | Border colour for every strength level |
15
+ | `--ui-block-decorated-shadow-colour` | `#000` | Drop shadow colour for every strength level; each level mixes in its own opacity (8% .. 18%) |
16
+ | `--ui-block-decorated-inner-shadow-colour` | `--ui-block-decorated-shadow-colour` | Inner shadow colour for every strength level (8% .. 14% opacity) |
17
+ | `--ui-block-decorated-border-1` .. `-6` | `1px` .. `6px solid` the border colour | Whole border at each strength level |
18
+ | `--ui-block-decorated-shadow-1` .. `-6` | `0 1px 2px` .. `0 24px 32px` in the shadow colour | Whole drop shadow at each strength level |
19
+ | `--ui-block-decorated-inner-shadow-1` .. `-4` | `inset 0 1px 2px` .. `inset 0 6px 8px` in the inner shadow colour | Whole inset shadow at each strength level |
14
20
  | `--ui-block-decorated-inner-shadow-highlight` | `inset 0 1px 0 rgba(255,255,255,0.15)` | Highlight layered on top of every inner shadow strength |
15
21
 
16
22
  ```css
17
23
  .my-page {
24
+ --ui-block-decorated-border-colour: #7c3aed;
25
+ --ui-block-decorated-shadow-colour: #7c3aed;
18
26
  --ui-block-decorated-shadow-3: 0 6px 12px rgba(0, 0, 0, 0.2);
19
27
  --ui-block-decorated-border-2: 2px dashed hotpink;
20
28
  }
@@ -43,48 +43,52 @@ watch(
43
43
  <style lang="css">
44
44
  @layer components {
45
45
  .ui-block-decorated {
46
+ --_border-colour: var(--ui-block-decorated-border-colour, var(--theme-border));
47
+ --_shadow-colour: var(--ui-block-decorated-shadow-colour, #000);
48
+ --_inner-shadow-colour: var(--ui-block-decorated-inner-shadow-colour, var(--_shadow-colour));
49
+
46
50
  /*
47
51
  * Border strength classes
48
52
  */
49
53
  &.ui-block-decorated-border-1 {
50
- border: var(--ui-block-decorated-border-1, 1px solid var(--theme-border));
54
+ border: var(--ui-block-decorated-border-1, 1px solid var(--_border-colour));
51
55
  }
52
56
  &.ui-block-decorated-border-2 {
53
- border: var(--ui-block-decorated-border-2, 2px solid var(--theme-border));
57
+ border: var(--ui-block-decorated-border-2, 2px solid var(--_border-colour));
54
58
  }
55
59
  &.ui-block-decorated-border-3 {
56
- border: var(--ui-block-decorated-border-3, 3px solid var(--theme-border));
60
+ border: var(--ui-block-decorated-border-3, 3px solid var(--_border-colour));
57
61
  }
58
62
  &.ui-block-decorated-border-4 {
59
- border: var(--ui-block-decorated-border-4, 4px solid var(--theme-border));
63
+ border: var(--ui-block-decorated-border-4, 4px solid var(--_border-colour));
60
64
  }
61
65
  &.ui-block-decorated-border-5 {
62
- border: var(--ui-block-decorated-border-5, 5px solid var(--theme-border));
66
+ border: var(--ui-block-decorated-border-5, 5px solid var(--_border-colour));
63
67
  }
64
68
  &.ui-block-decorated-border-6 {
65
- border: var(--ui-block-decorated-border-6, 6px solid var(--theme-border));
69
+ border: var(--ui-block-decorated-border-6, 6px solid var(--_border-colour));
66
70
  }
67
71
 
68
72
  /*
69
73
  * Shadow strength classes
70
74
  */
71
75
  &.ui-block-decorated-shadow-1 {
72
- box-shadow: var(--ui-block-decorated-shadow-1, 0 1px 2px rgba(0, 0, 0, 0.08));
76
+ box-shadow: var(--ui-block-decorated-shadow-1, 0 1px 2px color-mix(in srgb, var(--_shadow-colour) 8%, transparent));
73
77
  }
74
78
  &.ui-block-decorated-shadow-2 {
75
- box-shadow: var(--ui-block-decorated-shadow-2, 0 2px 4px rgba(0, 0, 0, 0.1));
79
+ box-shadow: var(--ui-block-decorated-shadow-2, 0 2px 4px color-mix(in srgb, var(--_shadow-colour) 10%, transparent));
76
80
  }
77
81
  &.ui-block-decorated-shadow-3 {
78
- box-shadow: var(--ui-block-decorated-shadow-3, 0 4px 8px rgba(0, 0, 0, 0.12));
82
+ box-shadow: var(--ui-block-decorated-shadow-3, 0 4px 8px color-mix(in srgb, var(--_shadow-colour) 12%, transparent));
79
83
  }
80
84
  &.ui-block-decorated-shadow-4 {
81
- box-shadow: var(--ui-block-decorated-shadow-4, 0 8px 16px rgba(0, 0, 0, 0.14));
85
+ box-shadow: var(--ui-block-decorated-shadow-4, 0 8px 16px color-mix(in srgb, var(--_shadow-colour) 14%, transparent));
82
86
  }
83
87
  &.ui-block-decorated-shadow-5 {
84
- box-shadow: var(--ui-block-decorated-shadow-5, 0 16px 24px rgba(0, 0, 0, 0.16));
88
+ box-shadow: var(--ui-block-decorated-shadow-5, 0 16px 24px color-mix(in srgb, var(--_shadow-colour) 16%, transparent));
85
89
  }
86
90
  &.ui-block-decorated-shadow-6 {
87
- box-shadow: var(--ui-block-decorated-shadow-6, 0 24px 32px rgba(0, 0, 0, 0.18));
91
+ box-shadow: var(--ui-block-decorated-shadow-6, 0 24px 32px color-mix(in srgb, var(--_shadow-colour) 18%, transparent));
88
92
  }
89
93
 
90
94
  /*
@@ -115,25 +119,25 @@ watch(
115
119
 
116
120
  &.ui-block-decorated-inner-shadow-1::before {
117
121
  box-shadow:
118
- var(--ui-block-decorated-inner-shadow-1, inset 0 1px 2px rgba(0, 0, 0, 0.08)),
122
+ var(--ui-block-decorated-inner-shadow-1, inset 0 1px 2px color-mix(in srgb, var(--_inner-shadow-colour) 8%, transparent)),
119
123
  var(--ui-block-decorated-inner-shadow-highlight, inset 0 1px 0 rgba(255, 255, 255, 0.15));
120
124
  }
121
125
 
122
126
  &.ui-block-decorated-inner-shadow-2::before {
123
127
  box-shadow:
124
- var(--ui-block-decorated-inner-shadow-2, inset 0 2px 4px rgba(0, 0, 0, 0.1)),
128
+ var(--ui-block-decorated-inner-shadow-2, inset 0 2px 4px color-mix(in srgb, var(--_inner-shadow-colour) 10%, transparent)),
125
129
  var(--ui-block-decorated-inner-shadow-highlight, inset 0 1px 0 rgba(255, 255, 255, 0.15));
126
130
  }
127
131
 
128
132
  &.ui-block-decorated-inner-shadow-3::before {
129
133
  box-shadow:
130
- var(--ui-block-decorated-inner-shadow-3, inset 0 4px 6px rgba(0, 0, 0, 0.12)),
134
+ var(--ui-block-decorated-inner-shadow-3, inset 0 4px 6px color-mix(in srgb, var(--_inner-shadow-colour) 12%, transparent)),
131
135
  var(--ui-block-decorated-inner-shadow-highlight, inset 0 1px 0 rgba(255, 255, 255, 0.15));
132
136
  }
133
137
 
134
138
  &.ui-block-decorated-inner-shadow-4::before {
135
139
  box-shadow:
136
- var(--ui-block-decorated-inner-shadow-4, inset 0 6px 8px rgba(0, 0, 0, 0.14)),
140
+ var(--ui-block-decorated-inner-shadow-4, inset 0 6px 8px color-mix(in srgb, var(--_inner-shadow-colour) 14%, transparent)),
137
141
  var(--ui-block-decorated-inner-shadow-highlight, inset 0 1px 0 rgba(255, 255, 255, 0.15));
138
142
  }
139
143
  }