@redseed/redseed-ui-vue3 8.62.0 → 8.64.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 +1 -1
- package/src/components/Button/ButtonPrimary.vue +7 -1
- package/src/components/Button/ButtonPrimaryFull.vue +8 -2
- package/src/components/Button/ButtonSecondary.vue +15 -1
- package/src/components/Button/ButtonSecondaryFull.vue +10 -1
- package/src/components/Button/ButtonSlot.vue +19 -0
- package/src/components/Button/ButtonTertiary.vue +15 -1
- package/src/components/Button/ButtonTertiaryFull.vue +10 -1
- package/src/components/Card/Card.vue +12 -0
- package/src/components/Card/CardHeader.vue +34 -8
- package/src/components/Icon/IconSlot.vue +19 -0
- package/src/components/MessageBox/MessageBox.vue +164 -30
- package/src/components/MetaInfo/MetaInfo.vue +25 -2
- package/src/components/Section/SectionFooter.vue +198 -8
- package/src/components/Section/SectionHeader.vue +6 -1
- package/src/components/Section/SectionSlider.vue +513 -74
- package/src/components/Section/SectionSliderAction.vue +65 -0
- package/src/components/Section/SectionSliderItem.vue +10 -0
- package/src/components/Section/index.js +2 -0
- package/src/components/Switcher/Switcher.vue +21 -0
package/package.json
CHANGED
|
@@ -7,6 +7,12 @@ const props = defineProps({
|
|
|
7
7
|
type: Boolean,
|
|
8
8
|
default: false,
|
|
9
9
|
},
|
|
10
|
+
// Declared rather than left to fall through: a prop named onXxx is ambiguous
|
|
11
|
+
// with an event listener, so it is forwarded by name. See ButtonSlot.
|
|
12
|
+
onColor: {
|
|
13
|
+
type: Boolean,
|
|
14
|
+
default: false,
|
|
15
|
+
},
|
|
10
16
|
})
|
|
11
17
|
|
|
12
18
|
const buttonClass = computed(() => [
|
|
@@ -17,7 +23,7 @@ const buttonClass = computed(() => [
|
|
|
17
23
|
])
|
|
18
24
|
</script>
|
|
19
25
|
<template>
|
|
20
|
-
<ButtonSlot :class="buttonClass">
|
|
26
|
+
<ButtonSlot :class="buttonClass" :on-color="props.onColor">
|
|
21
27
|
<slot></slot>
|
|
22
28
|
</ButtonSlot>
|
|
23
29
|
</template>
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import ButtonPrimary from './ButtonPrimary.vue'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// Forwarded by name for the same reason ButtonPrimary declares it: a prop called onXxx
|
|
5
|
+
// is ambiguous with an event listener, so it is not left to fall through.
|
|
6
|
+
const props = defineProps({
|
|
5
7
|
invert: {
|
|
6
8
|
type: Boolean,
|
|
7
9
|
default: false,
|
|
8
10
|
},
|
|
11
|
+
onColor: {
|
|
12
|
+
type: Boolean,
|
|
13
|
+
default: false,
|
|
14
|
+
},
|
|
9
15
|
})
|
|
10
16
|
</script>
|
|
11
17
|
<template>
|
|
12
|
-
<ButtonPrimary full :invert="invert">
|
|
18
|
+
<ButtonPrimary full :on-color="props.onColor" :invert="props.invert">
|
|
13
19
|
<slot></slot>
|
|
14
20
|
</ButtonPrimary>
|
|
15
21
|
</template>
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
2
3
|
import ButtonSlot from './ButtonSlot.vue'
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
// Declared rather than left to fall through: a prop named onXxx is ambiguous
|
|
7
|
+
// with an event listener, so it is forwarded by name. See ButtonSlot.
|
|
8
|
+
onColor: {
|
|
9
|
+
type: Boolean,
|
|
10
|
+
default: false,
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const buttonClass = computed(() => [
|
|
15
|
+
'rsui-button-secondary',
|
|
16
|
+
])
|
|
3
17
|
</script>
|
|
4
18
|
<template>
|
|
5
|
-
<ButtonSlot class="
|
|
19
|
+
<ButtonSlot :class="buttonClass" :on-color="props.onColor">
|
|
6
20
|
<slot></slot>
|
|
7
21
|
</ButtonSlot>
|
|
8
22
|
</template>
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import ButtonSecondary from './ButtonSecondary.vue'
|
|
3
|
+
|
|
4
|
+
// Forwarded by name for the same reason ButtonSecondary declares it: a prop called onXxx
|
|
5
|
+
// is ambiguous with an event listener, so it is not left to fall through.
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
onColor: {
|
|
8
|
+
type: Boolean,
|
|
9
|
+
default: false,
|
|
10
|
+
},
|
|
11
|
+
})
|
|
3
12
|
</script>
|
|
4
13
|
<template>
|
|
5
|
-
<ButtonSecondary full>
|
|
14
|
+
<ButtonSecondary full :on-color="props.onColor">
|
|
6
15
|
<slot></slot>
|
|
7
16
|
</ButtonSecondary>
|
|
8
17
|
</template>
|
|
@@ -35,6 +35,22 @@ const props = defineProps({
|
|
|
35
35
|
default: 'center',
|
|
36
36
|
validator: (value) => ['left', 'center', 'right'].includes(value)
|
|
37
37
|
},
|
|
38
|
+
// Set when the button sits on a COLOURED or DARK surface — a tonal PageHeader
|
|
39
|
+
// band, a toned MetricCard, the AI insight panel. Every variant is drawn for a
|
|
40
|
+
// light ground and has no correct rendering on a dark one: secondary's white
|
|
41
|
+
// fill disappears, tertiary's ink goes near-invisible.
|
|
42
|
+
//
|
|
43
|
+
// Named to match BodyText's and StageDial's `onColor` rather than the existing
|
|
44
|
+
// `invert` on Primary and Danger, which is a different thing: invert means a
|
|
45
|
+
// transparent fill with brand ink, an outline treatment for a LIGHT surface.
|
|
46
|
+
// The two compose.
|
|
47
|
+
//
|
|
48
|
+
// The ink for the filled treatment comes from the surface, through
|
|
49
|
+
// --rsui-button-on-color-ink — the button cannot know what it is sitting on.
|
|
50
|
+
onColor: {
|
|
51
|
+
type: Boolean,
|
|
52
|
+
default: false,
|
|
53
|
+
},
|
|
38
54
|
})
|
|
39
55
|
|
|
40
56
|
defineEmits(['click'])
|
|
@@ -90,6 +106,9 @@ const buttonSlotClass = computed(() => [
|
|
|
90
106
|
// button full width
|
|
91
107
|
'rsui-button-slot--full': props.full,
|
|
92
108
|
|
|
109
|
+
// button on a coloured or dark surface
|
|
110
|
+
'rsui-button-slot--on-color': props.onColor,
|
|
111
|
+
|
|
93
112
|
// button alignment
|
|
94
113
|
'rsui-button-slot--left': props.alignment === 'left',
|
|
95
114
|
'rsui-button-slot--center': props.alignment === 'center',
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
2
3
|
import ButtonSlot from './ButtonSlot.vue'
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
// Declared rather than left to fall through: a prop named onXxx is ambiguous
|
|
7
|
+
// with an event listener, so it is forwarded by name. See ButtonSlot.
|
|
8
|
+
onColor: {
|
|
9
|
+
type: Boolean,
|
|
10
|
+
default: false,
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const buttonClass = computed(() => [
|
|
15
|
+
'rsui-button-tertiary',
|
|
16
|
+
])
|
|
3
17
|
</script>
|
|
4
18
|
<template>
|
|
5
|
-
<ButtonSlot class="
|
|
19
|
+
<ButtonSlot :class="buttonClass" :on-color="props.onColor">
|
|
6
20
|
<slot></slot>
|
|
7
21
|
</ButtonSlot>
|
|
8
22
|
</template>
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import ButtonTertiary from './ButtonTertiary.vue'
|
|
3
|
+
|
|
4
|
+
// Forwarded by name for the same reason ButtonTertiary declares it: a prop called onXxx
|
|
5
|
+
// is ambiguous with an event listener, so it is not left to fall through.
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
onColor: {
|
|
8
|
+
type: Boolean,
|
|
9
|
+
default: false,
|
|
10
|
+
},
|
|
11
|
+
})
|
|
3
12
|
</script>
|
|
4
13
|
<template>
|
|
5
|
-
<ButtonTertiary full>
|
|
14
|
+
<ButtonTertiary full :on-color="props.onColor">
|
|
6
15
|
<slot></slot>
|
|
7
16
|
</ButtonTertiary>
|
|
8
17
|
</template>
|
|
@@ -27,6 +27,17 @@ const props = defineProps({
|
|
|
27
27
|
type: Boolean,
|
|
28
28
|
default: false,
|
|
29
29
|
},
|
|
30
|
+
// The AI family at full strength: a BOLD dark surface rather than the pale
|
|
31
|
+
// lilac wash `ai` gives. Every other Card variant is a light tint, so this is
|
|
32
|
+
// the first card that has to carry light type — it adapts CardHeader's ink and
|
|
33
|
+
// flips the hover scrim, both of which assume a light ground.
|
|
34
|
+
//
|
|
35
|
+
// BodyText inside it is NOT adapted: that component has its own `onColor` prop,
|
|
36
|
+
// which is the explicit route and would only fight an override from here.
|
|
37
|
+
aiBold: {
|
|
38
|
+
type: Boolean,
|
|
39
|
+
default: false,
|
|
40
|
+
},
|
|
30
41
|
secondary: {
|
|
31
42
|
type: Boolean,
|
|
32
43
|
default: false,
|
|
@@ -111,6 +122,7 @@ const cardClass = computed(() => [
|
|
|
111
122
|
'rsui-card--clickable': props.clickable && !props.disabled,
|
|
112
123
|
'rsui-card--bordered': props.bordered,
|
|
113
124
|
'rsui-card--ai': props.ai,
|
|
125
|
+
'rsui-card--ai-bold': props.aiBold,
|
|
114
126
|
'rsui-card--secondary': props.secondary,
|
|
115
127
|
'rsui-card--brand': props.brand,
|
|
116
128
|
'rsui-card--success': props.success,
|
|
@@ -14,6 +14,12 @@ const props = defineProps({
|
|
|
14
14
|
type: Boolean,
|
|
15
15
|
default: true,
|
|
16
16
|
},
|
|
17
|
+
// Matches showBadge and showSubtitle: the slot is opt-in by being filled, and
|
|
18
|
+
// this exists so a consumer can suppress it without unbinding the slot.
|
|
19
|
+
showEyebrow: {
|
|
20
|
+
type: Boolean,
|
|
21
|
+
default: true,
|
|
22
|
+
},
|
|
17
23
|
showSubtitle: {
|
|
18
24
|
type: Boolean,
|
|
19
25
|
default: true,
|
|
@@ -26,9 +32,14 @@ const props = defineProps({
|
|
|
26
32
|
type: Boolean,
|
|
27
33
|
default: true,
|
|
28
34
|
},
|
|
35
|
+
// Default false: Material 3 cards are one continuous padded surface, and a
|
|
36
|
+
// full-bleed rule between the header and the body made every card read as two
|
|
37
|
+
// stacked boxes rather than one tile. Still available for the cases that want
|
|
38
|
+
// it — a header over a table, or a disclosure — where it separates two genuinely
|
|
39
|
+
// different regions rather than splitting one.
|
|
29
40
|
showDivider: {
|
|
30
41
|
type: Boolean,
|
|
31
|
-
default:
|
|
42
|
+
default: false,
|
|
32
43
|
},
|
|
33
44
|
singleLine: {
|
|
34
45
|
type: Boolean,
|
|
@@ -95,6 +106,19 @@ function handleMoreActionsClick() {
|
|
|
95
106
|
|
|
96
107
|
<div class="rsui-card-header__text">
|
|
97
108
|
|
|
109
|
+
<!--
|
|
110
|
+
Eyebrow slot, optional. ABOVE the title, which is the one position
|
|
111
|
+
the header had no slot for: badge and subtitle both render below it
|
|
112
|
+
and the flex column has no order overrides, so there was no way to
|
|
113
|
+
put a label over a card title without hand-rolling the header.
|
|
114
|
+
Takes plain text or a badge — BadgeRedSeedAI is the common case.
|
|
115
|
+
-->
|
|
116
|
+
<div class="rsui-card-header__eyebrow"
|
|
117
|
+
v-if="showEyebrow && $slots.eyebrow"
|
|
118
|
+
>
|
|
119
|
+
<slot name="eyebrow"></slot>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
98
122
|
<!-- Title slot, default slot -->
|
|
99
123
|
<div class="rsui-card-header__title-container">
|
|
100
124
|
<div :class="[
|
|
@@ -119,13 +143,6 @@ function handleMoreActionsClick() {
|
|
|
119
143
|
>
|
|
120
144
|
<slot name="subtitle"></slot>
|
|
121
145
|
</div>
|
|
122
|
-
|
|
123
|
-
<!-- Badge slot, optional -->
|
|
124
|
-
<div class="rsui-card-header__badge"
|
|
125
|
-
v-if="showBadge && $slots.badge"
|
|
126
|
-
>
|
|
127
|
-
<slot name="badge"></slot>
|
|
128
|
-
</div>
|
|
129
146
|
</div>
|
|
130
147
|
|
|
131
148
|
<!-- Actions slot, optional -->
|
|
@@ -158,6 +175,15 @@ function handleMoreActionsClick() {
|
|
|
158
175
|
</div>
|
|
159
176
|
</div>
|
|
160
177
|
|
|
178
|
+
<!-- Badge slot, optional. A sibling of __header rather than a child of __text, so it
|
|
179
|
+
sits on the card's own left edge — level with the body below it — instead of being
|
|
180
|
+
indented past the avatar with the title. -->
|
|
181
|
+
<div class="rsui-card-header__badge"
|
|
182
|
+
v-if="showBadge && $slots.badge"
|
|
183
|
+
>
|
|
184
|
+
<slot name="badge"></slot>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
161
187
|
<!-- Mobile actions slot, optional -->
|
|
162
188
|
<div class="rsui-card-header__actions-mobile"
|
|
163
189
|
v-if="showActions && !responsiveWidth.specific && $slots.actions"
|
|
@@ -90,6 +90,24 @@ const props = defineProps({
|
|
|
90
90
|
type: Boolean,
|
|
91
91
|
default: false,
|
|
92
92
|
},
|
|
93
|
+
/**
|
|
94
|
+
* Expand a ring outwards from the icon a few times on arrival, to draw the
|
|
95
|
+
* eye to something that has just appeared.
|
|
96
|
+
*
|
|
97
|
+
* Runs a fixed number of iterations rather than looping. An animation that
|
|
98
|
+
* repeats indefinitely for more than five seconds needs a pause control
|
|
99
|
+
* (WCAG 2.2.2), and an icon has nowhere sensible to put one — a burst on
|
|
100
|
+
* arrival gets the attention without taking on that obligation. Suppressed
|
|
101
|
+
* entirely under `prefers-reduced-motion`.
|
|
102
|
+
*
|
|
103
|
+
* The ring is drawn around the icon's own box, so it suits a round glyph —
|
|
104
|
+
* the circle heroicons, or anything with `circle`. On a square icon it reads
|
|
105
|
+
* as a mistake rather than a pulse.
|
|
106
|
+
*/
|
|
107
|
+
pulse: {
|
|
108
|
+
type: Boolean,
|
|
109
|
+
default: false,
|
|
110
|
+
},
|
|
93
111
|
})
|
|
94
112
|
|
|
95
113
|
/**
|
|
@@ -162,6 +180,7 @@ const iconClass = computed(() => [
|
|
|
162
180
|
*/
|
|
163
181
|
'rsui-icon--background': props.background,
|
|
164
182
|
'rsui-icon--invert': props.invert,
|
|
183
|
+
'rsui-icon--pulse': props.pulse,
|
|
165
184
|
}
|
|
166
185
|
])
|
|
167
186
|
</script>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { ref, computed } from 'vue'
|
|
2
|
+
import { ref, computed, watch, watchEffect, useSlots, Comment, Fragment } from 'vue'
|
|
3
3
|
import Icon from '../Icon/Icon.vue'
|
|
4
4
|
import { XMarkIcon } from '@heroicons/vue/24/outline'
|
|
5
5
|
|
|
@@ -20,13 +20,101 @@ const props = defineProps({
|
|
|
20
20
|
type: String,
|
|
21
21
|
default: 'default',
|
|
22
22
|
validator: (value) => ['default', 'info', 'success', 'warning', 'error', 'ai'].includes(value)
|
|
23
|
-
}
|
|
23
|
+
},
|
|
24
|
+
/**
|
|
25
|
+
* Renders a quiet dismiss action in the actions row, wired to the same close
|
|
26
|
+
* path as the corner button. Owned by the component rather than left to the
|
|
27
|
+
* `actions` slot, so dismissal cannot drift in wording or come unwired.
|
|
28
|
+
*/
|
|
29
|
+
dismissLabel: {
|
|
30
|
+
type: String,
|
|
31
|
+
default: '',
|
|
32
|
+
},
|
|
24
33
|
})
|
|
25
34
|
|
|
26
35
|
const emit = defineEmits(['close'])
|
|
27
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Enforced at render, not just by the prop validator. Validators only warn and are
|
|
39
|
+
* stripped from production builds, so an unrecognised value used to reach the
|
|
40
|
+
* class attribute and the computeds below — and every fallback pointed the wrong
|
|
41
|
+
* way at once: no background (no matching CSS rule), an *information* glyph on
|
|
42
|
+
* what might be an error, and no live region at all, so a screen reader user was
|
|
43
|
+
* never told the operation failed.
|
|
44
|
+
*/
|
|
45
|
+
const VARIANTS = ['default', 'info', 'success', 'warning', 'error', 'ai']
|
|
46
|
+
|
|
47
|
+
const slots = useSlots()
|
|
48
|
+
|
|
28
49
|
const isClosed = ref(props.closed)
|
|
29
50
|
|
|
51
|
+
const isKnownVariant = computed(() => VARIANTS.includes(props.variant))
|
|
52
|
+
|
|
53
|
+
const safeVariant = computed(() => isKnownVariant.value ? props.variant : 'default')
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Dev-only, matching LinkSlot, ButtonSlot, Pill and InlineEditableText — this
|
|
57
|
+
* package ships raw `.vue` source, so an ungated warn reaches real users'
|
|
58
|
+
* consoles. Silence would leave a consumer with an unstyled box, a decoration that
|
|
59
|
+
* never appears, or an unnamed control, and no way to know why.
|
|
60
|
+
*/
|
|
61
|
+
watchEffect(() => {
|
|
62
|
+
if (process.env.NODE_ENV === 'production') return
|
|
63
|
+
|
|
64
|
+
if (!isKnownVariant.value) {
|
|
65
|
+
console.warn(`[RSUI] MessageBox: variant "${props.variant}" is not supported (${VARIANTS.join(', ')}). Falling back to "default".`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (props.dismissLabel !== '' && props.dismissLabel.trim() === '') {
|
|
69
|
+
console.warn('[RSUI] MessageBox: `dismissLabel` is only whitespace, so no dismiss control has been rendered — a button with no accessible name is worse than no button.')
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* `closed` is a two-way switch, not a starting position. Without this the ref was
|
|
75
|
+
* seeded once at setup and never looked at the prop again, so a consumer could
|
|
76
|
+
* neither close the box by setting it nor re-show a dismissed one by clearing it
|
|
77
|
+
* — and a flash message re-shown on the next visit would silently never appear,
|
|
78
|
+
* because Vue patches the same instance rather than remounting it.
|
|
79
|
+
*/
|
|
80
|
+
watch(() => props.closed, (value) => {
|
|
81
|
+
isClosed.value = value
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A Fragment is a wrapper, so its own presence answers nothing — only what is
|
|
86
|
+
* inside it does. Hence the recursion: a `v-for` compiles to a Fragment and
|
|
87
|
+
* leaves a comment placeholder for each hidden item, so counting children
|
|
88
|
+
* reported content for a list whose every action was filtered out (the usual
|
|
89
|
+
* shape for permission-gated actions), and a nested `<template>` puts another
|
|
90
|
+
* Fragment inside that one.
|
|
91
|
+
*
|
|
92
|
+
* `Array.isArray` rather than recursing straight into `children`, which is a raw
|
|
93
|
+
* string on a Fragment built by hand as `h(Fragment, null, 'text')`. Vue's own
|
|
94
|
+
* `ensureValidVNode` assumes the array and throws on that shape, so answering
|
|
95
|
+
* "renders" here only hands it a slot it cannot render; treating it as empty
|
|
96
|
+
* drops the row instead, which is the quieter of the two failures. Compiled
|
|
97
|
+
* templates always produce the array.
|
|
98
|
+
*/
|
|
99
|
+
function rendersNode(node) {
|
|
100
|
+
if (node.type === Comment) return false
|
|
101
|
+
if (node.type === Fragment) return Array.isArray(node.children) && node.children.some(rendersNode)
|
|
102
|
+
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Whether a slot renders anything at all. Slot *presence* is not the same
|
|
108
|
+
* question: a slot holding only a falsy `v-if` is still a truthy slot function
|
|
109
|
+
* that renders a comment placeholder, which would put an empty padded row under
|
|
110
|
+
* the message.
|
|
111
|
+
*/
|
|
112
|
+
function rendersContent(slot) {
|
|
113
|
+
const nodes = slot?.()
|
|
114
|
+
|
|
115
|
+
return Boolean(nodes) && nodes.some(rendersNode)
|
|
116
|
+
}
|
|
117
|
+
|
|
30
118
|
/**
|
|
31
119
|
* Announce the box as a live region so flash / MOTD content reaches assistive
|
|
32
120
|
* tech (WCAG 4.1.3 Status Messages). `alert` is assertive — reserved for errors
|
|
@@ -40,11 +128,28 @@ const isClosed = ref(props.closed)
|
|
|
40
128
|
* interrupt for something nobody asked about.
|
|
41
129
|
*/
|
|
42
130
|
const liveRole = computed(() => {
|
|
43
|
-
if (
|
|
44
|
-
if (['info', 'success', 'warning'].includes(
|
|
131
|
+
if (safeVariant.value === 'error') return 'alert'
|
|
132
|
+
if (['info', 'success', 'warning'].includes(safeVariant.value)) return 'status'
|
|
45
133
|
return undefined
|
|
46
134
|
})
|
|
47
135
|
|
|
136
|
+
/**
|
|
137
|
+
* The icon is the consumer's, not ours — matching Pill, MetricCard, ListItem and
|
|
138
|
+
* SectionHeader, which all take one through a slot. RSUI only hardcodes a glyph
|
|
139
|
+
* where it is chrome belonging to the control itself (Disclosure's chevron,
|
|
140
|
+
* Pagination's arrows); the marker beside a message is content, and which glyph
|
|
141
|
+
* says "this came from the model" or "this is a billing notice" is the caller's
|
|
142
|
+
* call, not a mapping this component can guess from a variant name.
|
|
143
|
+
*
|
|
144
|
+
* Colour, shape and the attention pulse all ride on the `Icon` the consumer
|
|
145
|
+
* passes — `<Icon ai circle background pulse>` — so none of it needs a prop here.
|
|
146
|
+
*/
|
|
147
|
+
function hasIcon() {
|
|
148
|
+
return rendersContent(slots.icon)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const hasDismiss = computed(() => props.dismissLabel.trim() !== '')
|
|
152
|
+
|
|
48
153
|
function close() {
|
|
49
154
|
isClosed.value = true
|
|
50
155
|
emit('close')
|
|
@@ -54,41 +159,70 @@ function close() {
|
|
|
54
159
|
<div v-if="!isClosed"
|
|
55
160
|
:class="[
|
|
56
161
|
'rsui-message-box',
|
|
57
|
-
`rsui-message-box--${
|
|
162
|
+
`rsui-message-box--${safeVariant}`,
|
|
163
|
+
{ 'rsui-message-box--with-icon': hasIcon() },
|
|
58
164
|
]"
|
|
59
165
|
:role="liveRole"
|
|
60
166
|
>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<XMarkIcon aria-hidden="true"></XMarkIcon>
|
|
73
|
-
</Icon>
|
|
74
|
-
</button>
|
|
75
|
-
</div>
|
|
167
|
+
<!--
|
|
168
|
+
A sibling of the whole text column rather than of the body, so it sits
|
|
169
|
+
beside the title and the text below it indents past it instead of wrapping
|
|
170
|
+
under it.
|
|
171
|
+
|
|
172
|
+
`aria-hidden` because the glyph restates the message rather than adding to
|
|
173
|
+
it, matching Pill and MetricCard — the variant's meaning reaches assistive
|
|
174
|
+
tech through the live region, not through a decorative icon.
|
|
175
|
+
-->
|
|
176
|
+
<div v-if="hasIcon()" class="rsui-message-box__icon" aria-hidden="true">
|
|
177
|
+
<slot name="icon"></slot>
|
|
76
178
|
</div>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
179
|
+
|
|
180
|
+
<div class="rsui-message-box__main">
|
|
181
|
+
<div v-if="$slots.title" class="rsui-message-box__head">
|
|
182
|
+
<div class="rsui-message-box__title">
|
|
183
|
+
<slot name="title"></slot>
|
|
184
|
+
</div>
|
|
80
185
|
</div>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
186
|
+
|
|
187
|
+
<div class="rsui-message-box__content">
|
|
188
|
+
<div class="rsui-message-box__body">
|
|
189
|
+
<slot></slot>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<!--
|
|
194
|
+
Actions stay neutral rather than taking the variant colour. Coloured
|
|
195
|
+
text on a coloured tint is a contrast problem, and the action is a way
|
|
196
|
+
out of the message rather than a restatement of its severity.
|
|
197
|
+
-->
|
|
198
|
+
<div v-if="hasDismiss || rendersContent($slots.actions)" class="rsui-message-box__actions">
|
|
199
|
+
<button v-if="hasDismiss"
|
|
200
|
+
type="button"
|
|
201
|
+
class="rsui-message-box__dismiss"
|
|
85
202
|
@click="close"
|
|
86
203
|
>
|
|
87
|
-
|
|
88
|
-
<XMarkIcon aria-hidden="true"></XMarkIcon>
|
|
89
|
-
</Icon>
|
|
204
|
+
{{ dismissLabel }}
|
|
90
205
|
</button>
|
|
206
|
+
|
|
207
|
+
<slot name="actions" :close="close"></slot>
|
|
91
208
|
</div>
|
|
92
209
|
</div>
|
|
210
|
+
|
|
211
|
+
<!--
|
|
212
|
+
Top level rather than inside the head or the body, so it stays in the
|
|
213
|
+
corner whether or not there is a title — the two duplicated buttons this
|
|
214
|
+
replaces each sat in a `justify-between` row to reach the same place.
|
|
215
|
+
-->
|
|
216
|
+
<div v-if="closeable" class="rsui-message-box__close">
|
|
217
|
+
<button type="button"
|
|
218
|
+
class="rsui-message-box__close-icon"
|
|
219
|
+
:aria-label="closeLabel"
|
|
220
|
+
@click="close"
|
|
221
|
+
>
|
|
222
|
+
<Icon secondary>
|
|
223
|
+
<XMarkIcon aria-hidden="true"></XMarkIcon>
|
|
224
|
+
</Icon>
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
93
227
|
</div>
|
|
94
228
|
</template>
|
|
@@ -16,6 +16,18 @@ const props = defineProps({
|
|
|
16
16
|
type: Boolean,
|
|
17
17
|
default: false
|
|
18
18
|
},
|
|
19
|
+
// Size ramp. Booleans, only one applies at a time — the library convention, and
|
|
20
|
+
// the same three steps StageDial uses. `sm` is the default and renders exactly
|
|
21
|
+
// as the unnamed default did, so nothing changes for existing consumers.
|
|
22
|
+
//
|
|
23
|
+
// Note the names sit one step below the type scale they produce: sm is text-xs,
|
|
24
|
+
// md is text-sm, lg is text-base. That predates this change and is left alone,
|
|
25
|
+
// because `md` and `lg` are used in consuming apps and renaming them would be a
|
|
26
|
+
// breaking change for a cosmetic gain.
|
|
27
|
+
sm: {
|
|
28
|
+
type: Boolean,
|
|
29
|
+
default: false
|
|
30
|
+
},
|
|
19
31
|
md: {
|
|
20
32
|
type: Boolean,
|
|
21
33
|
default: false
|
|
@@ -26,12 +38,23 @@ const props = defineProps({
|
|
|
26
38
|
}
|
|
27
39
|
})
|
|
28
40
|
|
|
41
|
+
// Largest flag wins, so passing more than one still gives a defined result. It used
|
|
42
|
+
// to spread the booleans straight onto the class list, which meant `<MetaInfo md lg>`
|
|
43
|
+
// emitted BOTH modifiers and the winner came down to the order meta_info.css
|
|
44
|
+
// happened to be imported in.
|
|
45
|
+
const activeSize = computed(() => {
|
|
46
|
+
if (props.lg) return 'lg'
|
|
47
|
+
if (props.md) return 'md'
|
|
48
|
+
if (props.sm) return 'sm'
|
|
49
|
+
|
|
50
|
+
return 'sm'
|
|
51
|
+
})
|
|
52
|
+
|
|
29
53
|
const metaInfoClass = computed(() => [
|
|
30
54
|
'rsui-meta-info',
|
|
55
|
+
`rsui-meta-info--${activeSize.value}`,
|
|
31
56
|
{
|
|
32
57
|
'rsui-meta-info--inline': props.inline,
|
|
33
|
-
'rsui-meta-info--md': props.md,
|
|
34
|
-
'rsui-meta-info--lg': props.lg,
|
|
35
58
|
}
|
|
36
59
|
])
|
|
37
60
|
|