@owdproject/nuxt-theme-paper 0.0.0-stage → 4.0.0-alpha.4

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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -2
  3. package/dist/module.d.mts +7 -0
  4. package/dist/module.json +9 -0
  5. package/dist/module.mjs +55 -0
  6. package/dist/runtime/assets/styles/index.css +533 -0
  7. package/dist/runtime/assets/styles/partials/checkbox.css +158 -0
  8. package/dist/runtime/assets/styles/partials/contextmenu.css +22 -0
  9. package/dist/runtime/assets/styles/partials/desktop.css +69 -0
  10. package/dist/runtime/assets/styles/partials/input.css +155 -0
  11. package/dist/runtime/assets/styles/partials/menu.css +72 -0
  12. package/dist/runtime/assets/styles/partials/scrollbar.css +25 -0
  13. package/dist/runtime/assets/styles/partials/tabs.css +163 -0
  14. package/dist/runtime/assets/styles/partials/window-chrome.css +45 -0
  15. package/dist/runtime/assets/styles/partials/window.css +30 -0
  16. package/dist/runtime/components/ButtonWindowNav.d.vue.ts +17 -0
  17. package/dist/runtime/components/ButtonWindowNav.vue +12 -0
  18. package/dist/runtime/components/ButtonWindowNav.vue.d.ts +17 -0
  19. package/dist/runtime/components/Desktop.d.vue.ts +13 -0
  20. package/dist/runtime/components/Desktop.vue +21 -0
  21. package/dist/runtime/components/Desktop.vue.d.ts +13 -0
  22. package/dist/runtime/components/DesktopContent.d.vue.ts +13 -0
  23. package/dist/runtime/components/DesktopContent.vue +9 -0
  24. package/dist/runtime/components/DesktopContent.vue.d.ts +13 -0
  25. package/dist/runtime/components/DesktopWindow.d.vue.ts +21 -0
  26. package/dist/runtime/components/DesktopWindow.vue +51 -0
  27. package/dist/runtime/components/DesktopWindow.vue.d.ts +21 -0
  28. package/dist/runtime/components/DesktopWindowContent.d.vue.ts +13 -0
  29. package/dist/runtime/components/DesktopWindowContent.vue +31 -0
  30. package/dist/runtime/components/DesktopWindowContent.vue.d.ts +13 -0
  31. package/dist/runtime/components/DesktopWindowIframe.d.vue.ts +20 -0
  32. package/dist/runtime/components/DesktopWindowIframe.vue +30 -0
  33. package/dist/runtime/components/DesktopWindowIframe.vue.d.ts +20 -0
  34. package/dist/runtime/components/DesktopWindowNav.d.vue.ts +15 -0
  35. package/dist/runtime/components/DesktopWindowNav.vue +113 -0
  36. package/dist/runtime/components/DesktopWindowNav.vue.d.ts +15 -0
  37. package/dist/runtime/components/DesktopWindowNavButton.d.vue.ts +17 -0
  38. package/dist/runtime/components/DesktopWindowNavButton.vue +54 -0
  39. package/dist/runtime/components/DesktopWindowNavButton.vue.d.ts +17 -0
  40. package/dist/runtime/components/PaperSystemBar.d.vue.ts +3 -0
  41. package/dist/runtime/components/PaperSystemBar.vue +808 -0
  42. package/dist/runtime/components/PaperSystemBar.vue.d.ts +3 -0
  43. package/dist/runtime/components/PaperWindowSnapHints.d.vue.ts +3 -0
  44. package/dist/runtime/components/PaperWindowSnapHints.vue +7 -0
  45. package/dist/runtime/components/PaperWindowSnapHints.vue.d.ts +3 -0
  46. package/dist/runtime/composables/usePaperAppearance.d.ts +8 -0
  47. package/dist/runtime/composables/usePaperAppearance.js +18 -0
  48. package/dist/runtime/plugins/paper-appearance.client.d.ts +6 -0
  49. package/dist/runtime/plugins/paper-appearance.client.js +17 -0
  50. package/dist/runtime/utils/paperAppearance.d.ts +10 -0
  51. package/dist/runtime/utils/paperAppearance.js +18 -0
  52. package/dist/types.d.mts +7 -0
  53. package/package.json +61 -4
  54. package/src/module.ts +71 -0
  55. package/src/runtime/assets/styles/index.scss +13 -0
  56. package/src/runtime/assets/styles/partials/_variables.scss +111 -0
  57. package/src/runtime/assets/styles/partials/checkbox.scss +64 -0
  58. package/src/runtime/assets/styles/partials/contextmenu.scss +25 -0
  59. package/src/runtime/assets/styles/partials/desktop.scss +70 -0
  60. package/src/runtime/assets/styles/partials/input.scss +55 -0
  61. package/src/runtime/assets/styles/partials/menu.scss +88 -0
  62. package/src/runtime/assets/styles/partials/scrollbar.scss +35 -0
  63. package/src/runtime/assets/styles/partials/tabs.scss +68 -0
  64. package/src/runtime/assets/styles/partials/window-chrome.scss +57 -0
  65. package/src/runtime/assets/styles/partials/window.scss +32 -0
  66. package/src/runtime/components/ButtonWindowNav.vue +12 -0
  67. package/src/runtime/components/Desktop.vue +22 -0
  68. package/src/runtime/components/DesktopContent.vue +14 -0
  69. package/src/runtime/components/DesktopWindow.vue +76 -0
  70. package/src/runtime/components/DesktopWindowContent.vue +31 -0
  71. package/src/runtime/components/DesktopWindowIframe.vue +30 -0
  72. package/src/runtime/components/DesktopWindowNav.vue +115 -0
  73. package/src/runtime/components/DesktopWindowNavButton.vue +55 -0
  74. package/src/runtime/components/PaperSystemBar.vue +1482 -0
  75. package/src/runtime/components/PaperWindowSnapHints.vue +7 -0
  76. package/src/runtime/composables/usePaperAppearance.ts +26 -0
  77. package/src/runtime/plugins/paper-appearance.client.ts +24 -0
  78. package/src/runtime/utils/paperAppearance.ts +31 -0
