webcake-ui-kit 1.1.28 → 1.1.30

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.
Files changed (44) hide show
  1. package/package.json +7 -2
  2. package/src/components/alert/Alert.vue +3 -2
  3. package/src/components/alert/alert.css +29 -3
  4. package/src/components/date-picker/CalendarPanel.vue +154 -0
  5. package/src/components/date-picker/CalendarView.vue +104 -0
  6. package/src/components/date-picker/DatePicker.vue +587 -0
  7. package/src/components/date-picker/PanelFooter.vue +45 -0
  8. package/src/components/date-picker/PanelHeader.vue +102 -0
  9. package/src/components/date-picker/TimePanel.vue +98 -0
  10. package/src/components/date-picker/calendar-panel.css +36 -0
  11. package/src/components/date-picker/calendar-view.css +134 -0
  12. package/src/components/date-picker/calendar.js +116 -0
  13. package/src/components/date-picker/date-picker.css +95 -0
  14. package/src/components/date-picker/dayjs.js +34 -0
  15. package/src/components/date-picker/panel-footer.css +42 -0
  16. package/src/components/date-picker/panel-header.css +71 -0
  17. package/src/components/date-picker/time-panel.css +63 -0
  18. package/src/components/dropdown/Dropdown.vue +14 -2
  19. package/src/components/dropdown/DropdownMenuItems.vue +7 -3
  20. package/src/components/dropdown/dropdown-menu-items.css +4 -11
  21. package/src/components/input/input.css +7 -2
  22. package/src/components/menu/Menu.vue +7 -2
  23. package/src/components/menu/menu.css +0 -1
  24. package/src/components/message/Message.vue +72 -0
  25. package/src/components/message/MessageContainer.vue +200 -0
  26. package/src/components/message/message-container.css +108 -0
  27. package/src/components/message/message.css +69 -0
  28. package/src/components/message/message.js +133 -0
  29. package/src/components/progress/Progress.vue +42 -0
  30. package/src/components/progress/progress.css +15 -0
  31. package/src/components/radio/Radio.vue +1 -1
  32. package/src/components/radio-group/RadioGroup.vue +2 -2
  33. package/src/components/select/Select.vue +5 -1
  34. package/src/components/select/select.css +13 -7
  35. package/src/components/select-option/SelectOption.vue +6 -0
  36. package/src/components/select-option/select_option.css +20 -0
  37. package/src/components/table/Table.vue +434 -92
  38. package/src/components/table/table.css +178 -5
  39. package/src/components/tooltip/Tooltip.vue +6 -3
  40. package/src/floating/click-outside-mixin.js +21 -1
  41. package/src/index.js +5 -0
  42. package/src/styles/color_general.css +20 -12
  43. package/src/styles/raw_colors.css +4 -4
  44. package/src/styles/shadow.css +3 -0
