@redseed/redseed-ui-vue3 8.65.0 → 8.66.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redseed/redseed-ui-vue3",
3
- "version": "8.65.0",
3
+ "version": "8.66.1",
4
4
  "description": "RedSeed UI Vue 3 components",
5
5
  "main": "index.js",
6
6
  "repository": "https://github.com/redseedtraining/redseed-ui",
@@ -73,7 +73,12 @@ const clampClasses = {
73
73
  6: 'rsui-body-text--line-clamp-6',
74
74
  }
75
75
 
76
+ // Only ONE of these applies, and the first match in this order wins — so
77
+ // `onColor` leads. It marks a dark ground, and a muted ink passed alongside it
78
+ // (`tertiary onColor` for a date under a body paragraph) is a light-ground grey
79
+ // that all but disappears there; white is the readable reading of that pair.
76
80
  const colorClasses = {
81
+ onColor: 'rsui-body-text--on-color',
77
82
  secondary: 'rsui-body-text--secondary',
78
83
  tertiary: 'rsui-body-text--tertiary',
79
84
  quaternary: 'rsui-body-text--quaternary',
@@ -84,7 +89,6 @@ const colorClasses = {
84
89
  error: 'rsui-body-text--error',
85
90
  info: 'rsui-body-text--info',
86
91
  ai: 'rsui-body-text--ai',
87
- onColor: 'rsui-body-text--on-color',
88
92
  }
89
93
 
90
94
  const clampClass = computed(() => clampClasses[props.lines] || null)
@@ -5,7 +5,9 @@ const props = defineProps({
5
5
  variant: {
6
6
  type: String,
7
7
  default: 'primary',
8
- validator: (value) => ['primary', 'secondary', 'brand', 'success', 'info', 'warning', 'error', 'ai', 'classic'].includes(value)
8
+ // `ai` is the pale animated wash; `ai-bold` is the same family at full
9
+ // strength — the dark plum surface Card's `aiBold` uses.
10
+ validator: (value) => ['primary', 'secondary', 'brand', 'success', 'info', 'warning', 'error', 'ai', 'ai-bold', 'classic'].includes(value)
9
11
  },
10
12
  bordered: {
11
13
  type: Boolean,
@@ -59,6 +61,7 @@ const variantClass = computed(() => [
59
61
  'rsui-section--warning': props.variant === 'warning',
60
62
  'rsui-section--error': props.variant === 'error',
61
63
  'rsui-section--ai': props.variant === 'ai',
64
+ 'rsui-section--ai-bold': props.variant === 'ai-bold',
62
65
  'rsui-section--classic': props.variant === 'classic',
63
66
  },
64
67
  ])
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
- import { computed, watchEffect, useSlots, Comment, Fragment } from 'vue'
2
+ import { computed, watchEffect, useSlots } from 'vue'
3
3
  import { isSafeHref } from '../../helpers/href'
4
+ import { rendersContent } from '../../helpers/slots'
4
5
 
5
6
  const props = defineProps({
6
7
  showContent: {
@@ -62,31 +63,6 @@ const showViewAll = computed(() => hasViewAllLabel.value && hasViewAllHref.value
62
63
 
63
64
  const slots = useSlots()
64
65
 
65
- /**
66
- * Whether a slot renders anything at all. Slot *presence* is not the same
67
- * question: `<template #default><Button v-if="canManage" /></template>` leaves a
68
- * truthy slot function that renders a comment placeholder, which would still
69
- * claim a `flex-1` region and shove the centred link into one half of the footer.
70
- */
71
- function rendersContent(slot) {
72
- const nodes = slot?.()
73
-
74
- if (!nodes) return false
75
-
76
- // Comment vnodes are what a falsy `v-if` leaves behind. Fragments wrap lists
77
- // and `<template v-if>` blocks, so a truthy wrapper around a falsy child is a
78
- // Fragment whose only child is a Comment — counting its length would call that
79
- // content and render an empty region claiming `flex-1`.
80
- return nodes.some(rendersNode)
81
- }
82
-
83
- function rendersNode(node) {
84
- if (node.type === Comment) return false
85
- if (node.type === Fragment) return Array.isArray(node.children) && node.children.some(rendersNode)
86
-
87
- return true
88
- }
89
-
90
66
  /**
91
67
  * Whether the view-all has the footer to itself, which is the case that wants
92
68
  * centring. With anything alongside it, a greedy region would split the row with
@@ -1,5 +1,6 @@
1
1
  <script setup>
2
2
  import { ref, computed, watch, watchEffect, useSlots } from 'vue'
3
+ import { rendersContent } from '../../helpers/slots'
3
4
  import { useScroll, useEventListener, watchDebounced } from '@vueuse/core'
4
5
  import Section from './Section.vue'
5
6
  import SectionHeader from './SectionHeader.vue'
@@ -124,13 +125,64 @@ const slots = useSlots()
124
125
  /**
125
126
  * A consumer-supplied `actions` slot always wins and always renders in the
126
127
  * header — that slot exists so consumers can put their own controls up there.
127
- * The built-in arrows go to whichever placement `hoverActions` selects.
128
+ * The built-in arrows go to whichever placement `hoverActions` selects, unless
129
+ * a consumer `actions` slot displaces them from the header, in which case header
130
+ * mode renders none at all.
131
+ *
132
+ * Rendered output, not slot presence, and a plain function rather than a
133
+ * `computed`. Both matter, and getting either wrong breaks the combination this
134
+ * component exists to support — a consumer control in the header with the arrows
135
+ * on the rail:
136
+ *
137
+ * - `Boolean(slots.actions)` is true for `<template #actions><Link v-if="hasMore"
138
+ * /></template>` even when `hasMore` is false, because the slot function still
139
+ * exists and just returns a comment. The arrows are this slot's FALLBACK
140
+ * content, so Vue renders them back into the header the moment the slot
141
+ * resolves to nothing — while `hoverActions` has already put a pair on the
142
+ * rail. Two pairs on one rail, identical accessible names, and disagreeing
143
+ * disabled states. Measured at four buttons.
144
+ * - `useSlots()` is not reactive, so a `computed` over it has no dependency and
145
+ * caches its first answer for the life of the component. A consumer writing
146
+ * `<template v-if="hasMore" #actions>` against a count that arrives with a
147
+ * fetch would never see their control appear at all.
148
+ *
149
+ * Same reasoning and the same helper as SectionFooter — see helpers/slots.
128
150
  */
129
- const hasCustomActions = computed(() => Boolean(slots.actions))
151
+ /**
152
+ * The scope the `actions` slot is handed. Built once so the probe below and the
153
+ * real `<slot>` in the template receive the same object — a consumer template
154
+ * that destructures these compiles to a function destructuring its only
155
+ * parameter, so probing bare threw and rendered nothing at all.
156
+ */
157
+ const actionsSlotScope = computed(() => ({
158
+ showNextSlide,
159
+ showPreviousSlide,
160
+ disabledPrevButton: disabledPrevButton.value,
161
+ disabledNextButton: disabledNextButton.value,
162
+ }))
163
+
164
+ function hasCustomActions() {
165
+ return rendersContent(slots.actions, actionsSlotScope.value)
166
+ }
130
167
 
131
- const showHeaderActions = computed(() => hasCustomActions.value || !props.hoverActions)
168
+ function showHeaderActions() {
169
+ return hasCustomActions() || !props.hoverActions
170
+ }
132
171
 
133
- const showOverlayActions = computed(() => !hasCustomActions.value && props.hoverActions)
172
+ /**
173
+ * Placement is `hoverActions`' decision alone. A custom `actions` slot does not
174
+ * suppress the overlay: the arrows are that slot's fallback content, so
175
+ * overriding it has already taken them out of the header — which is exactly when
176
+ * the rail needs them. Guarding on `hasCustomActions` here left the one
177
+ * combination consumers reach for (own header control, arrows on the rail) with
178
+ * no navigation at all from `lg` up, where the track is `overflow-x-hidden` and
179
+ * the arrows are the only way to move it. `hoverActions` exists to free the
180
+ * header for such a control in the first place.
181
+ *
182
+ * Safe as a `computed`: it reads only a prop. The header side is what has to
183
+ * track a slot.
184
+ */
185
+ const showOverlayActions = computed(() => props.hoverActions)
134
186
 
135
187
  /**
136
188
  * Unique id for the slider
@@ -548,13 +600,8 @@ function showPreviousSlide() {
548
600
  <slot name="see-all"></slot>
549
601
  </template>
550
602
 
551
- <template v-if="showHeaderActions" #actions>
552
- <slot name="actions"
553
- :showNextSlide="showNextSlide"
554
- :showPreviousSlide="showPreviousSlide"
555
- :disabledPrevButton="disabledPrevButton"
556
- :disabledNextButton="disabledNextButton"
557
- >
603
+ <template v-if="showHeaderActions()" #actions>
604
+ <slot name="actions" v-bind="actionsSlotScope">
558
605
  <SectionSliderAction direction="prev"
559
606
  :disabled="disabledPrevButton"
560
607
  :invert="featured"
@@ -0,0 +1,59 @@
1
+ import { Comment, Fragment } from 'vue'
2
+
3
+ /**
4
+ * Whether a slot renders anything at all.
5
+ *
6
+ * Internal on purpose — not re-exported from `helpers/index.js`. It walks vnode
7
+ * internals, which is a detail of how this library composes its own regions
8
+ * rather than something consuming apps should depend on.
9
+ *
10
+ * Slot *presence* is a different question, and the wrong one in almost every
11
+ * case. `Boolean(slots.actions)` is true for
12
+ * `<template #actions><Button v-if="canManage" /></template>` even when
13
+ * `canManage` is false, because the slot function still exists — it just returns
14
+ * a comment placeholder. Components that use presence to decide whether to stand
15
+ * their own content down then stand it down for a slot that renders nothing, and
16
+ * the region ends up empty, or the fallback it was suppressing comes back.
17
+ *
18
+ * Two callers, for two versions of that bug: SectionFooter, where an empty region
19
+ * claimed `flex-1` and shoved a centred link into one half of the footer, and
20
+ * SectionSlider, where an `#actions` slot rendering nothing let the arrows return
21
+ * to the header while `hoverActions` had already put a pair on the rail — two
22
+ * pairs on one rail, with identical accessible names.
23
+ *
24
+ * Call this from the template or from a plain function, never from a `computed`:
25
+ * `useSlots()` returns a non-reactive object, so a computed over it caches its
26
+ * first answer for the life of the component and a conditionally-supplied slot
27
+ * would never appear.
28
+ *
29
+ * `scope` is REQUIRED for a scoped slot, and passing it is the caller's job. A
30
+ * consumer template that destructures its scoped props — `<template #actions="{
31
+ * showPreviousSlide }">` — compiles to a function that destructures its only
32
+ * parameter, so probing it bare throws `Cannot destructure property … of
33
+ * 'undefined'` and takes the whole component down with it. Hand it the same
34
+ * object the real `<slot>` receives, so the probe and the render agree. A
35
+ * non-scoped slot ignores the argument, so passing nothing stays correct there.
36
+ *
37
+ * Deliberately not wrapped in try/catch: that would swallow a genuine error
38
+ * thrown by the consumer's own slot content and leave a silently empty region.
39
+ */
40
+ export function rendersContent(slot, scope) {
41
+ const nodes = slot?.(scope)
42
+
43
+ if (!nodes) return false
44
+
45
+ return nodes.some(rendersNode)
46
+ }
47
+
48
+ /**
49
+ * Comment vnodes are what a falsy `v-if` leaves behind. Fragments wrap lists and
50
+ * `<template v-if>` blocks, so a truthy wrapper around a falsy child is a
51
+ * Fragment whose only child is a Comment — counting length would treat that as
52
+ * content.
53
+ */
54
+ function rendersNode(node) {
55
+ if (node.type === Comment) return false
56
+ if (node.type === Fragment) return Array.isArray(node.children) && node.children.some(rendersNode)
57
+
58
+ return true
59
+ }