@yh-ui/hooks 0.1.16 → 0.1.21

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.
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useAiPersistence = useAiPersistence;
7
+ var _vue = require("vue");
8
+ var _storage = require("../storage/index.cjs");
9
+ function useAiPersistence(options = {}) {
10
+ const {
11
+ storage = new _storage.IndexedDBAdapter(),
12
+ conversationKey = "ai-conversations",
13
+ autoSave = true
14
+ } = options;
15
+ const conversations = (0, _vue.ref)([]);
16
+ const currentConversationId = (0, _vue.ref)(null);
17
+ const isLoading = (0, _vue.ref)(false);
18
+ const isSaving = (0, _vue.ref)(false);
19
+ const error = (0, _vue.ref)(null);
20
+ const loadConversations = async () => {
21
+ isLoading.value = true;
22
+ error.value = null;
23
+ try {
24
+ const data = await storage.get(conversationKey);
25
+ conversations.value = data || [];
26
+ if (conversations.value.length > 0) {
27
+ const lastConversation = conversations.value[conversations.value.length - 1];
28
+ currentConversationId.value = lastConversation.id;
29
+ }
30
+ } catch (e) {
31
+ error.value = e instanceof Error ? e : new Error("Failed to load conversations");
32
+ } finally {
33
+ isLoading.value = false;
34
+ }
35
+ };
36
+ const saveConversations = async () => {
37
+ isSaving.value = true;
38
+ error.value = null;
39
+ try {
40
+ await storage.set(conversationKey, conversations.value);
41
+ } catch (e) {
42
+ error.value = e instanceof Error ? e : new Error("Failed to save conversations");
43
+ } finally {
44
+ isSaving.value = false;
45
+ }
46
+ };
47
+ const createConversation = (title = "New Conversation") => {
48
+ const now = Date.now();
49
+ const conversation = {
50
+ id: `conv-${now}`,
51
+ title,
52
+ messages: [],
53
+ createdAt: now,
54
+ updatedAt: now
55
+ };
56
+ conversations.value.push(conversation);
57
+ currentConversationId.value = conversation.id;
58
+ if (autoSave) {
59
+ saveConversations();
60
+ }
61
+ return conversation;
62
+ };
63
+ const deleteConversation = id => {
64
+ const index = conversations.value.findIndex(c => c.id === id);
65
+ if (index !== -1) {
66
+ conversations.value.splice(index, 1);
67
+ if (currentConversationId.value === id) {
68
+ currentConversationId.value = conversations.value.length > 0 ? conversations.value[conversations.value.length - 1].id : null;
69
+ }
70
+ if (autoSave) {
71
+ saveConversations();
72
+ }
73
+ }
74
+ };
75
+ const getCurrentConversation = () => {
76
+ return conversations.value.find(c => c.id === currentConversationId.value);
77
+ };
78
+ const addMessage = message => {
79
+ const conversation = getCurrentConversation();
80
+ if (!conversation) return;
81
+ const newMessage = {
82
+ ...message,
83
+ id: `msg-${Date.now()}`,
84
+ timestamp: Date.now()
85
+ };
86
+ conversation.messages.push(newMessage);
87
+ conversation.updatedAt = Date.now();
88
+ if (autoSave) {
89
+ saveConversations();
90
+ }
91
+ return newMessage;
92
+ };
93
+ const updateMessage = (messageId, updates) => {
94
+ const conversation = getCurrentConversation();
95
+ if (!conversation) return;
96
+ const messageIndex = conversation.messages.findIndex(m => m.id === messageId);
97
+ if (messageIndex !== -1) {
98
+ conversation.messages[messageIndex] = {
99
+ ...conversation.messages[messageIndex],
100
+ ...updates
101
+ };
102
+ conversation.updatedAt = Date.now();
103
+ if (autoSave) {
104
+ saveConversations();
105
+ }
106
+ }
107
+ };
108
+ const clearCurrentConversation = () => {
109
+ const conversation = getCurrentConversation();
110
+ if (!conversation) return;
111
+ conversation.messages = [];
112
+ conversation.updatedAt = Date.now();
113
+ if (autoSave) {
114
+ saveConversations();
115
+ }
116
+ };
117
+ const exportConversations = () => {
118
+ return JSON.stringify(conversations.value, null, 2);
119
+ };
120
+ const importConversations = async json => {
121
+ try {
122
+ const imported = JSON.parse(json);
123
+ if (!Array.isArray(imported)) {
124
+ throw new Error("Invalid format: expected array");
125
+ }
126
+ conversations.value = imported;
127
+ await saveConversations();
128
+ return true;
129
+ } catch (e) {
130
+ error.value = e instanceof Error ? e : new Error("Failed to import conversations");
131
+ return false;
132
+ }
133
+ };
134
+ const setCurrentConversation = id => {
135
+ if (conversations.value.some(c => c.id === id)) {
136
+ currentConversationId.value = id;
137
+ }
138
+ };
139
+ (0, _vue.onMounted)(() => {
140
+ loadConversations();
141
+ });
142
+ return {
143
+ conversations,
144
+ currentConversationId,
145
+ isLoading,
146
+ isSaving,
147
+ error,
148
+ loadConversations,
149
+ saveConversations,
150
+ createConversation,
151
+ deleteConversation,
152
+ getCurrentConversation,
153
+ addMessage,
154
+ updateMessage,
155
+ clearCurrentConversation,
156
+ exportConversations,
157
+ importConversations,
158
+ setCurrentConversation
159
+ };
160
+ }
@@ -0,0 +1,62 @@
1
+ import { type StorageAdapter } from '../storage';
2
+ export interface ConversationMessage {
3
+ id: string;
4
+ role: 'user' | 'assistant' | 'system';
5
+ content: string;
6
+ timestamp: number;
7
+ metadata?: Record<string, unknown>;
8
+ }
9
+ export interface Conversation {
10
+ id: string;
11
+ title: string;
12
+ messages: ConversationMessage[];
13
+ createdAt: number;
14
+ updatedAt: number;
15
+ }
16
+ export interface UseAiPersistenceOptions {
17
+ storage?: StorageAdapter;
18
+ conversationKey?: string;
19
+ autoSave?: boolean;
20
+ }
21
+ export declare function useAiPersistence(options?: UseAiPersistenceOptions): {
22
+ conversations: import("vue").Ref<{
23
+ id: string;
24
+ title: string;
25
+ messages: {
26
+ id: string;
27
+ role: "user" | "assistant" | "system";
28
+ content: string;
29
+ timestamp: number;
30
+ metadata?: Record<string, unknown> | undefined;
31
+ }[];
32
+ createdAt: number;
33
+ updatedAt: number;
34
+ }[], Conversation[] | {
35
+ id: string;
36
+ title: string;
37
+ messages: {
38
+ id: string;
39
+ role: "user" | "assistant" | "system";
40
+ content: string;
41
+ timestamp: number;
42
+ metadata?: Record<string, unknown> | undefined;
43
+ }[];
44
+ createdAt: number;
45
+ updatedAt: number;
46
+ }[]>;
47
+ currentConversationId: import("vue").Ref<string | null, string | null>;
48
+ isLoading: import("vue").Ref<boolean, boolean>;
49
+ isSaving: import("vue").Ref<boolean, boolean>;
50
+ error: import("vue").Ref<Error | null, Error | null>;
51
+ loadConversations: () => Promise<void>;
52
+ saveConversations: () => Promise<void>;
53
+ createConversation: (title?: string) => Conversation;
54
+ deleteConversation: (id: string) => void;
55
+ getCurrentConversation: () => Conversation | undefined;
56
+ addMessage: (message: Omit<ConversationMessage, "id" | "timestamp">) => ConversationMessage | undefined;
57
+ updateMessage: (messageId: string, updates: Partial<ConversationMessage>) => void;
58
+ clearCurrentConversation: () => void;
59
+ exportConversations: () => string;
60
+ importConversations: (json: string) => Promise<boolean>;
61
+ setCurrentConversation: (id: string) => void;
62
+ };
@@ -0,0 +1,154 @@
1
+ import { ref, onMounted } from "vue";
2
+ import { IndexedDBAdapter } from "../storage/index.mjs";
3
+ export function useAiPersistence(options = {}) {
4
+ const {
5
+ storage = new IndexedDBAdapter(),
6
+ conversationKey = "ai-conversations",
7
+ autoSave = true
8
+ } = options;
9
+ const conversations = ref([]);
10
+ const currentConversationId = ref(null);
11
+ const isLoading = ref(false);
12
+ const isSaving = ref(false);
13
+ const error = ref(null);
14
+ const loadConversations = async () => {
15
+ isLoading.value = true;
16
+ error.value = null;
17
+ try {
18
+ const data = await storage.get(conversationKey);
19
+ conversations.value = data || [];
20
+ if (conversations.value.length > 0) {
21
+ const lastConversation = conversations.value[conversations.value.length - 1];
22
+ currentConversationId.value = lastConversation.id;
23
+ }
24
+ } catch (e) {
25
+ error.value = e instanceof Error ? e : new Error("Failed to load conversations");
26
+ } finally {
27
+ isLoading.value = false;
28
+ }
29
+ };
30
+ const saveConversations = async () => {
31
+ isSaving.value = true;
32
+ error.value = null;
33
+ try {
34
+ await storage.set(conversationKey, conversations.value);
35
+ } catch (e) {
36
+ error.value = e instanceof Error ? e : new Error("Failed to save conversations");
37
+ } finally {
38
+ isSaving.value = false;
39
+ }
40
+ };
41
+ const createConversation = (title = "New Conversation") => {
42
+ const now = Date.now();
43
+ const conversation = {
44
+ id: `conv-${now}`,
45
+ title,
46
+ messages: [],
47
+ createdAt: now,
48
+ updatedAt: now
49
+ };
50
+ conversations.value.push(conversation);
51
+ currentConversationId.value = conversation.id;
52
+ if (autoSave) {
53
+ saveConversations();
54
+ }
55
+ return conversation;
56
+ };
57
+ const deleteConversation = (id) => {
58
+ const index = conversations.value.findIndex((c) => c.id === id);
59
+ if (index !== -1) {
60
+ conversations.value.splice(index, 1);
61
+ if (currentConversationId.value === id) {
62
+ currentConversationId.value = conversations.value.length > 0 ? conversations.value[conversations.value.length - 1].id : null;
63
+ }
64
+ if (autoSave) {
65
+ saveConversations();
66
+ }
67
+ }
68
+ };
69
+ const getCurrentConversation = () => {
70
+ return conversations.value.find((c) => c.id === currentConversationId.value);
71
+ };
72
+ const addMessage = (message) => {
73
+ const conversation = getCurrentConversation();
74
+ if (!conversation) return;
75
+ const newMessage = {
76
+ ...message,
77
+ id: `msg-${Date.now()}`,
78
+ timestamp: Date.now()
79
+ };
80
+ conversation.messages.push(newMessage);
81
+ conversation.updatedAt = Date.now();
82
+ if (autoSave) {
83
+ saveConversations();
84
+ }
85
+ return newMessage;
86
+ };
87
+ const updateMessage = (messageId, updates) => {
88
+ const conversation = getCurrentConversation();
89
+ if (!conversation) return;
90
+ const messageIndex = conversation.messages.findIndex((m) => m.id === messageId);
91
+ if (messageIndex !== -1) {
92
+ conversation.messages[messageIndex] = {
93
+ ...conversation.messages[messageIndex],
94
+ ...updates
95
+ };
96
+ conversation.updatedAt = Date.now();
97
+ if (autoSave) {
98
+ saveConversations();
99
+ }
100
+ }
101
+ };
102
+ const clearCurrentConversation = () => {
103
+ const conversation = getCurrentConversation();
104
+ if (!conversation) return;
105
+ conversation.messages = [];
106
+ conversation.updatedAt = Date.now();
107
+ if (autoSave) {
108
+ saveConversations();
109
+ }
110
+ };
111
+ const exportConversations = () => {
112
+ return JSON.stringify(conversations.value, null, 2);
113
+ };
114
+ const importConversations = async (json) => {
115
+ try {
116
+ const imported = JSON.parse(json);
117
+ if (!Array.isArray(imported)) {
118
+ throw new Error("Invalid format: expected array");
119
+ }
120
+ conversations.value = imported;
121
+ await saveConversations();
122
+ return true;
123
+ } catch (e) {
124
+ error.value = e instanceof Error ? e : new Error("Failed to import conversations");
125
+ return false;
126
+ }
127
+ };
128
+ const setCurrentConversation = (id) => {
129
+ if (conversations.value.some((c) => c.id === id)) {
130
+ currentConversationId.value = id;
131
+ }
132
+ };
133
+ onMounted(() => {
134
+ loadConversations();
135
+ });
136
+ return {
137
+ conversations,
138
+ currentConversationId,
139
+ isLoading,
140
+ isSaving,
141
+ error,
142
+ loadConversations,
143
+ saveConversations,
144
+ createConversation,
145
+ deleteConversation,
146
+ getCurrentConversation,
147
+ addMessage,
148
+ updateMessage,
149
+ clearCurrentConversation,
150
+ exportConversations,
151
+ importConversations,
152
+ setCurrentConversation
153
+ };
154
+ }
@@ -40,11 +40,12 @@ function useAiVoice(options = {}) {
40
40
  if (e.data.size > 0) chunks.push(e.data);
41
41
  };
