@olares/lares-mobile 0.15.5 → 0.15.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +6 -2
  2. package/package.json +2 -2
  3. package/src/App.vue +918 -178
  4. package/src/chat/ChatBar.vue +2 -2
  5. package/src/chat/Composer.vue +467 -16
  6. package/src/chat/Deliverables.vue +22 -10
  7. package/src/chat/HistoryPanel.vue +747 -55
  8. package/src/chat/MessageActions.vue +88 -34
  9. package/src/chat/QuestionCard.vue +5 -5
  10. package/src/chat/StageRow.vue +8 -5
  11. package/src/chat/Transcript.vue +20 -6
  12. package/src/chat/composer-tokens.js +32 -0
  13. package/src/chat/format.js +37 -0
  14. package/src/desktop/DesktopComposer.vue +942 -0
  15. package/src/desktop/DesktopContextMeter.vue +81 -0
  16. package/src/desktop/DesktopGoal.vue +88 -0
  17. package/src/desktop/DesktopPreview.vue +58 -0
  18. package/src/desktop/DesktopPreviewTabs.vue +82 -0
  19. package/src/desktop/DesktopQuestions.vue +118 -0
  20. package/src/desktop/DesktopQueue.vue +203 -0
  21. package/src/desktop/DesktopShell.vue +229 -0
  22. package/src/desktop/DesktopSidebar.vue +645 -0
  23. package/src/desktop/DesktopTodos.vue +52 -0
  24. package/src/desktop/DirectoryBrowser.vue +231 -0
  25. package/src/desktop/DirectoryNewFolder.vue +124 -0
  26. package/src/desktop/ui/PcDialog.vue +100 -0
  27. package/src/desktop/ui/PcPopover.vue +110 -0
  28. package/src/desktop/ui/PcTooltip.vue +95 -0
  29. package/src/desktop/ui/place.js +24 -0
  30. package/src/desktop/workspace-view.js +121 -0
  31. package/src/host.js +20 -2
  32. package/src/index.d.ts +2 -0
  33. package/src/mobile/MobileDirectoryNewFolder.vue +121 -0
  34. package/src/mobile/MobileDirectorySheet.vue +162 -0
  35. package/src/mobile/MobileNewSession.vue +217 -0
  36. package/src/mobile/MobileShell.vue +367 -0
  37. package/src/motion.css +166 -0
  38. package/src/preview/Preview.vue +91 -150
  39. package/src/preview/PreviewBody.vue +185 -0
  40. package/src/settings/AgentSettings.vue +126 -36
  41. package/src/settings/Select.vue +183 -0
  42. package/src/settings/SettingRow.vue +5 -4
  43. package/src/settings/Sheet.vue +4 -4
  44. package/src/styles.css +9 -0