@@ -0,0 +1,808 @@
1
+ <script setup>
2
+ import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue";
3
+ import { useI18n } from "vue-i18n";
4
+ import { useApplicationManager } from "@owdproject/nuxt/runtime/composables/useApplicationManager";
5
+ import { useApplicationEntries } from "@owdproject/nuxt/runtime/composables/useApplicationEntries";
6
+ import { useDesktopVolumeStore } from "@owdproject/nuxt/runtime/stores/storeDesktopVolume";
7
+ import { usePaperAppearance } from "../composables/usePaperAppearance";
8
+ const open = ref(false);
9
+ const calendarOpen = ref(false);
10
+ const time = ref("");
11
+ const barRef = ref(null);
12
+ const triggerRef = ref(null);
13
+ const clockRef = ref(null);
14
+ const panelPos = ref({ top: "0px", left: "0px", minWidth: "18rem" });
15
+ const calendarPos = ref({ top: "0px", left: "0px", minWidth: "17.5rem" });
16
+ const volumeOpen = ref(false);
17
+ const volumeRef = ref(null);
18
+ const volumePos = ref({ top: "0px", left: "0px", minWidth: "11.5rem" });
19
+ const volumeBeforeMute = ref(100);
20
+ const muted = ref(false);
21
+ const WEEKDAYS = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
22
+ function startOfDay(date) {
23
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
24
+ }
25
+ const today = startOfDay(/* @__PURE__ */ new Date());
26
+ const selectedDate = ref(startOfDay(/* @__PURE__ */ new Date()));
27
+ const viewDate = ref(new Date(today.getFullYear(), today.getMonth(), 1));
28
+ const applicationManager = useApplicationManager();
29
+ const apps = useApplicationEntries().sortedAppEntries("title", "primary");
30
+ const { appearance, toggleAppearance } = usePaperAppearance();
31
+ const volumeStore = useDesktopVolumeStore();
32
+ const appCount = computed(() => apps.value.length);
33
+ const runningApps = computed(() => applicationManager.appsRunning.value);
34
+ const isDark = computed(() => appearance.value === "dark");
35
+ const appearanceLabel = computed(
36
+ () => isDark.value ? "Switch to light mode" : "Switch to dark mode"
37
+ );
38
+ const volumeLevel = computed(() => volumeStore.master);
39
+ const isVolumeMuted = computed(
40
+ () => muted.value || volumeLevel.value === 0
41
+ );
42
+ const volumeLabel = computed(() => {
43
+ if (isVolumeMuted.value) return "Volume, muted";
44
+ return `Volume, ${volumeLevel.value}%`;
45
+ });
46
+ const { te, t } = useI18n();
47
+ const groupedApps = computed(() => {
48
+ const map = /* @__PURE__ */ new Map();
49
+ for (const entry of apps.value) {
50
+ const category = entry.category?.trim() || "Programs";
51
+ const list = map.get(category) ?? [];
52
+ list.push(entry);
53
+ map.set(category, list);
54
+ }
55
+ return [...map.entries()].map(([category, entries]) => {
56
+ const translationKey = `applications.categories.${category}`;
57
+ const translatedName = te(translationKey) ? t(translationKey) : category;
58
+ return { category, translatedName, entries };
59
+ }).sort((a, b) => a.translatedName.localeCompare(b.translatedName)).map(({ category, entries }) => ({
60
+ category,
61
+ entries: [...entries].sort(
62
+ (a, b) => (a.title || "").localeCompare(b.title || "")
63
+ )
64
+ }));
65
+ });
66
+ const showGroupHeaders = computed(() => {
67
+ const groups = groupedApps.value;
68
+ if (groups.length <= 1) return false;
69
+ return !(groups.length === 1 && (groups[0]?.category === "Programs" || groups[0]?.category === "Other"));
70
+ });
71
+ function entrySubtitle(entry) {
72
+ const desc = entry.application.config.description?.trim();
73
+ if (desc && desc !== entry.title) return desc;
74
+ return "";
75
+ }
76
+ function isIconUrl(icon) {
77
+ return icon.startsWith("http://") || icon.startsWith("https://") || icon.startsWith("/") || icon.startsWith("data:");
78
+ }
79
+ function isAppFocused(application) {
80
+ return [...application.windows.values()].some(
81
+ (w) => w.state.active && w.state.focused
82
+ );
83
+ }
84
+ function getAppTargetWindow(application) {
85
+ const windows = [...application.windows.values()];
86
+ if (windows.length === 0) return void 0;
87
+ const focused = windows.find((w) => w.state.focused && w.state.active);
88
+ if (focused) return focused;
89
+ const active = windows.find((w) => w.state.active);
90
+ if (active) return active;
91
+ return windows[windows.length - 1];
92
+ }
93
+ function focusRunningApp(application) {
94
+ const window2 = getAppTargetWindow(application);
95
+ if (!window2) return;
96
+ if (window2.state.active && window2.state.focused) {
97
+ window2.actions.minimize();
98
+ return;
99
+ }
100
+ if (!window2.state.active) {
101
+ window2.actions.setActive(true);
102
+ }
103
+ window2.actions.bringToFront();
104
+ }
105
+ function isSameDay(a, b) {
106
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
107
+ }
108
+ const monthLabel = computed(
109
+ () => viewDate.value.toLocaleDateString(void 0, {
110
+ month: "long",
111
+ year: "numeric"
112
+ })
113
+ );
114
+ const calendarCells = computed(() => {
115
+ const year = viewDate.value.getFullYear();
116
+ const month = viewDate.value.getMonth();
117
+ const firstWeekday = (new Date(year, month, 1).getDay() + 6) % 7;
118
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
119
+ const cells = [];
120
+ for (let i = 0; i < firstWeekday; i++) {
121
+ cells.push({ date: null, key: `pad-${year}-${month}-${i}` });
122
+ }
123
+ for (let day = 1; day <= daysInMonth; day++) {
124
+ cells.push({
125
+ date: new Date(year, month, day),
126
+ key: `${year}-${month}-${day}`
127
+ });
128
+ }
129
+ return cells;
130
+ });
131
+ function tick() {
132
+ time.value = (/* @__PURE__ */ new Date()).toLocaleTimeString([], {
133
+ hour: "2-digit",
134
+ minute: "2-digit"
135
+ });
136
+ }
137
+ function updatePanelPosition() {
138
+ const trigger = triggerRef.value;
139
+ const bar = barRef.value;
140
+ if (!trigger || !bar) return;
141
+ const triggerRect = trigger.getBoundingClientRect();
142
+ const barRect = bar.getBoundingClientRect();
143
+ const panelMin = 288;
144
+ const panelMax = Math.min(360, window.innerWidth - 24);
145
+ const width = Math.max(panelMin, Math.min(panelMax, panelMax));
146
+ let left = triggerRect.left;
147
+ if (left + width > window.innerWidth - 12) {
148
+ left = window.innerWidth - 12 - width;
149
+ }
150
+ left = Math.max(12, left);
151
+ panelPos.value = {
152
+ top: `${barRect.bottom + 6}px`,
153
+ left: `${left}px`,
154
+ minWidth: `${width}px`
155
+ };
156
+ }
157
+ function updateCalendarPosition() {
158
+ const trigger = clockRef.value;
159
+ const bar = barRef.value;
160
+ if (!trigger || !bar) return;
161
+ const triggerRect = trigger.getBoundingClientRect();
162
+ const barRect = bar.getBoundingClientRect();
163
+ const width = 280;
164
+ let left = triggerRect.right - width;
165
+ left = Math.max(12, Math.min(left, window.innerWidth - width - 12));
166
+ calendarPos.value = {
167
+ top: `${barRect.bottom + 6}px`,
168
+ left: `${left}px`,
169
+ minWidth: `${width}px`
170
+ };
171
+ }
172
+ function updateVolumePosition() {
173
+ const trigger = volumeRef.value;
174
+ const bar = barRef.value;
175
+ if (!trigger || !bar) return;
176
+ const triggerRect = trigger.getBoundingClientRect();
177
+ const barRect = bar.getBoundingClientRect();
178
+ const width = 184;
179
+ let left = triggerRect.right - width;
180
+ left = Math.max(12, Math.min(left, window.innerWidth - width - 12));
181
+ volumePos.value = {
182
+ top: `${barRect.bottom + 6}px`,
183
+ left: `${left}px`,
184
+ minWidth: `${width}px`
185
+ };
186
+ }
187
+ function toggleApps() {
188
+ calendarOpen.value = false;
189
+ volumeOpen.value = false;
190
+ open.value = !open.value;
191
+ }
192
+ function closeApps() {
193
+ open.value = false;
194
+ }
195
+ function toggleCalendar() {
196
+ open.value = false;
197
+ volumeOpen.value = false;
198
+ if (!calendarOpen.value) {
199
+ const now = startOfDay(/* @__PURE__ */ new Date());
200
+ selectedDate.value = now;
201
+ viewDate.value = new Date(now.getFullYear(), now.getMonth(), 1);
202
+ }
203
+ calendarOpen.value = !calendarOpen.value;
204
+ }
205
+ function closeCalendar() {
206
+ calendarOpen.value = false;
207
+ }
208
+ function toggleVolume() {
209
+ open.value = false;
210
+ calendarOpen.value = false;
211
+ volumeOpen.value = !volumeOpen.value;
212
+ }
213
+ function closeVolume() {
214
+ volumeOpen.value = false;
215
+ }
216
+ function onVolumeInput(e) {
217
+ const value = Number(e.target.value);
218
+ muted.value = false;
219
+ volumeStore.setMasterVolume(value);
220
+ }
221
+ function toggleMute() {
222
+ if (isVolumeMuted.value) {
223
+ volumeStore.setMasterVolume(volumeBeforeMute.value || 50);
224
+ muted.value = false;
225
+ } else {
226
+ volumeBeforeMute.value = volumeLevel.value;
227
+ volumeStore.setMasterVolume(0);
228
+ muted.value = true;
229
+ }
230
+ }
231
+ function selectDay(date) {
232
+ selectedDate.value = startOfDay(date);
233
+ }
234
+ function prevMonth() {
235
+ const d = viewDate.value;
236
+ viewDate.value = new Date(d.getFullYear(), d.getMonth() - 1, 1);
237
+ }
238
+ function nextMonth() {
239
+ const d = viewDate.value;
240
+ viewDate.value = new Date(d.getFullYear(), d.getMonth() + 1, 1);
241
+ }
242
+ async function launch(entry) {
243
+ if (!entry.application?.id || !entry.command) return;
244
+ await applicationManager.execAppCommand(entry.application.id, entry.command);
245
+ closeApps();
246
+ }
247
+ let timer;
248
+ function onKey(e) {
249
+ if (e.key !== "Escape") return;
250
+ if (volumeOpen.value) closeVolume();
251
+ else if (calendarOpen.value) closeCalendar();
252
+ else if (open.value) closeApps();
253
+ }
254
+ function onResize() {
255
+ if (open.value) updatePanelPosition();
256
+ if (calendarOpen.value) updateCalendarPosition();
257
+ if (volumeOpen.value) updateVolumePosition();
258
+ }
259
+ watch(open, async (isOpen) => {
260
+ if (!isOpen) return;
261
+ await nextTick();
262
+ updatePanelPosition();
263
+ });
264
+ watch(calendarOpen, async (isOpen) => {
265
+ if (!isOpen) return;
266
+ await nextTick();
267
+ updateCalendarPosition();
268
+ });
269
+ watch(volumeOpen, async (isOpen) => {
270
+ if (!isOpen) return;
271
+ await nextTick();
272
+ updateVolumePosition();
273
+ });
274
+ onMounted(() => {
275
+ tick();
276
+ timer = setInterval(tick, 3e4);
277
+ window.addEventListener("keydown", onKey);
278
+ window.addEventListener("resize", onResize);
279
+ });
280
+ onUnmounted(() => {
281
+ clearInterval(timer);
282
+ window.removeEventListener("keydown", onKey);
283
+ window.removeEventListener("resize", onResize);
284
+ });
285
+ </script>
286
+
287
+ <template>
288
+ <header
289
+ ref="barRef"
290
+ class="paper-bar owd-desktop__system-bar"
291
+ :class="{ 'paper-bar--menu-open': open || calendarOpen || volumeOpen }"
292
+ >
293
+ <div class="paper-bar__start">
294
+ <button
295
+ ref="triggerRef"
296
+ type="button"
297
+ class="paper-bar__trigger"
298
+ :class="{ 'paper-bar__trigger--open': open }"
299
+ :aria-expanded="open"
300
+ aria-haspopup="menu"
301
+ @click="toggleApps()"
302
+ >
303
+ <span class="paper-bar__trigger-icon" aria-hidden="true">
304
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
305
+ <rect
306
+ x="2"
307
+ y="2"
308
+ width="5"
309
+ height="5"
310
+ rx="1"
311
+ fill="currentColor"
312
+ />
313
+ <rect
314
+ x="9"
315
+ y="2"
316
+ width="5"
317
+ height="5"
318
+ rx="1"
319
+ fill="currentColor"
320
+ />
321
+ <rect
322
+ x="2"
323
+ y="9"
324
+ width="5"
325
+ height="5"
326
+ rx="1"
327
+ fill="currentColor"
328
+ />
329
+ <rect
330
+ x="9"
331
+ y="9"
332
+ width="5"
333
+ height="5"
334
+ rx="1"
335
+ fill="currentColor"
336
+ />
337
+ </svg>
338
+ </span>
339
+ <span class="paper-bar__trigger-label">Applications</span>
340
+ <span v-if="appCount > 0" class="paper-bar__trigger-badge">{{
341
+ appCount
342
+ }}</span>
343
+ </button>
344
+ <div
345
+ v-if="runningApps.length > 0"
346
+ class="paper-bar__running"
347
+ role="list"
348
+ aria-label="Open applications"
349
+ >
350
+ <button
351
+ v-for="application in runningApps"
352
+ :key="`${application.id}-${application.openWindowCount.value}`"
353
+ type="button"
354
+ role="listitem"
355
+ class="paper-bar__app-btn"
356
+ :class="{ 'paper-bar__app-btn--active': isAppFocused(application) }"
357
+ :title="application.config.title"
358
+ :aria-label="application.config.title"
359
+ @click="focusRunningApp(application)"
360
+ >
361
+ <img
362
+ v-if="application.config.icon && isIconUrl(application.config.icon)"
363
+ class="paper-bar__app-btn-img"
364
+ :src="application.config.icon"
365
+ alt=""
366
+ />
367
+ <Icon
368
+ v-else-if="application.config.icon"
369
+ :name="application.config.icon"
370
+ :size="16"
371
+ />
372
+ <span v-else class="paper-bar__app-btn-fallback">
373
+ {{ (application.config.title || "?").charAt(0).toUpperCase() }}
374
+ </span>
375
+ </button>
376
+ </div>
377
+
378
+ </div>
379
+
380
+ <div class="paper-bar__end">
381
+ <button
382
+ type="button"
383
+ class="paper-bar__icon-btn"
384
+ :aria-label="appearanceLabel"
385
+ :aria-pressed="isDark"
386
+ @click="toggleAppearance()"
387
+ >
388
+ <svg
389
+ v-if="isDark"
390
+ class="paper-bar__icon-btn-svg"
391
+ width="16"
392
+ height="16"
393
+ viewBox="0 0 16 16"
394
+ fill="none"
395
+ aria-hidden="true"
396
+ >
397
+ <circle cx="8" cy="8" r="3.25" stroke="currentColor" stroke-width="1.5" />
398
+ <path
399
+ d="M8 1.25v1.5M8 13.25v1.5M1.25 8h1.5M13.25 8h1.5M3.1 3.1l1.06 1.06M11.84 11.84l1.06 1.06M3.1 12.9l1.06-1.06M11.84 4.16l1.06-1.06"
400
+ stroke="currentColor"
401
+ stroke-width="1.5"
402
+ stroke-linecap="round"
403
+ />
404
+ </svg>
405
+ <svg
406
+ v-else
407
+ class="paper-bar__icon-btn-svg"
408
+ width="16"
409
+ height="16"
410
+ viewBox="0 0 16 16"
411
+ fill="none"
412
+ aria-hidden="true"
413
+ >
414
+ <path
415
+ d="M8 2.5a5.5 5.5 0 1 0 0 11 4 4 0 0 1 0-11z"
416
+ stroke="currentColor"
417
+ stroke-width="1.5"
418
+ stroke-linejoin="round"
419
+ />
420
+ </svg>
421
+ </button>
422
+ <button
423
+ ref="volumeRef"
424
+ type="button"
425
+ class="paper-bar__icon-btn"
426
+ :class="{ 'paper-bar__icon-btn--open': volumeOpen }"
427
+ :aria-expanded="volumeOpen"
428
+ aria-haspopup="dialog"
429
+ :aria-label="volumeLabel"
430
+ @click="toggleVolume()"
431
+ >
432
+ <svg
433
+ v-if="isVolumeMuted"
434
+ class="paper-bar__icon-btn-svg"
435
+ width="16"
436
+ height="16"
437
+ viewBox="0 0 16 16"
438
+ fill="none"
439
+ aria-hidden="true"
440
+ >
441
+ <path
442
+ d="M3 6.5v3h2.5L9 13V3L5.5 6.5H3z"
443
+ stroke="currentColor"
444
+ stroke-width="1.5"
445
+ stroke-linejoin="round"
446
+ />
447
+ <path
448
+ d="M11.5 6l3 4-3 4M14.5 6l-3 4 3 4"
449
+ stroke="currentColor"
450
+ stroke-width="1.5"
451
+ stroke-linecap="round"
452
+ />
453
+ </svg>
454
+ <svg
455
+ v-else-if="volumeLevel <= 33"
456
+ class="paper-bar__icon-btn-svg"
457
+ width="16"
458
+ height="16"
459
+ viewBox="0 0 16 16"
460
+ fill="none"
461
+ aria-hidden="true"
462
+ >
463
+ <path
464
+ d="M3 6.5v3h2.5L9 13V3L5.5 6.5H3z"
465
+ stroke="currentColor"
466
+ stroke-width="1.5"
467
+ stroke-linejoin="round"
468
+ />
469
+ <path
470
+ d="M11 8.5c0-1-.5-1.75-1-2.25"
471
+ stroke="currentColor"
472
+ stroke-width="1.5"
473
+ stroke-linecap="round"
474
+ />
475
+ </svg>
476
+ <svg
477
+ v-else-if="volumeLevel <= 66"
478
+ class="paper-bar__icon-btn-svg"
479
+ width="16"
480
+ height="16"
481
+ viewBox="0 0 16 16"
482
+ fill="none"
483
+ aria-hidden="true"
484
+ >
485
+ <path
486
+ d="M3 6.5v3h2.5L9 13V3L5.5 6.5H3z"
487
+ stroke="currentColor"
488
+ stroke-width="1.5"
489
+ stroke-linejoin="round"
490
+ />
491
+ <path
492
+ d="M10.5 6.5a3 3 0 0 1 0 3M12.5 5a5 5 0 0 1 0 6"
493
+ stroke="currentColor"
494
+ stroke-width="1.5"
495
+ stroke-linecap="round"
496
+ />
497
+ </svg>
498
+ <svg
499
+ v-else
500
+ class="paper-bar__icon-btn-svg"
501
+ width="16"
502
+ height="16"
503
+ viewBox="0 0 16 16"
504
+ fill="none"
505
+ aria-hidden="true"
506
+ >
507
+ <path
508
+ d="M3 6.5v3h2.5L9 13V3L5.5 6.5H3z"
509
+ stroke="currentColor"
510
+ stroke-width="1.5"
511
+ stroke-linejoin="round"
512
+ />
513
+ <path
514
+ d="M10.5 5.5a4.5 4.5 0 0 1 0 5M12.5 3.5a7.5 7.5 0 0 1 0 9"
515
+ stroke="currentColor"
516
+ stroke-width="1.5"
517
+ stroke-linecap="round"
518
+ />
519
+ </svg>
520
+ </button>
521
+
522
+ <button
523
+ ref="clockRef"
524
+ type="button"
525
+ class="paper-bar__icon-btn paper-bar__time"
526
+ :class="{ 'paper-bar__icon-btn--open': calendarOpen }"
527
+ :aria-expanded="calendarOpen"
528
+ aria-haspopup="dialog"
529
+ :aria-label="`Calendar, ${monthLabel}`"
530
+ @click="toggleCalendar()"
531
+ >
532
+ <time :datetime="selectedDate.toISOString().slice(0, 10)">{{ time }}</time>
533
+ </button>
534
+ </div>
535
+ </header>
536
+
537
+ <Teleport to="body">
538
+ <Transition name="paper-menu">
539
+ <div v-if="open" class="paper-menu" role="presentation">
540
+ <button
541
+ type="button"
542
+ class="paper-menu__scrim"
543
+ aria-label="Close menu"
544
+ @click="closeApps()"
545
+ />
546
+ <nav
547
+ class="paper-menu__panel"
548
+ role="menu"
549
+ aria-label="Applications"
550
+ :style="{
551
+ top: panelPos.top,
552
+ left: panelPos.left,
553
+ minWidth: panelPos.minWidth
554
+ }"
555
+ >
556
+ <header class="paper-menu__header">
557
+ <p class="paper-menu__eyebrow">Launch</p>
558
+ <p class="paper-menu__title">Applications</p>
559
+ </header>
560
+
561
+ <p v-if="apps.length === 0" class="paper-menu__empty">
562
+ No applications installed.
563
+ </p>
564
+
565
+ <div v-else class="paper-menu__body">
566
+ <section
567
+ v-for="group in groupedApps"
568
+ :key="group.category"
569
+ class="paper-menu__section"
570
+ >
571
+ <h2
572
+ v-if="showGroupHeaders"
573
+ class="paper-menu__section-title"
574
+ >
575
+ {{ $te(`applications.categories.${group.category}`) ? $t(`applications.categories.${group.category}`) : group.category }}
576
+ </h2>
577
+ <ul class="paper-menu__list" role="none">
578
+ <li
579
+ v-for="(entry, i) in group.entries"
580
+ :key="`${entry.application.id}:${entry.command}:${i}`"
581
+ role="none"
582
+ >
583
+ <button
584
+ type="button"
585
+ role="menuitem"
586
+ class="paper-menu__item"
587
+ @click="launch(entry)"
588
+ >
589
+ <span class="paper-menu__icon" aria-hidden="true">
590
+ <img
591
+ v-if="entry.icon && isIconUrl(entry.icon)"
592
+ class="paper-menu__icon-img"
593
+ :src="entry.icon"
594
+ alt=""
595
+ />
596
+ <Icon
597
+ v-else-if="entry.icon"
598
+ :name="entry.icon"
599
+ :size="18"
600
+ />
601
+ <span v-else class="paper-menu__icon-fallback">
602
+ {{ (entry.title || "?").charAt(0).toUpperCase() }}
603
+ </span>
604
+ </span>
605
+ <span class="paper-menu__copy">
606
+ <span class="paper-menu__label">{{ entry.title }}</span>
607
+ <span
608
+ v-if="entrySubtitle(entry)"
609
+ class="paper-menu__meta"
610
+ >
611
+ {{ entrySubtitle(entry) }}
612
+ </span>
613
+ </span>
614
+ <span class="paper-menu__chevron" aria-hidden="true">
615
+ <svg width="14" height="14" viewBox="0 0 14 14">
616
+ <path
617
+ d="M5 3l4 4-4 4"
618
+ fill="none"
619
+ stroke="currentColor"
620
+ stroke-width="1.5"
621
+ stroke-linecap="round"
622
+ stroke-linejoin="round"
623
+ />
624
+ </svg>
625
+ </span>
626
+ </button>
627
+ </li>
628
+ </ul>
629
+ </section>
630
+ </div>
631
+ </nav>
632
+ </div>
633
+ </Transition>
634
+
635
+ <Transition name="paper-menu">
636
+ <div v-if="calendarOpen" class="paper-menu" role="presentation">
637
+ <button
638
+ type="button"
639
+ class="paper-menu__scrim"
640
+ aria-label="Close calendar"
641
+ @click="closeCalendar()"
642
+ />
643
+ <div
644
+ class="paper-calendar paper-menu__panel"
645
+ role="dialog"
646
+ aria-label="Calendar"
647
+ :style="{
648
+ top: calendarPos.top,
649
+ left: calendarPos.left,
650
+ minWidth: calendarPos.minWidth
651
+ }"
652
+ >
653
+ <header class="paper-calendar__nav">
654
+ <button
655
+ type="button"
656
+ class="paper-calendar__nav-btn"
657
+ aria-label="Previous month"
658
+ @click="prevMonth()"
659
+ >
660
+ ‹
661
+ </button>
662
+ <p class="paper-calendar__month">{{ monthLabel }}</p>
663
+ <button
664
+ type="button"
665
+ class="paper-calendar__nav-btn"
666
+ aria-label="Next month"
667
+ @click="nextMonth()"
668
+ >
669
+ ›
670
+ </button>
671
+ </header>
672
+
673
+ <div class="paper-calendar__weekdays" aria-hidden="true">
674
+ <span
675
+ v-for="label in WEEKDAYS"
676
+ :key="label"
677
+ class="paper-calendar__weekday"
678
+ >
679
+ {{ label }}
680
+ </span>
681
+ </div>
682
+
683
+ <div class="paper-calendar__grid" role="grid">
684
+ <button
685
+ v-for="cell in calendarCells"
686
+ :key="cell.key"
687
+ type="button"
688
+ class="paper-calendar__day"
689
+ :class="{
690
+ 'paper-calendar__day--empty': !cell.date,
691
+ 'paper-calendar__day--today': cell.date && isSameDay(cell.date, today),
692
+ 'paper-calendar__day--selected': cell.date && isSameDay(cell.date, selectedDate)
693
+ }"
694
+ role="gridcell"
695
+ :tabindex="cell.date ? 0 : -1"
696
+ :disabled="!cell.date"
697
+ :aria-label="
698
+ cell.date ? cell.date.toLocaleDateString(void 0, {
699
+ weekday: 'long',
700
+ day: 'numeric',
701
+ month: 'long',
702
+ year: 'numeric'
703
+ }) : void 0
704
+ "
705
+ :aria-selected="
706
+ cell.date ? isSameDay(cell.date, selectedDate) : void 0
707
+ "
708
+ @click="cell.date && selectDay(cell.date)"
709
+ >
710
+ {{ cell.date?.getDate() }}
711
+ </button>
712
+ </div>
713
+ </div>
714
+ </div>
715
+ </Transition>
716
+
717
+ <Transition name="paper-menu">
718
+ <div v-if="volumeOpen" class="paper-menu" role="presentation">
719
+ <button
720
+ type="button"
721
+ class="paper-menu__scrim"
722
+ aria-label="Close volume"
723
+ @click="closeVolume()"
724
+ />
725
+ <div
726
+ class="paper-volume paper-menu__panel"
727
+ role="dialog"
728
+ aria-label="Volume"
729
+ :style="{
730
+ top: volumePos.top,
731
+ left: volumePos.left,
732
+ minWidth: volumePos.minWidth
733
+ }"
734
+ >
735
+ <div class="paper-volume__row">
736
+ <button
737
+ type="button"
738
+ class="paper-volume__mute"
739
+ :aria-label="isVolumeMuted ? 'Unmute' : 'Mute'"
740
+ :aria-pressed="isVolumeMuted"
741
+ @click="toggleMute()"
742
+ >
743
+ <svg
744
+ v-if="isVolumeMuted"
745
+ width="14"
746
+ height="14"
747
+ viewBox="0 0 16 16"
748
+ fill="none"
749
+ aria-hidden="true"
750
+ >
751
+ <path
752
+ d="M3 6.5v3h2.5L9 13V3L5.5 6.5H3z"
753
+ stroke="currentColor"
754
+ stroke-width="1.5"
755
+ stroke-linejoin="round"
756
+ />
757
+ <path
758
+ d="M11.5 6l3 4-3 4M14.5 6l-3 4 3 4"
759
+ stroke="currentColor"
760
+ stroke-width="1.5"
761
+ stroke-linecap="round"
762
+ />
763
+ </svg>
764
+ <svg
765
+ v-else
766
+ width="14"
767
+ height="14"
768
+ viewBox="0 0 16 16"
769
+ fill="none"
770
+ aria-hidden="true"
771
+ >
772
+ <path
773
+ d="M3 6.5v3h2.5L9 13V3L5.5 6.5H3z"
774
+ stroke="currentColor"
775
+ stroke-width="1.5"
776
+ stroke-linejoin="round"
777
+ />
778
+ <path
779
+ d="M10.5 5.5a4.5 4.5 0 0 1 0 5M12.5 3.5a7.5 7.5 0 0 1 0 9"
780
+ stroke="currentColor"
781
+ stroke-width="1.5"
782
+ stroke-linecap="round"
783
+ />
784
+ </svg>
785
+ </button>
786
+ <input
787
+ type="range"
788
+ class="paper-volume__slider"
789
+ min="0"
790
+ max="100"
791
+ step="1"
792
+ :value="volumeLevel"
793
+ :aria-valuenow="volumeLevel"
794
+ aria-valuemin="0"
795
+ aria-valuemax="100"
796
+ aria-label="Volume level"
797
+ @input="onVolumeInput"
798
+ />
799
+ </div>
800
+ </div>
801
+ </div>
802
+ </Transition>
803
+ </Teleport>
804
+ </template>
805
+
806
+ <style scoped>
807
+ .paper-bar{align-items:center;background:var(--paper-surface);border-bottom:1px solid var(--paper-border);display:flex;flex-shrink:0;gap:1rem;height:var(--paper-bar-height);padding:0 1rem}.paper-bar--menu-open{border-bottom-color:var(--paper-border)}.paper-bar__start{gap:.375rem}.paper-bar__running,.paper-bar__start{align-items:center;display:flex;min-width:0}.paper-bar__running{gap:.125rem;max-width:min(40vw,24rem);overflow-x:auto;overscroll-behavior:contain;scrollbar-width:none}.paper-bar__running::-webkit-scrollbar{display:none}.paper-bar__app-btn{align-items:center;background:none;border:1px solid transparent;border-radius:var(--paper-radius);color:var(--paper-text-secondary);cursor:pointer;display:inline-flex;flex-shrink:0;height:1.625rem;justify-content:center;margin:0;opacity:.72;padding:0;transition:border-color .12s ease,background-color .12s ease,color .12s ease,opacity .12s ease;width:1.625rem}.paper-bar__app-btn:hover{background:color-mix(in srgb,var(--paper-bg) 80%,transparent);opacity:1}.paper-bar__app-btn--active,.paper-bar__app-btn--active:hover{background:var(--paper-accent-soft);border-color:var(--paper-border);color:var(--paper-accent);opacity:1}.paper-bar__app-btn-img{height:1rem;-o-object-fit:contain;object-fit:contain;width:1rem}.paper-bar__app-btn-fallback{font-size:.6875rem;font-weight:600;line-height:1}.paper-bar__trigger{align-items:center;background:var(--paper-surface);border:1px solid var(--paper-border);border-radius:var(--paper-radius);color:var(--paper-text);cursor:pointer;display:inline-flex;font:inherit;font-size:.8125rem;font-weight:500;gap:.5rem;line-height:1;margin:0;padding:.375rem .75rem .375rem .625rem;transition:border-color .15s ease,background-color .15s ease,box-shadow .15s ease,color .15s ease}.paper-bar__trigger:hover{background:var(--paper-bg);border-color:var(--paper-border)}.paper-bar__trigger--open,.paper-bar__trigger--open:hover{background:var(--paper-accent-soft);border-color:color-mix(in srgb,var(--paper-accent) 35%,var(--paper-border));box-shadow:0 0 0 3px color-mix(in srgb,var(--paper-accent) 12%,transparent);color:var(--paper-accent)}.paper-bar__trigger-icon{color:var(--paper-text-secondary);display:flex}.paper-bar__trigger--open .paper-bar__trigger-icon{color:var(--paper-accent)}.paper-bar__trigger-badge{align-items:center;background:var(--paper-bg);border-radius:var(--paper-radius-pill);color:var(--paper-text-secondary);display:inline-flex;font-size:.6875rem;font-variant-numeric:tabular-nums;font-weight:600;height:1.125rem;justify-content:center;min-width:1.125rem;padding:0 .25rem}.paper-bar__trigger--open .paper-bar__trigger-badge{background:var(--paper-accent-soft);color:var(--paper-accent)}.paper-bar__end{align-items:center;display:flex;gap:.125rem;margin-left:auto;margin-right:-.375rem}.paper-bar__icon-btn{align-items:center;background:none;border:none;border-radius:var(--paper-radius);color:var(--paper-text-secondary);cursor:pointer;display:inline-flex;font:inherit;justify-content:center;margin:0;padding:.25rem .375rem;transition:background-color .12s ease,color .12s ease}.paper-bar__icon-btn-svg{display:block}.paper-bar__time{font-family:var(--paper-mono);font-size:.75rem;font-variant-numeric:tabular-nums}.paper-bar__icon-btn:hover{background:color-mix(in srgb,var(--paper-bg) 80%,transparent);color:var(--paper-text)}.paper-bar__icon-btn--open,.paper-bar__icon-btn--open:hover{background:var(--paper-accent-soft);color:var(--paper-accent)}.paper-menu{inset:0;pointer-events:none;position:fixed;z-index:var(--paper-z-menu)}.paper-menu__scrim{background:var(--paper-scrim);border:0;cursor:default;inset:0;margin:0;padding:0;pointer-events:auto;position:absolute}.paper-menu__panel{background-color:var(--paper-menu-bg);border:1px solid var(--paper-border);border-radius:var(--paper-radius);box-shadow:none;box-sizing:border-box;display:flex;flex-direction:column;max-height:min(28rem,calc(100dvh - var(--paper-bar-height) - 1.5rem));overflow:hidden;pointer-events:auto;position:fixed}.paper-menu__header{background-color:var(--paper-menu-header-bg);border-bottom:1px solid var(--paper-border);flex-shrink:0;padding:.875rem 1rem .625rem}.paper-menu__eyebrow{color:var(--paper-accent);font-size:.6875rem;font-weight:600;letter-spacing:.08em;margin:0;text-transform:uppercase}.paper-menu__title{color:var(--paper-text);font-size:1rem;font-weight:600;letter-spacing:-.02em;margin:.125rem 0 0}.paper-menu__body{background-color:var(--paper-menu-bg);display:flex;flex-direction:column;overflow-y:auto;overscroll-behavior:contain;padding:.25rem 0 .375rem}.paper-menu__section+.paper-menu__section{border-top:1px solid var(--paper-border);margin-top:.25rem;padding-top:.25rem}.paper-menu__section-title{color:var(--paper-text-tertiary);font-size:.6875rem;font-weight:600;letter-spacing:.06em;margin:0;padding:.5rem 1rem .25rem;text-transform:uppercase}.paper-menu__list{display:flex;flex-direction:column;gap:.125rem;list-style:none;margin:0;padding:0 .375rem}.paper-menu__empty{background-color:var(--paper-menu-bg);color:var(--paper-text-secondary);font-size:.8125rem;margin:0;padding:1rem 1rem 1.125rem}.paper-menu__item{background:none;border:none;color:var(--paper-text);cursor:pointer;font:inherit;gap:.75rem;margin:0;padding:.5rem .625rem;text-align:left;transition:background-color .12s ease,color .12s ease;width:100%}.paper-menu__icon,.paper-menu__item{align-items:center;border-radius:var(--paper-radius);display:flex}.paper-menu__icon{background:var(--paper-bg);color:var(--paper-text-secondary);flex-shrink:0;height:2rem;justify-content:center;width:2rem}.paper-menu__icon-img{height:1.125rem;-o-object-fit:contain;object-fit:contain;width:1.125rem}.paper-menu__icon-fallback{font-size:.8125rem;font-weight:600;line-height:1}.paper-menu__copy{display:flex;flex:1;flex-direction:column;gap:.125rem;min-width:0}.paper-menu__label{font-size:.875rem;font-weight:500}.paper-menu__label,.paper-menu__meta{line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.paper-menu__meta{color:var(--paper-text-secondary);font-size:.75rem}.paper-menu__chevron{color:var(--paper-text-tertiary);flex-shrink:0;opacity:0;transform:translateX(-4px);transition:opacity .12s ease,transform .12s ease,color .12s ease}.paper-menu__item:focus-visible,.paper-menu__item:hover{background:var(--paper-accent-soft);outline:none}.paper-menu__item:focus-visible .paper-menu__label,.paper-menu__item:hover .paper-menu__label{color:var(--paper-accent)}.paper-menu__item:focus-visible .paper-menu__icon,.paper-menu__item:hover .paper-menu__icon{background:color-mix(in srgb,var(--paper-accent) 12%,#fff);color:var(--paper-accent)}.paper-menu__item:focus-visible .paper-menu__chevron,.paper-menu__item:hover .paper-menu__chevron{color:var(--paper-accent);opacity:1;transform:translateX(0)}.paper-menu-enter-active,.paper-menu-leave-active{transition:opacity .18s ease}.paper-menu-enter-active .paper-menu__panel,.paper-menu-leave-active .paper-menu__panel{transition:opacity .18s ease,transform .18s cubic-bezier(.22,1,.36,1)}.paper-menu-enter-from,.paper-menu-leave-to{opacity:0}.paper-menu-enter-from .paper-menu__panel,.paper-menu-leave-to .paper-menu__panel{opacity:0;transform:translateY(-6px) scale(.98)}.paper-calendar{background-color:var(--paper-menu-bg);padding:.75rem}.paper-calendar__nav{align-items:center;display:flex;gap:.5rem;justify-content:space-between;margin-bottom:.75rem}.paper-calendar__month{color:var(--paper-text);flex:1;font-size:.875rem;font-weight:600;margin:0;text-align:center}.paper-calendar__nav-btn{align-items:center;background:var(--paper-surface);border:1px solid var(--paper-border);border-radius:var(--paper-radius);color:var(--paper-text-secondary);cursor:pointer;display:flex;font-size:1.125rem;height:1.75rem;justify-content:center;line-height:1;margin:0;padding:0;width:1.75rem}.paper-calendar__nav-btn:hover{background:var(--paper-bg);border-color:var(--paper-border);color:var(--paper-text)}.paper-calendar__weekdays{display:grid;gap:.125rem;grid-template-columns:repeat(7,1fr);margin-bottom:.25rem}.paper-calendar__weekday{color:var(--paper-text-tertiary);font-size:.6875rem;font-weight:600;text-align:center}.paper-calendar__grid{display:grid;gap:.125rem;grid-template-columns:repeat(7,1fr)}.paper-calendar__day{align-items:center;aspect-ratio:1;background:none;border:1px solid transparent;border-radius:var(--paper-radius);color:var(--paper-text);cursor:pointer;display:flex;font:inherit;font-size:.8125rem;font-variant-numeric:tabular-nums;justify-content:center;margin:0;min-height:2rem;padding:0}.paper-calendar__day--empty{pointer-events:none;visibility:hidden}.paper-calendar__day:not(.paper-calendar__day--empty):hover{background:var(--paper-bg)}.paper-calendar__day--today:not(.paper-calendar__day--selected){border-color:var(--paper-border);font-weight:600}.paper-calendar__day--selected{border-color:color-mix(in srgb,var(--paper-accent) 40%,var(--paper-border));color:var(--paper-accent);font-weight:600}.paper-calendar__day--selected,.paper-calendar__day--selected:hover{background:var(--paper-accent-soft)}.paper-volume{background-color:var(--paper-menu-bg);padding:.625rem .75rem}.paper-volume__row{align-items:center;display:flex;gap:.625rem}.paper-volume__mute{align-items:center;background:var(--paper-surface);border:1px solid var(--paper-border);border-radius:var(--paper-radius);color:var(--paper-text-secondary);cursor:pointer;display:flex;flex-shrink:0;height:1.75rem;justify-content:center;margin:0;padding:0;width:1.75rem}.paper-volume__mute:hover{background:var(--paper-bg);border-color:var(--paper-border);color:var(--paper-text)}.paper-volume__slider{accent-color:var(--paper-accent);-webkit-appearance:none;-moz-appearance:none;appearance:none;background:var(--paper-border);border:none;border-radius:var(--paper-radius-pill);cursor:pointer;flex:1;height:.25rem;margin:0;min-width:0;padding:0}.paper-volume__slider::-webkit-slider-thumb{-webkit-appearance:none;background:var(--paper-surface);border:1px solid var(--paper-border);border-radius:var(--paper-radius-circle);box-shadow:none;cursor:pointer;height:.875rem;width:.875rem}.paper-volume__slider::-moz-range-thumb{background:var(--paper-surface);border:1px solid var(--paper-border);border-radius:var(--paper-radius-circle);box-shadow:none;cursor:pointer;height:.875rem;width:.875rem}.paper-volume__slider:focus-visible{outline:2px solid color-mix(in srgb,var(--paper-accent) 40%,transparent);outline-offset:2px}
808
+ </style>