@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,147 @@
1
+ <template>
2
+ <div class="select-wrapper">
3
+ <label v-if="label" :for="selectId" class="select-label">
4
+ {{ label }}
5
+ </label>
6
+ <select
7
+ :id="selectId"
8
+ :value="modelValue"
9
+ :disabled="disabled"
10
+ :required="required"
11
+ :class="[
12
+ 'select-field',
13
+ {
14
+ 'select-field--error': error,
15
+ 'select-field--disabled': disabled,
16
+ 'select-field--placeholder': !modelValue || modelValue === ''
17
+ }
18
+ ]"
19
+ @change="handleChange"
20
+ >
21
+ <option v-if="placeholder" value="" disabled>{{ placeholder }}</option>
22
+ <option
23
+ v-for="option in options"
24
+ :key="option.value"
25
+ :value="option.value"
26
+ >
27
+ {{ option.label }}
28
+ </option>
29
+ </select>
30
+ <span v-if="error" class="select-error">{{ error }}</span>
31
+ <span v-if="hint && !error" class="select-hint">{{ hint }}</span>
32
+ </div>
33
+ </template>
34
+
35
+ <script setup lang="ts">
36
+ import { computed } from 'vue'
37
+
38
+ let componentIdCounter = 0
39
+
40
+ export interface SelectOption {
41
+ value: string | number
42
+ label: string
43
+ }
44
+
45
+ export interface SelectProps {
46
+ modelValue?: string | number
47
+ id?: string
48
+ label?: string
49
+ options?: SelectOption[]
50
+ placeholder?: string
51
+ disabled?: boolean
52
+ required?: boolean
53
+ error?: string
54
+ hint?: string
55
+ }
56
+
57
+ const props = withDefaults(defineProps<SelectProps>(), {
58
+ options: () => [],
59
+ disabled: false,
60
+ required: false
61
+ })
62
+
63
+ const emit = defineEmits<{
64
+ 'update:modelValue': [value: string]
65
+ }>()
66
+
67
+ const generatedId = `select-${++componentIdCounter}`
68
+ const selectId = computed(() => props.id || generatedId)
69
+
70
+ const handleChange = (event: Event) => {
71
+ const target = event.target as HTMLSelectElement
72
+ emit('update:modelValue', target.value)
73
+ }
74
+ </script>
75
+
76
+ <style scoped>
77
+ .select-wrapper {
78
+ display: flex;
79
+ flex-direction: column;
80
+ gap: 0.375rem;
81
+ }
82
+
83
+ .select-label {
84
+ font-size: 0.875rem;
85
+ font-weight: 500;
86
+ color: #374151;
87
+ }
88
+
89
+
90
+ .select-field {
91
+ padding: 0.75rem 0.875rem;
92
+ font-size: 1rem;
93
+ line-height: 1.5;
94
+ height: 48px;
95
+ border: 1px solid #d1d5db;
96
+ border-radius: 0.375rem;
97
+ outline: none;
98
+ transition: all 0.2s ease-in-out;
99
+ background: white;
100
+ color: #1f2937;
101
+ cursor: pointer;
102
+ appearance: none;
103
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236B7280' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
104
+ background-repeat: no-repeat;
105
+ background-position: right 0.75rem center;
106
+ padding-right: 2.5rem;
107
+ box-sizing: border-box;
108
+ }
109
+
110
+ /* When no value is selected (showing placeholder), make text gray */
111
+ .select-field--placeholder {
112
+ color: #9ca3af;
113
+ }
114
+
115
+ .select-field:focus {
116
+ border-color: #3b82f6;
117
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
118
+ }
119
+
120
+ .select-field--error {
121
+ border-color: #fca5a5;
122
+ border-bottom: 2px solid #dc2626;
123
+ background-color: #fef2f2;
124
+ }
125
+
126
+ .select-field--error:focus {
127
+ border-color: #fca5a5;
128
+ border-bottom: 2px solid #dc2626;
129
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.12);
130
+ }
131
+
132
+ .select-field--disabled {
133
+ background-color: #f3f4f6;
134
+ cursor: not-allowed;
135
+ opacity: 0.6;
136
+ }
137
+
138
+ .select-error {
139
+ font-size: 0.75rem;
140
+ color: #ef4444;
141
+ }
142
+
143
+ .select-hint {
144
+ font-size: 0.75rem;
145
+ color: #6b7280;
146
+ }
147
+ </style>
@@ -0,0 +1,16 @@
1
+ export interface SelectOption {
2
+ value: string | number
3
+ label: string
4
+ }
5
+
6
+ export interface SelectProps {
7
+ modelValue?: string | number
8
+ id?: string
9
+ label?: string
10
+ options?: SelectOption[]
11
+ placeholder?: string
12
+ disabled?: boolean
13
+ required?: boolean
14
+ error?: string
15
+ hint?: string
16
+ }
@@ -0,0 +1,274 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import { ref } from 'vue'
3
+ import StatCard from './StatCard.vue'
4
+
5
+ const meta = {
6
+ title: 'Components/StatCard',
7
+ component: StatCard,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ title: {
11
+ control: 'text',
12
+ description: 'Card title'
13
+ },
14
+ value: {
15
+ control: 'text',
16
+ description: 'Stat value'
17
+ },
18
+ variant: {
19
+ control: 'select',
20
+ options: ['default', 'success', 'warning', 'danger'],
21
+ description: 'Card color variant'
22
+ },
23
+ prefix: {
24
+ control: 'text',
25
+ description: 'Value prefix (e.g., $)'
26
+ },
27
+ suffix: {
28
+ control: 'text',
29
+ description: 'Value suffix (e.g., %)'
30
+ },
31
+ editable: {
32
+ control: 'boolean',
33
+ description: 'Whether the value is editable'
34
+ },
35
+ onInput: { action: 'input' },
36
+ onChange: { action: 'change' }
37
+ },
38
+ args: {
39
+ variant: 'default',
40
+ editable: false
41
+ }
42
+ } satisfies Meta<typeof StatCard>
43
+
44
+ export default meta
45
+ type Story = StoryObj<typeof meta>
46
+
47
+ export const Default: Story = {
48
+ args: {
49
+ title: 'Total Revenue',
50
+ value: '45,231'
51
+ }
52
+ }
53
+
54
+ export const WithPrefix: Story = {
55
+ args: {
56
+ title: 'Total Sales',
57
+ value: '12,450',
58
+ prefix: '$'
59
+ }
60
+ }
61
+
62
+ export const WithSuffix: Story = {
63
+ args: {
64
+ title: 'Conversion Rate',
65
+ value: '3.24',
66
+ suffix: '%'
67
+ }
68
+ }
69
+
70
+ export const Success: Story = {
71
+ args: {
72
+ title: 'Active Users',
73
+ value: '2,345',
74
+ variant: 'success'
75
+ }
76
+ }
77
+
78
+ export const Warning: Story = {
79
+ args: {
80
+ title: 'Pending Orders',
81
+ value: '47',
82
+ variant: 'warning'
83
+ }
84
+ }
85
+
86
+ export const Danger: Story = {
87
+ args: {
88
+ title: 'Failed Payments',
89
+ value: '8',
90
+ variant: 'danger'
91
+ }
92
+ }
93
+
94
+ export const WithTrendUp: Story = {
95
+ args: {
96
+ title: 'Revenue Growth',
97
+ value: '23,456',
98
+ prefix: '$',
99
+ trend: {
100
+ value: 12.5,
101
+ direction: 'up'
102
+ }
103
+ }
104
+ }
105
+
106
+ export const WithTrendDown: Story = {
107
+ args: {
108
+ title: 'Bounce Rate',
109
+ value: '43.2',
110
+ suffix: '%',
111
+ variant: 'danger',
112
+ trend: {
113
+ value: 8.3,
114
+ direction: 'down'
115
+ }
116
+ }
117
+ }
118
+
119
+ export const Editable: Story = {
120
+ args: {
121
+ title: 'Budget',
122
+ value: '5000',
123
+ prefix: '$',
124
+ editable: true
125
+ },
126
+ render: (args: any) => ({
127
+ components: { StatCard },
128
+ setup() {
129
+ const value = ref(args.value)
130
+
131
+ const handleChange = (newValue: string | number) => {
132
+ value.value = newValue
133
+ if (args.onChange) {
134
+ args.onChange(newValue)
135
+ }
136
+ }
137
+
138
+ return { args, value, handleChange }
139
+ },
140
+ template: `
141
+ <StatCard
142
+ v-bind="args"
143
+ :value="value"
144
+ @change="handleChange"
145
+ />
146
+ `
147
+ })
148
+ }
149
+
150
+ export const NegativeValue: Story = {
151
+ args: {
152
+ title: 'Net Income',
153
+ value: '-1,234',
154
+ prefix: '$',
155
+ variant: 'danger'
156
+ }
157
+ }
158
+
159
+ export const LargeNumber: Story = {
160
+ args: {
161
+ title: 'Total Impressions',
162
+ value: '1,234,567',
163
+ variant: 'success'
164
+ }
165
+ }
166
+
167
+ export const Decimal: Story = {
168
+ args: {
169
+ title: 'Average Rating',
170
+ value: '4.8',
171
+ suffix: '/ 5'
172
+ }
173
+ }
174
+
175
+ export const Dashboard: Story = {
176
+ render: () => ({
177
+ components: { StatCard },
178
+ template: `
179
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem;">
180
+ <StatCard
181
+ title="Total Revenue"
182
+ value="45,231"
183
+ prefix="$"
184
+ variant="success"
185
+ :trend="{ value: 12.5, direction: 'up' }"
186
+ />
187
+ <StatCard
188
+ title="Active Users"
189
+ value="2,345"
190
+ :trend="{ value: 8.2, direction: 'up' }"
191
+ />
192
+ <StatCard
193
+ title="Conversion Rate"
194
+ value="3.24"
195
+ suffix="%"
196
+ :trend="{ value: 0.8, direction: 'down' }"
197
+ />
198
+ <StatCard
199
+ title="Pending Orders"
200
+ value="47"
201
+ variant="warning"
202
+ />
203
+ </div>
204
+ `
205
+ })
206
+ }
207
+
208
+ export const EditableDashboard: Story = {
209
+ render: () => ({
210
+ components: { StatCard },
211
+ setup() {
212
+ const budget = ref('5000')
213
+ const target = ref('10000')
214
+ const employees = ref('25')
215
+
216
+ return { budget, target, employees }
217
+ },
218
+ template: `
219
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem;">
220
+ <StatCard
221
+ title="Monthly Budget"
222
+ v-model:value="budget"
223
+ prefix="$"
224
+ editable
225
+ @change="(val) => console.log('Budget changed:', val)"
226
+ />
227
+ <StatCard
228
+ title="Sales Target"
229
+ v-model:value="target"
230
+ prefix="$"
231
+ editable
232
+ variant="success"
233
+ @change="(val) => console.log('Target changed:', val)"
234
+ />
235
+ <StatCard
236
+ title="Team Size"
237
+ v-model:value="employees"
238
+ editable
239
+ @change="(val) => console.log('Employees changed:', val)"
240
+ />
241
+ </div>
242
+ `
243
+ })
244
+ }
245
+
246
+ export const AllVariants: Story = {
247
+ render: () => ({
248
+ components: { StatCard },
249
+ template: `
250
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem;">
251
+ <StatCard
252
+ title="Default"
253
+ value="1,234"
254
+ variant="default"
255
+ />
256
+ <StatCard
257
+ title="Success"
258
+ value="5,678"
259
+ variant="success"
260
+ />
261
+ <StatCard
262
+ title="Warning"
263
+ value="910"
264
+ variant="warning"
265
+ />
266
+ <StatCard
267
+ title="Danger"
268
+ value="42"
269
+ variant="danger"
270
+ />
271
+ </div>
272
+ `
273
+ })
274
+ }
@@ -0,0 +1,226 @@
1
+ <template>
2
+ <div class="stat-card" :class="`stat-card--${variant}`">
3
+ <h3 class="stat-card__title">{{ title }}</h3>
4
+
5
+ <div v-if="editable" class="stat-card__input-wrapper">
6
+ <span v-if="prefix" class="stat-card__prefix">{{ prefix }}</span>
7
+ <input
8
+ :value="value"
9
+ @input="handleInput"
10
+ @change="handleChange"
11
+ type="number"
12
+ class="stat-card__input"
13
+ :placeholder="String(value)"
14
+ />
15
+ <span v-if="suffix" class="stat-card__suffix">{{ suffix }}</span>
16
+ </div>
17
+
18
+ <div v-else class="stat-card__value-wrapper">
19
+ <p class="stat-card__value" :class="{ 'stat-card__value--negative': isNegative }">
20
+ <span v-if="prefix" class="stat-card__prefix">{{ prefix }}</span>
21
+ <span>{{ formattedValue }}</span>
22
+ <span v-if="suffix" class="stat-card__suffix">{{ suffix }}</span>
23
+ </p>
24
+
25
+ <div v-if="trend" class="stat-card__trend" :class="`stat-card__trend--${trend.direction}`">
26
+ <span class="stat-card__trend-icon">{{ trend.direction === 'up' ? '↑' : '↓' }}</span>
27
+ <span class="stat-card__trend-value">{{ Math.abs(trend.value) }}%</span>
28
+ </div>
29
+ </div>
30
+
31
+ <slot name="footer" />
32
+ </div>
33
+ </template>
34
+
35
+ <script setup lang="ts">
36
+ import { computed } from 'vue'
37
+ import type { StatCardProps } from './types'
38
+
39
+ const props = withDefaults(defineProps<StatCardProps>(), {
40
+ variant: 'default',
41
+ prefix: '',
42
+ suffix: '',
43
+ editable: false
44
+ })
45
+
46
+ const emit = defineEmits<{
47
+ input: [value: number | string]
48
+ change: [value: number | string]
49
+ }>()
50
+
51
+ const formattedValue = computed(() => {
52
+ if (typeof props.value === 'number') {
53
+ return props.value.toLocaleString()
54
+ }
55
+ return props.value
56
+ })
57
+
58
+ const isNegative = computed(() => {
59
+ return typeof props.value === 'number' && props.value < 0
60
+ })
61
+
62
+ const handleInput = (event: Event) => {
63
+ const target = event.target as HTMLInputElement
64
+ const value = target.valueAsNumber || target.value
65
+ emit('input', value)
66
+ }
67
+
68
+ const handleChange = (event: Event) => {
69
+ const target = event.target as HTMLInputElement
70
+ const value = target.valueAsNumber || target.value
71
+ emit('change', value)
72
+ }
73
+ </script>
74
+
75
+ <style scoped>
76
+ .stat-card {
77
+ background: white;
78
+ border: 1px solid #e5e7eb;
79
+ border-radius: 12px;
80
+ padding: 1.5rem;
81
+ transition: all 0.2s ease;
82
+ }
83
+
84
+ .stat-card:hover {
85
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
86
+ transform: translateY(-2px);
87
+ }
88
+
89
+ .stat-card__title {
90
+ margin: 0 0 0.75rem 0;
91
+ font-size: 0.875rem;
92
+ font-weight: 600;
93
+ text-transform: uppercase;
94
+ letter-spacing: 0.05em;
95
+ color: #6b7280;
96
+ }
97
+
98
+ .stat-card__value-wrapper {
99
+ display: flex;
100
+ align-items: baseline;
101
+ justify-content: space-between;
102
+ gap: 0.5rem;
103
+ }
104
+
105
+ .stat-card__value {
106
+ margin: 0;
107
+ font-size: 2rem;
108
+ font-weight: 700;
109
+ color: #111827;
110
+ display: flex;
111
+ align-items: baseline;
112
+ gap: 0.25rem;
113
+ }
114
+
115
+ .stat-card__value--negative {
116
+ color: #dc2626;
117
+ }
118
+
119
+ .stat-card__prefix {
120
+ font-size: 1.5rem;
121
+ color: #6b7280;
122
+ font-weight: 500;
123
+ }
124
+
125
+ .stat-card__suffix {
126
+ font-size: 1rem;
127
+ color: #6b7280;
128
+ font-weight: 500;
129
+ margin-left: 0.25rem;
130
+ }
131
+
132
+ .stat-card__input-wrapper {
133
+ display: flex;
134
+ align-items: center;
135
+ gap: 0.5rem;
136
+ background: #f9fafb;
137
+ border: 2px solid #e5e7eb;
138
+ border-radius: 8px;
139
+ padding: 0.5rem 0.75rem;
140
+ transition: border-color 0.2s;
141
+ }
142
+
143
+ .stat-card__input-wrapper:focus-within {
144
+ border-color: #3b82f6;
145
+ }
146
+
147
+ .stat-card__input {
148
+ flex: 1;
149
+ border: none;
150
+ background: transparent;
151
+ font-size: 1.5rem;
152
+ font-weight: 700;
153
+ color: #111827;
154
+ outline: none;
155
+ min-width: 0;
156
+ }
157
+
158
+ .stat-card__input::placeholder {
159
+ color: #9ca3af;
160
+ }
161
+
162
+ /* Remove spinner buttons */
163
+ .stat-card__input::-webkit-outer-spin-button,
164
+ .stat-card__input::-webkit-inner-spin-button {
165
+ -webkit-appearance: none;
166
+ margin: 0;
167
+ }
168
+
169
+ .stat-card__input[type=number] {
170
+ -moz-appearance: textfield;
171
+ }
172
+
173
+ .stat-card__trend {
174
+ display: flex;
175
+ align-items: center;
176
+ gap: 0.25rem;
177
+ font-size: 0.875rem;
178
+ font-weight: 600;
179
+ padding: 0.25rem 0.5rem;
180
+ border-radius: 4px;
181
+ }
182
+
183
+ .stat-card__trend--up {
184
+ color: #059669;
185
+ background: #d1fae5;
186
+ }
187
+
188
+ .stat-card__trend--down {
189
+ color: #dc2626;
190
+ background: #fee2e2;
191
+ }
192
+
193
+ .stat-card__trend-icon {
194
+ font-size: 1rem;
195
+ }
196
+
197
+ /* Variants */
198
+ .stat-card--success {
199
+ border-color: #10b981;
200
+ background: linear-gradient(135deg, #ffffff 0%, #f0fdf4 100%);
201
+ }
202
+
203
+ .stat-card--warning {
204
+ border-color: #f59e0b;
205
+ background: linear-gradient(135deg, #ffffff 0%, #fffbeb 100%);
206
+ }
207
+
208
+ .stat-card--danger {
209
+ border-color: #ef4444;
210
+ background: linear-gradient(135deg, #ffffff 0%, #fef2f2 100%);
211
+ }
212
+
213
+ @media (max-width: 768px) {
214
+ .stat-card {
215
+ padding: 1rem;
216
+ }
217
+
218
+ .stat-card__value {
219
+ font-size: 1.5rem;
220
+ }
221
+
222
+ .stat-card__input {
223
+ font-size: 1.25rem;
224
+ }
225
+ }
226
+ </style>
@@ -0,0 +1,12 @@
1
+ export interface StatCardProps {
2
+ title: string
3
+ value: string | number
4
+ variant?: 'default' | 'success' | 'warning' | 'danger'
5
+ prefix?: string
6
+ suffix?: string
7
+ editable?: boolean
8
+ trend?: {
9
+ value: number
10
+ direction: 'up' | 'down'
11
+ }
12
+ }