@morscherlab/mld-sdk 0.10.0 → 0.10.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.
@@ -0,0 +1,25 @@
1
+ import { ExperimentSummary } from '../types';
2
+ export declare const mockExperiments: ExperimentSummary[];
3
+ export declare function installMockInterceptor(): void;
4
+ /**
5
+ * Wrapper component that fakes integrated mode and provides useAppExperiment context.
6
+ * Use in story files to demonstrate experiment selector integration.
7
+ *
8
+ * Props:
9
+ * preselect (boolean, default true) - pre-select the first mock experiment
10
+ */
11
+ export declare const ExperimentProvider: import('vue').DefineComponent<import('vue').ExtractPropTypes<{
12
+ preselect: {
13
+ type: BooleanConstructor;
14
+ default: boolean;
15
+ };
16
+ }>, () => import('vue').VNode<import('vue').RendererNode, import('vue').RendererElement, {
17
+ [key: string]: any;
18
+ }>[] | undefined, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
19
+ preselect: {
20
+ type: BooleanConstructor;
21
+ default: boolean;
22
+ };
23
+ }>> & Readonly<{}>, {
24
+ preselect: boolean;
25
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
@@ -7,6 +7,9 @@ interface Props {
7
7
  saveLoading?: boolean;
8
8
  saveSuccessMessage?: string;
9
9
  saveDisabledMessage?: string;
10
+ confirmSave?: boolean;
11
+ confirmTitle?: string;
12
+ confirmMessage?: string;
10
13
  }
11
14
  declare const _default: import('vue').DefineComponent<Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
12
15
  select: () => any;
@@ -21,6 +24,7 @@ declare const _default: import('vue').DefineComponent<Props, {}, {}, {}, {}, imp
21
24
  showDetach: boolean;
22
25
  saveDisabled: boolean;
23
26
  saveLoading: boolean;
27
+ confirmSave: boolean;
24
28
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
25
29
  popoverRef: HTMLDivElement;
26
30
  }, HTMLDivElement>;
@@ -1,51 +1,48 @@
1
- import { defineComponent, ref, watch, onMounted, onUnmounted, openBlock, createElementBlock, createElementVNode, withModifiers, normalizeClass, toDisplayString, createTextVNode, createCommentVNode, Fragment } from "vue";
1
+ import { defineComponent, ref, watch, onMounted, onUnmounted, openBlock, createElementBlock, createElementVNode, normalizeClass, withModifiers, toDisplayString, createCommentVNode, createTextVNode, createVNode } from "vue";
2
+ import _sfc_main$1 from "./ConfirmDialog.vue.js";
3
+ /* empty css */
2
4
  const _hoisted_1 = { class: "mld-experiment-popover__trigger-text" };
