@htlkg/components 0.0.1 → 0.0.3

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.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @htlkg/components
2
+
3
+ Vue 3 components with reactive state management for Hotelinking applications.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @htlkg/components
9
+ ```
10
+
11
+ ## Modules
12
+
13
+ ### [Overlays](src/overlays/overlays.md)
14
+ Modal dialogs, notifications, alerts, and drawers.
15
+
16
+ ### [Navigation](src/navigation/navigation.md)
17
+ Breadcrumbs, stepper, and tabs components.
18
+
19
+ ### [Data](src/data/data.md)
20
+ Table, DataList, SearchableSelect, and Chart components.
21
+
22
+ ### [Forms](src/forms/forms.md)
23
+ JsonSchemaForm and DateRange components.
24
+
25
+ ### [Domain](src/domain/domain.md)
26
+ Domain-specific components: BrandCard, BrandSelector, ProductBadge, UserAvatar.
27
+
28
+ ### [Composables](src/composables/composables.md)
29
+ Vue composables for component state management.
30
+
31
+ ### [Stores](src/stores/stores.md)
32
+ Pinia stores for global component state.
33
+
34
+ ## Quick Start
35
+
36
+ ```typescript
37
+ // Import individual components
38
+ import { Alert, Modal, Table } from '@htlkg/components';
39
+
40
+ // Import from category
41
+ import { Alert, Modal } from '@htlkg/components/overlays';
42
+ import { Breadcrumbs, Stepper } from '@htlkg/components/navigation';
43
+ import { Table, DataList } from '@htlkg/components/data';
44
+ ```
45
+
46
+ ## Features
47
+
48
+ - v-model support for two-way binding
49
+ - TypeScript support with full type definitions
50
+ - Exposed methods for programmatic control
51
+ - Consistent API across all components
52
+ - Wraps @hotelinking/ui design system
@@ -1,20 +1,74 @@
1
1
  // src/composables/useTable.ts
2
- import { ref, computed } from "vue";
2
+ import { ref, computed, unref } from "vue";
3
3
  function useTable(options) {
4
+ const idKey = options.idKey ?? "id";
5
+ const items = computed(() => {
6
+ try {
7
+ const unwrapped = unref(options.items);
8
+ if (!unwrapped) return [];
9
+ if (!Array.isArray(unwrapped)) {
10
+ console.warn("useTable: items is not an array", unwrapped);
11
+ return [];
12
+ }
13
+ return unwrapped;
14
+ } catch (error) {
15
+ console.error("useTable: Error unwrapping items", error);
16
+ return [];
17
+ }
18
+ });
4
19
  const currentPage = ref(1);
5
20
  const pageSize = ref(options.pageSize ?? 10);
6
21
  const sortKey = ref(options.sortKey ?? "");
7
22
  const sortOrder = ref(options.sortOrder ?? "asc");
8
23
  const selectedItems = ref([]);
24
+ const selectedItemIds = ref(/* @__PURE__ */ new Set());
25
+ const resetSelected = ref(false);
26
+ const activeFilters = ref([]);
27
+ const hiddenColumns = ref([]);
28
+ const filteredItems = computed(() => {
29
+ const filters = Array.isArray(activeFilters.value) ? activeFilters.value : [];
30
+ if (filters.length === 0) return items.value;
31
+ return items.value.filter((item) => {
32
+ return filters.every((filter) => {
33
+ const { category, operator, value } = filter;
34
+ if (value === void 0 || value === null || value === "") return true;
35
+ const itemValue = item[category];
36
+ switch (operator) {
37
+ case "contains":
38
+ return String(itemValue).toLowerCase().includes(String(value).toLowerCase());
39
+ case "is":
40
+ case "=":
41
+ return itemValue === value;
42
+ case ">":
43
+ case "greater":
44
+ return Number(itemValue) > Number(value);
45
+ case "<":
46
+ case "less":
47
+ return Number(itemValue) < Number(value);
48
+ case ">=":
49
+ case "greaterOrEqual":
50
+ return Number(itemValue) >= Number(value);
51
+ case "<=":
52
+ case "lessOrEqual":
53
+ return Number(itemValue) <= Number(value);
54
+ default:
55
+ return String(itemValue).toLowerCase().includes(String(value).toLowerCase());
56
+ }
57
+ });
58
+ });
59
+ });
9
60
  const sortedItems = computed(() => {
10
- if (!sortKey.value) return options.items;
11
- return [...options.items].sort((a, b) => {
61
+ if (!sortKey.value) return filteredItems.value;
62
+ return [...filteredItems.value].sort((a, b) => {
12
63
  const aVal = a[sortKey.value];
13
64
  const bVal = b[sortKey.value];
14
65
  const order = sortOrder.value === "asc" ? 1 : -1;
15
66
  if (aVal === bVal) return 0;
16
67
  if (aVal == null) return 1;
17
68
  if (bVal == null) return -1;
69
+ if (typeof aVal === "string" && typeof bVal === "string") {
70
+ return aVal.localeCompare(bVal) * order;
71
+ }
18
72
  return aVal > bVal ? order : -order;
19
73
  });
20
74
  });
@@ -24,8 +78,16 @@ function useTable(options) {
24
78
  return sortedItems.value.slice(start, end);
25
79
  });
26
80
  const totalPages = computed(
27
- () => Math.ceil(options.items.length / pageSize.value)
81
+ () => Math.ceil(sortedItems.value.length / pageSize.value)
28
82
  );
83
+ const totalItems = computed(() => sortedItems.value.length);
84
+ const allSelected = computed(() => {
85
+ if (items.value.length === 0) return false;
86
+ return selectedItemIds.value.size === items.value.length;
87
+ });
88
+ const someSelected = computed(() => {
89
+ return selectedItemIds.value.size > 0 && !allSelected.value;
90
+ });
29
91
  function setPage(page) {
30
92
  if (page >= 1 && page <= totalPages.value) {
31
93
  currentPage.value = page;
@@ -35,6 +97,13 @@ function useTable(options) {
35
97
  pageSize.value = size;
36
98
  currentPage.value = 1;
37
99
  }
100
+ function handlePageChange(page) {
101
+ setPage(page);
102
+ }
103
+ function handlePageSizeChange(size) {
104
+ const numSize = typeof size === "string" ? parseInt(size) : size;
105
+ setPageSize(numSize);
106
+ }
38
107
  function setSorting(key, order = "asc") {
39
108
  if (sortKey.value === key) {
40
109
  sortOrder.value = sortOrder.value === "asc" ? "desc" : "asc";
@@ -43,47 +112,180 @@ function useTable(options) {
43
112
  sortOrder.value = order;
44
113
  }
45
114
  }
115
+ function handleOrderBy(event) {
116
+ sortKey.value = event.value;
117
+ sortOrder.value = event.orderDirection;
118
+ }
119
+ function getItemId(item) {
120
+ return item[idKey];
121
+ }
46
122
  function selectItem(item) {
47
- if (!isSelected(item)) {
123
+ const id = getItemId(item);
124
+ if (!selectedItemIds.value.has(id)) {
125
+ selectedItemIds.value.add(id);
48
126
  selectedItems.value.push(item);
49
127
  }
50
128
  }
51
129
  function deselectItem(item) {
52
- const index = selectedItems.value.findIndex(
53
- (i) => JSON.stringify(i) === JSON.stringify(item)
54
- );
55
- if (index !== -1) {
56
- selectedItems.value.splice(index, 1);
130
+ const id = getItemId(item);
131
+ if (selectedItemIds.value.has(id)) {
132
+ selectedItemIds.value.delete(id);
133
+ const index = selectedItems.value.findIndex((i) => getItemId(i) === id);
134
+ if (index !== -1) {
135
+ selectedItems.value.splice(index, 1);
136
+ }
57
137
  }
58
138
  }
59
139
  function selectAll() {
60
- selectedItems.value = [...options.items];
140
+ selectedItemIds.value.clear();
141
+ selectedItems.value.length = 0;
142
+ items.value.forEach((item) => {
143
+ const id = getItemId(item);
144
+ selectedItemIds.value.add(id);
145
+ selectedItems.value.push(item);
146
+ });
147
+ }
148
+ function selectAllOnPage() {
149
+ paginatedItems.value.forEach((item) => {
150
+ selectItem(item);
151
+ });
61
152
  }
62
153
  function clearSelection() {
63
- selectedItems.value = [];
154
+ selectedItemIds.value.clear();
155
+ selectedItems.value.length = 0;
156
+ resetSelected.value = true;
157
+ setTimeout(() => {
158
+ resetSelected.value = false;
159
+ }, 100);
64
160
  }
65
161
  function isSelected(item) {
66
- return selectedItems.value.some(
67
- (i) => JSON.stringify(i) === JSON.stringify(item)
68
- );
162
+ const id = getItemId(item);
163
+ return selectedItemIds.value.has(id);
164
+ }
165
+ function applyFilters(filters) {
166
+ if (Array.isArray(filters)) {
167
+ activeFilters.value = filters;
168
+ } else if (filters && typeof filters === "object") {
169
+ if ("filters" in filters && Array.isArray(filters.filters)) {
170
+ activeFilters.value = filters.filters;
171
+ } else {
172
+ activeFilters.value = Object.entries(filters).filter(([_, value]) => value !== void 0 && value !== null && value !== "").map(([key, value]) => ({
173
+ category: key,
174
+ operator: "contains",
175
+ value
176
+ }));
177
+ }
178
+ } else {
179
+ activeFilters.value = [];
180
+ }
181
+ currentPage.value = 1;
182
+ }
183
+ function clearFilters() {
184
+ activeFilters.value = [];
185
+ currentPage.value = 1;
186
+ }
187
+ function removeFilter(index) {
188
+ if (index >= 0 && index < activeFilters.value.length) {
189
+ activeFilters.value.splice(index, 1);
190
+ currentPage.value = 1;
191
+ }
192
+ }
193
+ function handleSmartFiltersApplied(filters) {
194
+ applyFilters(filters);
195
+ }
196
+ function handleSmartFiltersCleared() {
197
+ clearFilters();
198
+ }
199
+ function handleSmartFilterDeleted(index) {
200
+ removeFilter(index);
201
+ }
202
+ function toggleColumn(index) {
203
+ const hiddenIndex = hiddenColumns.value.indexOf(index);
204
+ if (hiddenIndex > -1) {
205
+ hiddenColumns.value.splice(hiddenIndex, 1);
206
+ } else {
207
+ hiddenColumns.value.push(index);
208
+ }
209
+ }
210
+ function showColumn(index) {
211
+ const hiddenIndex = hiddenColumns.value.indexOf(index);
212
+ if (hiddenIndex > -1) {
213
+ hiddenColumns.value.splice(hiddenIndex, 1);
214
+ }
215
+ }
216
+ function hideColumn(index) {
217
+ if (!hiddenColumns.value.includes(index)) {
218
+ hiddenColumns.value.push(index);
219
+ }
220
+ }
221
+ function handleColumnsVisibilityChanged(event) {
222
+ if (event.hidden) {
223
+ hideColumn(event.index);
224
+ } else {
225
+ showColumn(event.index);
226
+ }
227
+ }
228
+ function handleModalAction(event) {
229
+ if (event.modal === "selectAllItemsModal" || event.modal.includes("selectAll")) {
230
+ if (event.action === "selectAll") {
231
+ selectAll();
232
+ } else if (event.action === "close") {
233
+ selectAllOnPage();
234
+ }
235
+ }
69
236
  }
70
237
  return {
238
+ // Pagination
71
239
  currentPage,
72
240
  pageSize,
241
+ totalPages,
242
+ totalItems,
243
+ paginatedItems,
244
+ // Sorting
73
245
  sortKey,
74
246
  sortOrder,
75
- selectedItems,
76
- paginatedItems,
77
- totalPages,
78
247
  sortedItems,
248
+ // Selection
249
+ selectedItems,
250
+ selectedItemIds,
251
+ allSelected,
252
+ someSelected,
253
+ // Filtering
254
+ activeFilters,
255
+ filteredItems,
256
+ // Column visibility
257
+ hiddenColumns,
258
+ // Reset state
259
+ resetSelected,
260
+ // Methods - Pagination
79
261
  setPage,
80
262
  setPageSize,
263
+ handlePageChange,
264
+ handlePageSizeChange,
265
+ // Methods - Sorting
81
266
  setSorting,
267
+ handleOrderBy,
268
+ // Methods - Selection
82
269
  selectItem,
83
270
  deselectItem,
84
271
  selectAll,
272
+ selectAllOnPage,
85
273
  clearSelection,
86
- isSelected
274
+ isSelected,
275
+ // Methods - Filtering
276
+ applyFilters,
277
+ clearFilters,
278
+ removeFilter,
279
+ handleSmartFiltersApplied,
280
+ handleSmartFiltersCleared,
281
+ handleSmartFilterDeleted,
282
+ // Methods - Column visibility
283
+ toggleColumn,
284
+ showColumn,
285
+ hideColumn,
286
+ handleColumnsVisibilityChanged,
287
+ // Methods - Modal actions
288
+ handleModalAction
87
289
  };
88
290
  }
89
291
 
@@ -377,12 +579,189 @@ function useNotifications() {
377
579
  error
378
580
  };
379
581
  }
582
+
583
+ // src/composables/usePageContext.ts
584
+ import { computed as computed4, inject } from "vue";
585
+
586
+ // ../../node_modules/.pnpm/@nanostores+vue@1.0.1_@nanostores+logger@1.0.0_nanostores@1.1.0__nanostores@1.1.0_vue@3.5.25_typescript@5.9.3_/node_modules/@nanostores/vue/use-store/index.js
587
+ import {
588
+ getCurrentInstance,
589
+ getCurrentScope,
590
+ onScopeDispose,
591
+ readonly,
592
+ shallowRef
593
+ } from "vue";
594
+ function registerStore(store) {
595
+ let instance = getCurrentInstance();
596
+ if (instance && instance.proxy) {
597
+ let vm = instance.proxy;
598
+ let cache = "_nanostores" in vm ? vm._nanostores : vm._nanostores = [];
599
+ cache.push(store);
600
+ }
601
+ }
602
+ function useStore(store) {
603
+ let state = shallowRef();
604
+ let unsubscribe = store.subscribe((value) => {
605
+ state.value = value;
606
+ });
607
+ getCurrentScope() && onScopeDispose(unsubscribe);
608
+ if (process.env.NODE_ENV !== "production") {
609
+ registerStore(store);
610
+ return readonly(state);
611
+ }
612
+ return state;
613
+ }
614
+
615
+ // ../../node_modules/.pnpm/nanostores@1.1.0/node_modules/nanostores/clean-stores/index.js
616
+ var clean = /* @__PURE__ */ Symbol("clean");
617
+
618
+ // ../../node_modules/.pnpm/nanostores@1.1.0/node_modules/nanostores/atom/index.js
619
+ var listenerQueue = [];
620
+ var lqIndex = 0;
621
+ var QUEUE_ITEMS_PER_LISTENER = 4;
622
+ var epoch = 0;
623
+ var atom = /* @__NO_SIDE_EFFECTS__ */ (initialValue) => {
624
+ let listeners = [];
625
+ let $atom = {
626
+ get() {
627
+ if (!$atom.lc) {
628
+ $atom.listen(() => {
629
+ })();
630
+ }
631
+ return $atom.value;
632
+ },
633
+ lc: 0,
634
+ listen(listener) {
635
+ $atom.lc = listeners.push(listener);
636
+ return () => {
637
+ for (let i = lqIndex + QUEUE_ITEMS_PER_LISTENER; i < listenerQueue.length; ) {
638
+ if (listenerQueue[i] === listener) {
639
+ listenerQueue.splice(i, QUEUE_ITEMS_PER_LISTENER);
640
+ } else {
641
+ i += QUEUE_ITEMS_PER_LISTENER;
642
+ }
643
+ }
644
+ let index = listeners.indexOf(listener);
645
+ if (~index) {
646
+ listeners.splice(index, 1);
647
+ if (!--$atom.lc) $atom.off();
648
+ }
649
+ };
650
+ },
651
+ notify(oldValue, changedKey) {
652
+ epoch++;
653
+ let runListenerQueue = !listenerQueue.length;
654
+ for (let listener of listeners) {
655
+ listenerQueue.push(listener, $atom.value, oldValue, changedKey);
656
+ }
657
+ if (runListenerQueue) {
658
+ for (lqIndex = 0; lqIndex < listenerQueue.length; lqIndex += QUEUE_ITEMS_PER_LISTENER) {
659
+ listenerQueue[lqIndex](
660
+ listenerQueue[lqIndex + 1],
661
+ listenerQueue[lqIndex + 2],
662
+ listenerQueue[lqIndex + 3]
663
+ );
664
+ }
665
+ listenerQueue.length = 0;
666
+ }
667
+ },
668
+ /* It will be called on last listener unsubscribing.
669
+ We will redefine it in onMount and onStop. */
670
+ off() {
671
+ },
672
+ set(newValue) {
673
+ let oldValue = $atom.value;
674
+ if (oldValue !== newValue) {
675
+ $atom.value = newValue;
676
+ $atom.notify(oldValue);
677
+ }
678
+ },
679
+ subscribe(listener) {
680
+ let unbind = $atom.listen(listener);
681
+ listener($atom.value);
682
+ return unbind;
683
+ },
684
+ value: initialValue
685
+ };
686
+ if (process.env.NODE_ENV !== "production") {
687
+ $atom[clean] = () => {
688
+ listeners = [];
689
+ $atom.lc = 0;
690
+ $atom.off();
691
+ };
692
+ }
693
+ return $atom;
694
+ };
695
+
696
+ // src/composables/usePageContext.ts
697
+ import { routes } from "@htlkg/core";
698
+ var PAGE_CONTEXT_KEY = /* @__PURE__ */ Symbol("pageContext");
699
+ var $user = atom(null);
700
+ var $currentBrand = atom(null);
701
+ function setUser(user) {
702
+ $user.set(user);
703
+ }
704
+ function setCurrentBrand(brand) {
705
+ $currentBrand.set(brand);
706
+ }
707
+ function usePageContext() {
708
+ const injected = inject(PAGE_CONTEXT_KEY, null);
709
+ if (injected) {
710
+ return computed4(() => injected);
711
+ }
712
+ const user = useStore($user);
713
+ const brand = useStore($currentBrand);
714
+ return computed4(() => ({
715
+ user: user.value,
716
+ brand: brand.value ?? void 0,
717
+ brandId: brand.value?.id,
718
+ isAdmin: user.value?.isAdmin ?? false,
719
+ isSuperAdmin: user.value?.isSuperAdmin ?? false,
720
+ routes
721
+ }));
722
+ }
723
+ function useHasAccessToBrand(brandId) {
724
+ const context = usePageContext();
725
+ return computed4(() => {
726
+ const user = context.value.user;
727
+ if (!user) return false;
728
+ if (user.isAdmin || user.isSuperAdmin) return true;
729
+ return user.brandIds?.includes(brandId) ?? false;
730
+ });
731
+ }
732
+ function useHasAccessToAccount(accountId) {
733
+ const context = usePageContext();
734
+ return computed4(() => {
735
+ const user = context.value.user;
736
+ if (!user) return false;
737
+ if (user.isAdmin || user.isSuperAdmin) return true;
738
+ return user.accountIds?.includes(accountId) ?? false;
739
+ });
740
+ }
741
+ function useUserRoles() {
742
+ const context = usePageContext();
743
+ return computed4(() => context.value.user?.roles ?? []);
744
+ }
745
+ function useHasRole(role) {
746
+ const roles = useUserRoles();
747
+ return computed4(() => roles.value.includes(role));
748
+ }
380
749
  export {
750
+ $currentBrand,
751
+ $user,
752
+ PAGE_CONTEXT_KEY,
753
+ setCurrentBrand,
754
+ setUser,
381
755
  useForm,
382
756
  useFormValidation,
757
+ useHasAccessToAccount,
758
+ useHasAccessToBrand,
759
+ useHasRole,
383
760
  useModal,
384
761
  useNotifications,
762
+ usePageContext,
385
763
  useTable,
386
- useTabs
764
+ useTabs,
765
+ useUserRoles
387
766
  };
388
767
  //# sourceMappingURL=index.js.map