@veritree/ui 0.89.5 → 0.90.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.
@@ -44,14 +44,12 @@ export const floatingUiContentMixin = {
44
44
 
45
45
  setTimeout(() => {
46
46
  if (!this.id) {
47
- console.log('No id provided');
48
47
  return;
49
48
  }
50
49
 
51
50
  this.el = document.getElementById(this.id);
52
51
 
53
52
  if (!this.el) {
54
- console.log('Element not found', this.id);
55
53
  return;
56
54
  }
57
55
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veritree/ui",
3
- "version": "0.89.5",
3
+ "version": "0.90.1",
4
4
  "description": "veritree ui library",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -1,53 +1,15 @@
1
1
  <template>
2
- <!-- :href="href" -->
3
2
  <component
4
3
  :is="tag"
5
4
  :to="to"
6
5
  :href="href || to"
7
6
  :type="type"
8
7
  :disabled="isDisabled"
9
- :class="[
10
- // default styles
11
- headless ? 'button' : isIcon ? null : 'button-base',
12
- // variant styles
13
- headless
14
- ? `button--${variant}`
15
- : isPrimary
16
- ? 'button--primary'
17
- : isSecondary
18
- ? isGhost
19
- ? 'border-gray-500 text-gray-100 hover:bg-white hover:text-gray-700'
20
- : 'button--secondary'
21
- : isTertiary
22
- ? 'button--tertiary'
23
- : isDark
24
- ? 'button--dark'
25
- : isIcon
26
- ? 'button--icon'
27
- : null,
28
- // sizes styles
29
- headless
30
- ? `button--${size}`
31
- : isLarge
32
- ? isIcon
33
- ? 'h-8 w-8'
34
- : 'button--large'
35
- : isSmall
36
- ? isIcon
37
- ? 'h-6 w-6 p-1'
38
- : 'button--small'
39
- : null,
40
- isDisabled ? (isIcon ? '[&_svg]:text-gray-400' : null) : null,
41
- ]"
8
+ :class="buttonClasses"
42
9
  v-on="$listeners"
43
10
  >
44
11
  <VTSpinner v-if="busy" class="absolute inset-0 m-auto" />
45
- <span
46
- :class="[
47
- headless ? null : 'mx-auto inline-flex items-center gap-2 self-center',
48
- headless && busy ? 'button--busy' : busy ? 'invisible' : null,
49
- ]"
50
- >
12
+ <span :class="spanClasses">
51
13
  <!-- @slot Use this slot for the button content -->
52
14
  <slot></slot>
53
15
  </span>
@@ -78,6 +40,11 @@ export default {
78
40
  variant: {
79
41
  type: String,
80
42
  default: 'primary',
43
+ validator(value) {
44
+ return ['primary', 'secondary', 'tertiary', 'dark', 'icon'].includes(
45
+ value,
46
+ );
47
+ },
81
48
  },
82
49
 
83
50
  /**
@@ -90,6 +57,9 @@ export default {
90
57
  size: {
91
58
  type: String,
92
59
  default: 'large',
60
+ validator(value) {
61
+ return ['small', 'large'].includes(value);
62
+ },
93
63
  },
94
64
 
95
65
  /**
@@ -156,37 +126,99 @@ export default {
156
126
 
157
127
  appearance: {
158
128
  type: String,
159
- default: 'normal', // normal or ghost
129
+ default: 'normal',
130
+ validator(value) {
131
+ return ['normal', 'ghost'].includes(value);
132
+ },
133
+ },
134
+
135
+ version: {
136
+ type: String,
137
+ default: '1',
160
138
  },
161
139
  },
162
140
 
163
141
  computed: {
164
- isPrimary() {
165
- return this.variant === 'primary';
166
- },
142
+ buttonClasses() {
143
+ const classes = [];
167
144
 
168
- isSecondary() {
169
- return this.variant === 'secondary';
170
- },
145
+ if (this.headless) {
146
+ // Headless mode - use semantic classes
147
+ classes.push('button');
148
+ classes.push(`button--${this.variant}`);
149
+ classes.push(`button--${this.size}`);
150
+ } else {
151
+ // Default mode - use component styles
171
152
 
172
- isTertiary() {
173
- return this.variant === 'tertiary';
174
- },
153
+ // Default styles
154
+ if (!this.isIcon) {
155
+ classes.push('button-base');
156
+ }
175
157
 
176
- isDark() {
177
- return this.variant === 'dark';
178
- },
158
+ // Variant styles
159
+ if (this.variant === 'primary') {
160
+ classes.push('button--primary');
161
+ }
179
162
 
180
- isIcon() {
181
- return this.variant === 'icon';
163
+ if (this.variant === 'secondary') {
164
+ classes.push('button--secondary');
165
+ }
166
+
167
+ if (this.variant === 'tertiary') {
168
+ classes.push('button--tertiary');
169
+ }
170
+
171
+ if (this.variant === 'dark') {
172
+ classes.push('button--dark');
173
+ }
174
+
175
+ if (this.isIcon) {
176
+ classes.push('button--icon');
177
+ }
178
+
179
+ // Ghost appearance can be applied to any variant
180
+ if (this.isGhost) {
181
+ classes.push('button--ghost');
182
+ }
183
+
184
+ // Size styles
185
+ if (this.size === 'large') {
186
+ classes.push(this.isIcon ? 'h-8 w-8' : 'button--large');
187
+ } else if (this.size === 'small') {
188
+ classes.push(this.isIcon ? 'h-6 w-6 p-1' : 'button--small');
189
+ }
190
+
191
+ if (this.version && this.version !== '1') {
192
+ classes.push(`button--version-${this.version}`);
193
+ }
194
+ }
195
+
196
+ // Disabled state (applies to both headless and non-headless)
197
+ if (this.isDisabled && this.isIcon) {
198
+ classes.push('[&_svg]:text-gray-400');
199
+ }
200
+
201
+ return classes.filter(Boolean);
182
202
  },
183
203
 
184
- isLarge() {
185
- return this.size === 'large';
204
+ spanClasses() {
205
+ const classes = [];
206
+
207
+ if (!this.headless) {
208
+ classes.push('mx-auto inline-flex items-center gap-2 self-center');
209
+ }
210
+
211
+ if (this.headless && this.busy) {
212
+ classes.push('button--busy');
213
+ } else if (this.busy) {
214
+ classes.push('invisible');
215
+ }
216
+
217
+ return classes.filter(Boolean);
186
218
  },
187
219
 
188
- isSmall() {
189
- return this.size === 'small';
220
+ isIcon() {
221
+ return this.variant === 'icon';
190
222
  },
191
223
 
192
224
  isDisabled() {
@@ -1,17 +1,11 @@
1
1
  <template>
2
2
  <header
3
3
  :id="id"
4
- :class="[
5
- headless
6
- ? 'details-header'
7
- : 'flex cursor-pointer justify-between gap-3 text-body font-semibold',
8
- ]"
9
- :aria-controls="ariaControls"
10
- :aria-expanded="ariaExpanded"
11
- role="button"
12
- tabindex="0"
13
- @click.prevent="toggle"
14
- @keydown.enter="toggle"
4
+ :class="headerClasses"
5
+ v-bind="ariaAttributes"
6
+ @click.prevent="handleClick"
7
+ @keydown.enter.prevent="handleKeydown"
8
+ @keydown.space.prevent="handleKeydown"
15
9
  >
16
10
  <slot></slot>
17
11
  </header>
@@ -24,6 +18,18 @@ export default {
24
18
  inject: ['apiDisclosure', 'apiDetails'],
25
19
 
26
20
  props: {
21
+ /**
22
+ * If true, the header will not toggle visibility of the content.
23
+ * Useful for headers that are purely informational.
24
+ */
25
+ actionless: {
26
+ type: Boolean,
27
+ default: false,
28
+ },
29
+ /**
30
+ * If true, the header will not have any visual styles applied.
31
+ * Useful for headless implementations where you want to style it yourself.
32
+ */
27
33
  headless: {
28
34
  type: Boolean,
29
35
  default: false,
@@ -31,15 +37,40 @@ export default {
31
37
  },
32
38
 
33
39
  computed: {
40
+ headerClasses() {
41
+ if (this.headless) {
42
+ return 'details-header';
43
+ }
44
+
45
+ const classes = ['flex justify-between gap-3 text-body font-semibold'];
46
+ classes.push(this.actionless ? 'cursor-default' : 'cursor-pointer');
47
+
48
+ return classes;
49
+ },
50
+
51
+ ariaAttributes() {
52
+ if (this.actionless) {
53
+ return {};
54
+ }
55
+
56
+ return {
57
+ 'aria-controls': this.ariaControls,
58
+ 'aria-expanded': this.ariaExpanded,
59
+ 'role': 'button',
60
+ 'tabindex': '0',
61
+ };
62
+ },
63
+
34
64
  id() {
35
65
  return this.apiDetails().idSummary;
36
66
  },
67
+
37
68
  ariaControls() {
38
69
  return this.apiDetails().idContent;
39
70
  },
40
71
 
41
72
  isVisible() {
42
- return this.apiDetails().isVisible();
73
+ return this.apiDetails().visible;
43
74
  },
44
75
 
45
76
  ariaExpanded() {
@@ -49,8 +80,24 @@ export default {
49
80
 
50
81
  methods: {
51
82
  toggle() {
83
+ if (this.actionless) {
84
+ return;
85
+ }
86
+
52
87
  this.apiDetails().setVisible(!this.isVisible);
53
88
  },
89
+
90
+ handleClick() {
91
+ if (!this.actionless) {
92
+ this.toggle();
93
+ }
94
+ },
95
+
96
+ handleKeydown() {
97
+ if (!this.actionless) {
98
+ this.toggle();
99
+ }
100
+ },
54
101
  },
55
102
  };
56
103
  </script>
@@ -1,12 +1,14 @@
1
1
  <template>
2
- <span
3
- :class="[
4
- headless ? 'disclosure-icon' : 'shrink-0 transition-all',
5
- headless ? null : expanded ? 'rotate-180' : 'rotate-0',
6
- ]"
2
+ <component
3
+ :is="as"
4
+ :class="iconClasses"
5
+ v-bind="ariaAttributes"
6
+ @click.stop="onClick"
7
+ @keydown.enter="onClick"
8
+ @keydown.space.prevent="onClick"
7
9
  >
8
10
  <slot><IconChevronDown /></slot>
9
- </span>
11
+ </component>
10
12
  </template>
11
13
 
12
14
  <script>
@@ -20,12 +22,53 @@ export default {
20
22
  type: Boolean,
21
23
  default: false,
22
24
  },
25
+ as: {
26
+ type: String,
27
+ default: 'span',
28
+ validator(value) {
29
+ return ['span', 'button', 'div', 'a'].includes(value);
30
+ },
31
+ },
23
32
  },
24
33
 
25
34
  computed: {
35
+ ariaAttributes() {
36
+ if (this.as !== 'button') {
37
+ return {};
38
+ }
39
+
40
+ return {
41
+ 'aria-expanded': String(this.expanded),
42
+ 'aria-controls': this.apiDetails().idContent,
43
+ };
44
+ },
45
+
46
+ iconClasses() {
47
+ const classes = [];
48
+
49
+ if (this.headless) {
50
+ classes.push('disclosure-icon');
51
+ } else {
52
+ classes.push('shrink-0 transition-all');
53
+ classes.push(this.expanded ? 'rotate-180' : 'rotate-0');
54
+ classes.push(this.as === 'button' ? null : 'pointer-events-none');
55
+ }
56
+
57
+ return classes.filter(Boolean);
58
+ },
59
+
26
60
  expanded() {
27
61
  return this.apiDetails().visible;
28
62
  },
29
63
  },
64
+
65
+ methods: {
66
+ onClick() {
67
+ // Only handle click if rendered as a button
68
+ if (this.as === 'button') {
69
+ this.apiDetails().setVisible(!this.expanded);
70
+ }
71
+ },
72
+ },
30
73
  };
31
74
  </script>