3
- const _hoisted_2 = {
5
+ const _hoisted_2 = ["disabled", "title"];
6
+ const _hoisted_3 = {
4
7
  key: 0,
5
- class: "mld-experiment-popover__panel"
6
- };
7
- const _hoisted_3 = { class: "mld-experiment-popover__header" };
8
- const _hoisted_4 = { class: "mld-experiment-popover__subtitle" };
9
- const _hoisted_5 = {
10
- key: 0,
11
- class: "mld-experiment-popover__body"
8
+ class: "mld-experiment-popover__spinner--inline"
12
9
  };
13
- const _hoisted_6 = {
10
+ const _hoisted_4 = {
14
11
  key: 1,
15
- class: "mld-experiment-popover__body"
16
- };
17
- const _hoisted_7 = { class: "mld-experiment-popover__card" };
18
- const _hoisted_8 = { class: "mld-experiment-popover__card-info" };
19
- const _hoisted_9 = { class: "mld-experiment-popover__card-name" };
20
- const _hoisted_10 = {
21
- key: 0,
22
- class: "mld-experiment-popover__card-status"
23
- };
24
- const _hoisted_11 = { class: "mld-experiment-popover__card-actions" };
25
- const _hoisted_12 = { class: "mld-experiment-popover__footer" };
26
- const _hoisted_13 = ["disabled"];
27
- const _hoisted_14 = {
28
- key: 0,
29
- class: "mld-experiment-popover__spinner"
30
- };
31
- const _hoisted_15 = {
32
- key: 1,
33
- class: "mld-experiment-popover__check-icon",
12
+ class: "mld-experiment-popover__save-trigger-icon",
34
13
  fill: "none",
35
14
  stroke: "currentColor",
36
15
  viewBox: "0 0 24 24"
37
16
  };
38
- const _hoisted_16 = {
17
+ const _hoisted_5 = {
39
18
  key: 2,
40
- class: "mld-experiment-popover__save-icon",
19
+ class: "mld-experiment-popover__save-trigger-icon",
41
20
  fill: "none",
42
21
  stroke: "currentColor",
43
22
  viewBox: "0 0 24 24"
44
23
  };
45
- const _hoisted_17 = {
24
+ const _hoisted_6 = {
46
25
  key: 0,
47
- class: "mld-experiment-popover__save-hint"
26
+ class: "mld-experiment-popover__panel"
48
27
  };
28
+ const _hoisted_7 = { class: "mld-experiment-popover__header" };
29
+ const _hoisted_8 = { class: "mld-experiment-popover__subtitle" };
30
+ const _hoisted_9 = {
31
+ key: 0,
32
+ class: "mld-experiment-popover__body"
33
+ };
34
+ const _hoisted_10 = {
35
+ key: 1,
36
+ class: "mld-experiment-popover__body"
37
+ };
38
+ const _hoisted_11 = { class: "mld-experiment-popover__card" };
39
+ const _hoisted_12 = { class: "mld-experiment-popover__card-info" };
40
+ const _hoisted_13 = { class: "mld-experiment-popover__card-name" };
41
+ const _hoisted_14 = {
42
+ key: 0,
43
+ class: "mld-experiment-popover__card-status"
44
+ };
45
+ const _hoisted_15 = { class: "mld-experiment-popover__card-actions" };
49
46
  const _sfc_main = /* @__PURE__ */ defineComponent({
50
47
  __name: "ExperimentPopover",
51
48
  props: {
@@ -56,7 +53,10 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
56
53
  saveDisabled: { type: Boolean, default: false },
57
54
  saveLoading: { type: Boolean, default: false },
58
55
  saveSuccessMessage: {},
59
- saveDisabledMessage: {}
56
+ saveDisabledMessage: {},
57
+ confirmSave: { type: Boolean, default: true },
58
+ confirmTitle: {},
59
+ confirmMessage: {}
60
60
  },
61
61
  emits: ["select", "save", "detach"],
62
62
  setup(__props, { emit: __emit }) {
@@ -65,6 +65,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
65
65
  const isOpen = ref(false);
66
66
  const popoverRef = ref(null);
67
67
  const showSuccess = ref(false);
68
+ const showConfirm = ref(false);
68
69
  function toggle() {
69
70
  isOpen.value = !isOpen.value;
70
71
  }
@@ -77,6 +78,14 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
77
78
  }
78
79
  function handleSave() {
79
80
  if (props.saveDisabled || props.saveLoading) return;
81
+ if (props.confirmSave) {
82
+ showConfirm.value = true;
83
+ } else {
84
+ emit("save");
85
+ }
86
+ }
87
+ function handleConfirmSave() {
88
+ showConfirm.value = false;
80
89
  emit("save");
81
90
  }
82
91
  function handleDetach() {
@@ -88,11 +97,14 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
88
97
  close();
89
98
  }
90
99
  }
100
+ let successTimer = null;
91
101
  watch(() => props.saveSuccessMessage, (msg) => {
102
+ if (successTimer) clearTimeout(successTimer);
92
103
  if (msg) {
93
104
  showSuccess.value = true;
94
- setTimeout(() => {
105
+ successTimer = setTimeout(() => {
95
106
  showSuccess.value = false;
107
+ successTimer = null;
96
108
  }, 3e3);
97
109
  }
98
110
  });
@@ -101,6 +113,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
101
113
  });
102
114
  onUnmounted(() => {
103
115
  document.removeEventListener("click", handleClickOutside);
116
+ if (successTimer) clearTimeout(successTimer);
104
117
  });
105
118
  function formatStatus(status) {
106
119
  return status.replace(/_/g, " ").replace(/^\w/, (c) => c.toUpperCase());
@@ -111,52 +124,88 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
111
124
  ref: popoverRef,
112
125
  class: "mld-experiment-popover"
113
126
  }, [
114
- createElementVNode("button", {
115
- type: "button",
127
+ createElementVNode("div", {
116
128
  class: normalizeClass([
117
- "mld-experiment-popover__trigger",
118
- { "mld-experiment-popover__trigger--active": isOpen.value },
119
- { "mld-experiment-popover__trigger--empty": !__props.experimentName }
120
- ]),
121
- onClick: withModifiers(toggle, ["stop"])
129
+ "mld-experiment-popover__split",
130
+ { "mld-experiment-popover__split--with-save": __props.showSave && __props.experimentName }
131
+ ])
122
132
  }, [
123
- _cache[0] || (_cache[0] = createElementVNode("svg", {
124
- class: "mld-experiment-popover__trigger-icon",
125
- fill: "none",
126
- stroke: "currentColor",
127
- viewBox: "0 0 24 24"
133
+ createElementVNode("button", {
134
+ type: "button",
135
+ class: normalizeClass([
136
+ "mld-experiment-popover__trigger",
137
+ { "mld-experiment-popover__trigger--active": isOpen.value },
138
+ { "mld-experiment-popover__trigger--empty": !__props.experimentName }
139
+ ]),
140
+ onClick: withModifiers(toggle, ["stop"])
128
141
  }, [
129
- createElementVNode("path", {
142
+ _cache[1] || (_cache[1] = createElementVNode("svg", {
143
+ class: "mld-experiment-popover__trigger-icon",
144
+ fill: "none",
145
+ stroke: "currentColor",
146
+ viewBox: "0 0 24 24"
147
+ }, [
148
+ createElementVNode("path", {
149
+ "stroke-linecap": "round",
150
+ "stroke-linejoin": "round",
151
+ "stroke-width": "1.75",
152
+ d: "M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"
153
+ })
154
+ ], -1)),
155
+ createElementVNode("span", _hoisted_1, toDisplayString(__props.experimentName || "No experiment"), 1),
156
+ _cache[2] || (_cache[2] = createElementVNode("svg", {
157
+ class: "mld-experiment-popover__trigger-chevron",
158
+ viewBox: "0 0 24 24",
159
+ fill: "none",
160
+ stroke: "currentColor",
161
+ "stroke-width": "2",
130
162
  "stroke-linecap": "round",
131
- "stroke-linejoin": "round",
132
- "stroke-width": "1.75",
133
- d: "M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"
134
- })
135
- ], -1)),
136
- createElementVNode("span", _hoisted_1, toDisplayString(__props.experimentName || "No experiment"), 1),
137
- _cache[1] || (_cache[1] = createElementVNode("svg", {
138
- class: "mld-experiment-popover__trigger-chevron",
139
- viewBox: "0 0 24 24",
140
- fill: "none",
141
- stroke: "currentColor",
142
- "stroke-width": "2",
143
- "stroke-linecap": "round",
144
- "stroke-linejoin": "round"
163
+ "stroke-linejoin": "round"
164
+ }, [
165
+ createElementVNode("path", { d: "m6 9 6 6 6-6" })
166
+ ], -1))
167
+ ], 2),
168
+ __props.showSave && __props.experimentName ? (openBlock(), createElementBlock("button", {
169
+ key: 0,
170
+ type: "button",
171
+ class: normalizeClass([
172
+ "mld-experiment-popover__save-trigger",
173
+ { "mld-experiment-popover__save-trigger--loading": __props.saveLoading },
174
+ { "mld-experiment-popover__save-trigger--success": showSuccess.value },
175
+ { "mld-experiment-popover__save-trigger--disabled": __props.saveDisabled && !showSuccess.value }
176
+ ]),
177
+ disabled: __props.saveDisabled && !showSuccess.value,
178
+ title: __props.saveDisabled && __props.saveDisabledMessage ? __props.saveDisabledMessage : showSuccess.value && __props.saveSuccessMessage ? __props.saveSuccessMessage : "Save to Experiment",
179
+ onClick: withModifiers(handleSave, ["stop"])
145
180
  }, [
146
- createElementVNode("path", { d: "m6 9 6 6 6-6" })
147
- ], -1))
181
+ __props.saveLoading ? (openBlock(), createElementBlock("span", _hoisted_3)) : showSuccess.value ? (openBlock(), createElementBlock("svg", _hoisted_4, [..._cache[3] || (_cache[3] = [
182
+ createElementVNode("path", {
183
+ "stroke-linecap": "round",
184
+ "stroke-linejoin": "round",
185
+ "stroke-width": "2.5",
186
+ d: "M5 13l4 4L19 7"
187
+ }, null, -1)
188
+ ])])) : (openBlock(), createElementBlock("svg", _hoisted_5, [..._cache[4] || (_cache[4] = [
189
+ createElementVNode("path", {
190
+ "stroke-linecap": "round",
191
+ "stroke-linejoin": "round",
192
+ "stroke-width": "2",
193
+ d: "M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"
194
+ }, null, -1)
195
+ ])]))
196
+ ], 10, _hoisted_2)) : createCommentVNode("", true)
148
197
  ], 2),
149
- isOpen.value ? (openBlock(), createElementBlock("div", _hoisted_2, [
150
- createElementVNode("div", _hoisted_3, [
151
- _cache[2] || (_cache[2] = createElementVNode("div", { class: "mld-experiment-popover__title" }, "Experiment", -1)),
152
- createElementVNode("div", _hoisted_4, toDisplayString(__props.experimentName ? "Linked experiment context" : "Link to an MLD experiment"), 1)
198
+ isOpen.value ? (openBlock(), createElementBlock("div", _hoisted_6, [
199
+ createElementVNode("div", _hoisted_7, [
200
+ _cache[5] || (_cache[5] = createElementVNode("div", { class: "mld-experiment-popover__title" }, "Experiment", -1)),
201
+ createElementVNode("div", _hoisted_8, toDisplayString(__props.experimentName ? "Linked experiment context" : "Link to an MLD experiment"), 1)
153
202
  ]),
154
- !__props.experimentName ? (openBlock(), createElementBlock("div", _hoisted_5, [
203
+ !__props.experimentName ? (openBlock(), createElementBlock("div", _hoisted_9, [
155
204
  createElementVNode("button", {
156
205
  type: "button",
157
206
  class: "mld-experiment-popover__select-btn",
158
207
  onClick: handleSelect
159
- }, [..._cache[3] || (_cache[3] = [
208
+ }, [..._cache[6] || (_cache[6] = [
160
209
  createElementVNode("svg", {
161
210
  width: "14",
162
211
  height: "14",
@@ -173,9 +222,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
173
222
  ], -1),
174
223
  createTextVNode(" Select Experiment ", -1)
175
224
  ])])
176
- ])) : (openBlock(), createElementBlock("div", _hoisted_6, [
177
- createElementVNode("div", _hoisted_7, [
178
- _cache[4] || (_cache[4] = createElementVNode("div", { class: "mld-experiment-popover__card-icon" }, [
225
+ ])) : (openBlock(), createElementBlock("div", _hoisted_10, [
226
+ createElementVNode("div", _hoisted_11, [
227
+ _cache[7] || (_cache[7] = createElementVNode("div", { class: "mld-experiment-popover__card-icon" }, [
179
228
  createElementVNode("svg", {
180
229
  fill: "none",
181
230
  stroke: "currentColor",
@@ -189,11 +238,11 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
189
238
  })
190
239
  ])
191
240
  ], -1)),
192
- createElementVNode("div", _hoisted_8, [
193
- createElementVNode("div", _hoisted_9, toDisplayString(__props.experimentName), 1),
194
- __props.experimentStatus ? (openBlock(), createElementBlock("div", _hoisted_10, toDisplayString(formatStatus(__props.experimentStatus)), 1)) : createCommentVNode("", true)
241
+ createElementVNode("div", _hoisted_12, [
242
+ createElementVNode("div", _hoisted_13, toDisplayString(__props.experimentName), 1),
243
+ __props.experimentStatus ? (openBlock(), createElementBlock("div", _hoisted_14, toDisplayString(formatStatus(__props.experimentStatus)), 1)) : createCommentVNode("", true)
195
244
  ]),
196
- createElementVNode("div", _hoisted_11, [
245
+ createElementVNode("div", _hoisted_15, [
197
246
  createElementVNode("button", {
198
247
  type: "button",
199
248
  class: "mld-experiment-popover__change-btn",
@@ -207,41 +256,18 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
207
256
  }, " Detach ")) : createCommentVNode("", true)
208
257
  ])
209
258
  ])
210
- ])),
211
- __props.showSave ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [
212
- _cache[7] || (_cache[7] = createElementVNode("div", { class: "mld-experiment-popover__divider" }, null, -1)),
213
- createElementVNode("div", _hoisted_12, [
214
- createElementVNode("button", {
215
- type: "button",
216
- class: normalizeClass([
217
- "mld-experiment-popover__save-btn",
218
- { "mld-experiment-popover__save-btn--loading": __props.saveLoading },
219
- { "mld-experiment-popover__save-btn--success": showSuccess.value }
220
- ]),
221
- disabled: __props.saveDisabled && !showSuccess.value,
222
- onClick: handleSave
223
- }, [
224
- __props.saveLoading ? (openBlock(), createElementBlock("span", _hoisted_14)) : showSuccess.value ? (openBlock(), createElementBlock("svg", _hoisted_15, [..._cache[5] || (_cache[5] = [
225
- createElementVNode("path", {
226
- "stroke-linecap": "round",
227
- "stroke-linejoin": "round",
228
- "stroke-width": "2",
229
- d: "M5 13l4 4L19 7"
230
- }, null, -1)
231
- ])])) : (openBlock(), createElementBlock("svg", _hoisted_16, [..._cache[6] || (_cache[6] = [
232
- createElementVNode("path", {
233
- "stroke-linecap": "round",
234
- "stroke-linejoin": "round",
235
- "stroke-width": "2",
236
- d: "M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"
237
- }, null, -1)
238
- ])])),
239
- createElementVNode("span", null, toDisplayString(showSuccess.value && __props.saveSuccessMessage ? __props.saveSuccessMessage : "Save to Experiment"), 1)
240
- ], 10, _hoisted_13),
241
- __props.saveDisabled && __props.saveDisabledMessage && !showSuccess.value ? (openBlock(), createElementBlock("div", _hoisted_17, toDisplayString(__props.saveDisabledMessage), 1)) : createCommentVNode("", true)
242
- ])
243
- ], 64)) : createCommentVNode("", true)
244
- ])) : createCommentVNode("", true)
259
+ ]))
260
+ ])) : createCommentVNode("", true),
261
+ createVNode(_sfc_main$1, {
262
+ modelValue: showConfirm.value,
263
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => showConfirm.value = $event),
264
+ title: __props.confirmTitle ?? "Save to Experiment",
265
+ message: __props.confirmMessage ?? `Save current data to ${__props.experimentName}?`,
266
+ variant: "info",
267
+ "confirm-label": "Save",
268
+ loading: __props.saveLoading,
269
+ onConfirm: handleConfirmSave
270
+ }, null, 8, ["modelValue", "title", "message", "loading"])
245
271
  ], 512);
246
272
  };
247
273
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ExperimentPopover.vue.js","sources":["../../src/components/ExperimentPopover.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ref, watch, onMounted, onUnmounted } from 'vue'\n\ninterface Props {\n experimentName?: string\n experimentStatus?: string\n showSave?: boolean\n showDetach?: boolean\n saveDisabled?: boolean\n saveLoading?: boolean\n saveSuccessMessage?: string\n saveDisabledMessage?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n showSave: false,\n showDetach: false,\n saveDisabled: false,\n saveLoading: false,\n})\n\nconst emit = defineEmits<{\n select: []\n save: []\n detach: []\n}>()\n\nconst isOpen = ref(false)\nconst popoverRef = ref<HTMLElement | null>(null)\nconst showSuccess = ref(false)\n\nfunction toggle() {\n isOpen.value = !isOpen.value\n}\n\nfunction close() {\n isOpen.value = false\n}\n\nfunction handleSelect() {\n emit('select')\n close()\n}\n\nfunction handleSave() {\n if (props.saveDisabled || props.saveLoading) return\n emit('save')\n}\n\nfunction handleDetach() {\n emit('detach')\n close()\n}\n\nfunction handleClickOutside(event: MouseEvent) {\n if (popoverRef.value && !popoverRef.value.contains(event.target as Node)) {\n close()\n }\n}\n\n// Show success state when saveSuccessMessage changes from empty to a value\nwatch(() => props.saveSuccessMessage, (msg) => {\n if (msg) {\n showSuccess.value = true\n setTimeout(() => {\n showSuccess.value = false\n }, 3000)\n }\n})\n\nonMounted(() => {\n document.addEventListener('click', handleClickOutside)\n})\n\nonUnmounted(() => {\n document.removeEventListener('click', handleClickOutside)\n})\n\n// Format status for display (e.g., \"ready_to_extract\" -> \"Ready to extract\")\nfunction formatStatus(status: string): string {\n return status.replace(/_/g, ' ').replace(/^\\w/, c => c.toUpperCase())\n}\n</script>\n\n<template>\n <div ref=\"popoverRef\" class=\"mld-experiment-popover\">\n <!-- Trigger button -->\n <button\n type=\"button\"\n :class=\"[\n 'mld-experiment-popover__trigger',\n { 'mld-experiment-popover__trigger--active': isOpen },\n { 'mld-experiment-popover__trigger--empty': !experimentName },\n ]\"\n @click.stop=\"toggle\"\n >\n <!-- Flask icon -->\n <svg class=\"mld-experiment-popover__trigger-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"1.75\"\n d=\"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z\"\n />\n </svg>\n <span class=\"mld-experiment-popover__trigger-text\">\n {{ experimentName || 'No experiment' }}\n </span>\n <!-- Chevron -->\n <svg class=\"mld-experiment-popover__trigger-chevron\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n\n <!-- Popover panel -->\n <div v-if=\"isOpen\" class=\"mld-experiment-popover__panel\">\n <!-- Header -->\n <div class=\"mld-experiment-popover__header\">\n <div class=\"mld-experiment-popover__title\">Experiment</div>\n <div class=\"mld-experiment-popover__subtitle\">\n {{ experimentName ? 'Linked experiment context' : 'Link to an MLD experiment' }}\n </div>\n </div>\n\n <!-- No experiment selected -->\n <div v-if=\"!experimentName\" class=\"mld-experiment-popover__body\">\n <button type=\"button\" class=\"mld-experiment-popover__select-btn\" @click=\"handleSelect\">\n <svg width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 4v16m8-8H4\" />\n </svg>\n Select Experiment\n </button>\n </div>\n\n <!-- Experiment selected -->\n <div v-else class=\"mld-experiment-popover__body\">\n <div class=\"mld-experiment-popover__card\">\n <div class=\"mld-experiment-popover__card-icon\">\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"1.75\"\n d=\"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z\"\n />\n </svg>\n </div>\n <div class=\"mld-experiment-popover__card-info\">\n <div class=\"mld-experiment-popover__card-name\">{{ experimentName }}</div>\n <div v-if=\"experimentStatus\" class=\"mld-experiment-popover__card-status\">\n {{ formatStatus(experimentStatus) }}\n </div>\n </div>\n <div class=\"mld-experiment-popover__card-actions\">\n <button type=\"button\" class=\"mld-experiment-popover__change-btn\" @click=\"handleSelect\">\n Change\n </button>\n <button v-if=\"showDetach\" type=\"button\" class=\"mld-experiment-popover__detach-btn\" @click=\"handleDetach\">\n Detach\n </button>\n </div>\n </div>\n </div>\n\n <!-- Save section -->\n <template v-if=\"showSave\">\n <div class=\"mld-experiment-popover__divider\" />\n <div class=\"mld-experiment-popover__footer\">\n <button\n type=\"button\"\n :class=\"[\n 'mld-experiment-popover__save-btn',\n { 'mld-experiment-popover__save-btn--loading': saveLoading },\n { 'mld-experiment-popover__save-btn--success': showSuccess },\n ]\"\n :disabled=\"saveDisabled && !showSuccess\"\n @click=\"handleSave\"\n >\n <!-- Loading spinner -->\n <span v-if=\"saveLoading\" class=\"mld-experiment-popover__spinner\" />\n <!-- Success check -->\n <svg v-else-if=\"showSuccess\" class=\"mld-experiment-popover__check-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 13l4 4L19 7\" />\n </svg>\n <!-- Save icon -->\n <svg v-else class=\"mld-experiment-popover__save-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4\" />\n </svg>\n <!-- Label -->\n <span>{{ showSuccess && saveSuccessMessage ? saveSuccessMessage : 'Save to Experiment' }}</span>\n </button>\n <div v-if=\"saveDisabled && saveDisabledMessage && !showSuccess\" class=\"mld-experiment-popover__save-hint\">\n {{ saveDisabledMessage }}\n </div>\n </div>\n </template>\n </div>\n </div>\n</template>\n\n<style>\n@import '../styles/components/experiment-popover.css';\n</style>\n"],"names":["_createElementBlock","_createElementVNode","_normalizeClass","_toDisplayString","_openBlock","_Fragment"],"mappings":"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"}
1
+ {"version":3,"file":"ExperimentPopover.vue.js","sources":["../../src/components/ExperimentPopover.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ref, watch, onMounted, onUnmounted } from 'vue'\nimport ConfirmDialog from './ConfirmDialog.vue'\n\ninterface Props {\n experimentName?: string\n experimentStatus?: string\n showSave?: boolean\n showDetach?: boolean\n saveDisabled?: boolean\n saveLoading?: boolean\n saveSuccessMessage?: string\n saveDisabledMessage?: string\n confirmSave?: boolean\n confirmTitle?: string\n confirmMessage?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n showSave: false,\n showDetach: false,\n saveDisabled: false,\n saveLoading: false,\n confirmSave: true,\n})\n\nconst emit = defineEmits<{\n select: []\n save: []\n detach: []\n}>()\n\nconst isOpen = ref(false)\nconst popoverRef = ref<HTMLElement | null>(null)\nconst showSuccess = ref(false)\nconst showConfirm = ref(false)\n\nfunction toggle() {\n isOpen.value = !isOpen.value\n}\n\nfunction close() {\n isOpen.value = false\n}\n\nfunction handleSelect() {\n emit('select')\n close()\n}\n\nfunction handleSave() {\n if (props.saveDisabled || props.saveLoading) return\n if (props.confirmSave) {\n showConfirm.value = true\n } else {\n emit('save')\n }\n}\n\nfunction handleConfirmSave() {\n showConfirm.value = false\n emit('save')\n}\n\nfunction handleDetach() {\n emit('detach')\n close()\n}\n\nfunction handleClickOutside(event: MouseEvent) {\n if (popoverRef.value && !popoverRef.value.contains(event.target as Node)) {\n close()\n }\n}\n\nlet successTimer: ReturnType<typeof setTimeout> | null = null\n\n// Show success state when saveSuccessMessage changes from empty to a value\nwatch(() => props.saveSuccessMessage, (msg) => {\n if (successTimer) clearTimeout(successTimer)\n if (msg) {\n showSuccess.value = true\n successTimer = setTimeout(() => {\n showSuccess.value = false\n successTimer = null\n }, 3000)\n }\n})\n\nonMounted(() => {\n document.addEventListener('click', handleClickOutside)\n})\n\nonUnmounted(() => {\n document.removeEventListener('click', handleClickOutside)\n if (successTimer) clearTimeout(successTimer)\n})\n\n// Format status for display (e.g., \"ready_to_extract\" -> \"Ready to extract\")\nfunction formatStatus(status: string): string {\n return status.replace(/_/g, ' ').replace(/^\\w/, c => c.toUpperCase())\n}\n</script>\n\n<template>\n <div ref=\"popoverRef\" class=\"mld-experiment-popover\">\n <!-- Split trigger: experiment pill + inline save -->\n <div\n :class=\"[\n 'mld-experiment-popover__split',\n { 'mld-experiment-popover__split--with-save': showSave && experimentName },\n ]\"\n >\n <!-- Left: experiment trigger (opens popover) -->\n <button\n type=\"button\"\n :class=\"[\n 'mld-experiment-popover__trigger',\n { 'mld-experiment-popover__trigger--active': isOpen },\n { 'mld-experiment-popover__trigger--empty': !experimentName },\n ]\"\n @click.stop=\"toggle\"\n >\n <!-- Flask icon -->\n <svg class=\"mld-experiment-popover__trigger-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"1.75\"\n d=\"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z\"\n />\n </svg>\n <span class=\"mld-experiment-popover__trigger-text\">\n {{ experimentName || 'No experiment' }}\n </span>\n <!-- Chevron -->\n <svg class=\"mld-experiment-popover__trigger-chevron\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n\n <!-- Right: inline save button (direct action) -->\n <button\n v-if=\"showSave && experimentName\"\n type=\"button\"\n :class=\"[\n 'mld-experiment-popover__save-trigger',\n { 'mld-experiment-popover__save-trigger--loading': saveLoading },\n { 'mld-experiment-popover__save-trigger--success': showSuccess },\n { 'mld-experiment-popover__save-trigger--disabled': saveDisabled && !showSuccess },\n ]\"\n :disabled=\"saveDisabled && !showSuccess\"\n :title=\"saveDisabled && saveDisabledMessage ? saveDisabledMessage : showSuccess && saveSuccessMessage ? saveSuccessMessage : 'Save to Experiment'\"\n @click.stop=\"handleSave\"\n >\n <!-- Loading spinner -->\n <span v-if=\"saveLoading\" class=\"mld-experiment-popover__spinner--inline\" />\n <!-- Success check -->\n <svg v-else-if=\"showSuccess\" class=\"mld-experiment-popover__save-trigger-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2.5\" d=\"M5 13l4 4L19 7\" />\n </svg>\n <!-- Save icon -->\n <svg v-else class=\"mld-experiment-popover__save-trigger-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4\" />\n </svg>\n </button>\n </div>\n\n <!-- Popover panel -->\n <div v-if=\"isOpen\" class=\"mld-experiment-popover__panel\">\n <!-- Header -->\n <div class=\"mld-experiment-popover__header\">\n <div class=\"mld-experiment-popover__title\">Experiment</div>\n <div class=\"mld-experiment-popover__subtitle\">\n {{ experimentName ? 'Linked experiment context' : 'Link to an MLD experiment' }}\n </div>\n </div>\n\n <!-- No experiment selected -->\n <div v-if=\"!experimentName\" class=\"mld-experiment-popover__body\">\n <button type=\"button\" class=\"mld-experiment-popover__select-btn\" @click=\"handleSelect\">\n <svg width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 4v16m8-8H4\" />\n </svg>\n Select Experiment\n </button>\n </div>\n\n <!-- Experiment selected -->\n <div v-else class=\"mld-experiment-popover__body\">\n <div class=\"mld-experiment-popover__card\">\n <div class=\"mld-experiment-popover__card-icon\">\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"1.75\"\n d=\"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z\"\n />\n </svg>\n </div>\n <div class=\"mld-experiment-popover__card-info\">\n <div class=\"mld-experiment-popover__card-name\">{{ experimentName }}</div>\n <div v-if=\"experimentStatus\" class=\"mld-experiment-popover__card-status\">\n {{ formatStatus(experimentStatus) }}\n </div>\n </div>\n <div class=\"mld-experiment-popover__card-actions\">\n <button type=\"button\" class=\"mld-experiment-popover__change-btn\" @click=\"handleSelect\">\n Change\n </button>\n <button v-if=\"showDetach\" type=\"button\" class=\"mld-experiment-popover__detach-btn\" @click=\"handleDetach\">\n Detach\n </button>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Save confirmation dialog -->\n <ConfirmDialog\n v-model=\"showConfirm\"\n :title=\"confirmTitle ?? 'Save to Experiment'\"\n :message=\"confirmMessage ?? `Save current data to ${experimentName}?`\"\n variant=\"info\"\n confirm-label=\"Save\"\n :loading=\"saveLoading\"\n @confirm=\"handleConfirmSave\"\n />\n </div>\n</template>\n\n<style>\n@import '../styles/components/experiment-popover.css';\n</style>\n"],"names":["_createElementBlock","_createElementVNode","_normalizeClass","_toDisplayString","_openBlock","_createVNode","ConfirmDialog"],"mappings":"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"}
@@ -32,8 +32,8 @@ declare const __VLS_component: import('vue').DefineComponent<Props, {}, {}, {},
32
32
  progress: number;
33
33
  indeterminate: boolean;
34
34
  state: FitState;
35
- results: FitResultSummary[];
36
35
  cancelLabel: string;
36
+ results: FitResultSummary[];
37
37
  progressLabel: string;
38
38
  runLabel: string;
39
39
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
@@ -6,6 +6,7 @@ const TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1e3;
6
6
  const TOKEN_REFRESH_CHECK_INTERVAL_MS = 60 * 1e3;
7
7
  let _refreshPromise = null;
8
8
  let _refreshTimerId = null;
9
+ let _mountedConsumerCount = 0;
9
10
  function useAuth() {
10
11
  const authStore = useAuthStore();
11
12
  const settingsStore = useSettingsStore();
@@ -246,14 +247,18 @@ function useAuth() {
246
247
  let checkInterval = null;
247
248
  if (getCurrentInstance()) {
248
249
  onMounted(() => {
250
+ _mountedConsumerCount += 1;
249
251
  checkInterval = window.setInterval(checkAndRefreshIfNeeded, TOKEN_REFRESH_CHECK_INTERVAL_MS);
250
252
  });
251
253
  onUnmounted(() => {
252
- stopTokenRefresh();
253
254
  if (checkInterval !== null) {
254
255
  window.clearInterval(checkInterval);
255
256
  checkInterval = null;
256
257
  }
258
+ _mountedConsumerCount = Math.max(0, _mountedConsumerCount - 1);
259
+ if (_mountedConsumerCount === 0) {
260
+ stopTokenRefresh();
261
+ }
257
262
  });
258
263
  watch(
259
264
  () => authStore.tokenExpires,
@@ -1 +1 @@
1
- {"version":3,"file":"useAuth.js","sources":["../../src/composables/useAuth.ts"],"sourcesContent":["import axios from 'axios'\nimport { ref, onMounted, onUnmounted, watch, getCurrentInstance, type Ref } from 'vue'\nimport { useAuthStore } from '../stores/auth'\nimport { useSettingsStore } from '../stores/settings'\nimport type { AuthConfig, UserInfo, LoginResponse, TokenVerifyResponse, UpdateProfileRequest } from '../types'\n\ninterface UserResponse {\n id: string\n username: string\n shortname: string | null\n email: string | null\n role: string\n is_active: boolean\n}\n\ninterface RefreshResponse {\n access_token: string\n expires_in: number\n token_type: string\n}\n\n// Token refresh configuration\nconst TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000 // Refresh 5 minutes before expiry\nconst TOKEN_REFRESH_CHECK_INTERVAL_MS = 60 * 1000 // Check every minute\n\n/**\n * Authentication composable with automatic token refresh.\n *\n * Features:\n * - Automatic token refresh before expiration\n * - Login/logout/register functionality\n * - Token verification on startup\n * - User profile management\n *\n * @example\n * ```typescript\n * const { login, logout, isAuthenticated, user } = useAuth()\n *\n * // Login\n * const success = await login('username', 'password')\n *\n * // Automatic refresh is enabled by default\n * // Tokens are refreshed 5 minutes before expiration\n * ```\n */\nexport interface UseAuthReturn {\n login: (username: string, password: string) => Promise<boolean>\n logout: () => void\n register: (username: string, password: string, email?: string) => Promise<boolean>\n verifyToken: () => Promise<boolean>\n fetchAuthConfig: () => Promise<AuthConfig>\n initializeAuth: () => Promise<void>\n getCurrentUser: () => Promise<UserInfo | null>\n getAuthHeader: () => Record<string, string>\n updateProfile: (data: { email?: string; shortname?: string; currentPassword?: string; newPassword?: string }) => Promise<{ success: boolean; error?: string }>\n refreshToken: () => Promise<boolean>\n isRefreshing: Ref<boolean>\n}\n\n// Module-level singletons to prevent duplicate refresh requests and timers\n// across multiple useAuth() instances\nlet _refreshPromise: Promise<boolean> | null = null\nlet _refreshTimerId: number | null = null\n\nexport function useAuth(): UseAuthReturn {\n const authStore = useAuthStore()\n const settingsStore = useSettingsStore()\n\n const isRefreshing = ref(false)\n\n function getApiBaseUrl(): string {\n return settingsStore.getApiBaseUrl()\n }\n\n async function fetchAuthConfig(): Promise<AuthConfig> {\n try {\n const response = await axios.get<{\n auth_required: boolean\n passkey_enabled: boolean\n passkey_registered?: boolean\n registration_enabled?: boolean\n database_mode?: string\n }>(`${getApiBaseUrl()}/setup/config/public`)\n\n const config: AuthConfig = {\n authRequired: response.data.auth_required,\n passkeyEnabled: response.data.passkey_enabled,\n passkeyRegistered: response.data.passkey_registered ?? false,\n registrationEnabled: response.data.registration_enabled ?? false,\n databaseMode: response.data.database_mode ?? 'none',\n }\n\n authStore.setAuthConfig(config)\n return config\n } catch (error) {\n console.error('Failed to fetch auth config:', error)\n return {\n authRequired: false,\n passkeyEnabled: false,\n passkeyRegistered: false,\n registrationEnabled: false,\n databaseMode: 'none',\n }\n }\n }\n\n async function login(username: string, password: string): Promise<boolean> {\n authStore.setLoading(true)\n authStore.setError(null)\n\n try {\n const response = await axios.post<LoginResponse>(\n `${getApiBaseUrl()}/auth/login`,\n { username, password }\n )\n\n authStore.setToken(response.data.access_token, response.data.expires_in)\n authStore.setUsername(username)\n\n await getCurrentUser()\n\n // Start auto-refresh after successful login\n scheduleTokenRefresh()\n\n return true\n } catch (error) {\n if (axios.isAxiosError(error) && error.response) {\n authStore.setError(error.response.data.detail || 'Login failed')\n } else {\n authStore.setError('Network error. Please try again.')\n }\n return false\n } finally {\n authStore.setLoading(false)\n }\n }\n\n async function register(username: string, password: string, email?: string): Promise<boolean> {\n authStore.setLoading(true)\n authStore.setError(null)\n\n try {\n await axios.post<UserResponse>(\n `${getApiBaseUrl()}/users/register`,\n { username, password, email }\n )\n\n return await login(username, password)\n } catch (error) {\n if (axios.isAxiosError(error) && error.response) {\n authStore.setError(error.response.data.detail || 'Registration failed')\n } else {\n authStore.setError('Network error. Please try again.')\n }\n return false\n } finally {\n authStore.setLoading(false)\n }\n }\n\n async function getCurrentUser(): Promise<UserInfo | null> {\n if (!authStore.token) {\n return null\n }\n\n try {\n const response = await axios.get<UserResponse>(\n `${getApiBaseUrl()}/users/me`,\n { headers: getAuthHeader() }\n )\n\n const userInfo: UserInfo = {\n id: response.data.id,\n username: response.data.username,\n shortname: response.data.shortname,\n email: response.data.email,\n role: response.data.role,\n isActive: response.data.is_active,\n }\n\n authStore.setUserInfo(userInfo)\n return userInfo\n } catch {\n return null\n }\n }\n\n async function verifyToken(): Promise<boolean> {\n if (!authStore.token) {\n return false\n }\n\n try {\n const response = await axios.get<TokenVerifyResponse>(\n `${getApiBaseUrl()}/auth/verify`,\n {\n headers: {\n Authorization: `Bearer ${authStore.token}`,\n },\n }\n )\n\n if (response.data.valid && response.data.username) {\n authStore.setUsername(response.data.username)\n return true\n }\n\n authStore.clearToken()\n return false\n } catch {\n authStore.clearToken()\n return false\n }\n }\n\n /**\n * Refresh the authentication token.\n * Called automatically before token expiration.\n * Uses promise caching to prevent concurrent refresh requests.\n */\n async function refreshToken(): Promise<boolean> {\n if (!authStore.token) return false\n if (_refreshPromise) return _refreshPromise\n\n _refreshPromise = (async () => {\n isRefreshing.value = true\n\n try {\n const response = await axios.post<RefreshResponse>(\n `${getApiBaseUrl()}/auth/refresh`,\n {},\n { headers: getAuthHeader() }\n )\n\n authStore.setToken(response.data.access_token, response.data.expires_in)\n\n // Reschedule next refresh\n scheduleTokenRefresh()\n\n return true\n } catch (error) {\n // If refresh fails, the token may have been revoked\n // Clear auth state and let user re-login\n if (axios.isAxiosError(error) && error.response?.status === 401) {\n console.warn('[Auth] Token refresh failed - session expired')\n authStore.clearToken()\n stopTokenRefresh()\n }\n return false\n } finally {\n isRefreshing.value = false\n _refreshPromise = null\n }\n })()\n\n return _refreshPromise\n }\n\n /**\n * Schedule automatic token refresh before expiration.\n */\n function scheduleTokenRefresh(): void {\n // Clear any existing timer\n stopTokenRefresh()\n\n if (!authStore.tokenExpires) {\n return\n }\n\n const expiresAt = authStore.tokenExpires.getTime()\n const refreshAt = expiresAt - TOKEN_REFRESH_MARGIN_MS\n const now = Date.now()\n\n if (refreshAt <= now) {\n // Token is already close to expiring or expired, refresh now\n refreshToken()\n return\n }\n\n // Schedule refresh\n const delay = refreshAt - now\n _refreshTimerId = window.setTimeout(() => {\n refreshToken()\n }, delay)\n }\n\n /**\n * Stop automatic token refresh.\n */\n function stopTokenRefresh(): void {\n if (_refreshTimerId !== null) {\n window.clearTimeout(_refreshTimerId)\n _refreshTimerId = null\n }\n }\n\n /**\n * Check if token needs refresh and refresh if necessary.\n * Called periodically as a safety net.\n */\n function checkAndRefreshIfNeeded(): void {\n if (!authStore.token || !authStore.tokenExpires) {\n return\n }\n\n const expiresAt = authStore.tokenExpires.getTime()\n const refreshAt = expiresAt - TOKEN_REFRESH_MARGIN_MS\n const now = Date.now()\n\n if (now >= refreshAt) {\n refreshToken()\n }\n }\n\n async function initializeAuth(): Promise<void> {\n authStore.initialize()\n await fetchAuthConfig()\n\n if (authStore.token) {\n const valid = await verifyToken()\n if (valid) {\n await getCurrentUser()\n // Start auto-refresh for existing session\n scheduleTokenRefresh()\n }\n }\n }\n\n function logout(): void {\n stopTokenRefresh()\n authStore.logout()\n }\n\n function getAuthHeader(): Record<string, string> {\n if (authStore.token) {\n return { Authorization: `Bearer ${authStore.token}` }\n }\n return {}\n }\n\n async function updateProfile(data: {\n email?: string\n shortname?: string\n currentPassword?: string\n newPassword?: string\n }): Promise<{ success: boolean; error?: string }> {\n if (!authStore.token) {\n return { success: false, error: 'Not authenticated' }\n }\n\n try {\n const requestData: UpdateProfileRequest = {}\n if (data.email !== undefined) requestData.email = data.email\n if (data.shortname !== undefined) requestData.shortname = data.shortname\n if (data.currentPassword) requestData.current_password = data.currentPassword\n if (data.newPassword) requestData.new_password = data.newPassword\n\n const response = await axios.put<UserResponse>(\n `${getApiBaseUrl()}/users/me`,\n requestData,\n { headers: getAuthHeader() }\n )\n\n const userInfo: UserInfo = {\n id: response.data.id,\n username: response.data.username,\n shortname: response.data.shortname,\n email: response.data.email,\n role: response.data.role,\n isActive: response.data.is_active,\n }\n authStore.setUserInfo(userInfo)\n\n return { success: true }\n } catch (error) {\n if (axios.isAxiosError(error) && error.response) {\n return { success: false, error: error.response.data.detail || 'Update failed' }\n }\n return { success: false, error: 'Network error. Please try again.' }\n }\n }\n\n // Set up periodic check as safety net (only inside component setup)\n let checkInterval: number | null = null\n\n if (getCurrentInstance()) {\n onMounted(() => {\n checkInterval = window.setInterval(checkAndRefreshIfNeeded, TOKEN_REFRESH_CHECK_INTERVAL_MS)\n })\n\n onUnmounted(() => {\n stopTokenRefresh()\n if (checkInterval !== null) {\n window.clearInterval(checkInterval)\n checkInterval = null\n }\n })\n\n // Watch for token changes to reschedule refresh\n watch(\n () => authStore.tokenExpires,\n (newExpires) => {\n if (newExpires) {\n scheduleTokenRefresh()\n } else {\n stopTokenRefresh()\n }\n }\n )\n }\n\n return {\n // Core auth methods\n login,\n logout,\n register,\n verifyToken,\n fetchAuthConfig,\n initializeAuth,\n getCurrentUser,\n getAuthHeader,\n updateProfile,\n\n // Token refresh\n refreshToken,\n isRefreshing,\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useAuth.js","sources":["../../src/composables/useAuth.ts"],"sourcesContent":["import axios from 'axios'\nimport { ref, onMounted, onUnmounted, watch, getCurrentInstance, type Ref } from 'vue'\nimport { useAuthStore } from '../stores/auth'\nimport { useSettingsStore } from '../stores/settings'\nimport type { AuthConfig, UserInfo, LoginResponse, TokenVerifyResponse, UpdateProfileRequest } from '../types'\n\ninterface UserResponse {\n id: string\n username: string\n shortname: string | null\n email: string | null\n role: string\n is_active: boolean\n}\n\ninterface RefreshResponse {\n access_token: string\n expires_in: number\n token_type: string\n}\n\n// Token refresh configuration\nconst TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000 // Refresh 5 minutes before expiry\nconst TOKEN_REFRESH_CHECK_INTERVAL_MS = 60 * 1000 // Check every minute\n\n/**\n * Authentication composable with automatic token refresh.\n *\n * Features:\n * - Automatic token refresh before expiration\n * - Login/logout/register functionality\n * - Token verification on startup\n * - User profile management\n *\n * @example\n * ```typescript\n * const { login, logout, isAuthenticated, user } = useAuth()\n *\n * // Login\n * const success = await login('username', 'password')\n *\n * // Automatic refresh is enabled by default\n * // Tokens are refreshed 5 minutes before expiration\n * ```\n */\nexport interface UseAuthReturn {\n login: (username: string, password: string) => Promise<boolean>\n logout: () => void\n register: (username: string, password: string, email?: string) => Promise<boolean>\n verifyToken: () => Promise<boolean>\n fetchAuthConfig: () => Promise<AuthConfig>\n initializeAuth: () => Promise<void>\n getCurrentUser: () => Promise<UserInfo | null>\n getAuthHeader: () => Record<string, string>\n updateProfile: (data: { email?: string; shortname?: string; currentPassword?: string; newPassword?: string }) => Promise<{ success: boolean; error?: string }>\n refreshToken: () => Promise<boolean>\n isRefreshing: Ref<boolean>\n}\n\n// Module-level singletons to prevent duplicate refresh requests and timers\n// across multiple useAuth() instances\nlet _refreshPromise: Promise<boolean> | null = null\nlet _refreshTimerId: number | null = null\nlet _mountedConsumerCount = 0\n\nexport function useAuth(): UseAuthReturn {\n const authStore = useAuthStore()\n const settingsStore = useSettingsStore()\n\n const isRefreshing = ref(false)\n\n function getApiBaseUrl(): string {\n return settingsStore.getApiBaseUrl()\n }\n\n async function fetchAuthConfig(): Promise<AuthConfig> {\n try {\n const response = await axios.get<{\n auth_required: boolean\n passkey_enabled: boolean\n passkey_registered?: boolean\n registration_enabled?: boolean\n database_mode?: string\n }>(`${getApiBaseUrl()}/setup/config/public`)\n\n const config: AuthConfig = {\n authRequired: response.data.auth_required,\n passkeyEnabled: response.data.passkey_enabled,\n passkeyRegistered: response.data.passkey_registered ?? false,\n registrationEnabled: response.data.registration_enabled ?? false,\n databaseMode: response.data.database_mode ?? 'none',\n }\n\n authStore.setAuthConfig(config)\n return config\n } catch (error) {\n console.error('Failed to fetch auth config:', error)\n return {\n authRequired: false,\n passkeyEnabled: false,\n passkeyRegistered: false,\n registrationEnabled: false,\n databaseMode: 'none',\n }\n }\n }\n\n async function login(username: string, password: string): Promise<boolean> {\n authStore.setLoading(true)\n authStore.setError(null)\n\n try {\n const response = await axios.post<LoginResponse>(\n `${getApiBaseUrl()}/auth/login`,\n { username, password }\n )\n\n authStore.setToken(response.data.access_token, response.data.expires_in)\n authStore.setUsername(username)\n\n await getCurrentUser()\n\n // Start auto-refresh after successful login\n scheduleTokenRefresh()\n\n return true\n } catch (error) {\n if (axios.isAxiosError(error) && error.response) {\n authStore.setError(error.response.data.detail || 'Login failed')\n } else {\n authStore.setError('Network error. Please try again.')\n }\n return false\n } finally {\n authStore.setLoading(false)\n }\n }\n\n async function register(username: string, password: string, email?: string): Promise<boolean> {\n authStore.setLoading(true)\n authStore.setError(null)\n\n try {\n await axios.post<UserResponse>(\n `${getApiBaseUrl()}/users/register`,\n { username, password, email }\n )\n\n return await login(username, password)\n } catch (error) {\n if (axios.isAxiosError(error) && error.response) {\n authStore.setError(error.response.data.detail || 'Registration failed')\n } else {\n authStore.setError('Network error. Please try again.')\n }\n return false\n } finally {\n authStore.setLoading(false)\n }\n }\n\n async function getCurrentUser(): Promise<UserInfo | null> {\n if (!authStore.token) {\n return null\n }\n\n try {\n const response = await axios.get<UserResponse>(\n `${getApiBaseUrl()}/users/me`,\n { headers: getAuthHeader() }\n )\n\n const userInfo: UserInfo = {\n id: response.data.id,\n username: response.data.username,\n shortname: response.data.shortname,\n email: response.data.email,\n role: response.data.role,\n isActive: response.data.is_active,\n }\n\n authStore.setUserInfo(userInfo)\n return userInfo\n } catch {\n return null\n }\n }\n\n async function verifyToken(): Promise<boolean> {\n if (!authStore.token) {\n return false\n }\n\n try {\n const response = await axios.get<TokenVerifyResponse>(\n `${getApiBaseUrl()}/auth/verify`,\n {\n headers: {\n Authorization: `Bearer ${authStore.token}`,\n },\n }\n )\n\n if (response.data.valid && response.data.username) {\n authStore.setUsername(response.data.username)\n return true\n }\n\n authStore.clearToken()\n return false\n } catch {\n authStore.clearToken()\n return false\n }\n }\n\n /**\n * Refresh the authentication token.\n * Called automatically before token expiration.\n * Uses promise caching to prevent concurrent refresh requests.\n */\n async function refreshToken(): Promise<boolean> {\n if (!authStore.token) return false\n if (_refreshPromise) return _refreshPromise\n\n _refreshPromise = (async () => {\n isRefreshing.value = true\n\n try {\n const response = await axios.post<RefreshResponse>(\n `${getApiBaseUrl()}/auth/refresh`,\n {},\n { headers: getAuthHeader() }\n )\n\n authStore.setToken(response.data.access_token, response.data.expires_in)\n\n // Reschedule next refresh\n scheduleTokenRefresh()\n\n return true\n } catch (error) {\n // If refresh fails, the token may have been revoked\n // Clear auth state and let user re-login\n if (axios.isAxiosError(error) && error.response?.status === 401) {\n console.warn('[Auth] Token refresh failed - session expired')\n authStore.clearToken()\n stopTokenRefresh()\n }\n return false\n } finally {\n isRefreshing.value = false\n _refreshPromise = null\n }\n })()\n\n return _refreshPromise\n }\n\n /**\n * Schedule automatic token refresh before expiration.\n */\n function scheduleTokenRefresh(): void {\n // Clear any existing timer\n stopTokenRefresh()\n\n if (!authStore.tokenExpires) {\n return\n }\n\n const expiresAt = authStore.tokenExpires.getTime()\n const refreshAt = expiresAt - TOKEN_REFRESH_MARGIN_MS\n const now = Date.now()\n\n if (refreshAt <= now) {\n // Token is already close to expiring or expired, refresh now\n refreshToken()\n return\n }\n\n // Schedule refresh\n const delay = refreshAt - now\n _refreshTimerId = window.setTimeout(() => {\n refreshToken()\n }, delay)\n }\n\n /**\n * Stop automatic token refresh.\n */\n function stopTokenRefresh(): void {\n if (_refreshTimerId !== null) {\n window.clearTimeout(_refreshTimerId)\n _refreshTimerId = null\n }\n }\n\n /**\n * Check if token needs refresh and refresh if necessary.\n * Called periodically as a safety net.\n */\n function checkAndRefreshIfNeeded(): void {\n if (!authStore.token || !authStore.tokenExpires) {\n return\n }\n\n const expiresAt = authStore.tokenExpires.getTime()\n const refreshAt = expiresAt - TOKEN_REFRESH_MARGIN_MS\n const now = Date.now()\n\n if (now >= refreshAt) {\n refreshToken()\n }\n }\n\n async function initializeAuth(): Promise<void> {\n authStore.initialize()\n await fetchAuthConfig()\n\n if (authStore.token) {\n const valid = await verifyToken()\n if (valid) {\n await getCurrentUser()\n // Start auto-refresh for existing session\n scheduleTokenRefresh()\n }\n }\n }\n\n function logout(): void {\n stopTokenRefresh()\n authStore.logout()\n }\n\n function getAuthHeader(): Record<string, string> {\n if (authStore.token) {\n return { Authorization: `Bearer ${authStore.token}` }\n }\n return {}\n }\n\n async function updateProfile(data: {\n email?: string\n shortname?: string\n currentPassword?: string\n newPassword?: string\n }): Promise<{ success: boolean; error?: string }> {\n if (!authStore.token) {\n return { success: false, error: 'Not authenticated' }\n }\n\n try {\n const requestData: UpdateProfileRequest = {}\n if (data.email !== undefined) requestData.email = data.email\n if (data.shortname !== undefined) requestData.shortname = data.shortname\n if (data.currentPassword) requestData.current_password = data.currentPassword\n if (data.newPassword) requestData.new_password = data.newPassword\n\n const response = await axios.put<UserResponse>(\n `${getApiBaseUrl()}/users/me`,\n requestData,\n { headers: getAuthHeader() }\n )\n\n const userInfo: UserInfo = {\n id: response.data.id,\n username: response.data.username,\n shortname: response.data.shortname,\n email: response.data.email,\n role: response.data.role,\n isActive: response.data.is_active,\n }\n authStore.setUserInfo(userInfo)\n\n return { success: true }\n } catch (error) {\n if (axios.isAxiosError(error) && error.response) {\n return { success: false, error: error.response.data.detail || 'Update failed' }\n }\n return { success: false, error: 'Network error. Please try again.' }\n }\n }\n\n // Set up periodic check as safety net (only inside component setup)\n let checkInterval: number | null = null\n\n if (getCurrentInstance()) {\n onMounted(() => {\n _mountedConsumerCount += 1\n checkInterval = window.setInterval(checkAndRefreshIfNeeded, TOKEN_REFRESH_CHECK_INTERVAL_MS)\n })\n\n onUnmounted(() => {\n if (checkInterval !== null) {\n window.clearInterval(checkInterval)\n checkInterval = null\n }\n\n _mountedConsumerCount = Math.max(0, _mountedConsumerCount - 1)\n if (_mountedConsumerCount === 0) {\n stopTokenRefresh()\n }\n })\n\n // Watch for token changes to reschedule refresh\n watch(\n () => authStore.tokenExpires,\n (newExpires) => {\n if (newExpires) {\n scheduleTokenRefresh()\n } else {\n stopTokenRefresh()\n }\n }\n )\n }\n\n return {\n // Core auth methods\n login,\n logout,\n register,\n verifyToken,\n fetchAuthConfig,\n initializeAuth,\n getCurrentUser,\n getAuthHeader,\n updateProfile,\n\n // Token refresh\n refreshToken,\n isRefreshing,\n }\n}\n"],"names":[],"mappings":";;;;AAsBA,MAAM,0BAA0B,IAAI,KAAK;AACzC,MAAM,kCAAkC,KAAK;AAsC7C,IAAI,kBAA2C;AAC/C,IAAI,kBAAiC;AACrC,IAAI,wBAAwB;AAErB,SAAS,UAAyB;AACvC,QAAM,YAAY,aAAA;AAClB,QAAM,gBAAgB,iBAAA;AAEtB,QAAM,eAAe,IAAI,KAAK;AAE9B,WAAS,gBAAwB;AAC/B,WAAO,cAAc,cAAA;AAAA,EACvB;AAEA,iBAAe,kBAAuC;AACpD,QAAI;AACF,YAAM,WAAW,MAAM,MAAM,IAM1B,GAAG,cAAA,CAAe,sBAAsB;AAE3C,YAAM,SAAqB;AAAA,QACzB,cAAc,SAAS,KAAK;AAAA,QAC5B,gBAAgB,SAAS,KAAK;AAAA,QAC9B,mBAAmB,SAAS,KAAK,sBAAsB;AAAA,QACvD,qBAAqB,SAAS,KAAK,wBAAwB;AAAA,QAC3D,cAAc,SAAS,KAAK,iBAAiB;AAAA,MAAA;AAG/C,gBAAU,cAAc,MAAM;AAC9B,aAAO;AAAA,IACT,SAAS,OAAO;AACd,cAAQ,MAAM,gCAAgC,KAAK;AACnD,aAAO;AAAA,QACL,cAAc;AAAA,QACd,gBAAgB;AAAA,QAChB,mBAAmB;AAAA,QACnB,qBAAqB;AAAA,QACrB,cAAc;AAAA,MAAA;AAAA,IAElB;AAAA,EACF;AAEA,iBAAe,MAAM,UAAkB,UAAoC;AACzE,cAAU,WAAW,IAAI;AACzB,cAAU,SAAS,IAAI;AAEvB,QAAI;AACF,YAAM,WAAW,MAAM,MAAM;AAAA,QAC3B,GAAG,eAAe;AAAA,QAClB,EAAE,UAAU,SAAA;AAAA,MAAS;AAGvB,gBAAU,SAAS,SAAS,KAAK,cAAc,SAAS,KAAK,UAAU;AACvE,gBAAU,YAAY,QAAQ;AAE9B,YAAM,eAAA;AAGN,2BAAA;AAEA,aAAO;AAAA,IACT,SAAS,OAAO;AACd,UAAI,MAAM,aAAa,KAAK,KAAK,MAAM,UAAU;AAC/C,kBAAU,SAAS,MAAM,SAAS,KAAK,UAAU,cAAc;AAAA,MACjE,OAAO;AACL,kBAAU,SAAS,kCAAkC;AAAA,MACvD;AACA,aAAO;AAAA,IACT,UAAA;AACE,gBAAU,WAAW,KAAK;AAAA,IAC5B;AAAA,EACF;AAEA,iBAAe,SAAS,UAAkB,UAAkB,OAAkC;AAC5F,cAAU,WAAW,IAAI;AACzB,cAAU,SAAS,IAAI;AAEvB,QAAI;AACF,YAAM,MAAM;AAAA,QACV,GAAG,eAAe;AAAA,QAClB,EAAE,UAAU,UAAU,MAAA;AAAA,MAAM;AAG9B,aAAO,MAAM,MAAM,UAAU,QAAQ;AAAA,IACvC,SAAS,OAAO;AACd,UAAI,MAAM,aAAa,KAAK,KAAK,MAAM,UAAU;AAC/C,kBAAU,SAAS,MAAM,SAAS,KAAK,UAAU,qBAAqB;AAAA,MACxE,OAAO;AACL,kBAAU,SAAS,kCAAkC;AAAA,MACvD;AACA,aAAO;AAAA,IACT,UAAA;AACE,gBAAU,WAAW,KAAK;AAAA,IAC5B;AAAA,EACF;AAEA,iBAAe,iBAA2C;AACxD,QAAI,CAAC,UAAU,OAAO;AACpB,aAAO;AAAA,IACT;AAEA,QAAI;AACF,YAAM,WAAW,MAAM,MAAM;AAAA,QAC3B,GAAG,eAAe;AAAA,QAClB,EAAE,SAAS,cAAA,EAAc;AAAA,MAAE;AAG7B,YAAM,WAAqB;AAAA,QACzB,IAAI,SAAS,KAAK;AAAA,QAClB,UAAU,SAAS,KAAK;AAAA,QACxB,WAAW,SAAS,KAAK;AAAA,QACzB,OAAO,SAAS,KAAK;AAAA,QACrB,MAAM,SAAS,KAAK;AAAA,QACpB,UAAU,SAAS,KAAK;AAAA,MAAA;AAG1B,gBAAU,YAAY,QAAQ;AAC9B,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,iBAAe,cAAgC;AAC7C,QAAI,CAAC,UAAU,OAAO;AACpB,aAAO;AAAA,IACT;AAEA,QAAI;AACF,YAAM,WAAW,MAAM,MAAM;AAAA,QAC3B,GAAG,eAAe;AAAA,QAClB;AAAA,UACE,SAAS;AAAA,YACP,eAAe,UAAU,UAAU,KAAK;AAAA,UAAA;AAAA,QAC1C;AAAA,MACF;AAGF,UAAI,SAAS,KAAK,SAAS,SAAS,KAAK,UAAU;AACjD,kBAAU,YAAY,SAAS,KAAK,QAAQ;AAC5C,eAAO;AAAA,MACT;AAEA,gBAAU,WAAA;AACV,aAAO;AAAA,IACT,QAAQ;AACN,gBAAU,WAAA;AACV,aAAO;AAAA,IACT;AAAA,EACF;AAOA,iBAAe,eAAiC;AAC9C,QAAI,CAAC,UAAU,MAAO,QAAO;AAC7B,QAAI,gBAAiB,QAAO;AAE5B,uBAAmB,YAAY;;AAC7B,mBAAa,QAAQ;AAErB,UAAI;AACF,cAAM,WAAW,MAAM,MAAM;AAAA,UAC3B,GAAG,eAAe;AAAA,UAClB,CAAA;AAAA,UACA,EAAE,SAAS,cAAA,EAAc;AAAA,QAAE;AAG7B,kBAAU,SAAS,SAAS,KAAK,cAAc,SAAS,KAAK,UAAU;AAGvE,6BAAA;AAEA,eAAO;AAAA,MACT,SAAS,OAAO;AAGd,YAAI,MAAM,aAAa,KAAK,OAAK,WAAM,aAAN,mBAAgB,YAAW,KAAK;AAC/D,kBAAQ,KAAK,+CAA+C;AAC5D,oBAAU,WAAA;AACV,2BAAA;AAAA,QACF;AACA,eAAO;AAAA,MACT,UAAA;AACE,qBAAa,QAAQ;AACrB,0BAAkB;AAAA,MACpB;AAAA,IACF,GAAA;AAEA,WAAO;AAAA,EACT;AAKA,WAAS,uBAA6B;AAEpC,qBAAA;AAEA,QAAI,CAAC,UAAU,cAAc;AAC3B;AAAA,IACF;AAEA,UAAM,YAAY,UAAU,aAAa,QAAA;AACzC,UAAM,YAAY,YAAY;AAC9B,UAAM,MAAM,KAAK,IAAA;AAEjB,QAAI,aAAa,KAAK;AAEpB,mBAAA;AACA;AAAA,IACF;AAGA,UAAM,QAAQ,YAAY;AAC1B,sBAAkB,OAAO,WAAW,MAAM;AACxC,mBAAA;AAAA,IACF,GAAG,KAAK;AAAA,EACV;AAKA,WAAS,mBAAyB;AAChC,QAAI,oBAAoB,MAAM;AAC5B,aAAO,aAAa,eAAe;AACnC,wBAAkB;AAAA,IACpB;AAAA,EACF;AAMA,WAAS,0BAAgC;AACvC,QAAI,CAAC,UAAU,SAAS,CAAC,UAAU,cAAc;AAC/C;AAAA,IACF;AAEA,UAAM,YAAY,UAAU,aAAa,QAAA;AACzC,UAAM,YAAY,YAAY;AAC9B,UAAM,MAAM,KAAK,IAAA;AAEjB,QAAI,OAAO,WAAW;AACpB,mBAAA;AAAA,IACF;AAAA,EACF;AAEA,iBAAe,iBAAgC;AAC7C,cAAU,WAAA;AACV,UAAM,gBAAA;AAEN,QAAI,UAAU,OAAO;AACnB,YAAM,QAAQ,MAAM,YAAA;AACpB,UAAI,OAAO;AACT,cAAM,eAAA;AAEN,6BAAA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,WAAS,SAAe;AACtB,qBAAA;AACA,cAAU,OAAA;AAAA,EACZ;AAEA,WAAS,gBAAwC;AAC/C,QAAI,UAAU,OAAO;AACnB,aAAO,EAAE,eAAe,UAAU,UAAU,KAAK,GAAA;AAAA,IACnD;AACA,WAAO,CAAA;AAAA,EACT;AAEA,iBAAe,cAAc,MAKqB;AAChD,QAAI,CAAC,UAAU,OAAO;AACpB,aAAO,EAAE,SAAS,OAAO,OAAO,oBAAA;AAAA,IAClC;AAEA,QAAI;AACF,YAAM,cAAoC,CAAA;AAC1C,UAAI,KAAK,UAAU,OAAW,aAAY,QAAQ,KAAK;AACvD,UAAI,KAAK,cAAc,OAAW,aAAY,YAAY,KAAK;AAC/D,UAAI,KAAK,gBAAiB,aAAY,mBAAmB,KAAK;AAC9D,UAAI,KAAK,YAAa,aAAY,eAAe,KAAK;AAEtD,YAAM,WAAW,MAAM,MAAM;AAAA,QAC3B,GAAG,eAAe;AAAA,QAClB;AAAA,QACA,EAAE,SAAS,cAAA,EAAc;AAAA,MAAE;AAG7B,YAAM,WAAqB;AAAA,QACzB,IAAI,SAAS,KAAK;AAAA,QAClB,UAAU,SAAS,KAAK;AAAA,QACxB,WAAW,SAAS,KAAK;AAAA,QACzB,OAAO,SAAS,KAAK;AAAA,QACrB,MAAM,SAAS,KAAK;AAAA,QACpB,UAAU,SAAS,KAAK;AAAA,MAAA;AAE1B,gBAAU,YAAY,QAAQ;AAE9B,aAAO,EAAE,SAAS,KAAA;AAAA,IACpB,SAAS,OAAO;AACd,UAAI,MAAM,aAAa,KAAK,KAAK,MAAM,UAAU;AAC/C,eAAO,EAAE,SAAS,OAAO,OAAO,MAAM,SAAS,KAAK,UAAU,gBAAA;AAAA,MAChE;AACA,aAAO,EAAE,SAAS,OAAO,OAAO,mCAAA;AAAA,IAClC;AAAA,EACF;AAGA,MAAI,gBAA+B;AAEnC,MAAI,sBAAsB;AACxB,cAAU,MAAM;AACd,+BAAyB;AACzB,sBAAgB,OAAO,YAAY,yBAAyB,+BAA+B;AAAA,IAC7F,CAAC;AAED,gBAAY,MAAM;AAChB,UAAI,kBAAkB,MAAM;AAC1B,eAAO,cAAc,aAAa;AAClC,wBAAgB;AAAA,MAClB;AAEA,8BAAwB,KAAK,IAAI,GAAG,wBAAwB,CAAC;AAC7D,UAAI,0BAA0B,GAAG;AAC/B,yBAAA;AAAA,MACF;AAAA,IACF,CAAC;AAGD;AAAA,MACE,MAAM,UAAU;AAAA,MAChB,CAAC,eAAe;AACd,YAAI,YAAY;AACd,+BAAA;AAAA,QACF,OAAO;AACL,2BAAA;AAAA,QACF;AAAA,MACF;AAAA,IAAA;AAAA,EAEJ;AAEA,SAAO;AAAA;AAAA,IAEL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAGA;AAAA,IACA;AAAA,EAAA;AAEJ;"}