@htlkg/components 0.0.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.
Files changed (79) hide show
  1. package/dist/composables/index.js +388 -0
  2. package/dist/composables/index.js.map +1 -0
  3. package/package.json +41 -0
  4. package/src/composables/index.ts +6 -0
  5. package/src/composables/useForm.test.ts +229 -0
  6. package/src/composables/useForm.ts +130 -0
  7. package/src/composables/useFormValidation.test.ts +189 -0
  8. package/src/composables/useFormValidation.ts +83 -0
  9. package/src/composables/useModal.property.test.ts +164 -0
  10. package/src/composables/useModal.ts +43 -0
  11. package/src/composables/useNotifications.test.ts +166 -0
  12. package/src/composables/useNotifications.ts +81 -0
  13. package/src/composables/useTable.property.test.ts +198 -0
  14. package/src/composables/useTable.ts +134 -0
  15. package/src/composables/useTabs.property.test.ts +247 -0
  16. package/src/composables/useTabs.ts +101 -0
  17. package/src/data/Chart.demo.vue +340 -0
  18. package/src/data/Chart.md +525 -0
  19. package/src/data/Chart.vue +133 -0
  20. package/src/data/DataList.md +80 -0
  21. package/src/data/DataList.test.ts +69 -0
  22. package/src/data/DataList.vue +46 -0
  23. package/src/data/SearchableSelect.md +107 -0
  24. package/src/data/SearchableSelect.vue +124 -0
  25. package/src/data/Table.demo.vue +296 -0
  26. package/src/data/Table.md +588 -0
  27. package/src/data/Table.property.test.ts +548 -0
  28. package/src/data/Table.test.ts +562 -0
  29. package/src/data/Table.unit.test.ts +544 -0
  30. package/src/data/Table.vue +321 -0
  31. package/src/data/index.ts +5 -0
  32. package/src/domain/BrandCard.md +81 -0
  33. package/src/domain/BrandCard.vue +63 -0
  34. package/src/domain/BrandSelector.md +84 -0
  35. package/src/domain/BrandSelector.vue +65 -0
  36. package/src/domain/ProductBadge.md +60 -0
  37. package/src/domain/ProductBadge.vue +47 -0
  38. package/src/domain/UserAvatar.md +84 -0
  39. package/src/domain/UserAvatar.vue +60 -0
  40. package/src/domain/domain-components.property.test.ts +449 -0
  41. package/src/domain/index.ts +4 -0
  42. package/src/forms/DateRange.demo.vue +273 -0
  43. package/src/forms/DateRange.md +337 -0
  44. package/src/forms/DateRange.vue +110 -0
  45. package/src/forms/JsonSchemaForm.demo.vue +549 -0
  46. package/src/forms/JsonSchemaForm.md +112 -0
  47. package/src/forms/JsonSchemaForm.property.test.ts +817 -0
  48. package/src/forms/JsonSchemaForm.test.ts +601 -0
  49. package/src/forms/JsonSchemaForm.unit.test.ts +801 -0
  50. package/src/forms/JsonSchemaForm.vue +615 -0
  51. package/src/forms/index.ts +3 -0
  52. package/src/index.ts +17 -0
  53. package/src/navigation/Breadcrumbs.demo.vue +142 -0
  54. package/src/navigation/Breadcrumbs.md +102 -0
  55. package/src/navigation/Breadcrumbs.test.ts +69 -0
  56. package/src/navigation/Breadcrumbs.vue +58 -0
  57. package/src/navigation/Stepper.demo.vue +337 -0
  58. package/src/navigation/Stepper.md +174 -0
  59. package/src/navigation/Stepper.vue +146 -0
  60. package/src/navigation/Tabs.demo.vue +293 -0
  61. package/src/navigation/Tabs.md +163 -0
  62. package/src/navigation/Tabs.test.ts +176 -0
  63. package/src/navigation/Tabs.vue +104 -0
  64. package/src/navigation/index.ts +5 -0
  65. package/src/overlays/Alert.demo.vue +377 -0
  66. package/src/overlays/Alert.md +248 -0
  67. package/src/overlays/Alert.test.ts +166 -0
  68. package/src/overlays/Alert.vue +70 -0
  69. package/src/overlays/Drawer.md +140 -0
  70. package/src/overlays/Drawer.test.ts +92 -0
  71. package/src/overlays/Drawer.vue +76 -0
  72. package/src/overlays/Modal.demo.vue +149 -0
  73. package/src/overlays/Modal.md +385 -0
  74. package/src/overlays/Modal.test.ts +128 -0
  75. package/src/overlays/Modal.vue +86 -0
  76. package/src/overlays/Notification.md +150 -0
  77. package/src/overlays/Notification.test.ts +96 -0
  78. package/src/overlays/Notification.vue +58 -0
  79. package/src/overlays/index.ts +4 -0
