@redseed/redseed-ui-vue3 11.0.2 → 11.2.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
|
@@ -46,6 +46,23 @@ const props = defineProps({
|
|
|
46
46
|
type: Boolean,
|
|
47
47
|
default: true,
|
|
48
48
|
},
|
|
49
|
+
/**
|
|
50
|
+
* Keep the actions in the header row, whatever the container measures.
|
|
51
|
+
*
|
|
52
|
+
* The swap this overrides treats a narrow container as a phone and drops the actions
|
|
53
|
+
* under the title. `CardHorizontal` is never a phone: its header sits beside a fixed
|
|
54
|
+
* thumbnail rail inside a body column, so it is narrow by construction at every width.
|
|
55
|
+
*
|
|
56
|
+
* Opt-in and `false` by default, so no existing card moves. Scoped rather than a change
|
|
57
|
+
* to the rule itself, because the rule is right for a Card — measured on
|
|
58
|
+
* `InNarrowContainer`, two labelled buttons take 244px of a 398px header and the avatar
|
|
59
|
+
* takes its share before the title sees any, leaving the title 48px. Capping the toolbar
|
|
60
|
+
* does not rescue that either; it leaves the title at 95px.
|
|
61
|
+
*/
|
|
62
|
+
inlineActions: {
|
|
63
|
+
type: Boolean,
|
|
64
|
+
default: false,
|
|
65
|
+
},
|
|
49
66
|
/**
|
|
50
67
|
* Force the overflow menu on, or suppress it.
|
|
51
68
|
*
|
|
@@ -155,6 +172,7 @@ const isColumnOnAWiderPage = useColumnOnAWiderPage(cardHeaderWidth)
|
|
|
155
172
|
* refs read below are still tracked, because the render effect is what calls this.
|
|
156
173
|
*/
|
|
157
174
|
function keepsActionsInline() {
|
|
175
|
+
if (props.inlineActions) return true
|
|
158
176
|
if (responsiveWidth.value.specific) return true
|
|
159
177
|
|
|
160
178
|
return isColumnOnAWiderPage.value && countsRenderedNodes(slots.actions) === 1
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { ref, computed } from 'vue'
|
|
3
|
-
import { useResizeObserver, useMutationObserver } from '@vueuse/core'
|
|
4
2
|
import Card from './Card.vue'
|
|
3
|
+
import CardHeader from './CardHeader.vue'
|
|
5
4
|
|
|
6
5
|
const props = defineProps({
|
|
7
6
|
/**
|
|
@@ -41,72 +40,16 @@ const props = defineProps({
|
|
|
41
40
|
|
|
42
41
|
defineEmits(['click'])
|
|
43
42
|
|
|
44
|
-
/*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Image component's picture is still v-show=false (img.clientHeight = 0).
|
|
55
|
-
2. thumbH < cardContentH
|
|
56
|
-
Tall-body: the body column is taller than the 160px thumbnail.
|
|
57
|
-
|
|
58
|
-
NOTE: thumbIsFloating applies --floating in both stacked (mobile) and rail
|
|
59
|
-
(≥512px container) layouts. In the stacked layout the --floating CSS rules are
|
|
60
|
-
scoped to the @container/card-horizontal block so they have no visual effect —
|
|
61
|
-
the flag may be true in the stacked layout but is intentionally not guarded here.
|
|
62
|
-
|
|
63
|
-
useResizeObserver watches three targets: the thumbnail element, its parent
|
|
64
|
-
(content wrapper), and the currently-rendered img (currentImg ref — auto-retargets
|
|
65
|
-
via vueuse's reactive watch when the img reference changes, replacing the manual
|
|
66
|
-
unobserve/observe swap in the previous hand-rolled implementation).
|
|
67
|
-
useMutationObserver fires updateFloating when DOM children change in the thumbnail
|
|
68
|
-
subtree — catches Image component adding its <img> when originalUrl is set after
|
|
69
|
-
the card has already mounted (skeleton-first / data-loaded-later pattern).
|
|
70
|
-
All observers auto-cleanup on scope dispose (no onMounted/onUnmounted needed). */
|
|
71
|
-
|
|
72
|
-
const thumbEl = ref(null)
|
|
73
|
-
const thumbIsFloating = ref(false)
|
|
74
|
-
const currentImg = ref(null)
|
|
75
|
-
const thumbParent = computed(() => thumbEl.value?.parentElement ?? null)
|
|
76
|
-
|
|
77
|
-
function updateFloating() {
|
|
78
|
-
const thumb = thumbEl.value
|
|
79
|
-
if (!thumb) return
|
|
80
|
-
|
|
81
|
-
// Scoped img lookup: prefer the img inside an rsui-image wrapper (the common
|
|
82
|
-
// case), then fall back to a raw direct-child img. The :scope > img limit
|
|
83
|
-
// prevents a badge or avatar img nested deeper in the slot from being measured.
|
|
84
|
-
const img =
|
|
85
|
-
thumb.querySelector('.rsui-image img') ||
|
|
86
|
-
thumb.querySelector(':scope > img')
|
|
87
|
-
|
|
88
|
-
// Keep currentImg in sync so useResizeObserver(currentImg) auto-retargets
|
|
89
|
-
// the img observer when a late-appearing element replaces the previous one.
|
|
90
|
-
currentImg.value = img ?? null
|
|
91
|
-
|
|
92
|
-
const imgH = img ? img.clientHeight : 0
|
|
93
|
-
const thumbH = thumb.clientHeight
|
|
94
|
-
const cardContentH = thumb.parentElement ? thumb.parentElement.clientHeight : thumbH
|
|
95
|
-
// imgH > 0 guard on condition 1: while the Image component's picture is v-show=false
|
|
96
|
-
// (loading / empty state), img.clientHeight is 0. Without the guard a square card would
|
|
97
|
-
// incorrectly receive --floating at mount; the class self-corrects once the image loads
|
|
98
|
-
// and condition 1 can be re-evaluated with the real rendered height.
|
|
99
|
-
//
|
|
100
|
-
// Strict comparison (no px tolerance): clientHeight is integer-rounded, so a truly-flush
|
|
101
|
-
// image (sub-pixel gap < 0.5 px) produces equal integer heights and the condition is
|
|
102
|
-
// false. ANY visible gap (≥ 1 integer px) correctly triggers floating.
|
|
103
|
-
thumbIsFloating.value = (imgH > 0 && imgH < thumbH) || (thumbH < cardContentH)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
useResizeObserver(thumbEl, updateFloating)
|
|
107
|
-
useResizeObserver(thumbParent, updateFloating)
|
|
108
|
-
useResizeObserver(currentImg, updateFloating)
|
|
109
|
-
useMutationObserver(thumbEl, updateFloating, { childList: true, subtree: true })
|
|
43
|
+
/*
|
|
44
|
+
* No floating-thumbnail detection any more.
|
|
45
|
+
*
|
|
46
|
+
* This used to run three ResizeObservers and a MutationObserver per card to work out
|
|
47
|
+
* whether the image reached the card's bottom, and add `--floating` so the CSS could
|
|
48
|
+
* put a radius back on its bottom-right corner. That answered a question the rail no
|
|
49
|
+
* longer asks: the image now covers a rail that fills the card's full height, so it
|
|
50
|
+
* always reaches the bottom and never carries a radius of its own. See #440 and the
|
|
51
|
+
* removal note in card_horizontal.css.
|
|
52
|
+
*/
|
|
110
53
|
</script>
|
|
111
54
|
<template>
|
|
112
55
|
<Card
|
|
@@ -125,25 +68,59 @@ useMutationObserver(thumbEl, updateFloating, { childList: true, subtree: true })
|
|
|
125
68
|
</template>
|
|
126
69
|
|
|
127
70
|
<!-- thumbnail slot: required for the horizontal rail layout.
|
|
128
|
-
Pass an Image component (originalUrl + alt) to
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
class="rsui-card-horizontal__thumbnail"
|
|
133
|
-
:class="{ 'rsui-card-horizontal__thumbnail--floating': thumbIsFloating }"
|
|
134
|
-
>
|
|
71
|
+
Pass an Image component (originalUrl + alt) to fill the rail — its width is
|
|
72
|
+
--ch-thumbnail-size and its height is the card's, and the image covers it
|
|
73
|
+
whatever its source ratio. An empty slot renders a blank rail. -->
|
|
74
|
+
<div class="rsui-card-horizontal__thumbnail">
|
|
135
75
|
<slot name="thumbnail"></slot>
|
|
136
76
|
</div>
|
|
137
77
|
|
|
138
78
|
<div class="rsui-card-horizontal__body">
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
79
|
+
<!--
|
|
80
|
+
CardHeader, not a title and a description of its own.
|
|
81
|
+
|
|
82
|
+
This used to hand-roll both, and the stylesheet admitted it: `__title` carried
|
|
83
|
+
"The same as .rsui-card-header__title — brand Header 4, Inter Semibold 20/26",
|
|
84
|
+
and `__meta` "Matches .rsui-card__meta". Both of those comments exist because
|
|
85
|
+
the copies had already drifted once and had to be re-synced by hand.
|
|
86
|
+
|
|
87
|
+
Composing instead means the title role, the subtitle and the actions row come
|
|
88
|
+
from the component that defines them, and the only thing this file still owns is
|
|
89
|
+
the thumbnail rail and the meta region beneath it.
|
|
90
|
+
|
|
91
|
+
It also gives the card somewhere to put ACTIONS. There was no slot for them, so
|
|
92
|
+
the one consumer in the product puts its favourite and download buttons inside
|
|
93
|
+
`#meta` and forces a line break with `w-full` — a row invented at the call site
|
|
94
|
+
because the component offered nowhere else.
|
|
95
|
+
|
|
96
|
+
`singleLine` is false: CardHeader clamps a subtitle to one line at `@md`, and a
|
|
97
|
+
description here runs to two or three. Nothing clamps it in their place —
|
|
98
|
+
see the subtitle note at the foot of card_horizontal.css.
|
|
99
|
+
-->
|
|
100
|
+
<CardHeader
|
|
101
|
+
:showAvatar="false"
|
|
102
|
+
:singleLine="false"
|
|
103
|
+
inlineActions
|
|
104
|
+
:showMoreActions="$slots['more-actions'] ? null : false"
|
|
105
|
+
>
|
|
106
|
+
<slot name="title"></slot>
|
|
107
|
+
|
|
108
|
+
<template v-if="$slots.badge" #badge>
|
|
109
|
+
<slot name="badge"></slot>
|
|
110
|
+
</template>
|
|
111
|
+
|
|
112
|
+
<template v-if="$slots.description" #subtitle>
|
|
144
113
|
<slot name="description"></slot>
|
|
145
|
-
</
|
|
146
|
-
|
|
114
|
+
</template>
|
|
115
|
+
|
|
116
|
+
<template v-if="$slots.actions" #actions>
|
|
117
|
+
<slot name="actions"></slot>
|
|
118
|
+
</template>
|
|
119
|
+
|
|
120
|
+
<template v-if="$slots['more-actions']" #more-actions>
|
|
121
|
+
<slot name="more-actions"></slot>
|
|
122
|
+
</template>
|
|
123
|
+
</CardHeader>
|
|
147
124
|
|
|
148
125
|
<div v-if="$slots.meta" class="rsui-card-horizontal__meta">
|
|
149
126
|
<slot name="meta"></slot>
|
|
@@ -156,22 +156,36 @@ function toggle() {
|
|
|
156
156
|
</a>
|
|
157
157
|
<Badge v-if="category.href === ''" neutral>{{ category.label }}</Badge>
|
|
158
158
|
</li>
|
|
159
|
-
</ul>
|
|
160
159
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
160
|
+
<!--
|
|
161
|
+
INSIDE the list, in its own `li`.
|
|
162
|
+
|
|
163
|
+
It was a sibling of the `ul`, to keep the toggle out of a list of categories
|
|
164
|
+
— it is a control, not a category. The cost was a layout one and it was not
|
|
165
|
+
small: a flex item is atomic, so the button could sit beside the whole list
|
|
166
|
+
or below it, never after the last badge. Once the badges wrapped to a second
|
|
167
|
+
row the toggle took a third, with the second row's free space beside it.
|
|
168
|
+
Measured at 700px: the last badge ended at 180px with 488px empty to its
|
|
169
|
+
right, and the control sat below at x=0. In the list, it lands at 196 and the
|
|
170
|
+
component is 60px tall instead of 88.
|
|
171
|
+
|
|
172
|
+
`display: contents` on the `ul` would have joined the rows without this, and
|
|
173
|
+
is why the button was outside in the first place — but it drops list
|
|
174
|
+
semantics in Chrome and Safari, which is the one thing the row exists to keep.
|
|
175
|
+
An `li` holding a button keeps them: the list is still a list, with the
|
|
176
|
+
control that governs it as its last item.
|
|
177
|
+
-->
|
|
178
|
+
<li v-if="isCapped" class="rsui-category-list__more-item">
|
|
179
|
+
<button
|
|
180
|
+
type="button"
|
|
181
|
+
class="rsui-category-list__more"
|
|
182
|
+
:aria-label="toggleAccessibleName"
|
|
183
|
+
:aria-expanded="isExpanded"
|
|
184
|
+
@click="toggle"
|
|
185
|
+
>
|
|
186
|
+
{{ toggleText }}
|
|
187
|
+
</button>
|
|
188
|
+
</li>
|
|
189
|
+
</ul>
|
|
176
190
|
</div>
|
|
177
191
|
</template>
|
|
@@ -125,9 +125,9 @@ const defaultSize = computed(() =>
|
|
|
125
125
|
)
|
|
126
126
|
|
|
127
127
|
/**
|
|
128
|
-
*
|
|
128
|
+
* Whether the consumer named a tone at all.
|
|
129
129
|
*/
|
|
130
|
-
const
|
|
130
|
+
const hasNoTone = computed(() =>
|
|
131
131
|
!props.primary
|
|
132
132
|
&& !props.secondary
|
|
133
133
|
&& !props.brand
|
|
@@ -158,7 +158,29 @@ const iconClass = computed(() => [
|
|
|
158
158
|
/**
|
|
159
159
|
* Colors
|
|
160
160
|
*/
|
|
161
|
-
|
|
161
|
+
/**
|
|
162
|
+
* No tone means INHERIT, not primary.
|
|
163
|
+
*
|
|
164
|
+
* The tone rules paint the svg (`*:text-…`), and a colour on the svg wins over
|
|
165
|
+
* anything around it — so defaulting to `primary` made an icon inside a coloured
|
|
166
|
+
* control render near-black while the control beside it was green. A colour on the
|
|
167
|
+
* wrapper would have been inherited and therefore overridable, which is the
|
|
168
|
+
* property that was missing. With no class at all the svg's own `currentColor`
|
|
169
|
+
* resolves against whatever encloses it. See #428.
|
|
170
|
+
*
|
|
171
|
+
* Anywhere the surroundings are already primary ink — body text, headings, most of
|
|
172
|
+
* a page — that is the same colour it was. Only icons in a coloured control move.
|
|
173
|
+
*
|
|
174
|
+
* `background` is the exception, and it is not a leftover. It paints a CONTRAST
|
|
175
|
+
* SURFACE behind the glyph, and which surface is a question only a tone can answer:
|
|
176
|
+
* `.rsui-icon--primary.rsui-icon--background` is `bg-background-primary-contrast`.
|
|
177
|
+
* Without a fallback `IconCircleBackground` and `IconRoundedBackground` — neither
|
|
178
|
+
* of which passes a tone — would render with no background at all.
|
|
179
|
+
*
|
|
180
|
+
* `invert` needed the same partner and no longer does: it carries
|
|
181
|
+
* `*:text-text-on-color` itself now, which is what it always meant.
|
|
182
|
+
*/
|
|
183
|
+
'rsui-icon--primary': props.primary || (hasNoTone.value && props.background),
|
|
162
184
|
'rsui-icon--secondary': props.secondary,
|
|
163
185
|
'rsui-icon--brand': props.brand,
|
|
164
186
|
'rsui-icon--success': props.success,
|