@@ -0,0 +1,52 @@
1
+ <template>
2
+ <Transition name="lares-panel">
3
+ <div v-if="rows.length" class="desktop-todos">
4
+ <button type="button" class="desktop-todos__header" :aria-expanded="open" @click="open = !open">
5
+ <span>{{ t("todo.progress", { done, total: rows.length }) }}</span>
6
+ <span class="desktop-todos__chevron" :data-open="open ? 'true' : 'false'" aria-hidden="true">⌄</span>
7
+ </button>
8
+ <ul class="desktop-todos__list" :data-open="open ? 'true' : 'false'">
9
+ <li v-for="(row, index) in rows" :key="`${index}-${row.content}`" :data-status="row.status">
10
+ <span aria-hidden="true">{{ row.status === "completed" ? "✓" : row.status === "in_progress" ? "●" : "○" }}</span>
11
+ <span>{{ row.content }}</span>
12
+ </li>
13
+ </ul>
14
+ </div>
15
+ </Transition>
16
+ </template>
17
+
18
+ <script>
19
+ export default {
20
+ name: "LaresDesktopTodos",
21
+ props: {
22
+ rows: { type: Array, default: () => [] },
23
+ t: { type: Function, required: true },
24
+ },
25
+ data() {
26
+ return { open: true };
27
+ },
28
+ computed: {
29
+ done() {
30
+ return this.rows.filter((row) => row.status === "completed").length;
31
+ },
32
+ },
33
+ };
34
+ </script>
35
+
36
+ <style scoped>
37
+ .desktop-todos { box-sizing:border-box; width:calc(100% - 24px); margin:0 auto -3px; border:1px solid var(--q-separator); border-bottom:0; border-radius:12px 12px 0 0; padding:3px 0 6px; background:var(--q-background-3); color:var(--q-ink-2); font-size:13px; }
38
+ .desktop-todos__header { display:flex; width:100%; height:32px; align-items:center; justify-content:space-between; border:0; padding:0 12px; background:transparent; color:inherit; font:inherit; font-weight:500; cursor:pointer; }
39
+ .desktop-todos__chevron { display:inline-block; transition:transform var(--lares-duration-fast, 120ms) var(--lares-ease-out, ease); }
40
+ .desktop-todos__chevron[data-open="false"] { transform:rotate(180deg); }
41
+ .desktop-todos__list { max-height:0; overflow:hidden; margin:0; padding:0 12px; 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)); }
42
+ .desktop-todos__list[data-open="true"] { max-height:120px; overflow:auto; opacity:1; }
43
+ @media (prefers-reduced-motion: reduce) {
44
+ .desktop-todos__chevron,
45
+ .desktop-todos__list { transition:none; }
46
+ .desktop-todos__list[data-open="false"] { opacity:1; }
47
+ }
48
+ .desktop-todos li { display:flex; min-height:25px; align-items:flex-start; gap:8px; }
49
+ .desktop-todos li > span:first-child { color:var(--q-ink-3); }
50
+ .desktop-todos li[data-status="in_progress"] > span:first-child { color:var(--q-blue-default); }
51
+ .desktop-todos li[data-status="completed"] { color:var(--q-ink-3); text-decoration:line-through; }
52
+ </style>
@@ -0,0 +1,231 @@
1
+ <template>
2
+ <LaresPcDialog
3
+ :open="state.open"
4
+ :title="t('desktop.selectWorkspaceDirectory')"
5
+ :close-label="t('desktop.close')"
6
+ :busy="state.creating || state.creatingFolder"
7
+ :width="620"
8
+ variant="directory"
9
+ @close="$emit('close')"
10
+ >
11
+ <template #header="{ titleId }">
12
+ <div class="directory-browser__header">
13
+ <h2 :id="titleId">{{ t("desktop.selectWorkspaceDirectory") }}</h2>
14
+ <div class="directory-browser__location">
15
+ <input
16
+ v-if="editingPath"
17
+ ref="pathInput"
18
+ v-model="pathDraft"
19
+ :aria-label="t('desktop.folderPath')"
20
+ @keydown.enter.prevent="submitPath"
21
+ @keydown.esc.prevent="editingPath = false"
22
+ @blur="editingPath = false"
23
+ />
24
+ <nav v-else-if="state.listing" :aria-label="t('desktop.folderPath')">
25
+ <template v-for="(crumb, index) in homeCrumbs" :key="crumb.path">
26
+ <span v-if="index" aria-hidden="true">/</span>
27
+ <button type="button" @click="$emit('browse', crumb.path)">
28
+ {{ crumb.path === state.listing.home ? t("desktop.home") : crumb.name }}
29
+ </button>
30
+ </template>
31
+ </nav>
32
+ <button
33
+ v-if="!editingPath"
34
+ type="button"
35
+ class="directory-browser__edit"
36
+ :aria-label="t('desktop.folderPath')"
37
+ @click="beginPathEdit"
38
+ >
39
+ <svg viewBox="0 0 24 24" aria-hidden="true">
40
+ <path d="m4 16-.75 4.25L7.5 19.5 18.4 8.6a2.1 2.1 0 0 0-3-3Z" />
41
+ <path d="m13.8 7.2 3 3M3 21h18" />
42
+ </svg>
43
+ </button>
44
+ </div>
45
+ </div>
46
+ </template>
47
+ <div class="directory-browser">
48
+ <div class="directory-browser__body">
49
+ <p v-if="state.loading" class="directory-browser__status">{{ t("desktop.loadingFolders") }}</p>
50
+ <p v-else-if="state.error" class="directory-browser__error">{{ state.error }}</p>
51
+ <template v-else-if="state.listing">
52
+ <div
53
+ v-for="entry in visibleEntries"
54
+ :key="entry.path"
55
+ class="directory-browser__row"
56
+ role="button"
57
+ tabindex="0"
58
+ @dblclick="$emit('browse', entry.path)"
59
+ @click="selected = entry.path"
60
+ @keydown.enter="$emit('browse', entry.path)"
61
+ :data-selected="selected === entry.path"
62
+ >
63
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3.5 7.5h6l1.6 2H20.5v9H3.5z" /></svg>
64
+ <span>{{ entry.name }}</span>
65
+ <button type="button" class="directory-browser__open" @click.stop="$emit('browse', entry.path)">
66
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="m9 6 6 6-6 6" /></svg>
67
+ </button>
68
+ </div>
69
+ <p v-if="visibleEntries.length === 0" class="directory-browser__status">{{ t("desktop.noFolders") }}</p>
70
+ </template>
71
+ </div>
72
+ </div>
73
+ <template #footer>
74
+ <div class="directory-browser__footer">
75
+ <div class="directory-browser__footer-left">
76
+ <button
77
+ type="button"
78
+ class="directory-browser__new-folder"
79
+ :disabled="!state.listing?.path || state.loading || state.creatingFolder"
80
+ @click="newFolderOpen = true"
81
+ >
82
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
83
+ <span>{{ t("desktop.newFolder") }}</span>
84
+ </button>
85
+ <label class="directory-browser__hidden">
86
+ <input
87
+ type="checkbox"
88
+ :checked="state.showHidden"
89
+ @change="$emit('toggle-hidden', $event.target.checked)"
90
+ />
91
+ <span>{{ t("desktop.showHiddenFiles") }}</span>
92
+ </label>
93
+ </div>
94
+ <div class="directory-browser__footer-right">
95
+ <button type="button" class="directory-browser__cancel" :disabled="state.creating" @click="$emit('close')">
96
+ {{ t("desktop.cancel") }}
97
+ </button>
98
+ <button
99
+ type="button"
100
+ class="directory-browser__choose"
101
+ :disabled="state.loading || state.creating || !(selected || state.listing?.path)"
102
+ @click="$emit('choose', selected || state.listing.path)"
103
+ >
104
+ {{ state.creating ? t("desktop.addingWorkspace") : t("desktop.open") }}
105
+ </button>
106
+ </div>
107
+ </div>
108
+ </template>
109
+ </LaresPcDialog>
110
+ <LaresDirectoryNewFolder
111
+ :open="newFolderOpen"
112
+ :busy="state.creatingFolder"
113
+ :t="t"
114
+ @close="newFolderOpen = false"
115
+ @create="onCreateFolder"
116
+ />
117
+ </template>
118
+
119
+ <script>
120
+ import LaresPcDialog from "./ui/PcDialog.vue";
121
+ import LaresDirectoryNewFolder from "./DirectoryNewFolder.vue";
122
+
123
+ export default {
124
+ name: "LaresDirectoryBrowser",
125
+ components: { LaresPcDialog, LaresDirectoryNewFolder },
126
+ props: {
127
+ state: { type: Object, required: true },
128
+ t: { type: Function, required: true },
129
+ },
130
+ emits: ["close", "browse", "choose", "create-folder", "toggle-hidden"],
131
+ data() {
132
+ return { selected: "", newFolderOpen: false, editingPath: false, pathDraft: "" };
133
+ },
134
+ computed: {
135
+ visibleEntries() {
136
+ const entries = this.state.listing?.entries ?? [];
137
+ return this.state.showHidden ? entries : entries.filter((entry) => !entry.hidden);
138
+ },
139
+ homeCrumbs() {
140
+ const crumbs = this.state.listing?.crumbs ?? [];
141
+ const home = this.state.listing?.home;
142
+ const homeIndex = crumbs.findIndex((crumb) => crumb.path === home);
143
+ return homeIndex >= 0 ? crumbs.slice(homeIndex) : crumbs;
144
+ },
145
+ },
146
+ watch: {
147
+ "state.listing.path"() {
148
+ this.selected = this.state.selectPath || "";
149
+ this.pathDraft = this.state.listing?.path || "";
150
+ this.editingPath = false;
151
+ },
152
+ "state.selectPath"(path) {
153
+ if (path) this.selected = path;
154
+ },
155
+ "state.creatingFolder"(busy, was) {
156
+ if (was && !busy && !this.state.error) this.newFolderOpen = false;
157
+ },
158
+ "state.open"(open) {
159
+ if (!open) {
160
+ this.newFolderOpen = false;
161
+ this.selected = "";
162
+ }
163
+ },
164
+ },
165
+ methods: {
166
+ onCreateFolder(name) {
167
+ this.$emit("create-folder", name);
168
+ },
169
+ beginPathEdit() {
170
+ this.pathDraft = this.state.listing?.path || "";
171
+ this.editingPath = true;
172
+ this.$nextTick(() => {
173
+ this.$refs.pathInput?.focus();
174
+ this.$refs.pathInput?.select();
175
+ });
176
+ },
177
+ submitPath() {
178
+ const path = this.pathDraft.trim();
179
+ if (!path) return;
180
+ this.editingPath = false;
181
+ this.$emit("browse", path);
182
+ },
183
+ },
184
+ };
185
+ </script>
186
+
187
+ <style scoped>
188
+ .directory-browser__header { height:76px; }
189
+ .directory-browser__header h2 { height:42px; margin:0; padding:19px 22px 0; font-size:16px; font-weight:500; line-height:22px; }
190
+ .directory-browser__location { display:flex; height:34px; align-items:center; gap:8px; padding:0 20px 7px 22px; }
191
+ .directory-browser__location nav { display:flex; min-width:0; flex:1; align-items:center; gap:7px; overflow:hidden; color:var(--q-ink-2); }
192
+ .directory-browser__location nav button { min-width:0; overflow:hidden; border:0; padding:0; background:transparent; color:inherit; font:inherit; font-size:14px; text-overflow:ellipsis; white-space:nowrap; cursor:pointer; }
193
+ .directory-browser__location nav button:hover { color:var(--q-ink-1); }
194
+ .directory-browser__location input { box-sizing:border-box; min-width:0; height:27px; flex:1; border:1px solid var(--q-input-stroke); border-radius:5px; padding:0 8px; outline:none; background:var(--q-background-2); color:var(--q-ink-1); font:inherit; font-size:13px; }
195
+ .directory-browser__edit { display:grid; width:30px; height:30px; flex-shrink:0; place-items:center; border:0; border-radius:6px; background:transparent; color:var(--q-ink-2); cursor:pointer; }
196
+ .directory-browser__edit:hover { background:var(--q-background-hover); color:var(--q-ink-1); }
197
+ .directory-browser__edit svg { width:17px; height:17px; fill:none; stroke:currentColor; stroke-width:1.6; stroke-linecap:round; stroke-linejoin:round; }
198
+ .directory-browser { display:flex; height:min(316px, calc(100vh - 186px)); flex-direction:column; }
199
+ .directory-browser svg { width:18px; height:18px; fill:none; stroke:currentColor; stroke-width:1.7; stroke-linecap:round; stroke-linejoin:round; }
200
+ .directory-browser__body { flex:1; min-height:0; overflow:auto; padding:13px 20px; }
201
+ .directory-browser__row { display:flex; width:100%; height:28px; align-items:center; gap:7px; border:0; border-radius:6px; padding:0 6px; background:transparent; color:var(--q-ink-1); font-size:14px; text-align:left; }
202
+ .directory-browser__row:hover { background:var(--q-background-hover); }
203
+ .directory-browser__row[data-selected="true"] { background:var(--q-background-selected); }
204
+ .directory-browser__row > span { min-width:0; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
205
+ .directory-browser__open { display:grid; width:26px; height:26px; place-items:center; border:0; background:transparent; color:var(--q-ink-3); }
206
+ .directory-browser__status,
207
+ .directory-browser__error { margin:20px; color:var(--q-ink-3); font-size:14px; text-align:center; }
208
+ .directory-browser__error { color:var(--q-orange-default); }
209
+ .directory-browser__footer { display:flex; width:100%; align-items:center; justify-content:space-between; gap:12px; }
210
+ .directory-browser__footer-left { display:flex; min-width:0; flex:1; align-items:center; gap:12px; }
211
+ .directory-browser__footer-right { display:flex; flex-shrink:0; align-items:center; gap:8px; }
212
+ .directory-browser__new-folder { display:inline-flex; align-items:center; gap:6px; border:1px solid var(--q-btn-stroke); border-radius:999px; padding:0 14px; height:34px; background:transparent; color:var(--q-ink-1); font:inherit; font-size:14px; white-space:nowrap; }
213
+ /* The footer is a dialog slot, outside `.directory-browser`, so it misses the shared icon rule. */
214
+ .directory-browser__new-folder svg { width:14px; height:14px; fill:none; stroke:currentColor; stroke-width:1.7; stroke-linecap:round; stroke-linejoin:round; }
215
+ .directory-browser__hidden { display:inline-flex; align-items:center; color:var(--q-ink-2); font-size:13px; white-space:nowrap; cursor:pointer; }
216
+ .directory-browser__hidden input { position:absolute; width:1px; height:1px; overflow:hidden; opacity:0; pointer-events:none; }
217
+ .directory-browser__hidden:has(input:checked) { color:var(--q-ink-1); }
218
+ .directory-browser__cancel,
219
+ .directory-browser__choose { min-width:67px; height:34px; border-radius:999px; padding:0 14px; font-size:14px; }
220
+ .directory-browser__cancel { border:1px solid var(--q-btn-stroke); background:transparent; color:var(--q-ink-1); }
221
+ .directory-browser__new-folder:hover:not(:disabled),
222
+ .directory-browser__cancel:hover:not(:disabled) { background:var(--q-btn-bg-hover); border-color:var(--q-btn-stroke-hover); }
223
+ /* The confirm button is the inverse surface: white on dark, dark on light. */
224
+ .directory-browser__choose { border:0; background:var(--q-background-9); color:var(--q-background-1); }
225
+ .directory-browser button,
226
+ .directory-browser__cancel,
227
+ .directory-browser__choose { cursor:pointer; font:inherit; }
228
+ .directory-browser button:disabled,
229
+ .directory-browser__cancel:disabled,
230
+ .directory-browser__choose:disabled { opacity:.5; cursor:default; }
231
+ </style>
@@ -0,0 +1,124 @@
1
+ <template>
2
+ <LaresPcDialog
3
+ :open="open"
4
+ :title="t('desktop.newFolder')"
5
+ :close-label="t('desktop.close')"
6
+ :busy="busy"
7
+ :width="420"
8
+ @close="$emit('close')"
9
+ >
10
+ <div class="directory-new-folder">
11
+ <input
12
+ ref="input"
13
+ v-model="value"
14
+ class="directory-new-folder__input"
15
+ :placeholder="t('desktop.untitledFolder')"
16
+ :disabled="busy"
17
+ @keydown.enter.prevent="submit"
18
+ />
19
+ </div>
20
+ <template #footer>
21
+ <button type="button" class="directory-new-folder__cancel" :disabled="busy" @click="$emit('close')">
22
+ {{ t("desktop.cancel") }}
23
+ </button>
24
+ <button
25
+ type="button"
26
+ class="directory-new-folder__create"
27
+ :disabled="busy || !value.trim()"
28
+ @click="submit"
29
+ >
30
+ {{ t("desktop.createFolder") }}
31
+ </button>
32
+ </template>
33
+ </LaresPcDialog>
34
+ </template>
35
+
36
+ <script>
37
+ import LaresPcDialog from "./ui/PcDialog.vue";
38
+
39
+ export default {
40
+ name: "LaresDirectoryNewFolder",
41
+ components: { LaresPcDialog },
42
+ props: {
43
+ open: { type: Boolean, default: false },
44
+ busy: { type: Boolean, default: false },
45
+ t: { type: Function, required: true },
46
+ },
47
+ emits: ["close", "create"],
48
+ data() {
49
+ return { value: "" };
50
+ },
51
+ watch: {
52
+ open(value) {
53
+ if (!value) {
54
+ this.value = "";
55
+ return;
56
+ }
57
+ this.value = this.t("desktop.untitledFolder");
58
+ this.$nextTick(() => {
59
+ this.$refs.input?.focus();
60
+ this.$refs.input?.select();
61
+ });
62
+ },
63
+ },
64
+ methods: {
65
+ submit() {
66
+ const name = this.value.trim();
67
+ if (!name || this.busy) return;
68
+ this.$emit("create", name);
69
+ },
70
+ },
71
+ };
72
+ </script>
73
+
74
+ <style scoped>
75
+ .directory-new-folder {
76
+ padding-inline: 12px;
77
+ }
78
+
79
+ .directory-new-folder__input {
80
+ box-sizing: border-box;
81
+ width: 100%;
82
+ height: 40px;
83
+ border: 1px solid var(--q-input-stroke, var(--q-separator));
84
+ border-radius: 999px;
85
+ padding: 0 14px;
86
+ background: transparent;
87
+ color: var(--q-ink-1);
88
+ font: inherit;
89
+ font-size: 15px;
90
+ }
91
+
92
+ .directory-new-folder__input:focus {
93
+ outline: none;
94
+ border-color: var(--q-blue-default);
95
+ }
96
+
97
+ .directory-new-folder__cancel,
98
+ .directory-new-folder__create {
99
+ min-width: 76px;
100
+ height: 32px;
101
+ border-radius: 999px;
102
+ padding: 0 14px;
103
+ font: inherit;
104
+ cursor: pointer;
105
+ }
106
+
107
+ .directory-new-folder__cancel {
108
+ border: 1px solid var(--q-separator);
109
+ background: transparent;
110
+ color: var(--q-ink-2);
111
+ }
112
+
113
+ .directory-new-folder__create {
114
+ border: 0;
115
+ background: var(--q-blue-default);
116
+ color: var(--q-ink-on-brand);
117
+ }
118
+
119
+ .directory-new-folder__cancel:disabled,
120
+ .directory-new-folder__create:disabled {
121
+ opacity: 0.5;
122
+ cursor: default;
123
+ }
124
+ </style>
@@ -0,0 +1,100 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <Transition name="lares-dialog">
4
+ <div
5
+ v-if="open"
6
+ class="lares-pc-dialog"
7
+ role="dialog"
8
+ aria-modal="true"
9
+ :aria-labelledby="titleId"
10
+ :data-variant="variant || undefined"
11
+ >
12
+ <button class="lares-pc-dialog__backdrop" type="button" :aria-label="closeLabel" @click="close" />
13
+ <section class="lares-pc-dialog__surface" :style="{ width: surfaceWidth }">
14
+ <header class="lares-pc-dialog__header">
15
+ <slot name="header" :title-id="titleId">
16
+ <div class="lares-pc-dialog__title">
17
+ <h2 :id="titleId">{{ title }}</h2>
18
+ <button type="button" class="lares-pc-dialog__close" :aria-label="closeLabel" @click="close">
19
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="m7 7 10 10M17 7 7 17" /></svg>
20
+ </button>
21
+ </div>
22
+ <p v-if="description">{{ description }}</p>
23
+ </slot>
24
+ </header>
25
+ <div class="lares-pc-dialog__body">
26
+ <slot />
27
+ </div>
28
+ <footer v-if="$slots.footer" class="lares-pc-dialog__footer">
29
+ <slot name="footer" />
30
+ </footer>
31
+ </section>
32
+ </div>
33
+ </Transition>
34
+ </Teleport>
35
+ </template>
36
+
37
+ <script>
38
+ let dialogId = 0;
39
+
40
+ export default {
41
+ name: "LaresPcDialog",
42
+ props: {
43
+ open: { type: Boolean, default: false },
44
+ title: { type: String, required: true },
45
+ description: { type: String, default: "" },
46
+ closeLabel: { type: String, default: "Close" },
47
+ width: { type: [Number, String], default: 440 },
48
+ busy: { type: Boolean, default: false },
49
+ variant: { type: String, default: "" },
50
+ },
51
+ emits: ["close"],
52
+ data() {
53
+ dialogId += 1;
54
+ return { titleId: `lares-pc-dialog-${dialogId}` };
55
+ },
56
+ computed: {
57
+ surfaceWidth() {
58
+ return typeof this.width === "number" ? `${this.width}px` : this.width;
59
+ },
60
+ },
61
+ watch: {
62
+ open(value) {
63
+ if (value) window.addEventListener("keydown", this.onKey);
64
+ else window.removeEventListener("keydown", this.onKey);
65
+ },
66
+ },
67
+ beforeUnmount() {
68
+ window.removeEventListener("keydown", this.onKey);
69
+ },
70
+ methods: {
71
+ close() {
72
+ if (!this.busy) this.$emit("close");
73
+ },
74
+ onKey(event) {
75
+ if (event.key === "Escape") this.close();
76
+ },
77
+ },
78
+ };
79
+ </script>
80
+
81
+ <style scoped>
82
+ .lares-pc-dialog { position:fixed; z-index:10000; inset:0; display:grid; place-items:center; color:var(--q-ink-1); }
83
+ .lares-pc-dialog__backdrop { position:absolute; inset:0; border:0; background:rgb(0 0 0 / 46%); }
84
+ .lares-pc-dialog__surface { position:relative; display:flex; max-width:calc(100vw - 48px); max-height:calc(100vh - 48px); flex-direction:column; overflow:hidden; border:1px solid var(--q-separator); border-radius:12px; background:var(--q-background-1); box-shadow:0 18px 60px rgb(0 0 0 / 32%); }
85
+ /* 12px + the 30px close button + 12px keeps a title-only header at 54px. */
86
+ .lares-pc-dialog__header { display:flex; flex-shrink:0; flex-direction:column; padding:12px 16px; border-bottom:1px solid var(--q-separator); }
87
+ .lares-pc-dialog__title { display:flex; min-height:30px; align-items:center; justify-content:space-between; gap:16px; }
88
+ .lares-pc-dialog__header h2 { margin:0; font-size:17px; font-weight:600; line-height:22px; }
89
+ .lares-pc-dialog__header p { margin:2px 0 0; color:var(--q-ink-3); font-size:13px; line-height:18px; }
90
+ .lares-pc-dialog__close { display:grid; width:30px; height:30px; flex-shrink:0; place-items:center; border:0; border-radius:7px; background:transparent; color:var(--q-ink-2); cursor:pointer; }
91
+ .lares-pc-dialog__close:hover { background:var(--q-background-hover); }
92
+ .lares-pc-dialog__close svg { width:18px; height:18px; fill:none; stroke:currentColor; stroke-width:1.7; stroke-linecap:round; }
93
+ .lares-pc-dialog__body { min-height:0; overflow:auto; padding:16px; }
94
+ .lares-pc-dialog__footer { display:flex; min-height:58px; flex-shrink:0; align-items:center; justify-content:flex-end; gap:8px; padding:0 16px; border-top:1px solid var(--q-separator); }
95
+ /* The directory variant only changes geometry; colors stay on the shared tokens. */
96
+ .lares-pc-dialog[data-variant="directory"] .lares-pc-dialog__surface { border-radius:20px; }
97
+ .lares-pc-dialog[data-variant="directory"] .lares-pc-dialog__header { padding:0; }
98
+ .lares-pc-dialog[data-variant="directory"] .lares-pc-dialog__body { padding:0; }
99
+ .lares-pc-dialog[data-variant="directory"] .lares-pc-dialog__footer { min-height:62px; padding:0 22px; }
100
+ </style>
@@ -0,0 +1,110 @@
1
+ <template>
2
+ <span ref="anchor" class="lares-pc-popover">
3
+ <slot name="trigger" :open="modelValue" :toggle="toggle" :close="close" />
4
+ <Teleport to="body">
5
+ <Transition name="lares-popover">
6
+ <div v-if="modelValue" class="lares-pc-popover__layer" @mousedown.self="close">
7
+ <div
8
+ ref="panel"
9
+ class="lares-pc-popover__panel"
10
+ :style="panelStyle"
11
+ role="menu"
12
+ @mousedown.stop
13
+ >
14
+ <slot :close="close" />
15
+ </div>
16
+ </div>
17
+ </Transition>
18
+ </Teleport>
19
+ </span>
20
+ </template>
21
+
22
+ <script>
23
+ import { placePanel } from "./place.js";
24
+
25
+ export default {
26
+ name: "LaresPcPopover",
27
+ props: {
28
+ modelValue: { type: Boolean, default: false },
29
+ width: { type: [Number, String], default: 220 },
30
+ placement: { type: String, default: "bottom-end" },
31
+ minPanelHeight: { type: Number, default: 120 },
32
+ },
33
+ emits: ["update:modelValue"],
34
+ data() {
35
+ return {
36
+ panel: { top: "", bottom: "", left: "0px", width: "220px", maxHeight: "320px" },
37
+ };
38
+ },
39
+ computed: {
40
+ panelStyle() {
41
+ return this.panel;
42
+ },
43
+ },
44
+ watch: {
45
+ modelValue(open) {
46
+ if (open) {
47
+ window.addEventListener("resize", this.place);
48
+ window.addEventListener("scroll", this.place, true);
49
+ window.addEventListener("keydown", this.onKey);
50
+ this.$nextTick(this.place);
51
+ } else {
52
+ this.unlisten();
53
+ }
54
+ },
55
+ },
56
+ beforeUnmount() {
57
+ this.unlisten();
58
+ },
59
+ methods: {
60
+ toggle() {
61
+ this.$emit("update:modelValue", !this.modelValue);
62
+ },
63
+ close() {
64
+ this.$emit("update:modelValue", false);
65
+ },
66
+ unlisten() {
67
+ window.removeEventListener("resize", this.place);
68
+ window.removeEventListener("scroll", this.place, true);
69
+ window.removeEventListener("keydown", this.onKey);
70
+ },
71
+ onKey(event) {
72
+ if (event.key === "Escape") this.close();
73
+ },
74
+ place() {
75
+ const anchor = this.$refs.anchor;
76
+ if (!anchor) return;
77
+ const box = anchor.getBoundingClientRect();
78
+ const width = this.width === "anchor"
79
+ ? box.width
80
+ : typeof this.width === "number"
81
+ ? this.width
82
+ : Number.parseFloat(this.width) || box.width;
83
+ this.panel = placePanel({
84
+ box,
85
+ viewport: { width: window.innerWidth, height: window.innerHeight },
86
+ width,
87
+ placement: this.placement,
88
+ minHeight: this.minPanelHeight,
89
+ });
90
+ },
91
+ },
92
+ };
93
+ </script>
94
+
95
+ <style scoped>
96
+ .lares-pc-popover { display:inline-flex; min-width:0; }
97
+ .lares-pc-popover__layer { position:fixed; z-index:9000; inset:0; }
98
+ .lares-pc-popover__panel {
99
+ position:fixed;
100
+ box-sizing:border-box;
101
+ overflow-x:hidden;
102
+ overflow-y:auto;
103
+ border:1px solid var(--q-separator);
104
+ border-radius:8px;
105
+ padding:6px;
106
+ background:var(--q-background-1);
107
+ color:var(--q-ink-1);
108
+ box-shadow:0 8px 28px rgb(0 0 0 / 20%);
109
+ }
110
+ </style>