@olares/lares-mobile 0.15.4 → 0.15.6
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 +6 -2
- package/package.json +2 -2
- package/src/App.vue +924 -184
- package/src/chat/ChatBar.vue +2 -2
- package/src/chat/Composer.vue +467 -16
- package/src/chat/Deliverables.vue +22 -10
- package/src/chat/HistoryPanel.vue +747 -55
- package/src/chat/MessageActions.vue +88 -34
- package/src/chat/QuestionCard.vue +5 -5
- package/src/chat/StageRow.vue +8 -5
- package/src/chat/Transcript.vue +20 -6
- package/src/chat/composer-tokens.js +32 -0
- package/src/chat/format.js +37 -0
- package/src/desktop/DesktopComposer.vue +871 -0
- package/src/desktop/DesktopContextMeter.vue +81 -0
- package/src/desktop/DesktopGoal.vue +88 -0
- package/src/desktop/DesktopPreview.vue +58 -0
- package/src/desktop/DesktopPreviewTabs.vue +82 -0
- package/src/desktop/DesktopQuestions.vue +118 -0
- package/src/desktop/DesktopQueue.vue +203 -0
- package/src/desktop/DesktopShell.vue +203 -0
- package/src/desktop/DesktopSidebar.vue +645 -0
- package/src/desktop/DesktopTodos.vue +52 -0
- package/src/desktop/DirectoryBrowser.vue +170 -0
- package/src/desktop/DirectoryNewFolder.vue +124 -0
- package/src/desktop/ui/PcDialog.vue +85 -0
- package/src/desktop/ui/PcPopover.vue +110 -0
- package/src/desktop/ui/PcTooltip.vue +95 -0
- package/src/desktop/ui/place.js +24 -0
- package/src/desktop/workspace-view.js +121 -0
- package/src/host.js +20 -2
- package/src/index.d.ts +2 -0
- package/src/mobile/MobileDirectoryNewFolder.vue +121 -0
- package/src/mobile/MobileDirectorySheet.vue +162 -0
- package/src/mobile/MobileNewSession.vue +217 -0
- package/src/mobile/MobileShell.vue +367 -0
- package/src/motion.css +166 -0
- package/src/preview/Preview.vue +91 -150
- package/src/preview/PreviewBody.vue +185 -0
- package/src/settings/AgentSettings.vue +126 -36
- package/src/settings/Select.vue +183 -0
- package/src/settings/SettingRow.vue +5 -4
- package/src/settings/Sheet.vue +4 -4
- package/src/styles.css +9 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<LaresPcPopover v-if="occupancy" v-model="open" :width="260" placement="top-end">
|
|
3
|
+
<template #trigger="{ toggle }">
|
|
4
|
+
<LaresPcTooltip :label="t('context.used', occupancy)" placement="top">
|
|
5
|
+
<button
|
|
6
|
+
type="button"
|
|
7
|
+
class="desktop-context"
|
|
8
|
+
:aria-label="t('context.used', occupancy)"
|
|
9
|
+
@click="toggle"
|
|
10
|
+
>
|
|
11
|
+
<svg viewBox="0 0 20 20" aria-hidden="true">
|
|
12
|
+
<circle cx="10" cy="10" r="7" class="desktop-context__track" />
|
|
13
|
+
<circle
|
|
14
|
+
cx="10"
|
|
15
|
+
cy="10"
|
|
16
|
+
r="7"
|
|
17
|
+
class="desktop-context__value"
|
|
18
|
+
pathLength="100"
|
|
19
|
+
:stroke-dasharray="`${occupancy.percent} 100`"
|
|
20
|
+
/>
|
|
21
|
+
</svg>
|
|
22
|
+
</button>
|
|
23
|
+
</LaresPcTooltip>
|
|
24
|
+
</template>
|
|
25
|
+
<div class="desktop-context__panel">
|
|
26
|
+
<strong>{{ t("context.title") }}</strong>
|
|
27
|
+
<span>{{ t("context.used", occupancy) }}</span>
|
|
28
|
+
<div class="desktop-context__bar"><i :style="{ width: `${occupancy.percent}%` }" /></div>
|
|
29
|
+
<dl v-if="breakdown">
|
|
30
|
+
<template v-for="row in rows" :key="row.key">
|
|
31
|
+
<dt>{{ t(`context.${row.key}`) }}</dt>
|
|
32
|
+
<dd>~{{ row.compact }}</dd>
|
|
33
|
+
</template>
|
|
34
|
+
</dl>
|
|
35
|
+
</div>
|
|
36
|
+
</LaresPcPopover>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script>
|
|
40
|
+
import { occupancyBreakdown, occupancyFromPressure } from "@olares/lares-core/larepass/context-occupancy";
|
|
41
|
+
import LaresPcPopover from "./ui/PcPopover.vue";
|
|
42
|
+
import LaresPcTooltip from "./ui/PcTooltip.vue";
|
|
43
|
+
|
|
44
|
+
export default {
|
|
45
|
+
name: "LaresDesktopContextMeter",
|
|
46
|
+
components: { LaresPcPopover, LaresPcTooltip },
|
|
47
|
+
props: {
|
|
48
|
+
pressure: { type: Object, default: null },
|
|
49
|
+
breakdown: { type: Object, default: null },
|
|
50
|
+
t: { type: Function, required: true },
|
|
51
|
+
},
|
|
52
|
+
data() {
|
|
53
|
+
return { open: false };
|
|
54
|
+
},
|
|
55
|
+
computed: {
|
|
56
|
+
occupancy() {
|
|
57
|
+
return occupancyFromPressure(this.pressure);
|
|
58
|
+
},
|
|
59
|
+
rows() {
|
|
60
|
+
return occupancyBreakdown(this.breakdown);
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<style scoped>
|
|
67
|
+
.desktop-context { display:grid; width:28px; height:28px; place-items:center; border:0; border-radius:50%; padding:0; background:transparent; color:var(--q-ink-3); cursor:pointer; }
|
|
68
|
+
.desktop-context:hover { background:var(--q-background-hover); color:var(--q-ink-2); }
|
|
69
|
+
.desktop-context svg { width:18px; height:18px; transform:rotate(-90deg); fill:none; }
|
|
70
|
+
.desktop-context circle { stroke-width:2.5; }
|
|
71
|
+
.desktop-context__track { stroke:var(--q-separator); }
|
|
72
|
+
.desktop-context__value { stroke:var(--q-blue-default); stroke-linecap:round; }
|
|
73
|
+
.desktop-context__panel { display:flex; flex-direction:column; gap:8px; padding:4px; color:var(--q-ink-2); font-size:12px; }
|
|
74
|
+
.desktop-context__panel strong { color:var(--q-ink-1); font-size:14px; }
|
|
75
|
+
.desktop-context__bar { height:6px; overflow:hidden; border-radius:3px; background:var(--q-background-3); }
|
|
76
|
+
.desktop-context__bar i { display:block; height:100%; border-radius:inherit; background:var(--q-blue-default); }
|
|
77
|
+
.desktop-context__panel dl { display:grid; grid-template-columns:1fr auto; gap:5px 12px; margin:3px 0 0; }
|
|
78
|
+
.desktop-context__panel dt,
|
|
79
|
+
.desktop-context__panel dd { margin:0; }
|
|
80
|
+
.desktop-context__panel dd { color:var(--q-ink-3); }
|
|
81
|
+
</style>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Transition name="lares-panel">
|
|
3
|
+
<div v-if="goal" class="desktop-goal">
|
|
4
|
+
<div class="desktop-goal__body">
|
|
5
|
+
<input
|
|
6
|
+
v-if="editing"
|
|
7
|
+
ref="editor"
|
|
8
|
+
v-model="objective"
|
|
9
|
+
:aria-label="t('goal.edit')"
|
|
10
|
+
@keydown.enter.exact.prevent="save"
|
|
11
|
+
@keydown.esc="editing = false"
|
|
12
|
+
/>
|
|
13
|
+
<template v-else>
|
|
14
|
+
<span class="desktop-goal__mark" aria-hidden="true">◎</span>
|
|
15
|
+
<span class="desktop-goal__text">{{ goal.objective }}</span>
|
|
16
|
+
<span class="desktop-goal__round">{{ goal.roundsStarted || 0 }}/{{ goal.maxGoalRounds }}</span>
|
|
17
|
+
</template>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="desktop-goal__actions">
|
|
20
|
+
<button v-if="editing" type="button" :disabled="busy || !objective.trim()" @click="save">✓</button>
|
|
21
|
+
<button v-if="editing" type="button" :disabled="busy" @click="editing = false">×</button>
|
|
22
|
+
<template v-else>
|
|
23
|
+
<button type="button" :aria-label="t('goal.edit')" :disabled="busy" @click="beginEdit">✎</button>
|
|
24
|
+
<button
|
|
25
|
+
v-if="goal.phase === 'active'"
|
|
26
|
+
type="button"
|
|
27
|
+
:aria-label="t('goal.pause')"
|
|
28
|
+
:disabled="busy"
|
|
29
|
+
@click="$emit('action', 'pause')"
|
|
30
|
+
>Ⅱ</button>
|
|
31
|
+
<button
|
|
32
|
+
v-else-if="goal.phase === 'paused'"
|
|
33
|
+
type="button"
|
|
34
|
+
:aria-label="t('goal.resume')"
|
|
35
|
+
:disabled="busy"
|
|
36
|
+
@click="$emit('action', 'resume')"
|
|
37
|
+
>▶</button>
|
|
38
|
+
<button type="button" :aria-label="t('goal.clear')" :disabled="busy" @click="$emit('action', 'clear')">×</button>
|
|
39
|
+
</template>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</Transition>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script>
|
|
46
|
+
export default {
|
|
47
|
+
name: "LaresDesktopGoal",
|
|
48
|
+
props: {
|
|
49
|
+
projection: { type: Object, default: null },
|
|
50
|
+
busy: { type: Boolean, default: false },
|
|
51
|
+
t: { type: Function, required: true },
|
|
52
|
+
},
|
|
53
|
+
emits: ["action"],
|
|
54
|
+
data() {
|
|
55
|
+
return { editing: false, objective: "" };
|
|
56
|
+
},
|
|
57
|
+
computed: {
|
|
58
|
+
goal() {
|
|
59
|
+
return this.projection?.goal ?? null;
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
methods: {
|
|
63
|
+
beginEdit() {
|
|
64
|
+
this.objective = this.goal?.objective ?? "";
|
|
65
|
+
this.editing = true;
|
|
66
|
+
this.$nextTick(() => this.$refs.editor?.focus?.());
|
|
67
|
+
},
|
|
68
|
+
save() {
|
|
69
|
+
if (!this.objective.trim()) return;
|
|
70
|
+
this.$emit("action", "edit", this.objective.trim());
|
|
71
|
+
this.editing = false;
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<style scoped>
|
|
78
|
+
.desktop-goal { display:flex; box-sizing:border-box; width:calc(100% - 24px); min-height:40px; align-items:center; gap:8px; margin:0 auto -3px; border:1px solid var(--q-separator); border-bottom:0; border-radius:12px 12px 0 0; padding:5px 8px 7px 12px; background:var(--q-background-3); }
|
|
79
|
+
.desktop-goal__body { display:flex; min-width:0; flex:1; align-items:center; gap:8px; }
|
|
80
|
+
.desktop-goal__mark { color:var(--q-blue-default); }
|
|
81
|
+
.desktop-goal__text { overflow:hidden; flex:1; color:var(--q-ink-2); font-size:13px; text-overflow:ellipsis; white-space:nowrap; }
|
|
82
|
+
.desktop-goal__round { color:var(--q-ink-3); font-size:11px; }
|
|
83
|
+
.desktop-goal input { box-sizing:border-box; width:100%; height:28px; border:1px solid var(--q-input-stroke); border-radius:6px; padding:0 8px; outline:none; background:var(--q-background-1); color:var(--q-ink-1); }
|
|
84
|
+
.desktop-goal__actions { display:flex; gap:3px; }
|
|
85
|
+
.desktop-goal button { display:grid; width:27px; height:27px; place-items:center; border:0; border-radius:50%; background:transparent; color:var(--q-ink-3); cursor:pointer; }
|
|
86
|
+
.desktop-goal button:hover:not(:disabled) { background:var(--q-background-hover); color:var(--q-ink-2); }
|
|
87
|
+
.desktop-goal button:disabled { opacity:.45; cursor:default; }
|
|
88
|
+
</style>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="desktop-preview" :aria-label="t('preview')">
|
|
3
|
+
<header class="desktop-preview__bar">
|
|
4
|
+
<span class="desktop-preview__path" :title="path">{{ path }}</span>
|
|
5
|
+
<a
|
|
6
|
+
v-if="downloadHref"
|
|
7
|
+
class="desktop-preview__download"
|
|
8
|
+
:href="downloadHref"
|
|
9
|
+
download
|
|
10
|
+
>{{ t("download") }}</a>
|
|
11
|
+
</header>
|
|
12
|
+
<div class="desktop-preview__content">
|
|
13
|
+
<LaresPreviewBody
|
|
14
|
+
:path="path"
|
|
15
|
+
:session-id="sessionId"
|
|
16
|
+
:status="status"
|
|
17
|
+
:data="data"
|
|
18
|
+
:error="error"
|
|
19
|
+
:media-src="mediaSrc"
|
|
20
|
+
:href-for="hrefFor"
|
|
21
|
+
:t="t"
|
|
22
|
+
@retry="$emit('retry')"
|
|
23
|
+
@open="$emit('open', $event)"
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
26
|
+
</section>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script>
|
|
30
|
+
import LaresPreviewBody from "../preview/PreviewBody.vue";
|
|
31
|
+
|
|
32
|
+
export default {
|
|
33
|
+
name: "LaresDesktopPreview",
|
|
34
|
+
components: { LaresPreviewBody },
|
|
35
|
+
props: {
|
|
36
|
+
path: { type: String, required: true },
|
|
37
|
+
sessionId: { type: String, default: "" },
|
|
38
|
+
status: { type: String, default: "loading" },
|
|
39
|
+
data: { type: Object, default: null },
|
|
40
|
+
error: { type: String, default: "" },
|
|
41
|
+
mediaSrc: { type: String, default: "" },
|
|
42
|
+
downloadHref: { type: String, default: "" },
|
|
43
|
+
hrefFor: { type: Function, default: null },
|
|
44
|
+
t: { type: Function, required: true },
|
|
45
|
+
},
|
|
46
|
+
emits: ["retry", "open"],
|
|
47
|
+
};
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<style scoped>
|
|
51
|
+
/* Covers the transcript and the composer instead of replacing them, so the
|
|
52
|
+
reader's place in the conversation survives a trip through the tabs. */
|
|
53
|
+
.desktop-preview { position:absolute; z-index:8; display:flex; min-width:0; min-height:0; flex-direction:column; inset:0; padding:12px 20px 20px; background:var(--q-background-1); }
|
|
54
|
+
.desktop-preview__bar { display:flex; min-width:0; flex:none; align-items:center; gap:12px; padding-bottom:10px; }
|
|
55
|
+
.desktop-preview__path { min-width:0; flex:1; overflow:hidden; padding:0 4px; color:var(--q-ink-3); font-size:12px; line-height:18px; text-overflow:ellipsis; white-space:nowrap; }
|
|
56
|
+
.desktop-preview__download { flex:none; color:var(--q-blue-default); font-size:12px; line-height:18px; text-decoration:none; }
|
|
57
|
+
.desktop-preview__content { display:flex; min-height:0; flex:1; flex-direction:column; overflow:hidden; border:1px solid var(--q-separator); border-radius:14px; background:var(--q-background-2); }
|
|
58
|
+
</style>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="preview-tabs" role="tablist" :aria-label="t('tabs')">
|
|
3
|
+
<button
|
|
4
|
+
type="button"
|
|
5
|
+
role="tab"
|
|
6
|
+
class="preview-tabs__chat"
|
|
7
|
+
:data-active="active ? 'false' : 'true'"
|
|
8
|
+
:aria-selected="!active"
|
|
9
|
+
@click="$emit('chat')"
|
|
10
|
+
>
|
|
11
|
+
{{ t("chat") }}
|
|
12
|
+
</button>
|
|
13
|
+
<span class="preview-tabs__divider" aria-hidden="true" />
|
|
14
|
+
<TransitionGroup name="lares-tab" tag="div" class="preview-tabs__list">
|
|
15
|
+
<div
|
|
16
|
+
v-for="tab in tabs"
|
|
17
|
+
:key="tab.path"
|
|
18
|
+
class="preview-tabs__tab"
|
|
19
|
+
:data-active="tab.path === active ? 'true' : 'false'"
|
|
20
|
+
role="tab"
|
|
21
|
+
:aria-selected="tab.path === active"
|
|
22
|
+
>
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
class="preview-tabs__label"
|
|
26
|
+
@click="$emit('activate', tab.path)"
|
|
27
|
+
>
|
|
28
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
29
|
+
<path d="M4 1.75h5l3 3V14.25H4zM9 1.75v3h3" />
|
|
30
|
+
</svg>
|
|
31
|
+
<span>{{ tab.name }}</span>
|
|
32
|
+
</button>
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
class="preview-tabs__close"
|
|
36
|
+
:aria-label="t('close', { name: tab.name })"
|
|
37
|
+
@click.stop="$emit('close', tab.path)"
|
|
38
|
+
>
|
|
39
|
+
<svg viewBox="0 0 16 16" aria-hidden="true">
|
|
40
|
+
<path d="m4.5 4.5 7 7m0-7-7 7" />
|
|
41
|
+
</svg>
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</TransitionGroup>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<script>
|
|
49
|
+
export default {
|
|
50
|
+
name: "LaresDesktopPreviewTabs",
|
|
51
|
+
props: {
|
|
52
|
+
tabs: { type: Array, required: true },
|
|
53
|
+
// Empty while the reader is on the chat, so no file tab looks selected.
|
|
54
|
+
active: { type: String, default: "" },
|
|
55
|
+
t: { type: Function, required: true },
|
|
56
|
+
},
|
|
57
|
+
emits: ["chat", "activate", "close"],
|
|
58
|
+
};
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<style scoped>
|
|
62
|
+
.preview-tabs { display:flex; min-width:0; flex:1; align-items:center; gap:8px; }
|
|
63
|
+
.preview-tabs__chat { height:24px; flex:none; padding:0 8px; border:0; border-radius:7px; background:transparent; color:var(--q-ink-3); cursor:pointer; font-size:12px; font-weight:500; }
|
|
64
|
+
.preview-tabs__chat:hover { background:var(--q-background-hover); color:var(--q-ink-1); }
|
|
65
|
+
.preview-tabs__chat[data-active="true"] { background:var(--q-background-3); color:var(--q-blue-default); }
|
|
66
|
+
.preview-tabs__divider { width:1px; height:16px; flex:none; background:var(--q-separator); }
|
|
67
|
+
.preview-tabs__list { position:relative; display:flex; min-width:0; flex:1; align-items:center; gap:6px; overflow-x:auto; scrollbar-width:none; }
|
|
68
|
+
.preview-tabs__chat,
|
|
69
|
+
.preview-tabs__tab,
|
|
70
|
+
.preview-tabs__label,
|
|
71
|
+
.preview-tabs__close { transition:background var(--lares-duration-fast, 120ms) var(--lares-ease-out, ease), color var(--lares-duration-fast, 120ms) var(--lares-ease-out, ease), border-color var(--lares-duration-fast, 120ms) var(--lares-ease-out, ease); }
|
|
72
|
+
.preview-tabs__list::-webkit-scrollbar { display:none; }
|
|
73
|
+
.preview-tabs__tab { display:inline-flex; max-width:180px; height:24px; flex:0 0 auto; align-items:center; padding:0 2px 0 8px; border:1px solid transparent; border-radius:7px; background:var(--q-background-3); color:var(--q-ink-2); }
|
|
74
|
+
.preview-tabs__tab:hover { background:var(--q-background-hover); color:var(--q-ink-1); }
|
|
75
|
+
.preview-tabs__tab[data-active="true"] { border-color:var(--q-input-stroke); background:var(--q-background-1); color:var(--q-ink-1); }
|
|
76
|
+
.preview-tabs__label { display:inline-flex; min-width:0; flex:1; align-items:center; gap:5px; padding:0; border:0; background:transparent; color:inherit; cursor:pointer; font-size:12px; font-weight:500; }
|
|
77
|
+
.preview-tabs__label svg { width:13px; height:13px; flex:none; fill:none; stroke:currentColor; stroke-linecap:round; stroke-linejoin:round; stroke-width:1.3; }
|
|
78
|
+
.preview-tabs__label span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
79
|
+
.preview-tabs__close { display:grid; width:18px; height:18px; flex:none; place-items:center; padding:0; border:0; border-radius:5px; background:transparent; color:var(--q-ink-3); cursor:pointer; }
|
|
80
|
+
.preview-tabs__close:hover { background:var(--q-background-hover); color:var(--q-red-default); }
|
|
81
|
+
.preview-tabs__close svg { width:11px; height:11px; fill:none; stroke:currentColor; stroke-linecap:round; stroke-width:1.8; }
|
|
82
|
+
</style>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="desktop-questions">
|
|
3
|
+
<div class="desktop-questions__strip">{{ current.header || t("question.waiting") }}</div>
|
|
4
|
+
<div class="desktop-questions__body">
|
|
5
|
+
<strong>{{ current.question }}</strong>
|
|
6
|
+
<p v-if="current.detail">{{ current.detail }}</p>
|
|
7
|
+
<label v-for="option in current.options || []" :key="option.label" class="desktop-questions__option">
|
|
8
|
+
<input
|
|
9
|
+
:type="current.multiSelect ? 'checkbox' : 'radio'"
|
|
10
|
+
:name="current.id"
|
|
11
|
+
:value="option.label"
|
|
12
|
+
:checked="selected(current.id).includes(option.label)"
|
|
13
|
+
@change="toggle(option.label, $event.target.checked)"
|
|
14
|
+
/>
|
|
15
|
+
<span><b>{{ option.label }}</b><small v-if="option.description">{{ option.description }}</small></span>
|
|
16
|
+
</label>
|
|
17
|
+
<label class="desktop-questions__other">
|
|
18
|
+
<span>{{ t("question.other") }}</span>
|
|
19
|
+
<input v-model="answers[current.id].custom" type="text" />
|
|
20
|
+
</label>
|
|
21
|
+
</div>
|
|
22
|
+
<footer>
|
|
23
|
+
<button v-if="index > 0" type="button" :disabled="busy" @click="index -= 1">{{ t("question.back") }}</button>
|
|
24
|
+
<span />
|
|
25
|
+
<button
|
|
26
|
+
v-if="index < questions.length - 1"
|
|
27
|
+
type="button"
|
|
28
|
+
:disabled="!answered || busy"
|
|
29
|
+
@click="index += 1"
|
|
30
|
+
>{{ t("question.next") }}</button>
|
|
31
|
+
<button v-else type="button" :disabled="!complete || busy" @click="submit">{{ t("question.submit") }}</button>
|
|
32
|
+
</footer>
|
|
33
|
+
</section>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script>
|
|
37
|
+
function blankAnswers(questions) {
|
|
38
|
+
return Object.fromEntries(questions.map((row) => [row.id, { selected: [], custom: "" }]));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
name: "LaresDesktopQuestions",
|
|
43
|
+
props: {
|
|
44
|
+
questions: { type: Array, required: true },
|
|
45
|
+
busy: { type: Boolean, default: false },
|
|
46
|
+
t: { type: Function, required: true },
|
|
47
|
+
},
|
|
48
|
+
emits: ["answer"],
|
|
49
|
+
data() {
|
|
50
|
+
return { index: 0, answers: blankAnswers(this.questions) };
|
|
51
|
+
},
|
|
52
|
+
computed: {
|
|
53
|
+
current() {
|
|
54
|
+
return this.questions[this.index] ?? {};
|
|
55
|
+
},
|
|
56
|
+
answered() {
|
|
57
|
+
const row = this.answers[this.current.id];
|
|
58
|
+
return Boolean(row && (row.selected.length || row.custom.trim()));
|
|
59
|
+
},
|
|
60
|
+
complete() {
|
|
61
|
+
return this.questions.every((question) => {
|
|
62
|
+
const row = this.answers[question.id];
|
|
63
|
+
return Boolean(row && (row.selected.length || row.custom.trim()));
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
watch: {
|
|
68
|
+
questions() {
|
|
69
|
+
this.index = 0;
|
|
70
|
+
this.answers = blankAnswers(this.questions);
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
methods: {
|
|
74
|
+
selected(id) {
|
|
75
|
+
return this.answers[id]?.selected ?? [];
|
|
76
|
+
},
|
|
77
|
+
toggle(label, checked) {
|
|
78
|
+
const id = this.current.id;
|
|
79
|
+
const current = this.answers[id];
|
|
80
|
+
current.selected = this.current.multiSelect
|
|
81
|
+
? (checked ? [...current.selected, label] : current.selected.filter((row) => row !== label))
|
|
82
|
+
: (checked ? [label] : []);
|
|
83
|
+
},
|
|
84
|
+
submit() {
|
|
85
|
+
if (!this.complete) return;
|
|
86
|
+
this.$emit("answer", {
|
|
87
|
+
answers: this.questions.map((question) => {
|
|
88
|
+
const row = this.answers[question.id];
|
|
89
|
+
return {
|
|
90
|
+
id: question.id,
|
|
91
|
+
selected: row.selected,
|
|
92
|
+
...(row.custom.trim() ? { custom: row.custom.trim() } : {}),
|
|
93
|
+
};
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<style scoped>
|
|
102
|
+
.desktop-questions { overflow:hidden; border:1px solid var(--q-separator); border-radius:14px; background:var(--q-background-2); box-shadow:0 1px 3px rgb(0 0 0 / 8%); }
|
|
103
|
+
.desktop-questions__strip { height:30px; padding:0 12px; background:var(--q-background-3); color:var(--q-ink-3); font-size:12px; line-height:30px; }
|
|
104
|
+
.desktop-questions__body { display:flex; max-height:240px; flex-direction:column; gap:8px; overflow:auto; padding:14px 16px; color:var(--q-ink-2); font-size:13px; }
|
|
105
|
+
.desktop-questions__body > strong { color:var(--q-ink-1); font-size:14px; }
|
|
106
|
+
.desktop-questions__body > p { margin:0; white-space:pre-wrap; }
|
|
107
|
+
.desktop-questions__option { display:flex; gap:9px; border:1px solid var(--q-separator); border-radius:8px; padding:8px; cursor:pointer; }
|
|
108
|
+
.desktop-questions__option span { display:flex; flex-direction:column; gap:2px; }
|
|
109
|
+
.desktop-questions__option b { color:var(--q-ink-1); font-weight:500; }
|
|
110
|
+
.desktop-questions__option small { color:var(--q-ink-3); }
|
|
111
|
+
.desktop-questions__other { display:flex; align-items:center; gap:8px; }
|
|
112
|
+
.desktop-questions__other input { min-width:0; height:28px; flex:1; border:1px solid var(--q-input-stroke); border-radius:6px; padding:0 8px; background:var(--q-background-1); color:var(--q-ink-1); }
|
|
113
|
+
.desktop-questions footer { display:flex; align-items:center; gap:8px; padding:8px 12px 12px; }
|
|
114
|
+
.desktop-questions footer span { flex:1; }
|
|
115
|
+
.desktop-questions footer button { height:32px; border:1px solid var(--q-separator); border-radius:7px; padding:0 13px; background:transparent; color:var(--q-ink-2); font:inherit; cursor:pointer; }
|
|
116
|
+
.desktop-questions footer button:last-child { border-color:transparent; background:var(--q-blue-default); color:var(--q-ink-on-brand); }
|
|
117
|
+
.desktop-questions footer button:disabled { opacity:.45; cursor:default; }
|
|
118
|
+
</style>
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Transition name="lares-panel">
|
|
3
|
+
<div v-if="rows.length" class="desktop-queue">
|
|
4
|
+
<div class="desktop-queue__panel">
|
|
5
|
+
<button
|
|
6
|
+
v-if="rows.length > 1"
|
|
7
|
+
type="button"
|
|
8
|
+
class="desktop-queue__header"
|
|
9
|
+
:aria-expanded="expanded"
|
|
10
|
+
:disabled="Boolean(editing || busy)"
|
|
11
|
+
@click="collapsed = !collapsed"
|
|
12
|
+
>
|
|
13
|
+
<QueueIcon />
|
|
14
|
+
<span>{{ t("queue.count", { n: rows.length }) }}</span>
|
|
15
|
+
<ChevronIcon :class="{ 'is-open': expanded }" />
|
|
16
|
+
</button>
|
|
17
|
+
<ul
|
|
18
|
+
class="desktop-queue__list"
|
|
19
|
+
:data-expanded="rows.length === 1 || expanded ? 'true' : 'false'"
|
|
20
|
+
>
|
|
21
|
+
<li v-for="row in rows" :key="row.id" class="desktop-queue__row">
|
|
22
|
+
<QueueIcon v-if="rows.length === 1" class="desktop-queue__lead" />
|
|
23
|
+
<input
|
|
24
|
+
v-if="editing?.id === row.id"
|
|
25
|
+
ref="editor"
|
|
26
|
+
v-model="editing.text"
|
|
27
|
+
class="desktop-queue__editor"
|
|
28
|
+
:aria-label="t('queue.edit')"
|
|
29
|
+
@keydown.esc="editing = null"
|
|
30
|
+
@keydown.enter.exact.prevent="saveEdit"
|
|
31
|
+
/>
|
|
32
|
+
<span v-else class="desktop-queue__preview">{{ row.preview }}</span>
|
|
33
|
+
<div class="desktop-queue__actions">
|
|
34
|
+
<template v-if="editing?.id === row.id">
|
|
35
|
+
<ActionButton
|
|
36
|
+
:label="t('queue.save')"
|
|
37
|
+
:disabled="Boolean(busy) || !editing.text.trim()"
|
|
38
|
+
icon="check"
|
|
39
|
+
@click="saveEdit"
|
|
40
|
+
/>
|
|
41
|
+
<ActionButton
|
|
42
|
+
:label="t('queue.cancelEdit')"
|
|
43
|
+
:disabled="Boolean(busy)"
|
|
44
|
+
icon="close"
|
|
45
|
+
@click="editing = null"
|
|
46
|
+
/>
|
|
47
|
+
</template>
|
|
48
|
+
<template v-else>
|
|
49
|
+
<ActionButton
|
|
50
|
+
:label="t('queue.edit')"
|
|
51
|
+
:disabled="Boolean(busy) || row.text == null"
|
|
52
|
+
icon="edit"
|
|
53
|
+
@click="beginEdit(row)"
|
|
54
|
+
/>
|
|
55
|
+
<ActionButton
|
|
56
|
+
:label="t('queue.remove')"
|
|
57
|
+
:disabled="Boolean(busy)"
|
|
58
|
+
icon="trash"
|
|
59
|
+
@click="$emit('action', row.id, { kind: 'remove' })"
|
|
60
|
+
/>
|
|
61
|
+
<ActionButton
|
|
62
|
+
:label="t('queue.steer')"
|
|
63
|
+
:disabled="Boolean(busy) || !running"
|
|
64
|
+
icon="send"
|
|
65
|
+
@click="$emit('action', row.id, { kind: 'steer' })"
|
|
66
|
+
/>
|
|
67
|
+
</template>
|
|
68
|
+
</div>
|
|
69
|
+
</li>
|
|
70
|
+
</ul>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</Transition>
|
|
74
|
+
</template>
|
|
75
|
+
|
|
76
|
+
<script>
|
|
77
|
+
import { h } from "vue";
|
|
78
|
+
import LaresPcTooltip from "./ui/PcTooltip.vue";
|
|
79
|
+
|
|
80
|
+
const paths = {
|
|
81
|
+
check: "m4 8 2.5 2.5L12 5",
|
|
82
|
+
close: "m4.5 4.5 7 7m0-7-7 7",
|
|
83
|
+
edit: "M3.5 11.8V13h1.2l7.7-7.7-1.7-1.7z",
|
|
84
|
+
trash: "M4 5h8M6 5V3.5h4V5m-5 0 .6 8h4.8l.6-8",
|
|
85
|
+
send: "m3 3 10 5-10 5 2-5z",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const glyph = (name, path, extra = {}) => ({
|
|
89
|
+
name,
|
|
90
|
+
render() {
|
|
91
|
+
return h("svg", { viewBox: "0 0 16 16", "aria-hidden": "true", ...extra }, [
|
|
92
|
+
h("path", { d: path }),
|
|
93
|
+
]);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const QueueIcon = glyph("QueueIcon", "M3 4.5h10M3 8h7M3 11.5h4");
|
|
98
|
+
const ChevronIcon = glyph("ChevronIcon", "m4 6 4 4 4-4");
|
|
99
|
+
|
|
100
|
+
const ActionButton = {
|
|
101
|
+
name: "QueueActionButton",
|
|
102
|
+
components: { LaresPcTooltip },
|
|
103
|
+
props: {
|
|
104
|
+
label: { type: String, required: true },
|
|
105
|
+
icon: { type: String, required: true },
|
|
106
|
+
disabled: { type: Boolean, default: false },
|
|
107
|
+
},
|
|
108
|
+
emits: ["click"],
|
|
109
|
+
render() {
|
|
110
|
+
return h(LaresPcTooltip, { label: this.label, placement: "bottom" }, {
|
|
111
|
+
default: () => h("button", {
|
|
112
|
+
type: "button",
|
|
113
|
+
class: "desktop-queue__action",
|
|
114
|
+
disabled: this.disabled,
|
|
115
|
+
"aria-label": this.label,
|
|
116
|
+
onClick: () => this.$emit("click"),
|
|
117
|
+
}, [
|
|
118
|
+
h("svg", { viewBox: "0 0 16 16", "aria-hidden": "true" }, [
|
|
119
|
+
h("path", { d: paths[this.icon] }),
|
|
120
|
+
]),
|
|
121
|
+
]),
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export default {
|
|
127
|
+
name: "LaresDesktopQueue",
|
|
128
|
+
components: { QueueIcon, ChevronIcon, ActionButton },
|
|
129
|
+
props: {
|
|
130
|
+
items: { type: Array, default: () => [] },
|
|
131
|
+
running: { type: Boolean, default: false },
|
|
132
|
+
busy: { type: String, default: "" },
|
|
133
|
+
t: { type: Function, required: true },
|
|
134
|
+
},
|
|
135
|
+
emits: ["action"],
|
|
136
|
+
data() {
|
|
137
|
+
return { collapsed: true, editing: null };
|
|
138
|
+
},
|
|
139
|
+
computed: {
|
|
140
|
+
rows() {
|
|
141
|
+
return this.items.filter((row) => row?.placement === "queued");
|
|
142
|
+
},
|
|
143
|
+
expanded() {
|
|
144
|
+
return !this.collapsed || Boolean(this.editing || this.busy);
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
watch: {
|
|
148
|
+
rows(next) {
|
|
149
|
+
if (!next.length) this.collapsed = true;
|
|
150
|
+
if (this.editing && !next.some((row) => row.id === this.editing.id)) this.editing = null;
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
methods: {
|
|
154
|
+
beginEdit(row) {
|
|
155
|
+
if (row.text == null) return;
|
|
156
|
+
this.editing = { id: row.id, text: row.text };
|
|
157
|
+
this.$nextTick(() => {
|
|
158
|
+
const editor = Array.isArray(this.$refs.editor) ? this.$refs.editor[0] : this.$refs.editor;
|
|
159
|
+
editor?.focus?.();
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
saveEdit() {
|
|
163
|
+
if (!this.editing?.text.trim()) return;
|
|
164
|
+
const { id, text } = this.editing;
|
|
165
|
+
this.$emit("action", id, { kind: "edit", content: [{ type: "text", text }] });
|
|
166
|
+
this.editing = null;
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
</script>
|
|
171
|
+
|
|
172
|
+
<style scoped>
|
|
173
|
+
.desktop-queue { box-sizing:border-box; width:calc(100% - 24px); margin:0 auto -3px; padding:0 8px; }
|
|
174
|
+
.desktop-queue__panel { overflow:hidden; border:1px solid var(--q-separator); border-bottom:0; border-radius:12px 12px 0 0; padding:2px 0; background:var(--q-background-3); }
|
|
175
|
+
.desktop-queue__header { display:flex; width:100%; height:36px; align-items:center; gap:10px; border:0; padding:4px 12px; background:transparent; color:var(--q-ink-1); text-align:left; cursor:pointer; }
|
|
176
|
+
.desktop-queue__header span { min-width:0; flex:1; font-size:13px; font-weight:500; }
|
|
177
|
+
.desktop-queue__header .is-open { transform:rotate(180deg); }
|
|
178
|
+
.desktop-queue__header > svg,
|
|
179
|
+
.desktop-queue__header .is-open { transition:transform var(--lares-duration-fast, 120ms) var(--lares-ease-out, ease); }
|
|
180
|
+
.desktop-queue__list { max-height:0; overflow:hidden; margin:0; padding:0; list-style:none; opacity:0; transition:max-height var(--lares-duration-normal, 220ms) var(--lares-ease-out, cubic-bezier(0.32, 0.72, 0, 1)), opacity var(--lares-duration-normal, 220ms) var(--lares-ease-out, cubic-bezier(0.32, 0.72, 0, 1)); }
|
|
181
|
+
.desktop-queue__list[data-expanded="true"] { max-height:180px; overflow-y:auto; opacity:1; }
|
|
182
|
+
@media (prefers-reduced-motion: reduce) {
|
|
183
|
+
.desktop-queue__list,
|
|
184
|
+
.desktop-queue__header > svg,
|
|
185
|
+
.desktop-queue__header .is-open { transition:none; }
|
|
186
|
+
.desktop-queue__list[data-expanded="false"] { opacity:1; }
|
|
187
|
+
}
|
|
188
|
+
.desktop-queue__row { display:flex; box-sizing:border-box; width:100%; height:36px; align-items:center; gap:10px; padding:4px 5px 4px 12px; }
|
|
189
|
+
.desktop-queue__row + .desktop-queue__row { border-top:1px solid var(--q-separator); }
|
|
190
|
+
.desktop-queue__lead,
|
|
191
|
+
.desktop-queue__header > svg { flex:none; color:var(--q-ink-3); }
|
|
192
|
+
.desktop-queue__preview,
|
|
193
|
+
.desktop-queue__editor { min-width:0; flex:1; font:inherit; font-size:13px; }
|
|
194
|
+
.desktop-queue__preview { overflow:hidden; color:var(--q-ink-2); text-overflow:ellipsis; white-space:nowrap; }
|
|
195
|
+
.desktop-queue__editor { box-sizing:border-box; height:28px; border:1px solid var(--q-input-stroke); border-radius:6px; padding:0 8px; outline:none; background:var(--q-background-1); color:var(--q-ink-1); }
|
|
196
|
+
.desktop-queue__editor:focus { border-color:var(--q-blue-default); }
|
|
197
|
+
.desktop-queue__actions { display:flex; flex:none; align-items:center; gap:4px; }
|
|
198
|
+
:deep(.desktop-queue__action) { display:grid; width:28px; height:28px; place-items:center; border:0; border-radius:50%; padding:0; background:transparent; color:var(--q-ink-3); cursor:pointer; }
|
|
199
|
+
:deep(.desktop-queue__action:hover:not(:disabled)) { background:var(--q-background-hover); color:var(--q-ink-2); }
|
|
200
|
+
:deep(.desktop-queue__action:disabled) { opacity:.45; cursor:default; }
|
|
201
|
+
.desktop-queue svg,
|
|
202
|
+
:deep(.desktop-queue__action svg) { width:16px; height:16px; fill:none; stroke:currentColor; stroke-width:1.4; stroke-linecap:round; stroke-linejoin:round; }
|
|
203
|
+
</style>
|