@@ -0,0 +1,71 @@
1
+ .ui-cal-header {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: var(--spacing-xs);
5
+ min-height: 32px;
6
+ font-family: var(--font-family-body);
7
+ }
8
+
9
+ .ui-cal-header__nav {
10
+ display: inline-flex;
11
+ align-items: center;
12
+ gap: var(--spacing-3xs);
13
+ flex-shrink: 0;
14
+ }
15
+
16
+ /* One label/select group per visible month, centered over its grid. */
17
+ .ui-cal-header__labels {
18
+ display: flex;
19
+ flex: 1;
20
+ align-items: center;
21
+ gap: var(--spacing-md);
22
+ }
23
+
24
+ .ui-cal-header__group {
25
+ display: flex;
26
+ flex: 1;
27
+ align-items: center;
28
+ justify-content: center;
29
+ gap: var(--spacing-2xs);
30
+ }
31
+
32
+ /* Clickable month/year text label (default when selects are disabled). */
33
+ .ui-cal-header__label {
34
+ border: 0;
35
+ background: transparent;
36
+ cursor: pointer;
37
+ font-family: inherit;
38
+ font-size: var(--paragraph-small-font-size);
39
+ line-height: var(--paragraph-small-line-height);
40
+ font-weight: 400;
41
+ color: var(--primary-fg);
42
+ border-radius: var(--rounded-md);
43
+ }
44
+
45
+ /* .ui-select fills its container (width:100%), so size via these wrappers. */
46
+ .ui-cal-header__select {
47
+ display: block;
48
+ }
49
+
50
+ .ui-cal-header__select--month {
51
+ flex: 1 1 auto;
52
+ max-width: 98px;
53
+ }
54
+
55
+ .ui-cal-header__select--year {
56
+ flex: 0 0 88px;
57
+ width: 88px;
58
+ }
59
+
60
+ .ui-cal-header__label_month_not_select,
61
+ .ui-cal-header__label_year_not_select {
62
+ font-weight: 500;
63
+ }
64
+
65
+ .ui-cal-header__label_year_not_select {
66
+ margin-left: 4px;
67
+ }
68
+
69
+ .ui-cal-header__label_month_not_select {
70
+ margin-right: 4px;
71
+ }
@@ -0,0 +1,63 @@
1
+ .ui-time-panel {
2
+ display: flex;
3
+ gap: var(--spacing-3xs);
4
+ height: 200px;
5
+ font-family: var(--font-family-body);
6
+ }
7
+
8
+ /* Each part (hour/minute/second) scrolls independently. */
9
+ .ui-time-panel__col {
10
+ display: flex;
11
+ flex-direction: column;
12
+ gap: var(--spacing-3xs);
13
+ overflow-y: auto;
14
+ overflow-x: hidden;
15
+ padding: 0 var(--spacing-3xs);
16
+ }
17
+
18
+ .ui-time-panel__col + .ui-time-panel__col {
19
+ border-left: 1px solid var(--border-primary);
20
+ }
21
+
22
+ .ui-time-panel__col::-webkit-scrollbar {
23
+ width: 6px;
24
+ height: 6px;
25
+ }
26
+
27
+ .ui-time-panel__col::-webkit-scrollbar-thumb {
28
+ background: var(--accent-2);
29
+ border-radius: 999px;
30
+
31
+ border: 0 solid transparent;
32
+ background-clip: padding-box;
33
+ }
34
+
35
+ .ui-time-panel__opt {
36
+ flex-shrink: 0;
37
+ width: 44px;
38
+ height: 30px;
39
+ border: 0;
40
+ background: transparent;
41
+ cursor: pointer;
42
+ border-radius: var(--rounded-md);
43
+ font-family: inherit;
44
+ font-size: var(--paragraph-small-font-size);
45
+ line-height: var(--paragraph-small-line-height);
46
+ color: var(--primary-fg);
47
+ text-align: center;
48
+ }
49
+
50
+ .ui-time-panel__opt:hover:not(.is-disabled):not(.is-active) {
51
+ background: var(--accent-1);
52
+ }
53
+
54
+ .ui-time-panel__opt.is-active {
55
+ background: var(--primary-brand-bg);
56
+ color: var(--inverse-fg);
57
+ font-weight: 600;
58
+ }
59
+
60
+ .ui-time-panel__opt.is-disabled {
61
+ color: var(--muted-fg);
62
+ cursor: not-allowed;
63
+ }
@@ -9,6 +9,8 @@
9
9
  @open="$emit('open')"
10
10
  @close="$emit('close')"
11
11
  :width="width"
12
+ :overlay-class-name="overlayClassName"
13
+ :overlay-style="overlayStyle"
12
14
  >
13
15
  <template #trigger="{ triggerRef }">
14
16
  <span
@@ -21,7 +23,14 @@
21
23
  <slot></slot>
22
24
  </span>
23
25
  </template>
24
- <DropdownMenuItems v-if="hasItems" :items="items" :value="value" @select="onItemSelect" :size="size" />
26
+ <DropdownMenuItems
27
+ v-if="hasItems"
28
+ :items="items"
29
+ :value="value"
30
+ @select="onItemSelect"
31
+ :size="size"
32
+ :showChecked="showChecked"
33
+ />
25
34
  <slot v-else name="overlay"></slot>
26
35
  </Menu>
27
36
  </template>
@@ -80,7 +89,10 @@ export default {
80
89
  type: String,
81
90
  default: 'md',
82
91
  validator: v => ['sm', 'md', 'lg'].includes(v)
83
- }
92
+ },
93
+ overlayClassName: { type: [String, Array, Object], default: null },
94
+ overlayStyle: { type: [Object, Array], default: null },
95
+ showChecked: { type: Boolean, default: false }
84
96
  },