@@ -0,0 +1,321 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue';
3
+ import {
4
+ type TableItemType,
5
+ type UiDropdownItemType,
6
+ type UiModalInterface,
7
+ type UiTableInterface,
8
+ uiTable
9
+ } from '@hotelinking/ui';
10
+
11
+ export interface Column {
12
+ name: string;
13
+ value: string;
14
+ tooltip?: string;
15
+ }
16
+
17
+ interface Props {
18
+ // Data
19
+ items: any[];
20
+ columns: Column[];
21
+
22
+ // State
23
+ loading?: boolean;
24
+ currentPage?: number;
25
+ totalPages?: number;
26
+ totalItems?: number;
27
+ pageSize?: number;
28
+ orderedBy?: string;
29
+ orderDirection?: 'asc' | 'desc';
30
+
31
+ // Selection
32
+ actions?: Array<{ name: string; id: string }>;
33
+ selectAllItemsModal?: UiModalInterface;
34
+ resetSelected?: boolean;
35
+
36
+ // Filters (now integrated in uiTable)
37
+ smartFilterCategories?: Array<{
38
+ id: string;
39
+ name: string;
40
+ componentType: 'uiInput' | 'uiSelect';
41
+ defaultProps: any;
42
+ }>;
43
+ filterLiterals?: {
44
+ filters: string;
45
+ contains: string;
46
+ is: string;
47
+ and: string;
48
+ or: string;
49
+ deleteAll: string;
50
+ filter: string;
51
+ };
52
+ filters?: {
53
+ logicOperator: string;
54
+ filters: Array<{
55
+ name: string;
56
+ label: string;
57
+ type: 'uiInput' | 'uiSelect';
58
+ value: string | undefined;
59
+ }>;
60
+ };
61
+
62
+ // Additional features
63
+ tableActionsDropdown?: { items: UiDropdownItemType[]; loading?: boolean };
64
+ tableActionButtons?: Array<{
65
+ text: string;
66
+ id: string;
67
+ color?: string;
68
+ size?: string;
69
+ disabled?: boolean;
70
+ icon?: any;
71
+ loading?: boolean;
72
+ }>;
73
+ noResults?: {
74
+ title: string;
75
+ message: string;
76
+ actions?: Array<{ action: string; text: string }>;
77
+ items?: UiDropdownItemType[];
78
+ select?: UiDropdownItemType;
79
+ };
80
+ hiddenColumns?: number[];
81
+ }
82
+
83
+ const props = withDefaults(defineProps<Props>(), {
84
+ loading: false,
85
+ currentPage: 1,
86
+ totalPages: 1,
87
+ totalItems: 0,
88
+ pageSize: 10,
89
+ orderedBy: '',
90
+ orderDirection: 'asc',
91
+ resetSelected: false,
92
+ filterLiterals: () => ({
93
+ filters: 'Smart Filters',
94
+ contains: 'contains',
95
+ is: 'is',
96
+ and: 'and',
97
+ or: 'or',
98
+ deleteAll: 'Delete All',
99
+ filter: 'Filter'
100
+ })
101
+ });
102
+
103
+ const emit = defineEmits<{
104
+ 'update:currentPage': [page: number];
105
+ 'update:pageSize': [size: number];
106
+ 'update:selected': [items: any[]];
107
+ 'update:orderedBy': [key: string];
108
+ 'update:orderDirection': [direction: 'asc' | 'desc'];
109
+ 'update:resetSelected': [value: boolean];
110
+ 'table-action': [data: { action: string; items: Array<string | number> }];
111
+ 'table-action-selected': [item: any];
112
+ 'table-action-button-clicked': [data: { id: string; text: string }];
113
+ 'smart-filters-sent': [filters: any];
114
+ 'smart-filters-cleared': [];
115
+ 'smart-filter-deleted': [index: number];
116
+ 'custom-emit': [data: any];
117
+ 'modal-action': [data: { modal: string; action: string }];
118
+ 'select-all-items': [];
119
+ 'deselect-all-items': [];
120
+ 'no-results-action': [action: string];
121
+ 'no-results-option-selected': [item: any];
122
+ 'columns-visibility-changed': [data: { index: number; hidden: boolean }];
123
+ 'order-by': [data: { value: string; orderDirection: 'asc' | 'desc' }];
124
+ 'change-page': [page: number];
125
+ 'change-page-size': [size: number | string];
126
+ }>();
127
+
128
+ // State management
129
+ const internalHiddenColumns = ref<number[]>(props.hiddenColumns || []);
130
+ const selectedItemIds = ref<Set<string | number>>(new Set());
131
+
132
+ // Convert columns to uiTable header format
133
+ const tableHeader = computed<UiTableInterface['header']>(() =>
134
+ props.columns.map(col => ({
135
+ name: col.name,
136
+ value: col.value,
137
+ tooltip: col.tooltip
138
+ }))
139
+ );
140
+
141
+ // Convert items to uiTable format
142
+ const tableItems = computed<TableItemType[]>(() =>
143
+ props.items.map((item, index) => ({
144
+ id: item.id || index,
145
+ disabled: item.disabled || false,
146
+ row: props.columns.map(col => {
147
+ const key = col.value || col.name.toLowerCase();
148
+ const value = item[key];
149
+ // Return the value as-is if it's already formatted for uiTable
150
+ if (typeof value === 'object' && value !== null && 'type' in value) {
151
+ return value;
152
+ }
153
+ return String(value ?? '');
154
+ })
155
+ }))
156
+ );
157
+
158
+ // Page size options for uiTable
159
+ const pageSizeOptions = computed(() => [
160
+ { name: '10', value: '10', active: props.pageSize === 10 },
161
+ { name: '25', value: '25', active: props.pageSize === 25 },
162
+ { name: '50', value: '50', active: props.pageSize === 50 },
163
+ { name: '100', value: '100', active: props.pageSize === 100 }
164
+ ]);
165
+
166
+ // Event handlers
167
+ function handleOrderBy(event: { value: string; orderDirection: 'asc' | 'desc' }) {
168
+ emit('update:orderedBy', event.value);
169
+ emit('update:orderDirection', event.orderDirection);
170
+ emit('order-by', event);
171
+ }
172
+
173
+ function handleChangePage(page: number) {
174
+ emit('update:currentPage', page);
175
+ emit('change-page', page);
176
+ }
177
+
178
+ function handleChangePageSize(size: number | string) {
179
+ const numSize = typeof size === 'string' ? Number(size) : size;
180
+ emit('update:pageSize', numSize);
181
+ emit('change-page-size', size);
182
+ emit('update:currentPage', 1);
183
+ }
184
+
185
+ function handleTableAction(data: { action: string; items: Array<string | number> }) {
186
+ selectedItemIds.value = new Set(data.items);
187
+ updateSelectedItems();
188
+ emit('table-action', data);
189
+ resetSelectedItems();
190
+ }
191
+
192
+ function handleTableActionSelected(item: any) {
193
+ emit('table-action-selected', item);
194
+ }
195
+
196
+ function handleTableActionButtonClicked(data: { id: string; text: string }) {
197
+ emit('table-action-button-clicked', data);
198
+ }
199
+
200
+ function handleSmartFiltersSent(filters: any) {
201
+ emit('smart-filters-sent', filters);
202
+ }
203
+
204
+ function handleSmartFiltersCleared() {
205
+ emit('smart-filters-cleared');
206
+ }
207
+
208
+ function handleSmartFilterDeleted(index: number) {
209
+ emit('smart-filter-deleted', index);
210
+ }
211
+
212
+ function handleCustomEmit(data: any) {
213
+ emit('custom-emit', data);
214
+ }
215
+
216
+ function handleColumnsVisibilityChanged(event: { index: number; hidden: boolean }) {
217
+ if (event.hidden) {
218
+ if (!internalHiddenColumns.value.includes(event.index)) {
219
+ internalHiddenColumns.value.push(event.index);
220
+ }
221
+ } else {
222
+ const index = internalHiddenColumns.value.indexOf(event.index);
223
+ if (index > -1) {
224
+ internalHiddenColumns.value.splice(index, 1);
225
+ }
226
+ }
227
+ emit('columns-visibility-changed', event);
228
+ }
229
+
230
+ function handleModalAction(data: { modal: string; action: string }) {
231
+ emit('modal-action', data);
232
+ }
233
+
234
+ function handleSelectAllItems() {
235
+ selectedItemIds.value = new Set(props.items.map(item => item.id));
236
+ updateSelectedItems();
237
+ emit('select-all-items');
238
+ }
239
+
240
+ function handleDeselectAllItems() {
241
+ selectedItemIds.value.clear();
242
+ updateSelectedItems();
243
+ emit('deselect-all-items');
244
+ }
245
+
246
+ function handleNoResultsAction(action: string) {
247
+ emit('no-results-action', action);
248
+ }
249
+
250
+ function handleNoResultsOptionSelected(item: any) {
251
+ emit('no-results-option-selected', item);
252
+ }
253
+
254
+ function handleSelectedItemsDeleted() {
255
+ emit('update:resetSelected', false);
256
+ }
257
+
258
+ function updateSelectedItems() {
259
+ const selected = props.items.filter(item => selectedItemIds.value.has(item.id));
260
+ emit('update:selected', selected);
261
+ }
262
+
263
+ // Expose methods for parent components
264
+ defineExpose({
265
+ clearSelection: () => {
266
+ emit('update:resetSelected', true);
267
+ selectedItemIds.value.clear();
268
+ updateSelectedItems();
269
+ },
270
+ getHiddenColumns: () => internalHiddenColumns.value,
271
+ getSelectedItems: () => Array.from(selectedItemIds.value)
272
+ });
273
+ </script>
274
+
275
+ <template>
276
+ <uiTable
277
+ :loading="loading"
278
+ :header="tableHeader"
279
+ :items="tableItems"
280
+ :ordered-by="orderedBy"
281
+ :order-direction="orderDirection"
282
+ :actions="(actions as any)"
283
+ :hidden-columns="internalHiddenColumns"
284
+ :reset-selected="resetSelected"
285
+ :select-all-items-modal="(selectAllItemsModal as any)"
286
+ :smart-filter-categories="smartFilterCategories"
287
+ :filter-literals="filterLiterals"
288
+ :filters="filters"
289
+ :table-actions-dropdown="tableActionsDropdown"
290
+ :table-action-buttons="tableActionButtons"
291
+ :pagination-current="currentPage"
292
+ :pagination-total="totalPages"
293
+ :pagination-total-items="totalItems"
294
+ :page-size-options="pageSizeOptions"
295
+ :current-page-size="pageSize"
296
+ :no-results="noResults"
297
+ @order-by="handleOrderBy"
298
+ @table-action="handleTableAction"
299
+ @table-action-selected="handleTableActionSelected"
300
+ @table-action-button-clicked="handleTableActionButtonClicked"
301
+ @smart-filters-sent="handleSmartFiltersSent"
302
+ @smart-filters-cleared="handleSmartFiltersCleared"
303
+ @smart-filter-deleted="handleSmartFilterDeleted"
304
+ @change-page="handleChangePage"
305
+ @change-page-size="handleChangePageSize"
306
+ @custom-emit="handleCustomEmit"
307
+ @columns-visibility-changed="handleColumnsVisibilityChanged"
308
+ @selected-items-deleted="handleSelectedItemsDeleted"
309
+ @modal-action="handleModalAction"
310
+ @select-all-items="handleSelectAllItems"
311
+ @deselect-all-items="handleDeselectAllItems"
312
+ @no-results-action="handleNoResultsAction"
313
+ @no-results-option-selected="handleNoResultsOptionSelected"
314
+ />
315
+ </template>
316
+
317
+ <style scoped>
318
+ .table-component {
319
+ width: 100%;
320
+ }
321
+ </style>
@@ -0,0 +1,5 @@
1
+ export { default as Table } from './Table.vue';
2
+ export { default as DataList } from './DataList.vue';
3
+ export { default as SearchableSelect } from './SearchableSelect.vue';
4
+ export { default as Chart } from './Chart.vue';
5
+ export type { ChartSeries, ChartOptions, ChartDateRange, ChartAnnotation, NpsLiterals } from './Chart.vue';
@@ -0,0 +1,81 @@
1
+ # BrandCard Component
2
+
3
+ A card component for displaying brand information with status indicators. Wraps `@hotelinking/ui`'s `uiCard` component.
4
+
5
+ ## Features
6
+
7
+ - **Status indicators**: Visual tags for brand status
8
+ - **Logo display**: Brand logo support
9
+ - **Click handling**: Emits click events
10
+ - **Loading state**: Skeleton loading indicator
11
+
12
+ ## Import
13
+
14
+ ```typescript
15
+ import { BrandCard } from '@htlkg/components';
16
+ // or
17
+ import { BrandCard } from '@htlkg/components/domain';
18
+ ```
19
+
20
+ ## Props
21
+
22
+ | Prop | Type | Default | Description |
23
+ |------|------|---------|-------------|
24
+ | `brand` | `Brand` | required | Brand information |
25
+ | `showStatus` | `boolean` | `true` | Display status tag |
26
+ | `loading` | `boolean` | `false` | Show loading state |
27
+
28
+ ### Brand Interface
29
+
30
+ ```typescript
31
+ interface Brand {
32
+ id: string;
33
+ name: string;
34
+ logo?: string;
35
+ status?: 'active' | 'inactive' | 'maintenance' | 'suspended';
36
+ timezone?: string;
37
+ }
38
+ ```
39
+
40
+ ## Events
41
+
42
+ | Event | Payload | Description |
43
+ |-------|---------|-------------|
44
+ | `click` | `Brand` | Brand card clicked |
45
+
46
+ ## Exposed Methods
47
+
48
+ | Method | Description |
49
+ |--------|-------------|
50
+ | `getBrand()` | Get brand object |
51
+
52
+ ## Usage Examples
53
+
54
+ ```vue
55
+ <script setup>
56
+ import { BrandCard } from '@htlkg/components';
57
+
58
+ const brand = {
59
+ id: '1',
60
+ name: 'Hotel Paradise',
61
+ logo: 'https://example.com/logo.png',
62
+ status: 'active',
63
+ timezone: 'America/New_York'
64
+ };
65
+
66
+ const handleClick = (brand) => {
67
+ console.log('Selected brand:', brand);
68
+ };
69
+ </script>
70
+
71
+ <template>
72
+ <BrandCard
73
+ :brand="brand"
74
+ @click="handleClick"
75
+ />
76
+ </template>
77
+ ```
78
+
79
+ ## Demo
80
+
81
+ See the [BrandCard demo page](/components/brand-card) for interactive examples.
@@ -0,0 +1,63 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import { uiCard } from '@hotelinking/ui';
4
+
5
+ interface Brand {
6
+ id: string;
7
+ name: string;
8
+ logo?: string;
9
+ status?: 'active' | 'inactive' | 'maintenance' | 'suspended';
10
+ timezone?: string;
11
+ }
12
+
13
+ interface Props {
14
+ brand: Brand;
15
+ showStatus?: boolean;
16
+ loading?: boolean;
17
+ }
18
+
19
+ const props = withDefaults(defineProps<Props>(), {
20
+ showStatus: true,
21
+ loading: false
22
+ });
23
+
24
+ const emit = defineEmits<{
25
+ 'click': [brand: Brand];
26
+ }>();
27
+
28
+ // Convert status to tag format
29
+ const tags = computed(() => {
30
+ if (!props.showStatus || !props.brand.status) return [];
31
+
32
+ const statusMap = {
33
+ active: { name: 'Active', color: 'green' as const },
34
+ inactive: { name: 'Inactive', color: 'gray' as const },
35
+ maintenance: { name: 'Maintenance', color: 'yellow' as const },
36
+ suspended: { name: 'Suspended', color: 'red' as const }
37
+ };
38
+
39
+ return [statusMap[props.brand.status]];
40
+ });
41
+
42
+ // Handle card selection
43
+ function handleCardSelected(data: any) {
44
+ emit('click', props.brand);
45
+ }
46
+
47
+ // Expose methods for parent components
48
+ defineExpose({
49
+ getBrand: () => props.brand
50
+ });
51
+ </script>
52
+
53
+ <template>
54
+ <uiCard
55
+ :id="brand.id"
56
+ :name="brand.name"
57
+ :type="brand.timezone || 'Brand'"
58
+ :logo="brand.logo || ''"
59
+ :tags="tags"
60
+ :loading="loading"
61
+ @card-selected="handleCardSelected"
62
+ />
63
+ </template>
@@ -0,0 +1,84 @@
1
+ # BrandSelector Component
2
+
3
+ A grid-based brand selection component with v-model support.
4
+
5
+ ## Features
6
+
7
+ - **v-model support**: Two-way binding for selected brand
8
+ - **Grid layout**: Responsive grid display
9
+ - **Loading state**: Loading indicator
10
+ - **Exposed methods**: Programmatic selection control
11
+
12
+ ## Import
13
+
14
+ ```typescript
15
+ import { BrandSelector } from '@htlkg/components';
16
+ // or
17
+ import { BrandSelector } from '@htlkg/components/domain';
18
+ ```
19
+
20
+ ## Props
21
+
22
+ | Prop | Type | Default | Description |
23
+ |------|------|---------|-------------|
24
+ | `modelValue` | `Brand \| null` | `null` | Selected brand (v-model) |
25
+ | `brands` | `Brand[]` | `[]` | Available brands |
26
+ | `loading` | `boolean` | `false` | Show loading state |
27
+
28
+ ### Brand Interface
29
+
30
+ ```typescript
31
+ interface Brand {
32
+ id: string;
33
+ name: string;
34
+ logo?: string;
35
+ type?: string;
36
+ }
37
+ ```
38
+
39
+ ## Events
40
+
41
+ | Event | Payload | Description |
42
+ |-------|---------|-------------|
43
+ | `update:modelValue` | `Brand \| null` | Selection changed |
44
+ | `change` | `Brand \| null` | Selection changed |
45
+
46
+ ## Exposed Methods
47
+
48
+ | Method | Description |
49
+ |--------|-------------|
50
+ | `getSelected()` | Get selected brand |
51
+ | `clearSelection()` | Clear selection |
52
+
53
+ ## Usage Examples
54
+
55
+ ```vue
56
+ <script setup>
57
+ import { ref } from 'vue';
58
+ import { BrandSelector } from '@htlkg/components';
59
+
60
+ const selectedBrand = ref(null);
61
+
62
+ const brands = [
63
+ { id: '1', name: 'Hotel Paradise', logo: '/logo1.png' },
64
+ { id: '2', name: 'Resort Oasis', logo: '/logo2.png' },
65
+ { id: '3', name: 'City Inn', logo: '/logo3.png' }
66
+ ];
67
+
68
+ const handleChange = (brand) => {
69
+ console.log('Selected:', brand);
70
+ };
71
+ </script>
72
+
73
+ <template>
74
+ <BrandSelector
75
+ v-model="selectedBrand"
76
+ :brands="brands"
77
+ @change="handleChange"
78
+ />
79
+ </template>
80
+ ```
81
+
82
+ ## Demo
83
+
84
+ See the [BrandSelector demo page](/components/brand-selector) for interactive examples.
@@ -0,0 +1,65 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import { uiCard } from '@hotelinking/ui';
4
+
5
+ interface Brand {
6
+ id: string;
7
+ name: string;
8
+ logo?: string;
9
+ type?: string;
10
+ }
11
+
12
+ interface Props {
13
+ modelValue?: Brand | null;
14
+ brands?: Brand[];
15
+ loading?: boolean;
16
+ }
17
+
18
+ const props = withDefaults(defineProps<Props>(), {
19
+ modelValue: null,
20
+ brands: () => [],
21
+ loading: false
22
+ });
23
+
24
+ const emit = defineEmits<{
25
+ 'update:modelValue': [value: Brand | null];
26
+ 'change': [value: Brand | null];
27
+ }>();
28
+
29
+ function selectBrand(data: any) {
30
+ const brand = props.brands.find(b => b.id === data.id);
31
+ if (brand) {
32
+ emit('update:modelValue', brand);
33
+ emit('change', brand);
34
+ }
35
+ }
36
+
37
+ // Expose methods for parent components
38
+ defineExpose({
39
+ getSelected: () => props.modelValue,
40
+ clearSelection: () => {
41
+ emit('update:modelValue', null);
42
+ emit('change', null);
43
+ }
44
+ });
45
+ </script>
46
+
47
+ <template>
48
+ <div class="w-full">
49
+ <div v-if="loading" class="text-center py-4 text-gray-500">
50
+ Loading brands...
51
+ </div>
52
+ <div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
53
+ <uiCard
54
+ v-for="brand in brands"
55
+ :key="brand.id"
56
+ :id="brand.id"
57
+ :name="brand.name"
58
+ :type="brand.type || 'Brand'"
59
+ :logo="brand.logo || ''"
60
+ :loading="false"
61
+ @card-selected="selectBrand"
62
+ />
63
+ </div>
64
+ </div>
65
+ </template>
@@ -0,0 +1,60 @@
1
+ # ProductBadge Component
2
+
3
+ A badge component for displaying product information with status indicators. Wraps `@hotelinking/ui`'s `uiTag` component.
4
+
5
+ ## Features
6
+
7
+ - **Status colors**: Visual indicators for enabled/disabled/pending states
8
+ - **Version display**: Optional version number
9
+ - **Loading state**: Skeleton loading indicator
10
+
11
+ ## Import
12
+
13
+ ```typescript
14
+ import { ProductBadge } from '@htlkg/components';
15
+ // or
16
+ import { ProductBadge } from '@htlkg/components/domain';
17
+ ```
18
+
19
+ ## Props
20
+
21
+ | Prop | Type | Default | Description |
22
+ |------|------|---------|-------------|
23
+ | `product` | `Product` | required | Product information |
24
+ | `showVersion` | `boolean` | `false` | Display version number |
25
+ | `loading` | `boolean` | `false` | Show loading state |
26
+
27
+ ### Product Interface
28
+
29
+ ```typescript
30
+ interface Product {
31
+ name: string;
32
+ status?: 'enabled' | 'disabled' | 'pending';
33
+ version?: string;
34
+ }
35
+ ```
36
+
37
+ ## Usage Examples
38
+
39
+ ```vue
40
+ <script setup>
41
+ import { ProductBadge } from '@htlkg/components';
42
+
43
+ const product = {
44
+ name: 'Booking Engine',
45
+ status: 'enabled',
46
+ version: '2.1.0'
47
+ };
48
+ </script>
49
+
50
+ <template>
51
+ <ProductBadge
52
+ :product="product"
53
+ :showVersion="true"
54
+ />
55
+ </template>
56
+ ```
57
+
58
+ ## Demo
59
+
60
+ See the [ProductBadge demo page](/components/product-badge) for interactive examples.