@olares/lares-mobile 0.15.6 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olares/lares-mobile",
3
- "version": "0.15.6",
3
+ "version": "0.15.7",
4
4
  "description": "Lares chat component for LarePass",
5
5
  "type": "module",
6
6
  "repository": {
@@ -28,7 +28,7 @@
28
28
  "vue": "^3.2.0"
29
29
  },
30
30
  "dependencies": {
31
- "@olares/lares-core": "0.15.6",
31
+ "@olares/lares-core": "0.15.7",
32
32
  "markdown-it": "^14.3.0"
33
33
  }
34
34
  }
package/src/App.vue CHANGED
@@ -14,6 +14,7 @@
14
14
  :effort-name="effortName"
15
15
  @create="newChat"
16
16
  @pick-session="pickSession"
17
+ @pick-workspace="pickWorkspace"
17
18
  @retry="retry"
18
19
  @open-file="openFile"
19
20
  @answer="answerQuestion"
@@ -1,7 +1,50 @@
1
1
  <template>
2
- <form class="desktop-composer" @submit.prevent>
2
+ <form class="desktop-composer" :data-hero="hero ? 'true' : undefined" @submit.prevent>
3
3
  <div class="desktop-composer__model-row">
4
- <LaresPcPopover v-model="modelOpen" :width="260" placement="top-end">
4
+ <LaresPcPopover
5
+ v-if="hero"
6
+ v-model="workspaceOpen"
7
+ class="desktop-composer__workspace-slot"
8
+ :width="240"
9
+ placement="top-start"
10
+ >
11
+ <template #trigger="{ toggle }">
12
+ <button
13
+ type="button"
14
+ class="desktop-composer__model desktop-composer__workspace"
15
+ :disabled="!state.workspacesReady"
16
+ aria-haspopup="menu"
17
+ :aria-expanded="workspaceOpen"
18
+ @mousedown.prevent
19
+ @click="toggle"
20
+ >
21
+ <svg viewBox="0 0 24 24" aria-hidden="true">
22
+ <path d="M3.5 7.5h6l1.6 2H20.5v9H3.5z" />
23
+ </svg>
24
+ <span>{{ workspaceLabel }}</span>
25
+ <ChevronIcon />
26
+ </button>
27
+ </template>
28
+ <div class="desktop-composer__menu">
29
+ <button
30
+ v-for="workspace in state.workspaces"
31
+ :key="workspace.workspaceId"
32
+ type="button"
33
+ class="desktop-composer__option"
34
+ :data-selected="workspace.workspaceId === state.currentWorkspaceId"
35
+ @click="pickWorkspace(workspace.workspaceId)"
36
+ >
37
+ <span>{{ workspace.title || defaultWorkspaceTitle }}</span>
38
+ <CheckIcon v-if="workspace.workspaceId === state.currentWorkspaceId" />
39
+ </button>
40
+ </div>
41
+ </LaresPcPopover>
42
+ <LaresPcPopover
43
+ v-model="modelOpen"
44
+ class="desktop-composer__model-slot"
45
+ :width="260"
46
+ placement="top-end"
47
+ >
5
48
  <template #trigger="{ toggle }">
6
49
  <button
7
50
  type="button"
@@ -397,6 +440,7 @@
397
440
  <script>
398
441
  import { h } from "vue";
399
442
  import { resolveSubmitMode } from "@olares/lares-core/larepass/submission-settings";
443
+ import { DEFAULT_WORKSPACE_TITLE } from "@olares/lares-core/workspace/constants";
400
444
  import { activeReferenceToken, insertReference } from "@olares/lares-core/larepass/references";
401
445
  import { draftImageStatus } from "@olares/lares-core/files/draft-images";
402
446
  import LaresDesktopQueue from "./DesktopQueue.vue";
