@veritree/ui 0.94.1 → 0.95.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.
@@ -76,11 +76,11 @@ export const formControlStyleMixin = {
76
76
  classComputed() {
77
77
  return [
78
78
  this.headless
79
- ? `${this.name}`
79
+ ? `${this.name ? this.name : ''}`
80
80
  : 'leading-0 bg-white has-[:disabled]:bg-gray-200 disabled:bg-gray-200 flex w-full max-w-full relative appearance-none placeholder:font-light placeholder:text-gray-500 items-center justify-between rounded border border-solid px-3 py-2 font-inherit text-inherit file:hidden focus-within:border-gray-600 focus:border-gray-600 focus-within:placeholder:text-gray-400 focus:placeholder:text-gray-400 has-[:disabled]:text-gray-500 disabled:text-gray-500',
81
81
  // variant styles
82
82
  this.headless
83
- ? `${this.name}--${this.variant}`
83
+ ? `${this.name ? this.name : ''}${this.variant ? '--' + this.variant : ''}`
84
84
  : this.isError
85
85
  ? 'border-error-300'
86
86
  : this.isSuccess
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.1",
3
+ "version": "0.95.0",
4
4
  "description": "veritree ui library",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <VTPopover>
2
+ <VTPopover :placement="placement">
3
3
  <VTPopoverTrigger ref="trigger">
4
4
  <button
5
5
  :id="id"
@@ -9,13 +9,29 @@
9
9
  @click.prevent
10
10
  >
11
11
  <span v-if="valueModel">{{ valueModelFormatted }}</span>
12
- <span class="text-gray-500" v-else>{{ format }}</span>
12
+ <slot v-else name="label">
13
+ <span
14
+ :class="{
15
+ 'text-gray-500': !headless,
16
+ }"
17
+ >{{ label || format }}</span
18
+ >
19
+ </slot>
13
20
  <span
14
21
  v-if="!iconless"
15
- class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2"
16
- :class="[disabled ? 'bg-gray-200' : 'bg-white']"
22
+ class=""
23
+ :class="[
24
+ headless ? '' : disabled ? 'bg-gray-200' : 'bg-white',
25
+ headless
26
+ ? ''
27
+ : 'pointer-events-none absolute right-3 top-1/2 -translate-y-1/2',
28
+ ]"
17
29
  >
18
- <IconCalendar class="-z-0 h-5 w-5 scale-90 text-gray-600" />
30
+ <IconCalendar
31
+ :class="{
32
+ '-z-0 h-5 w-5 scale-90 text-gray-600': !headless,
33
+ }"
34
+ />
19
35
  </span>
20
36
  </button>
21
37
  </VTPopoverTrigger>
@@ -89,6 +105,10 @@ export default {
89
105
  type: [Function, Object],
90
106
  default: null,
91
107
  },
108
+ label: {
109
+ type: String,
110
+ default: null,
111
+ },
92
112
  format: {
93
113
  type: String,
94
114
  default: 'YYYY-MM-DD',
@@ -125,6 +145,10 @@ export default {
125
145
  type: Boolean,
126
146
  default: false,
127
147
  },
148
+ placement: {
149
+ type: String,
150
+ default: 'bottom-start',
151
+ },
128
152
  },
129
153
 
130
154
  computed: {
@@ -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>