85
97
  emits: ['change', 'update:modelValue', 'open', 'close', 'select'],
86
98
  data() {
@@ -35,7 +35,7 @@
35
35
  child.label
36
36
  }}</span>
37
37
  <span v-if="child.extra" class="ui-dropdown-items__extra">{{ child.extra }}</span>
38
- <WkiCheck v-if="isActive(child.key)" :size="16" color="var(--muted-fg)" />
38
+ <WkiCheck v-if="isActive(child.key) && showChecked" :size="16" color="var(--muted-fg)" />
39
39
  </div>
40
40
  </div>
41
41
 
@@ -87,7 +87,7 @@
87
87
  child.label
88
88
  }}</span>
89
89
  <span v-if="child.extra" class="ui-dropdown-items__extra">{{ child.extra }}</span>
90
- <WkiCheck v-if="isActive(child.key)" :size="16" color="var(--muted-fg)" />
90
+ <WkiCheck v-if="isActive(child.key) && showChecked" :size="16" color="var(--muted-fg)" />
91
91
  </div>
92
92
  </div>
93
93
 
@@ -116,7 +116,7 @@
116
116
  item.label
117
117
  }}</span>
118
118
  <span v-if="item.extra" class="ui-dropdown-items__extra">{{ item.extra }}</span>
119
- <WkiCheck v-if="isActive(item.key)" :size="16" color="var(--muted-fg)" />
119
+ <WkiCheck v-if="isActive(item.key) && showChecked" :size="16" color="var(--muted-fg)" />
120
120
  </div>
121
121
  </div>
122
122
  </div>
@@ -150,6 +150,10 @@ export default {
150
150
  size: {
151
151
  type: String,
152
152
  default: 'md'
153
+ },
154
+ showChecked: {
155
+ type: Boolean,
156
+ default: false
153
157
  }
154
158
  },
155
159
 
@@ -54,6 +54,10 @@
54
54
  background: var(--destructive-subtle) !important;
55
55
  }
56
56
 
57
+ .ui-dropdown-items__item--destructive.ui-dropdown-items__item--active {
58
+ background: var(--destructive-subtle) !important;
59
+ }
60
+
57
61
  .ui-dropdown-items__item--sub {
58
62
  padding-left: calc(var(--spacing-sm) + var(--spacing-md));
59
63
  }
@@ -139,17 +143,6 @@
139
143
  user-select: none;
140
144
  }
141
145
 
142
- /* 20px container — icon SVG inside is typically 16px, centred */
143
- .ui-dropdown-items__icon {
144
- width: 20px;
145
- height: 20px;
146
- flex-shrink: 0;
147
- display: flex;
148
- align-items: center;
149
- justify-content: center;
150
- color: inherit;
151
- }
152
-
153
146
  .ui-dropdown-items__label {
154
147
  flex: 1;
155
148
  min-width: 0;
@@ -126,19 +126,24 @@
126
126
  cursor: not-allowed;
127
127
  }
128
128
 
129
+ .ui-input__field::-webkit-calendar-picker-indicator {
130
+ display: none;
131
+ -webkit-appearance: none;
132
+ }
133
+
129
134
  /* Decoration slot wrappers — fixed-size icon containers. */
130
135
  .ui-input__decoration {
131
136
  display: inline-flex;
132
137
  align-items: center;
133
138
  justify-content: center;
134
139
  flex-shrink: 0;
135
- width: 20px;
140
+ min-width: 20px;
136
141
  height: 20px;
137
142
  color: var(--muted-fg);
138
143
  }
139
144
 
140
145
  .ui-input--size-xs .ui-input__decoration,
141
146
  .ui-input--size-tiny .ui-input__decoration {
142
- width: 16px;
147
+ min-width: 16px;
143
148
  height: 16px;
144
149
  }
@@ -14,7 +14,7 @@
14
14
  role="menu"
15
15
  @keydown="_kmOnKeydown"
16
16
  >