@@ -456,16 +500,18 @@ export default {
456
500
  props: {
457
501
  state: { type: Object, required: true },
458
502
  t: { type: Function, required: true },
503
+ hero: { type: Boolean, default: false },
459
504
  modelKey: { type: Function, required: true },
460
505
  effortName: { type: Function, required: true },
461
506
  },
462
507
  emits: [
463
508
  "update-draft", "send", "stop", "steer-queue", "files", "choose-model", "choose-effort", "answer-approval", "answer-questions",
464
- "choose-permission", "disable-plan", "goal-action", "open-commands", "pick-command", "queue-action", "remove-image",
509
+ "choose-permission", "disable-plan", "goal-action", "open-commands", "pick-command", "queue-action", "remove-image", "pick-workspace",
465
510
  ],
466
511
  data() {
467
512
  return {
468
513
  modelOpen: false,
514
+ workspaceOpen: false,
469
515
  effortOpen: false,
470
516
  permissionOpen: false,
471
517
  permissionConfirm: false,
@@ -484,6 +530,16 @@ export default {
484
530
  };
485
531
  },
486
532
  computed: {
533
+ defaultWorkspaceTitle() {
534
+ return DEFAULT_WORKSPACE_TITLE;
535
+ },
536
+ workspaceLabel() {
537
+ if (!this.state.workspacesReady) return this.t("agent.loading");
538
+ const current = this.state.workspaces.find(
539
+ (row) => row.workspaceId === this.state.currentWorkspaceId,
540
+ );
541
+ return current?.title || this.state.workspaces[0]?.title || DEFAULT_WORKSPACE_TITLE;
542
+ },
487
543
  /** Held by id, so removing or sending the draft closes the dialog. */
488
544
  previewImage() {
489
545
  return this.state.draftImages.find((row) => row.id === this.previewImageId) ?? null;
@@ -524,6 +580,7 @@ export default {
524
580
  return this.t("chat.placeholderSteerQueue", { shortcut });
525
581
  }
526
582
  if (this.state.planActive) return this.t("chat.placeholderPlan");
583
+ if (this.hero) return this.t("chat.placeholderNewSession");
527
584
  return this.t("chat.placeholderDesktop");
528
585
  },
529
586
  },
@@ -544,6 +601,11 @@ export default {
544
601
  window.removeEventListener("mousedown", this.onComposerOverlayPointer, true);
545
602
  },
546
603
  methods: {
604
+ pickWorkspace(workspaceId) {
605
+ this.workspaceOpen = false;
606
+ if (!workspaceId || workspaceId === this.state.currentWorkspaceId) return;
607
+ this.$emit("pick-workspace", workspaceId);
608
+ },
547
609
  chooseModel(model) {
548
610
  this.modelOpen = false;
549
611
  this.$emit("choose-model", model);
@@ -768,12 +830,21 @@ export default {
768
830
  padding: 8px 0 20px;
769
831
  }
770
832
  .desktop-composer__model-row { display:flex; justify-content:flex-end; gap:2px; min-height:28px; margin-bottom:6px; }
833
+ .desktop-composer[data-hero="true"] .desktop-composer__model-row { justify-content:space-between; }
771
834
  .desktop-composer__model,
772
835
  .desktop-composer__circle,
773
836
  .desktop-composer__send {
774
837
  border:0; color:var(--q-ink-2); cursor:pointer;
775
838
  }
776
839
  .desktop-composer__model { display:flex; align-items:center; gap:4px; border-radius:999px; padding:4px 10px; background:transparent; font-size:14px; }
840
+ /* The model name is the long label of the pair, so it is the one that gives way;
841
+ without a floor of its own the workspace chip absorbs every pixel of shrink. */
842
+ .desktop-composer__workspace-slot { flex:0 0 auto; max-width:45%; }
843
+ .desktop-composer__model-slot { min-width:0; }
844
+ .desktop-composer__workspace,
845
+ .desktop-composer__model-slot .desktop-composer__model { min-width:0; max-width:100%; }
846
+ .desktop-composer__workspace span,
847
+ .desktop-composer__model-slot .desktop-composer__model span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
777
848
  .desktop-composer__model svg,
778
849
  .desktop-composer__permission svg,
779
850
  .desktop-composer__circle svg,
@@ -16,7 +16,7 @@
16
16
  @archive-session="$emit('archive-session', $event)"
17
17
  />
18
18
  <main class="desktop-shell__main">
19
- <header class="desktop-shell__header">
19
+ <header v-if="!newSessionHero" class="desktop-shell__header">
20
20
  <h1>{{ state.sessionTitle }}</h1>
21
21
  <Transition name="lares-tabs">
22
22
  <LaresDesktopPreviewTabs
@@ -30,12 +30,17 @@
30
30
  />
31
31
  </Transition>
32
32
  </header>
33
- <section class="desktop-shell__conversation">
33
+ <section class="desktop-shell__conversation" :data-hero="newSessionHero ? 'true' : 'false'">
34
34
  <div v-if="state.failed" class="desktop-shell__failure">
35
35
  <span>{{ state.failText }}</span>
36
36
  <button type="button" :disabled="state.starting" @click="$emit('retry')">{{ t("probe.retry") }}</button>
37
37
  </div>
38
+ <div v-if="newSessionHero" class="desktop-shell__brand">
39
+ <div class="desktop-shell__logo" :style="logoStyle" aria-hidden="true" />
40
+ <h1>{{ t("shell.title") }}</h1>
41
+ </div>
38
42
  <LaresTranscript
43
+ v-if="!newSessionHero"
39
44
  ref="transcript"
40
45
  :items="state.viewItems"
41
46
  :running="state.running"
@@ -58,6 +63,7 @@
58
63
  ref="composer"
59
64
  :state="state"
60
65
  :t="t"
66
+ :hero="newSessionHero"
61
67
  :model-key="modelKey"
62
68
  :effort-name="effortName"
63
69
  @update-draft="$emit('update-draft', $event)"
@@ -67,6 +73,7 @@
67
73
  @files="$emit('files', $event)"
68
74
  @remove-image="$emit('remove-image', $event)"
69
75
  @choose-model="$emit('choose-model', $event)"
76
+ @pick-workspace="$emit('pick-workspace', $event)"
70
77
  @choose-effort="$emit('choose-effort', $event)"
71
78
  @choose-permission="$emit('choose-permission', $event)"
72
79
  @disable-plan="$emit('disable-plan')"
@@ -115,6 +122,7 @@ import LaresDesktopPreviewTabs from "./DesktopPreviewTabs.vue";
115
122
  import LaresDesktopSidebar from "./DesktopSidebar.vue";
116
123
  import LaresDesktopComposer from "./DesktopComposer.vue";
117
124
  import LaresDirectoryBrowser from "./DirectoryBrowser.vue";
125
+ import { MARK_DATA_URI } from "@olares/lares-core/icons/mark";
118
126
 
119
127
  export default {
120
128
  name: "LaresDesktopShell",
@@ -138,7 +146,7 @@ export default {
138
146
  effortName: { type: Function, required: true },
139
147
  },
140
148
  emits: [
141
- "create", "pick-session", "retry", "open-file", "answer", "update-draft",
149
+ "create", "pick-session", "pick-workspace", "retry", "open-file", "answer", "update-draft",
142
150
  "send", "stop", "steer-queue", "files", "remove-image", "choose-model", "choose-effort", "choose-permission", "disable-plan", "goal-action", "answer-approval", "answer-questions",
143
151
  "open-commands", "open-references", "pick-command", "queue-action", "show-chat", "close-preview-tab",
144
152
  "refresh-workspaces", "browse-workspaces", "browse-directory",
@@ -146,6 +154,20 @@ export default {
146
154
  "delete-workspace", "move-workspace", "move-session", "rename-session",
147
155
  "fork-session", "archive-session",
148
156
  ],
157
+ computed: {
158
+ newSessionHero() {
159
+ if (this.state.previewMode === "preview" && this.state.preview.path) return false;
160
+ return (
161
+ this.state.viewItems.length === 0
162
+ && !this.state.running
163
+ && !this.state.historyLoading
164
+ && !this.state.starting
165
+ );
166
+ },
167
+ logoStyle() {
168
+ return { backgroundImage: MARK_DATA_URI };
169
+ },
170
+ },
149
171
  methods: {
150
172
  focusComposer() {
151
173
  this.$refs.composer?.focus?.();
@@ -188,6 +210,10 @@ export default {
188
210
  .desktop-shell__header { display:flex; height:44px; flex-shrink:0; align-items:center; gap:16px; padding:0 18px; border-bottom:1px solid var(--q-separator); }
189
211
  .desktop-shell__header h1 { max-width:min(280px, 40%); overflow:hidden; margin:0; font-size:15px; font-weight:500; line-height:22px; text-overflow:ellipsis; white-space:nowrap; }
190
212
  .desktop-shell__conversation { position:relative; display:flex; min-width:0; min-height:0; flex:1; flex-direction:column; }
213
+ .desktop-shell__conversation[data-hero="true"] { justify-content:center; }
214
+ .desktop-shell__brand { display:flex; align-items:center; justify-content:center; gap:12px; margin-bottom:36px; }
215
+ .desktop-shell__brand h1 { margin:0; color:var(--q-ink-1); font-size:26px; font-weight:600; line-height:34px; letter-spacing:-.01em; }
216
+ .desktop-shell__logo { width:44px; height:44px; background-position:center; background-repeat:no-repeat; background-size:cover; box-shadow:inset 0 1.5px 0 rgb(255 255 255 / 50%); }
191
217
  .desktop-shell__failure { display:flex; width:calc(var(--lares-column) - 2 * var(--lares-gutter)); align-items:center; gap:10px; margin:12px auto 0; color:var(--q-orange-default); font-size:14px; }
192
218
  .desktop-shell__failure button { border:0; border-radius:6px; padding:5px 8px; background:var(--q-blue-alpha); color:var(--q-blue-default); }
193
219
  /* The scroller stays full width so the wheel works over the gutters; the rows carry the reading column. */
@@ -1,18 +1,50 @@
1
1
  <template>
2
2
  <LaresPcDialog
3
3
  :open="state.open"
4
- :title="t('desktop.chooseFolder')"
4
+ :title="t('desktop.selectWorkspaceDirectory')"
5
5
  :close-label="t('desktop.close')"
6
6
  :busy="state.creating || state.creatingFolder"
7
7
  :width="620"
8
+ variant="directory"
8
9
  @close="$emit('close')"
9
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>
10
47
  <div class="directory-browser">
11
- <nav v-if="state.listing" class="directory-browser__crumbs" :aria-label="t('desktop.folderPath')">
12
- <button v-for="crumb in state.listing.crumbs" :key="crumb.path" type="button" @click="$emit('browse', crumb.path)">
13
- {{ crumb.path === state.listing.home ? t("desktop.home") : crumb.name }}
14
- </button>
15
- </nav>
16
48
  <div class="directory-browser__body">
17
49
  <p v-if="state.loading" class="directory-browser__status">{{ t("desktop.loadingFolders") }}</p>
18
50
  <p v-else-if="state.error" class="directory-browser__error">{{ state.error }}</p>
@@ -40,9 +72,7 @@
40
72
  </div>
41
73
  <template #footer>
42
74
  <div class="directory-browser__footer">
43
- <p class="directory-browser__path">{{ selected || state.listing?.path || "" }}</p>
44
- <div class="directory-browser__footer-row">
45
- <div class="directory-browser__footer-left">
75
+ <div class="directory-browser__footer-left">
46
76
  <button
47
77
  type="button"
48
78
  class="directory-browser__new-folder"
@@ -60,8 +90,8 @@
60
90
  />
61
91
  <span>{{ t("desktop.showHiddenFiles") }}</span>
62
92
  </label>
63
- </div>
64
- <div class="directory-browser__footer-right">
93
+ </div>
94
+ <div class="directory-browser__footer-right">
65
95
  <button type="button" class="directory-browser__cancel" :disabled="state.creating" @click="$emit('close')">
66
96
  {{ t("desktop.cancel") }}
67
97
  </button>
@@ -71,9 +101,8 @@
71
101
  :disabled="state.loading || state.creating || !(selected || state.listing?.path)"
72
102
  @click="$emit('choose', selected || state.listing.path)"
73
103
  >
74
- {{ state.creating ? t("desktop.addingWorkspace") : t("desktop.addWorkspace") }}
104
+ {{ state.creating ? t("desktop.addingWorkspace") : t("desktop.open") }}
75
105
  </button>
76
- </div>
77
106
  </div>
78
107
  </div>
79
108
  </template>
@@ -100,17 +129,25 @@ export default {
100
129
  },
101
130
  emits: ["close", "browse", "choose", "create-folder", "toggle-hidden"],
102
131
  data() {
103
- return { selected: "", newFolderOpen: false };
132
+ return { selected: "", newFolderOpen: false, editingPath: false, pathDraft: "" };
104
133
  },
105
134
  computed: {
106
135
  visibleEntries() {
107
136
  const entries = this.state.listing?.entries ?? [];
108
137
  return this.state.showHidden ? entries : entries.filter((entry) => !entry.hidden);
109
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
+ },
110
145
  },
111
146
  watch: {
112
147
  "state.listing.path"() {
113
148
  this.selected = this.state.selectPath || "";
149
+ this.pathDraft = this.state.listing?.path || "";
150
+ this.editingPath = false;
114
151
  },
115
152
  "state.selectPath"(path) {
116
153
  if (path) this.selected = path;
@@ -129,38 +166,62 @@ export default {
129
166
  onCreateFolder(name) {
130
167
  this.$emit("create-folder", name);
131
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
+ },
132
183
  },
133
184
  };
134
185
  </script>
135
186
 
136
187
  <style scoped>
137
- .directory-browser { display:flex; height:min(440px, calc(100vh - 180px)); flex-direction:column; margin:-16px; }
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; }
138
199
  .directory-browser svg { width:18px; height:18px; fill:none; stroke:currentColor; stroke-width:1.7; stroke-linecap:round; stroke-linejoin:round; }
139
- .directory-browser__crumbs { display:flex; gap:3px; min-height:38px; align-items:center; overflow:auto; padding:0 14px; border-bottom:1px solid var(--q-separator); }
140
- .directory-browser__crumbs button { flex-shrink:0; border:0; border-radius:5px; padding:4px 6px; background:transparent; color:var(--q-ink-3); font-size:14px; }
141
- .directory-browser__crumbs button:hover { background:var(--q-background-hover); color:var(--q-ink-1); }
142
- .directory-browser__body { flex:1; min-height:0; overflow:auto; padding:8px; }
143
- .directory-browser__row { display:flex; width:100%; align-items:center; gap:9px; border:0; border-radius:7px; padding:8px 10px; background:transparent; color:var(--q-ink-1); text-align:left; }
144
- .directory-browser__row:hover,
145
- .directory-browser__row[data-selected="true"] { background:var(--q-background-hover); }
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); }
146
204
  .directory-browser__row > span { min-width:0; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
147
205
  .directory-browser__open { display:grid; width:26px; height:26px; place-items:center; border:0; background:transparent; color:var(--q-ink-3); }
148
206
  .directory-browser__status,
149
- .directory-browser__error { margin:20px; color:var(--q-ink-3); font-size:15px; text-align:center; }
207
+ .directory-browser__error { margin:20px; color:var(--q-ink-3); font-size:14px; text-align:center; }
150
208
  .directory-browser__error { color:var(--q-orange-default); }
151
- .directory-browser__footer { display:flex; width:100%; flex-direction:column; gap:10px; }
152
- .directory-browser__path { min-width:0; margin:0; overflow:hidden; color:var(--q-ink-3); font-size:13px; text-overflow:ellipsis; white-space:nowrap; text-align:left; }
153
- .directory-browser__footer-row { display:flex; align-items:center; justify-content:space-between; gap:12px; }
209
+ .directory-browser__footer { display:flex; width:100%; align-items:center; justify-content:space-between; gap:12px; }
154
210
  .directory-browser__footer-left { display:flex; min-width:0; flex:1; align-items:center; gap:12px; }
155
211
  .directory-browser__footer-right { display:flex; flex-shrink:0; align-items:center; gap:8px; }
156
- .directory-browser__new-folder { display:inline-flex; align-items:center; gap:6px; border:1px solid var(--q-separator); border-radius:999px; padding:0 10px; height:32px; background:transparent; color:var(--q-ink-2); font:inherit; font-size:13px; white-space:nowrap; }
157
- .directory-browser__new-folder svg { width:14px; height:14px; }
158
- .directory-browser__hidden { display:inline-flex; align-items:center; gap:6px; color:var(--q-ink-3); font-size:13px; white-space:nowrap; cursor:pointer; }
159
- .directory-browser__hidden input { width:15px; height:15px; margin:0; }
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); }
160
218
  .directory-browser__cancel,
161
- .directory-browser__choose { min-width:76px; height:32px; border-radius:999px; padding:0 12px; }
162
- .directory-browser__cancel { border:1px solid var(--q-separator); background:transparent; color:var(--q-ink-2); }
163
- .directory-browser__choose { border:0; background:var(--q-blue-default); color:var(--q-ink-on-brand); }
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); }
164
225
  .directory-browser button,
165
226
  .directory-browser__cancel,
166
227
  .directory-browser__choose { cursor:pointer; font:inherit; }
@@ -1,17 +1,26 @@
1
1
  <template>
2
2
  <Teleport to="body">
3
3
  <Transition name="lares-dialog">
4
- <div v-if="open" class="lares-pc-dialog" role="dialog" aria-modal="true" :aria-labelledby="titleId">
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
+ >
5
12
  <button class="lares-pc-dialog__backdrop" type="button" :aria-label="closeLabel" @click="close" />
6
13
  <section class="lares-pc-dialog__surface" :style="{ width: surfaceWidth }">
7
14
  <header class="lares-pc-dialog__header">
8
- <div class="lares-pc-dialog__title">
9
- <h2 :id="titleId">{{ title }}</h2>
10
- <button type="button" class="lares-pc-dialog__close" :aria-label="closeLabel" @click="close">
11
- <svg viewBox="0 0 24 24" aria-hidden="true"><path d="m7 7 10 10M17 7 7 17" /></svg>
12
- </button>
13
- </div>
14
- <p v-if="description">{{ description }}</p>
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>
15
24
  </header>
16
25
  <div class="lares-pc-dialog__body">
17
26
  <slot />
@@ -37,6 +46,7 @@ export default {
37
46
  closeLabel: { type: String, default: "Close" },
38
47
  width: { type: [Number, String], default: 440 },
39
48
  busy: { type: Boolean, default: false },
49
+ variant: { type: String, default: "" },
40
50
  },
41
51
  emits: ["close"],
42
52
  data() {
@@ -82,4 +92,9 @@ export default {
82
92
  .lares-pc-dialog__close svg { width:18px; height:18px; fill:none; stroke:currentColor; stroke-width:1.7; stroke-linecap:round; }
83
93
  .lares-pc-dialog__body { min-height:0; overflow:auto; padding:16px; }
84
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; }
85
100
  </style>