@davidbirchall/core 1.0.6 → 1.0.8

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 (95) hide show
  1. package/.storybook/main.ts +18 -0
  2. package/.storybook/preview.ts +14 -0
  3. package/package.json +1 -4
  4. package/src/components/Badge/Badge.stories.ts +147 -0
  5. package/src/components/Badge/Badge.test.ts +57 -0
  6. package/src/components/Badge/Badge.vue +79 -0
  7. package/src/components/Button/Button.stories.ts +80 -0
  8. package/src/components/Button/Button.test.ts +145 -0
  9. package/src/components/Button/Button.vue +108 -0
  10. package/src/components/Button/types.ts +4 -0
  11. package/src/components/Calendar/Calendar.stories.ts +261 -0
  12. package/src/components/Calendar/Calendar.test.ts +119 -0
  13. package/src/components/Calendar/Calendar.vue +528 -0
  14. package/src/components/Calendar/types.ts +20 -0
  15. package/src/components/Card/Card.stories.ts +88 -0
  16. package/src/components/Card/Card.test.ts +173 -0
  17. package/src/components/Card/Card.vue +59 -0
  18. package/{dist/Card/types.d.ts → src/components/Card/types.ts} +1 -1
  19. package/src/components/Checkbox/Checkbox.stories.ts +126 -0
  20. package/src/components/Checkbox/Checkbox.test.ts +155 -0
  21. package/src/components/Checkbox/Checkbox.vue +121 -0
  22. package/src/components/Checkbox/types.ts +7 -0
  23. package/src/components/DataTable/DataTable.stories.ts +156 -0
  24. package/src/components/DataTable/DataTable.test.ts +185 -0
  25. package/src/components/DataTable/DataTable.vue +177 -0
  26. package/src/components/DataTable/types.ts +12 -0
  27. package/src/components/DatePicker/DatePicker.stories.ts +172 -0
  28. package/src/components/DatePicker/DatePicker.test.ts +87 -0
  29. package/src/components/DatePicker/DatePicker.vue +302 -0
  30. package/src/components/Dropdown/Dropdown.stories.ts +231 -0
  31. package/src/components/Dropdown/Dropdown.vue +314 -0
  32. package/src/components/Dropdown/types.ts +14 -0
  33. package/src/components/EmptyState/EmptyState.stories.ts +189 -0
  34. package/src/components/EmptyState/EmptyState.vue +215 -0
  35. package/src/components/EmptyState/types.ts +8 -0
  36. package/src/components/ErrorSummary/ErrorSummary.vue +78 -0
  37. package/src/components/ErrorSummary/types.ts +4 -0
  38. package/src/components/FormGroup/FormGroup.stories.ts +264 -0
  39. package/src/components/FormGroup/FormGroup.test.ts +63 -0
  40. package/src/components/FormGroup/FormGroup.vue +58 -0
  41. package/src/components/Heading/Heading.stories.ts +121 -0
  42. package/src/components/Heading/Heading.test.ts +184 -0
  43. package/src/components/Heading/Heading.vue +95 -0
  44. package/src/components/Heading/types.ts +6 -0
  45. package/src/components/Input/Input.stories.ts +172 -0
  46. package/src/components/Input/Input.test.ts +213 -0
  47. package/src/components/Input/Input.vue +121 -0
  48. package/src/components/Input/types.ts +11 -0
  49. package/src/components/Modal/Modal.stories.ts +341 -0
  50. package/src/components/Modal/Modal.test.ts +99 -0
  51. package/src/components/Modal/Modal.vue +278 -0
  52. package/src/components/ProgressBar/ProgressBar.stories.ts +313 -0
  53. package/src/components/ProgressBar/ProgressBar.test.ts +98 -0
  54. package/src/components/ProgressBar/ProgressBar.vue +117 -0
  55. package/src/components/Select/Select.stories.ts +177 -0
  56. package/src/components/Select/Select.test.ts +225 -0
  57. package/src/components/Select/Select.vue +147 -0
  58. package/src/components/Select/types.ts +16 -0
  59. package/src/components/StatCard/StatCard.stories.ts +274 -0
  60. package/src/components/StatCard/StatCard.vue +226 -0
  61. package/src/components/StatCard/types.ts +12 -0
  62. package/src/components/Tag/Tag.stories.ts +78 -0
  63. package/src/components/Tag/Tag.test.ts +50 -0
  64. package/src/components/Tag/Tag.vue +71 -0
  65. package/src/components/Tag/types.ts +4 -0
  66. package/src/components/TextArea/TextArea.stories.ts +171 -0
  67. package/src/components/TextArea/TextArea.test.ts +202 -0
  68. package/src/components/TextArea/TextArea.vue +122 -0
  69. package/src/components/TextArea/types.ts +11 -0
  70. package/src/components/index.ts +5 -0
  71. package/src/test/setup.ts +1 -0
  72. package/src/vite-env.d.ts +6 -0
  73. package/tsconfig.json +29 -0
  74. package/vite.config.ts +33 -0
  75. package/vitest.config.ts +28 -0
  76. package/dist/Button/types.d.ts +0 -4
  77. package/dist/Calendar/types.d.ts +0 -22
  78. package/dist/Checkbox/types.d.ts +0 -7
  79. package/dist/DataTable/types.d.ts +0 -11
  80. package/dist/Dropdown/types.d.ts +0 -13
  81. package/dist/EmptyState/types.d.ts +0 -8
  82. package/dist/ErrorSummary/types.d.ts +0 -4
  83. package/dist/Heading/types.d.ts +0 -6
  84. package/dist/Input/types.d.ts +0 -11
  85. package/dist/Select/types.d.ts +0 -15
  86. package/dist/StatCard/types.d.ts +0 -12
  87. package/dist/Tag/types.d.ts +0 -4
  88. package/dist/TextArea/types.d.ts +0 -11
  89. package/dist/core.css +0 -1
  90. package/dist/core.js +0 -24
  91. package/dist/core.js.map +0 -1
  92. package/dist/core.umd.cjs +0 -2
  93. package/dist/core.umd.cjs.map +0 -1
  94. package/dist/index.d.ts +0 -2
  95. package/dist/package.json +0 -27
