@redseed/redseed-ui-vue3 8.39.0 → 8.40.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
|
@@ -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,
|
|
@@ -196,6 +196,20 @@ function handleKeyup(event) {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
function handleKeydown(event) {
|
|
199
|
+
// Let selection modifiers pass through to the native input so keyboard
|
|
200
|
+
// text-selection shortcuts work while typing (e.g. Shift+Home, Ctrl+Shift+
|
|
201
|
+
// ArrowLeft on Windows; Cmd+Shift+ArrowLeft, Option+Shift+ArrowLeft on
|
|
202
|
+
// macOS). Without this, the preventDefault() calls below would intercept
|
|
203
|
+
// Arrow/Home/End and kill the browser's native text selection.
|
|
204
|
+
if (event.shiftKey || event.ctrlKey || event.metaKey) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
// Alt+ArrowDown is the standard ARIA combobox open shortcut — let it through.
|
|
208
|
+
// Other Alt combos (e.g. Option+Arrow on macOS) pass through to native.
|
|
209
|
+
if (event.altKey && event.key !== 'ArrowDown') {
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
199
213
|
if (event.key === 'ArrowDown') {
|
|
200
214
|
event.preventDefault()
|
|
201
215
|
if (!isOpen.value) {
|