@redseed/redseed-ui-vue3 8.39.0 → 8.40.0

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.39.0",
3
+ "version": "8.40.0",
4
4
  "description": "RedSeed UI Vue 3 components",
5
5
  "main": "index.js",
6
6
  "repository": "https://github.com/redseedtraining/redseed-ui",
@@ -0,0 +1,146 @@
1
+ <script setup>
2
+ import { ref, computed } from 'vue'
3
+ import { useResizeObserver, useMutationObserver } from '@vueuse/core'
4
+ import Card from './Card.vue'
5
+
6
+ const props = defineProps({
7
+ clickable: {
8
+ type: Boolean,
9
+ default: true,
10
+ },
11
+ hoverable: {
12
+ type: Boolean,
13
+ default: true,
14
+ },
15
+ bordered: {
16
+ type: Boolean,
17
+ default: true,
18
+ },
19
+ disabled: {
20
+ type: Boolean,
21
+ default: false,
22
+ },
23
+ // Accessible name for the clickable action layer (forwarded to Card's ariaLabel prop).
24
+ // Equivalent to the #aria-label slot — use either form, not both.
25
+ ariaLabel: {
26
+ type: String,
27
+ default: undefined,
28
+ },
29
+ phComponent: {
30
+ type: String,
31
+ default: 'CardHorizontal',
32
+ },
33
+ })
34
+
35
+ defineEmits(['click'])
36
+
37
+ /* ── Floating thumbnail detection ────────────────────────────────────────────
38
+ Adds --floating to the thumbnail whenever the image does not reach the card
39
+ bottom, so CSS can restore the image's bottom-right radius.
40
+
41
+ Two independent conditions trigger floating. clientHeight is integer-rounded, so
42
+ strict `<` comparisons are stable — sub-pixel gaps (< 0.5 px) round to 0 and
43
+ produce equal clientHeights, leaving truly-flush images unaffected:
44
+ 1. imgH > 0 && imgH < thumbH
45
+ Landscape: the loaded img is shorter than the 160px rail.
46
+ The imgH > 0 guard avoids a false positive on square cards while the
47
+ Image component's picture is still v-show=false (img.clientHeight = 0).
48
+ 2. thumbH < cardContentH
49
+ Tall-body: the body column is taller than the 160px thumbnail.
50
+
51
+ NOTE: thumbIsFloating applies --floating in both stacked (mobile) and rail
52
+ (≥512px container) layouts. In the stacked layout the --floating CSS rules are
53
+ scoped to the @container/card-horizontal block so they have no visual effect —
54
+ the flag may be true in the stacked layout but is intentionally not guarded here.
55
+
56
+ useResizeObserver watches three targets: the thumbnail element, its parent
57
+ (content wrapper), and the currently-rendered img (currentImg ref — auto-retargets
58
+ via vueuse's reactive watch when the img reference changes, replacing the manual
59
+ unobserve/observe swap in the previous hand-rolled implementation).
60
+ useMutationObserver fires updateFloating when DOM children change in the thumbnail
61
+ subtree — catches Image component adding its <img> when originalUrl is set after
62
+ the card has already mounted (skeleton-first / data-loaded-later pattern).
63
+ All observers auto-cleanup on scope dispose (no onMounted/onUnmounted needed). */
64
+
65
+ const thumbEl = ref(null)
66
+ const thumbIsFloating = ref(false)
67
+ const currentImg = ref(null)
68
+ const thumbParent = computed(() => thumbEl.value?.parentElement ?? null)
69
+
70
+ function updateFloating() {
71
+ const thumb = thumbEl.value
72
+ if (!thumb) return
73
+
74
+ // Scoped img lookup: prefer the img inside an rsui-image wrapper (the common
75
+ // case), then fall back to a raw direct-child img. The :scope > img limit
76
+ // prevents a badge or avatar img nested deeper in the slot from being measured.
77
+ const img =
78
+ thumb.querySelector('.rsui-image img') ||
79
+ thumb.querySelector(':scope > img')
80
+
81
+ // Keep currentImg in sync so useResizeObserver(currentImg) auto-retargets
82
+ // the img observer when a late-appearing element replaces the previous one.
83
+ currentImg.value = img ?? null
84
+
85
+ const imgH = img ? img.clientHeight : 0
86
+ const thumbH = thumb.clientHeight
87
+ const cardContentH = thumb.parentElement ? thumb.parentElement.clientHeight : thumbH
88
+ // imgH > 0 guard on condition 1: while the Image component's picture is v-show=false
89
+ // (loading / empty state), img.clientHeight is 0. Without the guard a square card would
90
+ // incorrectly receive --floating at mount; the class self-corrects once the image loads
91
+ // and condition 1 can be re-evaluated with the real rendered height.
92
+ //
93
+ // Strict comparison (no px tolerance): clientHeight is integer-rounded, so a truly-flush
94
+ // image (sub-pixel gap < 0.5 px) produces equal integer heights and the condition is
95
+ // false. ANY visible gap (≥ 1 integer px) correctly triggers floating.
96
+ thumbIsFloating.value = (imgH > 0 && imgH < thumbH) || (thumbH < cardContentH)
97
+ }
98
+
99
+ useResizeObserver(thumbEl, updateFloating)
100
+ useResizeObserver(thumbParent, updateFloating)
101
+ useResizeObserver(currentImg, updateFloating)
102
+ useMutationObserver(thumbEl, updateFloating, { childList: true, subtree: true })
103
+ </script>
104
+ <template>
105
+ <Card
106
+ class="rsui-card-horizontal"
107
+ :clickable="clickable"
108
+ :hoverable="hoverable"
109
+ :bordered="bordered"
110
+ :disabled="disabled"
111
+ :padded="false"
112
+ :aria-label="ariaLabel"
113
+ :ph-component="phComponent"
114
+ @click="$emit('click')"
115
+ >
116
+ <template v-if="$slots['aria-label']" #aria-label>
117
+ <slot name="aria-label"></slot>
118
+ </template>
119
+
120
+ <!-- thumbnail slot: required for the horizontal rail layout.
121
+ Pass an Image component (originalUrl + alt) to populate the 160px column.
122
+ An empty slot renders a blank 160px rail gap in the rail layout. -->
123
+ <div
124
+ ref="thumbEl"
125
+ class="rsui-card-horizontal__thumbnail"
126
+ :class="{ 'rsui-card-horizontal__thumbnail--floating': thumbIsFloating }"
127
+ >
128
+ <slot name="thumbnail"></slot>
129
+ </div>
130
+
131
+ <div class="rsui-card-horizontal__body">
132
+ <div class="rsui-card-horizontal__content">
133
+ <div v-if="$slots.title" class="rsui-card-horizontal__title">
134
+ <slot name="title"></slot>
135
+ </div>
136
+ <div v-if="$slots.description" class="rsui-card-horizontal__description">
137
+ <slot name="description"></slot>
138
+ </div>
139
+ </div>
140
+
141
+ <div v-if="$slots.meta" class="rsui-card-horizontal__meta">
142
+ <slot name="meta"></slot>
143
+ </div>
144
+ </div>
145
+ </Card>
146
+ </template>
@@ -2,6 +2,7 @@ import ButtonCard from './ButtonCard.vue'
2
2
  import CheckboxCard from './CheckboxCard.vue'
3
3
  import Card from './Card.vue'
4
4
  import CardHeader from './CardHeader.vue'
5
+ import CardHorizontal from './CardHorizontal.vue'
5
6
  import CardResource from './CardResource.vue'
6
7
  import RadioCard from './RadioCard.vue'
7
8
  import MetricCard from './MetricCard.vue'
@@ -10,6 +11,7 @@ export {
10
11
  CheckboxCard,
11
12
  Card,
12
13
  CardHeader,
14
+ CardHorizontal,
13
15
  CardResource,
14
16
  RadioCard,
15
17
  MetricCard,