@veritree/ui 0.94.2 → 0.95.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.
@@ -39,7 +39,7 @@ export const floatingUiItemMixin = {
39
39
  ? `${this.componentName}--disabled`
40
40
  : null
41
41
  : this.disabled
42
- ? 'pointer-events-none opacity-75'
42
+ ? 'pointer-events-none opacity-50'
43
43
  : null,
44
44
  // selected state styles
45
45
  this.headless
package/nuxt.js CHANGED
@@ -20,6 +20,7 @@ const components = [
20
20
  'src/components/Skeleton',
21
21
  'src/components/Spinner',
22
22
  'src/components/Tabs',
23
+ 'src/components/Toast',
23
24
  'src/components/Tooltip',
24
25
  'src/components/Switch',
25
26
  'src/components/Separator',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veritree/ui",
3
- "version": "0.94.2",
3
+ "version": "0.95.1",
4
4
  "description": "veritree ui library",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -71,11 +71,7 @@ export default {
71
71
 
72
72
  computed: {
73
73
  computedClasses() {
74
- const classes = this.classComputed;
75
- if (this.disabled) {
76
- classes.push('bg-gray-200');
77
- }
78
- return classes;
74
+ return this.classComputed;
79
75
  },
80
76
 
81
77
  componentContent() {
@@ -0,0 +1,263 @@
1
+ # Toast Component
2
+
3
+ A toast notification system using native HTML `<dialog>` elements with CSS transitions, supporting 4 corner positions, auto-dismiss, vertical stacking, and composable child components.
4
+
5
+ ## Features
6
+
7
+ - ✅ 4 corner positions (top-left, top-right, bottom-left, bottom-right)
8
+ - ✅ Auto-dismiss with configurable duration (default 5s)
9
+ - ✅ Pause on hover
10
+ - ✅ Vertical stacking of multiple toasts
11
+ - ✅ Variant support (default, success, error, warning)
12
+ - ✅ HTML `<dialog>` element with `@starting-style` animations
13
+ - ✅ Auto-show when first item added, auto-hide when empty
14
+ - ✅ Composable child components
15
+ - ✅ Headless mode support
16
+ - ✅ Accessible (ARIA attributes)
17
+
18
+ ## Components
19
+
20
+ - `VTToast` - Container component (renders `<dialog>`)
21
+ - `VTToastItem` - Individual toast wrapper
22
+ - `VTToastIcon` - Icon column with variant-based default icons
23
+ - `VTToastContent` - Content wrapper for Title and Description
24
+ - `VTToastTitle` - Title text
25
+ - `VTToastDescription` - Description text
26
+ - `VTToastAction` - Action buttons area
27
+ - `VTToastClose` - Close button
28
+
29
+ ## Basic Usage
30
+
31
+ ### Simple Toast
32
+
33
+ ```vue
34
+ <template>
35
+ <VTToast position="top-right">
36
+ <VTToastItem variant="success">
37
+ <VTToastIcon />
38
+ <VTToastContent>
39
+ <VTToastTitle>Success!</VTToastTitle>
40
+ <VTToastDescription>Your changes have been saved.</VTToastDescription>
41
+ </VTToastContent>
42
+ <VTToastClose />
43
+ </VTToastItem>
44
+ </VTToast>
45
+ </template>
46
+ ```
47
+
48
+ ### With Action Button
49
+
50
+ ```vue
51
+ <template>
52
+ <VTToast position="bottom-right">
53
+ <VTToastItem variant="warning" :duration="10000">
54
+ <VTToastIcon />
55
+ <VTToastContent>
56
+ <VTToastTitle>Session Expiring</VTToastTitle>
57
+ <VTToastDescription
58
+ >Your session will expire in 5 minutes.</VTToastDescription
59
+ >
60
+ </VTToastContent>
61
+ <VTToastAction>
62
+ <VTButton size="small" variant="primary" @click="extendSession">
63
+ Extend
64
+ </VTButton>
65
+ </VTToastAction>
66
+ <VTToastClose />
67
+ </VTToastItem>
68
+ </VTToast>
69
+ </template>
70
+ ```
71
+
72
+ ### Multiple Toasts (Stacked)
73
+
74
+ ```vue
75
+ <template>
76
+ <VTToast position="top-right">
77
+ <VTToastItem variant="success" :duration="5000">
78
+ <VTToastIcon />
79
+ <VTToastContent>
80
+ <VTToastTitle>File uploaded</VTToastTitle>
81
+ <VTToastDescription>document.pdf has been uploaded.</VTToastDescription>
82
+ </VTToastContent>
83
+ <VTToastClose />
84
+ </VTToastItem>
85
+
86
+ <VTToastItem variant="info" :duration="7000">
87
+ <VTToastIcon />
88
+ <VTToastContent>
89
+ <VTToastTitle>Processing</VTToastTitle>
90
+ <VTToastDescription>Your file is being processed.</VTToastDescription>
91
+ </VTToastContent>
92
+ <VTToastClose />
93
+ </VTToastItem>
94
+
95
+ <VTToastItem variant="error" :duration="0">
96
+ <VTToastIcon />
97
+ <VTToastContent>
98
+ <VTToastTitle>Upload Failed</VTToastTitle>
99
+ <VTToastDescription>Failed to upload image.png</VTToastDescription>
100
+ </VTToastContent>
101
+ <VTToastAction>
102
+ <VTButton size="small" @click="retry">Retry</VTButton>
103
+ </VTToastAction>
104
+ <VTToastClose />
105
+ </VTToastItem>
106
+ </VTToast>
107
+ </template>
108
+ ```
109
+
110
+ ### Custom Icon
111
+
112
+ ```vue
113
+ <template>
114
+ <VTToast position="bottom-left">
115
+ <VTToastItem variant="default">
116
+ <VTToastIcon>
117
+ <IconBell class="h-5 w-5" />
118
+ </VTToastIcon>
119
+ <VTToastContent>
120
+ <VTToastTitle>New Notification</VTToastTitle>
121
+ <VTToastDescription>You have a new message.</VTToastDescription>
122
+ </VTToastContent>
123
+ <VTToastClose />
124
+ </VTToastItem>
125
+ </VTToast>
126
+ </template>
127
+ ```
128
+
129
+ ### Minimal Toast (Title Only)
130
+
131
+ ```vue
132
+ <template>
133
+ <VTToast position="top-left">
134
+ <VTToastItem variant="success" :dismissable="false">
135
+ <VTToastIcon />
136
+ <VTToastContent>
137
+ <VTToastTitle>Saved successfully</VTToastTitle>
138
+ </VTToastContent>
139
+ </VTToastItem>
140
+ </VTToast>
141
+ </template>
142
+ ```
143
+
144
+ ## Props
145
+
146
+ ### VTToast
147
+
148
+ | Prop | Type | Default | Description |
149
+ | ---------- | ------- | ------------- | ------------------------------------------------------------------------------------------- |
150
+ | `position` | String | `'top-right'` | Position of toast container: `'top-left'`, `'top-right'`, `'bottom-left'`, `'bottom-right'` |
151
+ | `headless` | Boolean | `false` | Render without default styles (semantic class only) |
152
+
153
+ ### VTToastItem
154
+
155
+ | Prop | Type | Default | Description |
156
+ | ------------- | ------- | ----------- | ---------------------------------------------------------------- |
157
+ | `variant` | String | `'default'` | Visual variant: `'default'`, `'success'`, `'error'`, `'warning'` |
158
+ | `dismissable` | Boolean | `true` | Show/hide close button |
159
+ | `duration` | Number | `5000` | Auto-dismiss duration in ms (0 = no auto-dismiss) |
160
+ | `headless` | Boolean | `false` | Render without default styles |
161
+ | `as` | String | `'div'` | HTML element to render as |
162
+
163
+ ### VTToastIcon, VTToastContent, VTToastTitle, VTToastDescription, VTToastAction, VTToastClose
164
+
165
+ | Prop | Type | Default | Description |
166
+ | ---------- | ------- | ------- | ----------------------------- |
167
+ | `headless` | Boolean | `false` | Render without default styles |
168
+ | `as` | String | `'div'` | HTML element to render as |
169
+
170
+ ## Events
171
+
172
+ ### VTToastItem
173
+
174
+ | Event | Payload | Description |
175
+ | ------- | ------- | ------------------------------------------------ |
176
+ | `close` | - | Emitted when toast is dismissed (auto or manual) |
177
+
178
+ ### VTToastClose
179
+
180
+ | Event | Payload | Description |
181
+ | ------- | ------- | ------------------------------------ |
182
+ | `click` | - | Emitted when close button is clicked |
183
+
184
+ ## Behavior
185
+
186
+ ### Auto-show/Auto-hide
187
+
188
+ The `<dialog>` element automatically:
189
+
190
+ - Shows when the first `VTToastItem` is registered
191
+ - Hides when the last `VTToastItem` is unregistered
192
+
193
+ ### Auto-dismiss
194
+
195
+ Toast items automatically dismiss after the `duration` (in milliseconds):
196
+
197
+ - Default: 5000ms (5 seconds)
198
+ - Set to `0` to disable auto-dismiss
199
+ - Timer pauses on `mouseenter`, resumes on `mouseleave`
200
+
201
+ ### Stacking
202
+
203
+ Multiple toast items stack vertically with spacing (`gap-3`) at the configured position.
204
+
205
+ ## Layout
206
+
207
+ Toast items use a 3-column grid layout:
208
+
209
+ ```
210
+ ┌─────────┬──────────────────────┬───────────────┐
211
+ │ Icon │ Content (Title + │ Action + │
212
+ │ │ Description) │ Close │
213
+ └─────────┴──────────────────────┴───────────────┘
214
+ ```
215
+
216
+ Grid: `grid-cols-[auto_1fr_auto]`
217
+
218
+ - Column 1: Icon (auto-sized)
219
+ - Column 2: Content wrapper (fills remaining space)
220
+ - Column 3: Actions + Close button (auto-sized)
221
+
222
+ ## Accessibility
223
+
224
+ - `role="alert"` on toast items
225
+ - `aria-live="polite"` for screen reader announcements
226
+ - Unique IDs for title and description (for future `aria-labelledby`/`aria-describedby` support)
227
+
228
+ ## Headless Mode
229
+
230
+ All components support headless mode for custom styling:
231
+
232
+ ```vue
233
+ <VTToast headless position="top-right">
234
+ <VTToastItem headless variant="success">
235
+ <VTToastIcon headless />
236
+ <VTToastContent headless>
237
+ <VTToastTitle headless>Custom styled toast</VTToastTitle>
238
+ </VTToastContent>
239
+ </VTToastItem>
240
+ </VTToast>
241
+ ```
242
+
243
+ Headless classes:
244
+
245
+ - `.toast` - Container
246
+ - `.toast-item` - Individual toast
247
+ - `.toast-item--{variant}` - Variant modifier
248
+ - `.toast-icon` - Icon
249
+ - `.toast-content` - Content wrapper
250
+ - `.toast-title` - Title
251
+ - `.toast-description` - Description
252
+ - `.toast-action` - Action area
253
+ - `.toast-close` - Close button
254
+
255
+ ## Browser Support
256
+
257
+ The `@starting-style` CSS feature is used for dialog animations. Supported in:
258
+
259
+ - Chrome 117+
260
+ - Safari 17.5+
261
+ - Firefox 129+
262
+
263
+ For older browsers, toasts will still work but without the initial slide animation.
@@ -0,0 +1,150 @@
1
+ <template>
2
+ <Portal>
3
+ <dialog
4
+ ref="dialog"
5
+ :id="id"
6
+ :class="
7
+ headless
8
+ ? 'toast'
9
+ : `fixed ${positionClass} z-50 flex flex-col gap-2 border-none bg-transparent p-0`
10
+ "
11
+ >
12
+ <slot></slot>
13
+ </dialog>
14
+ </Portal>
15
+ </template>
16
+
17
+ <script>
18
+ import { Portal } from '@linusborg/vue-simple-portal';
19
+ import { genId } from '../../utils/ids';
20
+
21
+ export default {
22
+ name: 'VTToast',
23
+
24
+ components: {
25
+ Portal,
26
+ },
27
+
28
+ provide() {
29
+ return {
30
+ apiToast: () => {
31
+ const componentId = this.componentId;
32
+
33
+ const registerItem = (item) => {
34
+ if (!item) {
35
+ return;
36
+ }
37
+
38
+ this.items.push(item);
39
+ };
40
+
41
+ const unregisterItem = (item) => {
42
+ if (!item) {
43
+ return;
44
+ }
45
+
46
+ const index = this.items.indexOf(item);
47
+
48
+ if (index > -1) {
49
+ this.items.splice(index, 1);
50
+ }
51
+ };
52
+
53
+ return {
54
+ componentId,
55
+ registerItem,
56
+ unregisterItem,
57
+ position: this.position,
58
+ };
59
+ },
60
+ };
61
+ },
62
+
63
+ props: {
64
+ headless: {
65
+ type: Boolean,
66
+ default: false,
67
+ },
68
+ position: {
69
+ type: String,
70
+ default: 'top-right',
71
+ validator: (value) => {
72
+ return [
73
+ 'top-left',
74
+ 'top-right',
75
+ 'bottom-left',
76
+ 'bottom-right',
77
+ ].includes(value);
78
+ },
79
+ },
80
+ },
81
+
82
+ data() {
83
+ return {
84
+ componentId: genId(),
85
+ items: [],
86
+ };
87
+ },
88
+
89
+ computed: {
90
+ id() {
91
+ return `toast-${this.componentId}`;
92
+ },
93
+
94
+ positionClass() {
95
+ switch (this.position) {
96
+ case 'top-left':
97
+ return 'top-6 left-10 bottom-auto right-auto';
98
+ case 'top-right':
99
+ return 'top-6 right-10 bottom-auto left-auto';
100
+ case 'bottom-left':
101
+ return 'bottom-6 left-10 top-auto right-auto';
102
+ case 'bottom-right':
103
+ return 'bottom-6 right-10 top-auto left-auto';
104
+ default:
105
+ return 'top-6 right-10 bottom-auto left-auto';
106
+ }
107
+ },
108
+
109
+ isOpen() {
110
+ return this.items.length > 0;
111
+ },
112
+ },
113
+
114
+ watch: {
115
+ isOpen(newValue, oldValue) {
116
+ if (newValue && !oldValue) {
117
+ // Show dialog when first item is added
118
+ this.$refs.dialog?.show();
119
+ } else if (!newValue && oldValue) {
120
+ // Close dialog when last item is removed
121
+ this.$refs.dialog?.close();
122
+ }
123
+ },
124
+ },
125
+ };
126
+ </script>
127
+
128
+ <style scoped>
129
+ dialog {
130
+ transition: opacity 300ms ease-out;
131
+ opacity: 1;
132
+ }
133
+
134
+ dialog[open] {
135
+ opacity: 1;
136
+ }
137
+
138
+ /* Initial state - just fade in */
139
+ @starting-style {
140
+ dialog[open] {
141
+ opacity: 0;
142
+ }
143
+ }
144
+
145
+ /* Remove default dialog backdrop and allow clicks through */
146
+ dialog::backdrop {
147
+ background: transparent;
148
+ pointer-events: none;
149
+ }
150
+ </style>
@@ -0,0 +1,25 @@
1
+ <template>
2
+ <component
3
+ :is="as"
4
+ :class="[headless ? 'toast-action' : 'flex flex-col gap-2']"
5
+ >
6
+ <slot></slot>
7
+ </component>
8
+ </template>
9
+
10
+ <script>
11
+ export default {
12
+ name: 'VTToastAction',
13
+
14
+ props: {
15
+ headless: {
16
+ type: Boolean,
17
+ default: false,
18
+ },
19
+ as: {
20
+ type: String,
21
+ default: 'div',
22
+ },
23
+ },
24
+ };
25
+ </script>
@@ -0,0 +1,52 @@
1
+ <template>
2
+ <VTButton
3
+ variant="icon"
4
+ :id="id"
5
+ :class="[
6
+ headless
7
+ ? 'toast-close'
8
+ : 'self-start text-gray-400 size-2.5 focus:bg-transparent hover:bg-transparent',
9
+ ]"
10
+ @click.prevent="close"
11
+ >
12
+ <slot>
13
+ <IconClose class="h-4 w-4" />
14
+ </slot>
15
+ </VTButton>
16
+ </template>
17
+
18
+ <script>
19
+ import { IconClose } from '@veritree/icons';
20
+ import VTButton from '../Button/VTButton.vue';
21
+
22
+ export default {
23
+ name: 'VTToastClose',
24
+
25
+ components: {
26
+ IconClose,
27
+ VTButton,
28
+ },
29
+
30
+ inject: ['apiToastItem'],
31
+
32
+ props: {
33
+ headless: {
34
+ type: Boolean,
35
+ default: false,
36
+ },
37
+ },
38
+
39
+ computed: {
40
+ id() {
41
+ return `toast-close-${this.apiToastItem().componentId}`;
42
+ },
43
+ },
44
+
45
+ methods: {
46
+ close() {
47
+ this.apiToastItem().close();
48
+ this.$emit('click');
49
+ },
50
+ },
51
+ };
52
+ </script>
@@ -0,0 +1,25 @@
1
+ <template>
2
+ <component
3
+ :is="as"
4
+ :class="[headless ? 'toast-content' : 'flex flex-col gap-0.5']"
5
+ >
6
+ <slot></slot>
7
+ </component>
8
+ </template>
9
+
10
+ <script>
11
+ export default {
12
+ name: 'VTToastContent',
13
+
14
+ props: {
15
+ headless: {
16
+ type: Boolean,
17
+ default: false,
18
+ },
19
+ as: {
20
+ type: String,
21
+ default: 'div',
22
+ },
23
+ },
24
+ };
25
+ </script>
@@ -0,0 +1,36 @@
1
+ <template>
2
+ <component
3
+ :is="as"
4
+ :id="id"
5
+ :class="[
6
+ headless ? 'toast-description' : 'text-xs opacity-90 text-gray-400',
7
+ ]"
8
+ >
9
+ <slot></slot>
10
+ </component>
11
+ </template>
12
+
13
+ <script>
14
+ export default {
15
+ name: 'VTToastDescription',
16
+
17
+ inject: ['apiToastItem'],
18
+
19
+ props: {
20
+ headless: {
21
+ type: Boolean,
22
+ default: false,
23
+ },
24
+ as: {
25
+ type: String,
26
+ default: 'div',
27
+ },
28
+ },
29
+
30
+ computed: {
31
+ id() {
32
+ return `toast-description-${this.apiToastItem().componentId}`;
33
+ },
34
+ },
35
+ };
36
+ </script>
@@ -0,0 +1,72 @@
1
+ <template>
2
+ <component
3
+ :is="as"
4
+ :class="[
5
+ headless
6
+ ? 'toast-icon'
7
+ : 'flex h-5 w-5 shrink-0 items-center justify-center',
8
+ ]"
9
+ >
10
+ <slot>
11
+ <!-- Default icons based on variant -->
12
+ <IconCheck v-if="isSuccess" :class="iconColorClass" />
13
+ <IconWarning v-else-if="isError" :class="iconColorClass" />
14
+ <IconInfo v-else :class="iconColorClass" />
15
+ </slot>
16
+ </component>
17
+ </template>
18
+
19
+ <script>
20
+ import { IconCheck, IconWarning, IconInfo } from '@veritree/icons';
21
+
22
+ export default {
23
+ name: 'VTToastIcon',
24
+
25
+ components: {
26
+ IconCheck,
27
+ IconWarning,
28
+ IconInfo,
29
+ },
30
+
31
+ inject: ['apiToastItem'],
32
+
33
+ props: {
34
+ headless: {
35
+ type: Boolean,
36
+ default: false,
37
+ },
38
+ as: {
39
+ type: String,
40
+ default: 'div',
41
+ },
42
+ },
43
+
44
+ computed: {
45
+ variant() {
46
+ return this.apiToastItem().variant;
47
+ },
48
+
49
+ isSuccess() {
50
+ return this.variant === 'success';
51
+ },
52
+
53
+ isError() {
54
+ return this.variant === 'error';
55
+ },
56
+
57
+ isWarning() {
58
+ return this.variant === 'warning';
59
+ },
60
+
61
+ iconColorClass() {
62
+ if (this.headless) return '';
63
+
64
+ if (this.isSuccess) return 'text-secondary-200';
65
+ if (this.isError) return 'text-error-500';
66
+ if (this.isWarning) return 'text-warning-200';
67
+
68
+ return 'h-5 w-5';
69
+ },
70
+ },
71
+ };
72
+ </script>