42
42
  recorder.onstop = () => {
43
- audioBlob.value = new Blob(chunks, {
43
+ const blob = chunks.length > 0 ? new Blob(chunks, {
44
44
  type: "audio/webm"
45
- });
46
- if (isRecording.value === false && chunks.length > 0) {
47
- options.onStop?.(transcript.value, audioBlob.value);
45
+ }) : null;
46
+ audioBlob.value = blob;
47
+ if (!isRecording.value) {
48
+ options.onStop?.(transcript.value, blob);
48
49
  }
49
50
  };
50
51
  mediaRecorder.value = recorder;
@@ -124,7 +125,7 @@ function useAiVoice(options = {}) {
124
125
  }
125
126
  };
126
127
  const start = async () => {
127
- if (isRecording.value) return;
128
+ if (stream.value) return;
128
129
  try {
129
130
  transcript.value = "";
130
131
  interimTranscript.value = "";
@@ -151,31 +152,27 @@ function useAiVoice(options = {}) {
151
152
  }
152
153
  };
153
154
  const stop = () => {
154
- if (!isRecording.value) return;
155
+ if (!stream.value) return;
155
156
  isRecording.value = false;
156
- if (stream.value) {
157
- stream.value.getTracks().forEach(track => track.stop());
158
- stream.value = null;
157
+ if (mediaRecorder.value && mediaRecorder.value.state !== "inactive") {
158
+ try {
159
+ mediaRecorder.value.stop();
160
+ } catch {}
159
161
  }
160
162
  if (recognition.value) {
161
163
  try {
162
164
  recognition.value.stop();
163
165
  } catch {}
164
166
  }
165
- if (mediaRecorder.value && mediaRecorder.value.state !== "inactive") {
166
- try {
167
- mediaRecorder.value.stop();
168
- } catch {}
167
+ if (stream.value) {
168
+ stream.value.getTracks().forEach(track => track.stop());
169
+ stream.value = null;
169
170
  }
170
171
  cleanup();
171
172
  };
172
173
  const cancel = () => {
173
- if (!isRecording.value) return;
174
+ if (!isRecording.value && !stream.value) return;
174
175
  isRecording.value = false;
175
- if (stream.value) {
176
- stream.value.getTracks().forEach(track => track.stop());
177
- stream.value = null;
178
- }
179
176
  if (recognition.value) {
180
177
  try {
181
178
  recognition.value.abort();
@@ -186,6 +183,10 @@ function useAiVoice(options = {}) {
186
183
  mediaRecorder.value.stop();
187
184
  } catch {}
188
185
  }
186
+ if (stream.value) {
187
+ stream.value.getTracks().forEach(track => track.stop());
188
+ stream.value = null;
189
+ }
189
190
  cleanup();
190
191
  };
191
192
  const cleanup = () => {
@@ -34,9 +34,10 @@ export function useAiVoice(options = {}) {
34
34
  if (e.data.size > 0) chunks.push(e.data);
35
35
  };
36
36
  recorder.onstop = () => {
37
- audioBlob.value = new Blob(chunks, { type: "audio/webm" });
38
- if (isRecording.value === false && chunks.length > 0) {
39
- options.onStop?.(transcript.value, audioBlob.value);
37
+ const blob = chunks.length > 0 ? new Blob(chunks, { type: "audio/webm" }) : null;
38
+ audioBlob.value = blob;
39
+ if (!isRecording.value) {
40
+ options.onStop?.(transcript.value, blob);
40
41
  }
41
42
  };
42
43
  mediaRecorder.value = recorder;
@@ -117,7 +118,7 @@ export function useAiVoice(options = {}) {
117
118
  }
118
119
  };
119
120
  const start = async () => {
120
- if (isRecording.value) return;
121
+ if (stream.value) return;
121
122
  try {
122
123
  transcript.value = "";
123
124
  interimTranscript.value = "";
@@ -142,33 +143,29 @@ export function useAiVoice(options = {}) {
142
143
  }
143
144
  };
144
145
  const stop = () => {
145
- if (!isRecording.value) return;
146
+ if (!stream.value) return;
146
147
  isRecording.value = false;
147
- if (stream.value) {
148
- stream.value.getTracks().forEach((track) => track.stop());
149
- stream.value = null;
150
- }
151
- if (recognition.value) {
148
+ if (mediaRecorder.value && mediaRecorder.value.state !== "inactive") {
152
149
  try {
153
- recognition.value.stop();
150
+ mediaRecorder.value.stop();
154
151
  } catch {
155
152
  }
156
153
  }
157
- if (mediaRecorder.value && mediaRecorder.value.state !== "inactive") {
154
+ if (recognition.value) {
158
155
  try {
159
- mediaRecorder.value.stop();
156
+ recognition.value.stop();
160
157
  } catch {
161
158
  }
162
159
  }
163
- cleanup();
164
- };
165
- const cancel = () => {
166
- if (!isRecording.value) return;
167
- isRecording.value = false;
168
160
  if (stream.value) {
169
161
  stream.value.getTracks().forEach((track) => track.stop());
170
162
  stream.value = null;
171
163
  }
164
+ cleanup();
165
+ };
166
+ const cancel = () => {
167
+ if (!isRecording.value && !stream.value) return;
168
+ isRecording.value = false;
172
169
  if (recognition.value) {
173
170
  try {
174
171
  recognition.value.abort();
@@ -181,6 +178,10 @@ export function useAiVoice(options = {}) {
181
178
  } catch {
182
179
  }
183
180
  }
181
+ if (stream.value) {
182
+ stream.value.getTracks().forEach((track) => track.stop());
183
+ stream.value = null;
184
+ }
184
185
  cleanup();
185
186
  };
186
187
  const cleanup = () => {
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useCountdown = useCountdown;
7
+ var _vue = require("vue");
8
+ function useCountdown(options) {
9
+ const {
10
+ time,
11
+ interval = 1e3,
12
+ onFinish,
13
+ onChange
14
+ } = options;
15
+ const remain = (0, _vue.ref)(time);
16
+ const timer = (0, _vue.ref)(null);
17
+ const isRunning = (0, _vue.computed)(() => timer.value !== null);
18
+ const stop = () => {
19
+ if (timer.value) {
20
+ clearInterval(timer.value);
21
+ timer.value = null;
22
+ }
23
+ };
24
+ const start = () => {
25
+ if (isRunning.value) return;
26
+ if (remain.value <= 0) return;
27
+ timer.value = setInterval(() => {
28
+ remain.value -= interval;
29
+ if (onChange) onChange(remain.value);
30
+ if (remain.value <= 0) {
31
+ remain.value = 0;
32
+ stop();
33
+ if (onFinish) onFinish();
34
+ }
35
+ }, interval);
36
+ };
37
+ const reset = newTime => {
38
+ stop();
39
+ remain.value = newTime !== void 0 ? newTime : time;
40
+ };
41
+ (0, _vue.onUnmounted)(() => {
42
+ stop();
43
+ });
44
+ return {
45
+ remain,
46
+ isRunning,
47
+ start,
48
+ stop,
49
+ reset
50
+ };
51
+ }
@@ -0,0 +1,17 @@
1
+ export interface CountdownOptions {
2
+ time: number;
3
+ interval?: number;
4
+ onFinish?: () => void;
5
+ onChange?: (current: number) => void;
6
+ }
7
+ /**
8
+ * 倒计时逻辑 Hook
9
+ * @param options
10
+ */
11
+ export declare function useCountdown(options: CountdownOptions): {
12
+ remain: import("vue").Ref<number, number>;
13
+ isRunning: import("vue").ComputedRef<boolean>;
14
+ start: () => void;
15
+ stop: () => void;
16
+ reset: (newTime?: number) => void;
17
+ };
@@ -0,0 +1,40 @@
1
+ import { ref, computed, onUnmounted } from "vue";
2
+ export function useCountdown(options) {
3
+ const { time, interval = 1e3, onFinish, onChange } = options;
4
+ const remain = ref(time);
5
+ const timer = ref(null);
6
+ const isRunning = computed(() => timer.value !== null);
7
+ const stop = () => {
8
+ if (timer.value) {
9
+ clearInterval(timer.value);
10
+ timer.value = null;
11
+ }
12
+ };
13
+ const start = () => {
14
+ if (isRunning.value) return;
15
+ if (remain.value <= 0) return;
16
+ timer.value = setInterval(() => {
17
+ remain.value -= interval;
18
+ if (onChange) onChange(remain.value);
19
+ if (remain.value <= 0) {
20
+ remain.value = 0;
21
+ stop();
22
+ if (onFinish) onFinish();
23
+ }
24
+ }, interval);
25
+ };
26
+ const reset = (newTime) => {
27
+ stop();
28
+ remain.value = newTime !== void 0 ? newTime : time;
29
+ };
30
+ onUnmounted(() => {
31
+ stop();
32
+ });
33
+ return {
34
+ remain,
35
+ isRunning,
36
+ start,
37
+ stop,
38
+ reset
39
+ };
40
+ }
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useSKU = useSKU;
7
+ var _vue = require("vue");
8
+ function useSKU(specs, skus, initialSelection = []) {
9
+ const selectedValueIds = (0, _vue.ref)(initialSelection);
10
+ const pathDict = (0, _vue.computed)(() => {
11
+ const dict = {};
12
+ skus.forEach(sku => {
13
+ if (sku.stock <= 0) return;
14
+ const powerSet = getPowerSet(sku.specValueIds);
15
+ powerSet.forEach(path => {
16
+ const key = path.join(",");
17
+ dict[key] = (dict[key] || 0) + sku.stock;
18
+ });
19
+ });
20
+ return dict;
21
+ });
22
+ const isValueSelectable = (specIndex, valueId) => {
23
+ const tempSelected = [...selectedValueIds.value];
24
+ if (tempSelected[specIndex] === valueId) {
25
+ tempSelected[specIndex] = "";
26
+ } else {
27
+ tempSelected[specIndex] = valueId;
28
+ }
29
+ const query = tempSelected.filter(v => !!v).sort((a, b) => String(a).localeCompare(String(b))).join(",");
30
+ if (!query) return true;
31
+ return !!pathDict.value[query];
32
+ };
33
+ const toggleValue = (specIndex, valueId) => {
34
+ if (selectedValueIds.value[specIndex] === valueId) {
35
+ selectedValueIds.value[specIndex] = "";
36
+ } else {
37
+ selectedValueIds.value[specIndex] = valueId;
38
+ }
39
+ };
40
+ const selectedSku = (0, _vue.computed)(() => {
41
+ const completeSelection = selectedValueIds.value.every(v => !!v);
42
+ if (!completeSelection || selectedValueIds.value.length < specs.length) return null;
43
+ const targetKey = [...selectedValueIds.value].sort((a, b) => String(a).localeCompare(String(b))).join(",");
44
+ return skus.find(sku => [...sku.specValueIds].sort((a, b) => String(a).localeCompare(String(b))).join(",") === targetKey) || null;
45
+ });
46
+ function getPowerSet(arr) {
47
+ const result = [[]];
48
+ for (const item of arr) {
49
+ const size = result.length;
50
+ for (let i = 0; i < size; i++) {
51
+ result.push([...result[i], item]);
52
+ }
53
+ }
54
+ return result.filter(v => v.length > 0).map(v => [...v].sort((a, b) => String(a).localeCompare(String(b))));
55
+ }
56
+ return {
57
+ selectedValueIds,
58
+ isValueSelectable,
59
+ selectedSku,
60
+ toggleValue
61
+ };
62
+ }