17
- <div class="ui-menu__content">
17
+ <div :class="['ui-menu__content', overlayClassName]" :style="overlayStyle">
18
18
  <slot></slot>
19
19
  </div>
20
20
  </div>
@@ -67,7 +67,9 @@ export default {
67
67
  closeOnEsc: { type: Boolean, default: true },
68
68
  disabled: { type: Boolean, default: false },
69
69
  anchorWidth: { type: Boolean, default: false },
70
- width: { type: [String, Number], default: null }
70
+ width: { type: [String, Number], default: null },
71
+ overlayClassName: { type: [String, Array, Object], default: null },
72
+ overlayStyle: { type: [Object, Array], default: null }
71
73
  },
72
74
  emits: ['change', 'update:modelValue', 'open', 'close', 'select'],
73
75
  data() {
@@ -182,6 +184,9 @@ export default {
182
184
  this.$nextTick(() => {
183
185
  const float = this.$refs.floating
184
186
  if (!float) return
187
+ // Tag the floating panel with its trigger so click-outside-mixin can
188
+ // detect nested portals opened from within this panel.
189
+ float._wkTrigger = this._triggerEl
185
190
  this._coSetElements(this._triggerEl, float)
186
191
  this._kmSetFloatEl(float)
187
192
  this._pmAttach(this._triggerEl, float)
@@ -20,7 +20,6 @@
20
20
  display: flex;
21
21
  flex-direction: column;
22
22
  min-width: 176px;
23
- max-width: 320px;
24
23
  outline: none;
25
24
  pointer-events: auto;
26
25
  box-sizing: border-box;
@@ -0,0 +1,72 @@
1
+ <template>
2
+ <div class="ui-message" :class="`ui-message--${type}`" role="status" aria-live="polite">
3
+ <span class="ui-message__icon">
4
+ <WkSpinner v-if="type === 'loading'" size="sm" />
5
+ <component :is="iconComponent" v-else :size="18" />
6
+ </span>
7
+ <div class="ui-message__wrapper">
8
+ <span class="ui-message__content">
9
+ <slot>{{ content }}</slot>
10
+ </span>
11
+ <span v-if="description" class="ui-message__description">{{ description }}</span>
12
+ </div>
13
+ <WkButton v-if="action" size="mini" :variant="action.variant || 'primary'" @click="handleAction">{{
14
+ action.label
15
+ }}</WkButton>
16
+ </div>
17
+ </template>
18
+
19
+ <script>
20
+ import WkSpinner from '../spinner/Spinner.vue'
21
+ import WkButton from '../button/Button.vue'
22
+ import { WkiCircleCheck, WkiCircleX, WkiTriangleAlert, WkiInfo } from '../../icons'
23
+
24
+ const ICON_BY_TYPE = {
25
+ success: 'WkiCircleCheck',
26
+ error: 'WkiCircleX',
27
+ warning: 'WkiTriangleAlert',
28
+ info: 'WkiInfo'
29
+ }
30
+
31
+ export default {
32
+ name: 'Message',
33
+ components: { WkSpinner, WkButton, WkiCircleCheck, WkiCircleX, WkiTriangleAlert, WkiInfo },
34
+ props: {
35
+ type: {
36
+ type: String,
37
+ default: 'info',
38
+ validator: function (v) {
39
+ return ['info', 'success', 'error', 'warning', 'loading'].includes(v)
40
+ }
41
+ },
42
+ content: {
43
+ type: String,
44
+ default: ''
45
+ },
46
+ description: {
47
+ type: String,
48
+ default: null
49
+ },
50
+ action: {
51
+ type: Object,
52
+ default: null
53
+ }
54
+ },
55
+ emits: ['close'],
56
+ computed: {
57
+ iconComponent: function () {
58
+ return ICON_BY_TYPE[this.type] || ICON_BY_TYPE.info
59
+ }
60
+ },
61
+ methods: {
62
+ handleAction: function () {
63
+ if (typeof this.action.onClick === 'function') {
64
+ this.action.onClick()
65
+ }
66
+ this.$emit('close')
67
+ }
68
+ }
69
+ }
70
+ </script>
71
+
72
+ <style src="./message.css" scoped></style>
@@ -0,0 +1,200 @@
1
+ <template>
2
+ <div
3
+ class="ui-message-container"
4
+ :class="`ui-message-container--${placement}`"
5
+ :style="containerStyle"
6
+ @mouseenter="onMouseEnter"
7
+ @mouseleave="onMouseLeave"
8
+ >
9
+ <div :class="['ui-message-list', { 'ui-message-list--expanded': hovered }]">
10
+ <div v-for="(item, idx) in messages" :key="item.key" class="ui-message-item" :style="itemStyle(idx)">
11
+ <Message
12
+ :type="item.type"
13
+ :content="item.content"
14
+ :description="item.description"
15
+ :action="item.action"
16
+ @close="remove(item.key)"
17
+ />
18
+ </div>
19
+ </div>
20
+ </div>
21
+ </template>
22
+
23
+ <script>
24
+ import Message from './Message.vue'
25
+
26
+ let seed = 0
27
+
28
+ // Center placements ('top'/'bottom') flow messages as a spaced column; the four
29
+ // corner placements anchor to a corner and overlap messages (newest on top).
30
+ const PLACEMENTS = ['top', 'bottom', 'top-left', 'top-right', 'bottom-left', 'bottom-right']
31
+
32
+ export default {
33
+ name: 'MessageContainer',
34
+ components: { Message },
35
+ emits: [],
36
+ data() {
37
+ return {
38
+ messages: [],
39
+ placement: 'top',
40
+ offset: 24,
41
+ maxCount: 0,
42
+ hovered: false,
43
+ itemOffsets: []
44
+ }
45
+ },
46
+ computed: {
47
+ containerStyle() {
48
+ const p = this.placement
49
+ const o = this.offset + 'px'
50
+ const isBottom = p === 'bottom' || p === 'bottom-left' || p === 'bottom-right'
51
+ const style = {}
52
+ style[isBottom ? 'bottom' : 'top'] = o
53
+ // Corner placements pin a horizontal edge and drop the CSS center transform.
54
+ if (p === 'top-left' || p === 'bottom-left') {
55
+ style.left = o
56
+ style.right = 'auto'
57
+ style.transform = 'none'
58
+ } else if (p === 'top-right' || p === 'bottom-right') {
59
+ style.right = o
60
+ style.left = 'auto'
61
+ style.transform = 'none'
62
+ }
63
+ return style
64
+ }
65
+ },
66
+ watch: {
67
+ hovered(val) {
68
+ if (val) {
69
+ this.$nextTick(this.measureOffsets)
70
+ } else {
71
+ this.itemOffsets = []
72
+ }
73
+ },
74
+ 'messages.length'() {
75
+ if (this.hovered) this.$nextTick(this.measureOffsets)
76
+ }
77
+ },
78
+ // Timers live off the reactive data map so mutating them never triggers a render.
79
+ created() {
80
+ this._timers = {}
81
+ this._hoverTimer = null
82
+ },
83
+ // Declare both teardown hooks: Vue 2.7 fires beforeDestroy, Vue 3.4 fires beforeUnmount.
84
+ beforeUnmount() {
85
+ this.clear()
86
+ clearTimeout(this._hoverTimer)
87
+ },
88
+ // eslint-disable-next-line vue/no-deprecated-destroyed-lifecycle, vue/no-dupe-keys
89
+ beforeDestroy() {
90
+ this.clear()
91
+ clearTimeout(this._hoverTimer)
92
+ },
93
+ methods: {
94
+ onMouseEnter() {
95
+ clearTimeout(this._hoverTimer)
96
+ this.hovered = true
97
+ },
98
+ onMouseLeave() {
99
+ this._hoverTimer = setTimeout(() => {
100
+ this.hovered = false
101
+ }, 200)
102
+ },
103
+ // Measure each item's real offsetHeight and compute cumulative Y offsets
104
+ // for the expanded stack. Newest item (last in DOM) anchors at Y=0.
105
+ measureOffsets() {
106
+ if (!this.$el) return
107
+ const items = Array.from(this.$el.querySelectorAll('.ui-message-item'))
108
+ if (!items.length) {
109
+ this.itemOffsets = []
110
+ return
111
+ }
112
+ const GAP = 8
113
+ const n = items.length
114
+ const offsets = new Array(n)
115
+ let cumulative = 0
116
+ for (let reverseIdx = 0; reverseIdx < n; reverseIdx++) {
117
+ offsets[reverseIdx] = cumulative
118
+ cumulative += items[n - 1 - reverseIdx].offsetHeight + GAP
119
+ }
120
+ this.itemOffsets = offsets
121
+ },
122
+ // Returns inline style for item at v-for index idx (0 = oldest, last = newest).
123
+ itemStyle(idx) {
124
+ if (!this.hovered || !this.itemOffsets.length) return {}
125
+ const reverseIdx = this.messages.length - 1 - idx
126
+ const y = this.itemOffsets[reverseIdx]
127
+ if (y === undefined) return {}
128
+ const isBottom =
129
+ this.placement === 'bottom' || this.placement === 'bottom-left' || this.placement === 'bottom-right'
130
+ return { transform: 'translateY(' + (isBottom ? -y : y) + 'px) scaleX(1)', opacity: '1' }
131
+ },
132
+ // Merge global/service config into the container's own reactive state.
133
+ configure(opts) {
134
+ if (!opts) return
135
+ if (opts.placement) this.placement = PLACEMENTS.includes(opts.placement) ? opts.placement : 'top'
136
+ if (opts.offset != null) this.offset = opts.offset
137
+ if (opts.maxCount != null) this.maxCount = opts.maxCount
138
+ },
139
+ // Add (or update, when the key already exists) a message. Returns its key.
140
+ add(config) {
141
+ const key = config.key != null ? config.key : `wk-msg-${++seed}`
142
+ const existing = this.messages.find(m => m.key === key)
143
+ if (existing) {
144
+ existing.type = config.type || 'info'
145
+ existing.content = config.content != null ? config.content : ''
146
+ existing.description = config.description || null
147
+ existing.action = config.action || null
148
+ existing.onClose = config.onClose || null
149
+ this.scheduleRemoval(key, config.duration)
150
+ return key
151
+ }
152
+ this.messages.push({
153
+ key,
154
+ type: config.type || 'info',
155
+ content: config.content != null ? config.content : '',
156
+ description: config.description || null,
157
+ action: config.action || null,
158
+ onClose: config.onClose || null
159
+ })
160
+ // Enforce maxCount by evicting the oldest entries.
161
+ if (this.maxCount > 0 && this.messages.length > this.maxCount) {
162
+ const overflow = this.messages.slice(0, this.messages.length - this.maxCount)
163
+ overflow.forEach(m => this.remove(m.key))
164
+ }
165
+ this.scheduleRemoval(key, config.duration)
166
+ return key
167
+ },
168
+ scheduleRemoval(key, duration) {
169
+ this.clearTimer(key)
170
+ // duration <= 0 (or missing) => sticky; caller removes it manually.
171
+ if (duration && duration > 0) {
172
+ this._timers[key] = setTimeout(() => this.remove(key), duration)
173
+ }
174
+ },
175
+ remove(key) {
176
+ this.clearTimer(key)
177
+ const idx = this.messages.findIndex(m => m.key === key)
178
+ if (idx === -1) return
179
+ const [removed] = this.messages.splice(idx, 1)
180
+ if (removed && typeof removed.onClose === 'function') removed.onClose()
181
+ },
182
+ clear() {
183
+ Object.keys(this._timers).forEach(k => this.clearTimer(k))
184
+ const items = this.messages
185
+ this.messages = []
186
+ items.forEach(m => {
187
+ if (typeof m.onClose === 'function') m.onClose()
188
+ })
189
+ },
190
+ clearTimer(key) {
191
+ if (this._timers[key]) {
192
+ clearTimeout(this._timers[key])
193
+ delete this._timers[key]
194
+ }
195
+ }
196
+ }
197
+ }
198
+ </script>
199
+
200
+ <style src="./message-container.css" scoped></style>
@@ -0,0 +1,108 @@
1
+ .ui-message-container {
2
+ position: fixed;
3
+ left: 50%;
4
+ transform: translateX(-50%);
5
+ z-index: 2000;
6
+ pointer-events: none;
7
+ }
8
+
9
+ .ui-message-list {
10
+ display: grid;
11
+ pointer-events: auto;
12
+ --step: 44px;
13
+ }
14
+
15
+ /* All items share one grid cell; DOM order sets z — last in DOM = newest = on top */
16
+ .ui-message-item {
17
+ grid-area: 1 / 1;
18
+ pointer-events: auto;
19
+ transition:
20
+ transform 0.35s cubic-bezier(0.22, 1, 0.36, 1),
21
+ opacity 0.35s cubic-bezier(0.22, 1, 0.36, 1);
22
+ }
23
+
24
+ /* ── Collapsed stack peek ──────────────────────────────────── */
25
+
26
+ .ui-message-item:nth-last-child(1) {
27
+ z-index: 3;
28
+ }
29
+
30
+ .ui-message-item:nth-last-child(2) {
31
+ z-index: 2;
32
+ transform: translateY(8px) scaleX(0.95);
33
+ }
34
+
35
+ .ui-message-item:nth-last-child(n + 3) {
36
+ z-index: 1;
37
+ transform: translateY(16px) scaleX(0.9);
38
+ }
39
+
40
+ .ui-message-item:nth-last-child(n + 5) {
41
+ opacity: 0;
42
+ pointer-events: none;
43
+ }
44
+
45
+ /* Bottom placements: peek cards appear above the top card */
46
+ .ui-message-container--bottom .ui-message-item:nth-last-child(2),
47
+ .ui-message-container--bottom-left .ui-message-item:nth-last-child(2),
48
+ .ui-message-container--bottom-right .ui-message-item:nth-last-child(2) {
49
+ transform: translateY(-8px) scaleX(0.95);
50
+ }
51
+
52
+ .ui-message-container--bottom .ui-message-item:nth-last-child(n + 3),
53
+ .ui-message-container--bottom-left .ui-message-item:nth-last-child(n + 3),
54
+ .ui-message-container--bottom-right .ui-message-item:nth-last-child(n + 3) {
55
+ transform: translateY(-16px) scaleX(0.9);
56
+ }
57
+
58
+ /* ── Alignment ─────────────────────────────────────────────── */
59
+
60
+ .ui-message-container--top .ui-message-item,
61
+ .ui-message-container--bottom .ui-message-item {
62
+ justify-self: center;
63
+ }
64
+
65
+ .ui-message-container--top-left .ui-message-item,
66
+ .ui-message-container--bottom-left .ui-message-item {
67
+ justify-self: start;
68
+ }
69
+
70
+ .ui-message-container--top-right .ui-message-item,
71
+ .ui-message-container--bottom-right .ui-message-item {
72
+ justify-self: end;
73
+ }
74
+
75
+ /* ── Enter/leave animation ─────────────────────────────────── */
76
+ /*
77
+ * Vue 2 applies `-enter` / `-leave` for the start frame;
78
+ * Vue 3 applies `-enter-from` / `-leave-from`. Both listed for dual compat.
79
+ */
80
+ .ui-message-anim-enter-active,
81
+ .ui-message-anim-leave-active {
82
+ transition:
83
+ opacity 0.3s cubic-bezier(0.22, 1, 0.36, 1),
84
+ transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
85
+ }
86
+
87
+ .ui-message-anim-enter-from,
88
+ .ui-message-anim-enter,
89
+ .ui-message-anim-leave-to {
90
+ opacity: 0;
91
+ transform: translateY(-16px);
92
+ }
93
+
94
+ .ui-message-container--bottom .ui-message-anim-enter-from,
95
+ .ui-message-container--bottom .ui-message-anim-enter,
96
+ .ui-message-container--bottom .ui-message-anim-leave-to,
97
+ .ui-message-container--bottom-left .ui-message-anim-enter-from,
98
+ .ui-message-container--bottom-left .ui-message-anim-enter,
99
+ .ui-message-container--bottom-left .ui-message-anim-leave-to,
100
+ .ui-message-container--bottom-right .ui-message-anim-enter-from,
101
+ .ui-message-container--bottom-right .ui-message-anim-enter,
102
+ .ui-message-container--bottom-right .ui-message-anim-leave-to {
103
+ transform: translateY(16px);
104
+ }
105
+
106
+ .ui-message-anim-move {
107
+ transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
108
+ }