@@ -0,0 +1,314 @@
1
+ <template>
2
+ <div
3
+ ref="dropdownRef"
4
+ class="dropdown"
5
+ :class="{ 'dropdown--disabled': disabled }"
6
+ >
7
+ <div @click="toggleDropdown" class="dropdown__trigger">
8
+ <slot name="trigger">
9
+ <button type="button" class="dropdown__button">
10
+ Menu
11
+ </button>
12
+ </slot>
13
+ </div>
14
+
15
+ <Teleport to="body">
16
+ <Transition name="dropdown-fade">
17
+ <div
18
+ v-if="isOpen"
19
+ ref="dropdownMenuRef"
20
+ class="dropdown__menu"
21
+ :class="`dropdown__menu--${placement}`"
22
+ :style="menuStyles"
23
+ @click="handleMenuClick"
24
+ >
25
+ <div v-if="$slots.header" class="dropdown__header">
26
+ <slot name="header" />
27
+ </div>
28
+
29
+ <div class="dropdown__content">
30
+ <slot>
31
+ <template v-for="(item, index) in items" :key="index">
32
+ <div v-if="item.divider" class="dropdown__divider" />
33
+ <button
34
+ v-else
35
+ type="button"
36
+ class="dropdown__item"
37
+ :class="{ 'dropdown__item--disabled': item.disabled }"
38
+ :disabled="item.disabled"
39
+ @click="() => handleItemClick(item)"
40
+ >
41
+ <span v-if="item.icon" class="dropdown__item-icon">{{ item.icon }}</span>
42
+ <span class="dropdown__item-label">{{ item.label }}</span>
43
+ </button>
44
+ </template>
45
+ </slot>
46
+ </div>
47
+
48
+ <div v-if="$slots.footer" class="dropdown__footer">
49
+ <slot name="footer" />
50
+ </div>
51
+ </div>
52
+ </Transition>
53
+ </Teleport>
54
+ </div>
55
+ </template>
56
+
57
+ <script setup lang="ts">
58
+ import { ref, computed, onMounted, onUnmounted } from 'vue'
59
+ import type { DropdownProps, DropdownItem } from './types'
60
+
61
+ const props = withDefaults(defineProps<DropdownProps>(), {
62
+ items: () => [],
63
+ placement: 'bottom-left',
64
+ closeOnClick: true,
65
+ disabled: false
66
+ })
67
+
68
+ const emit = defineEmits<{
69
+ select: [item: DropdownItem]
70
+ open: []
71
+ close: []
72
+ }>()
73
+
74
+ const dropdownRef = ref<HTMLElement | null>(null)
75
+ const dropdownMenuRef = ref<HTMLElement | null>(null)
76
+ const isOpen = ref(false)
77
+ const menuStyles = ref<Record<string, string>>({})
78
+
79
+ const toggleDropdown = () => {
80
+ if (props.disabled) return
81
+
82
+ if (isOpen.value) {
83
+ closeDropdown()
84
+ } else {
85
+ openDropdown()
86
+ }
87
+ }
88
+
89
+ const openDropdown = () => {
90
+ isOpen.value = true
91
+ emit('open')
92
+
93
+ // Calculate position on next tick
94
+ setTimeout(() => {
95
+ updateMenuPosition()
96
+ }, 0)
97
+ }
98
+
99
+ const closeDropdown = () => {
100
+ isOpen.value = false
101
+ emit('close')
102
+ }
103
+
104
+ const updateMenuPosition = () => {
105
+ if (!dropdownRef.value || !dropdownMenuRef.value) return
106
+
107
+ const triggerRect = dropdownRef.value.getBoundingClientRect()
108
+ const menuRect = dropdownMenuRef.value.getBoundingClientRect()
109
+
110
+ let top = 0
111
+ let left = 0
112
+
113
+ switch (props.placement) {
114
+ case 'bottom-left':
115
+ top = triggerRect.bottom + 8
116
+ left = triggerRect.left
117
+ break
118
+ case 'bottom-right':
119
+ top = triggerRect.bottom + 8
120
+ left = triggerRect.right - menuRect.width
121
+ break
122
+ case 'top-left':
123
+ top = triggerRect.top - menuRect.height - 8
124
+ left = triggerRect.left
125
+ break
126
+ case 'top-right':
127
+ top = triggerRect.top - menuRect.height - 8
128
+ left = triggerRect.right - menuRect.width
129
+ break
130
+ }
131
+
132
+ menuStyles.value = {
133
+ top: `${top}px`,
134
+ left: `${left}px`
135
+ }
136
+ }
137
+
138
+ const handleItemClick = (item: DropdownItem) => {
139
+ if (item.disabled) return
140
+
141
+ emit('select', item)
142
+
143
+ if (props.closeOnClick) {
144
+ closeDropdown()
145
+ }
146
+ }
147
+
148
+ const handleMenuClick = (event: MouseEvent) => {
149
+ // Prevent closing when clicking inside menu (unless on an item)
150
+ event.stopPropagation()
151
+ }
152
+
153
+ const handleClickOutside = (event: MouseEvent) => {
154
+ if (!dropdownRef.value || !dropdownMenuRef.value) return
155
+
156
+ const target = event.target as Node
157
+ if (!dropdownRef.value.contains(target) && !dropdownMenuRef.value.contains(target)) {
158
+ closeDropdown()
159
+ }
160
+ }
161
+
162
+ onMounted(() => {
163
+ document.addEventListener('click', handleClickOutside)
164
+ window.addEventListener('scroll', updateMenuPosition, true)
165
+ window.addEventListener('resize', updateMenuPosition)
166
+ })
167
+
168
+ onUnmounted(() => {
169
+ document.removeEventListener('click', handleClickOutside)
170
+ window.removeEventListener('scroll', updateMenuPosition, true)
171
+ window.removeEventListener('resize', updateMenuPosition)
172
+ })
173
+ </script>
174
+
175
+ <style scoped>
176
+ .dropdown {
177
+ position: relative;
178
+ display: inline-block;
179
+ }
180
+
181
+ .dropdown--disabled {
182
+ opacity: 0.5;
183
+ cursor: not-allowed;
184
+ }
185
+
186
+ .dropdown__trigger {
187
+ cursor: pointer;
188
+ }
189
+
190
+ .dropdown__button {
191
+ padding: 0.5rem 1rem;
192
+ background: white;
193
+ border: 1px solid #d1d5db;
194
+ border-radius: 6px;
195
+ font-size: 0.875rem;
196
+ font-weight: 500;
197
+ color: #374151;
198
+ cursor: pointer;
199
+ transition: all 0.2s;
200
+ }
201
+
202
+ .dropdown__button:hover {
203
+ background: #f9fafb;
204
+ border-color: #9ca3af;
205
+ }
206
+
207
+ .dropdown__menu {
208
+ position: fixed;
209
+ z-index: 1000;
210
+ min-width: 200px;
211
+ background: white;
212
+ border: 1px solid #e5e7eb;
213
+ border-radius: 8px;
214
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
215
+ overflow: hidden;
216
+ }
217
+
218
+ .dropdown__header,
219
+ .dropdown__footer {
220
+ padding: 0.75rem 1rem;
221
+ border-bottom: 1px solid #e5e7eb;
222
+ background: #f9fafb;
223
+ }
224
+
225
+ .dropdown__footer {
226
+ border-top: 1px solid #e5e7eb;
227
+ border-bottom: none;
228
+ }
229
+
230
+ .dropdown__content {
231
+ padding: 0.5rem 0;
232
+ max-height: 300px;
233
+ overflow-y: auto;
234
+ }
235
+
236
+ .dropdown__item {
237
+ width: 100%;
238
+ display: flex;
239
+ align-items: center;
240
+ gap: 0.75rem;
241
+ padding: 0.625rem 1rem;
242
+ border: none;
243
+ background: transparent;
244
+ text-align: left;
245
+ font-size: 0.875rem;
246
+ color: #374151;
247
+ cursor: pointer;
248
+ transition: background-color 0.15s;
249
+ }
250
+
251
+ .dropdown__item:hover {
252
+ background: #f3f4f6;
253
+ }
254
+
255
+ .dropdown__item:active {
256
+ background: #e5e7eb;
257
+ }
258
+
259
+ .dropdown__item--disabled {
260
+ color: #9ca3af;
261
+ cursor: not-allowed;
262
+ opacity: 0.5;
263
+ }
264
+
265
+ .dropdown__item--disabled:hover {
266
+ background: transparent;
267
+ }
268
+
269
+ .dropdown__item-icon {
270
+ font-size: 1.125rem;
271
+ flex-shrink: 0;
272
+ }
273
+
274
+ .dropdown__item-label {
275
+ flex: 1;
276
+ }
277
+
278
+ .dropdown__divider {
279
+ height: 1px;
280
+ margin: 0.5rem 0;
281
+ background: #e5e7eb;
282
+ }
283
+
284
+ /* Transitions */
285
+ .dropdown-fade-enter-active,
286
+ .dropdown-fade-leave-active {
287
+ transition: opacity 0.15s ease, transform 0.15s ease;
288
+ }
289
+
290
+ .dropdown-fade-enter-from,
291
+ .dropdown-fade-leave-to {
292
+ opacity: 0;
293
+ transform: translateY(-8px);
294
+ }
295
+
296
+ /* Scrollbar styling */
297
+ .dropdown__content::-webkit-scrollbar {
298
+ width: 6px;
299
+ }
300
+
301
+ .dropdown__content::-webkit-scrollbar-track {
302
+ background: #f1f1f1;
303
+ border-radius: 3px;
304
+ }
305
+
306
+ .dropdown__content::-webkit-scrollbar-thumb {
307
+ background: #d1d5db;
308
+ border-radius: 3px;
309
+ }
310
+
311
+ .dropdown__content::-webkit-scrollbar-thumb:hover {
312
+ background: #9ca3af;
313
+ }
314
+ </style>
@@ -0,0 +1,14 @@
1
+ export interface DropdownItem {
2
+ label: string
3
+ value: string
4
+ icon?: string
5
+ disabled?: boolean
6
+ divider?: boolean
7
+ }
8
+
9
+ export interface DropdownProps {
10
+ items?: DropdownItem[]
11
+ placement?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right'
12
+ closeOnClick?: boolean
13
+ disabled?: boolean
14
+ }
@@ -0,0 +1,189 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import EmptyState from './EmptyState.vue'
3
+
4
+ const meta = {
5
+ title: 'Components/EmptyState',
6
+ component: EmptyState,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ title: {
10
+ control: 'text',
11
+ description: 'Empty state title'
12
+ },
13
+ description: {
14
+ control: 'text',
15
+ description: 'Empty state description'
16
+ },
17
+ icon: {
18
+ control: 'text',
19
+ description: 'Icon or emoji to display'
20
+ },
21
+ actionText: {
22
+ control: 'text',
23
+ description: 'Action button text'
24
+ },
25
+ actionVariant: {
26
+ control: 'select',
27
+ options: ['primary', 'secondary'],
28
+ description: 'Action button variant'
29
+ },
30
+ size: {
31
+ control: 'select',
32
+ options: ['small', 'medium', 'large'],
33
+ description: 'Empty state size'
34
+ },
35
+ onAction: { action: 'action-clicked' }
36
+ },
37
+ args: {
38
+ size: 'medium',
39
+ actionVariant: 'primary'
40
+ }
41
+ } satisfies Meta<typeof EmptyState>
42
+
43
+ export default meta
44
+ type Story = StoryObj<typeof meta>
45
+
46
+ export const Default: Story = {
47
+ args: {
48
+ title: 'No items found',
49
+ description: 'Get started by creating a new item',
50
+ icon: '📭'
51
+ }
52
+ }
53
+
54
+ export const WithAction: Story = {
55
+ args: {
56
+ title: 'No projects yet',
57
+ description: 'Create your first project to get started',
58
+ icon: '📁',
59
+ actionText: 'Create Project'
60
+ }
61
+ }
62
+
63
+ export const NoData: Story = {
64
+ args: {
65
+ title: 'No data available',
66
+ description: 'There is no data to display at this time',
67
+ icon: '📊'
68
+ }
69
+ }
70
+
71
+ export const NoResults: Story = {
72
+ args: {
73
+ title: 'No results found',
74
+ description: 'Try adjusting your search or filter to find what you\'re looking for',
75
+ icon: '🔍',
76
+ actionText: 'Clear Filters',
77
+ actionVariant: 'secondary'
78
+ }
79
+ }
80
+
81
+ export const EmptyInbox: Story = {
82
+ args: {
83
+ title: 'Inbox Zero!',
84
+ description: 'You\'ve read all your messages. Great job!',
85
+ icon: '✅'
86
+ }
87
+ }
88
+
89
+ export const Small: Story = {
90
+ args: {
91
+ title: 'No items',
92
+ description: 'Add your first item',
93
+ icon: '➕',
94
+ actionText: 'Add Item',
95
+ size: 'small'
96
+ }
97
+ }
98
+
99
+ export const Large: Story = {
100
+ args: {
101
+ title: 'Welcome to your dashboard',
102
+ description: 'Get started by creating your first project. You can add team members, set goals, and track progress all in one place.',
103
+ icon: '🎉',
104
+ actionText: 'Get Started',
105
+ size: 'large'
106
+ }
107
+ }
108
+
109
+ export const CustomIcon: Story = {
110
+ args: {
111
+ title: 'No notifications',
112
+ description: 'You\'re all caught up!',
113
+ icon: '🔔'
114
+ }
115
+ }
116
+
117
+ export const ErrorState: Story = {
118
+ args: {
119
+ title: 'Something went wrong',
120
+ description: 'We couldn\'t load your data. Please try again.',
121
+ icon: '⚠️',
122
+ actionText: 'Retry',
123
+ actionVariant: 'primary'
124
+ }
125
+ }
126
+
127
+ export const WithCustomSlots: Story = {
128
+ render: (args: any) => ({
129
+ components: { EmptyState },
130
+ setup() {
131
+ return { args }
132
+ },
133
+ template: `
134
+ <EmptyState v-bind="args">
135
+ <template #icon>
136
+ <div style="font-size: 4rem;">🚀</div>
137
+ </template>
138
+ <template #title>
139
+ <h2 style="color: #667eea;">Custom Title Slot</h2>
140
+ </template>
141
+ <template #description>
142
+ <p style="color: #6b7280; font-style: italic;">
143
+ You can customize any part of the empty state with slots
144
+ </p>
145
+ </template>
146
+ </EmptyState>
147
+ `
148
+ })
149
+ }
150
+
151
+ export const AllSizes: Story = {
152
+ render: () => ({
153
+ components: { EmptyState },
154
+ template: `
155
+ <div style="display: flex; flex-direction: column; gap: 3rem;">
156
+ <div>
157
+ <h3 style="margin-bottom: 1rem;">Small</h3>
158
+ <EmptyState
159
+ size="small"
160
+ title="Small empty state"
161
+ description="Compact size for inline usage"
162
+ icon="📦"
163
+ actionText="Action"
164
+ />
165
+ </div>
166
+ <div>
167
+ <h3 style="margin-bottom: 1rem;">Medium</h3>
168
+ <EmptyState
169
+ size="medium"
170
+ title="Medium empty state"
171
+ description="Default size for most use cases"
172
+ icon="📦"
173
+ actionText="Action"
174
+ />
175
+ </div>
176
+ <div>
177
+ <h3 style="margin-bottom: 1rem;">Large</h3>
178
+ <EmptyState
179
+ size="large"
180
+ title="Large empty state"
181
+ description="Larger size for prominent empty states with more detailed descriptions"
182
+ icon="📦"
183
+ actionText="Action"
184
+ />
185
+ </div>
186
+ </div>
187
+ `
188
+ })
189
+ }